@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
TypeScript
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 };