diff --git a/.context/migration-interface/CATALOG.md b/.context/migration-interface/CATALOG.md new file mode 100644 index 000000000..b40d19609 --- /dev/null +++ b/.context/migration-interface/CATALOG.md @@ -0,0 +1,145 @@ +# Catalog: Migration & Import Patterns + +Source: 4-agent parallel discovery on 2026-04-29 covering `scripts/src/common/migrations/`, `scripts-v2/runs/`, `scripts/mintlify-import.sh` chain, `scripts/firecrawl-import-from-csv.sh` chain, plus a deep read of billing v2 action machinery. + +--- + +## A. Migration archetypes (what scripts actually do) + +### A1. Version bump via `billingActions.migrate()` +- `scripts/src/common/migrations/perform-migration.ts`, `lingo.ts`, `lingo-prepaid-to-payperuse.ts` +- `scripts-v2/runs/browser-use/migrate-free/` (uses `billingActions.migrate` with `noBillingChanges: true`) +- Trivially fits `{ customerId, plans: [{ expire: cusProductId, insert: { productId, version } }] }`. +- Note: scripts loop multiple matching cusProducts per customer and reload state mid-loop because `migrate()` mutates state. + +### A2. Multi-cusProduct subscription update — **the canonical complex case** +- `scripts-v2/runs/mintlify/migrate-credits/multi-update/` — replaces legacy AI-credits prepaid with V2 prepaid+consumable across N cusProducts sharing one Stripe sub. +- Operates on `CusProductGroup = { subscriptionId, cusProducts[] }`. +- Per group: `setupStripeBillingContext` once → per-product `computeCustomPlanNewCustomerProduct` → aggregate into `AutumnBillingPlan` → `evaluateStripeBillingPlan` → `executeBillingPlan`. +- This is structurally ~95% of the proposed interface, coupled to AI-credits-specific helpers (`resolveAiCreditsQuantity`, `setupUpdateFullProductContext`). + +### A3. Pricing-shape swap, cusProduct identity preserved +- `scripts/src/common/migrations/firecrawl-credit-pack-to-usage-price.ts` +- Keeps the same `customer_product_id`, replaces its child cus_ents/cus_prices with custom rows. Preserves balance/reset/expiry. +- **Does NOT fit `{expire, insert}`** — cusProduct stays. + +### A4. 1→N cusProduct split +- `scripts/src/common/migrations/split-one-cus-product-into-two.ts` (Email → Domain + Inbox). +- Expires one cusProduct, inserts N new ones referencing the SAME `subscription_ids`. +- Fits `{ expire, insert: [...] }` only if interface explicitly supports shared subscription_ids and balance/option carryover per inserted plan. + +### A5. Flag-flip / balance-clamp on existing rows +- `cont-use-to-single-use.ts`, `fix-creator-connections-interval.ts`, `browser-use-reset-spam-users.ts`, `additive-entitlement-migration.ts` (gifting branch), `retroactively-add-plan-item-to-customers.ts` "update" mode. +- Pure UPDATE on `entitlements` / `customer_entitlements`. No cusProduct churn, no Stripe. +- **Does not fit `{expire, insert}` at all** — separate primitive. + +### A6. Raw-DB bulk replace (no Stripe, no billing v2) +- `scripts-v2/runs/sebipaps/sandbox-product-sync/migrate-plan.ts` — chunked `delete cus_ent + delete cus_price + update cusProduct.internal_product_id`. +- Free-tier or 1:1 product remap. Bypasses billing v2 entirely. +- Should be subsumed by a "DB-only bulk swap" mode of the unified interface. + +--- + +## B. Catalog-prep archetypes (what runs BEFORE migrations) + +These are *preparation* patterns — extending the product catalog so a migration can land. + +### B1. Add entitlement (+optional price) to all versions of a product +- `retroactively-add-plan-item-to-versions.ts` — uses latest version that already has the ent as a TEMPLATE, clones onto older versions. +- `FINAL-add-entitlement-to-plans.ts` — adds feature to all versions matching id-prefix. +- `add-creator-connections-entitlement.ts`, `add-boolean-customer-entitlement.ts`, `add-feature-to-customers.ts`, `add-new-boolean-feature.ts`. +- Pure catalog (`entitlements`, `prices`) mutations. No customer-side touch. + +### B2. Backfill cus_ents for existing customers on extended products +- `retroactively-add-plan-item-to-customers.ts` — sibling of B1; per active cusProduct on listed products, insert missing `customer_entitlements` (and raw `customer_prices` if `requirePrice`). +- `add-creator-connections-cus-entitlement.ts`, `add-boolean-customer-entitlement.ts` (also does B1). +- Uses `initCusEntitlement` + direct `CusEntService.insert`. **Zero scripts use** `createFullCusProduct`. + +### B3. Catalog clone (sandbox → live, or version bump) +- `scripts-v2/runs/sebipaps/sandbox-product-sync/copy-products.ts` — `handleCopyFeatures`, `handleVersionProductV2`, `createProduct`. +- `scripts-v2/runs/mintlify/migrate-credits/steps/init-plan-resources/` — deterministic-id init for V2 prepaid/consumable price+entitlement; sibling entrypoint `setup-credits-catalog.ts`. +- Pattern: prep entrypoint shares folder + `data/` with the migration entrypoint. + +### B4. Stripe-side prep (clone archived Stripe prices) +- `scripts-v2/runs/mintlify/fix-archived-price/` — clones archived Stripe prices, updates `Price.config.stripe_price_id`, then patches subscription items. +- Doesn't fit `{expire, insert}` — it's a price-id swap on the same cusProduct. + +--- + +## C. Import patterns (Stripe → Autumn) + +### C1. Mintlify import (`scripts/src/mintlify/import/`) +- Source: Mongo orgs/deployments/period_usage. Stripe customers/subs. +- Steps: create customers → create entities (per deployment) → attach base plans via `createFullCusProduct` → derive addons from Stripe subs → "loose" custom entitlements via direct Drizzle insert → sync period usage into balances → cleanup. +- Plan resolver: Mongo `plan` string field. +- Custom allowances: signature-deduped per-customer entitlement rows. + +### C2. Firecrawl import (`scripts/src/firecrawl/import/`) +- Source: BigQuery `autumn_shared` (teams, orgs, balances, coupons). Stripe subs/prices/customers. +- Steps: select Stripe subs → resolve plan from `lookup_key` → resolve/create customer → ensure team entity → build target product state with per-customer overrides (custom allowance, custom topup price, signature dedup) → flush via `customerProductService.insertIfNotExists` → repair Stripe sub link → schedule end-of-cycle plan switches via `billingActions.attach` with `no_billing_changes: true` → sync team credit state → import coupon balances directly to `customer_entitlements.balance` with deterministic ids. +- Plan resolver: Stripe `price.lookup_key`. +- Custom allowances: per-customer freshly-created `entitlements` + `prices` rows with signature dedup cache. + +### Cross-cutting import friction (top 5) +1. Stripe price → Autumn plan resolver is org-specific (Mongo field vs lookup_key vs price.metadata). +2. Per-customer catalog overrides require signature-deduped per-customer `entitlements`+`prices` rows. +3. Linking to existing Stripe sub (idempotent), not creating it. `initOptions.subscriptionId` + repair. +4. Balance carryover is a separate concern: deterministic `balanceId`, direct write to `customer_entitlements.balance`, bypass `createFullCusProduct`. +5. Entity scoping + duplicate cleanup (same Stripe sub re-attached to wrong entity, stale entities, duplicate plans across entities). + +--- + +## D. Billing v2 architecture (the substrate) + +### D1. The 5-stage pipeline (every action follows this) +``` +setupBillingContext({ ctx, params }) -> BillingContext +handleErrors({ ctx, billingContext }) -> throws RecaseError +computePlan({ ctx, billingContext }) -> AutumnBillingPlan +evaluateStripeBillingPlan({ ctx, billingContext, autumnBillingPlan }) -> StripeBillingPlan +executeBillingPlan({ ctx, billingContext, billingPlan }) -> BillingResult +``` + +### D2. AutumnBillingPlan already expresses every migration shape +- `insertCustomerProducts: FullCusProduct[]` +- `updateCustomerProduct(s)?: CustomerProductUpdate[]` — patch options/status/canceled/anchor/sub_ids +- `deleteCustomerProduct(s)?: FullCusProduct[]` — for scheduled product replacement +- `customPrices?: Price[]`, `customEntitlements?: Entitlement[]`, `customFreeTrial?` +- `updateByStripeScheduleId?` — for schedule swaps +- `insertCustomerEntitlements?`, `updateCustomerEntitlements?` +- `lineItems?`, `customLineItems?` +- `autoTopupRebalance`, `upsertSubscription`, `upsertInvoice`, `refundPlan` + +### D3. evaluateStripeBillingPlan capabilities +- ✅ Multi-product, mixed prepaid/metered, schedules, cancel+replace, multi-entity, refunds, manual invoices. +- ✅ DB-only mode: `billingContext.skipBillingChanges = true` returns `{}` and execute no-ops Stripe. +- ⚠️ Single-customer per evaluation. No batch mode. + +### D4. Existing `migrate` action +- `actions/migrate/migrate.ts` is already in the registry. Need to read what it actually does — likely the simple version-bump path; we may extend or replace. + +### D5. Critical gap: shared custom rows +- `setupCustomFullProduct` always mints fresh per-customer `Price`/`Entitlement` rows when `hasCustomItems` is true. +- For bulk migration where 10k customers should share the same prepared price/ent rows, we need either: + - A `prepared` mode that reuses existing catalog rows, OR + - A separate one-shot prep phase that creates the shared rows once, with the migration action only inserting `customer_products` + `customer_prices`/`customer_entitlements` referencing those existing IDs. + +### D6. customizePlanV1 is PUT-style +- `items?: ApiPlanItemV1[]` — when provided, replaces full list. +- For migrations we want PATCH-style (incremental add/remove/update of plan items). + +--- + +## E. Verdict on `{ customerId, plans: [{ expire, insert }] }` + +Fits cleanly: **A1, A2 (with subscription-group dimension), A4 (with shared sub_ids), A6 (with DB-only mode).** + +Does NOT fit, needs separate primitive(s): +- **A3** — pricing-shape swap on same cusProduct identity. +- **A5** — flag-flip / balance-clamp on existing rows. +- **B1, B2** — catalog prep & cus_ent backfill (different layer entirely). +- **B3** — catalog clone (different layer). +- **B4** — Stripe price-id swap (different layer). +- **C1, C2 imports** — need 5 additional dimensions on top of migration shape. + +**Implication: the unified "migration interface" is really a layered system, not a single action.** diff --git a/.context/migration-interface/DECISIONS.md b/.context/migration-interface/DECISIONS.md new file mode 100644 index 000000000..60d09bed3 --- /dev/null +++ b/.context/migration-interface/DECISIONS.md @@ -0,0 +1,29 @@ +# Decisions + +Append-only log of architectural and design decisions. + +## 2026-04-29 — Project lives in `autumn/.context/`, not the outer worktree +The work targets first-class billing actions in `autumn/server/src/internal/billing/v2/actions/` and ships with tests in `autumn/`. Context is colocated with the code it describes. The outer `.context/migration-interface/` holds only a redirect README. + +## 2026-04-29 — Phase 1 is cataloging, not designing +The user explicitly asked to survey existing migration & import scripts before committing to interface shape. Risk of premature lock-in is high — there are at least three classes of patterns (DB-only mutations, DB+Stripe mutations, imports with balance carryover) and each could break the proposed shape differently. Output of Phase 1 is a verdict-per-script catalog. + +## 2026-04-29 — Build as core product feature, not internal tooling +Will eventually surface to users on the dashboard. Implications: lives in `autumn/server/src/internal/operations/` (peer to `billing/v2/`), has integration tests, no `as any`, follows full billing-action conventions for the heavyweight executors (`setupBillingContext` → compute Autumn plan → `evaluateStripeBillingPlan` → execute). + +## 2026-04-29 — Planner + Executor model, not single billing action +Migrations and imports differ in HOW they produce a plan but converge in HOW the plan executes. A single `migrate` action would conflate heterogeneous mutation surfaces (catalog, customer product graph, balance, Stripe repair) into one sprawling compute function. Splitting produces: +- Durable, inspectable, persistable `OperationPlan` artifact +- Dry-run identical to live (only `apply()` differs) +- Pluggable planners (MigrationDefinition, ImportDefinition) emit same operation graph +- Specialized executors per mutation surface, each with own idempotency/guards +- Reuses billing v2 pipeline INSIDE the CustomerProductGraphOperation executor — no replacement, just orchestration above + +## 2026-04-29 — Operations typed by mutation surface, not lifecycle intent +Initial design used `PlanOp = expire | insert | swap | patch` (lifecycle-flavored). Better: `Operation = CatalogOperation | CustomerProductGraphOperation | CustomerEntitlementOperation | BalanceOperation | StripeRepairOperation | CacheInvalidationOperation`. Each surface has different idempotency, transaction boundaries, and concurrency rules — surface-typed ops let executors specialize cleanly. + +## 2026-04-29 — `is_custom` decoupled from "uses shared catalog rows" +Today `setupCustomFullProduct` always mints fresh per-customer Price/Entitlement rows when customize is provided. For 10k-customer migrations this creates 10k Stripe prices unnecessarily. New model: `ProductItemPatch.add_existing { entitlement_id, price_id }` references shared catalog rows by id; `is_custom` becomes purely an ownership flag (one-off vs catalog), not "this row is per-customer." Existing custom rows untouched; new code uses new model. + +## 2026-04-29 — Ownership is first-class +Today repair logic is bespoke per-script because we can't distinguish imported from user-created customer_products. New model: `Ownership { origin: "imported"|"user_created"|"migrated", source?, imported_at? }` on every TargetAssignment. Likely stored as new column on `customer_products`. Unblocks safe re-runnable imports and convergence/repair semantics. diff --git a/.context/migration-interface/PLAN.md b/.context/migration-interface/PLAN.md new file mode 100644 index 000000000..11af39ba0 --- /dev/null +++ b/.context/migration-interface/PLAN.md @@ -0,0 +1,71 @@ +# migration-interface Plan + +## Phase 1: Catalog & Stress-Test (IN PROGRESS) +Goal: enumerate every migration/import pattern we've shipped, then check the proposed interface against each one. Output: a catalog doc with per-script summaries and "interface covers? / what breaks?" verdicts. + +- Survey `scripts/src/common/migrations/` (retroactively-add-plan-item-to-customers, -to-versions, -plan-items, plus siblings) +- Survey `scripts-v2/runs/` migration scripts (mintlify migrate-credits is the canonical example) +- Survey `scripts/mintlify-import.sh` and `scripts/firecrawl-import.sh` import flows +- Categorize each script along axes: + - DB-only vs DB + Stripe + - Single-customer vs bulk + - Plan transformation shape (add item / swap plan / change pricing / cancel-and-replace / schedule) + - Custom per-customer overrides (rollover, lifetime balance, balance carryover) + - Preparation step required (new prices/products created before migration) +- Verdict per script: covered by proposed interface? What's missing? + +## Phase 2: Interface Design +- Lock the migration action signature(s) — likely one new `migrate` action alongside `multiAttach` and `createSchedule` +- Solve the "insert + customize" friction: + - Move customize from PUT-style (full item list) to PATCH-style (incremental add/remove/update) + - Decide: do migrations create new custom price/entitlement rows, or reuse a single per-plan row? (Stripe price/product creation is the binding constraint) +- Decide whether "preparation" (creating shared prices/entitlements ahead of migration) is a separate action or a step in the same one +- Validate design against catalog from Phase 1 + +## Phase 3: Implementation +- Build the action(s) under `autumn/server/src/internal/billing/v2/actions/` +- Tests: integration tests covering catalog scenarios from Phase 1 +- Reuse `evaluateStripeBillingPlan`, `setupBillingContext` patterns from existing actions + +## Phase 4: Migrate Existing Scripts +- Port mintlify migrate-credits to use the new action +- Port retroactively-add-plan-item-to-customers +- Each port validates the interface; refine as needed + +## Phase 5 (stretch): Imports +- Extend or sibling action for Stripe→Autumn linkage +- Handle the hard cases: balance carryover for metered features, custom configurations (rollovers, lifetime balances), many-plans Stripe accounts +- Goal: collapse the bespoke mintlify-import.sh / firecrawl-import.sh logic + +--- + +## Initial Design Sketch (from kickoff conversation) + +**Scope of one call:** one customer, one Stripe subscription's worth of customer_products. + +**Proposed shape:** +```ts +{ + customerId: string, + subscriptionId?: string, // inferable from plans + plans: [ + { + expire?: { customer_product_id?: string; plan_id?: string; entity_id?: string }, + insert?: { plan_id: string; customize?: ... }, + } + ], + schedules?: [...] // future, optional +} +``` + +**Open questions:** +1. Customize: PATCH vs PUT — strongly leaning PATCH (incremental add/remove/update of plan items) +2. Custom prices: per-customer rows vs shared per-plan row reused across migrations (Stripe-side cost is the constraint) +3. Preparation step (creating shared prices/entitlements + backfilling onto prior plan versions and customer_products): part of this interface or a separate "prepare migration" action? +4. Mapping public `apiPlanItemV1` ↔ DB `price + entitlement` pair — how does the interface accept input? + +## Architecture Notes +- Build on `evaluateStripeBillingPlan` — the Autumn-plan → Stripe-state mapper already handles schedules, multi-entity, mixed prepaid/metered. Don't reinvent. +- `customer.processor?.id` is Stripe customer ID (per autumn-operations-constraints rule). Watch the ID confusion. +- Stripe writes must respect: amount conversion via `atmnToStripeAmount`, idempotent price creation via `createStripePriceIFNotExist`, `proration_behavior: "none"` for surgical item changes. +- This is product code, not script code — full TypeScript discipline, no `as any`, full test coverage. diff --git a/.context/migration-interface/PROPOSAL.md b/.context/migration-interface/PROPOSAL.md new file mode 100644 index 000000000..1415f1d24 --- /dev/null +++ b/.context/migration-interface/PROPOSAL.md @@ -0,0 +1,306 @@ +# Proposal: Operation Plan + Pluggable Planners + +## Mental model + +> **Planning produces a typed `OperationPlan`. Execution consumes it.** +> Migrations and imports differ in how they *produce* the plan; everything downstream — guards, dry-run, persistence, execution, reconciliation — is shared. + +This is a strict departure from "build one big billing action." The migration interface is not a single action — it is a **planner / executor pair**, where the plan is a durable, inspectable artifact and execution is a typed reducer over operations. + +The reason: scripts today informally do exactly this lifecycle (scope → load → resolve → prepare → plan → guard → execute → verify), but the contracts between stages are bespoke. The win is canonizing the stage contracts, not unifying the verbs. + +--- + +## Why planner + executors (vs. single billing action) + +1. **Operations are heterogeneous.** Catalog mutations, customer-product graph changes, balance syncs, Stripe repair, cache invalidation — each has different idempotency, different transaction boundaries, different blast radius. One "migrate" action that handles all of them gets a sprawling `compute*Plan` function. Separate executors keep each surface clean. +2. **Plans are first-class artifacts.** Persistable. Diffable. Re-runnable. Approval-able. Eventually user-facing on the dashboard ("preview migration → approve → apply"). +3. **Dry-run = real plan.** Same code path produces the plan; only `apply()` differs. No drift. +4. **Imports vs migrations diverge cleanly.** They're different *planners* emitting the same operation graph. Shared execution; specialized planning. +5. **Concrete reuse path.** `CustomerProductGraphOperation.replace` executor wraps the existing 5-stage billing v2 pipeline. We're not replacing billing v2 — we're putting an orchestration layer above it. + +--- + +## Architecture + +### Module placement + +``` +autumn/server/src/internal/operations/ + types/ + operation.ts -- Operation discriminated union + operationPlan.ts -- OperationPlan, CustomerOperationPlan, RunContext + operationResult.ts -- OperationResult, GuardResult, MigrationDiff + productItemPatch.ts -- ProductItemPatch union (PATCH-style customize) + executors/ + catalog/ -- CatalogOperation executor + customerProductGraph/ -- CustomerProductGraphOperation executor (wraps billing v2) + customerEntitlement/ -- CustomerEntitlementOperation executor + balance/ -- BalanceOperation executor + stripeRepair/ -- StripeRepairOperation executor + cache/ -- CacheInvalidationOperation executor + planners/ + migration/ -- MigrationDefinition runner + import/ -- ImportDefinition runner + guards/ -- composable, named guard sets + reporting/ -- diff, audit, CSV export + persistence/ -- migration_runs / migration_plans schema + service + execute.ts -- top-level executePlan(plan, { dryRun }) +``` + +This sits **as a peer** to `billing/v2/`, not under it. `billing/v2/` is the substrate; `operations/` is the orchestration layer. + +### Type model + +```ts +// The durable artifact +type OperationPlan = { + run: RunContext; // org_id, env, dry_run, source description, created_at + catalog: Operation[]; // run-level (executes once, before per-customer) + customers: CustomerOperationPlan[];// per-customer + warnings: GuardResult[]; // non-blocking + blockers: GuardResult[]; // execution refuses if any blockers +}; + +type CustomerOperationPlan = { + customer_id: string; + // Operations declared in execution order. Within a customer, ops execute serially. + // Operations targeting the same Stripe subscription are batched by the executor + // into a single subscriptions.update — this is the existing multi-update atomicity + // boundary. + operations: Operation[]; + guards: GuardResult[]; + preview: CustomerPreview; // human-readable diff of intended changes +}; + +type Operation = + | CatalogOperation + | CustomerProductGraphOperation + | CustomerEntitlementOperation + | BalanceOperation + | StripeRepairOperation + | CacheInvalidationOperation; + +// Mutation surface 1: catalog +type CatalogOperation = + | { type: "ensure_feature"; spec: FeatureSpec } + | { type: "ensure_plan_item"; product_selector: ProductSelector; item: ProductItemSpec } + | { type: "ensure_shared_price"; signature: string; spec: PriceSpec } + | { type: "ensure_shared_entitlement"; signature: string; spec: EntitlementSpec } + | { type: "link_stripe_resource"; price_id: string; stripe_price_id?: string; stripe_product_id?: string } + | { type: "swap_archived_stripe_price"; price_id: string }; + +// Mutation surface 2: customer product graph (the big one) +type CustomerProductGraphOperation = + | { type: "attach"; assignment: TargetAssignment; ownership: Ownership } + | { type: "replace"; subscription_id?: string; + from: CusProductSelector[]; // expire these + to: TargetAssignment[]; // insert these + carry?: { balance: boolean; subscription_ids: boolean; anchors: boolean } } + | { type: "expire"; targets: CusProductSelector[]; reason: string } + | { type: "schedule"; phases: ScheduledPhase[] }; + +// Mutation surface 3: customer entitlement (B-layer in old proposal) +type CustomerEntitlementOperation = + | { type: "backfill_for_customers"; product_selector; feature_id; require_price?: boolean } + | { type: "patch_flag"; selector; flags: Partial> } + | { type: "clamp_balance"; selector; ceiling: number }; + +// Mutation surface 4: balance (imports especially) +type BalanceOperation = + | { type: "seed_balance"; balance_id: string; cus_ent_selector; amount: number; entity_id?: string } + | { type: "set_balance"; cus_ent_id: string; balance: number } + | { type: "delete_stale_balance"; cus_ent_id: string; reason: string }; + +// Mutation surface 5: stripe repair (imports especially) +type StripeRepairOperation = + | { type: "link_subscription"; cus_product_id: string; stripe_subscription_id: string } + | { type: "expire_orphaned_imported_cus_product"; cus_product_id: string }; + +type CacheInvalidationOperation = + | { type: "invalidate_customer_cache"; customer_ids: string[] } + | { type: "invalidate_products_cache" }; +``` + +### TargetAssignment + ProductItemPatch + +The two key composite types. + +```ts +type TargetAssignment = { + product: { id: string; version?: number }; + entity_id?: string | null; + subscription_id?: string | null; // for imports linking to existing Stripe sub + feature_quantities?: FeatureQuantity[]; + item_patches?: ProductItemPatch[]; // PATCH-style customize + billing_dates?: { trial_ends_at?: number; reset_cycle_anchor?: number; billing_cycle_anchor?: number }; + ownership?: Ownership; // imported / user-created / migrated +}; + +// PATCH-style. Compiles into FullProduct + customPrices + customEnts +// that existing billing v2 compute already understands. +type ProductItemPatch = + | { op: "add_existing"; entitlement_id: string; price_id?: string } // reference shared catalog rows + | { op: "replace_feature"; feature_id: string; entitlement_id: string; price_id?: string } + | { op: "remove_feature"; feature_id: string } + | { op: "override_allowance"; feature_id: string; allowance: number; unlimited?: boolean } + | { op: "override_price"; feature_id?: string; price: PricePatch } // mints a custom Price row + | { op: "set_options"; feature_id: string; options: { quantity?: number } }; + +type Ownership = { + origin: "imported" | "user_created" | "migrated"; + source?: string; // e.g. "stripe_subscription:sub_abc" + imported_at?: number; +}; +``` + +`add_existing` is the load-bearing fix: it lets a 10k-customer migration reference one shared catalog row instead of minting 10k custom rows. **`is_custom` becomes an ownership flag (one-off vs catalog) decoupled from "uses custom prepared rows."** + +### Execution model + +```ts +// Top-level entry point +async function executePlan(plan: OperationPlan, opts: { dryRun: boolean }): Promise + +// Strict execution rules: +// 1. If plan.blockers is non-empty AND !dryRun → throw, refuse. +// 2. Catalog ops execute first, in declared order, before any customer ops. +// 3. Customer plans execute in parallel (configurable concurrency); same-customer +// operations execute serially in declared order. +// 4. Within one customer, operations targeting the same Stripe subscription are +// GROUPED by the CustomerProductGraphOperation executor and emitted as ONE +// subscriptions.update via the existing evaluateStripeBillingPlan + executeBillingPlan +// path. This preserves multi-update atomicity. +// 5. Each operation returns OperationResult { status, diff?, error?, artifacts? }. +// 6. Dry-run produces the same operations list; executors emit `would_apply` results +// with diffs but mutate nothing. +``` + +**The CustomerProductGraphOperation executor for `replace` is essentially today's `multi-update/`** generalized: build the per-product context list, call `computeCustomPlanNewCustomerProduct` per item, aggregate into `AutumnBillingPlan`, run through `evaluateStripeBillingPlan` + `executeBillingPlan`. Everything downstream of the Operation type is reused. + +### Persistence + +Plans persist to DB for non-trivial runs. Schema: + +``` +migration_runs -- one per orchestrated run (org, env, source, status) +migration_plans -- the OperationPlan JSON, snapshot at planning time +migration_ops -- one row per operation, with status + diff + result +migration_artifacts -- CSV exports, snapshots, audit rows +``` + +Trivial cases (single-customer one-off) can skip persistence and go straight through `executePlan`. Bulk runs persist. + +States: `draft → planned → applying → completed | failed | partial`. A `partial` failed run is resumable: the executor skips ops with `status: applied` and re-runs `pending` / `failed` ops. + +### Planners + +```ts +type MigrationDefinition = { + id: string; + selectCandidates(ctx): Promise; + buildTargetState(ctx, customer): Promise; + plan(ctx, target): OperationPlan["customers"][number]; +}; + +type ImportDefinition = { + id: string; + loadExternalSource(ctx): Promise; + resolveIdentities(ctx, source): Promise; + mapTargets(ctx, source, identities): Promise; + plan(ctx, target): OperationPlan["customers"][number]; +}; +``` + +Mintlify import becomes `ImportDefinition`. Mintlify migrate-credits becomes `MigrationDefinition`. Both emit the same `OperationPlan` shape. + +--- + +## Phased build order + +### v0 — Migration Core (3-4 weeks) +**Concrete deliverable: port mintlify migrate-credits' multi-update path to use this system.** That single port validates 80% of the abstraction. + +1. `Operation` type + `ProductItemPatch` compiler (`patches → FullProduct + customPrices + customEnts`). +2. `CustomerProductGraphOperation.replace` executor wrapping the existing multi-update pipeline. +3. `OperationResult` + `CustomerPreview` shapes (Autumn diff + Stripe sub-update preview). +4. Guard set: `subscription-replace` (mismatched-sub-in-group, missing payment, anchor drift, collection_method preservation). +5. `executePlan` for in-memory plans (no DB persistence yet). +6. Integration tests: port the mintlify migrate-credits scenarios. +7. Migration runner harness for scripts-v2 (one-line: `runPlan(planner, opts)`). + +### v0.5 — Catalog Prep (1-2 weeks) +8. `CatalogOperation` executor: `ensure_feature`, `ensure_plan_item`, `ensure_shared_price`, `ensure_shared_entitlement`, `link_stripe_resource`. +9. Run-level catalog ops execute before per-customer ops. +10. Port `retroactively-add-plan-item-to-versions` + `retroactively-add-plan-item-to-customers` to the new system (validates catalog → customer-entitlement composition). + +### v0.75 — Persistence + Resume (1 week) +11. `migration_runs` / `migration_plans` / `migration_ops` schema. +12. `executePlan` writes operation results; partial-failure resume. +13. Read-only view of past runs (no UI yet, just service methods). + +### v1 — Auxiliary surfaces (2 weeks) +14. `BalanceOperation` executor (Mintlify period_usage, Firecrawl coupon balances). +15. `CustomerEntitlementOperation` executor (flag-flips, balance clamps, backfills). +16. `StripeRepairOperation` executor. +17. Port one full Firecrawl import slice as a smoke test. + +### v1.5 — Imports as planner (2-3 weeks) +18. `ImportDefinition` runner with identity resolution + source normalization stages. +19. Port mintlify-import to use this. Firecrawl-import follows. + +### v2 (deferred) — Dashboard + API +20. Dashboard view: list runs, inspect plan, dry-run preview, approve & apply. +21. Public API for self-serve migrations. + +--- + +## Synthesis: what changed from v1 of this proposal + +| v1 (PROPOSAL.md before) | v2 (now) | Why | +|---|---|---| +| Single `migrate` action with `PlanOp` discriminated union | Planner + executors with typed operations by mutation surface | Heterogeneous mutation surfaces don't belong in one action | +| 3 layers (catalog / backfill / transition) | Flat operation graph with execution ordering rules | Layers were artificial; ops at the same level with declarative dependencies is cleaner | +| `use_shared_rows` flag bolt-on | `ProductItemPatch.add_existing { entitlement_id, price_id }` first-class | Semantics are clearer when they're a discrete patch op | +| `is_custom` overloaded for "one-off" + "uses custom rows" | Decouple: `is_custom` = ownership flag; shared rows = first-class catalog rows | Resolves a long-standing concept conflation | +| Imports = migration + 5 extensions | Imports = different planner emitting same operation graph | Cleaner separation of concerns; identity/source/mapping is genuinely different from migration | +| Action-level `preview` flag | First-class plan artifact; dry-run = identical plan path | Plan becomes inspectable, persistable, approvable | +| No persistence story | `migration_runs/_plans/_ops` schema; resumable partial runs | User asked for "core feature, not internal tool" — first-class needs persistence | +| No ownership concept | `Ownership` first-class on every TargetAssignment | Repair logic is currently bespoke per-script because we can't tell imported from user-created | + +--- + +## Open design questions (the load-bearing ones) + +1. **Plan persistence: opt-in or default-on?** + Lean: **default-on for `bulk` planners (>1 customer); opt-out for one-shot operations.** Persistence enables resume, audit, dashboard surfacing — worth the schema cost. + +2. **Operation ordering within a customer plan: declared or topologically derived?** + Lean: **declared.** Planners produce ops in execution order. Simpler, and the planner already knows what depends on what (e.g. `attach` before `seed_balance`). No DAG solver needed. + +3. **Subscription-group atomicity: explicit or implicit?** + Lean: **implicit, in the executor.** The executor groups operations by Stripe sub before executing. This keeps the operation list flat and forgiving — caller doesn't have to think about Stripe's batching boundary, the executor handles it. + +4. **Ownership flag: stored where?** + Options: (a) new column on `customer_products`, (b) jsonb metadata, (c) separate `customer_product_ownership` table. + Lean: **new column** — it's load-bearing for repair queries; jsonb is too soft. + +5. **Existing `billingActions.migrate`: keep, deprecate, or absorb?** + Need to read it. If it's a thin version-bump helper, keep as a shorthand that produces a one-op plan. If it's heavier, deprecate in favor of `MigrationDefinition`. + +6. **Where does identity resolution live for imports?** + In the `ImportDefinition.resolveIdentities` stage. The output is an `IdentityMap` that subsequent stages consume. Don't bake identity logic into Operations — keep operations identity-agnostic (they take `customer_id`, not "find or create customer with this email"). + +7. **Public-facing types: when?** + Defer until v2 (dashboard). Internal types can move freely; public types lock when we expose this to users. Avoid premature stability commitments. + +8. **`is_custom` decoupling: backwards compat?** + Existing custom rows have `is_custom: true`. New "shared catalog rows referenced by patch" don't set this flag. We need a cleanup or co-existence story for current per-customer custom rows. Lean: leave existing rows alone; new code uses the new model; eventually a `null is_custom` migration cleans up. + +--- + +## Open question deferred from v1 + +- **PATCH vs PUT customize:** resolved by `ProductItemPatch` discriminated union (PATCH-style, with `add_existing` for shared rows). +- **Custom prices: per-customer rows vs reused per-plan rows:** resolved by `add_existing` / `ensure_shared_*` catalog ops. +- **Preparation step placement:** resolved — it's `CatalogOperation` ops inside the same plan. diff --git a/.context/migration-interface/STATUS.md b/.context/migration-interface/STATUS.md new file mode 100644 index 000000000..dcb9cbf3c --- /dev/null +++ b/.context/migration-interface/STATUS.md @@ -0,0 +1,35 @@ +# migration-interface + +## Goal +Design a shared billing-action interface for customer migrations and imports so scripts stop hand-rolling Stripe + Autumn DB orchestration. Eventually exposed to users on the dashboard, so it must be a first-class product feature with tests — not an internal tool. + +## Current Phase +Phase 1: Discovery complete (catalog written). PROPOSAL.md v2 drafted — planner + executor model with typed operations. Awaiting user review of architecture and 8 open design questions. + +## Branch +john/migration-interface + +## Active Tasks +- (none yet — will create parallel cataloging tasks if Phase 1 surface gets large) + +## What's Done +- Phase 1 discovery: cataloged `scripts/src/common/migrations/` (23 files), `scripts-v2/runs/` migration scripts, mintlify+firecrawl import chains, and billing v2 action machinery (`multiAttach`, `createSchedule`, `updateSubscription`, `evaluateStripeBillingPlan`, `AutumnBillingPlan`, `customizePlanV1`, `setupCustomFullProduct`). +- See `CATALOG.md` for full pattern inventory and `PROPOSAL.md` for the layered-interface design. + +## What's Next +- Walk through `PROPOSAL.md` with user — confirm 3-layer model and discuss the 7 open design questions +- Read existing `actions/migrate/migrate.ts` to decide extend-vs-replace +- Lock Layer 3 scope (include `patch` op? or split into `migrate` + extended `updateSubscription`?) +- Begin Phase 2: design `MigrateParams`, `MigrateBillingContext`, `computeMigratePlan` signatures concretely + +## Active Blockers +- None + +## Key Files +- `server/src/internal/billing/v2/actions/index.ts` — where new action(s) will register +- `server/src/internal/billing/v2/actions/multiAttach/multiAttach.ts` — closest existing analog +- `server/src/internal/billing/v2/actions/createSchedule/createSchedule.ts` — schedule pattern reference +- `server/src/internal/billing/v2/actions/updateSubscription/updateSubscription.ts` — single-subscription mutation reference +- `server/src/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan.ts` — Autumn plan → Stripe state mapper +- `shared/api/billing/common/customizePlan/customizePlanV1.ts` — public customize schema +- `shared/api/products/items/apiPlanItemV1.ts` — public plan item shape diff --git a/.gitignore b/.gitignore index b85bd1dfb..ea34c2ba8 100644 --- a/.gitignore +++ b/.gitignore @@ -145,5 +145,11 @@ TAKEHOME.md AGENTS.md server/.turbo + +.trigger .context .opencode/ + + +./AGENTS.md +./CLAUDE.md \ No newline at end of file diff --git a/.opencode/opencode.json b/.opencode/opencode.json index 958f8071c..14dbb803e 100644 --- a/.opencode/opencode.json +++ b/.opencode/opencode.json @@ -40,12 +40,7 @@ "url": "https://mcp.plain.com/mcp", "oauth": {} }, - "slack": { - "type": "remote", - "url": "https://mcp.slack.com/mcp", - "oauth": {} - }, - "autumn_prod_internal": { + "autumn-internal": { "type": "local", "command": [ "sh", @@ -53,20 +48,7 @@ "cd \"/Users/johnyeocx/Autumn/autumn-cloud\" && exec infisical run --env=prod --recursive -- bun run \"/Users/johnyeocx/Autumn/autumn-cloud/ai/src/mcp/index.ts\"" ], "env": { - "AUTUMN_CLOUD_ROOT": "/Users/johnyeocx/Autumn/autumn-cloud", - "AUTUMN_MCP_ENV": "prod" - } - }, - "autumn_dev_internal": { - "type": "local", - "command": [ - "sh", - "-c", - "cd \"/Users/johnyeocx/Autumn/autumn-cloud\" && exec infisical run --env=dev --recursive -- bun run \"/Users/johnyeocx/Autumn/autumn-cloud/ai/src/mcp/index.ts\"" - ], - "env": { - "AUTUMN_CLOUD_ROOT": "/Users/johnyeocx/Autumn/autumn-cloud", - "AUTUMN_MCP_ENV": "dev" + "AUTUMN_CLOUD_ROOT": "/Users/johnyeocx/Autumn/autumn-cloud" } } }, diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 5bf9e2c58..000000000 --- a/AGENTS.md +++ /dev/null @@ -1,153 +0,0 @@ - - -# Concise Comments - -Cap any single comment block at **two lines**. Most should be one. - -- Default to writing no comment. Only add one when the WHY is non-obvious — a hidden constraint, a subtle invariant, a workaround for a specific bug, behavior that would surprise a reader. -- If you're tempted to write a third line, the comment is wrong: either delete it, or replace it with a clearer name / smaller function so the code explains itself. -- Don't restate WHAT the code does — well-named identifiers already do that. -- Don't reference the current task, fix, or callers ("used by X", "added for the Y flow", "handles the case from issue #123") — those belong in the PR description and rot as the codebase evolves. -- This applies to inline `//` comments AND JSDoc/docstring blocks. A JSDoc whose summary line + one detail line covers the WHY is fine; a four-line paragraph is not. - -If something genuinely needs more explanation, link to a doc or a memory file rather than inlining a wall of text in source. - -# Autumn Shared Utils - -Before writing inline `.filter()`, `.find()`, `.some()`, boolean predicate, or `To` transform logic over Autumn objects (`Price`, `Entitlement`, `FullCusProduct`, `FullCustomer`, `Feature`, etc.), **check `autumn/shared/utils/` for an existing helper.** Reaching for `array.filter(... === id)` directly is almost always a sign the utility was missed. - -The package is organized resource-first, pattern-second. Within each `Utils/` folder: - -- `classify*/` — `is*` boolean predicates (`isPrepaidPrice`, `isCustomerProductPaidRecurring`) -- `convert*/` — `To` transforms (`cusProductToPrices`, `entToPrice`) -- `find*/` — `Array.find` lookups (`findFeatureById`, `findPriceByFeatureId`) -- `filter*/` — `Array.filter` collections (`filterCustomerProductsByFeatureId`) -- `enrich*` files — augment with joined data (`enrichEntitlementWithFeature`) - -**If the helper you need doesn't exist, ALWAYS ask the user before adding one.** Naming and folder placement are cross-cutting and non-trivial — wrong placement clutters `@autumn/shared` for every consumer. - -Full convention (folder tree, naming nuances, anti-patterns): see the `shared-utils` skill. - -# Installing External Skills - -Third-party skills installed via `bunx skills add ` land under each agent's local skill dir (`.claude/skills/`, `.cursor/skills/`, etc.). Those locations are NOT a source of truth — `bun ai sync` only reads from `ai/config/skills/**` and prunes anything else it manages, so a raw `bunx skills add` will not propagate to the other consumer repos (autumn, cloud). - -## Workflow - -1. Install via the CLI as usual: - ```sh - bunx skills add / - ``` -2. Move the installed skill folder(s) into `ai/config/skills/external//`. `external/` is core, so both `autumn` and `cloud` consume it. Use `cloud/external/` only if the skill references cloud-only code. -3. Delete the leftover copies from `.claude/skills/`, `.cursor/skills/`, `.agents/skills/`, `.opencode/skills/` — `bun ai sync` will recreate them as symlinks. -4. Run `bun ai sync` to symlink the skill into every agent dir across every repo that pulls the `ai/` submodule. - -## Notes - -- Skill folder names must be globally unique across `ai/config/skills/**` (sync flattens them). -- Keep upstream `SKILL.md` frontmatter intact — `name` and `description` drive when the agent loads it. Only edit if the description is not specific enough about WHEN to use the skill. -- If the skill ships a `references/` or `scripts/` subfolder, copy the whole directory tree, not just `SKILL.md`. -- Re-running `bunx skills add` upstream-updates: install fresh, diff against `ai/config/skills/external//`, then promote the changes. - -# Scope Cache Refresh Changes Safely - -When changing cache-refresh behavior for API routes: - -- Prefer editing `server/src/honoMiddlewares/refreshCacheConfigs.ts` to add or remove route entries. -- Do **not** remove or bypass logic in `server/src/honoMiddlewares/refreshCacheMiddleware.ts` unless the user explicitly asks to change middleware behavior globally. -- If the request is ambiguous, ask whether they want a route-level config change or a global middleware behavior change. - -# Cache Version Increment Policy - -Treat `cache_version` as a DB-side stale-sync guard for `syncItemV4`, not a general cache mutation counter. - -## Increment cache_version only when needed - -Increment only for DB updates that must not be overwritten by stale sync payloads read from cache (for example lifecycle or billing transitions). - -## Do not increment on cache-side/runtime patch paths - -For runtime balance/reset/cache patch flows, do not bump `cache_version` in cache writers or Lua update scripts. - -Examples: - -- FullSubject cache patch helpers (`updateSubjectBalanceCache`, reset/deduction cache patch sync helpers) -- FullSubject Lua cache update scripts (`updateSubjectBalances`) -- Update-balance runtime paths that patch Redis and then sync - -## Call-site rule for CusEntService.update - -When using `CusEntService.update(...)` in runtime FullSubject cache patch flows, set `incrementCacheVersion` explicitly. - -- Use `incrementCacheVersion: false` for routine balance/reset/adjustment updates that are mirrored to Redis. -- Use `incrementCacheVersion: true` only for intentional DB-side stale-write protection transitions. - -## Why - -Incorrect version bumps create `CACHE_VERSION_MISMATCH` conflicts in `syncItemV4`, causing repeated invalidation and stale/lost update behavior. - -## Legacy exception (review-required) - -There is a legacy-compatibility exception in the adjust-balance flow: - -- `adjustBalanceDbAndCache` currently uses `CusEntService.increment/decrement`, which increments `cache_version`. -- Treat this as a reviewable legacy action, not a pattern to copy into new runtime/cache patch paths. -- Any new or refactored runtime balance/reset/cache patch code should continue following this policy and avoid adding new cache-version bumps by default. - -## Project Context System - -Projects maintain state in `.context//` folders across sessions. Tasks are optional parallel workstreams within a project. - -### Default: NOT interacting with a project -**Unless the user explicitly mentions a project or task by name, assume the current conversation is NOT associated with any project.** Session-start hooks may surface a list of active projects as reference material — that alone is NOT a signal that the current work belongs to any of them. - -Do not: -- Read `.context/**` files proactively -- Write, update, or append to any project's STATUS.md / DECISIONS.md / sessions -- Assume a script, audit, or change is part of a project just because it touches files related to one (e.g. a user-of-framework script is not part of the framework's project) - -Only engage with `.context//` when the user explicitly references the project, opens a task in it, or asks for project-tracking actions. - -### Reading context (when a project IS referenced) -When the user mentions a project or task name and `.context//` exists: -1. Read project STATUS.md first (20-30 line "resume card") -2. If the project has `tasks/`, list active tasks -3. If the user mentions a specific task, read `tasks//STATUS.md` -4. Read the most recent session summary if more detail is needed -5. Do NOT read everything upfront. Use progressive disclosure. - -### Updating context (at breakpoints, NOT continuously) -Only update when the current work IS part of a project (see default-off rule above). When it is, update at these moments ONLY: -- Phase or milestone completed -- Architectural decision made (append to DECISIONS.md) -- User says they're done or switching tasks -- Blocker discovered or resolved -- Task created, completed, or handed off - -Do NOT update context during normal coding work. Work first, compact at breakpoints. - -A STATUS.md entry should record changes to the project itself -- not one-off work that merely uses the project (e.g. writing a consumer script of a framework is not a framework-project update). - -### Compaction quality -STATUS.md must be: -- Correct (reflects actual current state, not stale) -- Complete (no critical information missing) -- Concise (project < 30 lines, task < 20 lines) - -Always REWRITE STATUS.md completely rather than append. - -### File structure -``` -.context// - STATUS.md -- project resume card (includes Active Tasks section) - PLAN.md -- phases and architecture - DECISIONS.md -- append-only decision log - sessions/ -- dated session summaries - tasks/ -- optional parallel workstreams - / - STATUS.md -- task resume card - DECISIONS.md -``` - -### Task handoff -When a task is being handed to another agent, ensure the task's STATUS.md is up to date -- it's the handoff artifact. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index daa0320d5..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,137 +0,0 @@ -# Basic rules -- Never run a "dev" or "build" command, chances are I'm already running it in the background. Just ask me to check for updates or whatever you need -- Never ever ever write a "TO DO" comment. If you've been told to do something, DO IT. Don't stop halfway. Never give up and just leave a "to do" comment and say - "haha heres working code :)" - that is unacceptible. Always finish your task, no matter how many iterations you need to perform. -- DO NOT alter .gitignore -- JS Doc comments should be SHORT and SWEET. Don't need examples unless ABSOLUTELY necessary -- When using db schemas in Drizzle, import them from '@autumn/shared', and don't do schemas. -- **Spell out variable names in full form** - avoid abbreviations in variable/function names. Use `customerProduct` not `cusProduct`, `customerEntitlements` not `cusEnts`, `organization` not `org` (in new code). Clarity over brevity. - -# Testing -- When writing tests, ALWAYS read: - 1. `server/tests/_guides/general-test-guide.md` - Common patterns, client initialization, public keys - 2. Case-specific guide (e.g., `server/tests/_guides/check-endpoint-tests.md` for `/check` tests) -- When running tests, ALL server-side console logs go to the server's logs which you do not have access to. You must ask the user to paste you in the logs, instead of expecting the server logs to magically appear -in the test logs. Use your common sense - -# Linting and Codebase rules -- You can access the biome linter by running `bunx biome check `. Always specify a folder path, as the codebase is quite large and you will get out of scope errors that you are not burdened to correct. If you would like to let biome automatically fix as much as it can, use `bunx biome check --write ` - -- Note, biome does not perform typechecking. For typechecks, always `cd` into the relevant workspace and run `bun ts`. Do not run `bunx tsgo --build --noEmit` directly, and do not use `tsc` for workspace typechecks. - -- The `server/src/_luaScriptsV2/` folder contains Lua scripts for Redis atomic operations. Redis uses **Lua 5.1** - there is NO `goto` statement (added in Lua 5.2), so use if/else blocks instead. - -- This codebase uses Bun as its preferred package manager and Node runtime. - -- **ALWAYS import from `zod/v4`**, not from `zod` directly. Example: `import { z } from "zod/v4";` - -- **ALWAYS use named import for Decimal.js**: `import { Decimal } from "decimal.js";` NOT `import Decimal from "decimal.js";` - -- Always prefer foo({ bar }) over foo(bar) method signatures - no matter if we are using only one argument or not, object as param are always better, as in the future when wanting to change the order of parameters, or add new ones - its easier. - -- When creating "hooks" folders, don't nest them under "components" - -- Functions (unless there's a very good reason) should always take in objects as arguments. Object params are named and easy to understand. - -- For regular functions, use inline object types in the function signature rather than creating separate type definitions. Only create named types when they're reused across multiple functions or exported. - ```typescript - // ❌ BAD - Unnecessary type definition for single-use params - type DoSomethingParams = { - ctx: AutumnContext; - customerId: string; - }; - const doSomething = async ({ ctx, customerId }: DoSomethingParams) => { ... } - - // ✅ GOOD - Inline object type - const doSomething = async ({ ctx, customerId }: { ctx: AutumnContext; customerId: string }) => { ... } - ``` - -- This codebase uses Bun for all of its operations in `/server`, `/vite` and `/shared`. It uses Bun for the package management, Bun for the workspace management and Bun for the runtime. Prefer Bun over PNPM. If you ever want to trace a package dependency tree, run `bun why ` which will tell you why a certain package was installed and by who. -- Prefer Guard clauses "if(!admin) return;" over explicity "if(admin) do X;" Early returns are better - -- For single-line if statements (especially guard clauses), omit curly braces to keep code neat: `if (!isValid) throw error;` instead of wrapping in braces. - -- Do not run "npx tsc" - run "tsc" instead as it is installed globally. - -- **ALWAYS use `.meta()` for zod-openapi schema registration**, NOT `.openapi()`. Example: `ApiProductSchema.meta({ id: "Product" })` - -- **ALWAYS use `c.req.param()` to get route parameters in Hono handlers**, NOT `c.req.valid("param")`. Example: `const { customer_id } = c.req.param();` - -- When referring to a `customer_entitlement` object (or plural `customer_entitlements`), always use the full name. Do not abbreviate to "entitlement" or "entitlements" as this will be confused with the separate `entitlement` object. - -## Error Handling in API Routes -- NEVER use `c.json({ message: "...", code: "..." }, statusCode)` pattern for input validation or expected errors in Hono routes -- ALWAYS throw `RecaseError` from `@autumn/shared` for all validation errors, not found errors, forbidden errors, etc. -- For internal/unexpected errors (like missing configuration, database errors, etc.), throw `InternalError` from `@autumn/shared` -- The onError middleware automatically converts these errors to appropriate HTTP responses -- Examples: - ```typescript - // ❌ BAD - Don't do this - if (!org) { - return c.json({ message: "Org not found", code: "not_found" }, 404); - } - - // ✅ GOOD - Validation/expected errors use RecaseError - if (!org) { - throw new RecaseError({ - message: "Org not found", - code: ErrCode.NotFound, - statusCode: 404, - }); - } - - // ✅ GOOD - Internal/unexpected errors use InternalError - if (!upstash) { - throw new InternalError({ - message: "Upstash not configured", - code: "upstash_not_configured", - }); - } - ``` - -## Bad example -/ root --> components -|-> hooks -## Good example -/ root --> components --> hooks - -- Functions (unless there's a very good reason) should always take in objects as arguments. Object params are named and easy to understand. - -- This codebase uses Bun for all of its operations in `/server`, `/vite` and `/shared`. It uses Bun for the package management, Bun for the workspace management and Bun for the runtime. Prefer Bun over PNPM. If you ever want to trace a package dependency tree, run `bun why ` which will tell you why a certain package was installed and by who. - -- Prefer Guard clauses "if(!admin) return;" over explicity "if(admin) do X;" Early returns are better - -- Do not run "npx tsc" - run "tsc" instead as it is installed globally. - -# Figma MCP guidance -- When you are using the Figma MCP server, you **must** follow our design system. Below is an example implementation of CVA with out design system - -## File Naming -DON'T name files one word (like index.ts, model.ts, etc.). Give proper indication in the filename to which resource it's targeting. For example, a utility file for organizations should be named orgUtils.ts. This is because it's easier to search for files like this. That being said, the filename shouldn't be overly long (less than three words is ideal) - -## File Moving/Renaming -When restructuring, moving, or renaming files, **ALWAYS use terminal commands** (`mv`, `mkdir`) instead of rewriting files. This preserves git history and ensures no lines/logic are accidentally lost or changed - -## Deleting Files -- **NEVER use `rm` commands unless the file is confirmed to be unused** -- **ALWAYS ask for user approval before running any `rm` or `rm -rf` commands** -- Before deleting, verify the file has no imports/references in the codebase - -# Vite -## Components -- Always use v2 components from `@/components/v2/` (buttons, inputs, dialogs, sheets, selects, etc.) for new features. Old components in `@/components/ui/` are deprecated. - -## Sheets -- Use `Sheet.tsx` for overlay sheets (modal-style with backdrop). Use `SheetHeader`, `SheetFooter`, `SheetSection` from `SharedSheetComponents.tsx` for consistent styling. -- `InlineSheet.tsx` provides `SheetContainer` for inline sheets (embedded in page layout). It re-exports shared components for backwards compatibility. -- Both sheet types support the same header/footer/section components, ensuring consistent UI patterns across overlay and inline implementations. - -## Styling -- DO NOT hardcode styles when possible. Always try to reuse existing Tailwind classes or component patterns from similar components in the codebase. -- When adding interactive elements (hover, focus, active states), look for existing patterns in similar components and reuse those class combinations. -- Consistency is key - if a pattern exists, use it rather than creating a new one. - -## Form Elements -- When creating form input elements (inputs, selects, textareas, etc.) in the vite folder, ALWAYS read `vite/FORM_DESIGN_GUIDELINES.md` first to understand the atomic CSS class system. diff --git a/README.md b/README.md index 20ec18009..f02b0c383 100644 --- a/README.md +++ b/README.md @@ -25,48 +25,22 @@ All this without having to handle webhooks, upgrades/downgrades, cancellations o **Self Hosted**: If you'd like to self-host Autumn: 1. Make sure you have `bun` installed -2. Install the project dependencies +2. Install the project dependencies: ```bash bun install ``` -3. Run our set up script: +3. Run Autumn: ```bash -bun setup -``` - -4. Generate the relevant tables in your postgres DB -```bash -bun db:generate && bun db:migrate -``` - -5. Run Autumn: - -For Windows -```bash -docker compose -f docker-compose.dev.yml up -``` - -For mac/linux: - ```bash -docker compose -f docker-compose.unix.yml up +bun dev ``` That's it! You should be able to see the Autumn dashboard on `http://localhost:3000`. +> ℹ️ Autumn depends on a bunch of services. If you'd like help with self-hosting or running a local instance, contact the team on [Discord](https://discord.gg/53emPtY9tA). + > ⚠️ To log in, enter an email at the sign in page, and an OTP should appear in your console / terminal. Normally, we use Resend to email an OTP or Google OAuth -- these can be set up by providing your credentials in `server/.env` -> ℹ️ Our set up script initializes the required env vars and (optionally) a Supabase instance. If you'd like to use your own Postgres instance, you can do so -- just paste the connection string in the `DATABASE_URL` env variable at `server/.env` - -## Troubleshooting - -If you encounter a `SyntaxError: Unexpected end of JSON input` error when running `bun setup` again after previously running it, you may need to clear your database tables first. This is a [known issue](https://github.com/drizzle-team/drizzle-orm/issues/4529) that can occur when running database migrations multiple times. - -To resolve this: - -1. Connect to your database -2. Drop all existing tables -3. Run the setup script again: - +> ℹ️ If you'd like to use your own Postgres instance, paste the connection string in the `DATABASE_URL` env variable at `server/.env` ## Why Autumn diff --git a/bun.lock b/bun.lock index 71e9741c6..a2530da4e 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@aws-sdk/client-sqs": "^3.985.0", "@better-auth/core": "catalog:", "@better-auth/oauth-provider": "catalog:", + "@trigger.dev/sdk": "4.4.6", "@wooorm/starry-night": "^3.8.0", "ag-charts-react": "^12.3.0", "better-auth": "catalog:", @@ -19,12 +20,14 @@ "devDependencies": { "@better-auth/cli": "^1.4.21", "@biomejs/biome": "^2.2.7", + "@trigger.dev/build": "4.4.6", "@types/node": "^24.9.1", "concurrently": "^9.2.1", "dotenv": "^16.6.1", "husky": "^9.1.7", "inquirer": "^12.10.0", "knip": "^6.7.0", + "trigger.dev": "4.4.6", "ts-to-zod": "^5.1.0", "turbo": "^2.9.6", }, @@ -361,6 +364,9 @@ "@react-email/components": "^0.0.42", "@sentry/bun": "catalog:", "@supabase/supabase-js": "^2.46.2", + "@tinybirdco/sdk": "^0.0.69", + "@trigger.dev/build": "4.4.6", + "@trigger.dev/sdk": "4.4.6", "@types/qs": "^6.14.0", "@types/semver": "^7.7.1", "@typescript/native-preview": "^7.0.0-dev.20251114.1", @@ -499,6 +505,7 @@ "@tanstack/react-query": "catalog:", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.18", + "@trigger.dev/react-hooks": "4.4.5", "ag-charts-community": "^12.0.2", "ag-charts-react": "^12.0.2", "ag-grid-community": "^34.0.2", @@ -515,6 +522,7 @@ "date-fns": "^3.6.0", "decimal.js": "^10.5.0", "input-otp": "^1.4.2", + "json5": "^2.2.3", "lodash": "^4.17.21", "lucide-react": "^0.562.0", "motion": "^12.26.1", @@ -602,15 +610,15 @@ "vite-tsconfig-paths": "6.1.1", }, "packages": { - "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.77", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ML8C2M1YvPA1ulEx4TiyF0k1xvC2ikEiPBIC1PPQ0a5xELUGrO2lAaEzsTEoJ+eCeDd8PSBuFJjs+r+9yIwQXA=="], + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.76", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kOuvT9e6PygFvgYpkr4v9gjvmcMPfJp79jaXjeRl9Gpoj2OXdtc3ero7o1ic+tiSBw5IMubxXFO68BCA/axGJA=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.114", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MqkZ5sd+qiq6RgIxELkoFQXg2/JwK+WCMaot7U+rtrZpWJl3fSyYvc28SC03b256o4F7OXjQtdjTqs81B2w+dA=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.112", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jiBao9pR4owWyjo0BnuNc7WSQBGOD0thysE4AFgZXaG+zMFbISQXUkJr7ePw/phBvePy7jE5FSA2Lf7lwqUiiQ=="], "@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], - "@ai-sdk/react": ["@ai-sdk/react@3.0.182", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.27", "ai": "6.0.180", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-cnfF5N6XRsQ29VsSSoFqFpYQJx1y4sSCL3lGMIwgE0034aqn8i3+joRe52O9xV+EaJ7fziI7xn6ql7qjrf+Blw=="], + "@ai-sdk/react": ["@ai-sdk/react@3.0.179", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.27", "ai": "6.0.177", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-wq3gqDrwi6QvwHQuVrTpjeUQI/LHZ5ysokWTIS1hOFw0UIIX8eVpri5iVvqQuq5CeQw/6bYXSI15SfMtZJixpQ=="], "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.1.3", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw=="], @@ -630,6 +638,8 @@ "@ark/util": ["@ark/util@0.55.0", "", {}, "sha512-aWFNK7aqSvqFtVsl1xmbTjGbg91uqtJV7Za76YGNEwIO4qLjMfyY8flmmbhooYMuqPCO2jyxu8hve943D+w3bA=="], + "@arr/every": ["@arr/every@1.0.1", "", {}, "sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg=="], + "@artilleryio/int-commons": ["@artilleryio/int-commons@2.22.0", "", { "dependencies": { "async": "^2.6.4", "cheerio": "^1.2.0", "debug": "^4.4.3", "deep-for-each": "^3.0.0", "espree": "^10.3.0", "jsonpath-plus": "^10.3.0", "lodash": "^4.18.0", "ms": "^2.1.3" } }, "sha512-zbKXeAQuyVAAhmJJ9vjktNFo+ooS4mWci90btT0ycM69m4rGb6+ncEdaqwVbYOY6IeG/A/Br9wGZialmKkk/yg=="], "@artilleryio/int-core": ["@artilleryio/int-core@2.26.0", "", { "dependencies": { "@artilleryio/int-commons": "2.22.0", "@artilleryio/sketches-js": "^2.1.1", "agentkeepalive": "^4.6.0", "arrivals": "^2.1.2", "async": "^2.6.4", "chalk": "^2.4.2", "cheerio": "^1.2.0", "cookie-parser": "^1.4.7", "csv-parse": "^4.16.3", "debug": "^4.4.3", "decompress-response": "^6.0.0", "deep-for-each": "^3.0.0", "driftless": "^2.0.3", "esprima": "^4.0.1", "eventemitter3": "^5.0.4", "fast-deep-equal": "^3.1.3", "filtrex": "^0.5.4", "form-data": "^4.0.5", "got": "^14.6.6", "hpagent": "^0.1.1", "https-proxy-agent": "^5.0.0", "lodash": "^4.18.0", "ms": "^2.1.3", "protobufjs": "^7.5.5", "socket.io-client": "^4.8.3", "socketio-wildcard": "^2.0.0", "tough-cookie": "^5.1.2", "uuid": "^11.1.0", "ws": "^7.5.7" } }, "sha512-+dfsNTK11/6MbZhbu8whkiWXp7JkHa2JWJ0VEJzSEqfnvss0jyZrKlVYksiVstn+vcFsdViiM/fgnGJ+oZ877g=="], @@ -810,11 +820,11 @@ "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], - "@azure/msal-browser": ["@azure/msal-browser@5.10.1", "", { "dependencies": { "@azure/msal-common": "16.6.1" } }, "sha512-hTbvOi9Ko2Jvn+G/fSmjzHf9WbNcf/o3epMtbeGx/pMwMrVAbi6OgCJVeCfsAb8IybSRpaCSc4EDRlYAhgngUQ=="], + "@azure/msal-browser": ["@azure/msal-browser@5.10.0", "", { "dependencies": { "@azure/msal-common": "16.6.0" } }, "sha512-2Y4TlG5mCfxviHutfW50i8Xd8xhGKTgieL02vMYOE5ZbZrVM+drKSGD//tweRAmlmqqp+F9vrKoHWri/buzxWQ=="], - "@azure/msal-common": ["@azure/msal-common@16.6.1", "", {}, "sha512-VxKdEtUwDuLD0F1hOQP7kye0YadZxFJfv37Em440geEf/w9uggKnHpRrqwZJOdxmPUOdhZ9kyRtKuAJW8wUcRg=="], + "@azure/msal-common": ["@azure/msal-common@16.6.0", "", {}, "sha512-FemGljX0csPlBMUE5GUan7BfRn1emeMRUhHSARhqzLN6LA9nt+MgzmAQ1xVqdLm+6plVoxsq9mS5eoyKtpPSgA=="], - "@azure/msal-node": ["@azure/msal-node@5.2.1", "", { "dependencies": { "@azure/msal-common": "16.6.1", "jsonwebtoken": "^9.0.0" } }, "sha512-tmQiQ2HvtzaeLqYGy3BemiPOSGPY4wCy1IW5zDWITKSs/s35WEd7Zij/hCxvUdAOzj6U3qnyaGbYXY91ortFEQ=="], + "@azure/msal-node": ["@azure/msal-node@5.2.0", "", { "dependencies": { "@azure/msal-common": "16.6.0", "jsonwebtoken": "^9.0.0" } }, "sha512-b/ak8XAqpnGk1N1nsyTVV0Remp48BP3QrGQZ1uCMcvg2S8X1eSXzhHQZEae2oX276Q4KFAqCUswanDtcvIKLrw=="], "@azure/storage-blob": ["@azure/storage-blob@12.31.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.3", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/core-xml": "^1.4.5", "@azure/logger": "^1.1.4", "@azure/storage-common": "^12.3.0", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg=="], @@ -944,6 +954,8 @@ "@bufbuild/protobuf": ["@bufbuild/protobuf@2.12.0", "", {}, "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA=="], + "@bugsnag/cuid": ["@bugsnag/cuid@3.2.2", "", {}, "sha512-7onuYLTMqMmHE9BBPG0YER4nFsU1rB+me1/YIeMusqcLbVbKKuG9u9+BDVDpje5e0llkkrVNOKYwmzM9DRIo7A=="], + "@canvas/image-data": ["@canvas/image-data@1.1.0", "", {}, "sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA=="], "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], @@ -952,6 +964,8 @@ "@chevrotain/gast": ["@chevrotain/gast@10.5.0", "", { "dependencies": { "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A=="], + "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@12.0.0", "", {}, "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA=="], + "@chevrotain/types": ["@chevrotain/types@10.5.0", "", {}, "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A=="], "@chevrotain/utils": ["@chevrotain/utils@10.5.0", "", {}, "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ=="], @@ -978,7 +992,25 @@ "@dependents/detective-less": ["@dependents/detective-less@5.0.3", "", { "dependencies": { "gonzales-pe": "^4.3.0", "node-source-walk": "^7.0.1" } }, "sha512-v6oD9Ukp+N7V4n6p5I/+mM5fIohSfkrDSGlFm5w/pYmchvbk+sMIHsLxrFJ5Lnujewj1BzWL0K84d88lwZAMQA=="], - "@discoveryjs/json-ext": ["@discoveryjs/json-ext@1.1.0", "", {}, "sha512-Xc3VhU02wqZ1HvHRJUwL09HkZSTvidqY5Ya0NXBSYOxAp+Ln9dcJr9fySI+CkONzP3PekQo9WdzCv0PGER/mOA=="], + "@depot/cli": ["@depot/cli@0.0.1-cli.2.80.0", "", { "optionalDependencies": { "@depot/cli-darwin-arm64": "0.0.1-cli.2.80.0", "@depot/cli-darwin-x64": "0.0.1-cli.2.80.0", "@depot/cli-linux-arm": "0.0.1-cli.2.80.0", "@depot/cli-linux-arm64": "0.0.1-cli.2.80.0", "@depot/cli-linux-ia32": "0.0.1-cli.2.80.0", "@depot/cli-linux-x64": "0.0.1-cli.2.80.0", "@depot/cli-win32-arm64": "0.0.1-cli.2.80.0", "@depot/cli-win32-ia32": "0.0.1-cli.2.80.0", "@depot/cli-win32-x64": "0.0.1-cli.2.80.0" }, "bin": { "depot": "bin/depot" } }, "sha512-KvmOiQdpbamFziqnzzgqBm6RjfGhLJimBBYEOVriTxCPtVJuBIFm34xZllM36OQzPZIWpWBP+2/UnOyRG5smUg=="], + + "@depot/cli-darwin-arm64": ["@depot/cli-darwin-arm64@0.0.1-cli.2.80.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-H7tQ0zWXVmdYXGFvt3d/v5fmquMlMM1I9JC8C2yiBZ9En9a20hzSbKoiym92RtcfqjKQFvhXL0DT6vQmJ8bgQA=="], + + "@depot/cli-darwin-x64": ["@depot/cli-darwin-x64@0.0.1-cli.2.80.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-3RDyybnCC2YSvbJCkHxXBYbKR7adUO7U00O9DHreX8EyS2E/C3kQgET+rFXR+iDdeG0h+h+At2rDng7AxvUhDw=="], + + "@depot/cli-linux-arm": ["@depot/cli-linux-arm@0.0.1-cli.2.80.0", "", { "os": "linux", "cpu": "arm" }, "sha512-95kjKwKxP6RKkAhJoPlO6g3drEYpFWy9AC7QfvQMHDRnI3HXgbxA/BSTp4R3C4TTfp5JUPShVf8Js/NAcrUpUQ=="], + + "@depot/cli-linux-arm64": ["@depot/cli-linux-arm64@0.0.1-cli.2.80.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Fc1+Guqdsl2WM+b+76FirRjjRvEoYZX0MI4uuIQF9c9gwp6UZSZ5o3U7lAUpdkclVMKXSAW5bsIpgcOfUiJX3w=="], + + "@depot/cli-linux-ia32": ["@depot/cli-linux-ia32@0.0.1-cli.2.80.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-2KoyIoYqnyIuynPN+mWbugoOB2APiNNvFLKERWOwPa0KYLSfcENT3sYxDW8tDuIw2dftHlmZN73uvXrGFeMcDQ=="], + + "@depot/cli-linux-x64": ["@depot/cli-linux-x64@0.0.1-cli.2.80.0", "", { "os": "linux", "cpu": "x64" }, "sha512-B8xDlXFxhYHD2tUk0jUv7SKipLfWwJVlY7ZhDQixyQzatibXakahdqpvZJgE6FLZJd3lgFIEAVsKURZHa1l57A=="], + + "@depot/cli-win32-arm64": ["@depot/cli-win32-arm64@0.0.1-cli.2.80.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-CPJX691MKEhnKZX25xS+opWESgPQ73HKkEwkAvvVqdBzahndcHoqAeIxYLv2hoCqrDlkH3YCF+DJleLiEP3blA=="], + + "@depot/cli-win32-ia32": ["@depot/cli-win32-ia32@0.0.1-cli.2.80.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-6eewZ1zPEyNL3zcCwC01s3rZNMqMGVu1iv/EPywj3/uk4CNyUBaTlZ3StQ1BLFHPQnU0vk29yOzA5ZEuRLSyxA=="], + + "@depot/cli-win32-x64": ["@depot/cli-win32-x64@0.0.1-cli.2.80.0", "", { "os": "win32", "cpu": "x64" }, "sha512-9CRcc7D0/x4UrBkDuc35WVPMQG5gKMD1JckGLEl6VREE0Ppdny6n+hunQ8prwVc8aqzKG134XCC2U4DUjYg18A=="], "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.65.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.4", "which": "^4.0.0", "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-v4FA/Lw3pTEloLxBqTOaYDX6MNo0Jo7lGBsPZhwnJBqRJp0AzQg1ZZNxrFsh6HVC6QWeWrfIKLn0y2eyIXaVDg=="], @@ -986,6 +1018,8 @@ "@ecies/ciphers": ["@ecies/ciphers@0.2.6", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g=="], + "@electric-sql/client": ["@electric-sql/client@1.0.14", "", { "dependencies": { "@microsoft/fetch-event-source": "^2.0.1" }, "optionalDependencies": { "@rollup/rollup-darwin-arm64": "^4.18.1" } }, "sha512-LtPAfeMxXRiYS0hyDQ5hue2PjljUiK9stvzsVyVb4nwxWQxfOWTSF42bHTs/o5i3x1T4kAQ7mwHpxa4A+f8X7Q=="], + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -1080,6 +1114,8 @@ "@fortawesome/react-fontawesome": ["@fortawesome/react-fontawesome@0.2.6", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7", "react": "^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg=="], + "@google-cloud/precise-date": ["@google-cloud/precise-date@4.0.0", "", {}, "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA=="], + "@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="], "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], @@ -1210,6 +1246,8 @@ "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -1230,17 +1268,19 @@ "@jsep-plugin/ternary": ["@jsep-plugin/ternary@1.1.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg=="], + "@jsonhero/path": ["@jsonhero/path@1.0.21", "", {}, "sha512-gVUDj/92acpVoJwsVJ/RuWOaHyG4oFzn898WNGQItLCTQ+hOaVlEaImhwE1WqOTf+l3dGOUkbSiVKlb3q1hd1Q=="], + "@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="], "@kubiks/otel-drizzle": ["@kubiks/otel-drizzle@2.1.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "drizzle-orm": ">=0.28.0" } }, "sha512-9UHb0od3jwa6zTWMyEYPIZcUq5PDaziCmQLMLakSK2zeqy12SFZ3SAGWXJTgEr8valn/Wa+DKVs+Z3aqKQUpvg=="], - "@langchain/core": ["@langchain/core@1.1.46", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "@standard-schema/spec": "^1.1.0", "js-tiktoken": "^1.0.12", "langsmith": ">=0.5.0 <1.0.0", "mustache": "^4.2.0", "p-queue": "^6.6.2", "zod": "^3.25.76 || ^4" } }, "sha512-i8rDC83BpItxChCw4Lf+6tAr+k+OUcbirc5ZkrhI9ywYWmvxegUljLGOGYvtJNTbEAIFkhYIODPE5QRqyjF6sA=="], + "@langchain/core": ["@langchain/core@1.1.45", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "@standard-schema/spec": "^1.1.0", "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", "langsmith": ">=0.5.0 <1.0.0", "mustache": "^4.2.0", "p-queue": "^6.6.2", "zod": "^3.25.76 || ^4" } }, "sha512-Y/wvuglLTMKJahkl4QD9dBIdF/z/CxZJWdTfHJF/q2jtlJtoFf6Mb5JpGxZfsi3mBY6NSG941FSLTcqhCKrhBA=="], "@langchain/langgraph": ["@langchain/langgraph@1.3.0", "", { "dependencies": { "@langchain/langgraph-checkpoint": "^1.0.2", "@langchain/langgraph-sdk": "~1.9.0", "@langchain/protocol": "^0.0.15", "@standard-schema/spec": "1.1.0", "uuid": "^10.0.0" }, "peerDependencies": { "@langchain/core": "^1.1.44", "zod": "^3.25.32 || ^4.2.0", "zod-to-json-schema": "^3.x" }, "optionalPeers": ["zod-to-json-schema"] }, "sha512-QvhTjiyqFPz81A+y6LHs223w6DTjv5+882DT4mup72bd72rRhNjTYo5fhes5um0swnKArvY/arc7KeFInfHHWw=="], "@langchain/langgraph-checkpoint": ["@langchain/langgraph-checkpoint@1.0.2", "", { "dependencies": { "uuid": "^10.0.0" }, "peerDependencies": { "@langchain/core": "^1.1.44" } }, "sha512-F4E5Tr0nt8FGghgdscJtHw+ABzChOHeI80R7Y1pjIHdiJom6c2ieo76vL+FWiny80JmoGqhrVAEIWrw0cXKPxg=="], - "@langchain/langgraph-sdk": ["@langchain/langgraph-sdk@1.9.2", "", { "dependencies": { "@langchain/core": "^1.1.44", "@langchain/protocol": "^0.0.15", "@types/json-schema": "^7.0.15", "p-queue": "^9.0.1", "p-retry": "^7.1.1", "uuid": "^13.0.0" }, "peerDependencies": { "react": "^18 || ^19", "react-dom": "^18 || ^19", "svelte": "^4.0.0 || ^5.0.0", "vue": "^3.0.0" }, "optionalPeers": ["react", "react-dom", "svelte", "vue"] }, "sha512-1kDPjR0VH/39q2h8k0Sxi35KxOvEQPModVCepxGLlRkbZmuWUH+zfICuJd3rmD1ByeOKQBZEaB7Y+VCYmSMt1w=="], + "@langchain/langgraph-sdk": ["@langchain/langgraph-sdk@1.9.1", "", { "dependencies": { "@langchain/core": "^1.1.44", "@langchain/protocol": "^0.0.15", "@types/json-schema": "^7.0.15", "p-queue": "^9.0.1", "p-retry": "^7.1.1", "uuid": "^13.0.0" }, "peerDependencies": { "react": "^18 || ^19", "react-dom": "^18 || ^19", "svelte": "^4.0.0 || ^5.0.0", "vue": "^3.0.0" }, "optionalPeers": ["react", "react-dom", "svelte", "vue"] }, "sha512-pHojybde9HoMz7ZDtyW3pgDdomAN4C0pbdgXASjccFS+S2Cqx75iZBtBUUg1A/CSer5xh9GakdIqyihxZOf7VA=="], "@langchain/protocol": ["@langchain/protocol@0.0.15", "", {}, "sha512-MllvbpMjqHevUm+v94M422mH7XKN+wGCvJRBVROTWBotEDOATYB4Ktk2UheYP859y9o2LlhtPek5t1T9eyfAbQ=="], @@ -1252,27 +1292,29 @@ "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], - "@mermaid-js/parser": ["@mermaid-js/parser@1.1.1", "", { "dependencies": { "@chevrotain/types": "~11.1.1" } }, "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw=="], + "@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="], - "@mintlify/cli": ["@mintlify/cli@4.0.1163", "", { "dependencies": { "@inquirer/prompts": "7.9.0", "@mintlify/common": "1.0.892", "@mintlify/link-rot": "3.0.1071", "@mintlify/models": "0.0.307", "@mintlify/prebuild": "1.0.1036", "@mintlify/previewing": "4.0.1097", "@mintlify/validation": "0.1.698", "adm-zip": "0.5.16", "chalk": "5.2.0", "color": "4.2.3", "detect-port": "1.5.1", "front-matter": "4.0.2", "fs-extra": "11.2.0", "ink": "6.3.0", "inquirer": "12.3.0", "js-yaml": "4.1.0", "mdast-util-mdx-jsx": "3.2.0", "open": "^8.4.2", "openid-client": "^6.8.2", "posthog-node": "5.17.2", "react": "19.2.3", "semver": "7.7.2", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "^4.3.6" }, "optionalDependencies": { "keytar": "^7.9.0" }, "bin": { "mint": "bin/index.js", "mintlify": "bin/index.js" } }, "sha512-7sGs0RD5ctviP6j0H8D9GvQUF6k5Eqflag5cHQCQl0yvC/SlS0K1Qjr+jqs5QRADSCwjBMY41ZNMyBJlE9awbQ=="], + "@microsoft/fetch-event-source": ["@microsoft/fetch-event-source@2.0.1", "", {}, "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="], - "@mintlify/common": ["@mintlify/common@1.0.892", "", { "dependencies": { "@asyncapi/parser": "3.4.0", "@asyncapi/specs": "6.8.1", "@mintlify/mdx": "^3.0.4", "@mintlify/models": "0.0.307", "@mintlify/openapi-parser": "^0.0.8", "@mintlify/validation": "0.1.698", "@sindresorhus/slugify": "2.2.0", "@types/mdast": "4.0.4", "acorn": "8.11.2", "acorn-jsx": "5.3.2", "color-blend": "4.0.0", "estree-util-to-js": "2.0.0", "estree-walker": "3.0.3", "front-matter": "4.0.2", "hast-util-from-html": "2.0.3", "hast-util-to-html": "9.0.4", "hast-util-to-text": "4.0.2", "hex-rgb": "5.0.0", "ignore": "7.0.5", "js-yaml": "4.1.0", "lodash": "4.18.1", "mdast-util-from-markdown": "2.0.2", "mdast-util-gfm": "3.0.0", "mdast-util-mdx": "3.0.0", "mdast-util-mdx-jsx": "3.1.3", "micromark-extension-gfm": "3.0.0", "micromark-extension-mdx-jsx": "3.0.1", "micromark-extension-mdxjs": "3.0.0", "openapi-types": "12.1.3", "postcss": "8.5.6", "rehype-stringify": "10.0.1", "remark": "15.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.0", "remark-math": "6.0.0", "remark-mdx": "3.1.0", "remark-parse": "11.0.0", "remark-rehype": "11.1.1", "remark-stringify": "11.0.0", "sucrase": "^3.34.0", "tailwindcss": "^3.4.17", "unified": "11.0.5", "unist-builder": "4.0.0", "unist-util-map": "4.0.0", "unist-util-remove": "4.0.0", "unist-util-remove-position": "5.0.0", "unist-util-visit": "5.0.0", "unist-util-visit-parents": "6.0.1", "vfile": "6.0.3", "xss": "1.0.15" } }, "sha512-ijCT8lTXWaPnQq1w/sAU3uAgKp/ur3FwQIggIixR119sT4kHDKyQk5f5dNshaDDPEWcYCU/ILevN8KokMPGtvA=="], + "@mintlify/cli": ["@mintlify/cli@4.0.1161", "", { "dependencies": { "@inquirer/prompts": "7.9.0", "@mintlify/common": "1.0.890", "@mintlify/link-rot": "3.0.1069", "@mintlify/models": "0.0.306", "@mintlify/prebuild": "1.0.1034", "@mintlify/previewing": "4.0.1095", "@mintlify/validation": "0.1.696", "adm-zip": "0.5.16", "chalk": "5.2.0", "color": "4.2.3", "detect-port": "1.5.1", "front-matter": "4.0.2", "fs-extra": "11.2.0", "ink": "6.3.0", "inquirer": "12.3.0", "js-yaml": "4.1.0", "mdast-util-mdx-jsx": "3.2.0", "open": "^8.4.2", "openid-client": "^6.8.2", "posthog-node": "5.17.2", "react": "19.2.3", "semver": "7.7.2", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "^4.3.6" }, "optionalDependencies": { "keytar": "^7.9.0" }, "bin": { "mint": "bin/index.js", "mintlify": "bin/index.js" } }, "sha512-vUubQjIjeAb2eS6pztwSlmMQvGSIoJiaXb78/LCApiFnemDo0etwGZv5pBdMV2Z8E2MHx+zC4gbIjDK8NHER3w=="], - "@mintlify/link-rot": ["@mintlify/link-rot@3.0.1071", "", { "dependencies": { "@mintlify/common": "1.0.892", "@mintlify/models": "0.0.307", "@mintlify/prebuild": "1.0.1036", "@mintlify/previewing": "4.0.1097", "@mintlify/scraping": "4.0.522", "@mintlify/validation": "0.1.698", "fs-extra": "11.1.0", "unist-util-visit": "4.1.2" } }, "sha512-baHpjzKIbrn/T5e2TIA4dTyXREXq/n+JDsb9Sq9eqvJIbXow4IvRliUaAVHu4N6fq/x58/0Rbom1QWwyIbvLeg=="], + "@mintlify/common": ["@mintlify/common@1.0.890", "", { "dependencies": { "@asyncapi/parser": "3.4.0", "@asyncapi/specs": "6.8.1", "@mintlify/mdx": "^3.0.4", "@mintlify/models": "0.0.306", "@mintlify/openapi-parser": "^0.0.8", "@mintlify/validation": "0.1.696", "@sindresorhus/slugify": "2.2.0", "@types/mdast": "4.0.4", "acorn": "8.11.2", "acorn-jsx": "5.3.2", "color-blend": "4.0.0", "estree-util-to-js": "2.0.0", "estree-walker": "3.0.3", "front-matter": "4.0.2", "hast-util-from-html": "2.0.3", "hast-util-to-html": "9.0.4", "hast-util-to-text": "4.0.2", "hex-rgb": "5.0.0", "ignore": "7.0.5", "js-yaml": "4.1.0", "lodash": "4.18.1", "mdast-util-from-markdown": "2.0.2", "mdast-util-gfm": "3.0.0", "mdast-util-mdx": "3.0.0", "mdast-util-mdx-jsx": "3.1.3", "micromark-extension-gfm": "3.0.0", "micromark-extension-mdx-jsx": "3.0.1", "micromark-extension-mdxjs": "3.0.0", "openapi-types": "12.1.3", "postcss": "8.5.6", "rehype-stringify": "10.0.1", "remark": "15.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.0", "remark-math": "6.0.0", "remark-mdx": "3.1.0", "remark-parse": "11.0.0", "remark-rehype": "11.1.1", "remark-stringify": "11.0.0", "sucrase": "^3.34.0", "tailwindcss": "^3.4.17", "unified": "11.0.5", "unist-builder": "4.0.0", "unist-util-map": "4.0.0", "unist-util-remove": "4.0.0", "unist-util-remove-position": "5.0.0", "unist-util-visit": "5.0.0", "unist-util-visit-parents": "6.0.1", "vfile": "6.0.3", "xss": "1.0.15" } }, "sha512-MjyN5fh1ujeAemLbJGd1jf6U9cXto6zNqyjrDoFIvDoBEluOFExOYGwwrWU1SClDGC6cISsZcX1lxsdoqtygnQ=="], + + "@mintlify/link-rot": ["@mintlify/link-rot@3.0.1069", "", { "dependencies": { "@mintlify/common": "1.0.890", "@mintlify/models": "0.0.306", "@mintlify/prebuild": "1.0.1034", "@mintlify/previewing": "4.0.1095", "@mintlify/scraping": "4.0.522", "@mintlify/validation": "0.1.696", "fs-extra": "11.1.0", "unist-util-visit": "4.1.2" } }, "sha512-55euuVtWYOTGZ+YvlYAnhTLxjSjOAWHD4swqp27GyS/JBkBsa8Hu6iqdTBfRT2WiKeDqY2xYTswS5uCUkWKsaA=="], "@mintlify/mdx": ["@mintlify/mdx@3.0.4", "", { "dependencies": { "@shikijs/transformers": "^3.11.0", "@shikijs/twoslash": "^3.12.2", "arktype": "^2.1.26", "hast-util-to-string": "^3.0.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.1.0", "mdast-util-mdx-jsx": "^3.2.0", "mdast-util-to-hast": "^13.2.0", "next-mdx-remote-client": "^1.0.3", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-smartypants": "^3.0.2", "shiki": "^3.11.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@radix-ui/react-popover": "^1.1.15", "react": "^18.3.1", "react-dom": "^18.3.1" } }, "sha512-tJhdpnM5ReJLNJ2fuDRIEr0zgVd6id7/oAIfs26V46QlygiLsc8qx4Rz3LWIX51rUXW/cfakjj0EATxIciIw+g=="], - "@mintlify/models": ["@mintlify/models@0.0.307", "", { "dependencies": { "axios": "1.15.0", "openapi-types": "12.1.3" } }, "sha512-4lJrNXst3J1PcDhiFXHwYl+RnwhQyQt4l1sM2u6JotjoUR7Rx5NN5E8QukJmim1jLd7/v4qMzeD7s7F5TbV7zw=="], + "@mintlify/models": ["@mintlify/models@0.0.306", "", { "dependencies": { "axios": "1.15.0", "openapi-types": "12.1.3" } }, "sha512-3EUvTXg8OJ8mZzXRDga0bSGJwJWto2Wc7qIfoCzNH+MTOh531OWKPk/OAOcMaftJcDggETbCinfDb0GXiqLL8Q=="], "@mintlify/openapi-parser": ["@mintlify/openapi-parser@0.0.8", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-9MBRq9lS4l4HITYCrqCL7T61MOb20q9IdU7HWhqYMNMM1jGO1nHjXasFy61yZ8V6gMZyyKQARGVoZ0ZrYN48Og=="], - "@mintlify/prebuild": ["@mintlify/prebuild@1.0.1036", "", { "dependencies": { "@mintlify/common": "1.0.892", "@mintlify/openapi-parser": "^0.0.8", "@mintlify/scraping": "4.0.756", "@mintlify/validation": "0.1.698", "chalk": "5.3.0", "favicons": "7.2.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "js-yaml": "4.1.0", "openapi-types": "12.1.3", "sharp": "0.33.5", "sharp-ico": "0.1.5", "unist-util-visit": "4.1.2", "uuid": "11.1.0" } }, "sha512-kuAEEv20Q3xJq18Jvwn0zsOJoEFk23wnJuXGdF+yV+mafQdhYqMrUaaXfq0zXc0a3xFldXnsOcS7EfCNLxBr5Q=="], + "@mintlify/prebuild": ["@mintlify/prebuild@1.0.1034", "", { "dependencies": { "@mintlify/common": "1.0.890", "@mintlify/openapi-parser": "^0.0.8", "@mintlify/scraping": "4.0.754", "@mintlify/validation": "0.1.696", "chalk": "5.3.0", "favicons": "7.2.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "js-yaml": "4.1.0", "openapi-types": "12.1.3", "sharp": "0.33.5", "sharp-ico": "0.1.5", "unist-util-visit": "4.1.2", "uuid": "11.1.0" } }, "sha512-zUP77WYXE+ILMgPVF+aX61Gg6p6N3ZdbuBvzcjb/LWIs/rHfiAmAymil1PYm5Bj0bezdoyw8W/JDH4Gyl99/gg=="], - "@mintlify/previewing": ["@mintlify/previewing@4.0.1097", "", { "dependencies": { "@mintlify/common": "1.0.892", "@mintlify/prebuild": "1.0.1036", "@mintlify/validation": "0.1.698", "adm-zip": "0.5.16", "better-opn": "3.0.2", "chalk": "5.2.0", "chokidar": "3.5.3", "express": "4.22.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "got": "13.0.0", "ink": "6.3.0", "ink-spinner": "5.0.0", "is-online": "10.0.0", "js-yaml": "4.1.0", "openapi-types": "12.1.3", "react": "19.2.3", "socket.io": "4.8.0", "tar": "6.1.15", "unist-util-visit": "4.1.2", "yargs": "17.7.1" } }, "sha512-RQiJOdCU2nz5hGBVQzxxSDZOK75veRy/y6pcFDfGOdIGaGuzb0lTpfe0RoD9942s1T2ACJwbkknXdjl0ytWJYA=="], + "@mintlify/previewing": ["@mintlify/previewing@4.0.1095", "", { "dependencies": { "@mintlify/common": "1.0.890", "@mintlify/prebuild": "1.0.1034", "@mintlify/validation": "0.1.696", "adm-zip": "0.5.16", "better-opn": "3.0.2", "chalk": "5.2.0", "chokidar": "3.5.3", "express": "4.22.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "got": "13.0.0", "ink": "6.3.0", "ink-spinner": "5.0.0", "is-online": "10.0.0", "js-yaml": "4.1.0", "openapi-types": "12.1.3", "react": "19.2.3", "socket.io": "4.8.0", "tar": "6.1.15", "unist-util-visit": "4.1.2", "yargs": "17.7.1" } }, "sha512-6uqxswGNffUnx/DUfkKtbZ0B3h6h1EEv8VWRQXl1TczkkCRR65m9/7kyCAPIEw5/4WoiNOO4ElSJajpg7+VuaA=="], "@mintlify/scraping": ["@mintlify/scraping@4.0.522", "", { "dependencies": { "@mintlify/common": "1.0.661", "@mintlify/openapi-parser": "^0.0.8", "fs-extra": "11.1.1", "hast-util-to-mdast": "10.1.0", "js-yaml": "4.1.0", "mdast-util-mdx-jsx": "3.1.3", "neotraverse": "0.6.18", "puppeteer": "22.14.0", "rehype-parse": "9.0.1", "remark-gfm": "4.0.0", "remark-mdx": "3.0.1", "remark-parse": "11.0.0", "remark-stringify": "11.0.0", "unified": "11.0.5", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "3.21.4" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-PL2k52WT5S5OAgnT2K13bP7J2El6XwiVvQlrLvxDYw5KMMV+y34YVJI8ZscKb4trjitWDgyK0UTq2KN6NQgn6g=="], - "@mintlify/validation": ["@mintlify/validation@0.1.698", "", { "dependencies": { "@mintlify/mdx": "^3.0.4", "@mintlify/models": "0.0.307", "arktype": "2.1.27", "js-yaml": "4.1.0", "lcm": "0.0.3", "lodash": "4.18.1", "neotraverse": "0.6.18", "object-hash": "3.0.0", "openapi-types": "12.1.3", "uuid": "11.1.0", "zod": "3.24.0", "zod-to-json-schema": "3.20.4" } }, "sha512-iG4Wiwg4vavJMwp1TpTBugTCdzDWOLLfgpICEJHdxr5e59+ulPiUoQfHLocXoW1yO3SfI7mM71kt7PCTjp+XYw=="], + "@mintlify/validation": ["@mintlify/validation@0.1.696", "", { "dependencies": { "@mintlify/mdx": "^3.0.4", "@mintlify/models": "0.0.306", "arktype": "2.1.27", "js-yaml": "4.1.0", "lcm": "0.0.3", "lodash": "4.18.1", "neotraverse": "0.6.18", "object-hash": "3.0.0", "openapi-types": "12.1.3", "uuid": "11.1.0", "zod": "3.24.0", "zod-to-json-schema": "3.20.4" } }, "sha512-czZCxETAFP3aQID+TsQTurQM7WNegr2frBo0+pUDgb3s0VZb7vjf+3ppNuprWsepbH5UpVS3w2EOkxb1w47bfQ=="], "@mishieck/ink-titled-box": ["@mishieck/ink-titled-box@0.3.0", "", { "peerDependencies": { "ink": "^6.0.0", "react": "^19.1.0", "typescript": "^5" } }, "sha512-ugzVH9hixp3hwKfQ8On/qnsrdAxS3y9rTu/aGOFed4zVUvtZyGZNIR4rxAwXult8HKI4vJEh0OM8wib9NPrwUg=="], @@ -1296,7 +1338,7 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], - "@mswjs/interceptors": ["@mswjs/interceptors@0.41.9", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w=="], + "@mswjs/interceptors": ["@mswjs/interceptors@0.41.8", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], @@ -1358,9 +1400,9 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], - "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.202.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fTBjMqKCfotFWfLzaKyhjLvyEyq5vDKTTFfBmx21btv3gvy8Lq6N5Dh2OzqeuN4DjtpSvNT1uNVfg08eD2Rfxw=="], + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.203.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ=="], - "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.7.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ=="], + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw=="], "@opentelemetry/core": ["@opentelemetry/core@2.0.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw=="], @@ -1386,7 +1428,9 @@ "@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-a9eeyHIipfdxzCfc2XPrE+/TI3wmrZUDFtG2RRXHSbZZULAny7SyybSvaDvS77a7iib5MPiAvluwVvbGTsHxsw=="], - "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.202.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.202.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Uz3BxZWPgDwgHM2+vCKEQRh0R8WKrd/q6Tus1vThRClhlPO39Dyz7mDrOr6KuqGXAlBQ1e5Tnymzri4RMZNaWA=="], + "@opentelemetry/host-metrics": ["@opentelemetry/host-metrics@0.37.0", "", { "dependencies": { "systeminformation": "5.23.8" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gf6nRFci0PTni9R1QQKjZ2uZE4Y6olLKhlwdM0qqLbbn3SBVKyP2jyBMiosBTHtRNLjY7s8hzQ44eLdK5wkGNQ=="], + + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.203.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.203.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ=="], "@opentelemetry/instrumentation-amqplib": ["@opentelemetry/instrumentation-amqplib@0.58.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fjpQtH18J6GxzUZ+cwNhWUpb71u+DzT7rFkg5pLssDGaEber91Y2WNGdpVpwGivfEluMlNMZumzjEqfg8DeKXQ=="], @@ -1396,6 +1440,8 @@ "@opentelemetry/instrumentation-express": ["@opentelemetry/instrumentation-express@0.59.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pMKV/qnHiW/Q6pmbKkxt0eIhuNEtvJ7sUAyee192HErlr+a1Jx+FZ3WjfmzhQL1geewyGEiPGkmjjAgNY8TgDA=="], + "@opentelemetry/instrumentation-fetch": ["@opentelemetry/instrumentation-fetch@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/instrumentation": "0.203.0", "@opentelemetry/sdk-trace-web": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Z+mls3rOP2BaVykDZLLZPvchjj9l2oj3dYG1GTnrc27Y8o3biE+5M1b0izblycbbQHXjMPHQCpmjHbLMQuWtBg=="], + "@opentelemetry/instrumentation-fs": ["@opentelemetry/instrumentation-fs@0.30.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-n3Cf8YhG7reaj5dncGlRIU7iT40bxPOjsBEA5Bc1a1g6e9Qvb+JFJ7SEiMlPbUw4PBmxE3h40ltE8LZ3zVt6OA=="], "@opentelemetry/instrumentation-generic-pool": ["@opentelemetry/instrumentation-generic-pool@0.54.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-8dXMBzzmEdXfH/wjuRvcJnUFeWzZHUnExkmFJ2uPfa31wmpyBCMxO59yr8f/OXXgSogNgi/uPo9KW9H7LMIZ+g=="], @@ -1456,39 +1502,41 @@ "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.0.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.0.1", "@opentelemetry/core": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-UhdbPF19pMpBtCWYP5lHbTogLWx9N0EBxtdagvkn5YtsAnCBZzL7SjktG+ZmupRgifsHMjwUaCCaVmqGfSADmA=="], - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], + "@opentelemetry/sdk-trace-web": ["@opentelemetry/sdk-trace-web@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-R4/i0rISvAujG4Zwk3s6ySyrWG+Db3SerZVM4jZ2lEzjrNylF7nRAy1hVvWe8gTbwIxX+6w6ZvZwdtl2C7UQHQ=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.36.0", "", {}, "sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ=="], "@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.41.2", "", { "dependencies": { "@opentelemetry/core": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ=="], - "@orpc/client": ["@orpc/client@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "@orpc/standard-server-fetch": "1.14.3", "@orpc/standard-server-peer": "1.14.3" } }, "sha512-0HzeD/BgPctvFnd6ltjuQvx4/POXo0K01Tee/3whAm3ohXnlGqCfhzR2VMN8zBaGs1SYe6AFzjmGG928Ej3pAg=="], + "@orpc/client": ["@orpc/client@1.14.2", "", { "dependencies": { "@orpc/shared": "1.14.2", "@orpc/standard-server": "1.14.2", "@orpc/standard-server-fetch": "1.14.2", "@orpc/standard-server-peer": "1.14.2" } }, "sha512-/tFAua/w/mao2kQtJqjoCYEojHrKMisxOCK8qtkMKOUcXVxWMl+QWhP/MykjzFgkFdO9mzKOu1h7vJvpH73EBA=="], - "@orpc/contract": ["@orpc/contract@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/shared": "1.14.3", "@standard-schema/spec": "^1.1.0", "openapi-types": "^12.1.3" } }, "sha512-docXs4ALK3TADAnscEywjqvV1Dy+4+B6ihfo33hayvJdxZdpVmxjHOf7pcAYaJFJ6+LgKYoskaVVKad6LLxFlg=="], + "@orpc/contract": ["@orpc/contract@1.14.2", "", { "dependencies": { "@orpc/client": "1.14.2", "@orpc/shared": "1.14.2", "@standard-schema/spec": "^1.1.0", "openapi-types": "^12.1.3" } }, "sha512-51XFgfUYOfX7thwb8ww2EE0YHJDveW9HQNt7TN6sQMb3Bjx54h9r7vdfPJjUOJP3J4Ri2tOVstOsZ3CWUjbi5A=="], - "@orpc/interop": ["@orpc/interop@1.14.3", "", {}, "sha512-B8ANHAGVI8Mjw7Co0p+qBlkFG84i38WTKjR01HMkMXd6g9bHbgaqOfHcMpJMFaZzqvxBnXH4zPra2w6J8sQmhQ=="], + "@orpc/interop": ["@orpc/interop@1.14.2", "", {}, "sha512-FkrXlR0vmhx5D0t5WA6YN7XyC1WwjGGmkaaShVUGdnFu6m0bl4v3vZZWIMcPy5fG/6wF7KCTEGx0B6+kMmLWiQ=="], - "@orpc/json-schema": ["@orpc/json-schema@1.14.3", "", { "dependencies": { "@orpc/contract": "1.14.3", "@orpc/interop": "1.14.3", "@orpc/openapi": "1.14.3", "@orpc/server": "1.14.3", "@orpc/shared": "1.14.3", "json-schema-typed": "^8.0.2" } }, "sha512-Qcz2PzyZG2etpfB8ywy4Upf4SaI2x6x4fA8utVQXf5GcTPWfbTVz78MDDWnNtYEXSPj1BS95HqHC0AeGvYIB+g=="], + "@orpc/json-schema": ["@orpc/json-schema@1.14.2", "", { "dependencies": { "@orpc/contract": "1.14.2", "@orpc/interop": "1.14.2", "@orpc/openapi": "1.14.2", "@orpc/server": "1.14.2", "@orpc/shared": "1.14.2", "json-schema-typed": "^8.0.2" } }, "sha512-CpK8+evqU1j685f5M2UUO+tsm0HFKeykTQNTtq0WGMyFy7PPVOGYbJxZSorLJmWDfEw8ImPP2bj3wXEEOJsgag=="], - "@orpc/openapi": ["@orpc/openapi@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/contract": "1.14.3", "@orpc/interop": "1.14.3", "@orpc/openapi-client": "1.14.3", "@orpc/server": "1.14.3", "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "json-schema-typed": "^8.0.2", "rou3": "^0.7.12" } }, "sha512-0tZN91VoT6MEkOfw+ERKyozsnDXzmDSsBeMgEHN3Hl1WVU97T9l4aZzLlZILSNl3fat3HmduEDy/boNGmAWJkQ=="], + "@orpc/openapi": ["@orpc/openapi@1.14.2", "", { "dependencies": { "@orpc/client": "1.14.2", "@orpc/contract": "1.14.2", "@orpc/interop": "1.14.2", "@orpc/openapi-client": "1.14.2", "@orpc/server": "1.14.2", "@orpc/shared": "1.14.2", "@orpc/standard-server": "1.14.2", "json-schema-typed": "^8.0.2", "rou3": "^0.7.12" } }, "sha512-Xuy8auE1KubDjZGc5mvntSfaHy03p0tjDfEkx7k/S4SSJi0o4Rn+HYgCltgYznKwuyMnljl/0gShf99bo77lFg=="], - "@orpc/openapi-client": ["@orpc/openapi-client@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/contract": "1.14.3", "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3" } }, "sha512-1vp+hi858XDrCwYdhONl15YKNlHtj5F5gI3dq/McRRZ45tZ9/Ma03hxzABOOcayaT1L9nX7gPxhX7l5EuRf2zw=="], + "@orpc/openapi-client": ["@orpc/openapi-client@1.14.2", "", { "dependencies": { "@orpc/client": "1.14.2", "@orpc/contract": "1.14.2", "@orpc/shared": "1.14.2", "@orpc/standard-server": "1.14.2" } }, "sha512-KIXSVuGYh3JvO0lxUUztuMkEj6FV5C96QivR+H/HvqfFidC3hEH0nqi0vEWmUzlbzH3jbCaJV8KKV41HKnUNYw=="], - "@orpc/server": ["@orpc/server@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/contract": "1.14.3", "@orpc/interop": "1.14.3", "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "@orpc/standard-server-aws-lambda": "1.14.3", "@orpc/standard-server-fastify": "1.14.3", "@orpc/standard-server-fetch": "1.14.3", "@orpc/standard-server-node": "1.14.3", "@orpc/standard-server-peer": "1.14.3", "cookie": "^1.1.1" }, "peerDependencies": { "crossws": ">=0.3.4", "ws": ">=8.18.1" }, "optionalPeers": ["crossws", "ws"] }, "sha512-VQG1sgruPhWdzT/ChltJ5Ju9v1A8F+s8EQ1MMSI33z0AthZ3IuuMZdqMIOo5YSuHROoFxzMJgCShOWYR9qXhQA=="], + "@orpc/server": ["@orpc/server@1.14.2", "", { "dependencies": { "@orpc/client": "1.14.2", "@orpc/contract": "1.14.2", "@orpc/interop": "1.14.2", "@orpc/shared": "1.14.2", "@orpc/standard-server": "1.14.2", "@orpc/standard-server-aws-lambda": "1.14.2", "@orpc/standard-server-fastify": "1.14.2", "@orpc/standard-server-fetch": "1.14.2", "@orpc/standard-server-node": "1.14.2", "@orpc/standard-server-peer": "1.14.2", "cookie": "^1.1.1" }, "peerDependencies": { "crossws": ">=0.3.4", "ws": ">=8.18.1" }, "optionalPeers": ["crossws", "ws"] }, "sha512-+MkYqqI1CmR/eWsktpAxN4+Dd1rbDGO3xh9ZQae/V4zID5uz7smUnsIJL97GRDIZkwRUYlC8TDLLPAkxij22Iw=="], - "@orpc/shared": ["@orpc/shared@1.14.3", "", { "dependencies": { "radash": "^12.1.1", "type-fest": "^5.4.4" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-S7qmhZT4vchKEF6F6YduG5ub5lWnvQRVNq1/f5/kJkSnYMG5q6rWLcK7c3wYfDkeap05ZIiWTwksH+fv+yJOrw=="], + "@orpc/shared": ["@orpc/shared@1.14.2", "", { "dependencies": { "radash": "^12.1.1", "type-fest": "^5.4.4" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-5YtbVz4yGbJgcyj7CmEv5FAy4xagCS/hP/MMAlHpJKBSlMHuD7FrDO4LQLFUmSkcBcbLhzX8Ll1ziUw3vtPasw=="], - "@orpc/standard-server": ["@orpc/standard-server@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3" } }, "sha512-qO6xJy+S15Wx0elQeVojo3p5EgBLJDTEtElPcUF9o4ac8hrikYZJBeSg7qGgu/elCIrVbaFk/16Lu8P4qatPWg=="], + "@orpc/standard-server": ["@orpc/standard-server@1.14.2", "", { "dependencies": { "@orpc/shared": "1.14.2" } }, "sha512-XHySJICwDsJf211gcxtJBpzB1ldZrSHDW2mqbBQg+I2AewvTqWiqeGZV+SPvmq87q4IfBzncSuwMrRKplUJhsw=="], - "@orpc/standard-server-aws-lambda": ["@orpc/standard-server-aws-lambda@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "@orpc/standard-server-fetch": "1.14.3", "@orpc/standard-server-node": "1.14.3" } }, "sha512-/JpBBpLVcKTrALyhOB2zi5FfQi+X0uKNVkaZzGKd0iNLGLMYAQvfuWzdQRqWfnJb30yAPNVIjia+HFQgjyZBDA=="], + "@orpc/standard-server-aws-lambda": ["@orpc/standard-server-aws-lambda@1.14.2", "", { "dependencies": { "@orpc/shared": "1.14.2", "@orpc/standard-server": "1.14.2", "@orpc/standard-server-fetch": "1.14.2", "@orpc/standard-server-node": "1.14.2" } }, "sha512-/aPEC+WTQMHxmX8NauolIyzTRic96vPM17py3oe7LEaCyP/McyKUVrUsKu4He4zkEFrj3ErwVeOCkm19VvtIMQ=="], - "@orpc/standard-server-fastify": ["@orpc/standard-server-fastify@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "@orpc/standard-server-node": "1.14.3" }, "peerDependencies": { "fastify": ">=5.6.1" }, "optionalPeers": ["fastify"] }, "sha512-hDQCazvnlXR8+27qkm/uBwGd82l8UAz3LbBGmJyhDK96Cfuyx9QX5oECC21CeRZJylpbdvSuwwDlSpm6IQ0uRQ=="], + "@orpc/standard-server-fastify": ["@orpc/standard-server-fastify@1.14.2", "", { "dependencies": { "@orpc/shared": "1.14.2", "@orpc/standard-server": "1.14.2", "@orpc/standard-server-node": "1.14.2" }, "peerDependencies": { "fastify": ">=5.6.1" }, "optionalPeers": ["fastify"] }, "sha512-qmbNZKU+JdNKqaKHwUBCytx0G1RKYbfBwA5Cqf+S6Y+4InpwEdbcRSMgRnN+VH40lRPQAg1RO0AmH3NtvQ4yNw=="], - "@orpc/standard-server-fetch": ["@orpc/standard-server-fetch@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3" } }, "sha512-IHpBpyd+CTav7ycftKkQax6qrMGdpQfYKCuTLK+P3xsBl1A07UXvlpzPi/8MjyNGDCRAQkTaN7JIr/uqLL1B8A=="], + "@orpc/standard-server-fetch": ["@orpc/standard-server-fetch@1.14.2", "", { "dependencies": { "@orpc/shared": "1.14.2", "@orpc/standard-server": "1.14.2" } }, "sha512-FPXhHfGPA2Hcb3GBd6YjsbMbgcvb3XJ5aFr6TXmlpS8Qgek4fDM7nK1nZ+GoA5OitsL6B7Z9WoIbu39IiG1FcA=="], - "@orpc/standard-server-node": ["@orpc/standard-server-node@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "@orpc/standard-server-fetch": "1.14.3" } }, "sha512-jDMfxmicxwJq+UT3X9Ls/ijR1Inwv07Dkz+YIFiZ2MKlp3sXVZlxhleLqH5nxlsrANmNMpIENqosSBnaCcbQjg=="], + "@orpc/standard-server-node": ["@orpc/standard-server-node@1.14.2", "", { "dependencies": { "@orpc/shared": "1.14.2", "@orpc/standard-server": "1.14.2", "@orpc/standard-server-fetch": "1.14.2" } }, "sha512-9b/ffVIo8ZN6xk8hMxFvRxpaNmNMulPH9elaXkOwOdmH8TJnqAK79sKOSbDbmFx+wI0QPyjXOaqGpU9ie7J4uw=="], - "@orpc/standard-server-peer": ["@orpc/standard-server-peer@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3" } }, "sha512-Pk2Sccy+rnMYEDZnbO23NE6gP7ltk8pmlKABE4xlD1l87I/vENip94tiTG0QqmAmKCZ20Gec9vHtuLxtUqMOjQ=="], + "@orpc/standard-server-peer": ["@orpc/standard-server-peer@1.14.2", "", { "dependencies": { "@orpc/shared": "1.14.2", "@orpc/standard-server": "1.14.2" } }, "sha512-uzbgGaxvlZ0IA2lasaLck+yrbR3bKoqJnsZehEdbSm6eWaKln4COvxQJ2PuPow3gA6tklNnGkTfmRmGIHvS/rg=="], - "@orpc/zod": ["@orpc/zod@1.14.3", "", { "dependencies": { "@orpc/json-schema": "1.14.3", "@orpc/openapi": "1.14.3", "@orpc/shared": "1.14.3", "escape-string-regexp": "^5.0.0", "wildcard-match": "^5.1.4" }, "peerDependencies": { "@orpc/contract": "1.14.3", "@orpc/server": "1.14.3", "zod": ">=3.25.0" } }, "sha512-+SIDmqfkTLCeeZVN6Cic4aWeiBqf2O9F4Vto9npqOEXT1szIpHKJtCmdZBsTOSD46LV5Tcg1emOde9eUeY2EBg=="], + "@orpc/zod": ["@orpc/zod@1.14.2", "", { "dependencies": { "@orpc/json-schema": "1.14.2", "@orpc/openapi": "1.14.2", "@orpc/shared": "1.14.2", "escape-string-regexp": "^5.0.0", "wildcard-match": "^5.1.4" }, "peerDependencies": { "@orpc/contract": "1.14.2", "@orpc/server": "1.14.2", "zod": ">=3.25.0" } }, "sha512-47FBoaAz5GfHSdtD/ZNmTjorLD9SiD+I008UH2+DKehC4HMh8V1SbsMWCanUOkh1IFPZ1kzKaOOT+g0OAIOUWA=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -1500,47 +1548,47 @@ "@oslojs/jwt": ["@oslojs/jwt@0.2.0", "", { "dependencies": { "@oslojs/encoding": "0.4.1" } }, "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg=="], - "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.130.0", "", { "os": "android", "cpu": "arm" }, "sha512-h/xYU8/7ADWzVSf5I+YalLpj33LOy9CI/zgbJNIZ5eunRBG+Czqa3lZsvuPHHf3rOt6z1c5+UzoxjbAzAvhwVw=="], + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.128.0", "", { "os": "android", "cpu": "arm" }, "sha512-aca6ZvzmCBUGOANQRiRQRZuRKYI3ENhcit6GisnknOOmcezfQc7xJ4dxlPU7MV7mOvrC7RNR1u3LAD7xyaiCxA=="], - "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.130.0", "", { "os": "android", "cpu": "arm64" }, "sha512-oFWFJrsGv9siFM4HjMqKNB7IuIZD/SMmZdCXl8xyx7lDplGvPKyewpOo272rSWgMXe2Wx7bWI0Yj+gkHv4qbeg=="], + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.128.0", "", { "os": "android", "cpu": "arm64" }, "sha512-BbeDmuohoJ7Rz/it5wnkj69i/OsCPS3Z51nLEzwO/Y6YshtC4JU+15oNwhY8v4LRKRYclRc7ggOikwrsJ/eOEQ=="], - "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.130.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sGUzupdTplK9jQg7eJZ878HfEgQjJNBc6dAYVWJ9W5aU+J8rLfRJhTVsKThiu1pNwm6Y1qKCcbC6WhNWSXR3Ig=="], + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.128.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tRUHPt80417QmvNpoSslJT1VY8NUbWdrWR+L14Zn+RbOTcaqB8E6PYE/ZGN8jjWBzqporiA/H4MfO50ew/NCNA=="], - "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.130.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-PsB4cdCISbC00Uy8eiD8bc2AkGWjZqrSrJnkBFuG2ptrrf6mZ2F5gLFSjOAVMMgZPg8B1D7OydJwLWSfyI2Plg=="], + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.128.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-rWI2Hb1Nt3U/vKsjyNvZzDC8i/l144U20DKjhzaTmwIhIiSRGeroPWWiImwypmKLqrw8GuIixbWJkpGWLbkzrQ=="], - "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.130.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-DgABp3l38hS77JbXCV4qk1+n6DPym5u8zzwuweokezm2tX194nDSJDENbDRECxVsiNbprKATLbk+Z5wlHT0OHw=="], + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.128.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hhpdVMaNCLgQxjgNPeeFzSeJMmZPc5lKfv0NGSI3egZq9EdnEGqeC8JsYsQjK7PoQgbvZ17xlj0SO5ziH5Obkg=="], - "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.130.0", "", { "os": "linux", "cpu": "arm" }, "sha512-4Kn3CTEmwFrzhTSC/JuUW16qovmaMdX7jeSKbL8w0pLtLww7To1a2XJi9Z5uD8QWUkfUHhqfV+VD6dVzBnWzoA=="], + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.128.0", "", { "os": "linux", "cpu": "arm" }, "sha512-093zNw0zZ/e/obML+rhlSdmnzR0mVZluPcAkxunEc5E3F0yBVsFn24Y1ILfsEte11Ud041qn/gp2OJ1jxNqUng=="], - "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.130.0", "", { "os": "linux", "cpu": "arm" }, "sha512-D35KZM3F4rRu1uAFKyBlg3Gaf/ybCjyaPR1hfgvk5ex8NtcTmRgc0JgSighEyNg96TPrFhemFba68SZuxaha8w=="], + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.128.0", "", { "os": "linux", "cpu": "arm" }, "sha512-fq7DmKmfC+dvD97IXrgbph6Jzwe0EDu+PYMofmzZ6fv5X1k9vtaqLpDGMuICO9MmUnyKAQmVl+wIv2RNy4Dz8g=="], - "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.130.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Q9o7oVlo955KHwS8l1u0bCzIx+JsZUA3XToLXC+MsMhye/9LeBQbt84nh120cl2XLy+TEzvugYDiHShg5yaX6Q=="], + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.128.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Xvm48jJah8TlIrURIjNOP/gNiGe6aKvCB+r06VliflFo8Kq7VOLE8PxtgShJzZIqubrgdMdYfvuPPozn7F6MbQ=="], - "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.130.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EiJ/gC0ljbcwVpycC8YWw6ggMbtsPX8XMOt0mPx0aqWeMsNR+L9m05Flbvd5T+GlivG+GkSWQL7tM9SRFpM/dw=="], + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.128.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-M7iwBGmYJTx+pKOYFjI0buop4gJvlmcVzFGaXPt21DKpQkbQZG1f63Yg7LloIYT/t9yLxCw0Lhfx/RFlAlMSjA=="], - "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.130.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-b+h/lsLLurp756dMGizNs5uPaJfyEdWrTcV5t8M609jWm1DEHB1StpRXCkyvwtkJx3m+qL5BNQ0dEKan/4yGFA=="], + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.128.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-21LGNIZb1Pcfk5/EGsqabrxv4yqQOWis1407JJrClS7XpFCrbvr74YAB1V+m54cYbwvO6UWwQqS4WecxiyfCRg=="], - "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.130.0", "", { "os": "linux", "cpu": "none" }, "sha512-O19Cil83XAyjEFfo8WhkMwY58ALqZ7ckjGL+25mjMIuF84urWBeANH0FC8B8BsSSygWU3/1aY3ADdDbp+wlBnw=="], + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.128.0", "", { "os": "linux", "cpu": "none" }, "sha512-gyHjOTFpg9bTTYjxPmQirvufb89+VdZwVfcMtAUyPr6F5H8ZswvCQshK4qOW+Q+2Xyb33hduRgY/eFHJQjU/vQ=="], - "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.130.0", "", { "os": "linux", "cpu": "none" }, "sha512-BgXRVC0+83n3YzCscLQjj6nbyeBIVeZYPTI4fFMAE4WNm2+4RXhWp03IVizL7esIz36kgmT48aebk1iM+cs8sw=="], + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.128.0", "", { "os": "linux", "cpu": "none" }, "sha512-X6Q2oKUrP5GyDd2xniuEBLk6aFQCZ97W2+aVXGgJXdjx5t4/oFuA9ri0wLOUrBIX+qdSuK581snMBio4z910eA=="], - "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.130.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-6tJz0xvnGhsokE7N1WlUSBXibpYmT9xSJFS1Ce41Km/+8gQvdlW8MLhRv8PD0L7ix8vRG0FDDepp3jdOFzdVdw=="], + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.128.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-BdzTmqxfxoYkpgokoLaSnOX6T+R3/goL42klre2tnG+kHbG2TXS0VN+P5BPofH1axdKOHy5ei4ENZrjmCOt2lA=="], - "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.130.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9aCWj83dp3heTQGmGnZGdIWgxjZrr/7VQ0TGFHH5PKByxJKF2Hcr4qvaSUHhhGEa3MSsDjTL1YDP8RAgdL5/Cg=="], + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.128.0", "", { "os": "linux", "cpu": "x64" }, "sha512-OO1nW2Q7sSYYvJZpDHdvyFSdRaVcQqRijZSSmWVMqFxPYy8cEF45zJ9fcdIYuzIT3jYq6YRhEFm/VMWNWhE22Q=="], - "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.130.0", "", { "os": "linux", "cpu": "x64" }, "sha512-afXt87aZBqrUVli8TB/I8H1G50RDWcwirjWtXGXYqJ2ZqWEiErH7V72j3LUSDZaivmtu2OLX0KQ/mbhP81mr7A=="], + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.128.0", "", { "os": "linux", "cpu": "x64" }, "sha512-4NehAe404MRdoZVS9DW8C5XbJwbXIc/KfVlYdpi5vE4081zc9Y0YzKVqyOYj/Puye7/Do+ohaONBFWlEHYl9hw=="], - "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.130.0", "", { "os": "none", "cpu": "arm64" }, "sha512-I0NCrZV/YZuCGWgqwNN/GO/iXlLF2z+Wgc7u+Aa9N4P51oYeIa0XT+zVBUne4csO9GqxskXgI4g8JzzWGRpfOw=="], + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.128.0", "", { "os": "none", "cpu": "arm64" }, "sha512-kVbqgW9xLL8bh8oc7aYOJilRKXE5G33+tE0jan+duo/9OriaFRpijcCwT2waWs2oqYROYq0GlE7/p3ywoshVeg=="], - "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.130.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-sJgQkGaBX0WJvPUDfwciex6IcTk5O5NLQ1bhEb6f3nBruh1GshKMRSMt2bxZlYrgBzjyBbJzsnO+InPG0bg+fA=="], + "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.128.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-L38ojghJYHmgiz6fJd7jwLB/ESDBpB02NdFxh+smqVM6P2anCEvHn0jhaSrt5eVNR1Ak8+moOeftUlofeyvniA=="], - "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.130.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-bjcma99sQrNh6RY4mPO9yTkfxql6TDFoN3HWdK31RCKXwNhcDgJXW/l8PUtzKNiQ+9vpKJfJtQq+LklBuxSOBA=="], + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.128.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-xgvO35GyHBtjlQ5AEpaYr7Rll1rvY7zqIhT6ty8E3ezBW2J1SFLjIDEvI/tcgDg6oaseDAqVcM+jU1HuCekgZw=="], - "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.130.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-hRYbv6HhpSTzT4xTiIkadLI7upLQxuOdLPR/9nL1fTjwhgutBTPXrwaAPb/jTFVx6/8C7Jb5HcUKhmNwloTbFA=="], + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.128.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-OY+3eM2SN72prHKRB22mPz8o5A/7dJ+f5DFLBVvggyZhEaNDAH9IB+ElMjmOkOIwf5MDCUAowCK7pAncNxzpBA=="], - "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.130.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RBpA9TsRucJq6HNVNCFF1iKg+QeTkLdZf7hi4xaOGCPvMZWvDHjQgSOEZMUpuW4JNciHbxNhLEYmz5CVygjVGQ=="], + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.128.0", "", { "os": "win32", "cpu": "x64" }, "sha512-NE9ny+cPUCCObXa0IKLfj0tCdPd7pe/dz9ZpkxpUOymB3miNeMPybdlYYTBSGJUalMWeBM85/4JcCErCNTqOXw=="], - "@oxc-project/types": ["@oxc-project/types@0.130.0", "", {}, "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q=="], + "@oxc-project/types": ["@oxc-project/types@0.128.0", "", {}, "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ=="], "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.19.1", "", { "os": "android", "cpu": "arm" }, "sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg=="], @@ -1594,16 +1642,22 @@ "@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="], - "@posthog/ai": ["@posthog/ai@7.18.6", "", { "dependencies": { "@anthropic-ai/sdk": "^0.78.0", "@google/genai": "^1.43.0", "@langchain/core": "^1.1.29", "@posthog/core": "1.29.1", "langchain": "^1.2.28", "openai": "^6.25.0", "uuid": "^11.1.0", "zod": "^4.1.13" }, "peerDependencies": { "@ai-sdk/provider": "^2.0.0 || ^3.0.0", "@openai/agents": "^0.8.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.200.0 <1.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "posthog-node": "^5.0.0" }, "optionalPeers": ["@ai-sdk/provider", "@openai/agents", "@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/sdk-trace-base"] }, "sha512-qwEMysQXeLPh8SNaYvYIhzvl2tHl1MxOs0mHDTZ4AzWtHsDqZVgsYIeDJhm4Vm141UTzEDze36r6bDpfTxjIcQ=="], + "@polka/url": ["@polka/url@0.5.0", "", {}, "sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw=="], - "@posthog/core": ["@posthog/core@1.29.1", "", { "dependencies": { "@posthog/types": "1.373.4" } }, "sha512-q+/t/DZALr50YTE0dFgfGSS9EgwcyAlqsn+JS61wLkwdcDM5yu/YTDM8oMKmJupsyjSZlVkDuHZAMd4ab7AxzQ=="], + "@posthog/ai": ["@posthog/ai@7.18.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.78.0", "@google/genai": "^1.43.0", "@langchain/core": "^1.1.29", "@posthog/core": "1.28.4", "langchain": "^1.2.28", "openai": "^6.25.0", "uuid": "^11.1.0", "zod": "^4.1.13" }, "peerDependencies": { "@ai-sdk/provider": "^2.0.0 || ^3.0.0", "@openai/agents": "^0.8.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.200.0 <1.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "posthog-node": "^5.0.0" }, "optionalPeers": ["@ai-sdk/provider", "@openai/agents", "@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/sdk-trace-base"] }, "sha512-EzMWxRZt078DCpsCD6Iet/zEZmFQ6rJO07am+ErlbktY8L7LATe/sHRLMCkmg6HOlBa4Vcp2EUrMqNytl9bk+w=="], - "@posthog/types": ["@posthog/types@1.373.4", "", {}, "sha512-n+0AbGRYYsbi+CQXQi2rF1lwTSyASlaogcw4YSkzB5KeMa4Y6nhNb7+TTnu9aVor+BycsQYCa2OsBrMMbaTekw=="], + "@posthog/core": ["@posthog/core@1.28.4", "", { "dependencies": { "@posthog/types": "1.372.10" } }, "sha512-wmtUYHYqA3zIAKDKvYWRNWAQsWOIBwxV08e+bWzVy0wQQzpaS/LzzRupXWRMRrLOk+1x3JKFxbqA3n0QGvpqsQ=="], + + "@posthog/types": ["@posthog/types@1.372.10", "", {}, "sha512-KuT3vLu3LSFsNWCwasS4gqjH/ysAyIUcB/aJSmKyNhDd/85hAznHRz1eSSl0sMvtsDTYiQIq0I0ybduVbrpPew=="], "@prisma/client": ["@prisma/client@5.22.0", "", { "peerDependencies": { "prisma": "*" }, "optionalPeers": ["prisma"] }, "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA=="], + "@prisma/config": ["@prisma/config@6.19.3", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.21.0", "empathic": "2.0.0" } }, "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ=="], + "@prisma/instrumentation": ["@prisma/instrumentation@7.2.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-Rh9Z4x5kEj1OdARd7U18AtVrnL6rmLSI0qYShaB4W7Wx5BKbgzndWF+QnuzMb7GLfVdlT5aYCXoPQVYuYtVu0g=="], + "@protobuf-ts/runtime": ["@protobuf-ts/runtime@2.11.1", "", {}, "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], @@ -1624,7 +1678,7 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], - "@puppeteer/browsers": ["@puppeteer/browsers@2.13.2", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw=="], + "@puppeteer/browsers": ["@puppeteer/browsers@2.13.1", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-zmS4RTK9fbrc++WlAJhxYbfz3IjDeOmkK/CwwbLmk7ydfS9e2CiEeRJHEPvjDVElO/bwXbidwGA37Bsm6LzCnQ=="], "@puzzmo/revenue-cat-webhook-types": ["@puzzmo/revenue-cat-webhook-types@1.1.0", "", {}, "sha512-ChEa8v4dHUlZyQ7mt5i8x2lBcZ7STRSe3D+YO+DhXicJI0Thjcy23Ehbn9l071bxgmbeH7OErOphJrCLpq/SEg=="], @@ -1790,7 +1844,7 @@ "@react-email/text": ["@react-email/text@0.1.5", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg=="], - "@react-grab/cli": ["@react-grab/cli@0.1.34", "", { "dependencies": { "commander": "^14.0.3", "ignore": "^7.0.5", "jsonc-parser": "^3.3.1", "ora": "^9.4.0", "package-manager-detector": "^1.6.0", "picocolors": "^1.1.1", "prompts": "^2.4.2", "smol-toml": "^1.6.1", "tinyexec": "^1.1.2" }, "bin": { "react-grab": "bin/cli.js" } }, "sha512-L2eAxN46Vq2Ss3nDegrH7wQVMeWH03ahawp+OdzUtQWqL3cq6Bt149q9XhY3cWc9fJsxuWjLfCn+3T9uApIlBA=="], + "@react-grab/cli": ["@react-grab/cli@0.1.33", "", { "dependencies": { "@antfu/ni": "^30.1.0", "commander": "^14.0.3", "ignore": "^7.0.5", "jsonc-parser": "^3.3.1", "ora": "^9.4.0", "picocolors": "^1.1.1", "prompts": "^2.4.2", "smol-toml": "^1.6.1" }, "bin": { "react-grab": "bin/cli.js" } }, "sha512-UOc3PwN11Osw0NzaxRLK8trP4X+5iW1Dst3gvHRCafe3wXHyadzHYH8H1hdkcXdlIx3gsoD9ASJ+G/JH+A/jqA=="], "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], @@ -1848,21 +1902,23 @@ "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + "@s2-dev/streamstore": ["@s2-dev/streamstore@0.22.10", "", { "dependencies": { "@protobuf-ts/runtime": "^2.11.1", "debug": "^4.4.3" } }, "sha512-dtm+oFHVE8szINwOUoNQdx9xpGSJOrcAEvsxspPFvomjYKGnmhIRmU4OX8o6kxcPoiK76S1tPeU0smjZdmOngA=="], + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], - "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.53.1", "", { "dependencies": { "@sentry/core": "10.53.1" } }, "sha512-X4d6y8sBMjmNhcDW4eMBU3ASsNIMz8dqaFkhyIMN/dkYr/yZKnbRZPaVuVUGvHKjnlficPpIH0/HK9KBjrYxPw=="], + "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.52.0", "", { "dependencies": { "@sentry/core": "10.52.0" } }, "sha512-x/yEPZdpH6NGQeoeQnV9tj8reAH8twNttiltGZl2o8Rk7sQeUfe7E8yuYP2XbJ2RqyZK5qRS3COrNyMPzf6KFA=="], - "@sentry-internal/feedback": ["@sentry-internal/feedback@10.53.1", "", { "dependencies": { "@sentry/core": "10.53.1" } }, "sha512-vVpTI/aEYN5d9IgZeYJWMqVaN0+iFgidSrYNAsZTh1US5sJUzF/wrl+68KdpmCtFROrN3jiAn1oPSwL5CKvEJA=="], + "@sentry-internal/feedback": ["@sentry-internal/feedback@10.52.0", "", { "dependencies": { "@sentry/core": "10.52.0" } }, "sha512-5kAn1W8ZvCuHtEHXpq6iRkUMdNCilwww+YxaN2yofVrCivAbB3Ha5JJUMqmWOPW0pC27zGYmoJMIDvG+PczUxA=="], - "@sentry-internal/replay": ["@sentry-internal/replay@10.53.1", "", { "dependencies": { "@sentry-internal/browser-utils": "10.53.1", "@sentry/core": "10.53.1" } }, "sha512-wZNzTBYkgGUPWMuUQv7L64+OJmoCnz7GQNiTrTFK6EVAjJXFBCSsPp/nhif0bLhbk8+0g4xz633uOhpXuQbFdw=="], + "@sentry-internal/replay": ["@sentry-internal/replay@10.52.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.52.0", "@sentry/core": "10.52.0" } }, "sha512-diywyuc/H7VTUR+W5ryVmLF+0X4UP1OskMqb6V8RSAvJHcj2JmIm7uP+Fc6ACTno+b6AUShwT/L4xVXzO6X9Cw=="], - "@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.53.1", "", { "dependencies": { "@sentry-internal/replay": "10.53.1", "@sentry/core": "10.53.1" } }, "sha512-aueLaf/2prExwA76BGU5/bOXCKWqtt6jQXWA6WJQNrmKpPEtZJB4ypnpsou0McXQCF8tur2Y8U0TEkwQP13yJQ=="], + "@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.52.0", "", { "dependencies": { "@sentry-internal/replay": "10.52.0", "@sentry/core": "10.52.0" } }, "sha512-BI5ie4dxPuUJ344CXVSnAxY1xZCbghglPSCIlTOYODpR9so9yo5IZh+Mwspt0oWsUMaxWJiQSNYlbPWi7WDavg=="], "@sentry/babel-plugin-component-annotate": ["@sentry/babel-plugin-component-annotate@4.9.1", "", {}, "sha512-0gEoi2Lb54MFYPOmdTfxlNKxI7kCOvNV7gP8lxMXJ7nCazF5OqOOZIVshfWjDLrc0QrSV6XdVvwPV9GDn4wBMg=="], - "@sentry/browser": ["@sentry/browser@10.53.1", "", { "dependencies": { "@sentry-internal/browser-utils": "10.53.1", "@sentry-internal/feedback": "10.53.1", "@sentry-internal/replay": "10.53.1", "@sentry-internal/replay-canvas": "10.53.1", "@sentry/core": "10.53.1" } }, "sha512-zXF373hzUOGzUOrqd8xb1U3LQi5uYC3mwv+z5OMKUUinQlu30tTWBs7ypy6YTchtix9QlYaHWlayUF8vBZ5UjA=="], + "@sentry/browser": ["@sentry/browser@10.52.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.52.0", "@sentry-internal/feedback": "10.52.0", "@sentry-internal/replay": "10.52.0", "@sentry-internal/replay-canvas": "10.52.0", "@sentry/core": "10.52.0" } }, "sha512-ijL9jN86oXwXQWbwhPlEb70ODJSEmjxQEQdnZkC4gDWbjswcwvRsVJPYk+1xl2ir2iZixRIHipVxDcLwian35g=="], "@sentry/bun": ["@sentry/bun@10.38.0", "", { "dependencies": { "@sentry/core": "10.38.0", "@sentry/node": "10.38.0" } }, "sha512-8a2s+FVeqI2l12RNMFFEjAXpAUkqNZeGXTvHtjzcyWASW9szBNhOpiKN8oy0R/wUeIWgHpdnUeOSBhVKzH5YfQ=="], @@ -1894,7 +1950,7 @@ "@sentry/opentelemetry": ["@sentry/opentelemetry@10.38.0", "", { "dependencies": { "@sentry/core": "10.38.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" } }, "sha512-YPVhWfYmC7nD3EJqEHGtjp4fp5LwtAbE5rt9egQ4hqJlYFvr8YEz9sdoqSZxO0cZzgs2v97HFl/nmWAXe52G2Q=="], - "@sentry/react": ["@sentry/react@10.53.1", "", { "dependencies": { "@sentry/browser": "10.53.1", "@sentry/core": "10.53.1" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-lrwNq5T/zW84l60894TpKHPcvFuc1I/Hnohecc0TfYVpIcYYuw2orCHoU4v4wgkFaJUpegVetbgdOphViyLVjA=="], + "@sentry/react": ["@sentry/react@10.52.0", "", { "dependencies": { "@sentry/browser": "10.52.0", "@sentry/core": "10.52.0" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-2m72QCsja2cJJHD0ALxRnVt0qMEC2FV4LSi6AAiEdEG4lTb6mgcxavx5pJrW90jE+6dMGPbUz4q8c9vi4jh1qQ=="], "@sentry/vite-plugin": ["@sentry/vite-plugin@4.9.1", "", { "dependencies": { "@sentry/bundler-plugin-core": "4.9.1", "unplugin": "1.0.1" } }, "sha512-Tlyg2cyFYp/icX58GWvfpvZr9NLdLs2/xyFVyS8pQ0faZWmoXic3FMzoXYHV1gsdMbL1Yy5WQvGJy8j1rS8LGA=="], @@ -1934,102 +1990,104 @@ "@smithy/abort-controller": ["@smithy/abort-controller@3.1.9", "", { "dependencies": { "@smithy/types": "^3.7.2", "tslib": "^2.6.2" } }, "sha512-yiW0WI30zj8ZKoSYNx90no7ugVn3khlyH/z5W8qtKBtVE6awRALbhSG+2SAHA1r6bO/6M9utxYKVZ3PCJ1rWxw=="], - "@smithy/config-resolver": ["@smithy/config-resolver@4.5.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-abXk3LhODsvRHsk0ZS9ztrg/fZatTa9Z/z4pgx65YSLR+rY6kvUG/1IgcDKEUciR8MfdnkT5oPeHJTy/HhzDIQ=="], + "@smithy/config-resolver": ["@smithy/config-resolver@4.5.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-m5PNfr7xKdIegNG8DlLz+Gf/DlAhHWFGmFbe0DZo9pnvBwuZ3P/9OMtQU0UyWMYy8zjl+HDFVS7rdD9p2xEFjQ=="], - "@smithy/core": ["@smithy/core@3.24.1", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-3mT7o4qQyUWttYnVK3A0Z/u3Xha3E81tXn32Tz6vjZiUXhBrkEivpw1hBYfh84iFF9CSzkBU9Y1DJ3Q6RQ231g=="], + "@smithy/core": ["@smithy/core@3.24.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-rZ5YfycIXX6puoGjthnDiMpUgtKNOq3c7CndQYkCNYQTv26AiCrZQOJPy7ANSfZ6Okk3UvCRnmO1OYWlLnYZgg=="], - "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-0S/acwHnqX4WrjXzhdiDRxsG2s9SC0cpPIK9nZ1R6UOHd+j7uL28+4bHu22urbLk2TVw3fkp6na/+fkUt/pLNQ=="], + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-5gi+28FH+RurB2+tcRH1CK7KiLJ0dVnabjWLY3DgeFLiU45dbyrsq7NOYvMUcHgu9LVZH5F7G+Qk1GdXF0y6jg=="], "@smithy/eventstream-codec": ["@smithy/eventstream-codec@1.1.0", "", { "dependencies": { "@aws-crypto/crc32": "3.0.0", "@smithy/types": "^1.2.0", "@smithy/util-hex-encoding": "^1.1.0", "tslib": "^2.5.0" } }, "sha512-3tEbUb8t8an226jKB6V/Q2XU/J53lCwCzULuBPEaF4JjSh+FlCMp7TmogE/Aij5J9DwlsZ4VAD/IRDuQ/0ZtMw=="], - "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-X7MyI1fu8M84IPKk49kO4kb27Mqp6un9/0o/MsA1ngZ5OxxWKGUxPS3S/AJ9q1cPVTSGmRcbaGNfGUSsflTJkg=="], + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-JlY17/ZwBJ2O7FK/bKt8PZR+HBkyFwvgssgT6LiB0xYtz5/E5XG/HeKr5q2NMaVm8u8xjFfGk/6DVlbBe1qNkA=="], - "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.4.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-JZGbSXaBk7JY8VPzsh66ksJ0nTWXbApduFDkA/pEl3aTm2EoAiUZE1Iltp6c+X1bB8kxPQW0mHDfVdYCpWTOzg=="], + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-1Pg7aqxIdMilTbGJKCHTx0toIkKSrHdO6VHCh9oCncWJG+1wkJa90O/xb9mmRPuoOFCg2DLZAqnRyuBiUQnNIA=="], - "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-6Cn4xTNVxn9PWTHSbvf8zmcDhQW8lrLE1Xq5CJgmX6wEvdjS2S0KuE79Aiznv/jx51jpFJ98OuWyE+Bt+oG1MQ=="], + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-Xte1Td6CQpc/D0WnPZ2k98CvF7y1GopylMoGY/r26a9wbRHV5xusRbT6O9vouSeZlvtxoVb4ON/1fLRofO7m4Q=="], - "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-r7bN6spQ+caZC8AnyvSxkRUb57zt2jhhRw3Z+2Ez8hjq6coIikDBFUUI/+CQ1xx9K6eX1Gx6wUKo4ylU66TIqw=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-yxurumLvHfgYgM0FVtjOVIyBSJXfno4xKKOgD43wOk9Qh+2lTKfP9Qhu4JHU7IUwrqVPa888byUzomHMgvKVMg=="], - "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-2fbltQVQYmGd0OzPv2oDMRF0pxkzeIx8cbpx2x6W3UJWGaEyUzVPxF4d0sDXZ/r2obg+RbTyhTidXWlPDsKRKw=="], + "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-xOQ7w5hSzTe8IAwQ6BAbX+1d9s9SRwWUbU8cYYh5DHgkYOi131Q5+B9Blelg9u0zoPfM4VYOuuBRAmregPkzpQ=="], - "@smithy/hash-node": ["@smithy/hash-node@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-u0/zo11mg7yNneoYgTkH4sXwSmcBpbl49o4UNCtQ7hYsXxynsN25KYHmXzqi7TPk5HQL5klGnpU5koOY0O+9hw=="], + "@smithy/hash-node": ["@smithy/hash-node@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-4a+KoVqr1SZtw7cZvY24XU1S5OL+c23MdDQ3jFmMCQ5s9diBFdMG/UIgp5dNqlwvDrWA0U5KO+z3Gzq1ize+LA=="], - "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-4NOnngIoXngbJw9By3u8KXRgqt4vYATpAobNBnNWxOREP7JY3kB0bUmbBNhZ7dtZV/b4auO1eFMD4cLj9OauVg=="], + "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-DPDT0UyOREMPwipO7BzSJfD8z2YYp/x1FMpHqEppBF+gmGr7FoqUUFlXfK+YPOfHhlr8HDJRQpmShNb9C9po5Q=="], - "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-cLmwtDoulyZvRepAfyV+3rx5oMvuh51dbE+6En3vGC09j3uVSRt1U4oguNu32ub3soGX0oYtBs8E7S2Q4SxTqg=="], + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-TaoGtqi2ZNdGzxUgYcLczjW8rb/h5DQ8vlCMYDSdZ4LRzGQrrEYgUjlZVM9dAagTsLK5gZx1f7+44sFTjz5vuQ=="], - "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-9aVG6VjOFVFHC6Z4hGAzIIrsVWpp1QOO4ERQ2k1S19VrgCamUGIBE2ilAnMWCfr+mlowHlLRXBStsTk/2c5HfA=="], + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-V9ZCT5mHNteWOKtu3LkGHTheEyBWzTU3XydeSOHrqG6znEoCARjxFZ0XP9JJyKV9jxEMx7hNbzGlQaXtKkmvSQ=="], - "@smithy/md5-js": ["@smithy/md5-js@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-98NalujRdzv6ggVQNYPWpL2K57UKeUB8roIr61u6+JiHd7KUlMQ+sn/vk6IG4XxEjw2vlC7eu/xjYXshUE4XXg=="], + "@smithy/md5-js": ["@smithy/md5-js@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-bODwWrXILREpCL7XZq1/QxiMqFxdWadw3noiKGjNKsPl1/nvWtLVFInNjK6pmHjM4BUOgfgZkrl9+V56ez2FNA=="], - "@smithy/middleware-compression": ["@smithy/middleware-compression@4.4.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "fflate": "0.8.1", "tslib": "^2.6.2" } }, "sha512-p5HLi70K05XqhXHPQEhANMv3xhFAgcz0O11sCd8MZEXI45Y86A5hLEwDNQPbvoHh3U3B+NKdh1vqlsjpiozfXw=="], + "@smithy/middleware-compression": ["@smithy/middleware-compression@4.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "@smithy/types": "^4.14.1", "fflate": "0.8.1", "tslib": "^2.6.2" } }, "sha512-8/vRRUfFwJzBNlu1cCs4479OJM5OK3MBHkYWf61hbx/4N13MdgniCd4zWpJ+n51VIEPyTFQ7jkYGT1E8e93jkw=="], - "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-l4BUIP+wljW/Ar+0/QcGdmElI9lalrywfzNijXMBG34Z510FRzPyrDLx/blNTZOAm0C4Mvx5t/bf760CZo1ajg=="], + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-IbSiS/3nOxsimCthzElEoBrjQo+Na4bsQ63qyC8qSI8lkMjOv9+VlosDQd8gfNolAD9XmC5tLqYTI0bJGJsscg=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.5.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-qtqu5TS+8Y18ZDkJoiXN5AMW1G4JAg1+xytzpsUvIR5a4EUsgd5HQg12lekEHWpm2TDUmOgg+hBaHK7dvyWdkA=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.5.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-ux8LgN/m/X7ET2ISRc8G4aKFI1QhINZtkKpoayNPTrhwpsCVxb47mlpYFuWceTlesc0Wmb0S9y6DP195ReQoXA=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.6.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-eTaQhxs0rfUuAkL2MSKrH8DTO7YCeAgrdN0B2/RAeuHmXQ+x52dk5qUBsi/jtcqe5LxItgq5AG5tI6Cp8c0sow=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.6.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-8CtxY9aHT4f3UvZUbU2O0bccRckqTDfTKk3t1DawUZa5DWRZdV2AMABLsdMTdj7KE1uumhzEaT0X7/jTcOtoBw=="], - "@smithy/middleware-serde": ["@smithy/middleware-serde@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-t7YtUe076zWVypVmy1rX91oKi2TFJCkpfFpfMhJFpEIRPP0iL9JxjeSyFQ+1bF45JUfDzOzslUJa150WcSrBug=="], + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-c+V02hZlIStscI4ie2VllJjM4DLxdI2SymIBvXmqCqicrNb0NAbgDXDTBiwcMiruaBOqEFYxpKXbz6JjsNEN3Q=="], - "@smithy/middleware-stack": ["@smithy/middleware-stack@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-1jKwiKZxCMQNqmp4uVPYA6r+MLGjEtH07gnOUdPgbnjuOIrl/0JY/ICdpQtFgeBsQ/Up01gnSv8GYEL0fb8yvg=="], + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-KtYcs+sJn7AiT0YdM53/6MT0dKsaW2MSAr9MpprRVSfwN9qyKQf2dBIuCXt18/nEZaWerol/bGaQ63G949aovw=="], - "@smithy/node-config-provider": ["@smithy/node-config-provider@4.4.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-q7tDJEJXcaSG/8TVpu2f2l9bzxTzDM9geWmltbzsY6Hfh3yiuXXTpLIO8+zwYASPPVFaTJpdKwjSSjdoDoccgw=="], + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-5RutFJsYoqK4tWYZOjGQrPLowGf2Ku8rbNuVeGkNJ5axIDO4LV/fydBojPtwcDz2zf87YNCOXfNyuEyAwYgI7A=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-BdEYko85f/ldp68uH8XEyIvo810xFk6eyPH81SRggTOApYHWA+Xu7B2EzLuHbe37WVLaUA7F1fWR3/zBeme2WA=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-PxF57Jr3dPm+RgZWekOL+o96FPdaT62xZUyDfi47uMRFi5rHpwO/ewFbrztrASQ/7H8moNi1sspIHihHpfoKsQ=="], - "@smithy/property-provider": ["@smithy/property-provider@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-3NHoqVBhzpY2b4YBx9AqyKC4C8nnEjl5FyKuxrCjvnjinG0ODj+yg1xX360nNahT6wghYjSw1SooCt3kIdnqIA=="], + "@smithy/property-provider": ["@smithy/property-provider@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-/YBWtO2SdvPSAUk/Ke1Xpdg1E1lfaNGblla7mnIVGtaGkSQ5bK7KBZqpuj5IokHlU9UcLDvt2QwTLV7oRzBUTA=="], - "@smithy/protocol-http": ["@smithy/protocol-http@5.4.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-8irPNCQgYxcSFp1aGcnDNFkTwSA+xPUaFq9V/v1+JXWu8sKr5b3cFmg2kBTkjkvypDmGeNffuNu0x5iqw1NoAw=="], + "@smithy/protocol-http": ["@smithy/protocol-http@5.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-WG0LgSZg+WbvWYD04uwIYVyMEpyd0cPx1lkqx61JxunxiFti+wGoFiDKr6wswun1r25Z2f8yUoMQWyxjMnnXtw=="], - "@smithy/querystring-builder": ["@smithy/querystring-builder@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-toyi8sXPWDNoVH6yK7sXJ9dm5uxw2tWLCHzPy/t16Fvl62Es4vXQXzlilyNaw+DqFwxSlrFClh0rGLPUF2p9Lg=="], + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-w1EVgJXg1R/f5iJlQatMBt7sP9tHhEscvK0lv62j/esnqRgdoQqlkcgHotfOJpg1CTtY8eUvze3v3EU91631IQ=="], "@smithy/querystring-parser": ["@smithy/querystring-parser@3.0.11", "", { "dependencies": { "@smithy/types": "^3.7.2", "tslib": "^2.6.2" } }, "sha512-Je3kFvCsFMnso1ilPwA7GtlbPaTixa3WwC+K21kmMZHsBEOZYQaqxcMqeFFoU7/slFjKDIpiiPydvdJm8Q/MCw=="], "@smithy/service-error-classification": ["@smithy/service-error-classification@3.0.11", "", { "dependencies": { "@smithy/types": "^3.7.2" } }, "sha512-QnYDPkyewrJzCyaeI2Rmp7pDwbUETe+hU8ADkXmgNusO1bgHBH7ovXJiYmba8t0fNfJx75fE8dlM6SEmZxheog=="], - "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.5.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-FKoKxVzdFPhyynFI+SPTWrgOP60fZ4l1UwukWYj4eyhpSmEI7MJ6p58hawIIt9bwp+aek9NEm8Zika7E+GEoeg=="], + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.5.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-xATpw6gcurFztdsUrMNaKb2ugqk3545Whhqg7ZD4sxTg+zI27THjg3IY+InXsVWturOWdCdV+UHQx11g9Sp5Kw=="], - "@smithy/signature-v4": ["@smithy/signature-v4@5.4.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-728lZZEWYWubBESrfntNslZQYDKRlJDY4dcDnYbL50+gu35pGPLblu4S0/RH/RDLF6me1M87ECHsHELGL7dA/Q=="], + "@smithy/signature-v4": ["@smithy/signature-v4@5.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-nkdB9T8JS6iD5PukE5TB8KqcvMEPVPHVUY7J0odYJgyIM40Du2msUhBdoPNRqRArDDcGQqVQcbzu0CZA7b+Nkw=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.13.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-IcznNM8Qd9u1X3oflp12tkzyOB4HbT+sfYWlWiyEysgNzSHoWcHUUsTT4y1jjDjtVuuVVQbYks+g1kVd7u1eGQ=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.13.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-lysfoRCr7PdD9CsPp9VQuJYRGI5mWYb8FRkbdBSQttxpQmW7tZsFgmpBNKVcgvBsAgBCkYX/UQs0NmznuBcZQQ=="], "@smithy/types": ["@smithy/types@4.14.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg=="], - "@smithy/url-parser": ["@smithy/url-parser@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-tuelFlF2PZR/wogFC58NIrPOv+Zna4N1+3kA161/33D1Gbwvl6Nh4WsAsW05ZyPp0O6CMGsdbb0S2b/qVjRMCw=="], + "@smithy/url-parser": ["@smithy/url-parser@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-I5tCWs/ndLrJrbvlnsN1cOt8PVAbQEqg0nNeQqebD5ynQcbhgch9uA7KmpX9vfq/vEudq0iVYAOxt+4aBkUlWA=="], - "@smithy/util-base64": ["@smithy/util-base64@4.4.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-fTHiwW2xbiRiWzfSk4IGAr3gNZCH4fuRYqt8+IuarsP/YON35576iVdePraZ6yJlFxlCL0eMec3/F7xYqoKzlg=="], + "@smithy/util-base64": ["@smithy/util-base64@4.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-puJITyefgQ9a5F+wKylCLkf0VCwesWbaN4O3YCEalRin4N0CTPQu/XA3kz/QsMOTgd3knhd0BQwGCBm/tv0Y1A=="], - "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-1scg5t4nV3hV7CZs996/XHb80aDZ5YotH4NcvkW/w/rHj+cSz0aCIzwz8aUNKB4nCDPSHRCbrKoj+TvycYefmw=="], + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-83U8xa8EmdExGzFuqBzgXvtmbLQIYcCuCNm5no4rlPqpGdOPGUufzMvLdlw+sPTb01qHIsDDNwOecm4s8ROOPw=="], - "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-VRC8MKVPKrgUYThTA7ughcKMfjW6/X92H0wXGJoda0Apw4O5xbXL0GMLz40DTWlsb5hh2iItk6+XL72uJdxYcw=="], + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-Ok2v9zPFfd6uOJMTIIJ8HFdCpARD77q4OHYhwhG9y5X1Y9oeQ0CHUQVJD6LhT6l8FUkFYisqcUaZSg7SArFUTA=="], "@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - "@smithy/util-config-provider": ["@smithy/util-config-provider@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-lw6L5GF5+W19vO6o3fZwRT2cXEG+8b2LH0b9ppjDT6nIxjUgmljEQGninx5XorylwKZZ4XLVABeroJ8oaF9RmQ=="], + "@smithy/util-config-provider": ["@smithy/util-config-provider@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-kAC6/UB9qW9r2xQAOko2iDxAXmRD2VGMZjnXSEacAhQySdJs58CwvoOE0tHWdtc/lWF4g78X6Z9ucLanJnuVUw=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.4.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-1rA7w+LjK1WJClsffC81Z/ZtjFt22QsKhBjUYEnZsGVS2nOTfOENKBzdg4SxhdwFvBCjcbpjscUfXOPwE3UHWQ=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-jKezW5Taa+N2gbkB02UVijH1rFlEJC+cskZzwasFqFJMBBi/bcVgHqcYOX0WOnUk6MDZfHf0gEsr5Br4XMHiAg=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-1fk1wfQHBenQD5NitVKOFgW0wsISYAFPIXGyStJWAeCtMyRhgHYvtJxBk2rwGWA0L5QX6oM6yeHSLKPFMk59ww=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-xYRuNHHIztu5AzruMJ8kTyA1JsBL/yZKvX5z/A7OHUxsf+rkEESZFZWJDcAj5dDWSu6brWFe5KH6qJNTVztX/w=="], - "@smithy/util-endpoints": ["@smithy/util-endpoints@3.5.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-yORYzJD5zoGbSDkAACr0dIjDiSEA3X8h8lggDENl1dkKpCG0TQIoItPBqtvuJHzFFjRXumcoH+/09xIuixGyCw=="], + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.5.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-pcvTCp9Wch/9UnWWfRGoG5GJogDXFPjevE+CqALxtPFGA4GqFQRD6eUtgJhHN+NPtohcozI12u1skF2/iubGrQ=="], - "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-j6dAIaXfj2nsvv/sN9+fi7e/AJxBHgBoIdNjmQjp9jlii72rEniUGQkipnkHMP2XUKHx5q0B1iv0xQEG1AsLBA=="], + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-ZkAHu0SAsXPkVpaP6dhzu+DO/i4mlAMmwa4tejbGv9shozy/m4a2vIAk6HjPy7fKuGpANE1tZczGfCSLgyw5jA=="], - "@smithy/util-middleware": ["@smithy/util-middleware@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-SRRMDcIgVXVhVbxviBaSZbuWuVW3jD08wv4ESV0V2oiw0Mki8TPVQ5IxwD3MvSTPg52QYsRP+JoMw5WdUdeWAg=="], + "@smithy/util-middleware": ["@smithy/util-middleware@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-X/DNQxgUCbjjs3HosLmt5Yi1NocxjRFiiOgHml4tVV3w4mIbqZxPR8kq7apGPEMnhIpyxeTgFyypMrfxfn2DlQ=="], - "@smithy/util-retry": ["@smithy/util-retry@4.4.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-qkgWgwn1xw0GoY9Ea/B6FrYSPfHA0zyOtJkokwxZuvucRf2+2lfTut6adi4e4Y7LEAaxsFG7r6i05mtDCxbHKA=="], + "@smithy/util-retry": ["@smithy/util-retry@4.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-pV/Kq4jUuP9raOqwSPeBiut2IWmwbc9vM+nE3ly4YUkzPHbBZvfhikwMOyudER+KHPjakuc8r4TecEPMsI7nVg=="], - "@smithy/util-stream": ["@smithy/util-stream@4.6.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-GjZfEft0M0V3n2YM/LGkr5LeLd8gxHUIzW0rUz6VtTtlAq245GxHlJghvoPEjJHKTj255iHFAiA4IsIdK40Ueg=="], + "@smithy/util-stream": ["@smithy/util-stream@4.6.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-BlWg46UASokl3O5YqWmbLpINE5stmAxynXlyOe1nE4dx+tvwgqtT4ug/rPcRg0xVcBnj68XlcOqbXeaGGcH0DA=="], "@smithy/util-uri-escape": ["@smithy/util-uri-escape@1.1.0", "", { "dependencies": { "tslib": "^2.5.0" } }, "sha512-/jL/V1xdVRt5XppwiaEU8Etp5WHZj609n0xMTuehmCqdoOFbId1M+aEeDWZsQ+8JbEB/BJ6ynY2SlYmOaKtt8w=="], - "@smithy/util-utf8": ["@smithy/util-utf8@4.3.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-FtRrSnriXtOs4+J8/y9SbQ1xmN71hrOsN/YJr5PQQj5nR1l7YNkGS/TEk4gr0WN7gyrUqw8/RFaYVjI18732ZA=="], + "@smithy/util-utf8": ["@smithy/util-utf8@4.3.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-5hrmCc+dTgZkiFhX72Q16LemYPkvZ1M4pFMOhk0X9tQnLY7dn7zC1+C+aAJn0dw6CXldbqY/KMbMYCwm8yw14g=="], - "@smithy/util-waiter": ["@smithy/util-waiter@4.4.1", "", { "dependencies": { "@smithy/core": "^3.24.1", "tslib": "^2.6.2" } }, "sha512-G/gWDykZNL0NVcd1qXkoKm45jxJECp6q53DSomM5QKMsyAMEsGksVq+HwgonqYxfFJEzzHi6ljtWKXVS1pl0/Q=="], + "@smithy/util-waiter": ["@smithy/util-waiter@4.4.0", "", { "dependencies": { "@smithy/core": "^3.24.0", "tslib": "^2.6.2" } }, "sha512-7kAlrB3n7/BHyw+uLq83d5jdadPUcDkdMOUSGxvpXjrJ++G0hTedTnoNChjybIxhZ/Gk7sCrfIOLkMAB0LhRBA=="], "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + "@sodaru/yup-to-json-schema": ["@sodaru/yup-to-json-schema@2.0.1", "", {}, "sha512-lWb0Wiz8KZ9ip/dY1eUqt7fhTPmL24p6Hmv5Fd9pzlzAdw/YNcWZr+tiCT4oZ4Zyxzi9+1X4zv82o7jYvcFxYA=="], + "@squircle/core": ["@squircle/core@1.0.6", "", { "peerDependencies": { "@squircle/paint-polyfill": "^1.0.3" } }, "sha512-jvAIehtarEHBl6W80LNiZ9KWVKdJw6jGj/avwwbCoGVs9t+AUA5YVmUyAtEasFtoocHwxbbRsdnTeKxQ+061aQ=="], "@squircle/paint-polyfill": ["@squircle/paint-polyfill@1.0.6", "", {}, "sha512-w3FotA1v53MG+R6eGM37wv1aVoW3jwymwGteGCGXEM10Gq7zYeCus5dTgcyW6fhc9piRqsFLyU8MIl+EbtM9RA=="], @@ -2054,7 +2112,7 @@ "@stoplight/path": ["@stoplight/path@1.3.2", "", {}, "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ=="], - "@stoplight/spectral-core": ["@stoplight/spectral-core@1.23.0", "", { "dependencies": { "@stoplight/better-ajv-errors": "1.0.3", "@stoplight/json": "~3.21.0", "@stoplight/path": "1.3.2", "@stoplight/spectral-parsers": "^1.0.0", "@stoplight/spectral-ref-resolver": "^1.0.4", "@stoplight/spectral-runtime": "^1.1.2", "@stoplight/types": "~13.6.0", "@types/es-aggregate-error": "^1.0.2", "@types/json-schema": "^7.0.11", "ajv": "^8.18.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.1", "es-aggregate-error": "^1.0.7", "expr-eval-fork": "^3.0.1", "jsonpath-plus": "^10.3.0", "lodash": "^4.18.1", "lodash.topath": "^4.5.2", "minimatch": "^3.1.4", "nimma": "0.2.3", "pony-cause": "^1.1.1", "tslib": "^2.8.1" } }, "sha512-WvdgmiiJrjiMrcw7ByxfcYtUvAXNp2MhAfcEIXP3Mn8ZOVwyAWIsFjLlsE5zRqj0LuN8+7OQM/L+BMcHj6x/BQ=="], + "@stoplight/spectral-core": ["@stoplight/spectral-core@1.22.0", "", { "dependencies": { "@stoplight/better-ajv-errors": "1.0.3", "@stoplight/json": "~3.21.0", "@stoplight/path": "1.3.2", "@stoplight/spectral-parsers": "^1.0.0", "@stoplight/spectral-ref-resolver": "^1.0.4", "@stoplight/spectral-runtime": "^1.1.2", "@stoplight/types": "~13.6.0", "@types/es-aggregate-error": "^1.0.2", "@types/json-schema": "^7.0.11", "ajv": "^8.18.0", "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.1", "es-aggregate-error": "^1.0.7", "expr-eval-fork": "^3.0.1", "jsonpath-plus": "^10.3.0", "lodash": "^4.18.1", "lodash.topath": "^4.5.2", "minimatch": "^3.1.4", "nimma": "0.2.3", "pony-cause": "^1.1.1", "tslib": "^2.8.1" } }, "sha512-4hTxMDs4TFUG4/jKjaZttA65gNuV2PCKI9+51I+J4nL6ylo17DlbW+sl6byKnBuV/85HxaV33ri5fEGlp8lTSA=="], "@stoplight/spectral-formats": ["@stoplight/spectral-formats@1.8.2", "", { "dependencies": { "@stoplight/json": "^3.17.0", "@stoplight/spectral-core": "^1.19.2", "@types/json-schema": "^7.0.7", "tslib": "^2.8.1" } }, "sha512-c06HB+rOKfe7tuxg0IdKDEA5XnjL2vrn/m/OVIIxtINtBzphZrOgtRn7epQ5bQF5SWp84Ue7UJWaGgDwVngMFw=="], @@ -2150,8 +2208,20 @@ "@tanstack/virtual-core": ["@tanstack/virtual-core@3.14.0", "", {}, "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q=="], + "@tinybirdco/sdk": ["@tinybirdco/sdk@0.0.69", "", { "dependencies": { "@clack/prompts": "^1.0.0", "chokidar": "^4.0.0", "commander": "^12.0.0", "dotenv": "^16.0.0", "esbuild": "^0.24.0", "picocolors": "^1.1.1", "zod": "^3.25.0" }, "bin": { "tinybird": "bin/tinybird.js" } }, "sha512-ScwHmj/bIjjxc7skTv+lRES8x0OnCQ8s8CHhS8kVjL5R1A0nrLsoCamBbFQUe0R1RL5Cm+ylWkTTCLyvCqkjnw=="], + "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], + "@trigger.dev/build": ["@trigger.dev/build@4.4.6", "", { "dependencies": { "@prisma/config": "^6.10.0", "@trigger.dev/core": "4.4.6", "mlly": "^1.7.1", "pkg-types": "^1.1.3", "resolve": "^1.22.8", "tinyglobby": "^0.2.2", "tsconfck": "3.1.3" } }, "sha512-eHPPaeuFe9GZDndQzP4QUlxocyIJWYSx0FMx1GEiAnEVKwXWUqiW72DRFH7cr9v7IQnI9YbAWRuWvyMPHSVLwg=="], + + "@trigger.dev/core": ["@trigger.dev/core@4.4.6", "", { "dependencies": { "@bugsnag/cuid": "^3.1.1", "@electric-sql/client": "1.0.14", "@google-cloud/precise-date": "^4.0.0", "@jsonhero/path": "^1.0.21", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/exporter-logs-otlp-http": "0.203.0", "@opentelemetry/exporter-metrics-otlp-http": "0.203.0", "@opentelemetry/exporter-trace-otlp-http": "0.203.0", "@opentelemetry/host-metrics": "^0.37.0", "@opentelemetry/instrumentation": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-logs": "0.203.0", "@opentelemetry/sdk-metrics": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1", "@opentelemetry/sdk-trace-node": "2.0.1", "@opentelemetry/semantic-conventions": "1.36.0", "@s2-dev/streamstore": "0.22.5", "dequal": "^2.0.3", "eventsource": "^3.0.5", "eventsource-parser": "^3.0.0", "execa": "^8.0.1", "humanize-duration": "^3.27.3", "jose": "^5.4.0", "nanoid": "3.3.8", "prom-client": "^15.1.0", "socket.io": "4.7.4", "socket.io-client": "4.7.5", "std-env": "^3.8.1", "tinyexec": "^0.3.2", "uncrypto": "^0.1.3", "zod": "3.25.76", "zod-error": "1.5.0", "zod-validation-error": "^1.5.0" } }, "sha512-oXAjxBNVMiKXUKj1EnHUlO2ULujc4Dy8ad+H/59DYZGY1barkGVzrQAQIpBZo/kDygu7dKGT+F5yBkbIkYjAUw=="], + + "@trigger.dev/react-hooks": ["@trigger.dev/react-hooks@4.4.5", "", { "dependencies": { "@trigger.dev/core": "^4.4.5", "swr": "^2.2.5" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-csY7tmfg2wNAt3MOLgJ5C94y9Cf0g0f3K8NN5etNiI/48wnKV8PgSrZpARSQczhBoB6pt/cwvB4kiLAG/aVQug=="], + + "@trigger.dev/schema-to-json": ["@trigger.dev/schema-to-json@4.4.6", "", { "dependencies": { "@sodaru/yup-to-json-schema": "^2", "@trigger.dev/core": "4.4.6", "effect": "^3", "zod": "3.25.76", "zod-to-json-schema": "^3.24.0" }, "peerDependencies": { "@sinclair/typebox": ">=0.34.30", "arktype": ">=2.0.0", "runtypes": ">=5.0.0", "superstruct": ">=0.14.2", "valibot": ">=0.41.0" }, "optionalPeers": ["@sinclair/typebox", "arktype", "runtypes", "superstruct", "valibot"] }, "sha512-lxj9702efdwk5uAeUeCJJh+u9Cl+IK3M4hgbXHjBEYAUd1410Ft7CcIsaGQKK3uEZZhyb2zluBtKtL9RmQ3Puw=="], + + "@trigger.dev/sdk": ["@trigger.dev/sdk@4.4.6", "", { "dependencies": { "@opentelemetry/api": "1.9.0", "@opentelemetry/semantic-conventions": "1.36.0", "@trigger.dev/core": "4.4.6", "chalk": "^5.2.0", "cronstrue": "^2.21.0", "debug": "^4.3.4", "evt": "^2.4.13", "slug": "^6.0.0", "ulid": "^2.3.0", "uncrypto": "^0.1.3", "ws": "^8.11.0" }, "peerDependencies": { "ai": "^4.2.0 || ^5.0.0 || ^6.0.0", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai"] }, "sha512-FAC5/oES4dClLWlLGvnuNUlhfzWXDA6ResxB7B6/2UohSdxn3yWt9uOtjEL434JiN0xNUCiSAN8KTP1azpOgBg=="], + "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], "@tsconfig/node10": ["@tsconfig/node10@1.0.12", "", {}, "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ=="], @@ -2190,7 +2260,7 @@ "@types/buffer-from": ["@types/buffer-from@1.1.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-2lq4YC9uLUMGHkl2IDtX4tCXSo2+hwMpOJcY1qiIk1kybc31rIlPyM1HCVJhkPFIo75a/pOVxqyvwuf5TpCG/w=="], - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], @@ -2200,6 +2270,8 @@ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/cookie": ["@types/cookie@0.4.1", "", {}, "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="], + "@types/cookiejar": ["@types/cookiejar@2.1.5", "", {}, "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q=="], "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], @@ -2322,7 +2394,7 @@ "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], - "@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], + "@types/node": ["@types/node@24.12.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-8oljBDGun9cIsZRJR6fkihn0TSXJI0UDOOhncYaERq6M0JMDoPLxyscwruJcb4GKS6dvK/d8xebYBg27h/duaQ=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], @@ -2354,7 +2426,7 @@ "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], - "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], + "@types/retry": ["@types/retry@0.12.2", "", {}, "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow=="], "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], @@ -2370,6 +2442,8 @@ "@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="], + "@types/tinycolor2": ["@types/tinycolor2@1.4.6", "", {}, "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -2384,25 +2458,25 @@ "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.3", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/type-utils": "8.59.3", "@typescript-eslint/utils": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.3", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/type-utils": "8.59.2", "@typescript-eslint/utils": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.3", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.3", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.3", "@typescript-eslint/types": "^8.59.3", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3" } }, "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2" } }, "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.3", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/utils": "8.59.3", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.3", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.3", "@typescript-eslint/tsconfig-utils": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="], "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260220.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260220.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260220.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260220.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260220.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260220.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260220.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260220.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-trYXlG98/C7Q7pqnPrKo+ksXrWqWVMncCy2x0VftD2llfL99Z//g2mpB9TmzWeKgb4d1659ESvxTowCGnzMccw=="], @@ -2480,7 +2554,7 @@ "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], - "@vercel/sdk": ["@vercel/sdk@1.21.3", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", "zod": "^3.25.0 || ^4.0.0" }, "bin": { "mcp": "bin/mcp-server.js" } }, "sha512-6C00/bvqDuYLNXU70xEwDtAANVL+h4w+BIeMvo3Dcwb2EDTVeCGYNcuv8Uhet6pc6fdslHJITAHG9dj61Fhr+w=="], + "@vercel/sdk": ["@vercel/sdk@1.21.1", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", "zod": "^3.25.0 || ^4.0.0" }, "bin": { "mcp": "bin/mcp-server.js" } }, "sha512-cpZ/urr87Zq/OUii0Tb0iye2bVyANMhq2crDwVSBeuKmLs4VAeQOVb1LGpmQW/XKAMdafg4bbsHt6fb5bv+wFw=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], @@ -2570,7 +2644,7 @@ "aggregate-error": ["aggregate-error@4.0.1", "", { "dependencies": { "clean-stack": "^4.0.0", "indent-string": "^5.0.0" } }, "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w=="], - "ai": ["ai@6.0.180", "", { "dependencies": { "@ai-sdk/gateway": "3.0.114", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tOyRbwD0PEjMZKGvYQcTsv95K2zktwwNhQ49QOUAh0g8MNprO7ELIO1SgANMuPc0BFtkP6Ny6OAdjq3TtxLCbQ=="], + "ai": ["ai@6.0.177", "", { "dependencies": { "@ai-sdk/gateway": "3.0.112", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-1xQtbeWwNcLyyM86ixZhkKvT+WRXc1lvarIKqPVtsyn8F9NDikwUMBqYu+aQKDgMht50SMXh4qboYuU8MeHZZA=="], "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], @@ -2704,7 +2778,7 @@ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="], @@ -2738,7 +2812,7 @@ "better-result": ["better-result@2.9.2", "", {}, "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q=="], - "better-sqlite3": ["better-sqlite3@12.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ=="], + "better-sqlite3": ["better-sqlite3@12.9.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ=="], "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], @@ -2748,7 +2822,7 @@ "bintrees": ["bintrees@1.0.2", "", {}, "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw=="], - "bippy": ["bippy@0.5.40", "", { "peerDependencies": { "react": ">=17.0.1" } }, "sha512-3QPSDG5tgd7FCIkcKsUqmbGdlHxBxP7II5drXPNp1I0rpA9+4+/VjQOigDTbzeqTi6Xkaf1Dq7x4E8vbOuwOkA=="], + "bippy": ["bippy@0.5.39", "", { "peerDependencies": { "react": ">=17.0.1" } }, "sha512-8hE8rKSl8JWyeaY+JjpnmceWAZPpLEyzOZQpWXM5Rc7861c5WotMJHy2aRZKZrGA8nMpvLNF01t4yQQ+HcZG3w=="], "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], @@ -2760,7 +2834,7 @@ "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], - "brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -2780,9 +2854,9 @@ "builtins": ["builtins@5.1.0", "", { "dependencies": { "semver": "^7.0.0" } }, "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg=="], - "bullmq": ["bullmq@5.76.8", "", { "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", "msgpackr": "2.0.1", "node-abort-controller": "3.1.1", "semver": "7.8.0", "tslib": "2.8.1" } }, "sha512-v3WTwA8diFtsADaJ8eK2ozyi2CYK9rDZCeoKF+dIPF/MUL8HxAOa3H72Gmu1lC4yKlho6t1PwNr/QpDVqaNEZQ=="], + "bullmq": ["bullmq@5.76.6", "", { "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", "msgpackr": "2.0.1", "node-abort-controller": "3.1.1", "semver": "7.7.4", "tslib": "2.8.1" } }, "sha512-vlmL3B3NVMRy6se3c7jPHn1Nhqxrg7+wlv1t3XAQFBYZNJDMLP0OO5x2AX5ca7DAuS1SU/C+VfYi+NHVoFK1QQ=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -2850,9 +2924,11 @@ "chevrotain": ["chevrotain@10.5.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "10.5.0", "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "@chevrotain/utils": "10.5.0", "lodash": "4.17.21", "regexp-to-ast": "0.5.0" } }, "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A=="], - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "chevrotain-allstar": ["chevrotain-allstar@0.4.3", "", { "dependencies": { "lodash-es": "^4.18.1" }, "peerDependencies": { "chevrotain": "^12.0.0" } }, "sha512-2X4mkroolSMKqW+H22pyPMUVDqYZzPhephTmg/NODKb1IGYPHfxfhcW0EjS7wcPJNbze2i4vBWT7zT5FKF2lrQ=="], - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], @@ -2864,6 +2940,8 @@ "ci-parallel-vars": ["ci-parallel-vars@1.0.1", "", {}, "sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg=="], + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], @@ -2982,6 +3060,8 @@ "cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="], + "cronstrue": ["cronstrue@2.61.0", "", { "bin": { "cronstrue": "bin/cli.js" } }, "sha512-ootN5bvXbIQI9rW94+QsXN5eROtXWwew6NkdGxIRpS/UFWRggL0G5Al7a9GTBFEsuvVhJ2K3CntIIVt7L2ILhA=="], + "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], @@ -3104,7 +3184,7 @@ "debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decamelize": ["decamelize@4.0.0", "", {}, "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ=="], @@ -3136,6 +3216,8 @@ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], + "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], @@ -3166,7 +3248,7 @@ "dependency-graph": ["dependency-graph@0.11.0", "", {}, "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg=="], - "dependency-tree": ["dependency-tree@11.5.0", "", { "dependencies": { "@discoveryjs/json-ext": "^1.1.0", "commander": "^12.1.0", "filing-cabinet": "^5.5.1", "precinct": "^12.3.2", "typescript": "^5.9.3" }, "bin": { "dependency-tree": "bin/cli.js" } }, "sha512-K9zBwKDZrot3RkxizugpVSdImxULAg4Ycp3+ydy2r561k96oiiw6nfsOR15fwNDQ5BF2UXe+2JFM/H5Xz4MGQg=="], + "dependency-tree": ["dependency-tree@11.4.3", "", { "dependencies": { "commander": "^12.1.0", "filing-cabinet": "^5.3.0", "precinct": "^12.3.1", "typescript": "^5.9.3" }, "bin": { "dependency-tree": "bin/cli.js" } }, "sha512-Y2gzOJ2Rb2X7MN6pT9llWpXxl5J5s5/11CBpJ5b85DjEqZH7jv3T9RO6HRV/PI/3MDmaKn/g7uoYdYmSb9vLlw=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -3228,7 +3310,7 @@ "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - "dompurify": ["dompurify@3.4.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-VVwJidIJcp1hpg2OMXML3ZVRPYSZiq4aX7qBh83BSIpOaRDqI+qxhXjjIWnpzkOXhmp0L81lnoME1mnCc9H48A=="], + "dompurify": ["dompurify@3.4.2", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA=="], "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], @@ -3240,7 +3322,7 @@ "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], - "drizzle-orm": ["drizzle-orm@0.43.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-dUcDaZtE/zN4RV/xqGrVSMpnEczxd5cIaoDeor7Zst9wOe/HzC/7eAaulywWGYXdDEc9oBPMjayVEDg0ziTLJA=="], + "drizzle-orm": ["drizzle-orm@0.43.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-dUcDaZtE/zN4RV/xqGrVSMpnEczxd5cIaoDeor7Zst9wOe/HzC/7eAaulywWGYXdDEc9oBPMjayVEDg0ziTLJA=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -3252,14 +3334,18 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "effect": ["effect@3.21.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ=="], + "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - "electron-to-chromium": ["electron-to-chromium@1.5.354", "", {}, "sha512-JaBHwWcfIdmSAfWM5l3uwjGd431j8YEMikZ+K/2nXVuBqJKyZ0f+2h4n4JY5AyNiZmnY9qQr2RU3v9DxDmHMNg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.353", "", {}, "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w=="], "emittery": ["emittery@1.2.1", "", {}, "sha512-sFz64DCRjirhwHLxofFqxYQm6DCp6o0Ix7jwKQvuCHPn4GMRZNuBZyLPu9Ccmk/QSCAMZt6FOUqA8JZCQvA9fw=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], @@ -3268,13 +3354,13 @@ "engine.io": ["engine.io@6.6.7", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "@types/ws": "^8.5.12", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3" } }, "sha512-DgOngfDKM2EviOH3Mr9m7ks1q8roetLy/IMmYthAYzbpInMbYc/GS+fWFA3rl1gvwKVsQrVV61fo5emD1y3OJQ=="], - "engine.io-client": ["engine.io-client@6.6.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw=="], + "engine.io-client": ["engine.io-client@6.5.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.0.0" } }, "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ=="], "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], "enhance-visitors": ["enhance-visitors@1.0.0", "", { "dependencies": { "lodash": "^4.13.1" } }, "sha512-+29eJLiUixTEDRaZ35Vu8jP3gPLNcQQkQkOQjLp2X+6cZGGPDD/uasbFzvLsJKnGZnvmyZ0srxudwOtskHeIDA=="], - "enhanced-resolve": ["enhanced-resolve@5.21.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q=="], + "enhanced-resolve": ["enhanced-resolve@5.21.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ=="], "ensure-posix-path": ["ensure-posix-path@1.1.1", "", {}, "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw=="], @@ -3348,7 +3434,7 @@ "eslint-import-resolver-webpack": ["eslint-import-resolver-webpack@0.13.11", "", { "dependencies": { "debug": "^3.2.7", "enhanced-resolve": "^0.9.1", "find-root": "^1.1.0", "hasown": "^2.0.2", "interpret": "^1.4.0", "is-core-module": "^2.16.1", "is-regex": "^1.2.1", "lodash": "^4.18.1", "resolve": "^2.0.0-next.6", "semver": "^5.7.2" }, "peerDependencies": { "eslint-plugin-import": ">=1.4.0", "webpack": ">=1.11.0" } }, "sha512-RGFDrCHSmCKGuaoI1zmZT028weIFIEyfSy0nAwzp5rplutWDC+BBjvZS2l4bEgSOfjc+ILkSLxeszkslyNO6fQ=="], - "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" }, "peerDependencies": { "eslint": "*" }, "optionalPeers": ["eslint"] }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], "eslint-plugin-ava": ["eslint-plugin-ava@13.2.0", "", { "dependencies": { "enhance-visitors": "^1.0.0", "eslint-utils": "^3.0.0", "espree": "^9.0.0", "espurify": "^2.1.1", "import-modules": "^2.1.0", "micro-spelling-correcter": "^1.1.1", "pkg-dir": "^5.0.0", "resolve-from": "^5.0.0" }, "peerDependencies": { "eslint": ">=7.22.0" } }, "sha512-i5B5izsEdERKQLruk1nIWzTTE7C26/ju8qQf7JeyRv32XT2lRMW0zMFZNhIrEf5/5VvpSz2rqrV7UcjClGbKsw=="], @@ -3364,7 +3450,7 @@ "eslint-plugin-no-use-extend-native": ["eslint-plugin-no-use-extend-native@0.5.0", "", { "dependencies": { "is-get-set-prop": "^1.0.0", "is-js-type": "^2.0.0", "is-obj-prop": "^1.0.0", "is-proto-prop": "^2.0.0" } }, "sha512-dBNjs8hor8rJgeXLH4HTut5eD3RGWf9JUsadIfuL7UosVQ/dnvOKwxEcRrXrFxrMZ8llUVWT+hOimxJABsAUzQ=="], - "eslint-plugin-prettier": ["eslint-plugin-prettier@4.2.5", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0" }, "peerDependencies": { "eslint": ">=7.28.0", "prettier": ">=2.0.0" } }, "sha512-9Ni+xgemM2IWLq6aXEpP2+V/V30GeA/46Ar629vcMqVPodFFWC9skHu/D1phvuqtS8bJCFnNf01/qcmqYEwNfg=="], + "eslint-plugin-prettier": ["eslint-plugin-prettier@4.2.5", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0" }, "peerDependencies": { "eslint": ">=7.28.0", "eslint-config-prettier": "*", "prettier": ">=2.0.0" }, "optionalPeers": ["eslint-config-prettier"] }, "sha512-9Ni+xgemM2IWLq6aXEpP2+V/V30GeA/46Ar629vcMqVPodFFWC9skHu/D1phvuqtS8bJCFnNf01/qcmqYEwNfg=="], "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], @@ -3414,6 +3500,8 @@ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "event-target-polyfill": ["event-target-polyfill@0.0.4", "", {}, "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ=="], + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], @@ -3426,13 +3514,15 @@ "eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], + "evt": ["evt@2.5.9", "", { "dependencies": { "minimal-polyfills": "^2.2.3", "run-exclusive": "^2.2.19", "tsafe": "^1.8.5" } }, "sha512-GpjX476FSlttEGWHT8BdVMoI8wGXQGbEOtKcP4E+kggg+yJzXBZN2n4x7TS/zPBJ1DZqWI+rguZZApjjzQ0HpA=="], + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], "expr-eval-fork": ["expr-eval-fork@3.0.3", "", {}, "sha512-BhC+hbc5lIVjygr840n5DEkW3MQq7H9o+mc1/N7Z5uIiCFVyESLL5DIE7LNq4CYUNxy+XjA+3jRrL/h0Kt2xcg=="], - "express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], + "express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], @@ -3444,6 +3534,8 @@ "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], + "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "fast-copy": ["fast-copy@4.0.3", "", {}, "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -3460,6 +3552,8 @@ "fast-memoize": ["fast-memoize@2.5.2", "", {}, "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw=="], + "fast-npm-meta": ["fast-npm-meta@0.2.2", "", {}, "sha512-E+fdxeaOQGo/CMWc9f4uHFfgUPJRAu7N3uB8GBvB3SDPAIWJK4GKyYhkAGFq+GYrcbKNfQIz5VVQyJnDuPPCrg=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], @@ -3526,7 +3620,7 @@ "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], - "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], + "follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], @@ -3620,6 +3714,8 @@ "giget": ["giget@3.2.0", "", { "bin": { "giget": "dist/cli.mjs" } }, "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A=="], + "git-last-commit": ["git-last-commit@1.0.1", "", {}, "sha512-FDSgeMqa7GnJDxt/q0AbrxbfeTyxp4ImxEw1e4nw6NUHA5FMhFUq33dTXI4Xdgcj1VQ1q5QLWF6WxFrJ8KCBOg=="], + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], @@ -3648,6 +3744,8 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "gradient-string": ["gradient-string@2.0.2", "", { "dependencies": { "chalk": "^4.1.2", "tinygradient": "^1.1.5" } }, "sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw=="], + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], "graphql": ["graphql@16.14.0", "", {}, "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q=="], @@ -3662,7 +3760,7 @@ "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="], "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], @@ -3768,6 +3866,8 @@ "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + "humanize-duration": ["humanize-duration@3.33.2", "", {}, "sha512-K7Ny/ULO1hDm2nnhvAY+SJV1skxFb61fd073SG1IWJl+D44ULrruCuTyjHKjBVVcSuTlnY99DKtgEG39CM5QOQ=="], + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], @@ -3780,7 +3880,7 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "ignore-by-default": ["ignore-by-default@1.0.1", "", {}, "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="], @@ -3788,7 +3888,7 @@ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - "import-in-the-middle": ["import-in-the-middle@1.15.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA=="], + "import-in-the-middle": ["import-in-the-middle@1.11.0", "", { "dependencies": { "acorn": "^8.8.2", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q=="], "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], @@ -3802,7 +3902,7 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "ini": ["ini@5.0.0", "", {}, "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw=="], "ink": ["ink@5.2.1", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.22.0", "indent-string": "^5.0.0", "is-in-ci": "^1.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.29.0", "scheduler": "^0.23.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=18.0.0", "react": ">=18.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg=="], @@ -4038,14 +4138,18 @@ "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "json-stable-stringify": ["json-stable-stringify@1.3.0", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" } }, "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg=="], + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + "jsonc-parser": ["jsonc-parser@3.2.1", "", {}, "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA=="], "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "jsonify": ["jsonify@0.0.1", "", {}, "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg=="], + "jsonpath-plus": ["jsonpath-plus@10.4.0", "", { "dependencies": { "@jsep-plugin/assignment": "^1.3.0", "@jsep-plugin/regex": "^1.0.4", "jsep": "^1.4.0" }, "bin": { "jsonpath": "bin/jsonpath-cli.js", "jsonpath-plus": "bin/jsonpath-cli.js" } }, "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA=="], "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], @@ -4072,7 +4176,7 @@ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], - "knip": ["knip@6.13.1", "", { "dependencies": { "fdir": "^6.5.0", "formatly": "^0.3.0", "get-tsconfig": "4.14.0", "jiti": "^2.7.0", "minimist": "^1.2.8", "oxc-parser": "^0.130.0", "oxc-resolver": "^11.19.1", "picomatch": "^4.0.4", "smol-toml": "^1.6.1", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.16", "unbash": "^3.0.0", "yaml": "^2.9.0", "zod": "^4.1.11" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-hvSnb+YDpDWW1LXub4U0JFfkQhscwgInWuQOv99WTutPZavf1cEP3GwxzEzO2JJpGI9yATk6l0jPLY1V3fp1sQ=="], + "knip": ["knip@6.12.2", "", { "dependencies": { "fdir": "^6.5.0", "formatly": "^0.3.0", "get-tsconfig": "4.14.0", "jiti": "^2.7.0", "minimist": "^1.2.8", "oxc-parser": "^0.128.0", "oxc-resolver": "^11.19.1", "picomatch": "^4.0.4", "smol-toml": "^1.6.1", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.16", "unbash": "^3.0.0", "yaml": "^2.8.2", "zod": "^4.1.11" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-RcZpT1sVziKZgDk1F0hAcp+bq71VJAF8vg1Y9ZLXc1+UXQaMm1rjiUqpJQTIj+lqwmiBQT19/u7ikgazs23cvA=="], "ksuid": ["ksuid@3.0.0", "", { "dependencies": { "base-convert-int-array": "^1.0.1" } }, "sha512-81CkBGn/06ZVAjGvFZi6fVG8VcPeMH0JpJ4V1Z9VwrMMaGIeAjY4jrVdrIcxhL9I2ZUU6t5uiyswcmkk+KZegA=="], @@ -4080,6 +4184,8 @@ "langchain": ["langchain@1.4.0", "", { "dependencies": { "@langchain/langgraph": "^1.3.0", "@langchain/langgraph-checkpoint": "^1.0.1", "langsmith": ">=0.5.0 <1.0.0", "zod": "^3.25.76 || ^4" }, "peerDependencies": { "@langchain/core": "^1.1.44" } }, "sha512-p3H5U1vfO0T4ri/xxqI6jccRP3LYmOW6KGxaJcwI5mIGVR/G6eNhZyjSZB9d7me6J7an4Pc6zA8tH8Qa6/7xwA=="], + "langium": ["langium@4.2.3", "", { "dependencies": { "@chevrotain/regexp-to-ast": "~12.0.0", "chevrotain": "~12.0.0", "chevrotain-allstar": "~0.4.3", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-sOPIi4hISFnY7twwV97ca1TsxpBtXq0URu/LL1AvxwccPG/RIBBlKS7a/f/EL6w8lTNaS0EFs/F+IdSOaqYpng=="], + "langsmith": ["langsmith@0.6.3", "", { "dependencies": { "p-queue": "6.6.2" }, "peerDependencies": { "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", "openai": "*", "ws": ">=7" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-proto", "@opentelemetry/sdk-trace-base", "openai", "ws"] }, "sha512-pXrQ4/4myQvjFFOAUmt5pWRrLEZR20gzIJD7MNdUH+5/S5nLI4ZRBo/SYKC6coaYj9pYTfQdBIzcs+3kfJ5uDA=="], "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], @@ -4096,7 +4202,7 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - "libphonenumber-js": ["libphonenumber-js@1.13.1", "", {}, "sha512-GEw0GLL7YUUA6nv21IsCvVjtI5Ejn84sjbdfQ9KxdbqEVOk1PZh7xejn01EEiniKw+dBeCfim+8MGeuvVuE2BA=="], + "libphonenumber-js": ["libphonenumber-js@1.13.0", "", {}, "sha512-N12qmdu0BM1wVNkMKYOoJR4fTOZDblrKNsOqGbKoUZrYsYLX2zx1O5X+vhK0WJPBU/+/kh9tCr8x0a7t1puGWg=="], "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], @@ -4202,6 +4308,8 @@ "magic-string": ["magic-string@0.30.8", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ=="], + "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], + "mailchecker": ["mailchecker@6.0.20", "", {}, "sha512-mZ3kmtfXzGj06prtNm6d8an7D++Kf1G4jEkPZ1QQyhknYNLkmGoMtfaNPNHJU6E8J+Bm3AcZlIIfq5D6L4MS2g=="], "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], @@ -4220,6 +4328,8 @@ "matcher-collection": ["matcher-collection@1.1.2", "", { "dependencies": { "minimatch": "^3.0.2" } }, "sha512-YQ/teqaOIIfUHedRam08PB3NK7Mjct6BvzRnJmpGDm8uFXpNr1sbY4yuflI5JcEs6COpYA0FpRQhSDBf1tT95g=="], + "matchit": ["matchit@1.1.0", "", { "dependencies": { "@arr/every": "^1.0.0" } }, "sha512-+nGYoOlfHmxe5BW5tE0EMJppXEwdSf8uBA1GTZC7Q77kbT35+VKLYJMzVNWCHSsga1ps1tPYFtFyvxvKzWVmMA=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "md-to-react-email": ["md-to-react-email@5.0.6", "", { "dependencies": { "marked": "7.0.4" }, "peerDependencies": { "react": "^18.0 || ^19.0" } }, "sha512-lKaTJqpmO88JdE3FnE7V1dtS+ta0ORyKTRhk/YoCRmeE9LozyfR2ashnUW0p0hK13VnF2TqZURXtSwgyh0zZkA=="], @@ -4228,7 +4338,7 @@ "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], - "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="], @@ -4276,7 +4386,7 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - "mermaid": ["mermaid@11.15.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "es-toolkit": "^1.45.1", "katex": "^0.16.25", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw=="], + "mermaid": ["mermaid@11.14.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g=="], "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], @@ -4380,7 +4490,9 @@ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], - "minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + "minimal-polyfills": ["minimal-polyfills@2.2.3", "", {}, "sha512-oxdmJ9cL+xV72h0xYxp4tP2d5/fTBpP45H8DIOn9pASuF8a3IYTf+25fMGDYGiWW+MFsuog6KD6nfmhZJQ+uUw=="], + + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -4388,9 +4500,9 @@ "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], - "mintlify": ["mintlify@4.2.560", "", { "dependencies": { "@mintlify/cli": "4.0.1163" }, "bin": { "mintlify": "index.js" } }, "sha512-oMbZtx3JKS3T30G29OPQUFWwaG62Ekrq8zrqjoBU3gY6YL1mdPj7ZlRAbLiAlwGuTYdqrmuyr9Nc//QpjTuOUw=="], + "mintlify": ["mintlify@4.2.558", "", { "dependencies": { "@mintlify/cli": "4.0.1161" }, "bin": { "mintlify": "index.js" } }, "sha512-VSjQAAwC8tJZ+sGA+qjBqE8zLAnUPZv1ZTLNSLpLMpCgQa62bI7cxXK1vnsQiXKRNTuMmIjmE22W8R4kacx7mA=="], "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], @@ -4426,7 +4538,7 @@ "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], - "msw": ["msw@2.14.6", "", { "dependencies": { "@inquirer/confirm": "^6.0.11", "@mswjs/interceptors": "^0.41.3", "@open-draft/deferred-promise": "^3.0.0", "@types/statuses": "^2.0.6", "cookie": "^1.1.1", "graphql": "^16.13.2", "headers-polyfill": "^5.0.1", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.11.11", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.1", "type-fest": "^5.5.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg=="], + "msw": ["msw@2.14.5", "", { "dependencies": { "@inquirer/confirm": "^6.0.11", "@mswjs/interceptors": "^0.41.3", "@open-draft/deferred-promise": "^3.0.0", "@types/statuses": "^2.0.6", "cookie": "^1.1.1", "graphql": "^16.13.2", "headers-polyfill": "^5.0.1", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.11.11", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.1", "type-fest": "^5.5.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-X6G05oX4x0e+CNI55KMdhMmwHCBKf2iwazGr+iwsdoJ94JA1ED7wSXb6V+lLPdqFkmIlPiGYvayqnaNcOzobDA=="], "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], @@ -4436,7 +4548,7 @@ "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "nan": ["nan@2.27.0", "", {}, "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ=="], + "nan": ["nan@2.26.2", "", {}, "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw=="], "nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="], @@ -4482,11 +4594,13 @@ "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], - "node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], - "node-releases": ["node-releases@2.0.44", "", {}, "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ=="], + "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], "node-source-walk": ["node-source-walk@7.0.2", "", { "dependencies": { "@babel/parser": "^7.29.0" } }, "sha512-71kFFjYaSshDTA8/a2HiTYPLdASWjLJxUyJxGE+ffxU+KhxSBtM9kiLUX+R2yooFdSFKMFpi4n3PFtDy6qXv8A=="], @@ -4508,13 +4622,15 @@ "nuqs": ["nuqs@2.8.9", "", { "dependencies": { "@standard-schema/spec": "1.0.0" }, "peerDependencies": { "@remix-run/react": ">=2", "@tanstack/react-router": "^1", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", "react-router": "^5 || ^6 || ^7", "react-router-dom": "^5 || ^6 || ^7" }, "optionalPeers": ["@remix-run/react", "@tanstack/react-router", "next", "react-router", "react-router-dom"] }, "sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ=="], + "nypm": ["nypm@0.5.4", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "tinyexec": "^0.3.2", "ufo": "^1.5.4" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA=="], + "oauth4webapi": ["oauth4webapi@3.8.6", "", {}, "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ=="], "obj-props": ["obj-props@1.4.0", "", {}, "sha512-p7p/7ltzPDiBs6DqxOrIbtRdwxxVRBj5ROukeNb9RgA+fawhrz5n2hpNz8DDmYR//tviJSj7nUnlppGmONkjiQ=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -4540,7 +4656,7 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], @@ -4562,11 +4678,13 @@ "ora": ["ora@4.1.1", "", { "dependencies": { "chalk": "^3.0.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.2.0", "is-interactive": "^1.0.0", "log-symbols": "^3.0.0", "mute-stream": "0.0.8", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-sjYP8QyVWBpBZWD6Vr1M/KwknSw6kJOz41tvGMlwWeClHBtYKTbHMki1PsLZnxKpXMPbTKv9b3pjQu3REib96A=="], + "os-paths": ["os-paths@7.4.0", "", { "optionalDependencies": { "fsevents": "*" } }, "sha512-Ux1J4NUqC6tZayBqLN1kUlDAEvLiQlli/53sSddU4IN+h+3xxnv2HmRSMpVSvr1hvJzotfMs3ERvETGK+f4OwA=="], + "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], - "oxc-parser": ["oxc-parser@0.130.0", "", { "dependencies": { "@oxc-project/types": "^0.130.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.130.0", "@oxc-parser/binding-android-arm64": "0.130.0", "@oxc-parser/binding-darwin-arm64": "0.130.0", "@oxc-parser/binding-darwin-x64": "0.130.0", "@oxc-parser/binding-freebsd-x64": "0.130.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.130.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.130.0", "@oxc-parser/binding-linux-arm64-gnu": "0.130.0", "@oxc-parser/binding-linux-arm64-musl": "0.130.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.130.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.130.0", "@oxc-parser/binding-linux-riscv64-musl": "0.130.0", "@oxc-parser/binding-linux-s390x-gnu": "0.130.0", "@oxc-parser/binding-linux-x64-gnu": "0.130.0", "@oxc-parser/binding-linux-x64-musl": "0.130.0", "@oxc-parser/binding-openharmony-arm64": "0.130.0", "@oxc-parser/binding-wasm32-wasi": "0.130.0", "@oxc-parser/binding-win32-arm64-msvc": "0.130.0", "@oxc-parser/binding-win32-ia32-msvc": "0.130.0", "@oxc-parser/binding-win32-x64-msvc": "0.130.0" } }, "sha512-X0PJ+NmOok8qP3vK9uaW431ngkdM9UPEK7KG466urtIL2+EYTEgbZK2yqe2MWKJKBjRlFweP/pJPx0x9muMEVw=="], + "oxc-parser": ["oxc-parser@0.128.0", "", { "dependencies": { "@oxc-project/types": "^0.128.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.128.0", "@oxc-parser/binding-android-arm64": "0.128.0", "@oxc-parser/binding-darwin-arm64": "0.128.0", "@oxc-parser/binding-darwin-x64": "0.128.0", "@oxc-parser/binding-freebsd-x64": "0.128.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.128.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.128.0", "@oxc-parser/binding-linux-arm64-gnu": "0.128.0", "@oxc-parser/binding-linux-arm64-musl": "0.128.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.128.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.128.0", "@oxc-parser/binding-linux-riscv64-musl": "0.128.0", "@oxc-parser/binding-linux-s390x-gnu": "0.128.0", "@oxc-parser/binding-linux-x64-gnu": "0.128.0", "@oxc-parser/binding-linux-x64-musl": "0.128.0", "@oxc-parser/binding-openharmony-arm64": "0.128.0", "@oxc-parser/binding-wasm32-wasi": "0.128.0", "@oxc-parser/binding-win32-arm64-msvc": "0.128.0", "@oxc-parser/binding-win32-ia32-msvc": "0.128.0", "@oxc-parser/binding-win32-x64-msvc": "0.128.0" } }, "sha512-XkOw3eiIxAgQ19WRew/Bq9wc5Ga/guaWIzDBzq80z1PyuDNGvWBpPby9k6YGwV8A8uMw+Nlq3xqlzuDYmUFYUw=="], "oxc-resolver": ["oxc-resolver@11.19.1", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.19.1", "@oxc-resolver/binding-android-arm64": "11.19.1", "@oxc-resolver/binding-darwin-arm64": "11.19.1", "@oxc-resolver/binding-darwin-x64": "11.19.1", "@oxc-resolver/binding-freebsd-x64": "11.19.1", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.19.1", "@oxc-resolver/binding-linux-arm-musleabihf": "11.19.1", "@oxc-resolver/binding-linux-arm64-gnu": "11.19.1", "@oxc-resolver/binding-linux-arm64-musl": "11.19.1", "@oxc-resolver/binding-linux-ppc64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-musl": "11.19.1", "@oxc-resolver/binding-linux-s390x-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-musl": "11.19.1", "@oxc-resolver/binding-openharmony-arm64": "11.19.1", "@oxc-resolver/binding-wasm32-wasi": "11.19.1", "@oxc-resolver/binding-win32-arm64-msvc": "11.19.1", "@oxc-resolver/binding-win32-ia32-msvc": "11.19.1", "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, "sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg=="], @@ -4588,7 +4706,7 @@ "p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], - "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], + "p-retry": ["p-retry@6.2.1", "", { "dependencies": { "@types/retry": "0.12.2", "is-network-error": "^1.0.0", "retry": "^0.13.1" } }, "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ=="], "p-some": ["p-some@6.0.0", "", { "dependencies": { "aggregate-error": "^4.0.0", "p-cancelable": "^3.0.0" } }, "sha512-CJbQCKdfSX3fIh8/QKgS+9rjm7OBNUTmwWswAFQAhc8j1NR1dsEDETUEuVUtQHZpV+J03LqWBEwvu0g1Yn+TYg=="], @@ -4626,6 +4744,8 @@ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "partysocket": ["partysocket@1.1.18", "", { "dependencies": { "event-target-polyfill": "^0.0.4" }, "peerDependencies": { "react": ">=17" }, "optionalPeers": ["react"] }, "sha512-SyuvH9VavWOSa14v6dYdp3yfSUDII4BQB1+TkGOFBkjfZKjnDBiba4fhdhwBlqGBkqw4ea3gTA1DYhSffX24Wg=="], + "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], @@ -4696,11 +4816,11 @@ "pkg-dir": ["pkg-dir@5.0.0", "", { "dependencies": { "find-up": "^5.0.0" } }, "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA=="], - "pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], "playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="], - "playwright-core": ["playwright-core@1.60.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="], + "playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], "plimit-lit": ["plimit-lit@1.6.1", "", { "dependencies": { "queue-lit": "^1.5.1" } }, "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA=="], @@ -4714,6 +4834,8 @@ "polite-json": ["polite-json@5.0.0", "", {}, "sha512-OLS/0XeUAcE8a2fdwemNja+udKgXNnY6yKVIXqAD2zVRx1KvY6Ato/rZ2vdzbxqYwPW0u6SCNC/bAMPNzpzxbw=="], + "polka": ["polka@0.5.2", "", { "dependencies": { "@polka/url": "^0.5.0", "trouter": "^2.0.1" } }, "sha512-FVg3vDmCqP80tOrs+OeNlgXYmFppTXdjD5E7I4ET1NjvtNmQrb1/mJibybKkb/d4NA7YWAr1ojxuhpL3FHqdlw=="], + "pony-cause": ["pony-cause@1.1.1", "", {}, "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], @@ -4744,9 +4866,9 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "posthog-js": ["posthog-js@1.373.4", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.208.0", "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", "@posthog/core": "1.29.1", "@posthog/types": "1.373.4", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.28.2", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.1.0" } }, "sha512-6DueUS5KOGZlKsQlelLURmtp9PA52rdCxt/IvuoYKAErlkh2LczogMopUIpeqbwfOnGDQEf8gmDh4z/IFeU4CQ=="], + "posthog-js": ["posthog-js@1.372.10", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.208.0", "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", "@posthog/core": "1.28.4", "@posthog/types": "1.372.10", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.28.2", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.1.0" } }, "sha512-ZQslIenDM8UpwKhmeeEnJ+t2nXr8mOIjCG+Ej3DCJnTBk9NX9Sr5RMuwHeGG8UJitwvtGOADyiY7DignOXaZwg=="], - "posthog-node": ["posthog-node@5.34.1", "", { "dependencies": { "@posthog/core": "1.29.1" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-kGl0kSfh2+Ey3KL5Sji3yv9W5xwPK9sTkINRoFqCh9fbYXWWY6Zwi5Psv2QmRcbYiMJBk/iecnoOKVDRRga6PA=="], + "posthog-node": ["posthog-node@5.33.4", "", { "dependencies": { "@posthog/core": "1.28.4" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-kK6nepnkSStbbxWxB2ZFhKxE2nw9tSHLv/3tUKcypwzKfovp8e8icsndBVf9RTHxiXBiF1M3B13tWvjBT2UWBw=="], "powershell-utils": ["powershell-utils@0.2.0", "", {}, "sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw=="], @@ -4784,7 +4906,7 @@ "proto-props": ["proto-props@2.0.0", "", {}, "sha512-2yma2tog9VaRZY2mn3Wq51uiSW4NcPYT1cQdBagwyrznrilKSZwIZ0UG3ZPL/mx+axEns0hE35T5ufOYZXEnBQ=="], - "protobufjs": ["protobufjs@7.5.8", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA=="], + "protobufjs": ["protobufjs@7.5.7", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-NGnrxS/nLKUo5nkbVQxlC71sB4hdfImdYIbFeSCidxtwATx0AHRPcANSLd0q5Bb2BkoSWo2iisQhGg5/r+ihbA=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -4802,7 +4924,9 @@ "puppeteer": ["puppeteer@22.14.0", "", { "dependencies": { "@puppeteer/browsers": "2.3.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1312386", "puppeteer-core": "22.14.0" }, "bin": { "puppeteer": "lib/esm/puppeteer/node/cli.js" } }, "sha512-MGTR6/pM8zmWbTdazb6FKnwIihzsSEXBPH49mFFU96DNZpQOevCAZMnjBZGlZRGRzRK6aADCavR6SQtrbv5dQw=="], - "puppeteer-core": ["puppeteer-core@24.43.1", "", { "dependencies": { "@puppeteer/browsers": "2.13.2", "chromium-bidi": "14.0.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1608973", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.1", "ws": "^8.20.0" } }, "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw=="], + "puppeteer-core": ["puppeteer-core@24.43.0", "", { "dependencies": { "@puppeteer/browsers": "2.13.1", "chromium-bidi": "14.0.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1608973", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.1", "ws": "^8.20.0" } }, "sha512-cCRNXsUlhyPoKDz6+TiSpfZpRS3mD6Y1YFKhkdr6ik6TMfuJb7fAtXq9ThUFc4sphxObDk3BuAvdxc1Y6YOnqQ=="], + + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], @@ -4844,7 +4968,7 @@ "react-email": ["react-email@4.0.16", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/traverse": "^7.27.0", "chalk": "^5.0.0", "chokidar": "^4.0.3", "commander": "^13.0.0", "debounce": "^2.0.0", "esbuild": "^0.25.0", "glob": "^11.0.0", "log-symbols": "^7.0.0", "mime-types": "^3.0.0", "next": "^15.3.1", "normalize-path": "^3.0.0", "ora": "^8.0.0", "socket.io": "^4.8.1" }, "bin": { "email": "dist/cli/index.mjs" } }, "sha512-auhFU+nQxAkKkP6lQhPyGsa9exwfUEzp2BwZnjHokCwphZlg30tu4t1LgdKRwGPYsi7XNGy6asbVLAUhOVpzzg=="], - "react-grab": ["react-grab@0.1.34", "", { "dependencies": { "@react-grab/cli": "0.1.34", "bippy": "^0.5.40" }, "peerDependencies": { "react": ">=17.0.0" }, "optionalPeers": ["react"], "bin": { "react-grab": "bin/cli.js" } }, "sha512-jtdOdv0kb90oqL+pMszSh9DOLgVRaX4ZE6XN4GkDEpNNUqveQfZT014+EeJraljpqQfuWKW+96NrrRqUD93D2g=="], + "react-grab": ["react-grab@0.1.33", "", { "dependencies": { "@react-grab/cli": "0.1.33", "bippy": "^0.5.39" }, "peerDependencies": { "react": ">=17.0.0" }, "optionalPeers": ["react"], "bin": { "react-grab": "bin/cli.js" } }, "sha512-ER919JMsE4TTrb2CpEivqsIjNMSycD4HtS8v7mS3pq67U7WL1K3+C8m9AYOwW4dpuYh+EanC2eJBmfuczHJZ0A=="], "react-hotkeys-hook": ["react-hotkeys-hook@4.6.2", "", { "peerDependencies": { "react": ">=16.8.1", "react-dom": ">=16.8.1" } }, "sha512-FmP+ZriY3EG59Ug/lxNfrObCnW9xQShgk7Nb83+CkpfkcCpfS95ydv+E9JuXA5cp8KtskU7LGlIARpkc92X22Q=="], @@ -4862,7 +4986,7 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-resizable-panels": ["react-resizable-panels@4.11.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-kA4w58V6wYdRLm2rg9pzroZwGlqBLul1FjMP0J8kqTo3zSHtjeH+LXmZaldCo6+HWqs1e5hOcPoajKXdOze37Q=="], + "react-resizable-panels": ["react-resizable-panels@4.11.0", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-LPk/AkFDGkg7SsbOyL93ojrE6E7lhrxxDwnYNjfmnSeI6BE7Sje6dB24PXgZk8DeugdeXNk1LO+ohRqIjhxiLw=="], "react-router": ["react-router@7.15.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ=="], @@ -4880,7 +5004,7 @@ "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], @@ -4952,7 +5076,7 @@ "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="], - "remark-mdx": ["remark-mdx@3.1.0", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA=="], + "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], "remark-mdx-remove-esm": ["remark-mdx-remove-esm@1.3.1", "", { "dependencies": { "@types/mdast": "^4.0.4", "mdast-util-mdxjs-esm": "^2.0.1", "unist-util-remove": "^4.0.0" }, "peerDependencies": { "unified": "^11" } }, "sha512-POa8abdiuicD2e+zQkclxzJa5JEGLtV8XIOFVvisnGuw4l4xd6dfQozedwqR8JTeXQmxLebvYhlbwHoQP9RWkw=="], @@ -4980,7 +5104,7 @@ "resend": ["resend@4.8.0", "", { "dependencies": { "@react-email/render": "1.1.2" } }, "sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA=="], - "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], @@ -5030,6 +5154,8 @@ "run-async": ["run-async@4.0.6", "", {}, "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ=="], + "run-exclusive": ["run-exclusive@2.2.19", "", { "dependencies": { "minimal-polyfills": "^2.2.3" } }, "sha512-K3mdoAi7tjJ/qT7Flj90L7QyPozwUaAG+CVhkdDje4HLKXUYC3N/Jzkau3flHVDLQVhiHBtcimVodMjN9egYbA=="], + "run-jxa": ["run-jxa@3.0.0", "", { "dependencies": { "execa": "^5.1.1", "macos-version": "^6.0.0", "subsume": "^4.0.0", "type-fest": "^2.0.0" } }, "sha512-4f2CrY7H+sXkKXJn/cE6qRA3z+NMVO7zvlZ/nUV0e62yWftpiLAfw5eV9ZdomzWd2TXWwEIiGjAT57+lWIzzvA=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -5112,7 +5238,7 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], @@ -5128,6 +5254,8 @@ "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + "slug": ["slug@6.1.0", "", {}, "sha512-x6vLHCMasg4DR2LPiyFGI0gJJhywY6DTiGhCrOMzb3SOk/0JVLIaL4UhyFSHu04SD3uAavrKY/K3zZ3i6iRcgA=="], + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], @@ -5136,7 +5264,7 @@ "socket.io-adapter": ["socket.io-adapter@2.5.6", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.18.3" } }, "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ=="], - "socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="], + "socket.io-client": ["socket.io-client@4.7.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", "engine.io-client": "~6.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ=="], "socket.io-parser": ["socket.io-parser@4.2.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg=="], @@ -5188,6 +5316,8 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], @@ -5278,6 +5408,8 @@ "system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="], + "systeminformation": ["systeminformation@5.23.8", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-Osd24mNKe6jr/YoXLLK3k8TMdzaxDffhpCxgkfgBHcapykIkd50HXThM3TCEuHO2pPuCsSx2ms/SunqhU5MmsQ=="], + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], @@ -5290,7 +5422,7 @@ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tar": ["tar@6.1.15", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A=="], + "tar": ["tar@7.5.15", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ=="], "tar-fs": ["tar-fs@3.1.2", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw=="], @@ -5308,7 +5440,7 @@ "terser": ["terser@5.47.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw=="], - "terser-webpack-plugin": ["terser-webpack-plugin@5.6.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA=="], + "terser-webpack-plugin": ["terser-webpack-plugin@5.6.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "@minify-html/node": "*", "@swc/core": "*", "@swc/css": "*", "@swc/html": "*", "clean-css": "*", "cssnano": "*", "csso": "*", "esbuild": "*", "html-minifier-terser": "*", "lightningcss": "*", "postcss": "*", "uglify-js": "*", "webpack": "^5.1.0" }, "optionalPeers": ["@minify-html/node", "@swc/core", "@swc/css", "@swc/html", "clean-css", "cssnano", "csso", "esbuild", "html-minifier-terser", "lightningcss", "postcss", "uglify-js"] }, "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA=="], "text-camel-case": ["text-camel-case@1.2.10", "", { "dependencies": { "text-pascal-case": "1.2.10" } }, "sha512-KNrWeZzQT+gh73V1LnmgTkjK7V+tMRjLCc6VrGwkqbiRdnGVIWBUgIvVnvnaVCxIvZ/2Ke8DCmgPirlQcCqD3Q=="], @@ -5372,10 +5504,14 @@ "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "tinygradient": ["tinygradient@1.1.5", "", { "dependencies": { "@types/tinycolor2": "^1.4.0", "tinycolor2": "^1.0.0" } }, "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw=="], + "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], "tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="], @@ -5398,6 +5534,8 @@ "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + "trigger.dev": ["trigger.dev@4.4.6", "", { "dependencies": { "@clack/prompts": "0.11.0", "@depot/cli": "0.0.1-cli.2.80.0", "@modelcontextprotocol/sdk": "^1.25.2", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/exporter-trace-otlp-http": "0.203.0", "@opentelemetry/instrumentation": "0.203.0", "@opentelemetry/instrumentation-fetch": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-trace-node": "2.0.1", "@opentelemetry/semantic-conventions": "1.36.0", "@s2-dev/streamstore": "^0.22.5", "@trigger.dev/build": "4.4.6", "@trigger.dev/core": "4.4.6", "@trigger.dev/schema-to-json": "4.4.6", "ansi-escapes": "^7.0.0", "braces": "^3.0.3", "c12": "^1.11.1", "chalk": "^5.2.0", "chokidar": "^3.6.0", "cli-table3": "^0.6.3", "commander": "^9.4.1", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.4.5", "esbuild": "^0.23.0", "eventsource": "^3.0.2", "evt": "^2.4.13", "fast-npm-meta": "^0.2.2", "git-last-commit": "^1.0.1", "gradient-string": "^2.0.2", "has-flag": "^5.0.1", "ignore": "^7.0.5", "import-in-the-middle": "1.11.0", "import-meta-resolve": "^4.1.0", "ini": "^5.0.0", "json-stable-stringify": "^1.3.0", "jsonc-parser": "3.2.1", "magicast": "^0.3.4", "minimatch": "^10.0.1", "mlly": "^1.7.1", "nypm": "^0.5.4", "object-hash": "^3.0.0", "open": "^10.0.3", "p-limit": "^6.2.0", "p-retry": "^6.1.0", "partysocket": "^1.0.2", "pkg-types": "^1.1.3", "polka": "^0.5.2", "resolve": "^1.22.8", "semver": "^7.5.0", "signal-exit": "^4.1.0", "socket.io-client": "4.7.5", "source-map-support": "0.5.21", "std-env": "^3.7.0", "strip-ansi": "^7.1.0", "supports-color": "^10.0.0", "tar": "^7.5.13", "tiny-invariant": "^1.2.0", "tinyexec": "^0.3.1", "tinyglobby": "^0.2.10", "ws": "^8.18.0", "xdg-app-paths": "^8.3.0", "zod": "3.25.76", "zod-validation-error": "^1.5.0" }, "bin": { "trigger": "dist/esm/index.js" } }, "sha512-ceDXhGoR9uVg/Q1qmcphLmaUsh0pOPP59uGCKr11Wwe/ymnRjyo1fv1i5HRON07uoe/QVlpl/lekmfRggI+S5A=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trim-newlines": ["trim-newlines@4.1.1", "", {}, "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ=="], @@ -5406,6 +5544,8 @@ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "trouter": ["trouter@2.0.1", "", { "dependencies": { "matchit": "^1.0.0" } }, "sha512-kr8SKKw94OI+xTGOkfsvwZQ8mWoikZDd2n8XZHjJVZUARZT+4/VV6cacRS6CLsH9bNm+HFIPU1Zx4CnNnb4qlQ=="], + "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], @@ -5422,9 +5562,11 @@ "ts-to-zod": ["ts-to-zod@5.1.0", "", { "dependencies": { "@clack/prompts": "1.0.0-alpha.4", "@oclif/core": "^4.5.4", "@typescript/vfs": "^1.5.0", "chokidar": "^4.0.3", "listr2": "^9.0.4", "slash": "^5.1.0", "text-case": "^1.2.4", "tslib": "^2.3.1", "tsutils": "^3.21.0", "typescript": "^5.2.2", "zod": "^4.1.5" }, "bin": { "ts-to-zod": "bin/run" } }, "sha512-giqqlvRHunlJqG9tBL/KAO3wWIVZGF//mZiWLKm/fdQnKnz4EN2mtiK5cugN9slytBkdMEXQIaLvMzIScbhhFw=="], + "tsafe": ["tsafe@1.8.12", "", {}, "sha512-nFRqW0ttu/2o6XTXsHiVZWJBCOaxhVqZLg7dgs3coZNsCMPXPfwz+zPHAQA+70fNnVJLAPg1EgGIqK9Q84tvAw=="], + "tsc-alias": ["tsc-alias@1.8.17", "", { "dependencies": { "chokidar": "^3.5.3", "commander": "^9.0.0", "get-tsconfig": "^4.10.0", "globby": "^11.0.4", "mylas": "^2.1.9", "normalize-path": "^3.0.0", "plimit-lit": "^1.2.6" }, "bin": { "tsc-alias": "dist/bin/index.js" } }, "sha512-EIduCZHqbNwPm8BZYfq1aD7BQ697A4h6uSGMOFQfYGoQwfrYFTKwYfy9Bv42YxHkduVBcn9Zx0DkX111DKskyg=="], - "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], + "tsconfck": ["tsconfck@3.1.3", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ulNZP1SVpRDesxeMLON/LtWM8HIgAJEIVpVVhBM6gsmvQ8+Rh+ZG7FWGvHh7Ah3pRABwVJWklWCr/BTZSv0xnQ=="], "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], @@ -5466,12 +5608,14 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "typescript-eslint": ["typescript-eslint@8.59.3", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.3", "@typescript-eslint/parser": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/utils": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg=="], + "typescript-eslint": ["typescript-eslint@8.59.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="], "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + "ulid": ["ulid@2.4.0", "", { "bin": { "ulid": "bin/cli.js" } }, "sha512-fIRiVTJNcSRmXKPZtGzFQv9WRrZ3M9eoptl/teFJvjOzmpU+/K/JH6HZ8deBfb5vMEpicJcLn7JmvdknlMq7Zg=="], + "unbash": ["unbash@3.0.0", "", {}, "sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], @@ -5514,11 +5658,11 @@ "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], - "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], "unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="], - "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], @@ -5584,10 +5728,22 @@ "vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="], + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], + + "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], + + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], + + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], + + "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], + "vscode-oniguruma": ["vscode-oniguruma@2.0.1", "", {}, "sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ=="], "vscode-textmate": ["vscode-textmate@9.3.2", "", {}, "sha512-n2uGbUcrjhUEBH16uGA0TvUfhWwliFZ1e3+pTjrkim1Mt7ydB41lV08aUvsi70OlzDWp6X7Bx3w/x3fAXIsN0Q=="], + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + "walk-sync": ["walk-sync@0.2.7", "", { "dependencies": { "ensure-posix-path": "^1.0.0", "matcher-collection": "^1.0.0" } }, "sha512-OH8GdRMowEFr0XSHQeX5fGweO6zSVHo7bG/0yJQx6LAj9Oukz0C8heI3/FYectT66gY0IPGe89kOvU410/UNpg=="], "walk-up-path": ["walk-up-path@3.0.1", "", {}, "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA=="], @@ -5606,7 +5762,7 @@ "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "webpack": ["webpack@5.106.2", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.20.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "loader-runner": "^4.3.1", "mime-db": "^1.54.0", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.17", "watchpack": "^2.5.1", "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA=="], + "webpack": ["webpack@5.106.2", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.20.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "loader-runner": "^4.3.1", "mime-db": "^1.54.0", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.17", "watchpack": "^2.5.1", "webpack-sources": "^3.3.4" }, "peerDependencies": { "webpack-cli": "*" }, "optionalPeers": ["webpack-cli"], "bin": { "webpack": "bin/webpack.js" } }, "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA=="], "webpack-sources": ["webpack-sources@3.4.1", "", {}, "sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A=="], @@ -5652,15 +5808,19 @@ "write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], - "ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], + "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "xdg-app-paths": ["xdg-app-paths@8.3.0", "", { "dependencies": { "xdg-portable": "^10.6.0" }, "optionalDependencies": { "fsevents": "*" } }, "sha512-mgxlWVZw0TNWHoGmXq+NC3uhCIc55dDpAlDkMQUaIAcQzysb0kxctwv//fvuW61/nAAeUBJMQ8mnZjMmuYwOcQ=="], + + "xdg-portable": ["xdg-portable@10.6.0", "", { "dependencies": { "os-paths": "^7.4.0" }, "optionalDependencies": { "fsevents": "*" } }, "sha512-xrcqhWDvtZ7WLmt8G4f3hHy37iK7D2idtosRgkeiSPZEPmBShp0VfmRBLWAPC6zLF48APJ21yfea+RfQMF4/Aw=="], + "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], - "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], + "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.0.0", "", {}, "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="], "xo": ["xo@0.53.1", "", { "dependencies": { "@eslint/eslintrc": "^1.3.3", "@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/parser": "^5.43.0", "arrify": "^3.0.0", "cosmiconfig": "^7.1.0", "define-lazy-prop": "^3.0.0", "eslint": "^8.27.0", "eslint-config-prettier": "^8.5.0", "eslint-config-xo": "^0.43.1", "eslint-config-xo-typescript": "^0.55.0", "eslint-formatter-pretty": "^4.1.0", "eslint-import-resolver-webpack": "^0.13.2", "eslint-plugin-ava": "^13.2.0", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-n": "^15.5.1", "eslint-plugin-no-use-extend-native": "^0.5.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-unicorn": "^44.0.2", "esm-utils": "^4.1.0", "find-cache-dir": "^4.0.0", "find-up": "^6.3.0", "get-stdin": "^9.0.0", "globby": "^13.1.2", "imurmurhash": "^0.1.4", "json-stable-stringify-without-jsonify": "^1.0.1", "json5": "^2.2.1", "lodash-es": "^4.17.21", "meow": "^11.0.0", "micromatch": "^4.0.5", "open-editor": "^4.0.0", "prettier": "^2.7.1", "semver": "^7.3.8", "slash": "^5.0.0", "to-absolute-glob": "^2.0.2", "typescript": "^4.9.3" }, "bin": { "xo": "cli.js" } }, "sha512-/2R8SPehv1UhiIqJ9uSvrAjslcoygICNsUlEb/Zf2V6rMtr7YCoggc6hlt6b/kbncpR989Roqt6AvEO779dFxw=="], @@ -5670,9 +5830,9 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "yaml": ["yaml@2.8.4", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="], "yaml-js": ["yaml-js@0.3.1", "", {}, "sha512-LjoIFHCtSfkGzPsmYmfDsW+TShtQBcY7lwH1yLQ5brJHXRhjteUnVE2ThCbz1madq8JUZIAjFiavfnIFeTO8CQ=="], @@ -5700,11 +5860,13 @@ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "zod-error": ["zod-error@1.5.0", "", { "dependencies": { "zod": "^3.20.2" } }, "sha512-zzopKZ/skI9iXpqCEPj+iLCKl9b88E43ehcU+sbRoHuwGd9F1IDVGQ70TyO6kmfiRL1g4IXkjsXK+g1gLYl4WQ=="], + "zod-openapi": ["zod-openapi@5.4.6", "", { "peerDependencies": { "zod": "^3.25.74 || ^4.0.0" } }, "sha512-P2jsOOBAq/6hCwUsMCjUATZ8szkMsV5VAwZENfyxp2Hc/XPJQpVwAgevWZc65xZauCwWB9LAn7zYeiCJFAEL+A=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + "zod-validation-error": ["zod-validation-error@1.5.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-/7eFkAI4qV0tcxMBB/3+d2c1P6jzzZYdYSlBuAklzMuCrJu5bzJfHS0yVAS87dRHVlhftd6RFJDIvv03JgkSbw=="], "zustand": ["zustand@5.0.13", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ=="], @@ -5712,7 +5874,7 @@ "@antfu/install-pkg/tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], - "@antfu/ni/ansis": ["ansis@4.3.0", "", {}, "sha512-44mvgtPvohuU/70DdY5Oz2AIrLJ9k6/5x4KmoSvPwO+5Moijo0+N9D0fKbbYZQWP1hNm5CpOf+E01jhxG/r8xg=="], + "@antfu/ni/ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], "@antfu/ni/tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], @@ -5724,6 +5886,8 @@ "@artilleryio/int-core/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "@artilleryio/int-core/socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="], + "@artilleryio/int-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "@asyncapi/parser/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], @@ -5732,17 +5896,19 @@ "@asyncapi/parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "@asyncapi/parser/node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], + "@autumn/openapi/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], - "@autumn/server/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@autumn/server/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], - "@autumn/server/autumn-js": ["autumn-js@0.1.85", "", { "dependencies": { "query-string": "^9.2.2", "rou3": "^0.6.1", "swr": "^2.3.3", "zod": "^4.0.0" }, "peerDependencies": { "better-auth": "^1.3.17", "better-call": "^1.0.12", "convex": "^1.25.4" }, "optionalPeers": ["better-auth", "better-call", "convex"] }, "sha512-PDud/t8z5bDJcD7ptyHzTaoJ0A8zkxvQ4TYcJ48RtgKDdOkVY36D1T6udVLwLDnWw4J5KXwJgEuGxHdd+cuABw=="], + "@autumn/server/autumn-js": ["autumn-js@0.1.85", "", { "dependencies": { "query-string": "^9.2.2", "rou3": "^0.6.1", "swr": "^2.3.3", "zod": "^4.0.0" }, "peerDependencies": { "better-auth": "^1.3.17", "better-call": "^1.0.12", "convex": "^1.25.4", "react": "*" }, "optionalPeers": ["better-auth", "better-call", "convex", "react"] }, "sha512-PDud/t8z5bDJcD7ptyHzTaoJ0A8zkxvQ4TYcJ48RtgKDdOkVY36D1T6udVLwLDnWw4J5KXwJgEuGxHdd+cuABw=="], "@autumn/server/ink": ["ink@6.8.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.4", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^8.0.0", "stack-utils": "^2.0.6", "string-width": "^8.1.1", "terminal-size": "^4.0.1", "type-fest": "^5.4.1", "widest-line": "^6.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA=="], "@autumn/shared/@date-fns/utc": ["@date-fns/utc@2.1.0", "", {}, "sha512-176grgAgU2U303rD2/vcOmNg0kGPbhzckuH1TEP2al7n0AQipZIy9P15usd2TKQCG1g+E1jX/ZVQSzs4sUDwgA=="], - "@autumn/vite/@types/node": ["@types/node@22.19.19", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew=="], + "@autumn/vite/@types/node": ["@types/node@22.19.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ=="], "@autumn/vite/date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], @@ -5942,7 +6108,7 @@ "@better-auth/cli/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], - "@better-auth/cli/drizzle-orm": ["drizzle-orm@0.41.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="], + "@better-auth/cli/drizzle-orm": ["drizzle-orm@0.41.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="], "@better-auth/cli/yocto-spinner": ["yocto-spinner@0.2.3", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ=="], @@ -5950,9 +6116,9 @@ "@better-auth/core/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], - "@better-auth/core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@better-auth/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], - "@better-auth/dash/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], + "@better-auth/core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "@better-auth/drizzle-adapter/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], @@ -5968,7 +6134,7 @@ "@better-auth/prisma-adapter/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], - "@datadog/datadog-api-client/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@datadog/datadog-api-client/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], @@ -5976,6 +6142,8 @@ "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + "@dotenvx/dotenvx/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "@dotenvx/dotenvx/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], "@ecies/ciphers/@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], @@ -5986,6 +6154,8 @@ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "@eslint/eslintrc/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "@eslint/eslintrc/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], @@ -5994,14 +6164,14 @@ "@fortawesome/fontawesome-svg-core/@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@7.2.0", "", {}, "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw=="], + "@google/genai/p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], + + "@hono/otel/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@humanwhocodes/config-array/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "@hyperbrowser/sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "@infisical/sdk/@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.600.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.600.0", "@aws-sdk/client-sso": "3.598.0", "@aws-sdk/client-sts": "3.600.0", "@aws-sdk/credential-provider-cognito-identity": "3.600.0", "@aws-sdk/credential-provider-env": "3.598.0", "@aws-sdk/credential-provider-http": "3.598.0", "@aws-sdk/credential-provider-ini": "3.598.0", "@aws-sdk/credential-provider-node": "3.600.0", "@aws-sdk/credential-provider-process": "3.598.0", "@aws-sdk/credential-provider-sso": "3.598.0", "@aws-sdk/credential-provider-web-identity": "3.598.0", "@aws-sdk/types": "3.598.0", "@smithy/credential-provider-imds": "^3.1.1", "@smithy/property-provider": "^3.1.1", "@smithy/types": "^3.1.0", "tslib": "^2.6.2" } }, "sha512-cC9uqmX0rgx1efiJGqeR+i0EXr8RQ5SAzH7M45WNBZpYiLEe6reWgIYJY9hmOxuaoMdWSi8kekuN3IjTIORRjw=="], - "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], @@ -6016,6 +6186,10 @@ "@jridgewell/source-map/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@langchain/core/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "@langchain/core/decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + "@langchain/core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "@langchain/langgraph/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], @@ -6028,8 +6202,6 @@ "@langchain/langgraph-sdk/uuid": ["uuid@13.0.2", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw=="], - "@mermaid-js/parser/@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="], - "@mintlify/cli/@inquirer/prompts": ["@inquirer/prompts@7.9.0", "", { "dependencies": { "@inquirer/checkbox": "^4.3.0", "@inquirer/confirm": "^5.1.19", "@inquirer/editor": "^4.2.21", "@inquirer/expand": "^4.0.21", "@inquirer/input": "^4.2.5", "@inquirer/number": "^3.0.21", "@inquirer/password": "^4.0.21", "@inquirer/rawlist": "^4.1.9", "@inquirer/search": "^3.2.0", "@inquirer/select": "^4.4.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A=="], "@mintlify/cli/chalk": ["chalk@5.2.0", "", {}, "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA=="], @@ -6050,6 +6222,8 @@ "@mintlify/cli/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "@mintlify/cli/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + "@mintlify/cli/yargs": ["yargs@17.7.1", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw=="], "@mintlify/cli/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], @@ -6058,10 +6232,10 @@ "@mintlify/common/hast-util-to-html": ["hast-util-to-html@9.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA=="], - "@mintlify/common/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "@mintlify/common/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + "@mintlify/common/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + "@mintlify/common/mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="], "@mintlify/common/mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.1.3", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ=="], @@ -6070,10 +6244,16 @@ "@mintlify/common/remark-gfm": ["remark-gfm@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA=="], + "@mintlify/common/remark-mdx": ["remark-mdx@3.1.0", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA=="], + "@mintlify/common/remark-rehype": ["remark-rehype@11.1.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ=="], "@mintlify/common/tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], + "@mintlify/common/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + + "@mintlify/common/unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], + "@mintlify/link-rot/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], "@mintlify/link-rot/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], @@ -6082,7 +6262,7 @@ "@mintlify/openapi-parser/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "@mintlify/prebuild/@mintlify/scraping": ["@mintlify/scraping@4.0.756", "", { "dependencies": { "@mintlify/common": "1.0.892", "@mintlify/openapi-parser": "^0.0.8", "fs-extra": "11.1.1", "hast-util-to-mdast": "10.1.0", "js-yaml": "4.1.0", "mdast-util-mdx-jsx": "3.1.3", "neotraverse": "0.6.18", "puppeteer": "22.14.0", "rehype-parse": "9.0.1", "remark-gfm": "4.0.0", "remark-mdx": "3.0.1", "remark-parse": "11.0.0", "remark-stringify": "11.0.0", "unified": "11.0.5", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "3.24.0" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-Sm8pz053l3o0e9d32GT9298I7pdbLxkM8+QpwcE9ewlYdcQ8SWBtUlxP22pgiA8j5AbOLTcf+sCnvHs5pdUjgQ=="], + "@mintlify/prebuild/@mintlify/scraping": ["@mintlify/scraping@4.0.754", "", { "dependencies": { "@mintlify/common": "1.0.890", "@mintlify/openapi-parser": "^0.0.8", "fs-extra": "11.1.1", "hast-util-to-mdast": "10.1.0", "js-yaml": "4.1.0", "mdast-util-mdx-jsx": "3.1.3", "neotraverse": "0.6.18", "puppeteer": "22.14.0", "rehype-parse": "9.0.1", "remark-gfm": "4.0.0", "remark-mdx": "3.0.1", "remark-parse": "11.0.0", "remark-stringify": "11.0.0", "unified": "11.0.5", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "3.24.0" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-OEIWrc2hv1HBBBTOExx8QwXOIfCafqYKlqVZmk6cAuBJF6aRSvbhEHgSwySziicBftzOWSqE1iBWUFD9lDzxyQ=="], "@mintlify/prebuild/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="], @@ -6114,6 +6294,8 @@ "@mintlify/previewing/socket.io": ["socket.io@4.8.0", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA=="], + "@mintlify/previewing/tar": ["tar@6.1.15", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A=="], + "@mintlify/previewing/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], "@mintlify/previewing/yargs": ["yargs@17.7.1", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw=="], @@ -6130,14 +6312,14 @@ "@mintlify/scraping/remark-mdx": ["remark-mdx@3.0.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA=="], + "@mintlify/scraping/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + "@mintlify/scraping/yargs": ["yargs@17.7.1", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw=="], "@mintlify/scraping/zod": ["zod@3.21.4", "", {}, "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw=="], "@mintlify/validation/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - "@mintlify/validation/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - "@mintlify/validation/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], "@mintlify/validation/zod": ["zod@3.24.0", "", {}, "sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w=="], @@ -6174,14 +6356,18 @@ "@oclif/core/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - "@oclif/core/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "@oclif/core/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "@oclif/core/widest-line": ["widest-line@3.1.0", "", { "dependencies": { "string-width": "^4.0.0" } }, "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg=="], "@oclif/core/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.202.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fTBjMqKCfotFWfLzaKyhjLvyEyq5vDKTTFfBmx21btv3gvy8Lq6N5Dh2OzqeuN4DjtpSvNT1uNVfg08eD2Rfxw=="], + + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.202.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fTBjMqKCfotFWfLzaKyhjLvyEyq5vDKTTFfBmx21btv3gvy8Lq6N5Dh2OzqeuN4DjtpSvNT1uNVfg08eD2Rfxw=="], + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="], "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="], @@ -6200,20 +6386,30 @@ "@opentelemetry/exporter-zipkin/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="], + "@opentelemetry/exporter-zipkin/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-amqplib/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], "@opentelemetry/instrumentation-amqplib/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-amqplib/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-connect/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], "@opentelemetry/instrumentation-connect/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-connect/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-dataloader/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], "@opentelemetry/instrumentation-express/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], "@opentelemetry/instrumentation-express/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-express/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/instrumentation-fetch/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-fs/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], "@opentelemetry/instrumentation-fs/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], @@ -6226,60 +6422,102 @@ "@opentelemetry/instrumentation-hapi/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-hapi/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-http/@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], "@opentelemetry/instrumentation-http/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-http/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-ioredis/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-ioredis/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-kafkajs/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-kafkajs/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-knex/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-knex/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-koa/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], "@opentelemetry/instrumentation-koa/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-koa/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-lru-memoizer/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], "@opentelemetry/instrumentation-mongodb/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-mongodb/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-mongoose/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], "@opentelemetry/instrumentation-mongoose/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-mongoose/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-mysql/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-mysql/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-mysql2/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-mysql2/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-pg/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], "@opentelemetry/instrumentation-pg/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-pg/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], "@opentelemetry/instrumentation-redis/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-redis/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-tedious/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-tedious/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-undici/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], "@opentelemetry/instrumentation-undici/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], + "@opentelemetry/instrumentation-undici/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.202.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fTBjMqKCfotFWfLzaKyhjLvyEyq5vDKTTFfBmx21btv3gvy8Lq6N5Dh2OzqeuN4DjtpSvNT1uNVfg08eD2Rfxw=="], + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="], + "@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/sdk-logs/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.202.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fTBjMqKCfotFWfLzaKyhjLvyEyq5vDKTTFfBmx21btv3gvy8Lq6N5Dh2OzqeuN4DjtpSvNT1uNVfg08eD2Rfxw=="], + + "@opentelemetry/sdk-node/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.202.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fTBjMqKCfotFWfLzaKyhjLvyEyq5vDKTTFfBmx21btv3gvy8Lq6N5Dh2OzqeuN4DjtpSvNT1uNVfg08eD2Rfxw=="], + "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.202.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-exporter-base": "0.202.0", "@opentelemetry/otlp-transformer": "0.202.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-/hKE8DaFCJuaQqE1IxpgkcjOolUIwgi3TgHElPVKGdGRBSmJMTmN/cr6vWa55pCJIXPyhKvcMrbrya7DZ3VmzA=="], + "@opentelemetry/sdk-node/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.202.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.202.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Uz3BxZWPgDwgHM2+vCKEQRh0R8WKrd/q6Tus1vThRClhlPO39Dyz7mDrOr6KuqGXAlBQ1e5Tnymzri4RMZNaWA=="], + "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="], + "@opentelemetry/sdk-node/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], - "@opentelemetry/sdk-trace-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw=="], + "@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], "@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="], + "@opentelemetry/sdk-trace-web/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="], + "@opentelemetry/sql-common/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], "@orpc/openapi/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], @@ -6292,14 +6530,14 @@ "@paralleldrive/cuid2/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@playwright/browser-chromium/playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], - "@posthog/ai/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.78.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-PzQhR715td/m1UaaN5hHXjYB8Gl2lF9UVhrrGrZeysiF6Rb74Wc9GCB8hzLdzmQtBd1qe89F9OptgB9Za1Ib5w=="], "@posthog/ai/openai": ["openai@6.37.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-0H5dEGFmmLv6KSd0W1w2nyL8WsLkX6yoLeQpU+dZAOuGcany5qkYQMmj35ZrKgb6yiyYqpUzFOpR8mZQkgqeEQ=="], "@posthog/ai/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@prisma/config/c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], + "@prisma/instrumentation/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], "@radix-ui/react-accordion/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], @@ -6382,47 +6620,59 @@ "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - "@react-grab/cli/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "@react-grab/cli/@antfu/ni": ["@antfu/ni@30.1.0", "", { "dependencies": { "fzf": "^0.5.2", "package-manager-detector": "^1.6.0", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15" }, "bin": { "ni": "bin/ni.mjs", "nci": "bin/nci.mjs", "nr": "bin/nr.mjs", "nup": "bin/nup.mjs", "nd": "bin/nd.mjs", "nlx": "bin/nlx.mjs", "na": "bin/na.mjs", "nun": "bin/nun.mjs" } }, "sha512-3VuAbPjgY52rQNn4wABaXMhBU2Oq91uy6L8nX49eJ35OLI68CyckGU+HZxcaHix4ymuGM2nFL1D6sLpgODK5xw=="], + + "@react-grab/cli/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], "@react-grab/cli/ora": ["ora@9.4.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ=="], - "@react-grab/cli/tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], - "@reduxjs/toolkit/immer": ["immer@11.1.8", "", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="], - "@sentry-internal/browser-utils/@sentry/core": ["@sentry/core@10.53.1", "", {}, "sha512-XG4ezlkyuAPjBC5+9kXC94rXXuqYTw9NRhfaDHssbTFaGnqBR8vQX2UUgZfY7ucbeelRDGfBu1sywoU+mB04uA=="], + "@sentry-internal/browser-utils/@sentry/core": ["@sentry/core@10.52.0", "", {}, "sha512-VA/kAqLhkMnRWY2RXdBLyTemR9D4m7MVRy/gyapoq9yvllVPx9WXbvKgnMP2LQp7mFgT/oLFvw58aQKaYTGn3A=="], - "@sentry-internal/feedback/@sentry/core": ["@sentry/core@10.53.1", "", {}, "sha512-XG4ezlkyuAPjBC5+9kXC94rXXuqYTw9NRhfaDHssbTFaGnqBR8vQX2UUgZfY7ucbeelRDGfBu1sywoU+mB04uA=="], + "@sentry-internal/feedback/@sentry/core": ["@sentry/core@10.52.0", "", {}, "sha512-VA/kAqLhkMnRWY2RXdBLyTemR9D4m7MVRy/gyapoq9yvllVPx9WXbvKgnMP2LQp7mFgT/oLFvw58aQKaYTGn3A=="], - "@sentry-internal/replay/@sentry/core": ["@sentry/core@10.53.1", "", {}, "sha512-XG4ezlkyuAPjBC5+9kXC94rXXuqYTw9NRhfaDHssbTFaGnqBR8vQX2UUgZfY7ucbeelRDGfBu1sywoU+mB04uA=="], + "@sentry-internal/replay/@sentry/core": ["@sentry/core@10.52.0", "", {}, "sha512-VA/kAqLhkMnRWY2RXdBLyTemR9D4m7MVRy/gyapoq9yvllVPx9WXbvKgnMP2LQp7mFgT/oLFvw58aQKaYTGn3A=="], - "@sentry-internal/replay-canvas/@sentry/core": ["@sentry/core@10.53.1", "", {}, "sha512-XG4ezlkyuAPjBC5+9kXC94rXXuqYTw9NRhfaDHssbTFaGnqBR8vQX2UUgZfY7ucbeelRDGfBu1sywoU+mB04uA=="], + "@sentry-internal/replay-canvas/@sentry/core": ["@sentry/core@10.52.0", "", {}, "sha512-VA/kAqLhkMnRWY2RXdBLyTemR9D4m7MVRy/gyapoq9yvllVPx9WXbvKgnMP2LQp7mFgT/oLFvw58aQKaYTGn3A=="], - "@sentry/browser/@sentry/core": ["@sentry/core@10.53.1", "", {}, "sha512-XG4ezlkyuAPjBC5+9kXC94rXXuqYTw9NRhfaDHssbTFaGnqBR8vQX2UUgZfY7ucbeelRDGfBu1sywoU+mB04uA=="], + "@sentry/browser/@sentry/core": ["@sentry/core@10.52.0", "", {}, "sha512-VA/kAqLhkMnRWY2RXdBLyTemR9D4m7MVRy/gyapoq9yvllVPx9WXbvKgnMP2LQp7mFgT/oLFvw58aQKaYTGn3A=="], "@sentry/cli/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - "@sentry/cli/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "@sentry/cli/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "@sentry/node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.7.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ=="], + "@sentry/node/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], "@sentry/node/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], "@sentry/node/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + "@sentry/node/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@sentry/node/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], + "@sentry/node/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "@sentry/node-core/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.7.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ=="], + "@sentry/node-core/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], "@sentry/node-core/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + "@sentry/node-core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@sentry/node-core/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], + "@sentry/opentelemetry/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.7.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ=="], + "@sentry/opentelemetry/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], - "@sentry/react/@sentry/core": ["@sentry/core@10.53.1", "", {}, "sha512-XG4ezlkyuAPjBC5+9kXC94rXXuqYTw9NRhfaDHssbTFaGnqBR8vQX2UUgZfY7ucbeelRDGfBu1sywoU+mB04uA=="], + "@sentry/opentelemetry/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@sentry/react/@sentry/core": ["@sentry/core@10.52.0", "", {}, "sha512-VA/kAqLhkMnRWY2RXdBLyTemR9D4m7MVRy/gyapoq9yvllVPx9WXbvKgnMP2LQp7mFgT/oLFvw58aQKaYTGn3A=="], "@smithy/abort-controller/@smithy/types": ["@smithy/types@3.7.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg=="], @@ -6448,8 +6698,6 @@ "@stoplight/json/safe-stable-stringify": ["safe-stable-stringify@1.1.1", "", {}, "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw=="], - "@stoplight/json-ref-readers/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "@stoplight/json-ref-readers/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], "@stoplight/json-ref-resolver/immer": ["immer@9.0.21", "", {}, "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="], @@ -6468,8 +6716,6 @@ "@stoplight/spectral-parsers/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], - "@stoplight/spectral-runtime/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "@stoplight/yaml/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], "@tailwindcss/node/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -6494,53 +6740,77 @@ "@tailwindcss/vite/tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], - "@ts-morph/common/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "@tinybirdco/sdk/@clack/prompts": ["@clack/prompts@1.3.0", "", { "dependencies": { "@clack/core": "1.3.0", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-GgcWwRCs/xPtaqlMy8qRhPnZf9vlWcWZNHAitnVQ3yk7JmSralSiq5q07yaffYE8SogtDm7zFeKccx1QNVARpw=="], - "@types/body-parser/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@tinybirdco/sdk/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "@types/buffer-from/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@tinybirdco/sdk/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "@types/cacheable-request/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@trigger.dev/core/@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@types/chai-http/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@trigger.dev/core/@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.203.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-exporter-base": "0.203.0", "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/sdk-logs": "0.203.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-s0hys1ljqlMTbXx2XiplmMJg9wG570Z5lH7wMvrZX6lcODI56sG4HL03jklF63tBeyNwK2RV1/ntXGo3HgG4Qw=="], - "@types/connect/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@trigger.dev/core/@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-exporter-base": "0.203.0", "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-metrics": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HFSW10y8lY6BTZecGNpV3GpoSy7eaO0Z6GATwZasnT4bEsILp8UJXNG5OmEsz4SdwCSYvyCbTJdNbZP3/8LGCQ=="], - "@types/cors/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@trigger.dev/core/@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-exporter-base": "0.203.0", "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ZDiaswNYo0yq/cy1bBLJFe691izEJ6IgNmkjm4C6kE9ub/OMQqDXORx2D2j8fzTBTxONyzusbaZlqtfmyqURPw=="], - "@types/es-aggregate-error/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@trigger.dev/core/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.203.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-vM2+rPq0Vi3nYA5akQD2f3QwossDnTDLvKbea6u/A2NZ3XDkPxMfo/PNrDoXhDUD/0pPo2CdH5ce/thn9K0kLw=="], - "@types/express-serve-static-core/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@trigger.dev/core/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="], - "@types/keyv/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@trigger.dev/core/@s2-dev/streamstore": ["@s2-dev/streamstore@0.22.5", "", { "dependencies": { "@protobuf-ts/runtime": "^2.11.1", "debug": "^4.4.3" } }, "sha512-GqdOKIbIoIxT+40fnKzHbrsHB6gBqKdECmFe7D3Ojk4FoN1Hu0LhFzZv6ZmVMjoHHU+55debS1xSWjZwQmbIyQ=="], - "@types/mysql/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@trigger.dev/core/execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], - "@types/node-fetch/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@trigger.dev/core/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], - "@types/pg/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@trigger.dev/core/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], + + "@trigger.dev/core/socket.io": ["socket.io@4.7.4", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.5.2", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw=="], + + "@trigger.dev/sdk/@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@types/body-parser/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "@types/buffer-from/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "@types/cacheable-request/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "@types/chai-http/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "@types/connect/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "@types/cors/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "@types/es-aggregate-error/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "@types/express-serve-static-core/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "@types/keyv/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "@types/mysql/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "@types/node-fetch/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "@types/pg/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], "@types/react-syntax-highlighter/@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], - "@types/responselike/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@types/responselike/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], - "@types/send/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@types/send/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], - "@types/serve-static/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@types/serve-static/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], - "@types/set-cookie-parser/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@types/set-cookie-parser/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], - "@types/superagent/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@types/superagent/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], - "@types/tedious/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@types/tedious/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], - "@types/ws/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@types/ws/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], - "@types/yauzl/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], - - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "@types/yauzl/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], @@ -6562,6 +6832,8 @@ "aggregate-error/clean-stack": ["clean-stack@4.2.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg=="], + "ai/@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "ajv-errors/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], @@ -6572,8 +6844,6 @@ "artillery/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - "artillery/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - "artillery/csv-parse": ["csv-parse@4.16.3", "", {}, "sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg=="], "artillery/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], @@ -6584,6 +6854,8 @@ "artillery-plugin-expect/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "artillery-plugin-publish-metrics/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.7.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ=="], + "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.212.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.5.1", "@opentelemetry/exporter-metrics-otlp-http": "0.212.0", "@opentelemetry/otlp-exporter-base": "0.212.0", "@opentelemetry/otlp-grpc-exporter-base": "0.212.0", "@opentelemetry/otlp-transformer": "0.212.0", "@opentelemetry/resources": "2.5.1", "@opentelemetry/sdk-metrics": "2.5.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-/6Gqf9wpBq22XsomR1i0iPGnbQtCq2Vwnrq5oiDPjYSqveBdK1jtQbhGfmpK2mLLxk4cPDtD1ZEYdIou5K8EaA=="], "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.212.0", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-exporter-base": "0.212.0", "@opentelemetry/otlp-transformer": "0.212.0", "@opentelemetry/resources": "2.5.1", "@opentelemetry/sdk-metrics": "2.5.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-8hgBw3aTTRpSTkU4b9MLf/2YVLnfWp+hfnLq/1Fa2cky+vx6HqTodo+Zv1GTIrAKMOOwgysOjufy0gTxngqeBg=="], @@ -6602,13 +6874,15 @@ "artillery-plugin-publish-metrics/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="], + "artillery-plugin-publish-metrics/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "artillery-plugin-publish-metrics/prom-client": ["prom-client@14.2.0", "", { "dependencies": { "tdigest": "^0.1.1" } }, "sha512-sF308EhTenb/pDRPakm+WgiN+VdM/T1RaHj1x+MvAuT8UiQP8JmOEbxVqtkbfR4LrvOg5n7ic01kRBDGXjYikA=="], - "atmn/@tanstack/react-query": ["@tanstack/react-query@5.100.10", "", { "dependencies": { "@tanstack/query-core": "5.100.10" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q=="], + "atmn/@tanstack/react-query": ["@tanstack/react-query@5.100.9", "", { "dependencies": { "@tanstack/query-core": "5.100.9" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A=="], "atmn/@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], - "atmn/@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260513.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260513.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260513.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260513.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260513.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260513.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260513.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260513.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-osFAxaNZhSYIzq6tGbtTW7tk8OwoqF0d5kPAKZEFzgNd4OG8ZxARk4N19zlh/+HoSDr3V96fNcuD7++mSGptgA=="], + "atmn/@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260510.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260510.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260510.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260510.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260510.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260510.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260510.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260510.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-05U6/Im+vmqGrFAVrHSeuoXBCwShhbiA+93VpSwEBYP4LMWk2JW9q87MydamL5g6ISEjIVlwQ4Dx35CauPAwpA=="], "atmn/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], @@ -6620,7 +6894,7 @@ "atmn/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "autumn-js/@types/node": ["@types/node@22.19.19", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew=="], + "autumn-js/@types/node": ["@types/node@22.19.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ=="], "autumn-js/@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -6630,8 +6904,6 @@ "autumn-js/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "ava/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - "ava/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], "ava/cli-truncate": ["cli-truncate@3.1.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^5.0.0" } }, "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA=="], @@ -6662,12 +6934,16 @@ "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "bun-types/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "bullmq/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "bun-types/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + "c12/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], + "cacheable-request/get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], "cacheable-request/mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], @@ -6678,7 +6954,7 @@ "camelcase-keys/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], - "checkout/@tanstack/react-query": ["@tanstack/react-query@5.100.10", "", { "dependencies": { "@tanstack/query-core": "5.100.10" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q=="], + "checkout/@tanstack/react-query": ["@tanstack/react-query@5.100.9", "", { "dependencies": { "@tanstack/query-core": "5.100.9" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A=="], "checkout/@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -6690,6 +6966,10 @@ "checkout/react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "chevrotain-allstar/chevrotain": ["chevrotain@12.0.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "clean-regexp/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], "clean-stack/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], @@ -6714,8 +6994,6 @@ "cosmiconfig/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], - "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "crypto-random-string/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], @@ -6742,11 +7020,13 @@ "encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "engine.io/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "engine.io/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], "engine.io/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "engine.io-client/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "engine.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + + "engine.io-client/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -6754,6 +7034,8 @@ "eslint/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "eslint/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "eslint-config-next/eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="], @@ -6770,10 +7052,14 @@ "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-import-resolver-node/resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + "eslint-import-resolver-webpack/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-import-resolver-webpack/enhanced-resolve": ["enhanced-resolve@0.9.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "memory-fs": "^0.2.0", "tapable": "^0.1.8" } }, "sha512-kxpoMgrdtkXZ5h0SeraBS1iRntpTpQ3R8ussdb38+UAFnMGX5DDyJXePm+OCHOcoXvHDw7mc2erbJBpDnl7TPw=="], + "eslint-import-resolver-webpack/resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + "eslint-import-resolver-webpack/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -6784,6 +7070,8 @@ "eslint-plugin-eslint-comments/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + "eslint-plugin-eslint-comments/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-plugin-import/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], @@ -6796,12 +7084,14 @@ "eslint-plugin-jsx-a11y/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "eslint-plugin-n/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "eslint-plugin-n/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "eslint-plugin-n/resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - "eslint-plugin-react/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "eslint-plugin-react/resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "eslint-plugin-unicorn/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], @@ -6822,10 +7112,10 @@ "execa/pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], - "execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "express/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "favicons/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], @@ -6840,8 +7130,6 @@ "filing-cabinet/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "filing-cabinet/resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "find-cache-dir/pkg-dir": ["pkg-dir@7.0.0", "", { "dependencies": { "find-up": "^6.3.0" } }, "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA=="], @@ -6850,8 +7138,6 @@ "flat-cache/keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -6860,13 +7146,17 @@ "get-uri/data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], + "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "got/decompress-response": ["decompress-response@10.0.0", "", { "dependencies": { "mimic-response": "^4.0.0" } }, "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q=="], "got/form-data-encoder": ["form-data-encoder@4.1.0", "", {}, "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw=="], - "hast-util-to-mdast/hast-util-to-html": ["hast-util-to-html@9.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA=="], + "gradient-string/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "headers-polyfill/set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], @@ -6878,6 +7168,8 @@ "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "ink-confirm-input/ink-text-input": ["ink-text-input@3.3.0", "", { "dependencies": { "chalk": "^3.0.0", "prop-types": "^15.5.10" }, "peerDependencies": { "ink": "^2.0.0", "react": "^16.5.2" } }, "sha512-gO4wrOf2ie3YuEARTIwGlw37lMjFn3Gk6CKIDrMlHb46WFMagZU7DplohjM24zynlqfnXA5UDEIfC2NBcvD8kg=="], "ink-scroll-list/ink": ["ink@6.8.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.4", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^8.0.0", "stack-utils": "^2.0.6", "string-width": "^8.1.1", "terminal-size": "^4.0.1", "type-fest": "^5.4.1", "widest-line": "^6.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA=="], @@ -6886,6 +7178,8 @@ "ink-select-input/figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + "ink-table/object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], + "is-get-set-prop/lowercase-keys": ["lowercase-keys@1.0.1", "", {}, "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA=="], "is-number/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], @@ -6898,7 +7192,7 @@ "jake/async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - "jest-worker/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "jest-worker/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], @@ -6906,6 +7200,8 @@ "langchain/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "langium/chevrotain": ["chevrotain@12.0.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="], + "line-column-path/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], "listr2/cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], @@ -6932,18 +7228,16 @@ "minimist-options/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], - "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - - "minizlib/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "mixpanel/https-proxy-agent": ["https-proxy-agent@5.0.0", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA=="], - "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "mocha/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "mocha/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "mocha/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "mocha/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + "mocha/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "module-lookup-amd/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], @@ -6974,18 +7268,12 @@ "node-exports-info/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "nodemon/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - - "nodemon/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "nodemon/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "nuqs/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], - "onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "open-editor/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], "open-editor/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], @@ -7028,14 +7316,12 @@ "pkg-conf/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="], + "pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], - "playwright/playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], - "postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], - "postcss-import/resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - "postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], @@ -7054,7 +7340,7 @@ "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - "protobufjs/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "protobufjs/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], @@ -7074,12 +7360,16 @@ "radix-ui/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], "react-day-picker/date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], "react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "react-email/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "react-email/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], "react-email/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], @@ -7102,14 +7392,16 @@ "read-pkg-up/type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="], + "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "rehype-stringify/hast-util-to-html": ["hast-util-to-html@9.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA=="], - - "require-in-the-middle/resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - "requirejs-config-file/stringify-object": ["stringify-object@3.3.0", "", { "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", "is-regexp": "^1.0.0" } }, "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw=="], + "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "rollup/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], @@ -7126,7 +7418,7 @@ "sdk-test/@biomejs/biome": ["@biomejs/biome@2.2.0", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.0", "@biomejs/cli-darwin-x64": "2.2.0", "@biomejs/cli-linux-arm64": "2.2.0", "@biomejs/cli-linux-arm64-musl": "2.2.0", "@biomejs/cli-linux-x64": "2.2.0", "@biomejs/cli-linux-x64-musl": "2.2.0", "@biomejs/cli-win32-arm64": "2.2.0", "@biomejs/cli-win32-x64": "2.2.0" }, "bin": { "biome": "bin/biome" } }, "sha512-3On3RSYLsX+n9KnoSgfoYlckYBoU6VRM22cw1gB4Y0OuUVSYd/O/2saOJMrA4HFfA1Ff0eacOvMN1yAAvHtzIw=="], - "sdk-test/@types/node": ["@types/node@20.19.41", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ=="], + "sdk-test/@types/node": ["@types/node@20.19.40", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-xxx6M2IpSTnnKcR0cMvIiohkiCx20/oRPtWGbenFygKCGl3zqUzdNjQ/1V4solq1LU+dgv0nQzeGOuqkqZGg0Q=="], "sdk-test/@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -7160,6 +7452,8 @@ "socket.io-adapter/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "socket.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], @@ -7178,16 +7472,12 @@ "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "supports-hyperlinks/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "supports-hyperlinks/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], - - "tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], - - "tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - - "tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "tempy/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], @@ -7196,15 +7486,29 @@ "to-regex-range/is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "trigger.dev/@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "trigger.dev/@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-exporter-base": "0.203.0", "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ZDiaswNYo0yq/cy1bBLJFe691izEJ6IgNmkjm4C6kE9ub/OMQqDXORx2D2j8fzTBTxONyzusbaZlqtfmyqURPw=="], + + "trigger.dev/c12": ["c12@1.11.2", "", { "dependencies": { "chokidar": "^3.6.0", "confbox": "^0.1.7", "defu": "^6.1.4", "dotenv": "^16.4.5", "giget": "^1.2.3", "jiti": "^1.21.6", "mlly": "^1.7.1", "ohash": "^1.1.3", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", "pkg-types": "^1.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.4" }, "optionalPeers": ["magicast"] }, "sha512-oBs8a4uvSDO9dm8b7OCFW7+dgtVrwmwnrVXYzLm43ta7ep2jCn/0MhoUFygIWtxhyy6+/MG7/agvpY0U1Iemew=="], + + "trigger.dev/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], + + "trigger.dev/p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="], + + "trigger.dev/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + "ts-to-zod/@clack/prompts": ["@clack/prompts@1.0.0-alpha.4", "", { "dependencies": { "@clack/core": "1.0.0-alpha.4", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-KnmtDF2xQGoI5AlBme9akHtvCRV0RKAARUXHBQO2tMwnY8B08/4zPWigT7uLK25UPrMCEqnyQPkKRjNdhPbf8g=="], + "ts-to-zod/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "ts-to-zod/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "tsc-alias/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - "tsc-alias/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], - "tshy/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "tshy/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "tsup/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "tsutils/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], @@ -7214,8 +7518,6 @@ "unified/is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "unplugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - "webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], @@ -7226,8 +7528,6 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "write-file-atomic/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "xo/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], "xo/@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], @@ -7244,9 +7544,9 @@ "xo/@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], - "xo/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@5.62.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/type-utils": "5.62.0", "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", "semver": "^7.3.7", "tsutils": "^3.21.0" }, "peerDependencies": { "@typescript-eslint/parser": "^5.0.0", "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "bundled": true }, "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag=="], + "xo/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@5.62.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/type-utils": "5.62.0", "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", "semver": "^7.3.7", "tsutils": "^3.21.0" }, "peerDependencies": { "@typescript-eslint/parser": "^5.0.0", "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0", "typescript": "*" }, "optionalPeers": ["typescript"], "bundled": true }, "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag=="], - "xo/@typescript-eslint/parser": ["@typescript-eslint/parser@5.62.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "bundled": true }, "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA=="], + "xo/@typescript-eslint/parser": ["@typescript-eslint/parser@5.62.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0", "typescript": "*" }, "optionalPeers": ["typescript"], "bundled": true }, "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA=="], "xo/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0" } }, "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w=="], @@ -7254,7 +7554,7 @@ "xo/@typescript-eslint/types": ["@typescript-eslint/types@5.62.0", "", {}, "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ=="], - "xo/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "semver": "^7.3.7", "tsutils": "^3.21.0" } }, "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA=="], + "xo/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "semver": "^7.3.7", "tsutils": "^3.21.0" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA=="], "xo/@typescript-eslint/utils": ["@typescript-eslint/utils@5.62.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ=="], @@ -7264,7 +7564,7 @@ "xo/braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "xo/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "xo/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "xo/dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], @@ -7294,6 +7594,8 @@ "xo/graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + "xo/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "xo/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "xo/is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], @@ -7326,6 +7628,8 @@ "xo/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "xo/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "xo/to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "xo/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], @@ -7348,11 +7652,13 @@ "@artilleryio/int-core/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "@artilleryio/int-core/socket.io-client/engine.io-client": ["engine.io-client@6.6.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw=="], + "@asyncapi/parser/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "@asyncapi/parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "@autumn/server/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@autumn/server/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "@autumn/server/autumn-js/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], @@ -7368,6 +7674,8 @@ "@autumn/server/ink/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "@autumn/server/ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "@autumn/server/ink/slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], "@autumn/server/ink/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], @@ -7394,7 +7702,7 @@ "@autumn/website/eslint/espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], - "@autumn/website/eslint/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "@autumn/website/eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "@autumn/website/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -7492,7 +7800,9 @@ "@aws-sdk/signature-v4/@smithy/signature-v4/@smithy/util-utf8": ["@smithy/util-utf8@1.1.0", "", { "dependencies": { "@smithy/util-buffer-from": "^1.1.0", "tslib": "^2.5.0" } }, "sha512-p/MYV+JmqmPyjdgyN2UxAeYDj9cBqCjp0C/NsTWnnjoZUVqoeZ6IrW915L9CAKWVECgv9lVQGc4u/yz26/bI1A=="], - "@datadog/datadog-api-client/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@datadog/datadog-api-client/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], @@ -7502,6 +7812,10 @@ "@dotenvx/dotenvx/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + "@dotenvx/dotenvx/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "@dotenvx/dotenvx/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "@dotenvx/dotenvx/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], "@dotenvx/dotenvx/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], @@ -7512,6 +7826,8 @@ "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "@google/genai/p-retry/@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], + "@humanwhocodes/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], "@infisical/sdk/@aws-sdk/credential-providers/@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.600.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/client-sso-oidc": "3.600.0", "@aws-sdk/client-sts": "3.600.0", "@aws-sdk/core": "3.598.0", "@aws-sdk/credential-provider-node": "3.600.0", "@aws-sdk/middleware-host-header": "3.598.0", "@aws-sdk/middleware-logger": "3.598.0", "@aws-sdk/middleware-recursion-detection": "3.598.0", "@aws-sdk/middleware-user-agent": "3.598.0", "@aws-sdk/region-config-resolver": "3.598.0", "@aws-sdk/types": "3.598.0", "@aws-sdk/util-endpoints": "3.598.0", "@aws-sdk/util-user-agent-browser": "3.598.0", "@aws-sdk/util-user-agent-node": "3.598.0", "@smithy/config-resolver": "^3.0.2", "@smithy/core": "^2.2.1", "@smithy/fetch-http-handler": "^3.0.2", "@smithy/hash-node": "^3.0.1", "@smithy/invalid-dependency": "^3.0.1", "@smithy/middleware-content-length": "^3.0.1", "@smithy/middleware-endpoint": "^3.0.2", "@smithy/middleware-retry": "^3.0.4", "@smithy/middleware-serde": "^3.0.1", "@smithy/middleware-stack": "^3.0.1", "@smithy/node-config-provider": "^3.1.1", "@smithy/node-http-handler": "^3.0.1", "@smithy/protocol-http": "^4.0.1", "@smithy/smithy-client": "^3.1.2", "@smithy/types": "^3.1.0", "@smithy/url-parser": "^3.0.1", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", "@smithy/util-defaults-mode-browser": "^3.0.4", "@smithy/util-defaults-mode-node": "^3.0.4", "@smithy/util-endpoints": "^2.0.2", "@smithy/util-middleware": "^3.0.1", "@smithy/util-retry": "^3.0.1", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-8dYsnDLiD0rjujRiZZl0E57heUkHqMSFZHBi0YMs57SM8ODPxK3tahwDYZtS7bqanvFKZwGy+o9jIcij7jBOlA=="], @@ -7560,6 +7876,8 @@ "@mintlify/cli/ink/react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], + "@mintlify/cli/ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "@mintlify/cli/inquirer/@inquirer/prompts": ["@inquirer/prompts@7.10.1", "", { "dependencies": { "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", "@inquirer/editor": "^4.2.23", "@inquirer/expand": "^4.0.23", "@inquirer/input": "^4.3.1", "@inquirer/number": "^3.0.23", "@inquirer/password": "^4.0.23", "@inquirer/rawlist": "^4.1.11", "@inquirer/search": "^3.2.2", "@inquirer/select": "^4.4.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg=="], "@mintlify/cli/inquirer/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], @@ -7582,23 +7900,25 @@ "@mintlify/common/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@mintlify/common/mdast-util-gfm/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "@mintlify/common/mdast-util-mdx-jsx/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + "@mintlify/common/postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], - "@mintlify/common/tailwindcss/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + "@mintlify/common/remark-gfm/mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], - "@mintlify/common/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "@mintlify/common/tailwindcss/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], "@mintlify/common/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], "@mintlify/common/tailwindcss/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - "@mintlify/common/tailwindcss/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - "@mintlify/common/tailwindcss/postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], "@mintlify/common/tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], - "@mintlify/common/tailwindcss/resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + "@mintlify/common/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], "@mintlify/link-rot/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], @@ -7670,8 +7990,6 @@ "@mintlify/previewing/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "@mintlify/previewing/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - "@mintlify/previewing/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "@mintlify/previewing/express/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], @@ -7698,9 +8016,21 @@ "@mintlify/previewing/ink/react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], + "@mintlify/previewing/ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "@mintlify/previewing/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "@mintlify/previewing/socket.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + "@mintlify/previewing/socket.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + + "@mintlify/previewing/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "@mintlify/previewing/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "@mintlify/previewing/tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "@mintlify/previewing/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "@mintlify/previewing/tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "@mintlify/previewing/unist-util-visit/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], @@ -7718,7 +8048,7 @@ "@mintlify/scraping/@mintlify/common/hast-util-to-html": ["hast-util-to-html@9.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA=="], - "@mintlify/scraping/@mintlify/common/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "@mintlify/scraping/@mintlify/common/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], "@mintlify/scraping/@mintlify/common/mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="], @@ -7730,9 +8060,9 @@ "@mintlify/scraping/@mintlify/common/tailwindcss": ["tailwindcss@3.4.4", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A=="], - "@mintlify/scraping/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@mintlify/scraping/@mintlify/common/unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], - "@mintlify/scraping/remark-gfm/mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="], + "@mintlify/scraping/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "@mintlify/scraping/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -7748,6 +8078,8 @@ "@mishieck/ink-titled-box/ink/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "@mishieck/ink-titled-box/ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "@mishieck/ink-titled-box/ink/slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], "@mishieck/ink-titled-box/ink/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], @@ -7786,8 +8118,6 @@ "@oclif/core/is-wsl/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - "@oclif/core/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], - "@oclif/core/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "@oclif/core/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -7798,12 +8128,24 @@ "@oclif/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.213.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-zRM5/Qj6G84Ej3F1yt33xBVY/3tnMxtL1fiDIxYbDWYaZ/eudVw3/PBiZ8G7JwUxXxjW8gU4g6LnOyfGKYHYgw=="], "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.213.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.213.0", "@opentelemetry/core": "2.6.0", "@opentelemetry/resources": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-00xlU3GZXo3kXKve4DLdrAL0NAFUaZ9appU/mn00S/5kSUdAvyYsORaDUfR04Mp2CLagAOhrzfUvYozY/EZX2g=="], "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/resources": "2.6.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-CicxWZxX6z35HR83jl+PLgtFgUrKRQ9LCXyxgenMnz5A1lgYWfAog7VtdOvGkJYyQgMNPhXQwkYrDLujk7z1Iw=="], + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-amqplib/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg=="], "@opentelemetry/instrumentation-amqplib/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], @@ -7828,6 +8170,8 @@ "@opentelemetry/instrumentation-express/@opentelemetry/instrumentation/require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + "@opentelemetry/instrumentation-fs/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-fs/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg=="], "@opentelemetry/instrumentation-fs/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], @@ -7918,7 +8262,7 @@ "@opentelemetry/instrumentation-pg/@opentelemetry/instrumentation/require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], - "@opentelemetry/instrumentation-pg/@types/pg/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "@opentelemetry/instrumentation-pg/@types/pg/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], "@opentelemetry/instrumentation-redis/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg=="], @@ -7938,12 +8282,32 @@ "@opentelemetry/instrumentation-undici/@opentelemetry/instrumentation/require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/sdk-trace-web/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/sql-common/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@prisma/config/c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "@prisma/config/c12/giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + + "@prisma/config/c12/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + + "@prisma/config/c12/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], + + "@prisma/config/c12/rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + "@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], "@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], "@prisma/instrumentation/@opentelemetry/instrumentation/require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + "@react-grab/cli/@antfu/ni/tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], + "@react-grab/cli/ora/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], "@react-grab/cli/ora/cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], @@ -7968,6 +8332,8 @@ "@sentry/node/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "@sentry/node/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "@smithy/eventstream-codec/@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@3.0.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-utf8-browser": "^3.0.0", "tslib": "^1.11.1" } }, "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w=="], "@smithy/eventstream-codec/@aws-crypto/crc32/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], @@ -8008,49 +8374,75 @@ "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], - "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + "@tinybirdco/sdk/@clack/prompts/@clack/core": ["@clack/core@1.3.0", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-xJPHpAmEQUBrXSLx0gF+q5K/IyihXpsHZcha+jB+tyahsKRK3Dxo4D0coZDewHo12NhiuzC3dTtMPbm53GEAAA=="], - "@types/body-parser/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@tinybirdco/sdk/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "@types/buffer-from/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@trigger.dev/core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-transformer": "0.203.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ=="], - "@types/cacheable-request/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@trigger.dev/core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.203.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-logs": "0.203.0", "@opentelemetry/sdk-metrics": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A=="], - "@types/chai-http/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@trigger.dev/core/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-transformer": "0.203.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ=="], - "@types/connect/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@trigger.dev/core/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.203.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-logs": "0.203.0", "@opentelemetry/sdk-metrics": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A=="], - "@types/cors/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@trigger.dev/core/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-transformer": "0.203.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ=="], - "@types/es-aggregate-error/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@trigger.dev/core/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.203.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-logs": "0.203.0", "@opentelemetry/sdk-metrics": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A=="], - "@types/express-serve-static-core/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@trigger.dev/core/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], - "@types/keyv/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@trigger.dev/core/execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], - "@types/mysql/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@trigger.dev/core/execa/human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], - "@types/node-fetch/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@trigger.dev/core/execa/npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], - "@types/pg/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@trigger.dev/core/execa/strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], - "@types/responselike/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@trigger.dev/core/socket.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "@types/send/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@trigger.dev/core/socket.io/engine.io": ["engine.io@6.5.5", "", { "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1" } }, "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA=="], - "@types/serve-static/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@types/body-parser/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - "@types/set-cookie-parser/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@types/buffer-from/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - "@types/superagent/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@types/cacheable-request/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - "@types/tedious/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@types/chai-http/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - "@types/ws/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@types/connect/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - "@types/yauzl/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@types/cors/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + "@types/es-aggregate-error/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "@types/express-serve-static-core/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "@types/keyv/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "@types/mysql/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "@types/node-fetch/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "@types/pg/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "@types/responselike/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "@types/send/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "@types/serve-static/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "@types/set-cookie-parser/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "@types/superagent/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "@types/tedious/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "@types/ws/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "@types/yauzl/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -8146,25 +8538,21 @@ "artillery/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - "artillery/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "atmn/@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.100.9", "", {}, "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ=="], - "artillery/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "atmn/@typescript/native-preview/@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260510.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YpG99bf/Va1aLGP8SUQy1ClUvi4c6uTFrEQ0B5KzZb9TsOwH1RIrc/2n8UO3IAuilvwEA0EU4q8fEO3otVP2Sw=="], - "atmn/@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.100.10", "", {}, "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w=="], + "atmn/@typescript/native-preview/@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260510.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-NUwhwHpQn7aSX2GGBuY2bjec+hFnIz2DAna4ksVneexVE20h2U0MFzBvWrqH2C0PzPxVvGOMg4fGCvhTs93nlw=="], - "atmn/@typescript/native-preview/@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260513.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ACX4oq23lGy3w9OstNspV8tH36DLFJ7Oe1vepGLrnhocnLJ58VGw3LAL9ObB4T1EB9H0M7fsgMIY4IEDRo8j8g=="], + "atmn/@typescript/native-preview/@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260510.1", "", { "os": "linux", "cpu": "arm" }, "sha512-UE+PIWWg7vvszSU0gS9rzgIIHCWexz3hMZDHpHRSLAleAvULCNI3EzwTRFOA4BHyQ8eReD1KZ8e76BuStEPspw=="], - "atmn/@typescript/native-preview/@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260513.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-UeF02ln9pfY3Zao85PcdwZ2uxAobuFXNQXMCN3TqlDCdu8A2VLDc2Dyn1lipGbKxcDZp5iZVwdk5Kg/fvGBpCQ=="], + "atmn/@typescript/native-preview/@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260510.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-T7Zuy6h0sU+38w+N3A+YgW0XVqxIMjeHyu+945rJkiP9zk52Mwp663t1ndyeAE/N2zV+q0SWQmHNuFSXl99wJw=="], - "atmn/@typescript/native-preview/@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260513.1", "", { "os": "linux", "cpu": "arm" }, "sha512-k+mNjeV23fBp0Zc01svHH8pbEuFw2T967wJVtzau6mM6ZMALDt6pIyxSTWE3WdPHAvv8ru2yqC3je+RW3VziQA=="], + "atmn/@typescript/native-preview/@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260510.1", "", { "os": "linux", "cpu": "x64" }, "sha512-gJu4q4YREvjR2Lx1jUaCd/bRbTuyKf2r3rJ4tReuHyAvNse23HdGI0a9w4Z3wUbvRznxYt640IIItWsr/f3LEQ=="], - "atmn/@typescript/native-preview/@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260513.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-AXd34hsn3tly6n/o8CZQUXokBJmh364Edr/ydnQQrtnYhw4DAkSzDR/uSf832jCsC5qmNjWzImzufxmLW10Qyw=="], + "atmn/@typescript/native-preview/@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260510.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-00DtjrtkdAHOU/soYr8ncrjUvIsple8nvb29ZUATnLraNnzUgv5AS3yMve/pG/N7rVLlKy2FrXlVyVW7WAx29w=="], - "atmn/@typescript/native-preview/@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260513.1", "", { "os": "linux", "cpu": "x64" }, "sha512-O2y6XptcV9T0ziBPUb/emYgX+CB8yeHygr27ojZsZhXI1s26JvnmADK17N0NLoOMT4lRtU2cBmG+hjzm3H1pRg=="], - - "atmn/@typescript/native-preview/@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260513.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-+0Fc/8zXDq5tRxd1oGaLtkrM671oByY4nexbgBPQrT5baCDnEP7IW/p2ATMUhuGZbMU/8W1zrsFdPk0EiobdFQ=="], - - "atmn/@typescript/native-preview/@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260513.1", "", { "os": "win32", "cpu": "x64" }, "sha512-69RI4j2LkiBM9E6jydEoQAAtpmiTYaR38CDGU/GzxxVckfOZjRgcK2Cs0H77VhTwESQkBE6y0qB1C+Y3lbzjtw=="], + "atmn/@typescript/native-preview/@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260510.1", "", { "os": "win32", "cpu": "x64" }, "sha512-27UeujQTEPFxhfkZL7aHnA1TlNol3nwDVFp5d6jFoP14yTXMe47kBnAJLEU2ta3REZE5PzLCs7HLV8H4VdxGgA=="], "atmn/eslint-plugin-react-hooks/eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], @@ -8178,6 +8566,8 @@ "atmn/ink/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "atmn/ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "atmn/ink/slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], "atmn/ink/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], @@ -8212,14 +8602,12 @@ "autumn-js/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "ava/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "ava/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - "ava/cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], "ava/cli-truncate/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + "ava/globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "ava/globby/slash": ["slash@4.0.0", "", {}, "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew=="], "better-opn/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], @@ -8230,16 +8618,24 @@ "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "bun-types/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "bun-types/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "cacheable-request/get-stream/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], - "checkout/@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.100.10", "", {}, "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w=="], + "checkout/@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.100.9", "", {}, "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ=="], "checkout/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "chevrotain-allstar/chevrotain/@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@12.0.0", "", { "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" } }, "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg=="], + + "chevrotain-allstar/chevrotain/@chevrotain/gast": ["@chevrotain/gast@12.0.0", "", { "dependencies": { "@chevrotain/types": "12.0.0" } }, "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ=="], + + "chevrotain-allstar/chevrotain/@chevrotain/types": ["@chevrotain/types@12.0.0", "", {}, "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA=="], + + "chevrotain-allstar/chevrotain/@chevrotain/utils": ["@chevrotain/utils@12.0.0", "", {}, "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA=="], + "cli-table3/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "cli-table3/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -8266,10 +8662,12 @@ "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], - "engine.io/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "engine.io/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "eslint-config-next/eslint-plugin-react-hooks/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "eslint-config-next/eslint-plugin-react-hooks/zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + "eslint-formatter-pretty/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "eslint-formatter-pretty/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -8348,6 +8746,8 @@ "favicons/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "find-cache-dir/pkg-dir/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="], @@ -8356,9 +8756,13 @@ "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "got/decompress-response/mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], - "hast-util-to-mdast/hast-util-to-html/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], + "gradient-string/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "gradient-string/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "ink-confirm-input/ink-text-input/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], @@ -8376,6 +8780,8 @@ "ink-scroll-list/ink/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "ink-scroll-list/ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "ink-scroll-list/ink/slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], "ink-scroll-list/ink/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], @@ -8400,7 +8806,15 @@ "is-online/got/responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="], - "jest-worker/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "jest-worker/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "langium/chevrotain/@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@12.0.0", "", { "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" } }, "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg=="], + + "langium/chevrotain/@chevrotain/gast": ["@chevrotain/gast@12.0.0", "", { "dependencies": { "@chevrotain/types": "12.0.0" } }, "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ=="], + + "langium/chevrotain/@chevrotain/types": ["@chevrotain/types@12.0.0", "", {}, "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA=="], + + "langium/chevrotain/@chevrotain/utils": ["@chevrotain/utils@12.0.0", "", {}, "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA=="], "listr2/cli-truncate/slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], @@ -8422,10 +8836,12 @@ "mixpanel/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + "mocha/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "mocha/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "mocha/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "msw/@inquirer/confirm/@inquirer/core": ["@inquirer/core@11.1.10", "", { "dependencies": { "@inquirer/ansi": "^2.0.5", "@inquirer/figures": "^2.0.5", "@inquirer/type": "^4.0.5", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-a4Q5BXHQAHa9eO202sTaFCHFYVB3x5fauDuThEAdZ9gfn76pSxiKU7wWcEH0N1O0XmQvNfQNU6QXpiRxmYQx+A=="], "msw/@inquirer/confirm/@inquirer/type": ["@inquirer/type@4.0.5", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q=="], @@ -8450,12 +8866,6 @@ "ngrok/got/responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], - "nodemon/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "nodemon/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - - "nodemon/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], - "nodemon/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], "open-editor/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], @@ -8466,6 +8876,10 @@ "open-editor/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + "open-editor/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "open-editor/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "open-editor/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], "open-editor/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], @@ -8500,13 +8914,17 @@ "posthog-js/@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], + "posthog-js/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "posthog-js/@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], "posthog-js/@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], + "prebuild-install/tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "protobufjs/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "protobufjs/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "public-ip/got/@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], @@ -8530,9 +8948,9 @@ "puppeteer/puppeteer-core/chromium-bidi": ["chromium-bidi@0.6.2", "", { "dependencies": { "mitt": "3.0.1", "urlpattern-polyfill": "10.0.0", "zod": "3.23.8" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-4WVBa6ijmUTVr9cZD4eicQD8Mdy/HCX3bzEIYYpmk0glqYLoWH+LqQEvV9RpDRzoQSbY1KJHloYXbDMXMbDPhg=="], - "react-email/glob/jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], + "react-email/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "react-email/glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "react-email/glob/jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], "react-email/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], @@ -8570,16 +8988,14 @@ "read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], - "read-pkg/normalize-package-data/resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - "read-pkg/normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - "rehype-stringify/hast-util-to-html/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], - "requirejs-config-file/stringify-object/is-obj": ["is-obj@1.0.1", "", {}, "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg=="], "requirejs-config-file/stringify-object/is-regexp": ["is-regexp@1.0.0", "", {}, "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="], + "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "run-jxa/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], "run-jxa/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], @@ -8588,6 +9004,10 @@ "run-jxa/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + "run-jxa/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "run-jxa/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "run-jxa/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], "schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -8652,22 +9072,36 @@ "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "trigger.dev/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-transformer": "0.203.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ=="], + + "trigger.dev/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.203.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-logs": "0.203.0", "@opentelemetry/sdk-metrics": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A=="], + + "trigger.dev/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="], + + "trigger.dev/c12/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "trigger.dev/c12/giget": ["giget@1.2.5", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.5.4", "pathe": "^2.0.3", "tar": "^6.2.1" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug=="], + + "trigger.dev/c12/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "trigger.dev/c12/ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="], + + "trigger.dev/c12/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "trigger.dev/c12/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + + "trigger.dev/c12/rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + "ts-to-zod/@clack/prompts/@clack/core": ["@clack/core@1.0.0-alpha.4", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-VCtU+vjyKPMSakVrB9q1bOnXN7QW/w4+YQDQCOF59GrzydW+169i0fVx/qzRRXJgt8KGj/pZZ/JxXroFZIDByg=="], - "tsc-alias/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "ts-to-zod/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "tsc-alias/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "tshy/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], - "tshy/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "tshy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "tsup/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "unplugin/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -8680,6 +9114,8 @@ "xo/@eslint/eslintrc/globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + "xo/@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "xo/@eslint/eslintrc/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "xo/@eslint/eslintrc/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], @@ -8734,14 +9170,16 @@ "@artilleryio/int-core/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + "@artilleryio/int-core/socket.io-client/engine.io-client/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + + "@artilleryio/int-core/socket.io-client/engine.io-client/xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], + "@autumn/server/ink/@alcalzone/ansi-tokenize/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], "@autumn/server/ink/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], "@autumn/website/eslint/@eslint/config-array/@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], - "@autumn/website/eslint/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], - "@aws-sdk/client-sso-oidc/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ=="], "@aws-sdk/client-sso-oidc/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ=="], @@ -8804,6 +9242,14 @@ "@aws-sdk/signature-v4/@smithy/signature-v4/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@1.1.0", "", { "dependencies": { "@smithy/is-array-buffer": "^1.1.0", "tslib": "^2.5.0" } }, "sha512-9m6NXE0ww+ra5HKHCHig20T+FAwxBAm7DIdwc/767uGWbRcY720ybgPacQNB96JMOI7xVr/CDa3oMzKmW4a+kw=="], + "@dotenvx/dotenvx/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "@eslint/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "@humanwhocodes/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@infisical/sdk/@aws-sdk/credential-providers/@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.598.0", "", { "dependencies": { "@smithy/core": "^2.2.1", "@smithy/protocol-http": "^4.0.1", "@smithy/signature-v4": "^3.1.0", "@smithy/smithy-client": "^3.1.2", "@smithy/types": "^3.1.0", "fast-xml-parser": "4.2.5", "tslib": "^2.6.2" } }, "sha512-HaSjt7puO5Cc7cOlrXFCW0rtA0BM9lvzjl56x0A20Pt+0wxXGeTOZZOkXQIepbrFkV2e/HYukuT9e99vXDm59g=="], "@infisical/sdk/@aws-sdk/credential-providers/@aws-sdk/client-cognito-identity/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.598.0", "", { "dependencies": { "@aws-sdk/types": "3.598.0", "@smithy/protocol-http": "^4.0.1", "@smithy/types": "^3.1.0", "tslib": "^2.6.2" } }, "sha512-WiaG059YBQwQraNejLIi0gMNkX7dfPZ8hDIhvMr5aVPRbaHH8AYF3iNSsXYCHvA2Cfa1O9haYXsuMF9flXnCmA=="], @@ -8974,18 +9420,12 @@ "@mintlify/cli/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@mintlify/common/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "@mintlify/common/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "@mintlify/common/remark-gfm/mdast-util-gfm/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], "@mintlify/common/tailwindcss/postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], - "@mintlify/prebuild/@mintlify/scraping/remark-gfm/mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="], - "@mintlify/prebuild/@mintlify/scraping/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@mintlify/previewing/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "@mintlify/previewing/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "@mintlify/previewing/got/cacheable-request/keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], @@ -8996,6 +9436,8 @@ "@mintlify/previewing/ink/react-reconciler/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + "@mintlify/previewing/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "@mintlify/previewing/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "@mintlify/previewing/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -9004,32 +9446,26 @@ "@mintlify/scraping/@mintlify/common/@mintlify/models/axios": ["axios@1.10.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw=="], - "@mintlify/scraping/@mintlify/common/@mintlify/validation/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - "@mintlify/scraping/@mintlify/common/@mintlify/validation/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], "@mintlify/scraping/@mintlify/common/@mintlify/validation/zod-to-json-schema": ["zod-to-json-schema@3.20.4", "", { "peerDependencies": { "zod": "^3.20.0" } }, "sha512-Un9+kInJ2Zt63n6Z7mLqBifzzPcOyX+b+Exuzf7L1+xqck9Q2EPByyTRduV3kmSPaXaRer1JCsucubpgL1fipg=="], "@mintlify/scraping/@mintlify/common/hast-util-to-html/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], + "@mintlify/scraping/@mintlify/common/mdast-util-gfm/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + "@mintlify/scraping/@mintlify/common/postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], "@mintlify/scraping/@mintlify/common/tailwindcss/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], - "@mintlify/scraping/@mintlify/common/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - "@mintlify/scraping/@mintlify/common/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], - "@mintlify/scraping/@mintlify/common/tailwindcss/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - "@mintlify/scraping/@mintlify/common/tailwindcss/postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], "@mintlify/scraping/@mintlify/common/tailwindcss/postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="], "@mintlify/scraping/@mintlify/common/tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], - "@mintlify/scraping/@mintlify/common/tailwindcss/resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - "@mintlify/scraping/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "@mintlify/scraping/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -9046,12 +9482,12 @@ "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - "@oclif/core/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "@oclif/core/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "@oclif/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/instrumentation-amqplib/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], "@opentelemetry/instrumentation-connect/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], @@ -9090,7 +9526,7 @@ "@opentelemetry/instrumentation-pg/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], - "@opentelemetry/instrumentation-pg/@types/pg/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "@opentelemetry/instrumentation-pg/@types/pg/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "@opentelemetry/instrumentation-redis/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], @@ -9098,10 +9534,18 @@ "@opentelemetry/instrumentation-undici/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "@prisma/config/c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "@prisma/config/c12/giget/nypm": ["nypm@0.6.6", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.1.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q=="], + "@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], "@react-grab/cli/ora/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + "@sentry/node/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "@stoplight/spectral-core/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@tailwindcss/postcss/@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], "@tailwindcss/postcss/@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], @@ -9136,14 +9580,20 @@ "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@ts-morph/common/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "@trigger.dev/core/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "@trigger.dev/core/socket.io/engine.io/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "@trigger.dev/core/socket.io/engine.io/cookie": ["cookie@0.4.2", "", {}, "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="], + + "@trigger.dev/core/socket.io/engine.io/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], "artillery-plugin-ensure/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "artillery-plugin-ensure/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + "artillery-plugin-expect/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.212.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg=="], "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.212.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "@opentelemetry/core": "2.5.1", "@opentelemetry/resources": "2.5.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-qglb5cqTf0mOC1sDdZ7nfrPjgmAqs2OxkzOPIf2+Rqx8yKBK0pS7wRtB1xH30rqahBIut9QJDbDePyvtyqvH/Q=="], @@ -9196,8 +9646,6 @@ "artillery/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - "artillery/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "atmn/eslint-plugin-react-hooks/eslint/@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], "atmn/eslint-plugin-react-hooks/eslint/@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], @@ -9218,6 +9666,8 @@ "atmn/eslint-plugin-react-hooks/eslint/globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + "atmn/eslint-plugin-react-hooks/eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "atmn/eslint-plugin-react-hooks/eslint/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "atmn/eslint-plugin-react-hooks/eslint/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], @@ -9234,12 +9684,34 @@ "cli-table3/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "concurrently/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "eslint-formatter-pretty/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "eslint-formatter-pretty/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "eslint-plugin-import/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "eslint-plugin-jsx-a11y/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "eslint-plugin-n/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "eslint-plugin-react/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "eslint/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "find-cache-dir/pkg-dir/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], "find-cache-dir/pkg-dir/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "gradient-string/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "ink-confirm-input/ink-text-input/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "ink-confirm-input/ink-text-input/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -9256,6 +9728,8 @@ "ink-confirm-input/ink-text-input/ink/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "ink-confirm-input/ink-text-input/ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "ink-confirm-input/ink-text-input/ink/slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], "ink-confirm-input/ink-text-input/ink/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], @@ -9274,9 +9748,11 @@ "listr2/cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + "log-symbols/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "matcher-collection/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "meow/read-pkg-up/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], @@ -9284,23 +9760,27 @@ "meow/read-pkg-up/read-pkg/normalize-package-data": ["normalize-package-data@3.0.3", "", { "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", "semver": "^7.3.4", "validate-npm-package-license": "^3.0.1" } }, "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA=="], + "mocha/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "msw/@inquirer/confirm/@inquirer/core/@inquirer/ansi": ["@inquirer/ansi@2.0.5", "", {}, "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw=="], "msw/@inquirer/confirm/@inquirer/core/@inquirer/figures": ["@inquirer/figures@2.0.5", "", {}, "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ=="], "msw/@inquirer/confirm/@inquirer/core/mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], - "msw/@inquirer/confirm/@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "msw/tough-cookie/tldts/tldts-core": ["tldts-core@7.0.30", "", {}, "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q=="], "ngrok/got/cacheable-request/keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "ngrok/got/cacheable-request/normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], - "nodemon/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "open-editor/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "nodemon/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "ora/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "ora/cli-cursor/restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "ora/log-symbols/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], @@ -9310,12 +9790,18 @@ "pkg-conf/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + "posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], "posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], "posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + "posthog-js/@opentelemetry/sdk-logs/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "posthog-js/@opentelemetry/sdk-logs/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "prebuild-install/tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "public-ip/got/cacheable-request/keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], @@ -9328,8 +9814,6 @@ "react-email/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], - "react-email/glob/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], - "react-email/next/postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], "react-email/ora/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], @@ -9338,6 +9822,8 @@ "read-pkg-up/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "run-jxa/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "sdk-test/next/postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], "shadcn/cosmiconfig/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -9346,11 +9832,15 @@ "shadcn/ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], - "tsc-alias/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "trigger.dev/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.203.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-vM2+rPq0Vi3nYA5akQD2f3QwossDnTDLvKbea6u/A2NZ3XDkPxMfo/PNrDoXhDUD/0pPo2CdH5ce/thn9K0kLw=="], - "tshy/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "trigger.dev/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], - "unplugin/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "trigger.dev/c12/giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "trigger.dev/c12/giget/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + + "tshy/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "xo/@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -9382,8 +9872,6 @@ "@artilleryio/int-core/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "@autumn/website/eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "@aws-sdk/client-sso-oidc/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@4.1.3", "", { "dependencies": { "@smithy/protocol-http": "^4.1.8", "@smithy/querystring-builder": "^3.0.11", "@smithy/types": "^3.7.2", "@smithy/util-base64": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-6SxNltSncI8s689nvnzZQc/dPXcpHQ34KUj6gR/HBroytKOd/isMG3gJF/zBE1TBmTT18TXyzhg3O3SOOqGEhA=="], "@aws-sdk/client-sso-oidc/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@3.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA=="], @@ -9480,8 +9968,6 @@ "@mintlify/cli/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@mintlify/common/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "@mintlify/prebuild/@mintlify/scraping/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "@mintlify/prebuild/@mintlify/scraping/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -9492,33 +9978,33 @@ "@mintlify/scraping/@mintlify/common/@mintlify/models/axios/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - "@mintlify/scraping/@mintlify/common/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "@mintlify/scraping/@mintlify/common/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - "@mintlify/scraping/@mintlify/common/tailwindcss/postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], "@mintlify/scraping/@mintlify/common/tailwindcss/postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "@mintlify/scraping/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@prisma/config/c12/giget/nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], + + "@prisma/config/c12/giget/nypm/tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], + "@react-grab/cli/ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "@react-grab/cli/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "@trigger.dev/core/socket.io/engine.io/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "artillery-plugin-ensure/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], - "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], - "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], - "artillery-plugin-publish-metrics/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "artillery-plugin-publish-metrics/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], - "artillery-plugin-publish-metrics/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "artillery-plugin-publish-metrics/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], - "artillery-plugin-publish-metrics/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + "artillery-plugin-publish-metrics/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], "artillery/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], @@ -9540,6 +10026,8 @@ "find-cache-dir/pkg-dir/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + "ink-confirm-input/ink-text-input/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "ink-confirm-input/ink-text-input/ink/@alcalzone/ansi-tokenize/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], "ink-confirm-input/ink-text-input/ink/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], @@ -9548,28 +10036,42 @@ "meow/read-pkg-up/read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + "ora/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "ora/log-symbols/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "ora/log-symbols/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], "pkg-conf/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], - "react-email/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], "react-email/ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "react-email/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "shadcn/ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "shadcn/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "trigger.dev/c12/giget/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "trigger.dev/c12/giget/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "trigger.dev/c12/giget/tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "trigger.dev/c12/giget/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "trigger.dev/c12/giget/tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "xo/@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "xo/eslint/file-entry-cache/flat-cache/keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "xo/eslint/file-entry-cache/flat-cache/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "xo/eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "xo/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], "@aws-sdk/client-sso-oidc/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@3.0.11", "", { "dependencies": { "@smithy/types": "^3.7.2", "@smithy/util-uri-escape": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-u+5HV/9uJaeLj5XTb6+IEF/dokWWkEqJ0XiaRRogyREmKGUgZnNecLucADLdauWFKUNbQfulHFEZEdjwEBjXRg=="], @@ -9672,24 +10174,26 @@ "@mintlify/prebuild/@mintlify/scraping/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@mintlify/scraping/@mintlify/common/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - "artillery-plugin-publish-metrics/@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "artillery-plugin-publish-metrics/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - "artillery-plugin-publish-metrics/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "artillery-plugin-publish-metrics/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - "artillery-plugin-publish-metrics/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "artillery-plugin-publish-metrics/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - "artillery-plugin-publish-metrics/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "atmn/eslint-plugin-react-hooks/eslint/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "atmn/eslint-plugin-react-hooks/eslint/file-entry-cache/flat-cache/keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "atmn/eslint-plugin-react-hooks/eslint/file-entry-cache/flat-cache/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "atmn/eslint-plugin-react-hooks/eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "find-cache-dir/pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], "meow/read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], @@ -9698,6 +10202,8 @@ "ora/log-symbols/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "trigger.dev/c12/giget/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "xo/eslint/file-entry-cache/flat-cache/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "@aws-sdk/client-sso-oidc/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg=="], diff --git a/bunfig.toml b/bunfig.toml index a02df321e..c7b37e95f 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -13,4 +13,10 @@ NODE_ENV = "test" [install] # Only install package versions published at least 3 days ago -minimumReleaseAge = 259200 # seconds \ No newline at end of file +minimumReleaseAge = 259200 # seconds +minimumReleaseAgeExcludes = [ + "@trigger.dev/build", + "@trigger.dev/core", + "@trigger.dev/sdk", + "trigger.dev", +] diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml deleted file mode 100644 index 48334eaf5..000000000 --- a/docker-compose.ci.yml +++ /dev/null @@ -1,124 +0,0 @@ -services: - - valkey: - image: docker.io/bitnami/valkey:8.0 - environment: - - ALLOW_EMPTY_PASSWORD=yes - - VALKEY_DISABLE_COMMANDS=FLUSHDB,FLUSHALL - volumes: - - valkey-data:/bitnami/valkey/data - healthcheck: - test: [ "CMD", "redis-cli", "-h", "localhost", "-p", "6379", "ping" ] - interval: 10s - timeout: 5s - retries: 5 - ports: - - "6379:6379" - restart: unless-stopped - - shared: - build: - dockerfile: docker/dev.dockerfile - context: . - target: shared - volumes: - - ./shared:/app/shared - - shared-dist:/app/shared/dist - - shared-node-modules:/app/shared/node_modules - - root-node-modules:/app/node_modules - environment: - - NODE_ENV=development - restart: unless-stopped - - # Vite frontend - vite: - build: - dockerfile: docker/dev.dockerfile - context: . - target: vite - ports: - - "3000:3000" - volumes: - - ./vite:/app/vite - - shared-dist:/app/shared/dist - - vite-node-modules:/app/vite/node_modules - - root-node-modules:/app/node_modules - environment: - - NODE_ENV=development - depends_on: - - shared - restart: unless-stopped - - # Main Express server - server: - build: - dockerfile: docker/dev.dockerfile - context: . - target: server - ports: - - "8080:8080" - volumes: - - ./server:/app/server - - shared-dist:/app/shared/dist - - server-node-modules:/app/server/node_modules - - root-node-modules:/app/node_modules - environment: - - NODE_ENV=development - - REDIS_URL=redis://valkey:6379 - - DATABASE_URL=${DATABASE_URL} - - ENCRYPTION_IV=${ENCRYPTION_IV} - - ENCRYPTION_PASSWORD=${ENCRYPTION_PASSWORD} - - TESTS_ORG=${TESTS_ORG} - - TESTS_ORG_ID=${TESTS_ORG_ID} - - LOCALTUNNEL_RESERVED_KEY=${LOCALTUNNEL_RESERVED_KEY} - - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} - - BETTER_AUTH_URL=http://localhost:8080 - - STRIPE_WEBHOOK_URL=${STRIPE_WEBHOOK_URL} - - HYPERBROWSER_API_KEY=${HYPERBROWSER_API_KEY} - depends_on: - - shared - restart: unless-stopped - - # BullMQ Workers - workers: - build: - dockerfile: docker/dev.dockerfile - context: . - target: workers - volumes: - # Mount server source for hot reload (workers use server code) - - ./server:/app/server - - shared-dist:/app/shared/dist - - server-node-modules:/app/server/node_modules - - root-node-modules:/app/node_modules - environment: - - NODE_ENV=development - - REDIS_URL=redis://valkey:6379 - depends_on: - - shared - restart: unless-stopped - - # Run localtunnel - localtunnel: - build: - dockerfile: docker/dev.dockerfile - context: . - target: localtunnel - volumes: - - ./server:/app/server - environment: - - LOCALTUNNEL_RESERVED_KEY=${LOCALTUNNEL_RESERVED_KEY} - depends_on: - - server - restart: unless-stopped - -volumes: - # Shared package dist output - shared-dist: - valkey-data: - - # Node modules volumes to avoid host/container conflicts - shared-node-modules: - server-node-modules: - vite-node-modules: - root-node-modules: diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index 95f9a1d56..000000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,119 +0,0 @@ -services: - - valkey: - image: docker.io/valkey/valkey:8.0 - environment: - - ALLOW_EMPTY_PASSWORD=yes - - VALKEY_DISABLE_COMMANDS=FLUSHDB,FLUSHALL - volumes: - - valkey-data:/bitnami/valkey/data - healthcheck: - test: ['CMD', 'redis-cli', '-h', 'localhost', '-p', '6379', 'ping'] - interval: 10s - timeout: 5s - retries: 5 - ports: - - "6379:6379" - restart: unless-stopped - - shared: - build: - dockerfile: docker/dev.dockerfile - context: . - target: shared - volumes: - - ./shared:/app/shared - - shared-dist:/app/shared/dist - - shared-node-modules:/app/shared/node_modules - - root-node-modules:/app/node_modules - environment: - - NODE_ENV=development - restart: unless-stopped - - # Vite frontend - vite: - build: - dockerfile: docker/dev.dockerfile - context: . - target: vite - ports: - - "3000:3000" - volumes: - - ./vite:/app/vite - - shared-dist:/app/shared/dist - - vite-node-modules:/app/vite/node_modules - - root-node-modules:/app/node_modules - environment: - - NODE_ENV=development - - CHOKIDAR_USEPOLLING=true - - WATCHPACK_POLLING=true - depends_on: - - shared - restart: unless-stopped - - # Main Express server - server: - build: - dockerfile: docker/dev.dockerfile - context: . - target: server - ports: - - "8080:8080" - volumes: - - ./server:/app/server - - shared-dist:/app/shared/dist - - root-node-modules:/app/node_modules - - server-node-modules:/app/server/node_modules - environment: - - NODE_ENV=development - - REDIS_URL=redis://valkey:6379 - depends_on: - - shared - - valkey - restart: unless-stopped - - # BullMQ Workers - workers: - build: - dockerfile: docker/dev.dockerfile - context: . - target: workers - volumes: - # Mount server source for hot reload (workers use server code) - - ./server:/app/server - - shared-dist:/app/shared/dist - - root-node-modules:/app/node_modules - - server-node-modules:/app/server/node_modules - environment: - - NODE_ENV=development - - REDIS_URL=redis://valkey:6379 - depends_on: - - shared - - valkey - restart: unless-stopped - - - # Run localtunnel - localtunnel: - image: oven/bun:latest - build: - dockerfile: docker/dev.dockerfile - context: . - target: localtunnel - volumes: - - ./server:/app/server - depends_on: - - server - restart: unless-stopped - - -volumes: - # Shared package dist output - shared-dist: - valkey-data: - - # Node modules volumes to avoid host/container conflicts - shared-node-modules: - server-node-modules: - vite-node-modules: - root-node-modules: diff --git a/docker-compose.unix.yml b/docker-compose.unix.yml deleted file mode 100644 index 2aa6fd328..000000000 --- a/docker-compose.unix.yml +++ /dev/null @@ -1,111 +0,0 @@ -services: - valkey: - image: docker.io/valkey/valkey:8.0 - environment: - - ALLOW_EMPTY_PASSWORD=yes - - VALKEY_DISABLE_COMMANDS=FLUSHDB,FLUSHALL - volumes: - - valkey-data:/valkey/valkey/data - healthcheck: - test: ["CMD", "redis-cli", "-h", "localhost", "-p", "6379", "ping"] - interval: 10s - timeout: 5s - retries: 5 - ports: - - "6379:6379" - restart: unless-stopped - - shared: - build: - dockerfile: docker/dev.dockerfile - context: . - target: shared - volumes: - - ./shared:/app/shared - - shared-dist:/app/shared/dist - - shared-node-modules:/app/shared/node_modules - - root-node-modules:/app/node_modules - environment: - - NODE_ENV=development - restart: unless-stopped - - # Vite frontend - vite: - build: - dockerfile: docker/dev.dockerfile - context: . - target: vite - ports: - - "3000:3000" - volumes: - - ./vite:/app/vite - - shared-dist:/app/shared/dist - - vite-node-modules:/app/vite/node_modules - - root-node-modules:/app/node_modules - environment: - - NODE_ENV=development - depends_on: - - shared - restart: unless-stopped - - # Main Express server - server: - build: - dockerfile: docker/dev.dockerfile - context: . - target: server - ports: - - "8080:8080" - volumes: - - ./server:/app/server - - shared-dist:/app/shared/dist - - server-node-modules:/app/server/node_modules - - root-node-modules:/app/node_modules - environment: - - NODE_ENV=development - - REDIS_URL=redis://valkey:6379 - depends_on: - - shared - restart: unless-stopped - - # BullMQ Workers - workers: - build: - dockerfile: docker/dev.dockerfile - context: . - target: workers - volumes: - # Mount server source for hot reload (workers use server code) - - ./server:/app/server - - shared-dist:/app/shared/dist - - server-node-modules:/app/server/node_modules - - root-node-modules:/app/node_modules - environment: - - NODE_ENV=development - - REDIS_URL=redis://valkey:6379 - depends_on: - - shared - restart: unless-stopped - - # Run localtunnel - localtunnel: - build: - dockerfile: docker/dev.dockerfile - context: . - target: localtunnel - volumes: - - ./server:/app/server - depends_on: - - server - restart: unless-stopped - -volumes: - # Shared package dist output - shared-dist: - valkey-data: - - # Node modules volumes to avoid host/container conflicts - shared-node-modules: - server-node-modules: - vite-node-modules: - root-node-modules: diff --git a/knip.json b/knip.json index 06e5e760a..74649a7a2 100644 --- a/knip.json +++ b/knip.json @@ -20,7 +20,7 @@ ], "workspaces": { ".": { - "entry": ["apps/scope-picker/**/*"] + "entry": ["apps/scope-picker/**/*", "trigger.config.ts"] }, "server": { "entry": [ diff --git a/package.json b/package.json index 17f2dc385..3324a8b38 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,10 @@ "migrate-functions:prod": "infisical run --env=prod --recursive -- bun scripts/migrations/migrate-functions.ts", "validate-schema": "infisical run --env=prod --recursive -- bun scripts/migrations/validate-schema.ts", "validate-shebangs": "infisical run --env=prod --recursive -- bun scripts/migrations/validate-fullsubject-shebangs.ts", + "tinybird:info": "cd server && infisical run --env=dev --recursive -- bunx tinybird info", + "tinybird:deploy:dev:check": "cd server && infisical run --env=dev --recursive -- bunx tinybird deploy --check", + "tinybird:deploy:dev": "cd server && infisical run --env=dev --recursive -- bunx tinybird deploy", + "trigger:deploy": "bunx trigger.dev deploy", "setupci": "node scripts/setup/setupci.js", "replicate": "bun scripts/db/replicate.ts", "db:pull": "bun scripts/db/pull.ts", @@ -128,6 +132,7 @@ "@aws-sdk/client-sqs": "^3.985.0", "@better-auth/core": "catalog:", "@better-auth/oauth-provider": "catalog:", + "@trigger.dev/sdk": "4.4.6", "@wooorm/starry-night": "^3.8.0", "ag-charts-react": "^12.3.0", "better-auth": "catalog:", @@ -139,12 +144,14 @@ "devDependencies": { "@better-auth/cli": "^1.4.21", "@biomejs/biome": "^2.2.7", + "@trigger.dev/build": "4.4.6", "@types/node": "^24.9.1", "concurrently": "^9.2.1", "dotenv": "^16.6.1", "husky": "^9.1.7", "inquirer": "^12.10.0", "knip": "^6.7.0", + "trigger.dev": "4.4.6", "ts-to-zod": "^5.1.0", "turbo": "^2.9.6" } diff --git a/scripts/dev.ts b/scripts/dev.ts index 0219e1126..eb77bb5c2 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -49,6 +49,30 @@ function getEnvVariable(filePath: string, key: string): string | null { return null; } +function getPackageDependencyVersion({ + projectRoot, + packageName, +}: { + projectRoot: string; + packageName: string; +}): string { + const packageJson = JSON.parse( + readFileSync(join(projectRoot, "package.json"), "utf-8"), + ) as { + dependencies?: Record; + devDependencies?: Record; + }; + const version = + packageJson.dependencies?.[packageName] ?? + packageJson.devDependencies?.[packageName]; + + if (!version) { + throw new Error(`Missing ${packageName} in package.json`); + } + + return version; +} + function killPorts({ ports }: { ports: number[] }) { if (process.platform === "win32") { return; @@ -136,6 +160,11 @@ async function startDev() { // Use cmd on Windows, sh on Unix const isWindows = process.platform === "win32"; + const triggerDevVersion = getPackageDependencyVersion({ + projectRoot, + packageName: "trigger.dev", + }); + const bunInstallBin = dirname(process.execPath); let shellArgs: string[]; if (serverOnly) { @@ -174,6 +203,14 @@ async function startDev() { ? `"cd server && bun ${workersScript}"` : `"cd server && bun ${workersScript}"`, ); + + names.push("trigger"); + colors.push("cyan"); + cmds.push( + isWindows + ? `"bunx trigger.dev@${triggerDevVersion} dev"` + : `"bunx trigger.dev@${triggerDevVersion} dev"`, + ); } names.push("vite", "checkout"); @@ -196,12 +233,13 @@ async function startDev() { const concurrentlyProc = Bun.spawn(shellArgs, { cwd: projectRoot, - env: { - ...process.env, - VITE_PORT: VITE_PORT.toString(), - SERVER_PORT: SERVER_PORT.toString(), - CHECKOUT_PORT: CHECKOUT_PORT.toString(), - VITE_APP_ENV: viteAppEnv, + env: { + ...process.env, + VITE_PORT: VITE_PORT.toString(), + SERVER_PORT: SERVER_PORT.toString(), + CHECKOUT_PORT: CHECKOUT_PORT.toString(), + VITE_APP_ENV: viteAppEnv, + BUN_INSTALL_BIN: process.env.BUN_INSTALL_BIN ?? bunInstallBin, ...(worktreeNum > 1 && { CLIENT_URL: `http://localhost:${VITE_PORT}`, BETTER_AUTH_URL: `http://localhost:${SERVER_PORT}`, diff --git a/server/experiments/explainCustomerFilter.ts b/server/experiments/explainCustomerFilter.ts new file mode 100644 index 000000000..8db2ceb22 --- /dev/null +++ b/server/experiments/explainCustomerFilter.ts @@ -0,0 +1,170 @@ +import { AppEnv } from "@autumn/shared"; +import { + buildCustomerCount, + buildCustomerSelect, +} from "@/internal/migrations/v2/filters/customers/buildCustomerSelect.js"; +import type { CustomerFilter } from "@autumn/shared/api/migrations/filters/customerFilter.js"; +import { sql, type SQL } from "drizzle-orm"; +import { PgDialect } from "drizzle-orm/pg-core"; +// Import initDrizzle directly — avoid `experimentEnv` because its +// `loadLocalEnv()` reads `server/.env` and clobbers env vars injected by +// `infisical run --env=staging` (e.g. DATABASE_URL). +import { initDrizzle } from "../src/db/initDrizzle"; +import { FeatureService } from "../src/internal/features/FeatureService.js"; + +const prodTestOrgId = (() => { + const v = process.env.PROD_TEST_ORG_ID; + if (!v) throw new Error("PROD_TEST_ORG_ID env var is required"); + return v; +})(); + +// Mask password but show host so we can verify which DB we're hitting. +const dbUrl = process.env.DATABASE_URL ?? ""; +console.log( + "DATABASE URL host:", + dbUrl.replace(/:\/\/[^@]+@/, "://***:***@") || "(empty)", +); + +// To run against a remote env (e.g. staging) via infisical: +// infisical run --env=staging --recursive -- bun run server/experiments/explainCustomerFilter.ts + +// ─── Configuration ────────────────────────────────────────────────── +const ORG_ID = prodTestOrgId; +const ENV = AppEnv.Live; +const SAMPLE_LIMIT = 1_000; +const TRUNCATE_EXPLAIN = true; +const EXPLAIN_MAX_LINES = 20; + + +const FILTER: CustomerFilter = { + // plan: { plan_id: "free" }, + // plan: { item: { feature_id: "CREDITS", price: { $ne: null } } } + plan: { plan_id: "free", recurring: true } +}; + +const MIGRATION_PLAN = { + filter: { + customers: { + + } + } +}; + +// ═════════════════════════════════════════════════════════════════════ + +const truncateExplainText = (text: string, maxLines: number): string => { + const lines = text.split("\n"); + if (lines.length <= maxLines) return text; + const omitted = lines.length - maxLines; + return [...lines.slice(0, maxLines), `... (${omitted} more lines truncated)`].join( + "\n", + ); +}; + +const printExplainPlan = async ({ + db, + query, + label, +}: { + db: ReturnType["db"]; + query: SQL; + label: string; +}) => { + console.log(`\n--- EXPLAIN ANALYZE: ${label} ---`); + const explainResult = await db.execute( + sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${query}`, + ); + const lines: string[] = []; + for (const row of explainResult) + lines.push(String((row as Record)["QUERY PLAN"])); + const joined = lines.join("\n"); + console.log( + TRUNCATE_EXPLAIN ? truncateExplainText(joined, EXPLAIN_MAX_LINES) : joined, + ); +}; + +const dialect = new PgDialect(); + +const inlineParams = (text: string, params: readonly unknown[]): string => + text.replace(/\$(\d+)/g, (_, n) => { + const v = params[Number(n) - 1]; + if (v === null || v === undefined) return "NULL"; + if (typeof v === "number" || typeof v === "boolean") return String(v); + return `'${String(v).replace(/'/g, "''")}'`; + }); + +const printSqlQuery = ({ query, label }: { query: SQL; label: string }) => { + const { sql: text, params } = dialect.sqlToQuery(query); + console.log(`\n--- SQL: ${label} ---`); + console.log(inlineParams(text, params)); +}; + +const runMeasured = async ({ + db, + query, + label, +}: { + db: ReturnType["db"]; + query: SQL; + label: string; +}) => { + console.log(`\n=== ${label} ===`); + printSqlQuery({ query, label }); + const startedAt = performance.now(); + const result = await db.execute(query); + const elapsedMs = performance.now() - startedAt; + console.log(`Rows returned: ${result.length}`); + console.log(`Wall-clock: ${elapsedMs.toFixed(2)}ms`); + if (label === "COUNT" && result.length > 0) + console.log(`Count: ${(result[0] as Record).count}`); + await printExplainPlan({ db, query, label }); +}; + +const main = async () => { + const replicaUrl = process.env.DATABASE_REPLICA_URL; + const usingReplica = Boolean(replicaUrl); + if (!usingReplica) + console.warn( + "DATABASE_REPLICA_URL not set — falling back to DATABASE_URL (primary). Set the replica URL to test against the read replica.", + ); + const { db } = initDrizzle({ replica: usingReplica }); + + console.log( + `=== CUSTOMER FILTER EXPERIMENT (${usingReplica ? "REPLICA" : "PRIMARY"}) ===`, + ); + console.log(JSON.stringify({ ORG_ID, ENV, FILTER }, null, 2)); + + const orgFeatures = await FeatureService.list({ + db, + orgId: ORG_ID, + env: ENV, + }); + console.log(`\nLoaded ${orgFeatures.length} features for resolution context.`); + + const ctx = { features: orgFeatures }; + + const countQuery = buildCustomerCount({ + orgId: ORG_ID, + env: ENV, + filter: FILTER, + ctx, + }); + const selectQuery = buildCustomerSelect({ + orgId: ORG_ID, + env: ENV, + filter: FILTER, + ctx, + limit: SAMPLE_LIMIT, + }); + + await runMeasured({ db, query: countQuery, label: "COUNT" }); + await runMeasured({ + db, + query: selectQuery, + label: `SELECT (limit ${SAMPLE_LIMIT})`, + }); + + process.exit(0); +}; + +await main(); diff --git a/server/package.json b/server/package.json index fbee53607..c521511f8 100644 --- a/server/package.json +++ b/server/package.json @@ -80,6 +80,9 @@ "@react-email/components": "^0.0.42", "@sentry/bun": "catalog:", "@supabase/supabase-js": "^2.46.2", + "@tinybirdco/sdk": "^0.0.69", + "@trigger.dev/build": "4.4.6", + "@trigger.dev/sdk": "4.4.6", "@types/qs": "^6.14.0", "@types/semver": "^7.7.1", "@typescript/native-preview": "^7.0.0-dev.20251114.1", diff --git a/server/src/db/initDrizzle.ts b/server/src/db/initDrizzle.ts index eda32f0ca..935242464 100644 --- a/server/src/db/initDrizzle.ts +++ b/server/src/db/initDrizzle.ts @@ -98,7 +98,7 @@ export const { db: dbCritical, client: clientCritical } = initDrizzle({ // -- General pool: used by all other endpoints -- export const { db: dbGeneral, client: clientGeneral } = initDrizzle({ - // connectTimeout: 5, + connectTimeout: isProd ? 5 : 30, }); // -- Replica pool: used as fallback when primary is degraded -- diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts index f35574992..efd8157c3 100644 --- a/server/src/external/autumn/autumnCli.ts +++ b/server/src/external/autumn/autumnCli.ts @@ -34,6 +34,10 @@ import { type FinalizeLockParamsV0, type LegacyVersion, type ListEntitiesParams, + type Migration, + type MigrationFilter, + type MigrationRun, + type Operations, type OrgConfig, type ProductItem, type RestoreParamsV1, @@ -47,6 +51,8 @@ import { } from "@autumn/shared"; import { defaultApiVersion } from "@tests/constants.js"; import { timeout } from "@tests/utils/genUtils"; +import type { TinybirdMigrationItemEvent } from "@/external/tinybird/migrations/migrationItemEventsDataSource.js"; +import type { PrepareResponse } from "@/internal/migrations/v2/prepare/types"; export default class AutumnError extends Error { message: string; @@ -961,6 +967,92 @@ export class AutumnInt { return data; }; + migrationsV2 = { + create: async (params: { + id: string; + filter?: MigrationFilter | null; + operations?: Operations | null; + }): Promise => { + const data = await this.post(`/migrations.create`, params); + return data as Migration; + }, + list: async (): Promise<{ list: Migration[] }> => { + const data = await this.post(`/migrations.list`, {}); + return data as { list: Migration[] }; + }, + update: async (params: { + id: string; + updates: { + id?: string; + filter?: MigrationFilter | null; + operations?: Operations | null; + retry_failed?: boolean; + }; + }): Promise => { + const data = await this.post(`/migrations.update`, params); + return data as Migration; + }, + delete: async (params: { id: string }): Promise => { + const data = await this.post(`/migrations.delete`, params); + return data as Migration; + }, + deleteAndCreate: async (params: { + id: string; + filter?: MigrationFilter | null; + operations?: Operations | null; + }): Promise => { + try { + await this.post(`/migrations.delete`, { id: params.id }); + } catch {} + const data = await this.post(`/migrations.create`, params); + return data as Migration; + }, + prepare: async (params: { + id: string; + dry_run: boolean; + }): Promise => { + const data = await this.post(`/migrations.prepare`, params); + return data as PrepareResponse; + }, + run: async (params: { + id: string; + dry_run?: boolean; + }): Promise<{ + migration_id: string; + dry_run: boolean; + run_id: string; + }> => { + const data = await this.post(`/migrations.run`, params); + return data as { + migration_id: string; + dry_run: boolean; + run_id: string; + }; + }, + lazyRun: async (params: { + id: string; + }): Promise<{ + migration_id: string; + run_id: string; + }> => { + const data = await this.post(`/migrations.lazy_run`, params); + return data as { migration_id: string; run_id: string }; + }, + listRuns: async (params: { + migrationId: string; + }): Promise<{ list: MigrationRun[] }> => { + const data = await this.post(`/migrations.runs.list`, params); + return data as { list: MigrationRun[] }; + }, + listItemEvents: async (params: { + migrationId: string; + migrationRunId?: string; + }): Promise<{ list: TinybirdMigrationItemEvent[] }> => { + const data = await this.post(`/migrations.item_events.list`, params); + return data as { list: TinybirdMigrationItemEvent[] }; + }, + }; + balances = { create: async (params: CreateBalanceParamsV0) => { const data = await this.post(`/balances/create`, params); diff --git a/server/src/external/infisical/fetchInfisicalSecrets.ts b/server/src/external/infisical/fetchInfisicalSecrets.ts new file mode 100644 index 000000000..1ca76e2d8 --- /dev/null +++ b/server/src/external/infisical/fetchInfisicalSecrets.ts @@ -0,0 +1,115 @@ +/** + * Pure REST fetcher for Infisical secrets. Used at trigger.dev DEPLOY + * time via `syncEnvVars` to push secrets to the cloud env. Kept SDK-free + * so trigger.config.ts can import it without bloating the build. + * + * Runtime code uses `initInfisical` (SDK-based, populates process.env). + */ + +export type InfisicalSyncEnvVar = { name: string; value: string }; + +export type FetchInfisicalSecretsArgs = { + clientId?: string | null; + clientSecret?: string | null; + projectId?: string | null; + /** Defaults to "prod" if not set. */ + environment?: string | null; + secretPath?: string; + recursive?: boolean; + includeImports?: boolean; +}; + +/** + * Authenticate via Universal Auth, fetch secrets at the given path, and + * return them as `{ name, value }[]`. Imported groups are flattened in + * after primary secrets, with first-write-wins de-duplication. + */ +export const fetchInfisicalSecrets = async ({ + clientId, + clientSecret, + projectId, + environment, + secretPath = "/", + recursive = true, + includeImports = true, +}: FetchInfisicalSecretsArgs): Promise => { + const env = environment ?? "prod"; + + if (!clientId || !clientSecret || !projectId) { + throw new Error( + "Missing Infisical credentials. Set INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET, INFISICAL_PROJECT_ID.", + ); + } + + const authRes = await fetch( + "https://app.infisical.com/api/v1/auth/universal-auth/login", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ clientId, clientSecret }), + }, + ); + if (!authRes.ok) + throw new Error( + `Infisical auth failed: ${authRes.status} ${await authRes.text()}`, + ); + const { accessToken } = (await authRes.json()) as { accessToken: string }; + + const params = new URLSearchParams({ + environment: env, + workspaceId: projectId, + secretPath, + recursive: String(recursive), + includeImports: String(includeImports), + }); + const secretsRes = await fetch( + `https://app.infisical.com/api/v3/secrets/raw?${params}`, + { headers: { Authorization: `Bearer ${accessToken}` } }, + ); + if (!secretsRes.ok) + throw new Error( + `Infisical secrets failed: ${secretsRes.status} ${await secretsRes.text()}`, + ); + const data = (await secretsRes.json()) as { + secrets: Array<{ secretKey: string; secretValue: string }>; + imports?: Array<{ + secrets: Array<{ secretKey: string; secretValue: string }>; + }>; + }; + + const envVars: InfisicalSyncEnvVar[] = []; + const seen = new Set(); + + const push = (key: string, value: string) => { + if (!key || !value || seen.has(key)) return; + envVars.push({ name: key, value }); + seen.add(key); + }; + + for (const secret of data.secrets) push(secret.secretKey, secret.secretValue); + for (const importGroup of data.imports ?? []) + for (const secret of importGroup.secrets) + push(secret.secretKey, secret.secretValue); + + console.log( + `[fetchInfisicalSecrets] Synced ${envVars.length} secrets from Infisical (env=${env}, path=${secretPath})`, + ); + return envVars; +}; + +/** + * Read the four credential vars (`INFISICAL_CLIENT_ID`, `_SECRET`, + * `_PROJECT_ID`, `_ENVIRONMENT`) from the local process env first then + * trigger.dev's deploy-time `ctx.env`. Convenience for `syncEnvVars`. + */ +export const fetchInfisicalSecretsFromEnv = ( + ctxEnv: Record = {}, +): Promise => + fetchInfisicalSecrets({ + clientId: process.env.INFISICAL_CLIENT_ID ?? ctxEnv.INFISICAL_CLIENT_ID, + clientSecret: + process.env.INFISICAL_CLIENT_SECRET ?? ctxEnv.INFISICAL_CLIENT_SECRET, + projectId: process.env.INFISICAL_PROJECT_ID ?? ctxEnv.INFISICAL_PROJECT_ID, + environment: + process.env.INFISICAL_ENVIRONMENT ?? ctxEnv.INFISICAL_ENVIRONMENT, + }); diff --git a/server/src/external/logtail/logtailUtils.ts b/server/src/external/logtail/logtailUtils.ts index 29fa9ca91..d110eb816 100644 --- a/server/src/external/logtail/logtailUtils.ts +++ b/server/src/external/logtail/logtailUtils.ts @@ -1,5 +1,6 @@ import "dotenv/config"; +import type pino from "pino"; import { initLogger } from "@/utils/logging/initLogger"; const pinoLogger = initLogger(); @@ -72,34 +73,45 @@ const createLogMethod = (pinoMethod: any, logtailMethod?: any) => { }; }; -export const createLogger = () => { - // Helper function to create logger structure recursively - const createLoggerStructure = (basePinoLogger: any) => { - return { - debug: createLogMethod(basePinoLogger.debug.bind(basePinoLogger)), - info: createLogMethod(basePinoLogger.info.bind(basePinoLogger)), - warn: createLogMethod(basePinoLogger.warn.bind(basePinoLogger)), - error: createLogMethod(basePinoLogger.error.bind(basePinoLogger)), - child: ({ - context, - onlyProd = false, - }: { - context: any; - onlyProd?: boolean; - }) => { - if (onlyProd && process.env.NODE_ENV !== "production") { - return createLoggerStructure(basePinoLogger); - } +const createLoggerStructure = (basePinoLogger: pino.Logger): Logger => ({ + debug: createLogMethod(basePinoLogger.debug.bind(basePinoLogger)), + info: createLogMethod(basePinoLogger.info.bind(basePinoLogger)), + warn: createLogMethod(basePinoLogger.warn.bind(basePinoLogger)), + error: createLogMethod(basePinoLogger.error.bind(basePinoLogger)), + child: ({ + context, + onlyProd = false, + }: { + context: any; + onlyProd?: boolean; + }) => { + if (onlyProd && process.env.NODE_ENV !== "production") { + return createLoggerStructure(basePinoLogger); + } - const childPinoLogger = basePinoLogger.child(context); - return createLoggerStructure(childPinoLogger); - }, - }; - }; + const childPinoLogger = basePinoLogger.child(context); + return createLoggerStructure(childPinoLogger); + }, +}); - // Create the root logger using the helper function - return createLoggerStructure(pinoLogger); +export const createLogger = () => createLoggerStructure(pinoLogger); + +/** + * Lazy dual-output logger (stdout JSON + axiom). Used only by long-running + * trigger.dev tasks so their lines surface in both the trigger run UI and + * our axiom store. Default `logger` / `createLogger` are unaffected. + */ +let dualPinoLogger: pino.Logger | null = null; +export const createDualLogger = () => { + if (!dualPinoLogger) dualPinoLogger = initLogger({ mode: "dual" }); + return createLoggerStructure(dualPinoLogger); }; export const logger = createLogger(); -export type Logger = ReturnType; +export type Logger = { + debug: (...args: any[]) => void; + info: (...args: any[]) => void; + warn: (...args: any[]) => void; + error: (...args: any[]) => void; + child: (args: { context: any; onlyProd?: boolean }) => Logger; +}; diff --git a/server/src/external/redis/initRedisV2.ts b/server/src/external/redis/initRedisV2.ts index 59544112f..49fd6f09b 100644 --- a/server/src/external/redis/initRedisV2.ts +++ b/server/src/external/redis/initRedisV2.ts @@ -8,24 +8,19 @@ import { waitForRedisReady, } from "./initRedis.js"; import { - getRedisV2ConnectionConfig, REDIS_V2_COMMAND_TIMEOUT_MS, supportsUpstashShebangForRedisV2, } from "./initUtils/redisV2Config.js"; -const redisV2Config = getRedisV2ConnectionConfig({ - cacheV2Url: process.env.CACHE_V2_UPSTASH_URL, - primaryCacheUrl: process.env.CACHE_URL, - currentRegion, +export const redisV2: Redis = createRedisConnection({ + cacheUrl: process.env.CACHE_V2_DRAGONFLY_URL?.trim() || "", + region: `${currentRegion}:v2`, + supportsUpstashShebang: false, + commandTimeout: REDIS_V2_COMMAND_TIMEOUT_MS, }); -export const hasRedisV2Config = Boolean(redisV2Config); - -export const redisV2: Redis = redisV2Config - ? createRedisConnection(redisV2Config) - : redis; - const alternateInstanceUrls: Partial> = { + upstash: process.env.CACHE_V2_UPSTASH_URL?.trim() || undefined, redis: process.env.CACHE_V2_REDIS_URL?.trim() || undefined, dragonfly: process.env.CACHE_V2_DRAGONFLY_URL?.trim() || undefined, }; diff --git a/server/src/external/redis/initUtils/createRedisClient.ts b/server/src/external/redis/initUtils/createRedisClient.ts index 5858700c5..098cd4d49 100644 --- a/server/src/external/redis/initUtils/createRedisClient.ts +++ b/server/src/external/redis/initUtils/createRedisClient.ts @@ -6,6 +6,15 @@ import { registerRedisCommands } from "./registerRedisCommands.js"; const REDIS_COMMAND_TIMEOUT_MS = process.env.NODE_ENV === "production" ? 10_000 : 60_000; +const formatRedisEndpoint = ({ cacheUrl }: { cacheUrl: string }) => { + try { + const url = new URL(cacheUrl); + return `${url.protocol}//${url.host}`; + } catch { + return ""; + } +}; + /** Create a Redis connection for a specific region. * `supportsUpstashShebang` defaults to true; set false for non-Upstash * providers (ElastiCache, Dragonfly, self-hosted) that reject the @@ -13,7 +22,7 @@ const REDIS_COMMAND_TIMEOUT_MS = export const createRedisClient = ({ cacheUrl, region, - supportsUpstashShebang = true, + supportsUpstashShebang = false, commandTimeout = REDIS_COMMAND_TIMEOUT_MS, }: { cacheUrl: string; @@ -21,6 +30,10 @@ export const createRedisClient = ({ supportsUpstashShebang?: boolean; commandTimeout?: number; }): Redis => { + console.log( + `[Redis] ${region}: connecting to ${formatRedisEndpoint({ cacheUrl })}`, + ); + const instance = new Redis(cacheUrl, { tls: process.env.CACHE_CERT && !cacheBackupUrl diff --git a/server/src/external/redis/initUtils/redisV2Availability.ts b/server/src/external/redis/initUtils/redisV2Availability.ts index 9784ea723..d179c825e 100644 --- a/server/src/external/redis/initUtils/redisV2Availability.ts +++ b/server/src/external/redis/initUtils/redisV2Availability.ts @@ -1,15 +1,9 @@ -import { - hasRedisV2Config, - redisV2, -} from "../initRedisV2.js"; +import { redisV2 } from "../initRedisV2.js"; import { createRedisAvailability, type RedisAvailabilitySnapshot, } from "./createRedisAvailability.js"; -import { - getRedisAvailability, - shouldUseRedis, -} from "./redisAvailability.js"; +import { getRedisAvailability, shouldUseRedis } from "./redisAvailability.js"; import { redis as primaryRedis } from "./redisClientRegistry.js"; const usesPrimaryRedis = redisV2 === primaryRedis; @@ -18,7 +12,7 @@ const getPrimaryBackedRedisV2Availability = (): RedisAvailabilitySnapshot => { const availability = getRedisAvailability(); return { - configured: hasRedisV2Config, + configured: true, state: availability.state, status: availability.status, }; @@ -34,7 +28,7 @@ const redisV2Availability = usesPrimaryRedis } : createRedisAvailability({ redis: redisV2, - hasConfig: hasRedisV2Config, + hasConfig: true, logPrefix: "RedisV2", logType: "redis_v2_availability_state_set", }); @@ -50,7 +44,7 @@ const { export { getRedisV2Availability, primeRedisV2Monitor, + shouldUseRedisV2, startRedisV2Monitor, stopRedisV2Monitor, - shouldUseRedisV2, }; diff --git a/server/src/external/redis/initUtils/redisV2Config.ts b/server/src/external/redis/initUtils/redisV2Config.ts index 23e293255..ae8d44506 100644 --- a/server/src/external/redis/initUtils/redisV2Config.ts +++ b/server/src/external/redis/initUtils/redisV2Config.ts @@ -1,21 +1,22 @@ import type { RedisV2InstanceName } from "@/internal/misc/redisV2Cache/redisV2CacheSchemas.js"; -export const REDIS_V2_COMMAND_TIMEOUT_MS = 1_000; +export const REDIS_V2_COMMAND_TIMEOUT_MS = + process.env.NODE_ENV === "production" ? 1_000 : 10_000; export const getRedisV2ConnectionConfig = ({ cacheV2Url, - primaryCacheUrl, currentRegion, + instanceName, }: { cacheV2Url?: string; - primaryCacheUrl?: string; currentRegion: string; + instanceName: RedisV2InstanceName; }) => - cacheV2Url?.trim() && cacheV2Url.trim() !== primaryCacheUrl?.trim() + cacheV2Url?.trim() ? { cacheUrl: cacheV2Url.trim(), region: `${currentRegion}:v2`, - supportsUpstashShebang: supportsUpstashShebangForRedisV2("upstash"), + supportsUpstashShebang: supportsUpstashShebangForRedisV2(instanceName), commandTimeout: REDIS_V2_COMMAND_TIMEOUT_MS, } : null; diff --git a/server/src/external/redis/resolveRedisV2.ts b/server/src/external/redis/resolveRedisV2.ts index 38f2f4098..14d91f697 100644 --- a/server/src/external/redis/resolveRedisV2.ts +++ b/server/src/external/redis/resolveRedisV2.ts @@ -22,7 +22,7 @@ export const resolveRedisV2 = (): Redis => { lastLoggedInstance = activeInstance; } - if (activeInstance === "upstash") return redisV2Primary; + if (activeInstance === "dragonfly") return redisV2Primary; const alternate = getAlternateRedisV2Instance(activeInstance); return alternate ?? redisV2Primary; diff --git a/server/src/external/stripe/webhookHandlers/common/subscriptionSync/shouldSkipSubscriptionSync.ts b/server/src/external/stripe/webhookHandlers/common/subscriptionSync/shouldSkipSubscriptionSync.ts index b0b65cdb6..598913e0f 100644 --- a/server/src/external/stripe/webhookHandlers/common/subscriptionSync/shouldSkipSubscriptionSync.ts +++ b/server/src/external/stripe/webhookHandlers/common/subscriptionSync/shouldSkipSubscriptionSync.ts @@ -19,9 +19,14 @@ export type SkipSubscriptionSyncResult = export const shouldSkipSubscriptionSync = ({ subscription, fullCustomer, + requireRecent = true, }: { subscription: Stripe.Subscription; fullCustomer: FullCustomer; + /** For sub.created, pass false: any prior Autumn management is enough to + * skip. For sub.updated (default), only a recent stamp suppresses sync so + * later genuine changes still get picked up. */ + requireRecent?: boolean; }): SkipSubscriptionSyncResult => { const alreadyLinked = fullCustomer.customer_products?.some( (customerProduct) => @@ -33,6 +38,7 @@ export const shouldSkipSubscriptionSync = ({ const metadataDecision = isAutumnManagedSubscriptionMetadata({ metadata: subscription.metadata, + requireRecent, }); if (metadataDecision.skip) { return { skip: true, reason: metadataDecision.reason ?? "autumn metadata" }; diff --git a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionCreated/setupStripeSubscriptionCreatedContext.ts b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionCreated/setupStripeSubscriptionCreatedContext.ts index 68d01001f..b29f7e699 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionCreated/setupStripeSubscriptionCreatedContext.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionCreated/setupStripeSubscriptionCreatedContext.ts @@ -3,7 +3,6 @@ import type Stripe from "stripe"; import { ProductService } from "@/internal/products/ProductService.js"; import { getFullStripeSub } from "../../stripeSubUtils.js"; import type { StripeWebhookContext } from "../../webhookMiddlewares/stripeWebhookContext.js"; -import { shouldSkipSubscriptionSync } from "../common/subscriptionSync/shouldSkipSubscriptionSync.js"; export type StripeSubscriptionCreatedContext = { subscription: Stripe.Subscription; @@ -16,7 +15,7 @@ export const setupStripeSubscriptionCreatedContext = async ({ }: { ctx: StripeWebhookContext; }): Promise => { - const { db, org, env, fullCustomer, stripeCli, stripeEvent, logger } = ctx; + const { db, org, env, fullCustomer, stripeCli, stripeEvent } = ctx; const stripeObject = stripeEvent.data.object as Stripe.Subscription; // No auto-provisioning — only sync subs for customers already in Autumn. @@ -27,13 +26,5 @@ export const setupStripeSubscriptionCreatedContext = async ({ ProductService.listFull({ db, orgId: org.id, env }), ]); - const skip = shouldSkipSubscriptionSync({ subscription, fullCustomer }); - if (skip.skip) { - logger.info( - `sub.created auto-sync: skipping stripe sub ${subscription.id} (${skip.reason})`, - ); - return undefined; - } - return { subscription, fullCustomer, candidateProducts }; }; diff --git a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionCreated/tasks/autoSyncFromSubscription.ts b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionCreated/tasks/autoSyncFromSubscription.ts index b899de6ad..0e5fac446 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionCreated/tasks/autoSyncFromSubscription.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionCreated/tasks/autoSyncFromSubscription.ts @@ -2,6 +2,8 @@ import type { StripeWebhookContext } from "@/external/stripe/webhookMiddlewares/ import { billingActions } from "@/internal/billing/v2/actions"; import { canAutoSync } from "@/internal/billing/v2/actions/sync/canAutoSync.js"; import { subscriptionToSyncParams } from "@/internal/billing/v2/actions/sync/subscriptionToSyncParams.js"; +import { isAutumnCheckoutSubscription } from "@/internal/billing/v2/actions/sync/utils/isAutumnCheckoutSubscription.js"; +import { shouldSkipSubscriptionSync } from "../../common/subscriptionSync/shouldSkipSubscriptionSync.js"; import type { StripeSubscriptionCreatedContext } from "../setupStripeSubscriptionCreatedContext.js"; /** @@ -21,10 +23,29 @@ export const autoSyncFromSubscription = async ({ ctx: StripeWebhookContext; subscriptionCreatedContext: StripeSubscriptionCreatedContext; }) => { - const { logger } = ctx; + const { logger, stripeCli } = ctx; const { subscription, fullCustomer } = subscriptionCreatedContext; const customerId = fullCustomer.id ?? fullCustomer.internal_id; + const skip = shouldSkipSubscriptionSync({ + subscription, + fullCustomer, + requireRecent: false, + }); + if (skip.skip) { + logger.info( + `sub.created auto-sync skipping ${subscription.id} (${skip.reason})`, + ); + return; + } + + if (await isAutumnCheckoutSubscription({ stripeCli, subscription })) { + logger.info( + `sub.created auto-sync skipping ${subscription.id}: originated from Autumn checkout session`, + ); + return; + } + const { match, params } = await subscriptionToSyncParams({ ctx, customerId, diff --git a/server/src/external/tinybird/initTinybird.ts b/server/src/external/tinybird/initTinybird.ts index 25468e99e..82c7eeb5e 100644 --- a/server/src/external/tinybird/initTinybird.ts +++ b/server/src/external/tinybird/initTinybird.ts @@ -5,22 +5,16 @@ import { createAggregateSimplePipe } from "./pipes/aggregateSimplePipe.js"; import { createEstimatedMrrPipe } from "./pipes/estimatedMrrPipe.js"; import { createListEventNamesPipe } from "./pipes/listEventNamesPipe.js"; import { createListEventsPaginatedPipe } from "./pipes/listEventsPaginatedPipe.js"; +import { tinybirdConfig } from "./tinybirdUtils.js"; import { z } from "./tinybirdZod.js"; -const TINYBIRD_API_URL = process.env.TINYBIRD_API_URL; -const TINYBIRD_TOKEN = process.env.TINYBIRD_TOKEN; - /** Tinybird REST API client singleton. Null if not configured. */ -const tinybirdClient: Tinybird | null = - TINYBIRD_API_URL && TINYBIRD_TOKEN - ? new Tinybird({ - baseUrl: TINYBIRD_API_URL, - token: TINYBIRD_TOKEN, - }) - : null; +const tinybirdClient: Tinybird | null = tinybirdConfig + ? new Tinybird(tinybirdConfig) + : null; -if (tinybirdClient) { - console.log(`[Tinybird] Configured with URL: ${TINYBIRD_API_URL}`); +if (tinybirdConfig) { + console.log(`[Tinybird] Configured with URL: ${tinybirdConfig.baseUrl}`); } /** Zod schema for TinybirdEvent (matches events.datasource) */ @@ -82,11 +76,6 @@ export const getTinybirdIngest = () => { return tinybirdIngest; }; -/** Check if Tinybird is configured */ -export const isTinybirdConfigured = (): boolean => { - return tinybirdClient !== null; -}; - // Re-export types export type { AggregateGroupablePipeParams, diff --git a/server/src/external/tinybird/migrations/migrationItemEventsDataSource.ts b/server/src/external/tinybird/migrations/migrationItemEventsDataSource.ts new file mode 100644 index 000000000..dfa9a7b3d --- /dev/null +++ b/server/src/external/tinybird/migrations/migrationItemEventsDataSource.ts @@ -0,0 +1,124 @@ +import { + defineDatasource, + defineEndpoint, + engine, + type InferRow, + node, + p, + Tinybird, + t, +} from "@tinybirdco/sdk"; +import { tinybirdConfig } from "../tinybirdUtils.js"; + +export type MigrationItemEventStatus = "succeeded" | "skipped" | "failed"; + +export type MigrationItemPreview = { + id?: string | null; + name?: string | null; + email?: string | null; +}; + +export type MigrationItemEventResponse = Record | null; + +export const migrationItemEventsDatasource = defineDatasource( + "migration_item_events", + { + description: "One audit result row per migration item run.", + schema: { + timestamp: t.dateTime64(6), + org_id: t.string(), + env: t.string().lowCardinality(), + migration_internal_id: t.string(), + migration_run_id: t.string(), + dry_run: t.bool(), + item_kind: t.string().lowCardinality(), + item_id: t.string(), + item_preview: t.json(), + status: t.string().lowCardinality(), + response: t.json(), + }, + engine: engine.mergeTree({ + partitionKey: "toYYYYMM(timestamp)", + sortingKey: [ + "org_id", + "env", + "migration_run_id", + "item_kind", + "item_id", + "timestamp", + ], + }), + }, +); + +export type TinybirdMigrationItemEvent = InferRow< + typeof migrationItemEventsDatasource +>; + +export const listMigrationItemEventsEndpoint = defineEndpoint( + "list_migration_item_events", + { + description: "List audit result rows for a migration run.", + params: { + org_id: p.string(), + env: p.string(), + migration_internal_id: p.string(), + migration_run_id: p.string().optional(""), + limit: p.int32().optional(1000), + }, + nodes: [ + node({ + name: "endpoint", + sql: ` + SELECT + timestamp, + org_id, + env, + migration_internal_id, + migration_run_id, + dry_run, + item_kind, + item_id, + item_preview, + status, + response + FROM migration_item_events + WHERE org_id = {{String(org_id)}} + AND env = {{String(env)}} + AND migration_internal_id = {{String(migration_internal_id)}} + {% if defined(migration_run_id) and String(migration_run_id, '') != '' %} + AND migration_run_id = {{String(migration_run_id)}} + {% end %} + ORDER BY timestamp DESC, item_kind ASC, item_id ASC + LIMIT {{Int32(limit, 1000)}} + `, + }), + ], + output: { + timestamp: t.dateTime64(6), + org_id: t.string(), + env: t.string().lowCardinality(), + migration_internal_id: t.string(), + migration_run_id: t.string(), + dry_run: t.bool(), + item_kind: t.string().lowCardinality(), + item_id: t.string(), + item_preview: t.json(), + status: t.string().lowCardinality(), + response: t.json(), + }, + }, +); + +export const migrationTinybird = tinybirdConfig + ? new Tinybird({ + datasources: { + itemEvents: migrationItemEventsDatasource, + }, + pipes: { + listItemEvents: listMigrationItemEventsEndpoint, + }, + ...tinybirdConfig, + devMode: false, + }) + : null; diff --git a/server/src/external/tinybird/sendEvents/sendEvents.ts b/server/src/external/tinybird/sendEvents/sendEvents.ts index 719966947..dc6b491cb 100644 --- a/server/src/external/tinybird/sendEvents/sendEvents.ts +++ b/server/src/external/tinybird/sendEvents/sendEvents.ts @@ -2,7 +2,8 @@ import * as crypto from "node:crypto"; import type { EventInsert } from "@autumn/shared"; import * as Sentry from "@sentry/bun"; import type { Logger } from "@/external/logtail/logtailUtils.js"; -import { isTinybirdConfigured, tinybirdIngest } from "../initTinybird.js"; +import { tinybirdIngest } from "../initTinybird.js"; +import { isTinybirdConfigured } from "../tinybirdUtils.js"; import { mapToTinybirdEvent } from "./mapEvent.js"; /** Generate a unique error ID for tracking */ diff --git a/server/src/external/tinybird/tinybirdUtils.ts b/server/src/external/tinybird/tinybirdUtils.ts index 40fd7ef9d..b5d10c720 100644 --- a/server/src/external/tinybird/tinybirdUtils.ts +++ b/server/src/external/tinybird/tinybirdUtils.ts @@ -1,9 +1,28 @@ import { ErrCode, RecaseError } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; +const TINYBIRD_API_URL = process.env.TINYBIRD_API_URL; +const TINYBIRD_TOKEN = process.env.TINYBIRD_TOKEN; + +export type TinybirdConfig = { + baseUrl: string; + token: string; +}; + +export const tinybirdConfig: TinybirdConfig | null = + TINYBIRD_API_URL && TINYBIRD_TOKEN + ? { + baseUrl: TINYBIRD_API_URL, + token: TINYBIRD_TOKEN, + } + : null; + +/** Check if Tinybird is configured. */ +export const isTinybirdConfigured = (): boolean => tinybirdConfig !== null; + /** Throws SERVICE_UNAVAILABLE if Tinybird is not configured. */ export const assertTinybirdAvailable = () => { - if (!process.env.TINYBIRD_TOKEN) { + if (!isTinybirdConfigured()) { throw new RecaseError({ message: "Tinybird is not configured, cannot fetch analytics", code: ErrCode.TinybirdDisabled, diff --git a/server/src/honoUtils/HonoEnv.ts b/server/src/honoUtils/HonoEnv.ts index 68985d8e8..55644eef3 100644 --- a/server/src/honoUtils/HonoEnv.ts +++ b/server/src/honoUtils/HonoEnv.ts @@ -61,6 +61,13 @@ export type RequestContext = { expand: string[]; skipCache: boolean; + /** True when the context is built by `createTriggerContext` — i.e. we're + * executing inside a Trigger.dev task. Read by `checkPendingMigrationsForCustomer` + * to short-circuit: a migration task loads `CusService.getFull` / + * `getFullSubject` for its target customer, and that load must NOT + * re-enqueue another migration task. */ + insideTriggerTask?: boolean; + extraLogs: Record; fullCustomer?: FullCustomer; diff --git a/server/src/init.ts b/server/src/init.ts index 166cfec84..934caed93 100644 --- a/server/src/init.ts +++ b/server/src/init.ts @@ -26,6 +26,8 @@ import "./internal/misc/edgeConfig/orgLimitsStore.js"; import "./internal/misc/stripeSync/stripeSyncStore.js"; import "./internal/misc/redisV2Cache/redisV2CacheStore.js"; import "./internal/misc/jobQueues/jobQueueStore.js"; +// Side-effect: configures trigger.dev SDK to use TRIGGER_SERVER_SECRET_KEY. +import "./trigger/configureTrigger.js"; import { closeStripeSyncEngine } from "@autumn/stripe-sync"; import { startRedisMonitor, @@ -51,8 +53,6 @@ let shuttingDown = false; const init = async ({ startupStartedAt }: { startupStartedAt: number }) => { logger.info(getRedactedDatabaseUrls(), "DB URLs"); - console.log("DB URLs:", getRedactedDatabaseUrls()); - const app = createHonoApp(); initPgHealthMonitor({ client: clientCritical }); diff --git a/server/src/internal/billing/v2/actions/sync/utils/isAutumnCheckoutSubscription.ts b/server/src/internal/billing/v2/actions/sync/utils/isAutumnCheckoutSubscription.ts new file mode 100644 index 000000000..6024f0b14 --- /dev/null +++ b/server/src/internal/billing/v2/actions/sync/utils/isAutumnCheckoutSubscription.ts @@ -0,0 +1,23 @@ +import type Stripe from "stripe"; + +/** + * True when `subscription` was created by an Autumn-managed Checkout Session. + * + * Why: `checkout.session.completed` materializes the cus_product itself, so + * auto-sync from `customer.subscription.created` would race and produce a + * duplicate row on the same Stripe sub. + */ +export const isAutumnCheckoutSubscription = async ({ + stripeCli, + subscription, +}: { + stripeCli: Stripe; + subscription: Stripe.Subscription; +}): Promise => { + const sessions = await stripeCli.checkout.sessions.list({ + subscription: subscription.id, + limit: 1, + }); + const session = sessions.data[0]; + return Boolean(session?.metadata?.autumn_metadata_id); +}; diff --git a/server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelPlan.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelPlan.ts index e648f0773..9d1344596 100644 --- a/server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelPlan.ts +++ b/server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelPlan.ts @@ -1,7 +1,11 @@ import { type AutumnBillingPlan, + CusProductStatus, cp, type FullCusProduct, + findMainActiveCustomerProductByGroup, + isCustomerProductCanceling, + isFutureStartDate, type UpdateSubscriptionBillingContext, } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; @@ -39,6 +43,68 @@ const computeScheduledAddOnsToDelete = ({ }); }; +const shouldDeleteCustomerProductBeforeBillingStarts = ({ + customerProduct, + currentEpochMs, +}: { + customerProduct: FullCusProduct; + currentEpochMs: number; +}): boolean => { + if (customerProduct.status === CusProductStatus.Scheduled) return true; + + const hasStripeSchedule = (customerProduct.scheduled_ids?.length ?? 0) > 0; + const hasStripeSubscription = + (customerProduct.subscription_ids?.length ?? 0) > 0; + + return ( + hasStripeSchedule && + !hasStripeSubscription && + isFutureStartDate(customerProduct.starts_at, currentEpochMs) + ); +}; + +const computeScheduledCancelPlan = ({ + billingContext, + plan, +}: { + billingContext: UpdateSubscriptionBillingContext; + plan: AutumnBillingPlan; +}): AutumnBillingPlan => { + const { customerProduct, fullCustomer } = billingContext; + + const activeCustomerProduct = findMainActiveCustomerProductByGroup({ + fullCus: fullCustomer, + productGroup: customerProduct.product.group, + internalEntityId: customerProduct.internal_entity_id ?? undefined, + }); + + const scheduledCancelPlan: AutumnBillingPlan = { + ...plan, + updateCustomerProduct: undefined, + deleteCustomerProduct: customerProduct, + }; + + if ( + !activeCustomerProduct || + activeCustomerProduct.id === customerProduct.id || + !isCustomerProductCanceling(activeCustomerProduct) + ) { + return scheduledCancelPlan; + } + + return { + ...scheduledCancelPlan, + updateCustomerProduct: { + customerProduct: activeCustomerProduct, + updates: { + canceled: false, + canceled_at: null, + ended_at: null, + }, + }, + }; +}; + /** * Computes and applies the cancel plan for a subscription. * @@ -64,6 +130,18 @@ export const computeCancelPlan = ({ }); } + if ( + shouldDeleteCustomerProductBeforeBillingStarts({ + customerProduct: billingContext.customerProduct, + currentEpochMs: billingContext.currentEpochMs, + }) + ) { + return computeScheduledCancelPlan({ + billingContext, + plan, + }); + } + // Step 1: Calculate when the subscription ends const endOfCycleMs = computeEndOfCycleMs({ billingContext }); diff --git a/server/src/internal/billing/v2/actions/updateSubscription/errors/handleCurrentCustomerProductErrors.ts b/server/src/internal/billing/v2/actions/updateSubscription/errors/handleCurrentCustomerProductErrors.ts index acf4a4a4e..3b11ee082 100644 --- a/server/src/internal/billing/v2/actions/updateSubscription/errors/handleCurrentCustomerProductErrors.ts +++ b/server/src/internal/billing/v2/actions/updateSubscription/errors/handleCurrentCustomerProductErrors.ts @@ -12,7 +12,10 @@ export const handleCurrentCustomerProductErrors = ({ }) => { const { customerProduct } = billingContext; - if (isCustomerProductScheduled(customerProduct)) { + if ( + isCustomerProductScheduled(customerProduct) && + !billingContext.cancelAction + ) { throw new RecaseError({ message: `Cannot update subscription for '${customerProduct.product.name}' because it is scheduled and not yet active`, }); diff --git a/server/src/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionErrors.ts b/server/src/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionErrors.ts index 5d81dfdd1..38848a06a 100644 --- a/server/src/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionErrors.ts +++ b/server/src/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionErrors.ts @@ -85,5 +85,5 @@ export const handleUpdateSubscriptionErrors = async ({ handleUpdateCheckoutErrors({ billingContext }); // 12. Stripe billing plan errors (validate Stripe resources) - handleStripeBillingPlanErrors({ billingContext }); + handleStripeBillingPlanErrors({ billingContext, billingPlan }); }; diff --git a/server/src/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionProductContext.ts b/server/src/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionProductContext.ts index afe66f099..aa5c46f7b 100644 --- a/server/src/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionProductContext.ts +++ b/server/src/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionProductContext.ts @@ -8,7 +8,10 @@ import { type UpdateSubscriptionV1Params, } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import { setupPatchContext } from "@/internal/billing/v2/setup/patch"; +import { + type ReusePricesAndEntitlements, + setupPatchContext, +} from "@/internal/billing/v2/setup/patch"; import { ProductService } from "@/internal/products/ProductService"; import { setupCustomFullProduct } from "../../../setup/setupCustomFullProduct"; import { findTargetCustomerProduct } from "./findTargetCustomerProduct"; @@ -18,11 +21,13 @@ export const setupUpdateSubscriptionProductContext = async ({ fullCustomer, params, contextOverride = {}, + reusePricesAndEntitlements, }: { ctx: AutumnContext; fullCustomer: FullCustomer; params: UpdateSubscriptionV1Params; contextOverride?: UpdateSubscriptionBillingContextOverride; + reusePricesAndEntitlements?: ReusePricesAndEntitlements; }) => { const { productContext } = contextOverride; @@ -63,6 +68,7 @@ export const setupUpdateSubscriptionProductContext = async ({ params, customerProduct: targetCustomerProduct, fullProduct, + reusePricesAndEntitlements, }); const { diff --git a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts index 9e7d082bc..1595c8571 100644 --- a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts @@ -91,12 +91,40 @@ const getScheduleScenario = ({ return "multi_phase"; }; +const buildNoPhasesAction = ({ + hasSubscription, + scheduleId, +}: { + hasSubscription: boolean; + scheduleId: string | undefined; +}): StripeSubscriptionScheduleResult => { + if (!scheduleId) return {}; + + if (hasSubscription) { + return { + scheduleAction: { + type: "release", + stripeSubscriptionScheduleId: scheduleId, + }, + subscriptionCancelAt: null, + }; + } + + return { + scheduleAction: { + type: "cancel", + stripeSubscriptionScheduleId: scheduleId, + }, + }; +}; + /** * Builds the appropriate action for each scenario. */ const buildActionForScenario = ({ scenario, hasSchedule, + hasSubscription, scheduleId, scheduledPhases, cancelAtSeconds, @@ -105,6 +133,7 @@ const buildActionForScenario = ({ }: { scenario: ScheduleScenario; hasSchedule: boolean; + hasSubscription: boolean; scheduleId: string | undefined; scheduledPhases: Stripe.SubscriptionScheduleUpdateParams.Phase[]; cancelAtSeconds: number | undefined; @@ -113,7 +142,10 @@ const buildActionForScenario = ({ }): StripeSubscriptionScheduleResult => { switch (scenario) { case "no_phases": - return {}; + return buildNoPhasesAction({ + hasSubscription, + scheduleId, + }); case "single_indefinite": // Product continues indefinitely: release schedule if exists, clear any cancel_at @@ -285,6 +317,7 @@ export const buildStripeSubscriptionScheduleAction = ({ return buildActionForScenario({ scenario, hasSchedule: !!stripeSubscriptionSchedule, + hasSubscription: !!stripeSubscription, scheduleId: stripeSubscriptionSchedule?.id, scheduledPhases, cancelAtSeconds, diff --git a/server/src/internal/billing/v2/providers/stripe/errors/assertStripePlanNoCharges.ts b/server/src/internal/billing/v2/providers/stripe/errors/assertStripePlanNoCharges.ts new file mode 100644 index 000000000..6c47188b2 --- /dev/null +++ b/server/src/internal/billing/v2/providers/stripe/errors/assertStripePlanNoCharges.ts @@ -0,0 +1,90 @@ +import { RecaseError, type StripeBillingPlan } from "@autumn/shared"; +import { + billingPlanWillCharge, + getChargeReasonMessage, +} from "@/internal/billing/v2/utils/billingPlan/billingPlanWillCharge.js"; + +export type StripePlanNoChargesViolation = { + message: string; + details: Record; +}; + +export const hasStripePlanActions = (stripeBillingPlan: StripeBillingPlan) => + Object.values(stripeBillingPlan).some((action) => action !== undefined); + +const getStripePlanNoChargesViolation = ({ + stripeBillingPlan, + subscriptionId, +}: { + stripeBillingPlan: StripeBillingPlan; + subscriptionId?: string; +}): StripePlanNoChargesViolation | undefined => { + const details = subscriptionId ? { subscriptionId } : {}; + const chargeResult = billingPlanWillCharge({ + billingPlan: { stripe: stripeBillingPlan }, + }); + + if (chargeResult.willCharge) { + return { + message: `Stripe billing plan will charge because ${getChargeReasonMessage(chargeResult.reason)}`, + details, + }; + } + + if (stripeBillingPlan.invoiceAction) { + return { + message: "Stripe billing plan produced an invoice action", + details, + }; + } + + if (stripeBillingPlan.refundAction) { + return { + message: "Stripe billing plan produced a refund action", + details, + }; + } + + const { subscriptionAction } = stripeBillingPlan; + if (subscriptionAction?.type === "create") { + return { + message: "Stripe billing plan produced a subscription create action", + details, + }; + } + + if ( + subscriptionAction?.type === "update" && + subscriptionAction.params.proration_behavior !== "none" + ) { + return { + message: + "Stripe billing plan produced a subscription update without proration_behavior: none", + details, + }; + } + + return undefined; +}; + +export const assertStripePlanNoCharges = ({ + stripeBillingPlan, + subscriptionId, + createError = (violation) => + new RecaseError({ + message: violation.message, + data: violation.details, + }), +}: { + stripeBillingPlan: StripeBillingPlan; + subscriptionId?: string; + createError?: (violation: StripePlanNoChargesViolation) => Error; +}) => { + const violation = getStripePlanNoChargesViolation({ + stripeBillingPlan, + subscriptionId, + }); + if (!violation) return; + + throw createError(violation); +}; diff --git a/server/src/internal/billing/v2/providers/stripe/errors/handleStripeBillingPlanErrors.ts b/server/src/internal/billing/v2/providers/stripe/errors/handleStripeBillingPlanErrors.ts index 270e0c77a..c992a013f 100644 --- a/server/src/internal/billing/v2/providers/stripe/errors/handleStripeBillingPlanErrors.ts +++ b/server/src/internal/billing/v2/providers/stripe/errors/handleStripeBillingPlanErrors.ts @@ -1,5 +1,8 @@ +import type { + BillingPlan, + UpdateSubscriptionBillingContext, +} from "@autumn/shared"; import { ErrCode, InternalError } from "@autumn/shared"; -import type { UpdateSubscriptionBillingContext } from "@autumn/shared"; /** * Validates Stripe-specific billing context requirements before executing billing plan. @@ -7,22 +10,24 @@ import type { UpdateSubscriptionBillingContext } from "@autumn/shared"; */ export const handleStripeBillingPlanErrors = ({ billingContext, + billingPlan, }: { billingContext: UpdateSubscriptionBillingContext; + billingPlan: BillingPlan; }) => { - // If there's an existing subscription schedule, validate it has current_phase.start_date - // This is required for schedule updates (Stripe requires anchoring phases to the current phase start) + const { stripeSubscriptionSchedule } = billingContext; + const { subscriptionScheduleAction } = billingPlan.stripe; - if (billingContext.stripeSubscriptionSchedule) { - const currentPhaseStart = - billingContext.stripeSubscriptionSchedule.current_phase?.start_date; + if (subscriptionScheduleAction?.type !== "update") return; + if (!stripeSubscriptionSchedule?.subscription) return; - if (!currentPhaseStart) { - throw new InternalError({ - message: - "Cannot update subscription schedule: missing current phase start_date", - code: ErrCode.InternalError, - }); - } + const currentPhaseStart = + stripeSubscriptionSchedule.current_phase?.start_date; + if (!currentPhaseStart) { + throw new InternalError({ + message: + "Cannot update subscription schedule: missing current phase start_date", + code: ErrCode.InternalError, + }); } }; diff --git a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts index b9bb44de7..617852e06 100644 --- a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts @@ -257,5 +257,14 @@ export const executeStripeSubscriptionScheduleAction = async ({ subscriptionScheduleAction.stripeSubscriptionScheduleId, ); return null; + + case "cancel": + ctx.logger.debug( + `[executeStripeSubscriptionScheduleAction] Canceling schedule: ${subscriptionScheduleAction.stripeSubscriptionScheduleId}`, + ); + await stripeCli.subscriptionSchedules.cancel( + subscriptionScheduleAction.stripeSubscriptionScheduleId, + ); + return null; } }; diff --git a/server/src/internal/billing/v2/providers/stripe/utils/common/autumnStripeMetadata.ts b/server/src/internal/billing/v2/providers/stripe/utils/common/autumnStripeMetadata.ts index 819135906..bdc85f329 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/common/autumnStripeMetadata.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/common/autumnStripeMetadata.ts @@ -26,14 +26,23 @@ export const buildAutumnSubscriptionMetadata = ({ return meta; }; +/** + * @param requireRecent — when true (default), `autumn_managed_at` only counts + * if it falls within `windowMs`. Used by sub.updated where a stale stamp + * shouldn't suppress a genuinely new change. Pass false from sub.created: + * once a sub has ever been Autumn-managed, auto-sync should never run on + * its creation event. + */ export const isAutumnManagedSubscriptionMetadata = ({ metadata, windowMs = RECENT_AUTUMN_ACTION_WINDOW_MS, now = Date.now(), + requireRecent = true, }: { metadata: Stripe.Metadata | null | undefined; windowMs?: number; now?: number; + requireRecent?: boolean; }): { skip: boolean; reason?: string } => { if (!metadata) return { skip: false }; @@ -49,10 +58,14 @@ export const isAutumnManagedSubscriptionMetadata = ({ if (!managedAtRaw) return { skip: false }; const managedAt = Number(managedAtRaw); - if (!Number.isFinite(managedAt) || now - managedAt >= windowMs) { - return { skip: false }; + if (!Number.isFinite(managedAt)) return { skip: false }; + + if (!requireRecent) { + return { skip: true, reason: `autumn_managed_at present (source=unknown)` }; } + if (now - managedAt >= windowMs) return { skip: false }; + return { skip: true, reason: `recent autumn_managed_at (${now - managedAt}ms ago, source=unknown)`, diff --git a/server/src/internal/billing/v2/providers/stripe/utils/common/initStripeResourcesForProducts.ts b/server/src/internal/billing/v2/providers/stripe/utils/common/initStripeResourcesForProducts.ts index b28802438..134a930c2 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/common/initStripeResourcesForProducts.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/common/initStripeResourcesForProducts.ts @@ -20,6 +20,52 @@ import { import { PriceService } from "@/internal/products/prices/PriceService"; import { checkStripeProductExists } from "@/internal/products/productUtils"; +export const initStripeResourcesForProducts = async ({ + ctx, + products, + internalEntityId, +}: { + ctx: AutumnContext; + products: FullProduct[]; + internalEntityId?: string; +}) => { + const { db, org, env, logger } = ctx; + + const batchProductUpdates = []; + for (const product of products) { + if (product.processor?.id != null) continue; + + batchProductUpdates.push( + checkStripeProductExists({ + db, + org, + env, + product, + logger, + }), + ); + } + await Promise.all(batchProductUpdates); + + const batchPriceUpdates = []; + + for (const product of products) { + for (const price of product.prices) { + batchPriceUpdates.push( + createStripePriceIFNotExist({ + ctx, + price, + entitlements: product.entitlements, + product, + internalEntityId, + useCheckout: false, + }), + ); + } + } + await Promise.all(batchPriceUpdates); +}; + const shouldInitializeStripePrice = ({ price }: { price: Price }) => { if (!isFixedPrice(price)) return true; diff --git a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/logSubscriptionScheduleAction.ts b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/logSubscriptionScheduleAction.ts index eeb068548..9249c9df4 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/logSubscriptionScheduleAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/logSubscriptionScheduleAction.ts @@ -1,7 +1,9 @@ +import type { + BillingContext, + StripeSubscriptionScheduleAction, +} from "@autumn/shared"; import { formatSecondsToDate } from "@autumn/shared"; import type { AutumnContext } from "@server/honoUtils/HonoEnv"; -import type { BillingContext } from "@autumn/shared"; -import type { StripeSubscriptionScheduleAction } from "@autumn/shared"; import type Stripe from "stripe"; import { billingContextFormatPriceByStripePriceId } from "@/internal/billing/v2/utils/billingContextPriceLookup"; @@ -45,7 +47,10 @@ export const logSubscriptionScheduleAction = ({ billingContext: BillingContext; subscriptionScheduleAction: StripeSubscriptionScheduleAction; }): void => { - if (subscriptionScheduleAction.type === "release") { + if ( + subscriptionScheduleAction.type === "release" || + subscriptionScheduleAction.type === "cancel" + ) { ctx.logger.debug( `[logSubscriptionScheduleAction] Action type: ${subscriptionScheduleAction.type}`, ); diff --git a/server/src/internal/billing/v2/setup/patch/handleCustomizeAddItems.ts b/server/src/internal/billing/v2/setup/patch/handleCustomizeAddItems.ts index 99a0798e3..2ba52b657 100644 --- a/server/src/internal/billing/v2/setup/patch/handleCustomizeAddItems.ts +++ b/server/src/internal/billing/v2/setup/patch/handleCustomizeAddItems.ts @@ -7,15 +7,18 @@ import type { SharedContext, } from "@autumn/shared"; import { planItemV1ToPriceAndEnt } from "@shared/api/products/items/mappers/planItemV1ToPriceAndEnt"; +import type { ReusePricesAndEntitlements } from "./types"; export const handleCustomizeAddItems = ({ ctx, customize, fullProduct, + reusePricesAndEntitlements, }: { ctx: SharedContext; customize: CustomizePlanV1; fullProduct: FullProduct; + reusePricesAndEntitlements?: ReusePricesAndEntitlements; }): { prices: Price[]; entitlements: Entitlement[]; @@ -24,6 +27,19 @@ export const handleCustomizeAddItems = ({ const entitlements: Entitlement[] = []; for (const item of customize.add_items ?? []) { + const overridePrice = item.price_id + ? reusePricesAndEntitlements?.pricesById.get(item.price_id) + : undefined; + const overrideEntitlement = item.entitlement_id + ? reusePricesAndEntitlements?.entitlementsById.get(item.entitlement_id) + : undefined; + + if (overridePrice || overrideEntitlement) { + if (overridePrice) prices.push(overridePrice); + if (overrideEntitlement) entitlements.push(overrideEntitlement); + continue; + } + const { newPrice, newEnt } = planItemV1ToPriceAndEnt({ ctx, item, diff --git a/server/src/internal/billing/v2/setup/patch/handleCustomizePrice.ts b/server/src/internal/billing/v2/setup/patch/handleCustomizePrice.ts index d5799599e..bd45c8744 100644 --- a/server/src/internal/billing/v2/setup/patch/handleCustomizePrice.ts +++ b/server/src/internal/billing/v2/setup/patch/handleCustomizePrice.ts @@ -9,6 +9,7 @@ import type { import { basePriceToProductItem } from "@shared/api/products/components/basePrice/basePriceToProductItem"; import { customerProductToBasePrice } from "@shared/utils/cusProductUtils/convertCusProduct/customerProductToPrice"; import { itemToPriceAndEnt } from "@shared/utils/productV2Utils/productItemUtils/mappers/itemToPriceAndEnt"; +import type { ReusePricesAndEntitlements } from "./types"; const removeCurrentBasePrice = ({ targetCustomerProduct, @@ -41,11 +42,13 @@ export const handleCustomizePrice = ({ customize, targetCustomerProduct, fullProduct, + reusePricesAndEntitlements, }: { ctx: SharedContext; customize: CustomizePlanV1; targetCustomerProduct: FullCusProduct; fullProduct: FullProduct; + reusePricesAndEntitlements?: ReusePricesAndEntitlements; }): { customerPrices: FullCustomerPrice[]; prices: Price[]; @@ -60,18 +63,24 @@ export const handleCustomizePrice = ({ return { customerPrices, prices: [] }; } - const item = basePriceToProductItem({ - ctx, - basePrice: customize.price, - }); - const { newPrice, updatedPrice } = itemToPriceAndEnt({ - item, - orgId: fullProduct.org_id, - internalProductId: fullProduct.internal_id, - isCustom: true, - features: ctx.features, - }); - const price = newPrice ?? updatedPrice; + const overridePrice = customize.price.price_id + ? reusePricesAndEntitlements?.pricesById.get(customize.price.price_id) + : undefined; + let price = overridePrice; + if (!price) { + const item = basePriceToProductItem({ + ctx, + basePrice: customize.price, + }); + const { newPrice, updatedPrice } = itemToPriceAndEnt({ + item, + orgId: fullProduct.org_id, + internalProductId: fullProduct.internal_id, + isCustom: true, + features: ctx.features, + }); + price = newPrice ?? updatedPrice ?? undefined; + } const prices = price ? [price] : []; fullProduct.prices.push(...prices); diff --git a/server/src/internal/billing/v2/setup/patch/index.ts b/server/src/internal/billing/v2/setup/patch/index.ts index c02713267..8a1ddf6d8 100644 --- a/server/src/internal/billing/v2/setup/patch/index.ts +++ b/server/src/internal/billing/v2/setup/patch/index.ts @@ -1,3 +1,4 @@ export * from "./handleCustomizeAddItems"; export * from "./handleCustomizeDeleteItems"; export * from "./setupPatchContext"; +export * from "./types"; diff --git a/server/src/internal/billing/v2/setup/patch/setupPatchContext.ts b/server/src/internal/billing/v2/setup/patch/setupPatchContext.ts index 2a882e9d2..3d118b4d1 100644 --- a/server/src/internal/billing/v2/setup/patch/setupPatchContext.ts +++ b/server/src/internal/billing/v2/setup/patch/setupPatchContext.ts @@ -13,6 +13,7 @@ import { generateId } from "@/utils/genUtils"; import { handleCustomizeAddItems } from "./handleCustomizeAddItems"; import { handleCustomizeDeleteItems } from "./handleCustomizeDeleteItems"; import { handleCustomizePrice } from "./handleCustomizePrice"; +import type { ReusePricesAndEntitlements } from "./types"; const applyProductDefinitionToCustomerProduct = ({ fullProduct, @@ -52,8 +53,8 @@ const applyProductBasePriceToCustomerProduct = ({ const productBasePrice = fullProduct.prices.find(isFixedPrice); if (!productBasePrice) return; - const currentBasePrice = customerProduct.customer_prices.find((customerPrice) => - isFixedPrice(customerPrice.price), + const currentBasePrice = customerProduct.customer_prices.find( + (customerPrice) => isFixedPrice(customerPrice.price), ); if (!currentBasePrice) { @@ -77,11 +78,13 @@ export const setupPatchContext = ({ params, customerProduct, fullProduct, + reusePricesAndEntitlements, }: { ctx: SharedContext; params: UpdateSubscriptionV1Params; customerProduct: FullCusProduct; fullProduct: FullProduct; + reusePricesAndEntitlements?: ReusePricesAndEntitlements; }): PatchContext | undefined => { if (!isCustomizePlanPatchStyle(params.customize)) return undefined; @@ -131,6 +134,7 @@ export const setupPatchContext = ({ customize: params.customize, targetCustomerProduct: finalCustomerProduct, fullProduct: patchFullProduct, + reusePricesAndEntitlements, }); const { prices: customItemPrices, entitlements: customEntitlements } = @@ -138,6 +142,7 @@ export const setupPatchContext = ({ ctx, customize: params.customize, fullProduct: patchFullProduct, + reusePricesAndEntitlements, }); const patchContext: PatchContext = { diff --git a/server/src/internal/billing/v2/setup/patch/types.ts b/server/src/internal/billing/v2/setup/patch/types.ts new file mode 100644 index 000000000..427ed1f68 --- /dev/null +++ b/server/src/internal/billing/v2/setup/patch/types.ts @@ -0,0 +1,6 @@ +import type { Entitlement, Price } from "@autumn/shared"; + +export type ReusePricesAndEntitlements = { + pricesById: Map; + entitlementsById: Map; +}; diff --git a/server/src/internal/billing/v2/utils/autumnBillingPlanToFinalFullCustomer.ts b/server/src/internal/billing/v2/utils/autumnBillingPlanToFinalFullCustomer.ts index f3dff4871..dc1b826d9 100644 --- a/server/src/internal/billing/v2/utils/autumnBillingPlanToFinalFullCustomer.ts +++ b/server/src/internal/billing/v2/utils/autumnBillingPlanToFinalFullCustomer.ts @@ -1,4 +1,8 @@ -import type { AutumnBillingPlan, BillingContext } from "@autumn/shared"; +import type { + AutumnBillingPlan, + BillingContext, + FullCustomer, +} from "@autumn/shared"; import { applyCustomerProductPatch, applyCustomerProductUpdate, @@ -7,11 +11,11 @@ import { getUpdateCustomerProducts, } from "@/internal/billing/v2/utils/billingPlan/customerProductPlanMutations"; -export const autumnBillingPlanToFinalFullCustomer = ({ - billingContext, +export const applyAutumnBillingPlanToFullCustomer = ({ + fullCustomer, autumnBillingPlan, }: { - billingContext: BillingContext; + fullCustomer: FullCustomer; autumnBillingPlan: AutumnBillingPlan; }) => { const { insertCustomerProducts, updateCustomerEntitlements } = @@ -24,7 +28,7 @@ export const autumnBillingPlanToFinalFullCustomer = ({ autumnBillingPlan, }); - const finalFullCustomer = structuredClone(billingContext.fullCustomer); + const finalFullCustomer = structuredClone(fullCustomer); // 1. Combine existing customer products with new ones const combinedCustomerProducts = [ @@ -33,23 +37,23 @@ export const autumnBillingPlanToFinalFullCustomer = ({ ]; let customerProducts = combinedCustomerProducts.map((customerProduct) => { - const updateCustomerProduct = updateCustomerProducts.find( - (updateCustomerProduct) => - updateCustomerProduct.customerProduct.id === customerProduct.id, - ); - const patchCustomerProduct = patchCustomerProducts.find( - (patchCustomerProduct) => - patchCustomerProduct.customerProduct.id === customerProduct.id, - ); - let result = customerProduct; - if (updateCustomerProduct) { + for (const updateCustomerProduct of updateCustomerProducts) { + if (updateCustomerProduct.customerProduct.id !== customerProduct.id) { + continue; + } + result = applyCustomerProductUpdate({ customerProduct: result, updates: updateCustomerProduct.updates, }); } - if (patchCustomerProduct) { + + for (const patchCustomerProduct of patchCustomerProducts) { + if (patchCustomerProduct.customerProduct.id !== customerProduct.id) { + continue; + } + result = applyCustomerProductPatch({ customerProduct: result, patch: patchCustomerProduct, @@ -122,3 +126,15 @@ export const autumnBillingPlanToFinalFullCustomer = ({ customer_products: customerProducts, }; }; + +export const autumnBillingPlanToFinalFullCustomer = ({ + billingContext, + autumnBillingPlan, +}: { + billingContext: BillingContext; + autumnBillingPlan: AutumnBillingPlan; +}) => + applyAutumnBillingPlanToFullCustomer({ + fullCustomer: billingContext.fullCustomer, + autumnBillingPlan, + }); diff --git a/server/src/internal/billing/v2/utils/billingPlan/billingPlanWillCharge.ts b/server/src/internal/billing/v2/utils/billingPlan/billingPlanWillCharge.ts index a48981249..fe2868952 100644 --- a/server/src/internal/billing/v2/utils/billingPlan/billingPlanWillCharge.ts +++ b/server/src/internal/billing/v2/utils/billingPlan/billingPlanWillCharge.ts @@ -15,7 +15,7 @@ export type BillingPlanChargeResult = export const billingPlanWillCharge = ({ billingPlan, }: { - billingPlan: BillingPlan; + billingPlan: Pick; }): BillingPlanChargeResult => { const { subscriptionAction, invoiceAction } = billingPlan.stripe; diff --git a/server/src/internal/customers/CusService.ts b/server/src/internal/customers/CusService.ts index e8de29dcb..033383f59 100644 --- a/server/src/internal/customers/CusService.ts +++ b/server/src/internal/customers/CusService.ts @@ -33,6 +33,7 @@ import type { DrizzleCli } from "@/db/initDrizzle.js"; import { executeWithHealthTracking } from "@/db/pgHealthMonitor.js"; import type { RepoContext } from "@/db/repoContext.js"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { checkPendingMigrationsForCustomer } from "@/internal/migrations/v2/lazy/checkPendingMigrationsForCustomer.js"; import { withSpan } from "../analytics/tracer/spanUtils.js"; import { getOrgCusProductLimit, @@ -179,6 +180,10 @@ export class CusService { fullCus, ctx, }); + await checkPendingMigrationsForCustomer({ + ctx, + fullCustomer: fullCus, + }); } return fullCus; diff --git a/server/src/internal/customers/cache/fullSubject/actions/getCachedFullSubject.ts b/server/src/internal/customers/cache/fullSubject/actions/getCachedFullSubject.ts index 1387e39ff..e21463803 100644 --- a/server/src/internal/customers/cache/fullSubject/actions/getCachedFullSubject.ts +++ b/server/src/internal/customers/cache/fullSubject/actions/getCachedFullSubject.ts @@ -1,8 +1,13 @@ -import { type FullSubject, normalizedToFullSubject } from "@autumn/shared"; +import { + type FullSubject, + fullSubjectToFullCustomer, + normalizedToFullSubject, +} from "@autumn/shared"; import { isRedisMigrationCacheStale } from "@/external/redis/customerRedisRouting.js"; import { runRedisOp } from "@/external/redis/utils/runRedisOp.js"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { lazyResetSubjectEntitlements } from "@/internal/customers/actions/resetCustomerEntitlementsV2/lazyResetSubjectEntitlements.js"; +import { checkPendingMigrationsForCustomer } from "@/internal/migrations/v2/lazy/checkPendingMigrationsForCustomer.js"; import { getFullSubjectRolloutSnapshot } from "@/internal/misc/rollouts/fullSubjectRolloutUtils.js"; import { isSnapshotCacheStale } from "@/internal/misc/rollouts/rolloutUtils.js"; import { applyLiveAggregatedBalances } from "../balances/applyLiveAggregatedBalances.js"; @@ -234,6 +239,10 @@ export const getCachedFullSubject = async ({ const fullSubject = normalizedToFullSubject({ normalized }); await lazyResetSubjectEntitlements({ ctx, fullSubject }); + await checkPendingMigrationsForCustomer({ + ctx, + fullCustomer: fullSubjectToFullCustomer({ fullSubject }), + }); return { fullSubject, subjectViewEpoch: currentSubjectViewEpoch }; } catch (error) { logger.warn( diff --git a/server/src/internal/customers/cache/fullSubject/fullSubjectCacheModel.ts b/server/src/internal/customers/cache/fullSubject/fullSubjectCacheModel.ts index 91b6f3cd6..772616c04 100644 --- a/server/src/internal/customers/cache/fullSubject/fullSubjectCacheModel.ts +++ b/server/src/internal/customers/cache/fullSubject/fullSubjectCacheModel.ts @@ -1,12 +1,13 @@ import { CusProductSchema, - CustomerSchema, CustomerPriceSchema, + CustomerSchema, EntitlementWithFeatureSchema, EntityAggregationsSchema, EntitySchema, FreeTrialSchema, InvoiceSchema, + MigrationItemRunSchema, type NormalizedFullSubject, PriceSchema, ProductSchema, @@ -62,6 +63,11 @@ export const CachedFullSubjectSchema = z.object({ entity_aggregations: EntityAggregationsSchema.optional(), + // `.default([])` makes pre-existing cache entries (written before this + // field existed) hole-fill to an empty array via `normalizeFromSchema`. + // The empty-array vs empty-object Lua quirk is also handled there. + migration_item_runs: z.array(MigrationItemRunSchema).default([]), + _schemaVersion: z.number().optional(), _cachedAt: z.number(), meteredFeatures: z.array(z.string()), @@ -117,6 +123,7 @@ export const normalizedToCachedFullSubject = ({ subscriptions: normalized.subscriptions, invoices: normalized.invoices, entity_aggregations: normalized.entity_aggregations, + migration_item_runs: normalized.migration_item_runs ?? [], _schemaVersion: FULL_SUBJECT_CACHE_SCHEMA_VERSION, _cachedAt: Date.now(), meteredFeatures, @@ -151,5 +158,6 @@ export const cachedFullSubjectToNormalized = ({ subscriptions: cached.subscriptions, invoices: cached.invoices, entity_aggregations: cached.entity_aggregations, + migration_item_runs: cached.migration_item_runs ?? [], }; }; diff --git a/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/getCachedFullCustomer.ts b/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/getCachedFullCustomer.ts index 71cd3c51a..a7186bf1f 100644 --- a/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/getCachedFullCustomer.ts +++ b/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/getCachedFullCustomer.ts @@ -11,6 +11,7 @@ import { getDbHealth, PgHealth } from "@/db/pgHealthMonitor.js"; import { isRedisMigrationCacheStale } from "@/external/redis/customerRedisRouting.js"; import { redis } from "@/external/redis/initRedis.js"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { checkPendingMigrationsForCustomer } from "@/internal/migrations/v2/lazy/checkPendingMigrationsForCustomer.js"; import { isSnapshotCacheStale } from "@/internal/misc/rollouts/rolloutUtils.js"; import { tryRedisRead } from "@/utils/cacheUtils/cacheUtils.js"; import { normalizeFromSchema } from "@/utils/cacheUtils/normalizeFromSchema.js"; @@ -235,6 +236,7 @@ export const getCachedFullCustomer = async ({ // path to fail because the primary is down. if (getDbHealth() !== PgHealth.Degraded) { await resetCustomerEntitlements({ ctx, fullCus: fullCustomer }); + await checkPendingMigrationsForCustomer({ ctx, fullCustomer }); } // Round balance fields to handle floating-point precision from JSON.NUMINCRBY diff --git a/server/src/internal/customers/getFullCusQuery.ts b/server/src/internal/customers/getFullCusQuery.ts index c3bbe1673..976ce0976 100644 --- a/server/src/internal/customers/getFullCusQuery.ts +++ b/server/src/internal/customers/getFullCusQuery.ts @@ -366,6 +366,31 @@ export const getFullCusQuery = ({ sqlChunks.push(buildInvoicesCTE(!!entityId)); } + // Unconditional CTE for the customer's `migration_item_runs` scoped to + // the org's active lazy migrations. Empty in steady state — joins through + // `migration_runs` so callers don't have to thread the active list. + sqlChunks.push(sql`, `); + sqlChunks.push(sql` + customer_migration_item_runs AS ( + SELECT mir.* + FROM migration_item_runs mir + WHERE mir.item_kind = 'customer' + AND mir.dry_run = false + AND mir.item_id = (SELECT internal_id FROM customer_record) + AND mir.migration_internal_id IN ( + SELECT mr.migration_internal_id + FROM migration_runs mr + WHERE mr.org_id = ${orgId} + AND mr.env = ${env} + AND mr.status IN ('queued', 'running') + AND mr.dry_run = false + AND mr.lazy_run = true + ) + ORDER BY mir.updated_at DESC NULLS LAST, mir.created_at DESC + LIMIT 10 + ) + `); + // Conditionally add events CTE if (withEvents) { sqlChunks.push(sql`, `); @@ -456,6 +481,13 @@ export const getFullCusQuery = ({ (SELECT events FROM customer_events) AS events`); } + selectFieldsChunks.push(sql`, + COALESCE( + (SELECT json_agg(row_to_json(mir) ORDER BY mir.updated_at DESC NULLS LAST, mir.created_at DESC) + FROM customer_migration_item_runs mir), + '[]'::json + ) AS migration_item_runs`); + sqlChunks.push(sql` SELECT ${sql.join(selectFieldsChunks, sql``)} FROM customer_record cr diff --git a/server/src/internal/customers/repos/getFullSubject/getFullSubject.ts b/server/src/internal/customers/repos/getFullSubject/getFullSubject.ts index d3a383086..5f94e4c96 100644 --- a/server/src/internal/customers/repos/getFullSubject/getFullSubject.ts +++ b/server/src/internal/customers/repos/getFullSubject/getFullSubject.ts @@ -1,11 +1,13 @@ import { type CusProductStatus, type FullSubject, + fullSubjectToFullCustomer, type NormalizedFullSubject, normalizedToFullSubject, type SubjectQueryRow, } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { checkPendingMigrationsForCustomer } from "@/internal/migrations/v2/lazy/checkPendingMigrationsForCustomer.js"; import { lazyResetSubjectEntitlements } from "../../actions/resetCustomerEntitlementsV2/lazyResetSubjectEntitlements.js"; import { RELEVANT_STATUSES } from "../../cusProducts/CusProductService.js"; import { getFullSubjectQuery } from "./getFullSubjectQuery.js"; @@ -49,6 +51,10 @@ export async function getFullSubject({ allowMissingEntity, }); await lazyResetSubjectEntitlements({ ctx, fullSubject }); + await checkPendingMigrationsForCustomer({ + ctx, + fullCustomer: fullSubjectToFullCustomer({ fullSubject }), + }); return fullSubject; } @@ -92,6 +98,10 @@ export async function getFullSubjectNormalized({ const fullSubject = normalizedToFullSubject({ normalized }); await lazyResetSubjectEntitlements({ ctx, fullSubject, normalized }); + await checkPendingMigrationsForCustomer({ + ctx, + fullCustomer: fullSubjectToFullCustomer({ fullSubject }), + }); return { normalized, fullSubject }; } diff --git a/server/src/internal/customers/repos/getFullSubject/subjectQueryRowToNormalized.ts b/server/src/internal/customers/repos/getFullSubject/subjectQueryRowToNormalized.ts index 3f2179a2a..905bca89a 100644 --- a/server/src/internal/customers/repos/getFullSubject/subjectQueryRowToNormalized.ts +++ b/server/src/internal/customers/repos/getFullSubject/subjectQueryRowToNormalized.ts @@ -266,6 +266,7 @@ export const subjectQueryRowToNormalized = ({ subscriptions: row.subscriptions ?? [], invoices: row.invoices ?? [], entity_aggregations: entityAggregations, + migration_item_runs: row.migration_item_runs ?? [], }; }; diff --git a/server/src/internal/dev/api-keys/apiKeyUtils.ts b/server/src/internal/dev/api-keys/apiKeyUtils.ts index 803d4feb9..fea66846f 100644 --- a/server/src/internal/dev/api-keys/apiKeyUtils.ts +++ b/server/src/internal/dev/api-keys/apiKeyUtils.ts @@ -3,6 +3,7 @@ import { type ApiKey, AppEnv } from "@autumn/shared"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { generateId } from "@/utils/genUtils.js"; import { ApiKeyService } from "../ApiKeyService.js"; +import { apiKeyRepo } from "../repos/index.js"; import { getCachedSecretKeyVerification, SECRET_KEY_CACHE_TTL_SECONDS, @@ -148,19 +149,26 @@ export const verifyKey = async ({ : AppEnv.Live; const cached = await getCachedSecretKeyVerification< - Awaited> + Awaited> >({ hashedKey, }); if (cached) { + // Backfill `pendingMigrations` on payloads cached before the field + // existed — guarantees consumers can rely on the shape. + const pendingMigrations = cached.pendingMigrations ?? []; return { valid: true, - data: cached, + data: { + ...cached, + pendingMigrations, + org: { ...cached.org, pendingMigrations }, + }, }; } - const data = await ApiKeyService.verifyAndFetch({ + const data = await apiKeyRepo.verify({ db, hashedKey, env, diff --git a/server/src/internal/dev/repos/index.ts b/server/src/internal/dev/repos/index.ts new file mode 100644 index 000000000..1b3205af4 --- /dev/null +++ b/server/src/internal/dev/repos/index.ts @@ -0,0 +1,5 @@ +import { verifyApiKey } from "./verifyApiKey.js"; + +export const apiKeyRepo = { + verify: verifyApiKey, +}; diff --git a/server/src/internal/dev/repos/verifyApiKey.ts b/server/src/internal/dev/repos/verifyApiKey.ts new file mode 100644 index 000000000..e0e78508b --- /dev/null +++ b/server/src/internal/dev/repos/verifyApiKey.ts @@ -0,0 +1,35 @@ +import { type AppEnv, apiKeys } from "@autumn/shared"; +import { eq } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { orgRepo } from "@/internal/orgs/repos/index.js"; + +export const verifyApiKey = async ({ + db, + hashedKey, + env, +}: { + db: DrizzleCli; + hashedKey: string; + env: AppEnv; +}) => { + const apiKey = await db.query.apiKeys.findFirst({ + where: eq(apiKeys.hashed_key, hashedKey), + with: { user: true }, + }); + + if (!apiKey?.org_id) return null; + + const result = await orgRepo.findFull({ db, orgId: apiKey.org_id, env }); + if (!result) return null; + + return { + org: result.org, + features: result.features, + pendingMigrations: result.pendingMigrations, + fullOrg: result.fullOrg, + env, + userId: apiKey.user_id, + user: apiKey.user ?? null, + scopes: apiKey.scopes, + }; +}; diff --git a/server/src/internal/migrations/v2/actions/migrationItem/index.ts b/server/src/internal/migrations/v2/actions/migrationItem/index.ts new file mode 100644 index 000000000..819876894 --- /dev/null +++ b/server/src/internal/migrations/v2/actions/migrationItem/index.ts @@ -0,0 +1,7 @@ +import { withMigrationItemTracking } from "./withMigrationItemTracking.js"; + +export const migrationItemActions = { + withTracking: withMigrationItemTracking, +} as const; + +export { withMigrationItemTracking }; diff --git a/server/src/internal/migrations/v2/actions/migrationItem/withMigrationItemTracking.ts b/server/src/internal/migrations/v2/actions/migrationItem/withMigrationItemTracking.ts new file mode 100644 index 000000000..5799d97b7 --- /dev/null +++ b/server/src/internal/migrations/v2/actions/migrationItem/withMigrationItemTracking.ts @@ -0,0 +1,215 @@ +import type { + MigrationItemEventResponse, + MigrationItemEventStatus, + MigrationItemPreview, +} from "@/external/tinybird/migrations/migrationItemEventsDataSource.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { + migrationItemEventRepo, + migrationItemRunRepo, +} from "../../repos/index.js"; +import type { RunScopeItem } from "../../run/types/runScope.js"; + +export type MigrationItemTrackingResult = { + itemPreview: MigrationItemPreview | null; + status: Exclude; + response: MigrationItemEventResponse; +}; + +const errorToResponse = (error: unknown): MigrationItemEventResponse => ({ + error: { + message: error instanceof Error ? error.message : String(error), + }, +}); + +const itemToPreview = (item: RunScopeItem): MigrationItemPreview => ({ + id: item.id, + name: null, + email: null, +}); + +const recordMigrationItemEvent = async ({ + ctx, + migrationInternalId, + migrationRunId, + dryRun, + item, + status, + itemPreview, + response, +}: { + ctx: AutumnContext; + migrationInternalId: string; + migrationRunId: string; + dryRun: boolean; + item: RunScopeItem; + status: MigrationItemEventStatus; + itemPreview: MigrationItemPreview | null; + response: MigrationItemEventResponse; +}) => { + await migrationItemEventRepo.insert({ + ctx, + event: { + timestamp: new Date().toISOString(), + org_id: ctx.org.id, + env: ctx.env, + migration_internal_id: migrationInternalId, + migration_run_id: migrationRunId, + dry_run: dryRun, + item_kind: item.kind, + item_id: item.internal_id, + item_preview: itemPreview, + status, + response, + }, + }); +}; + +const markItemRunFinished = async ({ + ctx, + migrationInternalId, + migrationRunId, + dryRun, + item, + status, +}: { + ctx: AutumnContext; + migrationInternalId: string; + migrationRunId: string; + dryRun: boolean; + item: RunScopeItem; + status: Exclude; +}) => { + const params = { + ctx, + migrationInternalId, + migrationRunId, + dryRun, + itemKind: item.kind, + itemId: item.internal_id, + }; + + if (status === "skipped") { + await migrationItemRunRepo.markSkipped(params); + return; + } + + await migrationItemRunRepo.markSucceeded(params); +}; + +const runTrackedItem = async ({ + ctx, + migrationInternalId, + migrationRunId, + dryRun, + item, + run, +}: { + ctx: AutumnContext; + migrationInternalId: string; + migrationRunId: string; + dryRun: boolean; + item: RunScopeItem; + run: () => Promise; +}): Promise => { + try { + const result = await run(); + + await markItemRunFinished({ + ctx, + migrationInternalId, + migrationRunId, + dryRun, + item, + status: result.status, + }); + + await recordMigrationItemEvent({ + ctx, + migrationInternalId, + migrationRunId, + dryRun, + item, + status: result.status, + itemPreview: result.itemPreview, + response: result.response, + }); + + return result; + } catch (error) { + await migrationItemRunRepo.markFailed({ + ctx, + migrationInternalId, + migrationRunId, + dryRun, + itemKind: item.kind, + itemId: item.internal_id, + }); + + await recordMigrationItemEvent({ + ctx, + migrationInternalId, + migrationRunId, + dryRun, + item, + status: "failed", + itemPreview: itemToPreview(item), + response: errorToResponse(error), + }); + throw error; + } +}; + +export const withMigrationItemTracking = async < + T extends MigrationItemTrackingResult, +>({ + ctx, + migrationInternalId, + migrationRunId, + item, + dryRun, + claimItemRun = false, + retryFailed = false, + run, +}: { + ctx: AutumnContext; + migrationInternalId: string; + migrationRunId: string; + item: RunScopeItem; + dryRun: boolean; + claimItemRun?: boolean; + retryFailed?: boolean; + run: () => Promise; +}): Promise => { + if (claimItemRun) { + const claim = await migrationItemRunRepo.claim({ + ctx, + migrationInternalId, + migrationRunId, + dryRun, + itemKind: item.kind, + itemId: item.internal_id, + claimBehavior: retryFailed ? "retry_failed" : "claim_new", + }); + + if (!claim.claimed) { + ctx.logger.info("run-migration: item already claimed", { + data: { + kind: item.kind, + itemId: item.internal_id, + status: claim.itemRun?.status, + }, + }); + return undefined; + } + } + + return runTrackedItem({ + ctx, + migrationInternalId, + migrationRunId, + dryRun, + item, + run, + }); +}; diff --git a/server/src/internal/migrations/v2/actions/migrationRun/finishLazyMigrationRun.ts b/server/src/internal/migrations/v2/actions/migrationRun/finishLazyMigrationRun.ts new file mode 100644 index 000000000..acba2ac8f --- /dev/null +++ b/server/src/internal/migrations/v2/actions/migrationRun/finishLazyMigrationRun.ts @@ -0,0 +1,43 @@ +import { MigrationRunStatus } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { clearOrgCache } from "@/internal/orgs/orgUtils/clearOrgCache.js"; +import { migrationRunRepo } from "../../repos/index.js"; + +/** + * Mark a lazy migration run as terminally done and bust the org's api-key + * cache so `ctx.org.pendingMigrations` drops it on the next authed request. + * + * This is the "done with this lazy migration" hook — after calling, the + * customer-fetch hot path stops checking item_runs for this migration. + * + * Idempotent: if the run is already at a terminal status the update is a + * no-op; we still clear the org cache so callers can use this as a forced + * reload mechanism. + */ +export const finishLazyMigrationRun = async ({ + ctx, + runId, + status = MigrationRunStatus.Succeeded, + errorMessage, +}: { + ctx: AutumnContext; + runId: string; + status?: MigrationRunStatus; + errorMessage?: string; +}): Promise => { + await migrationRunRepo.update({ + ctx, + internalId: runId, + updates: { + status, + finished_at: Date.now(), + ...(errorMessage ? { error_message: errorMessage } : {}), + }, + }); + + await clearOrgCache({ + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + }); +}; diff --git a/server/src/internal/migrations/v2/actions/migrationRun/index.ts b/server/src/internal/migrations/v2/actions/migrationRun/index.ts new file mode 100644 index 000000000..657406edf --- /dev/null +++ b/server/src/internal/migrations/v2/actions/migrationRun/index.ts @@ -0,0 +1,15 @@ +import { finishLazyMigrationRun } from "./finishLazyMigrationRun.js"; +import { withMigrationRunClaim } from "./withMigrationRunClaim.js"; +import { withMigrationRunTracking } from "./withMigrationRunTracking.js"; + +export const migrationRunActions = { + finishLazy: finishLazyMigrationRun, + withClaim: withMigrationRunClaim, + withTracking: withMigrationRunTracking, +} as const; + +export { + finishLazyMigrationRun, + withMigrationRunClaim, + withMigrationRunTracking, +}; diff --git a/server/src/internal/migrations/v2/actions/migrationRun/withMigrationRunClaim.ts b/server/src/internal/migrations/v2/actions/migrationRun/withMigrationRunClaim.ts new file mode 100644 index 000000000..21182f5fa --- /dev/null +++ b/server/src/internal/migrations/v2/actions/migrationRun/withMigrationRunClaim.ts @@ -0,0 +1,107 @@ +import { + ErrCode, + type Migration, + MigrationRunStatus, + RecaseError, +} from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { clearOrgCache } from "@/internal/orgs/orgUtils/clearOrgCache.js"; +import { migrationRunRepo } from "../../repos/index.js"; + +/** Two-phase claim for a migration run. + * + * 1. Insert with `status='queued'` — locks the partial unique index + * `(org_id, env) WHERE status IN ('queued','running')` so nothing + * else can claim while the work is happening. + * 2. Run `claimed` (e.g. `prepare`, or trigger.dev dispatch). + * 3. On success, flip to `status='running'` with `started_at=now`. + * On failure, flip to `failed` so the constraint releases. + * 4. `claimed` may return `{ triggerRunId }` to persist a handle. */ +export const withMigrationRunClaim = async ({ + ctx, + migration, + dryRun, + lazyRun = false, + claimed, +}: { + ctx: AutumnContext; + migration: Migration; + dryRun: boolean; + lazyRun?: boolean; + claimed: ( + migrationRunId: string, + ) => Promise<{ triggerRunId?: string } | undefined>; +}): Promise<{ migrationRunId: string; triggerRunId?: string }> => { + const migrationRun = await migrationRunRepo.insert({ + ctx, + insert: { + migration_internal_id: migration.internal_id, + dry_run: dryRun, + lazy_run: lazyRun, + }, + }); + + if (!migrationRun) { + throw new RecaseError({ + message: + "A migration is already running. Please try again when it completes.", + code: ErrCode.MigrationAlreadyInProgress, + statusCode: 409, + }); + } + + // Lazy-mode runs need to land on `ctx.org.pendingMigrations` for every + // authed request, so bust the cached api-key payload here. Non-lazy runs + // have no effect on the hot path until the trigger task starts mutating. + if (lazyRun) { + await clearOrgCache({ db: ctx.db, orgId: ctx.org.id, env: ctx.env }); + } + + let result: { triggerRunId?: string } | undefined; + try { + result = await claimed(migrationRun.internal_id); + } catch (error) { + await migrationRunRepo.update({ + ctx, + internalId: migrationRun.internal_id, + updates: { + status: MigrationRunStatus.Failed, + error_message: error instanceof Error ? error.message : String(error), + finished_at: Date.now(), + }, + }); + throw error; + } + + await migrationRunRepo.update({ + ctx, + internalId: migrationRun.internal_id, + updates: { + status: MigrationRunStatus.Running, + started_at: Date.now(), + }, + }); + + if (result?.triggerRunId) { + try { + await migrationRunRepo.update({ + ctx, + internalId: migrationRun.internal_id, + updates: { trigger_run_id: result.triggerRunId }, + }); + } catch (error) { + ctx.logger.error("run-migration: failed to persist trigger run id", { + data: { + migrationRunId: migrationRun.internal_id, + triggerRunId: result.triggerRunId, + error: error instanceof Error ? error.message : String(error), + }, + }); + } + } + + return { + migrationRunId: migrationRun.internal_id, + triggerRunId: result?.triggerRunId, + }; +}; diff --git a/server/src/internal/migrations/v2/actions/migrationRun/withMigrationRunTracking.ts b/server/src/internal/migrations/v2/actions/migrationRun/withMigrationRunTracking.ts new file mode 100644 index 000000000..c7f9ef582 --- /dev/null +++ b/server/src/internal/migrations/v2/actions/migrationRun/withMigrationRunTracking.ts @@ -0,0 +1,46 @@ +import { MigrationRunStatus } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { migrationRunRepo } from "../../repos/index.js"; + +export const withMigrationRunTracking = async ({ + ctx, + migrationRunId, + run, +}: { + ctx: AutumnContext; + migrationRunId: string; + run: () => Promise; +}): Promise => { + await migrationRunRepo.update({ + ctx, + internalId: migrationRunId, + updates: { + status: MigrationRunStatus.Running, + started_at: Date.now(), + }, + }); + + try { + const result = await run(); + await migrationRunRepo.update({ + ctx, + internalId: migrationRunId, + updates: { + status: MigrationRunStatus.Succeeded, + finished_at: Date.now(), + }, + }); + return result; + } catch (error) { + await migrationRunRepo.update({ + ctx, + internalId: migrationRunId, + updates: { + status: MigrationRunStatus.Failed, + error_message: error instanceof Error ? error.message : String(error), + finished_at: Date.now(), + }, + }); + throw error; + } +}; diff --git a/server/src/internal/migrations/v2/cloudAdapter/runCloudScopeIteration.ts b/server/src/internal/migrations/v2/cloudAdapter/runCloudScopeIteration.ts new file mode 100644 index 000000000..d8077472f --- /dev/null +++ b/server/src/internal/migrations/v2/cloudAdapter/runCloudScopeIteration.ts @@ -0,0 +1,60 @@ +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import type { MigrationItemTrackingResult } from "../actions/migrationItem/withMigrationItemTracking.js"; +import type { RunScopeItem, RunScopeKind } from "../run/types/runScope.js"; +import type { MigrationBatchFn, MigrationRunControls } from "./types.js"; + +export const runCloudScopeIteration = async ({ + batch, + iterate, + kind, + controls, + perItem, +}: { + batch: MigrationBatchFn; + iterate: () => AsyncGenerator; + kind: RunScopeKind; + controls?: MigrationRunControls; + perItem: (args: { + item: RunScopeItem; + itemCtx: AutumnContext; + }) => Promise; +}): Promise => { + await batch({ + id: `run-${kind}-migration`, + source: scopeItems({ iterate }), + concurrency: controls?.concurrency, + limit: controls?.limit, + only: null, + itemKey, + checkpoint: false, + onError: "continue", + fn: async ({ item, ctx: itemCtx }) => { + if (item.kind !== "customer") + throw new Error( + `runMigration: per-item handler missing for kind "${item.kind}"`, + ); + const result = await perItem({ item, itemCtx }); + if (result) { + itemCtx.logger.set({ + migrationResult: { + status: result.status, + response: result.response, + }, + }); + } + return result; + }, + }); +}; + +const itemKey = (item: RunScopeItem) => item.id ?? item.internal_id; + +async function* scopeItems({ + iterate, +}: { + iterate: () => AsyncGenerator; +}): AsyncGenerator { + for await (const batch of iterate()) { + for (const item of batch) yield item; + } +} diff --git a/server/src/internal/migrations/v2/cloudAdapter/types.ts b/server/src/internal/migrations/v2/cloudAdapter/types.ts new file mode 100644 index 000000000..be9eb5a15 --- /dev/null +++ b/server/src/internal/migrations/v2/cloudAdapter/types.ts @@ -0,0 +1,50 @@ +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import type { RunScopeItem } from "../run/types/runScope.js"; + +export type MigrationRunControls = { + concurrency?: number; + limit?: number | null; + only?: string[] | null; + checkpoint?: boolean; + checkpointDryRun?: boolean; +}; + +export type MigrationBatchResult> = { + processed: number; + skipped: number; + errors: number; + duration: number; + rows: Row[]; + errorDetails: Array<{ item: unknown; error: Error }>; +}; + +export type MigrationScriptContext = Omit & { + logger: AutumnContext["logger"] & { + set: (data: Record) => void; + }; +}; + +export type MigrationBatchFn = < + R extends Record = Record, + Row extends Record = R, +>(opts: { + id?: string; + source?: AsyncIterable; + fn: (args: { + item: RunScopeItem; + ctx: MigrationScriptContext; + }) => Promise; + onResult?: (args: { + result: R; + item: RunScopeItem; + ctx: MigrationScriptContext; + }) => Row | null | undefined; + concurrency?: number; + limit?: number | null; + only?: string[] | null; + itemKey?: (item: RunScopeItem) => string; + checkpoint?: boolean; + checkpointDryRun?: boolean; + logItemResult?: boolean; + onError?: "continue" | "throw"; +}) => Promise>; diff --git a/server/src/internal/migrations/v2/filters/customers/buildCustomerSelect.ts b/server/src/internal/migrations/v2/filters/customers/buildCustomerSelect.ts new file mode 100644 index 000000000..b34c3f7ea --- /dev/null +++ b/server/src/internal/migrations/v2/filters/customers/buildCustomerSelect.ts @@ -0,0 +1,106 @@ +import type { CustomerFilter, MigrationItemRunStatus } from "@autumn/shared"; +import { compileFilter } from "@autumn/shared/api/migrations/compiler/compileFilter.js"; +import type { ResolutionContext } from "@autumn/shared/api/migrations/compiler/filterToIr/resolutionContext.js"; +import { type SQL, sql } from "drizzle-orm"; +import { rawWithParamsToDrizzle } from "../rawWithParamsToDrizzle.js"; + +export type CustomerQueryArgs = { + orgId: string; + env: string; + filter: CustomerFilter; + ctx: ResolutionContext; + checkpoint?: CustomerCheckpointExclusion; +}; + +const compileWhere = ({ orgId, env, filter, ctx }: CustomerQueryArgs): SQL => + rawWithParamsToDrizzle( + compileFilter({ filter, ctx, ambient: { orgId, env } }), + ); + +export type CustomerCheckpointExclusion = { + migrationInternalId: string; + migrationRunId: string; + dryRun: boolean; + excludedStatuses: MigrationItemRunStatus[]; +}; + +const buildCheckpointWhere = ( + checkpoint: CustomerCheckpointExclusion | undefined, +): SQL => { + if (!checkpoint || checkpoint.excludedStatuses.length === 0) return sql``; + + const checkpointScope = checkpoint.dryRun + ? sql`AND ( + mir.dry_run = false + OR ( + mir.dry_run = true + AND mir.migration_run_id = ${checkpoint.migrationRunId} + ) + )` + : sql`AND mir.dry_run = false`; + const statuses = sql.join( + checkpoint.excludedStatuses.map((status) => sql`${status}`), + sql`, `, + ); + + return sql` + AND NOT EXISTS ( + SELECT 1 + FROM migration_item_runs mir + WHERE mir.migration_internal_id = ${checkpoint.migrationInternalId} + ${checkpointScope} + AND mir.item_kind = 'customer' + AND mir.item_id = c.internal_id + AND mir.status IN (${statuses}) + ) + `; +}; + +/** + * Full SELECT. Returns `{ internal_id, id }` rows newest-first via keyset + * pagination on `c.internal_id DESC`, so successive iterations over an + * unchanged customer set yield rows in the same order. + */ +export const buildCustomerSelect = ({ + orgId, + env, + filter, + ctx, + checkpoint, + limit, + afterInternalId, +}: CustomerQueryArgs & { + limit?: number; + afterInternalId?: string; +}): SQL => { + const where = compileWhere({ orgId, env, filter, ctx }); + const checkpointWhere = buildCheckpointWhere(checkpoint); + const cursor = afterInternalId + ? sql`AND c.internal_id < ${afterInternalId}` + : sql``; + const limitClause = limit !== undefined ? sql`LIMIT ${limit}` : sql``; + return sql` + SELECT c.internal_id, c.id, c.name, c.email + FROM customers c + WHERE (${where}) ${checkpointWhere} ${cursor} + ORDER BY c.internal_id DESC + ${limitClause} + `; +}; + +/** COUNT(*) applying the same filter. */ +export const buildCustomerCount = ({ + orgId, + env, + filter, + ctx, + checkpoint, +}: CustomerQueryArgs): SQL => { + const where = compileWhere({ orgId, env, filter, ctx }); + const checkpointWhere = buildCheckpointWhere(checkpoint); + return sql` + SELECT COUNT(*)::bigint AS count + FROM customers c + WHERE (${where}) ${checkpointWhere} + `; +}; diff --git a/server/src/internal/migrations/v2/filters/customers/filterCustomers.ts b/server/src/internal/migrations/v2/filters/customers/filterCustomers.ts new file mode 100644 index 000000000..159dc61cc --- /dev/null +++ b/server/src/internal/migrations/v2/filters/customers/filterCustomers.ts @@ -0,0 +1,67 @@ +import type { CustomerFilter } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { iterateOverFilterResults } from "../iterateOverFilterResults.js"; +import { + buildCustomerCount, + buildCustomerSelect, + type CustomerCheckpointExclusion, +} from "./buildCustomerSelect.js"; + +export type CustomerRow = { + internal_id: string; + id: string | null; + name: string | null; + email: string | null; +}; + +/** + * Pure inner: takes a CustomerFilter directly. Used by `runFilter` shim + * (Migration-fed) and reusable from scripts that don't have a Migration. + */ +export const filterCustomers = ({ + ctx, + filter, + checkpoint, + batchSize, +}: { + ctx: AutumnContext; + filter: CustomerFilter; + checkpoint?: CustomerCheckpointExclusion; + batchSize?: number; +}): AsyncGenerator => { + const args = { + orgId: ctx.org.id, + env: ctx.env, + filter, + checkpoint, + ctx: { features: ctx.features }, + }; + return iterateOverFilterResults({ + db: ctx.db, + buildSelect: ({ limit, afterInternalId }) => + buildCustomerSelect({ ...args, limit, afterInternalId }), + batchSize, + }); +}; + +/** Count of customers matching `filter`. */ +export const countCustomers = async ({ + ctx, + filter, + checkpoint, +}: { + ctx: AutumnContext; + filter: CustomerFilter; + checkpoint?: CustomerCheckpointExclusion; +}): Promise => { + const [{ count }] = (await ctx.db.execute( + buildCustomerCount({ + orgId: ctx.org.id, + env: ctx.env, + filter, + checkpoint, + ctx: { features: ctx.features }, + }), + )) as Array<{ count: bigint | number }>; + return Number(count); +}; diff --git a/server/src/internal/migrations/v2/filters/iterateOverFilterResults.ts b/server/src/internal/migrations/v2/filters/iterateOverFilterResults.ts new file mode 100644 index 000000000..f6d7b9ea0 --- /dev/null +++ b/server/src/internal/migrations/v2/filters/iterateOverFilterResults.ts @@ -0,0 +1,30 @@ +import type { SQL } from "drizzle-orm"; + +export const DEFAULT_BATCH_SIZE = 10_000; + +/** + * Iterate keyset-paginated rows in batches. Caller supplies a `buildSelect` + * closure that returns the SQL for the next page given a cursor; rows + * MUST include `internal_id` for the cursor to advance. + */ +export async function* iterateOverFilterResults< + TRow extends { internal_id: string }, +>({ + db, + buildSelect, + batchSize = DEFAULT_BATCH_SIZE, +}: { + db: { execute: (query: SQL) => Promise }; + buildSelect: (args: { limit: number; afterInternalId?: string }) => SQL; + batchSize?: number; +}): AsyncGenerator { + let cursor: string | undefined; + while (true) { + const query = buildSelect({ limit: batchSize, afterInternalId: cursor }); + const rows = (await db.execute(query)) as unknown as TRow[]; + if (rows.length === 0) return; + yield rows; + if (rows.length < batchSize) return; + cursor = rows[rows.length - 1].internal_id; + } +} diff --git a/server/src/internal/migrations/v2/filters/rawWithParamsToDrizzle.ts b/server/src/internal/migrations/v2/filters/rawWithParamsToDrizzle.ts new file mode 100644 index 000000000..e7a622c5b --- /dev/null +++ b/server/src/internal/migrations/v2/filters/rawWithParamsToDrizzle.ts @@ -0,0 +1,22 @@ +import { type SQL, sql } from "drizzle-orm"; + +/** Convert the compiler's `{ sql, params }` output to a Drizzle SQL chunk. */ +export const rawWithParamsToDrizzle = ({ + sql: raw, + params, +}: { + sql: string; + params: readonly unknown[]; +}): SQL => { + const parts = raw.split("?"); + if (parts.length - 1 !== params.length) + throw new Error( + `Placeholder/param count mismatch: ${parts.length - 1} placeholders vs ${params.length} params`, + ); + const chunks: SQL[] = []; + for (let i = 0; i < parts.length; i++) { + chunks.push(sql.raw(parts[i])); + if (i < params.length) chunks.push(sql`${params[i]}`); + } + return sql.join(chunks, sql.raw("")); +}; diff --git a/server/src/internal/migrations/v2/filters/runFilter.ts b/server/src/internal/migrations/v2/filters/runFilter.ts new file mode 100644 index 000000000..95e5c5c0f --- /dev/null +++ b/server/src/internal/migrations/v2/filters/runFilter.ts @@ -0,0 +1,115 @@ +import { MigrationItemRunStatus } from "@autumn/shared"; +import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; +import type { MigrationRunControls } from "../cloudAdapter/types.js"; +import type { RunScopeItem, RunScopeKind } from "../run/types/runScope.js"; +import type { + MigrationRuntime, + MigrationRuntimeWithEventId, +} from "../types/migrationDefinition.js"; +import type { CustomerCheckpointExclusion } from "./customers/buildCustomerSelect.js"; +import { + countCustomers, + filterCustomers, +} from "./customers/filterCustomers.js"; + +/** + * Migration-fed shim. Dispatches by scope kind, unwraps the relevant + * filter from `migration.filter`, and delegates to the pure inner fns + * (`countCustomers` / `filterCustomers`). Empty filter ⇒ whole org+env. + */ +export const runFilter = async ({ + ctx, + migration, + migrationRunId, + dryRun, + kind, + controls, +}: { + ctx: AutumnContext; + migration: MigrationRuntimeWithEventId; + migrationRunId: string; + dryRun: boolean; + kind: RunScopeKind; + controls?: MigrationRunControls; +}): Promise<{ + kind: RunScopeKind; + count: number; + iterate: () => AsyncGenerator; +}> => { + if (kind !== "customer") + throw new Error( + `runFilter: scope kind "${kind}" not supported yet (phase 2+)`, + ); + + const filter = narrowCustomerFilter({ + filter: migration.filter?.customer ?? {}, + controls, + }); + const checkpoint = getCustomerCheckpointExclusion({ + migration, + migrationRunId, + dryRun, + controls, + }); + const count = await countCustomers({ ctx, filter, checkpoint }); + + const iterate = async function* () { + for await (const batch of filterCustomers({ ctx, filter, checkpoint })) { + yield batch.map( + (row): RunScopeItem => ({ + kind: "customer", + internal_id: row.internal_id, + id: row.id, + }), + ); + } + }; + + return { kind, count, iterate }; +}; + +const getCustomerCheckpointExclusion = ({ + migration, + migrationRunId, + dryRun, + controls, +}: { + migration: MigrationRuntimeWithEventId; + migrationRunId: string; + dryRun: boolean; + controls?: MigrationRunControls; +}): CustomerCheckpointExclusion | undefined => { + const enabled = + controls?.checkpoint !== false && + (!dryRun || controls?.checkpointDryRun === true); + if (!enabled) return undefined; + + const excludedStatuses = [ + MigrationItemRunStatus.Running, + MigrationItemRunStatus.Succeeded, + MigrationItemRunStatus.Skipped, + ...(migration.retry_failed ? [] : [MigrationItemRunStatus.Failed]), + ]; + + return { + migrationInternalId: migration.event_internal_id, + migrationRunId, + dryRun, + excludedStatuses, + }; +}; + +const narrowCustomerFilter = ({ + filter, + controls, +}: { + filter: NonNullable["customer"]; + controls?: MigrationRunControls; +}) => { + const only = controls?.only; + if (!only) return filter ?? {}; + return { + ...(filter ?? {}), + customer_id: { $in: only }, + }; +}; diff --git a/server/src/internal/migrations/v2/handlers/handleCreateMigration.ts b/server/src/internal/migrations/v2/handlers/handleCreateMigration.ts new file mode 100644 index 000000000..ac1439841 --- /dev/null +++ b/server/src/internal/migrations/v2/handlers/handleCreateMigration.ts @@ -0,0 +1,26 @@ +import { Scopes } from "@autumn/shared"; +import { MigrationFilterSchema } from "@autumn/shared/api/migrations/filters/migrationFilter.js"; +import { OperationsSchema } from "@autumn/shared/api/migrations/operations/operations.js"; +import { z } from "zod/v4"; +import { createRoute } from "@/honoMiddlewares/routeHandler"; +import { migrationRepo } from "@/internal/migrations/v2/repos/index.js"; + +const CreateMigrationBody = z.object({ + id: z.string().min(1).max(200), + filter: MigrationFilterSchema.nullable().optional(), + operations: OperationsSchema.nullable().optional(), +}); + +/** POST /migrations.create — create a draft migration. */ +export const handleCreateMigration = createRoute({ + scopes: [Scopes.Migrations.Write], + body: CreateMigrationBody, + handler: async (c) => { + const ctx = c.get("ctx"); + const insert = c.req.valid("json"); + + const migration = await migrationRepo.insert({ ctx, insert }); + + return c.json(migration); + }, +}); diff --git a/server/src/internal/migrations/v2/handlers/handleDeleteMigration.ts b/server/src/internal/migrations/v2/handlers/handleDeleteMigration.ts new file mode 100644 index 000000000..da3001cbf --- /dev/null +++ b/server/src/internal/migrations/v2/handlers/handleDeleteMigration.ts @@ -0,0 +1,28 @@ +import { ErrCode, RecaseError, Scopes } from "@autumn/shared"; +import { z } from "zod/v4"; +import { createRoute } from "@/honoMiddlewares/routeHandler"; +import { migrationRepo } from "@/internal/migrations/v2/repos/index.js"; + +const DeleteMigrationBody = z.object({ + id: z.string(), +}); + +/** POST /migrations.delete — delete by user `id`. */ +export const handleDeleteMigration = createRoute({ + scopes: [Scopes.Migrations.Write], + body: DeleteMigrationBody, + handler: async (c) => { + const ctx = c.get("ctx"); + const { id } = c.req.valid("json"); + + const deleted = await migrationRepo.delete({ ctx, id }); + if (!deleted) + throw new RecaseError({ + message: `Migration ${id} not found`, + code: ErrCode.MigrationNotFound, + statusCode: 404, + }); + + return c.json(deleted); + }, +}); diff --git a/server/src/internal/migrations/v2/handlers/handleLazyRunMigration.ts b/server/src/internal/migrations/v2/handlers/handleLazyRunMigration.ts new file mode 100644 index 000000000..292f8d9ee --- /dev/null +++ b/server/src/internal/migrations/v2/handlers/handleLazyRunMigration.ts @@ -0,0 +1,46 @@ +import { ErrCode, RecaseError, Scopes } from "@autumn/shared"; +import { z } from "zod/v4"; +import { createRoute } from "@/honoMiddlewares/routeHandler"; +import { withMigrationRunClaim } from "@/internal/migrations/v2/actions/migrationRun/index.js"; +import { prepare } from "@/internal/migrations/v2/prepare/index.js"; +import { migrationRepo } from "@/internal/migrations/v2/repos/index.js"; + +const LazyRunMigrationBody = z.object({ + id: z.string(), +}); + +/** POST /migrations.lazy_run — start a migration in lazy mode. + * Reuses `withMigrationRunClaim` so the partial unique index enforces one + * active run per (org, env), and prepare rolls back the claim on failure. */ +export const handleLazyRunMigration = createRoute({ + scopes: [Scopes.Migrations.Write], + body: LazyRunMigrationBody, + handler: async (c) => { + const ctx = c.get("ctx"); + const { id } = c.req.valid("json"); + + const migration = await migrationRepo.find({ ctx, id }); + + if (!migration.operations) + throw new RecaseError({ + message: `Migration ${id} has no operations to run`, + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + + const { migrationRunId } = await withMigrationRunClaim({ + ctx, + migration, + dryRun: false, + lazyRun: true, + claimed: async () => { + await prepare({ ctx, migration, dryRun: false }); + }, + }); + + return c.json({ + migration_id: id, + run_id: migrationRunId, + }); + }, +}); diff --git a/server/src/internal/migrations/v2/handlers/handleListMigrationItemEvents.ts b/server/src/internal/migrations/v2/handlers/handleListMigrationItemEvents.ts new file mode 100644 index 000000000..8cd8690ee --- /dev/null +++ b/server/src/internal/migrations/v2/handlers/handleListMigrationItemEvents.ts @@ -0,0 +1,25 @@ +import { Scopes } from "@autumn/shared"; +import { z } from "zod/v4"; +import { createRoute } from "@/honoMiddlewares/routeHandler"; +import { migrationItemEventRepo } from "../repos/index.js"; + +const ListMigrationItemEventsBody = z.object({ + migrationId: z.string(), + migrationRunId: z.string().optional(), +}); + +export const handleListMigrationItemEvents = createRoute({ + scopes: [Scopes.Migrations.Read], + body: ListMigrationItemEventsBody, + handler: async (c) => { + const ctx = c.get("ctx"); + const { migrationId, migrationRunId } = c.req.valid("json"); + const events = await migrationItemEventRepo.list({ + ctx, + migrationId, + migrationRunId, + }); + + return c.json({ list: events }); + }, +}); diff --git a/server/src/internal/migrations/v2/handlers/handleListMigrationRuns.ts b/server/src/internal/migrations/v2/handlers/handleListMigrationRuns.ts new file mode 100644 index 000000000..3813d8fde --- /dev/null +++ b/server/src/internal/migrations/v2/handlers/handleListMigrationRuns.ts @@ -0,0 +1,24 @@ +import { Scopes } from "@autumn/shared"; +import { z } from "zod/v4"; +import { createRoute } from "@/honoMiddlewares/routeHandler"; +import { migrationRepo, migrationRunRepo } from "../repos/index.js"; + +const ListMigrationRunsBody = z.object({ + migrationId: z.string(), +}); + +export const handleListMigrationRuns = createRoute({ + scopes: [Scopes.Migrations.Read], + body: ListMigrationRunsBody, + handler: async (c) => { + const ctx = c.get("ctx"); + const { migrationId } = c.req.valid("json"); + const migration = await migrationRepo.find({ ctx, id: migrationId }); + const runs = await migrationRunRepo.list({ + ctx, + migrationInternalId: migration.internal_id, + }); + + return c.json({ list: runs }); + }, +}); diff --git a/server/src/internal/migrations/v2/handlers/handleListMigrations.ts b/server/src/internal/migrations/v2/handlers/handleListMigrations.ts new file mode 100644 index 000000000..884b28058 --- /dev/null +++ b/server/src/internal/migrations/v2/handlers/handleListMigrations.ts @@ -0,0 +1,13 @@ +import { Scopes } from "@autumn/shared"; +import { createRoute } from "@/honoMiddlewares/routeHandler"; +import { migrationRepo } from "@/internal/migrations/v2/repos/index.js"; + +/** POST /migrations.list — list migrations for the current org + env. */ +export const handleListMigrations = createRoute({ + scopes: [Scopes.Migrations.Read], + handler: async (c) => { + const ctx = c.get("ctx"); + const migrations = await migrationRepo.get({ ctx }); + return c.json({ list: migrations }); + }, +}); diff --git a/server/src/internal/migrations/v2/handlers/handlePatchMigration.ts b/server/src/internal/migrations/v2/handlers/handlePatchMigration.ts new file mode 100644 index 000000000..66e9817d3 --- /dev/null +++ b/server/src/internal/migrations/v2/handlers/handlePatchMigration.ts @@ -0,0 +1,44 @@ +import { ErrCode, RecaseError, Scopes } from "@autumn/shared"; +import { MigrationFilterSchema } from "@autumn/shared/api/migrations/filters/migrationFilter.js"; +import { OperationsSchema } from "@autumn/shared/api/migrations/operations/operations.js"; +import { z } from "zod/v4"; +import { createRoute } from "@/honoMiddlewares/routeHandler"; +import { migrationRepo } from "@/internal/migrations/v2/repos/index.js"; + +const PatchMigrationBody = z.object({ + id: z.string(), + updates: z.object({ + id: z.string().min(1).max(200).optional(), + filter: MigrationFilterSchema.nullable().optional(), + operations: OperationsSchema.nullable().optional(), + retry_failed: z.boolean().optional(), + }), +}); + +/** POST /migrations.update — patch a migration's fields. */ +export const handlePatchMigration = createRoute({ + scopes: [Scopes.Migrations.Write], + body: PatchMigrationBody, + handler: async (c) => { + const ctx = c.get("ctx"); + const { id, updates } = c.req.valid("json"); + + const updated = await migrationRepo.update({ + ctx, + id, + updates: { + ...updates, + ...(updates.operations !== undefined ? { prepared_state: null } : {}), + }, + }); + + if (!updated) + throw new RecaseError({ + message: `Migration ${id} not found`, + code: ErrCode.MigrationNotFound, + statusCode: 404, + }); + + return c.json(updated); + }, +}); diff --git a/server/src/internal/migrations/v2/handlers/handlePrepareMigration.ts b/server/src/internal/migrations/v2/handlers/handlePrepareMigration.ts new file mode 100644 index 000000000..e68ec388c --- /dev/null +++ b/server/src/internal/migrations/v2/handlers/handlePrepareMigration.ts @@ -0,0 +1,32 @@ +import { ErrCode, RecaseError, Scopes } from "@autumn/shared"; +import { z } from "zod/v4"; +import { createRoute } from "@/honoMiddlewares/routeHandler"; +import { prepare } from "@/internal/migrations/v2/prepare/index.js"; +import { migrationRepo } from "@/internal/migrations/v2/repos/index.js"; + +const PrepareMigrationBody = z.object({ + id: z.string(), + dry_run: z.boolean(), +}); + +/** POST /migrations.prepare — run prep modules for a migration. */ +export const handlePrepareMigration = createRoute({ + scopes: [Scopes.Migrations.Write], + body: PrepareMigrationBody, + handler: async (c) => { + const ctx = c.get("ctx"); + const { id, dry_run: dryRun } = c.req.valid("json"); + + const migration = await migrationRepo.find({ ctx, id }); + + if (!migration.operations) + throw new RecaseError({ + message: `Migration ${id} has no operations to prepare`, + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + + const { response } = await prepare({ ctx, migration, dryRun }); + return c.json(response); + }, +}); diff --git a/server/src/internal/migrations/v2/handlers/handlePreviewMigrationFilter.ts b/server/src/internal/migrations/v2/handlers/handlePreviewMigrationFilter.ts new file mode 100644 index 000000000..02be77090 --- /dev/null +++ b/server/src/internal/migrations/v2/handlers/handlePreviewMigrationFilter.ts @@ -0,0 +1,148 @@ +import { + CustomerFilterSchema, + customerProducts, + customers, + products, + Scopes, +} from "@autumn/shared"; +import { eq, inArray } from "drizzle-orm"; +import { z } from "zod/v4"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { + countCustomers, + filterCustomers, +} from "@/internal/migrations/v2/filters/customers/filterCustomers.js"; + +const DEFAULT_PAGE_SIZE = 10; + +const PreviewFilterBody = z.object({ + filter: CustomerFilterSchema.optional().default({}), + search: z.string().optional().default(""), + page: z.number().int().min(0).optional().default(0), + pageSize: z.number().int().min(1).max(500).optional().default(DEFAULT_PAGE_SIZE), +}); + +/** POST /migrations.filter.preview — count + enriched paginated customers. */ +export const handlePreviewMigrationFilter = createRoute({ + scopes: [Scopes.Migrations.Read], + body: PreviewFilterBody, + handler: async (c) => { + const ctx = c.get("ctx"); + const { filter, search, page, pageSize } = c.req.valid("json"); + + const [count, pageRows] = await Promise.all([ + countCustomers({ ctx, filter }), + collectPage( + filterCustomers({ ctx, filter, batchSize: pageSize }), + page * pageSize, + pageSize, + ), + ]); + + if (pageRows.length === 0) { + return c.json({ count, customers: [], page, pageSize }); + } + + const enriched = await enrichCustomers( + ctx.db, + pageRows.map((r) => r.internal_id), + ); + + let grouped = groupByCustomer(enriched); + + if (search) { + const q = search.toLowerCase(); + grouped = grouped.filter((row) => { + const name = (row.name as string | null)?.toLowerCase() ?? ""; + const email = (row.email as string | null)?.toLowerCase() ?? ""; + const id = (row.id as string | null)?.toLowerCase() ?? ""; + return name.includes(q) || email.includes(q) || id.includes(q); + }); + } + + return c.json({ count, customers: grouped, page, pageSize }); + }, +}); + +async function enrichCustomers(db: DrizzleCli, ids: string[]) { + return db + .select({ + internal_id: customers.internal_id, + id: customers.id, + name: customers.name, + email: customers.email, + created_at: customers.created_at, + org_id: customers.org_id, + env: customers.env, + fingerprint: customers.fingerprint, + metadata: customers.metadata, + processor: customers.processor, + processors: customers.processors, + send_email_receipts: customers.send_email_receipts, + auto_topups: customers.auto_topups, + spend_limits: customers.spend_limits, + usage_alerts: customers.usage_alerts, + overage_allowed: customers.overage_allowed, + config: customers.config, + customer_product: { + id: customerProducts.id, + internal_product_id: customerProducts.internal_product_id, + product_id: customerProducts.product_id, + canceled_at: customerProducts.canceled_at, + status: customerProducts.status, + trial_ends_at: customerProducts.trial_ends_at, + created_at: customerProducts.created_at, + }, + product: { + internal_id: products.internal_id, + id: products.id, + name: products.name, + version: products.version, + is_add_on: products.is_add_on, + }, + }) + .from(customers) + .leftJoin(customerProducts, eq(customers.internal_id, customerProducts.internal_customer_id)) + .leftJoin(products, eq(customerProducts.internal_product_id, products.internal_id)) + .where(inArray(customers.internal_id, ids)); +} + +function groupByCustomer(rows: Array>) { + const map = new Map>(); + for (const row of rows) { + const id = row.internal_id as string; + if (!map.has(id)) { + const { customer_product, product, ...customer } = row; + map.set(id, { ...customer, customer_products: [] }); + } + if (row.customer_product && (row.customer_product as Record).id) { + const entry = map.get(id)!; + (entry.customer_products as unknown[]).push({ + ...(row.customer_product as Record), + product: row.product, + }); + } + } + return Array.from(map.values()); +} + +async function collectPage( + gen: AsyncGenerator, + skip: number, + take: number, +): Promise { + const rows: T[] = []; + let skipped = 0; + for await (const batch of gen) { + for (const row of batch) { + if (skipped < skip) { + skipped++; + continue; + } + rows.push(row); + if (rows.length >= take) return rows; + } + } + return rows; +} diff --git a/server/src/internal/migrations/v2/handlers/handleRunMigration.ts b/server/src/internal/migrations/v2/handlers/handleRunMigration.ts new file mode 100644 index 000000000..e1349c362 --- /dev/null +++ b/server/src/internal/migrations/v2/handlers/handleRunMigration.ts @@ -0,0 +1,94 @@ +import { ErrCode, RecaseError, Scopes } from "@autumn/shared"; +import { auth } from "@trigger.dev/sdk/v3"; +import { z } from "zod/v4"; +import { createRoute } from "@/honoMiddlewares/routeHandler"; +import { withMigrationRunClaim } from "@/internal/migrations/v2/actions/migrationRun/index.js"; +import { migrationRepo } from "@/internal/migrations/v2/repos/index.js"; +import { runMigrationTask } from "@/trigger/migrations/runMigrationTask.js"; + +const RunMigrationBody = z.object({ + id: z.string(), + dry_run: z.boolean().default(false), + limit: z.number().int().min(1).optional(), + only: z.array(z.string()).optional(), + concurrency: z.number().int().min(1).optional(), +}); + +const getRunMigrationTriggerOptions = ({ + orgId, + isDev, +}: { + orgId: string; + isDev: boolean; +}) => ({ + ...(isDev ? { region: "eu-west-1" } : {}), + concurrencyKey: orgId, +}); + +export const handleRunMigration = createRoute({ + scopes: [Scopes.Migrations.Write], + body: RunMigrationBody, + handler: async (c) => { + const ctx = c.get("ctx"); + const { + id, + dry_run: dryRun, + limit, + only, + concurrency, + } = c.req.valid("json"); + + const migration = await migrationRepo.find({ ctx, id }); + + if (!migration.operations) + throw new RecaseError({ + message: `Migration ${id} has no operations to run`, + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + + const isDev = process.env.NODE_ENV === "development"; + const { migrationRunId, triggerRunId } = await withMigrationRunClaim({ + ctx, + migration, + dryRun, + claimed: async (migrationRunId) => { + const handle = await runMigrationTask.trigger( + { + orgId: ctx.org.id, + env: ctx.env, + migrationId: id, + migrationRunId, + dryRun, + controls: { limit, only, concurrency }, + }, + getRunMigrationTriggerOptions({ + orgId: ctx.org.id, + isDev, + }), + ); + return { triggerRunId: handle.id }; + }, + }); + + let publicAccessToken: string | undefined; + if (triggerRunId) { + try { + publicAccessToken = await auth.createPublicToken({ + scopes: { read: { runs: [triggerRunId] } }, + expirationTime: "1hr", + }); + } catch { + ctx.logger.warn("run-migration: failed to create public access token"); + } + } + + return c.json({ + migration_id: id, + dry_run: dryRun, + run_id: migrationRunId, + trigger_run_id: triggerRunId, + public_access_token: publicAccessToken, + }); + }, +}); diff --git a/server/src/internal/migrations/v2/hooks/aroundMigrateCustomer/aroundMigrateCustomerHook.ts b/server/src/internal/migrations/v2/hooks/aroundMigrateCustomer/aroundMigrateCustomerHook.ts new file mode 100644 index 000000000..380680f8c --- /dev/null +++ b/server/src/internal/migrations/v2/hooks/aroundMigrateCustomer/aroundMigrateCustomerHook.ts @@ -0,0 +1,47 @@ +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import type { MigrateCustomerContext } from "../../operations/types/index.js"; +import type { + MigrateCustomerItemPreview, + MigrateCustomerResult, +} from "../../run/migrateCustomer/index.js"; + +export type AroundMigrateCustomerRun = () => Promise; + +export type AroundMigrateCustomerArgs = { + ctx: AutumnContext; + customerId: string; + context: MigrateCustomerContext; + preview: boolean; + run: AroundMigrateCustomerRun; +}; + +export type AroundMigrateCustomerResult = + | Promise + | MigrateCustomerResult; + +export type AroundMigrateCustomerSkip = { + reason: string; + response?: Record | null; + itemPreview?: MigrateCustomerItemPreview | null; +}; + +export const buildSkippedMigrateCustomerResult = ({ + context, + skip, +}: { + context: MigrateCustomerContext; + skip: AroundMigrateCustomerSkip; +}): MigrateCustomerResult => ({ + itemPreview: skip.itemPreview ?? { + id: context.fullCustomer.id ?? null, + name: context.fullCustomer.name ?? null, + email: context.fullCustomer.email ?? null, + }, + status: "skipped", + response: { + skipped: { + reason: skip.reason, + }, + ...(skip.response ?? {}), + }, +}); diff --git a/server/src/internal/migrations/v2/hooks/aroundMigrateCustomer/index.ts b/server/src/internal/migrations/v2/hooks/aroundMigrateCustomer/index.ts new file mode 100644 index 000000000..905529756 --- /dev/null +++ b/server/src/internal/migrations/v2/hooks/aroundMigrateCustomer/index.ts @@ -0,0 +1 @@ +export * from "./aroundMigrateCustomerHook.js"; diff --git a/server/src/internal/migrations/v2/hooks/composeMigrationHooks.ts b/server/src/internal/migrations/v2/hooks/composeMigrationHooks.ts new file mode 100644 index 000000000..75485037f --- /dev/null +++ b/server/src/internal/migrations/v2/hooks/composeMigrationHooks.ts @@ -0,0 +1,32 @@ +import type { + AroundMigrateCustomerArgs, + AroundMigrateCustomerRun, +} from "./aroundMigrateCustomer/index.js"; +import type { MigrationHooks, MigrationPlugin } from "./types.js"; + +export const composeMigrationHooks = ({ + hooks, + plugins = [], +}: { + hooks?: MigrationHooks; + plugins?: MigrationPlugin[]; +}): MigrationHooks | undefined => { + const entries = [...plugins.map((plugin) => plugin.hooks), hooks].filter( + (entry): entry is MigrationHooks => Boolean(entry), + ); + + if (entries.length === 0) return undefined; + + return { + aroundMigrateCustomer: async (args: AroundMigrateCustomerArgs) => { + const run = entries.reduceRight( + (next, entry) => async () => { + if (!entry.aroundMigrateCustomer) return next(); + return entry.aroundMigrateCustomer({ ...args, run: next }); + }, + args.run, + ); + return run(); + }, + }; +}; diff --git a/server/src/internal/migrations/v2/hooks/index.ts b/server/src/internal/migrations/v2/hooks/index.ts new file mode 100644 index 000000000..910884999 --- /dev/null +++ b/server/src/internal/migrations/v2/hooks/index.ts @@ -0,0 +1,4 @@ +export * from "./aroundMigrateCustomer/index.js"; +export * from "./composeMigrationHooks.js"; +export * from "./plugins/index.js"; +export * from "./types.js"; diff --git a/server/src/internal/migrations/v2/hooks/plugins/customerGuardPlugin.ts b/server/src/internal/migrations/v2/hooks/plugins/customerGuardPlugin.ts new file mode 100644 index 000000000..df9bd0ea2 --- /dev/null +++ b/server/src/internal/migrations/v2/hooks/plugins/customerGuardPlugin.ts @@ -0,0 +1,45 @@ +import { + type AroundMigrateCustomerArgs, + buildSkippedMigrateCustomerResult, +} from "../aroundMigrateCustomer/index.js"; +import type { MigrationPlugin } from "../types.js"; + +export type CustomerGuardResult = + | undefined + | null + | { + reason: string; + response?: Record | null; + }; + +export const customerGuardPlugin = ({ + id, + guard, +}: { + id: string; + guard: ( + args: AroundMigrateCustomerArgs, + ) => Promise | CustomerGuardResult; +}): MigrationPlugin => ({ + id, + hooks: { + aroundMigrateCustomer: async (args) => { + const result = await guard(args); + if (!result) return args.run(); + + return buildSkippedMigrateCustomerResult({ + context: args.context, + skip: { + reason: result.reason, + response: { + guard: { + pluginId: id, + reason: result.reason, + ...(result.response ?? {}), + }, + }, + }, + }); + }, + }, +}); diff --git a/server/src/internal/migrations/v2/hooks/plugins/index.ts b/server/src/internal/migrations/v2/hooks/plugins/index.ts new file mode 100644 index 000000000..1b6d17c05 --- /dev/null +++ b/server/src/internal/migrations/v2/hooks/plugins/index.ts @@ -0,0 +1 @@ +export * from "./customerGuardPlugin.js"; diff --git a/server/src/internal/migrations/v2/hooks/types.ts b/server/src/internal/migrations/v2/hooks/types.ts new file mode 100644 index 000000000..23fb7d00c --- /dev/null +++ b/server/src/internal/migrations/v2/hooks/types.ts @@ -0,0 +1,15 @@ +import type { + AroundMigrateCustomerArgs, + AroundMigrateCustomerResult, +} from "./aroundMigrateCustomer/index.js"; + +export type MigrationHooks = { + aroundMigrateCustomer?: ( + args: AroundMigrateCustomerArgs, + ) => AroundMigrateCustomerResult; +}; + +export type MigrationPlugin = { + id: string; + hooks?: MigrationHooks; +}; diff --git a/server/src/internal/migrations/v2/lazy/checkPendingMigrationsForCustomer.ts b/server/src/internal/migrations/v2/lazy/checkPendingMigrationsForCustomer.ts new file mode 100644 index 000000000..5ed32248f --- /dev/null +++ b/server/src/internal/migrations/v2/lazy/checkPendingMigrationsForCustomer.ts @@ -0,0 +1,83 @@ +import type { FullCustomer, MigrationItemRunData } from "@autumn/shared"; +import { customerFilterMatchesFullCustomer } from "@autumn/shared/api/customers/utils/match/index.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { runMigrationCustomerTask } from "@/trigger/migrations/runMigrationCustomerTask.js"; + +/** + * For each pending lazy migration on `ctx.org`, decide whether this customer + * needs migrating and enqueue a per-customer Trigger.dev task if so. + * + * Reads `migration_item_runs` from the already-loaded `fullCustomer` (embedded + * by the FullSubject / FullCustomer query) — no extra DB roundtrip. + * + * Fire-and-forget: the helper doesn't wait for the migration to complete. + * `executeMigrateCustomerPlan` inside the task busts the customer cache, + * so subsequent requests read post-migration state. + */ +export const checkPendingMigrationsForCustomer = async ({ + ctx, + fullCustomer, +}: { + ctx: AutumnContext; + fullCustomer: Pick< + FullCustomer, + "id" | "internal_id" | "customer_products" | "migration_item_runs" + >; +}): Promise => { + // Short-circuit when we're already inside a Trigger.dev task. A migration + // worker loading the customer via `CusService.getFull` would otherwise + // re-enter this helper and enqueue another task. Flag is set by + // `createTriggerContext`. + if (ctx.insideTriggerTask) return; + + const pending = ctx.org.pendingMigrations ?? []; + if (pending.length === 0) return; + + const itemRunsByMigrationInternalId = new Map(); + for (const itemRun of fullCustomer.migration_item_runs ?? []) { + const existing = itemRunsByMigrationInternalId.get( + itemRun.migration_internal_id, + ); + if ( + !existing || + (itemRun.updated_at ?? 0) > (existing.updated_at ?? 0) || + (itemRun.updated_at === existing.updated_at && + itemRun.created_at > existing.created_at) + ) { + itemRunsByMigrationInternalId.set(itemRun.migration_internal_id, itemRun); + } + } + + for (const pendingMigration of pending) { + const { internal_id: migrationRunId, migration } = pendingMigration; + + const matches = customerFilterMatchesFullCustomer({ + filter: migration.filter?.customer ?? {}, + fullCustomer, + }); + if (!matches) continue; + + const itemRun = itemRunsByMigrationInternalId.get(migration.internal_id); + if ( + itemRun?.status === "succeeded" || + itemRun?.status === "skipped" || + itemRun?.status === "running" + ) { + continue; + } + + await runMigrationCustomerTask.trigger( + { + orgId: ctx.org.id, + env: ctx.env, + migrationInternalId: migration.internal_id, + migrationRunId, + customerInternalId: fullCustomer.internal_id, + customerId: fullCustomer.id ?? null, + }, + { + concurrencyKey: `${migration.internal_id}:${fullCustomer.internal_id}`, + }, + ); + } +}; diff --git a/server/src/internal/migrations/v2/migrationRouter.ts b/server/src/internal/migrations/v2/migrationRouter.ts new file mode 100644 index 000000000..b72481564 --- /dev/null +++ b/server/src/internal/migrations/v2/migrationRouter.ts @@ -0,0 +1,36 @@ +import { Hono } from "hono"; +import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; +import { handleCreateMigration } from "./handlers/handleCreateMigration.js"; +import { handleDeleteMigration } from "./handlers/handleDeleteMigration.js"; +import { handleLazyRunMigration } from "./handlers/handleLazyRunMigration.js"; +import { handleListMigrationItemEvents } from "./handlers/handleListMigrationItemEvents.js"; +import { handleListMigrationRuns } from "./handlers/handleListMigrationRuns.js"; +import { handleListMigrations } from "./handlers/handleListMigrations.js"; +import { handlePatchMigration } from "./handlers/handlePatchMigration.js"; +import { handlePrepareMigration } from "./handlers/handlePrepareMigration.js"; +import { handlePreviewMigrationFilter } from "./handlers/handlePreviewMigrationFilter.js"; +import { handleRunMigration } from "./handlers/handleRunMigration.js"; + +/** + * V2 user-facing migrations RPC router. Distinct from the legacy + * `migrationRouter` (product-version migration system in + * `internal/products/productRouter.ts`). + */ +export const migrationRpcRouter = new Hono(); + +migrationRpcRouter.post("/migrations.create", ...handleCreateMigration); +migrationRpcRouter.post("/migrations.list", ...handleListMigrations); +migrationRpcRouter.post("/migrations.update", ...handlePatchMigration); +migrationRpcRouter.post("/migrations.delete", ...handleDeleteMigration); +migrationRpcRouter.post("/migrations.prepare", ...handlePrepareMigration); +migrationRpcRouter.post( + "/migrations.filter.preview", + ...handlePreviewMigrationFilter, +); +migrationRpcRouter.post("/migrations.run", ...handleRunMigration); +migrationRpcRouter.post("/migrations.lazy_run", ...handleLazyRunMigration); +migrationRpcRouter.post("/migrations.runs.list", ...handleListMigrationRuns); +migrationRpcRouter.post( + "/migrations.item_events.list", + ...handleListMigrationItemEvents, +); diff --git a/server/src/internal/migrations/v2/operations/addPlan/index.ts b/server/src/internal/migrations/v2/operations/addPlan/index.ts new file mode 100644 index 000000000..b350cdfe4 --- /dev/null +++ b/server/src/internal/migrations/v2/operations/addPlan/index.ts @@ -0,0 +1 @@ +export * from "./processAddPlan.js"; diff --git a/server/src/internal/migrations/v2/operations/addPlan/processAddPlan.ts b/server/src/internal/migrations/v2/operations/addPlan/processAddPlan.ts new file mode 100644 index 000000000..225db0bd4 --- /dev/null +++ b/server/src/internal/migrations/v2/operations/addPlan/processAddPlan.ts @@ -0,0 +1,151 @@ +import { + BillingVersion, + CollectionMethod, + CusProductStatus, + type Customer, + type CustomerEntitlement, + type CustomerPrice, + customerProductHasActiveStatus, + type FullCusProduct, +} from "@autumn/shared"; +import type { AddPlanOp } from "@autumn/shared/api/migrations/operations/customer/addPlan/index.js"; +import { initCusEntitlement } from "@/internal/customers/add-product/initCusEnt.js"; +import { getEntRelatedPrice } from "@/internal/products/entitlements/entitlementUtils.js"; +import { ProductService } from "@/internal/products/ProductService.js"; +import { getEntOptions } from "@/internal/products/prices/priceUtils.js"; +import { generateId } from "@/utils/genUtils.js"; +import type { OperationProcessor } from "../types/index.js"; +import { mergeAutumnBillingPlans } from "../utils/index.js"; + +export const processAddPlan: OperationProcessor = async ({ + ctx, + op, + plan, + projectedFullCustomer, +}) => { + const product = await ProductService.getFull({ + db: ctx.db, + idOrInternalId: op.plan_id, + orgId: ctx.org.id, + env: ctx.env, + version: op.version, + allowNotFound: true, + }); + + if (!product) { + throw new Error( + `add_plan: product "${op.plan_id}" (version ${op.version ?? "latest"}) not found in catalog`, + ); + } + + const alreadyHasPlan = projectedFullCustomer.customer_products.some( + (cp) => + cp.internal_product_id === product.internal_id && + customerProductHasActiveStatus(cp), + ); + if (alreadyHasPlan) + return { + plan, + projectedFullCustomer, + matchedCustomerProducts: 0, + billingContexts: [], + }; + + const cusProductId = generateId("cus_prod"); + const now = Date.now(); + const optionsList = + op.feature_quantities?.map((fq) => ({ + feature_id: fq.feature_id, + quantity: fq.quantity, + })) ?? []; + + const customer: Pick = { + internal_id: projectedFullCustomer.internal_id, + id: projectedFullCustomer.id, + }; + + const customerEntitlements: CustomerEntitlement[] = product.entitlements.map( + (entitlement) => + initCusEntitlement({ + entitlement, + customer: customer as Customer, + cusProductId, + freeTrial: null, + options: getEntOptions(optionsList, entitlement) || undefined, + relatedPrice: getEntRelatedPrice(entitlement, product.prices), + entities: [], + carryExistingUsages: false, + curCusProduct: undefined, + replaceables: [], + }), + ); + + const customerPrices: CustomerPrice[] = product.prices.map((price) => ({ + id: generateId("cus_price"), + internal_customer_id: projectedFullCustomer.internal_id, + customer_product_id: cusProductId, + created_at: now, + price_id: price.id || null, + })); + + const newCusProduct: FullCusProduct = { + id: cusProductId, + internal_customer_id: projectedFullCustomer.internal_id, + customer_id: projectedFullCustomer.id, + internal_product_id: product.internal_id, + product_id: product.id, + created_at: now, + updated_at: now, + canceled: false, + ended_at: null, + status: CusProductStatus.Active, + processor: projectedFullCustomer.processor ?? { type: "stripe" as const }, + starts_at: now, + trial_ends_at: null, + options: optionsList, + free_trial_id: null, + canceled_at: null, + collection_method: CollectionMethod.ChargeAutomatically, + subscription_ids: [], + scheduled_ids: [], + is_custom: false, + quantity: 1, + internal_entity_id: undefined, + entity_id: undefined, + api_semver: null, + billing_version: BillingVersion.V1, + external_id: null, + product, + customer_entitlements: customerEntitlements.map((ce) => { + const entitlement = product.entitlements.find( + (e) => e.id === ce.entitlement_id, + ); + if (!entitlement) + throw new Error( + `add_plan: entitlement ${ce.entitlement_id} not found on product ${product.id}`, + ); + return { ...ce, entitlement, replaceables: [], rollovers: [] }; + }), + customer_prices: customerPrices.map((cp) => { + const price = product.prices.find((p) => p.id === cp.price_id); + if (!price) + throw new Error( + `add_plan: price ${cp.price_id} not found on product ${product.id}`, + ); + return { ...cp, price }; + }), + }; + + return { + plan: mergeAutumnBillingPlans({ + base: plan, + incoming: { + customerId: plan.customerId, + insertCustomerProducts: [newCusProduct], + }, + }), + projectedFullCustomer, + matchedCustomerProducts: 1, + billingContexts: [], + }; +}; diff --git a/server/src/internal/migrations/v2/operations/errors/MigrationOperationError.ts b/server/src/internal/migrations/v2/operations/errors/MigrationOperationError.ts new file mode 100644 index 000000000..5c0a22c0d --- /dev/null +++ b/server/src/internal/migrations/v2/operations/errors/MigrationOperationError.ts @@ -0,0 +1,32 @@ +export type MigrationOperationErrorCode = + | "invalid_operation_input" + | "unsupported_operation_input" + | "missing_prepared_state"; + +export class MigrationOperationError extends Error { + readonly code: MigrationOperationErrorCode; + readonly operationType: string; + readonly field?: string; + readonly details?: Record; + + constructor({ + message, + code, + operationType, + field, + details, + }: { + message: string; + code: MigrationOperationErrorCode; + operationType: string; + field?: string; + details?: Record; + }) { + super(message); + this.name = "MigrationOperationError"; + this.code = code; + this.operationType = operationType; + this.field = field; + this.details = details; + } +} diff --git a/server/src/internal/migrations/v2/operations/errors/index.ts b/server/src/internal/migrations/v2/operations/errors/index.ts new file mode 100644 index 000000000..c31d29ada --- /dev/null +++ b/server/src/internal/migrations/v2/operations/errors/index.ts @@ -0,0 +1 @@ +export * from "./MigrationOperationError.js"; diff --git a/server/src/internal/migrations/v2/operations/operationRegistry.ts b/server/src/internal/migrations/v2/operations/operationRegistry.ts new file mode 100644 index 000000000..59702fd3a --- /dev/null +++ b/server/src/internal/migrations/v2/operations/operationRegistry.ts @@ -0,0 +1,32 @@ +import type { CustomerOperation } from "@autumn/shared/api/migrations/operations/customer/index.js"; +import { processAddPlan } from "./addPlan/index.js"; +import type { OperationProcessor } from "./types/index.js"; +import { processUpdatePlan } from "./updatePlan/index.js"; + +/** Execution order — operations are sorted to match this sequence. */ +const EXECUTION_ORDER: CustomerOperation["type"][] = [ + "add_plan", + "update_plan", +]; + +const processors: Record< + CustomerOperation["type"], + OperationProcessor +> = { + add_plan: processAddPlan as OperationProcessor, + update_plan: processUpdatePlan as OperationProcessor, +}; + +export function getProcessor({ + type, +}: { type: string }): OperationProcessor { + const processor = processors[type as CustomerOperation["type"]]; + if (!processor) + throw new Error(`No processor registered for operation type "${type}"`); + return processor; +} + +export function executionPriority({ type }: { type: string }): number { + const index = EXECUTION_ORDER.indexOf(type as CustomerOperation["type"]); + return index === -1 ? EXECUTION_ORDER.length : index; +} diff --git a/server/src/internal/migrations/v2/operations/types/index.ts b/server/src/internal/migrations/v2/operations/types/index.ts new file mode 100644 index 000000000..322985cf2 --- /dev/null +++ b/server/src/internal/migrations/v2/operations/types/index.ts @@ -0,0 +1,2 @@ +export * from "./migrateCustomerContext.js"; +export * from "./processOperationTypes.js"; diff --git a/server/src/internal/migrations/v2/operations/types/migrateCustomerContext.ts b/server/src/internal/migrations/v2/operations/types/migrateCustomerContext.ts new file mode 100644 index 000000000..bbadaff98 --- /dev/null +++ b/server/src/internal/migrations/v2/operations/types/migrateCustomerContext.ts @@ -0,0 +1,9 @@ +import type { FullCustomer } from "@autumn/shared"; +import type { MigrationStripeCache } from "@/internal/migrations/v2/stripeCache/index.js"; +import type { MigrationRuntime } from "../../types/migrationDefinition.js"; + +export type MigrateCustomerContext = { + migration: MigrationRuntime; + fullCustomer: FullCustomer; + stripeCache: MigrationStripeCache; +}; diff --git a/server/src/internal/migrations/v2/operations/types/processOperationTypes.ts b/server/src/internal/migrations/v2/operations/types/processOperationTypes.ts new file mode 100644 index 000000000..33034621f --- /dev/null +++ b/server/src/internal/migrations/v2/operations/types/processOperationTypes.ts @@ -0,0 +1,33 @@ +import type { + AutumnBillingPlan, + FullCustomer, + UpdateSubscriptionBillingContext, +} from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import type { MigrateCustomerContext } from "./migrateCustomerContext.js"; + +/** + * Output of one processor: the updated AutumnBillingPlan plus lightweight + * metadata used by the ordered operation fold. + */ +export type ProcessOperationResult = { + plan: AutumnBillingPlan; + projectedFullCustomer: FullCustomer; + matchedCustomerProducts: number; + billingContexts: UpdateSubscriptionBillingContext[]; +}; + +/** + * Shared contract for every per-op processor. Pure-ish: any DB reads + * are allowed (catalog lookups, migration inspection); all writes + * must go through the returned `plan`, which the orchestrator executes + * later. + */ +export type OperationProcessor = (args: { + ctx: AutumnContext; + op: Op; + opIndex: number; + context: MigrateCustomerContext; + plan: AutumnBillingPlan; + projectedFullCustomer: FullCustomer; +}) => Promise; diff --git a/server/src/internal/migrations/v2/operations/updatePlan/applyPrepareResults/applyPrepareResults.ts b/server/src/internal/migrations/v2/operations/updatePlan/applyPrepareResults/applyPrepareResults.ts new file mode 100644 index 000000000..ffe418413 --- /dev/null +++ b/server/src/internal/migrations/v2/operations/updatePlan/applyPrepareResults/applyPrepareResults.ts @@ -0,0 +1,241 @@ +import type { AutumnBillingPlan } from "@autumn/shared"; +import type { UpdatePlanOp } from "@autumn/shared/api/migrations/operations/customer/updatePlan/index.js"; +import type { ReusePricesAndEntitlements } from "@/internal/billing/v2/setup/patch/index.js"; +import { + EnsurePricesAndEntitlementsResultSchema, + type EnsurePricesAndEntitlementsResult, + type PreparedArtifactRef, +} from "@/internal/migrations/v2/prepare/modules/ensurePricesAndEntitlements/index.js"; +import { buildPrepareModuleKey } from "@/internal/migrations/v2/prepare/utils/index.js"; +import { hashJson } from "@/utils/hash/hashJson.js"; +import { MigrationOperationError } from "../../errors/index.js"; +import type { MigrateCustomerContext } from "../../types/index.js"; + +export type PreparedUpdatePlanArtifactIds = { + priceIds: Set; + entitlementIds: Set; +}; + +export const stripPreparedCatalogRows = ({ + plan, + preparedIds, +}: { + plan: AutumnBillingPlan; + preparedIds: PreparedUpdatePlanArtifactIds; +}): AutumnBillingPlan => ({ + ...plan, + customPrices: plan.customPrices?.filter( + (price) => !preparedIds.priceIds.has(price.id), + ), + customEntitlements: plan.customEntitlements?.filter( + (entitlement) => !preparedIds.entitlementIds.has(entitlement.id), + ), +}); + +const ensurePricesAndEntitlementsKey = buildPrepareModuleKey({ + kind: "ensure_prices_and_entitlements", + parts: ["update_plan"], +}); + +const getPreparedArtifacts = ({ + context, +}: { + context: MigrateCustomerContext; +}): EnsurePricesAndEntitlementsResult => { + const preparedState = + context.migration.prepared_state?.[ensurePricesAndEntitlementsKey]; + const result = + EnsurePricesAndEntitlementsResultSchema.safeParse(preparedState); + + if (!result.success) { + throw new MigrationOperationError({ + code: "missing_prepared_state", + operationType: "update_plan", + field: "prepared_state", + message: + "Migration update_plan requires prepared prices and entitlements. Run prepare before migrating customers.", + details: { prepareKey: ensurePricesAndEntitlementsKey }, + }); + } + + return result.data; +}; + +const findArtifact = ({ + artifacts, + opIndex, + kind, + itemIndex, + hash, + internalProductId, +}: { + artifacts: PreparedArtifactRef[]; + opIndex: number; + kind: PreparedArtifactRef["kind"]; + itemIndex?: number; + hash: string; + internalProductId: string; +}) => { + const artifact = artifacts.find( + (candidate) => + candidate.op_index === opIndex && + candidate.kind === kind && + candidate.item_index === itemIndex && + candidate.internal_product_id === internalProductId && + candidate.hash === hash, + ); + + if (!artifact) { + throw new MigrationOperationError({ + code: "missing_prepared_state", + operationType: "update_plan", + field: "prepared_state", + message: + "Migration update_plan prepared_state is missing an artifact for the current operation input.", + details: { opIndex, kind, itemIndex, internalProductId, hash }, + }); + } + + return artifact; +}; + +const addPreparedId = ({ + ids, + artifact, + reusePricesAndEntitlements, +}: { + ids: PreparedUpdatePlanArtifactIds; + artifact: PreparedArtifactRef; + reusePricesAndEntitlements: ReusePricesAndEntitlements; +}) => { + if (artifact.price_id) { + if (!reusePricesAndEntitlements.pricesById.has(artifact.price_id)) { + throw new MigrationOperationError({ + code: "missing_prepared_state", + operationType: "update_plan", + field: "prepared_state", + message: + "Migration update_plan prepared_state references a missing prepared price.", + details: { priceId: artifact.price_id }, + }); + } + ids.priceIds.add(artifact.price_id); + } + if (artifact.entitlement_id) { + if ( + !reusePricesAndEntitlements.entitlementsById.has(artifact.entitlement_id) + ) { + throw new MigrationOperationError({ + code: "missing_prepared_state", + operationType: "update_plan", + field: "prepared_state", + message: + "Migration update_plan prepared_state references a missing prepared entitlement.", + details: { entitlementId: artifact.entitlement_id }, + }); + } + ids.entitlementIds.add(artifact.entitlement_id); + } +}; + +export const applyPrepareResultsToUpdatePlan = ({ + context, + op, + opIndex, + internalProductId, +}: { + context: MigrateCustomerContext; + op: UpdatePlanOp; + opIndex: number; + internalProductId: string; +}): { + op: UpdatePlanOp; + preparedIds: PreparedUpdatePlanArtifactIds; + reusePricesAndEntitlements: ReusePricesAndEntitlements; +} => { + const preparedIds: PreparedUpdatePlanArtifactIds = { + priceIds: new Set(), + entitlementIds: new Set(), + }; + const emptyReusableRows: ReusePricesAndEntitlements = { + pricesById: new Map(), + entitlementsById: new Map(), + }; + + const customize = op.customize; + const needsPreparedArtifacts = + (customize?.price !== undefined && customize.price !== null) || + (customize?.add_items?.length ?? 0) > 0; + + if (!customize || !needsPreparedArtifacts) { + return { + op, + preparedIds, + reusePricesAndEntitlements: emptyReusableRows, + }; + } + + const preparedResult = getPreparedArtifacts({ context }); + const artifacts = preparedResult.artifacts; + const reusePricesAndEntitlements: ReusePricesAndEntitlements = { + pricesById: new Map( + preparedResult.prices.map((price) => [price.id, price]), + ), + entitlementsById: new Map( + preparedResult.entitlements.map((entitlement) => [ + entitlement.id, + entitlement, + ]), + ), + }; + const nextCustomize = { ...customize }; + + if (customize.price !== undefined && customize.price !== null) { + const artifact = findArtifact({ + artifacts, + opIndex, + kind: "base_price", + internalProductId, + hash: hashJson({ value: customize.price }), + }); + addPreparedId({ ids: preparedIds, artifact, reusePricesAndEntitlements }); + nextCustomize.price = { + ...customize.price, + ...(artifact.price_id ? { price_id: artifact.price_id } : {}), + }; + } + + if (customize.add_items) { + nextCustomize.add_items = customize.add_items.map((item, itemIndex) => { + const artifact = findArtifact({ + artifacts, + opIndex, + kind: "add_item", + itemIndex, + internalProductId, + hash: hashJson({ value: item }), + }); + addPreparedId({ + ids: preparedIds, + artifact, + reusePricesAndEntitlements, + }); + return { + ...item, + ...(artifact.price_id ? { price_id: artifact.price_id } : {}), + ...(artifact.entitlement_id + ? { entitlement_id: artifact.entitlement_id } + : {}), + }; + }); + } + + return { + op: { + ...op, + customize: nextCustomize, + }, + preparedIds, + reusePricesAndEntitlements, + }; +}; diff --git a/server/src/internal/migrations/v2/operations/updatePlan/applyPrepareResults/index.ts b/server/src/internal/migrations/v2/operations/updatePlan/applyPrepareResults/index.ts new file mode 100644 index 000000000..31f646122 --- /dev/null +++ b/server/src/internal/migrations/v2/operations/updatePlan/applyPrepareResults/index.ts @@ -0,0 +1 @@ +export * from "./applyPrepareResults.js"; diff --git a/server/src/internal/migrations/v2/operations/updatePlan/index.ts b/server/src/internal/migrations/v2/operations/updatePlan/index.ts new file mode 100644 index 000000000..e12357d71 --- /dev/null +++ b/server/src/internal/migrations/v2/operations/updatePlan/index.ts @@ -0,0 +1 @@ +export * from "./processUpdatePlan.js"; diff --git a/server/src/internal/migrations/v2/operations/updatePlan/processUpdatePlan.ts b/server/src/internal/migrations/v2/operations/updatePlan/processUpdatePlan.ts new file mode 100644 index 000000000..adde216ad --- /dev/null +++ b/server/src/internal/migrations/v2/operations/updatePlan/processUpdatePlan.ts @@ -0,0 +1,123 @@ +import type { + AutumnBillingPlan, + UpdateSubscriptionBillingContext, +} from "@autumn/shared"; +import type { UpdatePlanOp } from "@autumn/shared/api/migrations/operations/customer/updatePlan/index.js"; +import { computeUpdateSubscriptionPlan } from "@/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionPlan.js"; +import { logUpdateSubscriptionContext } from "@/internal/billing/v2/actions/updateSubscription/logs/logUpdateSubscriptionContext.js"; +import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumnBillingPlan.js"; +import { MigrationOperationError } from "../errors/index.js"; +import type { OperationProcessor } from "../types/index.js"; +import { + appendMigrationBillingLog, + filterCustomerProductsByPlanFilter, + mergeAutumnBillingPlans, +} from "../utils/index.js"; +import { stripPreparedCatalogRows } from "./applyPrepareResults/index.js"; +import { setupUpdatePlanProductContext } from "./setup/index.js"; + +const assertNoChargeArtifacts = ({ + plan, + customerProductId, +}: { + plan: AutumnBillingPlan; + customerProductId: string; +}) => { + const lineItemCount = plan.lineItems?.length ?? 0; + const customLineItemCount = plan.customLineItems?.length ?? 0; + const hasRefundPlan = plan.refundPlan !== undefined; + + if (lineItemCount === 0 && customLineItemCount === 0 && !hasRefundPlan) + return; + + throw new MigrationOperationError({ + code: "unsupported_operation_input", + operationType: "update_plan", + field: "customize", + message: "Migration update_plan produced charge artifacts", + details: { + customerProductId, + lineItemCount, + customLineItemCount, + hasRefundPlan, + }, + }); +}; + +export const processUpdatePlan = async ({ + ctx, + context, + op, + opIndex, + plan, + projectedFullCustomer, +}: Parameters>[0]) => { + const { customerProducts: matchedCustomerProducts } = + filterCustomerProductsByPlanFilter({ + customerProducts: projectedFullCustomer.customer_products, + planFilter: op.plan_filter, + }); + + let nextPlan = plan; + const billingContexts: UpdateSubscriptionBillingContext[] = []; + + for (const customerProduct of matchedCustomerProducts) { + const productContext = await setupUpdatePlanProductContext({ + ctx, + context, + op, + opIndex, + projectedFullCustomer, + customerProduct, + }); + if (!productContext) continue; + + appendMigrationBillingLog({ + ctx, + key: "billingContext", + log: (logCtx) => + logUpdateSubscriptionContext({ + ctx: logCtx, + billingContext: productContext.billingContext, + }), + }); + + const computedPlan = await computeUpdateSubscriptionPlan({ + ctx, + billingContext: productContext.billingContext, + params: productContext.params, + }); + appendMigrationBillingLog({ + ctx, + key: "autumnBillingPlan", + log: (logCtx) => + logAutumnBillingPlan({ + ctx: logCtx, + plan: computedPlan, + billingContext: productContext.billingContext, + }), + }); + + assertNoChargeArtifacts({ + plan: computedPlan, + customerProductId: customerProduct.id, + }); + const executablePlan = stripPreparedCatalogRows({ + plan: computedPlan, + preparedIds: productContext.preparedIds, + }); + + nextPlan = mergeAutumnBillingPlans({ + base: nextPlan, + incoming: executablePlan, + }); + billingContexts.push(productContext.billingContext); + } + + return { + plan: nextPlan, + projectedFullCustomer, + matchedCustomerProducts: matchedCustomerProducts.length, + billingContexts, + }; +}; diff --git a/server/src/internal/migrations/v2/operations/updatePlan/setup/index.ts b/server/src/internal/migrations/v2/operations/updatePlan/setup/index.ts new file mode 100644 index 000000000..037b1a4e7 --- /dev/null +++ b/server/src/internal/migrations/v2/operations/updatePlan/setup/index.ts @@ -0,0 +1,2 @@ +export * from "./setupUpdatePlanProductContext.js"; +export * from "./types.js"; diff --git a/server/src/internal/migrations/v2/operations/updatePlan/setup/itemAlreadyExists.ts b/server/src/internal/migrations/v2/operations/updatePlan/setup/itemAlreadyExists.ts new file mode 100644 index 000000000..521f2cc6f --- /dev/null +++ b/server/src/internal/migrations/v2/operations/updatePlan/setup/itemAlreadyExists.ts @@ -0,0 +1,88 @@ +import { + EntInterval, + type FullCusProduct, + findCustomerEntitlementByFeature, + findFeatureById, + isBooleanFeature, +} from "@autumn/shared"; +import type { CreatePlanItemParamsV1 } from "@autumn/shared/api/products/items/crud/createPlanItemParamsV1.js"; +import type { PlanItemFilter } from "@autumn/shared/api/products/items/filter/planItemFilter.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; + +const getAddItemEntitlementInterval = ({ + item, +}: { + item: CreatePlanItemParamsV1; +}): EntInterval => + (item.reset?.interval ?? + item.price?.interval ?? + EntInterval.Lifetime) as EntInterval; + +const itemWillReplaceRemovedItem = ({ + item, + removeItems, +}: { + item: CreatePlanItemParamsV1; + removeItems?: PlanItemFilter[]; +}) => { + if (!removeItems?.length) return false; + + const itemInterval = getAddItemEntitlementInterval({ item }); + + return removeItems.some((removeItem) => { + if ( + removeItem.feature_id !== undefined && + removeItem.feature_id !== item.feature_id + ) { + return false; + } + + if ( + removeItem.interval !== undefined && + String(removeItem.interval) !== String(itemInterval) + ) { + return false; + } + + return true; + }); +}; + +export const itemAlreadyExists = ({ + ctx, + customerProduct, + item, + removeItems, +}: { + ctx: AutumnContext; + customerProduct: FullCusProduct; + item: CreatePlanItemParamsV1; + removeItems?: PlanItemFilter[]; +}): boolean => { + if (itemWillReplaceRemovedItem({ item, removeItems })) return false; + + const feature = findFeatureById({ + features: ctx.features, + featureId: item.feature_id, + errorOnNotFound: true, + }); + + if (isBooleanFeature({ feature })) { + return Boolean( + findCustomerEntitlementByFeature({ + cusEnts: customerProduct.customer_entitlements, + featureId: item.feature_id, + }), + ); + } + + const itemInterval = getAddItemEntitlementInterval({ item }); + + return customerProduct.customer_entitlements.some( + (customerEntitlement) => + customerEntitlement.feature_id === item.feature_id && + String( + customerEntitlement.entitlement.interval ?? EntInterval.Lifetime, + ) === String(itemInterval), + ); +}; diff --git a/server/src/internal/migrations/v2/operations/updatePlan/setup/setupUpdatePlanProductContext.ts b/server/src/internal/migrations/v2/operations/updatePlan/setup/setupUpdatePlanProductContext.ts new file mode 100644 index 000000000..a690ab2f3 --- /dev/null +++ b/server/src/internal/migrations/v2/operations/updatePlan/setup/setupUpdatePlanProductContext.ts @@ -0,0 +1,172 @@ +import { + BillingVersion, + type FullCusProduct, + type FullCustomer, + hasCustomItems, + orgDisableStripeWrites, + type UpdateSubscriptionBillingContext, + UpdateSubscriptionIntent, + type UpdateSubscriptionV1Params, +} from "@autumn/shared"; +import type { UpdatePlanOp } from "@autumn/shared/api/migrations/operations/customer/updatePlan/index.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { setupUpdateSubscriptionProductContext } from "@/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionProductContext.js"; +import { setupAdjustableQuantities } from "@/internal/billing/v2/setup/setupAdjustableQuantities.js"; +import { setupAnchorResetRefund } from "@/internal/billing/v2/setup/setupAnchorResetRefund.js"; +import { setupFeatureQuantitiesContext } from "@/internal/billing/v2/setup/setupFeatureQuantitiesContext.js"; +import { setupInvoiceModeContext } from "@/internal/billing/v2/setup/setupInvoiceModeContext.js"; +import { setupMigrationOperationBillingContext } from "@/internal/migrations/v2/run/migrateCustomer/setup/index.js"; +import type { MigrateCustomerContext } from "../../types/index.js"; +import { applyPrepareResultsToUpdatePlan } from "../applyPrepareResults/index.js"; +import { itemAlreadyExists } from "./itemAlreadyExists.js"; +import type { UpdatePlanProductContext } from "./types.js"; + +export const setupUpdatePlanProductContext = async ({ + ctx, + context, + op, + opIndex, + projectedFullCustomer, + customerProduct, +}: { + ctx: AutumnContext; + context: MigrateCustomerContext; + op: UpdatePlanOp; + opIndex: number; + projectedFullCustomer: FullCustomer; + customerProduct: FullCusProduct; +}): Promise => { + const { + op: preparedOp, + preparedIds, + reusePricesAndEntitlements, + } = applyPrepareResultsToUpdatePlan({ + context, + op, + opIndex, + internalProductId: customerProduct.internal_product_id, + }); + + const addItems = preparedOp.customize?.add_items?.filter( + (item) => + !itemAlreadyExists({ + ctx, + customerProduct, + item, + removeItems: preparedOp.customize?.remove_items, + }), + ); + const customize = { + ...preparedOp.customize, + ...(addItems ? { add_items: addItems } : {}), + }; + if ( + preparedOp.version === undefined && + customize.price === undefined && + customize.add_items?.length === 0 && + customize.remove_items === undefined + ) { + return undefined; + } + + const customerId = + projectedFullCustomer.id ?? projectedFullCustomer.internal_id; + const entity = customerProduct.internal_entity_id + ? projectedFullCustomer.entities.find( + (candidate) => + candidate.internal_id === customerProduct.internal_entity_id, + ) + : undefined; + const productFullCustomer = entity + ? { ...projectedFullCustomer, entity } + : projectedFullCustomer; + + const params: UpdateSubscriptionV1Params = { + customer_id: customerId, + entity_id: customerProduct.entity_id ?? undefined, + customer_product_id: customerProduct.id, + plan_id: customerProduct.product.id, + version: preparedOp.version, + ...(preparedOp.customize ? { customize } : {}), + proration_behavior: "none", + no_billing_changes: + context.migration.no_billing_changes === true ? true : undefined, + }; + + const { + customerProduct: targetCustomerProduct, + fullProduct, + patchContext, + customPrices, + customEnts, + } = await setupUpdateSubscriptionProductContext({ + ctx, + fullCustomer: productFullCustomer, + params, + reusePricesAndEntitlements, + }); + + const operationBillingContext = await setupMigrationOperationBillingContext({ + ctx, + context, + fullCustomer: productFullCustomer, + customerProduct: targetCustomerProduct, + fullProduct, + }); + + const featureQuantities = setupFeatureQuantitiesContext({ + ctx, + featureQuantitiesParams: params, + fullProduct, + currentCustomerProduct: targetCustomerProduct, + initializeUndefinedQuantities: true, + }); + + const skipBillingChanges = + orgDisableStripeWrites({ ctx }) || + params.no_billing_changes === true || + operationBillingContext.stripeSubscription === undefined; + + const billingContext: UpdateSubscriptionBillingContext = { + intent: UpdateSubscriptionIntent.UpdatePlan, + fullCustomer: productFullCustomer, + fullProducts: [fullProduct], + customerProduct: targetCustomerProduct, + patchContext, + recalculateBalances: false, + stripeSubscription: operationBillingContext.stripeSubscription, + stripeSubscriptionSchedule: + operationBillingContext.stripeSubscriptionSchedule, + stripeDiscounts: operationBillingContext.stripeDiscounts, + stripeCustomer: operationBillingContext.stripeCustomer, + paymentMethod: operationBillingContext.paymentMethod, + currentEpochMs: operationBillingContext.currentEpochMs, + billingCycleAnchorMs: operationBillingContext.billingCycleAnchorMs, + resetCycleAnchorMs: operationBillingContext.resetCycleAnchorMs, + requestedBillingCycleAnchor: params.billing_cycle_anchor, + requestedProrationBehavior: params.proration_behavior, + invoiceMode: setupInvoiceModeContext({ params }), + featureQuantities, + adjustableFeatureQuantities: setupAdjustableQuantities({ params }), + customPrices, + customEnts, + trialContext: operationBillingContext.trialContext, + isCustom: hasCustomItems(params.customize), + billingVersion: BillingVersion.V2, + actionSource: "migration", + skipBillingChanges, + checkoutMode: null, + anchorResetRefund: setupAnchorResetRefund({ + billingCycleAnchor: params.billing_cycle_anchor, + prorationBehavior: params.proration_behavior, + outgoingCustomerProduct: targetCustomerProduct, + }), + }; + + return { + customerProduct: targetCustomerProduct, + params, + billingContext, + preparedIds, + }; +}; diff --git a/server/src/internal/migrations/v2/operations/updatePlan/setup/types.ts b/server/src/internal/migrations/v2/operations/updatePlan/setup/types.ts new file mode 100644 index 000000000..19860f8c2 --- /dev/null +++ b/server/src/internal/migrations/v2/operations/updatePlan/setup/types.ts @@ -0,0 +1,13 @@ +import type { + FullCusProduct, + UpdateSubscriptionBillingContext, + UpdateSubscriptionV1Params, +} from "@autumn/shared"; +import type { PreparedUpdatePlanArtifactIds } from "../applyPrepareResults/index.js"; + +export interface UpdatePlanProductContext { + customerProduct: FullCusProduct; + params: UpdateSubscriptionV1Params; + billingContext: UpdateSubscriptionBillingContext; + preparedIds: PreparedUpdatePlanArtifactIds; +} diff --git a/server/src/internal/migrations/v2/operations/utils/filterCustomerProductsByPlanFilter.ts b/server/src/internal/migrations/v2/operations/utils/filterCustomerProductsByPlanFilter.ts new file mode 100644 index 000000000..14756dd00 --- /dev/null +++ b/server/src/internal/migrations/v2/operations/utils/filterCustomerProductsByPlanFilter.ts @@ -0,0 +1,36 @@ +import type { FullCusProduct } from "@autumn/shared"; +import type { PlanFilter } from "@autumn/shared/api/migrations/filters/planFilter.js"; +import { planFilterMatchesCustomerProduct } from "@autumn/shared/api/products/utils/match/index.js"; + +export const filterCustomerProductsByPlanFilter = ({ + customerProducts, + planFilter, + requireLiteralPlanIdErrorMessage, +}: { + customerProducts: FullCusProduct[]; + planFilter: PlanFilter; + requireLiteralPlanIdErrorMessage?: string; +}): { + customerProducts: FullCusProduct[]; + planId?: string; +} => { + const planId = + typeof planFilter.plan_id === "string" ? planFilter.plan_id : undefined; + if (requireLiteralPlanIdErrorMessage && !planId) { + throw new Error(requireLiteralPlanIdErrorMessage); + } + + const matchedCustomerProducts = []; + for (const customerProduct of customerProducts) { + if ( + planFilterMatchesCustomerProduct({ + filter: planFilter, + cusProduct: customerProduct, + }) + ) { + matchedCustomerProducts.push(customerProduct); + } + } + + return { customerProducts: matchedCustomerProducts, planId }; +}; diff --git a/server/src/internal/migrations/v2/operations/utils/index.ts b/server/src/internal/migrations/v2/operations/utils/index.ts new file mode 100644 index 000000000..f9bd058a0 --- /dev/null +++ b/server/src/internal/migrations/v2/operations/utils/index.ts @@ -0,0 +1,3 @@ +export * from "./filterCustomerProductsByPlanFilter.js"; +export * from "./logs/index.js"; +export * from "./mergeAutumnBillingPlans.js"; diff --git a/server/src/internal/migrations/v2/operations/utils/logs/appendMigrationBillingLog.ts b/server/src/internal/migrations/v2/operations/utils/logs/appendMigrationBillingLog.ts new file mode 100644 index 000000000..3f2dd19db --- /dev/null +++ b/server/src/internal/migrations/v2/operations/utils/logs/appendMigrationBillingLog.ts @@ -0,0 +1,50 @@ +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { appendToExtraLogs } from "@/utils/logging/addToExtraLogs.js"; + +type BillingLogKey = + | "billingContext" + | "autumnBillingPlan" + | "stripeBillingPlan" + | "stripeBillingResult"; + +type MigrationBillingLogKey = + | "migrationBillingContexts" + | "migrationAutumnBillingPlans" + | "migrationStripeBillingPlans" + | "migrationStripeBillingResults"; + +const migrationLogKeyByBillingLogKey: Record< + BillingLogKey, + MigrationBillingLogKey +> = { + billingContext: "migrationBillingContexts", + autumnBillingPlan: "migrationAutumnBillingPlans", + stripeBillingPlan: "migrationStripeBillingPlans", + stripeBillingResult: "migrationStripeBillingResults", +}; + +export const appendMigrationBillingLog = ({ + ctx, + key, + log, +}: { + ctx: AutumnContext; + key: BillingLogKey; + log: (ctx: AutumnContext) => void; +}) => { + const captureCtx: AutumnContext = { + ...ctx, + extraLogs: {}, + }; + + log(captureCtx); + + const value = captureCtx.extraLogs[key]; + if (value === undefined) return; + + appendToExtraLogs({ + ctx, + key: migrationLogKeyByBillingLogKey[key], + value, + }); +}; diff --git a/server/src/internal/migrations/v2/operations/utils/logs/index.ts b/server/src/internal/migrations/v2/operations/utils/logs/index.ts new file mode 100644 index 000000000..7a4cf4afd --- /dev/null +++ b/server/src/internal/migrations/v2/operations/utils/logs/index.ts @@ -0,0 +1 @@ +export * from "./appendMigrationBillingLog.js"; diff --git a/server/src/internal/migrations/v2/operations/utils/mergeAutumnBillingPlans.ts b/server/src/internal/migrations/v2/operations/utils/mergeAutumnBillingPlans.ts new file mode 100644 index 000000000..bb6786dc6 --- /dev/null +++ b/server/src/internal/migrations/v2/operations/utils/mergeAutumnBillingPlans.ts @@ -0,0 +1,137 @@ +import type { AutumnBillingPlan } from "@autumn/shared"; + +export const mergeAutumnBillingPlans = ({ + base, + incoming, +}: { + base: AutumnBillingPlan; + incoming: AutumnBillingPlan; +}): AutumnBillingPlan => ({ + ...base, + insertCustomerProducts: mergeById({ + base: base.insertCustomerProducts, + incoming: incoming.insertCustomerProducts, + }), + updateCustomerProduct: undefined, + updateCustomerProducts: mergeByKey({ + base: [ + ...(base.updateCustomerProduct ? [base.updateCustomerProduct] : []), + ...(base.updateCustomerProducts ?? []), + ], + incoming: [ + ...(incoming.updateCustomerProduct + ? [incoming.updateCustomerProduct] + : []), + ...(incoming.updateCustomerProducts ?? []), + ], + getKey: (update) => update.customerProduct.id, + }), + deleteCustomerProduct: undefined, + deleteCustomerProducts: mergeById({ + base: [ + ...(base.deleteCustomerProduct ? [base.deleteCustomerProduct] : []), + ...(base.deleteCustomerProducts ?? []), + ], + incoming: [ + ...(incoming.deleteCustomerProduct + ? [incoming.deleteCustomerProduct] + : []), + ...(incoming.deleteCustomerProducts ?? []), + ], + }), + customPrices: mergeById({ + base: base.customPrices, + incoming: incoming.customPrices, + }), + customEntitlements: mergeById({ + base: base.customEntitlements, + incoming: incoming.customEntitlements, + }), + customFreeTrial: incoming.customFreeTrial ?? base.customFreeTrial, + lineItems: mergeById({ + base: base.lineItems, + incoming: incoming.lineItems, + }), + customLineItems: mergeByKey({ + base: base.customLineItems, + incoming: incoming.customLineItems, + getKey: (lineItem) => `${lineItem.description}:${lineItem.amount}`, + }), + insertCustomerEntitlements: mergeById({ + base: base.insertCustomerEntitlements, + incoming: incoming.insertCustomerEntitlements, + }), + patchCustomerProducts: mergePatchCustomerProducts({ + base: base.patchCustomerProducts, + incoming: incoming.patchCustomerProducts, + }), + updateCustomerEntitlements: mergeByKey({ + base: base.updateCustomerEntitlements, + incoming: incoming.updateCustomerEntitlements, + getKey: (update) => update.customerEntitlement.id, + }), + autoTopupRebalance: + base.autoTopupRebalance || incoming.autoTopupRebalance + ? { + deltas: + mergeByKey({ + base: base.autoTopupRebalance?.deltas, + incoming: incoming.autoTopupRebalance?.deltas, + getKey: (delta) => delta.cusEntId, + }) ?? [], + } + : undefined, + upsertSubscription: incoming.upsertSubscription ?? base.upsertSubscription, + upsertInvoice: incoming.upsertInvoice ?? base.upsertInvoice, + refundPlan: incoming.refundPlan ?? base.refundPlan, +}); + +const mergeById = ({ + base, + incoming, +}: { + base?: T[]; + incoming?: T[]; +}): T[] => mergeByKey({ base, incoming, getKey: (item) => item.id }) ?? []; + +const mergeByKey = ({ + base, + incoming, + getKey, +}: { + base?: T[]; + incoming?: T[]; + getKey: (item: T) => string; +}): T[] | undefined => { + if (!base?.length && !incoming?.length) return undefined; + + const itemByKey = new Map(); + for (const item of base ?? []) itemByKey.set(getKey(item), item); + for (const item of incoming ?? []) itemByKey.set(getKey(item), item); + + return Array.from(itemByKey.values()); +}; + +type PatchCustomerProduct = NonNullable< + AutumnBillingPlan["patchCustomerProducts"] +>[number]; + +const mergePatchCustomerProducts = ({ + base, + incoming, +}: { + base?: PatchCustomerProduct[]; + incoming?: PatchCustomerProduct[]; +}): PatchCustomerProduct[] | undefined => + mergeByKey({ + base, + incoming, + getKey: (patch) => + [ + patch.customerProduct.id, + ...patch.insertCustomerEntitlements.map(({ id }) => `ie:${id}`), + ...patch.insertCustomerPrices.map(({ id }) => `ip:${id}`), + ...patch.deleteCustomerEntitlements.map(({ id }) => `de:${id}`), + ...patch.deleteCustomerPrices.map(({ id }) => `dp:${id}`), + ].join("|"), + }); diff --git a/server/src/internal/migrations/v2/prepare/getImplicitPrepareModules.ts b/server/src/internal/migrations/v2/prepare/getImplicitPrepareModules.ts new file mode 100644 index 000000000..ca7b530fa --- /dev/null +++ b/server/src/internal/migrations/v2/prepare/getImplicitPrepareModules.ts @@ -0,0 +1,59 @@ +import type { Operations } from "@autumn/shared"; +import type { UpdatePlanOp } from "@autumn/shared/api/migrations/operations/customer/updatePlan/index.js"; +import type { + EnsurePricesAndEntitlementsInput, + ensurePricesAndEntitlements, +} from "./modules/ensurePricesAndEntitlements/index.js"; +import { ensurePricesAndEntitlements as ensurePricesAndEntitlementsModule } from "./modules/ensurePricesAndEntitlements/index.js"; +import { buildPrepareModuleKey } from "./utils/index.js"; + +/** One instance of a prep module to run. */ +export type ImplicitPrepInstance = { + key: string; + module: typeof ensurePricesAndEntitlements; + input: EnsurePricesAndEntitlementsInput; +}; + +/** + * Pure walker. Takes an `operations` object directly so scripts and + * other callers can derive prep instances without a Migration row. + * Module key format: `:update_plan`. + */ +export const getImplicitPrepareModules = ({ + operations, +}: { + operations: Operations | null | undefined; +}): ImplicitPrepInstance[] => { + const modulesByKey = new Map(); + const updatePlanOps: { opIndex: number; op: UpdatePlanOp }[] = []; + + for (const [opIndex, op] of (operations?.customer ?? []).entries()) { + if ( + op.type !== "update_plan" || + !( + (op.customize?.price !== undefined && op.customize.price !== null) || + (op.customize?.add_items?.length ?? 0) > 0 + ) + ) { + continue; + } + + updatePlanOps.push({ opIndex, op }); + } + + if (updatePlanOps.length > 0) { + const key = buildPrepareModuleKey({ + kind: ensurePricesAndEntitlementsModule.kind, + parts: ["update_plan"], + }); + modulesByKey.set(key, { + key, + module: ensurePricesAndEntitlementsModule, + input: { + updatePlanOps, + }, + }); + } + + return Array.from(modulesByKey.values()); +}; diff --git a/server/src/internal/migrations/v2/prepare/index.ts b/server/src/internal/migrations/v2/prepare/index.ts new file mode 100644 index 000000000..dfbaf0e8f --- /dev/null +++ b/server/src/internal/migrations/v2/prepare/index.ts @@ -0,0 +1,5 @@ +export * from "./getImplicitPrepareModules.js"; +export * from "./modules/ensurePricesAndEntitlements/index.js"; +export * from "./prepare.js"; +export * from "./runPrepareModules.js"; +export * from "./types/index.js"; diff --git a/server/src/internal/migrations/v2/prepare/modules/ensurePricesAndEntitlements/ensurePricesAndEntitlements.ts b/server/src/internal/migrations/v2/prepare/modules/ensurePricesAndEntitlements/ensurePricesAndEntitlements.ts new file mode 100644 index 000000000..39fa6d605 --- /dev/null +++ b/server/src/internal/migrations/v2/prepare/modules/ensurePricesAndEntitlements/ensurePricesAndEntitlements.ts @@ -0,0 +1,343 @@ +import { + type Entitlement, + type FullProduct, + findFeatureById, + type Price, +} from "@autumn/shared"; +import type { UpdatePlanOp } from "@autumn/shared/api/migrations/operations/customer/updatePlan/index.js"; +import { planFilterMatchesProduct } from "@autumn/shared/api/products/utils/match/index.js"; +import { basePriceToProductItem } from "@autumn/shared/api/products/components/basePrice/basePriceToProductItem.js"; +import { planItemV1ToPriceAndEnt } from "@autumn/shared/api/products/items/mappers/planItemV1ToPriceAndEnt.js"; +import { enrichEntitlementsWithFeatures } from "@autumn/shared/utils/productUtils/entUtils/enrichEntitlement.js"; +import { itemToPriceAndEnt } from "@autumn/shared/utils/productV2Utils/productItemUtils/mappers/itemToPriceAndEnt.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { initStripeResourcesForProducts } from "@/internal/billing/v2/providers/stripe/utils/common/initStripeResourcesForProducts.js"; +import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js"; +import { ProductService } from "@/internal/products/ProductService.js"; +import { PriceService } from "@/internal/products/prices/PriceService.js"; +import { hashJson } from "@/utils/hash/hashJson.js"; +import type { PrepareModule } from "../../types/prepareModule.js"; +import type { + EnsurePricesAndEntitlementsResult, + PreparedArtifactRef, +} from "./types.js"; +import { inheritStripeProductFromCatalog } from "./inheritStripeProductFromCatalog.js"; + +export type EnsurePricesAndEntitlementsInput = { + updatePlanOps: { + opIndex: number; + op: UpdatePlanOp; + }[]; +}; + +const artifactHash = ({ value }: { value: unknown }) => hashJson({ value }); + +const preparedRowId = ({ + prefix, + value, +}: { + prefix: "ent" | "pr"; + value: unknown; +}) => `${prefix}_${hashJson({ value })}`; + +export const basePriceIdFor = ({ + scopeId, + opIndex, + internalProductId, + hash, +}: { + scopeId: string; + opIndex: number; + internalProductId: string; + hash: string; +}): string => + preparedRowId({ + prefix: "pr", + value: { scopeId, opIndex, internalProductId, kind: "base_price", hash }, + }); + +export const priceIdFor = ({ + scopeId, + opIndex, + itemIndex, + internalFeatureId, + internalProductId, + hash, +}: { + scopeId: string; + opIndex: number; + itemIndex: number; + internalFeatureId: string; + internalProductId: string; + hash: string; +}): string => + preparedRowId({ + prefix: "pr", + value: { + scopeId, + opIndex, + itemIndex, + internalFeatureId, + internalProductId, + kind: "add_item", + hash, + }, + }); + +export const entitlementIdFor = ({ + scopeId, + opIndex, + itemIndex, + internalFeatureId, + internalProductId, + hash, +}: { + scopeId: string; + opIndex: number; + itemIndex: number; + internalFeatureId: string; + internalProductId: string; + hash: string; +}): string => + preparedRowId({ + prefix: "ent", + value: { + scopeId, + opIndex, + itemIndex, + internalFeatureId, + internalProductId, + kind: "add_item", + hash, + }, + }); + +const getMatchedProducts = async ({ + ctx, + op, +}: { + ctx: AutumnContext; + op: UpdatePlanOp; +}) => { + const products = await ProductService.listFull({ + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + returnAll: true, + }); + + return products.filter((product) => + planFilterMatchesProduct({ + filter: op.plan_filter, + product, + }), + ); +}; + +const buildProductsWithPreparedRows = ({ + products, + prices, + entitlements, + features, +}: { + products: FullProduct[]; + prices: Price[]; + entitlements: Entitlement[]; + features: Parameters[0]["features"]; +}): FullProduct[] => { + const productsByInternalId = new Map( + products.map((product) => [product.internal_id, product]), + ); + const preparedProductIds = new Set([ + ...prices + .map((price) => price.internal_product_id) + .filter((id): id is string => Boolean(id)), + ...entitlements + .map((entitlement) => entitlement.internal_product_id) + .filter((id): id is string => Boolean(id)), + ]); + + return Array.from(preparedProductIds).map((internalProductId) => { + const product = productsByInternalId.get(internalProductId); + if (!product) { + throw new Error( + `ensurePricesAndEntitlements: product ${internalProductId} not found for prepared rows`, + ); + } + + return { + ...product, + prices: prices.filter( + (price) => price.internal_product_id === internalProductId, + ), + entitlements: enrichEntitlementsWithFeatures({ + entitlements: entitlements.filter( + (entitlement) => + entitlement.internal_product_id === internalProductId, + ), + features, + }), + }; + }); +}; + +export const ensurePricesAndEntitlements: PrepareModule< + EnsurePricesAndEntitlementsInput, + EnsurePricesAndEntitlementsResult +> = { + kind: "ensure_prices_and_entitlements", + + async plan({ ctx, scopeId, input }) { + const entitlementsById = new Map(); + const pricesById = new Map(); + const artifacts: PreparedArtifactRef[] = []; + + for (const { opIndex, op } of input.updatePlanOps) { + const customize = op.customize; + if (!customize) continue; + const matchedProducts = await getMatchedProducts({ ctx, op }); + + for (const product of matchedProducts) { + if (customize.price) { + const hash = artifactHash({ value: customize.price }); + const priceId = basePriceIdFor({ + scopeId, + opIndex, + internalProductId: product.internal_id, + hash, + }); + const item = basePriceToProductItem({ + ctx, + basePrice: customize.price, + }); + const { newPrice, updatedPrice } = itemToPriceAndEnt({ + item, + orgId: ctx.org.id, + internalProductId: product.internal_id, + isCustom: true, + features: ctx.features, + }); + const price = newPrice ?? updatedPrice; + + if (price) { + pricesById.set(priceId, { + ...price, + id: priceId, + internal_product_id: product.internal_id, + }); + artifacts.push({ + op_index: opIndex, + kind: "base_price", + internal_product_id: product.internal_id, + hash, + price_id: priceId, + }); + } + } + + for (const [itemIndex, item] of (customize.add_items ?? []).entries()) { + const feature = findFeatureById({ + features: ctx.features, + featureId: item.feature_id, + errorOnNotFound: true, + }); + const hash = artifactHash({ value: item }); + const entitlementId = entitlementIdFor({ + scopeId, + opIndex, + itemIndex, + internalFeatureId: feature.internal_id, + internalProductId: product.internal_id, + hash, + }); + const priceId = priceIdFor({ + scopeId, + opIndex, + itemIndex, + internalFeatureId: feature.internal_id, + internalProductId: product.internal_id, + hash, + }); + + const { newEnt, newPrice } = planItemV1ToPriceAndEnt({ + ctx, + item, + orgId: ctx.org.id, + internalProductId: product.internal_id, + isCustom: true, + }); + + if (newEnt) { + entitlementsById.set(entitlementId, { + ...newEnt, + id: entitlementId, + internal_product_id: product.internal_id, + }); + } + if (newPrice) { + const preparedPrice = inheritStripeProductFromCatalog({ + price: { + ...newPrice, + id: priceId, + entitlement_id: newEnt + ? entitlementId + : newPrice.entitlement_id, + internal_product_id: product.internal_id, + }, + product, + products: matchedProducts, + }); + + pricesById.set(priceId, { + ...preparedPrice, + }); + } + + artifacts.push({ + op_index: opIndex, + kind: "add_item", + item_index: itemIndex, + internal_product_id: product.internal_id, + hash, + ...(newPrice ? { price_id: priceId } : {}), + ...(newEnt ? { entitlement_id: entitlementId } : {}), + }); + } + } + } + + return { + entitlements: Array.from(entitlementsById.values()), + prices: Array.from(pricesById.values()), + artifacts, + }; + }, + + async apply({ ctx, planned, input }) { + if (planned.entitlements.length > 0) { + await EntitlementService.upsert({ + db: ctx.db, + data: planned.entitlements, + }); + } + if (planned.prices.length > 0) { + await PriceService.upsert({ db: ctx.db, data: planned.prices }); + const allMatchedProducts = ( + await Promise.all( + input.updatePlanOps.map(({ op }) => getMatchedProducts({ ctx, op })), + ) + ).flat(); + await initStripeResourcesForProducts({ + ctx, + products: buildProductsWithPreparedRows({ + products: allMatchedProducts, + prices: planned.prices, + entitlements: planned.entitlements, + features: ctx.features, + }), + }); + } + + return planned; + }, +}; diff --git a/server/src/internal/migrations/v2/prepare/modules/ensurePricesAndEntitlements/index.ts b/server/src/internal/migrations/v2/prepare/modules/ensurePricesAndEntitlements/index.ts new file mode 100644 index 000000000..5fc1e7e78 --- /dev/null +++ b/server/src/internal/migrations/v2/prepare/modules/ensurePricesAndEntitlements/index.ts @@ -0,0 +1,2 @@ +export * from "./ensurePricesAndEntitlements.js"; +export * from "./types.js"; diff --git a/server/src/internal/migrations/v2/prepare/modules/ensurePricesAndEntitlements/inheritStripeProductFromCatalog.ts b/server/src/internal/migrations/v2/prepare/modules/ensurePricesAndEntitlements/inheritStripeProductFromCatalog.ts new file mode 100644 index 000000000..46b2d1be4 --- /dev/null +++ b/server/src/internal/migrations/v2/prepare/modules/ensurePricesAndEntitlements/inheritStripeProductFromCatalog.ts @@ -0,0 +1,55 @@ +import { + type FullProduct, + findPriceByFeatureId, + type Price, + type UsagePriceConfig, +} from "@autumn/shared"; + +const getStripeAnchorPrice = ({ + product, + featureId, +}: { + product: FullProduct; + featureId: string; +}) => { + const price = findPriceByFeatureId({ + prices: product.prices, + featureId, + }); + + return price?.config.stripe_product_id ? price : undefined; +}; + +export const inheritStripeProductFromCatalog = ({ + price, + product, + products, +}: { + price: Price; + product: FullProduct; + products: FullProduct[]; +}): Price => { + const featureId = price.config.feature_id; + if (!featureId || price.config.stripe_product_id) return price; + + const samePlanProducts = products + .filter((candidate) => candidate.id === product.id) + .sort((a, b) => a.version - b.version); + const sourcePrice = samePlanProducts + .map((candidate) => getStripeAnchorPrice({ product: candidate, featureId })) + .find((candidate): candidate is Price => Boolean(candidate)); + + if (!sourcePrice?.config.stripe_product_id) return price; + + const config = price.config as UsagePriceConfig; + const sourceConfig = sourcePrice.config as UsagePriceConfig; + + return { + ...price, + config: { + ...config, + stripe_product_id: sourceConfig.stripe_product_id, + stripe_meter_id: sourceConfig.stripe_meter_id ?? config.stripe_meter_id, + }, + }; +}; diff --git a/server/src/internal/migrations/v2/prepare/modules/ensurePricesAndEntitlements/types.ts b/server/src/internal/migrations/v2/prepare/modules/ensurePricesAndEntitlements/types.ts new file mode 100644 index 000000000..364f70467 --- /dev/null +++ b/server/src/internal/migrations/v2/prepare/modules/ensurePricesAndEntitlements/types.ts @@ -0,0 +1,44 @@ +import { EntitlementSchema, PriceSchema } from "@autumn/shared"; +import { z } from "zod/v4"; + +export const PreparedArtifactRefSchema = z.object({ + op_index: z.number(), + kind: z.enum(["base_price", "add_item"]), + item_index: z.number().optional(), + hash: z.string(), + internal_product_id: z.string(), + price_id: z.string().optional(), + entitlement_id: z.string().optional(), +}); + +export type PreparedArtifactRef = z.infer; + +/** + * Strict typed payload for this module. Stored under the module key in + * `migrations.prepared_state` and surfaced as `result` in the prepare + * response envelope. + */ +export const EnsurePricesAndEntitlementsResultSchema = z.object({ + entitlements: z.array(EntitlementSchema), + prices: z.array(PriceSchema), + artifacts: z.array(PreparedArtifactRefSchema), +}); + +export type EnsurePricesAndEntitlementsResult = z.infer< + typeof EnsurePricesAndEntitlementsResultSchema +>; + +/** + * Strict module-level envelope — same shape as the orchestrator's + * `PrepareModuleResult` but with a literal `kind` and the typed + * `result`. Used at the module's call site for type safety. + */ +export const EnsurePricesAndEntitlementsModuleResultSchema = z.object({ + key: z.string(), + kind: z.literal("ensure_prices_and_entitlements"), + result: EnsurePricesAndEntitlementsResultSchema, +}); + +export type EnsurePricesAndEntitlementsModuleResult = z.infer< + typeof EnsurePricesAndEntitlementsModuleResultSchema +>; diff --git a/server/src/internal/migrations/v2/prepare/prepare.ts b/server/src/internal/migrations/v2/prepare/prepare.ts new file mode 100644 index 000000000..3634b2bcc --- /dev/null +++ b/server/src/internal/migrations/v2/prepare/prepare.ts @@ -0,0 +1,62 @@ +import { hashJson } from "@/utils/hash/hashJson.js"; +import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; +import { migrationRepo } from "../repos/index.js"; +import type { MigrationRuntime } from "../types/migrationDefinition.js"; +import { isPersistedMigration } from "../types/migrationDefinition.js"; +import { getImplicitPrepareModules } from "./getImplicitPrepareModules.js"; +import { runPrepareModules } from "./runPrepareModules.js"; +import type { PreparedState, PrepareResponse } from "./types/index.js"; + +const scopeIdFor = ({ + ctx, + migration, +}: { + ctx: AutumnContext; + migration: MigrationRuntime; +}): string => { + if (isPersistedMigration(migration)) return `mig_${migration.internal_id}`; + + return `migdef_${hashJson({ + value: { orgId: ctx.org.id, env: ctx.env, id: migration.id }, + })}`; +}; + +/** Runs implicit prep modules and persists state only for stored migrations. */ +export const prepare = async ({ + ctx, + migration, + dryRun, +}: { + ctx: AutumnContext; + migration: MigrationRuntime; + dryRun: boolean; +}): Promise<{ response: PrepareResponse; preparedState: PreparedState }> => { + const modules = getImplicitPrepareModules({ + operations: migration.operations, + }); + + const { results, preparedState } = await runPrepareModules({ + ctx, + scopeId: scopeIdFor({ ctx, migration }), + modules, + dryRun, + }); + + if (!dryRun && isPersistedMigration(migration)) { + await migrationRepo.update({ + ctx, + id: migration.id, + updates: { prepared_state: preparedState }, + }); + } + + return { + response: { + migration_id: migration.id, + dry_run: dryRun, + modules: results, + warnings: [], + }, + preparedState, + }; +}; diff --git a/server/src/internal/migrations/v2/prepare/runPrepareModules.ts b/server/src/internal/migrations/v2/prepare/runPrepareModules.ts new file mode 100644 index 000000000..fe114987d --- /dev/null +++ b/server/src/internal/migrations/v2/prepare/runPrepareModules.ts @@ -0,0 +1,38 @@ +import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; +import type { ImplicitPrepInstance } from "./getImplicitPrepareModules.js"; +import type { PreparedState, PrepareModuleResult } from "./types/index.js"; + +/** + * Pure orchestrator. Walks a list of prep module instances under a + * given `scopeId`, runs plan → apply per module (apply skipped on + * dry-run), threads `preparedState` through. No DB reads/writes + * outside what the modules themselves do — script-callable. + */ +export const runPrepareModules = async ({ + ctx, + scopeId, + modules, + dryRun, +}: { + ctx: AutumnContext; + scopeId: string; + modules: ImplicitPrepInstance[]; + dryRun: boolean; +}): Promise<{ + results: PrepareModuleResult[]; + preparedState: PreparedState; +}> => { + const results: PrepareModuleResult[] = []; + const nextState: PreparedState = {}; + + for (const { key, module, input } of modules) { + const planned = await module.plan({ ctx, scopeId, input }); + const result = dryRun + ? planned + : await module.apply({ ctx, scopeId, input, planned }); + nextState[key] = result; + results.push({ key, kind: module.kind, result }); + } + + return { results, preparedState: nextState }; +}; diff --git a/server/src/internal/migrations/v2/prepare/types/index.ts b/server/src/internal/migrations/v2/prepare/types/index.ts new file mode 100644 index 000000000..4cfe0015b --- /dev/null +++ b/server/src/internal/migrations/v2/prepare/types/index.ts @@ -0,0 +1,4 @@ +export * from "./preparedState.js"; +export * from "./prepareModule.js"; +export * from "./prepareModuleResult.js"; +export * from "./prepareResponse.js"; diff --git a/server/src/internal/migrations/v2/prepare/types/prepareModule.ts b/server/src/internal/migrations/v2/prepare/types/prepareModule.ts new file mode 100644 index 000000000..1a676c622 --- /dev/null +++ b/server/src/internal/migrations/v2/prepare/types/prepareModule.ts @@ -0,0 +1,29 @@ +import type { AutumnContext } from "../../../../../honoUtils/HonoEnv"; + +/** + * Common shape every prepare module implements. `Input` and `Result` + * are module-specific. The orchestrator wraps the returned `Result` in + * the loose `{ key, kind, result }` envelope. + * + * `scopeId` is the namespace under which deterministic catalog rows + * are created — `mig_` for migrations, or any other prefix + * for ad-hoc script invocations. + */ +export type PrepareModule = { + kind: string; + + /** Pure planning. No writes. */ + plan: (args: { + ctx: AutumnContext; + scopeId: string; + input: Input; + }) => Promise; + + /** Persist the desired set. Idempotent (deterministic IDs). */ + apply: (args: { + ctx: AutumnContext; + scopeId: string; + input: Input; + planned: Result; + }) => Promise; +}; diff --git a/server/src/internal/migrations/v2/prepare/types/prepareModuleResult.ts b/server/src/internal/migrations/v2/prepare/types/prepareModuleResult.ts new file mode 100644 index 000000000..b0c9d2524 --- /dev/null +++ b/server/src/internal/migrations/v2/prepare/types/prepareModuleResult.ts @@ -0,0 +1,15 @@ +import { z } from "zod/v4"; + +/** + * Orchestrator-level envelope for one prepare module's result. `result` + * is intentionally loose (`z.unknown()`) — strict per-module schemas + * live in `modules//types.ts` and refine `result` to their typed + * payload. + */ +export const PrepareModuleResultSchema = z.object({ + key: z.string(), + kind: z.string(), + result: z.unknown(), +}); + +export type PrepareModuleResult = z.infer; diff --git a/server/src/internal/migrations/v2/prepare/types/prepareResponse.ts b/server/src/internal/migrations/v2/prepare/types/prepareResponse.ts new file mode 100644 index 000000000..dc0a7030d --- /dev/null +++ b/server/src/internal/migrations/v2/prepare/types/prepareResponse.ts @@ -0,0 +1,17 @@ +import { z } from "zod/v4"; +import { PrepareModuleResultSchema } from "./prepareModuleResult.js"; + +/** + * Envelope for `POST /migrations.prepare`. + * + * `modules` carries the orchestrator-level (loose) per-module results. + * Strict per-module typed schemas live in `modules//types.ts`. + */ +export const PrepareResponseSchema = z.object({ + migration_id: z.string(), + dry_run: z.boolean(), + modules: z.array(PrepareModuleResultSchema), + warnings: z.array(z.string()), +}); + +export type PrepareResponse = z.infer; diff --git a/server/src/internal/migrations/v2/prepare/types/preparedState.ts b/server/src/internal/migrations/v2/prepare/types/preparedState.ts new file mode 100644 index 000000000..04fe26aaf --- /dev/null +++ b/server/src/internal/migrations/v2/prepare/types/preparedState.ts @@ -0,0 +1,15 @@ +import { z } from "zod/v4"; + +/** + * Server-side mirror of the `migrations.prepared_state` JSONB column. + * Keyed by per-module deterministic keys (e.g. + * `ensure_prices_and_entitlements:update_plan`). + * + * Per-module output schemas live alongside each module + * (`modules//types.ts`). At the orchestrator layer we keep the + * shape loose to avoid forcing every module's output type into the + * orchestrator's import surface. + */ +export const PreparedStateSchema = z.record(z.string(), z.unknown()); + +export type PreparedState = z.infer; diff --git a/server/src/internal/migrations/v2/prepare/utils/buildPrepareModuleKey.ts b/server/src/internal/migrations/v2/prepare/utils/buildPrepareModuleKey.ts new file mode 100644 index 000000000..181582af8 --- /dev/null +++ b/server/src/internal/migrations/v2/prepare/utils/buildPrepareModuleKey.ts @@ -0,0 +1,7 @@ +export const buildPrepareModuleKey = ({ + kind, + parts, +}: { + kind: string; + parts: string[]; +}) => [kind, ...parts].join(":"); diff --git a/server/src/internal/migrations/v2/prepare/utils/index.ts b/server/src/internal/migrations/v2/prepare/utils/index.ts new file mode 100644 index 000000000..f528da839 --- /dev/null +++ b/server/src/internal/migrations/v2/prepare/utils/index.ts @@ -0,0 +1 @@ +export * from "./buildPrepareModuleKey.js"; diff --git a/server/src/internal/migrations/v2/preview/index.ts b/server/src/internal/migrations/v2/preview/index.ts new file mode 100644 index 000000000..92323e511 --- /dev/null +++ b/server/src/internal/migrations/v2/preview/index.ts @@ -0,0 +1 @@ +export * from "./previewMigrateCustomer/index.js"; diff --git a/server/src/internal/migrations/v2/preview/previewMigrateCustomer/buildBalanceChanges.ts b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/buildBalanceChanges.ts new file mode 100644 index 000000000..d7e61ad85 --- /dev/null +++ b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/buildBalanceChanges.ts @@ -0,0 +1,45 @@ +import type { ApiBalanceV1 } from "@autumn/shared"; +import type { PreviewBalanceChange } from "./types/index.js"; + +export const buildBalanceChanges = ({ + beforeBalances, + afterBalances, +}: { + beforeBalances: Record; + afterBalances: Record; +}): PreviewBalanceChange[] => { + const featureIds = new Set([ + ...Object.keys(beforeBalances), + ...Object.keys(afterBalances), + ]); + + return Array.from(featureIds).flatMap((featureId) => { + const before = beforeBalances[featureId]; + const after = afterBalances[featureId]; + const beforeSnapshot = { + granted: before?.granted ?? 0, + remaining: before?.remaining ?? 0, + usage: before?.usage ?? 0, + }; + const afterSnapshot = { + granted: after?.granted ?? 0, + remaining: after?.remaining ?? 0, + usage: after?.usage ?? 0, + }; + + if ( + beforeSnapshot.granted === afterSnapshot.granted && + beforeSnapshot.remaining === afterSnapshot.remaining && + beforeSnapshot.usage === afterSnapshot.usage + ) + return []; + + return [ + { + feature_id: featureId, + ...afterSnapshot, + before: beforeSnapshot, + }, + ]; + }); +}; diff --git a/server/src/internal/migrations/v2/preview/previewMigrateCustomer/buildFlagChanges.ts b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/buildFlagChanges.ts new file mode 100644 index 000000000..6f0cdd324 --- /dev/null +++ b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/buildFlagChanges.ts @@ -0,0 +1,23 @@ +import type { ApiFlagV0 } from "@autumn/shared"; +import type { PreviewFlagChange } from "./types/index.js"; + +export const buildFlagChanges = ({ + beforeFlags, + afterFlags, +}: { + beforeFlags: Record; + afterFlags: Record; +}): PreviewFlagChange[] => [ + ...Object.keys(afterFlags) + .filter((featureId) => !beforeFlags[featureId]) + .map((featureId) => ({ + action: "created" as const, + feature_id: featureId, + })), + ...Object.keys(beforeFlags) + .filter((featureId) => !afterFlags[featureId]) + .map((featureId) => ({ + action: "deleted" as const, + feature_id: featureId, + })), +]; diff --git a/server/src/internal/migrations/v2/preview/previewMigrateCustomer/buildPlanChanges.ts b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/buildPlanChanges.ts new file mode 100644 index 000000000..9768a36fb --- /dev/null +++ b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/buildPlanChanges.ts @@ -0,0 +1,65 @@ +import type { AutumnBillingPlan, FullCusProduct } from "@autumn/shared"; +import { + getDeleteCustomerProducts, + getPatchCustomerProducts, +} from "@/internal/billing/v2/utils/billingPlan/customerProductPlanMutations.js"; +import type { + PreviewPlanChange, + PreviewPlanItemChange, +} from "./types/index.js"; + +const customerProductToPlanChange = ({ + customerProduct, + action, + itemChanges = [], +}: { + customerProduct: FullCusProduct; + action: PreviewPlanChange["action"]; + itemChanges?: PreviewPlanItemChange[]; +}): PreviewPlanChange => ({ + action, + plan_id: customerProduct.product.id, + entity_id: customerProduct.entity_id ?? null, + item_changes: itemChanges, +}); + +const buildPatchItemChanges = ({ + patch, +}: { + patch: NonNullable[number]; +}): PreviewPlanItemChange[] => [ + ...patch.insertCustomerEntitlements.map((customerEntitlement) => ({ + action: "created" as const, + feature_id: customerEntitlement.entitlement.feature.id, + })), + ...patch.deleteCustomerEntitlements.map((customerEntitlement) => ({ + action: "deleted" as const, + feature_id: customerEntitlement.entitlement.feature.id, + })), +]; + +export const buildPlanChanges = ({ + autumnBillingPlan, +}: { + autumnBillingPlan: AutumnBillingPlan; +}): PreviewPlanChange[] => [ + ...autumnBillingPlan.insertCustomerProducts.map((customerProduct) => + customerProductToPlanChange({ + customerProduct, + action: "created", + }), + ), + ...getDeleteCustomerProducts({ autumnBillingPlan }).map((customerProduct) => + customerProductToPlanChange({ + customerProduct, + action: "deleted", + }), + ), + ...getPatchCustomerProducts({ autumnBillingPlan }).map((patch) => + customerProductToPlanChange({ + customerProduct: patch.customerProduct, + action: "updated", + itemChanges: buildPatchItemChanges({ patch }), + }), + ), +]; diff --git a/server/src/internal/migrations/v2/preview/previewMigrateCustomer/buildPreviewMigrateCustomer.ts b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/buildPreviewMigrateCustomer.ts new file mode 100644 index 000000000..8837b8eca --- /dev/null +++ b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/buildPreviewMigrateCustomer.ts @@ -0,0 +1,45 @@ +import { getApiBalances } from "@api/customers/cusFeatures"; +import type { AutumnBillingPlan, FullCustomer } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { applyAutumnBillingPlanToFullCustomer } from "@/internal/billing/v2/utils/autumnBillingPlanToFinalFullCustomer.js"; +import { buildBalanceChanges } from "./buildBalanceChanges.js"; +import { buildFlagChanges } from "./buildFlagChanges.js"; +import { buildPlanChanges } from "./buildPlanChanges.js"; +import { + type PreviewMigrateCustomer, + PreviewMigrateCustomerSchema, +} from "./types/index.js"; + +export const buildPreviewMigrateCustomer = async ({ + ctx, + originalFullCustomer, + autumnBillingPlan, +}: { + ctx: AutumnContext; + originalFullCustomer: FullCustomer; + autumnBillingPlan: AutumnBillingPlan; +}): Promise => { + const migratedFullCustomer = applyAutumnBillingPlanToFullCustomer({ + fullCustomer: originalFullCustomer, + autumnBillingPlan, + }); + + const [originalFeatures, migratedFeatures] = await Promise.all([ + getApiBalances({ ctx, fullCus: originalFullCustomer }), + getApiBalances({ ctx, fullCus: migratedFullCustomer }), + ]); + + return PreviewMigrateCustomerSchema.parse({ + object: "migration_customer_preview", + customer_id: originalFullCustomer.id ?? originalFullCustomer.internal_id, + plan_changes: buildPlanChanges({ autumnBillingPlan }), + balance_changes: buildBalanceChanges({ + beforeBalances: originalFeatures.balances, + afterBalances: migratedFeatures.balances, + }), + flag_changes: buildFlagChanges({ + beforeFlags: originalFeatures.flags, + afterFlags: migratedFeatures.flags, + }), + }); +}; diff --git a/server/src/internal/migrations/v2/preview/previewMigrateCustomer/index.ts b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/index.ts new file mode 100644 index 000000000..aa1b635c2 --- /dev/null +++ b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/index.ts @@ -0,0 +1,5 @@ +export * from "./buildBalanceChanges.js"; +export * from "./buildFlagChanges.js"; +export * from "./buildPlanChanges.js"; +export * from "./buildPreviewMigrateCustomer.js"; +export * from "./types/index.js"; diff --git a/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/index.ts b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/index.ts new file mode 100644 index 000000000..11933c54e --- /dev/null +++ b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/index.ts @@ -0,0 +1,5 @@ +export * from "./previewBalanceChange.js"; +export * from "./previewFlagChange.js"; +export * from "./previewMigrateCustomerSchema.js"; +export * from "./previewPlanChange.js"; +export * from "./previewScalarChange.js"; diff --git a/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/previewBalanceChange.ts b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/previewBalanceChange.ts new file mode 100644 index 000000000..8cf3b5929 --- /dev/null +++ b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/previewBalanceChange.ts @@ -0,0 +1,17 @@ +import { z } from "zod/v4"; + +const PreviewBalanceSnapshotSchema = z.object({ + granted: z.number(), + remaining: z.number(), + usage: z.number(), +}); + +export const PreviewBalanceChangeSchema = z.object({ + feature_id: z.string(), + granted: z.number(), + remaining: z.number(), + usage: z.number(), + before: PreviewBalanceSnapshotSchema, +}); + +export type PreviewBalanceChange = z.infer; diff --git a/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/previewFlagChange.ts b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/previewFlagChange.ts new file mode 100644 index 000000000..3ea1d3f7e --- /dev/null +++ b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/previewFlagChange.ts @@ -0,0 +1,8 @@ +import { z } from "zod/v4"; + +export const PreviewFlagChangeSchema = z.object({ + action: z.enum(["created", "deleted"]), + feature_id: z.string(), +}); + +export type PreviewFlagChange = z.infer; diff --git a/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/previewMigrateCustomerSchema.ts b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/previewMigrateCustomerSchema.ts new file mode 100644 index 000000000..9777aa52a --- /dev/null +++ b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/previewMigrateCustomerSchema.ts @@ -0,0 +1,16 @@ +import { z } from "zod/v4"; +import { PreviewBalanceChangeSchema } from "./previewBalanceChange.js"; +import { PreviewFlagChangeSchema } from "./previewFlagChange.js"; +import { PreviewPlanChangeSchema } from "./previewPlanChange.js"; + +export const PreviewMigrateCustomerSchema = z.object({ + object: z.literal("migration_customer_preview"), + customer_id: z.string(), + plan_changes: z.array(PreviewPlanChangeSchema), + balance_changes: z.array(PreviewBalanceChangeSchema), + flag_changes: z.array(PreviewFlagChangeSchema), +}); + +export type PreviewMigrateCustomer = z.infer< + typeof PreviewMigrateCustomerSchema +>; diff --git a/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/previewPlanChange.ts b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/previewPlanChange.ts new file mode 100644 index 000000000..b097ce902 --- /dev/null +++ b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/previewPlanChange.ts @@ -0,0 +1,17 @@ +import { z } from "zod/v4"; + +export const PreviewPlanItemChangeSchema = z.object({ + action: z.enum(["created", "deleted"]), + feature_id: z.string(), +}); + +export const PreviewPlanChangeSchema = z.object({ + action: z.enum(["created", "updated", "deleted"]), + plan_id: z.string(), + entity_id: z.string().nullable().optional(), + item_changes: z.array(PreviewPlanItemChangeSchema).default([]), +}); + +export type PreviewPlanItemChange = z.infer; + +export type PreviewPlanChange = z.infer; diff --git a/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/previewScalarChange.ts b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/previewScalarChange.ts new file mode 100644 index 000000000..9e10e5b4e --- /dev/null +++ b/server/src/internal/migrations/v2/preview/previewMigrateCustomer/types/previewScalarChange.ts @@ -0,0 +1,16 @@ +import { z } from "zod/v4"; + +export const PreviewScalarValueSchema = z.union([ + z.string(), + z.number(), + z.boolean(), + z.null(), +]); + +export const PreviewScalarChangeSchema = z.object({ + before: PreviewScalarValueSchema, + after: PreviewScalarValueSchema, +}); + +export type PreviewScalarValue = z.infer; +export type PreviewScalarChange = z.infer; diff --git a/server/src/internal/migrations/v2/repos/deleteMigration.ts b/server/src/internal/migrations/v2/repos/deleteMigration.ts new file mode 100644 index 000000000..cfdf3beb2 --- /dev/null +++ b/server/src/internal/migrations/v2/repos/deleteMigration.ts @@ -0,0 +1,29 @@ +import { type Migration, migrationItemRuns, migrations } from "@autumn/shared"; +import { and, eq } from "drizzle-orm"; +import type { RepoContext } from "@/db/repoContext.js"; + +/** Delete by user `id`, scoped to the current org + env. */ +export const deleteMigration = async ({ + ctx, + id, +}: { + ctx: RepoContext; + id: string; +}): Promise => { + const [row] = await ctx.db + .delete(migrations) + .where( + and( + eq(migrations.id, id), + eq(migrations.org_id, ctx.org.id), + eq(migrations.env, ctx.env), + ), + ) + .returning(); + if (row) { + await ctx.db + .delete(migrationItemRuns) + .where(eq(migrationItemRuns.migration_internal_id, row.internal_id)); + } + return row ?? null; +}; diff --git a/server/src/internal/migrations/v2/repos/findMigration.ts b/server/src/internal/migrations/v2/repos/findMigration.ts new file mode 100644 index 000000000..96e02d1d5 --- /dev/null +++ b/server/src/internal/migrations/v2/repos/findMigration.ts @@ -0,0 +1,39 @@ +import { ErrCode, type Migration, RecaseError } from "@autumn/shared"; +import { and, eq } from "drizzle-orm"; +import type { RepoContext } from "@/db/repoContext.js"; + +/** + * Single-row lookup by user `id` (or `internal_id`), scoped to the + * current org + env. Throws `MigrationNotFound` if missing — callers + * that need a nullable result use `migrationRepo.get(...)` instead. + */ +export const findMigration = async ({ + ctx, + id, + internalId, +}: { + ctx: RepoContext; + id?: string; + internalId?: string; +}): Promise => { + if (!id && !internalId) + throw new Error("findMigration: pass either `id` or `internalId`"); + + const row = await ctx.db.query.migrations.findFirst({ + where: (m) => + and( + eq(m.org_id, ctx.org.id), + eq(m.env, ctx.env), + id !== undefined ? eq(m.id, id) : eq(m.internal_id, internalId!), + ), + }); + + if (!row) + throw new RecaseError({ + message: `Migration ${id ?? internalId} not found`, + code: ErrCode.MigrationNotFound, + statusCode: 404, + }); + + return row; +}; diff --git a/server/src/internal/migrations/v2/repos/getMigration.ts b/server/src/internal/migrations/v2/repos/getMigration.ts new file mode 100644 index 000000000..7808866d2 --- /dev/null +++ b/server/src/internal/migrations/v2/repos/getMigration.ts @@ -0,0 +1,32 @@ +import { type Migration, migrations } from "@autumn/shared"; +import { and, desc, eq, type SQL } from "drizzle-orm"; +import type { RepoContext } from "@/db/repoContext.js"; + +/** + * Fetch migrations matching any combination of filter fields. Always + * returns an array; callers expecting a single row take `[0] ?? null`. + * Org + env scope is enforced via `ctx`. + */ +export const getMigration = async ({ + ctx, + id, + internalId, +}: { + ctx: RepoContext; + id?: string; + internalId?: string; +}): Promise => { + const where: SQL[] = [ + eq(migrations.org_id, ctx.org.id), + eq(migrations.env, ctx.env), + ]; + if (id !== undefined) where.push(eq(migrations.id, id)); + if (internalId !== undefined) + where.push(eq(migrations.internal_id, internalId)); + + return ctx.db + .select() + .from(migrations) + .where(and(...where)) + .orderBy(desc(migrations.created_at)); +}; diff --git a/server/src/internal/migrations/v2/repos/index.ts b/server/src/internal/migrations/v2/repos/index.ts new file mode 100644 index 000000000..1d147e874 --- /dev/null +++ b/server/src/internal/migrations/v2/repos/index.ts @@ -0,0 +1,18 @@ +import { deleteMigration } from "./deleteMigration.js"; +import { findMigration } from "./findMigration.js"; +import { getMigration } from "./getMigration.js"; +import { insertMigration } from "./insertMigration.js"; +import { updateMigration } from "./updateMigration.js"; + +export { migrationItemEventRepo } from "./migrationItemEvents/index.js"; +export type { MigrationItemRunClaimBehavior } from "./migrationItemRun/index.js"; +export { migrationItemRunRepo } from "./migrationItemRun/index.js"; +export { migrationRunRepo } from "./migrationRun/index.js"; + +export const migrationRepo = { + insert: insertMigration, + get: getMigration, + find: findMigration, + update: updateMigration, + delete: deleteMigration, +}; diff --git a/server/src/internal/migrations/v2/repos/insertMigration.ts b/server/src/internal/migrations/v2/repos/insertMigration.ts new file mode 100644 index 000000000..218070f24 --- /dev/null +++ b/server/src/internal/migrations/v2/repos/insertMigration.ts @@ -0,0 +1,36 @@ +import { + type Migration, + type MigrationInsert, + migrations, +} from "@autumn/shared"; +import type { RepoContext } from "@/db/repoContext.js"; +import { generateId } from "@/utils/genUtils.js"; + +/** + * Insert a new migration row and return the persisted entity. `filter` + * and `operations` default to null — users typically create a migration + * first, then author them via PATCH. + */ +export const insertMigration = async ({ + ctx, + insert, +}: { + ctx: RepoContext; + insert: Pick; +}): Promise => { + const row: MigrationInsert = { + internal_id: generateId("mig"), + id: insert.id, + org_id: ctx.org.id, + env: ctx.env, + filter: insert.filter ?? null, + operations: insert.operations ?? null, + retry_failed: false, + created_at: Date.now(), + updated_at: null, + }; + + await ctx.db.insert(migrations).values(row); + + return row as Migration; +}; diff --git a/server/src/internal/migrations/v2/repos/migrationItemEvents/index.ts b/server/src/internal/migrations/v2/repos/migrationItemEvents/index.ts new file mode 100644 index 000000000..206d02570 --- /dev/null +++ b/server/src/internal/migrations/v2/repos/migrationItemEvents/index.ts @@ -0,0 +1,13 @@ +import { + insertMigrationItemEvent, + insertMigrationItemEvents, +} from "./insertMigrationItemEvents.js"; +import { listLatestMigrationItemEvents } from "./listLatestMigrationItemEvents.js"; +import { listMigrationItemEvents } from "./listMigrationItemEvents.js"; + +export const migrationItemEventRepo = { + insert: insertMigrationItemEvent, + insertMany: insertMigrationItemEvents, + list: listMigrationItemEvents, + listLatest: listLatestMigrationItemEvents, +}; diff --git a/server/src/internal/migrations/v2/repos/migrationItemEvents/insertMigrationItemEvents.ts b/server/src/internal/migrations/v2/repos/migrationItemEvents/insertMigrationItemEvents.ts new file mode 100644 index 000000000..49c41e2db --- /dev/null +++ b/server/src/internal/migrations/v2/repos/migrationItemEvents/insertMigrationItemEvents.ts @@ -0,0 +1,49 @@ +import type { RepoContext } from "@/db/repoContext.js"; +import type { TinybirdMigrationItemEvent } from "@/external/tinybird/migrations/migrationItemEventsDataSource.js"; +import { migrationTinybird } from "@/external/tinybird/migrations/migrationItemEventsDataSource.js"; + +const TINYBIRD_MIGRATION_ITEM_EVENT_MAX_RETRIES = 2; + +export const insertMigrationItemEvents = async ({ + ctx, + events, +}: { + ctx: RepoContext; + events: TinybirdMigrationItemEvent[]; +}): Promise => { + if (events.length === 0) return; + + if (!migrationTinybird) { + ctx.logger.debug("Tinybird not configured, skipping migration item events"); + return; + } + + try { + const result = await migrationTinybird.itemEvents.ingestBatch(events, { + wait: false, + maxRetries: TINYBIRD_MIGRATION_ITEM_EVENT_MAX_RETRIES, + }); + ctx.logger.info(`Sent ${events.length} migration item events to Tinybird`, { + data: { + eventCount: events.length, + successfulRows: result.successful_rows, + quarantinedRows: result.quarantined_rows, + }, + }); + } catch (error) { + ctx.logger.error("Failed to send migration item events to Tinybird", { + data: { + eventCount: events.length, + error: error instanceof Error ? error.message : String(error), + }, + }); + } +}; + +export const insertMigrationItemEvent = async ({ + ctx, + event, +}: { + ctx: RepoContext; + event: TinybirdMigrationItemEvent; +}) => insertMigrationItemEvents({ ctx, events: [event] }); diff --git a/server/src/internal/migrations/v2/repos/migrationItemEvents/listLatestMigrationItemEvents.ts b/server/src/internal/migrations/v2/repos/migrationItemEvents/listLatestMigrationItemEvents.ts new file mode 100644 index 000000000..e926bbded --- /dev/null +++ b/server/src/internal/migrations/v2/repos/migrationItemEvents/listLatestMigrationItemEvents.ts @@ -0,0 +1,48 @@ +import type { RepoContext } from "@/db/repoContext.js"; +import { + migrationTinybird, + type TinybirdMigrationItemEvent, +} from "@/external/tinybird/migrations/migrationItemEventsDataSource.js"; + +export const listLatestMigrationItemEvents = async ({ + ctx, + migrationInternalId, + migrationRunId, + dryRun, + limit = 100000, +}: { + ctx: RepoContext; + migrationInternalId: string; + migrationRunId: string; + dryRun: boolean; + limit?: number; +}): Promise => { + if (!migrationTinybird) { + ctx.logger.debug( + "Tinybird not configured, skipping latest migration item event list", + ); + return []; + } + + const result = await migrationTinybird.listItemEvents.query({ + org_id: ctx.org.id, + env: ctx.env, + migration_internal_id: migrationInternalId, + migration_run_id: migrationRunId, + limit, + }); + + const latestByItem = new Map(); + for (const event of result.data as TinybirdMigrationItemEvent[]) { + if (event.dry_run !== dryRun) continue; + const key = `${event.item_kind}:${event.item_id}`; + if (!latestByItem.has(key)) latestByItem.set(key, event); + } + + return [...latestByItem.values()].sort((a, b) => { + if (a.item_kind !== b.item_kind) { + return a.item_kind.localeCompare(b.item_kind); + } + return a.item_id.localeCompare(b.item_id); + }); +}; diff --git a/server/src/internal/migrations/v2/repos/migrationItemEvents/listMigrationItemEvents.ts b/server/src/internal/migrations/v2/repos/migrationItemEvents/listMigrationItemEvents.ts new file mode 100644 index 000000000..7114a49ef --- /dev/null +++ b/server/src/internal/migrations/v2/repos/migrationItemEvents/listMigrationItemEvents.ts @@ -0,0 +1,41 @@ +import type { RepoContext } from "@/db/repoContext.js"; +import { + migrationTinybird, + type TinybirdMigrationItemEvent, +} from "@/external/tinybird/migrations/migrationItemEventsDataSource.js"; +import { findMigration } from "../findMigration.js"; + +export const listMigrationItemEvents = async ({ + ctx, + migrationId, + migrationRunId, +}: { + ctx: RepoContext; + migrationId: string; + migrationRunId?: string; +}): Promise => { + if (!migrationTinybird) { + ctx.logger.debug( + "Tinybird not configured, skipping migration item event list", + ); + return []; + } + + const migration = await findMigration({ ctx, id: migrationId }); + const queryParams = { + org_id: ctx.org.id, + env: ctx.env, + migration_internal_id: migration.internal_id, + migration_run_id: migrationRunId, + limit: 1000, + }; + ctx.logger.info( + `listMigrationItemEvents: querying org=${queryParams.org_id} env=${queryParams.env} migration=${queryParams.migration_internal_id}`, + ); + const result = await migrationTinybird.listItemEvents.query(queryParams); + ctx.logger.info( + `listMigrationItemEvents: got ${result.data.length} results`, + ); + + return result.data as TinybirdMigrationItemEvent[]; +}; diff --git a/server/src/internal/migrations/v2/repos/migrationItemRun/claimMigrationItemRun.ts b/server/src/internal/migrations/v2/repos/migrationItemRun/claimMigrationItemRun.ts new file mode 100644 index 000000000..c9cfdda2d --- /dev/null +++ b/server/src/internal/migrations/v2/repos/migrationItemRun/claimMigrationItemRun.ts @@ -0,0 +1,109 @@ +import { + type MigrationItemKind, + type MigrationItemRun, + MigrationItemRunStatus, + migrationItemRuns, +} from "@autumn/shared"; +import { eq, sql } from "drizzle-orm"; +import type { RepoContext } from "@/db/repoContext.js"; +import { generateId } from "@/utils/genUtils.js"; +import { getMigrationItemRun } from "./getMigrationItemRun.js"; + +type MigrationItemRunRepoContext = RepoContext & { + dbGeneral?: RepoContext["db"]; +}; + +export type MigrationItemRunClaimBehavior = "claim_new" | "retry_failed"; + +export type MigrationItemRunClaimResult = + | { claimed: true; itemRun: MigrationItemRun } + | { claimed: false; itemRun: MigrationItemRun | null }; + +export const claimMigrationItemRun = async ({ + ctx, + migrationInternalId, + migrationRunId, + dryRun = false, + itemKind, + itemId, + claimBehavior, +}: { + ctx: MigrationItemRunRepoContext; + migrationInternalId: string; + migrationRunId?: string; + dryRun?: boolean; + itemKind: MigrationItemKind; + itemId: string; + claimBehavior: MigrationItemRunClaimBehavior; +}): Promise => { + if (dryRun && !migrationRunId) + throw new Error( + "migrationItemRunRepo.claim: dryRun requires migrationRunId", + ); + + const db = ctx.dbGeneral ?? ctx.db; + const now = Date.now(); + const values = { + migration_item_run_id: generateId("mir"), + migration_internal_id: migrationInternalId, + migration_run_id: migrationRunId ?? null, + dry_run: dryRun, + item_kind: itemKind, + item_id: itemId, + status: MigrationItemRunStatus.Running, + created_at: now, + updated_at: null, + }; + const target = dryRun + ? [ + migrationItemRuns.migration_internal_id, + migrationItemRuns.migration_run_id, + migrationItemRuns.item_kind, + migrationItemRuns.item_id, + ] + : [ + migrationItemRuns.migration_internal_id, + migrationItemRuns.item_kind, + migrationItemRuns.item_id, + ]; + const targetWhere = dryRun + ? sql`${migrationItemRuns.dry_run} = true` + : sql`${migrationItemRuns.dry_run} = false`; + + const [claimed] = + claimBehavior === "retry_failed" + ? await db + .insert(migrationItemRuns) + .values(values) + .onConflictDoUpdate({ + target, + targetWhere, + set: { + status: MigrationItemRunStatus.Running, + updated_at: now, + }, + setWhere: eq( + migrationItemRuns.status, + MigrationItemRunStatus.Failed, + ), + }) + .returning() + : await db + .insert(migrationItemRuns) + .values(values) + .onConflictDoNothing({ target, where: targetWhere }) + .returning(); + + if (claimed) return { claimed: true, itemRun: claimed }; + + const itemRun = await getMigrationItemRun({ + ctx: { ...ctx, db }, + migrationInternalId, + migrationRunId, + dryRun, + itemKind, + itemId, + }); + + return { claimed: false, itemRun }; +}; diff --git a/server/src/internal/migrations/v2/repos/migrationItemRun/getMigrationItemRun.ts b/server/src/internal/migrations/v2/repos/migrationItemRun/getMigrationItemRun.ts new file mode 100644 index 000000000..6e537f40d --- /dev/null +++ b/server/src/internal/migrations/v2/repos/migrationItemRun/getMigrationItemRun.ts @@ -0,0 +1,59 @@ +import { MigrationItemKind, type MigrationItemRun } from "@autumn/shared"; +import { and, eq } from "drizzle-orm"; +import type { RepoContext } from "@/db/repoContext.js"; + +export const getMigrationItemRun = async ({ + ctx, + migrationInternalId, + migrationRunId, + dryRun = false, + itemKind, + itemId, +}: { + ctx: RepoContext; + migrationInternalId: string; + migrationRunId?: string; + dryRun?: boolean; + itemKind: MigrationItemKind; + itemId: string; +}): Promise => { + if (dryRun && !migrationRunId) + throw new Error("migrationItemRunRepo.get: dryRun requires migrationRunId"); + + const row = await ctx.db.query.migrationItemRuns.findFirst({ + where: (r) => + and( + eq(r.migration_internal_id, migrationInternalId), + eq(r.dry_run, dryRun), + ...(dryRun && migrationRunId + ? [eq(r.migration_run_id, migrationRunId)] + : []), + eq(r.item_kind, itemKind), + eq(r.item_id, itemId), + ), + }); + + return row ?? null; +}; + +export const getCustomerMigrationItemRun = async ({ + ctx, + migrationInternalId, + migrationRunId, + dryRun, + internalCustomerId, +}: { + ctx: RepoContext; + migrationInternalId: string; + migrationRunId?: string; + dryRun?: boolean; + internalCustomerId: string; +}): Promise => + getMigrationItemRun({ + ctx, + migrationInternalId, + migrationRunId, + dryRun, + itemKind: MigrationItemKind.Customer, + itemId: internalCustomerId, + }); diff --git a/server/src/internal/migrations/v2/repos/migrationItemRun/index.ts b/server/src/internal/migrations/v2/repos/migrationItemRun/index.ts new file mode 100644 index 000000000..acf1d30bf --- /dev/null +++ b/server/src/internal/migrations/v2/repos/migrationItemRun/index.ts @@ -0,0 +1,21 @@ +import { claimMigrationItemRun } from "./claimMigrationItemRun.js"; +import { + getCustomerMigrationItemRun, + getMigrationItemRun, +} from "./getMigrationItemRun.js"; +import { + markMigrationItemRunFailed, + markMigrationItemRunSkipped, + markMigrationItemRunSucceeded, +} from "./markMigrationItemRun.js"; + +export const migrationItemRunRepo = { + claim: claimMigrationItemRun, + get: getMigrationItemRun, + getCustomer: getCustomerMigrationItemRun, + markSucceeded: markMigrationItemRunSucceeded, + markSkipped: markMigrationItemRunSkipped, + markFailed: markMigrationItemRunFailed, +}; + +export type { MigrationItemRunClaimBehavior } from "./claimMigrationItemRun.js"; diff --git a/server/src/internal/migrations/v2/repos/migrationItemRun/markMigrationItemRun.ts b/server/src/internal/migrations/v2/repos/migrationItemRun/markMigrationItemRun.ts new file mode 100644 index 000000000..ade278504 --- /dev/null +++ b/server/src/internal/migrations/v2/repos/migrationItemRun/markMigrationItemRun.ts @@ -0,0 +1,100 @@ +import { + type MigrationItemKind, + type MigrationItemRun, + MigrationItemRunStatus, + type MigrationItemRunStatus as MigrationItemRunStatusType, + migrationItemRuns, +} from "@autumn/shared"; +import { sql } from "drizzle-orm"; +import type { RepoContext } from "@/db/repoContext.js"; +import { generateId } from "@/utils/genUtils.js"; + +type MigrationItemRunRepoContext = RepoContext & { + dbGeneral?: RepoContext["db"]; +}; + +const markMigrationItemRun = async ({ + ctx, + migrationInternalId, + migrationRunId, + dryRun = false, + itemKind, + itemId, + status, +}: { + ctx: MigrationItemRunRepoContext; + migrationInternalId: string; + migrationRunId?: string; + dryRun?: boolean; + itemKind: MigrationItemKind; + itemId: string; + status: MigrationItemRunStatusType; +}): Promise => { + if (dryRun && !migrationRunId) + throw new Error( + "migrationItemRunRepo.mark: dryRun requires migrationRunId", + ); + + const now = Date.now(); + const target = dryRun + ? [ + migrationItemRuns.migration_internal_id, + migrationItemRuns.migration_run_id, + migrationItemRuns.item_kind, + migrationItemRuns.item_id, + ] + : [ + migrationItemRuns.migration_internal_id, + migrationItemRuns.item_kind, + migrationItemRuns.item_id, + ]; + const targetWhere = dryRun + ? sql`${migrationItemRuns.dry_run} = true` + : sql`${migrationItemRuns.dry_run} = false`; + + const [row] = await (ctx.dbGeneral ?? ctx.db) + .insert(migrationItemRuns) + .values({ + migration_item_run_id: generateId("mir"), + migration_internal_id: migrationInternalId, + migration_run_id: migrationRunId ?? null, + dry_run: dryRun, + item_kind: itemKind, + item_id: itemId, + status, + created_at: now, + updated_at: now, + }) + .onConflictDoUpdate({ + target, + targetWhere, + set: { status, updated_at: now }, + }) + .returning(); + + return row ?? null; +}; + +export const markMigrationItemRunSucceeded = async ( + params: Omit[0], "status">, +): Promise => + markMigrationItemRun({ + ...params, + status: MigrationItemRunStatus.Succeeded, + }); + +export const markMigrationItemRunSkipped = async ( + params: Omit[0], "status">, +): Promise => + markMigrationItemRun({ + ...params, + status: MigrationItemRunStatus.Skipped, + }); + +export const markMigrationItemRunFailed = async ( + params: Omit[0], "status">, +): Promise => + markMigrationItemRun({ + ...params, + status: MigrationItemRunStatus.Failed, + }); diff --git a/server/src/internal/migrations/v2/repos/migrationRun/index.ts b/server/src/internal/migrations/v2/repos/migrationRun/index.ts new file mode 100644 index 000000000..27f11c892 --- /dev/null +++ b/server/src/internal/migrations/v2/repos/migrationRun/index.ts @@ -0,0 +1,9 @@ +import { insertMigrationRun } from "./insertMigrationRun.js"; +import { listMigrationRuns } from "./listMigrationRuns.js"; +import { updateMigrationRun } from "./updateMigrationRun.js"; + +export const migrationRunRepo = { + insert: insertMigrationRun, + list: listMigrationRuns, + update: updateMigrationRun, +}; diff --git a/server/src/internal/migrations/v2/repos/migrationRun/insertMigrationRun.ts b/server/src/internal/migrations/v2/repos/migrationRun/insertMigrationRun.ts new file mode 100644 index 000000000..dc3047197 --- /dev/null +++ b/server/src/internal/migrations/v2/repos/migrationRun/insertMigrationRun.ts @@ -0,0 +1,48 @@ +import { + type MigrationRun, + type MigrationRunInsert, + MigrationRunStatus, + migrationRuns, +} from "@autumn/shared"; +import { sql } from "drizzle-orm"; +import type { RepoContext } from "@/db/repoContext.js"; +import { generateId } from "@/utils/genUtils.js"; + +/** Insert a new `migration_runs` row in `queued` status. The partial unique + * index on `(org_id, env) WHERE status IN ('queued','running')` blocks + * concurrent claims for the same org/env. Returns `null` on conflict. */ +export const insertMigrationRun = async ({ + ctx, + insert, +}: { + ctx: RepoContext; + insert: Pick< + MigrationRunInsert, + "migration_internal_id" | "dry_run" | "lazy_run" + >; +}): Promise => { + const now = Date.now(); + + const [row] = await ctx.db + .insert(migrationRuns) + .values({ + internal_id: generateId("mrun"), + migration_internal_id: insert.migration_internal_id, + org_id: ctx.org.id, + env: ctx.env, + status: MigrationRunStatus.Queued, + dry_run: insert.dry_run, + lazy_run: insert.lazy_run ?? false, + created_at: now, + updated_at: null, + started_at: null, + finished_at: null, + }) + .onConflictDoNothing({ + target: [migrationRuns.migration_internal_id], + where: sql`${migrationRuns.status} IN ('queued', 'running')`, + }) + .returning(); + + return row ?? null; +}; diff --git a/server/src/internal/migrations/v2/repos/migrationRun/listMigrationRuns.ts b/server/src/internal/migrations/v2/repos/migrationRun/listMigrationRuns.ts new file mode 100644 index 000000000..ec7fc0e78 --- /dev/null +++ b/server/src/internal/migrations/v2/repos/migrationRun/listMigrationRuns.ts @@ -0,0 +1,41 @@ +import { + ACTIVE_MIGRATION_RUN_STATUSES, + type MigrationRun, + migrationRuns, +} from "@autumn/shared"; +import { and, desc, eq, inArray, type SQL } from "drizzle-orm"; +import type { RepoContext } from "@/db/repoContext.js"; + +export const listMigrationRuns = async ({ + ctx, + internalId, + migrationInternalId, + triggerRunId, + active, +}: { + ctx: RepoContext; + internalId?: string; + migrationInternalId?: string; + triggerRunId?: string; + active?: boolean; +}): Promise => { + const where: SQL[] = [ + eq(migrationRuns.org_id, ctx.org.id), + eq(migrationRuns.env, ctx.env), + ]; + + if (internalId !== undefined) + where.push(eq(migrationRuns.internal_id, internalId)); + if (migrationInternalId !== undefined) + where.push(eq(migrationRuns.migration_internal_id, migrationInternalId)); + if (triggerRunId !== undefined) + where.push(eq(migrationRuns.trigger_run_id, triggerRunId)); + if (active === true) + where.push(inArray(migrationRuns.status, ACTIVE_MIGRATION_RUN_STATUSES)); + + return ctx.db + .select() + .from(migrationRuns) + .where(and(...where)) + .orderBy(desc(migrationRuns.created_at)); +}; diff --git a/server/src/internal/migrations/v2/repos/migrationRun/updateMigrationRun.ts b/server/src/internal/migrations/v2/repos/migrationRun/updateMigrationRun.ts new file mode 100644 index 000000000..f29dac475 --- /dev/null +++ b/server/src/internal/migrations/v2/repos/migrationRun/updateMigrationRun.ts @@ -0,0 +1,40 @@ +import { + type MigrationRun, + type MigrationRunInsert, + migrationRuns, +} from "@autumn/shared"; +import { and, eq } from "drizzle-orm"; +import type { RepoContext } from "@/db/repoContext.js"; + +export const updateMigrationRun = async ({ + ctx, + internalId, + updates, +}: { + ctx: RepoContext; + internalId: string; + updates: Partial< + Pick< + MigrationRunInsert, + | "status" + | "trigger_run_id" + | "error_message" + | "started_at" + | "finished_at" + > + >; +}): Promise => { + const [row] = await ctx.db + .update(migrationRuns) + .set({ ...updates, updated_at: Date.now() }) + .where( + and( + eq(migrationRuns.internal_id, internalId), + eq(migrationRuns.org_id, ctx.org.id), + eq(migrationRuns.env, ctx.env), + ), + ) + .returning(); + + return row ?? null; +}; diff --git a/server/src/internal/migrations/v2/repos/updateMigration.ts b/server/src/internal/migrations/v2/repos/updateMigration.ts new file mode 100644 index 000000000..88c3f38e2 --- /dev/null +++ b/server/src/internal/migrations/v2/repos/updateMigration.ts @@ -0,0 +1,42 @@ +import { + type Migration, + type MigrationInsert, + migrations, +} from "@autumn/shared"; +import { and, eq } from "drizzle-orm"; +import type { RepoContext } from "@/db/repoContext.js"; + +/** + * Patch a migration. Drizzle's `set()` only updates keys present in the + * passed object, so callers pass only the fields they want changed. + * Scoped to the current org + env. Returns the persisted row, or null + * if not found. + */ +export const updateMigration = async ({ + ctx, + id, + updates, +}: { + ctx: RepoContext; + id: string; + updates: Partial< + Pick< + MigrationInsert, + "id" | "filter" | "operations" | "prepared_state" | "retry_failed" + > + >; +}): Promise => { + const [row] = await ctx.db + .update(migrations) + .set({ ...updates, updated_at: Date.now() }) + .where( + and( + eq(migrations.id, id), + eq(migrations.org_id, ctx.org.id), + eq(migrations.env, ctx.env), + ), + ) + .returning(); + + return row ?? null; +}; diff --git a/server/src/internal/migrations/v2/run/migrateCustomer/evaluateMigrateCustomerStripe.ts b/server/src/internal/migrations/v2/run/migrateCustomer/evaluateMigrateCustomerStripe.ts new file mode 100644 index 000000000..a1c790134 --- /dev/null +++ b/server/src/internal/migrations/v2/run/migrateCustomer/evaluateMigrateCustomerStripe.ts @@ -0,0 +1,130 @@ +import type { + AutumnBillingPlan, + BillingPlan, + StripeBillingPlan, + UpdateSubscriptionBillingContext, +} from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { evaluateStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan.js"; +import { + assertStripePlanNoCharges, + hasStripePlanActions, +} from "@/internal/billing/v2/providers/stripe/errors/assertStripePlanNoCharges.js"; +import { logStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingPlan.js"; +import { MigrationOperationError } from "@/internal/migrations/v2/operations/errors/index.js"; +import type { MigrateCustomerContext } from "@/internal/migrations/v2/operations/types/index.js"; +import { appendMigrationBillingLog } from "@/internal/migrations/v2/operations/utils/index.js"; + +export type MigrateCustomerStripeBillingPlan = { + subscriptionId: string; + billingContext: UpdateSubscriptionBillingContext; + stripeBillingPlan: StripeBillingPlan; +}; + +export type MigrateCustomerBillingPlan = BillingPlan & { + stripeBillingPlans: MigrateCustomerStripeBillingPlan[]; +}; + +const contextBySubscriptionId = ({ + billingContexts, +}: { + billingContexts: UpdateSubscriptionBillingContext[]; +}) => { + const result = new Map(); + for (const billingContext of billingContexts) { + const subscriptionId = billingContext.stripeSubscription?.id; + if (!subscriptionId) continue; + if (!result.has(subscriptionId)) result.set(subscriptionId, billingContext); + } + + return result; +}; + +/** + * Build the StripeBillingPlan from a computed AutumnBillingPlan and + * enforce the no-charges guard: + * - throw on `invoiceAction` / `invoiceItemsAction` / `refundAction` + * - reject `subscriptionAction` params that would create proration + * invoice items + * + * Evaluates at most once per Stripe subscription because one migration can + * patch multiple customer products on the same customer. + */ +export const evaluateMigrateCustomerStripe = async ({ + ctx, + context, + billingContexts, + autumnBillingPlan, +}: { + ctx: AutumnContext; + context: MigrateCustomerContext; + billingContexts: UpdateSubscriptionBillingContext[]; + autumnBillingPlan: AutumnBillingPlan; +}): Promise => { + const stripeBillingPlans: MigrateCustomerStripeBillingPlan[] = []; + + for (const [subscriptionId, billingContext] of contextBySubscriptionId({ + billingContexts, + })) { + const shouldValidateForcedNoBillingChanges = + context.migration.no_billing_changes === true; + const evaluationContext = shouldValidateForcedNoBillingChanges + ? { ...billingContext, skipBillingChanges: false } + : billingContext; + + const stripeBillingPlan = await evaluateStripeBillingPlan({ + ctx, + billingContext: evaluationContext, + autumnBillingPlan, + }); + appendMigrationBillingLog({ + ctx, + key: "stripeBillingPlan", + log: (logCtx) => + logStripeBillingPlan({ + ctx: logCtx, + stripeBillingPlan, + billingContext: evaluationContext, + }), + }); + + assertStripePlanNoCharges({ + stripeBillingPlan, + subscriptionId, + createError: (violation) => + new MigrationOperationError({ + code: "unsupported_operation_input", + operationType: "update_plan", + field: "customize", + message: `Migration update_plan ${violation.message}`, + details: violation.details, + }), + }); + + if ( + shouldValidateForcedNoBillingChanges && + hasStripePlanActions(stripeBillingPlan) + ) { + throw new MigrationOperationError({ + code: "unsupported_operation_input", + operationType: "update_plan", + field: "no_billing_changes", + message: + "Migration no_billing_changes=true was set, but update_plan produced Stripe mutations", + details: { subscriptionId }, + }); + } + + stripeBillingPlans.push({ + subscriptionId, + billingContext, + stripeBillingPlan, + }); + } + + return { + autumn: autumnBillingPlan, + stripe: stripeBillingPlans[0]?.stripeBillingPlan ?? {}, + stripeBillingPlans, + }; +}; diff --git a/server/src/internal/migrations/v2/run/migrateCustomer/executeMigrateCustomerPlan.ts b/server/src/internal/migrations/v2/run/migrateCustomer/executeMigrateCustomerPlan.ts new file mode 100644 index 000000000..ba111469d --- /dev/null +++ b/server/src/internal/migrations/v2/run/migrateCustomer/executeMigrateCustomerPlan.ts @@ -0,0 +1,48 @@ +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { executeAutumnBillingPlan } from "@/internal/billing/v2/execute/executeAutumnBillingPlan.js"; +import { executeStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/execute/executeStripeBillingPlan.js"; +import { logStripeBillingResult } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingResult.js"; +import { deleteCachedFullCustomer } from "@/internal/customers/cusUtils/fullCustomerCacheUtils/deleteCachedFullCustomer.js"; +import type { MigrateCustomerContext } from "@/internal/migrations/v2/operations/types/index.js"; +import { appendMigrationBillingLog } from "@/internal/migrations/v2/operations/utils/index.js"; +import type { MigrateCustomerBillingPlan } from "./evaluateMigrateCustomerStripe.js"; + +export const executeMigrateCustomerPlan = async ({ + ctx, + context, + billingPlan, +}: { + ctx: AutumnContext; + context: MigrateCustomerContext; + billingPlan: MigrateCustomerBillingPlan; +}): Promise => { + for (const stripeBillingPlan of billingPlan.stripeBillingPlans) { + const stripeResult = await executeStripeBillingPlan({ + ctx, + billingContext: stripeBillingPlan.billingContext, + billingPlan: { + autumn: billingPlan.autumn, + stripe: stripeBillingPlan.stripeBillingPlan, + }, + }); + appendMigrationBillingLog({ + ctx, + key: "stripeBillingResult", + log: (logCtx) => + logStripeBillingResult({ ctx: logCtx, result: stripeResult }), + }); + } + + await executeAutumnBillingPlan({ + ctx, + autumnBillingPlan: billingPlan.autumn, + }); + + const customerId = + context.fullCustomer.id ?? context.fullCustomer.internal_id; + await deleteCachedFullCustomer({ + ctx, + customerId, + source: "executeMigrateCustomerPlan", + }); +}; diff --git a/server/src/internal/migrations/v2/run/migrateCustomer/index.ts b/server/src/internal/migrations/v2/run/migrateCustomer/index.ts new file mode 100644 index 000000000..22b699fa3 --- /dev/null +++ b/server/src/internal/migrations/v2/run/migrateCustomer/index.ts @@ -0,0 +1,5 @@ +export * from "./evaluateMigrateCustomerStripe.js"; +export * from "./executeMigrateCustomerPlan.js"; +export * from "./migrateCustomer.js"; +export * from "./processOperations.js"; +export * from "./setup/index.js"; diff --git a/server/src/internal/migrations/v2/run/migrateCustomer/logs/createMigrateCustomerRunContext.ts b/server/src/internal/migrations/v2/run/migrateCustomer/logs/createMigrateCustomerRunContext.ts new file mode 100644 index 000000000..50c286705 --- /dev/null +++ b/server/src/internal/migrations/v2/run/migrateCustomer/logs/createMigrateCustomerRunContext.ts @@ -0,0 +1,66 @@ +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { + addAppContextToLogs, + addExtrasToLogs, +} from "@/utils/logging/addContextToLogs.js"; +import { addToExtraLogs } from "@/utils/logging/addToExtraLogs.js"; +import { + isPersistedMigration, + type MigrationRuntime, +} from "../../../types/migrationDefinition.js"; + +export const createMigrateCustomerRunContext = ({ + ctx, + customerId, + migration, + preview, +}: { + ctx: AutumnContext; + customerId: string; + migration: MigrationRuntime; + preview: boolean; +}): AutumnContext => { + const migrationCtx: AutumnContext = { + ...ctx, + customerId, + entityId: undefined, + timestamp: Date.now(), + extraLogs: {}, + // Migration setup should read source-of-truth customer state from DB. + // Execute still invalidates cache after writes. + skipCache: true, + }; + + migrationCtx.logger = addAppContextToLogs({ + logger: ctx.logger, + appContext: { + org_id: ctx.org?.id, + org_slug: ctx.org?.slug, + env: ctx.env, + auth_type: ctx.authType, + customer_id: customerId, + user_id: ctx.userId || undefined, + user_email: ctx.user?.email || undefined, + api_version: ctx.apiVersion?.semver, + scopes: ctx.scopes, + }, + }); + migrationCtx.logger = addExtrasToLogs({ + logger: migrationCtx.logger, + extras: {}, + }); + addToExtraLogs({ + ctx: migrationCtx, + extras: { + migrationCustomer: { + migrationId: migration.id, + migrationInternalId: isPersistedMigration(migration) + ? migration.internal_id + : undefined, + preview, + }, + }, + }); + + return migrationCtx; +}; diff --git a/server/src/internal/migrations/v2/run/migrateCustomer/logs/index.ts b/server/src/internal/migrations/v2/run/migrateCustomer/logs/index.ts new file mode 100644 index 000000000..292597d30 --- /dev/null +++ b/server/src/internal/migrations/v2/run/migrateCustomer/logs/index.ts @@ -0,0 +1,2 @@ +export * from "./createMigrateCustomerRunContext.js"; +export * from "./logMigrateCustomerResult.js"; diff --git a/server/src/internal/migrations/v2/run/migrateCustomer/logs/logMigrateCustomerResult.ts b/server/src/internal/migrations/v2/run/migrateCustomer/logs/logMigrateCustomerResult.ts new file mode 100644 index 000000000..f5cd329b1 --- /dev/null +++ b/server/src/internal/migrations/v2/run/migrateCustomer/logs/logMigrateCustomerResult.ts @@ -0,0 +1,70 @@ +import chalk from "chalk"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { addExtrasToLogs } from "@/utils/logging/addContextToLogs.js"; +import { addToExtraLogs } from "@/utils/logging/addToExtraLogs.js"; +import { maskExtraLogs } from "@/utils/logging/maskExtraLogs.js"; + +type MigrateCustomerLogResult = { + status: "success" | "error"; + error?: unknown; +}; + +const getMigrationCustomerExtras = ({ + ctx, +}: { + ctx: AutumnContext; +}): Record => { + const extras = ctx.extraLogs.migrationCustomer; + return extras && typeof extras === "object" && !Array.isArray(extras) + ? (extras as Record) + : {}; +}; + +export const logMigrateCustomerResult = ({ + ctx, + result, +}: { + ctx: AutumnContext; + result: MigrateCustomerLogResult; +}) => { + const durationMs = Date.now() - ctx.timestamp; + const statusCode = result.status === "success" ? 200 : 500; + + addToExtraLogs({ + ctx, + extras: { + migrationCustomer: { + ...getMigrationCustomerExtras({ ctx }), + status: result.status, + }, + }, + }); + + ctx.logger = addExtrasToLogs({ + logger: ctx.logger, + extras: ctx.extraLogs, + }); + + const log = result.status === "success" ? ctx.logger.info : ctx.logger.warn; + const statusColor = result.status === "success" ? chalk.green : chalk.yellow; + + log( + `[${statusColor(statusCode)}] migrate customer ${ctx.customerId} (${ctx.org?.slug}) ${durationMs}ms`, + { + statusCode, + durationMs, + error: + result.error instanceof Error + ? { name: result.error.name, message: result.error.message } + : undefined, + }, + ); + + if ( + Object.keys(ctx.extraLogs).length > 0 && + process.env.NODE_ENV === "development" + ) { + const maskedLogs = maskExtraLogs(ctx.extraLogs); + ctx.logger.debug(`EXTRA LOGS: ${JSON.stringify(maskedLogs, null, 2)}`); + } +}; diff --git a/server/src/internal/migrations/v2/run/migrateCustomer/migrateCustomer.ts b/server/src/internal/migrations/v2/run/migrateCustomer/migrateCustomer.ts new file mode 100644 index 000000000..76e43d3bb --- /dev/null +++ b/server/src/internal/migrations/v2/run/migrateCustomer/migrateCustomer.ts @@ -0,0 +1,118 @@ +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { buildPreviewMigrateCustomer } from "@/internal/migrations/v2/preview/index.js"; +import type { MigrationHooks } from "../../hooks/index.js"; +import type { MigrationRuntime } from "../../types/migrationDefinition.js"; +import { evaluateMigrateCustomerStripe } from "./evaluateMigrateCustomerStripe.js"; +import { executeMigrateCustomerPlan } from "./executeMigrateCustomerPlan.js"; +import { + createMigrateCustomerRunContext, + logMigrateCustomerResult, +} from "./logs/index.js"; +import { processOperations } from "./processOperations.js"; +import { setupMigrateCustomerContext } from "./setup/setupMigrateCustomerContext.js"; + +export type MigrateCustomerItemPreview = { + id: string | null; + name: string | null; + email: string | null; +}; + +export type MigrateCustomerResult = { + itemPreview: MigrateCustomerItemPreview | null; + status: "succeeded" | "skipped"; + response: Record | null; +}; + +/** Top-level per-customer migration runner. Preview evaluates without writes. */ +export const migrateCustomer = async ({ + ctx, + customerId, + migration, + preview = false, + hooks, +}: { + ctx: AutumnContext; + customerId: string; + migration: MigrationRuntime; + preview?: boolean; + hooks?: MigrationHooks; +}): Promise => { + const migrationCtx = createMigrateCustomerRunContext({ + ctx, + customerId, + migration, + preview, + }); + + const context = await setupMigrateCustomerContext({ + ctx: migrationCtx, + migration, + customerId, + }); + + const baseArgs = { + ctx: migrationCtx, + customerId, + context, + preview, + }; + + const run = async (): Promise => { + const { + plan: autumnPlan, + billingContexts, + matchedCustomerProducts, + } = await processOperations({ + ctx: migrationCtx, + context, + plan: { + customerId: context.fullCustomer.id ?? context.fullCustomer.internal_id, + insertCustomerProducts: [], + }, + }); + + const billingPlan = await evaluateMigrateCustomerStripe({ + ctx: migrationCtx, + context, + billingContexts, + autumnBillingPlan: autumnPlan, + }); + + if (!preview) { + await executeMigrateCustomerPlan({ + ctx: migrationCtx, + context, + billingPlan, + }); + } + + const response = { + preview: await buildPreviewMigrateCustomer({ + ctx: migrationCtx, + originalFullCustomer: context.fullCustomer, + autumnBillingPlan: billingPlan.autumn, + }), + }; + + logMigrateCustomerResult({ + ctx: migrationCtx, + result: { + status: "success", + }, + }); + + return { + itemPreview: { + id: context.fullCustomer.id ?? null, + name: context.fullCustomer.name ?? null, + email: context.fullCustomer.email ?? null, + }, + status: matchedCustomerProducts === 0 ? "skipped" : "succeeded", + response, + }; + }; + + return hooks?.aroundMigrateCustomer + ? hooks.aroundMigrateCustomer({ ...baseArgs, run }) + : run(); +}; diff --git a/server/src/internal/migrations/v2/run/migrateCustomer/processOperations.ts b/server/src/internal/migrations/v2/run/migrateCustomer/processOperations.ts new file mode 100644 index 000000000..bcea9c1f0 --- /dev/null +++ b/server/src/internal/migrations/v2/run/migrateCustomer/processOperations.ts @@ -0,0 +1,65 @@ +import type { AutumnBillingPlan } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { applyAutumnBillingPlanToFullCustomer } from "@/internal/billing/v2/utils/autumnBillingPlanToFinalFullCustomer.js"; +import { + executionPriority, + getProcessor, +} from "@/internal/migrations/v2/operations/operationRegistry.js"; +import type { + MigrateCustomerContext, + ProcessOperationResult, +} from "@/internal/migrations/v2/operations/types/index.js"; + +/** + * Fold ordered customer operations onto one AutumnBillingPlan. + * + * Operations are sorted by execution order (add_plan → update_plan), + * preserving original array order within the same type. Each op sees + * the projected customer state from all previous ops. + */ +export const processOperations = async ({ + ctx, + context, + plan, +}: { + ctx: AutumnContext; + context: MigrateCustomerContext; + plan: AutumnBillingPlan; +}): Promise => { + let state: ProcessOperationResult = { + plan, + projectedFullCustomer: context.fullCustomer, + matchedCustomerProducts: 0, + billingContexts: [], + }; + + const operations = context.migration.operations?.customer ?? []; + const sorted = operations + .map((op, originalIndex) => ({ op, originalIndex })) + .sort((a, b) => executionPriority(a.op) - executionPriority(b.op)); + + for (const { op, originalIndex } of sorted) { + const processor = getProcessor(op); + const result = await processor({ + ctx, + context, + op, + opIndex: originalIndex, + plan: state.plan, + projectedFullCustomer: state.projectedFullCustomer, + }); + + state = { + plan: result.plan, + projectedFullCustomer: applyAutumnBillingPlanToFullCustomer({ + fullCustomer: context.fullCustomer, + autumnBillingPlan: result.plan, + }), + matchedCustomerProducts: + state.matchedCustomerProducts + result.matchedCustomerProducts, + billingContexts: [...state.billingContexts, ...result.billingContexts], + }; + } + + return state; +}; diff --git a/server/src/internal/migrations/v2/run/migrateCustomer/setup/index.ts b/server/src/internal/migrations/v2/run/migrateCustomer/setup/index.ts new file mode 100644 index 000000000..37e01cfea --- /dev/null +++ b/server/src/internal/migrations/v2/run/migrateCustomer/setup/index.ts @@ -0,0 +1,2 @@ +export * from "./setupMigrateCustomerContext.js"; +export * from "./setupMigrationOperationBillingContext.js"; diff --git a/server/src/internal/migrations/v2/run/migrateCustomer/setup/setupMigrateCustomerContext.ts b/server/src/internal/migrations/v2/run/migrateCustomer/setup/setupMigrateCustomerContext.ts new file mode 100644 index 000000000..34b9e36bd --- /dev/null +++ b/server/src/internal/migrations/v2/run/migrateCustomer/setup/setupMigrateCustomerContext.ts @@ -0,0 +1,31 @@ +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { setupFullCustomerContext } from "@/internal/billing/v2/setup/setupFullCustomerContext.js"; +import type { MigrateCustomerContext } from "@/internal/migrations/v2/operations/types/index.js"; +import { createMigrationStripeCache } from "@/internal/migrations/v2/stripeCache/index.js"; +import type { MigrationRuntime } from "../../../types/migrationDefinition.js"; + +/** Loads customer state and creates the lazy Stripe cache for migration ops. */ +export const setupMigrateCustomerContext = async ({ + ctx, + migration, + customerId, +}: { + ctx: AutumnContext; + migration: MigrationRuntime; + customerId: string; +}): Promise => { + const fullCustomer = await setupFullCustomerContext({ + ctx, + params: { customer_id: customerId }, + }); + + return { + migration, + fullCustomer, + stripeCache: createMigrationStripeCache({ + ctx, + fullCustomer, + allowStripeCustomerCreation: migration.no_billing_changes !== true, + }), + }; +}; diff --git a/server/src/internal/migrations/v2/run/migrateCustomer/setup/setupMigrationOperationBillingContext.ts b/server/src/internal/migrations/v2/run/migrateCustomer/setup/setupMigrationOperationBillingContext.ts new file mode 100644 index 000000000..80755c8d6 --- /dev/null +++ b/server/src/internal/migrations/v2/run/migrateCustomer/setup/setupMigrationOperationBillingContext.ts @@ -0,0 +1,101 @@ +import { + cusProductToProduct, + type FeatureOptions, + type FullCusProduct, + type FullCustomer, + type FullProduct, + type StripeDiscountWithCoupon, + type TrialContext, +} from "@autumn/shared"; +import type Stripe from "stripe"; +import type { StripeSubscriptionWithDiscounts } from "@/external/stripe/subscriptions/index.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { setupBillingCycleAnchor } from "@/internal/billing/v2/setup/setupBillingCycleAnchor.js"; +import { setupResetCycleAnchor } from "@/internal/billing/v2/setup/setupResetCycleAnchor.js"; +import type { MigrateCustomerContext } from "@/internal/migrations/v2/operations/types/index.js"; + +export type MigrationOperationBillingContext = { + fullCustomer: FullCustomer; + customerProduct: FullCusProduct; + featureQuantities: FeatureOptions[]; + currentEpochMs: number; + billingCycleAnchorMs: number | "now"; + resetCycleAnchorMs: number | "now"; + trialContext?: TrialContext; + stripeCustomer?: Stripe.Customer; + stripeSubscription?: StripeSubscriptionWithDiscounts; + stripeSubscriptionSchedule?: Stripe.SubscriptionSchedule; + stripeDiscounts: StripeDiscountWithCoupon[]; + paymentMethod?: Stripe.PaymentMethod; + skipExistingUsageCarry?: boolean; +}; + +export const setupMigrationOperationBillingContext = async ({ + ctx, + context, + fullCustomer, + customerProduct, + fullProduct, +}: { + ctx: AutumnContext; + context: MigrateCustomerContext; + fullCustomer?: FullCustomer; + customerProduct: FullCusProduct; + fullProduct?: FullProduct; +}): Promise => { + const subscriptionId = customerProduct.subscription_ids?.[0]; + const stripeSubscription = subscriptionId + ? await context.stripeCache.getStripeSubscription({ customerProduct }) + : undefined; + const stripeCustomerContext = subscriptionId + ? await context.stripeCache.getStripeCustomer() + : { + stripeCustomer: undefined, + paymentMethod: undefined, + testClockFrozenTime: undefined, + }; + const [stripeSubscriptionSchedule, stripeDiscounts] = subscriptionId + ? await Promise.all([ + context.stripeCache.getStripeSubscriptionSchedule({ + customerProduct, + stripeSubscription, + }), + context.stripeCache.getStripeDiscounts({ + customerProduct, + stripeSubscription, + }), + ]) + : [undefined, []]; + + const currentEpochMs = + stripeCustomerContext.testClockFrozenTime ?? Date.now(); + const resolvedFullProduct = + fullProduct ?? cusProductToProduct({ cusProduct: customerProduct }); + const billingCycleAnchorMs = setupBillingCycleAnchor({ + stripeSubscription, + customerProduct, + newFullProduct: resolvedFullProduct, + currentEpochMs, + }); + const resetCycleAnchorMs = setupResetCycleAnchor({ + billingCycleAnchorMs, + customerProduct, + newFullProduct: resolvedFullProduct, + }); + + void ctx; + + return { + fullCustomer: fullCustomer ?? context.fullCustomer, + customerProduct, + featureQuantities: [], + currentEpochMs, + billingCycleAnchorMs, + resetCycleAnchorMs, + stripeCustomer: stripeCustomerContext.stripeCustomer, + stripeSubscription, + stripeSubscriptionSchedule, + stripeDiscounts, + paymentMethod: stripeCustomerContext.paymentMethod, + }; +}; diff --git a/server/src/internal/migrations/v2/run/orchestrators/iterateScope.ts b/server/src/internal/migrations/v2/run/orchestrators/iterateScope.ts new file mode 100644 index 000000000..cd8a9e09d --- /dev/null +++ b/server/src/internal/migrations/v2/run/orchestrators/iterateScope.ts @@ -0,0 +1,52 @@ +import type { RunScopeItem } from "../types/runScope.js"; + +export type IterateScopeItemResult = + | { status: "ok"; item: RunScopeItem; value: T } + | { status: "failed"; item: RunScopeItem; error: Error }; + +export type IterateScopeSummary = { + processed: number; + succeeded: number; + failed: number; + results: IterateScopeItemResult[]; +}; + +/** + * Generic iteration over any kind-tagged scope iterator. Calls `perItem` + * for every item, collecting per-item results into a summary. Sequential + * by default; concurrency layer is intentionally out of scope here. + * + * On error: keeps going with `onError: "continue"` (default), or rethrows + * the first error with `onError: "throw"`. Either way every visited + * item shows up in `results` so callers see the full audit trail. + */ +export const iterateScope = async ({ + iterate, + perItem, + onError = "continue", +}: { + iterate: () => AsyncGenerator; + perItem: (item: RunScopeItem) => Promise; + onError?: "throw" | "continue"; +}): Promise> => { + const results: IterateScopeItemResult[] = []; + let succeeded = 0; + let failed = 0; + + for await (const batch of iterate()) { + for (const item of batch) { + try { + const value = await perItem(item); + results.push({ status: "ok", item, value }); + succeeded++; + } catch (raw) { + const error = raw instanceof Error ? raw : new Error(String(raw)); + results.push({ status: "failed", item, error }); + failed++; + if (onError === "throw") throw error; + } + } + } + + return { processed: succeeded + failed, succeeded, failed, results }; +}; diff --git a/server/src/internal/migrations/v2/run/orchestrators/runScopeIteration.ts b/server/src/internal/migrations/v2/run/orchestrators/runScopeIteration.ts new file mode 100644 index 000000000..36dba6461 --- /dev/null +++ b/server/src/internal/migrations/v2/run/orchestrators/runScopeIteration.ts @@ -0,0 +1,102 @@ +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { withMigrationItemTracking } from "../../actions/migrationItem/index.js"; +import { runCloudScopeIteration } from "../../cloudAdapter/runCloudScopeIteration.js"; +import type { + MigrationBatchFn, + MigrationRunControls, +} from "../../cloudAdapter/types.js"; +import { runFilter } from "../../filters/runFilter.js"; +import type { MigrationHooks } from "../../hooks/index.js"; +import { + isPersistedMigration, + type MigrationRuntimeWithEventId, +} from "../../types/migrationDefinition.js"; +import { migrateCustomer } from "../migrateCustomer/index.js"; +import type { RunScopeItem, RunScopeKind } from "../types/runScope.js"; +import { iterateScope } from "./iterateScope.js"; + +/** Runs one filtered migration scope iteration. */ +export const runScopeIteration = async ({ + ctx, + migration, + migrationRunId, + dryRun, + kind, + batch, + controls, + hooks, +}: { + ctx: AutumnContext; + migration: MigrationRuntimeWithEventId; + migrationRunId: string; + dryRun: boolean; + kind: RunScopeKind; + batch?: MigrationBatchFn; + controls?: MigrationRunControls; + hooks?: MigrationHooks; +}) => { + const { count, iterate } = await runFilter({ + ctx, + migration, + migrationRunId, + dryRun, + kind, + controls, + }); + ctx.logger.info(`run-migration: iterating scope`, { + data: { kind, count, dryRun }, + }); + const checkpointReadEnabled = + controls?.checkpoint !== false && + (!dryRun || controls?.checkpointDryRun === true); + + const perItem = async ({ + item, + itemCtx, + }: { + item: RunScopeItem; + itemCtx: AutumnContext; + }) => { + if (item.kind !== "customer") + throw new Error( + `runMigration: per-item handler missing for kind "${item.kind}"`, + ); + + const run = () => + migrateCustomer({ + ctx: itemCtx, + customerId: item.id ?? item.internal_id, + migration, + preview: dryRun, + hooks, + }); + + return withMigrationItemTracking({ + ctx: itemCtx, + migrationInternalId: isPersistedMigration(migration) + ? migration.internal_id + : migration.event_internal_id, + migrationRunId, + item, + dryRun, + claimItemRun: checkpointReadEnabled, + retryFailed: migration.retry_failed === true, + run, + }); + }; + + if (!batch) { + return iterateScope({ + iterate, + perItem: (item) => perItem({ item, itemCtx: ctx }), + }); + } + + return runCloudScopeIteration({ + batch, + iterate, + kind, + controls, + perItem, + }); +}; diff --git a/server/src/internal/migrations/v2/run/runMigration.ts b/server/src/internal/migrations/v2/run/runMigration.ts new file mode 100644 index 000000000..0858c3cbd --- /dev/null +++ b/server/src/internal/migrations/v2/run/runMigration.ts @@ -0,0 +1,72 @@ +import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; +import { generateId } from "../../../../utils/genUtils.js"; +import type { + MigrationBatchFn, + MigrationRunControls, +} from "../cloudAdapter/types.js"; +import { + composeMigrationHooks, + type MigrationHooks as RunMigrationHooks, + type MigrationPlugin as RunMigrationPlugin, +} from "../hooks/index.js"; +import { prepare } from "../prepare/index.js"; +import { + type MigrationRuntime, + withMigrationEventId, +} from "../types/migrationDefinition.js"; +import { runScopeIteration } from "./orchestrators/runScopeIteration.js"; +import { getRunScopes } from "./types/getRunScopes.js"; + +/** Top-level migration run: prepare -> per-scope filter+iterate -> per-item ops. */ +export const runMigration = async ({ + ctx, + migration, + migrationRunId, + dryRun, + batch, + controls, + hooks, + plugins, +}: { + ctx: AutumnContext; + migration: MigrationRuntime; + migrationRunId?: string; + dryRun: boolean; + batch?: MigrationBatchFn; + controls?: MigrationRunControls; + hooks?: RunMigrationHooks; + plugins?: RunMigrationPlugin[]; +}): Promise => { + const eventMigrationRunId = migrationRunId ?? generateId("mrun"); + const migrationHooks = composeMigrationHooks({ hooks, plugins }); + const migrationWithEventId = withMigrationEventId({ + orgId: ctx.org.id, + env: ctx.env, + migration, + }); + + const { preparedState } = await prepare({ + ctx, + migration: migrationWithEventId, + dryRun, + }); + const preparedMigration = { + ...migrationWithEventId, + prepared_state: preparedState, + }; + + for (const kind of getRunScopes({ migration: preparedMigration })) { + await runScopeIteration({ + ctx, + migration: preparedMigration, + migrationRunId: eventMigrationRunId, + dryRun, + kind, + batch, + controls, + hooks: migrationHooks, + }); + } +}; + +export type { RunMigrationHooks, RunMigrationPlugin }; diff --git a/server/src/internal/migrations/v2/run/types/getRunScopes.ts b/server/src/internal/migrations/v2/run/types/getRunScopes.ts new file mode 100644 index 000000000..c27e26473 --- /dev/null +++ b/server/src/internal/migrations/v2/run/types/getRunScopes.ts @@ -0,0 +1,14 @@ +import type { MigrationRuntime } from "../../types/migrationDefinition.js"; +import type { RunScopeKind } from "./runScope.js"; + +/** Returns active run scopes from top-level migration operations. */ +export const getRunScopes = ({ + migration, +}: { + migration: MigrationRuntime; +}): RunScopeKind[] => { + const scopes: RunScopeKind[] = []; + if (migration.operations?.customer) scopes.push("customer"); + // future: if (migration.operations?.plan) scopes.push("plan"); + return scopes; +}; diff --git a/server/src/internal/migrations/v2/run/types/runScope.ts b/server/src/internal/migrations/v2/run/types/runScope.ts new file mode 100644 index 000000000..0a7cb90fb --- /dev/null +++ b/server/src/internal/migrations/v2/run/types/runScope.ts @@ -0,0 +1,9 @@ +/** Resource kind being iterated. Phase 2+ adds catalog-rooted kinds. */ +export type RunScopeKind = "customer" | "plan"; + +/** One iterated item, kind-tagged for generic dispatch. */ +export type RunScopeItem = { + kind: RunScopeKind; + internal_id: string; + id: string | null; +}; diff --git a/server/src/internal/migrations/v2/stripeCache/createMigrationStripeCache.ts b/server/src/internal/migrations/v2/stripeCache/createMigrationStripeCache.ts new file mode 100644 index 000000000..5a8b3b0ce --- /dev/null +++ b/server/src/internal/migrations/v2/stripeCache/createMigrationStripeCache.ts @@ -0,0 +1,181 @@ +import type { + FullCusProduct, + FullCustomer, + StripeDiscountWithCoupon, +} from "@autumn/shared"; +import type Stripe from "stripe"; +import type { + StripeCustomerWithDiscount, + StripeSubscriptionWithDiscounts, +} from "@/external/stripe/subscriptions/index.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { fetchStripeCustomerForBilling } from "@/internal/billing/v2/providers/stripe/setup/fetchStripeCustomerForBilling.js"; +import { fetchStripeDiscountsForBilling } from "@/internal/billing/v2/providers/stripe/setup/fetchStripeDiscountsForBilling.js"; +import { fetchStripeSubscriptionForBilling } from "@/internal/billing/v2/providers/stripe/setup/fetchStripeSubscriptionForBilling.js"; +import { fetchStripeSubscriptionScheduleForBilling } from "@/internal/billing/v2/providers/stripe/setup/fetchStripeSubscriptionScheduleForBilling.js"; + +type StripeCustomerContext = { + stripeCustomer?: StripeCustomerWithDiscount; + paymentMethod?: Stripe.PaymentMethod; + testClockFrozenTime?: number; +}; + +export type MigrationStripeCache = { + getStripeCustomer: () => Promise; + getStripeSubscription: (args: { + customerProduct: FullCusProduct; + }) => Promise; + getStripeSubscriptionSchedule: (args: { + customerProduct: FullCusProduct; + stripeSubscription?: Stripe.Subscription; + }) => Promise; + getStripeDiscounts: (args: { + customerProduct: FullCusProduct; + stripeSubscription?: StripeSubscriptionWithDiscounts; + }) => Promise; +}; + +const getSubscriptionId = ({ + customerProduct, +}: { + customerProduct: FullCusProduct; +}) => customerProduct.subscription_ids?.[0] ?? null; + +const getScheduleCacheKey = ({ + customerProduct, + stripeSubscription, +}: { + customerProduct: FullCusProduct; + stripeSubscription?: Stripe.Subscription; +}) => { + const scheduleId = + typeof stripeSubscription?.schedule === "string" + ? stripeSubscription.schedule + : customerProduct.scheduled_ids?.[0]; + + return scheduleId ?? getSubscriptionId({ customerProduct }); +}; + +export const createMigrationStripeCache = ({ + ctx, + fullCustomer, + allowStripeCustomerCreation = true, +}: { + ctx: AutumnContext; + fullCustomer: FullCustomer; + allowStripeCustomerCreation?: boolean; +}): MigrationStripeCache => { + let stripeCustomerPromise: Promise | undefined; + const subscriptionPromises = new Map< + string, + Promise + >(); + const schedulePromises = new Map< + string, + Promise + >(); + const discountPromises = new Map< + string, + Promise + >(); + + const getStripeCustomer = async () => { + if (!stripeCustomerPromise) { + stripeCustomerPromise = fetchStripeCustomerForBilling({ + ctx, + fullCus: fullCustomer, + createIfMissing: allowStripeCustomerCreation, + }).then(({ stripeCus, paymentMethod, testClockFrozenTime }) => ({ + stripeCustomer: stripeCus as StripeCustomerWithDiscount | undefined, + paymentMethod, + testClockFrozenTime, + })); + } + + return stripeCustomerPromise; + }; + + const getStripeSubscription = async ({ + customerProduct, + }: { + customerProduct: FullCusProduct; + }) => { + const subscriptionId = getSubscriptionId({ customerProduct }); + if (!subscriptionId) return undefined; + + const existingPromise = subscriptionPromises.get(subscriptionId); + if (existingPromise) return existingPromise; + + const promise = fetchStripeSubscriptionForBilling({ + ctx, + fullCus: fullCustomer, + targetCusProductId: customerProduct.id, + }); + subscriptionPromises.set(subscriptionId, promise); + + return promise; + }; + + const getStripeSubscriptionSchedule = async ({ + customerProduct, + stripeSubscription, + }: { + customerProduct: FullCusProduct; + stripeSubscription?: Stripe.Subscription; + }) => { + const cacheKey = getScheduleCacheKey({ + customerProduct, + stripeSubscription, + }); + if (!cacheKey) return undefined; + + const existingPromise = schedulePromises.get(cacheKey); + if (existingPromise) return existingPromise; + + const promise = fetchStripeSubscriptionScheduleForBilling({ + ctx, + fullCus: fullCustomer, + products: [], + targetCusProductId: customerProduct.id, + subscriptionScheduleId: + typeof stripeSubscription?.schedule === "string" + ? stripeSubscription.schedule + : undefined, + }); + schedulePromises.set(cacheKey, promise); + + return promise; + }; + + const getStripeDiscounts = async ({ + customerProduct, + stripeSubscription, + }: { + customerProduct: FullCusProduct; + stripeSubscription?: StripeSubscriptionWithDiscounts; + }) => { + const subscriptionId = getSubscriptionId({ customerProduct }); + const cacheKey = subscriptionId ?? "customer"; + + const existingPromise = discountPromises.get(cacheKey); + if (existingPromise) return existingPromise; + + const promise = getStripeCustomer().then(({ stripeCustomer }) => + fetchStripeDiscountsForBilling({ + ctx, + stripeSubscription, + stripeCustomer, + }), + ); + discountPromises.set(cacheKey, promise); + + return promise; + }; + + return { + getStripeCustomer, + getStripeSubscription, + getStripeSubscriptionSchedule, + getStripeDiscounts, + }; +}; diff --git a/server/src/internal/migrations/v2/stripeCache/index.ts b/server/src/internal/migrations/v2/stripeCache/index.ts new file mode 100644 index 000000000..24c35a99e --- /dev/null +++ b/server/src/internal/migrations/v2/stripeCache/index.ts @@ -0,0 +1 @@ +export * from "./createMigrationStripeCache.js"; diff --git a/server/src/internal/migrations/v2/types/migrationDefinition.ts b/server/src/internal/migrations/v2/types/migrationDefinition.ts new file mode 100644 index 000000000..9e2e083db --- /dev/null +++ b/server/src/internal/migrations/v2/types/migrationDefinition.ts @@ -0,0 +1,40 @@ +import type { Migration } from "@autumn/shared"; +import type { MigrationFilter } from "@autumn/shared/api/migrations/filters/migrationFilter.js"; +import type { Operations } from "@autumn/shared/api/migrations/operations/operations.js"; +import { hashJson } from "@/utils/hash/hashJson.js"; +import type { PreparedState } from "../prepare/types/index.js"; + +export type MigrationDefinition = { + id: string; + filter?: MigrationFilter | null; + operations?: Operations | null; + prepared_state?: PreparedState | null; + no_billing_changes?: boolean | null; + retry_failed?: boolean; +}; + +export type MigrationRuntime = Migration | MigrationDefinition; + +export type MigrationRuntimeWithEventId = MigrationRuntime & { + event_internal_id: string; +}; + +export const isPersistedMigration = ( + migration: MigrationRuntime, +): migration is Migration => + "internal_id" in migration && typeof migration.internal_id === "string"; + +export const withMigrationEventId = ({ + orgId, + env, + migration, +}: { + orgId: string; + env: string; + migration: MigrationRuntime; +}): MigrationRuntimeWithEventId => ({ + ...migration, + event_internal_id: isPersistedMigration(migration) + ? migration.internal_id + : `script_${hashJson({ value: { orgId, env, id: migration.id } })}`, +}); diff --git a/server/src/internal/misc/redisV2Cache/redisV2CacheSchemas.ts b/server/src/internal/misc/redisV2Cache/redisV2CacheSchemas.ts index 3e84a4379..c184e5b19 100644 --- a/server/src/internal/misc/redisV2Cache/redisV2CacheSchemas.ts +++ b/server/src/internal/misc/redisV2Cache/redisV2CacheSchemas.ts @@ -4,7 +4,7 @@ export const RedisV2InstanceName = z.enum(["upstash", "redis", "dragonfly"]); export type RedisV2InstanceName = z.infer; export const RedisV2CacheConfigSchema = z.object({ - activeInstance: RedisV2InstanceName.default("upstash"), + activeInstance: RedisV2InstanceName.default("dragonfly"), }); export type RedisV2CacheConfig = z.infer; diff --git a/server/src/internal/misc/redisV2Cache/redisV2CacheStore.ts b/server/src/internal/misc/redisV2Cache/redisV2CacheStore.ts index 5cc3e6cd2..7763731dc 100644 --- a/server/src/internal/misc/redisV2Cache/redisV2CacheStore.ts +++ b/server/src/internal/misc/redisV2Cache/redisV2CacheStore.ts @@ -11,7 +11,7 @@ import { const store = createEdgeConfigStore({ s3Key: ADMIN_REDIS_V2_CACHE_CONFIG_KEY, schema: RedisV2CacheConfigSchema, - defaultValue: () => ({ activeInstance: "upstash" }), + defaultValue: () => ({ activeInstance: "dragonfly" }), pollIntervalMs: ms.seconds(10), }); diff --git a/server/src/internal/orgs/repos/findFullOrg.ts b/server/src/internal/orgs/repos/findFullOrg.ts new file mode 100644 index 000000000..0afdfbd53 --- /dev/null +++ b/server/src/internal/orgs/repos/findFullOrg.ts @@ -0,0 +1,88 @@ +import { + type AppEnv, + type Feature, + type FullOrg, + features, + migrationRuns, + type Organization, + OrgConfigSchema, + organizations, + type PendingMigration, +} from "@autumn/shared"; +import { and, eq, inArray } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; + +export const findFullOrg = async ({ + db, + orgId, + env, +}: { + db: DrizzleCli; + orgId: string; + env: AppEnv; +}): Promise<{ + org: Organization; + features: Feature[]; + pendingMigrations: PendingMigration[]; + fullOrg: FullOrg; +} | null> => { + const row = await db.query.organizations.findFirst({ + where: eq(organizations.id, orgId), + with: { + features: { where: eq(features.env, env) }, + master: true, + migration_runs: { + where: and( + eq(migrationRuns.env, env), + inArray(migrationRuns.status, ["queued", "running"]), + eq(migrationRuns.dry_run, false), + eq(migrationRuns.lazy_run, true), + ), + with: { migration: true }, + }, + }, + }); + + if (!row) return null; + + const cloned = structuredClone(row); + const { + features: rawFeatures, + migration_runs: rawMigrationRuns, + master: rawMaster, + ...orgCore + } = cloned; + + // Drizzle's $inferSelect types features more loosely (e.g. `type: string`, + // `created_at: number | null`) than the hand-written `Feature` zod shape. + // Same trade-off as `OrgService.getWithFeatures` — bridged with one cast. + const orgFeatures = (rawFeatures ?? []) as unknown as Feature[]; + const pendingMigrations: PendingMigration[] = rawMigrationRuns ?? []; + + const master: Organization | null = rawMaster + ? { + ...rawMaster, + master: null, + config: OrgConfigSchema.parse(rawMaster.config || {}), + } + : null; + + const org: Organization = { + ...orgCore, + master, + config: OrgConfigSchema.parse(orgCore.config || {}), + pendingMigrations, + }; + + const fullOrg: FullOrg = { + ...org, + features: orgFeatures, + }; + + return { + org, + features: orgFeatures, + pendingMigrations, + fullOrg, + }; +}; diff --git a/server/src/internal/orgs/repos/index.ts b/server/src/internal/orgs/repos/index.ts new file mode 100644 index 000000000..350e05eb3 --- /dev/null +++ b/server/src/internal/orgs/repos/index.ts @@ -0,0 +1,5 @@ +import { findFullOrg } from "./findFullOrg.js"; + +export const orgRepo = { + findFull: findFullOrg, +}; diff --git a/server/src/internal/platform/platformBeta/platformBetaRouter.ts b/server/src/internal/platform/platformBeta/platformBetaRouter.ts index ea55f069d..f620a7b0d 100644 --- a/server/src/internal/platform/platformBeta/platformBetaRouter.ts +++ b/server/src/internal/platform/platformBeta/platformBetaRouter.ts @@ -1,4 +1,3 @@ -import { Autumn } from "autumn-js"; import { Hono } from "hono"; import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; import { handleCreatePlatformOrg } from "./handlers/handleCreatePlatformOrg.js"; @@ -24,12 +23,13 @@ platformBetaRouter.use("*", async (c, next) => { } try { - const autumn = new Autumn(); - const { allowed } = await autumn.check({ - customerId: org.id, - featureId: "platform", - }); + // const autumn = new Autumn(); + // const { allowed } = await autumn.check({ + // customerId: org.id, + // featureId: "platform", + // }); + const allowed = true; if (!allowed) { return c.json( { diff --git a/server/src/internal/products/prices/PriceService.ts b/server/src/internal/products/prices/PriceService.ts index 7afe4c1c4..0b7120999 100644 --- a/server/src/internal/products/prices/PriceService.ts +++ b/server/src/internal/products/prices/PriceService.ts @@ -1,4 +1,5 @@ import { + type DbPrice, type FixedPriceConfig, type Price, type Product, @@ -59,7 +60,7 @@ export class PriceService { }: { db: DrizzleCli; id: string; - update: Partial; + update: Partial; }) { await db.update(prices).set(update).where(eq(prices.id, id)); } diff --git a/server/src/internal/rewards/rewardUtils.ts b/server/src/internal/rewards/rewardUtils.ts index 579d03b41..d7365c945 100644 --- a/server/src/internal/rewards/rewardUtils.ts +++ b/server/src/internal/rewards/rewardUtils.ts @@ -4,6 +4,7 @@ import { DiscountConfigSchema, ErrCode, isFixedPrice, + notNullish, type Price, type Product, RecaseError, @@ -197,7 +198,7 @@ export const initRewardStripePrices = async ({ } const internalProductIds = getUnique( - prices.map((p: Price) => p.internal_product_id), + prices.map((p: Price) => p.internal_product_id).filter(notNullish), ); const products = await ProductService.listByInternalIds({ db: ctx.db, diff --git a/server/src/routers/internalRouter.ts b/server/src/routers/internalRouter.ts index b86f99c71..907fd6285 100644 --- a/server/src/routers/internalRouter.ts +++ b/server/src/routers/internalRouter.ts @@ -14,6 +14,7 @@ import { honoAdminRouter } from "../internal/admin/adminRouter"; import { internalAnalyticsRouter } from "../internal/analytics/internalAnalyticsRouter"; import { internalCusRouter } from "../internal/customers/internalCusRouter"; import { internalDevRouter } from "../internal/dev/devRouter"; +import { migrationRpcRouter } from "../internal/migrations/v2/migrationRouter"; import { consentRouter } from "../internal/misc/consent/consentRouter"; import { feedbackRouter } from "../internal/misc/feedback/feedbackRouter"; import { pricingAgentRouter } from "../internal/misc/pricingAgent/pricingAgentRouter"; @@ -46,6 +47,7 @@ internalRouter.route("/trmnl", internalTrmnlRouter); internalRouter.route("/feedback", feedbackRouter); internalRouter.route("/saved_views", savedViewsRouter); internalRouter.route("/query", internalAnalyticsRouter); +internalRouter.route("", migrationRpcRouter); internalRouter.route("/workbench", workbenchRouter); // Autumn SDK handler (requires session auth) diff --git a/server/src/routers/rpcRouter.ts b/server/src/routers/rpcRouter.ts index 4da4bfa24..b36b08d42 100644 --- a/server/src/routers/rpcRouter.ts +++ b/server/src/routers/rpcRouter.ts @@ -5,6 +5,7 @@ import { billingRpcRouter } from "@/internal/billing/billingRouter"; import { entityRpcRouter } from "@/internal/entities/entityRouter"; import { eventsRpcRouter } from "@/internal/events/eventsRouter"; import { featureRpcRouter } from "@/internal/features/featureRouter"; +import { migrationRpcRouter } from "@/internal/migrations/v2/migrationRouter"; import { plansRpcRouter } from "@/internal/products/productRouter"; import type { HonoEnv } from "../honoUtils/HonoEnv"; import { customerRpcRouter } from "../internal/customers/cusRouter"; @@ -30,3 +31,4 @@ rpcRouter.route("", eventsRpcRouter); rpcRouter.route("", referralRpcRouter); rpcRouter.route("", entityRpcRouter); rpcRouter.route("", featureRpcRouter); +rpcRouter.route("", migrationRpcRouter); diff --git a/server/src/trigger/configureTrigger.ts b/server/src/trigger/configureTrigger.ts new file mode 100644 index 000000000..0462edc79 --- /dev/null +++ b/server/src/trigger/configureTrigger.ts @@ -0,0 +1,18 @@ +import { configure } from "@trigger.dev/sdk/v3"; + +/** + * Point the trigger.dev SDK at autumn's project key. + * + * autumn and autumn-cloud each have their own trigger.dev project. We + * keep autumn's secret under `TRIGGER_SERVER_SECRET_KEY` so the two + * never collide in a shared shell — the SDK's default `TRIGGER_SECRET_KEY` + * is left to autumn-cloud. + * + * Module side-effect: this `configure` call runs once on first import. + * Anything that triggers tasks server-side imports from + * `@/trigger/migrations/...`, which re-exports from this file's siblings, + * so the configure happens before any `.trigger()` call. + */ +if (process.env.TRIGGER_SERVER_SECRET_KEY) { + configure({ secretKey: process.env.TRIGGER_SERVER_SECRET_KEY }); +} diff --git a/server/src/trigger/migrations/runMigrationCustomerTask.ts b/server/src/trigger/migrations/runMigrationCustomerTask.ts new file mode 100644 index 000000000..314828d4e --- /dev/null +++ b/server/src/trigger/migrations/runMigrationCustomerTask.ts @@ -0,0 +1,103 @@ +import { AppEnv } from "@autumn/shared"; +import { task } from "@trigger.dev/sdk/v3"; +import { z } from "zod/v4"; +import { deleteCachedFullCustomer } from "@/internal/customers/cusUtils/fullCustomerCacheUtils/deleteCachedFullCustomer.js"; +import { withMigrationItemTracking } from "@/internal/migrations/v2/actions/migrationItem/index.js"; +import { migrationRepo } from "@/internal/migrations/v2/repos/index.js"; +import { migrateCustomer } from "@/internal/migrations/v2/run/migrateCustomer/index.js"; +import { createTriggerContext } from "@/trigger/utils/createTriggerContext.js"; + +const PayloadSchema = z.object({ + orgId: z.string(), + env: z.enum(AppEnv), + migrationInternalId: z.string(), + migrationRunId: z.string(), + customerInternalId: z.string(), + customerId: z.string().nullable(), +}); + +export type RunMigrationCustomerPayload = z.infer; + +/** + * Per-customer lazy migration task. Enqueued by `checkPendingMigrationsForCustomer` + * on the customer-fetch path. Claims the `migration_item_runs` row, busts the + * customer cache, then runs `migrateCustomer` under the existing tracking machinery. + * + * Trigger.dev's `concurrencyKey` (set at enqueue time) serializes parallel + * requests for the same customer + migration; the server-side claim is the + * real authority if another worker raced ahead. + */ +export const runMigrationCustomerTask = task({ + id: "run-migration-customer", + maxDuration: 600, + run: async (rawPayload: unknown, { ctx: triggerCtx }) => { + const { + orgId, + env, + migrationInternalId, + migrationRunId, + customerInternalId, + customerId, + } = PayloadSchema.parse(rawPayload); + + const { ctx, logger } = await createTriggerContext({ + orgId, + env, + triggerCtx, + customerId: customerId ?? customerInternalId, + }); + + logger.info("run-migration-customer: starting", { + data: { migrationInternalId, migrationRunId, customerInternalId }, + }); + + const migration = await migrationRepo.find({ + ctx, + internalId: migrationInternalId, + }); + + await withMigrationItemTracking({ + ctx, + migrationInternalId, + migrationRunId, + item: { + kind: "customer", + internal_id: customerInternalId, + id: customerId, + }, + dryRun: false, + claimItemRun: true, + run: async () => { + // Bust the customer cache as soon as we own the claim so in-flight + // reads load fresh state and see the `running` item_run. + // `deleteCachedFullCustomer` also invalidates the FullSubject cache. + const cacheKey = customerId ?? customerInternalId; + await deleteCachedFullCustomer({ + ctx, + customerId: cacheKey, + source: "runMigrationCustomerTask", + }); + + const result = await migrateCustomer({ + ctx, + customerId: cacheKey, + migration, + }); + + return { + itemPreview: { + id: customerId, + name: null, + email: null, + }, + status: result.status, + response: result.response, + }; + }, + }); + + logger.info("run-migration-customer: done", { + data: { migrationInternalId, customerInternalId }, + }); + }, +}); diff --git a/server/src/trigger/migrations/runMigrationTask.ts b/server/src/trigger/migrations/runMigrationTask.ts new file mode 100644 index 000000000..a49b611da --- /dev/null +++ b/server/src/trigger/migrations/runMigrationTask.ts @@ -0,0 +1,71 @@ +import { AppEnv } from "@autumn/shared"; +import { task } from "@trigger.dev/sdk/v3"; +import { z } from "zod/v4"; +import { withMigrationRunTracking } from "@/internal/migrations/v2/actions/migrationRun/index.js"; +import { migrationRepo } from "@/internal/migrations/v2/repos/index.js"; +import { runMigration } from "@/internal/migrations/v2/run/runMigration.js"; +import { createTriggerContext } from "@/trigger/utils/createTriggerContext.js"; + +const ControlsSchema = z.object({ + limit: z.number().int().min(1).optional(), + only: z.array(z.string()).optional(), + concurrency: z.number().int().min(1).optional(), +}).optional(); + +const PayloadSchema = z.object({ + orgId: z.string(), + env: z.enum(AppEnv), + migrationId: z.string(), + migrationRunId: z.string(), + dryRun: z.boolean().default(false), + controls: ControlsSchema, +}); + +export type RunMigrationPayload = z.infer; + +export const runMigrationTaskQueue = { + concurrencyLimit: 1, +}; + +export const runMigrationTask = task({ + id: "run-migration", + queue: runMigrationTaskQueue, + maxDuration: 3600, + run: async (rawPayload: unknown, { ctx: triggerCtx }) => { + const { orgId, env, migrationId, migrationRunId, dryRun, controls } = + PayloadSchema.parse(rawPayload); + + const { ctx, logger } = await createTriggerContext({ + orgId, + env, + triggerCtx, + }); + + logger.info("run-migration: starting", { + data: { migrationId, dryRun }, + }); + + await withMigrationRunTracking({ + ctx, + migrationRunId, + run: async () => { + const migration = await migrationRepo.find({ ctx, id: migrationId }); + + await runMigration({ + ctx, + migration, + dryRun, + migrationRunId, + controls: controls ?? undefined, + }); + }, + }); + + logger.info("run-migration: done", { + data: { + migrationId, + dryRun, + }, + }); + }, +}); diff --git a/server/src/trigger/utils/createTriggerContext.ts b/server/src/trigger/utils/createTriggerContext.ts new file mode 100644 index 000000000..67096e164 --- /dev/null +++ b/server/src/trigger/utils/createTriggerContext.ts @@ -0,0 +1,50 @@ +import type { AppEnv } from "@autumn/shared"; +import type { Context as TriggerRunContext } from "@trigger.dev/sdk/v3"; +import { db } from "@/db/initDrizzle.js"; +import { + createDualLogger, + type Logger, +} from "@/external/logtail/logtailUtils.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { createWorkerContext } from "@/queue/createWorkerContext.js"; +import { addTriggerToLogs } from "@/utils/logging/addContextToLogs.js"; + +/** + * Build an `AutumnContext` for a trigger.dev task run. Uses the dual + * logger (stdout + axiom) and tags every line with run/task/attempt ids. + */ +export const createTriggerContext = async ({ + orgId, + env, + triggerCtx, + customerId, +}: { + orgId: string; + env: AppEnv; + triggerCtx: TriggerRunContext; + customerId?: string; +}): Promise<{ ctx: AutumnContext; logger: Logger }> => { + const logger = addTriggerToLogs({ + logger: createDualLogger(), + triggerContext: { + run_id: triggerCtx.run.id, + task_id: triggerCtx.task.id, + attempt_number: triggerCtx.attempt.number, + }, + }); + + const ctx = await createWorkerContext({ + db, + payload: { orgId, env, customerId, requestId: triggerCtx.run.id }, + logger, + }); + + if (!ctx) + throw new Error( + `createTriggerContext: failed to build context for org=${orgId} env=${env}`, + ); + + ctx.insideTriggerTask = true; + + return { ctx, logger }; +}; diff --git a/server/src/utils/envUtils.ts b/server/src/utils/envUtils.ts index d4a31cd09..251c913e7 100644 --- a/server/src/utils/envUtils.ts +++ b/server/src/utils/envUtils.ts @@ -1,17 +1,36 @@ +import { existsSync } from "node:fs"; import { join } from "node:path"; import { config } from "dotenv"; let hasLoadedLocalEnv = false; const shouldLogLocalEnvLoading = false; +/** + * Resolve the directory holding `.env`, robust to cwd: + * - cwd already a `server/` dir + * - cwd is `autumn/` (typical workspace root) + * - cwd is the monorepo root (one level above `autumn/`) — happens with + * `bun test autumn/...` invocations from VSCode tasks + */ +const resolveServerDir = (): string => { + const cwd = process.cwd(); + const candidates = [ + cwd, + join(cwd, "server"), + join(cwd, "autumn", "server"), + ]; + for (const dir of candidates) { + if (existsSync(join(dir, "package.json"))) return dir; + } + // Fall back to first guess so dotenv silently no-ops if missing. + return cwd.includes("server") ? cwd : join(cwd, "server"); +}; + export const loadLocalEnv = ({ force = false }: { force?: boolean } = {}) => { if (hasLoadedLocalEnv && !force) return; hasLoadedLocalEnv = true; - const processDir = process.cwd(); - const serverDir = processDir.includes("server") - ? processDir - : join(processDir, "server"); + const serverDir = resolveServerDir(); // Determine which env file to load based on ENV_FILE environment variable // Defaults to .env if not specified diff --git a/server/src/utils/hash/hashJson.ts b/server/src/utils/hash/hashJson.ts index 810277519..91743ff3f 100644 --- a/server/src/utils/hash/hashJson.ts +++ b/server/src/utils/hash/hashJson.ts @@ -1,3 +1,5 @@ +import { createHash } from "node:crypto"; + /** * Recursively sort object keys, strip undefined values, and produce a stable string. * Array element order is preserved; object key order is not. @@ -23,7 +25,7 @@ const deterministicStringify = (value: unknown): string => { /** Produce a SHA-256 hex digest from any JSON-serialisable value, key-order independent. */ export const hashJson = ({ value }: { value: unknown }): string => { - const hasher = new Bun.CryptoHasher("sha256"); - hasher.update(deterministicStringify(value)); - return hasher.digest("hex"); + return createHash("sha256") + .update(deterministicStringify(value)) + .digest("hex"); }; diff --git a/server/src/utils/logging/addContextToLogs.ts b/server/src/utils/logging/addContextToLogs.ts index adf1c255b..26a3e2d63 100644 --- a/server/src/utils/logging/addContextToLogs.ts +++ b/server/src/utils/logging/addContextToLogs.ts @@ -4,6 +4,7 @@ import type { LogRedisData, LogRequestContext, LogStripeEventContext, + LogTriggerContext, LogWorkflowContext, } from "./loggerTypes.js"; @@ -47,6 +48,16 @@ export const addWorkflowToLogs = ({ return logger.child({ context: { workflow: workflowContext } }); }; +export const addTriggerToLogs = ({ + logger, + triggerContext, +}: { + logger: Logger; + triggerContext: LogTriggerContext; +}): Logger => { + return logger.child({ context: { trigger: triggerContext } }); +}; + export const addRedisToLogs = ({ logger, redisData, diff --git a/server/src/utils/logging/initLogger.ts b/server/src/utils/logging/initLogger.ts index 49198e64d..ba019dcba 100644 --- a/server/src/utils/logging/initLogger.ts +++ b/server/src/utils/logging/initLogger.ts @@ -2,58 +2,88 @@ import { Writable } from "node:stream"; import pino from "pino"; import { getAwsTaskIdentity } from "@/external/aws/ecs/awsTaskIdentity.js"; -// Custom log formatter for Bun compatibility -const createDevLogStream = () => { - const colors = { - reset: "\x1b[0m", - bright: "\x1b[1m", - dim: "\x1b[2m", - red: "\x1b[31m", - green: "\x1b[32m", - yellow: "\x1b[33m", - blue: "\x1b[34m", - magenta: "\x1b[35m", - cyan: "\x1b[36m", - white: "\x1b[37m", - gray: "\x1b[90m", - bgRed: "\x1b[41m", - }; +/** + * Fields that don't render in the formatted dev/local console output + * (they still go to JSON sinks). Trim noise like trigger metadata, + * request envelopes, and high-cardinality structured data. + */ +const FORMATTED_LOG_EXCLUDE_FIELDS = new Set([ + // pino housekeeping + "time", + "level", + "msg", + "pid", + "hostname", + // request / response envelopes + "req", + "res", + "statusCode", + "body", + "query", + "durationMs", + // app-context blocks (added via `addContextToLogs`) + "context", + "workflow", + "trigger", + "stripe_event", + "worker", + "extras", + "type", + // structured payloads + "data", + // AWS task identity (mixin) + "aws", +]); - const levelColors: Record = { - // Numeric levels - 10: colors.gray, // trace - 20: colors.blue, // debug - 30: colors.green, // info - 40: colors.yellow, // warn - 50: colors.red, // error - 60: colors.bgRed, // fatal - // String levels - TRACE: colors.gray, - DEBUG: colors.blue, - INFO: colors.green, - WARN: colors.yellow, - ERROR: colors.red, - FATAL: colors.bgRed, - }; +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + white: "\x1b[37m", + gray: "\x1b[90m", + bgRed: "\x1b[41m", +}; - const levelNames: Record = { - // Numeric levels - 10: "TRACE", - 20: "DEBUG", - 30: "INFO", - 40: "WARN", - 50: "ERROR", - 60: "FATAL", - // String levels (pass through) - TRACE: "TRACE", - DEBUG: "DEBUG", - INFO: "INFO", - WARN: "WARN", - ERROR: "ERROR", - FATAL: "FATAL", - }; +const levelColors: Record = { + 10: colors.gray, + 20: colors.blue, + 30: colors.green, + 40: colors.yellow, + 50: colors.red, + 60: colors.bgRed, + TRACE: colors.gray, + DEBUG: colors.blue, + INFO: colors.green, + WARN: colors.yellow, + ERROR: colors.red, + FATAL: colors.bgRed, +}; - return new Writable({ +const levelNames: Record = { + 10: "TRACE", + 20: "DEBUG", + 30: "INFO", + 40: "WARN", + 50: "ERROR", + 60: "FATAL", + TRACE: "TRACE", + DEBUG: "DEBUG", + INFO: "INFO", + WARN: "WARN", + ERROR: "ERROR", + FATAL: "FATAL", +}; + +/** Bun-friendly pino sink that prints ` `. */ +const createDevLogStream = ({ + trailingNewline = true, +}: { + trailingNewline?: boolean; +} = {}) => + new Writable({ write(chunk, _encoding, callback) { try { const log = JSON.parse(chunk.toString()); @@ -66,45 +96,23 @@ const createDevLogStream = () => { const levelName = levelNames[level] || (typeof level === "string" ? level : "UNKNOWN"); - // Format the message let message = log.msg || ""; - // Add any additional fields (excluding standard pino fields) - const excludeFields = [ - "time", - "level", - "msg", - "pid", - "hostname", - "res", - "statusCode", - "worker", - "context", - "req", - "data", - "body", - "query", - - "workflow", - "stripe_event", - "extras", - "type", - - "durationMs", - ]; const additionalFields = Object.keys(log) - .filter((key) => !excludeFields.includes(key)) - .reduce((acc, key) => { - acc[key] = log[key]; - return acc; - }, {} as any); + .filter((key) => !FORMATTED_LOG_EXCLUDE_FIELDS.has(key)) + .reduce( + (acc, key) => { + acc[key] = log[key]; + return acc; + }, + {} as Record, + ); if (Object.keys(additionalFields).length > 0) { message += ` ${JSON.stringify(additionalFields, null, 2)}`; } - // Format the final log line - const formattedLog = `${colors.gray}${timestamp}${colors.reset} ${levelColor}${colors.bright}${levelName}${colors.reset} ${message}\n`; + const formattedLog = `${colors.gray}${timestamp}${colors.reset} ${levelColor}${colors.bright}${levelName}${colors.reset} ${message}${trailingNewline ? "\n" : ""}`; process.stdout.write(formattedLog); callback(); @@ -115,47 +123,81 @@ const createDevLogStream = () => { } }, }); + +/** + * `mode: "dual"` is opt-in only — used by trigger.dev tasks so logs hit + * both stdout (trigger run UI) and axiom (long-term store). + * + * In dev/test the stdout stream uses the formatted dev sink so trigger + * pane lines look like server lines. In prod it stays raw JSON so + * trigger.dev's cloud UI gets structured logs. + * + * `mode: "default"` preserves existing prod behavior verbatim. + */ +export type InitLoggerOptions = { + mode?: "default" | "dual"; }; -export const initLogger = () => { - // Create separate streams for console and HyperDX - const streams: pino.StreamEntry[] = []; +export const initLogger = (options: InitLoggerOptions = {}) => { + const { mode = "default" } = options; + const streams: pino.StreamEntry[] = []; const isDev = process.env.NODE_ENV === "development"; const isTest = process.env.NODE_ENV === "test"; + const isDevOrTest = isDev || isTest; - // Enable dev logging for development OR test environments - if (isDev || isTest) { + if (mode === "dual") { streams.push({ - level: "debug", - stream: createDevLogStream(), + level: isDevOrTest ? "debug" : "info", + stream: isDevOrTest + ? createDevLogStream({ trailingNewline: false }) + : process.stdout, }); - } + if (process.env.AXIOM_TOKEN) { + streams.push({ + level: "info", + stream: pino.transport({ + target: "@axiomhq/pino", + options: { + dataset: "express", + token: process.env.AXIOM_TOKEN, + }, + }), + }); + } + } else { + // DEFAULT FLOW — exact prior behavior. DO NOT MODIFY. + if (isDev || isTest) { + streams.push({ + level: "debug", + stream: createDevLogStream(), + }); + } - if (process.env.AXIOM_TOKEN) { - streams.push({ - level: "info", - stream: pino.transport({ - target: "@axiomhq/pino", - options: { - dataset: "express", - token: process.env.AXIOM_TOKEN, - }, - }), - }); - } + if (process.env.AXIOM_TOKEN) { + streams.push({ + level: "info", + stream: pino.transport({ + target: "@axiomhq/pino", + options: { + dataset: "express", + token: process.env.AXIOM_TOKEN, + }, + }), + }); + } - // Fallback: if no streams configured, add dev stream anyway - if (streams.length === 0) { - streams.push({ - level: "info", - stream: createDevLogStream(), - }); + if (streams.length === 0) { + streams.push({ + level: "info", + stream: createDevLogStream(), + }); + } } const logger = pino( { - level: isDev || isTest ? "debug" : "info", + level: isDev || isTest || mode === "dual" ? "debug" : "info", // Tag every log line with this process's AWS task identity so Axiom // can distinguish blue/green task sets. Returns {} until // `resolveAwsTaskIdentity` finishes (~100ms after boot) and on @@ -172,14 +214,13 @@ export const initLogger = () => { }; }, formatters: { - level: (label: any) => { + level: (label: string) => { return { level: label.toUpperCase(), }; }, }, }, - // Use multistream to send logs to multiple destinations pino.multistream(streams), ); diff --git a/server/src/utils/logging/loggerTypes.ts b/server/src/utils/logging/loggerTypes.ts index c6f7e04a6..5f852785a 100644 --- a/server/src/utils/logging/loggerTypes.ts +++ b/server/src/utils/logging/loggerTypes.ts @@ -52,6 +52,13 @@ export type LogWorkflowContext = { name: string; // workflow / job name }; +/** trigger.dev run context — goes under context.trigger */ +export type LogTriggerContext = { + run_id: string; + task_id: string; + attempt_number?: number; +}; + /** Redis slow-command context - goes under context.redis.data (map field) */ export type LogRedisData = { operation: string; diff --git a/server/tests/integration/billing/attach/params/start-date/starts-at-scheduling.test.ts b/server/tests/integration/billing/attach/params/start-date/starts-at-scheduling.test.ts index 148d06fef..ef885d16e 100644 --- a/server/tests/integration/billing/attach/params/start-date/starts-at-scheduling.test.ts +++ b/server/tests/integration/billing/attach/params/start-date/starts-at-scheduling.test.ts @@ -101,6 +101,116 @@ test.concurrent(`${chalk.yellowBright("starts_at: future attach creates schedule await expectCustomerInvoiceCorrect({ customerId, count: 0 }); }); +test.concurrent(`${chalk.yellowBright("starts_at: scheduled subscription can be canceled by customer_product_id")}`, async () => { + const customerId = "attach-start-date-cancel-scheduled"; + const pro = products.pro({ + id: "pro", + items: [items.monthlyMessages({ includedUsage: 100 })], + }); + + const { autumnV1, autumnV2_2, ctx, advancedTo } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [], + }); + + const startDate = addDays(advancedTo, 1).getTime(); + await autumnV2_2.billing.attach({ + customer_id: customerId, + plan_id: pro.id, + starts_at: startDate, + }); + + const scheduledCustomerProduct = await getCustomerProduct({ + ctx, + customerId, + productId: pro.id, + }); + const scheduleId = scheduledCustomerProduct.scheduled_ids?.[0]; + if (!scheduleId) + throw new Error("Expected scheduled product to have schedule"); + + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + customer_product_id: scheduledCustomerProduct.id, + cancel_action: "cancel_immediately", + }); + expect(preview.total).toBe(0); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + customer_product_id: scheduledCustomerProduct.id, + cancel_action: "cancel_immediately", + }); + + const customer = await autumnV2_2.customers.get(customerId); + await expectCustomerProducts({ + customer, + notPresent: [pro.id], + }); + + const stripeSchedule = + await ctx.stripeCli.subscriptionSchedules.retrieve(scheduleId); + expect(stripeSchedule.status).toBe("canceled"); + await expectCustomerInvoiceCorrect({ customerId, count: 0 }); +}); + +test.concurrent(`${chalk.yellowBright("starts_at: immediate-access future subscription cancels by deleting schedule")}`, async () => { + const customerId = "attach-start-date-cancel-immediate-access-v2"; + const pro = products.pro({ + id: "pro-immediate-access-cancel", + items: [items.monthlyMessages({ includedUsage: 100 })], + }); + + const { autumnV1, autumnV2_2, ctx, advancedTo } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [], + }); + + const startDate = addDays(advancedTo, 1).getTime(); + await autumnV2_2.billing.attach({ + customer_id: customerId, + plan_id: pro.id, + starts_at: startDate, + enable_plan_immediately: true, + }); + + const customerProduct = await getCustomerProduct({ + ctx, + customerId, + productId: pro.id, + }); + const scheduleId = customerProduct.scheduled_ids?.[0]; + if (!scheduleId) + throw new Error("Expected immediate-access product to have schedule"); + expect(customerProduct.status).toBe(CusProductStatus.Active); + expect(customerProduct.subscription_ids ?? []).toEqual([]); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + customer_product_id: customerProduct.id, + cancel_action: "cancel_end_of_cycle", + }); + + const customer = await autumnV2_2.customers.get(customerId); + await expectCustomerProducts({ + customer, + notPresent: [pro.id], + }); + + const stripeSchedule = + await ctx.stripeCli.subscriptionSchedules.retrieve(scheduleId); + expect(stripeSchedule.status).toBe("canceled"); + await expectCustomerInvoiceCorrect({ customerId, count: 0 }); +}); + test.concurrent(`${chalk.yellowBright("starts_at: future attach without payment method creates invoice schedule")}`, async () => { const customerId = "attach-start-date-future-invoice"; const pro = products.pro({ diff --git a/server/tests/integration/billing/migrations-v2/add-plan-operation/add-plan-op-basic.test.ts b/server/tests/integration/billing/migrations-v2/add-plan-operation/add-plan-op-basic.test.ts new file mode 100644 index 000000000..207de9af0 --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/add-plan-operation/add-plan-op-basic.test.ts @@ -0,0 +1,146 @@ +/** + * Integration tests for the add_plan migration operation — basic cases. + * + * Contract under test: + * - add_plan attaches a new plan to a customer who doesn't have it. + * - add_plan is idempotent: skipped if the customer already has the plan active. + * - add_plan runs before update_plan so a later update_plan can target the new plan. + * - add_plan with a non-existent plan_id fails the item. + */ + +import { expect, test } from "bun:test"; +import { TestFeature } from "@tests/setup/v2Features"; +import { items } from "@tests/utils/fixtures/items"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import { runMigrationAndWait } from "../utils/runMigrationPreview"; + +test(`${chalk.yellowBright("add_plan: attaches a new free plan")}`, async () => { + const suffix = Date.now(); + const customerId = `add-plan-basic-${suffix}`; + const existing = products.base({ + id: `add-plan-existing-${suffix}`, + items: [items.monthlyMessages({ includedUsage: 100 })], + }); + const newPlan = products.base({ + id: `add-plan-new-${suffix}`, + items: [items.dashboard()], + }); + + const { autumnV2_2 } = await initScenario({ + customerId, + setup: [s.customer(), s.products({ list: [existing, newPlan] })], + actions: [s.billing.attach({ productId: existing.id })], + }); + + const result = await runMigrationAndWait({ + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + filter: { customer: { plan: { plan_id: existing.id } } }, + operations: { + customer: [{ type: "add_plan", plan_id: newPlan.id }], + }, + }); + + expect(result.status).toBe("succeeded"); + const preview = result.response.preview as Record; + const planChanges = preview.plan_changes as unknown[]; + expect(planChanges.length).toBe(1); +}); + +test(`${chalk.yellowBright("add_plan: idempotent — skipped if customer already has plan")}`, async () => { + const suffix = Date.now(); + const customerId = `add-plan-idem-${suffix}`; + const plan = products.base({ + id: `add-plan-idem-plan-${suffix}`, + items: [items.dashboard()], + }); + + const { autumnV2_2 } = await initScenario({ + customerId, + setup: [s.customer(), s.products({ list: [plan] })], + actions: [s.billing.attach({ productId: plan.id })], + }); + + const result = await runMigrationAndWait({ + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + filter: { customer: { customer_id: customerId } }, + operations: { + customer: [{ type: "add_plan", plan_id: plan.id }], + }, + }); + + expect(result.status).toBe("skipped"); +}); + +test(`${chalk.yellowBright("add_plan: runs before update_plan (ordering)")}`, async () => { + const suffix = Date.now(); + const customerId = `add-then-update-${suffix}`; + const existing = products.base({ + id: `add-upd-existing-${suffix}`, + items: [items.monthlyMessages({ includedUsage: 100 })], + }); + const newPlan = products.base({ + id: `add-upd-new-${suffix}`, + items: [items.dashboard()], + }); + + const { autumnV2_2 } = await initScenario({ + customerId, + setup: [s.customer(), s.products({ list: [existing, newPlan] })], + actions: [s.billing.attach({ productId: existing.id })], + }); + + const result = await runMigrationAndWait({ + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + filter: { customer: { plan: { plan_id: existing.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: newPlan.id }, + customize: { + add_items: [{ feature_id: TestFeature.AdminRights }], + }, + }, + { type: "add_plan", plan_id: newPlan.id }, + ], + }, + }); + + expect(result.status).toBe("succeeded"); + const preview = result.response.preview as Record; + const planChanges = preview.plan_changes as unknown[]; + expect(planChanges.length).toBe(2); +}); + +test(`${chalk.yellowBright("add_plan: non-existent plan_id fails")}`, async () => { + const suffix = Date.now(); + const customerId = `add-plan-missing-${suffix}`; + const existing = products.base({ + id: `add-plan-miss-${suffix}`, + items: [items.dashboard()], + }); + + const { autumnV2_2 } = await initScenario({ + customerId, + setup: [s.customer(), s.products({ list: [existing] })], + actions: [s.billing.attach({ productId: existing.id })], + }); + + const result = await runMigrationAndWait({ + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + filter: { customer: { customer_id: customerId } }, + operations: { + customer: [{ type: "add_plan", plan_id: "nonexistent-plan-id" }], + }, + }); + + expect(result.status).toBe("failed"); + const error = result.response.error as { message: string } | undefined; + expect(error?.message).toContain("not found"); +}); diff --git a/server/tests/integration/billing/migrations-v2/add-plan-operation/add-plan-op-features.test.ts b/server/tests/integration/billing/migrations-v2/add-plan-operation/add-plan-op-features.test.ts new file mode 100644 index 000000000..0eef078d3 --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/add-plan-operation/add-plan-op-features.test.ts @@ -0,0 +1,134 @@ +/** + * Integration tests for add_plan — feature quantities and paid plans. + * + * Contract under test: + * - add_plan with feature_quantities sets initial balances on the new plan. + * - add_plan attaching a paid plan creates prices on the cusProduct. + * - add_plan with a specific version targets that catalog version. + */ + +import { expect, test } from "bun:test"; +import { TestFeature } from "@tests/setup/v2Features"; +import { items } from "@tests/utils/fixtures/items"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import { runMigrationAndWait } from "../utils/runMigrationPreview"; + +test(`${chalk.yellowBright("add_plan: feature_quantities sets initial balances")}`, async () => { + const suffix = Date.now(); + const customerId = `add-plan-quantities-${suffix}`; + const existing = products.base({ + id: `add-qty-existing-${suffix}`, + items: [items.dashboard()], + }); + const newPlan = products.base({ + id: `add-qty-new-${suffix}`, + items: [items.prepaidMessages({ includedUsage: 0, billingUnits: 100 })], + }); + + const { autumnV2_2 } = await initScenario({ + customerId, + setup: [s.customer(), s.products({ list: [existing, newPlan] })], + actions: [s.billing.attach({ productId: existing.id })], + }); + + const result = await runMigrationAndWait({ + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + filter: { customer: { customer_id: customerId } }, + operations: { + customer: [ + { + type: "add_plan", + plan_id: newPlan.id, + feature_quantities: [ + { feature_id: TestFeature.Messages, quantity: 500 }, + ], + }, + ], + }, + }); + + expect(result.status).toBe("succeeded"); + const preview = result.response.preview as Record; + const planChanges = preview.plan_changes as unknown[]; + expect(planChanges.length).toBe(1); +}); + +test(`${chalk.yellowBright("add_plan: paid plan creates cusProduct with prices")}`, async () => { + const suffix = Date.now(); + const customerId = `add-plan-paid-${suffix}`; + const free = products.base({ + id: `add-paid-free-${suffix}`, + items: [items.dashboard()], + }); + const paid = products.pro({ + id: `add-paid-pro-${suffix}`, + items: [items.monthlyMessages({ includedUsage: 100 })], + }); + + const { autumnV2_2 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [free, paid] }), + ], + actions: [s.billing.attach({ productId: free.id })], + }); + + const result = await runMigrationAndWait({ + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + filter: { customer: { customer_id: customerId } }, + operations: { + customer: [{ type: "add_plan", plan_id: paid.id }], + }, + }); + + expect(result.status).toBe("succeeded"); + const preview = result.response.preview as Record; + const planChanges = preview.plan_changes as unknown[]; + expect(planChanges.length).toBe(1); +}); + +test(`${chalk.yellowBright("add_plan: targets specific catalog version")}`, async () => { + const suffix = Date.now(); + const customerId = `add-plan-version-${suffix}`; + const existing = products.base({ + id: `add-ver-existing-${suffix}`, + items: [items.dashboard()], + }); + const versionedPlan = products.base({ + id: `add-ver-target-${suffix}`, + items: [items.monthlyMessages({ includedUsage: 50 })], + }); + + const { autumnV1, autumnV2_2 } = await initScenario({ + customerId, + setup: [s.customer(), s.products({ list: [existing, versionedPlan] })], + actions: [s.billing.attach({ productId: existing.id })], + }); + + await autumnV1.products.update(versionedPlan.id, { + items: [ + { + feature_id: TestFeature.Messages, + included: 200, + reset: { interval: "month" }, + }, + ], + new_version: true, + }); + + const result = await runMigrationAndWait({ + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + filter: { customer: { customer_id: customerId } }, + operations: { + customer: [{ type: "add_plan", plan_id: versionedPlan.id, version: 1 }], + }, + }); + + expect(result.status).toBe("succeeded"); +}); diff --git a/server/tests/integration/billing/migrations-v2/add-plan-operation/add-plan-op-none-filter.test.ts b/server/tests/integration/billing/migrations-v2/add-plan-operation/add-plan-op-none-filter.test.ts new file mode 100644 index 000000000..ce380582f --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/add-plan-operation/add-plan-op-none-filter.test.ts @@ -0,0 +1,53 @@ +/** + * Integration tests for $none filter quantifier with add_plan. + * + * Contract under test: + * - $none with empty filter matches customers with no active plans. + * - $none with plan_id matches customers who don't have that specific plan. + * - Combining $none filter + add_plan attaches a plan to customers who lack it. + */ + +import { expect, test } from "bun:test"; +import { items } from "@tests/utils/fixtures/items"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import { runMigrationAndWait } from "../utils/runMigrationPreview"; + +test(`${chalk.yellowBright("$none filter: add_plan to customers without a specific plan")}`, async () => { + const suffix = Date.now(); + const customerId = `none-filter-add-${suffix}`; + const planA = products.base({ + id: `none-plan-a-${suffix}`, + items: [items.dashboard()], + }); + const planB = products.base({ + id: `none-plan-b-${suffix}`, + items: [items.monthlyMessages({ includedUsage: 100 })], + }); + + const { autumnV2_2 } = await initScenario({ + customerId, + setup: [s.customer(), s.products({ list: [planA, planB] })], + actions: [s.billing.attach({ productId: planA.id })], + }); + + const result = await runMigrationAndWait({ + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + filter: { + customer: { + plan: { $none: { plan_id: planB.id } }, + }, + }, + operations: { + customer: [{ type: "add_plan", plan_id: planB.id }], + }, + }); + + expect(result.status).toBe("succeeded"); + const preview = result.response.preview as Record; + const planChanges = preview.plan_changes as unknown[]; + expect(planChanges.length).toBe(1); +}); + diff --git a/server/tests/integration/billing/migrations-v2/add-plan-operation/add-plan-op-preview.test.ts b/server/tests/integration/billing/migrations-v2/add-plan-operation/add-plan-op-preview.test.ts new file mode 100644 index 000000000..a0480f25e --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/add-plan-operation/add-plan-op-preview.test.ts @@ -0,0 +1,69 @@ +/** + * Integration tests for add_plan — preview output. + * + * Contract under test: + * - add_plan preview emits a "created" plan_change with the plan_id. + * - add_plan with boolean features emits flag_changes. + * - add_plan with metered features emits balance_changes. + */ + +import { expect, test } from "bun:test"; +import { TestFeature } from "@tests/setup/v2Features"; +import { items } from "@tests/utils/fixtures/items"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import { runMigrationAndWait } from "../utils/runMigrationPreview"; + +type PreviewPlanChange = { + action: string; + plan_id: string; + item_changes: Array<{ action: string; feature_id: string }>; +}; + +test(`${chalk.yellowBright("add_plan preview: emits created plan_change")}`, async () => { + const suffix = Date.now(); + const customerId = `add-plan-preview-created-${suffix}`; + const existing = products.base({ + id: `add-prev-existing-${suffix}`, + items: [items.monthlyMessages({ includedUsage: 100 })], + }); + const newPlan = products.base({ + id: `add-prev-new-${suffix}`, + items: [items.dashboard(), items.monthlyCredits({ includedUsage: 50 })], + }); + + const { autumnV2_2 } = await initScenario({ + customerId, + setup: [s.customer(), s.products({ list: [existing, newPlan] })], + actions: [s.billing.attach({ productId: existing.id })], + }); + + const result = await runMigrationAndWait({ + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + filter: { customer: { customer_id: customerId } }, + operations: { + customer: [{ type: "add_plan", plan_id: newPlan.id }], + }, + }); + + expect(result.status).toBe("succeeded"); + const preview = result.response.preview as { + plan_changes: PreviewPlanChange[]; + balance_changes: unknown[]; + flag_changes: unknown[]; + }; + + expect(preview.plan_changes.length).toBe(1); + const planChange = JSON.parse( + typeof preview.plan_changes[0] === "string" + ? preview.plan_changes[0] + : JSON.stringify(preview.plan_changes[0]), + ) as PreviewPlanChange; + expect(planChange.action).toBe("created"); + expect(planChange.plan_id).toBe(newPlan.id); + + expect(preview.flag_changes.length).toBeGreaterThan(0); + expect(preview.balance_changes.length).toBeGreaterThan(0); +}); diff --git a/server/tests/integration/billing/migrations-v2/events/migration-events-basic.test.ts b/server/tests/integration/billing/migrations-v2/events/migration-events-basic.test.ts new file mode 100644 index 000000000..bf1d3a057 --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/events/migration-events-basic.test.ts @@ -0,0 +1,75 @@ +import { test } from "bun:test"; +import { itemsV2 } from "@tests/utils/fixtures/itemsV2.js"; +import { products } from "@tests/utils/fixtures/products.js"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js"; +import chalk from "chalk"; + +const timeout = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +test(`${chalk.yellowBright("migrations events: basic free-plan boolean feature migration")}`, async () => { + const suffix = Date.now(); + const firstCustomerId = `migration-events-free-first-${suffix}`; + const secondCustomerId = `migration-events-free-second-${suffix}`; + const thirdCustomerId = `migration-events-free-third-${suffix}`; + const freePlan = products.base({ + id: `migration-events-free-${suffix}`, + items: [], + }); + + const { autumnV2_2 } = await initScenario({ + customerId: firstCustomerId, + setup: [ + s.customer(), + s.otherCustomers([{ id: secondCustomerId }, { id: thirdCustomerId }]), + s.products({ list: [freePlan] }), + ], + actions: [ + s.parallel( + s.billing.attach({ productId: freePlan.id }), + s.billing.attach({ + customerId: secondCustomerId, + productId: freePlan.id, + }), + s.billing.attach({ + customerId: thirdCustomerId, + productId: freePlan.id, + }), + ), + ], + }); + + const migration = await autumnV2_2.migrationsV2.deleteAndCreate({ + id: `migration-events-free-dashboard-${suffix}`, + filter: { customer: { plan: { plan_id: freePlan.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: freePlan.id }, + customize: { + add_items: [itemsV2.dashboard()], + }, + }, + ], + }, + }); + + const runResponse = await autumnV2_2.migrationsV2.run({ + id: migration.id, + dry_run: false, + }); + + await timeout(10_000); + + const runs = await autumnV2_2.migrationsV2.listRuns({ + migrationId: migration.id, + }); + const events = await autumnV2_2.migrationsV2.listItemEvents({ + migrationId: migration.id, + migrationRunId: runResponse.run_id, + }); + + console.log("migration runs", JSON.stringify(runs, null, 2)); + console.log("migration item events", JSON.stringify(events, null, 2)); +}); diff --git a/server/tests/integration/billing/migrations-v2/idempotency/migration-idempotency.test.ts b/server/tests/integration/billing/migrations-v2/idempotency/migration-idempotency.test.ts new file mode 100644 index 000000000..42aed5e48 --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/idempotency/migration-idempotency.test.ts @@ -0,0 +1,518 @@ +// Contract: migrations-v2 run API must be idempotent per migration/customer. +// It must also serialize migration task execution per org. + +import { expect, test } from "bun:test"; +import { + type ApiCustomerV5, + ErrCode, + type Migration, + MigrationItemKind, + MigrationItemRunStatus, +} from "@autumn/shared"; +import { expectFlagCorrect } from "@tests/integration/utils/expectFlagCorrect.js"; +import { TestFeature } from "@tests/setup/v2Features.js"; +import { itemsV2 } from "@tests/utils/fixtures/itemsV2.js"; +import { products } from "@tests/utils/fixtures/products.js"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js"; +import chalk from "chalk"; +import { CusService } from "@/internal/customers/CusService.js"; +import { migrationItemRunRepo } from "@/internal/migrations/v2/repos/index.js"; +import { waitForMigrationResult } from "../utils/runUpdatePlanMigration.js"; + +const timeout = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +const buildDashboardMigration = ({ + id, + planId, +}: { + id: string; + planId: string; +}) => ({ + id, + filter: { customer: { plan: { plan_id: planId } } }, + operations: { + customer: [ + { + type: "update_plan" as const, + plan_filter: { plan_id: planId }, + customize: { + add_items: [itemsV2.dashboard()], + }, + }, + ], + }, +}); + +const getInternalCustomerId = async ({ + customerId, + ctx, +}: { + customerId: string; + ctx: Awaited>["ctx"]; +}) => { + const customer = await CusService.get({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); + if (!customer) throw new Error(`Expected customer ${customerId}`); + return customer.internal_id; +}; + +const getCustomerItemRun = ({ + ctx, + migration, + internalCustomerId, +}: { + ctx: Awaited>["ctx"]; + migration: Migration; + internalCustomerId: string; +}) => + migrationItemRunRepo.getCustomer({ + ctx, + migrationInternalId: migration.internal_id, + internalCustomerId, + }); + +const waitForCustomerItemRunStatus = async ({ + ctx, + migration, + internalCustomerId, + status, +}: { + ctx: Awaited>["ctx"]; + migration: Migration; + internalCustomerId: string; + status: MigrationItemRunStatus; +}) => + waitForMigrationResult({ + timeoutMs: 60_000, + pollIntervalMs: 1_000, + waitFor: async () => { + expect( + await getCustomerItemRun({ + ctx, + migration, + internalCustomerId, + }), + ).toMatchObject({ status }); + }, + }); + +const waitForMigrationRunAccepted = async ({ + autumnV2_2, + id, + dryRun = false, +}: { + autumnV2_2: Awaited>["autumnV2_2"]; + id: string; + dryRun?: boolean; +}) => + waitForMigrationResult({ + timeoutMs: 60_000, + pollIntervalMs: 1_000, + waitFor: async () => + autumnV2_2.migrationsV2.run({ + id, + dry_run: dryRun, + }), + }); + +test(`${chalk.yellowBright("migrations idempotency: run API does not replay a succeeded customer")}`, async () => { + const customerId = "migration-idem-succeeded"; + const plan = products.pro({ items: [] }); + const { autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [plan] }), + ], + actions: [s.billing.attach({ productId: plan.id })], + }); + const internalCustomerId = await getInternalCustomerId({ customerId, ctx }); + const migration = await autumnV2_2.migrationsV2.deleteAndCreate( + buildDashboardMigration({ + id: `${customerId}-mig`, + planId: plan.id, + }), + ); + + await waitForMigrationRunAccepted({ autumnV2_2, id: migration.id }); + await waitForCustomerItemRunStatus({ + ctx, + migration, + internalCustomerId, + status: MigrationItemRunStatus.Succeeded, + }); + const firstRun = await getCustomerItemRun({ + ctx, + migration, + internalCustomerId, + }); + expect(firstRun).toMatchObject({ + status: MigrationItemRunStatus.Succeeded, + }); + + await waitForMigrationRunAccepted({ autumnV2_2, id: migration.id }); + await timeout(3_000); + const secondRun = await getCustomerItemRun({ + ctx, + migration, + internalCustomerId, + }); + expect(secondRun).toMatchObject({ + status: MigrationItemRunStatus.Succeeded, + updated_at: firstRun?.updated_at, + }); + + const otherMigration = await autumnV2_2.migrationsV2.deleteAndCreate( + buildDashboardMigration({ + id: `${customerId}-other-mig`, + planId: plan.id, + }), + ); + await waitForMigrationRunAccepted({ autumnV2_2, id: otherMigration.id }); + await waitForCustomerItemRunStatus({ + ctx, + migration: otherMigration, + internalCustomerId, + status: MigrationItemRunStatus.Succeeded, + }); +}); + +test(`${chalk.yellowBright("migrations idempotency: run API skips running and failed customer rows")}`, async () => { + const customerId = "migration-idem-skips"; + const plan = products.pro({ items: [] }); + const { autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [plan] }), + ], + actions: [s.billing.attach({ productId: plan.id })], + }); + const internalCustomerId = await getInternalCustomerId({ customerId, ctx }); + + const runningMigration = await autumnV2_2.migrationsV2.deleteAndCreate( + buildDashboardMigration({ + id: `${customerId}-running-mig`, + planId: plan.id, + }), + ); + await migrationItemRunRepo.claim({ + ctx, + migrationInternalId: runningMigration.internal_id, + itemKind: MigrationItemKind.Customer, + itemId: internalCustomerId, + claimBehavior: "claim_new", + }); + await waitForMigrationRunAccepted({ autumnV2_2, id: runningMigration.id }); + await timeout(3_000); + expect( + await getCustomerItemRun({ + ctx, + migration: runningMigration, + internalCustomerId, + }), + ).toMatchObject({ status: MigrationItemRunStatus.Running }); + + const failedMigration = await autumnV2_2.migrationsV2.deleteAndCreate( + buildDashboardMigration({ + id: `${customerId}-failed-mig`, + planId: plan.id, + }), + ); + await migrationItemRunRepo.claim({ + ctx, + migrationInternalId: failedMigration.internal_id, + itemKind: MigrationItemKind.Customer, + itemId: internalCustomerId, + claimBehavior: "claim_new", + }); + await migrationItemRunRepo.markFailed({ + ctx, + migrationInternalId: failedMigration.internal_id, + itemKind: MigrationItemKind.Customer, + itemId: internalCustomerId, + }); + await waitForMigrationRunAccepted({ autumnV2_2, id: failedMigration.id }); + await timeout(3_000); + expect( + await getCustomerItemRun({ + ctx, + migration: failedMigration, + internalCustomerId, + }), + ).toMatchObject({ status: MigrationItemRunStatus.Failed }); + + const customer = await autumnV2_2.customers.get(customerId); + expectFlagCorrect({ + customer, + featureId: TestFeature.Dashboard, + present: false, + }); +}); + +test(`${chalk.yellowBright("migrations idempotency: retry_failed and dry_run are honored through run API")}`, async () => { + const retryCustomerId = "migration-idem-retry"; + const dryRunCustomerId = "migration-idem-dry-run"; + const retryPlan = products.pro({ id: "retry-pro", items: [] }); + const dryRunPlan = products.premium({ id: "dry-run-premium", items: [] }); + const { autumnV2_2, ctx } = await initScenario({ + customerId: retryCustomerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.otherCustomers([{ id: dryRunCustomerId, paymentMethod: "success" }]), + s.products({ list: [retryPlan, dryRunPlan] }), + ], + actions: [ + s.billing.attach({ productId: retryPlan.id }), + s.billing.attach({ + customerId: dryRunCustomerId, + productId: dryRunPlan.id, + }), + ], + }); + const retryInternalCustomerId = await getInternalCustomerId({ + customerId: retryCustomerId, + ctx, + }); + const dryRunInternalCustomerId = await getInternalCustomerId({ + customerId: dryRunCustomerId, + ctx, + }); + + const retryMigration = await autumnV2_2.migrationsV2.deleteAndCreate( + buildDashboardMigration({ + id: `${retryCustomerId}-mig`, + planId: retryPlan.id, + }), + ); + const retryableMigration = await autumnV2_2.migrationsV2.update({ + id: retryMigration.id, + updates: { retry_failed: true }, + }); + await migrationItemRunRepo.claim({ + ctx, + migrationInternalId: retryableMigration.internal_id, + itemKind: MigrationItemKind.Customer, + itemId: retryInternalCustomerId, + claimBehavior: "claim_new", + }); + await migrationItemRunRepo.markFailed({ + ctx, + migrationInternalId: retryableMigration.internal_id, + itemKind: MigrationItemKind.Customer, + itemId: retryInternalCustomerId, + }); + await waitForMigrationRunAccepted({ autumnV2_2, id: retryableMigration.id }); + await waitForCustomerItemRunStatus({ + ctx, + migration: retryableMigration, + internalCustomerId: retryInternalCustomerId, + status: MigrationItemRunStatus.Succeeded, + }); + expect( + await getCustomerItemRun({ + ctx, + migration: retryableMigration, + internalCustomerId: retryInternalCustomerId, + }), + ).toMatchObject({ status: MigrationItemRunStatus.Succeeded }); + + const dryRunMigration = await autumnV2_2.migrationsV2.deleteAndCreate( + buildDashboardMigration({ + id: `${dryRunCustomerId}-mig`, + planId: dryRunPlan.id, + }), + ); + await waitForMigrationRunAccepted({ + autumnV2_2, + id: dryRunMigration.id, + dryRun: true, + }); + await timeout(3_000); + expect( + await getCustomerItemRun({ + ctx, + migration: dryRunMigration, + internalCustomerId: dryRunInternalCustomerId, + }), + ).toBeNull(); + const dryRunCustomer = + await autumnV2_2.customers.get(dryRunCustomerId); + expectFlagCorrect({ + customer: dryRunCustomer, + featureId: TestFeature.Dashboard, + present: false, + }); +}); + +test(`${chalk.yellowBright("migrations idempotency: item run checkpoints are dry-run scoped")}`, async () => { + const customerId = "migration-idem-dry-run-scope"; + const plan = products.pro({ items: [] }); + const { ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [plan] }), + ], + actions: [s.billing.attach({ productId: plan.id })], + }); + const internalCustomerId = await getInternalCustomerId({ customerId, ctx }); + const migrationInternalId = "script_test_dry_run_scope"; + + await migrationItemRunRepo.claim({ + ctx, + migrationInternalId, + migrationRunId: "dry-run-v1", + dryRun: true, + itemKind: MigrationItemKind.Customer, + itemId: internalCustomerId, + claimBehavior: "claim_new", + }); + await migrationItemRunRepo.markSucceeded({ + ctx, + migrationInternalId, + migrationRunId: "dry-run-v1", + dryRun: true, + itemKind: MigrationItemKind.Customer, + itemId: internalCustomerId, + }); + + expect( + await migrationItemRunRepo.getCustomer({ + ctx, + migrationInternalId, + migrationRunId: "dry-run-v1", + dryRun: true, + internalCustomerId, + }), + ).toMatchObject({ status: MigrationItemRunStatus.Succeeded }); + expect( + await migrationItemRunRepo.getCustomer({ + ctx, + migrationInternalId, + migrationRunId: "dry-run-v2", + dryRun: true, + internalCustomerId, + }), + ).toBeNull(); + + const manualReplayMigrationInternalId = "script_test_manual_replay"; + await migrationItemRunRepo.markSucceeded({ + ctx, + migrationInternalId: manualReplayMigrationInternalId, + itemKind: MigrationItemKind.Customer, + itemId: internalCustomerId, + }); + expect( + await migrationItemRunRepo.getCustomer({ + ctx, + migrationInternalId: manualReplayMigrationInternalId, + internalCustomerId, + }), + ).toMatchObject({ + dry_run: false, + status: MigrationItemRunStatus.Succeeded, + }); + + await expect( + migrationItemRunRepo.claim({ + ctx, + migrationInternalId, + itemKind: MigrationItemKind.Customer, + itemId: internalCustomerId, + claimBehavior: "claim_new", + }), + ).resolves.toMatchObject({ claimed: true }); +}); + +test(`${chalk.yellowBright("migrations idempotency: run API rejects concurrent migration runs per org")}`, async () => { + const firstCustomerId = "migration-idem-serial-first"; + const secondCustomerId = "migration-idem-serial-second"; + const firstPlan = products.pro({ id: "serial-pro", items: [] }); + const secondPlan = products.premium({ id: "serial-premium", items: [] }); + const { autumnV2_2, ctx } = await initScenario({ + customerId: firstCustomerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.otherCustomers([{ id: secondCustomerId, paymentMethod: "success" }]), + s.products({ list: [firstPlan, secondPlan] }), + ], + actions: [ + s.billing.attach({ productId: firstPlan.id }), + s.billing.attach({ + customerId: secondCustomerId, + productId: secondPlan.id, + }), + ], + }); + const firstMigration = await autumnV2_2.migrationsV2.deleteAndCreate( + buildDashboardMigration({ + id: `${firstCustomerId}-mig`, + planId: firstPlan.id, + }), + ); + const secondMigration = await autumnV2_2.migrationsV2.deleteAndCreate( + buildDashboardMigration({ + id: `${secondCustomerId}-mig`, + planId: secondPlan.id, + }), + ); + + await autumnV2_2.migrationsV2.run({ + id: firstMigration.id, + dry_run: false, + }); + await expect( + autumnV2_2.migrationsV2.run({ + id: secondMigration.id, + dry_run: false, + }), + ).rejects.toMatchObject({ + code: ErrCode.MigrationAlreadyInProgress, + message: expect.stringContaining("already running"), + }); + + const firstInternalCustomerId = await getInternalCustomerId({ + customerId: firstCustomerId, + ctx, + }); + const secondInternalCustomerId = await getInternalCustomerId({ + customerId: secondCustomerId, + ctx, + }); + await waitForCustomerItemRunStatus({ + ctx, + migration: firstMigration, + internalCustomerId: firstInternalCustomerId, + status: MigrationItemRunStatus.Succeeded, + }); + + await waitForMigrationRunAccepted({ autumnV2_2, id: secondMigration.id }); + await waitForCustomerItemRunStatus({ + ctx, + migration: secondMigration, + internalCustomerId: secondInternalCustomerId, + status: MigrationItemRunStatus.Succeeded, + }); + const firstItemRun = await getCustomerItemRun({ + ctx, + migration: firstMigration, + internalCustomerId: firstInternalCustomerId, + }); + const secondItemRun = await getCustomerItemRun({ + ctx, + migration: secondMigration, + internalCustomerId: secondInternalCustomerId, + }); + expect(firstItemRun).toMatchObject({ + status: MigrationItemRunStatus.Succeeded, + }); + expect(secondItemRun).toMatchObject({ + status: MigrationItemRunStatus.Succeeded, + }); +}); diff --git a/server/tests/integration/billing/migrations-v2/lazy/lazy-migration-basic.test.ts b/server/tests/integration/billing/migrations-v2/lazy/lazy-migration-basic.test.ts new file mode 100644 index 000000000..a418140dd --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/lazy/lazy-migration-basic.test.ts @@ -0,0 +1,249 @@ +/** + * TDD test for Phase 2 of lazy migrations: customer-fetch path triggers the + * per-customer Trigger.dev task and the customer ends up migrated. + * + * Contract under test: + * Behavior: + * - POST /migrations.lazy_run starts a lazy-mode migration_run + * (lazy_run=true, status='running') + * - On the next /customers.get for an eligible customer: + * * runMigrationCustomerTask is enqueued with concurrencyKey + * `${migration_internal_id}:${customer.internal_id}` + * * eventually `migration_item_runs` has one `succeeded` row + * * subsequent /customers.get returns post-migration state + * (e.g. the new `dashboard` feature is now present) + * Side effects: + * - executeMigrateCustomerPlan invalidates the customer cache + * - exactly one `migration_item_runs` row per (migration, customer) + */ + +import { expect, test } from "bun:test"; +import { + MigrationItemKind, + MigrationItemRunStatus, + migrationItemRuns, +} from "@autumn/shared"; +import { TestFeature } from "@tests/setup/v2Features.js"; +import { products } from "@tests/utils/fixtures/products.js"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js"; +import chalk from "chalk"; +import { and, eq } from "drizzle-orm"; +import { finishLazyMigrationRun } from "@/internal/migrations/v2/actions/migrationRun/finishLazyMigrationRun.js"; +import { + countCustomerItemRunRows, + getCustomerAndAwaitMigration, + getInternalCustomerId, + releaseLazyMigrationRun, + startLazyMigration, + waitForCustomerItemRunStatus, +} from "./utils/lazyMigrationTestUtils.js"; + +const timeout = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +test.concurrent(`${chalk.yellowBright("lazy migration: multiple customers on pro fetch + auto-migrate")}`, async () => { + const firstCustomerId = "lazy-basic-first"; + const secondCustomerId = "lazy-basic-second"; + const thirdCustomerId = "lazy-basic-third"; + const plan = products.pro({ id: "lazy-basic-pro", items: [] }); + + const { autumnV2_2, ctx } = await initScenario({ + customerId: firstCustomerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.otherCustomers([ + { id: secondCustomerId, paymentMethod: "success" }, + { id: thirdCustomerId, paymentMethod: "success" }, + ]), + s.products({ list: [plan] }), + ], + actions: [ + s.billing.attach({ productId: plan.id }), + s.billing.attach({ + customerId: secondCustomerId, + productId: plan.id, + }), + s.billing.attach({ + customerId: thirdCustomerId, + productId: plan.id, + }), + ], + }); + + const { migration, run_id } = await startLazyMigration({ + autumnV2_2, + ctx, + id: `${firstCustomerId}-mig`, + planId: plan.id, + }); + + try { + const customerIds = [firstCustomerId, secondCustomerId, thirdCustomerId]; + + // ── Contract assertion 1: fetch + auto-migrate for each customer ── + for (const customerId of customerIds) { + const customer = await getCustomerAndAwaitMigration({ + autumnV2_2, + customerId, + }); + expect(customer.flags[TestFeature.Dashboard]).toBeDefined(); + } + + // ── Contract assertion 2: each customer has exactly one succeeded row ── + for (const customerId of customerIds) { + const internalCustomerId = await getInternalCustomerId({ + customerId, + ctx, + }); + await waitForCustomerItemRunStatus({ + ctx, + migration, + internalCustomerId, + status: MigrationItemRunStatus.Succeeded, + }); + + const rowCount = await countCustomerItemRunRows({ + ctx, + migration, + internalCustomerId, + }); + expect(rowCount).toBe(1); + } + } finally { + await releaseLazyMigrationRun({ ctx, runId: run_id }); + } +}); + +test.concurrent(`${chalk.yellowBright("lazy migration: non-matching customer is not migrated")}`, async () => { + const matchingCustomerId = "lazy-basic-matching"; + const mismatchCustomerId = "lazy-basic-mismatch"; + const proPlan = products.pro({ id: "lazy-basic-target-pro", items: [] }); + const premiumPlan = products.premium({ + id: "lazy-basic-other-premium", + items: [], + }); + + const { autumnV2_2, ctx } = await initScenario({ + customerId: matchingCustomerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.otherCustomers([{ id: mismatchCustomerId, paymentMethod: "success" }]), + s.products({ list: [proPlan, premiumPlan] }), + ], + actions: [ + s.billing.attach({ productId: proPlan.id }), + s.billing.attach({ + customerId: mismatchCustomerId, + productId: premiumPlan.id, + }), + ], + }); + + const { migration, run_id } = await startLazyMigration({ + autumnV2_2, + ctx, + id: `${matchingCustomerId}-mig`, + planId: proPlan.id, + }); + + try { + // Matching customer migrates as expected. + await getCustomerAndAwaitMigration({ + autumnV2_2, + customerId: matchingCustomerId, + }); + + // Mismatching customer's fetch is the in-memory pre-check site — assert + // it neither queues a task nor writes a migration_item_runs row. + const mismatchCustomer = await autumnV2_2.customers.get(mismatchCustomerId); + expect(mismatchCustomer).toBeDefined(); + + const mismatchInternalId = await getInternalCustomerId({ + customerId: mismatchCustomerId, + ctx, + }); + const mismatchRowCount = await countCustomerItemRunRows({ + ctx, + migration, + internalCustomerId: mismatchInternalId, + }); + expect(mismatchRowCount).toBe(0); + } finally { + await releaseLazyMigrationRun({ ctx, runId: run_id }); + } +}); + +test.concurrent(`${chalk.yellowBright("lazy migration: marking the run as done clears the org cache and stops the in-memory check")}`, async () => { + const customerId = "lazy-basic-done-clears"; + const plan = products.pro({ id: "lazy-basic-done-pro", items: [] }); + + const { autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [plan] }), + ], + actions: [s.billing.attach({ productId: plan.id })], + }); + const internalCustomerId = await getInternalCustomerId({ customerId, ctx }); + + const { migration, run_id } = await startLazyMigration({ + autumnV2_2, + ctx, + id: `${customerId}-mig`, + planId: plan.id, + }); + + try { + // Trigger the migration once and wait for it to land. + await getCustomerAndAwaitMigration({ autumnV2_2, customerId }); + await waitForCustomerItemRunStatus({ + ctx, + migration, + internalCustomerId, + status: MigrationItemRunStatus.Succeeded, + }); + expect( + await countCustomerItemRunRows({ + ctx, + migration, + internalCustomerId, + }), + ).toBe(1); + + // Finish the lazy run — should mark succeeded + clear org cache. + await finishLazyMigrationRun({ ctx, runId: run_id }); + + // Manually delete the customer's item_run row so we can detect whether + // the helper still queues a task post-completion. If `pendingMigrations` + // is correctly empty on the next request, no task should run and the + // row should stay deleted. + await ctx.db + .delete(migrationItemRuns) + .where( + and( + eq(migrationItemRuns.migration_internal_id, migration.internal_id), + eq(migrationItemRuns.item_kind, MigrationItemKind.Customer), + eq(migrationItemRuns.item_id, internalCustomerId), + ), + ); + + // Hit /customers.get a few times and give any queued task time to run. + for (let i = 0; i < 5; i++) { + await autumnV2_2.customers.get(customerId); + } + await timeout(3_000); + + // Helper should have short-circuited at `pendingMigrations.length === 0` + // → no new `migration_item_runs` row recreated. + expect( + await countCustomerItemRunRows({ + ctx, + migration, + internalCustomerId, + }), + ).toBe(0); + } finally { + await releaseLazyMigrationRun({ ctx, runId: run_id }); + } +}); diff --git a/server/tests/integration/billing/migrations-v2/lazy/lazy-migration-concurrency.test.ts b/server/tests/integration/billing/migrations-v2/lazy/lazy-migration-concurrency.test.ts new file mode 100644 index 000000000..7c7334f5f --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/lazy/lazy-migration-concurrency.test.ts @@ -0,0 +1,214 @@ +/** + * TDD test for Phase 2 lazy migrations — concurrency safety. + * + * Contract under test: + * - Concurrent /customers.get requests for the same customer during an + * active lazy migration may enqueue multiple Trigger.dev tasks, but + * `migration_item_runs` claim machinery guarantees the migration runs + * exactly once for that customer. + * - The partial unique index on `migration_runs (migration_internal_id) + * WHERE status IN ('queued','running')` rejects a second `lazy_run` + * call for the SAME migration definition while one is already active. + * - Different migration definitions CAN have active lazy runs + * concurrently — correctness lives at the per-customer claim layer. + */ + +import { expect, test } from "bun:test"; +import { ErrCode, MigrationItemRunStatus } from "@autumn/shared"; +import { TestFeature } from "@tests/setup/v2Features.js"; +import { itemsV2 } from "@tests/utils/fixtures/itemsV2.js"; +import { products } from "@tests/utils/fixtures/products.js"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js"; +import chalk from "chalk"; +import { + buildDashboardLazyMigration, + countCustomerItemRunRows, + getCustomerAndAwaitMigration, + getInternalCustomerId, + releaseLazyMigrationRun, + startLazyMigration, + waitForCustomerItemRunStatus, +} from "./utils/lazyMigrationTestUtils.js"; + +test.concurrent(`${chalk.yellowBright("lazy migration concurrency: concurrent fetches execute migration exactly once")}`, async () => { + const customerId = "lazy-concurrent-fetch"; + const plan = products.pro({ id: "lazy-concurrent-pro", items: [] }); + + const { autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [plan] }), + ], + actions: [s.billing.attach({ productId: plan.id })], + }); + + const { migration, run_id } = await startLazyMigration({ + autumnV2_2, + ctx, + id: `${customerId}-mig`, + planId: plan.id, + }); + + try { + const concurrency = 10; + await Promise.all( + Array.from({ length: concurrency }, () => + autumnV2_2.customers.get(customerId), + ), + ); + + const internalCustomerId = await getInternalCustomerId({ + customerId, + ctx, + }); + await waitForCustomerItemRunStatus({ + ctx, + migration, + internalCustomerId, + status: MigrationItemRunStatus.Succeeded, + }); + + const rowCount = await countCustomerItemRunRows({ + ctx, + migration, + internalCustomerId, + }); + expect(rowCount).toBe(1); + + const migrated = await getCustomerAndAwaitMigration({ + autumnV2_2, + customerId, + }); + expect(migrated.flags[TestFeature.Dashboard]).toBeDefined(); + } finally { + await releaseLazyMigrationRun({ ctx, runId: run_id }); + } +}); + +test.concurrent(`${chalk.yellowBright("lazy migration concurrency: starting a second lazy_run on the SAME migration is rejected (409)")}`, async () => { + const customerId = "lazy-same-mig-rejected"; + const plan = products.pro({ id: "lazy-same-mig-pro", items: [] }); + + const { autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [plan] }), + ], + actions: [s.billing.attach({ productId: plan.id })], + }); + + const { migration, run_id } = await startLazyMigration({ + autumnV2_2, + ctx, + id: `${customerId}-mig`, + planId: plan.id, + }); + + try { + // Same migration id → 409 (partial unique index on migration_internal_id). + await expect( + autumnV2_2.migrationsV2.lazyRun({ id: migration.id }), + ).rejects.toMatchObject({ + code: ErrCode.MigrationAlreadyInProgress, + message: expect.stringContaining("already running"), + }); + } finally { + await releaseLazyMigrationRun({ ctx, runId: run_id }); + } +}); + +test.concurrent(`${chalk.yellowBright("lazy migration concurrency: two different migrations on the same customer can run concurrently")}`, async () => { + const customerId = "lazy-two-migs"; + const plan = products.pro({ id: "lazy-two-migs-pro", items: [] }); + + const { autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [plan] }), + ], + actions: [s.billing.attach({ productId: plan.id })], + }); + const internalCustomerId = await getInternalCustomerId({ customerId, ctx }); + + // First migration — adds Dashboard. + const { migration: firstMigration, run_id: firstRunId } = + await startLazyMigration({ + autumnV2_2, + ctx, + id: `${customerId}-first-mig`, + planId: plan.id, + }); + + // Second migration — adds AdminRights. Starts WHILE first is still active. + const secondMigrationDef = await autumnV2_2.migrationsV2.deleteAndCreate({ + id: `${customerId}-second-mig`, + filter: { customer: { plan: { plan_id: plan.id } } }, + operations: { + customer: [ + { + type: "update_plan" as const, + plan_filter: { plan_id: plan.id }, + customize: { + add_items: [{ feature_id: TestFeature.AdminRights }], + }, + }, + ], + }, + }); + const secondRun = await autumnV2_2.migrationsV2.lazyRun({ + id: secondMigrationDef.id, + }); + + try { + // Customer fetch eventually picks up BOTH migrations — poll in parallel + // since each migration is independent and triggers its own task. + await Promise.all([ + getCustomerAndAwaitMigration({ + autumnV2_2, + customerId, + featureId: TestFeature.Dashboard, + }), + getCustomerAndAwaitMigration({ + autumnV2_2, + customerId, + featureId: TestFeature.AdminRights, + }), + ]); + + await Promise.all([ + waitForCustomerItemRunStatus({ + ctx, + migration: firstMigration, + internalCustomerId, + status: MigrationItemRunStatus.Succeeded, + }), + waitForCustomerItemRunStatus({ + ctx, + migration: secondMigrationDef, + internalCustomerId, + status: MigrationItemRunStatus.Succeeded, + }), + ]); + + expect( + await countCustomerItemRunRows({ + ctx, + migration: firstMigration, + internalCustomerId, + }), + ).toBe(1); + expect( + await countCustomerItemRunRows({ + ctx, + migration: secondMigrationDef, + internalCustomerId, + }), + ).toBe(1); + } finally { + await releaseLazyMigrationRun({ ctx, runId: firstRunId }); + await releaseLazyMigrationRun({ ctx, runId: secondRun.run_id }); + } +}); diff --git a/server/tests/integration/billing/migrations-v2/lazy/lazy-migration-multi-entity.test.ts b/server/tests/integration/billing/migrations-v2/lazy/lazy-migration-multi-entity.test.ts new file mode 100644 index 000000000..01ee802a2 --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/lazy/lazy-migration-multi-entity.test.ts @@ -0,0 +1,187 @@ +/** + * TDD test for Phase 2 lazy migrations — multi-entity customers. + * + * Contract under test: + * - When a customer has multiple entities on the same plan and the + * lazy migration filter matches the customer (not entity-specific), + * concurrent entity-scoped fetches must NOT cause double execution. + * - The migration's `migration_item_runs` row is keyed by customer, + * not entity, so even with N entities only one row should exist + * for the customer after migration. + * - After the migration completes, every entity returns post-migration + * state on subsequent fetches. + * - When entities each have their own (free) attached plan, migrating + * that plan touches BOTH entity-scoped customer_products through one + * customer-level item_run. + */ + +import { expect, test } from "bun:test"; +import { + type ApiCustomerV5, + MigrationItemRunStatus, +} from "@autumn/shared"; +import { TestFeature } from "@tests/setup/v2Features.js"; +import { products } from "@tests/utils/fixtures/products.js"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js"; +import chalk from "chalk"; +import { + countCustomerItemRunRows, + getCustomerAndAwaitMigration, + getInternalCustomerId, + releaseLazyMigrationRun, + startLazyMigration, + waitForCustomerItemRunStatus, +} from "./utils/lazyMigrationTestUtils.js"; + +test.concurrent( + `${chalk.yellowBright("lazy migration multi-entity: concurrent entity fetches still migrate the customer exactly once")}`, + async () => { + const customerId = "lazy-multi-entity"; + const plan = products.pro({ id: "lazy-multi-entity-pro", items: [] }); + + const { autumnV2_2, ctx, entities } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [plan] }), + s.entities({ count: 2, featureId: TestFeature.Users }), + ], + actions: [s.billing.attach({ productId: plan.id })], + }); + + expect(entities.length).toBe(2); + const [firstEntity, secondEntity] = entities; + if (!firstEntity || !secondEntity) + throw new Error("expected 2 entities to be generated"); + + const { migration, run_id } = await startLazyMigration({ + autumnV2_2, + ctx, + id: `${customerId}-mig`, + planId: plan.id, + }); + + try { + // Concurrent entity-scoped fetches. Both may enqueue their own + // runMigrationCustomerTask — the claim must serialize them. + await Promise.all([ + autumnV2_2.entities.get(customerId, firstEntity.id), + autumnV2_2.entities.get(customerId, secondEntity.id), + ]); + + // Migration eventually completes for the underlying customer. + const internalCustomerId = await getInternalCustomerId({ + customerId, + ctx, + }); + await waitForCustomerItemRunStatus({ + ctx, + migration, + internalCustomerId, + status: MigrationItemRunStatus.Succeeded, + }); + + // Exactly one item_run row (keyed by customer, not entity). + const rowCount = await countCustomerItemRunRows({ + ctx, + migration, + internalCustomerId, + }); + expect(rowCount).toBe(1); + + // Subsequent /customers.get returns post-migration state. + const customer = await getCustomerAndAwaitMigration({ + autumnV2_2, + customerId, + }); + expect(customer.flags[TestFeature.Dashboard]).toBeDefined(); + } finally { + await releaseLazyMigrationRun({ ctx, runId: run_id }); + } + }, +); + +test.concurrent( + `${chalk.yellowBright("lazy migration multi-entity: per-entity free plans both migrated under one customer item_run")}`, + async () => { + const customerId = "lazy-multi-entity-free"; + const freePlan = products.base({ + id: "lazy-multi-entity-free-plan", + items: [], + }); + + const { autumnV2_2, ctx, entities } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [freePlan] }), + s.entities({ count: 2, featureId: TestFeature.Users }), + ], + actions: [ + // Attach the free plan once per entity. Each attach produces an + // entity-scoped customer_product row (`internal_entity_id` set). + s.billing.attach({ productId: freePlan.id, entityIndex: 0 }), + s.billing.attach({ productId: freePlan.id, entityIndex: 1 }), + ], + }); + + expect(entities.length).toBe(2); + const [firstEntity, secondEntity] = entities; + if (!firstEntity || !secondEntity) + throw new Error("expected 2 entities to be generated"); + + const { migration, run_id } = await startLazyMigration({ + autumnV2_2, + ctx, + id: `${customerId}-mig`, + planId: freePlan.id, + }); + + try { + // Entity-scoped fetches surface entity-scoped customer_products in + // FullSubject, so the helper's filter pre-check matches there. The + // task itself loads a fresh customer-level view inside + // `setupMigrateCustomerContext` (`withEntities: true`) and applies + // the operation to BOTH entity-scoped plans in one execution. + await Promise.all([ + autumnV2_2.entities.get(customerId, firstEntity.id), + autumnV2_2.entities.get(customerId, secondEntity.id), + ]); + + const internalCustomerId = await getInternalCustomerId({ + customerId, + ctx, + }); + await waitForCustomerItemRunStatus({ + ctx, + migration, + internalCustomerId, + status: MigrationItemRunStatus.Succeeded, + }); + + // ── A. Migration ran once ──────────────────────────────────────── + expect( + await countCustomerItemRunRows({ + ctx, + migration, + internalCustomerId, + }), + ).toBe(1); + + // ── B. Both entity-scoped free plans carry the migrated state ───── + const firstEntityView = await autumnV2_2.entities.get( + customerId, + firstEntity.id, + ); + const secondEntityView = await autumnV2_2.entities.get( + customerId, + secondEntity.id, + ); + + expect(firstEntityView.flags[TestFeature.Dashboard]).toBeDefined(); + expect(secondEntityView.flags[TestFeature.Dashboard]).toBeDefined(); + } finally { + await releaseLazyMigrationRun({ ctx, runId: run_id }); + } + }, +); diff --git a/server/tests/integration/billing/migrations-v2/lazy/utils/lazyMigrationTestUtils.ts b/server/tests/integration/billing/migrations-v2/lazy/utils/lazyMigrationTestUtils.ts new file mode 100644 index 000000000..a209fee61 --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/lazy/utils/lazyMigrationTestUtils.ts @@ -0,0 +1,230 @@ +import { expect } from "bun:test"; +import { + type ApiCustomerV5, + type Migration, + MigrationItemKind, + type MigrationItemRun, + MigrationItemRunStatus, + migrationRuns, +} from "@autumn/shared"; +import { TestFeature } from "@tests/setup/v2Features.js"; +import { itemsV2 } from "@tests/utils/fixtures/itemsV2.js"; +import type { initScenario } from "@tests/utils/testInitUtils/initScenario.js"; +import { and, eq } from "drizzle-orm"; +import { CusService } from "@/internal/customers/CusService.js"; +import { finishLazyMigrationRun } from "@/internal/migrations/v2/actions/migrationRun/finishLazyMigrationRun.js"; +import { migrationItemRunRepo } from "@/internal/migrations/v2/repos/index.js"; +import { waitForMigrationResult } from "../../utils/runUpdatePlanMigration.js"; + +type ScenarioCtx = Awaited>["ctx"]; +type AutumnV2_2 = Awaited>["autumnV2_2"]; + +/** Boilerplate dashboard migration — adds the `TestFeature.Dashboard` boolean + * feature to every customer matching the given plan filter. */ +export const buildDashboardLazyMigration = ({ + id, + planId, +}: { + id: string; + planId: string; +}) => ({ + id, + filter: { customer: { plan: { plan_id: planId } } }, + operations: { + customer: [ + { + type: "update_plan" as const, + plan_filter: { plan_id: planId }, + customize: { + add_items: [itemsV2.dashboard()], + }, + }, + ], + }, +}); + +export const getInternalCustomerId = async ({ + customerId, + ctx, +}: { + customerId: string; + ctx: ScenarioCtx; +}) => { + const customer = await CusService.get({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); + if (!customer) throw new Error(`Expected customer ${customerId}`); + return customer.internal_id; +}; + +export const getCustomerItemRun = ({ + ctx, + migration, + internalCustomerId, +}: { + ctx: ScenarioCtx; + migration: Migration; + internalCustomerId: string; +}) => + migrationItemRunRepo.getCustomer({ + ctx, + migrationInternalId: migration.internal_id, + internalCustomerId, + }); + +export const waitForCustomerItemRunStatus = async ({ + ctx, + migration, + internalCustomerId, + status, + timeoutMs = 60_000, + pollIntervalMs = 1_000, +}: { + ctx: ScenarioCtx; + migration: Migration; + internalCustomerId: string; + status: MigrationItemRunStatus; + timeoutMs?: number; + pollIntervalMs?: number; +}) => + waitForMigrationResult({ + timeoutMs, + pollIntervalMs, + waitFor: async () => { + expect( + await getCustomerItemRun({ + ctx, + migration, + internalCustomerId, + }), + ).toMatchObject({ status }); + }, + }); + +/** Count `migration_item_runs` rows for a given (migration, customer). The + * partial unique index `migration_item_runs_live_unique` guarantees at most + * one row, so this should always be 0 or 1. */ +export const countCustomerItemRunRows = async ({ + ctx, + migration, + internalCustomerId, +}: { + ctx: ScenarioCtx; + migration: Migration; + internalCustomerId: string; +}): Promise => { + const rows = (await ctx.db.query.migrationItemRuns.findMany({ + where: (mir, { and, eq }) => + and( + eq(mir.migration_internal_id, migration.internal_id), + eq(mir.item_kind, MigrationItemKind.Customer), + eq(mir.item_id, internalCustomerId), + eq(mir.dry_run, false), + ), + })) as MigrationItemRun[]; + return rows.length; +}; + +/** Start a lazy migration. Only one active `migration_runs` row can exist + * per `(org, env)` (partial unique index), so concurrent tests in the same + * file serialize here: if another test holds the claim, we poll until it + * releases (succeeded / failed) then try again. */ +export const startLazyMigration = async ({ + autumnV2_2, + ctx, + id, + planId, + timeoutMs = 120_000, + pollIntervalMs = 500, +}: { + autumnV2_2: AutumnV2_2; + ctx: ScenarioCtx; + id: string; + planId: string; + timeoutMs?: number; + pollIntervalMs?: number; +}): Promise<{ migration: Migration; run_id: string }> => { + const migration = await autumnV2_2.migrationsV2.deleteAndCreate( + buildDashboardLazyMigration({ id, planId }), + ); + + const deadline = Date.now() + timeoutMs; + let lastError: unknown; + while (Date.now() < deadline) { + try { + const run = await autumnV2_2.migrationsV2.lazyRun({ id: migration.id }); + return { migration, run_id: run.run_id }; + } catch (error) { + lastError = error; + const code = (error as { code?: string } | undefined)?.code; + if (code !== "migration_already_in_progress") throw error; + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); + } + } + + throw new Error( + `startLazyMigration: timed out after ${timeoutMs}ms waiting for another run to release. last error: ${ + lastError instanceof Error ? lastError.message : String(lastError) + }`, + ); +}; + +/** Fetch the customer and poll until the migration's added feature shows up. + * Used as the standard “fetch + wait for lazy migration to land” primitive. */ +export const getCustomerAndAwaitMigration = async ({ + autumnV2_2, + customerId, + featureId = TestFeature.Dashboard, + timeoutMs = 60_000, + pollIntervalMs = 1_000, +}: { + autumnV2_2: AutumnV2_2; + customerId: string; + featureId?: string; + timeoutMs?: number; + pollIntervalMs?: number; +}): Promise => { + let latest: ApiCustomerV5 | undefined; + await waitForMigrationResult({ + timeoutMs, + pollIntervalMs, + waitFor: async () => { + latest = await autumnV2_2.customers.get(customerId); + expect(latest.flags[featureId]).toBeDefined(); + }, + }); + if (!latest) throw new Error("getCustomerAndAwaitMigration: no response"); + return latest; +}; + +/** Wipe every `migration_runs` row for the test org/env. */ +export const cleanupMigrationRunsForOrg = async ({ + ctx, +}: { + ctx: ScenarioCtx; +}): Promise => { + await ctx.db + .delete(migrationRuns) + .where( + and(eq(migrationRuns.org_id, ctx.org.id), eq(migrationRuns.env, ctx.env)), + ); +}; + +/** Mark a lazy run as done via the real `finishLazyMigrationRun` action and + * bust the org cache. Use this in `finally` blocks so each test cleans up + * ITS OWN run without stepping on concurrent tests. Idempotent — safe to + * call multiple times. */ +export const releaseLazyMigrationRun = async ({ + ctx, + runId, +}: { + ctx: ScenarioCtx; + runId: string; +}): Promise => { + await finishLazyMigrationRun({ ctx, runId }); +}; + +export { MigrationItemRunStatus }; diff --git a/server/tests/integration/billing/migrations-v2/migrations-v2.test.ts b/server/tests/integration/billing/migrations-v2/migrations-v2.test.ts new file mode 100644 index 000000000..e69de29bb diff --git a/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/ensure-prices-and-ents-runtime.test.ts b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/ensure-prices-and-ents-runtime.test.ts new file mode 100644 index 000000000..140455093 --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/ensure-prices-and-ents-runtime.test.ts @@ -0,0 +1,471 @@ +/** + * Runtime coverage for ensure_prices_and_entitlements preparation. + * + * These tests verify that product-version anchored prepared catalog rows can + * feed the customer migration execution path and are reused by customer_prices + * and customer_entitlements. + */ + +import { expect, test } from "bun:test"; +import { TestFeature } from "@tests/setup/v2Features"; +import { items } from "@tests/utils/fixtures/items"; +import { itemsV2 } from "@tests/utils/fixtures/itemsV2"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import { + buildUpdatePlanOperations, + createMigration, + updateMigrationOperations, +} from "../../utils/migrationTestUtils.js"; +import { + expectPreparedArtifact, + expectPreparedArtifactFieldsChanged, + expectPreparedArtifactRowIds, + expectPreparedCatalogContainsRows, + prepareMigration, +} from "./utils/ensurePrepareTestUtils.js"; +import { + expectPreparedRowsAnchoredToProducts, + waitForPreparedRowsReusedByCustomerProducts, + waitForPreparedRowsReusedByCustomers, +} from "./utils/expectPreparedCustomerRows.js"; +import { expectPreparedStripeProductCount } from "./utils/expectPreparedStripeProducts.js"; +import { getPreparedCustomerRows } from "./utils/getPreparedCustomerRows.js"; + +test.concurrent(`${chalk.yellowBright("migrations prepare runtime: prepared product catalog rows are reused across customers")}`, async () => { + const customerId = "prep-ensure-reuse-primary"; + const otherCustomerId = "prep-ensure-reuse-secondary"; + const free = products.base({ + id: "free", + items: [], + }); + + const { autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.otherCustomers([{ id: otherCustomerId, paymentMethod: "success" }]), + s.products({ list: [free] }), + ], + actions: [ + s.billing.attach({ productId: free.id }), + s.billing.attach({ + productId: free.id, + customerId: otherCustomerId, + }), + ], + }); + + const firstOperations = buildUpdatePlanOperations({ + planId: free.id, + customize: { + add_items: [itemsV2.dashboard(), itemsV2.prepaidWords({ amount: 3 })], + }, + }); + const migration = await createMigration({ + migrationClient: autumnV2_2, + id: `${customerId}-mig`, + filter: { customer: { plan: { plan_id: free.id } } }, + operations: firstOperations, + }); + const firstPrepared = await prepareMigration({ + ctx, + migration, + dryRun: true, + }); + expectPreparedArtifact({ + result: firstPrepared, + opIndex: 0, + kind: "add_item", + itemIndex: 1, + }); + + const operations = buildUpdatePlanOperations({ + planId: free.id, + customize: { + add_items: [itemsV2.dashboard(), itemsV2.prepaidWords({ amount: 4 })], + }, + }); + const updatedMigration = await updateMigrationOperations({ + migrationClient: autumnV2_2, + id: `${customerId}-mig`, + operations, + }); + const updatedPrepared = await prepareMigration({ + ctx, + migration: updatedMigration, + dryRun: true, + }); + + expectPreparedArtifactFieldsChanged({ + before: firstPrepared, + after: updatedPrepared, + artifact: { opIndex: 0, kind: "add_item", itemIndex: 1 }, + fields: ["price_id", "entitlement_id"], + }); + + const dashboard = expectPreparedArtifact({ + result: updatedPrepared, + opIndex: 0, + kind: "add_item", + itemIndex: 0, + }); + const priced = expectPreparedArtifact({ + result: updatedPrepared, + opIndex: 0, + kind: "add_item", + itemIndex: 1, + }); + const dashboardEntitlementId = dashboard.entitlement_id; + const pricedRows = expectPreparedArtifactRowIds({ artifact: priced }); + if (!dashboardEntitlementId) { + throw new Error("Expected prepared artifacts to include reusable row IDs"); + } + expectPreparedCatalogContainsRows({ + result: updatedPrepared, + priceIds: [pricedRows.priceId], + entitlementIds: [pricedRows.entitlementId], + }); + + await autumnV2_2.migrationsV2.run({ + id: `${customerId}-mig`, + dry_run: false, + }); + + const loadRows = () => + getPreparedCustomerRows({ + ctx, + customerIds: [customerId, otherCustomerId], + productId: free.id, + }); + + const rows = await waitForPreparedRowsReusedByCustomers({ + loadRows, + customerIds: [customerId, otherCustomerId], + priceId: pricedRows.priceId, + entitlementIds: [dashboardEntitlementId, pricedRows.entitlementId], + }); + + expectPreparedRowsAnchoredToProducts({ + rows, + priceIdToInternalProductId: { + [pricedRows.priceId]: priced.internal_product_id, + }, + entitlementIdToInternalProductId: { + [dashboardEntitlementId]: dashboard.internal_product_id, + [pricedRows.entitlementId]: priced.internal_product_id, + }, + }); + + await expectPreparedStripeProductCount({ + ctx, + priceIds: [pricedRows.priceId], + count: 1, + }); +}); + +test.concurrent(`${chalk.yellowBright("migrations prepare runtime: customer products on different versions use version-specific prepared rows")}`, async () => { + const oldVersionCustomerId = "prep-ensure-version-old"; + const latestVersionCustomerId = "prep-ensure-version-latest"; + const pro = products.pro({ + id: "pro", + items: [items.prepaidMessages({ price: 3 })], + }); + + const { autumnV1, autumnV2_2, ctx } = await initScenario({ + customerId: oldVersionCustomerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.otherCustomers([ + { id: latestVersionCustomerId, paymentMethod: "success" }, + ]), + s.products({ list: [pro] }), + ], + actions: [s.billing.attach({ productId: pro.id })], + }); + + await autumnV1.products.update(pro.id, { + items: [items.prepaidMessages({ price: 3 }), items.dashboard()], + }); + + await autumnV2_2.billing.attach({ + customer_id: latestVersionCustomerId, + plan_id: pro.id, + }); + + const customerProductRows = await getPreparedCustomerRows({ + ctx, + customerIds: [oldVersionCustomerId, latestVersionCustomerId], + productId: pro.id, + }); + const oldVersionInternalProductId = customerProductRows.find( + (row) => row.customerId === oldVersionCustomerId, + )?.customerProductInternalProductId; + const latestVersionInternalProductId = customerProductRows.find( + (row) => row.customerId === latestVersionCustomerId, + )?.customerProductInternalProductId; + if (!oldVersionInternalProductId || !latestVersionInternalProductId) { + throw new Error("Expected both customers to have a customer product"); + } + expect(latestVersionInternalProductId).not.toBe(oldVersionInternalProductId); + + const operations = buildUpdatePlanOperations({ + planId: pro.id, + customize: { + remove_items: [{ feature_id: TestFeature.Messages }], + add_items: [itemsV2.prepaidMessages({ amount: 6 })], + }, + }); + const migration = await createMigration({ + migrationClient: autumnV2_2, + id: `${oldVersionCustomerId}-mig`, + filter: { customer: { plan: { plan_id: pro.id } } }, + operations, + }); + const prepared = await prepareMigration({ + ctx, + migration, + dryRun: true, + }); + const oldVersionArtifact = expectPreparedArtifact({ + result: prepared, + opIndex: 0, + kind: "add_item", + itemIndex: 0, + internalProductId: oldVersionInternalProductId, + }); + const latestVersionArtifact = expectPreparedArtifact({ + result: prepared, + opIndex: 0, + kind: "add_item", + itemIndex: 0, + internalProductId: latestVersionInternalProductId, + }); + const oldVersionRows = expectPreparedArtifactRowIds({ + artifact: oldVersionArtifact, + }); + const latestVersionRows = expectPreparedArtifactRowIds({ + artifact: latestVersionArtifact, + }); + expect(oldVersionRows.priceId).not.toBe(latestVersionRows.priceId); + expect(oldVersionRows.entitlementId).not.toBe( + latestVersionRows.entitlementId, + ); + + await autumnV2_2.migrationsV2.run({ + id: `${oldVersionCustomerId}-mig`, + dry_run: false, + }); + + const loadRows = () => + getPreparedCustomerRows({ + ctx, + customerIds: [oldVersionCustomerId, latestVersionCustomerId], + productId: pro.id, + }); + + const rows = await waitForPreparedRowsReusedByCustomerProducts({ + loadRows, + expected: [ + { + customerId: oldVersionCustomerId, + customerProductInternalProductId: oldVersionInternalProductId, + priceId: oldVersionRows.priceId, + entitlementIds: [oldVersionRows.entitlementId], + }, + { + customerId: latestVersionCustomerId, + customerProductInternalProductId: latestVersionInternalProductId, + priceId: latestVersionRows.priceId, + entitlementIds: [latestVersionRows.entitlementId], + }, + ], + }); + + expectPreparedRowsAnchoredToProducts({ + rows, + priceIdToInternalProductId: { + [oldVersionRows.priceId]: oldVersionInternalProductId, + [latestVersionRows.priceId]: latestVersionInternalProductId, + }, + entitlementIdToInternalProductId: { + [oldVersionRows.entitlementId]: oldVersionInternalProductId, + [latestVersionRows.entitlementId]: latestVersionInternalProductId, + }, + }); + + await expectPreparedStripeProductCount({ + ctx, + priceIds: [oldVersionRows.priceId, latestVersionRows.priceId], + count: 1, + }); +}); + +test.concurrent(`${chalk.yellowBright("migrations prepare runtime: multi-plan op reuses prepared rows across customers and entities")}`, async () => { + const customerId = "prep-ensure-multi-plan-primary"; + const otherCustomerId = "prep-ensure-multi-plan-secondary"; + const pro = products.pro({ items: [] }); + const premium = products.premium({ items: [] }); + + const { autumnV1, autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.otherCustomers([{ id: otherCustomerId, paymentMethod: "success" }]), + s.products({ list: [pro, premium] }), + s.entities({ count: 2, featureId: TestFeature.Users }), + ], + actions: [], + }); + + await autumnV1.entities.create(otherCustomerId, [ + { id: "ent-1", name: "Entity 1", feature_id: TestFeature.Users }, + { id: "ent-2", name: "Entity 2", feature_id: TestFeature.Users }, + ]); + + for (const targetCustomerId of [customerId, otherCustomerId]) { + await autumnV2_2.billing.attach({ + customer_id: targetCustomerId, + plan_id: pro.id, + entity_id: "ent-1", + }); + await autumnV2_2.billing.attach({ + customer_id: targetCustomerId, + plan_id: premium.id, + entity_id: "ent-2", + }); + } + + const customerProductRows = await getPreparedCustomerRows({ + ctx, + customerIds: [customerId, otherCustomerId], + productIds: [pro.id, premium.id], + }); + const proInternalProductId = customerProductRows.find( + (row) => row.customerProductProductId === pro.id, + )?.customerProductInternalProductId; + const premiumInternalProductId = customerProductRows.find( + (row) => row.customerProductProductId === premium.id, + )?.customerProductInternalProductId; + if (!proInternalProductId || !premiumInternalProductId) { + throw new Error("Expected pro and premium customer products"); + } + expect(proInternalProductId).not.toBe(premiumInternalProductId); + + const operations = { + customer: [ + { + type: "update_plan" as const, + plan_filter: { + $or: [{ plan_id: pro.id }, { plan_id: premium.id }], + }, + customize: { + add_items: [itemsV2.prepaidMessages({ amount: 6 })], + }, + }, + ], + }; + const migration = await createMigration({ + migrationClient: autumnV2_2, + id: `${customerId}-mig`, + filter: { + customer: { + plan: { $or: [{ plan_id: pro.id }, { plan_id: premium.id }] }, + }, + }, + operations, + }); + const prepared = await prepareMigration({ + ctx, + migration, + dryRun: true, + }); + const proArtifact = expectPreparedArtifact({ + result: prepared, + opIndex: 0, + kind: "add_item", + itemIndex: 0, + internalProductId: proInternalProductId, + }); + const premiumArtifact = expectPreparedArtifact({ + result: prepared, + opIndex: 0, + kind: "add_item", + itemIndex: 0, + internalProductId: premiumInternalProductId, + }); + const proRows = expectPreparedArtifactRowIds({ artifact: proArtifact }); + const premiumRows = expectPreparedArtifactRowIds({ + artifact: premiumArtifact, + }); + expect(proRows.priceId).not.toBe(premiumRows.priceId); + expect(proRows.entitlementId).not.toBe(premiumRows.entitlementId); + + await autumnV2_2.migrationsV2.run({ + id: `${customerId}-mig`, + dry_run: false, + }); + + const loadRows = () => + getPreparedCustomerRows({ + ctx, + customerIds: [customerId, otherCustomerId], + productIds: [pro.id, premium.id], + }); + const rows = await waitForPreparedRowsReusedByCustomerProducts({ + loadRows, + expected: [ + { + customerId, + productId: pro.id, + entityId: "ent-1", + customerProductInternalProductId: proInternalProductId, + priceId: proRows.priceId, + entitlementIds: [proRows.entitlementId], + }, + { + customerId, + productId: premium.id, + entityId: "ent-2", + customerProductInternalProductId: premiumInternalProductId, + priceId: premiumRows.priceId, + entitlementIds: [premiumRows.entitlementId], + }, + { + customerId: otherCustomerId, + productId: pro.id, + entityId: "ent-1", + customerProductInternalProductId: proInternalProductId, + priceId: proRows.priceId, + entitlementIds: [proRows.entitlementId], + }, + { + customerId: otherCustomerId, + productId: premium.id, + entityId: "ent-2", + customerProductInternalProductId: premiumInternalProductId, + priceId: premiumRows.priceId, + entitlementIds: [premiumRows.entitlementId], + }, + ], + }); + + expectPreparedRowsAnchoredToProducts({ + rows, + priceIdToInternalProductId: { + [proRows.priceId]: proInternalProductId, + [premiumRows.priceId]: premiumInternalProductId, + }, + entitlementIdToInternalProductId: { + [proRows.entitlementId]: proInternalProductId, + [premiumRows.entitlementId]: premiumInternalProductId, + }, + }); + + await expectPreparedStripeProductCount({ + ctx, + priceIds: [proRows.priceId, premiumRows.priceId], + count: 2, + }); +}); diff --git a/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/ensure-prices-and-ents-scope.test.ts b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/ensure-prices-and-ents-scope.test.ts new file mode 100644 index 000000000..444c02bd8 --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/ensure-prices-and-ents-scope.test.ts @@ -0,0 +1,205 @@ +/** + * Scope coverage for ensure_prices_and_entitlements preparation. + * + * Contract under test: + * - plan filters prepare catalog rows only for matched products. + * - all matched product versions get distinct prepared rows. + * - unmatched products do not receive prepared price / entitlement pairs. + */ + +import { expect, test } from "bun:test"; +import { prepare } from "@/internal/migrations/v2/prepare/prepare.js"; +import { ProductService } from "@/internal/products/ProductService.js"; +import { itemsV2 } from "@tests/utils/fixtures/itemsV2"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import { + buildUpdatePlanOperations, + createMigration, +} from "../../utils/migrationTestUtils.js"; +import { prepareMigration } from "./utils/ensurePrepareTestUtils.js"; + +test.concurrent(`${chalk.yellowBright("migrations prepare: plan filter prepares every matched product version")}`, async () => { + const id = "prep-ensure-all-product-versions"; + const pro = products.pro({ items: [] }); + const { autumnV1, autumnV2_2, ctx } = await initScenario({ + customerId: id, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [s.billing.attach({ productId: pro.id })], + }); + + await autumnV1.products.update(pro.id, { + items: [itemsV2.dashboard()], + }); + + const migration = await createMigration({ + migrationClient: autumnV2_2, + id, + operations: buildUpdatePlanOperations({ + planId: pro.id, + customize: { add_items: [itemsV2.prepaidWords({ amount: 6 })] }, + }), + }); + const prepared = await prepareMigration({ ctx, migration }); + const artifacts = prepared.result.artifacts.filter( + (artifact) => + artifact.op_index === 0 && + artifact.kind === "add_item" && + artifact.item_index === 0, + ); + + expect(artifacts).toHaveLength(2); + expect( + new Set(artifacts.map((artifact) => artifact.internal_product_id)).size, + ).toBe(2); + expect(new Set(artifacts.map((artifact) => artifact.price_id)).size).toBe(2); + expect( + new Set(artifacts.map((artifact) => artifact.entitlement_id)).size, + ).toBe(2); +}); + +test.concurrent(`${chalk.yellowBright("migrations prepare: one update_plan op prepares every matched plan")}`, async () => { + const id = "prep-ensure-multi-plan-op"; + const pro = products.pro({ items: [] }); + const premium = products.premium({ items: [] }); + const { autumnV2_2, ctx } = await initScenario({ + setup: [s.products({ list: [pro, premium] })], + actions: [], + }); + + const migration = await createMigration({ + migrationClient: autumnV2_2, + id, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { + $or: [{ plan_id: pro.id }, { plan_id: premium.id }], + }, + customize: { + add_items: [itemsV2.prepaidMessages({ amount: 6 })], + }, + }, + ], + }, + }); + const prepared = await prepareMigration({ ctx, migration, dryRun: true }); + const artifacts = prepared.result.artifacts.filter( + (artifact) => + artifact.op_index === 0 && + artifact.kind === "add_item" && + artifact.item_index === 0, + ); + + expect(artifacts).toHaveLength(2); + expect( + new Set(artifacts.map((artifact) => artifact.internal_product_id)).size, + ).toBe(2); + expect(new Set(artifacts.map((artifact) => artifact.price_id)).size).toBe(2); + expect( + new Set(artifacts.map((artifact) => artifact.entitlement_id)).size, + ).toBe(2); +}); + +test.concurrent(`${chalk.yellowBright("migrations prepare: unmatched plans do not get prepared catalog rows")}`, async () => { + const id = "prep-ensure-unmatched-plan-skipped"; + const pro = products.pro({ items: [] }); + const premium = products.premium({ items: [] }); + const growth = products.growth({ items: [] }); + const { autumnV2_2, ctx } = await initScenario({ + setup: [s.products({ list: [pro, premium, growth] })], + actions: [], + }); + + const fullProducts = await ProductService.listFull({ + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + returnAll: true, + inIds: [pro.id, premium.id, growth.id], + }); + const growthProduct = fullProducts.find( + (product) => product.id === growth.id, + ); + if (!growthProduct) { + throw new Error("Expected growth product to exist"); + } + + const migration = await createMigration({ + migrationClient: autumnV2_2, + id, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { + $or: [{ plan_id: pro.id }, { plan_id: premium.id }], + }, + customize: { + add_items: [itemsV2.prepaidMessages({ amount: 6 })], + }, + }, + ], + }, + }); + const prepared = await prepareMigration({ ctx, migration, dryRun: true }); + + const addItemArtifacts = prepared.result.artifacts.filter( + (artifact) => + artifact.op_index === 0 && + artifact.kind === "add_item" && + artifact.item_index === 0, + ); + expect(addItemArtifacts).toHaveLength(2); + expect( + addItemArtifacts.some( + (artifact) => artifact.internal_product_id === growthProduct.internal_id, + ), + ).toBe(false); + expect( + prepared.result.prices.some( + (price) => price.internal_product_id === growthProduct.internal_id, + ), + ).toBe(false); + expect( + prepared.result.entitlements.some( + (entitlement) => + entitlement.internal_product_id === growthProduct.internal_id, + ), + ).toBe(false); +}); + +test.concurrent(`${chalk.yellowBright("migrations prepare: version-only update_plan has no output")}`, async () => { + const id = "prep-ensure-version-only-no-output"; + const pro = products.pro({ items: [] }); + const { autumnV2_2, ctx } = await initScenario({ + setup: [s.products({ list: [pro] })], + actions: [], + }); + + const migration = await createMigration({ + migrationClient: autumnV2_2, + id, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: pro.id }, + version: 2, + }, + ], + }, + }); + const { preparedState } = await prepare({ + ctx, + migration, + dryRun: true, + }); + + expect(preparedState).toEqual({}); +}); diff --git a/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/ensure-prices-and-ents.test.ts b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/ensure-prices-and-ents.test.ts new file mode 100644 index 000000000..bfae48b6f --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/ensure-prices-and-ents.test.ts @@ -0,0 +1,279 @@ +/** + * TDD coverage for ensure_prices_and_entitlements preparation. + * + * Contract under test: + * - prepared catalog rows are content-addressed by the exact update_plan input, + * operation position, and matched product version. + * - unchanged add_items keep their prepared row IDs across migration edits. + * - changed add_items/base prices receive new prepared row IDs. + */ + +import { expect, test } from "bun:test"; +import type { Price } from "@autumn/shared"; +import { prepare } from "@/internal/migrations/v2/prepare/prepare.js"; +import { itemsV2 } from "@tests/utils/fixtures/itemsV2"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import { + buildUpdatePlanOperations, + createMigration, + updateMigrationOperations, +} from "../../utils/migrationTestUtils.js"; +import { + prepaidWordsWithMaxPurchase, + rolloverCredits, +} from "./utils/ensurePrepareItems.js"; +import { + expectPreparedArtifact, + expectPreparedArtifactFieldsChanged, + expectPreparedArtifactFieldsStable, + expectPreparedArtifactRowIds, + expectPreparedCatalogContainsRows, + prepareMigration, +} from "./utils/ensurePrepareTestUtils.js"; + +test.concurrent(`${chalk.yellowBright("migrations prepare: unchanged add_items keep row IDs while edited add_items change")}`, async () => { + const id = "prep-ensure-stable-add-items"; + const pro = products.pro({ items: [] }); + const { autumnV2_2, ctx } = await initScenario({ + setup: [s.products({ list: [pro] })], + actions: [], + }); + + const firstOps = buildUpdatePlanOperations({ + planId: pro.id, + customize: { + add_items: [itemsV2.dashboard(), itemsV2.prepaidWords({ amount: 2 })], + }, + }); + const migration = await createMigration({ + migrationClient: autumnV2_2, + id, + operations: firstOps, + }); + const first = await prepareMigration({ ctx, migration, dryRun: true }); + + const secondOps = buildUpdatePlanOperations({ + planId: pro.id, + customize: { + add_items: [itemsV2.dashboard(), itemsV2.prepaidWords({ amount: 3 })], + }, + }); + const updatedMigration = await updateMigrationOperations({ + migrationClient: autumnV2_2, + id, + operations: secondOps, + }); + expect(updatedMigration.prepared_state).toBeNull(); + const second = await prepareMigration({ + ctx, + migration: updatedMigration, + dryRun: true, + }); + + expectPreparedArtifactFieldsStable({ + before: first, + after: second, + artifact: { opIndex: 0, kind: "add_item", itemIndex: 0 }, + fields: ["entitlement_id"], + }); + + expectPreparedArtifactFieldsChanged({ + before: first, + after: second, + artifact: { opIndex: 0, kind: "add_item", itemIndex: 1 }, + fields: ["hash", "price_id", "entitlement_id"], + }); +}); + +test.concurrent(`${chalk.yellowBright("migrations prepare: base price create update and remove rewrite prepared_state")}`, async () => { + const id = "prep-ensure-base-price"; + const pro = products.pro({ items: [] }); + const { autumnV2_2, ctx } = await initScenario({ + setup: [s.products({ list: [pro] })], + actions: [], + }); + + const createOps = buildUpdatePlanOperations({ + planId: pro.id, + customize: { price: itemsV2.monthlyPrice({ amount: 20 }) }, + }); + const migration = await createMigration({ + migrationClient: autumnV2_2, + id, + operations: createOps, + }); + const created = await prepareMigration({ ctx, migration, dryRun: true }); + const createdBasePrice = expectPreparedArtifact({ + result: created, + opIndex: 0, + kind: "base_price", + }); + expect(created.result.prices).toHaveLength(1); + expect((created.result.prices[0] as Price).internal_product_id).toBe( + createdBasePrice.internal_product_id, + ); + + const updateOps = buildUpdatePlanOperations({ + planId: pro.id, + customize: { price: itemsV2.monthlyPrice({ amount: 25 }) }, + }); + const updatedMigration = await updateMigrationOperations({ + migrationClient: autumnV2_2, + id, + operations: updateOps, + }); + const updated = await prepareMigration({ + ctx, + migration: updatedMigration, + dryRun: true, + }); + expectPreparedArtifactFieldsChanged({ + before: created, + after: updated, + artifact: { opIndex: 0, kind: "base_price" }, + fields: ["hash", "price_id"], + }); + expect(createdBasePrice.price_id).toBeDefined(); + + const removeOps = buildUpdatePlanOperations({ + planId: pro.id, + customize: { price: null }, + }); + const removedMigration = await updateMigrationOperations({ + migrationClient: autumnV2_2, + id, + operations: removeOps, + }); + const { preparedState: removedState } = await prepare({ + ctx, + migration: removedMigration, + dryRun: false, + }); + expect(removedState).toEqual({}); +}); + +test.concurrent(`${chalk.yellowBright("migrations prepare: nested item field changes produce new artifacts")}`, async () => { + const id = "prep-ensure-nested-item-hash"; + const pro = products.pro({ items: [] }); + const { autumnV2_2, ctx } = await initScenario({ + setup: [s.products({ list: [pro] })], + actions: [], + }); + + const migration = await createMigration({ + migrationClient: autumnV2_2, + id, + operations: buildUpdatePlanOperations({ + planId: pro.id, + customize: { + add_items: [prepaidWordsWithMaxPurchase({ maxPurchase: 100 })], + }, + }), + }); + const first = await prepareMigration({ ctx, migration, dryRun: true }); + + const maxPurchaseMigration = await updateMigrationOperations({ + migrationClient: autumnV2_2, + id, + operations: buildUpdatePlanOperations({ + planId: pro.id, + customize: { + add_items: [prepaidWordsWithMaxPurchase({ maxPurchase: 101 })], + }, + }), + }); + const maxPurchase = await prepareMigration({ + ctx, + migration: maxPurchaseMigration, + dryRun: true, + }); + expectPreparedArtifactFieldsChanged({ + before: first, + after: maxPurchase, + artifact: { opIndex: 0, kind: "add_item", itemIndex: 0 }, + fields: ["hash", "price_id", "entitlement_id"], + }); + + const rolloverMigration = await updateMigrationOperations({ + migrationClient: autumnV2_2, + id, + operations: buildUpdatePlanOperations({ + planId: pro.id, + customize: { add_items: [rolloverCredits({ max: 250 })] }, + }), + }); + const rollover = await prepareMigration({ + ctx, + migration: rolloverMigration, + dryRun: true, + }); + + const rolloverChangedMigration = await updateMigrationOperations({ + migrationClient: autumnV2_2, + id, + operations: buildUpdatePlanOperations({ + planId: pro.id, + customize: { add_items: [rolloverCredits({ max: 251 })] }, + }), + }); + const rolloverChanged = await prepareMigration({ + ctx, + migration: rolloverChangedMigration, + dryRun: true, + }); + expectPreparedArtifactFieldsChanged({ + before: rollover, + after: rolloverChanged, + artifact: { opIndex: 0, kind: "add_item", itemIndex: 0 }, + fields: ["hash", "entitlement_id"], + }); +}); + +test.concurrent(`${chalk.yellowBright("migrations prepare: same item on different op indexes gets distinct rows")}`, async () => { + const id = "prep-ensure-op-index-isolation"; + const pro = products.pro({ items: [] }); + const premium = products.premium({ items: [] }); + const { autumnV2_2, ctx } = await initScenario({ + setup: [s.products({ list: [pro, premium] })], + actions: [], + }); + + const migration = await createMigration({ + migrationClient: autumnV2_2, + id, + operations: buildUpdatePlanOperations({ + planId: pro.id, + customize: { add_items: [itemsV2.prepaidWords({ amount: 6 })] }, + secondPlanId: premium.id, + secondCustomize: { + add_items: [itemsV2.prepaidWords({ amount: 6 })], + }, + }), + }); + const prepared = await prepareMigration({ ctx, migration, dryRun: true }); + + const first = expectPreparedArtifact({ + result: prepared, + opIndex: 0, + kind: "add_item", + itemIndex: 0, + }); + const second = expectPreparedArtifact({ + result: prepared, + opIndex: 1, + kind: "add_item", + itemIndex: 0, + }); + expect(second.hash).toBe(first.hash); + expect(second.price_id).not.toBe(first.price_id); + expect(second.entitlement_id).not.toBe(first.entitlement_id); + const firstRows = expectPreparedArtifactRowIds({ artifact: first }); + const secondRows = expectPreparedArtifactRowIds({ artifact: second }); + expectPreparedCatalogContainsRows({ + result: prepared, + priceIds: [firstRows.priceId, secondRows.priceId], + entitlementIds: [firstRows.entitlementId, secondRows.entitlementId], + }); +}); diff --git a/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/utils/ensurePrepareItems.ts b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/utils/ensurePrepareItems.ts new file mode 100644 index 000000000..882c2821a --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/utils/ensurePrepareItems.ts @@ -0,0 +1,29 @@ +import { ResetInterval, RolloverExpiryDurationType } from "@autumn/shared"; +import { TestFeature } from "@tests/setup/v2Features"; +import { itemsV2 } from "@tests/utils/fixtures/itemsV2"; + +export const prepaidWordsWithMaxPurchase = ({ + maxPurchase, +}: { + maxPurchase: number; +}) => { + const item = itemsV2.prepaidWords(); + return { + ...item, + price: { + ...item.price, + max_purchase: maxPurchase, + }, + }; +}; + +export const rolloverCredits = ({ max = 100 }: { max?: number } = {}) => ({ + feature_id: TestFeature.Credits, + included: 50, + reset: { interval: ResetInterval.Month }, + rollover: { + max, + expiry_duration_type: RolloverExpiryDurationType.Month, + expiry_duration_length: 2, + }, +}); diff --git a/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/utils/ensurePrepareTestUtils.ts b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/utils/ensurePrepareTestUtils.ts new file mode 100644 index 000000000..8df739be6 --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/utils/ensurePrepareTestUtils.ts @@ -0,0 +1,171 @@ +import { expect } from "bun:test"; +import type { Migration } from "@autumn/shared"; +import { + EnsurePricesAndEntitlementsResultSchema, + type PreparedArtifactRef, + type EnsurePricesAndEntitlementsResult, +} from "@/internal/migrations/v2/prepare/modules/ensurePricesAndEntitlements/index.js"; +import { prepare } from "@/internal/migrations/v2/prepare/prepare.js"; +import type { PreparedState } from "@/internal/migrations/v2/prepare/types/index.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; + +const prepareKey = "ensure_prices_and_entitlements:update_plan"; + +export type PreparedMigrationResult = { + preparedState: PreparedState; + result: EnsurePricesAndEntitlementsResult; +}; + +type PreparedResultLike = + | EnsurePricesAndEntitlementsResult + | PreparedMigrationResult; + +type PreparedArtifactSelector = { + opIndex: number; + kind: "base_price" | "add_item"; + itemIndex?: number; + internalProductId?: string; +}; + +type PreparedArtifactField = + | "hash" + | "price_id" + | "entitlement_id" + | "internal_product_id"; + +const toResult = ({ + prepared, +}: { + prepared: PreparedResultLike; +}): EnsurePricesAndEntitlementsResult => + "artifacts" in prepared ? prepared : prepared.result; + +export const extractEnsureResult = ({ + preparedState, +}: { + preparedState: unknown; +}): EnsurePricesAndEntitlementsResult => { + const state = preparedState as Record; + return EnsurePricesAndEntitlementsResultSchema.parse(state[prepareKey]); +}; + +export const expectPreparedArtifact = ({ + result, + opIndex, + kind, + itemIndex, + internalProductId, +}: { + result: PreparedResultLike; +} & PreparedArtifactSelector): PreparedArtifactRef => { + const preparedResult = toResult({ prepared: result }); + const artifact = preparedResult.artifacts.find( + (candidate) => + candidate.op_index === opIndex && + candidate.kind === kind && + candidate.item_index === itemIndex && + (internalProductId === undefined || + candidate.internal_product_id === internalProductId), + ); + expect(artifact).toBeDefined(); + return artifact!; +}; + +export const expectPreparedArtifactFieldsStable = ({ + before, + after, + artifact, + fields, +}: { + before: PreparedResultLike; + after: PreparedResultLike; + artifact: PreparedArtifactSelector; + fields: PreparedArtifactField[]; +}) => { + const beforeArtifact = expectPreparedArtifact({ + result: before, + ...artifact, + }); + const afterArtifact = expectPreparedArtifact({ result: after, ...artifact }); + + for (const field of fields) { + expect(afterArtifact[field]).toBe(beforeArtifact[field]); + } +}; + +export const expectPreparedArtifactFieldsChanged = ({ + before, + after, + artifact, + fields, +}: { + before: PreparedResultLike; + after: PreparedResultLike; + artifact: PreparedArtifactSelector; + fields: PreparedArtifactField[]; +}) => { + const beforeArtifact = expectPreparedArtifact({ + result: before, + ...artifact, + }); + const afterArtifact = expectPreparedArtifact({ result: after, ...artifact }); + + for (const field of fields) { + expect(afterArtifact[field]).not.toBe(beforeArtifact[field]); + } +}; + +export const expectPreparedArtifactRowIds = ({ + artifact, +}: { + artifact: PreparedArtifactRef; +}) => { + const priceId = artifact.price_id; + const entitlementId = artifact.entitlement_id; + if (!priceId || !entitlementId) { + throw new Error( + "Expected prepared artifact to include price and entitlement IDs", + ); + } + + return { priceId, entitlementId }; +}; + +export const expectPreparedCatalogContainsRows = ({ + result, + priceIds = [], + entitlementIds = [], +}: { + result: PreparedResultLike; + priceIds?: string[]; + entitlementIds?: string[]; +}) => { + const preparedResult = toResult({ prepared: result }); + const preparedPriceIds = preparedResult.prices.map((price) => price.id); + const preparedEntitlementIds = preparedResult.entitlements.map( + (entitlement) => entitlement.id, + ); + + for (const priceId of priceIds) { + expect(preparedPriceIds).toContain(priceId); + } + for (const entitlementId of entitlementIds) { + expect(preparedEntitlementIds).toContain(entitlementId); + } +}; + +export const prepareMigration = async ({ + ctx, + migration, + dryRun = false, +}: { + ctx: AutumnContext; + migration: Migration; + dryRun?: boolean; +}): Promise => { + const { preparedState } = await prepare({ ctx, migration, dryRun }); + return { + preparedState, + result: extractEnsureResult({ preparedState }), + }; +}; diff --git a/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/utils/expectPreparedCustomerRows.ts b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/utils/expectPreparedCustomerRows.ts new file mode 100644 index 000000000..c8e1a8035 --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/utils/expectPreparedCustomerRows.ts @@ -0,0 +1,183 @@ +import { expect } from "bun:test"; +import { waitForMigrationResult } from "../../../utils/runUpdatePlanMigration.js"; +import type { getPreparedCustomerRows } from "./getPreparedCustomerRows.js"; + +type PreparedCustomerRows = Awaited>; + +export const expectPreparedRowsReusedByCustomers = ({ + rows, + customerIds, + priceId, + entitlementIds, +}: { + rows: PreparedCustomerRows; + customerIds: string[]; + priceId: string; + entitlementIds: string[]; +}) => { + const migratedCustomerIds = new Set(rows.map((row) => row.customerId)); + expect(migratedCustomerIds).toEqual(new Set(customerIds)); + + for (const customerId of customerIds) { + const customerRows = rows.filter((row) => row.customerId === customerId); + + expect( + customerRows.some((row) => row.priceId === priceId), + `expected customer ${customerId} to reuse prepared price ${priceId}`, + ).toBe(true); + + for (const entitlementId of entitlementIds) { + expect( + customerRows.some((row) => row.entitlementId === entitlementId), + `expected customer ${customerId} to reuse prepared entitlement ${entitlementId}`, + ).toBe(true); + } + } +}; + +export const waitForPreparedRowsReusedByCustomers = async ({ + loadRows, + customerIds, + priceId, + entitlementIds, + timeoutMs = 60_000, + pollIntervalMs = 1_000, +}: { + loadRows: () => Promise; + customerIds: string[]; + priceId: string; + entitlementIds: string[]; + timeoutMs?: number; + pollIntervalMs?: number; +}) => { + await waitForMigrationResult({ + timeoutMs, + pollIntervalMs, + waitFor: async () => { + const rows = await loadRows(); + expectPreparedRowsReusedByCustomers({ + rows, + customerIds, + priceId, + entitlementIds, + }); + }, + }); + + return loadRows(); +}; + +type PreparedCustomerProductExpectation = { + customerId: string; + productId?: string; + entityId?: string; + customerProductInternalProductId?: string; + priceId: string; + entitlementIds: string[]; +}; + +export const expectPreparedRowsReusedByCustomerProducts = ({ + rows, + expected, +}: { + rows: PreparedCustomerRows; + expected: PreparedCustomerProductExpectation[]; +}) => { + const migratedCustomerIds = new Set(rows.map((row) => row.customerId)); + expect(migratedCustomerIds).toEqual( + new Set(expected.map((row) => row.customerId)), + ); + + for (const rowExpectation of expected) { + const customerRows = rows.filter( + (row) => + row.customerId === rowExpectation.customerId && + (rowExpectation.productId === undefined || + row.customerProductProductId === rowExpectation.productId) && + (rowExpectation.entityId === undefined || + row.customerProductEntityId === rowExpectation.entityId), + ); + + if (rowExpectation.customerProductInternalProductId) { + expect( + customerRows.some( + (row) => + row.customerProductInternalProductId === + rowExpectation.customerProductInternalProductId, + ), + `expected customer ${rowExpectation.customerId} to have customer product ${rowExpectation.customerProductInternalProductId}`, + ).toBe(true); + } + + expect( + customerRows.some((row) => row.priceId === rowExpectation.priceId), + `expected customer ${rowExpectation.customerId} to reuse prepared price ${rowExpectation.priceId}`, + ).toBe(true); + + for (const entitlementId of rowExpectation.entitlementIds) { + expect( + customerRows.some((row) => row.entitlementId === entitlementId), + `expected customer ${rowExpectation.customerId} to reuse prepared entitlement ${entitlementId}`, + ).toBe(true); + } + } +}; + +export const waitForPreparedRowsReusedByCustomerProducts = async ({ + loadRows, + expected, + timeoutMs = 60_000, + pollIntervalMs = 1_000, +}: { + loadRows: () => Promise; + expected: PreparedCustomerProductExpectation[]; + timeoutMs?: number; + pollIntervalMs?: number; +}) => { + await waitForMigrationResult({ + timeoutMs, + pollIntervalMs, + waitFor: async () => { + const rows = await loadRows(); + expectPreparedRowsReusedByCustomerProducts({ rows, expected }); + }, + }); + + return loadRows(); +}; + +export const expectPreparedRowsAnchoredToProducts = ({ + rows, + priceIdToInternalProductId = {}, + entitlementIdToInternalProductId = {}, +}: { + rows: PreparedCustomerRows; + priceIdToInternalProductId?: Record; + entitlementIdToInternalProductId?: Record; +}) => { + for (const [priceId, internalProductId] of Object.entries( + priceIdToInternalProductId, + )) { + const preparedPriceRows = rows.filter((row) => row.priceId === priceId); + expect(preparedPriceRows.length).toBeGreaterThan(0); + expect( + preparedPriceRows.every( + (row) => row.priceInternalProductId === internalProductId, + ), + ).toBe(true); + } + + for (const [entitlementId, internalProductId] of Object.entries( + entitlementIdToInternalProductId, + )) { + const preparedEntitlementRows = rows.filter( + (row) => row.entitlementId === entitlementId, + ); + expect(preparedEntitlementRows.length).toBeGreaterThan(0); + expect( + preparedEntitlementRows.every( + (row) => row.entitlementInternalProductId === internalProductId, + ), + ).toBe(true); + } +}; diff --git a/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/utils/expectPreparedStripeProducts.ts b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/utils/expectPreparedStripeProducts.ts new file mode 100644 index 000000000..521a35c9f --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/utils/expectPreparedStripeProducts.ts @@ -0,0 +1,49 @@ +import { expect } from "bun:test"; +import { prices } from "@autumn/shared"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { and, eq, inArray } from "drizzle-orm"; + +type PriceConfigWithStripeProduct = { + stripe_product_id?: string | null; +}; + +export const expectPreparedStripeProductCount = async ({ + ctx, + priceIds, + count, +}: { + ctx: AutumnContext; + priceIds: string[]; + count: number; +}) => { + const rows = await ctx.db + .select({ + id: prices.id, + config: prices.config, + }) + .from(prices) + .where(and(eq(prices.org_id, ctx.org.id), inArray(prices.id, priceIds))); + + expect(rows.length).toBe(priceIds.length); + + const stripeProductIds = new Set( + rows + .map( + (row) => (row.config as PriceConfigWithStripeProduct).stripe_product_id, + ) + .filter((stripeProductId): stripeProductId is string => + Boolean(stripeProductId), + ), + ); + + expect(stripeProductIds.size).toBe(count); + + const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env }); + for (const stripeProductId of stripeProductIds) { + const stripeProduct = await stripeCli.products.retrieve(stripeProductId); + expect(stripeProduct.id).toBe(stripeProductId); + } + + return stripeProductIds; +}; diff --git a/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/utils/getPreparedCustomerRows.ts b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/utils/getPreparedCustomerRows.ts new file mode 100644 index 000000000..36b28cf4a --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/prepare/ensure-prices-and-ents/utils/getPreparedCustomerRows.ts @@ -0,0 +1,68 @@ +import { + customerEntitlements, + customerPrices, + customerProducts, + customers, + entitlements, + prices, +} from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { and, eq, inArray } from "drizzle-orm"; + +export const getPreparedCustomerRows = async ({ + ctx, + customerIds, + productId, + productIds, +}: { + ctx: AutumnContext; + customerIds: string[]; + productId?: string; + productIds?: string[]; +}) => { + const productIdsToMatch = productIds ?? (productId ? [productId] : []); + if (productIdsToMatch.length === 0) { + throw new Error( + "getPreparedCustomerRows: productId or productIds required", + ); + } + + return ctx.db + .select({ + customerId: customers.id, + customerProductId: customerProducts.id, + customerProductInternalProductId: customerProducts.internal_product_id, + customerProductProductId: customerProducts.product_id, + customerProductEntityId: customerProducts.entity_id, + priceId: customerPrices.price_id, + priceInternalProductId: prices.internal_product_id, + entitlementId: customerEntitlements.entitlement_id, + entitlementInternalProductId: entitlements.internal_product_id, + }) + .from(customers) + .innerJoin( + customerProducts, + eq(customerProducts.internal_customer_id, customers.internal_id), + ) + .leftJoin( + customerPrices, + eq(customerPrices.customer_product_id, customerProducts.id), + ) + .leftJoin(prices, eq(customerPrices.price_id, prices.id)) + .leftJoin( + customerEntitlements, + eq(customerEntitlements.customer_product_id, customerProducts.id), + ) + .leftJoin( + entitlements, + eq(customerEntitlements.entitlement_id, entitlements.id), + ) + .where( + and( + eq(customers.org_id, ctx.org.id), + eq(customers.env, ctx.env), + inArray(customers.id, customerIds), + inArray(customerProducts.product_id, productIdsToMatch), + ), + ); +}; diff --git a/server/tests/integration/billing/migrations-v2/preview/migrate-customer-preview.test.ts b/server/tests/integration/billing/migrations-v2/preview/migrate-customer-preview.test.ts new file mode 100644 index 000000000..36bda2cf7 --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/preview/migrate-customer-preview.test.ts @@ -0,0 +1,379 @@ +/** + * TDD coverage for migrateCustomer preview audit responses. + * + * Contract under test: + * - response.preview is emitted on migration item events. + * - Boolean add/remove item migrations populate flag_changes and no balance_changes. + * - Metered grant updates populate balance_changes and omit untouched balances. + * - Version migrations populate plan_changes, balance_changes, and flag_changes. + * - Entity-scoped customer products surface entity_id on plan_changes. + */ + +import { expect, test } from "bun:test"; +import { ResetInterval } from "@autumn/shared"; +import { TestFeature } from "@tests/setup/v2Features"; +import { items } from "@tests/utils/fixtures/items"; +import { itemsV2 } from "@tests/utils/fixtures/itemsV2"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; + +type PreviewPlanChange = { + action: "created" | "updated" | "deleted"; + plan_id: string; + entity_id?: string | null; + item_changes: Array<{ + action: "created" | "deleted"; + feature_id: string; + }>; +}; + +type PreviewBalanceChange = { + feature_id: string; + granted: number; + remaining: number; + usage: number; + before: { + granted: number; + remaining: number; + usage: number; + }; +}; + +type PreviewFlagChange = { + action: "created" | "deleted"; + feature_id: string; +}; + +type PreviewMigrateCustomer = { + object: "migration_customer_preview"; + customer_id: string; + plan_changes: PreviewPlanChange[]; + balance_changes: PreviewBalanceChange[]; + flag_changes: PreviewFlagChange[]; +}; + +type MigrationClient = Awaited>["autumnV2_2"]; + +const timeout = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +const parseResponse = (response: unknown): Record => { + if (typeof response === "string") return JSON.parse(response); + if (response && typeof response === "object") + return response as Record; + throw new Error(`Invalid migration event response: ${String(response)}`); +}; + +const waitForPreview = async ({ + autumnV2_2, + migrationId, + migrationRunId, + timeoutMs = 45_000, +}: { + autumnV2_2: MigrationClient; + migrationId: string; + migrationRunId: string; + timeoutMs?: number; +}): Promise => { + const start = Date.now(); + let lastError: unknown; + + while (Date.now() - start < timeoutMs) { + try { + const events = await autumnV2_2.migrationsV2.listItemEvents({ + migrationId, + migrationRunId, + }); + const event = events.list[0]; + if (!event) throw new Error("No migration item event found"); + + const response = parseResponse(event.response); + const preview = response.preview; + if (!preview) throw new Error("Migration item event missing preview"); + + return preview as PreviewMigrateCustomer; + } catch (error) { + lastError = error; + await timeout(1_000); + } + } + + throw new Error( + `Timed out waiting for migration preview: ${ + lastError instanceof Error ? lastError.message : String(lastError) + }`, + ); +}; + +const runPreviewMigration = async ({ + autumnV2_2, + migrationId, + filter, + operations, +}: { + autumnV2_2: MigrationClient; + migrationId: string; + filter: Parameters< + MigrationClient["migrationsV2"]["deleteAndCreate"] + >[0]["filter"]; + operations: Parameters< + MigrationClient["migrationsV2"]["deleteAndCreate"] + >[0]["operations"]; +}) => { + const migration = await autumnV2_2.migrationsV2.deleteAndCreate({ + id: migrationId, + filter, + operations, + }); + const runResponse = await autumnV2_2.migrationsV2.run({ + id: migration.id, + dry_run: true, + }); + + return waitForPreview({ + autumnV2_2, + migrationId: migration.id, + migrationRunId: runResponse.run_id, + }); +}; + +test(`${chalk.yellowBright("migrations preview: boolean item add/remove emits flag changes")}`, async () => { + const suffix = Date.now(); + const customerId = `migration-preview-flags-${suffix}`; + const freePlan = products.base({ + id: `migration-preview-flags-plan-${suffix}`, + items: [items.adminRights()], + }); + + const { autumnV2_2 } = await initScenario({ + customerId, + setup: [s.customer(), s.products({ list: [freePlan] })], + actions: [s.billing.attach({ productId: freePlan.id })], + }); + + const preview = await runPreviewMigration({ + autumnV2_2, + migrationId: `${customerId}-mig`, + filter: { customer: { plan: { plan_id: freePlan.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: freePlan.id }, + customize: { + remove_items: [{ feature_id: TestFeature.AdminRights }], + add_items: [itemsV2.dashboard()], + }, + }, + ], + }, + }); + + expect(preview.balance_changes).toEqual([]); + expect(preview.flag_changes).toEqual( + expect.arrayContaining([ + { action: "deleted", feature_id: TestFeature.AdminRights }, + { action: "created", feature_id: TestFeature.Dashboard }, + ]), + ); + expect(preview.plan_changes).toEqual([ + expect.objectContaining({ + action: "updated", + plan_id: freePlan.id, + item_changes: expect.arrayContaining([ + { action: "deleted", feature_id: TestFeature.AdminRights }, + { action: "created", feature_id: TestFeature.Dashboard }, + ]), + }), + ]); +}); + +test(`${chalk.yellowBright("migrations preview: metered grant replacement emits balance change only for touched feature")}`, async () => { + const suffix = Date.now(); + const customerId = `migration-preview-balances-${suffix}`; + const freePlan = products.base({ + id: `migration-preview-balances-plan-${suffix}`, + items: [ + items.monthlyCredits({ includedUsage: 100 }), + items.monthlyMessages({ includedUsage: 50 }), + ], + }); + + const { autumnV2_2 } = await initScenario({ + customerId, + setup: [s.customer(), s.products({ list: [freePlan] })], + actions: [s.billing.attach({ productId: freePlan.id })], + }); + + const preview = await runPreviewMigration({ + autumnV2_2, + migrationId: `${customerId}-mig`, + filter: { customer: { plan: { plan_id: freePlan.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: freePlan.id }, + customize: { + remove_items: [{ feature_id: TestFeature.Credits }], + add_items: [ + { + feature_id: TestFeature.Credits, + included: 300, + reset: { interval: ResetInterval.Month }, + }, + ], + }, + }, + ], + }, + }); + + expect(preview.flag_changes).toEqual([]); + expect(preview.balance_changes).toEqual([ + { + feature_id: TestFeature.Credits, + granted: 300, + remaining: 300, + usage: 0, + before: { + granted: 100, + remaining: 100, + usage: 0, + }, + }, + ]); + expect( + preview.balance_changes.some( + (change) => change.feature_id === TestFeature.Messages, + ), + ).toBe(false); + expect(preview.plan_changes).toEqual([ + expect.objectContaining({ + action: "updated", + plan_id: freePlan.id, + item_changes: expect.arrayContaining([ + { action: "deleted", feature_id: TestFeature.Credits }, + { action: "created", feature_id: TestFeature.Credits }, + ]), + }), + ]); +}); + +test(`${chalk.yellowBright("migrations preview: version update emits plan, balance, and flag changes")}`, async () => { + const suffix = Date.now(); + const customerId = `migration-preview-version-${suffix}`; + const freePlan = products.base({ + id: `migration-preview-version-plan-${suffix}`, + items: [items.monthlyMessages({ includedUsage: 100 }), items.adminRights()], + }); + + const { autumnV1, autumnV2_2 } = await initScenario({ + customerId, + setup: [s.customer(), s.products({ list: [freePlan] })], + actions: [s.billing.attach({ productId: freePlan.id })], + }); + + await autumnV1.products.update(freePlan.id, { + items: [ + items.monthlyMessages({ includedUsage: 200 }), + items.monthlyCredits({ includedUsage: 50 }), + ], + }); + + const preview = await runPreviewMigration({ + autumnV2_2, + migrationId: `${customerId}-mig`, + filter: { customer: { plan: { plan_id: freePlan.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: freePlan.id }, + version: 2, + }, + ], + }, + }); + + expect(preview.plan_changes.length).toBeGreaterThan(0); + expect(preview.balance_changes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + feature_id: TestFeature.Messages, + granted: 200, + remaining: 200, + usage: 0, + before: { + granted: 100, + remaining: 100, + usage: 0, + }, + }), + expect.objectContaining({ + feature_id: TestFeature.Credits, + granted: 50, + remaining: 50, + usage: 0, + before: { + granted: 0, + remaining: 0, + usage: 0, + }, + }), + ]), + ); + expect(preview.flag_changes).toEqual([ + { action: "deleted", feature_id: TestFeature.AdminRights }, + ]); +}); + +test(`${chalk.yellowBright("migrations preview: plan changes include entity_id")}`, async () => { + const suffix = Date.now(); + const customerId = `migration-preview-entity-${suffix}`; + const entityPlan = products.base({ + id: `migration-preview-entity-plan-${suffix}`, + items: [], + }); + + const { autumnV2_2, entities } = await initScenario({ + customerId, + setup: [ + s.customer(), + s.entities({ count: 1, featureId: TestFeature.Users }), + s.products({ list: [entityPlan] }), + ], + actions: [ + s.billing.attach({ + productId: entityPlan.id, + entityIndex: 0, + }), + ], + }); + + const preview = await runPreviewMigration({ + autumnV2_2, + migrationId: `${customerId}-mig`, + filter: { customer: { plan: { plan_id: entityPlan.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: entityPlan.id }, + customize: { + add_items: [itemsV2.dashboard()], + }, + }, + ], + }, + }); + + expect(preview.plan_changes).toEqual([ + expect.objectContaining({ + action: "updated", + plan_id: entityPlan.id, + entity_id: entities[0].id, + }), + ]); +}); diff --git a/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-carry-rollover.test.ts b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-carry-rollover.test.ts new file mode 100644 index 000000000..d6515decf --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-carry-rollover.test.ts @@ -0,0 +1,111 @@ +/** + * TDD coverage for update_plan item patch migrations. + * + * Contract under test: + * - update_plan reuses update-subscription patch semantics for add_items, + * remove_items, usage carry, and rollover carry. + * - Migration execution does not create extra invoices. + * - Existing customer products are patched, not replaced or expired. + */ + +import { test } from "bun:test"; +import type { ApiCustomerV3, ApiCustomerV5 } from "@autumn/shared"; +import { + BillingInterval, + ResetInterval, + RolloverExpiryDurationType, +} from "@autumn/shared"; +import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; +import { expectCustomerProducts } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { expectNoExpiredCustomerProducts } from "@tests/integration/billing/utils/expectNoExpiredCustomerProducts"; +import { expectBalanceCorrect } from "@tests/integration/utils/expectBalanceCorrect"; +import { TestFeature } from "@tests/setup/v2Features"; +import { items } from "@tests/utils/fixtures/items"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import { runUpdatePlanMigration } from "../utils/runUpdatePlanMigration"; + +test.concurrent(`${chalk.yellowBright("migrations update_plan: metered rollover carries to added item")}`, async () => { + const customerId = "migration-update-carry-rollover"; + const base = products.base({ + items: [ + items.monthlyMessagesWithRollover({ + includedUsage: 400, + rolloverConfig: { + max: 500, + length: 1, + duration: RolloverExpiryDurationType.Month, + }, + }), + ], + }); + + const { autumnV1, autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [base] }), + ], + actions: [ + s.billing.attach({ productId: base.id }), + s.track({ featureId: TestFeature.Messages, value: 250, timeout: 2000 }), + s.resetFeature({ featureId: TestFeature.Messages }), + ], + }); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + filter: { customer: { plan: { plan_id: base.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: base.id }, + customize: { + remove_items: [ + { + feature_id: TestFeature.Messages, + interval: BillingInterval.Month, + }, + ], + add_items: [ + { + feature_id: TestFeature.Messages, + included: 500, + reset: { interval: ResetInterval.Month }, + rollover: { + max: 500, + expiry_duration_type: RolloverExpiryDurationType.Month, + expiry_duration_length: 1, + }, + }, + ], + }, + }, + ], + }, + }); + + const customer = await autumnV2_2.customers.get(customerId); + await expectCustomerProducts({ customer, active: [base.id] }); + expectBalanceCorrect({ + customer, + featureId: TestFeature.Messages, + remaining: 650, + usage: 0, + rollovers: [{ balance: 150 }], + }); + await expectCustomerInvoiceCorrect({ + customer: await autumnV1.customers.get(customerId), + count: 0, + }); + await expectNoExpiredCustomerProducts({ + ctx, + customerId, + productId: base.id, + }); +}); diff --git a/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-carry-usage.test.ts b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-carry-usage.test.ts new file mode 100644 index 000000000..6e7b174af --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-carry-usage.test.ts @@ -0,0 +1,105 @@ +/** + * TDD coverage for update_plan item patch migrations. + * + * Contract under test: + * - update_plan reuses update-subscription patch semantics for add_items, + * remove_items, usage carry, and rollover carry. + * - Migration execution does not create extra invoices. + * - Existing customer products are patched, not replaced or expired. + */ + +import { test } from "bun:test"; +import type { ApiCustomerV5 } from "@autumn/shared"; +import { BillingInterval, ResetInterval } from "@autumn/shared"; +import { expectCustomerProducts } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { expectNoExpiredCustomerProducts } from "@tests/integration/billing/utils/expectNoExpiredCustomerProducts"; +import { expectStripeSubscriptionCorrect } from "@tests/integration/billing/utils/expectStripeSubCorrect"; +import { expectBalanceCorrect } from "@tests/integration/utils/expectBalanceCorrect"; +import { TestFeature } from "@tests/setup/v2Features"; +import { items } from "@tests/utils/fixtures/items"; +import { itemsV2 } from "@tests/utils/fixtures/itemsV2"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import { runUpdatePlanMigration } from "../utils/runUpdatePlanMigration"; + +test.concurrent(`${chalk.yellowBright("migrations update_plan: same-feature replacement carries only matching usage")}`, async () => { + const customerId = "migration-update-carry-usage-same-feature"; + const pro = products.pro({ + items: [ + items.monthlyMessages({ includedUsage: 100 }), + items.lifetimeMessages({ includedUsage: 500 }), + ], + }); + + const { autumnV2, autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [s.billing.attach({ productId: pro.id })], + }); + + await autumnV2.balances.update({ + customer_id: customerId, + feature_id: TestFeature.Messages, + current_balance: 50, + interval: ResetInterval.Month, + }); + await autumnV2.balances.update({ + customer_id: customerId, + feature_id: TestFeature.Messages, + current_balance: 200, + interval: ResetInterval.OneOff, + }); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + filter: { customer: { plan: { plan_id: pro.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: pro.id }, + customize: { + remove_items: [ + { + feature_id: TestFeature.Messages, + interval: BillingInterval.Month, + }, + ], + add_items: [itemsV2.monthlyMessages({ included: 200 })], + }, + }, + ], + }, + }); + + const customer = await autumnV2_2.customers.get(customerId); + await expectCustomerProducts({ customer, active: [pro.id] }); + expectBalanceCorrect({ + customer, + featureId: TestFeature.Messages, + remaining: 350, + usage: 350, + planId: pro.id, + breakdown: { + [ResetInterval.Month]: { + included_grant: 200, + remaining: 150, + usage: 50, + }, + [ResetInterval.OneOff]: { + included_grant: 500, + remaining: 200, + usage: 300, + }, + }, + }); + await expectNoExpiredCustomerProducts({ ctx, customerId, productId: pro.id }); + await expectStripeSubscriptionCorrect({ ctx, customerId }); +}); diff --git a/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-items.test.ts b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-items.test.ts new file mode 100644 index 000000000..1295f40aa --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-items.test.ts @@ -0,0 +1,92 @@ +/** + * TDD coverage for update_plan item patch migrations. + * + * Contract under test: + * - update_plan reuses update-subscription patch semantics for add_items, + * remove_items, usage carry, and rollover carry. + * - Migration execution does not create extra invoices. + * - Existing customer products are patched, not replaced or expired. + */ + +import { test } from "bun:test"; +import type { ApiCustomerV3, ApiCustomerV5 } from "@autumn/shared"; +import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; +import { expectCustomerProducts } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { expectNoExpiredCustomerProducts } from "@tests/integration/billing/utils/expectNoExpiredCustomerProducts"; +import { expectStripeSubscriptionCorrect } from "@tests/integration/billing/utils/expectStripeSubCorrect"; +import { expectBalanceCorrect } from "@tests/integration/utils/expectBalanceCorrect"; +import { expectFlagCorrect } from "@tests/integration/utils/expectFlagCorrect"; +import { TestFeature } from "@tests/setup/v2Features"; +import { itemsV2 } from "@tests/utils/fixtures/itemsV2"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import { runUpdatePlanMigration } from "../utils/runUpdatePlanMigration"; + +test.concurrent(`${chalk.yellowBright("migrations update_plan: add boolean and metered entitlements")}`, async () => { + const customerId = "migration-update-add-items"; + const pro = products.pro({ items: [] }); + + const { autumnV1, autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [s.billing.attach({ productId: pro.id })], + }); + + const expectMigrationApplied = async () => { + const customer = await autumnV2_2.customers.get(customerId); + await expectCustomerProducts({ customer, active: [pro.id] }); + expectFlagCorrect({ + customer, + featureId: TestFeature.Dashboard, + planId: pro.id, + }); + expectBalanceCorrect({ + customer, + featureId: TestFeature.Words, + remaining: 150, + usage: 0, + planId: pro.id, + }); + return customer; + }; + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + filter: { customer: { plan: { plan_id: pro.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: pro.id }, + customize: { + add_items: [ + itemsV2.dashboard(), + itemsV2.monthlyWords({ included: 150 }), + ], + }, + }, + ], + }, + runOnServer: true, + waitFor: async () => { + await expectMigrationApplied(); + }, + timeoutMs: 60_000, + }); + + await expectMigrationApplied(); + await expectCustomerInvoiceCorrect({ + customer: await autumnV1.customers.get(customerId), + count: 1, + latestTotal: 20, + }); + await expectNoExpiredCustomerProducts({ ctx, customerId, productId: pro.id }); + await expectStripeSubscriptionCorrect({ ctx, customerId }); +}); diff --git a/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-multi-targets.test.ts b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-multi-targets.test.ts new file mode 100644 index 000000000..4bf8c4d71 --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-multi-targets.test.ts @@ -0,0 +1,332 @@ +/** + * TDD coverage for update_plan multi-target migrations. + * + * Contract under test: + * - plan_filter can match multiple customer products on different entities. + * - multiple update_plan operations run in order on the same customer. + */ + +import { expect, test } from "bun:test"; +import type { ApiCustomerV3, ApiEntityV2 } from "@autumn/shared"; +import { + customerPrices, + customerProducts, + customers, + prices, + ResetInterval, +} from "@autumn/shared"; +import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; +import { expectNoExpiredCustomerProducts } from "@tests/integration/billing/utils/expectNoExpiredCustomerProducts"; +import { expectStripeSubscriptionCorrect } from "@tests/integration/billing/utils/expectStripeSubCorrect"; +import { expectBalanceCorrect } from "@tests/integration/utils/expectBalanceCorrect"; +import { expectFlagCorrect } from "@tests/integration/utils/expectFlagCorrect"; +import { TestFeature } from "@tests/setup/v2Features"; +import { items } from "@tests/utils/fixtures/items"; +import { itemsV2 } from "@tests/utils/fixtures/itemsV2"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import { and, eq } from "drizzle-orm"; +import { runUpdatePlanMigration } from "../utils/runUpdatePlanMigration"; + +const getCustomerProductPriceAmounts = async ({ + ctx, + customerId, + productId, + entityId, +}: { + ctx: Awaited>["ctx"]; + customerId: string; + productId: string; + entityId: string; +}) => + ( + await ctx.db + .select({ config: prices.config }) + .from(customerProducts) + .innerJoin( + customers, + eq(customerProducts.internal_customer_id, customers.internal_id), + ) + .innerJoin( + customerPrices, + eq(customerPrices.customer_product_id, customerProducts.id), + ) + .innerJoin(prices, eq(customerPrices.price_id, prices.id)) + .where( + and( + eq(customers.org_id, ctx.org.id), + eq(customers.env, ctx.env), + eq(customers.id, customerId), + eq(customerProducts.product_id, productId), + eq(customerProducts.entity_id, entityId), + ), + ) + ) + .map((row) => + row.config && "amount" in row.config ? row.config.amount : undefined, + ) + .filter((amount): amount is number => typeof amount === "number") + .sort((a, b) => a - b); + +test.concurrent(`${chalk.yellowBright("migrations update_plan: plan filter patches multiple entity products")}`, async () => { + const customerId = "migration-update-multi-entity"; + const pro = products.pro({ items: [] }); + + const { autumnV1, autumnV2_2, ctx, entities } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + s.entities({ count: 2, featureId: TestFeature.Users }), + ], + actions: [ + s.billing.attach({ productId: pro.id, entityIndex: 0 }), + s.billing.attach({ productId: pro.id, entityIndex: 1 }), + ], + }); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + filter: { customer: { plan: { plan_id: pro.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: pro.id }, + customize: { + add_items: [ + itemsV2.dashboard(), + itemsV2.monthlyWords({ included: 75 }), + ], + }, + }, + ], + }, + }); + + const entity1 = await autumnV2_2.entities.get( + customerId, + entities[0].id, + ); + const entity2 = await autumnV2_2.entities.get( + customerId, + entities[1].id, + ); + + for (const entity of [entity1, entity2]) { + expectFlagCorrect({ + customer: entity, + featureId: TestFeature.Dashboard, + planId: pro.id, + }); + expectBalanceCorrect({ + customer: entity, + featureId: TestFeature.Words, + remaining: 75, + usage: 0, + planId: pro.id, + }); + } + + await expectCustomerInvoiceCorrect({ + customer: await autumnV1.customers.get(customerId), + count: 2, + }); + await expectNoExpiredCustomerProducts({ + ctx, + customerId, + productId: pro.id, + entityId: entities[0].id, + }); + await expectNoExpiredCustomerProducts({ + ctx, + customerId, + productId: pro.id, + entityId: entities[1].id, + }); + await expectStripeSubscriptionCorrect({ ctx, customerId }); +}); + +test.concurrent(`${chalk.yellowBright("migrations update_plan: two operations run in order")}`, async () => { + const customerId = "migration-update-two-ops"; + const pro = products.pro({ items: [] }); + const premium = products.premium({ + items: [items.monthlyCredits({ includedUsage: 100 })], + }); + + const { autumnV1, autumnV2_2, ctx, entities } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro, premium] }), + s.entities({ count: 2, featureId: TestFeature.Users }), + ], + actions: [ + s.billing.attach({ productId: pro.id, entityIndex: 0 }), + s.billing.attach({ productId: premium.id, entityIndex: 1 }), + ], + }); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + filter: { + customer: { + plan: { $or: [{ plan_id: pro.id }, { plan_id: premium.id }] }, + }, + }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: pro.id }, + customize: { add_items: [itemsV2.dashboard()] }, + }, + { + type: "update_plan", + plan_filter: { plan_id: premium.id }, + customize: { + remove_items: [{ feature_id: TestFeature.Credits }], + add_items: [ + { + feature_id: TestFeature.Credits, + included: 250, + reset: { interval: ResetInterval.Month }, + }, + ], + }, + }, + ], + }, + }); + + const proEntity = await autumnV2_2.entities.get( + customerId, + entities[0].id, + ); + const premiumEntity = await autumnV2_2.entities.get( + customerId, + entities[1].id, + ); + + expectFlagCorrect({ + customer: proEntity, + featureId: TestFeature.Dashboard, + planId: pro.id, + }); + expectBalanceCorrect({ + customer: premiumEntity, + featureId: TestFeature.Credits, + remaining: 250, + usage: 0, + planId: premium.id, + }); + await expectCustomerInvoiceCorrect({ + customer: await autumnV1.customers.get(customerId), + count: 2, + }); + await expectNoExpiredCustomerProducts({ + ctx, + customerId, + productId: pro.id, + entityId: entities[0].id, + }); + await expectNoExpiredCustomerProducts({ + ctx, + customerId, + productId: premium.id, + entityId: entities[1].id, + }); + await expectStripeSubscriptionCorrect({ ctx, customerId }); +}); + +test.concurrent(`${chalk.yellowBright("migrations update_plan: multiple plan price updates run in one customer migration")}`, async () => { + const customerId = "migration-update-multi-price"; + const pro = products.pro({ items: [] }); + const premium = products.premium({ items: [] }); + + const { autumnV1, autumnV2_2, ctx, entities } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro, premium] }), + s.entities({ count: 2, featureId: TestFeature.Users }), + ], + actions: [ + s.billing.attach({ productId: pro.id, entityIndex: 0 }), + s.billing.attach({ productId: premium.id, entityIndex: 1 }), + ], + }); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + runOnServer: false, + filter: { + customer: { + plan: { $or: [{ plan_id: pro.id }, { plan_id: premium.id }] }, + }, + }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: pro.id }, + customize: { + price: itemsV2.monthlyPrice({ amount: 50 }), + }, + }, + { + type: "update_plan", + plan_filter: { plan_id: premium.id }, + customize: { + price: itemsV2.monthlyPrice({ amount: 100 }), + }, + }, + ], + }, + }); + + expect( + await getCustomerProductPriceAmounts({ + ctx, + customerId, + productId: pro.id, + entityId: entities[0].id, + }), + ).toEqual([50]); + expect( + await getCustomerProductPriceAmounts({ + ctx, + customerId, + productId: premium.id, + entityId: entities[1].id, + }), + ).toEqual([100]); + + await expectCustomerInvoiceCorrect({ + customer: await autumnV1.customers.get(customerId), + count: 2, + }); + await expectNoExpiredCustomerProducts({ + ctx, + customerId, + productId: pro.id, + entityId: entities[0].id, + }); + await expectNoExpiredCustomerProducts({ + ctx, + customerId, + productId: premium.id, + entityId: entities[1].id, + }); + await expectStripeSubscriptionCorrect({ ctx, customerId }); +}); diff --git a/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-paid-features.test.ts b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-paid-features.test.ts new file mode 100644 index 000000000..89ab13a08 --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-paid-features.test.ts @@ -0,0 +1,108 @@ +/** + * TDD coverage for update_plan item patch migrations. + * + * Contract under test: + * - update_plan reuses update-subscription patch semantics for add_items, + * remove_items, usage carry, and rollover carry. + * - Migration execution does not create extra invoices. + * - Existing customer products are patched, not replaced or expired. + */ + +import { test } from "bun:test"; +import type { ApiCustomerV3, ApiCustomerV5 } from "@autumn/shared"; +import { BillingMethod } from "@autumn/shared"; +import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; +import { expectCustomerProducts } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { expectNoExpiredCustomerProducts } from "@tests/integration/billing/utils/expectNoExpiredCustomerProducts"; +import { expectStripeSubscriptionCorrect } from "@tests/integration/billing/utils/expectStripeSubCorrect"; +import { expectBalanceCorrect } from "@tests/integration/utils/expectBalanceCorrect"; +import { expectFlagCorrect } from "@tests/integration/utils/expectFlagCorrect"; +import { TestFeature } from "@tests/setup/v2Features"; +import { items } from "@tests/utils/fixtures/items"; +import { itemsV2 } from "@tests/utils/fixtures/itemsV2"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import { runUpdatePlanMigration } from "../utils/runUpdatePlanMigration"; + +test.concurrent(`${chalk.yellowBright("migrations update_plan: consumable paid feature carries usage without charging")}`, async () => { + const customerId = "migration-update-paid-consumable"; + const messagesUsage = 60; + const included = 50; + const pro = products.pro({ + items: [items.monthlyMessages({ includedUsage: 100 })], + }); + + const { autumnV1, autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [s.billing.attach({ productId: pro.id })], + }); + + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messagesUsage, + }, + { timeout: 2000 }, + ); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + filter: { customer: { plan: { plan_id: pro.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: pro.id }, + customize: { + remove_items: [{ feature_id: TestFeature.Messages }], + add_items: [ + itemsV2.dashboard(), + { + ...itemsV2.consumableMessages({ amount: 0.1 }), + included, + }, + ], + }, + }, + ], + }, + }); + + const customer = await autumnV2_2.customers.get(customerId); + await expectCustomerProducts({ customer, active: [pro.id] }); + expectFlagCorrect({ + customer, + featureId: TestFeature.Dashboard, + planId: pro.id, + }); + expectBalanceCorrect({ + customer, + featureId: TestFeature.Messages, + remaining: 0, + usage: messagesUsage, + planId: pro.id, + breakdown: { + [BillingMethod.UsageBased]: { + included_grant: included, + remaining: 0, + usage: messagesUsage, + }, + }, + }); + await expectCustomerInvoiceCorrect({ + customer: await autumnV1.customers.get(customerId), + count: 1, + latestTotal: 20, + }); + await expectNoExpiredCustomerProducts({ ctx, customerId, productId: pro.id }); + await expectStripeSubscriptionCorrect({ ctx, customerId }); +}); diff --git a/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-price.test.ts b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-price.test.ts new file mode 100644 index 000000000..8c3757ac9 --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-price.test.ts @@ -0,0 +1,134 @@ +import { expect, test } from "bun:test"; +import type { ApiCustomerV3, ApiCustomerV5 } from "@autumn/shared"; +import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; +import { expectCustomerProducts } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { expectNoExpiredCustomerProducts } from "@tests/integration/billing/utils/expectNoExpiredCustomerProducts"; +import { expectStripeSubscriptionCorrect } from "@tests/integration/billing/utils/expectStripeSubCorrect"; +import { expectBalanceCorrect } from "@tests/integration/utils/expectBalanceCorrect"; +import { expectFlagCorrect } from "@tests/integration/utils/expectFlagCorrect"; +import { TestFeature } from "@tests/setup/v2Features"; +import { items } from "@tests/utils/fixtures/items"; +import { itemsV2 } from "@tests/utils/fixtures/itemsV2"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import { runUpdatePlanMigration } from "../utils/runUpdatePlanMigration"; + +test.concurrent(`${chalk.yellowBright("migrations update_plan: update price and add boolean entitlement")}`, async () => { + const customerId = "migration-update-price"; + const pro = products.pro({ items: [] }); + + const { autumnV1, autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [s.billing.attach({ productId: pro.id })], + }); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + filter: { customer: { plan: { plan_id: pro.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: pro.id }, + customize: { + price: itemsV2.monthlyPrice({ amount: 32 }), + add_items: [itemsV2.dashboard()], + }, + }, + ], + }, + }); + + const customer = await autumnV2_2.customers.get(customerId); + await expectCustomerProducts({ customer, active: [pro.id] }); + expectFlagCorrect({ + customer, + featureId: TestFeature.Dashboard, + planId: pro.id, + }); + await expectCustomerInvoiceCorrect({ + customer: await autumnV1.customers.get(customerId), + count: 1, + latestTotal: 20, + }); + await expectNoExpiredCustomerProducts({ ctx, customerId, productId: pro.id }); + await expectStripeSubscriptionCorrect({ ctx, customerId }); +}); + +test.concurrent(`${chalk.yellowBright("migrations update_plan: put update then migration patch")}`, async () => { + const customerId = "migration-update-put-then-patch"; + const pro = products.pro({ + items: [items.monthlyMessages({ includedUsage: 100 })], + }); + + const { autumnV1, autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [s.billing.attach({ productId: pro.id })], + }); + + await autumnV2_2.subscriptions.update({ + customer_id: customerId, + plan_id: pro.id, + customize: { + price: itemsV2.monthlyPrice({ amount: 25 }), + items: [itemsV2.monthlyWords({ included: 120 })], + }, + }); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + filter: { customer: { plan: { plan_id: pro.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: pro.id }, + customize: { + remove_items: [{ feature_id: TestFeature.Words }], + add_items: [ + itemsV2.dashboard(), + itemsV2.monthlyMessages({ included: 200 }), + ], + }, + }, + ], + }, + }); + + const customer = await autumnV2_2.customers.get(customerId); + expect(customer.balances[TestFeature.Words]).toBeUndefined(); + expectFlagCorrect({ + customer, + featureId: TestFeature.Dashboard, + planId: pro.id, + }); + expectBalanceCorrect({ + customer, + featureId: TestFeature.Messages, + remaining: 200, + usage: 0, + planId: pro.id, + }); + await expectCustomerInvoiceCorrect({ + customer: await autumnV1.customers.get(customerId), + count: 2, + latestTotal: 5, + }); + await expectNoExpiredCustomerProducts({ ctx, customerId, productId: pro.id }); + await expectStripeSubscriptionCorrect({ ctx, customerId }); +}); diff --git a/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-states.test.ts b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-states.test.ts new file mode 100644 index 000000000..28a4cf79d --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-states.test.ts @@ -0,0 +1,476 @@ +/** + * TDD coverage for update_plan migrations preserving in-flight subscription + * states. + * + * Contract under test: + * - Updating the active plan's base price does not clear a scheduled downgrade. + * - Updating a canceling plan's base price does not clear end-of-cycle cancel. + * - Entity-scoped and multi-product states survive a customer migration. + */ + +import { expect, test } from "bun:test"; +import type { ApiCustomerV3, ApiEntityV0 } from "@autumn/shared"; +import { + CusProductStatus, + customerPrices, + customerProducts, + customers, + prices, +} from "@autumn/shared"; +import { + expectProductCanceling, + expectProductNotPresent, + expectProductScheduled, +} from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { expectNoExpiredCustomerProducts } from "@tests/integration/billing/utils/expectNoExpiredCustomerProducts"; +import { expectStripeSubscriptionCorrect } from "@tests/integration/billing/utils/expectStripeSubCorrect"; +import { TestFeature } from "@tests/setup/v2Features"; +import { items } from "@tests/utils/fixtures/items"; +import { itemsV2 } from "@tests/utils/fixtures/itemsV2"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import { and, eq, isNull } from "drizzle-orm"; +import { runUpdatePlanMigration } from "../utils/runUpdatePlanMigration"; + +const getScheduledIds = async ({ + ctx, + customerId, + productId, + entityId, +}: { + ctx: Awaited>["ctx"]; + customerId: string; + productId: string; + entityId?: string; +}) => + ( + await ctx.db + .select({ scheduledIds: customerProducts.scheduled_ids }) + .from(customerProducts) + .innerJoin( + customers, + eq(customerProducts.internal_customer_id, customers.internal_id), + ) + .where( + and( + eq(customers.org_id, ctx.org.id), + eq(customers.env, ctx.env), + eq(customers.id, customerId), + eq(customerProducts.product_id, productId), + eq(customerProducts.status, CusProductStatus.Scheduled), + entityId + ? eq(customerProducts.entity_id, entityId) + : isNull(customerProducts.entity_id), + ), + ) + ) + .map((row) => row.scheduledIds ?? []) + .flat() + .sort(); + +const getCustomerProductPriceAmounts = async ({ + ctx, + customerId, + productId, + entityId, +}: { + ctx: Awaited>["ctx"]; + customerId: string; + productId: string; + entityId?: string; +}) => + ( + await ctx.db + .select({ config: prices.config }) + .from(customerProducts) + .innerJoin( + customers, + eq(customerProducts.internal_customer_id, customers.internal_id), + ) + .innerJoin( + customerPrices, + eq(customerPrices.customer_product_id, customerProducts.id), + ) + .innerJoin(prices, eq(customerPrices.price_id, prices.id)) + .where( + and( + eq(customers.org_id, ctx.org.id), + eq(customers.env, ctx.env), + eq(customers.id, customerId), + eq(customerProducts.product_id, productId), + entityId + ? eq(customerProducts.entity_id, entityId) + : isNull(customerProducts.entity_id), + ), + ) + ) + .map((row) => + row.config && "amount" in row.config ? row.config.amount : undefined, + ) + .filter((amount): amount is number => typeof amount === "number") + .sort((a, b) => a - b); + +test.concurrent(`${chalk.yellowBright("migrations update_plan states: scheduled downgrade survives active plan price update")}`, async () => { + const customerId = "migration-update-state-downgrade"; + const pro = products.pro({ + items: [items.monthlyMessages({ includedUsage: 500 })], + }); + const premium = products.premium({ + items: [items.monthlyMessages({ includedUsage: 1000 })], + }); + + const { autumnV1, autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro, premium] }), + ], + actions: [ + s.billing.attach({ productId: premium.id }), + s.billing.attach({ productId: pro.id }), + ], + }); + + const before = await autumnV1.customers.get(customerId); + await expectProductCanceling({ customer: before, productId: premium.id }); + await expectProductScheduled({ customer: before, productId: pro.id }); + const scheduledIdsBefore = await getScheduledIds({ + ctx, + customerId, + productId: pro.id, + }); + expect(scheduledIdsBefore.length).toBeGreaterThan(0); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + runOnServer: false, + filter: { customer: { plan: { plan_id: premium.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: premium.id }, + customize: { + price: itemsV2.monthlyPrice({ amount: 100 }), + }, + }, + ], + }, + }); + + const after = await autumnV1.customers.get(customerId); + await expectProductCanceling({ customer: after, productId: premium.id }); + await expectProductScheduled({ customer: after, productId: pro.id }); + expect( + await getCustomerProductPriceAmounts({ + ctx, + customerId, + productId: premium.id, + }), + ).toEqual([100]); + expect( + await getScheduledIds({ + ctx, + customerId, + productId: pro.id, + }), + ).toEqual(scheduledIdsBefore); + await expectNoExpiredCustomerProducts({ + ctx, + customerId, + productId: premium.id, + }); + await expectStripeSubscriptionCorrect({ ctx, customerId }); +}); + +test.concurrent(`${chalk.yellowBright("migrations update_plan states: end-of-cycle cancel survives price update")}`, async () => { + const customerId = "migration-update-state-cancel"; + const pro = products.pro({ + items: [items.monthlyMessages({ includedUsage: 500 })], + }); + + const { autumnV1, autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [ + s.billing.attach({ productId: pro.id }), + s.updateSubscription({ + productId: pro.id, + cancelAction: "cancel_end_of_cycle", + }), + ], + }); + + const before = await autumnV1.customers.get(customerId); + await expectProductCanceling({ customer: before, productId: pro.id }); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + runOnServer: false, + filter: { customer: { plan: { plan_id: pro.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: pro.id }, + customize: { + price: itemsV2.monthlyPrice({ amount: 50 }), + }, + }, + ], + }, + }); + + const after = await autumnV1.customers.get(customerId); + await expectProductCanceling({ customer: after, productId: pro.id }); + expect( + await getCustomerProductPriceAmounts({ + ctx, + customerId, + productId: pro.id, + }), + ).toEqual([50]); + expect( + await getScheduledIds({ + ctx, + customerId, + productId: pro.id, + }), + ).toEqual([]); + await expectNoExpiredCustomerProducts({ ctx, customerId, productId: pro.id }); + await expectStripeSubscriptionCorrect({ ctx, customerId }); +}); + +test.concurrent(`${chalk.yellowBright("migrations update_plan states: entity scheduled and canceling states survive")}`, async () => { + const customerId = "migration-update-state-entities"; + const pro = products.pro({ + items: [items.monthlyMessages({ includedUsage: 500 })], + }); + const premium = products.premium({ + items: [items.monthlyMessages({ includedUsage: 1000 })], + }); + + const { autumnV1, autumnV2_2, ctx, entities } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro, premium] }), + s.entities({ count: 2, featureId: TestFeature.Users }), + ], + actions: [ + s.billing.attach({ productId: premium.id, entityIndex: 0 }), + s.billing.attach({ productId: premium.id, entityIndex: 1 }), + s.billing.attach({ productId: pro.id, entityIndex: 0 }), + s.updateSubscription({ + productId: premium.id, + entityIndex: 1, + cancelAction: "cancel_end_of_cycle", + }), + ], + }); + + const entity1Before = await autumnV1.entities.get( + customerId, + entities[0].id, + ); + const entity2Before = await autumnV1.entities.get( + customerId, + entities[1].id, + ); + await expectProductCanceling({ + customer: entity1Before, + productId: premium.id, + }); + await expectProductScheduled({ customer: entity1Before, productId: pro.id }); + await expectProductCanceling({ + customer: entity2Before, + productId: premium.id, + }); + await expectProductNotPresent({ customer: entity2Before, productId: pro.id }); + const scheduledIdsBefore = await getScheduledIds({ + ctx, + customerId, + productId: pro.id, + entityId: entities[0].id, + }); + expect(scheduledIdsBefore.length).toBeGreaterThan(0); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + runOnServer: false, + filter: { customer: { plan: { plan_id: premium.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: premium.id }, + customize: { + price: itemsV2.monthlyPrice({ amount: 100 }), + }, + }, + ], + }, + }); + + const entity1After = await autumnV1.entities.get( + customerId, + entities[0].id, + ); + const entity2After = await autumnV1.entities.get( + customerId, + entities[1].id, + ); + await expectProductCanceling({ + customer: entity1After, + productId: premium.id, + }); + await expectProductScheduled({ customer: entity1After, productId: pro.id }); + await expectProductCanceling({ + customer: entity2After, + productId: premium.id, + }); + await expectProductNotPresent({ customer: entity2After, productId: pro.id }); + expect( + await getCustomerProductPriceAmounts({ + ctx, + customerId, + productId: premium.id, + entityId: entities[0].id, + }), + ).toEqual([100]); + expect( + await getCustomerProductPriceAmounts({ + ctx, + customerId, + productId: premium.id, + entityId: entities[1].id, + }), + ).toEqual([100]); + expect( + await getScheduledIds({ + ctx, + customerId, + productId: pro.id, + entityId: entities[0].id, + }), + ).toEqual(scheduledIdsBefore); + await expectStripeSubscriptionCorrect({ ctx, customerId }); +}); + +test.concurrent(`${chalk.yellowBright("migrations update_plan states: multi-product scheduled downgrade and canceling addon survive")}`, async () => { + const customerId = "migration-update-state-products"; + const pro = products.pro({ + items: [items.monthlyMessages({ includedUsage: 500 })], + }); + const premium = products.premium({ + items: [items.monthlyMessages({ includedUsage: 1000 })], + }); + const addon = products.recurringAddOn({ + items: [items.monthlyWords({ includedUsage: 300 })], + }); + + const { autumnV1, autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro, premium, addon] }), + ], + actions: [ + s.billing.attach({ productId: premium.id }), + s.billing.attach({ productId: addon.id }), + s.billing.attach({ productId: pro.id }), + s.updateSubscription({ + productId: addon.id, + cancelAction: "cancel_end_of_cycle", + }), + ], + }); + + const before = await autumnV1.customers.get(customerId); + await expectProductCanceling({ customer: before, productId: premium.id }); + await expectProductScheduled({ customer: before, productId: pro.id }); + await expectProductCanceling({ customer: before, productId: addon.id }); + const scheduledIdsBefore = await getScheduledIds({ + ctx, + customerId, + productId: pro.id, + }); + expect(scheduledIdsBefore.length).toBeGreaterThan(0); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + runOnServer: false, + filter: { + customer: { + plan: { $or: [{ plan_id: premium.id }, { plan_id: addon.id }] }, + }, + }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: premium.id }, + customize: { + price: itemsV2.monthlyPrice({ amount: 100 }), + }, + }, + { + type: "update_plan", + plan_filter: { plan_id: addon.id }, + customize: { + price: itemsV2.monthlyPrice({ amount: 40 }), + }, + }, + ], + }, + }); + + const after = await autumnV1.customers.get(customerId); + await expectProductCanceling({ customer: after, productId: premium.id }); + await expectProductScheduled({ customer: after, productId: pro.id }); + await expectProductCanceling({ customer: after, productId: addon.id }); + expect( + await getCustomerProductPriceAmounts({ + ctx, + customerId, + productId: premium.id, + }), + ).toEqual([100]); + expect( + await getCustomerProductPriceAmounts({ + ctx, + customerId, + productId: addon.id, + }), + ).toEqual([40]); + expect( + await getScheduledIds({ + ctx, + customerId, + productId: pro.id, + }), + ).toEqual(scheduledIdsBefore); + await expectNoExpiredCustomerProducts({ + ctx, + customerId, + productId: premium.id, + }); + await expectNoExpiredCustomerProducts({ ctx, customerId, productId: addon.id }); + await expectStripeSubscriptionCorrect({ ctx, customerId }); +}); diff --git a/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-version.test.ts b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-version.test.ts new file mode 100644 index 000000000..5ac2ce0bd --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-version.test.ts @@ -0,0 +1,212 @@ +/** + * TDD coverage for update_plan version migrations. + * + * Contract under test: + * - update_plan can migrate matched customer products to a target plan version. + * - Usage/quantity state carries across the version update. + * - Migration execution does not create extra invoices. + */ + +import { test } from "bun:test"; +import type { ApiCustomerV3, ApiCustomerV5 } from "@autumn/shared"; +import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; +import { expectCustomerProducts } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { expectStripeSubscriptionCorrect } from "@tests/integration/billing/utils/expectStripeSubCorrect"; +import { expectBalanceCorrect } from "@tests/integration/utils/expectBalanceCorrect"; +import { TestFeature } from "@tests/setup/v2Features"; +import { items } from "@tests/utils/fixtures/items"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import { runUpdatePlanMigration } from "../utils/runUpdatePlanMigration"; + +test.concurrent(`${chalk.yellowBright("migrations update_plan: free version update carries usage")}`, async () => { + const customerId = "migration-update-free-version"; + const free = products.base({ + id: "free", + items: [items.monthlyMessages({ includedUsage: 500 })], + }); + + const { autumnV1, autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [free] }), + ], + actions: [ + s.billing.attach({ productId: free.id }), + s.track({ featureId: TestFeature.Messages, value: 100, timeout: 2000 }), + ], + }); + + await autumnV1.products.update(free.id, { + items: [items.monthlyMessages({ includedUsage: 600 })], + }); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + filter: { customer: { plan: { plan_id: free.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: free.id }, + version: 2, + }, + ], + }, + }); + + const customer = await autumnV2_2.customers.get(customerId); + await expectCustomerProducts({ customer, active: [free.id] }); + expectBalanceCorrect({ + customer, + featureId: TestFeature.Messages, + remaining: 500, + usage: 100, + planId: free.id, + }); + await expectCustomerInvoiceCorrect({ + customer: await autumnV1.customers.get(customerId), + count: 0, + }); +}); + +test.concurrent(`${chalk.yellowBright("migrations update_plan: paid version update does not invoice")}`, async () => { + const customerId = "migration-update-paid-version"; + const pro = products.pro({ + id: "pro", + items: [items.consumableMessages({ includedUsage: 500 })], + }); + + const { autumnV1, autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [ + s.billing.attach({ productId: pro.id }), + s.track({ featureId: TestFeature.Messages, value: 600, timeout: 2000 }), + ], + }); + const invoiceCountBefore = + (await autumnV1.customers.get(customerId)).invoices + ?.length ?? 0; + + await autumnV1.products.update(pro.id, { + items: [ + items.monthlyPrice({ price: 30 }), + items.consumableMessages({ includedUsage: 600 }), + ], + }); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + filter: { customer: { plan: { plan_id: pro.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: pro.id }, + version: 2, + }, + ], + }, + }); + + const customer = await autumnV2_2.customers.get(customerId); + await expectCustomerProducts({ customer, active: [pro.id] }); + expectBalanceCorrect({ + customer, + featureId: TestFeature.Messages, + remaining: 0, + usage: 600, + planId: pro.id, + }); + await expectCustomerInvoiceCorrect({ + customer: await autumnV1.customers.get(customerId), + count: invoiceCountBefore, + }); + await expectStripeSubscriptionCorrect({ ctx, customerId }); +}); + +test.concurrent(`${chalk.yellowBright("migrations update_plan: prepaid version update preserves quantity")}`, async () => { + const customerId = "migration-update-prepaid-version"; + const pro = products.pro({ + id: "pro", + items: [ + items.prepaidMessages({ + includedUsage: 0, + billingUnits: 100, + price: 10, + }), + ], + }); + + const { autumnV1, autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [ + s.billing.attach({ + productId: pro.id, + options: [{ feature_id: TestFeature.Messages, quantity: 200 }], + }), + ], + }); + const invoiceCountBefore = + (await autumnV1.customers.get(customerId)).invoices + ?.length ?? 0; + + await autumnV1.products.update(pro.id, { + items: [ + items.monthlyPrice({ price: 20 }), + items.prepaidMessages({ + includedUsage: 100, + billingUnits: 100, + price: 10, + }), + ], + }); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + filter: { customer: { plan: { plan_id: pro.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: pro.id }, + version: 2, + }, + ], + }, + }); + + const customer = await autumnV2_2.customers.get(customerId); + await expectCustomerProducts({ customer, active: [pro.id] }); + expectBalanceCorrect({ + customer, + featureId: TestFeature.Messages, + remaining: 200, + usage: 0, + planId: pro.id, + }); + await expectCustomerInvoiceCorrect({ + customer: await autumnV1.customers.get(customerId), + count: invoiceCountBefore, + }); + await expectStripeSubscriptionCorrect({ ctx, customerId }); +}); diff --git a/server/tests/integration/billing/migrations-v2/utils/migrationTestUtils.ts b/server/tests/integration/billing/migrations-v2/utils/migrationTestUtils.ts new file mode 100644 index 000000000..610688eea --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/utils/migrationTestUtils.ts @@ -0,0 +1,81 @@ +import type { Migration } from "@autumn/shared"; +import type { MigrationFilter } from "@autumn/shared/api/migrations/filters/migrationFilter.js"; +import type { + MigrationUpdatePlanCustomize, + UpdatePlanOp, +} from "@autumn/shared/api/migrations/operations/customer/updatePlan/index.js"; +import type { Operations } from "@autumn/shared/api/migrations/operations/operations.js"; + +export type MigrationClient = { + migrationsV2: { + deleteAndCreate: (params: { + id: string; + filter?: MigrationFilter | null; + operations?: Operations | null; + }) => Promise; + update: (params: { + id: string; + updates: { operations?: Operations | null }; + }) => Promise; + }; +}; + +export const createMigration = async ({ + migrationClient, + id, + filter = { customer: { plan: { plan_id: "pro" } } }, + operations, +}: { + migrationClient: MigrationClient; + id: string; + filter?: MigrationFilter | null; + operations: Operations; +}) => + migrationClient.migrationsV2.deleteAndCreate({ + id, + filter, + operations, + }); + +export const updateMigrationOperations = ({ + migrationClient, + id, + operations, +}: { + migrationClient: MigrationClient; + id: string; + operations: Operations; +}) => + migrationClient.migrationsV2.update({ + id, + updates: { operations }, + }); + +export const buildUpdatePlanOperations = ({ + customize, + planId = "pro", + secondCustomize, + secondPlanId = "premium", +}: { + customize: MigrationUpdatePlanCustomize; + planId?: string; + secondCustomize?: MigrationUpdatePlanCustomize; + secondPlanId?: string; +}): Operations => ({ + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: planId }, + customize, + }, + ...(secondCustomize + ? [ + { + type: "update_plan", + plan_filter: { plan_id: secondPlanId }, + customize: secondCustomize, + } satisfies UpdatePlanOp, + ] + : []), + ], +}); diff --git a/server/tests/integration/billing/migrations-v2/utils/runMigrationPreview.ts b/server/tests/integration/billing/migrations-v2/utils/runMigrationPreview.ts new file mode 100644 index 000000000..4d578f298 --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/utils/runMigrationPreview.ts @@ -0,0 +1,97 @@ +import type { Migration } from "@autumn/shared"; +import type { MigrationFilter } from "@autumn/shared/api/migrations/filters/migrationFilter.js"; +import type { Operations } from "@autumn/shared/api/migrations/operations/operations.js"; + +type MigrationClient = { + migrationsV2: { + deleteAndCreate: (params: { + id: string; + filter?: MigrationFilter | null; + operations?: Operations | null; + }) => Promise; + run: (params: { id: string; dry_run?: boolean }) => Promise<{ + migration_id: string; + dry_run: boolean; + run_id: string; + }>; + listItemEvents: (params: { + migrationId: string; + migrationRunId?: string; + }) => Promise<{ list: MigrationItemEvent[] }>; + }; +}; + +type MigrationItemEvent = { + status: string; + dry_run: boolean; + item_id: string; + response: unknown; +}; + +type PreviewResult = { + status: string; + dryRun: boolean; + response: Record; +}; + +const timeout = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +function parseResponse(response: unknown): Record { + if (typeof response === "string") return JSON.parse(response); + if (response && typeof response === "object") + return response as Record; + throw new Error(`Invalid migration event response: ${String(response)}`); +} + +export const runMigrationAndWait = async ({ + migrationClient, + migrationId, + filter, + operations, + dryRun = true, + timeoutMs = 45_000, +}: { + migrationClient: MigrationClient; + migrationId: string; + filter: MigrationFilter; + operations: Operations; + dryRun?: boolean; + timeoutMs?: number; +}): Promise => { + const migration = await migrationClient.migrationsV2.deleteAndCreate({ + id: migrationId, + filter, + operations, + }); + const runResponse = await migrationClient.migrationsV2.run({ + id: migration.id, + dry_run: dryRun, + }); + + const start = Date.now(); + let lastError: unknown; + while (Date.now() - start < timeoutMs) { + try { + const events = await migrationClient.migrationsV2.listItemEvents({ + migrationId: migration.id, + migrationRunId: runResponse.run_id, + }); + const event = events.list[0]; + if (!event) throw new Error("No migration item event found"); + return { + status: event.status, + dryRun: event.dry_run, + response: parseResponse(event.response), + }; + } catch (error) { + lastError = error; + await timeout(1_000); + } + } + throw new Error( + `Timed out waiting for migration result: ${ + lastError instanceof Error ? lastError.message : String(lastError) + }`, + ); +}; diff --git a/server/tests/integration/billing/migrations-v2/utils/runUpdatePlanMigration.ts b/server/tests/integration/billing/migrations-v2/utils/runUpdatePlanMigration.ts new file mode 100644 index 000000000..0ae75278e --- /dev/null +++ b/server/tests/integration/billing/migrations-v2/utils/runUpdatePlanMigration.ts @@ -0,0 +1,114 @@ +import type { Migration } from "@autumn/shared"; +import type { MigrationFilter } from "@autumn/shared/api/migrations/filters/migrationFilter.js"; +import type { Operations } from "@autumn/shared/api/migrations/operations/operations.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { prepare } from "@/internal/migrations/v2/prepare/prepare.js"; +import { migrateCustomer } from "@/internal/migrations/v2/run/migrateCustomer/index.js"; + +type MigrationClient = { + migrationsV2: { + deleteAndCreate: (params: { + id: string; + filter?: MigrationFilter | null; + operations?: Operations | null; + }) => Promise; + run: (params: { id: string; dry_run?: boolean }) => Promise<{ + migration_id: string; + dry_run: boolean; + run_id: string; + }>; + }; +}; + +const timeout = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +export const waitForMigrationResult = async ({ + waitFor, + timeoutMs, + pollIntervalMs, +}: { + waitFor: () => Promise; + timeoutMs: number; + pollIntervalMs: number; +}) => { + const start = Date.now(); + let lastError: unknown; + + while (Date.now() - start < timeoutMs) { + try { + await waitFor(); + return; + } catch (error) { + lastError = error; + await timeout(pollIntervalMs); + } + } + + throw new Error( + `Timed out waiting for migration result after ${timeoutMs}ms: ${ + lastError instanceof Error ? lastError.message : String(lastError) + }`, + ); +}; + +export const runUpdatePlanMigration = async ({ + ctx, + migrationClient, + migrationId, + customerId, + filter, + operations, + runOnServer = true, + waitFor, + timeoutMs = 30_000, + pollIntervalMs = 1_000, +}: { + ctx: AutumnContext; + migrationClient: MigrationClient; + migrationId: string; + customerId: string; + filter: MigrationFilter; + operations: Operations; + runOnServer?: boolean; + waitFor?: () => Promise; + timeoutMs?: number; + pollIntervalMs?: number; +}) => { + const migration = await migrationClient.migrationsV2.deleteAndCreate({ + id: migrationId, + filter, + operations, + }); + + if (runOnServer) { + await migrationClient.migrationsV2.run({ + id: migrationId, + dry_run: false, + }); + + if (waitFor) { + await waitForMigrationResult({ waitFor, timeoutMs, pollIntervalMs }); + } else { + await timeout(timeoutMs); + } + + return migration; + } + + const { preparedState } = await prepare({ + ctx, + migration, + dryRun: false, + }); + + const preparedMigration = { ...migration, prepared_state: preparedState }; + + await migrateCustomer({ + ctx, + customerId, + migration: preparedMigration, + }); + + return preparedMigration; +}; diff --git a/server/tests/integration/billing/stripe-webhooks/subscription-created/sub-created-checkout-session-guard.test.ts b/server/tests/integration/billing/stripe-webhooks/subscription-created/sub-created-checkout-session-guard.test.ts new file mode 100644 index 000000000..98b396268 --- /dev/null +++ b/server/tests/integration/billing/stripe-webhooks/subscription-created/sub-created-checkout-session-guard.test.ts @@ -0,0 +1,97 @@ +import { expect, test } from "bun:test"; +import { completeStripeCheckoutFormV2 } from "@tests/utils/browserPool/completeStripeCheckoutFormV2"; +import { items } from "@tests/utils/fixtures/items"; +import { products } from "@tests/utils/fixtures/products"; +import { timeout } from "@tests/utils/genUtils"; +import ctx, { + type TestContext, +} from "@tests/utils/testInitUtils/createTestContext"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import { isAutumnCheckoutSubscription } from "@/internal/billing/v2/actions/sync/utils/isAutumnCheckoutSubscription"; +import { CusService } from "@/internal/customers/CusService"; + +const getLatestStripeSubscription = async ({ + ctx, + customerId, +}: { + ctx: TestContext; + customerId: string; +}): Promise => { + const fullCustomer = await CusService.getFull({ + ctx, + idOrInternalId: customerId, + }); + const stripeCustomerId = fullCustomer.processor?.id; + if (!stripeCustomerId) { + throw new Error(`Customer ${customerId} has no Stripe customer ID`); + } + const subs = await ctx.stripeCli.subscriptions.list({ + customer: stripeCustomerId, + limit: 1, + }); + const sub = subs.data[0]; + if (!sub) throw new Error(`Customer ${customerId} has no Stripe subs`); + return sub; +}; + +test(`${chalk.yellowBright("isAutumnCheckoutSubscription: true for sub from Autumn checkout")}`, async () => { + const customerId = "checkout-guard-positive"; + + const pro = products.pro({ + id: "pro", + items: [items.monthlyMessages({ includedUsage: 100 })], + }); + + const { autumnV1 } = await initScenario({ + customerId, + ctx, + setup: [s.customer({ testClock: true }), s.products({ list: [pro] })], + actions: [], + }); + + const attachResult = await autumnV1.billing.attach( + { customer_id: customerId, product_id: pro.id }, + { timeout: 0 }, + ); + expect(attachResult.payment_url).toContain("checkout.stripe.com"); + + await completeStripeCheckoutFormV2({ url: attachResult.payment_url! }); + await timeout(8000); + + const subscription = await getLatestStripeSubscription({ ctx, customerId }); + + const isFromCheckout = await isAutumnCheckoutSubscription({ + stripeCli: ctx.stripeCli, + subscription, + }); + expect(isFromCheckout).toBe(true); +}); + +test(`${chalk.yellowBright("isAutumnCheckoutSubscription: false for sub created directly")}`, async () => { + const customerId = "checkout-guard-negative"; + + const pro = products.pro({ + id: "pro", + items: [items.monthlyMessages({ includedUsage: 100 })], + }); + + await initScenario({ + customerId, + ctx, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [s.billing.attach({ productId: pro.id })], + }); + + const subscription = await getLatestStripeSubscription({ ctx, customerId }); + + const isFromCheckout = await isAutumnCheckoutSubscription({ + stripeCli: ctx.stripeCli, + subscription, + }); + expect(isFromCheckout).toBe(false); +}); diff --git a/server/tests/integration/billing/update-subscription/errors/cancel-errors.test.ts b/server/tests/integration/billing/update-subscription/errors/cancel-errors.test.ts index 7b4c6cc8f..9f1b5ba4f 100644 --- a/server/tests/integration/billing/update-subscription/errors/cancel-errors.test.ts +++ b/server/tests/integration/billing/update-subscription/errors/cancel-errors.test.ts @@ -5,12 +5,14 @@ */ import { expect, test } from "bun:test"; -import { type ApiCustomerV3, ErrCode, FreeTrialDuration } from "@autumn/shared"; +import { type ApiCustomerV3, FreeTrialDuration } from "@autumn/shared"; import { + expectCustomerProducts, expectProductCanceling, expectProductNotPresent, expectProductScheduled, } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect"; import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils"; import { items } from "@tests/utils/fixtures/items"; import { products } from "@tests/utils/fixtures/products"; @@ -18,20 +20,21 @@ import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; import chalk from "chalk"; // ═══════════════════════════════════════════════════════════════════════════════ -// TEST 1: Cannot cancel a scheduled product +// TEST 1: Cancel a scheduled product // ═══════════════════════════════════════════════════════════════════════════════ /** * Scenario: * - User is on Premium ($50/mo) * - User downgrades to Pro ($20/mo) → Premium is canceling, Pro is scheduled - * - User tries to cancel Pro (the scheduled product) + * - User cancels Pro (the scheduled product) * * Expected Result: - * - Should return an error - cannot cancel a scheduled product + * - Pro scheduled attachment is removed + * - Premium is uncanceled and remains active */ -test.concurrent(`${chalk.yellowBright("error: cannot cancel scheduled product")}`, async () => { - const customerId = "err-cancel-scheduled"; +test.concurrent(`${chalk.yellowBright("cancel: scheduled product removes pending schedule")}`, async () => { + const customerId = "cancel-scheduled-product"; const messagesItem = items.monthlyMessages({ includedUsage: 100 }); @@ -46,7 +49,7 @@ test.concurrent(`${chalk.yellowBright("error: cannot cancel scheduled product")} items: [messagesItem, premiumPriceItem], }); - const { autumnV1 } = await initScenario({ + const { autumnV1, ctx } = await initScenario({ customerId, setup: [ s.customer({ paymentMethod: "success" }), @@ -70,16 +73,24 @@ test.concurrent(`${chalk.yellowBright("error: cannot cancel scheduled product")} productId: pro.id, }); - // Try to cancel the scheduled product - should fail - await expectAutumnError({ - errCode: ErrCode.InvalidRequest, - func: async () => { - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: pro.id, - cancel_action: "cancel_immediately", - }); - }, + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: pro.id, + cancel_action: "cancel_immediately", + }); + + const customerAfterCancel = + await autumnV1.customers.get(customerId); + await expectCustomerProducts({ + customer: customerAfterCancel, + active: [premium.id], + notPresent: [pro.id], + }); + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, }); }); diff --git a/server/tests/unit/full-subject-cache/invalidateCachedFullSubject.test.ts b/server/tests/integration/others/redis/invalidate-cached-full-subject.test.ts similarity index 64% rename from server/tests/unit/full-subject-cache/invalidateCachedFullSubject.test.ts rename to server/tests/integration/others/redis/invalidate-cached-full-subject.test.ts index d1b50e16c..cfcd1144d 100644 --- a/server/tests/unit/full-subject-cache/invalidateCachedFullSubject.test.ts +++ b/server/tests/integration/others/redis/invalidate-cached-full-subject.test.ts @@ -11,12 +11,13 @@ import { buildFullSubjectKey, buildFullSubjectViewEpochKey, getCachedFullSubject, - getOrSetCachedFullSubject, invalidateCachedFullSubject, } from "@/internal/customers/cache/fullSubject/index.js"; -import { cleanupFullSubjectScenario } from "../../integration/db/full-subject/utils/cleanupFullSubjectScenario.js"; -import { buildEntitySubjectScenario } from "../../integration/db/full-subject/utils/fullSubjectScenarioBuilders.js"; -import { insertFullSubjectScenario } from "../../integration/db/full-subject/utils/insertFullSubjectScenario.js"; +import { normalizedToCachedFullSubject } from "@/internal/customers/cache/fullSubject/fullSubjectCacheModel.js"; +import { getFullSubjectNormalized } from "@/internal/customers/repos/getFullSubject/index.js"; +import { cleanupFullSubjectScenario } from "../../db/full-subject/utils/cleanupFullSubjectScenario.js"; +import { buildEntitySubjectScenario } from "../../db/full-subject/utils/fullSubjectScenarioBuilders.js"; +import { insertFullSubjectScenario } from "../../db/full-subject/utils/insertFullSubjectScenario.js"; const describeDb = process.env.TESTS_ORG ? describe : describe.skip; @@ -24,6 +25,47 @@ describeDb("invalidateCachedFullSubject", () => { let ctx: TestContext; let scenario: ReturnType; + const cleanupScenarioState = async () => { + const customerKeys = await ctx.redisV2.keys(`{${scenario.ids.customerId}}:*`); + if (customerKeys.length > 0) await ctx.redisV2.unlink(...customerKeys); + + await cleanupFullSubjectScenario({ ctx, scenario }); + }; + + const getSubjectViewEpoch = async () => { + const epoch = await ctx.redisV2.get( + buildFullSubjectViewEpochKey({ + orgId: ctx.org.id, + env: ctx.env, + customerId: scenario.ids.customerId, + }), + ); + + return epoch ? Number.parseInt(epoch, 10) : 0; + }; + + const seedCachedFullSubject = async ({ entityId }: { entityId?: string } = {}) => { + const result = await getFullSubjectNormalized({ + ctx, + customerId: scenario.ids.customerId, + entityId, + }); + if (!result) throw new Error("Failed to build full subject cache fixture"); + + const cached = normalizedToCachedFullSubject({ + normalized: result.normalized, + subjectViewEpoch: await getSubjectViewEpoch(), + }); + const subjectKey = buildFullSubjectKey({ + orgId: ctx.org.id, + env: ctx.env, + customerId: scenario.ids.customerId, + entityId, + }); + + await ctx.redisV2.set(subjectKey, JSON.stringify(cached)); + }; + beforeAll(async () => { const { createTestContext } = await import( "@tests/utils/testInitUtils/createTestContext.js" @@ -36,36 +78,19 @@ describeDb("invalidateCachedFullSubject", () => { }); beforeEach(async () => { + await cleanupScenarioState(); await insertFullSubjectScenario({ ctx, scenario }); - await getOrSetCachedFullSubject({ - ctx, - customerId: scenario.ids.customerId, - source: "invalidateCachedFullSubjectTest", - }); - await getOrSetCachedFullSubject({ - ctx, - customerId: scenario.ids.customerId, - entityId: scenario.ids.entityIds[0], - source: "invalidateCachedFullSubjectTest", - }); - await getOrSetCachedFullSubject({ - ctx, - customerId: scenario.ids.customerId, - entityId: scenario.ids.entityIds[1], - source: "invalidateCachedFullSubjectTest", - }); + await seedCachedFullSubject(); + await seedCachedFullSubject({ entityId: scenario.ids.entityIds[0] }); + await seedCachedFullSubject({ entityId: scenario.ids.entityIds[1] }); }); afterEach(async () => { - const customerKeys = await ctx.redisV2.keys(`{${scenario.ids.customerId}}:*`); - if (customerKeys.length > 0) { - await ctx.redisV2.unlink(...customerKeys); - } - - await cleanupFullSubjectScenario({ ctx, scenario }); + await cleanupScenarioState(); }); test("invalidates direct entity cache and increments subject view epoch", async () => { + const initialSubjectViewEpoch = await getSubjectViewEpoch(); const entityAKey = buildFullSubjectKey({ orgId: ctx.org.id, env: ctx.env, @@ -93,18 +118,11 @@ describeDb("invalidateCachedFullSubject", () => { expect(await ctx.redisV2.exists(customerKey)).toBe(0); expect(await ctx.redisV2.exists(entityAKey)).toBe(0); expect(await ctx.redisV2.exists(entityBKey)).toBe(1); - expect( - await ctx.redisV2.get( - buildFullSubjectViewEpochKey({ - orgId: ctx.org.id, - env: ctx.env, - customerId: scenario.ids.customerId, - }), - ), - ).toBe("1"); + expect(await getSubjectViewEpoch()).toBe(initialSubjectViewEpoch + 1); }); test("increments subject view epoch for customer invalidation", async () => { + const initialSubjectViewEpoch = await getSubjectViewEpoch(); const entityAKey = buildFullSubjectKey({ orgId: ctx.org.id, env: ctx.env, @@ -131,7 +149,9 @@ describeDb("invalidateCachedFullSubject", () => { expect(await ctx.redisV2.exists(entityAKey)).toBe(1); expect(await ctx.redisV2.exists(entityBKey)).toBe(1); - expect(await ctx.redisV2.get(epochKey)).toBe("1"); + expect(Number.parseInt((await ctx.redisV2.get(epochKey)) ?? "0", 10)).toBe( + initialSubjectViewEpoch + 1, + ); }); test("sibling entity cache becomes stale after direct entity invalidation", async () => { diff --git a/server/tests/integration/others/secret-key-verification/secret-key-verification.test.ts b/server/tests/integration/others/secret-key-verification/secret-key-verification.test.ts new file mode 100644 index 000000000..352388a2b --- /dev/null +++ b/server/tests/integration/others/secret-key-verification/secret-key-verification.test.ts @@ -0,0 +1,179 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { MigrationRunStatus, migrationRuns } from "@autumn/shared"; +import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import { and, eq } from "drizzle-orm"; +import { hashApiKey, verifyKey } from "@/internal/dev/api-keys/apiKeyUtils.js"; +import { clearSecretKeyCache } from "@/internal/dev/api-keys/cacheApiKeyUtils.js"; +import { + migrationRepo, + migrationRunRepo, +} from "@/internal/migrations/v2/repos/index.js"; + +const MIGRATION_ID = "secret-key-verification-test-migration"; + +describe("secret-key-verification: verifyKey shape (DB + cache)", () => { + const hashedKey = hashApiKey(ctx.orgSecretKey); + + const cleanupDbState = async () => { + // Remove any active lazy/background runs we might leave behind on + // re-runs. The FK on migrations.internal_id → migration_runs cascades, + // but we delete by id-scoped to org/env so we have to do it explicitly. + await ctx.db + .delete(migrationRuns) + .where( + and( + eq(migrationRuns.org_id, ctx.org.id), + eq(migrationRuns.env, ctx.env), + ), + ); + + await migrationRepo.delete({ ctx, id: MIGRATION_ID }).catch(() => null); + }; + + beforeAll(async () => { + await cleanupDbState(); + await clearSecretKeyCache({ hashedKey }); + }); + + afterAll(async () => { + await cleanupDbState(); + await clearSecretKeyCache({ hashedKey }); + }); + + test("loads org + features with no pending migrations on first call (DB)", async () => { + const { valid, data } = await verifyKey({ + db: ctx.db, + key: ctx.orgSecretKey, + }); + + expect(valid).toBe(true); + expect(data).not.toBeNull(); + if (!data) throw new Error("verifyKey returned no data"); + + expect(data.org.id).toBe(ctx.org.id); + expect(data.env).toBe(ctx.env); + expect(Array.isArray(data.features)).toBe(true); + expect(data.features.length).toBe(ctx.features.length); + expect(data.pendingMigrations).toEqual([]); + expect(data.org.pendingMigrations).toEqual([]); + }); + + test("second call returns identical shape from cache", async () => { + const { valid, data } = await verifyKey({ + db: ctx.db, + key: ctx.orgSecretKey, + }); + + expect(valid).toBe(true); + if (!data) throw new Error("verifyKey returned no data"); + + expect(data.org.id).toBe(ctx.org.id); + expect(data.features.length).toBe(ctx.features.length); + expect(data.pendingMigrations).toEqual([]); + expect(data.org.pendingMigrations).toEqual([]); + }); + + test("after seeding a lazy migration_run, pendingMigrations surfaces it (DB + cache)", async () => { + const migration = await migrationRepo.insert({ + ctx, + insert: { + id: MIGRATION_ID, + filter: null, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: "noop" }, + version: 1, + }, + ], + }, + }, + }); + + const run = await migrationRunRepo.insert({ + ctx, + insert: { + migration_internal_id: migration.internal_id, + dry_run: false, + lazy_run: true, + }, + }); + expect(run).not.toBeNull(); + if (!run) throw new Error("failed to insert lazy migration_run"); + + // Bust the cached payload so verifyKey re-loads from DB. + await clearSecretKeyCache({ hashedKey }); + + const fromDb = await verifyKey({ db: ctx.db, key: ctx.orgSecretKey }); + expect(fromDb.valid).toBe(true); + if (!fromDb.data) throw new Error("verifyKey returned no data"); + + expect(fromDb.data.pendingMigrations).toHaveLength(1); + expect(fromDb.data.pendingMigrations[0]?.internal_id).toBe(run.internal_id); + expect(fromDb.data.pendingMigrations[0]?.lazy_run).toBe(true); + const status = fromDb.data.pendingMigrations[0]?.status; + expect( + status === MigrationRunStatus.Queued || + status === MigrationRunStatus.Running, + ).toBe(true); + expect(fromDb.data.pendingMigrations[0]?.migration.id).toBe(MIGRATION_ID); + expect(fromDb.data.org.pendingMigrations).toHaveLength(1); + expect(fromDb.data.org.pendingMigrations?.[0]?.migration.id).toBe( + MIGRATION_ID, + ); + + // Second call hits cache — should be the same shape (deep-equal). + const fromCache = await verifyKey({ db: ctx.db, key: ctx.orgSecretKey }); + expect(fromCache.valid).toBe(true); + if (!fromCache.data) throw new Error("verifyKey returned no cached data"); + + expect(fromCache.data.pendingMigrations).toEqual( + fromDb.data.pendingMigrations, + ); + expect(fromCache.data.org.pendingMigrations).toEqual( + fromDb.data.org.pendingMigrations, + ); + }); + + test("non-lazy (background) runs do not surface on pendingMigrations", async () => { + // Wipe state, seed a fresh migration with a background-mode run. + await cleanupDbState(); + await clearSecretKeyCache({ hashedKey }); + + const migration = await migrationRepo.insert({ + ctx, + insert: { + id: MIGRATION_ID, + filter: null, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: "noop" }, + version: 1, + }, + ], + }, + }, + }); + + const run = await migrationRunRepo.insert({ + ctx, + insert: { + migration_internal_id: migration.internal_id, + dry_run: false, + lazy_run: false, + }, + }); + expect(run).not.toBeNull(); + + const { data } = await verifyKey({ + db: ctx.db, + key: ctx.orgSecretKey, + }); + + expect(data?.pendingMigrations).toEqual([]); + expect(data?.org.pendingMigrations).toEqual([]); + }); +}); diff --git a/server/tests/unit/billing/update-subscription/compute-update-subscription-intent.spec.ts b/server/tests/unit/billing/update-subscription/compute-update-subscription-intent.spec.ts index 57cbfba8f..eb582e773 100644 --- a/server/tests/unit/billing/update-subscription/compute-update-subscription-intent.spec.ts +++ b/server/tests/unit/billing/update-subscription/compute-update-subscription-intent.spec.ts @@ -17,7 +17,8 @@ import chalk from "chalk"; import { setupUpdateSubscriptionIntent } from "@/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionIntent"; const customerProduct = { - prices: [], + customer_prices: [], + customer_entitlements: [], } as unknown as FullCusProduct; const baseParams: UpdateSubscriptionV1Params = { diff --git a/server/tests/unit/compiler/customer/basic.test.ts b/server/tests/unit/compiler/customer/basic.test.ts new file mode 100644 index 000000000..ea305dd0a --- /dev/null +++ b/server/tests/unit/compiler/customer/basic.test.ts @@ -0,0 +1,161 @@ +import { describe, expect, test } from "bun:test"; +import type { Feature } from "@autumn/shared"; +import { compileFilter } from "@autumn/shared/api/migrations/compiler/compileFilter.js"; +import { contexts } from "@tests/utils/fixtures/db/contexts"; + +const features: Feature[] = [ + { id: "credits", internal_id: "fea_credits_internal" } as Feature, +]; + +const ctx = contexts.create({ features }); +const ambient = { orgId: "org_test", env: "live" }; + +const ROOT_AMBIENT = "c.org_id = ? AND c.env = ?"; +const PLAN_AMBIENT = "cp.status IN (?, ?)"; +const PLAN_AMBIENT_PARAMS = ["active", "past_due"]; + +const normalize = (sql: string) => + sql.replace(/\s+/g, " ").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").trim(); + +const BASE_PRICE_EXISTS = [ + "(SELECT base_cpr.id FROM customer_prices base_cpr", + "JOIN prices base_pr ON base_pr.id = base_cpr.price_id", + "WHERE base_cpr.customer_product_id = cp.id", + "AND base_pr.entitlement_id IS NULL LIMIT 1)", +].join(" "); + +describe("compileFilter — customer / basic plan-level filters", () => { + test("customer.customer_id $in", () => { + const result = compileFilter({ + filter: { customer_id: { $in: ["cus_a", "cus_b"] } }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(`${ROOT_AMBIENT} AND c.id IN (?, ?)`), + ); + expect(result.params).toEqual(["org_test", "live", "cus_a", "cus_b"]); + }); + + test("plan.plan_id eq", () => { + const result = compileFilter({ + filter: { plan: { plan_id: "pro" } }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND p.id = ? + ) + `), + ); + expect(result.params).toEqual([ + "org_test", + "live", + ...PLAN_AMBIENT_PARAMS, + "pro", + ]); + }); + + test("plan.addon false", () => { + const result = compileFilter({ + filter: { plan: { addon: false } }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND p.is_add_on = ? + ) + `), + ); + expect(result.params).toEqual([ + "org_test", + "live", + ...PLAN_AMBIENT_PARAMS, + false, + ]); + }); + + test("paid plan — plan.price: { $ne: null }", () => { + const result = compileFilter({ + filter: { plan: { price: { $ne: null } } }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND ${BASE_PRICE_EXISTS} IS NOT NULL + ) + `), + ); + expect(result.params).toEqual(["org_test", "live", ...PLAN_AMBIENT_PARAMS]); + }); + + test("free plan — plan.price: null (bare)", () => { + const result = compileFilter({ + filter: { plan: { price: null } }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND ${BASE_PRICE_EXISTS} IS NULL + ) + `), + ); + expect(result.params).toEqual(["org_test", "live", ...PLAN_AMBIENT_PARAMS]); + }); + + test("plan.plan_id $in", () => { + const result = compileFilter({ + filter: { plan: { plan_id: { $in: ["pro", "team"] } } }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND p.id IN (?, ?) + ) + `), + ); + expect(result.params).toEqual([ + "org_test", + "live", + ...PLAN_AMBIENT_PARAMS, + "pro", + "team", + ]); + }); +}); diff --git a/server/tests/unit/compiler/customer/derived-and-or.test.ts b/server/tests/unit/compiler/customer/derived-and-or.test.ts new file mode 100644 index 000000000..273c20e93 --- /dev/null +++ b/server/tests/unit/compiler/customer/derived-and-or.test.ts @@ -0,0 +1,191 @@ +import { describe, expect, test } from "bun:test"; +import type { Feature } from "@autumn/shared"; +import { compileFilter } from "@autumn/shared/api/migrations/compiler/compileFilter.js"; +import { contexts } from "@tests/utils/fixtures/db/contexts"; + +const features: Feature[] = [ + { id: "credits", internal_id: "fea_credits_internal" } as Feature, +]; + +const ctx = contexts.create({ features }); +const ambient = { orgId: "org_test", env: "live" }; + +const ROOT_AMBIENT = "c.org_id = ? AND c.env = ?"; +const PLAN_AMBIENT = "cp.status IN (?, ?)"; +const PLAN_AMBIENT_PARAMS = ["active", "past_due"]; + +const BASE_PRICE_EXISTS = [ + "(SELECT base_cpr.id FROM customer_prices base_cpr", + "JOIN prices base_pr ON base_pr.id = base_cpr.price_id", + "WHERE base_cpr.customer_product_id = cp.id", + "AND base_pr.entitlement_id IS NULL LIMIT 1)", +].join(" "); + +const PAID_EXISTS = + "EXISTS (SELECT 1 FROM customer_prices cpr WHERE cpr.customer_product_id = cp.id)"; + +const RECURRING_EXISTS = [ + "EXISTS (SELECT 1 FROM customer_prices cpr", + "JOIN prices pr ON pr.id = cpr.price_id", + "WHERE cpr.customer_product_id = cp.id", + "AND pr.config->>'interval' <> 'one_off')", +].join(" "); + +const ITEM_PAID_FROM = [ + "customer_prices cpr", + "JOIN prices pr ON pr.id = cpr.price_id", + "JOIN entitlements e ON e.id = pr.entitlement_id", +].join(" "); + +const normalize = (sql: string) => + sql.replace(/\s+/g, " ").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").trim(); + +describe("compileFilter — derived boolean filters", () => { + test("plan.paid: true → EXISTS over customer_prices", () => { + const result = compileFilter({ + filter: { plan: { paid: true } }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND ${PAID_EXISTS} = ? + ) + `), + ); + expect(result.params).toEqual([ + "org_test", + "live", + ...PLAN_AMBIENT_PARAMS, + true, + ]); + }); + + test("plan.paid: false → EXISTS = false (i.e. no customer_prices)", () => { + const result = compileFilter({ + filter: { plan: { paid: false } }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(result.params).toEqual([ + "org_test", + "live", + ...PLAN_AMBIENT_PARAMS, + false, + ]); + }); + + test("plan.recurring: true → EXISTS with interval <> 'one_off'", () => { + const result = compileFilter({ + filter: { plan: { recurring: true } }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND ${RECURRING_EXISTS} = ? + ) + `), + ); + expect(result.params).toEqual([ + "org_test", + "live", + ...PLAN_AMBIENT_PARAMS, + true, + ]); + }); +}); + +describe("compileFilter — $or operator on plan", () => { + test("$or: [{ price: $ne: null }, { item.price: $ne: null }] — base OR paid item", () => { + const result = compileFilter({ + filter: { + plan: { + $or: [{ price: { $ne: null } }, { item: { price: { $ne: null } } }], + }, + }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND ( + ${BASE_PRICE_EXISTS} IS NOT NULL + OR EXISTS ( + SELECT 1 + FROM ${ITEM_PAID_FROM} + WHERE cpr.customer_product_id = cp.id + AND cpr.id IS NOT NULL + ) + ) + ) + `), + ); + expect(result.params).toEqual(["org_test", "live", ...PLAN_AMBIENT_PARAMS]); + }); + + test("$or alongside sibling field — sibling is AND'd with the OR group", () => { + const result = compileFilter({ + filter: { + plan: { + plan_id: "pro", + $or: [{ paid: true }, { recurring: true }], + }, + }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND ( + p.id = ? + AND (${PAID_EXISTS} = ? OR ${RECURRING_EXISTS} = ?) + ) + ) + `), + ); + expect(result.params).toEqual([ + "org_test", + "live", + ...PLAN_AMBIENT_PARAMS, + "pro", + true, + true, + ]); + }); + + test("empty $or array throws", () => { + expect(() => + compileFilter({ + filter: { plan: { $or: [] } }, + ctx: { features: ctx.features }, + ambient, + }), + ).toThrow("$or requires at least one branch"); + }); +}); diff --git a/server/tests/unit/compiler/customer/nested-item.test.ts b/server/tests/unit/compiler/customer/nested-item.test.ts new file mode 100644 index 000000000..3414b9790 --- /dev/null +++ b/server/tests/unit/compiler/customer/nested-item.test.ts @@ -0,0 +1,169 @@ +import { describe, expect, test } from "bun:test"; +import type { Feature } from "@autumn/shared"; +import { compileFilter } from "@autumn/shared/api/migrations/compiler/compileFilter.js"; +import { contexts } from "@tests/utils/fixtures/db/contexts"; + +const features: Feature[] = [ + { id: "credits", internal_id: "fea_credits_internal" } as Feature, +]; + +const ctx = contexts.create({ features }); +const ambient = { orgId: "org_test", env: "live" }; + +const ROOT_AMBIENT = "c.org_id = ? AND c.env = ?"; +const PLAN_AMBIENT = "cp.status IN (?, ?)"; +const PLAN_AMBIENT_PARAMS = ["active", "past_due"]; + +const ITEM_FROM = [ + "customer_entitlements ce", + "JOIN entitlements e ON e.id = ce.entitlement_id", + "LEFT JOIN prices pr ON pr.entitlement_id = e.id", + "LEFT JOIN customer_prices cpr ON cpr.price_id = pr.id AND cpr.customer_product_id = ce.customer_product_id", +].join(" "); + +const normalize = (sql: string) => + sql.replace(/\s+/g, " ").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").trim(); + +describe("compileFilter — customer / nested item filters", () => { + test("plan.item.feature_id eq — resolves through entitlements", () => { + const result = compileFilter({ + filter: { plan: { item: { feature_id: "credits" } } }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND EXISTS ( + SELECT 1 + FROM ${ITEM_FROM} + WHERE ce.customer_product_id = cp.id + AND e.internal_feature_id = ? + ) + ) + `), + ); + expect(result.params).toEqual([ + "org_test", + "live", + ...PLAN_AMBIENT_PARAMS, + "fea_credits_internal", + ]); + }); + + test("paid credits — uses paid-side join from customer_prices", () => { + const result = compileFilter({ + filter: { + plan: { item: { feature_id: "credits", price: { $ne: null } } }, + }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND EXISTS ( + SELECT 1 + FROM customer_prices cpr JOIN prices pr ON pr.id = cpr.price_id JOIN entitlements e ON e.id = pr.entitlement_id + WHERE cpr.customer_product_id = cp.id + AND (e.internal_feature_id = ? AND cpr.id IS NOT NULL) + ) + ) + `), + ); + expect(result.params).toEqual([ + "org_test", + "live", + ...PLAN_AMBIENT_PARAMS, + "fea_credits_internal", + ]); + }); + + test("free credits — feature_id + price: null (bare)", () => { + const result = compileFilter({ + filter: { plan: { item: { feature_id: "credits", price: null } } }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND EXISTS ( + SELECT 1 + FROM ${ITEM_FROM} + WHERE ce.customer_product_id = cp.id + AND (e.internal_feature_id = ? AND cpr.id IS NULL) + ) + ) + `), + ); + expect(result.params).toEqual([ + "org_test", + "live", + ...PLAN_AMBIENT_PARAMS, + "fea_credits_internal", + ]); + }); + + test("plan_id + nested item.feature_id — AND on plan scope", () => { + const result = compileFilter({ + filter: { + plan: { plan_id: "pro", item: { feature_id: "credits" } }, + }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND ( + p.id = ? + AND EXISTS ( + SELECT 1 + FROM ${ITEM_FROM} + WHERE ce.customer_product_id = cp.id + AND e.internal_feature_id = ? + ) + ) + ) + `), + ); + expect(result.params).toEqual([ + "org_test", + "live", + ...PLAN_AMBIENT_PARAMS, + "pro", + "fea_credits_internal", + ]); + }); + + test("unknown feature_id throws at parse", () => { + expect(() => + compileFilter({ + filter: { plan: { item: { feature_id: "nonexistent" } } }, + ctx: { features: ctx.features }, + ambient, + }), + ).toThrow("Unknown feature_id: nonexistent"); + }); +}); diff --git a/server/tests/unit/compiler/customer/rollover-existence.test.ts b/server/tests/unit/compiler/customer/rollover-existence.test.ts new file mode 100644 index 000000000..976ec97a6 --- /dev/null +++ b/server/tests/unit/compiler/customer/rollover-existence.test.ts @@ -0,0 +1,242 @@ +import { describe, expect, test } from "bun:test"; +import type { Feature } from "@autumn/shared"; +import { compileFilter } from "@autumn/shared/api/migrations/compiler/compileFilter.js"; +import { contexts } from "@tests/utils/fixtures/db/contexts"; + +const features: Feature[] = [ + { id: "credits", internal_id: "fea_credits_internal" } as Feature, +]; + +const ctx = contexts.create({ features }); +const ambient = { orgId: "org_test", env: "live" }; + +const ROOT_AMBIENT = "c.org_id = ? AND c.env = ?"; +const PLAN_AMBIENT = "cp.status IN (?, ?)"; +const PLAN_AMBIENT_PARAMS = ["active", "past_due"]; + +const ITEM_FROM = [ + "customer_entitlements ce", + "JOIN entitlements e ON e.id = ce.entitlement_id", + "LEFT JOIN prices pr ON pr.entitlement_id = e.id", + "LEFT JOIN customer_prices cpr ON cpr.price_id = pr.id AND cpr.customer_product_id = ce.customer_product_id", +].join(" "); + +const ITEM_PAID_FROM = + "customer_prices cpr JOIN prices pr ON pr.id = cpr.price_id JOIN entitlements e ON e.id = pr.entitlement_id"; + +const normalize = (sql: string) => + sql.replace(/\s+/g, " ").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").trim(); + +describe("compileFilter — plan.item.rollover existence", () => { + test("rollover: { $ne: null } — IS NOT NULL", () => { + const result = compileFilter({ + filter: { plan: { item: { rollover: { $ne: null } } } }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND EXISTS ( + SELECT 1 + FROM ${ITEM_FROM} + WHERE ce.customer_product_id = cp.id + AND e.rollover IS NOT NULL + ) + ) + `), + ); + expect(result.params).toEqual(["org_test", "live", ...PLAN_AMBIENT_PARAMS]); + }); + + test("rollover: null (bare) — IS NULL", () => { + const result = compileFilter({ + filter: { plan: { item: { rollover: null } } }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND EXISTS ( + SELECT 1 + FROM ${ITEM_FROM} + WHERE ce.customer_product_id = cp.id + AND e.rollover IS NULL + ) + ) + `), + ); + expect(result.params).toEqual(["org_test", "live", ...PLAN_AMBIENT_PARAMS]); + }); + + test("feature_id + rollover — AND'd inside item scope", () => { + const result = compileFilter({ + filter: { + plan: { + item: { feature_id: "credits", rollover: { $ne: null } }, + }, + }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND EXISTS ( + SELECT 1 + FROM ${ITEM_FROM} + WHERE ce.customer_product_id = cp.id + AND (e.internal_feature_id = ? AND e.rollover IS NOT NULL) + ) + ) + `), + ); + expect(result.params).toEqual([ + "org_test", + "live", + ...PLAN_AMBIENT_PARAMS, + "fea_credits_internal", + ]); + }); + + test("rollover works alongside paid-only scope variant", () => { + const result = compileFilter({ + filter: { + plan: { + item: { price: { $ne: null }, rollover: { $ne: null } }, + }, + }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND EXISTS ( + SELECT 1 + FROM ${ITEM_PAID_FROM} + WHERE cpr.customer_product_id = cp.id + AND (cpr.id IS NOT NULL AND e.rollover IS NOT NULL) + ) + ) + `), + ); + expect(result.params).toEqual(["org_test", "live", ...PLAN_AMBIENT_PARAMS]); + }); + + test("nested-field filtering on rollover throws", () => { + expect(() => + compileFilter({ + filter: { + plan: { item: { rollover: { max: { $ne: null } } as never } }, + }, + ctx: { features: ctx.features }, + ambient, + }), + ).toThrow( + "plan.item.rollover filtering on nested fields is not supported in phase 1", + ); + }); +}); + +describe("compileFilter — customer.item shortcut", () => { + test("customer.item.X compiles identically to customer.plan.item.X", () => { + const shortcut = compileFilter({ + filter: { item: { rollover: { $ne: null } } }, + ctx: { features: ctx.features }, + ambient, + }); + + const full = compileFilter({ + filter: { plan: { item: { rollover: { $ne: null } } } }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(shortcut.sql)).toBe(normalize(full.sql)); + expect(shortcut.params).toEqual(full.params); + }); + + test("customer.item AND'd with sibling customer_id", () => { + const result = compileFilter({ + filter: { + customer_id: "cus_x", + item: { feature_id: "credits" }, + }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(` + ${ROOT_AMBIENT} AND ( + c.id = ? + AND EXISTS ( + SELECT 1 + FROM customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id + WHERE cp.internal_customer_id = c.internal_id + AND ${PLAN_AMBIENT} + AND EXISTS ( + SELECT 1 + FROM ${ITEM_FROM} + WHERE ce.customer_product_id = cp.id + AND e.internal_feature_id = ? + ) + ) + ) + `), + ); + expect(result.params).toEqual([ + "org_test", + "live", + "cus_x", + ...PLAN_AMBIENT_PARAMS, + "fea_credits_internal", + ]); + }); + + test("customer.item sibling with customer.plan — both AND'd", () => { + const result = compileFilter({ + filter: { + plan: { plan_id: "pro" }, + item: { feature_id: "credits" }, + }, + ctx: { features: ctx.features }, + ambient, + }); + + // Two separate plan-EXISTS clauses — one for `plan_id`, one wrapping + // the item shortcut. AND'd at customer scope. + expect(normalize(result.sql)).toContain("p.id = ?"); + expect(normalize(result.sql)).toContain("e.internal_feature_id = ?"); + expect(result.params).toEqual([ + "org_test", + "live", + ...PLAN_AMBIENT_PARAMS, + "pro", + ...PLAN_AMBIENT_PARAMS, + "fea_credits_internal", + ]); + }); +}); diff --git a/server/tests/unit/compiler/plan/basic.test.ts b/server/tests/unit/compiler/plan/basic.test.ts new file mode 100644 index 000000000..dc557a0d8 --- /dev/null +++ b/server/tests/unit/compiler/plan/basic.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "bun:test"; +import { compilePlanFilter } from "@autumn/shared/api/migrations/compiler/compilePlanFilter.js"; +import { contexts } from "@tests/utils/fixtures/db/contexts"; + +const ctx = contexts.create({ features: [] }); +const ambient = { orgId: "org_test", env: "live" }; + +const PLAN_AMBIENT = "p.org_id = ? AND p.env = ?"; + +const normalize = (sql: string) => + sql.replace(/\s+/g, " ").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").trim(); + +describe("compilePlanFilter — basic plan-rooted filters", () => { + test("plan_id eq", () => { + const result = compilePlanFilter({ + filter: { plan_id: "pro" }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(`${PLAN_AMBIENT} AND p.id = ?`), + ); + expect(result.params).toEqual(["org_test", "live", "pro"]); + }); + + test("plan_id $in", () => { + const result = compilePlanFilter({ + filter: { plan_id: { $in: ["pro", "team"] } }, + ctx: { features: ctx.features }, + ambient, + }); + + expect(normalize(result.sql)).toBe( + normalize(`${PLAN_AMBIENT} AND p.id IN (?, ?)`), + ); + expect(result.params).toEqual(["org_test", "live", "pro", "team"]); + }); +}); diff --git a/server/tests/unit/migrations-v2/compiler/none-quantifier.test.ts b/server/tests/unit/migrations-v2/compiler/none-quantifier.test.ts new file mode 100644 index 000000000..5b4107ab9 --- /dev/null +++ b/server/tests/unit/migrations-v2/compiler/none-quantifier.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, test } from "bun:test"; +import { filterToIr } from "@autumn/shared/api/migrations/compiler/filterToIr/filterToIr.js"; +import type { ResolutionContext } from "@autumn/shared/api/migrations/compiler/filterToIr/resolutionContext.js"; +import { irToSql } from "@autumn/shared/api/migrations/compiler/irToSql/irToSql.js"; +import { customerRegistry } from "@autumn/shared/api/migrations/compiler/registry/customerRegistry.js"; +import type { CustomerFilter } from "@autumn/shared/api/migrations/filters/customerFilter.js"; + +const ctx: ResolutionContext = { features: [] }; +const ambient = { orgId: "org_test", env: "sandbox" }; + +function compile(filter: CustomerFilter) { + const ir = filterToIr({ filter, ctx }); + return irToSql({ ir, root: customerRegistry, ambient }); +} + +describe("$none quantifier", () => { + test("$none with empty filter selects customers with no active plans", () => { + const { sql } = compile({ plan: { $none: {} } }); + expect(sql).toContain("NOT EXISTS"); + }); + + test("string shorthand '$none' is equivalent to { $none: {} }", () => { + const full = compile({ plan: { $none: {} } }); + const shorthand = compile({ plan: "$none" }); + expect(shorthand.sql).toBe(full.sql); + expect(shorthand.params).toEqual(full.params); + }); + + test("$none with plan_id filter selects customers without that plan", () => { + const { sql, params } = compile({ + plan: { $none: { plan_id: "pro" } }, + }); + expect(sql).toContain("NOT EXISTS"); + expect(sql).toContain("p.id = ?"); + expect(params).toContain("pro"); + }); + + test("$some still produces EXISTS", () => { + const { sql } = compile({ + plan: { $some: { plan_id: "pro" } }, + }); + expect(sql).not.toContain("NOT EXISTS"); + expect(sql).toContain("EXISTS"); + }); + + test("bare plan filter (implicit $some) produces EXISTS", () => { + const { sql } = compile({ + plan: { plan_id: "pro" }, + }); + expect(sql).not.toContain("NOT EXISTS"); + expect(sql).toContain("EXISTS"); + }); +}); diff --git a/server/tests/unit/migrations-v2/create-migration-stripe-cache.test.ts b/server/tests/unit/migrations-v2/create-migration-stripe-cache.test.ts new file mode 100644 index 000000000..e7b94824b --- /dev/null +++ b/server/tests/unit/migrations-v2/create-migration-stripe-cache.test.ts @@ -0,0 +1,66 @@ +import { beforeEach, describe, expect, mock, test } from "bun:test"; +import type { FullCustomer } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; + +const state = { + customerFetchArgs: [] as unknown[], +}; + +mock.module( + "@/internal/billing/v2/providers/stripe/setup/fetchStripeCustomerForBilling.js", + () => ({ + fetchStripeCustomerForBilling: async (args: unknown) => { + state.customerFetchArgs.push(args); + return { + stripeCus: undefined, + paymentMethod: undefined, + testClockFrozenTime: undefined, + }; + }, + }), +); + +import { createMigrationStripeCache } from "@/internal/migrations/v2/stripeCache/createMigrationStripeCache"; + +const ctx = {} as AutumnContext; +const fullCustomer = { + id: "cus_1", + internal_id: "cus_internal", +} as FullCustomer; + +describe("createMigrationStripeCache", () => { + beforeEach(() => { + state.customerFetchArgs = []; + }); + + test("passes read-only customer fetch mode when Stripe customer creation is disabled", async () => { + const cache = createMigrationStripeCache({ + ctx, + fullCustomer, + allowStripeCustomerCreation: false, + }); + + await cache.getStripeCustomer(); + + expect(state.customerFetchArgs).toHaveLength(1); + expect(state.customerFetchArgs[0]).toMatchObject({ + ctx, + fullCus: fullCustomer, + createIfMissing: false, + }); + }); + + test("allows Stripe customer creation by default", async () => { + const cache = createMigrationStripeCache({ + ctx, + fullCustomer, + }); + + await cache.getStripeCustomer(); + + expect(state.customerFetchArgs).toHaveLength(1); + expect(state.customerFetchArgs[0]).toMatchObject({ + createIfMissing: true, + }); + }); +}); diff --git a/server/tests/unit/migrations-v2/hooks/compose-migration-hooks.test.ts b/server/tests/unit/migrations-v2/hooks/compose-migration-hooks.test.ts new file mode 100644 index 000000000..b26ba1d63 --- /dev/null +++ b/server/tests/unit/migrations-v2/hooks/compose-migration-hooks.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, test } from "bun:test"; +import type { FullCustomer } from "@autumn/shared"; +import { + type AroundMigrateCustomerArgs, + buildSkippedMigrateCustomerResult, + composeMigrationHooks, + customerGuardPlugin, +} from "@/internal/migrations/v2/hooks/index.js"; +import type { MigrateCustomerResult } from "@/internal/migrations/v2/run/migrateCustomer/index.js"; + +const fullCustomer = { + id: "customer_1", + internal_id: "internal_customer_1", + name: "Test Customer", + email: "test@example.com", +} as unknown as FullCustomer; + +const baseResult: MigrateCustomerResult = { + itemPreview: { + id: "customer_1", + name: "Test Customer", + email: "test@example.com", + }, + status: "succeeded", + response: { preview: { ok: true } }, +}; + +const args = { + customerId: "customer_1", + context: { fullCustomer }, + preview: true, + run: async () => baseResult, +} as unknown as AroundMigrateCustomerArgs; + +describe("migration hooks", () => { + test("runs plugin around hooks before direct hooks and can return early", async () => { + const calls: string[] = []; + const hooks = composeMigrationHooks({ + plugins: [ + { + id: "skip-plugin", + hooks: { + aroundMigrateCustomer: ({ context }) => { + calls.push("plugin"); + return buildSkippedMigrateCustomerResult({ + context, + skip: { reason: "manual_review" }, + }); + }, + }, + }, + ], + hooks: { + aroundMigrateCustomer: ({ run }) => { + calls.push("direct"); + return run(); + }, + }, + }); + + await expect(hooks?.aroundMigrateCustomer?.(args)).resolves.toMatchObject({ + status: "skipped", + response: { + skipped: { + reason: "manual_review", + }, + }, + }); + expect(calls).toEqual(["plugin"]); + }); + + test("wraps around hooks in plugin order then direct hooks", async () => { + const calls: string[] = []; + const hooks = composeMigrationHooks({ + plugins: [ + { + id: "first", + hooks: { + aroundMigrateCustomer: async ({ run }) => { + calls.push("first:before"); + const result = await run(); + calls.push("first:after"); + return { + ...result, + response: { ...(result.response ?? {}), first: true }, + }; + }, + }, + }, + ], + hooks: { + aroundMigrateCustomer: async ({ run }) => { + calls.push("direct:before"); + const result = await run(); + calls.push("direct:after"); + return { + ...result, + response: { ...(result.response ?? {}), second: true }, + }; + }, + }, + }); + + await expect( + hooks?.aroundMigrateCustomer?.({ + ...args, + run: async () => { + calls.push("base"); + return baseResult; + }, + }), + ).resolves.toMatchObject({ + response: { + first: true, + second: true, + }, + }); + expect(calls).toEqual([ + "first:before", + "direct:before", + "base", + "direct:after", + "first:after", + ]); + }); + + test("customerGuardPlugin converts a guard hit into a skipped hook result", async () => { + const plugin = customerGuardPlugin({ + id: "custom-guard", + guard: () => ({ + reason: "custom_condition", + response: { guardReasons: ["needs_manual_review"] }, + }), + }); + + await expect( + plugin.hooks?.aroundMigrateCustomer?.(args), + ).resolves.toMatchObject({ + status: "skipped", + response: { + guard: { + pluginId: "custom-guard", + reason: "custom_condition", + guardReasons: ["needs_manual_review"], + }, + }, + }); + }); +}); diff --git a/server/tests/unit/redis/redis-v2-config.spec.ts b/server/tests/unit/redis/redis-v2-config.spec.ts index 9e599f3a4..ccc8d3693 100644 --- a/server/tests/unit/redis/redis-v2-config.spec.ts +++ b/server/tests/unit/redis/redis-v2-config.spec.ts @@ -1,43 +1,7 @@ import { describe, expect, test } from "bun:test"; -import { - getRedisV2ConnectionConfig, - REDIS_V2_COMMAND_TIMEOUT_MS, - supportsUpstashShebangForRedisV2, -} from "@/external/redis/initUtils/redisV2Config.js"; +import { supportsUpstashShebangForRedisV2 } from "@/external/redis/initUtils/redisV2Config.js"; describe("redis V2 connection config", () => { - test("uses a distinct CACHE_V2_UPSTASH_URL with the Upstash shebang", () => { - expect( - getRedisV2ConnectionConfig({ - cacheV2Url: " redis://v2 ", - primaryCacheUrl: "redis://primary", - currentRegion: "us-west-2", - }), - ).toEqual({ - cacheUrl: "redis://v2", - region: "us-west-2:v2", - supportsUpstashShebang: true, - commandTimeout: REDIS_V2_COMMAND_TIMEOUT_MS, - }); - }); - - test("falls back to primary Redis when CACHE_V2_UPSTASH_URL is absent or matches primary", () => { - expect( - getRedisV2ConnectionConfig({ - cacheV2Url: undefined, - primaryCacheUrl: "redis://primary", - currentRegion: "us-west-2", - }), - ).toBeNull(); - expect( - getRedisV2ConnectionConfig({ - cacheV2Url: " redis://primary ", - primaryCacheUrl: "redis://primary", - currentRegion: "us-west-2", - }), - ).toBeNull(); - }); - test("enables the Upstash shebang only for the upstash instance", () => { expect(supportsUpstashShebangForRedisV2("upstash")).toBe(true); expect(supportsUpstashShebangForRedisV2("redis")).toBe(false); diff --git a/server/tests/utils/testInitUtils/initScenario.ts b/server/tests/utils/testInitUtils/initScenario.ts index 41fb656f0..80fe339da 100644 --- a/server/tests/utils/testInitUtils/initScenario.ts +++ b/server/tests/utils/testInitUtils/initScenario.ts @@ -192,6 +192,11 @@ type ResetFeatureAction = { timeout?: number; }; +type ParallelAction = { + type: "parallel"; + actions: ScenarioAction[]; +}; + type RedeemRewardAction = { type: "redeemReward"; code: string; @@ -213,6 +218,7 @@ type ScenarioAction = | RedeemReferralCodeAction | CreateAndRedeemReferralCodeAction | ResetFeatureAction + | ParallelAction | RedeemRewardAction; type CleanupConfig = { @@ -301,7 +307,7 @@ const customer = ({ name?: string | null; email?: string | null; stripeCustomerOverrides?: Partial; -}): ConfigFn => { +} = {}): ConfigFn => { return (config) => ({ ...config, testClock, @@ -772,12 +778,29 @@ const billingMultiAttach = ({ /** Top-level alias for billing multi-attach. */ const multiAttach = billingMultiAttach; +/** Run independent scenario actions concurrently. */ +const parallel = (...actions: ConfigFn[]): ConfigFn => { + return (config) => { + const parallelConfig = actions.reduce((c, fn) => fn(c), { + ...config, + actions: [], + } as ScenarioConfig); + + return { + ...config, + actions: [ + ...config.actions, + { type: "parallel" as const, actions: parallelConfig.actions }, + ], + }; + }; +}; + /** * Define a feature grant reward for this test scenario. * Inserts a reward of type `feature_grant` with entitlements and promo codes. * Feature IDs are external (e.g., TestFeature.Messages) and resolved to internal IDs during setup. * @param config - Feature grant configuration - * @example s.featureGrant({ entitlements: [{ feature_id: TestFeature.Messages, allowance: 10 }], promoCodes: [{ code: "MSG10" }] }) */ const featureGrant = (config: FeatureGrantConfig): ConfigFn => { return (scenarioConfig) => ({ @@ -790,8 +813,6 @@ const featureGrant = (config: FeatureGrantConfig): ConfigFn => { * Redeem a reward promo code for a customer. * @param code - The promo code to redeem * @param customerId - Optional: use this customer instead of primary - * @example s.rewards.redeem({ code: "MSG10" }) - * @example s.rewards.redeem({ code: "MSG10", customerId: "other-customer" }) */ const redeemReward = ({ code, @@ -898,6 +919,7 @@ export const s = { multiAttach: billingMultiAttach, }, multiAttach, + parallel, featureGrant, referral: { createCode: createReferralCode, @@ -952,6 +974,27 @@ type OtherCustomerResult = { customer: Awaited>["customer"]; }; +type InitScenarioImplementationResult = { + customerId: string | undefined; + autumnV0: AutumnInt; + autumnV1: AutumnInt; + autumnV1Beta: AutumnInt; + /** @deprecated Use autumnV2_2 instead */ + autumnV2: AutumnInt; + /** @deprecated Use autumnV2_2 instead */ + autumnV2_1: AutumnInt; + autumnV2_2: AutumnInt; + testClockId: string | undefined; + testClockIds: Record; + customer: Awaited>["customer"] | null; + ctx: TestContext; + entities: GeneratedEntity[]; + advancedTo: number; + otherCustomers: Map; + referralCode: ReferralCode | null; + redemption: RewardRedemption | null; +}; + // customerId provided -> customerId: string in return. export async function initScenario(params: { customerId: string; @@ -1016,7 +1059,7 @@ export async function initScenario({ setup: ConfigFn[]; actions: ConfigFn[]; ctx?: TestContext; -}) { +}): Promise { // Use override or default ctx; may be rebound below if s.platform.create. let ctx = ctxOverride ?? defaultCtx; const config = [...setup, ...actions].reduce((c, fn) => fn(c), defaultConfig); @@ -1311,8 +1354,10 @@ export async function initScenario({ let referralCode: ReferralCode | null = null; let redemption: RewardRedemption | null = null; - for (const action of config.actions) { - if (action.type === "attach") { + const runAction = async (action: ScenarioAction): Promise => { + if (action.type === "parallel") { + await Promise.all(action.actions.map((child) => runAction(child))); + } else if (action.type === "attach") { // Override or fall back to primary customerId. const targetCustomerId = action.customerId ?? customerId; if (!targetCustomerId) { @@ -1656,6 +1701,10 @@ export async function initScenario({ customerId: targetCustomerId, }); } + }; + + for (const action of config.actions) { + await runAction(action); } return { diff --git a/server/tinybird.config.json b/server/tinybird.config.json new file mode 100644 index 000000000..bfacb940c --- /dev/null +++ b/server/tinybird.config.json @@ -0,0 +1,10 @@ +{ + "include": [ + "tinybird/**/*.datasource", + "tinybird/**/*.pipe", + "src/external/tinybird/migrations/migrationItemEventsDataSource.ts" + ], + "token": "${TINYBIRD_TOKEN}", + "baseUrl": "${TINYBIRD_API_URL}", + "devMode": "branch" +} diff --git a/shared/api/customers/utils/match/customerFilterMatchesFullCustomer.ts b/shared/api/customers/utils/match/customerFilterMatchesFullCustomer.ts new file mode 100644 index 000000000..7d4f36961 --- /dev/null +++ b/shared/api/customers/utils/match/customerFilterMatchesFullCustomer.ts @@ -0,0 +1,66 @@ +import type { FullCustomer } from "../../../../models/cusModels/fullCusModel.js"; +import { customerProductHasActiveStatus } from "../../../../utils/index.js"; +import type { CustomerFilter } from "../../../migrations/filters/customerFilter.js"; +import { + arrayFilterMatches, + stringMatcherMatches, +} from "../../../migrations/filters/match/index.js"; +import { planFilterMatchesCustomerProduct } from "../../../products/utils/match/planFilterMatchesCustomerProduct.js"; + +/** + * Predicate: does `filter` match a customer with `customer_products`? + * + * JS-side mirror of the SQL compiler's `customerRegistry`. Used by the lazy + * migration helper to skip non-matching customers without queueing work. + * Mirrors the `cp.status IN ACTIVE_STATUSES` ambient predicate baked into + * the SQL plan scope — non-active cusProducts are ignored. + * + * Supports `customer_id` and `plan` (`$some` / `$every` / `$none` and the + * implicit-`$some` bare form). `item` sugar throws to make the gap explicit, + * matching the convention in `planFilterMatchesCustomerProduct`. + */ +export const customerFilterMatchesFullCustomer = ({ + filter, + fullCustomer, +}: { + filter: CustomerFilter; + fullCustomer: Pick; +}): boolean => { + if ( + filter.customer_id !== undefined && + !stringMatcherMatches({ + matcher: filter.customer_id, + value: fullCustomer.id, + }) + ) { + return false; + } + + if (filter.plan !== undefined) { + const activeProducts = fullCustomer.customer_products.filter( + customerProductHasActiveStatus, + ); + const planFilter = filter.plan === "$none" ? { $none: {} } : filter.plan; + if ( + !arrayFilterMatches({ + filter: planFilter, + items: activeProducts, + matchesElement: ({ filter: planFilter, item: customerProduct }) => + planFilterMatchesCustomerProduct({ + filter: planFilter, + cusProduct: customerProduct, + }), + }) + ) { + return false; + } + } + + if (filter.item !== undefined) { + throw new Error( + "customerFilterMatchesFullCustomer: filter.item not supported in JS matcher yet", + ); + } + + return true; +}; diff --git a/shared/api/customers/utils/match/index.ts b/shared/api/customers/utils/match/index.ts new file mode 100644 index 000000000..3c0142dce --- /dev/null +++ b/shared/api/customers/utils/match/index.ts @@ -0,0 +1 @@ +export * from "./customerFilterMatchesFullCustomer.js"; diff --git a/shared/api/migrations/compiler/compileFilter.ts b/shared/api/migrations/compiler/compileFilter.ts new file mode 100644 index 000000000..0faf59724 --- /dev/null +++ b/shared/api/migrations/compiler/compileFilter.ts @@ -0,0 +1,29 @@ +import type { CustomerFilter } from "../filters/customerFilter.js"; +import { filterToIr } from "./filterToIr/filterToIr.js"; +import type { ResolutionContext } from "./filterToIr/resolutionContext.js"; +import { + type AmbientContext, + type CompiledSql, + irToSql, +} from "./irToSql/irToSql.js"; +import { customerRegistry } from "./registry/customerRegistry.js"; + +/** + * End-to-end: validated `CustomerFilter` → parameterized SQL fragment. + * + * `ambient` carries org/env scoping (and any other ambient values the + * registry expects). Required because the customer registry declares + * `org_id` / `env` predicates at every scope. + */ +export function compileFilter({ + filter, + ctx, + ambient, +}: { + filter: CustomerFilter; + ctx: ResolutionContext; + ambient: AmbientContext; +}): CompiledSql { + const ir = filterToIr({ filter, ctx }); + return irToSql({ ir, root: customerRegistry, ambient }); +} diff --git a/shared/api/migrations/compiler/compilePlanFilter.ts b/shared/api/migrations/compiler/compilePlanFilter.ts new file mode 100644 index 000000000..f1cf1ce61 --- /dev/null +++ b/shared/api/migrations/compiler/compilePlanFilter.ts @@ -0,0 +1,26 @@ +import type { PlanFilter } from "../filters/planFilter.js"; +import { planFilterToIr } from "./filterToIr/filterToIr.js"; +import type { ResolutionContext } from "./filterToIr/resolutionContext.js"; +import { + type AmbientContext, + type CompiledSql, + irToSql, +} from "./irToSql/irToSql.js"; +import { planRegistry } from "./registry/planRegistry.js"; + +/** + * End-to-end: validated `PlanFilter` → parameterized SQL fragment, rooted + * at the catalog `products` table. + */ +export function compilePlanFilter({ + filter, + ctx, + ambient, +}: { + filter: PlanFilter; + ctx: ResolutionContext; + ambient: AmbientContext; +}): CompiledSql { + const ir = planFilterToIr({ filter, ctx }); + return irToSql({ ir, root: planRegistry, ambient }); +} diff --git a/shared/api/migrations/compiler/filterToIr/fields/makeExistenceParser.ts b/shared/api/migrations/compiler/filterToIr/fields/makeExistenceParser.ts new file mode 100644 index 000000000..7bd668c86 --- /dev/null +++ b/shared/api/migrations/compiler/filterToIr/fields/makeExistenceParser.ts @@ -0,0 +1,44 @@ +import type { IRNode } from "../../ir/irTypes.js"; + +/** + * Factory for nullable-existence field parsers. The pattern is identical + * across `price`, `rollover`, and any future field where the only phase-1 + * questions are "is this column null?" / "is it non-null?": + * field: null → exists(false) + * field: { $eq: null } → exists(false) + * field: { $ne: null } → exists(true) + * + * `scopePath` is the dotted public path used in error messages + * (e.g. "plan.item.rollover"). The IR `field` is the bare name. + */ +export function makeExistenceParser({ + field, + scopePath, +}: { + field: string; + scopePath: string; +}): (raw: unknown) => IRNode { + return (raw: unknown): IRNode => { + if (raw === null) + return { kind: "leaf", field, op: "exists", value: false }; + + if (typeof raw !== "object") + throw new Error(`${scopePath} must be null or an object`); + + const ops = raw as Record; + const hasOnlyNullOps = + Object.keys(ops).every((k) => k === "$eq" || k === "$ne") && + Object.values(ops).every((v) => v === null || v === undefined); + if (!hasOnlyNullOps) + throw new Error( + `${scopePath} filtering on nested fields is not supported in phase 1`, + ); + + if ("$ne" in ops && ops.$ne === null) + return { kind: "leaf", field, op: "exists", value: true }; + if ("$eq" in ops && ops.$eq === null) + return { kind: "leaf", field, op: "exists", value: false }; + + throw new Error(`${scopePath} requires $eq: null or $ne: null`); + }; +} diff --git a/shared/api/migrations/compiler/filterToIr/fields/parseLeaf.ts b/shared/api/migrations/compiler/filterToIr/fields/parseLeaf.ts new file mode 100644 index 000000000..825383719 --- /dev/null +++ b/shared/api/migrations/compiler/filterToIr/fields/parseLeaf.ts @@ -0,0 +1,65 @@ +import type { IRLeaf, IRNode, LeafOp } from "../../ir/irTypes.js"; +import type { ResolutionContext } from "../resolutionContext.js"; +import { translateValue } from "./translateValue.js"; + +/** + * Parse a single field's matcher value into one IR leaf or a small AND of + * leaves. Handles the four supported operators: eq, ne, in, exists. + * + * Spelling normalization: + * - bare value → eq + * - { $eq: x } → eq + * - { $ne: null } → exists (true) + * - { $eq: null } → eq null + * - { $in: [...] } → in + * + * Multiple operators on one field are combined with AND. + */ +export function parseLeaf({ + field, + rawValue, + ctx, +}: { + field: string; + rawValue: unknown; + ctx: ResolutionContext; +}): IRNode { + if (isBareValue(rawValue)) return makeLeaf(field, "eq", rawValue, ctx); + + const ops = rawValue as Record; + const leaves: IRLeaf[] = []; + + if ("$eq" in ops) leaves.push(makeLeaf(field, "eq", ops.$eq, ctx) as IRLeaf); + if ("$ne" in ops) { + // $ne: null is the existence-check shorthand for nullable fields. + if (ops.$ne === null) + leaves.push(makeLeaf(field, "exists", true, ctx) as IRLeaf); + else leaves.push(makeLeaf(field, "ne", ops.$ne, ctx) as IRLeaf); + } + if ("$in" in ops) leaves.push(makeLeaf(field, "in", ops.$in, ctx) as IRLeaf); + + if (leaves.length === 0) + throw new Error(`No supported operator found on field "${field}"`); + if (leaves.length === 1) return leaves[0]; + return { kind: "and", children: leaves }; +} + +function isBareValue(v: unknown): boolean { + if (v === null) return true; + const t = typeof v; + return t === "string" || t === "number" || t === "boolean"; +} + +function makeLeaf( + field: string, + op: LeafOp, + value: unknown, + ctx: ResolutionContext, +): IRLeaf { + return { + kind: "leaf", + field, + op, + value: translateValue({ field, value, ctx }) as IRLeaf["value"], + }; +} diff --git a/shared/api/migrations/compiler/filterToIr/fields/parsePriceExistence.ts b/shared/api/migrations/compiler/filterToIr/fields/parsePriceExistence.ts new file mode 100644 index 000000000..e4e19b189 --- /dev/null +++ b/shared/api/migrations/compiler/filterToIr/fields/parsePriceExistence.ts @@ -0,0 +1,12 @@ +import { makeExistenceParser } from "./makeExistenceParser.js"; + +/** + * Phase 1 supports only existence checks on `price` (paid vs free): + * price: null → entitlement-only item (no price) + * price: { $ne: null } → has a price (paid item) + * Filtering on nested price fields (billing_method, etc.) is deferred. + */ +export const parsePriceExistence = makeExistenceParser({ + field: "price", + scopePath: "plan.item.price", +}); diff --git a/shared/api/migrations/compiler/filterToIr/fields/parseRolloverExistence.ts b/shared/api/migrations/compiler/filterToIr/fields/parseRolloverExistence.ts new file mode 100644 index 000000000..b6c1f2252 --- /dev/null +++ b/shared/api/migrations/compiler/filterToIr/fields/parseRolloverExistence.ts @@ -0,0 +1,13 @@ +import { makeExistenceParser } from "./makeExistenceParser.js"; + +/** + * Phase 1 supports only existence checks on `rollover`: + * rollover: null → entitlement has no rollover config + * rollover: { $ne: null } → has rollover config + * Nested-field filtering (e.g. `rollover: { max: { $ne: null } }`) is + * deferred — the schema's empty inner object leaves room for it. + */ +export const parseRolloverExistence = makeExistenceParser({ + field: "rollover", + scopePath: "plan.item.rollover", +}); diff --git a/shared/api/migrations/compiler/filterToIr/fields/translateValue.ts b/shared/api/migrations/compiler/filterToIr/fields/translateValue.ts new file mode 100644 index 000000000..b447fcabc --- /dev/null +++ b/shared/api/migrations/compiler/filterToIr/fields/translateValue.ts @@ -0,0 +1,39 @@ +import type { ResolutionContext } from "../resolutionContext.js"; + +/** + * Translate a public ID (or array of public IDs) to internal IDs for the + * fields that need it: `feature_id`. All other fields pass through + * unchanged. Unknown IDs throw. + * + * Note: `plan_id` is NOT translated. A public plan_id (e.g. "pro") maps + * to many `internal_product_id`s — one per plan version — so we resolve + * it via a JOIN to `products` instead, filtering on `products.id`. + */ +export function translateValue({ + field, + value, + ctx, +}: { + field: string; + value: unknown; + ctx: ResolutionContext; +}): unknown { + const lookup = TRANSLATORS[field]; + if (!lookup) return value; + if (value === null) return value; + if (Array.isArray(value)) return value.map((v) => lookup(v, ctx)); + return lookup(value, ctx); +} + +const TRANSLATORS: Record< + string, + (value: unknown, ctx: ResolutionContext) => unknown +> = { + feature_id: (value, ctx) => { + if (typeof value !== "string") + throw new Error(`feature_id must be a string, got ${typeof value}`); + const feature = ctx.features.find((f) => f.id === value); + if (!feature) throw new Error(`Unknown feature_id: ${value}`); + return feature.internal_id; + }, +}; diff --git a/shared/api/migrations/compiler/filterToIr/filterToIr.ts b/shared/api/migrations/compiler/filterToIr/filterToIr.ts new file mode 100644 index 000000000..e5d10f9ff --- /dev/null +++ b/shared/api/migrations/compiler/filterToIr/filterToIr.ts @@ -0,0 +1,45 @@ +import type { CustomerFilter } from "../../filters/customerFilter.js"; +import type { PlanFilter } from "../../filters/planFilter.js"; +import type { IRNode } from "../ir/irTypes.js"; +import type { ResolutionContext } from "./resolutionContext.js"; +import { parseCustomerFilter } from "./scopes/parseCustomerFilter.js"; +import { parsePlanFilter } from "./scopes/parsePlanFilter.js"; + +/** + * Top-level entry: take a validated `CustomerFilter` plus a resolution + * context (features, products) and produce IR ready for compilation. + * + * The parser walks the filter shape, recurses into each scope (customer → + * plan → item), and converts each field-matcher pair into one or more IR + * leaves. AND is always implicit between sibling fields. Bare nested + * filters become `nav` nodes with implicit `$some` quantifier. + * + * Internal layout (each one-thing-per-file): + * scopes/ per-scope dispatchers (customer, plan, plan.item) + * navs/ scope-to-scope transitions (plan, item; routes scope variants) + * fields/ per-field parsers + parseLeaf + makeExistenceParser factory + * helpers/ wrapAnd, isQuantifierWrapper + */ +export function filterToIr({ + filter, + ctx, +}: { + filter: CustomerFilter; + ctx: ResolutionContext; +}): IRNode { + return parseCustomerFilter({ filter, ctx }); +} + +/** + * Plan-rooted entry: validated `PlanFilter` → IR. Used when the migration + * targets the catalog directly rather than a customer's plan instance. + */ +export function planFilterToIr({ + filter, + ctx, +}: { + filter: PlanFilter; + ctx: ResolutionContext; +}): IRNode { + return parsePlanFilter({ filter, ctx }); +} diff --git a/shared/api/migrations/compiler/filterToIr/helpers/isQuantifierWrapper.ts b/shared/api/migrations/compiler/filterToIr/helpers/isQuantifierWrapper.ts new file mode 100644 index 000000000..e81503f45 --- /dev/null +++ b/shared/api/migrations/compiler/filterToIr/helpers/isQuantifierWrapper.ts @@ -0,0 +1,7 @@ +export function isQuantifierWrapper( + raw: unknown, +): raw is { $some?: unknown; $every?: unknown; $none?: unknown } { + if (!raw || typeof raw !== "object") return false; + const keys = Object.keys(raw as object); + return keys.some((k) => k === "$some" || k === "$every" || k === "$none"); +} diff --git a/shared/api/migrations/compiler/filterToIr/helpers/wrapAnd.ts b/shared/api/migrations/compiler/filterToIr/helpers/wrapAnd.ts new file mode 100644 index 000000000..c6b739e4f --- /dev/null +++ b/shared/api/migrations/compiler/filterToIr/helpers/wrapAnd.ts @@ -0,0 +1,8 @@ +import type { IRNode } from "../../ir/irTypes.js"; + +export function wrapAnd(children: IRNode[]): IRNode { + if (children.length === 0) + throw new Error("Empty filter scope: at least one field is required"); + if (children.length === 1) return children[0]; + return { kind: "and", children }; +} diff --git a/shared/api/migrations/compiler/filterToIr/navs/parseItemNav.ts b/shared/api/migrations/compiler/filterToIr/navs/parseItemNav.ts new file mode 100644 index 000000000..531793427 --- /dev/null +++ b/shared/api/migrations/compiler/filterToIr/navs/parseItemNav.ts @@ -0,0 +1,38 @@ +import type { PlanFilter } from "../../../filters/planFilter.js"; +import type { PlanItemFilter } from "../../../filters/planItemFilter.js"; +import type { IRNav } from "../../ir/irTypes.js"; +import { isQuantifierWrapper } from "../helpers/isQuantifierWrapper.js"; +import type { ResolutionContext } from "../resolutionContext.js"; +import { parsePlanItemFilter } from "../scopes/parsePlanItemFilter.js"; + +export function parseItemNav({ + raw, + ctx, +}: { + raw: NonNullable; + ctx: ResolutionContext; +}): IRNav { + const itemFilter = isQuantifierWrapper(raw) + ? raw.$some + : (raw as PlanItemFilter); + if (!itemFilter) throw new Error("item: only $some is supported in phase 1"); + // Route to the paid-only scope when the filter requires a non-null + // price. Walks `customer_prices` forward instead of the entitlement + // spine — substantially fewer rows on paid-feature migrations. + const navName = isPaidOnlyPriceFilter(itemFilter.price) + ? "item_paid" + : "item"; + return { + kind: "nav", + name: navName, + quantifier: "some", + child: parsePlanItemFilter({ filter: itemFilter, ctx }), + }; +} + +function isPaidOnlyPriceFilter(price: PlanItemFilter["price"]): boolean { + if (price === undefined || price === null) return false; + if (typeof price !== "object") return false; + const ops = price as Record; + return "$ne" in ops && ops.$ne === null; +} diff --git a/shared/api/migrations/compiler/filterToIr/navs/parsePlanNav.ts b/shared/api/migrations/compiler/filterToIr/navs/parsePlanNav.ts new file mode 100644 index 000000000..a67ed73b4 --- /dev/null +++ b/shared/api/migrations/compiler/filterToIr/navs/parsePlanNav.ts @@ -0,0 +1,53 @@ +import type { CustomerFilter } from "../../../filters/customerFilter.js"; +import type { PlanFilter } from "../../../filters/planFilter.js"; +import type { IRNav, IRNode, Quantifier } from "../../ir/irTypes.js"; +import { isQuantifierWrapper } from "../helpers/isQuantifierWrapper.js"; +import type { ResolutionContext } from "../resolutionContext.js"; +import { parsePlanFilter } from "../scopes/parsePlanFilter.js"; + +const QUANTIFIER_KEYS: Record = { + $some: "some", + $none: "none", +}; + +export function parsePlanNav({ + raw, + ctx, +}: { + raw: NonNullable; + ctx: ResolutionContext; +}): IRNav { + if (raw === "$none") + return buildNav({ quantifier: "none", filter: {} as PlanFilter, ctx }); + + if (!isQuantifierWrapper(raw)) + return buildNav({ quantifier: "some", filter: raw as PlanFilter, ctx }); + + for (const [key, quantifier] of Object.entries(QUANTIFIER_KEYS)) { + const filter = (raw as Record)[key] as + | PlanFilter + | undefined; + if (filter !== undefined) return buildNav({ quantifier, filter, ctx }); + } + + const unsupported = Object.keys(raw).find((k) => k.startsWith("$")); + throw new Error( + `plan: ${unsupported ?? "unknown quantifier"} is not supported yet`, + ); +} + +function buildNav({ + quantifier, + filter, + ctx, +}: { + quantifier: Quantifier; + filter: PlanFilter; + ctx: ResolutionContext; +}): IRNav { + const hasFields = Object.keys(filter).length > 0; + const child: IRNode = hasFields + ? parsePlanFilter({ filter, ctx }) + : { kind: "and", children: [] }; + return { kind: "nav", name: "plan", quantifier, child }; +} diff --git a/shared/api/migrations/compiler/filterToIr/resolutionContext.ts b/shared/api/migrations/compiler/filterToIr/resolutionContext.ts new file mode 100644 index 000000000..c4b669dc3 --- /dev/null +++ b/shared/api/migrations/compiler/filterToIr/resolutionContext.ts @@ -0,0 +1,16 @@ +/** + * Public IDs that map 1:1 to internal IDs are translated at parse time, + * before the IR is built. This keeps the compiler pure — it only ever + * sees resolved internal IDs for those fields. + * + * Currently only `feature_id` is translated (features are not versioned, + * so the mapping is 1:1). `plan_id` is NOT translated — a public plan_id + * maps to many internal_product_ids (one per version), so we resolve it + * via a JOIN to `products` instead. + * + * Unknown IDs throw at parse time, never silently miss. + */ + +export type ResolutionContext = { + features: ReadonlyArray<{ id: string; internal_id: string }>; +}; diff --git a/shared/api/migrations/compiler/filterToIr/scopes/parseCustomerFilter.ts b/shared/api/migrations/compiler/filterToIr/scopes/parseCustomerFilter.ts new file mode 100644 index 000000000..97083392f --- /dev/null +++ b/shared/api/migrations/compiler/filterToIr/scopes/parseCustomerFilter.ts @@ -0,0 +1,35 @@ +import type { CustomerFilter } from "../../../filters/customerFilter.js"; +import type { IRNode } from "../../ir/irTypes.js"; +import { parseLeaf } from "../fields/parseLeaf.js"; +import { wrapAnd } from "../helpers/wrapAnd.js"; +import { parseItemNav } from "../navs/parseItemNav.js"; +import { parsePlanNav } from "../navs/parsePlanNav.js"; +import type { ResolutionContext } from "../resolutionContext.js"; + +export function parseCustomerFilter({ + filter, + ctx, +}: { + filter: CustomerFilter; + ctx: ResolutionContext; +}): IRNode { + const children: IRNode[] = []; + + if (filter.customer_id !== undefined) + children.push( + parseLeaf({ field: "customer_id", rawValue: filter.customer_id, ctx }), + ); + if (filter.plan !== undefined) + children.push(parsePlanNav({ raw: filter.plan, ctx })); + // `item` is sugar for `plan: { item: ... }` — wraps the item nav in a + // plan nav so the IR stays canonical (no separate scope/registry entry). + if (filter.item !== undefined) + children.push({ + kind: "nav", + name: "plan", + quantifier: "some", + child: parseItemNav({ raw: filter.item, ctx }), + }); + + return wrapAnd(children); +} diff --git a/shared/api/migrations/compiler/filterToIr/scopes/parsePlanFilter.ts b/shared/api/migrations/compiler/filterToIr/scopes/parsePlanFilter.ts new file mode 100644 index 000000000..0b47539ff --- /dev/null +++ b/shared/api/migrations/compiler/filterToIr/scopes/parsePlanFilter.ts @@ -0,0 +1,44 @@ +import type { PlanFilter } from "../../../filters/planFilter.js"; +import type { IRNode } from "../../ir/irTypes.js"; +import { parseLeaf } from "../fields/parseLeaf.js"; +import { parsePriceExistence } from "../fields/parsePriceExistence.js"; +import { wrapAnd } from "../helpers/wrapAnd.js"; +import { parseItemNav } from "../navs/parseItemNav.js"; +import type { ResolutionContext } from "../resolutionContext.js"; + +export function parsePlanFilter({ + filter, + ctx, +}: { + filter: PlanFilter; + ctx: ResolutionContext; +}): IRNode { + const children: IRNode[] = []; + + if (filter.plan_id !== undefined) + children.push( + parseLeaf({ field: "plan_id", rawValue: filter.plan_id, ctx }), + ); + if (filter.price !== undefined) + children.push(parsePriceExistence(filter.price)); + if (filter.addon !== undefined) + children.push(parseLeaf({ field: "addon", rawValue: filter.addon, ctx })); + if (filter.paid !== undefined) + children.push(parseLeaf({ field: "paid", rawValue: filter.paid, ctx })); + if (filter.recurring !== undefined) + children.push( + parseLeaf({ field: "recurring", rawValue: filter.recurring, ctx }), + ); + if (filter.item !== undefined) + children.push(parseItemNav({ raw: filter.item, ctx })); + if (filter.$or !== undefined) { + if (filter.$or.length === 0) + throw new Error("$or requires at least one branch"); + const orChildren = filter.$or.map((sub) => + parsePlanFilter({ filter: sub, ctx }), + ); + children.push({ kind: "or", children: orChildren }); + } + + return wrapAnd(children); +} diff --git a/shared/api/migrations/compiler/filterToIr/scopes/parsePlanItemFilter.ts b/shared/api/migrations/compiler/filterToIr/scopes/parsePlanItemFilter.ts new file mode 100644 index 000000000..05c9c80ea --- /dev/null +++ b/shared/api/migrations/compiler/filterToIr/scopes/parsePlanItemFilter.ts @@ -0,0 +1,34 @@ +import type { PlanItemFilter } from "../../../filters/planItemFilter.js"; +import type { IRNode } from "../../ir/irTypes.js"; +import { parseLeaf } from "../fields/parseLeaf.js"; +import { parsePriceExistence } from "../fields/parsePriceExistence.js"; +import { parseRolloverExistence } from "../fields/parseRolloverExistence.js"; +import { wrapAnd } from "../helpers/wrapAnd.js"; +import type { ResolutionContext } from "../resolutionContext.js"; + +export function parsePlanItemFilter({ + filter, + ctx, +}: { + filter: PlanItemFilter; + ctx: ResolutionContext; +}): IRNode { + const children: IRNode[] = []; + + if (filter.feature_id !== undefined) + children.push( + parseLeaf({ field: "feature_id", rawValue: filter.feature_id, ctx }), + ); + + if (filter.price !== undefined) + children.push(parsePriceExistence(filter.price)); + + if (filter.rollover !== undefined) + children.push(parseRolloverExistence(filter.rollover)); + + // `unlimited` deferred to phase 2. + if (filter.unlimited !== undefined) + throw new Error("plan.item.unlimited is not supported in phase 1"); + + return wrapAnd(children); +} diff --git a/shared/api/migrations/compiler/ir/irTypes.ts b/shared/api/migrations/compiler/ir/irTypes.ts new file mode 100644 index 000000000..76c978cd2 --- /dev/null +++ b/shared/api/migrations/compiler/ir/irTypes.ts @@ -0,0 +1,51 @@ +/** + * Intermediate Representation (IR) for migration filters. + * + * The filter pipeline is: + * Filter (Zod, public DSL) → IR (canonical AST) → SQL (Drizzle fragment) + * + * The IR is the compiler-friendly middle layer. It collapses every spelling + * of the public DSL (`feature_id: "x"` ≡ `{ $eq: "x" }`, `price: null` ≡ + * `{ $eq: null }`) into one canonical shape, so the compiler only handles + * one form per concept. + */ + +export type LeafOp = "eq" | "ne" | "in" | "exists"; + +export type LeafValue = + | string + | number + | boolean + | null + | readonly string[] + | readonly number[]; + +export type IRLeaf = { + kind: "leaf"; + /** Field name in the current scope's registry, e.g. "plan_id". */ + field: string; + op: LeafOp; + value: LeafValue; +}; + +export type IRAnd = { + kind: "and"; + children: readonly IRNode[]; +}; + +export type IROr = { + kind: "or"; + children: readonly IRNode[]; +}; + +export type Quantifier = "some" | "none"; + +export type IRNav = { + kind: "nav"; + /** Single segment naming the nav, e.g. "plan" or "item". */ + name: string; + quantifier: Quantifier; + child: IRNode; +}; + +export type IRNode = IRLeaf | IRAnd | IROr | IRNav; diff --git a/shared/api/migrations/compiler/irToSql/irToSql.ts b/shared/api/migrations/compiler/irToSql/irToSql.ts new file mode 100644 index 000000000..44466991f --- /dev/null +++ b/shared/api/migrations/compiler/irToSql/irToSql.ts @@ -0,0 +1,195 @@ +import type { IRLeaf, IRNode, Quantifier } from "../ir/irTypes.js"; +import type { + AmbientPredicate, + FieldDef, + NavScope, + RootScope, +} from "../registry/registryTypes.js"; + +export type CompiledSql = { + /** WHERE-clause fragment with `?` placeholders. */ + sql: string; + /** Values to bind in placeholder order. */ + params: unknown[]; +}; + +/** Values supplied to ambient predicates with `source.kind === "context"`. */ +export type AmbientContext = Record; + +/** + * Compile an IR tree into a parameterized SQL fragment. The root scope's + * ambient predicates are emitted at the top of the WHERE clause; every + * nested scope's ambient predicates are auto-injected into its EXISTS + * subquery. + */ +export function irToSql({ + ir, + root, + ambient, +}: { + ir: IRNode; + root: RootScope; + ambient: AmbientContext; +}): CompiledSql { + const params: unknown[] = []; + const rootAmbient = compileAmbient({ + predicates: root.ambient, + ambient, + params, + }); + const irSql = compileNode({ + node: ir, + fields: root.fields, + params, + ambient, + }); + const parts = [...rootAmbient, irSql].filter((s) => s.length > 0); + return { sql: parts.join(" AND "), params }; +} + +function compileAmbient({ + predicates, + ambient, + params, +}: { + predicates: AmbientPredicate[] | undefined; + ambient: AmbientContext; + params: unknown[]; +}): string[] { + if (!predicates) return []; + return predicates.map((pred) => { + if (pred.source.kind === "context") { + const value = ambient[pred.source.key]; + if (value === undefined) + throw new Error( + `Missing ambient value for key "${pred.source.key}" (column ${pred.column})`, + ); + params.push(value); + return `${pred.column} = ?`; + } + // values: static IN list + if (pred.source.values.length === 0) return "FALSE"; + const placeholders = pred.source.values + .map((v) => { + params.push(v); + return "?"; + }) + .join(", "); + return `${pred.column} IN (${placeholders})`; + }); +} + +function compileNode({ + node, + fields, + params, + ambient, +}: { + node: IRNode; + fields: Record; + params: unknown[]; + ambient: AmbientContext; +}): string { + if (node.kind === "leaf") return compileLeaf({ leaf: node, fields, params }); + if (node.kind === "and") { + if (node.children.length === 0) return "TRUE"; + const parts = node.children.map((c) => + compileNode({ node: c, fields, params, ambient }), + ); + return `(${parts.join(" AND ")})`; + } + if (node.kind === "or") { + if (node.children.length === 0) return "FALSE"; + const parts = node.children.map((c) => + compileNode({ node: c, fields, params, ambient }), + ); + return `(${parts.join(" OR ")})`; + } + // nav + const def = fields[node.name]; + if (!def) throw new Error(`Unknown nav field: ${node.name}`); + if (def.kind !== "nav") throw new Error(`Field "${node.name}" is not a nav`); + return existsForScope({ + scope: def.scope, + child: node.child, + quantifier: node.quantifier, + params, + ambient, + }); +} + +function existsForScope({ + scope, + child, + quantifier, + params, + ambient, +}: { + scope: NavScope; + child: IRNode; + quantifier: Quantifier; + params: unknown[]; + ambient: AmbientContext; +}): string { + const ambientPreds = compileAmbient({ + predicates: scope.ambient, + ambient, + params, + }); + const childSql = compileNode({ + node: child, + fields: scope.fields, + params, + ambient, + }); + const conditions = [scope.correlation, ...ambientPreds, childSql].filter( + (s) => s.length > 0 && s !== "TRUE", + ); + const whereClause = conditions.length > 0 ? conditions.join(" AND ") : "TRUE"; + const keyword = quantifier === "none" ? "NOT EXISTS" : "EXISTS"; + return `${keyword} (SELECT 1 FROM ${scope.from} WHERE ${whereClause})`; +} + +function compileLeaf({ + leaf, + fields, + params, +}: { + leaf: IRLeaf; + fields: Record; + params: unknown[]; +}): string { + const def = fields[leaf.field]; + if (!def) throw new Error(`Unknown leaf field: ${leaf.field}`); + if (def.kind !== "leaf") + throw new Error(`Field "${leaf.field}" is not a leaf`); + + const col = def.sql; + + if (leaf.op === "exists") { + return leaf.value === true ? `${col} IS NOT NULL` : `${col} IS NULL`; + } + if (leaf.op === "eq") { + if (leaf.value === null) return `${col} IS NULL`; + params.push(leaf.value); + return `${col} = ?`; + } + if (leaf.op === "ne") { + if (leaf.value === null) return `${col} IS NOT NULL`; + params.push(leaf.value); + return `${col} <> ?`; + } + if (leaf.op === "in") { + if (!Array.isArray(leaf.value)) + throw new Error(`$in expects an array on field "${leaf.field}"`); + if (leaf.value.length === 0) return "FALSE"; + const placeholders = leaf.value + .map((v) => { + params.push(v); + return "?"; + }) + .join(", "); + return `${col} IN (${placeholders})`; + } + throw new Error(`Unsupported op: ${(leaf as IRLeaf).op}`); +} diff --git a/shared/api/migrations/compiler/registry/customerRegistry.ts b/shared/api/migrations/compiler/registry/customerRegistry.ts new file mode 100644 index 000000000..ce63482de --- /dev/null +++ b/shared/api/migrations/compiler/registry/customerRegistry.ts @@ -0,0 +1,138 @@ +import { ACTIVE_STATUSES } from "../../../../utils/cusProductUtils/cusProductConstants.js"; +import type { NavScope, RootScope } from "./registryTypes.js"; + +/** + * Phase 1 field registry, rooted at the `customers` table. + * + * Supported paths: + * plan.plan_id + * plan.item.feature_id, plan.item.price (existence only: null / $ne null) + * plan.item.rollover (existence only: null / $ne null) + * + * Aliases: + * c customers + * cp customer_products + * p products + * ce customer_entitlements + * e entitlements + * pr prices (LEFT JOIN — only present for paid items) + * cpr customer_prices (LEFT JOIN — only present for paid items) + * + * Item resolution: a plan item is identified by its entitlement. + * `feature_id` ALWAYS resolves via `customer_entitlements → entitlements`. + * The price side is reached by reversing the link: `prices.entitlement_id + * = entitlements.id`. Customer prices are LEFT JOINed so existence checks + * compile to `cpr.id IS NULL` / `IS NOT NULL`. + * + * Ambient predicates push `org_id` / `env` down into every scope whose + * table has those columns. Without this, multi-tenant scans bloat 10x+. + * + * `cp.status IN ACTIVE_STATUSES` is also baked in — customer-rooted + * filters always operate on active plan instances. + */ + +/** + * Default item scope: entitlement spine. Handles both free and paid items. + * `price` filtering is done by LEFT JOINing prices/customer_prices and + * checking `cpr.id IS NULL / NOT NULL`. + */ +const itemScope: NavScope = { + from: [ + "customer_entitlements ce", + "JOIN entitlements e ON e.id = ce.entitlement_id", + "LEFT JOIN prices pr ON pr.entitlement_id = e.id", + "LEFT JOIN customer_prices cpr ON cpr.price_id = pr.id AND cpr.customer_product_id = ce.customer_product_id", + ].join(" "), + correlation: "ce.customer_product_id = cp.id", + fields: { + feature_id: { kind: "leaf", sql: "e.internal_feature_id" }, + price: { kind: "leaf", sql: "cpr.id" }, + rollover: { kind: "leaf", sql: "e.rollover" }, + }, +}; + +/** + * Paid-only optimization: when the filter requires a non-null price, walk + * customer_prices forward instead of the entitlement spine. Skips every + * free entitlement row entirely — typically ~10x fewer rows scanned. + * + * Selected by the parser when it sees `price: { $ne: null }`. + */ +const itemPaidScope: NavScope = { + from: [ + "customer_prices cpr", + "JOIN prices pr ON pr.id = cpr.price_id", + "JOIN entitlements e ON e.id = pr.entitlement_id", + ].join(" "), + correlation: "cpr.customer_product_id = cp.id", + fields: { + feature_id: { kind: "leaf", sql: "e.internal_feature_id" }, + // `cpr.id` is the driving column — always non-null in this scope. + // Emitting `cpr.id IS NOT NULL` is redundant but harmless; Postgres + // elides it. + price: { kind: "leaf", sql: "cpr.id" }, + rollover: { kind: "leaf", sql: "e.rollover" }, + }, +}; + +const planScope: NavScope = { + from: "customer_products cp JOIN products p ON p.internal_id = cp.internal_product_id", + correlation: "cp.internal_customer_id = c.internal_id", + ambient: [ + { + column: "cp.status", + source: { kind: "values", values: ACTIVE_STATUSES }, + }, + ], + fields: { + plan_id: { kind: "leaf", sql: "p.id" }, + addon: { kind: "leaf", sql: "p.is_add_on" }, + // Base price existence: a leaf whose SQL is a scalar subquery that + // evaluates to NULL when the customer has no base customer_price on + // this cusproduct, non-NULL otherwise. The `exists` op (compiled + // via `IS NULL` / `IS NOT NULL`) works against this exactly the + // same way it works against `cpr.id` inside `itemScope` — the leaf + // abstraction unifies "existence-of-related-row" semantics across + // any scope that needs them. + price: { + kind: "leaf", + sql: [ + "(SELECT base_cpr.id FROM customer_prices base_cpr", + "JOIN prices base_pr ON base_pr.id = base_cpr.price_id", + "WHERE base_cpr.customer_product_id = cp.id", + "AND base_pr.entitlement_id IS NULL LIMIT 1)", + ].join(" "), + }, + // Derived boolean filters: SQL is a boolean EXISTS expression so + // `paid: true` compiles to `EXISTS(...) = true` (Postgres treats + // this as the same as `EXISTS(...)`). See SKILL.md "Derived + // boolean filters" for the pattern. + paid: { + kind: "leaf", + sql: "EXISTS (SELECT 1 FROM customer_prices cpr WHERE cpr.customer_product_id = cp.id)", + }, + recurring: { + kind: "leaf", + sql: [ + "EXISTS (SELECT 1 FROM customer_prices cpr", + "JOIN prices pr ON pr.id = cpr.price_id", + "WHERE cpr.customer_product_id = cp.id", + "AND pr.config->>'interval' <> 'one_off')", + ].join(" "), + }, + item: { kind: "nav", scope: itemScope }, + item_paid: { kind: "nav", scope: itemPaidScope }, + }, +}; + +export const customerRegistry: RootScope = { + from: "customers c", + ambient: [ + { column: "c.org_id", source: { kind: "context", key: "orgId" } }, + { column: "c.env", source: { kind: "context", key: "env" } }, + ], + fields: { + customer_id: { kind: "leaf", sql: "c.id" }, + plan: { kind: "nav", scope: planScope }, + }, +}; diff --git a/shared/api/migrations/compiler/registry/planRegistry.ts b/shared/api/migrations/compiler/registry/planRegistry.ts new file mode 100644 index 000000000..b8ac238f7 --- /dev/null +++ b/shared/api/migrations/compiler/registry/planRegistry.ts @@ -0,0 +1,17 @@ +import type { RootScope } from "./registryTypes.js"; + +/** + * Phase 1 plan-rooted registry. Used for migrations that target the + * catalog directly (e.g. "find all plans where plan_id = 'pro'"). + */ +export const planRegistry: RootScope = { + from: "products p", + ambient: [ + { column: "p.org_id", source: { kind: "context", key: "orgId" } }, + { column: "p.env", source: { kind: "context", key: "env" } }, + ], + fields: { + plan_id: { kind: "leaf", sql: "p.id" }, + addon: { kind: "leaf", sql: "p.is_add_on" }, + }, +}; diff --git a/shared/api/migrations/compiler/registry/registryTypes.ts b/shared/api/migrations/compiler/registry/registryTypes.ts new file mode 100644 index 000000000..c864ff87d --- /dev/null +++ b/shared/api/migrations/compiler/registry/registryTypes.ts @@ -0,0 +1,58 @@ +/** + * Field registry — the single source of truth mapping each logical filter + * field to its physical SQL shape. Adding a filter field = adding a + * registry entry. Compiler stays untouched. + * + * Two field kinds: + * - `leaf`: a column or expression. Used for direct comparisons. + * - `nav`: a 1:N relation. Compiles to `EXISTS (SELECT 1 FROM + * WHERE AND AND )`. + * + * Ambient predicates: each scope can declare predicates that get + * auto-injected when the scope is entered. Two flavors: + * - `context` source: value pulled from an ambient context (e.g. orgId, + * env). Pushed as a `?` param. + * - `values` source: static list known at registry-construction time + * (e.g. `cp.status IN ACTIVE_STATUSES`). Each element is pushed as a + * `?` param so quoting/escaping is the driver's responsibility. + */ + +export type AmbientPredicate = { + /** SQL column reference, e.g. "cp.org_id". */ + column: string; + source: + | { kind: "context"; key: string } + | { kind: "values"; values: readonly (string | number)[] }; +}; + +export type LeafField = { + kind: "leaf"; + /** SQL expression for this field, ready to drop into a WHERE clause. */ + sql: string; +}; + +export type NavScope = { + /** Source clause (without leading FROM), incl. JOINs. */ + from: string; + /** Predicate correlating this scope to the parent scope. */ + correlation: string; + /** Ambient predicates auto-injected on scope entry. */ + ambient?: AmbientPredicate[]; + /** Fields available in this scope. */ + fields: Record; +}; + +export type NavField = { + kind: "nav"; + scope: NavScope; +}; + +export type FieldDef = LeafField | NavField; + +export type RootScope = { + /** Root table aliased into the WHERE clause. */ + from: string; + /** Ambient predicates auto-injected at the root. */ + ambient?: AmbientPredicate[]; + fields: Record; +}; diff --git a/shared/api/migrations/filters/arrayFilter.ts b/shared/api/migrations/filters/arrayFilter.ts new file mode 100644 index 000000000..850ca08e2 --- /dev/null +++ b/shared/api/migrations/filters/arrayFilter.ts @@ -0,0 +1,21 @@ +import { z } from "zod/v4"; + +/** + * Array-navigation aggregations. Use one of: + * - `$some` — at least one element matches (the implicit default if you + * pass a bare element filter, e.g. `item: { feature_id: "credits" }`). + * - `$every` — all elements match. + * - `$none` — no elements match. + * + * `$count` and group-by predicates are intentionally NOT supported here — + * those belong in a higher-level Selector layer. + */ +export const arrayFilter = (element: T) => + z.union([ + element, + z.object({ + $some: element.optional(), + $every: element.optional(), + $none: element.optional(), + }), + ]); diff --git a/shared/api/migrations/filters/customerFilter.ts b/shared/api/migrations/filters/customerFilter.ts new file mode 100644 index 000000000..a49f6177f --- /dev/null +++ b/shared/api/migrations/filters/customerFilter.ts @@ -0,0 +1,26 @@ +import { z } from "zod/v4"; +import { arrayFilter } from "./arrayFilter.js"; +import { StringMatcherSchema } from "./matcher.js"; +import { PlanFilterSchema } from "./planFilter.js"; +import { PlanItemFilterSchema } from "./planItemFilter.js"; + +/** + * Filter over Autumn customers. Migration-scoped — stable contract + * decoupled from `ApiCustomerV5`. + * + * `plan` and `item` are array-navigation: a bare filter is implicit `$some`. + * Use `{ $every: ... }` or `{ $none: ... }` for stricter checks. + * + * `item` is sugar for `plan: { item: ... }` — matches customers with at + * least one active plan that has at least one matching item. Sibling with + * `plan` it AND's (each must independently have a match). + */ +export const CustomerFilterSchema = z.object({ + customer_id: StringMatcherSchema.optional(), + plan: z.union([arrayFilter(PlanFilterSchema), z.literal("$none")]).optional(), + item: arrayFilter(PlanItemFilterSchema).optional(), +}); + +export type CustomerFilter = z.infer; + +export const DEFAULT_CUSTOMER_FILTER: CustomerFilter = {}; diff --git a/shared/api/migrations/filters/index.ts b/shared/api/migrations/filters/index.ts new file mode 100644 index 000000000..54aae4f75 --- /dev/null +++ b/shared/api/migrations/filters/index.ts @@ -0,0 +1,11 @@ +export * from "./arrayFilter.js"; +export * from "./customerFilter.js"; +export * from "./match/index.js"; +export * from "./matcher.js"; +export * from "./migrationFilter.js"; +export * from "./planFilter.js"; +export { + DEFAULT_PLAN_ITEM_FILTER as DEFAULT_MIGRATION_PLAN_ITEM_FILTER, + type PlanItemFilter as MigrationPlanItemFilter, + PlanItemFilterSchema as MigrationPlanItemFilterSchema, +} from "./planItemFilter.js"; diff --git a/shared/api/migrations/filters/match/arrayFilterMatches.ts b/shared/api/migrations/filters/match/arrayFilterMatches.ts new file mode 100644 index 000000000..4ce6252e3 --- /dev/null +++ b/shared/api/migrations/filters/match/arrayFilterMatches.ts @@ -0,0 +1,76 @@ +/** + * Generic in-memory evaluator for the `arrayFilter` quantifier shape. + * + * `filter` is either a bare element filter (implicit `$some`) or a + * `{ $some?, $every?, $none? }` wrapper. Multiple quantifiers in the + * same wrapper are AND'd. + * + * Reusable across any nav — pass the element matcher as `matchesElement`. + */ +export type ArrayQuantifierFilter = + | ElementFilter + | { + $some?: ElementFilter; + $every?: ElementFilter; + $none?: ElementFilter; + }; + +export const arrayFilterMatches = ({ + filter, + items, + matchesElement, +}: { + filter: ArrayQuantifierFilter; + items: Item[]; + matchesElement: ({ + filter, + item, + }: { + filter: ElementFilter; + item: Item; + }) => boolean; +}): boolean => { + const wrapped = isQuantifierWrapper(filter) + ? filter + : { $some: filter }; + + if ( + wrapped.$some !== undefined && + !items.some((item) => + matchesElement({ filter: wrapped.$some as ElementFilter, item }), + ) + ) { + return false; + } + + if ( + wrapped.$every !== undefined && + !items.every((item) => + matchesElement({ filter: wrapped.$every as ElementFilter, item }), + ) + ) { + return false; + } + + if ( + wrapped.$none !== undefined && + items.some((item) => + matchesElement({ filter: wrapped.$none as ElementFilter, item }), + ) + ) { + return false; + } + + return true; +}; + +const isQuantifierWrapper = ( + value: ArrayQuantifierFilter, +): value is { + $some?: ElementFilter; + $every?: ElementFilter; + $none?: ElementFilter; +} => { + if (typeof value !== "object" || value === null) return false; + return "$some" in value || "$every" in value || "$none" in value; +}; diff --git a/shared/api/migrations/filters/match/index.ts b/shared/api/migrations/filters/match/index.ts new file mode 100644 index 000000000..3156f1b7b --- /dev/null +++ b/shared/api/migrations/filters/match/index.ts @@ -0,0 +1,4 @@ +export * from "./arrayFilterMatches.js"; +export * from "./nullableFieldMatches.js"; +export * from "./numberMatcherMatches.js"; +export * from "./stringMatcherMatches.js"; diff --git a/shared/api/migrations/filters/match/nullableFieldMatches.ts b/shared/api/migrations/filters/match/nullableFieldMatches.ts new file mode 100644 index 000000000..038d94b54 --- /dev/null +++ b/shared/api/migrations/filters/match/nullableFieldMatches.ts @@ -0,0 +1,46 @@ +/** + * Evaluate a `nullableObjectFilter(...)` value against a field. Mirrors + * the three-branch shape of the schema: + * - bare `null` → match if value is null/undefined + * - `{ $eq: null }` → same + * - `{ $ne: null }` → match if value is non-null + * - inner shape → value must be non-null AND `innerMatches` returns true + * + * `innerMatches` is the caller-supplied nested matcher (e.g. for a + * nested `PlanItemFilter`). It only sees non-null values. + */ +export const nullableFieldMatches = ({ + filter, + value, + innerMatches, +}: { + filter: TInner | null | { $eq?: null; $ne?: null } | undefined; + value: unknown; + innerMatches: (args: { + filter: TInner; + value: NonNullable; + }) => boolean; +}): boolean => { + if (filter === undefined) return true; + + const isNull = value === null || value === undefined; + + if (filter === null) return isNull; + + if (typeof filter === "object" && filter !== null) { + if ("$eq" in filter && filter.$eq === null) return isNull; + if ("$ne" in filter && filter.$ne === null) return !isNull; + // Anything else with `$eq`/`$ne` keys is shape-invalid for a nullable + // wrapper — fall through to the inner matcher. + const onlyNullKeys = + Object.keys(filter).every((k) => k === "$eq" || k === "$ne") && + Object.keys(filter).length > 0; + if (onlyNullKeys) return true; + } + + if (isNull) return false; + return innerMatches({ + filter: filter as TInner, + value: value as NonNullable, + }); +}; diff --git a/shared/api/migrations/filters/match/numberMatcherMatches.ts b/shared/api/migrations/filters/match/numberMatcherMatches.ts new file mode 100644 index 000000000..3dbda75ce --- /dev/null +++ b/shared/api/migrations/filters/match/numberMatcherMatches.ts @@ -0,0 +1,52 @@ +import type { NumberMatcher } from "../matcher.js"; + +/** + * Evaluate a `NumberMatcher` against a value. Returns `true` when the + * value satisfies the matcher. + * + * Implemented operators: bare value, `null` shorthand, `$eq`, `$ne`, + * `$in`, `$nin`, `$gt`, `$gte`, `$lt`, `$lte`. + * + * `null` and `undefined` on the value side are treated identically. + * Ordering operators (`$gt` etc.) skip null/undefined values (they + * can't be compared meaningfully — predicate returns `false`). + */ +export const numberMatcherMatches = ({ + matcher, + value, +}: { + matcher: NumberMatcher | undefined; + value: number | null | undefined; +}): boolean => { + if (matcher === undefined) return true; + if (matcher === null) return value === null || value === undefined; + if (typeof matcher === "number") return value === matcher; + + if ("$eq" in matcher && matcher.$eq !== undefined) { + if (matcher.$eq === null) return value === null || value === undefined; + if (value !== matcher.$eq) return false; + } + if ("$ne" in matcher && matcher.$ne !== undefined) { + if (matcher.$ne === null) { + if (value === null || value === undefined) return false; + } else if (value === matcher.$ne) return false; + } + if (matcher.$in && !matcher.$in.includes(value ?? Number.NaN)) return false; + if (matcher.$nin?.includes(value ?? Number.NaN)) return false; + + const requiresOrdering = + matcher.$gt !== undefined || + matcher.$gte !== undefined || + matcher.$lt !== undefined || + matcher.$lte !== undefined; + if (requiresOrdering && (value === null || value === undefined)) return false; + + if (matcher.$gt !== undefined && !((value ?? 0) > matcher.$gt)) return false; + if (matcher.$gte !== undefined && !((value ?? 0) >= matcher.$gte)) + return false; + if (matcher.$lt !== undefined && !((value ?? 0) < matcher.$lt)) return false; + if (matcher.$lte !== undefined && !((value ?? 0) <= matcher.$lte)) + return false; + + return true; +}; diff --git a/shared/api/migrations/filters/match/stringMatcherMatches.ts b/shared/api/migrations/filters/match/stringMatcherMatches.ts new file mode 100644 index 000000000..e9dd13549 --- /dev/null +++ b/shared/api/migrations/filters/match/stringMatcherMatches.ts @@ -0,0 +1,42 @@ +import type { StringMatcher } from "../matcher.js"; + +/** + * Evaluate a `StringMatcher` against a value. Returns `true` when the + * value satisfies the matcher. + * + * Implemented operators: bare value (equality), `null` shorthand, + * `$eq`, `$ne`, `$in`, `$nin`. `$regex` / `$startsWith` throw — explicit + * gap signal so callers see what's missing instead of silently passing. + * + * `null` and `undefined` on the value side are treated identically. + */ +export const stringMatcherMatches = ({ + matcher, + value, +}: { + matcher: StringMatcher | undefined; + value: string | null | undefined; +}): boolean => { + if (matcher === undefined) return true; + if (matcher === null) return value === null || value === undefined; + if (typeof matcher === "string") return value === matcher; + + if ("$eq" in matcher && matcher.$eq !== undefined) { + if (matcher.$eq === null) return value === null || value === undefined; + if (value !== matcher.$eq) return false; + } + if ("$ne" in matcher && matcher.$ne !== undefined) { + if (matcher.$ne === null) { + if (value === null || value === undefined) return false; + } else if (value === matcher.$ne) return false; + } + if (matcher.$in && !matcher.$in.includes(value ?? "")) return false; + if (matcher.$nin?.includes(value ?? "")) return false; + + const unsupported = ["$regex", "$startsWith"] as const; + for (const op of unsupported) { + if (op in matcher && (matcher as Record)[op] !== undefined) + throw new Error(`stringMatcherMatches: operator ${op} not supported yet`); + } + return true; +}; diff --git a/shared/api/migrations/filters/matcher.ts b/shared/api/migrations/filters/matcher.ts new file mode 100644 index 000000000..6eba96a7f --- /dev/null +++ b/shared/api/migrations/filters/matcher.ts @@ -0,0 +1,80 @@ +import { z } from "zod/v4"; + +/** + * Mongo-style matchers for migration filters. + * + * Rules: + * - Bare value = equality (Mongo convention). `feature_id: "credits"` ≡ + * `feature_id: { $eq: "credits" }`. + * - `null` is a valid bare value: `price: null` matches null fields. + * - Object form lets the caller use operators ($in, $ne, $gt, $regex, ...). + * - Operator keys are prefixed with `$` to disambiguate from nested-object + * filters. Keys without `$` are treated as field names on the resource. + */ + +export const StringMatcherSchema = z.union([ + z.string(), + z.null(), + z.object({ + $eq: z.union([z.string(), z.null()]).optional(), + $ne: z.union([z.string(), z.null()]).optional(), + $in: z.array(z.string()).optional(), + $nin: z.array(z.string()).optional(), + $regex: z.string().optional(), + $startsWith: z.string().optional(), + }), +]); + +export type StringMatcher = z.infer; + +export const NumberMatcherSchema = z.union([ + z.number(), + z.null(), + z.object({ + $eq: z.union([z.number(), z.null()]).optional(), + $ne: z.union([z.number(), z.null()]).optional(), + $in: z.array(z.number()).optional(), + $nin: z.array(z.number()).optional(), + $gt: z.number().optional(), + $gte: z.number().optional(), + $lt: z.number().optional(), + $lte: z.number().optional(), + }), +]); + +export type NumberMatcher = z.infer; + +export const BooleanMatcherSchema = z.boolean(); +export type BooleanMatcher = z.infer; + +/** Build a string-enum matcher: bare literal, null, or { $eq, $ne, $in, $nin }. */ +export const enumMatcher = ( + values: T, +) => { + const literal = z.enum(values); + return z.union([ + literal, + z.null(), + z.object({ + $eq: z.union([literal, z.null()]).optional(), + $ne: z.union([literal, z.null()]).optional(), + $in: z.array(literal).optional(), + $nin: z.array(literal).optional(), + }), + ]); +}; + +/** + * Wrap a nested-object filter with null-equality shorthand. Allows callers to + * write `price: null` (matches null), `price: { $ne: null }` (matches + * non-null), or `price: { interval: "month" }` (non-null AND nested matches). + */ +export const nullableObjectFilter = (inner: T) => + z.union([ + z.null(), + z.object({ + $eq: z.null().optional(), + $ne: z.null().optional(), + }), + inner, + ]); diff --git a/shared/api/migrations/filters/migrationFilter.ts b/shared/api/migrations/filters/migrationFilter.ts new file mode 100644 index 000000000..aa66068ea --- /dev/null +++ b/shared/api/migrations/filters/migrationFilter.ts @@ -0,0 +1,29 @@ +import { z } from "zod/v4"; +import { CustomerFilterSchema } from "./customerFilter.js"; +import { PlanFilterSchema } from "./planFilter.js"; + +/** + * Top-level migration filter, scoped by resource. Mirrors the + * operations shape (`{ customer, plan }`). + * + * - `customer` selects customers via `CustomerFilter`. + * - `plan` selects catalog plans via `PlanFilter` (phase 2+ resource). + * + * At least one resource block is required at runtime. + */ +export const MigrationFilterSchema = z + .object({ + customer: CustomerFilterSchema.optional(), + plan: PlanFilterSchema.optional(), + }) + .check((ctx) => { + if (ctx.value.customer === undefined && ctx.value.plan === undefined) { + ctx.issues.push({ + code: "custom", + message: "filter requires at least one resource block", + input: ctx.value, + }); + } + }); + +export type MigrationFilter = z.infer; diff --git a/shared/api/migrations/filters/planFilter.ts b/shared/api/migrations/filters/planFilter.ts new file mode 100644 index 000000000..fc2df579f --- /dev/null +++ b/shared/api/migrations/filters/planFilter.ts @@ -0,0 +1,73 @@ +import { z } from "zod/v4"; +import { arrayFilter } from "./arrayFilter.js"; +import { + BooleanMatcherSchema, + nullableObjectFilter, + StringMatcherSchema, +} from "./matcher.js"; +import { PlanItemFilterSchema } from "./planItemFilter.js"; + +/** + * Filter over a plan. Migration-scoped: stable contract decoupled from + * `ApiPlanV1`. + * + * Customer-rooted filters automatically scope to active customer-product + * status (`cp.status IN ACTIVE_STATUSES`). + * + * `price` is the plan's BASE price (customer_price linked to a price with + * `entitlement_id IS NULL`). Use `price: null` for free plans, + * `price: { $ne: null }` for paid plans. + * + * `paid` / `recurring` are DERIVED filters — boolean shortcuts that + * compile to EXISTS expressions: + * - `paid: true` → has at least one customer_price (base or item) + * - `recurring: true`→ has at least one customer_price whose price's + * interval is not 'one_off' + * - `addon: false` → product.is_add_on is false + * + * Note: `recurring: true` is a strict subset of `paid: true`. Every + * recurring plan is paid (a recurring price is still a price), but a + * paid plan may be one-off. Combine them only when you mean it — e.g. + * `paid: true, recurring: false` selects one-off-paid plans. + * + * `item` is array-navigation: a bare `PlanItemFilter` is implicit `$some`. + * + * `$or` joins sibling filters with OR instead of the default AND. Sibling + * fields outside `$or` continue to be ANDed with the OR group. + */ + +const PlanPriceFilterInner = z.object({}); + +export type PlanFilter = { + plan_id?: z.infer; + price?: + | null + | { $eq?: null; $ne?: null } + | z.infer; + addon?: z.infer; + /** `recurring: true` already implies a paid plan. */ + paid?: z.infer; + recurring?: z.infer; + item?: + | z.infer + | { + $some?: z.infer; + $every?: z.infer; + $none?: z.infer; + }; + $or?: PlanFilter[]; +}; + +export const PlanFilterSchema: z.ZodType = z.lazy(() => + z.object({ + plan_id: StringMatcherSchema.optional(), + price: nullableObjectFilter(PlanPriceFilterInner).optional(), + addon: BooleanMatcherSchema.optional(), + paid: BooleanMatcherSchema.optional(), + recurring: BooleanMatcherSchema.optional(), + item: arrayFilter(PlanItemFilterSchema).optional(), + $or: z.array(PlanFilterSchema).optional(), + }), +); + +export const DEFAULT_PLAN_FILTER: PlanFilter = {}; diff --git a/shared/api/migrations/filters/planItemFilter.ts b/shared/api/migrations/filters/planItemFilter.ts new file mode 100644 index 000000000..ffef60ae1 --- /dev/null +++ b/shared/api/migrations/filters/planItemFilter.ts @@ -0,0 +1,35 @@ +import { z } from "zod/v4"; +import { + BooleanMatcherSchema, + enumMatcher, + nullableObjectFilter, + StringMatcherSchema, +} from "./matcher.js"; + +/** + * Filter over plan items (the unified price + entitlement view of a feature + * within a plan). Migration-scoped: a stable contract decoupled from the + * public API schema so plan-side changes don't break existing migrations. + * + * Use `price: null` for "free" items (entitlement-only) and + * `price: { $ne: null }` for paid items. + */ + +const BillingMethodSchema = enumMatcher(["prepaid", "usage_based"]); + +const PriceFilterInner = z.object({ + billing_method: BillingMethodSchema.optional(), +}); + +const RolloverFilterInner = z.object({}); + +export const PlanItemFilterSchema = z.object({ + feature_id: StringMatcherSchema.optional(), + unlimited: BooleanMatcherSchema.optional(), + price: nullableObjectFilter(PriceFilterInner).optional(), + rollover: nullableObjectFilter(RolloverFilterInner).optional(), +}); + +export type PlanItemFilter = z.infer; + +export const DEFAULT_PLAN_ITEM_FILTER: PlanItemFilter = {}; diff --git a/shared/api/migrations/operations/customer/addPlan/addPlanOp.ts b/shared/api/migrations/operations/customer/addPlan/addPlanOp.ts new file mode 100644 index 000000000..5c952fe84 --- /dev/null +++ b/shared/api/migrations/operations/customer/addPlan/addPlanOp.ts @@ -0,0 +1,25 @@ +import { z } from "zod/v4"; + +/** + * Ordered customer operation: attach a plan to the customer. Skipped + * (idempotent) if the customer already has an active cusProduct for the + * target plan_id + version. + * + * Runs BEFORE update_plan operations so that a subsequent update_plan + * can target the newly-attached plan. + */ +export const AddPlanOpSchema = z.object({ + type: z.literal("add_plan"), + plan_id: z.string(), + version: z.number().int().positive().optional(), + feature_quantities: z + .array( + z.object({ + feature_id: z.string(), + quantity: z.number().int().nonnegative(), + }), + ) + .optional(), +}); + +export type AddPlanOp = z.infer; diff --git a/shared/api/migrations/operations/customer/addPlan/index.ts b/shared/api/migrations/operations/customer/addPlan/index.ts new file mode 100644 index 000000000..2d52630ab --- /dev/null +++ b/shared/api/migrations/operations/customer/addPlan/index.ts @@ -0,0 +1 @@ +export * from "./addPlanOp.js"; diff --git a/shared/api/migrations/operations/customer/customerOperations.ts b/shared/api/migrations/operations/customer/customerOperations.ts new file mode 100644 index 000000000..0bf784fbc --- /dev/null +++ b/shared/api/migrations/operations/customer/customerOperations.ts @@ -0,0 +1,23 @@ +import { z } from "zod/v4"; +import { AddPlanOpSchema } from "./addPlan/index.js"; +import { UpdatePlanOpSchema } from "./updatePlan/index.js"; + +/** + * Ordered operations applied to each matched customer. + * + * Each operation sees the projected customer state produced by the + * operations before it. `add_plan` inserts a cusProduct that a later + * `update_plan` operation can target. + * + * Execution order: add_plan → update_plan (regardless of array order). + */ +export const CustomerOperationSchema = z.discriminatedUnion("type", [ + AddPlanOpSchema, + UpdatePlanOpSchema, +]); + +export const CustomerOperationsSchema = z.array(CustomerOperationSchema).min(1); + +export type CustomerOperation = z.infer; + +export type CustomerOperations = z.infer; diff --git a/shared/api/migrations/operations/customer/index.ts b/shared/api/migrations/operations/customer/index.ts new file mode 100644 index 000000000..88ec5aead --- /dev/null +++ b/shared/api/migrations/operations/customer/index.ts @@ -0,0 +1,3 @@ +export * from "./addPlan/index.js"; +export * from "./customerOperations.js"; +export * from "./updatePlan/index.js"; diff --git a/shared/api/migrations/operations/customer/updatePlan/index.ts b/shared/api/migrations/operations/customer/updatePlan/index.ts new file mode 100644 index 000000000..5a7d02a90 --- /dev/null +++ b/shared/api/migrations/operations/customer/updatePlan/index.ts @@ -0,0 +1 @@ +export * from "./updatePlanOp.js"; diff --git a/shared/api/migrations/operations/customer/updatePlan/updatePlanOp.ts b/shared/api/migrations/operations/customer/updatePlan/updatePlanOp.ts new file mode 100644 index 000000000..96a39deb5 --- /dev/null +++ b/shared/api/migrations/operations/customer/updatePlan/updatePlanOp.ts @@ -0,0 +1,46 @@ +import { BasePriceParamsSchema } from "@api/products/components/basePrice/basePrice"; +import { PlanItemFilterSchema } from "@api/products/items/filter/planItemFilter"; +import { z } from "zod/v4"; +import { CreatePlanItemParamsV1Schema } from "../../../../products/items/crud/createPlanItemParamsV1.js"; +import { PlanFilterSchema } from "../../../filters/planFilter.js"; + +export const MigrationUpdatePlanCustomizeSchema = z + .object({ + price: BasePriceParamsSchema.nullable().optional(), + add_items: z.array(CreatePlanItemParamsV1Schema).optional(), + remove_items: z.array(PlanItemFilterSchema).optional(), + }) + .refine( + (data) => + data.price !== undefined || + data.add_items !== undefined || + data.remove_items !== undefined, + { + message: + "update_plan.customize requires at least one of price, add_items, or remove_items", + }, + ); + +/** + * Ordered customer operation: update every customer product matched by + * `plan_filter` using update-subscription plan-update semantics. + */ +export const UpdatePlanOpSchema = z + .object({ + type: z.literal("update_plan"), + plan_filter: PlanFilterSchema, + version: z.number().int().positive().optional(), + customize: MigrationUpdatePlanCustomizeSchema.optional(), + }) + .refine( + (data) => data.version !== undefined || data.customize !== undefined, + { + message: "update_plan requires at least one of version or customize", + }, + ); + +export type MigrationUpdatePlanCustomize = z.infer< + typeof MigrationUpdatePlanCustomizeSchema +>; + +export type UpdatePlanOp = z.infer; diff --git a/shared/api/migrations/operations/index.ts b/shared/api/migrations/operations/index.ts new file mode 100644 index 000000000..9429d6d6b --- /dev/null +++ b/shared/api/migrations/operations/index.ts @@ -0,0 +1,2 @@ +export * from "./customer/index.js"; +export * from "./operations.js"; diff --git a/shared/api/migrations/operations/operations.ts b/shared/api/migrations/operations/operations.ts new file mode 100644 index 000000000..b817f50cc --- /dev/null +++ b/shared/api/migrations/operations/operations.ts @@ -0,0 +1,25 @@ +import { z } from "zod/v4"; +import { CustomerOperationsSchema } from "./customer/customerOperations.js"; + +/** + * Top-level migration operations payload, scoped by resource. Mirrors + * the filter shape (`{ customer, plan }`). + * + * Phase 1: only `customer` operations are implemented. `plan` + * (catalog-level operations) is a phase 2+ slot. + */ +export const OperationsSchema = z + .object({ + customer: CustomerOperationsSchema.optional(), + }) + .check((ctx) => { + if (ctx.value.customer === undefined) { + ctx.issues.push({ + code: "custom", + message: "operations requires at least one resource block", + input: ctx.value, + }); + } + }); + +export type Operations = z.infer; diff --git a/shared/api/products/items/mappers/planItemV1ToPriceAndEnt.ts b/shared/api/products/items/mappers/planItemV1ToPriceAndEnt.ts index 7ca4b9323..bc706429c 100644 --- a/shared/api/products/items/mappers/planItemV1ToPriceAndEnt.ts +++ b/shared/api/products/items/mappers/planItemV1ToPriceAndEnt.ts @@ -14,7 +14,7 @@ export const planItemV1ToPriceAndEnt = ({ ctx: SharedContext; item: CreatePlanItemParamsV1; orgId: string; - internalProductId: string; + internalProductId?: string; isCustom: boolean; }) => { const planItemV0 = planItemV1ToV0({ ctx, item }); diff --git a/shared/api/products/utils/match/index.ts b/shared/api/products/utils/match/index.ts new file mode 100644 index 000000000..cf16756d2 --- /dev/null +++ b/shared/api/products/utils/match/index.ts @@ -0,0 +1,2 @@ +export * from "./planFilterMatchesCustomerProduct.js"; +export * from "./planFilterMatchesProduct.js"; diff --git a/shared/api/products/utils/match/planFilterMatchesCustomerProduct.ts b/shared/api/products/utils/match/planFilterMatchesCustomerProduct.ts new file mode 100644 index 000000000..468b84cc5 --- /dev/null +++ b/shared/api/products/utils/match/planFilterMatchesCustomerProduct.ts @@ -0,0 +1,74 @@ +import type { FullCusProduct } from "../../../../models/cusProductModels/cusProductModels.js"; +import { + isCustomerProductAddOn, + isCustomerProductPaid, + isCustomerProductPaidRecurring, +} from "../../../../utils/cusProductUtils/classifyCustomerProduct/classifyCustomerProduct.js"; +import { stringMatcherMatches } from "../../../migrations/filters/match/index.js"; +import type { PlanFilter } from "../../../migrations/filters/planFilter.js"; + +/** + * Predicate: does `filter` match `cusProduct`? + * + * JS-side mirror of `compilePlanFilter` for callers that already have + * the cusproduct in memory (migration runner, scripts). Today supports + * `plan_id`, `addon`, `paid`, `recurring`, and `$or`; `price` and `item` throw to + * make the gap explicit. + */ +export const planFilterMatchesCustomerProduct = ({ + filter, + cusProduct, +}: { + filter: PlanFilter; + cusProduct: FullCusProduct; +}): boolean => { + if (filter.$or !== undefined) { + if ( + !filter.$or.some((subFilter) => + planFilterMatchesCustomerProduct({ filter: subFilter, cusProduct }), + ) + ) { + return false; + } + } + + if (filter.plan_id !== undefined) { + if ( + !stringMatcherMatches({ + matcher: filter.plan_id, + value: cusProduct.product_id, + }) + ) + return false; + } + + if ( + filter.addon !== undefined && + isCustomerProductAddOn(cusProduct) !== filter.addon + ) { + return false; + } + + if ( + filter.paid !== undefined && + isCustomerProductPaid(cusProduct) !== filter.paid + ) { + return false; + } + + if ( + filter.recurring !== undefined && + isCustomerProductPaidRecurring(cusProduct) !== filter.recurring + ) { + return false; + } + + const unsupported = ["price", "item"] as const; + for (const key of unsupported) { + if ((filter as Record)[key] !== undefined) + throw new Error( + `planFilterMatchesCustomerProduct: filter.${key} not supported in JS matcher yet`, + ); + } + return true; +}; diff --git a/shared/api/products/utils/match/planFilterMatchesProduct.ts b/shared/api/products/utils/match/planFilterMatchesProduct.ts new file mode 100644 index 000000000..ba2745147 --- /dev/null +++ b/shared/api/products/utils/match/planFilterMatchesProduct.ts @@ -0,0 +1,61 @@ +import type { FullProduct } from "../../../../models/productModels/productModels.js"; +import { + isFreeProduct, + isOneOffProduct, +} from "../../../../utils/productUtils/classifyProduct/classifyProductUtils.js"; +import { stringMatcherMatches } from "../../../migrations/filters/match/index.js"; +import type { PlanFilter } from "../../../migrations/filters/planFilter.js"; + +export const planFilterMatchesProduct = ({ + filter, + product, +}: { + filter: PlanFilter; + product: FullProduct; +}): boolean => { + if (filter.$or !== undefined) { + if ( + !filter.$or.some((subFilter) => + planFilterMatchesProduct({ filter: subFilter, product }), + ) + ) { + return false; + } + } + + if (filter.plan_id !== undefined) { + if ( + !stringMatcherMatches({ + matcher: filter.plan_id, + value: product.id, + }) + ) { + return false; + } + } + + const paid = !isFreeProduct({ prices: product.prices }); + if (filter.paid !== undefined && paid !== filter.paid) { + return false; + } + + if (filter.addon !== undefined && product.is_add_on !== filter.addon) { + return false; + } + + const recurring = !isOneOffProduct({ prices: product.prices }); + if (filter.recurring !== undefined && recurring !== filter.recurring) { + return false; + } + + const unsupported = ["price", "item"] as const; + for (const key of unsupported) { + if ((filter as Record)[key] !== undefined) { + throw new Error( + `planFilterMatchesProduct: filter.${key} not supported in JS matcher yet`, + ); + } + } + + return true; +}; diff --git a/shared/db/schema.ts b/shared/db/schema.ts index 00bb9328f..566847785 100644 --- a/shared/db/schema.ts +++ b/shared/db/schema.ts @@ -38,6 +38,11 @@ import { migrationErrorRelations } from "../models/migrationModels/migrationErro import { migrationErrors } from "../models/migrationModels/migrationErrorTable.js"; // Migration Tables import { migrationJobs } from "../models/migrationModels/migrationJobTable.js"; +// Migrations V2 +import { migrationItemRuns } from "../models/migrationV2Models/migrationItemRunTable.js"; +import { migrationRunsRelations } from "../models/migrationV2Models/migrationRunRelations.js"; +import { migrationRuns } from "../models/migrationV2Models/migrationRunTable.js"; +import { migrations } from "../models/migrationV2Models/migrationTable.js"; /* RELATIONS */ import { organizationsRelations } from "../models/orgModels/orgRelations.js"; import { organizations } from "../models/orgModels/orgTable.js"; @@ -89,77 +94,81 @@ import { } from "./auth-schema.js"; export { - // Tables - organizations, + account, + actions, + apiKeyRelations, + apiKeys, + autoTopupLimitStates as autoTopupLimits, chatResults, checkouts, - freeTrials, - entitlements, - prices, - features, - products, - customerProducts, - customerPrices, + checkoutsRelations, customerEntitlements, - invoices, - invoiceLineItems, + customerEntitlementsRelations, + customerPrices, + customerPricesRelations, + customerProducts, + customerProductsRelations, customers, - autoTopupLimitStates as autoTopupLimits, + customersRelations, entities, - apiKeys, - metadata, - subscriptions, - rewards, - rewardPrograms, - referralCodes, - rewardRedemptions, - migrationJobs, - migrationErrors, - actions, + entitiesRelations, + entitlements, + entitlementsRelations, events, - replaceables, - rollovers, - schedules, - schedulePhases, - vercelResources, - revenuecatMappings as revcatMappings, - // Auth - user, - session, - account, - verification, - member, + featureRelations, + features, + freeTrialRelations, + freeTrials, invitation, + inviteRelations, + invoiceLineItems, + invoiceRelations, + invoices, // OAuth Provider jwks, - oauthClient, + member, + memberRelations, + metadata, + migrationErrorRelations, + migrationErrors, + migrationItemRuns, + migrationJobs, + migrationRuns, + migrationRunsRelations, + migrations, oauthAccessToken, - oauthRefreshToken, + oauthClient, oauthConsent, + oauthRefreshToken, + // Tables + organizations, // Relations organizationsRelations, - entitlementsRelations, - featureRelations, priceRelations, + prices, productRelations, - freeTrialRelations, - customerProductsRelations, - customerPricesRelations, - customerEntitlementsRelations, - customersRelations, - entitiesRelations, - apiKeyRelations, + products, rewardRelations, rewardProgramRelations, referralCodeRelations, - rewardRedemptionRelations, - migrationErrorRelations, + referralCodes, replaceableRelations, - invoiceRelations, + replaceables, + revenuecatMappings as revcatMappings, + rewardPrograms, + rewardRedemptionRelations, + rewardRedemptions, + rewards, rolloverRelations, - checkoutsRelations, + rollovers, + schedulePhases, + schedules, + session, + subscriptions, + // Auth + user, // Auth Relations userRelations, - memberRelations, - inviteRelations, + vercelResources, + verification, }; diff --git a/shared/drizzle/0010_steady_ares.sql b/shared/drizzle/0010_steady_ares.sql deleted file mode 100644 index 45517c314..000000000 --- a/shared/drizzle/0010_steady_ares.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE UNIQUE INDEX "schedules_customer_scope_unique" - ON "schedules" USING btree ("org_id","env","internal_customer_id") - WHERE "internal_entity_id" IS NULL; ---> statement-breakpoint -CREATE UNIQUE INDEX "schedules_entity_scope_unique" - ON "schedules" USING btree ("org_id","env","internal_customer_id","internal_entity_id") - WHERE "internal_entity_id" IS NOT NULL; diff --git a/shared/enums/ErrCode.ts b/shared/enums/ErrCode.ts index 19b006f98..a091bbc57 100644 --- a/shared/enums/ErrCode.ts +++ b/shared/enums/ErrCode.ts @@ -35,6 +35,9 @@ export const ErrCode = { OrgNotFound: "org_not_found", OrgHasCustomers: "org_has_customers", + // Migrations (v2) + MigrationNotFound: "migration_not_found", + // Feature FeatureNotFound: "feature_not_found", InvalidFeature: "invalid_feature", diff --git a/shared/index.ts b/shared/index.ts index e4b58980a..ee277e6da 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -19,6 +19,9 @@ export * from "./api/billing/updateSubscription/previewUpdateSubscriptionRespons // Cursor pagination utilities export * from "./api/common/cursorPaginationSchemas"; export * from "./api/customers/components/customerExpand/customerExpand"; +// Migrations v2 (operations + entity schemas) +export * from "./api/migrations/filters/index"; +export * from "./api/migrations/operations/index"; // API MODELS export * from "./api/models"; // API VERSIONING SYSTEM @@ -117,9 +120,15 @@ export * from "./models/genModels/processorSchemas"; export * from "./models/migrationModels/migrationErrorTable"; export * from "./models/migrationModels/migrationJobTable"; export * from "./models/migrationModels/migrationModels"; +export * from "./models/migrationV2Models/migrationItemRunSchema"; +export * from "./models/migrationV2Models/migrationItemRunTable"; +export * from "./models/migrationV2Models/migrationRunTable"; +export * from "./models/migrationV2Models/migrationTable"; +export * from "./models/migrationV2Models/pendingMigrationModel"; export * from "./models/orgModels/frontendOrg"; // 1. Org Models export * from "./models/orgModels/frontendOrg"; +export * from "./models/orgModels/fullOrgModel"; export * from "./models/orgModels/orgConfig"; export * from "./models/orgModels/orgConfig"; export * from "./models/orgModels/orgTable"; @@ -188,6 +197,7 @@ export * from "./models/subModels/subTable"; export * from "./types"; // Agent Types (for pricing agent AI) export * from "./utils/agentTypes"; +export * from "./utils/authAccessControl"; export * from "./utils/billingUtils/index"; // Checkout Utils export * from "./utils/checkoutUtils/index"; @@ -199,6 +209,7 @@ export * from "./utils/cusEntUtils/balanceUtils/cusEntToUsageAllowed"; export * from "./utils/cusEntUtils/index"; // Utils export * from "./utils/displayUtils"; +export * from "./utils/fullSubjectUtils"; export * from "./utils/index"; export * from "./utils/intervalUtils"; export * from "./utils/invoices/index"; @@ -224,6 +235,4 @@ export * from "./utils/productV2Utils/productItemUtils/itemIntervalUtils"; export * from "./utils/productV3Utils/productItemUtils/productV3ItemUtils"; export * from "./utils/rewardUtils/rewardFilterUtils"; export * from "./utils/rewardUtils/rewardMigrationUtils"; -export * from "./utils/authAccessControl"; export * from "./utils/scopeDefinitions"; -export * from "./utils/fullSubjectUtils"; diff --git a/shared/models/billingModels/stripe/stripeSubscriptionScheduleAction.ts b/shared/models/billingModels/stripe/stripeSubscriptionScheduleAction.ts index d676822f2..481111615 100644 --- a/shared/models/billingModels/stripe/stripeSubscriptionScheduleAction.ts +++ b/shared/models/billingModels/stripe/stripeSubscriptionScheduleAction.ts @@ -17,6 +17,10 @@ export const StripeSubscriptionScheduleActionSchema = z.discriminatedUnion( type: z.literal("release"), stripeSubscriptionScheduleId: z.string(), }), + z.object({ + type: z.literal("cancel"), + stripeSubscriptionScheduleId: z.string(), + }), ], ); diff --git a/shared/models/cusModels/fullCusModel.ts b/shared/models/cusModels/fullCusModel.ts index fb6290542..948a9b399 100644 --- a/shared/models/cusModels/fullCusModel.ts +++ b/shared/models/cusModels/fullCusModel.ts @@ -7,6 +7,10 @@ import { FullCusProductSchema, } from "../cusProductModels/cusProductModels.js"; import type { Event } from "../eventModels/eventTable.js"; +import { + type MigrationItemRunData, + MigrationItemRunSchema, +} from "../migrationV2Models/migrationItemRunSchema.js"; import type { Schedule, SchedulePhase, @@ -56,6 +60,10 @@ export const FullCustomerSchema = CustomerSchema.extend({ .optional(), invoices: z.array(InvoiceSchema).optional(), schedule: FullCustomerScheduleSchema.optional(), + // `.default([])` makes the FullCustomer cache (which uses this schema for + // hole-filling via `normalizeFromSchema`) tolerant of entries written + // before this field existed. Empty array also matches the SQL default. + migration_item_runs: z.array(MigrationItemRunSchema).optional(), }); export type FullCustomerSchedule = Schedule & { phases: SchedulePhase[] }; @@ -74,6 +82,7 @@ export type FullCustomer = Customer & { events?: Event[]; extra_customer_entitlements: FullCustomerEntitlement[]; schedule?: FullCustomerSchedule; + migration_item_runs?: MigrationItemRunData[]; }; export const CustomerWithProductsSchema = CustomerSchema.extend({ diff --git a/shared/models/cusModels/fullSubject/fullSubjectModel.ts b/shared/models/cusModels/fullSubject/fullSubjectModel.ts index d7ba3f3ce..17899c6ea 100644 --- a/shared/models/cusModels/fullSubject/fullSubjectModel.ts +++ b/shared/models/cusModels/fullSubject/fullSubjectModel.ts @@ -2,6 +2,7 @@ import { z } from "zod/v4"; import { FullAggregatedFeatureBalanceSchema } from "../../cusProductModels/cusEntModels/aggregatedCusEnt.js"; import { FullCustomerEntitlementSchema } from "../../cusProductModels/cusEntModels/cusEntModels.js"; import { FullCusProductSchema } from "../../cusProductModels/cusProductModels.js"; +import { MigrationItemRunSchema } from "../../migrationV2Models/migrationItemRunSchema.js"; import { SubscriptionSchema } from "../../subModels/subModels.js"; import { CustomerSchema } from "../cusModels.js"; import { EntitySchema } from "../entityModels/entityModels.js"; @@ -38,6 +39,8 @@ export const FullSubjectSchema = z.object({ aggregated_subject_flags: z .record(z.string(), AggregatedSubjectFlagSchema) .optional(), + + migration_item_runs: z.array(MigrationItemRunSchema).optional(), }); export type FullSubject = z.infer; diff --git a/shared/models/cusModels/fullSubject/normalizedFullSubjectModel.ts b/shared/models/cusModels/fullSubject/normalizedFullSubjectModel.ts index b837744db..ed2faa099 100644 --- a/shared/models/cusModels/fullSubject/normalizedFullSubjectModel.ts +++ b/shared/models/cusModels/fullSubject/normalizedFullSubjectModel.ts @@ -18,6 +18,7 @@ import { FeatureOptionsSchema, } from "../../cusProductModels/cusProductModels.js"; import type { DbCustomerProduct } from "../../cusProductModels/cusProductTable.js"; +import type { MigrationItemRunData } from "../../migrationV2Models/migrationItemRunSchema.js"; import type { EntitlementWithFeature } from "../../productModels/entModels/entModels.js"; import type { DbFreeTrial } from "../../productModels/freeTrialModels/freeTrialTable.js"; import type { DbPrice } from "../../productModels/priceModels/priceTable.js"; @@ -178,4 +179,9 @@ export type NormalizedFullSubject = { invoices: Invoice[]; entity_aggregations?: EntityAggregations; + + /** Latest 10 `migration_item_runs` for this customer, scoped to the + * org's active lazy/background migrations. Empty when no migrations + * are active for the org. */ + migration_item_runs?: MigrationItemRunData[]; }; diff --git a/shared/models/cusModels/fullSubject/subjectQueryRow.ts b/shared/models/cusModels/fullSubject/subjectQueryRow.ts index 22d2e518a..57becb439 100644 --- a/shared/models/cusModels/fullSubject/subjectQueryRow.ts +++ b/shared/models/cusModels/fullSubject/subjectQueryRow.ts @@ -5,6 +5,7 @@ import type { DbRollover } from "../../cusProductModels/cusEntModels/rolloverMod import type { DbCustomerPrice } from "../../cusProductModels/cusPriceModels/cusPriceTable.js"; import type { DbCustomerProduct } from "../../cusProductModels/cusProductTable.js"; import type { DbFeature } from "../../featureModels/featureTable.js"; +import type { MigrationItemRunData } from "../../migrationV2Models/migrationItemRunSchema.js"; import type { DbEntitlement } from "../../productModels/entModels/entTable.js"; import type { DbFreeTrial } from "../../productModels/freeTrialModels/freeTrialTable.js"; import type { DbPrice } from "../../productModels/priceModels/priceTable.js"; @@ -38,4 +39,5 @@ export type SubjectQueryRow = { subscriptions: Subscription[]; invoices?: Invoice[]; entity?: Entity; + migration_item_runs?: MigrationItemRunData[]; }; diff --git a/shared/models/migrationV2Models/migrationItemRunSchema.ts b/shared/models/migrationV2Models/migrationItemRunSchema.ts new file mode 100644 index 000000000..483ddea18 --- /dev/null +++ b/shared/models/migrationV2Models/migrationItemRunSchema.ts @@ -0,0 +1,25 @@ +import { z } from "zod/v4"; + +/** + * Zod mirror of the `migration_item_runs` row shape. Used for FullSubject / + * FullCustomer cache hole-filling and as the canonical schema-derived type + * (`MigrationItemRunData`) for embedded `migration_item_runs` on those. + * + * Looser than the Drizzle table type by design — `timestamp` arrives as a + * string after `row_to_json` JSON serialization, and embedded item_runs only + * need shallow consumers (the lazy migration helper). + */ +export const MigrationItemRunSchema = z.object({ + migration_item_run_id: z.string(), + migration_internal_id: z.string(), + migration_run_id: z.string().nullable(), + dry_run: z.boolean(), + item_kind: z.string(), + item_id: z.string(), + status: z.enum(["running", "succeeded", "skipped", "failed"]), + timestamp: z.union([z.string(), z.date()]).nullish(), + created_at: z.number(), + updated_at: z.number().nullable(), +}); + +export type MigrationItemRunData = z.infer; diff --git a/shared/models/migrationV2Models/migrationItemRunTable.ts b/shared/models/migrationV2Models/migrationItemRunTable.ts new file mode 100644 index 000000000..f6f01a723 --- /dev/null +++ b/shared/models/migrationV2Models/migrationItemRunTable.ts @@ -0,0 +1,66 @@ +import { sql } from "drizzle-orm"; +import { + boolean, + index, + numeric, + pgTable, + text, + timestamp, + uniqueIndex, +} from "drizzle-orm/pg-core"; + +export const MigrationItemRunStatus = { + Running: "running", + Succeeded: "succeeded", + Skipped: "skipped", + Failed: "failed", +} as const; + +export type MigrationItemRunStatus = + (typeof MigrationItemRunStatus)[keyof typeof MigrationItemRunStatus]; + +export const MigrationItemKind = { + Customer: "customer", + Plan: "plan", + Price: "price", + Feature: "feature", +} as const; + +export type MigrationItemKind = + | (typeof MigrationItemKind)[keyof typeof MigrationItemKind] + | (string & {}); + +export const migrationItemRuns = pgTable( + "migration_item_runs", + { + migration_item_run_id: text().primaryKey().notNull(), + migration_internal_id: text().notNull(), + migration_run_id: text(), + dry_run: boolean().notNull().default(false), + item_kind: text().$type().notNull(), + item_id: text().notNull(), + status: text().$type().notNull(), + timestamp: timestamp({ withTimezone: true }).notNull().default(sql`now()`), + created_at: numeric({ mode: "number" }).notNull(), + updated_at: numeric({ mode: "number" }), + }, + (table) => [ + uniqueIndex("migration_item_runs_live_unique") + .on(table.migration_internal_id, table.item_kind, table.item_id) + .where(sql`${table.dry_run} = false`), + uniqueIndex("migration_item_runs_dry_run_unique") + .on( + table.migration_internal_id, + table.migration_run_id, + table.item_kind, + table.item_id, + ) + .where(sql`${table.dry_run} = true`), + index("migration_item_runs_customer_recent_idx") + .on(table.item_id, sql`${table.updated_at} DESC`) + .where(sql`${table.item_kind} = 'customer'`), + ], +); + +export type MigrationItemRun = typeof migrationItemRuns.$inferSelect; +export type MigrationItemRunInsert = typeof migrationItemRuns.$inferInsert; diff --git a/shared/models/migrationV2Models/migrationRunRelations.ts b/shared/models/migrationV2Models/migrationRunRelations.ts new file mode 100644 index 000000000..ce909c860 --- /dev/null +++ b/shared/models/migrationV2Models/migrationRunRelations.ts @@ -0,0 +1,15 @@ +import { relations } from "drizzle-orm"; +import { organizations } from "../orgModels/orgTable.js"; +import { migrationRuns } from "./migrationRunTable.js"; +import { migrations } from "./migrationTable.js"; + +export const migrationRunsRelations = relations(migrationRuns, ({ one }) => ({ + migration: one(migrations, { + fields: [migrationRuns.migration_internal_id], + references: [migrations.internal_id], + }), + organization: one(organizations, { + fields: [migrationRuns.org_id], + references: [organizations.id], + }), +})); diff --git a/shared/models/migrationV2Models/migrationRunTable.ts b/shared/models/migrationV2Models/migrationRunTable.ts new file mode 100644 index 000000000..6f40ebfaa --- /dev/null +++ b/shared/models/migrationV2Models/migrationRunTable.ts @@ -0,0 +1,66 @@ +import { sql } from "drizzle-orm"; +import { + boolean, + foreignKey, + numeric, + pgTable, + text, + uniqueIndex, +} from "drizzle-orm/pg-core"; +import { organizations } from "../orgModels/orgTable.js"; +import { migrations } from "./migrationTable.js"; + +export const MigrationRunStatus = { + Queued: "queued", + Running: "running", + Succeeded: "succeeded", + Failed: "failed", +} as const; + +export type MigrationRunStatus = + (typeof MigrationRunStatus)[keyof typeof MigrationRunStatus]; + +export const ACTIVE_MIGRATION_RUN_STATUSES = [ + MigrationRunStatus.Queued, + MigrationRunStatus.Running, +] as const; + +export const migrationRuns = pgTable( + "migration_runs", + { + internal_id: text().primaryKey().notNull(), + migration_internal_id: text().notNull(), + org_id: text().notNull(), + env: text().notNull(), + status: text().$type().notNull(), + dry_run: boolean().notNull(), + lazy_run: boolean().notNull().default(false), + trigger_run_id: text(), + error_message: text(), + created_at: numeric({ mode: "number" }).notNull(), + updated_at: numeric({ mode: "number" }), + started_at: numeric({ mode: "number" }), + finished_at: numeric({ mode: "number" }), + }, + (table) => [ + foreignKey({ + columns: [table.migration_internal_id], + foreignColumns: [migrations.internal_id], + name: "migration_runs_migration_internal_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "migration_runs_org_id_fkey", + }).onDelete("cascade"), + // One queued/running run per migration definition. Different migrations + // can be active concurrently — correctness lives at the per-customer + // `migration_item_runs_live_unique` claim. + uniqueIndex("migration_runs_active_per_migration_unique") + .on(table.migration_internal_id) + .where(sql`${table.status} IN ('queued', 'running')`), + ], +); + +export type MigrationRun = typeof migrationRuns.$inferSelect; +export type MigrationRunInsert = typeof migrationRuns.$inferInsert; diff --git a/shared/models/migrationV2Models/migrationTable.ts b/shared/models/migrationV2Models/migrationTable.ts new file mode 100644 index 000000000..0124dd10c --- /dev/null +++ b/shared/models/migrationV2Models/migrationTable.ts @@ -0,0 +1,69 @@ +import { + boolean, + foreignKey, + jsonb, + numeric, + pgTable, + text, + uniqueIndex, +} from "drizzle-orm/pg-core"; +import type { MigrationFilter } from "../../api/migrations/filters/migrationFilter.js"; +import type { Operations } from "../../api/migrations/operations/operations.js"; +import { organizations } from "../orgModels/orgTable.js"; + +/** + * Loose typing here — the structured `PreparedState` definition lives + * server-side under `server/src/internal/migrations/v2/prepare/types.ts` + * (alongside the prep modules that produce it). Server callers narrow as + * needed via the Zod schema there. + */ +type LoosePreparedState = Record; + +/** + * User-authored, customer-state-mutating migrations. Distinct from the + * legacy `migration_jobs` table (product-version migration system). + * + * `internal_id` is the ksuid primary key; `id` is the user-provided + * slug, unique per `(org_id, env)`. `filter` and `operations` are + * typed jsonb blobs validated by the Zod schemas at + * `shared/api/migrations/{filters,operations}/`. + */ +export const migrations = pgTable( + "migrations", + { + internal_id: text().primaryKey().notNull(), + id: text().notNull(), + org_id: text().notNull(), + env: text().notNull(), + + filter: jsonb().$type(), + operations: jsonb().$type(), + // Snapshot of the last successful prepare-run output, keyed by + // module key. See shared/api/migrations/prepare/preparedStateTypes.ts. + prepared_state: jsonb().$type(), + + // `null` (default) → infer from compute output. + // `true` → force DB-only path; throw if any Stripe-relevant mutation slips in. + // `false` → force Stripe path even when inference would say DB-only. + no_billing_changes: boolean(), + retry_failed: boolean().notNull().default(false), + + created_at: numeric({ mode: "number" }).notNull(), + updated_at: numeric({ mode: "number" }), + }, + (table) => [ + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "migrations_org_id_fkey", + }).onDelete("cascade"), + uniqueIndex("migrations_org_env_id_unique").on( + table.org_id, + table.env, + table.id, + ), + ], +); + +export type Migration = typeof migrations.$inferSelect; +export type MigrationInsert = typeof migrations.$inferInsert; diff --git a/shared/models/migrationV2Models/pendingMigrationModel.ts b/shared/models/migrationV2Models/pendingMigrationModel.ts new file mode 100644 index 000000000..967b8722e --- /dev/null +++ b/shared/models/migrationV2Models/pendingMigrationModel.ts @@ -0,0 +1,4 @@ +import type { MigrationRun } from "./migrationRunTable.js"; +import type { Migration } from "./migrationTable.js"; + +export type PendingMigration = MigrationRun & { migration: Migration }; diff --git a/shared/models/orgModels/fullOrgModel.ts b/shared/models/orgModels/fullOrgModel.ts new file mode 100644 index 000000000..3982039ef --- /dev/null +++ b/shared/models/orgModels/fullOrgModel.ts @@ -0,0 +1,6 @@ +import type { Feature } from "../featureModels/featureModels.js"; +import type { Organization } from "./orgTable.js"; + +export type FullOrg = Organization & { + features: Feature[]; +}; diff --git a/shared/models/orgModels/orgRelations.ts b/shared/models/orgModels/orgRelations.ts index a2dbac475..fb1381534 100644 --- a/shared/models/orgModels/orgRelations.ts +++ b/shared/models/orgModels/orgRelations.ts @@ -2,6 +2,7 @@ import { relations } from "drizzle-orm"; import { member } from "../../db/auth-schema.js"; import { apiKeys } from "../devModels/apiKeyTable.js"; import { features } from "../featureModels/featureTable.js"; +import { migrationRuns } from "../migrationV2Models/migrationRunTable.js"; import { organizations } from "./orgTable.js"; export const organizationsRelations = relations( @@ -10,6 +11,7 @@ export const organizationsRelations = relations( api_keys: many(apiKeys), features: many(features), members: many(member), + migration_runs: many(migrationRuns), master: one(organizations, { fields: [organizations.created_by], references: [organizations.id], diff --git a/shared/models/orgModels/orgTable.ts b/shared/models/orgModels/orgTable.ts index e560e85cf..da54cb996 100644 --- a/shared/models/orgModels/orgTable.ts +++ b/shared/models/orgModels/orgTable.ts @@ -1,4 +1,5 @@ import type { ProcessorConfigs } from "@models/genModels/processorSchemas.js"; +import type { PendingMigration } from "@models/migrationV2Models/pendingMigrationModel.js"; import { sql } from "drizzle-orm"; import { boolean, @@ -120,6 +121,7 @@ export const organizations = pgTable( export type Organization = typeof organizations.$inferSelect & { master: Organization | null; + pendingMigrations?: PendingMigration[]; }; // Multi tenancy flow <-> stripe connect... diff --git a/shared/models/productModels/priceModels/priceModels.ts b/shared/models/productModels/priceModels/priceModels.ts index 1b5facca1..8163e196b 100644 --- a/shared/models/productModels/priceModels/priceModels.ts +++ b/shared/models/productModels/priceModels/priceModels.ts @@ -17,7 +17,7 @@ const ProrationConfigSchema = z.object({ export const PriceSchema = z.object({ id: z.string(), - internal_product_id: z.string(), + internal_product_id: z.string().nullable(), org_id: z.string().optional(), created_at: z.number().optional(), diff --git a/shared/utils/fullSubjectUtils/fullSubjectToFullCustomer.ts b/shared/utils/fullSubjectUtils/fullSubjectToFullCustomer.ts index 2407730d6..7dbdf90ee 100644 --- a/shared/utils/fullSubjectUtils/fullSubjectToFullCustomer.ts +++ b/shared/utils/fullSubjectUtils/fullSubjectToFullCustomer.ts @@ -13,4 +13,5 @@ export const fullSubjectToFullCustomer = ({ extra_customer_entitlements: fullSubject.extra_customer_entitlements, subscriptions: fullSubject.subscriptions, invoices: fullSubject.invoices, + migration_item_runs: fullSubject.migration_item_runs, }); diff --git a/shared/utils/fullSubjectUtils/normalizedToFullSubject.ts b/shared/utils/fullSubjectUtils/normalizedToFullSubject.ts index 2cd23066a..cd6766e72 100644 --- a/shared/utils/fullSubjectUtils/normalizedToFullSubject.ts +++ b/shared/utils/fullSubjectUtils/normalizedToFullSubject.ts @@ -397,5 +397,6 @@ export const normalizedToFullSubject = ({ ...(aggregatedSubjectFlags ? { aggregated_subject_flags: aggregatedSubjectFlags } : {}), + migration_item_runs: normalized.migration_item_runs ?? [], } as FullSubject; }; diff --git a/shared/utils/productV2Utils/productItemUtils/mappers/itemToPriceAndEnt.ts b/shared/utils/productV2Utils/productItemUtils/mappers/itemToPriceAndEnt.ts index 9cb66087b..aea21aa28 100644 --- a/shared/utils/productV2Utils/productItemUtils/mappers/itemToPriceAndEnt.ts +++ b/shared/utils/productV2Utils/productItemUtils/mappers/itemToPriceAndEnt.ts @@ -72,7 +72,7 @@ const toPrice = ({ }: { item: ProductItem; orgId: string; - internalProductId: string; + internalProductId?: string; isCustom: boolean; newVersion?: boolean; curPrice?: Price; @@ -91,13 +91,13 @@ const toPrice = ({ id: item.price_id || curPrice?.id || priceId(), created_at: item.created_at || Date.now(), org_id: orgId, - internal_product_id: internalProductId, + internal_product_id: internalProductId ?? null, is_custom: isCustom, config, proration_config: null, }; - if (isCustom || newVersion) { + if ((isCustom || newVersion) && !item.price_id) { price = { ...price, id: priceId(), @@ -158,7 +158,7 @@ export const toFeature = ({ rollover: item.config?.rollover, }; - if (isCustom || newVersion) { + if ((isCustom || newVersion) && !item.entitlement_id) { ent = { ...ent, id: entitlementId(), @@ -182,7 +182,7 @@ const toFeatureAndPrice = ({ item: ProductItem; orgId: string; internalFeatureId: string; - internalProductId: string; + internalProductId?: string; isCustom: boolean; curPrice?: Price; curEnt?: Entitlement; @@ -199,7 +199,7 @@ const toFeatureAndPrice = ({ org_id: orgId, created_at: item.created_at || Date.now(), is_custom: isCustom, - internal_product_id: internalProductId, + internal_product_id: internalProductId ?? null, internal_feature_id: internalFeatureId, feature_id: item.feature_id!, @@ -218,7 +218,7 @@ const toFeatureAndPrice = ({ // Will only create new ent id if const newEnt = !curEnt || (isCustom && !entsAreSame(curEnt, ent)); - if (newEnt || newVersion) { + if ((newEnt || newVersion) && !item.entitlement_id) { ent = { ...ent, id: entitlementId(), @@ -284,7 +284,7 @@ const toFeatureAndPrice = ({ id: item.price_id || curPrice?.id || priceId(), created_at: item.created_at || Date.now(), org_id: orgId, - internal_product_id: internalProductId, + internal_product_id: internalProductId ?? null, is_custom: isCustom, config, entitlement_id: ent.id, @@ -317,7 +317,7 @@ const toFeatureAndPrice = ({ price.config = newConfig; } - if (isCustom || newVersion) { + if ((isCustom || newVersion) && !item.price_id) { price = { ...price, id: priceId(), @@ -341,7 +341,7 @@ export const itemToPriceAndEnt = ({ }: { item: ProductItem; orgId: string; - internalProductId: string; + internalProductId?: string; feature?: Feature; curPrice?: Price; curEnt?: Entitlement; diff --git a/shared/utils/scopeDefinitions.ts b/shared/utils/scopeDefinitions.ts index 3516b3e8e..ed3073396 100644 --- a/shared/utils/scopeDefinitions.ts +++ b/shared/utils/scopeDefinitions.ts @@ -34,6 +34,7 @@ export const RESOURCES = [ "rewards", "balances", "billing", + "migrations", "analytics", "apiKeys", "platform", @@ -146,6 +147,10 @@ export const Scopes = { Read: "billing:read", Write: "billing:write", }, + Migrations: { + Read: "migrations:read", + Write: "migrations:write", + }, Analytics: { Read: "analytics:read", }, @@ -211,6 +216,8 @@ export const MODERN_SCOPES: readonly ScopeString[] = [ Scopes.Balances.Write, Scopes.Billing.Read, Scopes.Billing.Write, + Scopes.Migrations.Read, + Scopes.Migrations.Write, Scopes.Analytics.Read, Scopes.ApiKeys.Read, Scopes.ApiKeys.Write, @@ -373,6 +380,7 @@ export const ROLE_SCOPES: Record = { Scopes.Plans.Write, Scopes.Balances.Write, Scopes.Billing.Write, + Scopes.Migrations.Write, Scopes.Analytics.Read, Scopes.ApiKeys.Write, Scopes.Platform.Write, @@ -394,6 +402,7 @@ export const ROLE_SCOPES: Record = { Scopes.Rewards.Read, Scopes.Balances.Read, Scopes.Billing.Read, + Scopes.Migrations.Read, Scopes.Analytics.Read, Scopes.ApiKeys.Read, Scopes.Platform.Read, @@ -450,6 +459,11 @@ export const RESOURCE_METADATA: Record< namePlural: "Billing", description: "Attach, cancel, and update subscriptions", }, + migrations: { + name: "Migration", + namePlural: "Migrations", + description: "Bulk customer migrations and operations", + }, analytics: { name: "Analytics", namePlural: "Analytics", diff --git a/trigger.config.ts b/trigger.config.ts new file mode 100644 index 000000000..7f9f2cc1a --- /dev/null +++ b/trigger.config.ts @@ -0,0 +1,126 @@ +import { + additionalFiles, + additionalPackages, + aptGet, + syncEnvVars, +} from "@trigger.dev/build/extensions/core"; +import { defineConfig } from "@trigger.dev/sdk/v3"; +import { fetchInfisicalSecretsFromEnv } from "./server/src/external/infisical/fetchInfisicalSecrets.js"; + +const workspacePackageJsonPaths = [ + "server/package.json", + "shared/package.json", + "vite/package.json", + "scripts/package.json", + "apps/checkout/package.json", + "apps/docs/package.json", + "apps/website/package.json", + "apps/sdk-test/package.json", + "packages/atmn/package.json", + "packages/atmn-tests/package.json", + "packages/sdk/package.json", + "packages/autumn-js/package.json", + "packages/openapi/package.json", + "packages/ksuid/package.json", + "packages/stripe-sync/package.json", +]; + +const workspacePackageDirs = workspacePackageJsonPaths.map((path) => + path.replace(/\/package\.json$/, ""), +); + +export default defineConfig({ + project: "proj_cwiutfmpdzfcshxevkok", + runtime: "bun", + logLevel: "log", + maxDuration: 3600, + retries: { + enabledInDev: true, + default: { + maxAttempts: 3, + minTimeoutInMs: 1000, + maxTimeoutInMs: 10000, + factor: 2, + randomize: true, + }, + }, + dirs: ["server/src/trigger"], + build: { + // Native / heavy deps stay external — bundling them inflates the deploy + // and breaks platform-specific binaries (pg, ioredis, etc.). + external: [ + "@aws-sdk/client-s3", + "@google-cloud/bigquery", + "ioredis", + "pino", + "@axiomhq/pino", + "drizzle-orm", + "postgres", + "pg", + "zod", + ], + extensions: [ + additionalFiles({ + files: workspacePackageJsonPaths, + }), + { + name: "monorepo-workspace-manifests", + onBuildComplete: (context) => { + if (context.target !== "deploy") { + return; + } + + context.addLayer({ + id: "monorepo-workspace-manifests", + image: { + instructions: [ + "WORKDIR /app", + `RUN mkdir -p ${workspacePackageDirs.join(" ")} && chown -R bun:bun ${workspacePackageDirs.join(" ")}`, + ...workspacePackageJsonPaths.map( + (path) => `COPY --chown=bun:bun ${path} ./${path}`, + ), + "RUN chown -R bun:bun /app", + ], + }, + }); + }, + }, + // Server runtime imports `*.lua` as raw text via Bun's loader. esbuild + // (used by the trigger build) doesn't see bunfig — register a plugin + // that reads .lua files as text so the same imports work post-bundle. + { + name: "lua-text-loader", + onBuildStart: async (context) => { + context.registerPlugin({ + name: "lua-text-loader-plugin", + setup(build) { + build.onLoad({ filter: /\.lua$/ }, async (args) => { + const { readFileSync } = await import("node:fs"); + return { + contents: readFileSync(args.path, "utf-8"), + loader: "text", + }; + }); + }, + }); + }, + }, + aptGet({ packages: ["pkg-config", "liblzma-dev"] }), + additionalPackages({ + packages: [ + "@axiomhq/pino", + "postgres", + "pg", + "drizzle-orm", + "ioredis", + "zod", + ], + }), + // In DEV: `bun d` runs `bunx trigger.dev dev` under + // `infisical run --env=dev --recursive --`, so the trigger CLI + // inherits secrets from process.env directly. `syncEnvVars` only + // runs at DEPLOY time (pushes Infisical secrets to trigger cloud). + syncEnvVars(async (ctx) => fetchInfisicalSecretsFromEnv(ctx.env)), + ], + }, +}); diff --git a/vite/package.json b/vite/package.json index 98fdb7c35..98e9438f3 100644 --- a/vite/package.json +++ b/vite/package.json @@ -38,6 +38,7 @@ "@tanstack/react-query": "catalog:", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.18", + "@trigger.dev/react-hooks": "4.4.5", "ag-charts-community": "^12.0.2", "ag-charts-react": "^12.0.2", "ag-grid-community": "^34.0.2", @@ -54,6 +55,7 @@ "date-fns": "^3.6.0", "decimal.js": "^10.5.0", "input-otp": "^1.4.2", + "json5": "^2.2.3", "lodash": "^4.17.21", "lucide-react": "^0.562.0", "motion": "^12.26.1", diff --git a/vite/src/App.tsx b/vite/src/App.tsx index ff358ac5b..279ea4748 100644 --- a/vite/src/App.tsx +++ b/vite/src/App.tsx @@ -24,6 +24,8 @@ import CustomerPlanEditor from "./views/customers2/customer-plan/CustomerPlanEdi import { DefaultView } from "./views/DefaultView"; import DevScreen from "./views/developer/DevView"; import { CloseScreen } from "./views/general/CloseScreen"; +import { MigrationsView } from "./views/migrations/MigrationsView"; +import { MigrationView } from "./views/migrations/migration/MigrationView"; import QuickstartView from "./views/onboarding4/QuickstartView"; import ProductsView from "./views/products/ProductsView"; import PlanEditorView from "./views/products/plan/PlanEditorView"; @@ -106,6 +108,13 @@ export default function App() { path="/sandbox/products" element={} /> + } /> + } /> + } /> + } + /> ( props?.className, )} {...props} + onClick={(e) => { + e.stopPropagation(); + e.preventDefault(); + props.onClick?.(e); + }} > diff --git a/vite/src/components/v2/breadcrumb.tsx b/vite/src/components/v2/breadcrumb.tsx index 99170024f..bb1c5da61 100644 --- a/vite/src/components/v2/breadcrumb.tsx +++ b/vite/src/components/v2/breadcrumb.tsx @@ -37,7 +37,7 @@ export default function V2Breadcrumb({ onClick={() => item.href && navigateTo(item.href, navigate, env)} className="cursor-pointer" > - {item.name} + {item.name} {index < items.length - 1 && } diff --git a/vite/src/components/v2/dropdowns/FeatureSearchDropdown.tsx b/vite/src/components/v2/dropdowns/FeatureSearchDropdown.tsx index 0913d19fb..c2b94f6f8 100644 --- a/vite/src/components/v2/dropdowns/FeatureSearchDropdown.tsx +++ b/vite/src/components/v2/dropdowns/FeatureSearchDropdown.tsx @@ -20,6 +20,7 @@ export function FeatureSearchDropdown({ onOpenChange, renderExtra, footer, + triggerClassName, }: { features: Feature[]; value: string | null; @@ -29,6 +30,7 @@ export function FeatureSearchDropdown({ onOpenChange?: (open: boolean) => void; renderExtra?: (feature: Feature) => React.ReactNode; footer?: React.ReactNode; + triggerClassName?: string; }) { const [internalOpen, setInternalOpen] = useState(false); const [search, setSearch] = useState(""); @@ -58,6 +60,7 @@ export function FeatureSearchDropdown({ type="button" className={cn( "flex items-center justify-between w-full rounded-lg border bg-transparent text-sm outline-none h-input input-base input-shadow-default input-state-open p-2", + triggerClassName, )} > {selectedFeature ? ( @@ -75,22 +78,22 @@ export function FeatureSearchDropdown({ -
- +
+ setSearch(e.target.value)} onKeyDown={(e) => e.stopPropagation()} - className="flex-1 bg-transparent text-sm outline-none placeholder:text-t4" + className="flex-1 bg-transparent text-xs outline-none placeholder:text-t4" />
-
+
{filteredFeatures.length === 0 ? ( -
+
No features found.
) : ( @@ -101,18 +104,20 @@ export function FeatureSearchDropdown({ onSelect(feature.id); setIsOpen(false); }} - className="py-2 px-2.5" + className="py-1.5 px-2" > -
-
{getFeatureIcon({ feature })}
- {feature.name} - {renderExtra?.(feature)} -
+
{getFeatureIcon({ feature })}
+ {feature.name} + {renderExtra?.(feature)} )) )}
- {footer} + {footer && ( +
+ {footer} +
+ )} ); diff --git a/vite/src/components/v2/empty-states/EmptyState.tsx b/vite/src/components/v2/empty-states/EmptyState.tsx index 05f1f5d11..9c15ceb42 100644 --- a/vite/src/components/v2/empty-states/EmptyState.tsx +++ b/vite/src/components/v2/empty-states/EmptyState.tsx @@ -17,7 +17,8 @@ export const EmptyState = ({ | "rewards" | "archived-plans" | "no-customers-found" - | "analytics"; + | "analytics" + | "migrations"; actionButton?: React.ReactNode; }) => { const getEmptyStateContent = () => { @@ -76,6 +77,14 @@ export const EmptyState = ({ "Create an API key to authenticate requests to the Autumn API", svg: apiKeysSvg, }; + case "migrations": + return { + title: "Migrations", + description: + "Define filters and operations to migrate sets of customers in bulk", + // Reuses plans illustration for now — replace once we have a dedicated svg. + svg: plansSvg, + }; } }; diff --git a/vite/src/contexts/ThemeProvider.tsx b/vite/src/contexts/ThemeProvider.tsx index 37c9988b6..d9d0a27a8 100644 --- a/vite/src/contexts/ThemeProvider.tsx +++ b/vite/src/contexts/ThemeProvider.tsx @@ -1,4 +1,5 @@ import { createContext, useContext, useEffect, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; import { useLocalStorage } from "@/hooks/common/useLocalStorage"; type Theme = "light" | "dark" | "system"; @@ -50,6 +51,11 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { return () => mediaQuery.removeEventListener("change", handleChange); }, [theme]); + useHotkeys("t", () => setTheme(isDark ? "light" : "dark"), { + enabled: import.meta.env.DEV, + enableOnFormTags: false, + }); + return ( {children} diff --git a/vite/src/hooks/common/useAutumnFlags.tsx b/vite/src/hooks/common/useAutumnFlags.tsx index 265632458..9a3bac22a 100644 --- a/vite/src/hooks/common/useAutumnFlags.tsx +++ b/vite/src/hooks/common/useAutumnFlags.tsx @@ -8,7 +8,7 @@ export const useAutumnFlags = () => { const [flags, setFlags] = useLocalStorage("autumn.flags", { pkey: false, - webhooks: false, + webhooks: true, stripe_key: false, platform: false, vercel: false, @@ -20,7 +20,7 @@ export const useAutumnFlags = () => { const nextFlags = { pkey: notNullish(customer.flags.pkey), - webhooks: notNullish(customer.flags.webhooks), + webhooks: true, stripe_key: notNullish(customer.flags.stripe_key), platform: notNullish(customer.flags.platform), vercel: notNullish(customer.flags.vercel), @@ -40,5 +40,5 @@ export const useAutumnFlags = () => { } }, [customer?.flags]); - return flags; + return { ...flags, webhooks: true }; }; diff --git a/vite/src/hooks/common/useTab.tsx b/vite/src/hooks/common/useTab.tsx index 74044d014..ae1750ed4 100644 --- a/vite/src/hooks/common/useTab.tsx +++ b/vite/src/hooks/common/useTab.tsx @@ -23,6 +23,11 @@ export const useTab = () => { pathname.startsWith("/sandbox/products") ) { return "products"; + } else if ( + pathname.startsWith("/migrations") || + pathname.startsWith("/sandbox/migrations") + ) { + return "migrations"; } else if ( pathname.startsWith("/customers") || pathname.startsWith("/sandbox/customers") diff --git a/vite/src/hooks/queries/useMigrationFilterPreview.ts b/vite/src/hooks/queries/useMigrationFilterPreview.ts new file mode 100644 index 000000000..6c0a85224 --- /dev/null +++ b/vite/src/hooks/queries/useMigrationFilterPreview.ts @@ -0,0 +1,50 @@ +import type { CustomerFilter, CustomerWithProducts } from "@autumn/shared"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { useMemo } from "react"; +import { useQueryKeyFactory } from "@/hooks/common/useQueryKeyFactory"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; + +const DEFAULT_PAGE_SIZE = 10; + +interface FilterPreviewResponse { + count: number; + customers: CustomerWithProducts[]; + page: number; + pageSize: number; +} + +export const useMigrationFilterPreview = ({ + filter, + search = "", + page = 0, + pageSize = DEFAULT_PAGE_SIZE, +}: { + filter: CustomerFilter; + search?: string; + page?: number; + pageSize?: number; +}) => { + const axiosInstance = useAxiosInstance(); + const buildKey = useQueryKeyFactory(); + const filterKey = useMemo(() => JSON.stringify(filter), [filter]); + const queryKey = buildKey(["migration-filter-preview", filterKey, search, page, pageSize]); + + const query = useQuery({ + queryKey, + queryFn: async () => { + const { data } = await axiosInstance.post( + "/migrations.filter.preview", + { filter, search, page, pageSize }, + ); + return data; + }, + staleTime: 500, + placeholderData: keepPreviousData, + }); + + return { + count: query.data?.count ?? null, + customers: query.data?.customers ?? [], + isLoading: query.isLoading, + }; +}; diff --git a/vite/src/hooks/queries/useMigrationRunsQuery.ts b/vite/src/hooks/queries/useMigrationRunsQuery.ts new file mode 100644 index 000000000..fd1e454a3 --- /dev/null +++ b/vite/src/hooks/queries/useMigrationRunsQuery.ts @@ -0,0 +1,105 @@ +import type { + MigrationItemKind, + MigrationRun, + MigrationRunStatus, +} from "@autumn/shared"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useCallback } from "react"; +import { useQueryKeyFactory } from "@/hooks/common/useQueryKeyFactory"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; + +const ACTIVE_STATUSES: MigrationRunStatus[] = ["queued", "running"]; +const POLL_MS = 30000; + +export type MigrationItemEventStatus = "succeeded" | "skipped" | "failed"; + +export interface MigrationItemPreview { + id?: string | null; + name?: string | null; + email?: string | null; +} + +export interface MigrationItemEvent { + timestamp: string; + org_id: string; + env: string; + migration_internal_id: string; + migration_run_id: string; + dry_run: boolean; + item_kind: MigrationItemKind; + item_id: string; + item_preview: MigrationItemPreview | null; + status: MigrationItemEventStatus; + response: Record | null; +} + +function findActiveRun(runs: MigrationRun[]): MigrationRun | undefined { + return runs.find((r) => ACTIVE_STATUSES.includes(r.status)); +} + +export const useMigrationRunsQuery = ({ + migrationId, + migrationRunId, + enabled = true, +}: { + migrationId: string; + migrationRunId?: string; + enabled?: boolean; +}) => { + const axiosInstance = useAxiosInstance(); + const queryClient = useQueryClient(); + const buildKey = useQueryKeyFactory(); + const runsQueryKey = buildKey(["migration-runs", migrationId]); + const eventsQueryKey = buildKey([ + "migration-item-events", + migrationId, + migrationRunId ?? "all", + ]); + + const runsQuery = useQuery<{ list: MigrationRun[] }>({ + queryKey: runsQueryKey, + queryFn: async () => { + const { data } = await axiosInstance.post<{ list: MigrationRun[] }>( + "/migrations.runs.list", + { migrationId }, + ); + return data; + }, + enabled, + refetchOnWindowFocus: true, + staleTime: 0, + refetchInterval: POLL_MS, + }); + + const activeRun = findActiveRun(runsQuery.data?.list ?? []); + const isActive = !!activeRun; + + const eventsQuery = useQuery<{ list: MigrationItemEvent[] }>({ + queryKey: eventsQueryKey, + queryFn: async () => { + const { data } = await axiosInstance.post<{ + list: MigrationItemEvent[]; + }>("/migrations.item_events.list", { migrationId, migrationRunId }); + return data; + }, + enabled, + refetchOnWindowFocus: true, + staleTime: 0, + refetchInterval: POLL_MS, + }); + + const invalidate = useCallback(() => { + queryClient.invalidateQueries({ queryKey: runsQueryKey }); + queryClient.invalidateQueries({ queryKey: eventsQueryKey }); + }, [queryClient, runsQueryKey, eventsQueryKey]); + + return { + runs: (runsQuery.data?.list ?? []) as MigrationRun[], + isLoadingRuns: runsQuery.isLoading, + isActive, + activeRunDryRun: activeRun?.dry_run ?? null, + itemEvents: (eventsQuery.data?.list ?? []) as MigrationItemEvent[], + isLoadingEvents: eventsQuery.isLoading, + invalidate, + }; +}; diff --git a/vite/src/hooks/queries/useMigrationsQuery.tsx b/vite/src/hooks/queries/useMigrationsQuery.tsx new file mode 100644 index 000000000..f2457f99b --- /dev/null +++ b/vite/src/hooks/queries/useMigrationsQuery.tsx @@ -0,0 +1,128 @@ +import type { Migration } from "@autumn/shared"; +import type { MigrationFilter } from "@autumn/shared/api/migrations/filters/migrationFilter.js"; +import type { Operations } from "@autumn/shared/api/migrations/operations/operations.js"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQueryKeyFactory } from "@/hooks/common/useQueryKeyFactory"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; + +interface PrepareModuleResult { + key: string; + kind: string; + result: unknown; +} + +interface PrepareResponse { + migration_id: string; + dry_run: boolean; + modules: PrepareModuleResult[]; + warnings: string[]; +} + +export const useMigrationsQuery = () => { + const axiosInstance = useAxiosInstance(); + const buildKey = useQueryKeyFactory(); + const queryClient = useQueryClient(); + const queryKey = buildKey(["migrations"]); + + const { data, isLoading, error, refetch } = useQuery<{ list: Migration[] }>({ + queryKey, + queryFn: async () => { + const { data } = await axiosInstance.post<{ list: Migration[] }>( + "/migrations.list", + ); + return data; + }, + }); + + const invalidate = () => queryClient.invalidateQueries({ queryKey }); + + const createMutation = useMutation({ + mutationFn: async (body: { id: string }) => { + const { data } = await axiosInstance.post( + "/migrations.create", + body, + ); + return data; + }, + onSuccess: invalidate, + }); + + const updateMutation = useMutation({ + mutationFn: async (body: { + id: string; + updates: { + id?: string; + filter?: MigrationFilter | null; + operations?: Operations | null; + retry_failed?: boolean; + no_billing_changes?: boolean; + }; + }) => { + const { data } = await axiosInstance.post( + "/migrations.update", + body, + ); + return data; + }, + onSuccess: invalidate, + }); + + const deleteMutation = useMutation({ + mutationFn: async (body: { id: string }) => { + const { data } = await axiosInstance.post( + "/migrations.delete", + body, + ); + return data; + }, + onSuccess: invalidate, + }); + + const prepareMutation = useMutation({ + mutationFn: async (body: { id: string; dry_run: boolean }) => { + const { data } = await axiosInstance.post( + "/migrations.prepare", + body, + ); + return data; + }, + onSuccess: invalidate, + }); + + const runMutation = useMutation({ + mutationFn: async (body: { + id: string; + dry_run?: boolean; + limit?: number; + only?: string[]; + concurrency?: number; + }) => { + const { data } = await axiosInstance.post<{ + migration_id: string; + dry_run: boolean; + run_id: string; + trigger_run_id?: string; + public_access_token?: string; + }>("/migrations.run", body); + return data; + }, + onSuccess: invalidate, + }); + + return { + migrations: (data?.list ?? []) as Migration[], + isLoading, + error, + refetch, + createMigration: createMutation.mutateAsync, + isCreating: createMutation.isPending, + updateMigration: updateMutation.mutateAsync, + isUpdating: updateMutation.isPending, + deleteMigration: deleteMutation.mutateAsync, + isDeleting: deleteMutation.isPending, + prepareMigration: prepareMutation.mutateAsync, + isPreparing: prepareMutation.isPending, + runMigration: runMutation.mutateAsync, + isRunning: runMutation.isPending, + }; +}; diff --git a/vite/src/views/customers/components/filter-dropdown/SavedViews.tsx b/vite/src/views/customers/components/filter-dropdown/SavedViews.tsx index 73acebb0f..74588605d 100644 --- a/vite/src/views/customers/components/filter-dropdown/SavedViews.tsx +++ b/vite/src/views/customers/components/filter-dropdown/SavedViews.tsx @@ -84,10 +84,10 @@ export const SavedViews = ({ return ( <> - - Saved views - + + Saved views + {views.map((view: SavedView) => (
void; + hideSavedViews?: boolean; +} + +export function CustomerListFilterButton({ + extraMenuItems, + hasActiveExtraFilters, + onClearExtra, + hideSavedViews, +}: CustomerListFilterButtonProps = {}) { const { queryStates, setFilters } = useCustomerFilters(); const [open, setOpen] = useState(false); const hasActiveFilters = + hasActiveExtraFilters || queryStates.status.length > 0 || queryStates.version.length > 0 || queryStates.none || @@ -34,6 +47,7 @@ export function CustomerListFilterButton() { const clearFilters = () => { setFilters({ status: [], version: [], none: false, processor: [] }); + onClearExtra?.(); }; const closeFilterModal = () => { @@ -59,12 +73,13 @@ export function CustomerListFilterButton() { align="start" > + {extraMenuItems} - {views.length > 0 && ( + {!hideSavedViews && views.length > 0 && ( ( ( + "sidebar.customerGroupOpen", + true, + ); const [devGroupOpen, setDevGroupOpen] = useLocalStorage( "sidebar.devGroupOpen", true, @@ -188,12 +194,36 @@ export const MainSidebar = ({ }, ]} /> - } - title="Customers" - env={env} - /> + {isAdmin ? ( + } + title="Customers" + env={env} + isOpen={customerGroupOpen} + onToggle={() => setCustomerGroupOpen((prev) => !prev)} + subTabs={[ + { + title: "All Customers", + value: "customers", + icon: , + }, + { + title: "Migrations", + value: "migrations", + path: "/migrations", + icon: , + }, + ]} + /> + ) : ( + } + title="Customers" + env={env} + /> + )} } diff --git a/vite/src/views/migrations/MigrationsView.tsx b/vite/src/views/migrations/MigrationsView.tsx new file mode 100644 index 000000000..af54db24e --- /dev/null +++ b/vite/src/views/migrations/MigrationsView.tsx @@ -0,0 +1,11 @@ +import { MigrationListTable } from "./migration-list/MigrationListTable"; + +export const MigrationsView = () => { + return ( +
+
+ +
+
+ ); +}; diff --git a/vite/src/views/migrations/components/CreateMigrationDialog.tsx b/vite/src/views/migrations/components/CreateMigrationDialog.tsx new file mode 100644 index 000000000..a569755d1 --- /dev/null +++ b/vite/src/views/migrations/components/CreateMigrationDialog.tsx @@ -0,0 +1,90 @@ +import type { AxiosError } from "axios"; +import { useState } from "react"; +import { useNavigate } from "react-router"; +import { toast } from "sonner"; +import { ShortcutButton } from "@/components/v2/buttons/ShortcutButton"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/v2/dialogs/Dialog"; +import { Input } from "@/components/v2/inputs/Input"; +import { useMigrationsQuery } from "@/hooks/queries/useMigrationsQuery"; +import { getBackendErr, navigateTo } from "@/utils/genUtils"; + +export function CreateMigrationDialog({ + open: controlledOpen, + onOpenChange: controlledOnOpenChange, +}: { + open?: boolean; + onOpenChange?: (open: boolean) => void; +} = {}) { + const [internalOpen, setInternalOpen] = useState(false); + const [id, setId] = useState(""); + const navigate = useNavigate(); + + const open = controlledOpen !== undefined ? controlledOpen : internalOpen; + const handleOpenChange = (nextOpen: boolean) => { + if (nextOpen) setId(""); + (controlledOnOpenChange || setInternalOpen)(nextOpen); + }; + + const { createMigration, isCreating } = useMigrationsQuery(); + + const handleCreateMigration = async () => { + if (!id.trim()) { + toast.error("Migration ID is required"); + return; + } + try { + const created = await createMigration({ id: id.trim() }); + toast.success("Migration created"); + handleOpenChange(false); + navigateTo(`/migrations/${created.id}`, navigate); + } catch (error: unknown) { + toast.error( + getBackendErr(error as AxiosError, "Failed to create migration"), + ); + } + }; + + return ( + + + + Create a migration + + Give your migration a unique ID. You can configure its filter and + operations after creation. + + + +
+ + setId(e.target.value)} + /> +
+ + + + Create migration + + +
+
+ ); +} diff --git a/vite/src/views/migrations/migration-list/MigrationListColumns.tsx b/vite/src/views/migrations/migration-list/MigrationListColumns.tsx new file mode 100644 index 000000000..f93c47449 --- /dev/null +++ b/vite/src/views/migrations/migration-list/MigrationListColumns.tsx @@ -0,0 +1,48 @@ +import type { Migration } from "@autumn/shared"; +import type { ColumnDef, Row } from "@tanstack/react-table"; +import { format } from "date-fns"; +import { MiniCopyButton } from "@/components/v2/buttons/CopyButton"; + +export const createMigrationListColumns = (): ColumnDef< + Migration, + unknown +>[] => [ + { + header: "ID", + size: 240, + accessorKey: "id", + cell: ({ row }: { row: Row }) => ( +
+ +
+ ), + }, + { + header: "Filter", + size: 120, + cell: ({ row }: { row: Row }) => ( + + {row.original.filter ? "Configured" : "—"} + + ), + }, + { + header: "Operations", + size: 120, + cell: ({ row }: { row: Row }) => ( + + {row.original.operations ? "Configured" : "—"} + + ), + }, + { + header: "Created", + size: 160, + accessorKey: "created_at", + cell: ({ row }: { row: Row }) => ( + + {format(new Date(row.original.created_at), "PP")} + + ), + }, +]; diff --git a/vite/src/views/migrations/migration-list/MigrationListCreateButton.tsx b/vite/src/views/migrations/migration-list/MigrationListCreateButton.tsx new file mode 100644 index 000000000..b99f0954e --- /dev/null +++ b/vite/src/views/migrations/migration-list/MigrationListCreateButton.tsx @@ -0,0 +1,26 @@ +import { useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { Button } from "@/components/v2/buttons/Button"; +import { CreateMigrationDialog } from "../components/CreateMigrationDialog"; + +export function MigrationListCreateButton() { + const [open, setOpen] = useState(false); + + useHotkeys( + "n", + (event) => { + event.preventDefault(); + setOpen(true); + }, + { enableOnFormTags: false }, + ); + + return ( + <> + + + + ); +} diff --git a/vite/src/views/migrations/migration-list/MigrationListTable.tsx b/vite/src/views/migrations/migration-list/MigrationListTable.tsx new file mode 100644 index 000000000..44c9db333 --- /dev/null +++ b/vite/src/views/migrations/migration-list/MigrationListTable.tsx @@ -0,0 +1,78 @@ +import type { Migration } from "@autumn/shared"; +import { ArrowsClockwiseIcon } from "@phosphor-icons/react"; +import { useMemo } from "react"; +import { Table } from "@/components/general/table"; +import { EmptyState } from "@/components/v2/empty-states/EmptyState"; +import { useMigrationsQuery } from "@/hooks/queries/useMigrationsQuery"; +import { pushPage } from "@/utils/genUtils"; +import { useProductTable } from "@/views/products/hooks/useProductTable"; +import { createMigrationListColumns } from "./MigrationListColumns"; +import { MigrationListCreateButton } from "./MigrationListCreateButton"; + +export function MigrationListTable() { + const { migrations, isLoading } = useMigrationsQuery(); + + const columns = useMemo(() => createMigrationListColumns(), []); + + const table = useProductTable({ + data: migrations, + columns, + options: { + globalFilterFn: "includesString", + enableGlobalFilter: true, + }, + }); + + const hasRows = table.getRowModel().rows.length > 0; + + const getRowHref = (row: Migration) => + pushPage({ path: `/migrations/${row.id}` }); + + if (!isLoading && !hasRows) { + return ( + } + /> + ); + } + + return ( + + +
+ + + Migrations + + +
+ +
+
+
+
+
+ + + + + + +
+
+ ); +} diff --git a/vite/src/views/migrations/migration/FilterStep.tsx b/vite/src/views/migrations/migration/FilterStep.tsx new file mode 100644 index 000000000..fdd8ac6ee --- /dev/null +++ b/vite/src/views/migrations/migration/FilterStep.tsx @@ -0,0 +1,47 @@ +import type { MigrationFilter } from "@autumn/shared"; +import { ArrowRightIcon } from "@phosphor-icons/react"; +import { Button } from "@/components/v2/buttons/Button"; +import { CustomerPreview, useCustomerCount } from "./filters/CustomerPreview"; +import { FilterForm } from "./filters/FilterForm"; +import { type StepId, StepIndicator } from "./StepIndicator"; +import type { useMigrationEditorForm } from "./useMigrationEditorForm"; + +type FormInstance = ReturnType["form"]; + +export function FilterStep({ + form, + filter, + step, + onStepChange, + onNext, +}: { + form: FormInstance; + filter: MigrationFilter; + step: StepId; + onStepChange: (step: StepId) => void; + onNext: () => void; +}) { + const customerCount = useCustomerCount(filter.customer ?? {}); + const hasCustomers = customerCount !== null && customerCount > 0; + + return ( +
+ + + + form.setFieldValue("filter", v)} + /> + +
+ ); +} diff --git a/vite/src/views/migrations/migration/MigrationEditor.tsx b/vite/src/views/migrations/migration/MigrationEditor.tsx new file mode 100644 index 000000000..97e2b12c9 --- /dev/null +++ b/vite/src/views/migrations/migration/MigrationEditor.tsx @@ -0,0 +1,83 @@ +import type { Migration } from "@autumn/shared"; +import { useStore } from "@tanstack/react-form"; +import { useEffect } from "react"; +import { parseAsStringLiteral, useQueryState } from "nuqs"; +import { FilterStep } from "./FilterStep"; +import { useCustomerCount } from "./filters/CustomerPreview"; +import { useGuardedStepNavigation } from "./hooks/useGuardedStepNavigation"; +import { MigrationLiveView } from "./live/MigrationLiveView"; +import { useMigrationSheetStore } from "./live/useMigrationSheetStore"; +import { OperationsStep } from "./OperationsStep"; +import { STEPS, type StepId } from "./StepIndicator"; +import { useMigrationEditorForm } from "./useMigrationEditorForm"; + +const STEP_IDS = STEPS.map((s) => s.id); + +export function MigrationEditor({ migration }: { migration: Migration }) { + const [step, setStep] = useQueryState( + "step", + parseAsStringLiteral(STEP_IDS).withDefault("filter"), + ); + const { form, saveError, enableErrorDisplay } = useMigrationEditorForm({ + migration, + }); + const filter = useStore(form.store, (s) => s.values.filter); + const operations = useStore(form.store, (s) => s.values.operations); + const noBillingChanges = useStore( + form.store, + (s) => s.values.noBillingChanges, + ); + const customerCount = useCustomerCount(filter.customer ?? {}); + const hasCustomers = customerCount !== null && customerCount > 0; + + const setLiveFormState = useMigrationSheetStore((s) => s.setLiveFormState); + useEffect(() => { + setLiveFormState({ operations, noBillingChanges }); + }, [operations, noBillingChanges, setLiveFormState]); + + const guardedSetStep = useGuardedStepNavigation({ + step, + hasCustomers, + operations, + saveError, + enableErrorDisplay, + setStep, + }); + + return ( +
+ {step === "filter" && ( + guardedSetStep("operations")} + /> + )} + {step === "operations" && ( + setStep("filter")} + onNext={() => guardedSetStep("live")} + saveError={saveError} + /> + )} + {step === "live" && ( + setStep("operations")} + /> + )} +
+ ); +} diff --git a/vite/src/views/migrations/migration/MigrationView.tsx b/vite/src/views/migrations/migration/MigrationView.tsx new file mode 100644 index 000000000..25ddc2516 --- /dev/null +++ b/vite/src/views/migrations/migration/MigrationView.tsx @@ -0,0 +1,127 @@ +import { AnimatePresence, motion } from "motion/react"; +import { useCallback, useEffect } from "react"; +import { createPortal } from "react-dom"; +import { useHotkeys } from "react-hotkeys-hook"; +import { useNavigate, useParams } from "react-router"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { SheetContainer } from "@/components/v2/sheets/InlineSheet"; +import { SheetCloseButton } from "@/components/v2/sheets/SheetCloseButton"; +import { useMigrationsQuery } from "@/hooks/queries/useMigrationsQuery"; +import { navigateTo } from "@/utils/genUtils"; +import { SHEET_ANIMATION } from "@/views/customers2/customer/customerAnimations"; +import ErrorScreen from "@/views/general/ErrorScreen"; +import LoadingScreen from "@/views/general/LoadingScreen"; +import { MigrationCustomerSheet } from "./live/MigrationCustomerSheet"; +import { useMigrationSheetStore } from "./live/useMigrationSheetStore"; +import { MigrationEditor } from "./MigrationEditor"; + +export function MigrationView() { + const { migration_id } = useParams<{ migration_id: string }>(); + const { migrations, isLoading } = useMigrationsQuery(); + const navigate = useNavigate(); + + const selectedCustomer = useMigrationSheetStore((s) => s.selectedCustomer); + const setSelectedCustomer = useMigrationSheetStore( + (s) => s.setSelectedCustomer, + ); + const liveFormState = useMigrationSheetStore((s) => s.liveFormState); + const closeSheet = useCallback( + () => setSelectedCustomer(null), + [setSelectedCustomer], + ); + + useHotkeys("escape", closeSheet); + useEffect(() => () => closeSheet(), [closeSheet]); + + const migration = migrations.find((m) => m.id === migration_id); + + const goToMigrations = () => navigateTo("/migrations", navigate); + + if (isLoading) return ; + + if (!migration) { + return ( + +
Migration not found
+
+ ); + } + + return ( +
+ +
+
+
+ + + + Migrations + + + + {migration.id} + + + + +
+
+
+
+ + {createPortal( + + {selectedCustomer && ( + + )} + , + document.body, + )} + + + {selectedCustomer && ( + + + + + + + )} + +
+ ); +} diff --git a/vite/src/views/migrations/migration/OperationsStep.tsx b/vite/src/views/migrations/migration/OperationsStep.tsx new file mode 100644 index 000000000..c0cb9f106 --- /dev/null +++ b/vite/src/views/migrations/migration/OperationsStep.tsx @@ -0,0 +1,86 @@ +import type { Operations } from "@autumn/shared"; +import { + ArrowLeftIcon, + ArrowRightIcon, + InfoIcon, + WarningCircleIcon, +} from "@phosphor-icons/react"; +import { Button } from "@/components/v2/buttons/Button"; +import { OperationsForm } from "./operations/OperationsForm"; +import { type StepId, StepIndicator } from "./StepIndicator"; +import { hasValidOperations } from "./shared/operationUtils"; +import type { useMigrationEditorForm } from "./useMigrationEditorForm"; + +type FormInstance = ReturnType["form"]; + +function hasVersionOperation(operations: Operations): boolean { + return ( + operations.customer?.some( + (op) => op.type === "update_plan" && op.version !== undefined, + ) ?? false + ); +} + +export function OperationsStep({ + form, + operations, + noBillingChanges, + step, + onStepChange, + onPrevious, + onNext, + saveError, +}: { + form: FormInstance; + operations: Operations; + noBillingChanges: boolean; + step: StepId; + onStepChange: (step: StepId) => void; + onPrevious: () => void; + onNext: () => void; + saveError: string | null; +}) { + const canProceed = hasValidOperations(operations) && !saveError; + + return ( +
+ + + + + form.setFieldValue("operations", v)} + noBillingChanges={noBillingChanges} + onNoBillingChangesChange={(v) => + form.setFieldValue("noBillingChanges", v) + } + /> + {saveError && ( +
+ + {saveError} +
+ )} + {hasVersionOperation(operations) && ( +
+ + + Version updates won't apply to customers with custom plans. + +
+ )} +
+ ); +} diff --git a/vite/src/views/migrations/migration/StepIndicator.tsx b/vite/src/views/migrations/migration/StepIndicator.tsx new file mode 100644 index 000000000..9569e3c8b --- /dev/null +++ b/vite/src/views/migrations/migration/StepIndicator.tsx @@ -0,0 +1,59 @@ +import type { Icon } from "@phosphor-icons/react"; +import { + CaretRightIcon, + FunnelSimpleIcon, + GearIcon, + UsersIcon, +} from "@phosphor-icons/react"; +import type { ReactNode } from "react"; +import { cn } from "@/lib/utils"; + +export type StepId = "filter" | "operations" | "live"; + +export const STEPS: { id: StepId; label: string; icon: Icon }[] = [ + { id: "filter", label: "Filter", icon: FunnelSimpleIcon }, + { id: "operations", label: "Operations", icon: GearIcon }, + { id: "live", label: "Execution", icon: UsersIcon }, +]; + +export function StepIndicator({ + step, + onStepChange, + children, +}: { + step: StepId; + onStepChange: (step: StepId) => void; + children?: ReactNode; +}) { + return ( +
+
+ {STEPS.map((s, i) => { + const isActive = step === s.id; + const StepIcon = s.icon; + return ( +
+ {i > 0 && } + +
+ ); + })} +
+ {children &&
{children}
} +
+ ); +} diff --git a/vite/src/views/migrations/migration/filters/CustomerPreview.tsx b/vite/src/views/migrations/migration/filters/CustomerPreview.tsx new file mode 100644 index 000000000..d90809027 --- /dev/null +++ b/vite/src/views/migrations/migration/filters/CustomerPreview.tsx @@ -0,0 +1,187 @@ +import type { CustomerFilter, CustomerWithProducts } from "@autumn/shared"; +import { + CaretLeftIcon, + CaretRightIcon, + ListMagnifyingGlassIcon, + UsersIcon, +} from "@phosphor-icons/react"; +import type { PaginationState } from "@tanstack/react-table"; +import { debounce } from "lodash"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Table } from "@/components/general/table"; +import { IconButton } from "@/components/v2/buttons/IconButton"; +import { Input } from "@/components/v2/inputs/Input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/v2/selects/Select"; +import { Separator } from "@/components/v2/separator"; +import { useMigrationFilterPreview } from "@/hooks/queries/useMigrationFilterPreview"; +import { cn } from "@/lib/utils"; +import { createCustomerListColumns } from "@/views/customers2/components/table/customer-list/CustomerListColumns"; +import { useProductTable } from "@/views/products/hooks/useProductTable"; + +const PAGE_SIZE_OPTIONS = [10, 50, 100, 250]; + +export function CustomerPreview({ filter }: { filter: CustomerFilter }) { + const [search, setSearch] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const debouncedSetSearch = useMemo( + () => debounce((q: string) => setDebouncedSearch(q), 350), + [], + ); + + useEffect(() => () => debouncedSetSearch.cancel(), [debouncedSetSearch]); + + const handleSearchChange = useCallback( + (e: React.ChangeEvent) => { + setSearch(e.target.value); + setPagination((p) => ({ ...p, pageIndex: 0 })); + debouncedSetSearch(e.target.value.trim()); + }, + [debouncedSetSearch], + ); + + const filterKey = useMemo(() => JSON.stringify(filter), [filter]); + useEffect(() => { + setPagination((p) => ({ ...p, pageIndex: 0 })); + }, [filterKey]); + + const { count, customers, isLoading } = useMigrationFilterPreview({ + filter, + search: debouncedSearch, + page: pagination.pageIndex, + pageSize: pagination.pageSize, + }); + + const pageCount = + count !== null ? Math.max(Math.ceil(count / pagination.pageSize), 1) : 1; + const columns = useMemo( + () => createCustomerListColumns().filter((col) => col.id !== "actions"), + [], + ); + + const table = useProductTable({ + data: customers, + columns, + options: { + manualPagination: true, + pageCount, + state: { pagination }, + onPaginationChange: setPagination, + }, + }); + + const currentPage = pagination.pageIndex + 1; + const canPrev = pagination.pageIndex > 0; + const canNext = count !== null && currentPage < pageCount; + + return ( +
+ + + + + Filtered Customers + + + + {count !== null + ? `${count} ${count === 1 ? "match" : "matches"}` + : ""} + + + +
+
+ + +
+
+ } + onClick={() => + setPagination((p) => ({ ...p, pageIndex: p.pageIndex - 1 })) + } + disabled={!canPrev} + className={cn(!canPrev && "pointer-events-none opacity-50")} + /> + + {currentPage} / {pageCount} + + } + onClick={() => + setPagination((p) => ({ ...p, pageIndex: p.pageIndex + 1 })) + } + disabled={!canNext} + className={cn(!canNext && "pointer-events-none opacity-50")} + /> + +
+
+ + + + + + + + +
+ ); +} + +export function useCustomerCount(filter: CustomerFilter): number | null { + const { count } = useMigrationFilterPreview({ filter }); + return count; +} diff --git a/vite/src/views/migrations/migration/filters/FilterForm.tsx b/vite/src/views/migrations/migration/filters/FilterForm.tsx new file mode 100644 index 000000000..37351f807 --- /dev/null +++ b/vite/src/views/migrations/migration/filters/FilterForm.tsx @@ -0,0 +1,205 @@ +import type { + MigrationFilter, + PlanFilter, + StringMatcher, +} from "@autumn/shared"; +import { FunnelSimpleIcon } from "@phosphor-icons/react"; +import { useMemo, useRef, useState } from "react"; +import { Button } from "@/components/v2/buttons/Button"; +import { ActionCard } from "../shared/ActionCard"; +import { AddButton } from "../shared/AddButton"; +import { FilterGroup } from "./FilterGroup"; +import { + customerIdToStrings, + type FilterGroupData, + type FilterOperator, + type FilterRule, + groupsToPlanFilter, + planFilterToGroups, + stringsToCustomerId, +} from "./filterRowTypes"; + +const DEFAULT_PLAN_FILTER: PlanFilter = { plan_id: "" }; + +function inferCustomerIdOperator( + matcher: StringMatcher | undefined, + count: number, +): FilterOperator { + if (matcher && typeof matcher === "object") { + if ("$nin" in matcher) return "not_in"; + if ("$ne" in matcher) return "is_not"; + if ("$in" in matcher) return "in"; + } + return count > 1 ? "in" : "is"; +} + +function buildGroups(value: MigrationFilter): FilterGroupData[] { + const planFilter = + (value.customer?.plan as PlanFilter) ?? DEFAULT_PLAN_FILTER; + const planGroups = planFilterToGroups(planFilter); + const matcher = value.customer?.customer_id as StringMatcher | undefined; + const ids = customerIdToStrings(matcher); + if (ids.length === 0) return planGroups; + const rule: FilterRule = { + field: "customer_id", + operator: inferCustomerIdOperator(matcher, ids.length), + values: ids, + }; + const [first, ...rest] = planGroups; + return [{ rules: [rule, ...(first?.rules ?? [])] }, ...rest]; +} + +function ruleToCustomerIdMatcher(rule: FilterRule): StringMatcher | undefined { + if (rule.values.length === 0) return undefined; + switch (rule.operator) { + case "is": + return rule.values[0]; + case "is_not": + return { $ne: rule.values[0] }; + case "in": + return { $in: rule.values }; + case "not_in": + return { $nin: rule.values }; + default: + return stringsToCustomerId(rule.values); + } +} + +function groupsToMigrationFilter( + groups: FilterGroupData[], + base: MigrationFilter, +): MigrationFilter { + let customerIdMatcher: StringMatcher | undefined; + const cleaned = groups.map((g) => ({ + rules: g.rules.filter((r) => { + if (r.field === "customer_id") { + customerIdMatcher = ruleToCustomerIdMatcher(r); + return false; + } + return true; + }), + })); + const planFilter = groupsToPlanFilter(cleaned); + const hasPlanFilter = Object.keys(planFilter).length > 0; + return { + ...base, + customer: { + ...base.customer, + customer_id: customerIdMatcher, + plan: hasPlanFilter ? planFilter : undefined, + }, + }; +} + +const EMPTY_GROUP: FilterGroupData = { + rules: [{ field: "plan_id", operator: "is", values: [] }], +}; + +function isEmptyFilter(groups: FilterGroupData[]): boolean { + if (groups.length !== 1) return false; + const rules = groups[0].rules; + if (rules.length === 0) return true; + return rules.length === 1 && rules[0].values.length === 0; +} + +export function FilterForm({ + value, + onChange, +}: { + value: MigrationFilter; + onChange: (value: MigrationFilter) => void; +}) { + const [autoOpenField, setAutoOpenField] = useState(false); + const externalKey = useMemo(() => JSON.stringify(value), [value]); + const lastSyncedKey = useRef(externalKey); + const groupsRef = useRef(buildGroups(value)); + + if (lastSyncedKey.current !== externalKey) { + lastSyncedKey.current = externalKey; + groupsRef.current = buildGroups(value); + } + + const setGroups = (next: FilterGroupData[]) => { + groupsRef.current = next; + const updated = groupsToMigrationFilter(next, value); + lastSyncedKey.current = JSON.stringify(updated); + onChange(updated); + }; + + const groups = groupsRef.current; + const isEmpty = isEmptyFilter(groups) && !autoOpenField; + + const updateGroup = (index: number, group: FilterGroupData) => { + const next = [...groups]; + next[index] = group; + setGroups(next); + }; + + const deleteGroup = (index: number) => { + const next = groups.filter((_, i) => i !== index); + if (next.length === 0) { + setAutoOpenField(false); + setGroups([EMPTY_GROUP]); + } else { + setGroups(next); + } + }; + + const addGroup = () => setGroups([...groups, EMPTY_GROUP]); + + const clearAll = () => { + setAutoOpenField(false); + setGroups([EMPTY_GROUP]); + }; + + const hasConditions = groups.some((g) => g.rules.length > 0); + + if (isEmpty) { + return ( + + } + heading="Add Filter" + subheading="Define which customers this migration applies to" + onClick={() => setAutoOpenField(true)} + className="w-full" + /> + ); + } + + return ( +
+ {groups.map((group, index) => ( +
+ {index > 0 &&
} + updateGroup(index, updated)} + onDelete={() => deleteGroup(index)} + showDelete={groups.length > 1} + autoOpenField={index === 0 && autoOpenField} + /> +
+ ))} + {hasConditions && ( +
+ + +
+ )} +
+ ); +} diff --git a/vite/src/views/migrations/migration/filters/FilterGroup.tsx b/vite/src/views/migrations/migration/filters/FilterGroup.tsx new file mode 100644 index 000000000..2cabdc93a --- /dev/null +++ b/vite/src/views/migrations/migration/filters/FilterGroup.tsx @@ -0,0 +1,163 @@ +import { UserIcon } from "@phosphor-icons/react"; +import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; +import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; +import { useCusSearchQueryV2 } from "@/views/customers/hooks/useCusSearchQuery"; +import { AddButton } from "../shared/AddButton"; +import { buildFeatureSuggestions } from "../shared/featureSuggestions"; +import { buildPlanSuggestions } from "../shared/planSuggestions"; +import { RemoveButton } from "../shared/RemoveButton"; +import type { ValuePickerOption } from "../shared/ValuePicker"; +import { FilterRow } from "./FilterRow"; +import { + FIELD_CONFIGS, + FILTER_FIELD_OPTIONS, + type FilterField, + type FilterGroupData, + type FilterRule, +} from "./filterRowTypes"; + +const BILLING_METHOD_SUGGESTIONS: ValuePickerOption[] = [ + { value: "prepaid", label: "Prepaid" }, + { value: "usage_based", label: "Usage Based" }, +]; + +const ITEM_MODE_SUGGESTIONS: ValuePickerOption[] = [ + { value: "some", label: "Any match (some)" }, + { value: "every", label: "All match (every)" }, + { value: "none", label: "None match (none)" }, +]; + +function useSuggestionsForField( + field: string, +): ValuePickerOption[] | undefined { + const { products } = useProductsQuery(); + const { features } = useFeaturesQuery(); + const { customers } = useCusSearchQueryV2({ + search: "", + page: 1, + page_size: 250, + }); + + if (field === "customer_id") { + return customers + .filter((c): c is typeof c & { id: string } => Boolean(c.id)) + .map((c) => ({ + value: c.id, + label: c.name ?? c.email ?? c.id, + icon: , + })); + } + if (field === "plan_id") return buildPlanSuggestions(products); + if (field === "item_feature_id") { + return buildFeatureSuggestions(features); + } + if (field === "item_billing_method") return BILLING_METHOD_SUGGESTIONS; + if (field === "item_mode") return ITEM_MODE_SUGGESTIONS; + return undefined; +} + +function getNextAvailableField(usedFields: Set): FilterRule { + const available = FILTER_FIELD_OPTIONS.find( + (opt) => !usedFields.has(opt.value), + ); + const field: FilterField = available?.value ?? "plan_id"; + return { + field, + operator: FIELD_CONFIGS[field].operators[0].value, + values: [], + }; +} + +export function FilterGroup({ + group, + onChange, + onDelete, + showDelete, + groupIndex, + autoOpenField = false, +}: { + group: FilterGroupData; + onChange: (group: FilterGroupData) => void; + onDelete: () => void; + showDelete: boolean; + groupIndex: number; + autoOpenField?: boolean; +}) { + const updateRule = (index: number, rule: FilterRule) => { + const newRules = [...group.rules]; + newRules[index] = rule; + onChange({ rules: newRules }); + }; + + const removeRule = (index: number) => { + const newRules = group.rules.filter((_, i) => i !== index); + onChange({ rules: newRules }); + }; + + const addRule = () => { + const usedFields = new Set(group.rules.map((r) => r.field)); + onChange({ + rules: [...group.rules, getNextAvailableField(usedFields)], + }); + }; + + return ( +
+
+
+ + {groupIndex === 0 ? "Where" : "Or"} + + {group.rules.length > 0 && ( + + {group.rules.length}{" "} + {group.rules.length === 1 ? "condition" : "conditions"} + + )} +
+ {showDelete && } +
+ {group.rules.map((rule, index) => ( + 0 ? "And" : undefined} + onChange={(updated) => updateRule(index, updated)} + onRemove={() => removeRule(index)} + defaultOpenField={index === 0 && autoOpenField} + /> + ))} + +
+ ); +} + +function FilterRowWithSuggestions({ + rule, + connector, + onChange, + onRemove, + defaultOpenField = false, +}: { + rule: FilterRule; + connector?: "And"; + onChange: (rule: FilterRule) => void; + onRemove: () => void; + defaultOpenField?: boolean; +}) { + const suggestions = useSuggestionsForField(rule.field); + return ( + + ); +} diff --git a/vite/src/views/migrations/migration/filters/FilterRow.tsx b/vite/src/views/migrations/migration/filters/FilterRow.tsx new file mode 100644 index 000000000..424ccad39 --- /dev/null +++ b/vite/src/views/migrations/migration/filters/FilterRow.tsx @@ -0,0 +1,199 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/v2/selects/Select"; +import { cn } from "@/lib/utils"; +import { BooleanPill } from "../shared/BooleanPill"; +import { RemoveButton } from "../shared/RemoveButton"; +import { ValuePicker } from "../shared/ValuePicker"; +import { + FIELD_CONFIGS, + FILTER_FIELD_OPTIONS, + type FieldConfig, + type FilterField, + type FilterOperator, + type FilterRule, +} from "./filterRowTypes"; + +function inferOperator(current: FilterOperator, count: number): FilterOperator { + if (count > 1 && current === "is") return "in"; + if (count > 1 && current === "is_not") return "not_in"; + if (count <= 1 && current === "in") return "is"; + if (count <= 1 && current === "not_in") return "is_not"; + return current; +} + +export function FilterRow({ + rule, + connector, + onChange, + onRemove, + suggestions, + defaultOpenField = false, +}: { + rule: FilterRule; + connector?: "And"; + onChange: (rule: FilterRule) => void; + onRemove: () => void; + suggestions?: { value: string; label: string }[]; + defaultOpenField?: boolean; +}) { + const config = FIELD_CONFIGS[rule.field]; + const fieldLabel = + FILTER_FIELD_OPTIONS.find((f) => f.value === rule.field)?.label ?? + rule.field; + + const handleToggle = (toggled: string) => { + const isSelected = rule.values.includes(toggled); + const next = isSelected + ? rule.values.filter((v) => v !== toggled) + : [...rule.values, toggled]; + onChange({ + ...rule, + operator: inferOperator(rule.operator, next.length), + values: next, + }); + }; + + const handleChipRemove = (removed: string) => { + const next = rule.values.filter((v) => v !== removed); + onChange({ + ...rule, + operator: inferOperator(rule.operator, next.length), + values: next, + }); + }; + + return ( +
+ {connector && ( + + {connector} + + )} + + + + {config.operators.length > 1 ? ( +
+ +
+ ) : ( + + {config.operators[0].label} + + )} + + + + +
+ ); +} + +function FilterValueInput({ + config, + rule, + fieldLabel, + suggestions, + onChange, + onToggle, + onChipRemove, +}: { + config: FieldConfig; + rule: FilterRule; + fieldLabel: string; + suggestions?: { value: string; label: string }[]; + onChange: (rule: FilterRule) => void; + onToggle: (value: string) => void; + onChipRemove: (value: string) => void; +}) { + if (config.valueType === "none") return null; + + if (config.valueType === "boolean") + return ( +
+ onChange({ ...rule, values: [String(val)] })} + /> +
+ ); + + if (suggestions && suggestions.length > 0) + return ( + + ); + + return ( + onChange({ ...rule, values: [e.target.value] })} + /> + ); +} diff --git a/vite/src/views/migrations/migration/filters/filterRowTypes.ts b/vite/src/views/migrations/migration/filters/filterRowTypes.ts new file mode 100644 index 000000000..57a6b575e --- /dev/null +++ b/vite/src/views/migrations/migration/filters/filterRowTypes.ts @@ -0,0 +1,345 @@ +import type { PlanFilter, StringMatcher } from "@autumn/shared"; + +export type FilterField = + | "customer_id" + | "plan_id" + | "paid" + | "recurring" + | "price" + | "item_feature_id" + | "item_unlimited" + | "item_price" + | "item_billing_method" + | "item_mode"; + +export type FilterOperator = + | "is" + | "is_not" + | "in" + | "not_in" + | "regex" + | "starts_with" + | "exists" + | "not_exists"; + +export type FilterRule = { + field: FilterField; + operator: FilterOperator; + values: string[]; +}; + +export type FilterGroupData = { + rules: FilterRule[]; +}; + +export const FILTER_FIELD_OPTIONS: { + value: FilterField; + label: string; +}[] = [ + { value: "customer_id", label: "Customer" }, + { value: "plan_id", label: "Plan" }, + { value: "paid", label: "Paid" }, + { value: "recurring", label: "Recurring" }, + { value: "price", label: "Base Price" }, + { value: "item_feature_id", label: "Feature" }, + { value: "item_unlimited", label: "Unlimited" }, + { value: "item_price", label: "Item Price" }, + { value: "item_billing_method", label: "Billing Method" }, + { value: "item_mode", label: "Match Mode" }, +]; + +type OperatorOption = { value: FilterOperator; label: string }; +export type FieldConfig = { + operators: OperatorOption[]; + valueType: "string" | "boolean" | "none"; +}; + +const STRING_OPERATORS: OperatorOption[] = [ + { value: "is", label: "is" }, + { value: "is_not", label: "is not" }, + { value: "in", label: "in" }, + { value: "not_in", label: "not in" }, + { value: "regex", label: "regex" }, + { value: "starts_with", label: "starts with" }, +]; + +const STRING_MATCH_OPERATORS: OperatorOption[] = [ + { value: "is", label: "is" }, + { value: "is_not", label: "is not" }, + { value: "in", label: "in" }, + { value: "not_in", label: "not in" }, +]; + +const BOOLEAN_ONLY: FieldConfig = { + operators: [{ value: "is", label: "is" }], + valueType: "boolean", +}; + +const NULLABLE_ONLY: FieldConfig = { + operators: [ + { value: "exists", label: "exists" }, + { value: "not_exists", label: "does not exist" }, + ], + valueType: "none", +}; + +export const FIELD_CONFIGS: Record = { + customer_id: { operators: STRING_MATCH_OPERATORS, valueType: "string" }, + plan_id: { operators: STRING_OPERATORS, valueType: "string" }, + paid: BOOLEAN_ONLY, + recurring: BOOLEAN_ONLY, + price: NULLABLE_ONLY, + item_feature_id: { operators: STRING_MATCH_OPERATORS, valueType: "string" }, + item_unlimited: BOOLEAN_ONLY, + item_price: NULLABLE_ONLY, + item_billing_method: { + operators: STRING_MATCH_OPERATORS, + valueType: "string", + }, + item_mode: { + operators: [{ value: "is", label: "is" }], + valueType: "string", + }, +}; + +function stringMatcherToRule( + field: FilterField, + matcher: StringMatcher | undefined, +): FilterRule | null { + if (matcher === undefined) return null; + if (matcher === null) return { field, operator: "is", values: [] }; + if (typeof matcher === "string") + return { field, operator: "is", values: matcher ? [matcher] : [] }; + if (matcher.$eq !== undefined) + return { + field, + operator: "is", + values: matcher.$eq ? [matcher.$eq] : [], + }; + if (matcher.$ne !== undefined) + return { + field, + operator: "is_not", + values: matcher.$ne ? [matcher.$ne] : [], + }; + if (matcher.$in !== undefined) + return { field, operator: "in", values: matcher.$in }; + if (matcher.$nin !== undefined) + return { field, operator: "not_in", values: matcher.$nin }; + if (matcher.$regex !== undefined) + return { field, operator: "regex", values: [matcher.$regex] }; + if (matcher.$startsWith !== undefined) + return { + field, + operator: "starts_with", + values: [matcher.$startsWith], + }; + return { field, operator: "is", values: [] }; +} + +function ruleToStringMatcher(rule: FilterRule): StringMatcher { + if ( + rule.operator === "in" || + (rule.operator === "is" && rule.values.length > 1) + ) + return { $in: rule.values }; + + const val = rule.values[0]; + switch (rule.operator) { + case "is": + return val ?? ""; + case "is_not": + return { $ne: val ?? "" }; + case "in": + return { $in: rule.values }; + case "not_in": + return { $nin: rule.values }; + case "regex": + return { $regex: val ?? "" }; + case "starts_with": + return { $startsWith: val ?? "" }; + default: + return val ?? ""; + } +} + +function booleanRule(field: FilterField, value: boolean): FilterRule { + return { field, operator: "is", values: [String(value)] }; +} + +function nullableToRule(field: FilterField, value: unknown): FilterRule | null { + if (value === undefined) return null; + if (value === null) return { field, operator: "not_exists", values: [] }; + return { field, operator: "exists", values: [] }; +} + +type ArrayFilterMode = "$some" | "$every" | "$none"; + +function detectArrayFilterMode(item: Record): { + mode: ArrayFilterMode; + inner: Record; +} { + for (const key of ["$some", "$every", "$none"] as const) { + if (key in item && item[key] && typeof item[key] === "object") + return { mode: key, inner: item[key] as Record }; + } + return { mode: "$some", inner: item }; +} + +export function planFilterToGroups(filter: PlanFilter): FilterGroupData[] { + const mainRules: FilterRule[] = []; + + const planIdRule = stringMatcherToRule("plan_id", filter.plan_id); + if (planIdRule) mainRules.push(planIdRule); + + if (filter.paid !== undefined) + mainRules.push(booleanRule("paid", filter.paid)); + + if (filter.recurring !== undefined) + mainRules.push(booleanRule("recurring", filter.recurring)); + + const priceRule = nullableToRule("price", filter.price); + if (priceRule) mainRules.push(priceRule); + + if (filter.item !== undefined) { + const item = + typeof filter.item === "object" && filter.item !== null + ? filter.item + : {}; + const { mode, inner } = detectArrayFilterMode( + item as Record, + ); + + if (mode !== "$some") { + mainRules.push({ + field: "item_mode", + operator: "is", + values: [mode.slice(1)], + }); + } + + const featureRule = stringMatcherToRule( + "item_feature_id", + inner.feature_id as StringMatcher | undefined, + ); + if (featureRule) mainRules.push(featureRule); + + if (inner.unlimited !== undefined) + mainRules.push(booleanRule("item_unlimited", Boolean(inner.unlimited))); + + const itemPriceRule = nullableToRule("item_price", inner.price); + if (itemPriceRule) mainRules.push(itemPriceRule); + + if ( + inner.price && + typeof inner.price === "object" && + inner.price !== null + ) { + const priceObj = inner.price as Record; + if (priceObj.billing_method !== undefined) { + const bmRule = stringMatcherToRule( + "item_billing_method", + priceObj.billing_method as StringMatcher | undefined, + ); + if (bmRule) mainRules.push(bmRule); + } + } + } + + const groups: FilterGroupData[] = [{ rules: mainRules }]; + + if (filter.$or) { + for (const orFilter of filter.$or) { + const orGroups = planFilterToGroups(orFilter); + if (orGroups.length > 0) groups.push(orGroups[0]); + } + } + + return groups; +} + +export function groupsToPlanFilter(groups: FilterGroupData[]): PlanFilter { + const main = groups[0]; + if (!main) return {}; + + const filter: PlanFilter = {}; + let hasItemFields = false; + const itemInner: Record = {}; + let itemMode: ArrayFilterMode = "$some"; + + for (const rule of main.rules) { + switch (rule.field) { + case "plan_id": + filter.plan_id = ruleToStringMatcher(rule); + break; + case "paid": + filter.paid = rule.values[0] === "true"; + break; + case "recurring": + filter.recurring = rule.values[0] === "true"; + break; + case "price": + filter.price = rule.operator === "exists" ? { $ne: null } : null; + break; + case "item_feature_id": + hasItemFields = true; + itemInner.feature_id = ruleToStringMatcher(rule); + break; + case "item_unlimited": + hasItemFields = true; + itemInner.unlimited = rule.values[0] === "true"; + break; + case "item_price": + hasItemFields = true; + itemInner.price = rule.operator === "exists" ? { $ne: null } : null; + break; + case "item_billing_method": { + hasItemFields = true; + const existingPrice = + itemInner.price && typeof itemInner.price === "object" + ? (itemInner.price as Record) + : {}; + itemInner.price = { + ...existingPrice, + billing_method: ruleToStringMatcher(rule), + }; + break; + } + case "item_mode": + itemMode = `$${rule.values[0] ?? "some"}` as ArrayFilterMode; + break; + } + } + + if (hasItemFields) { + filter.item = + itemMode === "$some" + ? (itemInner as PlanFilter["item"]) + : ({ [itemMode]: itemInner } as PlanFilter["item"]); + } + + if (groups.length > 1) { + filter.$or = groups.slice(1).map((group) => groupsToPlanFilter([group])); + } + + return filter; +} + +export function customerIdToStrings( + matcher: StringMatcher | undefined, +): string[] { + if (matcher === undefined || matcher === null) return []; + if (typeof matcher === "string") return matcher ? [matcher] : []; + if (matcher.$eq) return [matcher.$eq]; + if (matcher.$ne) return [matcher.$ne]; + if (matcher.$in) return matcher.$in; + if (matcher.$nin) return matcher.$nin; + return []; +} + +export function stringsToCustomerId(ids: string[]): StringMatcher | undefined { + const filtered = ids.map((s) => s.trim()).filter(Boolean); + if (filtered.length === 0) return undefined; + if (filtered.length === 1) return filtered[0]; + return { $in: filtered }; +} diff --git a/vite/src/views/migrations/migration/hooks/useGuardedStepNavigation.ts b/vite/src/views/migrations/migration/hooks/useGuardedStepNavigation.ts new file mode 100644 index 000000000..284bc5820 --- /dev/null +++ b/vite/src/views/migrations/migration/hooks/useGuardedStepNavigation.ts @@ -0,0 +1,36 @@ +import type { Operations } from "@autumn/shared"; +import { useCallback } from "react"; +import type { StepId } from "../StepIndicator"; +import { hasValidOperations } from "../shared/operationUtils"; + +const STEP_ORDER: StepId[] = ["filter", "operations", "live"]; + +export function useGuardedStepNavigation({ + step, + hasCustomers, + operations, + saveError, + enableErrorDisplay, + setStep, +}: { + step: StepId; + hasCustomers: boolean; + operations: Operations; + saveError: string | null; + enableErrorDisplay: () => void; + setStep: (step: StepId) => void; +}) { + return useCallback( + (target: StepId) => { + const currentIndex = STEP_ORDER.indexOf(step); + const targetIndex = STEP_ORDER.indexOf(target); + if (targetIndex <= currentIndex) return setStep(target); + if (targetIndex >= 1 && !hasCustomers) return; + if (targetIndex >= 2 && (!hasValidOperations(operations) || !!saveError)) + return; + if (targetIndex === 2) enableErrorDisplay(); + setStep(target); + }, + [step, hasCustomers, operations, saveError, enableErrorDisplay, setStep], + ); +} diff --git a/vite/src/views/migrations/migration/hooks/useMigrationRunRealtime.ts b/vite/src/views/migrations/migration/hooks/useMigrationRunRealtime.ts new file mode 100644 index 000000000..8ad68d7d7 --- /dev/null +++ b/vite/src/views/migrations/migration/hooks/useMigrationRunRealtime.ts @@ -0,0 +1,47 @@ +import { useRealtimeRun } from "@trigger.dev/react-hooks"; +import { useCallback } from "react"; + +export type RealtimeRunSubscription = { + triggerRunId: string; + publicAccessToken: string; + isDryRun: boolean; +}; + +export type RealtimeRunStatus = "queued" | "running" | "succeeded" | "failed"; + +function resolveRunStatus(run: { + isQueued: boolean; + isExecuting: boolean; + isSuccess: boolean; + isFailed: boolean; + isCancelled: boolean; +}): RealtimeRunStatus | null { + if (run.isQueued) return "queued"; + if (run.isExecuting) return "running"; + if (run.isSuccess) return "succeeded"; + if (run.isFailed || run.isCancelled) return "failed"; + return null; +} + +export function useMigrationRunRealtime({ + triggerRunId, + publicAccessToken, + onComplete, +}: { + triggerRunId: string; + publicAccessToken: string; + onComplete: () => void; +}): { status: RealtimeRunStatus | null; isActive: boolean } { + const stableOnComplete = useCallback(onComplete, [onComplete]); + + const { run } = useRealtimeRun(triggerRunId, { + accessToken: publicAccessToken, + skipColumns: ["payload", "output"], + onComplete: stableOnComplete, + }); + + if (!run) return { status: null, isActive: false }; + + const status = resolveRunStatus(run); + return { status, isActive: run.isQueued || run.isExecuting }; +} diff --git a/vite/src/views/migrations/migration/hooks/useRealtimeSubscriptions.ts b/vite/src/views/migrations/migration/hooks/useRealtimeSubscriptions.ts new file mode 100644 index 000000000..13215e41c --- /dev/null +++ b/vite/src/views/migrations/migration/hooks/useRealtimeSubscriptions.ts @@ -0,0 +1,73 @@ +import type { AxiosError } from "axios"; +import { useCallback, useState } from "react"; +import { toast } from "sonner"; +import { useMigrationsQuery } from "@/hooks/queries/useMigrationsQuery"; +import { getBackendErr } from "@/utils/genUtils"; +import type { RealtimeRunSubscription } from "./useMigrationRunRealtime"; + +export function useRealtimeSubscriptions({ + migrationId, + invalidateRuns, +}: { + migrationId: string; + invalidateRuns: () => void; +}) { + const { runMigration, isRunning } = useMigrationsQuery(); + const [subscriptions, setSubscriptions] = useState( + [], + ); + + const handleComplete = useCallback( + (triggerRunId: string) => { + setSubscriptions((prev) => + prev.filter((s) => s.triggerRunId !== triggerRunId), + ); + invalidateRuns(); + }, + [invalidateRuns], + ); + + const triggerRun = async ({ + dryRun, + limit, + only, + }: { + dryRun: boolean; + limit?: number; + only?: string[]; + }) => { + try { + const result = await runMigration({ + id: migrationId, + dry_run: dryRun, + limit, + only, + }); + if (result.trigger_run_id && result.public_access_token) { + setSubscriptions((prev) => [ + ...prev, + { + triggerRunId: result.trigger_run_id as string, + publicAccessToken: result.public_access_token as string, + isDryRun: dryRun, + }, + ]); + } + const label = dryRun ? "Dry run" : "Migration run"; + toast.success(`${label} triggered (${result.run_id})`); + invalidateRuns(); + } catch (error) { + toast.error( + getBackendErr(error as AxiosError, "Failed to run migration"), + ); + } + }; + + return { + subscriptions, + hasActive: subscriptions.length > 0, + handleComplete, + triggerRun, + isRunning, + }; +} diff --git a/vite/src/views/migrations/migration/live/ActiveDot.tsx b/vite/src/views/migrations/migration/live/ActiveDot.tsx new file mode 100644 index 000000000..10a341205 --- /dev/null +++ b/vite/src/views/migrations/migration/live/ActiveDot.tsx @@ -0,0 +1,17 @@ +import { cn } from "@/lib/utils"; + +export function ActiveDot({ color }: { color: "green" | "orange" }) { + const bg = color === "green" ? "bg-green-500" : "bg-orange-400"; + const ping = color === "green" ? "bg-green-400" : "bg-orange-300"; + return ( + + + + + ); +} diff --git a/vite/src/views/migrations/migration/live/CustomerRunSheet.tsx b/vite/src/views/migrations/migration/live/CustomerRunSheet.tsx new file mode 100644 index 000000000..5bf10e093 --- /dev/null +++ b/vite/src/views/migrations/migration/live/CustomerRunSheet.tsx @@ -0,0 +1,284 @@ +import type { CustomerWithProducts, Operations } from "@autumn/shared"; +import { + CalendarBlankIcon, + EyeIcon, + LightningIcon, + PlayIcon, + UserIcon, +} from "@phosphor-icons/react"; +import { format } from "date-fns"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { Badge } from "@/components/v2/badges/Badge"; +import { Button } from "@/components/v2/buttons/Button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/v2/dialogs/Dialog"; +import { InfoRow } from "@/components/v2/InfoRow"; +import { SheetHeader, SheetSection } from "@/components/v2/sheets/InlineSheet"; +import type { MigrationItemEvent } from "@/hooks/queries/useMigrationRunsQuery"; +import { ActiveRunDot, ItemEventStatusBadge } from "../runs/RunStatusBadge"; +import { RunSummaryRows } from "../shared/RunSummaryRows"; +import { EventResultDetail } from "./EventResultDetail"; + +function formatEventTimestamp(timestamp: string): string { + return format(new Date(timestamp), "MMM d, HH:mm:ss"); +} + +function StatusValue({ + latestDryEvent, + latestLiveEvent, + isActive, + activeRunDryRun, +}: { + latestDryEvent: MigrationItemEvent | undefined; + latestLiveEvent: MigrationItemEvent | undefined; + isActive: boolean; + activeRunDryRun: boolean | null; +}) { + if (isActive) + return ( +
+ + + {activeRunDryRun ? "Dry run in progress" : "Running"} + +
+ ); + const event = latestLiveEvent ?? latestDryEvent; + if (event) + return ( +
+ {event.dry_run && ( + Dry Run: + )} + +
+ ); + return Not Run; +} + +export function CustomerRunSheet({ + customer, + latestDryEvent, + latestLiveEvent, + allEvents, + isActive, + activeRunDryRun, + isRunning, + onTriggerRun, + operations, + noBillingChanges, +}: { + customer: CustomerWithProducts; + latestDryEvent: MigrationItemEvent | undefined; + latestLiveEvent: MigrationItemEvent | undefined; + allEvents: MigrationItemEvent[]; + isActive: boolean; + activeRunDryRun: boolean | null; + isRunning: boolean; + onTriggerRun: (opts: { dryRun: boolean; only?: string[] }) => void; + operations: Operations; + noBillingChanges: boolean; +}) { + const customerId = customer.id ?? customer.internal_id; + const [isRunDialogOpen, setIsRunDialogOpen] = useState(false); + const lastActionRef = useRef<"dry" | "live" | null>(null); + + const prevRunningRef = useRef(isRunning); + useEffect(() => { + if (prevRunningRef.current && !isRunning) lastActionRef.current = null; + prevRunningRef.current = isRunning; + }, [isRunning]); + + const hasSuccessfulLiveRun = allEvents.some( + (e) => e.status === "succeeded" && !e.dry_run, + ); + + const handleDryRun = () => { + lastActionRef.current = "dry"; + onTriggerRun({ dryRun: true, only: [customerId] }); + }; + + const handleLiveRun = () => { + setIsRunDialogOpen(false); + lastActionRef.current = "live"; + onTriggerRun({ dryRun: false, only: [customerId] }); + }; + + const sortedEvents = useMemo( + () => + [...allEvents].sort( + (a, b) => + new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), + ), + [allEvents], + ); + + const lastRunTimestamp = + latestLiveEvent?.timestamp ?? latestDryEvent?.timestamp; + + return ( +
+ + {customer.name || customerId} + {isActive && } + + } + description={customer.email || "No email"} + /> + + +
+ } + label="Status" + value={ + + } + /> + {lastRunTimestamp && ( + } + label="Last Run" + value={formatEventTimestamp(lastRunTimestamp)} + /> + )} +
+
+ + {latestLiveEvent && ( + + + + Live Run + + + {formatEventTimestamp(latestLiveEvent.timestamp)} + +
+ } + > + + + )} + + {latestDryEvent && ( + + + + Preview + + + {formatEventTimestamp(latestDryEvent.timestamp)} + +
+ } + > + + + )} + + {sortedEvents.length > 0 && ( + +
+ {sortedEvents.map((event, index) => ( +
+ + + {formatEventTimestamp(event.timestamp)} + +
+ ))} +
+
+ )} + +
+ + +
+ + + + + Run Migration + + This will apply the migration to the following scope. + + + + } + customerLabel={customer.name || customer.id || "1 customer"} + operations={operations} + noBillingChanges={noBillingChanges} + /> + + + + + + +
+ ); +} diff --git a/vite/src/views/migrations/migration/live/EventResultDetail.tsx b/vite/src/views/migrations/migration/live/EventResultDetail.tsx new file mode 100644 index 000000000..a621f6fc1 --- /dev/null +++ b/vite/src/views/migrations/migration/live/EventResultDetail.tsx @@ -0,0 +1,272 @@ +import type { Feature } from "@autumn/shared"; +import { PackageIcon } from "@phosphor-icons/react"; +import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; +import type { MigrationItemEvent } from "@/hooks/queries/useMigrationRunsQuery"; +import { cn } from "@/lib/utils"; +import { getFeatureIconConfig } from "@/views/products/features/utils/getFeatureIcon"; + +type ItemChange = { action?: string; feature_id?: string }; +type PlanChange = { + action?: string; + plan_id?: string; + entity_id?: string | null; + item_changes?: ItemChange[]; +}; +type BalanceChange = { + feature_id?: string; + before?: { granted?: number; remaining?: number; usage?: number }; + granted?: number; +}; +type FlagChange = { action?: string; feature_id?: string }; +type MigrationPreview = { + plan_changes?: (string | PlanChange)[]; + balance_changes?: (string | BalanceChange)[]; + flag_changes?: (string | FlagChange)[]; +}; + +function parseJson(raw: string | T): T | null { + if (typeof raw !== "string") return raw; + try { + return JSON.parse(raw) as T; + } catch { + return null; + } +} + +function parseList(raw: (string | T)[] | undefined): T[] { + return (raw ?? []) + .map((r) => parseJson(r)) + .filter((c): c is T => c !== null); +} + +const DOT_COLORS: Record = { + updated: "bg-amber-500", + created: "bg-green-500", + removed: "bg-red-500", + deleted: "bg-red-500", +}; + +const ACTION_LABELS: Record = { + updated: "Changed", + created: "New", + removed: "Removed", + deleted: "Removed", +}; + +function StatusDot({ action }: { action: string }) { + return ( + + ); +} + +function FeatureIcon({ + featureId, + features, +}: { + featureId: string | undefined; + features: Feature[]; +}) { + const feature = features.find((f) => f.id === featureId); + const config = feature + ? getFeatureIconConfig(feature.type, feature.config?.usage_type, 14) + : getFeatureIconConfig(null, null, 14); + + return {config.icon}; +} + +const ROW_TINTS: Record = { + created: "border-green-500/20 bg-green-500/5", + updated: "border-amber-500/20 bg-amber-500/5", + removed: "border-red-500/20 bg-red-500/5", + deleted: "border-red-500/20 bg-red-500/5", +}; + +function ChangeRow({ + action, + children, + className, +}: { + action?: string; + children: React.ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} + +function PlanChangeRows({ + change, + features, +}: { + change: PlanChange; + features: Feature[]; +}) { + const action = change.action ?? "unknown"; + const items = change.item_changes ?? []; + + return ( + <> + + + + {ACTION_LABELS[action] ?? action} + + + + {change.plan_id ?? "Unknown plan"} + + + {items.map((item, i) => ( + + + + {ACTION_LABELS[item.action ?? "unknown"] ?? item.action} + + + + {features.find((f) => f.id === item.feature_id)?.name ?? + item.feature_id} + + + ))} + {items.length === 0 && action === "updated" && ( +
+ + Price, version, or settings changed + +
+ )} + + ); +} + +function BalanceChangeRow({ + change, + features, +}: { + change: BalanceChange; + features: Feature[]; +}) { + const feature = features.find((f) => f.id === change.feature_id); + + return ( + + + Updated + + + {feature?.name ?? change.feature_id} + + + {change.before ? ( + <> + {change.before.granted ?? 0} + + {change.granted ?? 0} + + ) : ( + {change.granted ?? 0} + )} + + + ); +} + +function FlagChangeRow({ + change, + features, +}: { + change: FlagChange; + features: Feature[]; +}) { + const feature = features.find((f) => f.id === change.feature_id); + + const action = change.action ?? "unknown"; + return ( + + + + {ACTION_LABELS[action] ?? action} + + + + {feature?.name ?? change.feature_id} + + + ); +} + +function PreviewSummary({ preview }: { preview: MigrationPreview }) { + const { features } = useFeaturesQuery(); + const planChanges = parseList(preview.plan_changes); + const balanceChanges = parseList(preview.balance_changes); + const flagChanges = parseList(preview.flag_changes); + + if (planChanges.length + balanceChanges.length + flagChanges.length === 0) + return No changes; + + return ( +
+ {planChanges.map((c, i) => ( + + ))} + {balanceChanges.map((c, i) => ( + + ))} + {flagChanges.map((c, i) => ( + + ))} +
+ ); +} + +export function EventResultDetail({ event }: { event: MigrationItemEvent }) { + const response = event.response as Record | null; + if (!response) return null; + + if (event.status === "failed") { + const error = response.error as { message?: string } | undefined; + if (!error?.message) return null; + return ( +
+ + {error.message} +
+ ); + } + + const preview = response.preview as MigrationPreview | undefined; + if (preview) return ; + + if (event.status === "skipped") { + const skipped = response.skipped as { reason?: string } | undefined; + const guard = response.guard as { reason?: string } | undefined; + const reason = skipped?.reason ?? guard?.reason; + if (reason) return {reason}; + } + + return null; +} diff --git a/vite/src/views/migrations/migration/live/ExecutionStatusSubMenu.tsx b/vite/src/views/migrations/migration/live/ExecutionStatusSubMenu.tsx new file mode 100644 index 000000000..f6a397d5a --- /dev/null +++ b/vite/src/views/migrations/migration/live/ExecutionStatusSubMenu.tsx @@ -0,0 +1,72 @@ +import { Checkbox } from "@/components/v2/checkboxes/Checkbox"; +import { + DropdownMenuItem, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, +} from "@/components/v2/dropdowns/DropdownMenu"; + +const EXECUTION_STATUSES = [ + { value: "not_run", label: "Not Run" }, + { value: "succeeded", label: "Succeeded" }, + { value: "skipped", label: "Skipped" }, + { value: "failed", label: "Failed" }, +] as const; + +export type ExecutionStatus = (typeof EXECUTION_STATUSES)[number]["value"]; + +export function hasActiveExecutionFilters( + statuses: ExecutionStatus[], +): boolean { + return statuses.length > 0; +} + +export function ExecutionStatusSubMenu({ + selected, + onChange, +}: { + selected: ExecutionStatus[]; + onChange: (statuses: ExecutionStatus[]) => void; +}) { + const hasSelections = selected.length > 0; + + const toggle = (status: ExecutionStatus) => { + const isSelected = selected.includes(status); + const updated = isSelected + ? selected.filter((s) => s !== status) + : [...selected, status]; + onChange(updated); + }; + + return ( + + + Execution Status + {hasSelections && ( + + {selected.length} + + )} + + + {EXECUTION_STATUSES.map(({ value, label }) => { + const isActive = selected.includes(value); + return ( + { + e.preventDefault(); + toggle(value); + }} + onSelect={(e) => e.preventDefault()} + className="flex items-center gap-2 cursor-pointer text-sm" + > + + {label} + + ); + })} + + + ); +} diff --git a/vite/src/views/migrations/migration/live/MigrationCustomerSheet.tsx b/vite/src/views/migrations/migration/live/MigrationCustomerSheet.tsx new file mode 100644 index 000000000..67cb606e3 --- /dev/null +++ b/vite/src/views/migrations/migration/live/MigrationCustomerSheet.tsx @@ -0,0 +1,81 @@ +import type { CustomerWithProducts, Operations } from "@autumn/shared"; +import { useMemo } from "react"; +import { useMigrationRunsQuery } from "@/hooks/queries/useMigrationRunsQuery"; +import { useRealtimeSubscriptions } from "../hooks/useRealtimeSubscriptions"; +import { CustomerRunSheet } from "./CustomerRunSheet"; +import { RealtimeRunWatcher } from "./RealtimeRunWatcher"; + +export function MigrationCustomerSheet({ + migrationId, + customer, + operations, + noBillingChanges, +}: { + migrationId: string; + customer: CustomerWithProducts; + operations: Operations; + noBillingChanges: boolean; +}) { + const { + itemEvents, + isActive, + activeRunDryRun, + invalidate: invalidateRuns, + } = useMigrationRunsQuery({ migrationId }); + + const { + subscriptions: realtimeSubscriptions, + hasActive: hasRealtimeActive, + handleComplete: handleRealtimeComplete, + triggerRun, + isRunning, + } = useRealtimeSubscriptions({ migrationId, invalidateRuns }); + + const customerEvents = useMemo( + () => + itemEvents.filter( + (e) => e.item_kind === "customer" && e.item_id === customer.internal_id, + ), + [itemEvents, customer.internal_id], + ); + + const latestDryEvent = useMemo(() => { + const dryEvents = customerEvents.filter((e) => e.dry_run); + if (dryEvents.length === 0) return undefined; + return dryEvents.reduce((latest, event) => + event.timestamp > latest.timestamp ? event : latest, + ); + }, [customerEvents]); + + const latestLiveEvent = useMemo(() => { + const liveEvents = customerEvents.filter((e) => !e.dry_run); + if (liveEvents.length === 0) return undefined; + return liveEvents.reduce((latest, event) => + event.timestamp > latest.timestamp ? event : latest, + ); + }, [customerEvents]); + + return ( + <> + {realtimeSubscriptions.map((sub) => ( + + ))} + + + ); +} diff --git a/vite/src/views/migrations/migration/live/MigrationLiveView.tsx b/vite/src/views/migrations/migration/live/MigrationLiveView.tsx new file mode 100644 index 000000000..571d850d6 --- /dev/null +++ b/vite/src/views/migrations/migration/live/MigrationLiveView.tsx @@ -0,0 +1,513 @@ +import type { + CustomerWithProducts, + MigrationFilter, + Operations, +} from "@autumn/shared"; +import { + ArrowLeftIcon, + CaretDownIcon, + CaretLeftIcon, + CaretRightIcon, + EyeIcon, + ListMagnifyingGlassIcon, + PlayIcon, + UsersIcon, + WarningIcon, + XIcon, +} from "@phosphor-icons/react"; +import type { ColumnDef, PaginationState, Row } from "@tanstack/react-table"; +import { debounce } from "lodash"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Table } from "@/components/general/table"; +import { Badge } from "@/components/v2/badges/Badge"; +import { Button } from "@/components/v2/buttons/Button"; +import { IconButton } from "@/components/v2/buttons/IconButton"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/v2/dialogs/Dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/v2/dropdowns/DropdownMenu"; +import { Input } from "@/components/v2/inputs/Input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/v2/selects/Select"; +import { useMigrationFilterPreview } from "@/hooks/queries/useMigrationFilterPreview"; +import { + type MigrationItemEvent, + useMigrationRunsQuery, +} from "@/hooks/queries/useMigrationRunsQuery"; +import { cn } from "@/lib/utils"; +import { useCustomerFilters } from "@/views/customers/hooks/useCustomerFilters"; +import { createCustomerListColumns } from "@/views/customers2/components/table/customer-list/CustomerListColumns"; +import { CustomerListFilterButton } from "@/views/customers2/components/table/customer-list/CustomerListFilterButton"; +import { useProductTable } from "@/views/products/hooks/useProductTable"; +import { useRealtimeSubscriptions } from "../hooks/useRealtimeSubscriptions"; +import { ItemEventStatusBadge } from "../runs/RunStatusBadge"; +import { type StepId, StepIndicator } from "../StepIndicator"; +import { RunSummaryRows } from "../shared/RunSummaryRows"; +import { ActiveDot } from "./ActiveDot"; +import { + type ExecutionStatus, + ExecutionStatusSubMenu, + hasActiveExecutionFilters, +} from "./ExecutionStatusSubMenu"; +import { RealtimeRunWatcher } from "./RealtimeRunWatcher"; +import { useMigrationSheetStore } from "./useMigrationSheetStore"; + +const PAGE_SIZE_OPTIONS = [10, 50, 100, 250]; + +type ActiveRunStatus = "queued" | "running" | null; + +type CustomerRow = CustomerWithProducts & { + _event?: MigrationItemEvent; + _activeStatus?: ActiveRunStatus; + _activeRunId?: string; +}; + +function buildEventsByCustomer(itemEvents: MigrationItemEvent[]) { + const map = new Map(); + for (const event of itemEvents) { + if (event.item_kind !== "customer") continue; + const existing = map.get(event.item_id); + if (!existing || event.timestamp > existing.timestamp) + map.set(event.item_id, event); + } + return map; +} + +const statusColumn: ColumnDef = { + id: "migration_status", + header: "Status", + size: 140, + cell: ({ row }: { row: Row }) => { + const event = row.original._event; + const activeStatus = row.original._activeStatus; + const activeRunId = row.original._activeRunId; + const processedInCurrentRun = + event && activeRunId && event.migration_run_id === activeRunId; + + if (activeStatus && !processedInCurrentRun) { + const color = activeStatus === "running" ? "green" : "orange"; + const label = activeStatus === "running" ? "Running" : "Queued"; + return ( + + + {label} + + ); + } + + if (event) + return ( + + ); + + return Not Run; + }, +}; + +const baseColumns = createCustomerListColumns().filter( + (col) => col.id !== "actions", +) as ColumnDef[]; + +const columns: ColumnDef[] = [ + ...baseColumns, + statusColumn, +]; + +export function MigrationLiveView({ + migrationId, + filter, + operations, + noBillingChanges, + step, + onStepChange, + onPrevious, +}: { + migrationId: string; + filter: MigrationFilter; + operations: Operations; + noBillingChanges: boolean; + step: StepId; + onStepChange: (step: StepId) => void; + onPrevious?: () => void; +}) { + const { queryStates: customerFilters } = useCustomerFilters(); + const [executionStatuses, setExecutionStatuses] = useState( + [], + ); + const [search, setSearch] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 50, + }); + const [dismissedError, setDismissedError] = useState(null); + const [isRunDialogOpen, setIsRunDialogOpen] = useState(false); + + const debouncedSetSearch = useMemo( + () => debounce((q: string) => setDebouncedSearch(q), 350), + [], + ); + useEffect(() => () => debouncedSetSearch.cancel(), [debouncedSetSearch]); + + const handleSearchChange = useCallback( + (e: React.ChangeEvent) => { + setSearch(e.target.value); + setPagination((p) => ({ ...p, pageIndex: 0 })); + debouncedSetSearch(e.target.value.trim()); + }, + [debouncedSetSearch], + ); + + const { + customers, + count, + isLoading: isLoadingCustomers, + } = useMigrationFilterPreview({ + filter: filter.customer ?? {}, + search: debouncedSearch, + page: pagination.pageIndex, + pageSize: pagination.pageSize, + }); + + const { + itemEvents, + runs, + invalidate: invalidateRuns, + } = useMigrationRunsQuery({ migrationId }); + + const { + subscriptions: realtimeSubscriptions, + hasActive: hasRealtimeActive, + handleComplete: handleRealtimeComplete, + triggerRun, + isRunning, + } = useRealtimeSubscriptions({ migrationId, invalidateRuns }); + + const setSelectedCustomer = useMigrationSheetStore( + (s) => s.setSelectedCustomer, + ); + + const eventsByCustomer = useMemo( + () => buildEventsByCustomer(itemEvents), + [itemEvents], + ); + + const activeRun = runs.find( + (r) => r.status === "queued" || r.status === "running", + ); + const activeRunStatus: ActiveRunStatus = hasRealtimeActive + ? "running" + : ((activeRun?.status as ActiveRunStatus) ?? null); + const activeRunId = activeRun?.internal_id ?? null; + + const enrichedCustomers = useMemo( + (): CustomerRow[] => + customers.map((c) => ({ + ...c, + _event: eventsByCustomer.get(c.internal_id), + _activeStatus: activeRunStatus, + _activeRunId: activeRunId ?? undefined, + })), + [customers, eventsByCustomer, activeRunStatus, activeRunId], + ); + + const filteredCustomers = useMemo(() => { + const hasExecution = executionStatuses.length > 0; + const hasStatus = customerFilters.status.length > 0; + const hasVersion = customerFilters.version.length > 0; + const hasProcessor = customerFilters.processor.length > 0; + const hasNone = customerFilters.none; + if (!hasExecution && !hasStatus && !hasVersion && !hasProcessor && !hasNone) + return enrichedCustomers; + return enrichedCustomers.filter((c) => { + if (hasExecution) { + const status = c._event?.status; + if (!status && !executionStatuses.includes("not_run")) return false; + if (status && !executionStatuses.includes(status as ExecutionStatus)) + return false; + } + const cusProducts = c.customer_products ?? []; + if (hasNone && cusProducts.length === 0) return true; + if (hasStatus) { + if ( + !cusProducts.some((cp) => customerFilters.status.includes(cp.status)) + ) + return false; + } + if (hasVersion) { + if ( + !cusProducts.some((cp) => + customerFilters.version.includes( + `${cp.product?.id}:${cp.product?.version ?? 1}`, + ), + ) + ) + return false; + } + if (hasProcessor) { + const processors = c.processors ?? {}; + if ( + !customerFilters.processor.some( + (p) => processors[p as keyof typeof processors] != null, + ) + ) + return false; + } + return true; + }); + }, [enrichedCustomers, executionStatuses, customerFilters]); + + const pageCount = + count !== null ? Math.max(Math.ceil(count / pagination.pageSize), 1) : 1; + + const table = useProductTable({ + data: filteredCustomers, + columns, + options: { + manualPagination: true, + pageCount, + state: { pagination }, + onPaginationChange: setPagination, + }, + }); + + const currentPage = pagination.pageIndex + 1; + const canPrev = pagination.pageIndex > 0; + const canNext = count !== null && currentPage < pageCount; + + const latestRun = runs[0]; + const latestFailedRun = + latestRun?.status === "failed" && latestRun.error_message + ? latestRun + : undefined; + + return ( +
+ {realtimeSubscriptions.map((sub) => ( + + ))} + {latestFailedRun && latestFailedRun.internal_id !== dismissedError && ( +
+ + + Run failed: {latestFailedRun.error_message} + + +
+ )} + + + {count !== null && ( + + {count} {count === 1 ? "customer" : "customers"} + + )} + {onPrevious && ( + + )} +
+ + + + + + + triggerRun({ dryRun: true })}> + + Dry Run All + + triggerRun({ dryRun: false, limit: 10 })} + > + + Sample (10) + + + +
+ + + + + Run Migration + + This will apply the migration to the following scope. + + + + } + customerLabel={ + count !== null + ? `${count} ${count === 1 ? "customer" : "customers"}` + : "All matched customers" + } + operations={operations} + noBillingChanges={noBillingChanges} + /> + + + + + + +
+ +
+ + } + hasActiveExtraFilters={hasActiveExecutionFilters(executionStatuses)} + onClearExtra={() => setExecutionStatuses([])} + hideSavedViews + /> +
+ + +
+
+ } + onClick={() => + setPagination((p) => ({ ...p, pageIndex: p.pageIndex - 1 })) + } + disabled={!canPrev} + className={cn(!canPrev && "pointer-events-none opacity-50")} + /> + + {currentPage} / {pageCount} + + } + onClick={() => + setPagination((p) => ({ ...p, pageIndex: p.pageIndex + 1 })) + } + disabled={!canNext} + className={cn(!canNext && "pointer-events-none opacity-50")} + /> + +
+
+ + + + + + + + + +
+ ); +} diff --git a/vite/src/views/migrations/migration/live/RealtimeRunWatcher.tsx b/vite/src/views/migrations/migration/live/RealtimeRunWatcher.tsx new file mode 100644 index 000000000..610c61bd7 --- /dev/null +++ b/vite/src/views/migrations/migration/live/RealtimeRunWatcher.tsx @@ -0,0 +1,20 @@ +import { + type RealtimeRunSubscription, + useMigrationRunRealtime, +} from "../hooks/useMigrationRunRealtime"; + +export function RealtimeRunWatcher({ + subscription, + onComplete, +}: { + subscription: RealtimeRunSubscription; + onComplete: (triggerRunId: string) => void; +}) { + useMigrationRunRealtime({ + triggerRunId: subscription.triggerRunId, + publicAccessToken: subscription.publicAccessToken, + onComplete: () => onComplete(subscription.triggerRunId), + }); + + return null; +} diff --git a/vite/src/views/migrations/migration/live/useMigrationSheetStore.ts b/vite/src/views/migrations/migration/live/useMigrationSheetStore.ts new file mode 100644 index 000000000..1897a5f85 --- /dev/null +++ b/vite/src/views/migrations/migration/live/useMigrationSheetStore.ts @@ -0,0 +1,16 @@ +import type { CustomerWithProducts, Operations } from "@autumn/shared"; +import { create } from "zustand"; + +interface MigrationSheetState { + selectedCustomer: CustomerWithProducts | null; + setSelectedCustomer: (customer: CustomerWithProducts | null) => void; + liveFormState: { operations: Operations; noBillingChanges: boolean }; + setLiveFormState: (state: { operations: Operations; noBillingChanges: boolean }) => void; +} + +export const useMigrationSheetStore = create((set) => ({ + selectedCustomer: null, + setSelectedCustomer: (customer) => set({ selectedCustomer: customer }), + liveFormState: { operations: {}, noBillingChanges: false }, + setLiveFormState: (liveFormState) => set({ liveFormState }), +})); diff --git a/vite/src/views/migrations/migration/operations/AddPlanOpForm.tsx b/vite/src/views/migrations/migration/operations/AddPlanOpForm.tsx new file mode 100644 index 000000000..4fda183a0 --- /dev/null +++ b/vite/src/views/migrations/migration/operations/AddPlanOpForm.tsx @@ -0,0 +1,71 @@ +import type { AddPlanOp, CustomerOperation } from "@autumn/shared"; +import { useMemo } from "react"; +import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; +import { buildPlanSuggestions } from "../shared/planSuggestions"; +import { RemoveButton } from "../shared/RemoveButton"; +import { ValuePicker } from "../shared/ValuePicker"; + +export function AddPlansSection({ + operations, + onUpdate, + onRemoveAll, + defaultOpenPicker = false, +}: { + operations: CustomerOperation[]; + onUpdate: (addPlanOps: AddPlanOp[]) => void; + onRemoveAll: () => void; + defaultOpenPicker?: boolean; +}) { + const { products } = useProductsQuery(); + const planSuggestions = useMemo( + () => buildPlanSuggestions(products), + [products], + ); + + const selectedPlanIds = operations + .filter((op): op is AddPlanOp => op.type === "add_plan") + .map((op) => op.plan_id) + .filter(Boolean); + + const handleToggle = (planId: string) => { + const isSelected = selectedPlanIds.includes(planId); + const nextIds = isSelected + ? selectedPlanIds.filter((id) => id !== planId) + : [...selectedPlanIds, planId]; + onUpdate(nextIds.map((id) => ({ type: "add_plan" as const, plan_id: id }))); + }; + + const handleRemove = (planId: string) => { + const nextIds = selectedPlanIds.filter((id) => id !== planId); + onUpdate(nextIds.map((id) => ({ type: "add_plan" as const, plan_id: id }))); + }; + + return ( +
+
+
+ + {selectedPlanIds.length > 1 ? "Add Plans" : "Add Plan"} + + {selectedPlanIds.length > 0 && ( + + {selectedPlanIds.length}{" "} + {selectedPlanIds.length === 1 ? "plan" : "plans"} + + )} +
+ +
+ + +
+ ); +} diff --git a/vite/src/views/migrations/migration/operations/ItemSummaryRow.tsx b/vite/src/views/migrations/migration/operations/ItemSummaryRow.tsx new file mode 100644 index 000000000..fb3268d8f --- /dev/null +++ b/vite/src/views/migrations/migration/operations/ItemSummaryRow.tsx @@ -0,0 +1,53 @@ +import { getProductItemDisplay } from "@autumn/shared"; +import { useOrg } from "@/hooks/common/useOrg"; +import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; +import { cn } from "@/lib/utils"; +import { PlanFeatureIcon } from "@/views/products/plan/components/plan-card/PlanFeatureIcon"; +import { CustomDotIcon } from "@/views/products/plan/components/plan-card/PlanFeatureRow"; +import { migrationItemToProductItem } from "../shared/migrationItemUtils"; + +export function ItemSummaryRow({ + item, + onClick, +}: { + item: Record; + onClick: () => void; +}) { + const { features } = useFeaturesQuery(); + const { org } = useOrg(); + const productItem = migrationItemToProductItem(item, features); + + const display = getProductItemDisplay({ + item: productItem, + features, + currency: org?.default_currency || "USD", + fullDisplay: true, + amountFormatOptions: { currencyDisplay: "narrowSymbol" }, + }); + + const feature = features.find((f) => f.id === productItem.feature_id); + const hasFeatureName = feature?.name && feature.name.trim() !== ""; + + return ( + + ); +} diff --git a/vite/src/views/migrations/migration/operations/MigrationOperationSheet.tsx b/vite/src/views/migrations/migration/operations/MigrationOperationSheet.tsx new file mode 100644 index 000000000..887be64eb --- /dev/null +++ b/vite/src/views/migrations/migration/operations/MigrationOperationSheet.tsx @@ -0,0 +1,213 @@ +import type { FrontendProduct, ProductItem } from "@autumn/shared"; +import { useCallback, useRef, useState } from "react"; +import { Button } from "@/components/v2/buttons/Button"; +import { ProductProvider } from "@/components/v2/inline-custom-plan-editor/PlanEditorContext"; +import { Sheet, SheetContent } from "@/components/v2/sheets/Sheet"; +import { disabledItemDraftController } from "@/hooks/inline-editor/useItemDraftController"; +import { getItemId } from "@/utils/product/productItemUtils"; +import { EditPlanPriceSheet } from "@/views/products/plan/components/EditPlanPriceSheet"; +import { EditPlanFeatureSheet } from "@/views/products/plan/components/edit-plan-feature/EditPlanFeatureSheet"; +import { SelectFeatureSheet } from "@/views/products/plan/components/SelectFeatureSheet"; +import { ProductItemContext } from "@/views/products/product/product-item/ProductItemContext"; + +export type OperationSheetMode = "add-feature" | "edit-feature" | "edit-price"; + +const EMPTY_PRODUCT: FrontendProduct = { + id: "__migration_temp__", + name: "Migration", + internal_id: "__migration_temp__", + org_id: "", + version: 1, + items: [], + free_trial: null, + planType: null, + basePriceType: null, +} as unknown as FrontendProduct; + +const MODE_TO_SHEET: Record = { + "add-feature": "select-feature", + "edit-feature": "edit-feature", + "edit-price": "edit-plan-price", +}; + +interface MigrationOperationSheetProps { + open: boolean; + onOpenChange: (open: boolean) => void; + mode: OperationSheetMode; + initialProduct?: Partial; + editItem?: ProductItem; + onSave: (product: FrontendProduct) => void; +} + +export function MigrationOperationSheet({ + open, + onOpenChange, + mode, + initialProduct, + editItem, + onSave, +}: MigrationOperationSheetProps) { + const [key, setKey] = useState(0); + + return ( + { + if (isOpen) setKey((k) => k + 1); + onOpenChange(isOpen); + }} + > + + {open && ( + { + onSave(product); + onOpenChange(false); + }} + onCancel={() => onOpenChange(false)} + /> + )} + + + ); +} + +function MigrationOperationSheetContent({ + mode, + initialProduct, + editItem, + onSave, + onCancel, +}: { + mode: OperationSheetMode; + initialProduct?: Partial; + editItem?: ProductItem; + onSave: (product: FrontendProduct) => void; + onCancel: () => void; +}) { + const buildInit = (): FrontendProduct => { + const base = { ...EMPTY_PRODUCT, ...initialProduct }; + if (mode === "edit-feature" && editItem) { + return { ...base, items: [editItem, ...(base.items ?? [])] }; + } + return base; + }; + + const [product, setProductState] = useState(buildInit); + const latestProduct = useRef(product); + + const wrappedSetProduct = ( + p: FrontendProduct | ((prev: FrontendProduct) => FrontendProduct), + ) => { + if (typeof p === "function") { + setProductState((prev) => { + const next = p(prev); + latestProduct.current = next; + return next; + }); + } else { + latestProduct.current = p; + setProductState(p); + } + }; + + const [sheetType, setSheetType] = useState(MODE_TO_SHEET[mode]); + const [itemId, setItemId] = useState( + mode === "edit-feature" && editItem ? "item-0" : null, + ); + const [initialItem, setInitialItem] = useState( + editItem ? structuredClone(editItem) : null, + ); + + const handleSetSheet = useCallback( + ({ type, itemId: id }: { type: string | null; itemId?: string | null }) => { + if (type) setSheetType(type); + setItemId(id ?? null); + + if (type === "edit-feature" && id) { + const items = latestProduct.current.items ?? []; + const match = items.find( + (item, i) => getItemId({ item, itemIndex: i }) === id, + ); + if (match) setInitialItem(structuredClone(match)); + } + }, + [], + ); + + const handleApply = () => { + onSave(latestProduct.current); + }; + + const handleFeatureCommit = async () => { + onSave(latestProduct.current); + return null; + }; + + const currentItem = + product.items?.find( + (item, i) => getItemId({ item, itemIndex: i }) === itemId, + ) ?? null; + + const setCurrentItem = (updatedItem: ProductItem) => { + if (!product.items || !itemId) return; + const index = product.items.findIndex( + (item, i) => getItemId({ item, itemIndex: i }) === itemId, + ); + if (index === -1) return; + const updatedItems = [...product.items]; + updatedItems[index] = updatedItem; + wrappedSetProduct((prev) => ({ ...prev, items: updatedItems })); + }; + + return ( + +
+
+ {sheetType === "select-feature" && } + {sheetType === "edit-plan-price" && } + {sheetType === "edit-feature" && currentItem && ( + {}, + isUpdate: !!editItem, + handleUpdateProductItem: handleFeatureCommit, + }} + > + + + )} +
+ {sheetType === "edit-plan-price" && ( +
+ + +
+ )} +
+
+ ); +} diff --git a/vite/src/views/migrations/migration/operations/OperationsForm.tsx b/vite/src/views/migrations/migration/operations/OperationsForm.tsx new file mode 100644 index 000000000..43beb28a3 --- /dev/null +++ b/vite/src/views/migrations/migration/operations/OperationsForm.tsx @@ -0,0 +1,231 @@ +import type { + AddPlanOp, + CustomerOperation, + Operations, + UpdatePlanOp, +} from "@autumn/shared"; +import { useState } from "react"; +import { + CaretDownIcon, + CheckIcon, + PackageIcon, + PencilSimpleIcon, +} from "@phosphor-icons/react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/v2/dropdowns/DropdownMenu"; +import { ActionCard } from "../shared/ActionCard"; +import { AutumnMark, StripeMark } from "../shared/BillingScopeMarks"; +import { AddPlansSection } from "./AddPlanOpForm"; +import { UpdatePlanOpForm } from "./UpdatePlanOpForm"; + +const DEFAULT_UPDATE_PLAN: CustomerOperation = { + type: "update_plan", + plan_filter: {}, +}; + +export function OperationsForm({ + value, + onChange, + noBillingChanges, + onNoBillingChangesChange, +}: { + value: Operations; + onChange: (value: Operations) => void; + noBillingChanges: boolean; + onNoBillingChangesChange: (value: boolean) => void; +}) { + const [autoOpenPicker, setAutoOpenPicker] = useState(false); + const operations = value.customer ?? []; + const addPlanOps = operations.filter( + (op): op is AddPlanOp => op.type === "add_plan", + ); + const hasAddPlans = addPlanOps.length > 0; + const isEmpty = operations.length === 0; + + const setOperations = (next: CustomerOperation[]) => + onChange({ ...value, customer: next }); + + const updateAt = (index: number, updated: CustomerOperation) => { + const next = [...operations]; + next[index] = updated; + setOperations(next); + }; + + const removeAt = (index: number) => + setOperations(operations.filter((_, i) => i !== index)); + + const handleAddPlansUpdate = (newAddPlanOps: AddPlanOp[]) => { + const nonAdd = operations.filter((op) => op.type !== "add_plan"); + setOperations([...nonAdd, ...newAddPlanOps]); + }; + + const removeAllAddPlans = () => + setOperations(operations.filter((op) => op.type !== "add_plan")); + + return ( +
+
+ Billing Scope + + + + + + onNoBillingChangesChange(true)} + > + + + Billing changes apply to Autumn only + + {noBillingChanges && } + + onNoBillingChangesChange(false)} + > + + + Billing changes apply to Autumn and Stripe + + {!noBillingChanges && } + + + +
+ + {isEmpty ? ( +
+ Add Operation +
+ + } + heading="Update Plan" + subheading="Modify existing customer plans" + onClick={() => { + setOperations([DEFAULT_UPDATE_PLAN]); + setAutoOpenPicker(true); + }} + className="flex-1" + /> + + } + heading="Add Plan" + subheading="Assign a new plan to customers" + onClick={() => { + setOperations([{ type: "add_plan", plan_id: "" }]); + setAutoOpenPicker(true); + }} + className="flex-1" + /> +
+
+ ) : ( + <> + {operations.map((operation, index) => { + if (operation.type === "add_plan") return null; + return ( +
+ {index > 0 &&
} + updateAt(index, updated)} + onRemove={() => removeAt(index)} + defaultOpenPicker={index === 0 && autoOpenPicker} + /> +
+ ); + })} + + {hasAddPlans && ( + <> + {operations.some((op) => op.type === "update_plan") && ( +
+ )} + + + )} + +
+ Add Operation +
+ + } + heading="Update Plan" + subheading="Modify existing customer plans" + onClick={() => + setOperations([...operations, DEFAULT_UPDATE_PLAN]) + } + className="flex-1" + /> + {!hasAddPlans && ( + + } + heading="Add Plan" + subheading="Assign a new plan to customers" + onClick={() => + setOperations([ + ...operations, + { type: "add_plan", plan_id: "" }, + ]) + } + className="flex-1" + /> + )} +
+
+ + )} +
+ ); +} diff --git a/vite/src/views/migrations/migration/operations/RemoveItemRows.tsx b/vite/src/views/migrations/migration/operations/RemoveItemRows.tsx new file mode 100644 index 000000000..b439b4a3b --- /dev/null +++ b/vite/src/views/migrations/migration/operations/RemoveItemRows.tsx @@ -0,0 +1,33 @@ +import { FeatureSearchDropdown } from "@/components/v2/dropdowns/FeatureSearchDropdown"; +import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; +import { cn } from "@/lib/utils"; +import { RemoveButton } from "../shared/RemoveButton"; + +export function RemoveItemRows({ + item, + onChange, + onRemove, +}: { + item: Record; + onChange: (item: Record) => void; + onRemove: () => void; +}) { + const { features } = useFeaturesQuery(); + const featureId = (item.feature_id as string) || null; + + return ( +
+ Remove + onChange({ ...item, feature_id: v })} + placeholder="Select feature to remove..." + triggerClassName={cn( + featureId && "!border-destructive/50 hover:!border-destructive/60", + )} + /> + +
+ ); +} diff --git a/vite/src/views/migrations/migration/operations/UpdatePlanOpForm.tsx b/vite/src/views/migrations/migration/operations/UpdatePlanOpForm.tsx new file mode 100644 index 000000000..d2bc70415 --- /dev/null +++ b/vite/src/views/migrations/migration/operations/UpdatePlanOpForm.tsx @@ -0,0 +1,399 @@ +import type { + BillingInterval, + FrontendProduct, + ProductItem, + UpdatePlanOp, +} from "@autumn/shared"; +import { productV2ToBasePrice } from "@autumn/shared"; +import { + CurrencyCircleDollarIcon, + GitBranchIcon, + PlusIcon, +} from "@phosphor-icons/react"; +import { useMemo, useState } from "react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/v2/dropdowns/DropdownMenu"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/v2/selects/Select"; +import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; +import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; +import { DASHED_BUTTON_CLASS } from "../shared/AddButton"; +import { + migrationItemToProductItem, + productItemToMigrationItem, +} from "../shared/migrationItemUtils"; +import { buildPlanSuggestions } from "../shared/planSuggestions"; +import { RemoveButton } from "../shared/RemoveButton"; +import { ValuePicker } from "../shared/ValuePicker"; +import { ItemSummaryRow } from "./ItemSummaryRow"; +import { + MigrationOperationSheet, + type OperationSheetMode, +} from "./MigrationOperationSheet"; +import { RemoveItemRows } from "./RemoveItemRows"; + +function useVersionOptions(planFilter: UpdatePlanOp["plan_filter"]) { + const { products } = useProductsQuery(); + + const targetPlanId = + typeof planFilter.plan_id === "string" && planFilter.plan_id + ? planFilter.plan_id + : null; + + const matchingProducts = targetPlanId + ? products.filter((p) => p.id === targetPlanId) + : []; + + return matchingProducts + .map((p) => ({ + value: String(p.version), + label: `v${p.version}`, + })) + .sort((a, b) => Number(a.value) - Number(b.value)); +} + +export function UpdatePlanOpForm({ + value, + onChange, + onRemove, + defaultOpenPicker = false, +}: { + value: UpdatePlanOp; + onChange: (value: UpdatePlanOp) => void; + onRemove: () => void; + defaultOpenPicker?: boolean; +}) { + const { products } = useProductsQuery(); + const versionOptions = useVersionOptions(value.plan_filter); + const { features } = useFeaturesQuery(); + + const [sheetOpen, setSheetOpen] = useState(false); + const [sheetMode, setSheetMode] = useState("add-feature"); + const [editingItemIndex, setEditingItemIndex] = useState(null); + + const update = (patch: Partial) => + onChange({ ...value, ...patch }); + + const planSuggestions = useMemo( + () => buildPlanSuggestions(products), + [products], + ); + + const selectedPlanIds = extractPlanIds(value.plan_filter.plan_id); + + const handlePlanToggle = (planId: string) => { + const next = selectedPlanIds.includes(planId) + ? selectedPlanIds.filter((id) => id !== planId) + : [...selectedPlanIds, planId]; + update({ + plan_filter: { ...value.plan_filter, plan_id: toPlanIdMatcher(next) }, + }); + }; + + const handlePlanRemove = (planId: string) => { + const next = selectedPlanIds.filter((id) => id !== planId); + update({ + plan_filter: { ...value.plan_filter, plan_id: toPlanIdMatcher(next) }, + }); + }; + + const customize = value.customize; + const addItems = customize?.add_items ?? []; + + const openSheet = (mode: OperationSheetMode, itemIndex?: number) => { + setSheetMode(mode); + setEditingItemIndex(itemIndex ?? null); + setSheetOpen(true); + }; + + const editItem: ProductItem | undefined = + editingItemIndex !== null + ? migrationItemToProductItem(addItems[editingItemIndex], features) + : undefined; + + const initialProduct = buildInitialProduct(value); + + const handleSheetSave = (product: FrontendProduct) => { + if (sheetMode === "edit-price") { + const basePrice = productV2ToBasePrice({ product }); + if (basePrice) { + const amount = + ((basePrice as Record).price as number) ?? + basePrice.tiers?.[0]?.amount ?? + 0; + update({ + customize: { + ...customize, + price: { + amount, + interval: (basePrice.interval as BillingInterval) ?? "month", + }, + }, + }); + } else if (product.planType === "free") { + update({ + customize: { + ...customize, + price: undefined, + }, + }); + } + } else { + const newItems = (product.items ?? []) + .filter((pi) => pi.feature_id) + .map(productItemToMigrationItem); + + if (newItems.length === 0) return; + + const items = [...addItems]; + if (editingItemIndex !== null) { + items[editingItemIndex] = newItems[0]; + } else { + items.push(...newItems); + } + update({ customize: { ...customize, add_items: items } }); + } + }; + + const handleRemoveItem = (index: number) => { + const items = addItems.filter((_, i) => i !== index); + update({ + customize: { + ...customize, + add_items: items.length > 0 ? items : undefined, + }, + }); + }; + + return ( +
+
+
+ + {selectedPlanIds.length > 1 ? "Update Plans" : "Update Plan"} + + {selectedPlanIds.length > 0 && ( + + {selectedPlanIds.length}{" "} + {selectedPlanIds.length === 1 ? "plan" : "plans"} + + )} +
+ +
+ +
+ +
+ + {value.version !== undefined && ( +
+ + Version + + + update({ version: undefined })} /> +
+ )} + + {customize?.price !== undefined && ( +
+ + Price + + + + update({ customize: { ...customize, price: undefined } }) + } + /> +
+ )} + + {addItems.map((item, index) => ( +
+ Add + openSheet("edit-feature", index)} + /> + handleRemoveItem(index)} /> +
+ ))} + + {(customize?.remove_items ?? []).map((item, index) => ( + { + const items = [...(customize?.remove_items ?? [])]; + items[index] = updated; + update({ customize: { ...customize, remove_items: items } }); + }} + onRemove={() => { + const items = (customize?.remove_items ?? []).filter( + (_, i) => i !== index, + ); + update({ + customize: { + ...customize, + remove_items: items.length > 0 ? items : undefined, + }, + }); + }} + /> + ))} + + + + + Add modification + + + {value.version === undefined && ( + update({ version: 1 })} + > + Version + + )} + {(!customize || customize.price === undefined) && ( + openSheet("edit-price")} + > + Base Price + + )} + openSheet("add-feature")} + > + Add Item + + + update({ + customize: { + ...customize, + remove_items: [ + ...(customize?.remove_items ?? []), + {} as Record, + ], + }, + }) + } + > + Remove Item + + + + + +
+ ); +} + +function extractPlanIds( + planId: UpdatePlanOp["plan_filter"]["plan_id"], +): string[] { + if (!planId) return []; + if (typeof planId === "string") return planId ? [planId] : []; + if (planId.$in) + return planId.$in.filter((v): v is string => typeof v === "string"); + if (planId.$eq) return typeof planId.$eq === "string" ? [planId.$eq] : []; + return []; +} + +function toPlanIdMatcher( + ids: string[], +): UpdatePlanOp["plan_filter"]["plan_id"] { + if (ids.length === 0) return undefined; + if (ids.length === 1) return ids[0]; + return { $in: ids }; +} + +function buildInitialProduct(value: UpdatePlanOp): Partial { + const items: ProductItem[] = []; + + if (value.customize?.price) { + const amount = value.customize.price.amount ?? 0; + items.push({ + price: amount, + tiers: [{ to: "inf", amount }], + interval: value.customize.price.interval ?? "month", + billing_units: 1, + } as ProductItem); + } + + return { + version: value.version ?? 1, + planType: value.customize?.price ? "paid" : "free", + basePriceType: "recurring", + items, + }; +} diff --git a/vite/src/views/migrations/migration/runs/RunStatusBadge.tsx b/vite/src/views/migrations/migration/runs/RunStatusBadge.tsx new file mode 100644 index 000000000..4681520ae --- /dev/null +++ b/vite/src/views/migrations/migration/runs/RunStatusBadge.tsx @@ -0,0 +1,79 @@ +import { Badge } from "@/components/v2/badges/Badge"; +import type { MigrationItemEventStatus } from "@/hooks/queries/useMigrationRunsQuery"; +import { cn } from "@/lib/utils"; + +export function ActiveRunDot({ className }: { className?: string }) { + return ( + + + + + ); +} + +const LIVE_STYLES: Record = { + succeeded: "bg-green-500/10 text-green-500 border-transparent", + skipped: "bg-muted text-t2 border-transparent", + failed: "bg-red-500/10 text-red-500 border-transparent", +}; + +const DRY_STYLES: Record = { + succeeded: "bg-blue-500/10 text-blue-500 border-blue-500/30 border-dashed", + skipped: "bg-muted text-t2 border-border border-dashed", + failed: "bg-orange-500/10 text-orange-500 border-orange-500/30 border-dashed", +}; + +const STATUS_LABELS: Record = { + succeeded: "Passed", + skipped: "Skipped", + failed: "Failed", +}; + +function isNoOpResponse(response: Record | null): boolean { + if (!response) return false; + const preview = response.preview as + | { + plan_changes?: unknown[]; + balance_changes?: unknown[]; + flag_changes?: unknown[]; + } + | undefined; + if (!preview) return false; + return ( + (preview.plan_changes?.length ?? 0) === 0 && + (preview.balance_changes?.length ?? 0) === 0 && + (preview.flag_changes?.length ?? 0) === 0 + ); +} + +export function ItemEventStatusBadge({ + status, + dryRun = false, + response = null, +}: { + status: MigrationItemEventStatus; + dryRun?: boolean; + response?: Record | null; +}) { + if (status === "skipped" && isNoOpResponse(response)) + return ( + + No Changes + + ); + + return ( + + {STATUS_LABELS[status]} + + ); +} diff --git a/vite/src/views/migrations/migration/shared/ActionCard.tsx b/vite/src/views/migrations/migration/shared/ActionCard.tsx new file mode 100644 index 000000000..c77a7d197 --- /dev/null +++ b/vite/src/views/migrations/migration/shared/ActionCard.tsx @@ -0,0 +1,33 @@ +import type { ReactNode } from "react"; +import { cn } from "@/lib/utils"; + +const ACTION_CARD_CLASS = + "flex items-center gap-3 min-h-16 py-3 px-4 rounded-xl bg-transparent border border-dashed border-border/50 text-left cursor-pointer outline-none hover:border-border hover:bg-muted/40 active:bg-muted/60 focus-visible:bg-muted/50 transition-colors"; + +export function ActionCard({ + icon, + heading, + subheading, + onClick, + className, +}: { + icon: ReactNode; + heading: string; + subheading: string; + onClick: () => void; + className?: string; +}) { + return ( + + ); +} diff --git a/vite/src/views/migrations/migration/shared/AddButton.tsx b/vite/src/views/migrations/migration/shared/AddButton.tsx new file mode 100644 index 000000000..f5e0c7dff --- /dev/null +++ b/vite/src/views/migrations/migration/shared/AddButton.tsx @@ -0,0 +1,26 @@ +import { PlusIcon } from "@phosphor-icons/react"; +import { cn } from "@/lib/utils"; + +export const DASHED_BUTTON_CLASS = + "flex items-center gap-2 w-full h-8 px-3 rounded-xl bg-transparent border border-dashed border-border/50 text-t4 text-sm cursor-pointer outline-none hover:border-border hover:text-t2 active:border-border focus-visible:bg-muted/50 transition-colors"; + +export function AddButton({ + label, + onClick, + className, +}: { + label: string; + onClick: () => void; + className?: string; +}) { + return ( + + ); +} diff --git a/vite/src/views/migrations/migration/shared/BillingScopeMarks.tsx b/vite/src/views/migrations/migration/shared/BillingScopeMarks.tsx new file mode 100644 index 000000000..59fa6af3f --- /dev/null +++ b/vite/src/views/migrations/migration/shared/BillingScopeMarks.tsx @@ -0,0 +1,34 @@ +export function AutumnMark({ size = 14 }: { size?: number }) { + return ( + + ); +} + +export function StripeMark({ size = 14 }: { size?: number }) { + return ( + + ); +} diff --git a/vite/src/views/migrations/migration/shared/BooleanPill.tsx b/vite/src/views/migrations/migration/shared/BooleanPill.tsx new file mode 100644 index 000000000..32f583393 --- /dev/null +++ b/vite/src/views/migrations/migration/shared/BooleanPill.tsx @@ -0,0 +1,24 @@ +import { CheckCircleIcon, XCircleIcon } from "@phosphor-icons/react"; + +export function BooleanPill({ + value, + onChange, +}: { + value: boolean; + onChange: (value: boolean) => void; +}) { + return ( + + ); +} diff --git a/vite/src/views/migrations/migration/shared/RemoveButton.tsx b/vite/src/views/migrations/migration/shared/RemoveButton.tsx new file mode 100644 index 000000000..a2d6adf14 --- /dev/null +++ b/vite/src/views/migrations/migration/shared/RemoveButton.tsx @@ -0,0 +1,26 @@ +import { XIcon } from "@phosphor-icons/react"; +import { Button } from "@/components/v2/buttons/Button"; +import { cn } from "@/lib/utils"; + +export function RemoveButton({ + onClick, + className, +}: { + onClick: () => void; + className?: string; +}) { + return ( + + ); +} diff --git a/vite/src/views/migrations/migration/shared/RunSummaryRows.tsx b/vite/src/views/migrations/migration/shared/RunSummaryRows.tsx new file mode 100644 index 000000000..286bc7a52 --- /dev/null +++ b/vite/src/views/migrations/migration/shared/RunSummaryRows.tsx @@ -0,0 +1,46 @@ +import type { Operations } from "@autumn/shared"; +import { GearIcon } from "@phosphor-icons/react"; +import type { ReactNode } from "react"; +import { AutumnMark, StripeMark } from "./BillingScopeMarks"; +import { getOperationsSummaryText } from "./operationUtils"; + +const SUMMARY_ROW_CLASS = + "flex items-center gap-2 rounded-xl h-8 px-3 text-sm input-base text-t1"; + +export function RunSummaryRows({ + customerIcon, + customerLabel, + operations, + noBillingChanges, +}: { + customerIcon: ReactNode; + customerLabel: ReactNode; + operations: Operations; + noBillingChanges: boolean; +}) { + return ( +
+
+ {customerIcon} + {customerLabel} +
+
+ + {getOperationsSummaryText(operations)} +
+
+ {noBillingChanges ? ( + <> + + Billing changes apply to Autumn only + + ) : ( + <> + + Billing changes apply to Autumn and Stripe + + )} +
+
+ ); +} diff --git a/vite/src/views/migrations/migration/shared/ValuePicker.tsx b/vite/src/views/migrations/migration/shared/ValuePicker.tsx new file mode 100644 index 000000000..cf69a7483 --- /dev/null +++ b/vite/src/views/migrations/migration/shared/ValuePicker.tsx @@ -0,0 +1,137 @@ +import { CheckIcon, XIcon } from "@phosphor-icons/react"; +import type { ReactNode } from "react"; +import { useState } from "react"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; + +const MAX_VISIBLE_CHIPS = 3; + +export type ValuePickerOption = { + value: string; + label: string; + icon?: ReactNode; +}; + +export function ValuePicker({ + suggestions, + selectedValues, + onToggle, + onRemove, + placeholder = "Select...", + className: triggerClassName, + defaultOpen = false, +}: { + suggestions: ValuePickerOption[]; + selectedValues: string[]; + onToggle: (value: string) => void; + onRemove: (value: string) => void; + placeholder?: string; + className?: string; + defaultOpen?: boolean; +}) { + const [open, setOpen] = useState(defaultOpen); + + const getOption = (val: string) => suggestions.find((s) => s.value === val); + + return ( +
+ + + + + + + + + + No results + + + {suggestions.map((suggestion) => { + const isSelected = selectedValues.includes(suggestion.value); + return ( + onToggle(suggestion.value)} + className="text-sm" + > + {suggestion.icon && ( + {suggestion.icon} + )} + + {suggestion.label} + + {isSelected && ( + + )} + + ); + })} + + + + + +
+ ); +} diff --git a/vite/src/views/migrations/migration/shared/featureSuggestions.tsx b/vite/src/views/migrations/migration/shared/featureSuggestions.tsx new file mode 100644 index 000000000..ec37533c1 --- /dev/null +++ b/vite/src/views/migrations/migration/shared/featureSuggestions.tsx @@ -0,0 +1,13 @@ +import type { Feature } from "@autumn/shared"; +import { getFeatureIcon } from "@/views/products/features/utils/getFeatureIcon"; +import type { ValuePickerOption } from "./ValuePicker"; + +export function buildFeatureSuggestions( + features: Feature[], +): ValuePickerOption[] { + return features.map((f) => ({ + value: f.id, + label: f.name || f.id, + icon: getFeatureIcon({ feature: f }), + })); +} diff --git a/vite/src/views/migrations/migration/shared/migrationItemUtils.ts b/vite/src/views/migrations/migration/shared/migrationItemUtils.ts new file mode 100644 index 000000000..3d9574464 --- /dev/null +++ b/vite/src/views/migrations/migration/shared/migrationItemUtils.ts @@ -0,0 +1,48 @@ +import type { + Feature, + ProductItem, + ProductItemInterval, + UsageModel, +} from "@autumn/shared"; +import { getDefaultItem } from "@/views/products/plan/utils/getDefaultItem"; + +export function migrationItemToProductItem( + migItem: Record, + features: Feature[], +): ProductItem { + const featureId = migItem.feature_id as string | undefined; + const feature = featureId ? features.find((f) => f.id === featureId) : null; + const base = feature + ? (getDefaultItem({ feature }) as ProductItem) + : ({ feature_id: featureId } as ProductItem); + + if (migItem.included !== undefined) { + base.included_usage = migItem.included as number; + } + const price = migItem.price as Record | undefined; + if (price) { + base.tiers = [{ to: "inf", amount: Number(price.amount ?? 0) }]; + if (price.interval) base.interval = price.interval as ProductItemInterval; + if (price.billing_method) + base.usage_model = price.billing_method as UsageModel; + base.billing_units = 1; + } + return base; +} + +export function productItemToMigrationItem( + item: ProductItem, +): Record { + const result: Record = { feature_id: item.feature_id }; + if (item.included_usage !== null && item.included_usage !== undefined) { + result.included = item.included_usage; + } + if (item.tiers && item.tiers.length > 0) { + result.price = { + amount: item.tiers[0].amount ?? 0, + interval: item.interval ?? undefined, + billing_method: item.usage_model ?? undefined, + }; + } + return result; +} diff --git a/vite/src/views/migrations/migration/shared/operationUtils.ts b/vite/src/views/migrations/migration/shared/operationUtils.ts new file mode 100644 index 000000000..77bdd217e --- /dev/null +++ b/vite/src/views/migrations/migration/shared/operationUtils.ts @@ -0,0 +1,28 @@ +import type { Operations } from "@autumn/shared"; + +export function hasValidOperations(operations: Operations): boolean { + const ops = operations.customer ?? []; + if (ops.length === 0) return false; + return ops.every((op) => { + if (op.type === "update_plan") + return ( + op.version !== undefined || (op.customize && op.customize.length > 0) + ); + if (op.type === "add_plan") return !!op.plan_id; + return false; + }); +} + +export function getOperationsSummaryText(operations: Operations): string { + const ops = operations.customer ?? []; + const updateCount = ops.filter((op) => op.type === "update_plan").length; + const addCount = ops.filter((op) => op.type === "add_plan").length; + const parts: string[] = []; + if (updateCount > 0) + parts.push( + `${updateCount} plan ${updateCount === 1 ? "update" : "updates"}`, + ); + if (addCount > 0) + parts.push(`${addCount} plan ${addCount === 1 ? "addition" : "additions"}`); + return parts.join(", ") || "No operations"; +} diff --git a/vite/src/views/migrations/migration/shared/planSuggestions.tsx b/vite/src/views/migrations/migration/shared/planSuggestions.tsx new file mode 100644 index 000000000..ce4fe3025 --- /dev/null +++ b/vite/src/views/migrations/migration/shared/planSuggestions.tsx @@ -0,0 +1,20 @@ +import type { FrontendProduct } from "@autumn/shared"; +import { PackageIcon } from "@phosphor-icons/react"; +import type { ValuePickerOption } from "./ValuePicker"; + +export function buildPlanSuggestions( + products: FrontendProduct[], +): ValuePickerOption[] { + const seen = new Set(); + return products + .filter((p) => { + if (!p.id || seen.has(p.id)) return false; + seen.add(p.id); + return true; + }) + .map((p) => ({ + value: p.id, + label: p.name || p.id, + icon: , + })); +} diff --git a/vite/src/views/migrations/migration/useMigrationEditorForm.ts b/vite/src/views/migrations/migration/useMigrationEditorForm.ts new file mode 100644 index 000000000..d3a9a4c23 --- /dev/null +++ b/vite/src/views/migrations/migration/useMigrationEditorForm.ts @@ -0,0 +1,116 @@ +import type { Migration, MigrationFilter, Operations } from "@autumn/shared"; +import { useStore } from "@tanstack/react-form"; +import type { AxiosError } from "axios"; +import { debounce } from "lodash"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { toast } from "sonner"; +import { useAppForm } from "@/hooks/form/form"; +import { useMigrationsQuery } from "@/hooks/queries/useMigrationsQuery"; +import { getBackendErr } from "@/utils/genUtils"; + +const AUTO_SAVE_DEBOUNCE_MS = 1000; + +const FRIENDLY_MESSAGES: Record = { + "update_plan requires at least one of version or customize": + "Each Update Plan needs at least a version or customization", + "operations requires at least one resource block": + "Add at least one operation", +}; + +function humanizeValidationError(raw: string): string { + const stripped = raw.replace(/^\[Validation Errors\]\s*/, ""); + const segments = stripped.split(";").map((s) => s.trim()); + const messages = segments + .map((segment) => { + const withoutPath = segment.replace(/^[\w.[\]]+:\s*/, ""); + const friendly = FRIENDLY_MESSAGES[withoutPath]; + if (friendly) return friendly; + return withoutPath.charAt(0).toUpperCase() + withoutPath.slice(1); + }) + .filter(Boolean); + return [...new Set(messages)].join(". "); +} + +export function useMigrationEditorForm({ + migration, +}: { + migration: Migration; +}) { + const { updateMigration } = useMigrationsQuery(); + const [saveError, setSaveError] = useState(null); + const showErrors = useRef(false); + + const form = useAppForm({ + defaultValues: { + filter: (migration.filter ?? {}) as MigrationFilter, + operations: (migration.operations ?? {}) as Operations, + noBillingChanges: migration.no_billing_changes ?? false, + }, + onSubmit: async ({ value }) => { + try { + await updateMigration({ + id: migration.id, + updates: { + filter: value.filter, + operations: value.operations, + no_billing_changes: value.noBillingChanges, + }, + }); + setSaveError(null); + } catch (error) { + toast.error( + getBackendErr(error as AxiosError, "Failed to save migration"), + ); + } + }, + }); + + const values = useStore(form.store, (s) => s.values); + const serialized = JSON.stringify(values); + const isInitialMount = useRef(true); + + const debouncedSave = useMemo( + () => + debounce(async () => { + try { + const { filter, operations, noBillingChanges } = + form.store.state.values; + await updateMigration({ + id: migration.id, + updates: { + filter, + operations, + no_billing_changes: noBillingChanges, + }, + }); + setSaveError(null); + } catch (error) { + if (showErrors.current) { + const raw = getBackendErr(error as AxiosError, ""); + setSaveError(humanizeValidationError(raw)); + } + } + }, AUTO_SAVE_DEBOUNCE_MS), + [migration.id, updateMigration, form.store], + ); + + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + return; + } + debouncedSave(); + }, [serialized, debouncedSave]); + + useEffect(() => () => debouncedSave.cancel(), [debouncedSave]); + + const enableErrorDisplay = () => { + showErrors.current = true; + }; + + return { form, saveError, enableErrorDisplay }; +} + +export type MigrationEditorFormInstance = ReturnType< + typeof useMigrationEditorForm +>; diff --git a/vite/src/views/products/plan/components/PlanToolbar.tsx b/vite/src/views/products/plan/components/PlanToolbar.tsx index 6f01ef5bf..e1aa69a5b 100644 --- a/vite/src/views/products/plan/components/PlanToolbar.tsx +++ b/vite/src/views/products/plan/components/PlanToolbar.tsx @@ -52,27 +52,29 @@ export const PlanToolbar = () => { /> - { - e.stopPropagation(); - e.preventDefault(); - setCopyOpen(true); - }} - > -
- Copy - -
-
- { - e.stopPropagation(); - e.preventDefault(); - setDeleteOpen(true); - }} - > + { + e.stopPropagation(); + e.preventDefault(); + setDropdownOpen(false); + setCopyOpen(true); + }} + > +
+ Copy + +
+
+ { + e.stopPropagation(); + e.preventDefault(); + setDropdownOpen(false); + setDeleteOpen(true); + }} + >
Delete Plan diff --git a/vite/src/views/products/plan/components/SelectFeatureSheet.tsx b/vite/src/views/products/plan/components/SelectFeatureSheet.tsx index 389af0bd8..a4a95e664 100644 --- a/vite/src/views/products/plan/components/SelectFeatureSheet.tsx +++ b/vite/src/views/products/plan/components/SelectFeatureSheet.tsx @@ -134,16 +134,14 @@ export function SelectFeatureSheet({ ) : null } footer={ -
- -
+ } /> diff --git a/vite/src/views/products/plan/components/edit-plan-feature/EditPlanFeatureSheet.tsx b/vite/src/views/products/plan/components/edit-plan-feature/EditPlanFeatureSheet.tsx index df685ed8a..490f9a082 100644 --- a/vite/src/views/products/plan/components/edit-plan-feature/EditPlanFeatureSheet.tsx +++ b/vite/src/views/products/plan/components/edit-plan-feature/EditPlanFeatureSheet.tsx @@ -32,7 +32,7 @@ export function EditPlanFeatureSheet({ }: { isOnboarding?: boolean; }) { - const { item, setItem } = useProductItemContext(); + const { item, setItem, isUpdate } = useProductItemContext(); const { features, refetch } = useFeaturesQuery(); const { product, setProduct } = useProduct(); const { setInitialItem } = useSheet(); @@ -124,7 +124,7 @@ export function EditPlanFeatureSheet({ item.tiers[0].amount === 0 && !item.included_usage; - const hasChanges = hasItemChanges || isZeroPriceItem; + const hasChanges = hasItemChanges || isZeroPriceItem || !isUpdate; return (
diff --git a/vite/tests/views/migrations/migration/filters/filterRowTypes.test.ts b/vite/tests/views/migrations/migration/filters/filterRowTypes.test.ts new file mode 100644 index 000000000..35313473d --- /dev/null +++ b/vite/tests/views/migrations/migration/filters/filterRowTypes.test.ts @@ -0,0 +1,243 @@ +import { describe, expect, test } from "bun:test"; +import type { PlanFilter } from "@autumn/shared"; +import { + customerIdToStrings, + groupsToPlanFilter, + planFilterToGroups, + stringsToCustomerId, +} from "@/views/migrations/migration/filters/filterRowTypes"; + +describe("planFilterToGroups -> groupsToPlanFilter roundtrip", () => { + const roundtrip = (input: PlanFilter): PlanFilter => { + const groups = planFilterToGroups(input); + return groupsToPlanFilter(groups); + }; + + test("empty filter", () => { + expect(roundtrip({})).toEqual({}); + }); + + test("plan_id bare string", () => { + expect(roundtrip({ plan_id: "pro" })).toEqual({ plan_id: "pro" }); + }); + + test("plan_id $in", () => { + const input = { plan_id: { $in: ["a", "b"] } }; + expect(roundtrip(input)).toEqual(input); + }); + + test("plan_id $nin", () => { + const input = { plan_id: { $nin: ["x"] } }; + expect(roundtrip(input)).toEqual(input); + }); + + test("plan_id $ne", () => { + const input = { plan_id: { $ne: "old" } }; + expect(roundtrip(input)).toEqual(input); + }); + + test("plan_id $regex", () => { + const input = { plan_id: { $regex: "^pro" } }; + expect(roundtrip(input)).toEqual(input); + }); + + test("plan_id $startsWith", () => { + const input = { plan_id: { $startsWith: "pro" } }; + expect(roundtrip(input)).toEqual(input); + }); + + test("paid: true", () => { + expect(roundtrip({ paid: true })).toEqual({ paid: true }); + }); + + test("paid: false", () => { + expect(roundtrip({ paid: false })).toEqual({ paid: false }); + }); + + test("recurring: true", () => { + expect(roundtrip({ recurring: true })).toEqual({ recurring: true }); + }); + + test("price: null (free plan)", () => { + expect(roundtrip({ price: null })).toEqual({ price: null }); + }); + + test("price: { $ne: null } (paid plan)", () => { + expect(roundtrip({ price: { $ne: null } })).toEqual({ + price: { $ne: null }, + }); + }); + + test("item with implicit $some (feature_id)", () => { + const input: PlanFilter = { item: { feature_id: "credits" } }; + expect(roundtrip(input)).toEqual(input); + }); + + test("item with $every wrapper", () => { + const input: PlanFilter = { + item: { $every: { feature_id: "credits" } }, + }; + expect(roundtrip(input)).toEqual(input); + }); + + test("item with $none wrapper", () => { + const input: PlanFilter = { + item: { $none: { feature_id: "credits" } }, + }; + expect(roundtrip(input)).toEqual(input); + }); + + test("item unlimited boolean", () => { + const input: PlanFilter = { item: { unlimited: true } }; + expect(roundtrip(input)).toEqual(input); + }); + + test("item price null (free item)", () => { + const input: PlanFilter = { item: { price: null } }; + expect(roundtrip(input)).toEqual(input); + }); + + test("item price { $ne: null } (paid item)", () => { + const input: PlanFilter = { item: { price: { $ne: null } } }; + expect(roundtrip(input)).toEqual(input); + }); + + test("item billing_method", () => { + const input: PlanFilter = { + item: { price: { billing_method: "prepaid" } }, + }; + const result = roundtrip(input); + expect(result.item).toBeDefined(); + const price = (result.item as Record).price as Record; + expect(price.billing_method).toBe("prepaid"); + }); + + test("$or groups", () => { + const input: PlanFilter = { + plan_id: "pro", + $or: [{ plan_id: "enterprise" }, { plan_id: "team" }], + }; + expect(roundtrip(input)).toEqual(input); + }); + + test("combined filters", () => { + const input: PlanFilter = { + plan_id: "pro", + paid: true, + recurring: true, + item: { feature_id: "credits", unlimited: false }, + }; + expect(roundtrip(input)).toEqual(input); + }); +}); + +describe("planFilterToGroups structure", () => { + test("empty filter produces one group with no rules", () => { + const groups = planFilterToGroups({}); + expect(groups).toHaveLength(1); + expect(groups[0].rules).toHaveLength(0); + }); + + test("$or produces multiple groups", () => { + const groups = planFilterToGroups({ + plan_id: "a", + $or: [{ plan_id: "b" }], + }); + expect(groups).toHaveLength(2); + }); + + test("$every item mode produces item_mode rule", () => { + const groups = planFilterToGroups({ + item: { $every: { feature_id: "x" } }, + }); + const modeRule = groups[0].rules.find((r) => r.field === "item_mode"); + expect(modeRule).toBeDefined(); + expect(modeRule!.values).toEqual(["every"]); + }); + + test("implicit $some does not produce item_mode rule", () => { + const groups = planFilterToGroups({ + item: { feature_id: "x" }, + }); + const modeRule = groups[0].rules.find((r) => r.field === "item_mode"); + expect(modeRule).toBeUndefined(); + }); +}); + +describe("customerIdToStrings", () => { + test("undefined returns empty array", () => { + expect(customerIdToStrings(undefined)).toEqual([]); + }); + + test("null returns empty array", () => { + expect(customerIdToStrings(null)).toEqual([]); + }); + + test("bare string returns single-element array", () => { + expect(customerIdToStrings("cus_1")).toEqual(["cus_1"]); + }); + + test("empty string returns empty array", () => { + expect(customerIdToStrings("")).toEqual([]); + }); + + test("$eq returns single-element array", () => { + expect(customerIdToStrings({ $eq: "cus_1" })).toEqual(["cus_1"]); + }); + + test("$ne returns single-element array", () => { + expect(customerIdToStrings({ $ne: "cus_1" })).toEqual(["cus_1"]); + }); + + test("$in returns all values", () => { + expect(customerIdToStrings({ $in: ["a", "b", "c"] })).toEqual([ + "a", + "b", + "c", + ]); + }); + + test("$nin returns all values", () => { + expect(customerIdToStrings({ $nin: ["x"] })).toEqual(["x"]); + }); +}); + +describe("stringsToCustomerId", () => { + test("empty array returns undefined", () => { + expect(stringsToCustomerId([])).toBeUndefined(); + }); + + test("array with only whitespace returns undefined", () => { + expect(stringsToCustomerId([" ", ""])).toBeUndefined(); + }); + + test("single value returns bare string", () => { + expect(stringsToCustomerId(["cus_1"])).toBe("cus_1"); + }); + + test("multiple values returns $in", () => { + expect(stringsToCustomerId(["a", "b"])).toEqual({ $in: ["a", "b"] }); + }); + + test("trims whitespace", () => { + expect(stringsToCustomerId([" a ", " b "])).toEqual({ + $in: ["a", "b"], + }); + }); +}); + +describe("customerIdToStrings -> stringsToCustomerId roundtrip", () => { + test("bare string roundtrips", () => { + const input = "cus_1"; + expect(stringsToCustomerId(customerIdToStrings(input))).toBe(input); + }); + + test("$in roundtrips", () => { + const input = { $in: ["a", "b"] as string[] }; + expect(stringsToCustomerId(customerIdToStrings(input))).toEqual(input); + }); + + test("undefined roundtrips", () => { + expect(stringsToCustomerId(customerIdToStrings(undefined))).toBeUndefined(); + }); +});