From 5832d9149069d62afee7106b9b85d7f94a74daa9 Mon Sep 17 00:00:00 2001
From: tbeaumont79
Date: Thu, 2 Jul 2026 15:35:30 +0200
Subject: [PATCH 1/2] =?UTF-8?q?feat(billing):=20page=20cr=C3=A9dits=20avec?=
=?UTF-8?q?=20polling=20du=20solde=20au=20retour=20de=20Checkout?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Le webhook Stripe crédite de façon asynchrone (1–3 s après le retour de
Checkout). L'ancien unique refresh à 1,5 s manquait le solde si le webhook
arrivait plus tard. On mémorise le solde avant la redirection (localStorage,
survit au rechargement de page) et on relit le solde avec backoff jusqu'à ce
qu'il dépasse la référence, avec toast de confirmation.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
apps/app/pages/credits.vue | 143 +++++++++++++++++++++++++++++++++++++
1 file changed, 143 insertions(+)
create mode 100644 apps/app/pages/credits.vue
diff --git a/apps/app/pages/credits.vue b/apps/app/pages/credits.vue
new file mode 100644
index 0000000..92561b9
--- /dev/null
+++ b/apps/app/pages/credits.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
{{ packs[pack.key].name }}
+
+
+ {{ fr.pricing.featuredBadge }}
+
+
+
+
+ {{ pack.priceEur }} €
+ · {{ pack.credits }} crédits
+
+
+
{{ packs[pack.key].tagline }}
+
+
+
+
+
+ {{ packs[pack.key].cta }}
+
+
+
+
+
+
+
{{ fr.pricing.legal }}
+
+
From e499bf03bdfc97c52afab74727faa91ceb5baebb Mon Sep 17 00:00:00 2001
From: tbeaumont79
Date: Thu, 2 Jul 2026 15:39:00 +0200
Subject: [PATCH 2/2] =?UTF-8?q?feat(billing):=20packs=20de=20cr=C3=A9dits?=
=?UTF-8?q?=20Stripe=20(ledger,=20webhook,=20gate=20g=C3=A9n=C3=A9ration)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Introduit le billing par packs de crédits one-shot (Stripe Checkout hébergé),
sans abonnement. Le gating de génération passe du quota mensuel aux crédits.
- Ledger append-only (credits.ts) : solde = somme des deltas, invariant
« jamais déficitaire », idempotence par idempotencyKey (FREE_GRANT unique,
PURCHASE = session id, GENERATION = -1 transactionnel).
- API billing : checkout (session hébergée, prix résolu par lookup_key),
webhook signé (crédite sur checkout.session.completed payé), summary (solde
+ octroi gratuit idempotent).
- Client Stripe serveur (stripe.ts) + seed idempotent (scripts/stripe-seed.ts)
créant produits/prix depuis config/pricing.ts.
- Schéma Prisma + migration : table credit_ledger, stripeCustomerId.
- Génération : gate solde >= 1 (consumeOneCredit), remplace le quota mensuel
generation ; l'UI candidature et la nav basculent sur le solde de crédits.
- Types partagés (@cvo/shared/billing) + tests (pricing, usage).
Co-Authored-By: Claude Opus 4.8 (1M context)
---
apps/app/.env.example | 13 +-
apps/app/config/pricing.ts | 22 ++-
apps/app/layouts/default.vue | 1 +
apps/app/package.json | 3 +
apps/app/pages/candidature.vue | 41 +++--
.../migration.sql | 28 ++++
apps/app/prisma/schema.prisma | 88 +++++++---
apps/app/scripts/stripe-seed.ts | 50 ++++++
apps/app/server/api/billing/checkout.post.ts | 83 ++++++++++
apps/app/server/api/billing/summary.get.ts | 13 ++
apps/app/server/api/billing/webhook.post.ts | 50 ++++++
.../server/api/candidature/generate.post.ts | 31 ++--
apps/app/server/api/usage/current.get.ts | 12 +-
apps/app/server/utils/credits.ts | 110 +++++++++++++
apps/app/server/utils/stripe.ts | 26 +++
apps/app/test/pricing.spec.ts | 46 ++++++
apps/app/test/usage.spec.ts | 10 +-
packages/shared/src/billing.ts | 34 ++++
packages/shared/src/index.ts | 3 +
packages/shared/src/usage.ts | 8 +-
pnpm-lock.yaml | 155 +++++++++++-------
21 files changed, 691 insertions(+), 136 deletions(-)
create mode 100644 apps/app/prisma/migrations/20260622084853_billing_credits/migration.sql
create mode 100644 apps/app/scripts/stripe-seed.ts
create mode 100644 apps/app/server/api/billing/checkout.post.ts
create mode 100644 apps/app/server/api/billing/summary.get.ts
create mode 100644 apps/app/server/api/billing/webhook.post.ts
create mode 100644 apps/app/server/utils/credits.ts
create mode 100644 apps/app/server/utils/stripe.ts
create mode 100644 apps/app/test/pricing.spec.ts
create mode 100644 packages/shared/src/billing.ts
diff --git a/apps/app/.env.example b/apps/app/.env.example
index f232577..830ccf1 100644
--- a/apps/app/.env.example
+++ b/apps/app/.env.example
@@ -14,10 +14,19 @@ APP_URL=http://localhost:3000
# (THI-124 / flux candidature). Sans cette clé, /api/candidature/* répond 502.
ANTHROPIC_API_KEY=sk-ant-changeme
-# Dev uniquement : si "true", désactive le quota de générations gratuites
-# (le modèle freemium reste codé ; à ne PAS activer en prod).
+# Dev uniquement : si "true", désactive le gate de crédits à la génération
+# (le modèle crédits reste codé ; à ne PAS activer en prod).
# DISABLE_GENERATION_LIMIT=true
+# Stripe — billing par packs de crédits (Checkout hébergé). Clés de TEST en dev.
+# STRIPE_SECRET_KEY : clé restreinte (rk_test_…) recommandée, ou sk_test_…. JAMAIS committée.
+# STRIPE_WEBHOOK_SECRET : donné par `stripe listen --forward-to localhost:3000/api/billing/webhook`.
+# STRIPE_PUBLISHABLE_KEY : publique (pk_test_…) — NON requise pour Checkout hébergé
+# (réservée à un futur Payment Element embarqué).
+STRIPE_SECRET_KEY=rk_test_changeme
+STRIPE_WEBHOOK_SECRET=whsec_changeme
+# STRIPE_PUBLISHABLE_KEY=pk_test_changeme
+
# SMTP (optionnel en dev — si absent, le magic-link est loggé dans la console).
# SMTP_HOST=smtp.example.fr
# SMTP_PORT=587
diff --git a/apps/app/config/pricing.ts b/apps/app/config/pricing.ts
index 76a8a62..da0e605 100644
--- a/apps/app/config/pricing.ts
+++ b/apps/app/config/pricing.ts
@@ -7,8 +7,8 @@
* scénario déficitaire (coût API ~0,30–0,40 € par génération, marge packs ~60 %).
*/
-/** Générations offertes à l'inscription (aligné sur FREE_TIER_QUOTAS.generation). */
-export const FREE_GENERATIONS = 2 as const
+/** Générations offertes À VIE pour tester (octroyées une fois via le ledger crédits). */
+export const FREE_GENERATIONS = 5 as const
export interface CreditPackConfig {
/** clé i18n du pack dans i18n/fr.ts > pricing.packs */
@@ -23,3 +23,21 @@ export const CREDIT_PACKS: readonly CreditPackConfig[] = [
{ key: 'starter', priceEur: 5, credits: 5, featured: false },
{ key: 'standard', priceEur: 12, credits: 15, featured: true },
]
+
+/**
+ * `lookup_key` Stripe d'un pack — clé stable partagée entre le seed (création des
+ * prix) et le checkout (résolution du prix sans stocker d'ID Stripe en base).
+ */
+export function packLookupKey(key: CreditPackConfig['key']): string {
+ return `teven_credits_${key}`
+}
+
+/** Résout un pack par sa clé (validation d'entrée d'API). `null` si inconnue. Pur. */
+export function resolveCreditPack(key: string): CreditPackConfig | null {
+ return CREDIT_PACKS.find((p) => p.key === key) ?? null
+}
+
+/** Montant en plus petite unité (centimes) pour Stripe. Pur. */
+export function packAmountCents(pack: CreditPackConfig): number {
+ return Math.round(pack.priceEur * 100)
+}
diff --git a/apps/app/layouts/default.vue b/apps/app/layouts/default.vue
index 74a2e9b..b27ae53 100644
--- a/apps/app/layouts/default.vue
+++ b/apps/app/layouts/default.vue
@@ -19,6 +19,7 @@ const navLinks = [
{ label: 'Nouvelle candidature', to: '/candidature' },
{ label: 'Mon profil', to: '/profil' },
{ label: 'Mes candidatures', to: '/candidatures' },
+ { label: 'Crédits', to: '/credits' },
] as const
function isActive(to: string) {
diff --git a/apps/app/package.json b/apps/app/package.json
index 7fcdada..dbbc264 100644
--- a/apps/app/package.json
+++ b/apps/app/package.json
@@ -7,6 +7,7 @@
"prisma:generate": "prisma generate",
"prisma:migrate:dev": "prisma migrate dev",
"prisma:migrate:deploy": "prisma migrate deploy",
+ "stripe:seed": "node --env-file=.env --import tsx scripts/stripe-seed.ts",
"postinstall": "nuxt prepare",
"dev": "prisma generate && nuxt dev",
"build": "prisma generate && nuxt build",
@@ -22,6 +23,7 @@
"better-auth": "^1.6.15",
"nodemailer": "^8.0.10",
"playwright-core": "^1.60.0",
+ "stripe": "^22.2.2",
"zod": "^4.4.3"
},
"devDependencies": {
@@ -32,6 +34,7 @@
"nuxt": "^3.15.0",
"prisma": "^6.2.1",
"tailwindcss": "^4.0.0",
+ "tsx": "^4.22.4",
"typescript": "^5.7.2",
"vitest": "^3.2.4",
"vue-tsc": "^2.2.0"
diff --git a/apps/app/pages/candidature.vue b/apps/app/pages/candidature.vue
index 39e95ef..099cc02 100644
--- a/apps/app/pages/candidature.vue
+++ b/apps/app/pages/candidature.vue
@@ -2,16 +2,16 @@
import type {
AnalyzeCandidatureRequest,
AnalyzeCandidatureResponse,
+ BillingSummary,
GenerateCandidatureRequest,
GenerateCandidatureResponse,
ProfileDTO,
- UsageSummary,
} from '@cvo/shared'
import {
+ BILLING_SUMMARY_PATH,
CANDIDATURE_ANALYZE_PATH,
CANDIDATURE_GENERATE_PATH,
QUOTA_EXCEEDED_CODE,
- USAGE_CURRENT_PATH,
matchVerdict,
} from '@cvo/shared'
import type { MatchVerdict } from '@cvo/shared'
@@ -32,22 +32,21 @@ const offerText = ref('')
const offerError = ref('')
const analysis = ref(null)
-// ─── Quota de générations (badge + gating des boutons) ───────────────────────
+// ─── Solde de crédits (badge + gating des boutons) ───────────────────────────
-const { data: usage, refresh: refreshUsage } = await useFetch(USAGE_CURRENT_PATH, {
- lazy: true,
-})
-
-/** `null` = illimité (ou usage pas encore chargé) → pas de badge, pas de blocage. */
-const generationRemaining = computed(
- () => usage.value?.quotas.find((q) => q.type === 'generation')?.remaining ?? null,
+const { data: billing, refresh: refreshUsage } = await useFetch(
+ BILLING_SUMMARY_PATH,
+ { lazy: true },
)
-const quotaExhausted = computed(() => generationRemaining.value === 0)
+
+/** `null` = solde pas encore chargé → pas de blocage prématuré. */
+const creditBalance = computed(() => billing.value?.creditBalance ?? null)
+const quotaExhausted = computed(() => creditBalance.value !== null && creditBalance.value < 1)
const quotaBadgeLabel = computed(() => {
- const r = generationRemaining.value
- if (r === null) return null
- return r > 1 ? `${r} générations offertes restantes` : `${r} génération offerte restante`
+ const c = creditBalance.value
+ if (c === null) return null
+ return c > 1 ? `${c} crédits restants` : `${c} crédit restant`
})
// ─── Garde profil : pas de génération sans matière première ──────────────────
@@ -168,7 +167,7 @@ async function generate() {
if (status === 403 || errorCode(err) === QUOTA_EXCEEDED_CODE) {
// Le refresh fait passer remaining à 0 → l'encart quota s'affiche seul.
await refreshUsage()
- toast.error('Quota atteint', 'Tu as épuisé tes générations offertes.')
+ toast.error('Plus de crédits', 'Achète un pack pour continuer à générer.')
} else if (status === 422) {
toast.error('Génération impossible', errorMessage(err, 'Réessaie dans quelques instants.'))
} else if (status === 409) {
@@ -363,20 +362,20 @@ watch(step, async (s) => {
-
+
- Tu as utilisé toutes tes générations offertes ce mois-ci.
+ Tu n'as plus de crédits. Achète un pack pour continuer à générer.
-
- Recharger en crédits
-
+ Acheter des crédits
+
diff --git a/apps/app/prisma/migrations/20260622084853_billing_credits/migration.sql b/apps/app/prisma/migrations/20260622084853_billing_credits/migration.sql
new file mode 100644
index 0000000..86adf07
--- /dev/null
+++ b/apps/app/prisma/migrations/20260622084853_billing_credits/migration.sql
@@ -0,0 +1,28 @@
+-- CreateEnum
+CREATE TYPE "CreditReason" AS ENUM ('FREE_GRANT', 'PURCHASE', 'GENERATION', 'ADJUSTMENT');
+
+-- AlterTable
+ALTER TABLE "users" ADD COLUMN "stripeCustomerId" TEXT;
+
+-- CreateTable
+CREATE TABLE "credit_ledger" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "delta" INTEGER NOT NULL,
+ "reason" "CreditReason" NOT NULL,
+ "idempotencyKey" TEXT,
+ "packKey" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "credit_ledger_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "credit_ledger_userId_idx" ON "credit_ledger"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "credit_ledger_idempotencyKey_key" ON "credit_ledger"("idempotencyKey");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "users_stripeCustomerId_key" ON "users"("stripeCustomerId");
+
diff --git a/apps/app/prisma/schema.prisma b/apps/app/prisma/schema.prisma
index 7cb3942..348954b 100644
--- a/apps/app/prisma/schema.prisma
+++ b/apps/app/prisma/schema.prisma
@@ -73,15 +73,17 @@ enum LanguageLevel {
// Compte utilisateur — ancre d'authentification (le WS Auth y greffera Better
// Auth + ses tables satellites). Données auth hébergées en FR/UE (RGPD).
model User {
- id String @id @default(uuid())
- email String @unique
+ id String @id @default(uuid())
+ email String @unique
// Champs compatibles Better Auth (renseignés par le WS Auth) :
- emailVerified Boolean @default(false)
- name String?
- image String?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- deletedAt DateTime? // soft-delete RGPD (droit à l'effacement)
+ emailVerified Boolean @default(false)
+ name String?
+ image String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime? // soft-delete RGPD (droit à l'effacement)
+ // Client Stripe (billing crédits) — créé à la 1re intention d'achat. Opaque côté app.
+ stripeCustomerId String? @unique
profile Profile?
sessions Session[]
@@ -93,25 +95,25 @@ model User {
// Profil candidat — 1-1 avec User. En-tête + résumé libres.
model Profile {
- id String @id @default(uuid())
- userId String @unique
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ id String @id @default(uuid())
+ userId String @unique
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Identité affichée en tête de CV — FACTUELLE, jamais reformulée par le moteur
// (cf. @cvo/shared cv.ts). Injectée déterministe-ment dans le header généré.
- fullName String? // nom complet du candidat (ex. « Camille Martin »)
- email String? // email de contact affiché (défaut : email du compte)
- phone String? // téléphone de contact
- location String? // localisation (ex. « Paris, France »)
- links String[] // liens (portfolio, LinkedIn, GitHub…)
- keySkills String[] // compétences clés : phrases (verbe d'action), section au-dessus des expériences
- headline String? // intitulé court (ex. « Développeur Full-Stack »)
- summary String? // résumé / accroche
+ fullName String? // nom complet du candidat (ex. « Camille Martin »)
+ email String? // email de contact affiché (défaut : email du compte)
+ phone String? // téléphone de contact
+ location String? // localisation (ex. « Paris, France »)
+ links String[] // liens (portfolio, LinkedIn, GitHub…)
+ keySkills String[] // compétences clés : phrases (verbe d'action), section au-dessus des expériences
+ headline String? // intitulé court (ex. « Développeur Full-Stack »)
+ summary String? // résumé / accroche
// Thème « CV de base » capturé depuis un PDF (style uniquement, jamais de
// contenu) : { css, googleFont, summary }. Le PDF source n'est JAMAIS stocké.
baseCvDesign Json?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- deletedAt DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime?
experiences Experience[]
skills Skill[]
@@ -349,3 +351,45 @@ model Verification {
// Back-relations Better Auth sur User (ajoutées ici pour éviter de modifier
// le bloc User déjà posé dans le data model slice 1).
// Prisma les génère via la relation inverse côté Session/Account.
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Billing — crédits (Stripe Checkout, packs one-shot) · voir config/pricing.ts
+//
+// Modèle : grand livre (ledger) APPEND-ONLY. Le solde = somme des `delta`. Aucune
+// écriture directe d'un solde : chaque octroi (gratuit/achat) ou consommation est
+// une ligne, ce qui garantit l'audit et l'invariant « jamais déficitaire ».
+// FREE_GRANT : crédits offerts à l'inscription (1 seule ligne/user, idempotent).
+// PURCHASE : achat d'un pack (webhook Stripe ; idempotent par session id).
+// GENERATION : consommation d'1 crédit à la génération d'un CV (delta négatif).
+// ADJUSTMENT : correction manuelle (SAV) — trace explicite.
+//
+// RGPD : aucune donnée carte ici (gérée par Stripe). On ne stocke qu'un id de
+// session/customer Stripe opaque pour l'idempotence et le rapprochement.
+// ─────────────────────────────────────────────────────────────────────────────
+
+enum CreditReason {
+ FREE_GRANT
+ PURCHASE
+ GENERATION
+ ADJUSTMENT
+}
+
+model CreditLedger {
+ id String @id @default(uuid())
+ userId String
+ // Variation de solde : +N (octroi/achat) ou -1 (génération). Jamais 0.
+ delta Int
+ reason CreditReason
+ // Clé d'idempotence. PURCHASE : id de session Stripe (1 crédit/paiement).
+ // FREE_GRANT : sentinelle `free:` (1 octroi gratuit/utilisateur).
+ // GENERATION / ADJUSTMENT : null (plusieurs lignes — NULLs distincts en Postgres).
+ idempotencyKey String?
+ // Nombre de crédits du pack acheté (PURCHASE) — info d'affichage/audit.
+ packKey String?
+ createdAt DateTime @default(now())
+
+ // Un même paiement OU un même octroi gratuit ne s'applique qu'une fois.
+ @@unique([idempotencyKey])
+ @@index([userId])
+ @@map("credit_ledger")
+}
diff --git a/apps/app/scripts/stripe-seed.ts b/apps/app/scripts/stripe-seed.ts
new file mode 100644
index 0000000..d56ad8c
--- /dev/null
+++ b/apps/app/scripts/stripe-seed.ts
@@ -0,0 +1,50 @@
+/**
+ * Seed Stripe idempotent — crée les produits + prix des packs de crédits depuis
+ * `config/pricing.ts`. Source de vérité = le code (reproductible test → prod).
+ *
+ * Idempotence : on cherche le prix par `lookup_key` ; s'il existe, on passe. Le
+ * checkout résout ensuite le prix par cette même clé (aucun ID Stripe en base).
+ *
+ * Lancement (depuis apps/app, .env chargé) :
+ * node --env-file=.env --import tsx scripts/stripe-seed.ts
+ * (ou `pnpm --filter @cvo/app stripe:seed`)
+ */
+import Stripe from 'stripe'
+import { CREDIT_PACKS, packLookupKey, packAmountCents } from '../config/pricing'
+
+async function main() {
+ const key = process.env.STRIPE_SECRET_KEY
+ if (!key || key.includes('changeme')) {
+ throw new Error('STRIPE_SECRET_KEY manquante : renseigne apps/app/.env (clé de TEST).')
+ }
+ const stripe = new Stripe(key)
+
+ for (const pack of CREDIT_PACKS) {
+ const lookupKey = packLookupKey(pack.key)
+ const existing = await stripe.prices.list({ lookup_keys: [lookupKey], active: true, limit: 1 })
+ if (existing.data.length > 0) {
+ console.log(`= pack "${pack.key}" : prix déjà présent (${lookupKey}) — ignoré`)
+ continue
+ }
+
+ const product = await stripe.products.create({
+ name: `Teven — ${pack.credits} crédits`,
+ description: `${pack.credits} générations de CV optimisé`,
+ metadata: { packKey: pack.key, credits: String(pack.credits) },
+ })
+ const price = await stripe.prices.create({
+ product: product.id,
+ currency: 'eur',
+ unit_amount: packAmountCents(pack),
+ lookup_key: lookupKey,
+ metadata: { packKey: pack.key, credits: String(pack.credits) },
+ })
+ console.log(`+ pack "${pack.key}" : ${pack.priceEur}€ / ${pack.credits} crédits (price ${price.id})`)
+ }
+ console.log('Seed Stripe terminé.')
+}
+
+main().catch((err) => {
+ console.error(err instanceof Error ? err.message : err)
+ process.exit(1)
+})
diff --git a/apps/app/server/api/billing/checkout.post.ts b/apps/app/server/api/billing/checkout.post.ts
new file mode 100644
index 0000000..1721503
--- /dev/null
+++ b/apps/app/server/api/billing/checkout.post.ts
@@ -0,0 +1,83 @@
+/**
+ * POST /api/billing/checkout — démarre l'achat d'un pack de crédits.
+ *
+ * Crée (ou réutilise) le client Stripe de l'utilisateur, puis une session
+ * Checkout hébergée (mode `payment`, one-shot). Le prix est résolu par `lookup_key`
+ * (posé par le seed) → aucun ID Stripe stocké en base. Les crédits ne sont accordés
+ * qu'au webhook `checkout.session.completed` (paiement confirmé), jamais ici.
+ *
+ * Sécurité : montant/crédits déterminés SERVER-SIDE depuis le catalogue (le client
+ * n'envoie qu'une clé de pack) — pas de prix piloté par le client.
+ */
+import { z } from 'zod'
+import type { CreateCheckoutResponse } from '@cvo/shared'
+import { requireUserId } from '../../utils/session'
+import { prisma } from '../../utils/prisma'
+import { getStripe, StripeNotConfiguredError } from '../../utils/stripe'
+import { resolveCreditPack, packLookupKey } from '../../../config/pricing'
+
+const bodySchema = z.object({ packKey: z.string() })
+
+export default defineEventHandler(async (event): Promise => {
+ const userId = requireUserId(event)
+
+ const parsed = bodySchema.safeParse(await readBody(event))
+ if (!parsed.success) throw createError({ statusCode: 400, message: 'Corps invalide : { packKey } attendu.' })
+
+ // Catalogue = source de vérité serveur (prix/crédits jamais pilotés par le client).
+ const pack = resolveCreditPack(parsed.data.packKey)
+ if (!pack) throw createError({ statusCode: 400, message: 'Pack inconnu.' })
+
+ try {
+ const stripe = getStripe()
+
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: { email: true, name: true, stripeCustomerId: true },
+ })
+ if (!user) throw createError({ statusCode: 401, message: 'Non authentifié.' })
+
+ // Client Stripe : créé à la première intention d'achat, puis mémorisé.
+ let customerId = user.stripeCustomerId
+ if (!customerId) {
+ const customer = await stripe.customers.create({
+ email: user.email,
+ name: user.name ?? undefined,
+ metadata: { userId },
+ })
+ customerId = customer.id
+ await prisma.user.update({ where: { id: userId }, data: { stripeCustomerId: customerId } })
+ }
+
+ // Prix résolu par lookup_key (posé par le seed). Absent ⇒ seed non exécuté.
+ const prices = await stripe.prices.list({
+ lookup_keys: [packLookupKey(pack.key)],
+ active: true,
+ limit: 1,
+ })
+ const price = prices.data[0]
+ if (!price) {
+ throw createError({ statusCode: 503, message: 'Catalogue Stripe non initialisé (lance le seed).' })
+ }
+
+ const origin = process.env.APP_URL || getRequestURL(event).origin
+ const session = await stripe.checkout.sessions.create({
+ mode: 'payment',
+ customer: customerId,
+ line_items: [{ price: price.id, quantity: 1 }],
+ success_url: `${origin}/credits?status=success`,
+ cancel_url: `${origin}/credits?status=cancel`,
+ // Métadonnées relues par le webhook pour créditer le bon compte.
+ metadata: { userId, packKey: pack.key, credits: String(pack.credits) },
+ payment_intent_data: { metadata: { userId, packKey: pack.key, credits: String(pack.credits) } },
+ })
+
+ if (!session.url) throw createError({ statusCode: 502, message: 'Session Checkout sans URL.' })
+ return { url: session.url }
+ } catch (err) {
+ if (err instanceof StripeNotConfiguredError) {
+ throw createError({ statusCode: 503, message: 'Paiement indisponible (Stripe non configuré).' })
+ }
+ throw err
+ }
+})
diff --git a/apps/app/server/api/billing/summary.get.ts b/apps/app/server/api/billing/summary.get.ts
new file mode 100644
index 0000000..82d9745
--- /dev/null
+++ b/apps/app/server/api/billing/summary.get.ts
@@ -0,0 +1,13 @@
+/**
+ * GET /api/billing/summary — solde de crédits de l'utilisateur connecté.
+ * Octroie les crédits gratuits au premier accès (idempotent).
+ */
+import type { BillingSummary } from '@cvo/shared'
+import { requireUserId } from '../../utils/session'
+import { getOrInitBalance } from '../../utils/credits'
+
+export default defineEventHandler(async (event): Promise => {
+ const userId = requireUserId(event)
+ const creditBalance = await getOrInitBalance(userId)
+ return { creditBalance }
+})
diff --git a/apps/app/server/api/billing/webhook.post.ts b/apps/app/server/api/billing/webhook.post.ts
new file mode 100644
index 0000000..f41d3e3
--- /dev/null
+++ b/apps/app/server/api/billing/webhook.post.ts
@@ -0,0 +1,50 @@
+/**
+ * POST /api/billing/webhook — réception des événements Stripe.
+ *
+ * Sécurité : la signature est vérifiée avec `STRIPE_WEBHOOK_SECRET` sur le corps
+ * BRUT (readRawBody) — un webhook non signé/altéré est rejeté (400). Idempotent :
+ * créditer un achat repose sur la contrainte unique du ledger (rejeu sans effet).
+ *
+ * On ne crédite QUE sur `checkout.session.completed` payé : les crédits proviennent
+ * d'un paiement confirmé, jamais d'un appel client.
+ */
+import type Stripe from 'stripe'
+import { getStripe, StripeNotConfiguredError } from '../../utils/stripe'
+import { grantPurchasedCredits } from '../../utils/credits'
+
+export default defineEventHandler(async (event) => {
+ const secret = process.env.STRIPE_WEBHOOK_SECRET
+ if (!secret || secret.includes('changeme')) {
+ throw createError({ statusCode: 503, message: 'Webhook Stripe non configuré.' })
+ }
+
+ const signature = getHeader(event, 'stripe-signature')
+ const raw = await readRawBody(event)
+ if (!signature || !raw) throw createError({ statusCode: 400, message: 'Signature ou corps manquant.' })
+
+ let stripeEvent: Stripe.Event
+ try {
+ stripeEvent = getStripe().webhooks.constructEvent(raw, signature, secret)
+ } catch (err) {
+ if (err instanceof StripeNotConfiguredError) {
+ throw createError({ statusCode: 503, message: 'Webhook Stripe non configuré.' })
+ }
+ // Signature invalide ⇒ requête non authentique (ou corps altéré).
+ throw createError({ statusCode: 400, message: 'Signature de webhook invalide.' })
+ }
+
+ if (stripeEvent.type === 'checkout.session.completed') {
+ const session = stripeEvent.data.object as Stripe.Checkout.Session
+ if (session.payment_status === 'paid') {
+ const userId = session.metadata?.userId
+ const packKey = session.metadata?.packKey ?? ''
+ const credits = Number(session.metadata?.credits)
+ if (userId && Number.isFinite(credits) && credits > 0) {
+ // Idempotent : un rejeu du même paiement ne crédite pas deux fois.
+ await grantPurchasedCredits({ userId, sessionId: session.id, packKey, credits })
+ }
+ }
+ }
+
+ return { received: true }
+})
diff --git a/apps/app/server/api/candidature/generate.post.ts b/apps/app/server/api/candidature/generate.post.ts
index 6b97eb8..f21961d 100644
--- a/apps/app/server/api/candidature/generate.post.ts
+++ b/apps/app/server/api/candidature/generate.post.ts
@@ -12,7 +12,6 @@
import { z } from 'zod'
import { Prisma } from '@prisma/client'
import {
- FREE_TIER_QUOTAS,
ProvenanceError,
QUOTA_EXCEEDED_CODE,
type AnalyzedOffer,
@@ -24,7 +23,8 @@ import {
import { requireUserId } from '../../utils/session'
import { prisma } from '../../utils/prisma'
import { NOT_DELETED, toProfileDTO } from '../../utils/profile-serialize'
-import { isUsageAllowed, recordUsageEvent } from '../../utils/metering'
+import { recordUsageEvent } from '../../utils/metering'
+import { getOrInitBalance, consumeOneCredit } from '../../utils/credits'
import { anthropicComplete, LlmError, type LlmComplete } from '../../utils/anthropic'
import { matchProfileToOffer } from '../../services/matching'
import { adaptKeySkills } from '../../services/keyskills-adapt'
@@ -58,18 +58,19 @@ export default defineEventHandler(async (event): Promise => {
const userId = requireUserId(event)
- // Cohérent avec le gate de /generate : si la limite est désactivée (dev), la
- // génération est reportée comme illimitée (l'UI ne grise plus le bouton).
- const quotas: UsageQuotas =
- process.env.DISABLE_GENERATION_LIMIT === 'true'
- ? { ...FREE_TIER_QUOTAS, generation: undefined }
- : FREE_TIER_QUOTAS
- return readUsageSummary(prisma, userId, quotas, new Date())
+ return readUsageSummary(prisma, userId, FREE_TIER_QUOTAS, new Date())
})
diff --git a/apps/app/server/utils/credits.ts b/apps/app/server/utils/credits.ts
new file mode 100644
index 0000000..7a11f68
--- /dev/null
+++ b/apps/app/server/utils/credits.ts
@@ -0,0 +1,110 @@
+/**
+ * Service crédits — grand livre (ledger) append-only. Le solde = somme des `delta`.
+ *
+ * Invariant produit « jamais déficitaire » : aucune écriture directe du solde ;
+ * chaque octroi (gratuit/achat) ou consommation est une ligne. L'idempotence est
+ * portée par `idempotencyKey` (unique) :
+ * - FREE_GRANT : `free:` → 1 octroi gratuit par utilisateur ;
+ * - PURCHASE : id de session Stripe → 1 crédit par paiement ;
+ * - GENERATION : null → plusieurs consommations autorisées.
+ *
+ * RGPD : aucune donnée carte ; on ne stocke qu'un id Stripe opaque.
+ */
+import { Prisma, type PrismaClient } from '@prisma/client'
+import { FREE_GENERATIONS } from '../../config/pricing'
+import { prisma as defaultPrisma } from './prisma'
+
+/** Client Prisma ou client de transaction (les deux exposent `creditLedger`). */
+type Db = PrismaClient | Prisma.TransactionClient
+
+/** Erreur métier : solde insuffisant pour consommer un crédit. */
+export class InsufficientCreditsError extends Error {
+ constructor() {
+ super('Solde de crédits insuffisant')
+ this.name = 'InsufficientCreditsError'
+ }
+}
+
+/** Clé d'idempotence de l'octroi gratuit (1 par utilisateur). Pur. */
+export function freeGrantKey(userId: string): string {
+ return `free:${userId}`
+}
+
+/** Solde courant = somme des deltas du ledger (≥ 0 par construction). */
+export async function getCreditBalance(db: Db, userId: string): Promise {
+ const agg = await db.creditLedger.aggregate({ where: { userId }, _sum: { delta: true } })
+ return agg._sum.delta ?? 0
+}
+
+/**
+ * Octroie les crédits gratuits une seule fois par utilisateur (idempotent via la
+ * contrainte unique sur `idempotencyKey`). No-op si déjà octroyés.
+ */
+export async function ensureFreeGrant(db: Db, userId: string): Promise {
+ if (FREE_GENERATIONS <= 0) return
+ try {
+ await db.creditLedger.create({
+ data: {
+ userId,
+ delta: FREE_GENERATIONS,
+ reason: 'FREE_GRANT',
+ idempotencyKey: freeGrantKey(userId),
+ },
+ })
+ } catch (err) {
+ // P2002 = violation d'unicité ⇒ octroi déjà effectué : on ignore.
+ if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002') return
+ throw err
+ }
+}
+
+/** Octroi gratuit (si besoin) puis lecture du solde. */
+export async function getOrInitBalance(userId: string, db: Db = defaultPrisma): Promise {
+ await ensureFreeGrant(db, userId)
+ return getCreditBalance(db, userId)
+}
+
+/**
+ * Consomme 1 crédit pour une génération RÉUSSIE, de façon atomique et sûre :
+ * dans une transaction, on relit le solde puis on insère la ligne -1 seulement
+ * s'il reste ≥ 1 crédit. Lève `InsufficientCreditsError` sinon (jamais déficitaire).
+ */
+export async function consumeOneCredit(userId: string, db: PrismaClient = defaultPrisma): Promise {
+ // Octroi gratuit AVANT la transaction : il s'appuie sur un catch de violation
+ // d'unicité (P2002), or une erreur SQL dans une transaction Postgres l'avorte
+ // entièrement (25P02). On le fait donc hors transaction (idempotent, sans risque).
+ await ensureFreeGrant(db, userId)
+ return db.$transaction(async (tx) => {
+ const balance = await getCreditBalance(tx, userId)
+ if (balance < 1) throw new InsufficientCreditsError()
+ await tx.creditLedger.create({
+ data: { userId, delta: -1, reason: 'GENERATION' },
+ })
+ return balance - 1
+ })
+}
+
+/**
+ * Crédite un achat (webhook Stripe), idempotent par id de session. Renvoie `true`
+ * si l'achat a été appliqué, `false` s'il l'avait déjà été (rejeu de webhook).
+ */
+export async function grantPurchasedCredits(
+ params: { userId: string; sessionId: string; packKey: string; credits: number },
+ db: Db = defaultPrisma,
+): Promise {
+ try {
+ await db.creditLedger.create({
+ data: {
+ userId: params.userId,
+ delta: params.credits,
+ reason: 'PURCHASE',
+ idempotencyKey: params.sessionId,
+ packKey: params.packKey,
+ },
+ })
+ return true
+ } catch (err) {
+ if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002') return false
+ throw err
+ }
+}
diff --git a/apps/app/server/utils/stripe.ts b/apps/app/server/utils/stripe.ts
new file mode 100644
index 0000000..1e4185e
--- /dev/null
+++ b/apps/app/server/utils/stripe.ts
@@ -0,0 +1,26 @@
+/**
+ * Client Stripe serveur (singleton paresseux). La clé vient de l'env
+ * `STRIPE_SECRET_KEY` (restreinte `rk_` recommandée) — JAMAIS en dur, jamais loggée.
+ *
+ * On n'épingle pas `apiVersion` explicitement : le SDK utilise sa version intégrée,
+ * et la version par défaut du compte gouverne — suffisant pour Checkout + webhooks.
+ */
+import Stripe from 'stripe'
+
+let client: Stripe | null = null
+
+/** Erreur de configuration (clé absente) — distincte des erreurs Stripe runtime. */
+export class StripeNotConfiguredError extends Error {
+ constructor() {
+ super('STRIPE_SECRET_KEY manquante')
+ this.name = 'StripeNotConfiguredError'
+ }
+}
+
+/** Retourne le client Stripe, ou lève `StripeNotConfiguredError` si non configuré. */
+export function getStripe(): Stripe {
+ const key = process.env.STRIPE_SECRET_KEY
+ if (!key || key.includes('changeme')) throw new StripeNotConfiguredError()
+ if (!client) client = new Stripe(key)
+ return client
+}
diff --git a/apps/app/test/pricing.spec.ts b/apps/app/test/pricing.spec.ts
new file mode 100644
index 0000000..3651f1b
--- /dev/null
+++ b/apps/app/test/pricing.spec.ts
@@ -0,0 +1,46 @@
+import { describe, it, expect } from 'vitest'
+import {
+ CREDIT_PACKS,
+ FREE_GENERATIONS,
+ packLookupKey,
+ resolveCreditPack,
+ packAmountCents,
+} from '../config/pricing'
+import { freeGrantKey } from '../server/utils/credits'
+
+describe('pricing — catalogue de packs', () => {
+ it('packs attendus (starter 5€/5, standard 12€/15)', () => {
+ expect(CREDIT_PACKS.map((p) => [p.key, p.priceEur, p.credits])).toEqual([
+ ['starter', 5, 5],
+ ['standard', 12, 15],
+ ])
+ })
+
+ it('FREE_GENERATIONS = 5 (offertes à vie)', () => {
+ expect(FREE_GENERATIONS).toBe(5)
+ })
+
+ it('resolveCreditPack : clé connue → pack, inconnue → null', () => {
+ expect(resolveCreditPack('standard')?.credits).toBe(15)
+ expect(resolveCreditPack('starter')?.priceEur).toBe(5)
+ expect(resolveCreditPack('inconnu')).toBeNull()
+ expect(resolveCreditPack('')).toBeNull()
+ })
+
+ it('packAmountCents : euros → centimes entiers', () => {
+ expect(packAmountCents({ key: 'starter', priceEur: 5, credits: 5, featured: false })).toBe(500)
+ expect(packAmountCents({ key: 'standard', priceEur: 12, credits: 15, featured: true })).toBe(1200)
+ })
+
+ it('packLookupKey : clé Stripe stable et préfixée', () => {
+ expect(packLookupKey('starter')).toBe('teven_credits_starter')
+ expect(packLookupKey('standard')).toBe('teven_credits_standard')
+ })
+})
+
+describe('credits — clé d’idempotence de l’octroi gratuit', () => {
+ it('freeGrantKey : 1 clé déterministe par utilisateur', () => {
+ expect(freeGrantKey('user-123')).toBe('free:user-123')
+ expect(freeGrantKey('abc')).not.toBe(freeGrantKey('abd'))
+ })
+})
diff --git a/apps/app/test/usage.spec.ts b/apps/app/test/usage.spec.ts
index bf082a1..f1a7810 100644
--- a/apps/app/test/usage.spec.ts
+++ b/apps/app/test/usage.spec.ts
@@ -74,13 +74,19 @@ describe('buildUsageSummary', () => {
it('assemble période + compteur + un quota par type', () => {
const summary = buildUsageSummary(
'202606',
- { ...EMPTY_USAGE_SNAPSHOT, generation: 5 },
+ { ...EMPTY_USAGE_SNAPSHOT, generation: 5, export_pdf: 4 },
FREE_TIER_QUOTAS,
)
expect(summary.period).toBe('202606')
expect(summary.counter.generation).toBe(5)
expect(summary.quotas.map((q) => q.type)).toEqual(['generation', 'export_pdf', 'extraction'])
+ // La génération n'est plus bornée par un quota mensuel (gouvernée par les crédits).
const gen = summary.quotas.find((q) => q.type === 'generation')!
- expect(gen.exceeded).toBe(true) // 5 utilisées ≥ quota gratuit (2)
+ expect(gen.limit).toBeNull()
+ expect(gen.exceeded).toBe(false)
+ // L'export PDF reste borné par période (4) → 4 utilisés = atteint.
+ const pdf = summary.quotas.find((q) => q.type === 'export_pdf')!
+ expect(pdf.limit).toBe(4)
+ expect(pdf.exceeded).toBe(true)
})
})
diff --git a/packages/shared/src/billing.ts b/packages/shared/src/billing.ts
new file mode 100644
index 0000000..c3d3783
--- /dev/null
+++ b/packages/shared/src/billing.ts
@@ -0,0 +1,34 @@
+/**
+ * Billing — contrats partagés client ↔ serveur (Nitro).
+ *
+ * Modèle : packs de crédits one-shot (Stripe Checkout hébergé). 1 crédit = 1
+ * génération de CV. Le catalogue des packs (prix/crédits) vit dans
+ * `apps/app/config/pricing.ts` (source de vérité serveur+client de l'app) ; ce
+ * module ne porte que les CONTRATS d'API neutres réutilisables des deux côtés.
+ *
+ * RGPD : aucune donnée carte ne transite par notre API (gérée par Stripe).
+ */
+
+/** Clé d'un pack de crédits (miroir de CreditPackConfig.key côté app). */
+export type CreditPackKey = 'starter' | 'standard'
+
+/** Réponse de `GET /api/billing/summary` — solde de crédits de l'utilisateur. */
+export interface BillingSummary {
+ /** Crédits disponibles (générations restantes). Jamais négatif. */
+ creditBalance: number
+}
+
+/** Corps de `POST /api/billing/checkout` — démarre l'achat d'un pack. */
+export interface CreateCheckoutRequest {
+ packKey: CreditPackKey
+}
+
+/** Réponse de `POST /api/billing/checkout` — URL de la page Stripe hébergée. */
+export interface CreateCheckoutResponse {
+ /** URL Stripe Checkout vers laquelle rediriger le navigateur. */
+ url: string
+}
+
+export const BILLING_SUMMARY_PATH = '/api/billing/summary' as const
+export const BILLING_CHECKOUT_PATH = '/api/billing/checkout' as const
+export const BILLING_WEBHOOK_PATH = '/api/billing/webhook' as const
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index ff6571d..ce4c6ee 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -39,3 +39,6 @@ export * from './match.js'
// Candidature persistée (offre + CV éditable + suivi).
export * from './candidature.js'
+
+// Billing — contrats d'achat de crédits (Stripe Checkout).
+export * from './billing.js'
diff --git a/packages/shared/src/usage.ts b/packages/shared/src/usage.ts
index 3bfa86e..3556163 100644
--- a/packages/shared/src/usage.ts
+++ b/packages/shared/src/usage.ts
@@ -59,12 +59,12 @@ export const EMPTY_USAGE_SNAPSHOT: UsageCounterSnapshot = {
export type UsageQuotas = Partial>
/**
- * Palier gratuit — modèle acté (2026-06) : 2 générations offertes pour tester,
- * ensuite achat de crédits (config/pricing.ts). Les exports PDF sont plus larges
- * (ré-export d'un CV déjà généré) mais bornés pour éviter l'abus de Chromium.
+ * Quotas par période. La GÉNÉRATION n'est plus un quota mensuel : elle est
+ * gouvernée par le solde de CRÉDITS (générations offertes + packs achetés, cf.
+ * billing/credits). Restent ici les bornes par période non monétisées : l'export
+ * PDF (ré-export d'un CV déjà généré) borné pour éviter l'abus de Chromium.
*/
export const FREE_TIER_QUOTAS: UsageQuotas = {
- generation: 2,
export_pdf: 4,
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cb23ad9..94b4861 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -43,23 +43,26 @@ importers:
version: 6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3)
better-auth:
specifier: ^1.6.15
- version: 1.6.15(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(vitest@3.2.6(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))
+ version: 1.6.15(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(vitest@3.2.6(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))
nodemailer:
specifier: ^8.0.10
version: 8.0.10
playwright-core:
specifier: ^1.60.0
version: 1.60.0
+ stripe:
+ specifier: ^22.2.2
+ version: 22.2.2(@types/node@22.19.20)
zod:
specifier: ^4.4.3
version: 4.4.3
devDependencies:
'@nuxt/fonts':
specifier: ^0.14.0
- version: 0.14.0(db0@0.3.4)(ioredis@5.11.1)(magicast@0.5.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))
+ version: 0.14.0(db0@0.3.4)(ioredis@5.11.1)(magicast@0.5.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))
'@tailwindcss/vite':
specifier: ^4.0.0
- version: 4.3.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))
+ version: 4.3.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))
'@types/node':
specifier: ^22.10.5
version: 22.19.20
@@ -68,19 +71,22 @@ importers:
version: 8.0.0
nuxt:
specifier: ^3.15.0
- version: 3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0)
+ version: 3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(tsx@4.22.4)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0)
prisma:
specifier: ^6.2.1
version: 6.19.3(magicast@0.5.3)(typescript@5.9.3)
tailwindcss:
specifier: ^4.0.0
version: 4.3.0
+ tsx:
+ specifier: ^4.22.4
+ version: 4.22.4
typescript:
specifier: ^5.7.2
version: 5.9.3
vitest:
specifier: ^3.2.4
- version: 3.2.6(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ version: 3.2.6(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
vue-tsc:
specifier: ^2.2.0
version: 2.2.12(typescript@5.9.3)
@@ -92,7 +98,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.2.4
- version: 3.2.6(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ version: 3.2.6(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
packages:
@@ -3984,6 +3990,15 @@ packages:
strip-literal@3.1.0:
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
+ stripe@22.2.2:
+ resolution: {integrity: sha512-lRXLiarjW/I2QVa6QuFfz1Ma7Dc2yBQ7vlYH6NEmp5htMJNQGfiWExmOv0McfqY5wMCGV8DLuCvsyVRIPJlUUQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
structured-clone-es@2.0.0:
resolution: {integrity: sha512-5UuAHmBLXYPCl22xWJrFuGmIhBKQzxISPVz6E7nmTmTcAOpUzlbjKJsRrCE4vADmMQ0dzeCnlWn9XufnAGf76Q==}
@@ -4099,6 +4114,11 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+ tsx@4.22.4:
+ resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -5179,11 +5199,11 @@ snapshots:
'@nuxt/devalue@2.0.2': {}
- '@nuxt/devtools-kit@3.2.4(magicast@0.5.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))':
+ '@nuxt/devtools-kit@3.2.4(magicast@0.5.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))':
dependencies:
'@nuxt/kit': 4.4.8(magicast@0.5.3)
execa: 8.0.1
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
transitivePeerDependencies:
- magicast
@@ -5198,9 +5218,9 @@ snapshots:
pkg-types: 2.3.1
semver: 7.8.3
- '@nuxt/devtools@3.2.4(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))':
+ '@nuxt/devtools@3.2.4(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))':
dependencies:
- '@nuxt/devtools-kit': 3.2.4(magicast@0.5.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))
+ '@nuxt/devtools-kit': 3.2.4(magicast@0.5.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))
'@nuxt/devtools-wizard': 3.2.4
'@nuxt/kit': 4.4.8(magicast@0.5.3)
'@vue/devtools-core': 8.1.2(vue@3.5.35(typescript@5.9.3))
@@ -5228,9 +5248,9 @@ snapshots:
sirv: 3.0.2
structured-clone-es: 2.0.0
tinyglobby: 0.2.17
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
- vite-plugin-inspect: 11.4.1(@nuxt/kit@4.4.8(magicast@0.5.3))(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))
- vite-plugin-vue-tracer: 1.4.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
+ vite-plugin-inspect: 11.4.1(@nuxt/kit@4.4.8(magicast@0.5.3))(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))
+ vite-plugin-vue-tracer: 1.4.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))
which: 6.0.1
ws: 8.21.0
transitivePeerDependencies:
@@ -5239,13 +5259,13 @@ snapshots:
- utf-8-validate
- vue
- '@nuxt/fonts@0.14.0(db0@0.3.4)(ioredis@5.11.1)(magicast@0.5.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))':
+ '@nuxt/fonts@0.14.0(db0@0.3.4)(ioredis@5.11.1)(magicast@0.5.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))':
dependencies:
- '@nuxt/devtools-kit': 3.2.4(magicast@0.5.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))
+ '@nuxt/devtools-kit': 3.2.4(magicast@0.5.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))
'@nuxt/kit': 4.4.8(magicast@0.5.3)
consola: 3.4.2
defu: 6.1.7
- fontless: 0.2.1(db0@0.3.4)(ioredis@5.11.1)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))
+ fontless: 0.2.1(db0@0.3.4)(ioredis@5.11.1)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))
h3: 1.15.11
magic-regexp: 0.10.0
ofetch: 1.5.1
@@ -5330,7 +5350,7 @@ snapshots:
transitivePeerDependencies:
- magicast
- '@nuxt/nitro-server@3.21.8(db0@0.3.4)(ioredis@5.11.1)(magicast@0.5.3)(nuxt@3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0))(oxc-parser@0.132.0)(srvx@0.11.16)(typescript@5.9.3)':
+ '@nuxt/nitro-server@3.21.8(db0@0.3.4)(ioredis@5.11.1)(magicast@0.5.3)(nuxt@3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(tsx@4.22.4)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0))(oxc-parser@0.132.0)(srvx@0.11.16)(typescript@5.9.3)':
dependencies:
'@nuxt/devalue': 2.0.2
'@nuxt/kit': 3.21.8(magicast@0.5.3)
@@ -5348,7 +5368,7 @@ snapshots:
klona: 2.0.6
mocked-exports: 0.1.1
nitropack: 2.13.4(oxc-parser@0.132.0)(srvx@0.11.16)
- nuxt: 3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0)
+ nuxt: 3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(tsx@4.22.4)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0)
ohash: 2.0.11
pathe: 2.0.3
pkg-types: 2.3.1
@@ -5415,12 +5435,12 @@ snapshots:
rc9: 3.0.1
std-env: 4.1.0
- '@nuxt/vite-builder@3.21.8(@types/node@22.19.20)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.32.0)(magicast@0.5.3)(nuxt@3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(terser@5.48.0)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3))(vue@3.5.35(typescript@5.9.3))(yaml@2.9.0)':
+ '@nuxt/vite-builder@3.21.8(@types/node@22.19.20)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.32.0)(magicast@0.5.3)(nuxt@3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(tsx@4.22.4)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(terser@5.48.0)(tsx@4.22.4)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3))(vue@3.5.35(typescript@5.9.3))(yaml@2.9.0)':
dependencies:
'@nuxt/kit': 3.21.8(magicast@0.5.3)
'@rollup/plugin-replace': 6.0.3(rollup@4.61.1)
- '@vitejs/plugin-vue': 6.0.7(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))
- '@vitejs/plugin-vue-jsx': 5.1.5(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))
+ '@vitejs/plugin-vue': 6.0.7(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))
+ '@vitejs/plugin-vue-jsx': 5.1.5(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))
autoprefixer: 10.5.0(postcss@8.5.15)
consola: 3.4.2
cssnano: 7.1.9(postcss@8.5.15)
@@ -5434,7 +5454,7 @@ snapshots:
magic-string: 0.30.21
mlly: 1.8.2
mocked-exports: 0.1.1
- nuxt: 3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0)
+ nuxt: 3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(tsx@4.22.4)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0)
nypm: 0.6.6
ohash: 2.0.11
pathe: 2.0.3
@@ -5445,9 +5465,9 @@ snapshots:
std-env: 4.1.0
ufo: 1.6.4
unenv: 2.0.0-rc.24
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
- vite-node: 5.3.0(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
- vite-plugin-checker: 0.13.0(eslint@9.39.4(jiti@2.7.0))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
+ vite-node: 5.3.0(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
+ vite-plugin-checker: 0.13.0(eslint@9.39.4(jiti@2.7.0))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))
vue: 3.5.35(typescript@5.9.3)
vue-bundle-renderer: 2.2.0
optionalDependencies:
@@ -6005,12 +6025,12 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.0
'@tailwindcss/oxide-win32-x64-msvc': 4.3.0
- '@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))':
+ '@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))':
dependencies:
'@tailwindcss/node': 4.3.0
'@tailwindcss/oxide': 4.3.0
tailwindcss: 4.3.0
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
'@tybys/wasm-util@0.10.2':
dependencies:
@@ -6154,22 +6174,22 @@ snapshots:
- rollup
- supports-color
- '@vitejs/plugin-vue-jsx@5.1.5(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))':
+ '@vitejs/plugin-vue-jsx@5.1.5(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))':
dependencies:
'@babel/core': 7.29.7
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
'@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7)
'@rolldown/pluginutils': 1.0.1
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7)
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
vue: 3.5.35(typescript@5.9.3)
transitivePeerDependencies:
- supports-color
- '@vitejs/plugin-vue@6.0.7(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))':
+ '@vitejs/plugin-vue@6.0.7(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.1
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
vue: 3.5.35(typescript@5.9.3)
'@vitest/expect@3.2.6':
@@ -6180,13 +6200,13 @@ snapshots:
chai: 5.3.3
tinyrainbow: 2.0.0
- '@vitest/mocker@3.2.6(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))':
+ '@vitest/mocker@3.2.6(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))':
dependencies:
'@vitest/spy': 3.2.6
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
'@vitest/pretty-format@3.2.6':
dependencies:
@@ -6509,7 +6529,7 @@ snapshots:
baseline-browser-mapping@2.10.34: {}
- better-auth@1.6.15(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(vitest@3.2.6(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3)):
+ better-auth@1.6.15(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(vitest@3.2.6(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3)):
dependencies:
'@better-auth/core': 1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
'@better-auth/drizzle-adapter': 1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)
@@ -6531,7 +6551,7 @@ snapshots:
optionalDependencies:
'@prisma/client': 6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3)
prisma: 6.19.3(magicast@0.5.3)(typescript@5.9.3)
- vitest: 3.2.6(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ vitest: 3.2.6(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
vue: 3.5.35(typescript@5.9.3)
transitivePeerDependencies:
- '@cloudflare/workers-types'
@@ -7204,7 +7224,7 @@ snapshots:
dependencies:
tiny-inflate: 1.0.3
- fontless@0.2.1(db0@0.3.4)(ioredis@5.11.1)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)):
+ fontless@0.2.1(db0@0.3.4)(ioredis@5.11.1)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)):
dependencies:
consola: 3.4.2
css-tree: 3.2.1
@@ -7220,7 +7240,7 @@ snapshots:
unifont: 0.7.4
unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
optionalDependencies:
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@@ -7861,16 +7881,16 @@ snapshots:
dependencies:
boolbase: 1.0.0
- nuxt@3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0):
+ nuxt@3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(tsx@4.22.4)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0):
dependencies:
'@dxup/nuxt': 0.4.1(magicast@0.5.3)(typescript@5.9.3)
'@nuxt/cli': 3.35.2(@nuxt/schema@3.21.8)(cac@6.7.14)(magicast@0.5.3)
- '@nuxt/devtools': 3.2.4(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))
+ '@nuxt/devtools': 3.2.4(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3))
'@nuxt/kit': 3.21.8(magicast@0.5.3)
- '@nuxt/nitro-server': 3.21.8(db0@0.3.4)(ioredis@5.11.1)(magicast@0.5.3)(nuxt@3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0))(oxc-parser@0.132.0)(srvx@0.11.16)(typescript@5.9.3)
+ '@nuxt/nitro-server': 3.21.8(db0@0.3.4)(ioredis@5.11.1)(magicast@0.5.3)(nuxt@3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(tsx@4.22.4)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0))(oxc-parser@0.132.0)(srvx@0.11.16)(typescript@5.9.3)
'@nuxt/schema': 3.21.8
'@nuxt/telemetry': 2.8.0(@nuxt/kit@3.21.8(magicast@0.5.3))
- '@nuxt/vite-builder': 3.21.8(@types/node@22.19.20)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.32.0)(magicast@0.5.3)(nuxt@3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(terser@5.48.0)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3))(vue@3.5.35(typescript@5.9.3))(yaml@2.9.0)
+ '@nuxt/vite-builder': 3.21.8(@types/node@22.19.20)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.32.0)(magicast@0.5.3)(nuxt@3.21.8(@parcel/watcher@2.5.6)(@types/node@22.19.20)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.4(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(srvx@0.11.16)(terser@5.48.0)(tsx@4.22.4)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3))(yaml@2.9.0))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.61.1))(rollup@4.61.1)(terser@5.48.0)(tsx@4.22.4)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3))(vue@3.5.35(typescript@5.9.3))(yaml@2.9.0)
'@unhead/vue': 2.1.15(vue@3.5.35(typescript@5.9.3))
'@vue/shared': 3.5.35
c12: 3.3.4(magicast@0.5.3)
@@ -8652,6 +8672,10 @@ snapshots:
dependencies:
js-tokens: 9.0.1
+ stripe@22.2.2(@types/node@22.19.20):
+ optionalDependencies:
+ '@types/node': 22.19.20
+
structured-clone-es@2.0.0: {}
stylehacks@7.0.11(postcss@8.5.15):
@@ -8763,6 +8787,12 @@ snapshots:
tslib@2.8.1:
optional: true
+ tsx@4.22.4:
+ dependencies:
+ esbuild: 0.28.0
+ optionalDependencies:
+ fsevents: 2.3.3
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@@ -8934,23 +8964,23 @@ snapshots:
util-deprecate@1.0.2: {}
- vite-dev-rpc@2.0.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)):
+ vite-dev-rpc@2.0.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)):
dependencies:
birpc: 4.0.0
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
- vite-hot-client: 2.2.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
+ vite-hot-client: 2.2.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))
- vite-hot-client@2.2.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)):
+ vite-hot-client@2.2.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)):
dependencies:
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
- vite-node@3.2.4(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0):
+ vite-node@3.2.4(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -8965,13 +8995,13 @@ snapshots:
- tsx
- yaml
- vite-node@5.3.0(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0):
+ vite-node@5.3.0(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0):
dependencies:
cac: 6.7.14
es-module-lexer: 2.1.0
obug: 2.1.2
pathe: 2.0.3
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -8985,7 +9015,7 @@ snapshots:
- tsx
- yaml
- vite-plugin-checker@0.13.0(eslint@9.39.4(jiti@2.7.0))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3)):
+ vite-plugin-checker@0.13.0(eslint@9.39.4(jiti@2.7.0))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue-tsc@2.2.12(typescript@5.9.3)):
dependencies:
'@babel/code-frame': 7.29.7
chokidar: 4.0.3
@@ -8995,7 +9025,7 @@ snapshots:
proper-lockfile: 4.1.2
tiny-invariant: 1.3.3
tinyglobby: 0.2.17
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
vscode-uri: 3.1.0
optionalDependencies:
eslint: 9.39.4(jiti@2.7.0)
@@ -9003,7 +9033,7 @@ snapshots:
typescript: 5.9.3
vue-tsc: 2.2.12(typescript@5.9.3)
- vite-plugin-inspect@11.4.1(@nuxt/kit@4.4.8(magicast@0.5.3))(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)):
+ vite-plugin-inspect@11.4.1(@nuxt/kit@4.4.8(magicast@0.5.3))(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)):
dependencies:
ansis: 4.3.1
error-stack-parser-es: 1.0.5
@@ -9013,22 +9043,22 @@ snapshots:
perfect-debounce: 2.1.0
sirv: 3.0.2
unplugin-utils: 0.3.1
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
- vite-dev-rpc: 2.0.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
+ vite-dev-rpc: 2.0.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))
optionalDependencies:
'@nuxt/kit': 4.4.8(magicast@0.5.3)
- vite-plugin-vue-tracer@1.4.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3)):
+ vite-plugin-vue-tracer@1.4.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@5.9.3)):
dependencies:
estree-walker: 3.0.3
exsolve: 1.0.8
magic-string: 0.30.21
pathe: 2.0.3
source-map-js: 1.2.1
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
vue: 3.5.35(typescript@5.9.3)
- vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0):
+ vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0):
dependencies:
esbuild: 0.27.7
fdir: 6.5.0(picomatch@4.0.4)
@@ -9042,13 +9072,14 @@ snapshots:
jiti: 2.7.0
lightningcss: 1.32.0
terser: 5.48.0
+ tsx: 4.22.4
yaml: 2.9.0
- vitest@3.2.6(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0):
+ vitest@3.2.6(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.6
- '@vitest/mocker': 3.2.6(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))
+ '@vitest/mocker': 3.2.6(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))
'@vitest/pretty-format': 3.2.6
'@vitest/runner': 3.2.6
'@vitest/snapshot': 3.2.6
@@ -9066,8 +9097,8 @@ snapshots:
tinyglobby: 0.2.17
tinypool: 1.1.1
tinyrainbow: 2.0.0
- vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
- vite-node: 3.2.4(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
+ vite-node: 3.2.4(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.19.20