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/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 }}
+
+
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