UNPKG

@eggermarc/better-auth-usage

Version:

**⚠️ Warning!** This package is a **work in progress**! Expect breaking changes and functionality changes.

905 lines (899 loc) 35 kB
import * as better_auth from 'better-auth'; import * as zod from 'zod'; import { z } from 'zod'; declare const customerSchema: z.ZodObject<{ referenceId: z.ZodString; referenceType: z.ZodString; email: z.ZodOptional<z.ZodString>; name: z.ZodOptional<z.ZodString>; overrideKey: z.ZodOptional<z.ZodString>; }, z.core.$strip>; /** * Core Customer type used across the plugin. * * - `referenceId`: Unique ID of the customer (e.g. UUID, tenant ID). * - `referenceType`: Logical grouping or type of reference (e.g. "org", "user"). * - `email` / `name`: Optional metadata for identification. * */ type Customer = z.infer<typeof customerSchema>; /** * Represents the deltas of usage for a single operation. * * - `beforeAmount`: Usage count before consumption. * - `afterAmount`: Usage count after consumption. * - `amount`: Amount consumed in this operation. */ type UsageData = { beforeAmount: number; afterAmount: number; amount: number; }; /** * Feature definition. * * Each feature represents a quota, limit, or tracked resource * that customers can consume. */ type Feature = { /** * Unique identifier of the feature (e.g. `"api-tokens"`). */ key: string; /** * Maximum allowed usage for this feature. */ maxLimit?: number; /** * Minimum allowed usage for this feature. */ minLimit?: number; /** * Optional descriptive metadata (could be displayed in UI). */ details?: string[]; /** * Associated Stripe product/price ID (for billing integration). */ stripeId?: string; /** * Defines how often the feature usage resets. */ reset?: ResetType; /** * Optional numeric reset modifier (e.g. reset every 3 days). */ resetValue?: number; /** * Lifecycle hooks triggered before/after consumption. * Useful for enforcing business rules or side effects. */ hooks?: { /** * Executed *before* consumption is persisted. */ before?: (props: { usage: UsageData; customer: Customer; feature: Feature; }) => Promise<void> | void; /** * Executed *after* consumption is persisted. */ after?: (props: { usage: UsageData; customer: Customer; feature: Feature; }) => Promise<void> | void; }; /** * Optional authorization function that decides if a given * customer is allowed to consume this feature. */ authorizeReference?: <BT>(params: { body: BT; customer: Customer; }) => Promise<boolean> | boolean; }; /** * Dictionary of features keyed by their unique `key`. */ type Features = Record<string, Feature>; /** * Overrides allow customizing features at a customer or plan level. * * Example: * ```ts * { * "customer-123": { * features: { * "api-tokens": { maxLimit: 5000 } * } * } * } * ``` */ type Overrides = Record<string, { features: Record<string, Partial<Omit<Feature, "key">>>; }>; /** * Valid reset intervals for features. */ type ResetType = "hourly" | "6-hourly" | "daily" | "weekly" | "monthly" | "quarterly" | "yearly" | "never"; /** * Possible states when checking consumption limits. */ type ConsumptionLimitType = "in-limit" | "above-max-limit" | "below-min-limit"; /** * Options for configuring the usage plugin. * * - `features`: Required list of all trackable features. * - `overrides`: Optional per-customer or per-plan overrides. * - `customers`: Optional pre-registered customer dictionary. */ interface UsageOptions { features: Features; overrides?: Overrides; } /** * Creates an authentication middleware that authorizes a reference against a resolved feature. * * @param options - Configuration for the middleware * @param options.features - Registered features used when resolving the feature referenced by the request * @param options.overrides - Overrides that can alter feature resolution * @returns A middleware function that, when `ctx.body.referenceId` and `ctx.body.featureKey` are present, resolves the feature and enforces its `authorizeReference` check. * @throws APIError with type `"UNAUTHORIZED"` if the resolved feature's `authorizeReference` returns `false` */ declare function usageMiddleware({ features, overrides }: UsageOptions): (inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<void>; /** * Creates a BetterAuth plugin that provides usage metering, customer schema, and endpoints for feature consumption, checks, listing, upserting customers, and syncing resets. * * @param options - Configuration and overrides for the usage plugin and its endpoint factories * @returns A BetterAuthPlugin exposing `usage` and `customer` schemas and endpoints: `getFeature`, `consumeFeature`, `listFeatures`, `checkUsage`, `upsertCustomer`, and `syncUsage` */ declare function usage<O extends UsageOptions = UsageOptions>(options: O): { id: "@eggermarc/usage"; schema: { usage: { fields: { referenceId: { type: "string"; required: true; input: true; }; referenceType: { type: "string"; required: true; input: true; }; feature: { type: "string"; required: true; input: true; }; amount: { type: "number"; required: true; input: true; }; afterAmount: { type: "number"; required: true; input: true; }; event: { type: "string"; required: true; }; lastResetAt: { type: "date"; required: true; }; createdAt: { type: "date"; required: true; }; }; }; customer: { fields: { referenceId: { type: "string"; required: true; input: true; unique: true; }; referenceType: { type: "string"; required: true; input: true; }; email: { type: "string"; required: false; input: true; }; name: { type: "string"; required: false; input: true; }; }; }; }; endpoints: { /** * Get feature metadata (merged with overrides if provided). */ getFeature: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: { body: { overrideKey?: string | undefined; }; } & { method?: "GET" | undefined; } & { query?: Record<string, any> | undefined; } & { params: { featureKey: string; }; } & { request?: Request; } & { headers?: HeadersInit; } & { asResponse?: boolean; returnHeaders?: boolean; use?: better_auth.Middleware[]; path?: string; } & { asResponse?: AsResponse | undefined; returnHeaders?: ReturnHeaders | undefined; }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? { headers: Headers; response: { feature: { key: string; maxLimit?: number; minLimit?: number; details?: string[]; stripeId?: string; reset?: ResetType; resetValue?: number; hooks?: { before?: (props: { usage: UsageData; customer: Customer; feature: Feature; }) => Promise<void> | void; after?: (props: { usage: UsageData; customer: Customer; feature: Feature; }) => Promise<void> | void; }; authorizeReference?: <BT>(params: { body: BT; customer: Customer; }) => Promise<boolean> | boolean; }; }; } : { feature: { key: string; maxLimit?: number; minLimit?: number; details?: string[]; stripeId?: string; reset?: ResetType; resetValue?: number; hooks?: { before?: (props: { usage: UsageData; customer: Customer; feature: Feature; }) => Promise<void> | void; after?: (props: { usage: UsageData; customer: Customer; feature: Feature; }) => Promise<void> | void; }; authorizeReference?: <BT>(params: { body: BT; customer: Customer; }) => Promise<boolean> | boolean; }; }>; options: { method: "GET"; body: zod.ZodObject<{ overrideKey: zod.ZodOptional<zod.ZodString>; }, better_auth.$strip>; metadata: { openapi: { description: string; parameters: { in: "path"; name: string; required: true; schema: { type: "string"; }; description: string; }[]; requestBody: { required: boolean; content: { "application/json": { schema: { type: "object"; properties: { overrideKey: { type: string; }; }; }; }; }; }; responses: { 200: { description: string; content: { "application/json": { schema: { type: "object"; }; }; }; }; 404: { description: string; }; }; }; }; } & { use: any[]; }; path: "/usage/features/:featureKey"; }; /** * Consume (meter) a feature for a given referenceId. * - runs before hook * - inserts usage row (adapter) * - runs after hook */ consumeFeature: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: { body: { featureKey: string; amount: number; referenceId: string; overrideKey?: string | undefined; event?: string | undefined; }; } & { method?: "POST" | undefined; } & { query?: Record<string, any> | undefined; } & { params?: Record<string, any>; } & { request?: Request; } & { headers?: HeadersInit; } & { asResponse?: boolean; returnHeaders?: boolean; use?: better_auth.Middleware[]; path?: string; } & { asResponse?: AsResponse | undefined; returnHeaders?: ReturnHeaders | undefined; }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? { headers: Headers; response: { referenceId: string; referenceType: string; createdAt: Date; lastResetAt: Date; amount: number; afterAmount: number; feature: string; event?: string | undefined; }; } : { referenceId: string; referenceType: string; createdAt: Date; lastResetAt: Date; amount: number; afterAmount: number; feature: string; event?: string | undefined; }>; options: { method: "POST"; middleware: (((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{ session: { session: Record<string, any> & { id: string; createdAt: Date; updatedAt: Date; userId: string; expiresAt: Date; token: string; ipAddress?: string | null | undefined; userAgent?: string | null | undefined; }; user: Record<string, any> & { id: string; createdAt: Date; updatedAt: Date; email: string; emailVerified: boolean; name: string; image?: string | null | undefined; }; }; }>) | ((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<void>))[]; body: zod.ZodObject<{ featureKey: zod.ZodString; overrideKey: zod.ZodOptional<zod.ZodString>; amount: zod.ZodNumber; referenceId: zod.ZodString; event: zod.ZodDefault<zod.ZodString>; }, better_auth.$strip>; metadata: { openapi: { description: string; requestBody: { required: boolean; content: { "application/json": { schema: { type: "object"; properties: { featureKey: { type: string; description: string; }; overrideKey: { type: string; description: string; }; amount: { type: string; description: string; }; referenceId: { type: string; description: string; }; event: { type: string; description: string; }; }; required: string[]; }; }; }; }; responses: { 200: { description: string; content: { "application/json": { schema: { type: "object"; }; }; }; }; 404: { description: string; }; 401: { description: string; }; }; }; }; } & { use: any[]; }; path: "/usage/consume"; }; listFeatures: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0?: ({ body?: undefined; } & { method?: "GET" | undefined; } & { query?: Record<string, any> | undefined; } & { params?: Record<string, any>; } & { request?: Request; } & { headers?: HeadersInit; } & { asResponse?: boolean; returnHeaders?: boolean; use?: better_auth.Middleware[]; path?: string; } & { asResponse?: AsResponse | undefined; returnHeaders?: ReturnHeaders | undefined; }) | undefined): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? { headers: Headers; response: { featureKey: string; details: string[] | undefined; }[]; } : { featureKey: string; details: string[] | undefined; }[]>; options: { method: "GET"; metadata: { openapi: { description: string; responses: { 200: { description: string; content: { "application/json": { schema: { type: "array"; items: { type: string; properties: { featureKey: { type: string; }; details: { type: string; items: { type: string; }; }; }; required: string[]; }; }; }; }; }; }; }; }; } & { use: any[]; }; path: "/usage/features"; }; /** * Check usage limit for a feature for a specific reference. * Returns a small enum ("in-limit"|"above-limit"|"below-limit") based on checkLimit util. */ checkUsage: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: { body: { referenceId: string; featureKey: string; overrideKey?: string | undefined; }; } & { method?: "POST" | undefined; } & { query?: Record<string, any> | undefined; } & { params?: Record<string, any>; } & { request?: Request; } & { headers?: HeadersInit; } & { asResponse?: boolean; returnHeaders?: boolean; use?: better_auth.Middleware[]; path?: string; } & { asResponse?: AsResponse | undefined; returnHeaders?: ReturnHeaders | undefined; }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? { headers: Headers; response: ConsumptionLimitType; } : ConsumptionLimitType>; options: { method: "POST"; middleware: (((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{ session: { session: Record<string, any> & { id: string; createdAt: Date; updatedAt: Date; userId: string; expiresAt: Date; token: string; ipAddress?: string | null | undefined; userAgent?: string | null | undefined; }; user: Record<string, any> & { id: string; createdAt: Date; updatedAt: Date; email: string; emailVerified: boolean; name: string; image?: string | null | undefined; }; }; }>) | typeof usageMiddleware)[]; body: zod.ZodObject<{ referenceId: zod.ZodString; featureKey: zod.ZodString; overrideKey: zod.ZodOptional<zod.ZodString>; }, better_auth.$strip>; metadata: { openapi: { description: string; requestBody: { required: boolean; content: { "application/json": { schema: { type: "object"; properties: { referenceId: { type: string; }; featureKey: { type: string; }; overrideKey: { type: string; }; }; required: string[]; }; }; }; }; responses: { 200: { description: string; }; }; }; }; } & { use: any[]; }; path: "/usage/check"; }; upsertCustomer: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: { body: { referenceId: string; referenceType: string; email?: string | undefined; name?: string | undefined; overrideKey?: string | undefined; }; } & { method?: "POST" | undefined; } & { query?: Record<string, any> | undefined; } & { params?: Record<string, any>; } & { request?: Request; } & { headers?: HeadersInit; } & { asResponse?: boolean; returnHeaders?: boolean; use?: better_auth.Middleware[]; path?: string; } & { asResponse?: AsResponse | undefined; returnHeaders?: ReturnHeaders | undefined; }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? { headers: Headers; response: { referenceId: string; referenceType: string; email?: string | undefined; name?: string | undefined; overrideKey?: string | undefined; } | null; } : { referenceId: string; referenceType: string; email?: string | undefined; name?: string | undefined; overrideKey?: string | undefined; } | null>; options: { method: "POST"; body: zod.ZodObject<{ referenceId: zod.ZodString; referenceType: zod.ZodString; email: zod.ZodOptional<zod.ZodString>; name: zod.ZodOptional<zod.ZodString>; overrideKey: zod.ZodOptional<zod.ZodString>; }, better_auth.$strip>; middleware: ((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{ session: { session: Record<string, any> & { id: string; createdAt: Date; updatedAt: Date; userId: string; expiresAt: Date; token: string; ipAddress?: string | null | undefined; userAgent?: string | null | undefined; }; user: Record<string, any> & { id: string; createdAt: Date; updatedAt: Date; email: string; emailVerified: boolean; name: string; image?: string | null | undefined; }; }; }>)[]; metadata: { openapi: { description: string; requestBody: { required: boolean; content: { "application/json": { schema: { type: "object"; properties: { referenceId: { type: string; }; referenceType: { type: string; }; name: { type: string; }; email: { type: string; }; overrideKey: { type: string; }; }; required: string[]; }; }; }; responses: { 200: { description: string; }; }; }; }; }; } & { use: any[]; }; path: "/usage/upsert-customer"; }; /** * Sync usage according to feature.reset rules. * This will insert a reset event row with zeroed usage if the feature requires it. * * Note: you might prefer running this as a background job for many customers, * rather than via an endpoint. */ syncUsage: { <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: { body: { referenceId: string; featureKey: string; overrideKey?: string | undefined; }; } & { method?: "POST" | undefined; } & { query?: Record<string, any> | undefined; } & { params?: Record<string, any>; } & { request?: Request; } & { headers?: HeadersInit; } & { asResponse?: boolean; returnHeaders?: boolean; use?: better_auth.Middleware[]; path?: string; } & { asResponse?: AsResponse | undefined; returnHeaders?: ReturnHeaders | undefined; }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? { headers: Headers; response: { referenceId: string; referenceType: string; createdAt: Date; lastResetAt: Date; amount: number; afterAmount: number; feature: string; event?: string | undefined; } | undefined; } : { referenceId: string; referenceType: string; createdAt: Date; lastResetAt: Date; amount: number; afterAmount: number; feature: string; event?: string | undefined; } | undefined>; options: { method: "POST"; body: zod.ZodObject<{ referenceId: zod.ZodString; featureKey: zod.ZodString; overrideKey: zod.ZodOptional<zod.ZodString>; }, better_auth.$strip>; metadata: { openapi: { description: string; requestBody: { required: boolean; content: { "application/json": { schema: { type: "object"; properties: { referenceId: { type: string; }; featureKey: { type: string; }; }; required: string[]; }; }; }; }; responses: { 200: { description: string; }; 404: { description: string; }; }; }; }; } & { use: any[]; }; path: "/usage/sync"; }; }; }; export { usage };