123 lines
3.8 KiB
TypeScript
123 lines
3.8 KiB
TypeScript
// schema: boundary-only — TenantStrategySpecSchema is exposed for
|
|
// consumers building stack-specific factories on top of the plugin's
|
|
// option shape. The plugin itself parses options via
|
|
// TenantPluginOptionsSchema (which embeds the strategy schema), so
|
|
// `TenantStrategySpecSchema.parse(...)` does not appear at the plugin's
|
|
// own call sites.
|
|
import z from "zod";
|
|
|
|
/**
|
|
* Strategy spec schemas — one per resolver type. Composed into a Zod
|
|
* discriminated union so the plugin can route each spec to the right
|
|
* concrete `TenantResolver` implementation.
|
|
*/
|
|
const HeaderStrategySchema = z.object({
|
|
type: z.literal("header"),
|
|
headerName: z.string().min(1),
|
|
});
|
|
|
|
const SubdomainStrategySchema = z.object({
|
|
type: z.literal("subdomain"),
|
|
baseDomain: z.string().min(1),
|
|
});
|
|
|
|
const DomainStrategySchema = z.object({ type: z.literal("domain") });
|
|
|
|
const PathStrategySchema = z.object({
|
|
type: z.literal("path"),
|
|
prefix: z.string().regex(/^\//u, "prefix must start with '/'"),
|
|
});
|
|
|
|
const JWTStrategySchema = z.object({
|
|
type: z.literal("jwt"),
|
|
claimName: z.string().min(1),
|
|
});
|
|
|
|
const QueryStrategySchema = z.object({
|
|
type: z.literal("query"),
|
|
paramName: z.string().min(1),
|
|
});
|
|
|
|
const CookieStrategySchema = z.object({
|
|
type: z.literal("cookie"),
|
|
cookieName: z.string().min(1),
|
|
});
|
|
|
|
export const TenantStrategySpecSchema = z.discriminatedUnion("type", [
|
|
HeaderStrategySchema,
|
|
SubdomainStrategySchema,
|
|
DomainStrategySchema,
|
|
PathStrategySchema,
|
|
JWTStrategySchema,
|
|
QueryStrategySchema,
|
|
CookieStrategySchema,
|
|
]);
|
|
|
|
const CacheSchema = z.object({
|
|
ttlMs: z.number().int().positive(),
|
|
negativeTtlMs: z.number().int().positive().optional(),
|
|
maxEntries: z.number().int().positive().optional(),
|
|
});
|
|
|
|
const RateLimitSchema = z.object({
|
|
maxRequests: z.number().int().positive(),
|
|
windowMs: z.number().int().positive(),
|
|
store: z
|
|
.custom<unknown>(
|
|
(val) => val !== null && typeof val === "object",
|
|
"store must be a RateLimitStore object"
|
|
)
|
|
.optional(),
|
|
keyPrefix: z.string().min(1).optional(),
|
|
});
|
|
|
|
const StoreSchema = z.custom<unknown>(
|
|
(val) =>
|
|
val !== null &&
|
|
typeof val === "object" &&
|
|
typeof (val as { getById?: unknown }).getById === "function" &&
|
|
typeof (val as { create?: unknown }).create === "function",
|
|
"store must implement the TenantStore interface (getById/create/update/delete)"
|
|
);
|
|
|
|
const LoggerSchema = z.custom<unknown>(
|
|
(val) =>
|
|
val !== null &&
|
|
typeof val === "object" &&
|
|
typeof (val as { info?: unknown }).info === "function",
|
|
"logger must be a LoggerLike with debug/info/warn/error"
|
|
);
|
|
|
|
/**
|
|
* Zod schema for `TenantPluginOptions`. Validated by `createTenantPlugin`
|
|
* at the factory boundary — invalid input throws
|
|
* `TenantPluginError(INVALID_INPUT)` carrying the `ZodError` as `cause`.
|
|
*
|
|
* `routePrefix` MUST start with `/` and MUST NOT end with `/` (unless it
|
|
* is exactly `/`).
|
|
*/
|
|
export const TenantPluginOptionsSchema = z.object({
|
|
strategies: z.array(TenantStrategySpecSchema).min(1),
|
|
cache: CacheSchema.optional(),
|
|
required: z.boolean().default(false),
|
|
excludePaths: z.array(z.string()).optional(),
|
|
allowSuspended: z.boolean().default(false),
|
|
routePrefix: z
|
|
.string()
|
|
.regex(/^\/[A-Za-z0-9\-_/]*[A-Za-z0-9\-_]$|^\/$/u, {
|
|
message:
|
|
"routePrefix must start with '/' and not end with '/' (unless it is exactly '/')",
|
|
})
|
|
.default("/tenants"),
|
|
store: StoreSchema.optional(),
|
|
logger: LoggerSchema.optional(),
|
|
rateLimit: RateLimitSchema.optional(),
|
|
});
|
|
|
|
export type TenantPluginOptionsInput = z.input<
|
|
typeof TenantPluginOptionsSchema
|
|
>;
|
|
export type TenantPluginOptionsResolved = z.output<
|
|
typeof TenantPluginOptionsSchema
|
|
>;
|