feat: WIP stripe billing refactor update for 2025

pull/715/head
Travis Fischer 2025-05-16 17:35:45 +07:00
rodzic f55ba7f060
commit 6ac5b3d589
17 zmienionych plików z 674 dodań i 691 usunięć

Wyświetl plik

@ -48,6 +48,7 @@
"bcryptjs": "^3.0.2",
"eventid": "^2.0.1",
"exit-hook": "catalog:",
"hash-object": "^5.0.1",
"hono": "^4.7.9",
"jsonwebtoken": "^9.0.2",
"p-all": "^5.0.0",

Wyświetl plik

@ -15,7 +15,7 @@ const route = createRoute({
description: 'Updates a project.',
tags: ['projects'],
operationId: 'updateProject',
method: 'put',
method: 'post',
path: 'projects/{projectId}',
security: openapiAuthenticatedSecuritySchemas,
request: {

Wyświetl plik

@ -17,7 +17,7 @@ const route = createRoute({
description: 'Updates a team member.',
tags: ['teams'],
operationId: 'updateTeamMember',
method: 'put',
method: 'post',
path: 'teams/{team}/members/{userId}',
security: openapiAuthenticatedSecuritySchemas,
request: {

Wyświetl plik

@ -16,7 +16,7 @@ const route = createRoute({
description: 'Updates a team.',
tags: ['teams'],
operationId: 'updateTeam',
method: 'put',
method: 'post',
path: 'teams/{team}',
security: openapiAuthenticatedSecuritySchemas,
request: {

Wyświetl plik

@ -16,7 +16,7 @@ const route = createRoute({
description: 'Updates a user',
tags: ['users'],
operationId: 'updateUser',
method: 'put',
method: 'post',
path: 'users/{userId}',
security: openapiAuthenticatedSecuritySchemas,
request: {

Wyświetl plik

@ -40,8 +40,8 @@ export function registerV1StripeWebhook(app: OpenAPIHono) {
})
}
// Shouldn't ever happen because the signatures should be different, but it's
// a useful sanity check just in case.
// Shouldn't ever happen because the signatures _should_ be different, but
// it's a useful sanity check just in case.
assert(
event.livemode === isStripeLive,
400,
@ -89,7 +89,8 @@ export function registerV1StripeWebhook(app: OpenAPIHono) {
await db
.update(schema.consumers)
.set({
stripeStatus: consumer.stripeStatus
stripeStatus: consumer.stripeStatus,
enabled: consumer.enabled
})
.where(eq(schema.consumers.id, consumer.id))

Wyświetl plik

@ -31,10 +31,10 @@ import {
*
* Consumers are used to track usage and billing for a project.
*
* Consumers are linked to a corresponding Stripe Customer. The Stripe customer
* will either be the user's default Stripe Customer for the platform account,
* or a customer on the project's connected Stripe account if the project has
* Stripe Connect enabled.
* Consumers are linked to a corresponding Stripe Customer and Subscription.
* The Stripe customer will either be the user's default Stripe Customer for
* the platform account, or a customer on the project's connected Stripe
* account if the project has Stripe Connect enabled.
*/
export const consumers = pgTable(
'consumers',

Wyświetl plik

@ -12,10 +12,10 @@ import { z } from '@hono/zod-openapi'
import { projects } from './project'
import { teams, teamSelectSchema } from './team'
import {
type Coupon,
couponSchema,
type PricingPlan,
pricingPlanSchema
// type Coupon,
// couponSchema,
type PricingPlanMapByInterval,
pricingPlanMapByIntervalSchema
} from './types'
import { users, userSelectSchema } from './user'
import {
@ -54,13 +54,9 @@ export const deployments = pgTable(
onDelete: 'cascade'
}),
// TODO: tools?
// TODO: Tool definitions or OpenAPI spec
// services: jsonb().$type<Service[]>().default([]),
// TODO: Environment variables & secrets
// build: jsonb().$type<object>(),
// env: jsonb().$type<object>(),
// TODO: metadata config (logo, keywords, etc)
// TODO: webhooks
// TODO: third-party auth provider config?
@ -68,8 +64,13 @@ export const deployments = pgTable(
// Backend API URL
_url: text().notNull(),
pricingPlans: jsonb().$type<PricingPlan[]>().notNull(),
coupons: jsonb().$type<Coupon[]>().default([]).notNull()
// NOTE: this does not have a default value and must be given a value at creation.
// Record<PricingInterval, Record<string, PricingPlan>>
pricingPlanMapByInterval: jsonb()
.$type<PricingPlanMapByInterval>()
.notNull()
// coupons: jsonb().$type<Coupon[]>().default([]).notNull()
},
(table) => [
index('deployment_userId_idx').on(table.userId),
@ -108,8 +109,9 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
export const deploymentSelectSchema = createSelectSchema(deployments, {
// build: z.object({}),
// env: z.object({}),
pricingPlans: z.array(pricingPlanSchema),
coupons: z.array(couponSchema)
pricingPlanMapByInterval: pricingPlanMapByIntervalSchema
// coupons: z.array(couponSchema)
})
.omit({
_url: true
@ -141,10 +143,12 @@ export const deploymentInsertSchema = createInsertSchema(deployments, {
_url: (schema) => schema.url(),
// build: z.object({}),
// env: z.object({}),
pricingPlans: z.array(pricingPlanSchema),
coupons: z.array(couponSchema).optional()
// TODO: should this public resource be decoupled from the internal pricing
// plan structure?
pricingPlanMapByInterval: pricingPlanMapByIntervalSchema
// TODO
// coupons: z.array(couponSchema).optional()
})
.omit({ id: true, createdAt: true, updatedAt: true })
.strict()

Wyświetl plik

@ -12,7 +12,16 @@ import { z } from '@hono/zod-openapi'
import { deployments, deploymentSelectSchema } from './deployment'
import { teams, teamSelectSchema } from './team'
import { type Webhook } from './types'
import {
pricingIntervalSchema,
type StripeMeterIdMap,
stripeMeterIdMapSchema,
type StripePriceIdMap,
stripePriceIdMapSchema,
type StripeProductIdMap,
stripeProductIdMapSchema,
type Webhook
} from './types'
import { users, userSelectSchema } from './user'
import {
createInsertSchema,
@ -20,6 +29,8 @@ import {
createUpdateSchema,
cuid,
deploymentId,
pricingCurrencyEnum,
pricingIntervalEnum,
projectId,
stripeId,
timestamps
@ -51,6 +62,18 @@ export const projects = pgTable(
// TODO: This is going to need to vary from dev to prod
isStripeConnectEnabled: boolean().default(false).notNull(),
// Which pricing intervals are supported for subscriptions to this project
pricingIntervals: pricingIntervalEnum()
.array()
.default(['month'])
.notNull(),
// Default pricing interval for subscriptions to this project
defaultPricingInterval: pricingIntervalEnum().default('month').notNull(),
// Pricing currency used across all prices and subscriptions to this project
pricingCurrency: pricingCurrencyEnum().default('usd').notNull(),
// All deployments share the same underlying proxy secret
_secret: text().notNull(),
@ -62,41 +85,45 @@ export const projects = pgTable(
_webhooks: jsonb().$type<Webhook[]>().default([]).notNull(),
// Stripe products corresponding to the stripe plans across deployments
stripeBaseProductId: stripeId(),
stripeRequestProductId: stripeId(),
// Map between metric slugs and stripe product ids
// [metricSlug: string]: string
stripeMetricProductIds: jsonb()
.$type<Record<string, string>>()
.default({})
.notNull(),
// TODO: currency?
// Stripe coupons associated with this project, mapping from unique coupon
// hash to stripe coupon id.
// object hash to stripe coupon id.
// `[hash: string]: string`
_stripeCouponIds: jsonb()
.$type<Record<string, string>>()
// _stripeCouponsMap: jsonb()
// .$type<Record<string, string>>()
// .default({})
// .notNull(),
// Stripe billing Products associated with this project across deployments,
// mapping from PricingPlanMetric **slug** to Stripe Product id.
// NOTE: This map uses slugs as keys, unlike `_stripePriceIdMap`, because
// Stripe Products are agnostic to the PricingPlanMetric config. This is
// important for them to be shared across deployments even if the pricing
// details change.
_stripeProductIdMap: jsonb()
.$type<StripeProductIdMap>()
.default({})
.notNull(),
// Stripe billing plans associated with this project (created lazily),
// mapping from unique plan hash to stripe plan ids for base and request
// respectively.
// `[hash: string]: { basePlanId: string, requestPlanId: string }`
_stripePlanIds: jsonb()
.$type<Record<string, { basePlanId: string; requestPlanId: string }>>()
.default({})
.notNull(),
// Stripe billing Prices associated with this project, mapping from unique
// PricingPlanMetric **hash** to Stripe Price id.
// NOTE: This map uses hashes as keys, because Stripe Prices are dependent
// on the PricingPlanMetric config. This is important for them to be shared
// across deployments even if the pricing details change.
_stripePriceIdMap: jsonb().$type<StripePriceIdMap>().default({}).notNull(),
// Stripe billing Metrics associated with this project, mapping from unique
// PricingPlanMetric **slug** to Stripe Meter id.
// NOTE: This map uses slugs as keys, unlike `_stripePriceIdMap`, because
// Stripe Products are agnostic to the PricingPlanMetric config. This is
// important for them to be shared across deployments even if the pricing
// details change.
_stripeMeterIdMap: jsonb().$type<StripeMeterIdMap>().default({}).notNull(),
// Connected Stripe account (standard or express).
// If not defined, then subscriptions for this project route through our
// main Stripe account.
// NOTE: the connected account is shared between dev and prod, so we're not using
// the stripeID utility.
// TODO: is it wise to share this between dev and prod?
// TODO: is it okay for this to be public?
_stripeAccountId: stripeId()
},
(table) => [
@ -149,26 +176,21 @@ export const projectRelationsSchema: z.ZodType<ProjectRelationFields> = z.enum([
export const projectSelectSchema = createSelectSchema(projects, {
applicationFeePercent: (schema) => schema.nonnegative(),
stripeMetricProductIds: z.record(z.string(), z.string()).optional()
// _webhooks: z.array(webhookSchema),
// _stripeCouponIds: z.record(z.string(), z.string()).optional(),
// _stripePlanIds: z
// .record(
// z.string(),
// z.object({
// basePlanId: z.string(),
// requestPlanId: z.string()
// })
// )
// .optional()
_stripeProductIdMap: stripeProductIdMapSchema,
_stripePriceIdMap: stripePriceIdMapSchema,
_stripeMeterIdMap: stripeMeterIdMapSchema,
pricingIntervals: z.array(pricingIntervalSchema).optional(),
defaultPricingInterval: pricingIntervalSchema.optional()
})
.omit({
_secret: true,
_providerToken: true,
_text: true,
_webhooks: true,
_stripeCouponIds: true,
_stripePlanIds: true,
_stripeProductIdMap: true,
_stripePriceIdMap: true,
_stripeMeterIdMap: true,
_stripeAccountId: true
})
.extend({
@ -229,7 +251,9 @@ export const projectDebugSelectSchema = createSelectSchema(projects).pick({
updatedAt: true,
isStripeConnectEnabled: true,
lastPublishedDeploymentId: true,
lastDeploymentId: true
lastDeploymentId: true,
pricingIntervals: true,
defaultPricingInterval: true
})
// TODO: virtual saasUrl

Wyświetl plik

@ -61,11 +61,11 @@ export const rateLimitSchema = z
.object({
enabled: z.boolean(),
// informal description that overrides any other properties
desc: z.string().optional(),
interval: z.number(), // seconds
maxPerInterval: z.number() // unitless
maxPerInterval: z.number(), // unitless
// informal description that overrides any other properties
desc: z.string().optional()
})
.openapi('RateLimit')
export type RateLimit = z.infer<typeof rateLimitSchema>
@ -86,91 +86,152 @@ export const pricingPlanTierSchema = z
.openapi('PricingPlanTier')
export type PricingPlanTier = z.infer<typeof pricingPlanTierSchema>
export const pricingIntervalSchema = z
.enum(['day', 'week', 'month', 'year'])
.openapi('PricingInterval')
export type PricingInterval = z.infer<typeof pricingIntervalSchema>
export const pricingPlanMetricHashSchema = z
.string()
.nonempty()
.describe('Internal PricingPlanMetric hash')
export const pricingPlanMetricSlugSchema = z
.string()
.nonempty()
.describe('PricingPlanMetric slug')
export const stripePriceIdMapSchema = z
.record(pricingPlanMetricHashSchema, z.string().describe('Stripe Price id'))
.describe('Map from internal PricingPlanMetric **hash** to Stripe Price id')
.openapi('StripePriceIdMap')
export type StripePriceIdMap = z.infer<typeof stripePriceIdMapSchema>
export const stripeMeterIdMapSchema = z
.record(pricingPlanMetricHashSchema, z.string().describe('Stripe Meter id'))
.describe('Map from internal PricingPlanMetric **slug** to Stripe Meter id')
.openapi('StripeMeterIdMap')
export type StripeMeterIdMap = z.infer<typeof stripeMeterIdMapSchema>
const commonPricingPlanMetricSchema = z.object({
/**
* Slugs act as the primary key for metrics. They should be lower and
* kebab-cased ("base", "requests", "image-transformations").
*/
slug: z.union([z.string(), z.literal('base'), z.literal('requests')]),
interval: pricingIntervalSchema,
label: z.string().optional().openapi('label', { example: 'API calls' }),
rateLimit: rateLimitSchema.optional(),
stripePriceId: z.string().optional()
})
export const pricingPlanMetricSchema = z
.object({
// slug acts as a primary key for metrics
slug: z.string(),
.discriminatedUnion('usageType', [
commonPricingPlanMetricSchema.merge(
z.object({
usageType: z.literal('licensed'),
amount: z.number()
})
),
amount: z.number(),
commonPricingPlanMetricSchema.merge(
z.object({
usageType: z.literal('metered'),
unitLabel: z.string().optional(),
label: z.string(),
unitLabel: z.string(),
billingScheme: z.enum(['per_unit', 'tiered']),
// TODO: should this default be 'licensed' or 'metered'?
// methinks licensed for "sites", "jobs", etc...
// TODO: this should probably be explicit since its easy to confuse
usageType: z.enum(['licensed', 'metered']),
// Only applicable for `per_unit` billing schemes
amount: z.number().optional(),
billingScheme: z.enum(['per_unit', 'tiered']),
// Only applicable for `tiered` billing schemes
tiersMode: z.enum(['graduated', 'volume']).optional(),
tiers: z.array(pricingPlanTierSchema).optional(),
tiersMode: z.enum(['graduated', 'volume']),
tiers: z.array(pricingPlanTierSchema),
// TODO: add support for tiered rate limits?
// TODO (low priority): add aggregateUsage
defaultAggregation: z
.object({
formula: z.enum(['sum', 'count', 'last'])
})
.optional(),
rateLimit: rateLimitSchema.optional()
})
// Stripe metric id, which is created lazily upon first use.
stripeMetricId: z.string().optional()
})
)
])
.describe('Stripe billing Price and possibly corresponding Metric')
.openapi('PricingPlanMetric')
export type PricingPlanMetric = z.infer<typeof pricingPlanMetricSchema>
export const pricingPlanSchema = z
.object({
name: z.string(),
slug: z.string(),
name: z.string().openapi('name', { example: 'Starter Monthly' }),
slug: z.string().openapi('slug', { example: 'starter-monthly' }),
desc: z.string().optional(),
features: z.array(z.string()),
auth: z.boolean(),
amount: z.number(),
interval: pricingIntervalSchema,
trialPeriodDays: z.number().optional(),
requests: pricingPlanMetricSchema,
metrics: z.array(pricingPlanMetricSchema),
rateLimit: rateLimitSchema.optional(),
// used to uniquely identify this pricing plan across deployments
baseId: z.string(),
// used to uniquely identify this pricing plan across deployments
requestsId: z.string(),
// [metricSlug: string]: string
metricIds: z.record(z.string()),
// NOTE: the stripe billing plan id(s) for this PricingPlan are referenced
// in the Project._stripePlans mapping via the plan's hash.
// NOTE: all metered billing usage is stored in stripe
stripeBasePlanId: z.string(),
stripeRequestPlanId: z.string(),
// Record mapping metric slugs to stripe plan IDs
// [metricSlug: string]: string
stripeMetricPlans: z.record(z.string())
metricsMap: z
.record(pricingPlanMetricSlugSchema, pricingPlanMetricSchema)
.refine((metricsMap) => {
// Stripe Checkout currently supports a max of 20 line items per
// subscription.
return Object.keys(metricsMap).length <= 20
})
.default({})
})
.openapi('PricingPlan')
export type PricingPlan = z.infer<typeof pricingPlanSchema>
export const couponSchema = z
.object({
// used to uniquely identify this coupon across deployments
id: z.string(),
export const stripeProductIdMapSchema = z
.record(pricingPlanMetricSlugSchema, z.string().describe('Stripe Product id'))
.describe('Map from PricingPlanMetric **slug** to Stripe Product id')
.openapi('StripeProductIdMap')
export type StripeProductIdMap = z.infer<typeof stripeProductIdMapSchema>
valid: z.boolean(),
stripeCoupon: z.string(),
export const pricingPlanMapBySlugSchema = z
.record(z.string().describe('PricingPlan slug'), pricingPlanSchema)
.describe('Map from PricingPlan slug to PricingPlan')
export type PricingPlanMapBySlug = z.infer<typeof pricingPlanMapBySlugSchema>
name: z.string().optional(),
export const pricingPlanMapByIntervalSchema = z
.record(pricingIntervalSchema, pricingPlanMapBySlugSchema)
.describe(
'Map from PricingInterval to a map from PricingPlan slug to PricingPlan'
)
export type PricingPlanMapByInterval = z.infer<
typeof pricingPlanMapByIntervalSchema
>
currency: z.string().optional(),
amount_off: z.number().optional(),
percent_off: z.number().optional(),
// export const couponSchema = z
// .object({
// // used to uniquely identify this coupon across deployments
// id: z.string(),
duration: z.string(),
duration_in_months: z.number().optional(),
// valid: z.boolean(),
// stripeCoupon: z.string(),
redeem_by: z.date().optional(),
max_redemptions: z.number().optional()
})
.openapi('Coupon')
export type Coupon = z.infer<typeof couponSchema>
// name: z.string().optional(),
// currency: z.string().optional(),
// amount_off: z.number().optional(),
// percent_off: z.number().optional(),
// duration: z.string(),
// duration_in_months: z.number().optional(),
// redeem_by: z.date().optional(),
// max_redemptions: z.number().optional()
// })
// .openapi('Coupon')
// export type Coupon = z.infer<typeof couponSchema>

Wyświetl plik

@ -13,6 +13,11 @@ import { createSchemaFactory } from '@fisch0920/drizzle-zod'
import { z } from '@hono/zod-openapi'
import { createId } from '@paralleldrive/cuid2'
import { hashObject, omit } from '@/lib/utils'
import type { RawProject } from '../types'
import type { PricingPlanMetric } from './types'
const usernameAndTeamSlugLength = 64 as const
/**
@ -100,6 +105,13 @@ export const logEntryLevelEnum = pgEnum('LogEntryLevel', [
'warn',
'error'
])
export const pricingIntervalEnum = pgEnum('PricingInterval', [
'day',
'week',
'month',
'year'
])
export const pricingCurrencyEnum = pgEnum('PricingCurrency', ['usd'])
export const { createInsertSchema, createSelectSchema, createUpdateSchema } =
createSchemaFactory({
@ -109,3 +121,47 @@ export const { createInsertSchema, createSelectSchema, createUpdateSchema } =
date: true
}
})
/**
* Gets the hash used to uniquely map a PricingPlanMetric to its corresponding
* Stripe Price in a stable way across deployments within a project.
*
* This hash is used as the key for the `Project._stripePriceIdMap`.
*/
export function getPricingPlanMetricHashForStripePrice({
pricingPlanMetric,
project
}: {
pricingPlanMetric: PricingPlanMetric
project: RawProject
}) {
const hash = hashObject({
...omit(pricingPlanMetric, 'stripePriceId', 'stripeMetricId'),
projectId: project.id,
stripeAccountId: project._stripeAccountId
})
return `price:${pricingPlanMetric.slug}:${hash}`
}
/**
* Gets the hash used to uniquely map a PricingPlanMetric to its corresponding
* Stripe Meter in a stable way across deployments within a project.
*
* This hash is used as the key for the `Project._stripePriceIdMap`.
*/
export function getPricingPlanMetricHashForStripeMeter({
pricingPlanMetric,
project
}: {
pricingPlanMetric: PricingPlanMetric
project: RawProject
}) {
const hash = hashObject({
...omit(pricingPlanMetric, 'stripePriceId', 'stripeMetricId'),
projectId: project.id,
stripeAccountId: project._stripeAccountId
})
return `price:${pricingPlanMetric.slug}:${hash}`
}

Wyświetl plik

@ -4,6 +4,8 @@ import { db, eq, type RawConsumer, type RawProject, schema } from '@/db'
import { stripe } from '@/lib/stripe'
import { assert } from '@/lib/utils'
// TODO: Update this for the new / updated Stripe Connect API
export async function upsertStripeConnectCustomer({
stripeCustomer,
consumer,

Wyświetl plik

@ -1,254 +0,0 @@
import type Stripe from 'stripe'
import pAll from 'p-all'
import type { PricingPlan, PricingPlanMetric } from '@/db/schema'
import { db, eq, type RawDeployment, type RawProject, schema } from '@/db'
import { stripe } from '@/lib/stripe'
import { assert } from '@/lib/utils'
// TODO: move these to config
const currency = 'usd'
const interval = 'month'
export async function upsertStripePricingPlans({
deployment,
project
}: {
deployment: RawDeployment
project: RawProject
}): Promise<void> {
const stripeConnectParams = project._stripeAccountId
? [
{
stripeAccount: project._stripeAccountId
}
]
: []
let dirty = false
async function upsertStripeBaseProduct() {
if (!project.stripeBaseProductId) {
const product = await stripe.products.create(
{
name: `${project.id} base`,
type: 'service'
},
...stripeConnectParams
)
project.stripeBaseProductId = product.id
dirty = true
}
}
async function upsertStripeRequestProduct() {
if (!project.stripeRequestProductId) {
const product = await stripe.products.create(
{
name: `${project.id} requests`,
type: 'service',
unit_label: 'request'
},
...stripeConnectParams
)
project.stripeRequestProductId = product.id
dirty = true
}
}
async function upsertStripeMetricProduct(metric: PricingPlanMetric) {
const { slug: metricSlug } = metric
if (!project.stripeMetricProductIds[metricSlug]) {
const product = await stripe.products.create(
{
name: `${project.id} ${metricSlug}`,
type: 'service',
unit_label: metric.unitLabel
},
...stripeConnectParams
)
project.stripeMetricProductIds[metricSlug] = product.id
dirty = true
}
}
async function upsertStripeBasePlan(pricingPlan: PricingPlan) {
if (!pricingPlan.stripeBasePlanId) {
const hash = pricingPlan.baseId
const stripePlan = project._stripePlanIds[hash]
assert(stripePlan, 400, 'Missing stripe base plan')
pricingPlan.stripeBasePlanId = stripePlan.basePlanId
dirty = true
if (!pricingPlan.stripeBasePlanId) {
const stripePlan = await stripe.plans.create(
{
product: project.stripeBaseProductId,
currency,
interval,
amount_decimal: pricingPlan.amount.toFixed(12),
nickname: `${project.id}-${pricingPlan.slug}-base`
},
...stripeConnectParams
)
pricingPlan.stripeBasePlanId = stripePlan.id
project._stripePlanIds[hash]!.basePlanId = stripePlan.id
}
}
}
async function upsertStripeRequestPlan(pricingPlan: PricingPlan) {
const { requests } = pricingPlan
if (!pricingPlan.stripeRequestPlanId) {
const hash = pricingPlan.requestsId
const projectStripePlan = project._stripePlanIds[hash]
assert(projectStripePlan, 400, 'Missing stripe request plan')
pricingPlan.stripeRequestPlanId = projectStripePlan.requestPlanId
dirty = true
if (!pricingPlan.stripeRequestPlanId) {
const planParams: Stripe.PlanCreateParams = {
product: project.stripeRequestProductId,
currency,
interval,
usage_type: 'metered',
billing_scheme: requests.billingScheme,
nickname: `${project.id}-${pricingPlan.slug}-requests`
}
if (requests.billingScheme === 'tiered') {
planParams.tiers_mode = requests.tiersMode
planParams.tiers = requests.tiers.map((tier) => {
const result: Stripe.PlanCreateParams.Tier = {
up_to: tier.upTo
}
if (tier.unitAmount !== undefined) {
result.unit_amount_decimal = tier.unitAmount.toFixed(12)
}
if (tier.flatAmount !== undefined) {
result.flat_amount_decimal = tier.flatAmount.toFixed(12)
}
return result
})
} else {
planParams.amount_decimal = requests.amount.toFixed(12)
}
const stripePlan = await stripe.plans.create(
planParams,
...stripeConnectParams
)
pricingPlan.stripeRequestPlanId = stripePlan.id
projectStripePlan.requestPlanId = stripePlan.id
}
}
}
async function upsertStripeMetricPlan(
pricingPlan: PricingPlan,
metric: PricingPlanMetric
) {
const { slug: metricSlug } = metric
if (!pricingPlan.stripeMetricPlans[metricSlug]) {
const hash = pricingPlan.metricIds[metricSlug]
assert(hash, 500, `Missing stripe metric "${metricSlug}"`)
const projectStripePlan = project._stripePlanIds[hash]
assert(projectStripePlan, 500, 'Missing stripe request plan')
// TODO: is this right? differs from original source
pricingPlan.stripeMetricPlans[metricSlug] = projectStripePlan.basePlanId
dirty = true
if (!pricingPlan.stripeMetricPlans[metricSlug]) {
const stripeProductId = project.stripeMetricProductIds[metricSlug]
assert(
stripeProductId,
500,
`Missing stripe product ID for metric "${metricSlug}"`
)
const planParams: Stripe.PlanCreateParams = {
product: stripeProductId,
currency,
interval,
usage_type: metric.usageType,
billing_scheme: metric.billingScheme,
nickname: `${project.id}-${pricingPlan.slug}-${metricSlug}`
}
if (metric.billingScheme === 'tiered') {
planParams.tiers_mode = metric.tiersMode
planParams.tiers = metric.tiers.map((tier) => {
const result: Stripe.PlanCreateParams.Tier = {
up_to: tier.upTo
}
if (tier.unitAmount !== undefined) {
result.unit_amount_decimal = tier.unitAmount.toFixed(12)
}
if (tier.flatAmount !== undefined) {
result.flat_amount_decimal = tier.flatAmount.toFixed(12)
}
return result
})
} else {
planParams.amount_decimal = metric.amount.toFixed(12)
}
const stripePlan = await stripe.plans.create(
planParams,
...stripeConnectParams
)
pricingPlan.stripeMetricPlans[metricSlug] = stripePlan.id
projectStripePlan.basePlanId = stripePlan.id
}
}
}
await Promise.all([upsertStripeBaseProduct(), upsertStripeRequestProduct()])
const upserts = []
for (const pricingPlan of deployment.pricingPlans) {
upserts.push(() => upsertStripeBasePlan(pricingPlan))
upserts.push(() => upsertStripeRequestPlan(pricingPlan))
for (const metric of pricingPlan.metrics) {
upserts.push(async () => {
await upsertStripeMetricProduct(metric)
return upsertStripeMetricPlan(pricingPlan, metric)
})
}
}
await pAll(upserts, { concurrency: 4 })
if (dirty) {
await Promise.all([
db
.update(schema.projects)
.set(project)
.where(eq(schema.projects.id, project.id)),
db
.update(schema.deployments)
.set(deployment)
.where(eq(schema.deployments.id, deployment.id))
])
}
}

Wyświetl plik

@ -0,0 +1,198 @@
import type Stripe from 'stripe'
import pAll from 'p-all'
import { db, eq, type RawDeployment, type RawProject, schema } from '@/db'
import {
getPricingPlanMetricHash,
type PricingPlan,
type PricingPlanMetric
} from '@/db/schema'
import { stripe } from '@/lib/stripe'
import { assert } from '@/lib/utils'
export async function upsertStripeProductsAndPricing({
deployment,
project
}: {
deployment: RawDeployment
project: RawProject
}): Promise<void> {
const stripeConnectParams = project._stripeAccountId
? [
{
stripeAccount: project._stripeAccountId
}
]
: []
let dirty = false
async function upsertStripeProductAndPricingForMetric({
pricingPlan,
pricingPlanMetric
}: {
pricingPlan: PricingPlan
pricingPlanMetric: PricingPlanMetric
}) {
const { slug: pricingPlanSlug } = pricingPlan // TODO
const { slug: pricingPlanMetricSlug } = pricingPlanMetric
const pricingPlanMetricHash = getPricingPlanMetricHash({
pricingPlanMetric,
project
})
// Upsert the Stripe Product
if (!project._stripeProductIdMap[pricingPlanMetricSlug]) {
const productParams: Stripe.ProductCreateParams = {
name: `${project.id} ${pricingPlanMetricSlug}`,
type: 'service',
metadata: {
projectId: project.id,
pricingPlanMetricSlug
}
}
if (pricingPlanMetric.usageType === 'licensed') {
productParams.unit_label = pricingPlanMetric.label
} else {
productParams.unit_label = pricingPlanMetric.unitLabel
}
const product = await stripe.products.create(
productParams,
...stripeConnectParams
)
project._stripeProductIdMap[pricingPlanMetricSlug] = product.id
dirty = true
}
assert(project._stripeProductIdMap[pricingPlanMetricSlug])
// Upsert the Stripe Meter
if (
pricingPlanMetric.usageType === 'metered' &&
!project._stripeMeterIdMap[pricingPlanMetricSlug]
) {
const meter = await stripe.billing.meters.create(
{
display_name: `${project.id} ${pricingPlanMetric.label || pricingPlanMetricSlug}`,
event_name: `meter-${project.id}-${pricingPlanMetricSlug}`,
default_aggregation: {
formula: 'sum'
},
customer_mapping: {
event_payload_key: 'stripe_customer_id',
type: 'by_id'
},
value_settings: {
event_payload_key: 'value'
}
},
...stripeConnectParams
)
project._stripeMeterIdMap[pricingPlanMetricSlug] = meter.id
dirty = true
}
assert(
pricingPlanMetric.usageType === 'licensed' ||
project._stripeMeterIdMap[pricingPlanMetricSlug]
)
// Upsert the Stripe Price
if (!project._stripePriceIdMap[pricingPlanMetricHash]) {
const priceParams: Stripe.PriceCreateParams = {
nickname: `price-${project.id}-${pricingPlan.slug}-${pricingPlanMetricSlug}`,
product: project._stripeProductIdMap[pricingPlanMetricSlug],
currency: project.pricingCurrency,
recurring: {
interval: pricingPlanMetric.interval,
// TODO: support this
interval_count: 1,
usage_type: pricingPlanMetric.usageType
}
}
if (pricingPlanMetric.usageType === 'licensed') {
priceParams.unit_amount_decimal = pricingPlanMetric.amount.toFixed(12)
} else {
priceParams.billing_scheme = pricingPlanMetric.billingScheme
if (pricingPlanMetric.billingScheme === 'tiered') {
assert(
pricingPlanMetric.tiers,
`Invalid pricing plan metric: ${pricingPlanMetricSlug}`
)
priceParams.tiers_mode = pricingPlanMetric.tiersMode
priceParams.tiers = pricingPlanMetric.tiers!.map((tier) => {
const result: Stripe.PriceCreateParams.Tier = {
up_to: tier.upTo
}
if (tier.unitAmount !== undefined) {
result.unit_amount_decimal = tier.unitAmount.toFixed(12)
}
if (tier.flatAmount !== undefined) {
result.flat_amount_decimal = tier.flatAmount.toFixed(12)
}
return result
})
}
}
const stripePrice = await stripe.prices.create(
priceParams,
...stripeConnectParams
)
project._stripePriceIdMap[pricingPlanMetricHash] = stripePrice.id
dirty = true
}
assert(project._stripePriceIdMap[pricingPlanMetricHash])
}
const upserts: Array<() => Promise<void>> = []
for (const pricingInterval of project.pricingIntervals) {
const pricingPlanMap = deployment.pricingPlanMapByInterval[pricingInterval]
assert(
pricingPlanMap,
`Invalid pricing config for deployment "${deployment.id}": missing pricing plan map for interval "${pricingInterval}"`
)
for (const pricingPlan of Object.values(pricingPlanMap)) {
for (const pricingPlanMetric of Object.values(pricingPlan.metricsMap)) {
upserts.push(() =>
upsertStripeProductAndPricingForMetric({
pricingPlan,
pricingPlanMetric
})
)
}
}
}
await pAll(upserts, { concurrency: 4 })
if (dirty) {
await Promise.all([
db
.update(schema.projects)
.set(project)
.where(eq(schema.projects.id, project.id)),
db
.update(schema.deployments)
.set(deployment)
.where(eq(schema.deployments.id, deployment.id))
])
}
}

Wyświetl plik

@ -2,11 +2,50 @@ import { createHash, randomUUID } from 'node:crypto'
import type { ContentfulStatusCode } from 'hono/utils/http-status'
import type { ZodSchema, ZodTypeDef } from 'zod'
import hashObjectImpl, { type Options as HashObjectOptions } from 'hash-object'
import { HttpError, ZodValidationError } from './errors'
export function sha256(input: string = randomUUID()) {
return createHash('sha256').update(input).digest('hex')
/**
* From `inputObj`, create a new object that does not include `keys`.
*
* @example
* ```js
* omit({ a: 1, b: 2, c: 3 }, 'a', 'c') // { b: 2 }
* ```
*/
export const omit = <
T extends Record<string, unknown> | object,
K extends keyof any
>(
inputObj: T,
...keys: K[]
): Omit<T, K> => {
const keysSet = new Set(keys)
return Object.fromEntries(
Object.entries(inputObj).filter(([k]) => !keysSet.has(k as any))
) as any
}
/**
* From `inputObj`, create a new object that only includes `keys`.
*
* @example
* ```js
* pick({ a: 1, b: 2, c: 3 }, 'a', 'c') // { a: 1, c: 3 }
* ```
*/
export const pick = <
T extends Record<string, unknown> | object,
K extends keyof T
>(
inputObj: T,
...keys: K[]
): Pick<T, K> => {
const keysSet = new Set(keys)
return Object.fromEntries(
Object.entries(inputObj).filter(([k]) => keysSet.has(k as any))
) as any
}
export function assert(expr: unknown, message?: string): asserts expr
@ -31,6 +70,10 @@ export function assert(
}
}
/**
* Parses the given input against the given Zod schema, throwing a
* `ZodValidationError` if the input is invalid.
*/
export function parseZodSchema<
Output,
Def extends ZodTypeDef = ZodTypeDef,
@ -53,3 +96,22 @@ export function parseZodSchema<
})
}
}
export function sha256(input: string = randomUUID()) {
return createHash('sha256').update(input).digest('hex')
}
/**
* Returns a stable, deterministic hash of the given object, defaulting to
* using `sha256` as the hashing algorithm and `hex` as the encoding.
*/
export function hashObject(
object: Record<string, any>,
options?: HashObjectOptions
): string {
return hashObjectImpl(object, {
algorithm: 'sha256',
encoding: 'hex',
...options
})
}

Wyświetl plik

@ -6,15 +6,60 @@ settings:
catalogs:
default:
'@fisch0920/config':
specifier: ^1.1.0
version: 1.1.0
'@types/node':
specifier: ^22.15.18
version: 22.15.18
del-cli:
specifier: ^6.0.0
version: 6.0.0
dotenv:
specifier: ^16.5.0
version: 16.5.0
eslint:
specifier: ^9.26.0
version: 9.26.0
exit-hook:
specifier: ^4.0.0
version: 4.0.0
lint-staged:
specifier: ^16.0.0
version: 16.0.0
npm-run-all2:
specifier: ^8.0.1
version: 8.0.1
only-allow:
specifier: ^1.2.1
version: 1.2.1
prettier:
specifier: ^3.5.3
version: 3.5.3
restore-cursor:
specifier: ^5.1.0
version: 5.1.0
simple-git-hooks:
specifier: ^2.13.0
version: 2.13.0
tsup:
specifier: ^8.4.0
version: 8.4.0
tsx:
specifier: ^4.19.4
version: 4.19.4
turbo:
specifier: ^2.5.3
version: 2.5.3
type-fest:
specifier: ^4.41.0
version: 4.41.0
typescript:
specifier: ^5.8.3
version: 5.8.3
vitest:
specifier: ^3.1.3
version: 3.1.3
zod:
specifier: ^3.24.4
version: 3.24.4
@ -116,6 +161,9 @@ importers:
exit-hook:
specifier: 'catalog:'
version: 4.0.0
hash-object:
specifier: ^5.0.1
version: 5.0.1
hono:
specifier: ^4.7.9
version: 4.7.9
@ -196,12 +244,6 @@ packages:
resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==}
deprecated: 'Merged into tsx: https://tsx.is'
'@esbuild/aix-ppc64@0.25.3':
resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/aix-ppc64@0.25.4':
resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==}
engines: {node: '>=18'}
@ -214,12 +256,6 @@ packages:
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.25.3':
resolution: {integrity: sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.25.4':
resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==}
engines: {node: '>=18'}
@ -232,12 +268,6 @@ packages:
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.25.3':
resolution: {integrity: sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.25.4':
resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==}
engines: {node: '>=18'}
@ -250,12 +280,6 @@ packages:
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.25.3':
resolution: {integrity: sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.25.4':
resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==}
engines: {node: '>=18'}
@ -268,12 +292,6 @@ packages:
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.25.3':
resolution: {integrity: sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.25.4':
resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==}
engines: {node: '>=18'}
@ -286,12 +304,6 @@ packages:
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.25.3':
resolution: {integrity: sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.25.4':
resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==}
engines: {node: '>=18'}
@ -304,12 +316,6 @@ packages:
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.25.3':
resolution: {integrity: sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.25.4':
resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==}
engines: {node: '>=18'}
@ -322,12 +328,6 @@ packages:
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.3':
resolution: {integrity: sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.4':
resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==}
engines: {node: '>=18'}
@ -340,12 +340,6 @@ packages:
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.25.3':
resolution: {integrity: sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.25.4':
resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==}
engines: {node: '>=18'}
@ -358,12 +352,6 @@ packages:
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.25.3':
resolution: {integrity: sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.25.4':
resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==}
engines: {node: '>=18'}
@ -376,12 +364,6 @@ packages:
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.25.3':
resolution: {integrity: sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.25.4':
resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==}
engines: {node: '>=18'}
@ -394,12 +376,6 @@ packages:
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.25.3':
resolution: {integrity: sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.25.4':
resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==}
engines: {node: '>=18'}
@ -412,12 +388,6 @@ packages:
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.25.3':
resolution: {integrity: sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.25.4':
resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==}
engines: {node: '>=18'}
@ -430,12 +400,6 @@ packages:
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.25.3':
resolution: {integrity: sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.25.4':
resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==}
engines: {node: '>=18'}
@ -448,12 +412,6 @@ packages:
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.25.3':
resolution: {integrity: sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.25.4':
resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==}
engines: {node: '>=18'}
@ -466,12 +424,6 @@ packages:
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.25.3':
resolution: {integrity: sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.25.4':
resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==}
engines: {node: '>=18'}
@ -484,24 +436,12 @@ packages:
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.25.3':
resolution: {integrity: sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.25.4':
resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.3':
resolution: {integrity: sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-arm64@0.25.4':
resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==}
engines: {node: '>=18'}
@ -514,24 +454,12 @@ packages:
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.3':
resolution: {integrity: sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.4':
resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.3':
resolution: {integrity: sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-arm64@0.25.4':
resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==}
engines: {node: '>=18'}
@ -544,12 +472,6 @@ packages:
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.3':
resolution: {integrity: sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.4':
resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==}
engines: {node: '>=18'}
@ -562,12 +484,6 @@ packages:
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.25.3':
resolution: {integrity: sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.25.4':
resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==}
engines: {node: '>=18'}
@ -580,12 +496,6 @@ packages:
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.25.3':
resolution: {integrity: sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.25.4':
resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==}
engines: {node: '>=18'}
@ -598,12 +508,6 @@ packages:
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.25.3':
resolution: {integrity: sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.25.4':
resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==}
engines: {node: '>=18'}
@ -616,12 +520,6 @@ packages:
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.25.3':
resolution: {integrity: sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.25.4':
resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==}
engines: {node: '>=18'}
@ -654,10 +552,6 @@ packages:
resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@9.25.1':
resolution: {integrity: sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@9.26.0':
resolution: {integrity: sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -1642,6 +1536,10 @@ packages:
supports-color:
optional: true
decircular@0.1.1:
resolution: {integrity: sha512-V2Vy+QYSXdgxRPmOZKQWCDf1KQNTUP/Eqswv/3W20gz7+6GB1HTosNrWqK3PqstVpFw/Dd/cGTmXSTKPeOiGVg==}
engines: {node: '>=18'}
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
@ -1853,11 +1751,6 @@ packages:
engines: {node: '>=12'}
hasBin: true
esbuild@0.25.3:
resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==}
engines: {node: '>=18'}
hasBin: true
esbuild@0.25.4:
resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==}
engines: {node: '>=18'}
@ -2219,6 +2112,10 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hash-object@5.0.1:
resolution: {integrity: sha512-iaRY4jYOow1caHkXW7wotYRjZDQk2nq4U7904anGJj8l4x1SLId+vuR8RpGoywZz9puD769hNFVFLFH9t+baJw==}
engines: {node: '>=18'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@ -2357,6 +2254,10 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
is-obj@3.0.0:
resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==}
engines: {node: '>=12'}
is-path-cwd@3.0.0:
resolution: {integrity: sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -2365,6 +2266,10 @@ packages:
resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==}
engines: {node: '>=12'}
is-plain-obj@4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
@ -3019,11 +2924,6 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
semver@7.7.1:
resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
engines: {node: '>=10'}
hasBin: true
semver@7.7.2:
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
@ -3106,6 +3006,10 @@ packages:
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
engines: {node: '>=18'}
sort-keys@5.1.0:
resolution: {integrity: sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ==}
engines: {node: '>=12'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@ -3355,10 +3259,6 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
type-fest@4.40.1:
resolution: {integrity: sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==}
engines: {node: '>=16'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
@ -3633,219 +3533,144 @@ snapshots:
'@esbuild-kit/core-utils': 3.3.2
get-tsconfig: 4.10.0
'@esbuild/aix-ppc64@0.25.3':
optional: true
'@esbuild/aix-ppc64@0.25.4':
optional: true
'@esbuild/android-arm64@0.18.20':
optional: true
'@esbuild/android-arm64@0.25.3':
optional: true
'@esbuild/android-arm64@0.25.4':
optional: true
'@esbuild/android-arm@0.18.20':
optional: true
'@esbuild/android-arm@0.25.3':
optional: true
'@esbuild/android-arm@0.25.4':
optional: true
'@esbuild/android-x64@0.18.20':
optional: true
'@esbuild/android-x64@0.25.3':
optional: true
'@esbuild/android-x64@0.25.4':
optional: true
'@esbuild/darwin-arm64@0.18.20':
optional: true
'@esbuild/darwin-arm64@0.25.3':
optional: true
'@esbuild/darwin-arm64@0.25.4':
optional: true
'@esbuild/darwin-x64@0.18.20':
optional: true
'@esbuild/darwin-x64@0.25.3':
optional: true
'@esbuild/darwin-x64@0.25.4':
optional: true
'@esbuild/freebsd-arm64@0.18.20':
optional: true
'@esbuild/freebsd-arm64@0.25.3':
optional: true
'@esbuild/freebsd-arm64@0.25.4':
optional: true
'@esbuild/freebsd-x64@0.18.20':
optional: true
'@esbuild/freebsd-x64@0.25.3':
optional: true
'@esbuild/freebsd-x64@0.25.4':
optional: true
'@esbuild/linux-arm64@0.18.20':
optional: true
'@esbuild/linux-arm64@0.25.3':
optional: true
'@esbuild/linux-arm64@0.25.4':
optional: true
'@esbuild/linux-arm@0.18.20':
optional: true
'@esbuild/linux-arm@0.25.3':
optional: true
'@esbuild/linux-arm@0.25.4':
optional: true
'@esbuild/linux-ia32@0.18.20':
optional: true
'@esbuild/linux-ia32@0.25.3':
optional: true
'@esbuild/linux-ia32@0.25.4':
optional: true
'@esbuild/linux-loong64@0.18.20':
optional: true
'@esbuild/linux-loong64@0.25.3':
optional: true
'@esbuild/linux-loong64@0.25.4':
optional: true
'@esbuild/linux-mips64el@0.18.20':
optional: true
'@esbuild/linux-mips64el@0.25.3':
optional: true
'@esbuild/linux-mips64el@0.25.4':
optional: true
'@esbuild/linux-ppc64@0.18.20':
optional: true
'@esbuild/linux-ppc64@0.25.3':
optional: true
'@esbuild/linux-ppc64@0.25.4':
optional: true
'@esbuild/linux-riscv64@0.18.20':
optional: true
'@esbuild/linux-riscv64@0.25.3':
optional: true
'@esbuild/linux-riscv64@0.25.4':
optional: true
'@esbuild/linux-s390x@0.18.20':
optional: true
'@esbuild/linux-s390x@0.25.3':
optional: true
'@esbuild/linux-s390x@0.25.4':
optional: true
'@esbuild/linux-x64@0.18.20':
optional: true
'@esbuild/linux-x64@0.25.3':
optional: true
'@esbuild/linux-x64@0.25.4':
optional: true
'@esbuild/netbsd-arm64@0.25.3':
optional: true
'@esbuild/netbsd-arm64@0.25.4':
optional: true
'@esbuild/netbsd-x64@0.18.20':
optional: true
'@esbuild/netbsd-x64@0.25.3':
optional: true
'@esbuild/netbsd-x64@0.25.4':
optional: true
'@esbuild/openbsd-arm64@0.25.3':
optional: true
'@esbuild/openbsd-arm64@0.25.4':
optional: true
'@esbuild/openbsd-x64@0.18.20':
optional: true
'@esbuild/openbsd-x64@0.25.3':
optional: true
'@esbuild/openbsd-x64@0.25.4':
optional: true
'@esbuild/sunos-x64@0.18.20':
optional: true
'@esbuild/sunos-x64@0.25.3':
optional: true
'@esbuild/sunos-x64@0.25.4':
optional: true
'@esbuild/win32-arm64@0.18.20':
optional: true
'@esbuild/win32-arm64@0.25.3':
optional: true
'@esbuild/win32-arm64@0.25.4':
optional: true
'@esbuild/win32-ia32@0.18.20':
optional: true
'@esbuild/win32-ia32@0.25.3':
optional: true
'@esbuild/win32-ia32@0.25.4':
optional: true
'@esbuild/win32-x64@0.18.20':
optional: true
'@esbuild/win32-x64@0.25.3':
optional: true
'@esbuild/win32-x64@0.25.4':
optional: true
@ -3859,7 +3684,7 @@ snapshots:
'@eslint/config-array@0.20.0':
dependencies:
'@eslint/object-schema': 2.1.6
debug: 4.4.0
debug: 4.4.1
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@ -3873,7 +3698,7 @@ snapshots:
'@eslint/eslintrc@3.3.1':
dependencies:
ajv: 6.12.6
debug: 4.4.0
debug: 4.4.1
espree: 10.3.0
globals: 14.0.0
ignore: 5.3.2
@ -3884,8 +3709,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@eslint/js@9.25.1': {}
'@eslint/js@9.26.0': {}
'@eslint/object-schema@2.1.6': {}
@ -3907,7 +3730,7 @@ snapshots:
'@fisch0920/config@1.1.0(@typescript-eslint/parser@8.31.0(eslint@9.26.0)(typescript@5.8.3))(@typescript-eslint/utils@8.31.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0)(prettier@3.5.3)(typescript@5.8.3)(vitest@3.1.3(@types/node@22.15.18)(tsx@4.19.4)(yaml@2.7.1))':
dependencies:
'@eslint/js': 9.25.1
'@eslint/js': 9.26.0
'@total-typescript/ts-reset': 0.6.1
'@vitest/eslint-plugin': 1.1.43(@typescript-eslint/utils@8.31.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0)(typescript@5.8.3)(vitest@3.1.3(@types/node@22.15.18)(tsx@4.19.4)(yaml@2.7.1))
eslint: 9.26.0
@ -4484,7 +4307,7 @@ snapshots:
'@typescript-eslint/types': 8.31.0
'@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.31.0
debug: 4.4.0
debug: 4.4.1
eslint: 9.26.0
typescript: 5.8.3
transitivePeerDependencies:
@ -4715,7 +4538,7 @@ snapshots:
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 4.4.0
debug: 4.4.1
http-errors: 2.0.0
iconv-lite: 0.6.3
on-finished: 2.4.1
@ -4751,9 +4574,9 @@ snapshots:
builtin-modules@5.0.0: {}
bundle-require@5.1.0(esbuild@0.25.3):
bundle-require@5.1.0(esbuild@0.25.4):
dependencies:
esbuild: 0.25.3
esbuild: 0.25.4
load-tsconfig: 0.2.5
bytes@3.1.2: {}
@ -4892,6 +4715,8 @@ snapshots:
dependencies:
ms: 2.1.3
decircular@0.1.1: {}
deep-eql@5.0.2: {}
deep-is@0.1.4: {}
@ -5105,34 +4930,6 @@ snapshots:
'@esbuild/win32-ia32': 0.18.20
'@esbuild/win32-x64': 0.18.20
esbuild@0.25.3:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.3
'@esbuild/android-arm': 0.25.3
'@esbuild/android-arm64': 0.25.3
'@esbuild/android-x64': 0.25.3
'@esbuild/darwin-arm64': 0.25.3
'@esbuild/darwin-x64': 0.25.3
'@esbuild/freebsd-arm64': 0.25.3
'@esbuild/freebsd-x64': 0.25.3
'@esbuild/linux-arm': 0.25.3
'@esbuild/linux-arm64': 0.25.3
'@esbuild/linux-ia32': 0.25.3
'@esbuild/linux-loong64': 0.25.3
'@esbuild/linux-mips64el': 0.25.3
'@esbuild/linux-ppc64': 0.25.3
'@esbuild/linux-riscv64': 0.25.3
'@esbuild/linux-s390x': 0.25.3
'@esbuild/linux-x64': 0.25.3
'@esbuild/netbsd-arm64': 0.25.3
'@esbuild/netbsd-x64': 0.25.3
'@esbuild/openbsd-arm64': 0.25.3
'@esbuild/openbsd-x64': 0.25.3
'@esbuild/sunos-x64': 0.25.3
'@esbuild/win32-arm64': 0.25.3
'@esbuild/win32-ia32': 0.25.3
'@esbuild/win32-x64': 0.25.3
esbuild@0.25.4:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.4
@ -5310,7 +5107,7 @@ snapshots:
read-package-up: 11.0.0
regexp-tree: 0.1.27
regjsparser: 0.12.0
semver: 7.7.1
semver: 7.7.2
strip-indent: 4.0.0
eslint-scope@8.3.0:
@ -5341,7 +5138,7 @@ snapshots:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.0
debug: 4.4.1
escape-string-regexp: 4.0.0
eslint-scope: 8.3.0
eslint-visitor-keys: 4.2.0
@ -5416,7 +5213,7 @@ snapshots:
content-type: 1.0.5
cookie: 0.7.2
cookie-signature: 1.2.2
debug: 4.4.0
debug: 4.4.1
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
@ -5472,7 +5269,7 @@ snapshots:
finalhandler@2.1.0:
dependencies:
debug: 4.4.0
debug: 4.4.1
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
@ -5615,6 +5412,13 @@ snapshots:
dependencies:
has-symbols: 1.1.0
hash-object@5.0.1:
dependencies:
decircular: 0.1.1
is-obj: 3.0.0
sort-keys: 5.1.0
type-fest: 4.41.0
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@ -5749,10 +5553,14 @@ snapshots:
is-number@7.0.0: {}
is-obj@3.0.0: {}
is-path-cwd@3.0.0: {}
is-path-inside@4.0.0: {}
is-plain-obj@4.1.0: {}
is-promise@4.0.0: {}
is-regex@1.2.1:
@ -5897,7 +5705,7 @@ snapshots:
dependencies:
chalk: 5.4.1
commander: 13.1.0
debug: 4.4.0
debug: 4.4.1
lilconfig: 3.1.3
listr2: 8.3.3
micromatch: 4.0.8
@ -6027,7 +5835,7 @@ snapshots:
normalize-package-data@6.0.2:
dependencies:
hosted-git-info: 7.0.2
semver: 7.7.1
semver: 7.7.2
validate-npm-package-license: 3.0.4
npm-normalize-package-bin@4.0.0: {}
@ -6146,7 +5954,7 @@ snapshots:
dependencies:
'@babel/code-frame': 7.26.2
index-to-position: 1.1.0
type-fest: 4.40.1
type-fest: 4.41.0
parseurl@1.3.3: {}
@ -6266,14 +6074,14 @@ snapshots:
dependencies:
find-up-simple: 1.0.1
read-pkg: 9.0.1
type-fest: 4.40.1
type-fest: 4.41.0
read-pkg@9.0.1:
dependencies:
'@types/normalize-package-data': 2.4.4
normalize-package-data: 6.0.2
parse-json: 8.3.0
type-fest: 4.40.1
type-fest: 4.41.0
unicorn-magic: 0.1.0
readdirp@4.1.2: {}
@ -6371,7 +6179,7 @@ snapshots:
router@2.2.0:
dependencies:
debug: 4.4.0
debug: 4.4.1
depd: 2.0.0
is-promise: 4.0.0
parseurl: 1.3.3
@ -6412,13 +6220,11 @@ snapshots:
semver@6.3.1: {}
semver@7.7.1: {}
semver@7.7.2: {}
send@1.2.0:
dependencies:
debug: 4.4.0
debug: 4.4.1
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
@ -6521,6 +6327,10 @@ snapshots:
ansi-styles: 6.2.1
is-fullwidth-code-point: 5.0.0
sort-keys@5.1.0:
dependencies:
is-plain-obj: 4.1.0
source-map-js@1.2.1: {}
source-map-support@0.5.21:
@ -6722,12 +6532,12 @@ snapshots:
tsup@8.4.0(postcss@8.5.3)(tsx@4.19.4)(typescript@5.8.3)(yaml@2.7.1):
dependencies:
bundle-require: 5.1.0(esbuild@0.25.3)
bundle-require: 5.1.0(esbuild@0.25.4)
cac: 6.7.14
chokidar: 4.0.3
consola: 3.4.2
debug: 4.4.0
esbuild: 0.25.3
debug: 4.4.1
esbuild: 0.25.4
joycon: 3.1.1
picocolors: 1.1.1
postcss-load-config: 6.0.1(postcss@8.5.3)(tsx@4.19.4)(yaml@2.7.1)
@ -6749,7 +6559,7 @@ snapshots:
tsx@4.19.4:
dependencies:
esbuild: 0.25.3
esbuild: 0.25.4
get-tsconfig: 4.10.0
optionalDependencies:
fsevents: 2.3.3
@ -6785,8 +6595,6 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
type-fest@4.40.1: {}
type-fest@4.41.0: {}
type-is@2.0.1:
@ -6877,7 +6685,7 @@ snapshots:
vite-node@3.1.3(@types/node@22.15.18)(tsx@4.19.4)(yaml@2.7.1):
dependencies:
cac: 6.7.14
debug: 4.4.0
debug: 4.4.1
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.3.3(@types/node@22.15.18)(tsx@4.19.4)(yaml@2.7.1)
@ -6908,7 +6716,7 @@ snapshots:
vite@6.3.3(@types/node@22.15.18)(tsx@4.19.4)(yaml@2.7.1):
dependencies:
esbuild: 0.25.3
esbuild: 0.25.4
fdir: 6.4.4(picomatch@4.0.2)
picomatch: 4.0.2
postcss: 8.5.3
@ -6930,7 +6738,7 @@ snapshots:
'@vitest/spy': 3.1.3
'@vitest/utils': 3.1.3
chai: 5.2.0
debug: 4.4.0
debug: 4.4.1
expect-type: 1.2.1
magic-string: 0.30.17
pathe: 2.0.3

Wyświetl plik

@ -5,6 +5,26 @@
# Agentic <!-- omit from toc -->
## TODO
- stripe
- plans => prices
- monthly vs yearly prices
- re-add custom metrics
- re-add coupons
- declarative json-based pricing
- like https://github.com/tierrun/tier and Saasify
- https://github.com/tierrun/tier/blob/main/pricing/schema.json
- https://blog.tier.run/tier-hello-world-demo
- auth
- decide on approach for auth
- built-in, first-party, tight coupling
- https://github.com/toolbeam/openauth
- https://github.com/aipotheosis-labs/aci/tree/main/backend/apps
- https://github.com/NangoHQ/nango
- https://github.com/transitive-bullshit?submit=Search&q=oauth&tab=stars&type=&sort=&direction=&submit=Search
- clerk / workos / auth0
## License
UNLICENSED PROPRIETARY © [Agentic](https://x.com/transitive_bs)