@vertz/server 0.2.14 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +962 -191
  2. package/dist/index.js +3273 -422
  3. package/package.json +5 -4
package/dist/index.d.ts CHANGED
@@ -1,13 +1,71 @@
1
- import { AccumulateProvides, AppBuilder as AppBuilder2, AppConfig as AppConfig2, CorsConfig, Ctx, DeepReadonly, Deps, EnvConfig, HandlerCtx, HttpMethod, HttpStatusCode, Infer, InferSchema, ListenOptions, MiddlewareDef, NamedMiddlewareDef, RawRequest, ServerAdapter, ServerHandle } from "@vertz/core";
1
+ import { AccumulateProvides, AppBuilder as AppBuilder2, AppConfig as AppConfig2, CorsConfig, Ctx, DeepReadonly, Deps, EnvConfig, HandlerCtx, HttpMethod, HttpStatusCode, Infer as Infer2, InferSchema, ListenOptions, MiddlewareDef, NamedMiddlewareDef, RawRequest, ServerAdapter, ServerHandle } from "@vertz/core";
2
2
  import { BadRequestException, ConflictException, createEnv, createImmutableProxy, createMiddleware, deepFreeze, ForbiddenException, InternalServerErrorException, makeImmutable, NotFoundException, ServiceUnavailableException, UnauthorizedException, ValidationException, VertzException, vertz } from "@vertz/core";
3
3
  import { ModelEntry } from "@vertz/db";
4
4
  import { AuthError, Result } from "@vertz/errors";
5
+ import { Infer, ObjectSchema, StringSchema } from "@vertz/schema";
5
6
  /**
6
- * defineAccess()Phase 6 RBAC & Access Control configuration
7
+ * rules.* builders declarative access rule data structures.
7
8
  *
8
- * Replaces createAccess() with hierarchical RBAC: resources form trees,
9
- * roles inherit down the hierarchy, and entitlements map to roles/plans/flags.
9
+ * These are pure data structures with no evaluation logic.
10
+ * The access context evaluates them at runtime (sub-phase 4).
10
11
  */
12
+ interface RoleRule {
13
+ readonly type: "role";
14
+ readonly roles: readonly string[];
15
+ }
16
+ interface EntitlementRule {
17
+ readonly type: "entitlement";
18
+ readonly entitlement: string;
19
+ }
20
+ interface WhereRule {
21
+ readonly type: "where";
22
+ readonly conditions: Record<string, unknown>;
23
+ }
24
+ interface AllRule {
25
+ readonly type: "all";
26
+ readonly rules: readonly AccessRule[];
27
+ }
28
+ interface AnyRule {
29
+ readonly type: "any";
30
+ readonly rules: readonly AccessRule[];
31
+ }
32
+ interface AuthenticatedRule {
33
+ readonly type: "authenticated";
34
+ }
35
+ interface PublicRule {
36
+ readonly type: "public";
37
+ }
38
+ interface FvaRule {
39
+ readonly type: "fva";
40
+ readonly maxAge: number;
41
+ }
42
+ type AccessRule = PublicRule | RoleRule | EntitlementRule | WhereRule | AllRule | AnyRule | AuthenticatedRule | FvaRule;
43
+ interface UserMarker {
44
+ readonly __marker: string;
45
+ }
46
+ declare const rules: {
47
+ /** Endpoint is public — no authentication required */
48
+ public: PublicRule;
49
+ /** User has at least one of the specified roles (OR) */
50
+ role(...roleNames: string[]): RoleRule;
51
+ /** User has the specified entitlement (resolves role+plan+flag from defineAccess config) */
52
+ entitlement(name: string): EntitlementRule;
53
+ /** Row-level condition — DB query syntax. Use rules.user.id for dynamic user markers */
54
+ where(conditions: Record<string, unknown>): WhereRule;
55
+ /** All sub-rules must pass (AND) */
56
+ all(...ruleList: AccessRule[]): AllRule;
57
+ /** At least one sub-rule must pass (OR) */
58
+ any(...ruleList: AccessRule[]): AnyRule;
59
+ /** User must be authenticated (no specific role required) */
60
+ authenticated(): AuthenticatedRule;
61
+ /** User must have verified MFA within maxAge seconds */
62
+ fva(maxAge: number): FvaRule;
63
+ /** Declarative user markers — resolved at evaluation time */
64
+ user: {
65
+ readonly id: UserMarker;
66
+ readonly tenantId: UserMarker;
67
+ };
68
+ };
11
69
  /** Denial reasons ordered by actionability (most actionable first) */
12
70
  type DenialReason = "plan_required" | "role_required" | "limit_reached" | "flag_disabled" | "hierarchy_denied" | "step_up_required" | "not_authenticated";
13
71
  /** Metadata attached to denial reasons */
@@ -16,9 +74,12 @@ interface DenialMeta {
16
74
  requiredRoles?: string[];
17
75
  disabledFlags?: string[];
18
76
  limit?: {
77
+ key?: string;
19
78
  max: number;
20
79
  consumed: number;
21
80
  remaining: number;
81
+ /** True when the tenant is consuming beyond the limit under overage billing */
82
+ overage?: boolean;
22
83
  };
23
84
  fvaMaxAge?: number;
24
85
  }
@@ -29,43 +90,120 @@ interface AccessCheckResult {
29
90
  reason?: DenialReason;
30
91
  meta?: DenialMeta;
31
92
  }
32
- /** Entitlement definition — maps to roles, optional plans and flags */
33
- interface EntitlementDef {
34
- roles: string[];
35
- plans?: string[];
36
- flags?: string[];
37
- }
38
93
  /** Billing period for plan limits */
39
- type BillingPeriod = "month" | "day" | "hour";
40
- /** Limit definition within a plan */
94
+ type BillingPeriod = "month" | "day" | "hour" | "quarter" | "year";
95
+ /** Valid price intervals for plans */
96
+ type PriceInterval = "month" | "quarter" | "year" | "one_off";
97
+ /** Plan price definition */
98
+ interface PlanPrice {
99
+ amount: number;
100
+ interval: PriceInterval;
101
+ }
102
+ /** Overage billing configuration for a limit */
103
+ interface OverageConfig {
104
+ /** Price per unit of overage (e.g., 0.01 for $0.01 per extra unit) */
105
+ amount: number;
106
+ /** Units per charge (e.g., 1 = charge per unit, 100 = charge per 100 units) */
107
+ per: number;
108
+ /** Maximum overage charge per period (safety cap). Omit for no cap. */
109
+ cap?: number;
110
+ }
111
+ /** Limit definition within a plan — gates an entitlement with optional scoping */
41
112
  interface LimitDef {
42
- per: BillingPeriod;
43
113
  max: number;
44
- }
45
- /** Plan definition which entitlements are included and their usage limits */
114
+ gates: string;
115
+ /** Billing period for the limit. Omitted = lifetime (no reset). */
116
+ per?: BillingPeriod;
117
+ /** Entity scope — omitted = tenant-level, string = per entity instance */
118
+ scope?: string;
119
+ /** Overage billing: allow usage beyond limit at per-unit cost */
120
+ overage?: OverageConfig;
121
+ }
122
+ /** Add-on compatibility — restricts which base plans an add-on can be attached to */
123
+ interface AddOnRequires {
124
+ group: string;
125
+ plans: readonly string[] | string[];
126
+ }
127
+ /** Grace period duration for grandfathering policy */
128
+ type GraceDuration = "1m" | "3m" | "6m" | "12m" | "indefinite";
129
+ /** Grandfathering policy for a plan */
130
+ interface GrandfatheringPolicy {
131
+ grace?: GraceDuration;
132
+ }
133
+ /** Plan definition — features, limits, metadata, and billing */
46
134
  interface PlanDef {
47
- entitlements: readonly string[] | string[];
135
+ title?: string;
136
+ description?: string;
137
+ group?: string;
138
+ addOn?: boolean;
139
+ price?: PlanPrice;
140
+ features?: readonly string[] | string[];
48
141
  limits?: Record<string, LimitDef>;
142
+ /** Grandfathering policy — how long existing tenants keep old version on plan change */
143
+ grandfathering?: GrandfatheringPolicy;
144
+ /** Add-on only: restricts attachment to tenants on compatible base plans */
145
+ requires?: AddOnRequires;
146
+ }
147
+ /** Entity definition — roles and optional inheritance from parent entity roles */
148
+ interface EntityDef {
149
+ roles: readonly string[] | string[];
150
+ inherits?: Record<string, string>;
151
+ }
152
+ /** Resolved entitlement definition — roles + optional rules */
153
+ interface EntitlementDef {
154
+ roles: string[];
155
+ rules?: AccessRule[];
156
+ flags?: string[];
157
+ /** Plan names that gate this entitlement (evaluated in Layer 4) */
158
+ plans?: string[];
49
159
  }
160
+ /** Entitlement callback context — provides `where()` and `user` for attribute rules */
161
+ interface RuleContext {
162
+ where(conditions: Record<string, unknown>): AccessRule;
163
+ user: {
164
+ readonly id: {
165
+ readonly __marker: string;
166
+ };
167
+ readonly tenantId: {
168
+ readonly __marker: string;
169
+ };
170
+ };
171
+ }
172
+ /** Entitlement value: object or callback returning object */
173
+ type EntitlementValue = EntitlementDef | ((r: RuleContext) => EntitlementDef);
50
174
  /** The input config for defineAccess() */
51
175
  interface DefineAccessInput {
52
- hierarchy: string[];
53
- roles: Record<string, string[]>;
54
- inheritance?: Record<string, Record<string, string>>;
55
- entitlements: Record<string, EntitlementDef>;
176
+ entities: Record<string, EntityDef>;
177
+ entitlements: Record<string, EntitlementValue>;
56
178
  plans?: Record<string, PlanDef>;
57
179
  /** Fallback plan name when an org's plan expires. Defaults to 'free'. */
58
180
  defaultPlan?: string;
59
181
  }
60
182
  /** The frozen config returned by defineAccess() */
61
183
  interface AccessDefinition {
184
+ /** Inferred hierarchy from inherits declarations */
62
185
  readonly hierarchy: readonly string[];
186
+ /** Entities keyed by name */
187
+ readonly entities: Readonly<Record<string, Readonly<EntityDef>>>;
188
+ /** Roles per entity (derived from entities for downstream compat) */
63
189
  readonly roles: Readonly<Record<string, readonly string[]>>;
190
+ /** Inheritance map per entity (derived from entities for downstream compat) */
64
191
  readonly inheritance: Readonly<Record<string, Readonly<Record<string, string>>>>;
192
+ /** Resolved entitlements */
65
193
  readonly entitlements: Readonly<Record<string, Readonly<EntitlementDef>>>;
66
194
  readonly plans?: Readonly<Record<string, Readonly<PlanDef>>>;
67
195
  /** Fallback plan name when an org's plan expires. Defaults to 'free'. */
68
196
  readonly defaultPlan?: string;
197
+ /**
198
+ * Computed: Set of entitlements that are gated by at least one plan's `features`.
199
+ * If an entitlement is in this set, a plan check is required during `can()`.
200
+ */
201
+ readonly _planGatedEntitlements: ReadonlySet<string>;
202
+ /**
203
+ * Computed: Map from entitlement to all limit keys that gate it.
204
+ * Used during can() to find all limits that need to pass for an entitlement.
205
+ */
206
+ readonly _entitlementToLimitKeys: Readonly<Record<string, readonly string[]>>;
69
207
  }
70
208
  declare function defineAccess(input: DefineAccessInput): AccessDefinition;
71
209
  /**
@@ -143,60 +281,77 @@ declare class InMemoryRoleAssignmentStore implements RoleAssignmentStore {
143
281
  * Used by Layer 1 of access context to gate entitlements on feature flags.
144
282
  */
145
283
  interface FlagStore {
146
- setFlag(orgId: string, flag: string, enabled: boolean): void;
147
- getFlag(orgId: string, flag: string): boolean;
148
- getFlags(orgId: string): Record<string, boolean>;
284
+ setFlag(tenantId: string, flag: string, enabled: boolean): void;
285
+ getFlag(tenantId: string, flag: string): boolean;
286
+ getFlags(tenantId: string): Record<string, boolean>;
149
287
  }
150
288
  declare class InMemoryFlagStore implements FlagStore {
151
289
  private flags;
152
- setFlag(orgId: string, flag: string, enabled: boolean): void;
153
- getFlag(orgId: string, flag: string): boolean;
154
- getFlags(orgId: string): Record<string, boolean>;
290
+ setFlag(tenantId: string, flag: string, enabled: boolean): void;
291
+ getFlag(tenantId: string, flag: string): boolean;
292
+ getFlags(tenantId: string): Record<string, boolean>;
155
293
  }
156
- /**
157
- * PlanStore — org-level plan assignments with overrides.
158
- *
159
- * Stores which plan an organization is on, when it started,
160
- * optional expiration, and per-customer limit overrides.
161
- */
162
- /** Per-customer limit override. Only affects the cap, not the billing period. */
294
+ /** Per-tenant limit override. Only affects the cap, not the billing period. */
163
295
  interface LimitOverride {
164
296
  max: number;
165
297
  }
166
- interface OrgPlan {
167
- orgId: string;
298
+ interface Subscription {
299
+ tenantId: string;
168
300
  planId: string;
169
301
  startedAt: Date;
170
302
  expiresAt: Date | null;
171
303
  overrides: Record<string, LimitOverride>;
172
304
  }
173
- interface PlanStore {
305
+ interface SubscriptionStore {
174
306
  /**
175
- * Assign a plan to an org. Resets per-customer overrides (overrides are plan-specific).
176
- * To preserve overrides across plan changes, re-apply them after calling assignPlan().
307
+ * Assign a plan to a tenant. Resets per-tenant overrides (overrides are plan-specific).
308
+ * To preserve overrides across plan changes, re-apply them after calling assign().
177
309
  */
178
- assignPlan(orgId: string, planId: string, startedAt?: Date, expiresAt?: Date | null): Promise<void>;
179
- getPlan(orgId: string): Promise<OrgPlan | null>;
180
- updateOverrides(orgId: string, overrides: Record<string, LimitOverride>): Promise<void>;
181
- removePlan(orgId: string): Promise<void>;
310
+ assign(tenantId: string, planId: string, startedAt?: Date, expiresAt?: Date | null): Promise<void>;
311
+ get(tenantId: string): Promise<Subscription | null>;
312
+ updateOverrides(tenantId: string, overrides: Record<string, LimitOverride>): Promise<void>;
313
+ remove(tenantId: string): Promise<void>;
314
+ /** Attach an add-on to a tenant. */
315
+ attachAddOn?(tenantId: string, addOnId: string): Promise<void>;
316
+ /** Detach an add-on from a tenant. */
317
+ detachAddOn?(tenantId: string, addOnId: string): Promise<void>;
318
+ /** Get all active add-on IDs for a tenant. */
319
+ getAddOns?(tenantId: string): Promise<string[]>;
320
+ /** List all tenant IDs assigned to a specific plan. */
321
+ listByPlan?(planId: string): Promise<string[]>;
182
322
  dispose(): void;
183
323
  }
184
- declare class InMemoryPlanStore implements PlanStore {
185
- private plans;
186
- assignPlan(orgId: string, planId: string, startedAt?: Date, expiresAt?: Date | null): Promise<void>;
187
- getPlan(orgId: string): Promise<OrgPlan | null>;
188
- updateOverrides(orgId: string, overrides: Record<string, LimitOverride>): Promise<void>;
189
- removePlan(orgId: string): Promise<void>;
324
+ declare class InMemorySubscriptionStore implements SubscriptionStore {
325
+ private subscriptions;
326
+ private addOns;
327
+ assign(tenantId: string, planId: string, startedAt?: Date, expiresAt?: Date | null): Promise<void>;
328
+ get(tenantId: string): Promise<Subscription | null>;
329
+ updateOverrides(tenantId: string, overrides: Record<string, LimitOverride>): Promise<void>;
330
+ remove(tenantId: string): Promise<void>;
331
+ attachAddOn(tenantId: string, addOnId: string): Promise<void>;
332
+ detachAddOn(tenantId: string, addOnId: string): Promise<void>;
333
+ getAddOns(tenantId: string): Promise<string[]>;
334
+ listByPlan(planId: string): Promise<string[]>;
190
335
  dispose(): void;
191
336
  }
192
337
  /**
338
+ * Check if an add-on is compatible with a given base plan.
339
+ * Returns true if the add-on has no `requires` or if the plan is in the requires list.
340
+ */
341
+ declare function checkAddOnCompatibility(accessDef: AccessDefinition, addOnId: string, currentPlanId: string): boolean;
342
+ /**
343
+ * Get add-ons that are incompatible with a target plan.
344
+ * Used to flag incompatible add-ons when a tenant downgrades.
345
+ */
346
+ declare function getIncompatibleAddOns(accessDef: AccessDefinition, activeAddOnIds: string[], targetPlanId: string): string[];
347
+ /**
193
348
  * WalletStore — consumption tracking for plan-limited entitlements.
194
349
  *
195
- * Tracks per-org usage within billing periods with atomic
350
+ * Tracks per-tenant usage within billing periods with atomic
196
351
  * check-and-increment operations.
197
352
  */
198
353
  interface WalletEntry {
199
- orgId: string;
354
+ tenantId: string;
200
355
  entitlement: string;
201
356
  periodStart: Date;
202
357
  periodEnd: Date;
@@ -209,17 +364,99 @@ interface ConsumeResult {
209
364
  remaining: number;
210
365
  }
211
366
  interface WalletStore {
212
- consume(orgId: string, entitlement: string, periodStart: Date, periodEnd: Date, limit: number, amount?: number): Promise<ConsumeResult>;
213
- unconsume(orgId: string, entitlement: string, periodStart: Date, periodEnd: Date, amount?: number): Promise<void>;
214
- getConsumption(orgId: string, entitlement: string, periodStart: Date, periodEnd: Date): Promise<number>;
367
+ consume(tenantId: string, entitlement: string, periodStart: Date, periodEnd: Date, limit: number, amount?: number): Promise<ConsumeResult>;
368
+ unconsume(tenantId: string, entitlement: string, periodStart: Date, periodEnd: Date, amount?: number): Promise<void>;
369
+ getConsumption(tenantId: string, entitlement: string, periodStart: Date, periodEnd: Date): Promise<number>;
215
370
  dispose(): void;
216
371
  }
217
372
  declare class InMemoryWalletStore implements WalletStore {
218
373
  private entries;
219
374
  private key;
220
- consume(orgId: string, entitlement: string, periodStart: Date, periodEnd: Date, limit: number, amount?: number): Promise<ConsumeResult>;
221
- unconsume(orgId: string, entitlement: string, periodStart: Date, _periodEnd: Date, amount?: number): Promise<void>;
222
- getConsumption(orgId: string, entitlement: string, periodStart: Date, _periodEnd: Date): Promise<number>;
375
+ consume(tenantId: string, entitlement: string, periodStart: Date, periodEnd: Date, limit: number, amount?: number): Promise<ConsumeResult>;
376
+ unconsume(tenantId: string, entitlement: string, periodStart: Date, _periodEnd: Date, amount?: number): Promise<void>;
377
+ getConsumption(tenantId: string, entitlement: string, periodStart: Date, _periodEnd: Date): Promise<number>;
378
+ dispose(): void;
379
+ }
380
+ /** Limit override: add (additive) or max (hard cap) */
381
+ interface LimitOverrideDef {
382
+ add?: number;
383
+ max?: number;
384
+ }
385
+ /** Per-tenant overrides */
386
+ interface TenantOverrides {
387
+ features?: string[];
388
+ limits?: Record<string, LimitOverrideDef>;
389
+ }
390
+ interface OverrideStore {
391
+ set(tenantId: string, overrides: TenantOverrides): Promise<void>;
392
+ remove(tenantId: string, keys: {
393
+ features?: string[];
394
+ limits?: string[];
395
+ }): Promise<void>;
396
+ get(tenantId: string): Promise<TenantOverrides | null>;
397
+ dispose(): void;
398
+ }
399
+ declare class InMemoryOverrideStore implements OverrideStore {
400
+ private overrides;
401
+ set(tenantId: string, overrides: TenantOverrides): Promise<void>;
402
+ remove(tenantId: string, keys: {
403
+ features?: string[];
404
+ limits?: string[];
405
+ }): Promise<void>;
406
+ get(tenantId: string): Promise<TenantOverrides | null>;
407
+ dispose(): void;
408
+ }
409
+ /**
410
+ * Validate tenant overrides against the access definition.
411
+ * Throws on invalid limit keys, invalid feature names, or invalid max values.
412
+ */
413
+ declare function validateOverrides(accessDef: AccessDefinition, overrides: TenantOverrides): void;
414
+ /**
415
+ * Plan Version Store — tracks versioned snapshots of plan configurations.
416
+ *
417
+ * When a plan's config changes (hash differs), a new version is created
418
+ * with a snapshot of the plan's features, limits, and price at that point.
419
+ */
420
+ interface PlanSnapshot {
421
+ features: readonly string[] | string[];
422
+ limits: Record<string, unknown>;
423
+ price: {
424
+ amount: number;
425
+ interval: string;
426
+ } | null;
427
+ }
428
+ interface PlanVersionInfo {
429
+ planId: string;
430
+ version: number;
431
+ hash: string;
432
+ snapshot: PlanSnapshot;
433
+ createdAt: Date;
434
+ }
435
+ interface PlanVersionStore {
436
+ /** Create a new version for a plan. Returns the version number. */
437
+ createVersion(planId: string, hash: string, snapshot: PlanSnapshot): Promise<number>;
438
+ /** Get the current (latest) version number for a plan. Returns null if no versions exist. */
439
+ getCurrentVersion(planId: string): Promise<number | null>;
440
+ /** Get a specific version's info. Returns null if not found. */
441
+ getVersion(planId: string, version: number): Promise<PlanVersionInfo | null>;
442
+ /** Get the version number a tenant is on for a given plan. Returns null if not set. */
443
+ getTenantVersion(tenantId: string, planId: string): Promise<number | null>;
444
+ /** Set the version number a tenant is on for a given plan. */
445
+ setTenantVersion(tenantId: string, planId: string, version: number): Promise<void>;
446
+ /** Get the hash of the current (latest) version for a plan. Returns null if no versions. */
447
+ getCurrentHash(planId: string): Promise<string | null>;
448
+ /** Clean up resources. */
449
+ dispose(): void;
450
+ }
451
+ declare class InMemoryPlanVersionStore implements PlanVersionStore {
452
+ private versions;
453
+ private tenantVersions;
454
+ createVersion(planId: string, hash: string, snapshot: PlanSnapshot): Promise<number>;
455
+ getCurrentVersion(planId: string): Promise<number | null>;
456
+ getVersion(planId: string, version: number): Promise<PlanVersionInfo | null>;
457
+ getTenantVersion(tenantId: string, planId: string): Promise<number | null>;
458
+ setTenantVersion(tenantId: string, planId: string, version: number): Promise<void>;
459
+ getCurrentHash(planId: string): Promise<string | null>;
223
460
  dispose(): void;
224
461
  }
225
462
  interface ResourceRef {
@@ -235,25 +472,39 @@ interface AccessContextConfig {
235
472
  fva?: number;
236
473
  /** Flag store — required for Layer 1 feature flag checks */
237
474
  flagStore?: FlagStore;
238
- /** Plan store — required for Layer 4 plan checks */
239
- planStore?: PlanStore;
475
+ /** Subscription store — required for Layer 4 plan checks */
476
+ subscriptionStore?: SubscriptionStore;
240
477
  /** Wallet store — required for Layer 5 wallet checks and canAndConsume() */
241
478
  walletStore?: WalletStore;
479
+ /** Override store — per-tenant feature and limit overrides */
480
+ overrideStore?: OverrideStore;
242
481
  /** Resolves an org ID from a resource. Required for plan/wallet checks. */
243
482
  orgResolver?: (resource?: ResourceRef) => Promise<string | null>;
483
+ /** Plan version store — required for versioned plan resolution (grandfathered tenants) */
484
+ planVersionStore?: PlanVersionStore;
244
485
  }
486
+ /**
487
+ * Entitlement registry — augmented by @vertz/codegen to narrow entitlement strings.
488
+ * When empty (no codegen), Entitlement falls back to `string`.
489
+ * When codegen runs, it populates this with `{ 'entity:action': true }` entries.
490
+ */
491
+ interface EntitlementRegistry {}
492
+ /** Entitlement type — narrows to literal union when codegen populates EntitlementRegistry. */
493
+ type Entitlement = keyof EntitlementRegistry extends never ? string : Extract<keyof EntitlementRegistry, string>;
245
494
  interface AccessContext {
246
- can(entitlement: string, resource?: ResourceRef): Promise<boolean>;
247
- check(entitlement: string, resource?: ResourceRef): Promise<AccessCheckResult>;
248
- authorize(entitlement: string, resource?: ResourceRef): Promise<void>;
495
+ can(entitlement: Entitlement, resource?: ResourceRef): Promise<boolean>;
496
+ check(entitlement: Entitlement, resource?: ResourceRef): Promise<AccessCheckResult>;
497
+ authorize(entitlement: Entitlement, resource?: ResourceRef): Promise<void>;
249
498
  canAll(checks: Array<{
250
- entitlement: string;
499
+ entitlement: Entitlement;
251
500
  resource?: ResourceRef;
252
501
  }>): Promise<Map<string, boolean>>;
502
+ /** Batch check: single entitlement across multiple entities. Returns Map<entityId, boolean>. */
503
+ canBatch(entitlement: Entitlement, resources: ResourceRef[]): Promise<Map<string, boolean>>;
253
504
  /** Atomic check + consume. Runs full can() then increments wallet if all layers pass. */
254
- canAndConsume(entitlement: string, resource?: ResourceRef, amount?: number): Promise<boolean>;
505
+ canAndConsume(entitlement: Entitlement, resource?: ResourceRef, amount?: number): Promise<boolean>;
255
506
  /** Rollback a previous canAndConsume(). Use when the operation fails after consumption. */
256
- unconsume(entitlement: string, resource?: ResourceRef, amount?: number): Promise<void>;
507
+ unconsume(entitlement: Entitlement, resource?: ResourceRef, amount?: number): Promise<void>;
257
508
  }
258
509
  declare function createAccessContext(config: AccessContextConfig): AccessContext;
259
510
  interface AccessCheckData {
@@ -273,17 +524,16 @@ interface ComputeAccessSetConfig {
273
524
  accessDef: AccessDefinition;
274
525
  roleStore: RoleAssignmentStore;
275
526
  closureStore: ClosureStore;
276
- plan?: string | null;
277
527
  /** Flag store — for feature flag state in access set */
278
528
  flagStore?: FlagStore;
279
- /** Plan store — for limit info in access set */
280
- planStore?: PlanStore;
529
+ /** Subscription store — for limit info in access set */
530
+ subscriptionStore?: SubscriptionStore;
281
531
  /** Wallet store — for consumption info in access set */
282
532
  walletStore?: WalletStore;
283
533
  /** Org resolver — for plan/wallet lookups */
284
534
  orgResolver?: (resource?: ResourceRef) => Promise<string | null>;
285
- /** Org ID — direct org ID for global access set (bypass orgResolver) */
286
- orgId?: string | null;
535
+ /** Tenant ID — direct tenant ID for global access set (bypass orgResolver) */
536
+ tenantId?: string | null;
287
537
  }
288
538
  declare function computeAccessSet(config: ComputeAccessSetConfig): Promise<AccessSet>;
289
539
  /** Sparse encoding for JWT. Only includes allowed + denied-with-meta entries. */
@@ -351,6 +601,7 @@ interface SessionStore {
351
601
  expiresAt: Date;
352
602
  currentTokens?: AuthTokens;
353
603
  }): Promise<StoredSession>;
604
+ findActiveSessionById(id: string): Promise<StoredSession | null>;
354
605
  findByRefreshHash(hash: string): Promise<StoredSession | null>;
355
606
  findByPreviousRefreshHash(hash: string): Promise<StoredSession | null>;
356
607
  revokeSession(id: string): Promise<void>;
@@ -390,6 +641,8 @@ interface UserStore {
390
641
  findById(id: string): Promise<AuthUser | null>;
391
642
  updatePasswordHash(userId: string, passwordHash: string): Promise<void>;
392
643
  updateEmailVerified(userId: string, verified: boolean): Promise<void>;
644
+ /** Delete a user by id. Used for rollback when onUserCreated fails. */
645
+ deleteUser(id: string): Promise<void>;
393
646
  }
394
647
  interface MfaConfig {
395
648
  enabled?: boolean;
@@ -431,8 +684,7 @@ interface OAuthUserInfo {
431
684
  providerId: string;
432
685
  email: string;
433
686
  emailVerified: boolean;
434
- name?: string;
435
- avatarUrl?: string;
687
+ raw: Record<string, unknown>;
436
688
  }
437
689
  interface OAuthProvider {
438
690
  id: string;
@@ -453,6 +705,45 @@ interface OAuthAccountStore {
453
705
  unlinkAccount(userId: string, provider: string): Promise<void>;
454
706
  dispose(): void;
455
707
  }
708
+ /** Context provided to auth lifecycle callbacks. */
709
+ interface AuthCallbackContext {
710
+ /**
711
+ * System-level entity access — bypasses access rules.
712
+ * During sign-up, the user isn't authenticated yet,
713
+ * so access rules like rules.authenticated() would block the callback.
714
+ */
715
+ entities: Record<string, AuthEntityProxy>;
716
+ }
717
+ /** Minimal CRUD interface for entity access within auth callbacks. */
718
+ interface AuthEntityProxy {
719
+ get(id: string): Promise<unknown>;
720
+ list(options?: unknown): Promise<unknown>;
721
+ create(data: Record<string, unknown>): Promise<unknown>;
722
+ update(id: string, data: Record<string, unknown>): Promise<unknown>;
723
+ delete(id: string): Promise<void>;
724
+ }
725
+ /**
726
+ * Discriminated union for onUserCreated callback payload.
727
+ * OAuth and email/password sign-ups provide different data shapes.
728
+ */
729
+ type OnUserCreatedPayload = {
730
+ /** The auth user that was just created. */
731
+ user: AuthUser;
732
+ /** The OAuth provider that created this user. */
733
+ provider: {
734
+ id: string;
735
+ name: string;
736
+ };
737
+ /** Full provider API response (cast to GithubProfile, GoogleProfile, etc.). */
738
+ profile: Record<string, unknown>;
739
+ } | {
740
+ /** The auth user that was just created. */
741
+ user: AuthUser;
742
+ /** null for email/password sign-up. */
743
+ provider: null;
744
+ /** Extra fields from the sign-up form (via schema passthrough). */
745
+ signUpData: Record<string, unknown>;
746
+ };
456
747
  interface EmailVerificationConfig {
457
748
  enabled: boolean;
458
749
  tokenTtl?: string | number;
@@ -548,6 +839,21 @@ interface AuthConfig {
548
839
  passwordResetStore?: PasswordResetStore;
549
840
  /** Access control configuration — enables ACL claim in JWT */
550
841
  access?: AuthAccessConfig;
842
+ /** Tenant switching configuration — enables POST /auth/switch-tenant */
843
+ tenant?: TenantConfig;
844
+ /**
845
+ * Called after a new user is created in the auth system.
846
+ * Fires before the session is created.
847
+ * If this throws, the auth user is rolled back (deleted).
848
+ */
849
+ onUserCreated?: (payload: OnUserCreatedPayload, ctx: AuthCallbackContext) => Promise<void>;
850
+ /** @internal Entity proxy for onUserCreated callback. Set by createServer(). */
851
+ _entityProxy?: Record<string, AuthEntityProxy>;
852
+ }
853
+ /** Configuration for multi-tenant session switching. */
854
+ interface TenantConfig {
855
+ /** Verify that user has membership in the target tenant. Return false to deny. */
856
+ verifyMembership: (userId: string, tenantId: string) => Promise<boolean>;
551
857
  }
552
858
  /** Access control configuration for JWT acl claim computation. */
553
859
  interface AuthAccessConfig {
@@ -555,16 +861,16 @@ interface AuthAccessConfig {
555
861
  roleStore: RoleAssignmentStore;
556
862
  closureStore: ClosureStore;
557
863
  flagStore?: FlagStore;
864
+ subscriptionStore?: SubscriptionStore;
865
+ walletStore?: WalletStore;
558
866
  }
559
867
  interface AuthUser {
560
868
  id: string;
561
869
  email: string;
562
870
  role: string;
563
- plan?: string;
564
871
  emailVerified?: boolean;
565
872
  createdAt: Date;
566
873
  updatedAt: Date;
567
- [key: string]: unknown;
568
874
  }
569
875
  interface SessionPayload {
570
876
  sub: string;
@@ -574,6 +880,7 @@ interface SessionPayload {
574
880
  exp: number;
575
881
  jti: string;
576
882
  sid: string;
883
+ tenantId?: string;
577
884
  claims?: Record<string, unknown>;
578
885
  fva?: number;
579
886
  acl?: AclClaim;
@@ -608,16 +915,18 @@ interface SessionInfo {
608
915
  expiresAt: Date;
609
916
  isCurrent: boolean;
610
917
  }
611
- interface SignUpInput {
612
- email: string;
613
- password: string;
614
- role?: string;
615
- [key: string]: unknown;
616
- }
617
- interface SignInInput {
618
- email: string;
619
- password: string;
620
- }
918
+ type ReservedSignUpField = "role" | "emailVerified" | "id" | "createdAt" | "updatedAt";
919
+ type ReservedSignUpFields = { [K in ReservedSignUpField]? : never };
920
+ declare const signUpInputSchema: ObjectSchema<{
921
+ email: StringSchema;
922
+ password: StringSchema;
923
+ }>;
924
+ declare const signInInputSchema: ObjectSchema<{
925
+ email: StringSchema;
926
+ password: StringSchema;
927
+ }>;
928
+ type SignUpInput = Infer<typeof signUpInputSchema> & ReservedSignUpFields & Record<string, unknown>;
929
+ type SignInInput = Infer<typeof signInInputSchema>;
621
930
  interface AuthApi {
622
931
  signUp: (data: SignUpInput, ctx?: {
623
932
  headers: Headers;
@@ -647,6 +956,19 @@ interface AuthInstance {
647
956
  initialize: () => Promise<void>;
648
957
  /** Dispose stores and cleanup intervals */
649
958
  dispose: () => void;
959
+ /**
960
+ * JWT-only session resolver for SSR injection.
961
+ * Reads the session cookie, verifies JWT (no DB lookup), and returns
962
+ * minimal session data + optional access set for client hydration.
963
+ */
964
+ resolveSessionForSSR: (request: Request) => Promise<{
965
+ session: {
966
+ user: Record<string, unknown>;
967
+ expiresAt: number;
968
+ };
969
+ /** AccessSet | null at runtime; typed as unknown to avoid cross-package dependency on @vertz/ui */
970
+ accessSet?: unknown;
971
+ } | null>;
650
972
  }
651
973
  interface AuthContext {
652
974
  headers: Headers;
@@ -672,9 +994,6 @@ interface UserTableEntry extends ModelEntry<any, any> {
672
994
  role: {
673
995
  type: string;
674
996
  };
675
- plan?: {
676
- type: string;
677
- };
678
997
  createdAt: {
679
998
  type: Date;
680
999
  };
@@ -703,9 +1022,8 @@ import { AuthValidationError } from "@vertz/errors";
703
1022
  declare function hashPassword(password: string): Promise<string>;
704
1023
  declare function verifyPassword(password: string, hash: string): Promise<boolean>;
705
1024
  declare function validatePassword(password: string, requirements?: PasswordRequirements): AuthValidationError | null;
706
- type Entitlement = string;
707
1025
  interface RoleDefinition {
708
- entitlements: Entitlement[];
1026
+ entitlements: string[];
709
1027
  }
710
1028
  interface EntitlementDefinition {
711
1029
  roles: string[];
@@ -718,20 +1036,20 @@ interface AccessConfig {
718
1036
  }
719
1037
  interface AccessInstance {
720
1038
  /** Check if user has a specific entitlement */
721
- can(entitlement: Entitlement, user: AuthUser | null): Promise<boolean>;
1039
+ can(entitlement: string, user: AuthUser | null): Promise<boolean>;
722
1040
  /** Check with resource context */
723
- canWithResource(entitlement: Entitlement, resource: Resource, user: AuthUser | null): Promise<boolean>;
1041
+ canWithResource(entitlement: string, resource: Resource, user: AuthUser | null): Promise<boolean>;
724
1042
  /** Throws if not authorized */
725
- authorize(entitlement: Entitlement, user: AuthUser | null): Promise<void>;
1043
+ authorize(entitlement: string, user: AuthUser | null): Promise<void>;
726
1044
  /** Authorize with resource context */
727
- authorizeWithResource(entitlement: Entitlement, resource: Resource, user: AuthUser | null): Promise<void>;
1045
+ authorizeWithResource(entitlement: string, resource: Resource, user: AuthUser | null): Promise<void>;
728
1046
  /** Check multiple entitlements at once */
729
1047
  canAll(checks: Array<{
730
- entitlement: Entitlement;
1048
+ entitlement: string;
731
1049
  resource?: Resource;
732
1050
  }>, user: AuthUser | null): Promise<Map<string, boolean>>;
733
1051
  /** Get all entitlements for a role */
734
- getEntitlementsForRole(role: string): Entitlement[];
1052
+ getEntitlementsForRole(role: string): string[];
735
1053
  /** Middleware that adds ctx.can() and ctx.authorize() to context */
736
1054
  middleware: () => any;
737
1055
  }
@@ -742,9 +1060,9 @@ interface Resource {
742
1060
  [key: string]: unknown;
743
1061
  }
744
1062
  declare class AuthorizationError extends Error {
745
- readonly entitlement: Entitlement;
1063
+ readonly entitlement: string;
746
1064
  readonly userId?: string | undefined;
747
- constructor(message: string, entitlement: Entitlement, userId?: string | undefined);
1065
+ constructor(message: string, entitlement: string, userId?: string | undefined);
748
1066
  }
749
1067
  declare function createAccess(config: AccessConfig): AccessInstance;
750
1068
  declare const defaultAccess: AccessInstance;
@@ -766,6 +1084,23 @@ type AccessEvent = {
766
1084
  } | {
767
1085
  type: "access:plan_changed";
768
1086
  orgId: string;
1087
+ } | {
1088
+ type: "access:plan_assigned";
1089
+ orgId: string;
1090
+ planId: string;
1091
+ } | {
1092
+ type: "access:addon_attached";
1093
+ orgId: string;
1094
+ addonId: string;
1095
+ } | {
1096
+ type: "access:addon_detached";
1097
+ orgId: string;
1098
+ addonId: string;
1099
+ } | {
1100
+ type: "access:limit_reset";
1101
+ orgId: string;
1102
+ entitlement: string;
1103
+ max: number;
769
1104
  };
770
1105
  interface AccessWsData {
771
1106
  userId: string;
@@ -790,6 +1125,10 @@ interface AccessEventBroadcaster {
790
1125
  broadcastLimitUpdate(orgId: string, entitlement: string, consumed: number, remaining: number, max: number): void;
791
1126
  broadcastRoleChange(userId: string): void;
792
1127
  broadcastPlanChange(orgId: string): void;
1128
+ broadcastPlanAssigned(orgId: string, planId: string): void;
1129
+ broadcastAddonAttached(orgId: string, addonId: string): void;
1130
+ broadcastAddonDetached(orgId: string, addonId: string): void;
1131
+ broadcastLimitReset(orgId: string, entitlement: string, max: number): void;
793
1132
  getConnectionCount: number;
794
1133
  }
795
1134
  /** Minimal Bun.Server interface for WebSocket upgrade */
@@ -806,6 +1145,156 @@ interface BunWebSocket<T> {
806
1145
  ping(): void;
807
1146
  }
808
1147
  declare function createAccessEventBroadcaster(config: AccessEventBroadcasterConfig): AccessEventBroadcaster;
1148
+ import { ModelDef } from "@vertz/db";
1149
+ declare const authModels: Record<string, ModelDef>;
1150
+ import { DatabaseClient } from "@vertz/db";
1151
+ type AuthModels = typeof authModels;
1152
+ /**
1153
+ * Minimal database interface for auth stores.
1154
+ *
1155
+ * Keeps raw query support for legacy stores, but exposes the typed
1156
+ * `auth_sessions` delegate so session lookups can go through the generated
1157
+ * client instead of ad hoc SQL strings. Includes `transaction` for atomic
1158
+ * multi-statement writes in plan and closure stores.
1159
+ */
1160
+ type AuthDbClient = Pick<DatabaseClient<AuthModels>, "auth_sessions" | "query" | "_internals" | "transaction">;
1161
+ /**
1162
+ * Dialect-aware DDL helpers for auth table creation.
1163
+ *
1164
+ * Produces SQL column type fragments for SQLite and PostgreSQL,
1165
+ * enabling portable CREATE TABLE statements.
1166
+ */
1167
+ type DbDialectName = "sqlite" | "postgres";
1168
+ /**
1169
+ * Names of all auth tables — used for model validation.
1170
+ */
1171
+ declare const AUTH_TABLE_NAMES: readonly ["auth_users", "auth_sessions", "auth_oauth_accounts", "auth_role_assignments", "auth_closure", "auth_plans", "auth_plan_addons", "auth_flags", "auth_overrides"];
1172
+ /**
1173
+ * Validate that all required auth models are registered in the DatabaseClient.
1174
+ *
1175
+ * Throws a prescriptive error when models are missing, telling the developer
1176
+ * exactly what to add to their createDb() call.
1177
+ */
1178
+ declare function validateAuthModels(db: AuthDbClient): void;
1179
+ /**
1180
+ * Initialize auth tables in the database.
1181
+ *
1182
+ * Executes CREATE TABLE IF NOT EXISTS for all 9 auth tables.
1183
+ * Idempotent — safe to call on every server start.
1184
+ */
1185
+ declare function initializeAuthTables(db: AuthDbClient): Promise<void>;
1186
+ interface BillingAdapter {
1187
+ /** Push plan definitions to the payment processor. Idempotent. */
1188
+ syncPlans(plans: Record<string, PlanDef>): Promise<void>;
1189
+ /** Create a subscription for a tenant. */
1190
+ createSubscription(tenantId: string, planId: string): Promise<string>;
1191
+ /** Cancel a tenant's subscription. */
1192
+ cancelSubscription(tenantId: string): Promise<void>;
1193
+ /** Attach an add-on to a tenant's subscription. */
1194
+ attachAddOn(tenantId: string, addOnId: string): Promise<void>;
1195
+ /** Report metered usage (overage) for a tenant. */
1196
+ reportOverage(tenantId: string, limitKey: string, amount: number): Promise<void>;
1197
+ /** Get the billing portal URL for a tenant. */
1198
+ getPortalUrl(tenantId: string): Promise<string>;
1199
+ }
1200
+ /**
1201
+ * Billing Event Emitter — typed event system for billing lifecycle events.
1202
+ *
1203
+ * Events: subscription:created, subscription:canceled, billing:payment_failed
1204
+ */
1205
+ type BillingEventType = "subscription:created" | "subscription:canceled" | "billing:payment_failed";
1206
+ interface BillingEvent {
1207
+ tenantId: string;
1208
+ planId: string;
1209
+ attempt?: number;
1210
+ }
1211
+ type BillingEventHandler = (event: BillingEvent) => void;
1212
+ interface BillingEventEmitter {
1213
+ on(eventType: BillingEventType, handler: BillingEventHandler): void;
1214
+ off(eventType: BillingEventType, handler: BillingEventHandler): void;
1215
+ emit(eventType: BillingEventType, event: BillingEvent): void;
1216
+ }
1217
+ declare function createBillingEventEmitter(): BillingEventEmitter;
1218
+ /**
1219
+ * Overage Computation — calculates overage charges for limit usage.
1220
+ *
1221
+ * Formula: (consumed - max) * rate, capped if cap is specified.
1222
+ * Returns 0 if usage is within the limit.
1223
+ */
1224
+ interface OverageInput {
1225
+ consumed: number;
1226
+ max: number;
1227
+ rate: number;
1228
+ cap?: number;
1229
+ }
1230
+ /**
1231
+ * Compute overage charge for a given usage period.
1232
+ *
1233
+ * @returns The overage charge amount (0 if within limit)
1234
+ */
1235
+ declare function computeOverage(input: OverageInput): number;
1236
+ interface StripeProduct {
1237
+ id: string;
1238
+ name: string;
1239
+ metadata: Record<string, string>;
1240
+ }
1241
+ interface StripePrice {
1242
+ id: string;
1243
+ product: string;
1244
+ unit_amount: number;
1245
+ recurring: {
1246
+ interval: string;
1247
+ } | null;
1248
+ active: boolean;
1249
+ metadata: Record<string, string>;
1250
+ }
1251
+ interface StripeClient {
1252
+ products: {
1253
+ list(): Promise<{
1254
+ data: StripeProduct[];
1255
+ }>;
1256
+ create(params: {
1257
+ name: string;
1258
+ metadata: Record<string, string>;
1259
+ description?: string;
1260
+ }): Promise<StripeProduct>;
1261
+ update(id: string, params: {
1262
+ name?: string;
1263
+ metadata?: Record<string, string>;
1264
+ }): Promise<StripeProduct>;
1265
+ };
1266
+ prices: {
1267
+ list(params: {
1268
+ product: string;
1269
+ }): Promise<{
1270
+ data: StripePrice[];
1271
+ }>;
1272
+ create(params: {
1273
+ product: string;
1274
+ unit_amount: number;
1275
+ currency: string;
1276
+ recurring?: {
1277
+ interval: string;
1278
+ };
1279
+ metadata: Record<string, string>;
1280
+ }): Promise<StripePrice>;
1281
+ update(id: string, params: {
1282
+ active: boolean;
1283
+ }): Promise<StripePrice>;
1284
+ };
1285
+ }
1286
+ interface StripeBillingAdapterConfig {
1287
+ stripe: StripeClient;
1288
+ currency?: string;
1289
+ }
1290
+ declare function createStripeBillingAdapter(config: StripeBillingAdapterConfig): BillingAdapter;
1291
+ interface WebhookHandlerConfig {
1292
+ subscriptionStore: SubscriptionStore;
1293
+ emitter: BillingEventEmitter;
1294
+ defaultPlan: string;
1295
+ webhookSecret: string;
1296
+ }
1297
+ declare function createWebhookHandler(config: WebhookHandlerConfig): (request: Request) => Promise<Response>;
809
1298
  interface Period {
810
1299
  periodStart: Date;
811
1300
  periodEnd: Date;
@@ -817,6 +1306,107 @@ interface Period {
817
1306
  * the period containing now. For 'day'/'hour': uses fixed-duration periods.
818
1307
  */
819
1308
  declare function calculateBillingPeriod(startedAt: Date, per: BillingPeriod, now?: Date): Period;
1309
+ declare class DbClosureStore implements ClosureStore {
1310
+ private db;
1311
+ constructor(db: AuthDbClient);
1312
+ addResource(type: string, id: string, parent?: ParentRef): Promise<void>;
1313
+ removeResource(type: string, id: string): Promise<void>;
1314
+ getAncestors(type: string, id: string): Promise<ClosureEntry[]>;
1315
+ getDescendants(type: string, id: string): Promise<ClosureEntry[]>;
1316
+ hasPath(ancestorType: string, ancestorId: string, descendantType: string, descendantId: string): Promise<boolean>;
1317
+ dispose(): void;
1318
+ }
1319
+ declare class DbFlagStore implements FlagStore {
1320
+ private db;
1321
+ private cache;
1322
+ constructor(db: AuthDbClient);
1323
+ /**
1324
+ * Load all flags from DB into memory. Call once on initialization.
1325
+ */
1326
+ loadFlags(): Promise<void>;
1327
+ setFlag(orgId: string, flag: string, enabled: boolean): void;
1328
+ getFlag(orgId: string, flag: string): boolean;
1329
+ getFlags(orgId: string): Record<string, boolean>;
1330
+ }
1331
+ declare class DbOAuthAccountStore implements OAuthAccountStore {
1332
+ private db;
1333
+ constructor(db: AuthDbClient);
1334
+ linkAccount(userId: string, provider: string, providerId: string, email?: string): Promise<void>;
1335
+ findByProviderAccount(provider: string, providerId: string): Promise<string | null>;
1336
+ findByUserId(userId: string): Promise<{
1337
+ provider: string;
1338
+ providerId: string;
1339
+ }[]>;
1340
+ unlinkAccount(userId: string, provider: string): Promise<void>;
1341
+ dispose(): void;
1342
+ }
1343
+ declare class DbRoleAssignmentStore implements RoleAssignmentStore {
1344
+ private db;
1345
+ constructor(db: AuthDbClient);
1346
+ assign(userId: string, resourceType: string, resourceId: string, role: string): Promise<void>;
1347
+ revoke(userId: string, resourceType: string, resourceId: string, role: string): Promise<void>;
1348
+ getRoles(userId: string, resourceType: string, resourceId: string): Promise<string[]>;
1349
+ getRolesForUser(userId: string): Promise<RoleAssignment[]>;
1350
+ getEffectiveRole(userId: string, resourceType: string, resourceId: string, accessDef: AccessDefinition, closureStore: ClosureStore): Promise<string | null>;
1351
+ dispose(): void;
1352
+ }
1353
+ declare class DbSessionStore implements SessionStore {
1354
+ private db;
1355
+ constructor(db: AuthDbClient);
1356
+ createSessionWithId(id: string, data: {
1357
+ userId: string;
1358
+ refreshTokenHash: string;
1359
+ ipAddress: string;
1360
+ userAgent: string;
1361
+ expiresAt: Date;
1362
+ currentTokens?: AuthTokens;
1363
+ }): Promise<StoredSession>;
1364
+ findByRefreshHash(hash: string): Promise<StoredSession | null>;
1365
+ findActiveSessionById(id: string): Promise<StoredSession | null>;
1366
+ findByPreviousRefreshHash(hash: string): Promise<StoredSession | null>;
1367
+ revokeSession(id: string): Promise<void>;
1368
+ listActiveSessions(userId: string): Promise<StoredSession[]>;
1369
+ countActiveSessions(userId: string): Promise<number>;
1370
+ getCurrentTokens(sessionId: string): Promise<AuthTokens | null>;
1371
+ updateSession(id: string, data: {
1372
+ refreshTokenHash: string;
1373
+ previousRefreshHash: string;
1374
+ lastActiveAt: Date;
1375
+ currentTokens?: AuthTokens;
1376
+ }): Promise<void>;
1377
+ dispose(): void;
1378
+ /** Map a raw SQL row (snake_case) to StoredSession. */
1379
+ private rowToSession;
1380
+ /** Map an ORM record (camelCase) to StoredSession. */
1381
+ private recordToSession;
1382
+ }
1383
+ declare class DbSubscriptionStore implements SubscriptionStore {
1384
+ private db;
1385
+ constructor(db: AuthDbClient);
1386
+ assign(tenantId: string, planId: string, startedAt?: Date, expiresAt?: Date | null): Promise<void>;
1387
+ get(tenantId: string): Promise<Subscription | null>;
1388
+ updateOverrides(tenantId: string, overrides: Record<string, LimitOverride>): Promise<void>;
1389
+ remove(tenantId: string): Promise<void>;
1390
+ attachAddOn(tenantId: string, addOnId: string): Promise<void>;
1391
+ detachAddOn(tenantId: string, addOnId: string): Promise<void>;
1392
+ getAddOns(tenantId: string): Promise<string[]>;
1393
+ dispose(): void;
1394
+ private loadOverrides;
1395
+ }
1396
+ declare class DbUserStore implements UserStore {
1397
+ private db;
1398
+ constructor(db: AuthDbClient);
1399
+ createUser(user: AuthUser, passwordHash: string | null): Promise<void>;
1400
+ findByEmail(email: string): Promise<{
1401
+ user: AuthUser;
1402
+ passwordHash: string | null;
1403
+ } | null>;
1404
+ findById(id: string): Promise<AuthUser | null>;
1405
+ updatePasswordHash(userId: string, passwordHash: string): Promise<void>;
1406
+ updateEmailVerified(userId: string, verified: boolean): Promise<void>;
1407
+ deleteUser(id: string): Promise<void>;
1408
+ private rowToUser;
1409
+ }
820
1410
  declare class InMemoryEmailVerificationStore implements EmailVerificationStore {
821
1411
  private byId;
822
1412
  private byTokenHash;
@@ -848,6 +1438,38 @@ declare function computeEntityAccess(entitlements: string[], entity: {
848
1438
  * Returns true if `fva` exists and (now - fva) < maxAgeSeconds.
849
1439
  */
850
1440
  declare function checkFva(payload: SessionPayload, maxAgeSeconds: number): boolean;
1441
+ /**
1442
+ * Grandfathering Store — tracks which tenants are grandfathered on old plan versions.
1443
+ *
1444
+ * When a plan version changes, existing tenants keep their old version
1445
+ * during a grace period. This store tracks that state.
1446
+ */
1447
+ interface GrandfatheringState {
1448
+ tenantId: string;
1449
+ planId: string;
1450
+ version: number;
1451
+ graceEnds: Date | null;
1452
+ }
1453
+ interface GrandfatheringStore {
1454
+ /** Mark a tenant as grandfathered on a specific plan version. */
1455
+ setGrandfathered(tenantId: string, planId: string, version: number, graceEnds: Date | null): Promise<void>;
1456
+ /** Get grandfathering state for a tenant on a plan. Returns null if not grandfathered. */
1457
+ getGrandfathered(tenantId: string, planId: string): Promise<GrandfatheringState | null>;
1458
+ /** List all grandfathered tenants for a plan. */
1459
+ listGrandfathered(planId: string): Promise<GrandfatheringState[]>;
1460
+ /** Remove grandfathering state (after migration). */
1461
+ removeGrandfathered(tenantId: string, planId: string): Promise<void>;
1462
+ /** Clean up resources. */
1463
+ dispose(): void;
1464
+ }
1465
+ declare class InMemoryGrandfatheringStore implements GrandfatheringStore {
1466
+ private states;
1467
+ setGrandfathered(tenantId: string, planId: string, version: number, graceEnds: Date | null): Promise<void>;
1468
+ getGrandfathered(tenantId: string, planId: string): Promise<GrandfatheringState | null>;
1469
+ listGrandfathered(planId: string): Promise<GrandfatheringState[]>;
1470
+ removeGrandfathered(tenantId: string, planId: string): Promise<void>;
1471
+ dispose(): void;
1472
+ }
851
1473
  declare class InMemoryMFAStore implements MFAStore {
852
1474
  private secrets;
853
1475
  private backupCodes;
@@ -887,6 +1509,71 @@ declare class InMemoryPasswordResetStore implements PasswordResetStore {
887
1509
  deleteByUserId(userId: string): Promise<void>;
888
1510
  dispose(): void;
889
1511
  }
1512
+ interface PlanHashInput {
1513
+ features?: readonly string[] | string[];
1514
+ limits?: Record<string, unknown>;
1515
+ price?: {
1516
+ amount: number;
1517
+ interval: string;
1518
+ } | null;
1519
+ }
1520
+ /**
1521
+ * Compute SHA-256 hash of canonical JSON representation of plan config.
1522
+ * Keys are sorted recursively for deterministic output regardless of object key order.
1523
+ */
1524
+ declare function computePlanHash(config: PlanHashInput): Promise<string>;
1525
+ type PlanEventType = "plan:version_created" | "plan:grace_approaching" | "plan:grace_expiring" | "plan:migrated";
1526
+ interface PlanEvent {
1527
+ type: PlanEventType;
1528
+ planId: string;
1529
+ tenantId?: string;
1530
+ version?: number;
1531
+ previousVersion?: number;
1532
+ currentVersion?: number;
1533
+ graceEnds?: Date | null;
1534
+ timestamp: Date;
1535
+ }
1536
+ type PlanEventHandler = (event: PlanEvent) => void;
1537
+ interface TenantPlanState {
1538
+ planId: string;
1539
+ version: number;
1540
+ currentVersion: number;
1541
+ grandfathered: boolean;
1542
+ graceEnds: Date | null;
1543
+ snapshot: PlanSnapshot;
1544
+ }
1545
+ interface MigrateOpts {
1546
+ tenantId?: string;
1547
+ }
1548
+ interface ScheduleOpts {
1549
+ at: Date | string;
1550
+ }
1551
+ interface PlanManagerConfig {
1552
+ plans: Record<string, PlanDef>;
1553
+ versionStore: PlanVersionStore;
1554
+ grandfatheringStore: GrandfatheringStore;
1555
+ subscriptionStore: SubscriptionStore;
1556
+ clock?: () => Date;
1557
+ }
1558
+ interface PlanManager {
1559
+ /** Hash plan configs, compare with stored, create new versions if different. Idempotent. */
1560
+ initialize(): Promise<void>;
1561
+ /** Migrate tenants past grace period (or specific tenant immediately). */
1562
+ migrate(planId: string, opts?: MigrateOpts): Promise<void>;
1563
+ /** Schedule future migration date for all grandfathered tenants. */
1564
+ schedule(planId: string, opts: ScheduleOpts): Promise<void>;
1565
+ /** Return tenant's plan state (planId, version, grandfathered, snapshot). */
1566
+ resolve(tenantId: string): Promise<TenantPlanState | null>;
1567
+ /** List all grandfathered tenants for a plan. */
1568
+ grandfathered(planId: string): Promise<GrandfatheringState[]>;
1569
+ /** Check all grandfathered tenants and emit grace_approaching / grace_expiring events. */
1570
+ checkGraceEvents(): Promise<void>;
1571
+ /** Register an event handler. */
1572
+ on(handler: PlanEventHandler): void;
1573
+ /** Remove an event handler. */
1574
+ off(handler: PlanEventHandler): void;
1575
+ }
1576
+ declare function createPlanManager(config: PlanManagerConfig): PlanManager;
890
1577
  declare function discord(config: OAuthProviderConfig): OAuthProvider;
891
1578
  declare function github(config: OAuthProviderConfig): OAuthProvider;
892
1579
  declare function google(config: OAuthProviderConfig): OAuthProvider;
@@ -898,64 +1585,6 @@ declare class InMemoryRateLimitStore implements RateLimitStore {
898
1585
  dispose(): void;
899
1586
  private cleanup;
900
1587
  }
901
- /**
902
- * rules.* builders — declarative access rule data structures.
903
- *
904
- * These are pure data structures with no evaluation logic.
905
- * The access context evaluates them at runtime (sub-phase 4).
906
- */
907
- interface RoleRule {
908
- readonly type: "role";
909
- readonly roles: readonly string[];
910
- }
911
- interface EntitlementRule {
912
- readonly type: "entitlement";
913
- readonly entitlement: string;
914
- }
915
- interface WhereRule {
916
- readonly type: "where";
917
- readonly conditions: Record<string, unknown>;
918
- }
919
- interface AllRule {
920
- readonly type: "all";
921
- readonly rules: readonly AccessRule[];
922
- }
923
- interface AnyRule {
924
- readonly type: "any";
925
- readonly rules: readonly AccessRule[];
926
- }
927
- interface AuthenticatedRule {
928
- readonly type: "authenticated";
929
- }
930
- interface FvaRule {
931
- readonly type: "fva";
932
- readonly maxAge: number;
933
- }
934
- type AccessRule = RoleRule | EntitlementRule | WhereRule | AllRule | AnyRule | AuthenticatedRule | FvaRule;
935
- interface UserMarker {
936
- readonly __marker: string;
937
- }
938
- declare const rules: {
939
- /** User has at least one of the specified roles (OR) */
940
- role(...roleNames: string[]): RoleRule;
941
- /** User has the specified entitlement (resolves role+plan+flag from defineAccess config) */
942
- entitlement(name: string): EntitlementRule;
943
- /** Row-level condition — DB query syntax. Use rules.user.id for dynamic user markers */
944
- where(conditions: Record<string, unknown>): WhereRule;
945
- /** All sub-rules must pass (AND) */
946
- all(...ruleList: AccessRule[]): AllRule;
947
- /** At least one sub-rule must pass (OR) */
948
- any(...ruleList: AccessRule[]): AnyRule;
949
- /** User must be authenticated (no specific role required) */
950
- authenticated(): AuthenticatedRule;
951
- /** User must have verified MFA within maxAge seconds */
952
- fva(maxAge: number): FvaRule;
953
- /** Declarative user markers — resolved at evaluation time */
954
- user: {
955
- readonly id: UserMarker;
956
- readonly tenantId: UserMarker;
957
- };
958
- };
959
1588
  declare class InMemorySessionStore implements SessionStore {
960
1589
  private sessions;
961
1590
  private currentTokens;
@@ -978,6 +1607,7 @@ declare class InMemorySessionStore implements SessionStore {
978
1607
  currentTokens?: AuthTokens;
979
1608
  }): Promise<StoredSession>;
980
1609
  findByRefreshHash(hash: string): Promise<StoredSession | null>;
1610
+ findActiveSessionById(id: string): Promise<StoredSession | null>;
981
1611
  findByPreviousRefreshHash(hash: string): Promise<StoredSession | null>;
982
1612
  revokeSession(id: string): Promise<void>;
983
1613
  listActiveSessions(userId: string): Promise<StoredSession[]>;
@@ -1003,14 +1633,78 @@ declare class InMemoryUserStore implements UserStore {
1003
1633
  findById(id: string): Promise<AuthUser | null>;
1004
1634
  updatePasswordHash(userId: string, passwordHash: string): Promise<void>;
1005
1635
  updateEmailVerified(userId: string, verified: boolean): Promise<void>;
1636
+ deleteUser(id: string): Promise<void>;
1006
1637
  }
1007
1638
  declare function createAuth(config: AuthConfig): AuthInstance;
1639
+ import { SchemaLike } from "@vertz/db";
1640
+ /**
1641
+ * A content type descriptor that implements SchemaLike.
1642
+ * Carries HTTP metadata alongside parse/validate behavior.
1643
+ */
1644
+ interface ContentDescriptor<T> extends SchemaLike<T> {
1645
+ /** Discriminator — distinguishes from plain SchemaLike */
1646
+ readonly _kind: "content";
1647
+ /** MIME type for HTTP headers */
1648
+ readonly _contentType: string;
1649
+ }
1650
+ /**
1651
+ * Runtime check: is the given SchemaLike a ContentDescriptor?
1652
+ */
1653
+ declare function isContentDescriptor(value: SchemaLike<unknown>): value is ContentDescriptor<unknown>;
1654
+ declare const content: {
1655
+ /** application/xml → string */
1656
+ readonly xml: () => ContentDescriptor<string>;
1657
+ /** text/html → string */
1658
+ readonly html: () => ContentDescriptor<string>;
1659
+ /** text/plain → string */
1660
+ readonly text: () => ContentDescriptor<string>;
1661
+ /** application/octet-stream → Uint8Array */
1662
+ readonly binary: () => ContentDescriptor<Uint8Array>;
1663
+ };
1008
1664
  import { AppBuilder, AppConfig } from "@vertz/core";
1009
- import { DatabaseClient, EntityDbAdapter as EntityDbAdapter3, ModelEntry as ModelEntry2 } from "@vertz/db";
1010
- import { ModelDef as ModelDef2, RelationDef, SchemaLike, TableDef } from "@vertz/db";
1011
- import { ModelDef } from "@vertz/db";
1012
- import { EntityDbAdapter, ListOptions } from "@vertz/db";
1013
- import { EntityError, Result as Result2 } from "@vertz/errors";
1665
+ import { DatabaseClient as DatabaseClient2, EntityDbAdapter as EntityDbAdapter3, ModelEntry as ModelEntry2 } from "@vertz/db";
1666
+ import { ModelDef as ModelDef4, RelationDef as RelationDef2, SchemaLike as SchemaLike2, TableDef } from "@vertz/db";
1667
+ import { ListOptions as ListOptions3, ModelDef as ModelDef3 } from "@vertz/db";
1668
+ import { EntityDbAdapter, GetOptions, ListOptions, ModelDef as ModelDef2 } from "@vertz/db";
1669
+ import { EntityError, Result as Result3 } from "@vertz/errors";
1670
+ import { RelationDef, TenantGraph } from "@vertz/db";
1671
+ /** One hop in the relation chain from entity to tenant root. */
1672
+ interface TenantChainHop {
1673
+ /** Target table name (e.g., 'projects') */
1674
+ readonly tableName: string;
1675
+ /** FK column on the current table (e.g., 'projectId') */
1676
+ readonly foreignKey: string;
1677
+ /** PK column on the target table (e.g., 'id') */
1678
+ readonly targetColumn: string;
1679
+ }
1680
+ /** Full chain from an indirectly scoped entity to the tenant root. */
1681
+ interface TenantChain {
1682
+ /** Ordered hops from entity → ... → directly-scoped table */
1683
+ readonly hops: readonly TenantChainHop[];
1684
+ /** The tenant FK column on the final hop's target table (e.g., 'organizationId') */
1685
+ readonly tenantColumn: string;
1686
+ }
1687
+ interface ModelRegistryEntry {
1688
+ readonly table: {
1689
+ readonly _name: string;
1690
+ readonly _columns: Record<string, unknown>;
1691
+ };
1692
+ readonly relations: Record<string, RelationDef>;
1693
+ readonly _tenant?: string | null;
1694
+ }
1695
+ type ModelRegistry = Record<string, ModelRegistryEntry>;
1696
+ /**
1697
+ * Resolves the relation chain from an indirectly scoped entity back to the
1698
+ * tenant root.
1699
+ *
1700
+ * Returns null if the entity is not indirectly scoped (i.e., it's the root,
1701
+ * directly scoped, shared, or unscoped).
1702
+ *
1703
+ * The chain is computed by walking `ref.one` relations from the entity until
1704
+ * we reach a directly-scoped model. The tenant column is read from the
1705
+ * directly-scoped model's `_tenant` relation FK.
1706
+ */
1707
+ declare function resolveTenantChain(entityKey: string, tenantGraph: TenantGraph, registry: ModelRegistry): TenantChain | null;
1014
1708
  import { EntityDbAdapter as EntityDbAdapter2, ListOptions as ListOptions2 } from "@vertz/db";
1015
1709
  interface ListResult<T = Record<string, unknown>> {
1016
1710
  items: T[];
@@ -1023,29 +1717,38 @@ interface CrudResult<T = unknown> {
1023
1717
  status: number;
1024
1718
  body: T;
1025
1719
  }
1026
- interface CrudHandlers {
1027
- list(ctx: EntityContext, options?: ListOptions): Promise<Result2<CrudResult<ListResult>, EntityError>>;
1028
- get(ctx: EntityContext, id: string): Promise<Result2<CrudResult<Record<string, unknown>>, EntityError>>;
1029
- create(ctx: EntityContext, data: Record<string, unknown>): Promise<Result2<CrudResult<Record<string, unknown>>, EntityError>>;
1030
- update(ctx: EntityContext, id: string, data: Record<string, unknown>): Promise<Result2<CrudResult<Record<string, unknown>>, EntityError>>;
1031
- delete(ctx: EntityContext, id: string): Promise<Result2<CrudResult<null>, EntityError>>;
1032
- }
1033
- declare function createCrudHandlers(def: EntityDefinition, db: EntityDbAdapter): CrudHandlers;
1720
+ interface CrudHandlers<TModel extends ModelDef2 = ModelDef2> {
1721
+ list(ctx: EntityContext<TModel>, options?: ListOptions): Promise<Result3<CrudResult<ListResult<TModel["table"]["$response"]>>, EntityError>>;
1722
+ get(ctx: EntityContext<TModel>, id: string, options?: GetOptions): Promise<Result3<CrudResult<TModel["table"]["$response"]>, EntityError>>;
1723
+ create(ctx: EntityContext<TModel>, data: Record<string, unknown>): Promise<Result3<CrudResult<TModel["table"]["$response"]>, EntityError>>;
1724
+ update(ctx: EntityContext<TModel>, id: string, data: Record<string, unknown>): Promise<Result3<CrudResult<TModel["table"]["$response"]>, EntityError>>;
1725
+ delete(ctx: EntityContext<TModel>, id: string): Promise<Result3<CrudResult<null>, EntityError>>;
1726
+ }
1727
+ /** Resolves IDs from a table matching a where condition. Used for indirect tenant filtering. */
1728
+ type QueryParentIdsFn = (tableName: string, where: Record<string, unknown>) => Promise<string[]>;
1729
+ /** Options for the CRUD pipeline factory. */
1730
+ interface CrudPipelineOptions {
1731
+ /** Tenant chain for indirectly scoped entities. */
1732
+ tenantChain?: TenantChain | null;
1733
+ /** Resolves parent IDs for indirect tenant chain traversal. */
1734
+ queryParentIds?: QueryParentIdsFn;
1735
+ }
1736
+ declare function createCrudHandlers<TModel extends ModelDef2 = ModelDef2>(def: EntityDefinition<TModel>, db: EntityDbAdapter, options?: CrudPipelineOptions): CrudHandlers<TModel>;
1034
1737
  /**
1035
1738
  * EntityOperations — typed CRUD facade for a single entity.
1036
1739
  *
1037
1740
  * When used as `ctx.entity`, TModel fills in actual column types.
1038
1741
  * When used as `ctx.entities.*`, TModel defaults to `ModelDef` (loose typing).
1039
1742
  */
1040
- interface EntityOperations<TModel extends ModelDef = ModelDef> {
1743
+ interface EntityOperations<TModel extends ModelDef3 = ModelDef3> {
1041
1744
  get(id: string): Promise<TModel["table"]["$response"]>;
1042
- list(options?: ListOptions2): Promise<ListResult<TModel["table"]["$response"]>>;
1745
+ list(options?: ListOptions3<TModel>): Promise<ListResult<TModel["table"]["$response"]>>;
1043
1746
  create(data: TModel["table"]["$create_input"]): Promise<TModel["table"]["$response"]>;
1044
1747
  update(id: string, data: TModel["table"]["$update_input"]): Promise<TModel["table"]["$response"]>;
1045
1748
  delete(id: string): Promise<void>;
1046
1749
  }
1047
1750
  /** Extracts the model type from an EntityDefinition */
1048
- type ExtractModel<T> = T extends EntityDefinition<infer M> ? M : ModelDef2;
1751
+ type ExtractModel<T> = T extends EntityDefinition<infer M> ? M : ModelDef4;
1049
1752
  /**
1050
1753
  * Maps an inject config `{ key: EntityDefinition<TModel> }` to
1051
1754
  * `{ key: EntityOperations<TModel> }` for typed ctx.entities access.
@@ -1053,12 +1756,13 @@ type ExtractModel<T> = T extends EntityDefinition<infer M> ? M : ModelDef2;
1053
1756
  type InjectToOperations<TInject extends Record<string, EntityDefinition> = {}> = { readonly [K in keyof TInject] : EntityOperations<ExtractModel<TInject[K]>> };
1054
1757
  interface BaseContext {
1055
1758
  readonly userId: string | null;
1759
+ readonly tenantId: string | null;
1056
1760
  authenticated(): boolean;
1057
1761
  tenant(): boolean;
1058
1762
  role(...roles: string[]): boolean;
1059
1763
  }
1060
1764
  interface EntityContext<
1061
- TModel extends ModelDef2 = ModelDef2,
1765
+ TModel extends ModelDef4 = ModelDef4,
1062
1766
  TInject extends Record<string, EntityDefinition> = {}
1063
1767
  > extends BaseContext {
1064
1768
  /** Typed CRUD on the current entity */
@@ -1066,7 +1770,7 @@ interface EntityContext<
1066
1770
  /** Typed access to injected entities only */
1067
1771
  readonly entities: InjectToOperations<TInject>;
1068
1772
  }
1069
- type AccessRule2 = false | ((ctx: BaseContext, row: Record<string, unknown>) => boolean | Promise<boolean>);
1773
+ type AccessRule2 = false | AccessRule | ((ctx: BaseContext, row: Record<string, unknown>) => boolean | Promise<boolean>);
1070
1774
  interface EntityBeforeHooks<
1071
1775
  TCreateInput = unknown,
1072
1776
  TUpdateInput = unknown
@@ -1087,20 +1791,35 @@ interface EntityActionDef<
1087
1791
  > {
1088
1792
  readonly method?: string;
1089
1793
  readonly path?: string;
1090
- readonly body: SchemaLike<TInput>;
1091
- readonly response: SchemaLike<TOutput>;
1794
+ readonly body: SchemaLike2<TInput>;
1795
+ readonly response: SchemaLike2<TOutput>;
1092
1796
  readonly handler: (input: TInput, ctx: TCtx, row: TResponse | null) => Promise<TOutput>;
1093
1797
  }
1094
1798
  /** Extract column keys from a RelationDef's target table. */
1095
- type RelationColumnKeys<R> = R extends RelationDef<infer TTarget> ? TTarget extends TableDef<infer TCols> ? Extract<keyof TCols, string> : string : string;
1096
- type EntityRelationsConfig<TRelations extends Record<string, RelationDef> = Record<string, RelationDef>> = { [K in keyof TRelations]? : true | false | { [F in RelationColumnKeys<TRelations[K]>]? : true } };
1799
+ type RelationColumnKeys<R> = R extends RelationDef2<infer TTarget> ? TTarget extends TableDef<infer TCols> ? Extract<keyof TCols, string> : string : string;
1800
+ /** Structured relation config with select, allowWhere, allowOrderBy, maxLimit. */
1801
+ interface RelationConfigObject<TColumnKeys extends string = string> {
1802
+ /** Which fields can be selected. Record<field, true>. */
1803
+ readonly select?: { readonly [F in TColumnKeys]? : true };
1804
+ /** Which fields can be filtered via `where`. */
1805
+ readonly allowWhere?: readonly string[];
1806
+ /** Which fields can be sorted via `orderBy`. */
1807
+ readonly allowOrderBy?: readonly string[];
1808
+ /** Max items per parent row. Defaults to DEFAULT_RELATION_LIMIT (100). */
1809
+ readonly maxLimit?: number;
1810
+ }
1811
+ type EntityRelationsConfig<TRelations extends Record<string, RelationDef2> = Record<string, RelationDef2>> = { [K in keyof TRelations]? : true | false | RelationConfigObject<RelationColumnKeys<TRelations[K]>> };
1097
1812
  interface EntityConfig<
1098
- TModel extends ModelDef2 = ModelDef2,
1813
+ TModel extends ModelDef4 = ModelDef4,
1099
1814
  TActions extends Record<string, EntityActionDef<any, any, any, any>> = {},
1100
1815
  TInject extends Record<string, EntityDefinition> = {}
1101
1816
  > {
1102
1817
  readonly model: TModel;
1103
1818
  readonly inject?: TInject;
1819
+ /** Override the DB table name (defaults to entity name). For admin entities over shared tables. */
1820
+ readonly table?: string;
1821
+ /** Whether CRUD auto-filters by tenantId. Defaults to true when model has tenantId column. */
1822
+ readonly tenantScoped?: boolean;
1104
1823
  readonly access?: Partial<Record<"list" | "get" | "create" | "update" | "delete" | Extract<keyof NoInfer<TActions>, string>, AccessRule2>>;
1105
1824
  readonly before?: {
1106
1825
  readonly create?: (data: TModel["table"]["$create_input"], ctx: EntityContext<TModel, TInject>) => TModel["table"]["$create_input"] | Promise<TModel["table"]["$create_input"]>;
@@ -1114,7 +1833,7 @@ interface EntityConfig<
1114
1833
  readonly actions?: { readonly [K in keyof TActions] : TActions[K] };
1115
1834
  readonly relations?: EntityRelationsConfig<TModel["relations"]>;
1116
1835
  }
1117
- interface EntityDefinition<TModel extends ModelDef2 = ModelDef2> {
1836
+ interface EntityDefinition<TModel extends ModelDef4 = ModelDef4> {
1118
1837
  readonly kind: "entity";
1119
1838
  readonly name: string;
1120
1839
  readonly model: TModel;
@@ -1124,8 +1843,14 @@ interface EntityDefinition<TModel extends ModelDef2 = ModelDef2> {
1124
1843
  readonly after: EntityAfterHooks;
1125
1844
  readonly actions: Record<string, EntityActionDef>;
1126
1845
  readonly relations: EntityRelationsConfig<TModel["relations"]>;
1127
- }
1128
- import { SchemaLike as SchemaLike2 } from "@vertz/db";
1846
+ /** DB table name (defaults to entity name). */
1847
+ readonly table: string;
1848
+ /** Whether CRUD auto-filters by tenantId. */
1849
+ readonly tenantScoped: boolean;
1850
+ /** Relation chain for indirect tenant scoping. Null for direct or unscoped. */
1851
+ readonly tenantChain: TenantChain | null;
1852
+ }
1853
+ import { SchemaLike as SchemaLike3 } from "@vertz/db";
1129
1854
  /** Extracts the model type from an EntityDefinition */
1130
1855
  type ExtractModel2<T> = T extends EntityDefinition<infer M> ? M : never;
1131
1856
  /**
@@ -1133,9 +1858,18 @@ type ExtractModel2<T> = T extends EntityDefinition<infer M> ? M : never;
1133
1858
  * `{ key: EntityOperations<TModel> }` for typed ctx.entities access.
1134
1859
  */
1135
1860
  type InjectToOperations2<TInject extends Record<string, EntityDefinition> = {}> = { readonly [K in keyof TInject] : EntityOperations<ExtractModel2<TInject[K]>> };
1861
+ /** Raw request metadata exposed to service handlers */
1862
+ interface ServiceRequestInfo {
1863
+ readonly url: string;
1864
+ readonly method: string;
1865
+ readonly headers: Headers;
1866
+ readonly body: unknown;
1867
+ }
1136
1868
  interface ServiceContext<TInject extends Record<string, EntityDefinition> = {}> extends BaseContext {
1137
1869
  /** Typed access to injected entities only */
1138
1870
  readonly entities: InjectToOperations2<TInject>;
1871
+ /** Raw request metadata — URL, method, headers, pre-parsed body */
1872
+ readonly request: ServiceRequestInfo;
1139
1873
  }
1140
1874
  interface ServiceActionDef<
1141
1875
  TInput = unknown,
@@ -1144,8 +1878,8 @@ interface ServiceActionDef<
1144
1878
  > {
1145
1879
  readonly method?: string;
1146
1880
  readonly path?: string;
1147
- readonly body: SchemaLike2<TInput>;
1148
- readonly response: SchemaLike2<TOutput>;
1881
+ readonly body?: SchemaLike3<TInput>;
1882
+ readonly response: SchemaLike3<TOutput>;
1149
1883
  readonly handler: (input: TInput, ctx: TCtx) => Promise<TOutput>;
1150
1884
  }
1151
1885
  interface ServiceConfig<
@@ -1163,6 +1897,12 @@ interface ServiceDefinition {
1163
1897
  readonly access: Partial<Record<string, AccessRule2>>;
1164
1898
  readonly actions: Record<string, ServiceActionDef>;
1165
1899
  }
1900
+ interface ServerInstance extends AppBuilder {
1901
+ auth: AuthInstance;
1902
+ initialize(): Promise<void>;
1903
+ /** Routes auth requests (/api/auth/*) to auth.handler, everything else to entity handler */
1904
+ readonly requestHandler: (request: Request) => Promise<Response>;
1905
+ }
1166
1906
  interface ServerConfig extends Omit<AppConfig, "_entityDbFactory" | "entities"> {
1167
1907
  /** Entity definitions created via entity() from @vertz/server */
1168
1908
  entities?: EntityDefinition[];
@@ -1174,16 +1914,37 @@ interface ServerConfig extends Omit<AppConfig, "_entityDbFactory" | "entities">
1174
1914
  * - A DatabaseClient from createDb() (recommended — auto-bridged per entity)
1175
1915
  * - An EntityDbAdapter (deprecated — simple adapter with get/list/create/update/delete)
1176
1916
  */
1177
- db?: DatabaseClient<Record<string, ModelEntry2>> | EntityDbAdapter3;
1917
+ db?: DatabaseClient2<Record<string, ModelEntry2>> | EntityDbAdapter3;
1178
1918
  /** @internal Factory to create a DB adapter for each entity. Prefer `db` instead. */
1179
1919
  _entityDbFactory?: (entityDef: EntityDefinition) => EntityDbAdapter3;
1920
+ /** @internal Resolves parent IDs for indirect tenant chain traversal. For testing only. */
1921
+ _queryParentIds?: QueryParentIdsFn;
1922
+ /** @internal Tenant chains for indirect scoping. For testing without DatabaseClient. */
1923
+ _tenantChains?: Map<string, TenantChain>;
1924
+ /** Auth configuration — when combined with db, auto-wires DB-backed stores */
1925
+ auth?: AuthConfig;
1180
1926
  }
1181
1927
  /**
1182
1928
  * Creates an HTTP server with entity route generation.
1183
1929
  * Wraps @vertz/core's createServer to inject entity CRUD handlers.
1930
+ *
1931
+ * When both `db` (DatabaseClient) and `auth` are provided:
1932
+ * - Validates auth models are registered in the DatabaseClient
1933
+ * - Auto-wires DB-backed UserStore and SessionStore
1934
+ * - Returns ServerInstance with `.auth` and `.initialize()`
1184
1935
  */
1936
+ declare function createServer(config: ServerConfig & {
1937
+ db: DatabaseClient2<Record<string, ModelEntry2>>;
1938
+ auth: AuthConfig;
1939
+ }): ServerInstance;
1185
1940
  declare function createServer(config: ServerConfig): AppBuilder;
1186
- import { EntityForbiddenError, Result as Result3 } from "@vertz/errors";
1941
+ import { EntityForbiddenError, Result as Result4 } from "@vertz/errors";
1942
+ interface EnforceAccessOptions {
1943
+ /** Evaluate an entitlement via the access context (defineAccess) */
1944
+ readonly can?: (entitlement: string) => Promise<boolean>;
1945
+ /** Seconds since last MFA verification (for rules.fva) */
1946
+ readonly fvaAge?: number;
1947
+ }
1187
1948
  /**
1188
1949
  * Evaluates an access rule for the given operation.
1189
1950
  * Returns err(EntityForbiddenError) if access is denied.
@@ -1192,10 +1953,16 @@ import { EntityForbiddenError, Result as Result3 } from "@vertz/errors";
1192
1953
  *
1193
1954
  * - No rule defined → deny (deny by default)
1194
1955
  * - Rule is false → operation is disabled
1195
- * - Rule is a function evaluate and deny if returns false
1956
+ * - Descriptor ruledispatch by type
1957
+ * - Function rule → evaluate and deny if returns false
1958
+ *
1959
+ * When `skipWhere` is true, `where` rules are treated as ok() — used
1960
+ * when where conditions have already been pushed to the DB query.
1196
1961
  */
1197
- declare function enforceAccess(operation: string, accessRules: Partial<Record<string, AccessRule2>>, ctx: BaseContext, row?: Record<string, unknown>): Promise<Result3<void, EntityForbiddenError>>;
1198
- import { ModelDef as ModelDef3 } from "@vertz/db";
1962
+ declare function enforceAccess(operation: string, accessRules: Partial<Record<string, AccessRule2>>, ctx: BaseContext, row?: Record<string, unknown>, options?: EnforceAccessOptions & {
1963
+ skipWhere?: boolean;
1964
+ }): Promise<Result4<void, EntityForbiddenError>>;
1965
+ import { ModelDef as ModelDef6 } from "@vertz/db";
1199
1966
  /**
1200
1967
  * Request info extracted from HTTP context / auth middleware.
1201
1968
  */
@@ -1207,10 +1974,10 @@ interface RequestInfo {
1207
1974
  /**
1208
1975
  * Creates an EntityContext from request info, entity operations, and registry proxy.
1209
1976
  */
1210
- declare function createEntityContext<TModel extends ModelDef3 = ModelDef3>(request: RequestInfo, entityOps: EntityOperations<TModel>, registryProxy: Record<string, EntityOperations>): EntityContext<TModel>;
1211
- import { ModelDef as ModelDef4 } from "@vertz/db";
1977
+ declare function createEntityContext<TModel extends ModelDef6 = ModelDef6>(request: RequestInfo, entityOps: EntityOperations<TModel>, registryProxy: Record<string, EntityOperations>): EntityContext<TModel>;
1978
+ import { ModelDef as ModelDef7 } from "@vertz/db";
1212
1979
  declare function entity<
1213
- TModel extends ModelDef4,
1980
+ TModel extends ModelDef7,
1214
1981
  TInject extends Record<string, EntityDefinition> = {},
1215
1982
  TActions extends Record<string, EntityActionDef<any, any, TModel["table"]["$response"], EntityContext<TModel, TInject>>> = {}
1216
1983
  >(name: string, config: EntityConfig<TModel, TActions, TInject>): EntityDefinition<TModel>;
@@ -1258,6 +2025,10 @@ declare function stripReadOnlyFields(table: TableDef2, data: Record<string, unkn
1258
2025
  import { EntityRouteEntry } from "@vertz/core";
1259
2026
  interface EntityRouteOptions {
1260
2027
  apiPrefix?: string;
2028
+ /** Tenant chain for indirectly scoped entities. */
2029
+ tenantChain?: TenantChain | null;
2030
+ /** Resolves parent IDs for indirect tenant chain traversal. */
2031
+ queryParentIds?: QueryParentIdsFn;
1261
2032
  }
1262
2033
  /**
1263
2034
  * Generates HTTP route entries for a single entity definition.
@@ -1271,4 +2042,4 @@ declare function service<
1271
2042
  TInject extends Record<string, EntityDefinition> = {},
1272
2043
  TActions extends Record<string, ServiceActionDef<any, any, any>> = Record<string, ServiceActionDef<any, any, any>>
1273
2044
  >(name: string, config: ServiceConfig<TActions, TInject>): ServiceDefinition;
1274
- export { vertz, verifyPassword, validatePassword, stripReadOnlyFields, stripHiddenFields, service, rules, makeImmutable, hashPassword, google, github, generateEntityRoutes, entityErrorHandler, entity, enforceAccess, encodeAccessSet, discord, defineAccess, defaultAccess, deepFreeze, decodeAccessSet, createServer, createMiddleware, createImmutableProxy, createEnv, createEntityContext, createCrudHandlers, createAuth, createAccessEventBroadcaster, createAccessContext, createAccess, computeEntityAccess, computeAccessSet, checkFva, calculateBillingPeriod, WalletStore, WalletEntry, VertzException, ValidationException, UserTableEntry, UserStore, UnauthorizedException, StoredSession, StoredPasswordReset, StoredEmailVerification, SignUpInput, SignInInput, SessionStrategy, SessionStore, SessionPayload, SessionInfo, SessionConfig, Session, ServiceUnavailableException, ServiceDefinition, ServiceContext, ServiceConfig, ServiceActionDef, ServerHandle, ServerConfig, ServerAdapter, RoleAssignmentTableEntry, RoleAssignmentStore, RoleAssignment, ResourceRef, Resource, RequestInfo, RawRequest, RateLimitStore, RateLimitResult, RateLimitConfig, PlanStore, PlanDef, Period, PasswordResetStore, PasswordResetConfig, PasswordRequirements, ParentRef, OrgPlan, OAuthUserInfo, OAuthTokens, OAuthProviderConfig, OAuthProvider, OAuthAccountStore, NotFoundException, NamedMiddlewareDef, MiddlewareDef, MfaSetupData, MfaConfig, MfaChallengeData, MFAStore, ListenOptions, ListResult, ListOptions2 as ListOptions, LimitOverride, LimitDef, InternalServerErrorException, InferSchema, Infer, InMemoryWalletStore, InMemoryUserStore, InMemorySessionStore, InMemoryRoleAssignmentStore, InMemoryRateLimitStore, InMemoryPlanStore, InMemoryPasswordResetStore, InMemoryOAuthAccountStore, InMemoryMFAStore, InMemoryFlagStore, InMemoryEmailVerificationStore, InMemoryClosureStore, HttpStatusCode, HttpMethod, HandlerCtx, ForbiddenException, FlagStore, EnvConfig, EntityRouteOptions, EntityRelationsConfig, EntityRegistry, EntityOperations, EntityErrorResult, EntityDefinition, EntityDbAdapter2 as EntityDbAdapter, EntityContext, EntityConfig, EntityActionDef, EntitlementDefinition, EntitlementDef, Entitlement, EncodedAccessSet, EmailVerificationStore, EmailVerificationConfig, EmailPasswordConfig, Deps, DenialReason, DenialMeta, DefineAccessInput, DeepReadonly, Ctx, CrudResult, CrudHandlers, CorsConfig, CookieConfig, ConsumeResult, ConflictException, ComputeAccessSetConfig, ClosureStore, ClosureRow, ClosureEntry, BillingPeriod, BaseContext, BadRequestException, AuthorizationError, AuthUser, AuthTokens, AuthInstance, AuthContext, AuthConfig, AuthApi, AppConfig2 as AppConfig, AppBuilder2 as AppBuilder, AclClaim, AccumulateProvides, AccessWsData, AccessSet, AccessRule2 as AccessRule, AccessInstance, AccessEventBroadcasterConfig, AccessEventBroadcaster, AccessEvent, AccessDefinition, AccessContextConfig, AccessContext, AccessConfig, AccessCheckResult, AccessCheckData };
2045
+ export { vertz, verifyPassword, validatePassword, validateOverrides, validateAuthModels, stripReadOnlyFields, stripHiddenFields, service, rules, resolveTenantChain, makeImmutable, isContentDescriptor, initializeAuthTables, hashPassword, google, github, getIncompatibleAddOns, generateEntityRoutes, entityErrorHandler, entity, enforceAccess, encodeAccessSet, discord, defineAccess, defaultAccess, deepFreeze, decodeAccessSet, createWebhookHandler, createStripeBillingAdapter, createServer, createPlanManager, createMiddleware, createImmutableProxy, createEnv, createEntityContext, createCrudHandlers, createBillingEventEmitter, createAuth, createAccessEventBroadcaster, createAccessContext, createAccess, content, computePlanHash, computeOverage, computeEntityAccess, computeAccessSet, checkFva, checkAddOnCompatibility, calculateBillingPeriod, authModels, WebhookHandlerConfig, WalletStore, WalletEntry, VertzException, ValidationException, UserTableEntry, UserStore, UnauthorizedException, TenantOverrides, TenantChainHop, TenantChain, SubscriptionStore, Subscription, StripeProduct, StripePrice, StripeClient, StripeBillingAdapterConfig, StoredSession, StoredPasswordReset, StoredEmailVerification, SignUpInput, SignInInput, SessionStrategy, SessionStore, SessionPayload, SessionInfo, SessionConfig, Session, ServiceUnavailableException, ServiceRequestInfo, ServiceDefinition, ServiceContext, ServiceConfig, ServiceActionDef, ServerInstance, ServerHandle, ServerConfig, ServerAdapter, RuleContext, RoleAssignmentTableEntry, RoleAssignmentStore, RoleAssignment, ResourceRef, Resource, RequestInfo, RawRequest, RateLimitStore, RateLimitResult, RateLimitConfig, PriceInterval, PlanVersionStore, PlanVersionInfo, PlanSnapshot, PlanPrice, PlanHashInput, PlanDef, Period, PasswordResetStore, PasswordResetConfig, PasswordRequirements, ParentRef, OverrideStore, OverageInput, OverageConfig, OnUserCreatedPayload, OAuthUserInfo, OAuthTokens, OAuthProviderConfig, OAuthProvider, OAuthAccountStore, NotFoundException, NamedMiddlewareDef, MiddlewareDef, MfaSetupData, MfaConfig, MfaChallengeData, MFAStore, ListenOptions, ListResult, ListOptions2 as ListOptions, LimitOverrideDef, LimitOverride, LimitDef, InternalServerErrorException, InferSchema, Infer2 as Infer, InMemoryWalletStore, InMemoryUserStore, InMemorySubscriptionStore, InMemorySessionStore, InMemoryRoleAssignmentStore, InMemoryRateLimitStore, InMemoryPlanVersionStore, InMemoryPasswordResetStore, InMemoryOverrideStore, InMemoryOAuthAccountStore, InMemoryMFAStore, InMemoryGrandfatheringStore, InMemoryFlagStore, InMemoryEmailVerificationStore, InMemoryClosureStore, HttpStatusCode, HttpMethod, HandlerCtx, GrandfatheringStore, GrandfatheringState, ForbiddenException, FlagStore, EnvConfig, EntityRouteOptions, EntityRelationsConfig, EntityRegistry, EntityOperations, EntityErrorResult, EntityDefinition, EntityDef, EntityDbAdapter2 as EntityDbAdapter, EntityContext, EntityConfig, EntityActionDef, EntitlementValue, EntitlementRegistry, EntitlementDefinition, EntitlementDef, Entitlement, EncodedAccessSet, EmailVerificationStore, EmailVerificationConfig, EmailPasswordConfig, Deps, DenialReason, DenialMeta, DefineAccessInput, DeepReadonly, DbUserStore, DbSubscriptionStore, DbSessionStore, DbRoleAssignmentStore, DbOAuthAccountStore, DbFlagStore, DbDialectName, DbClosureStore, Ctx, CrudResult, CrudHandlers, CorsConfig, CookieConfig, ContentDescriptor, ConsumeResult, ConflictException, ComputeAccessSetConfig, ClosureStore, ClosureRow, ClosureEntry, BillingPeriod, BillingEventType, BillingEventHandler, BillingEventEmitter, BillingEvent, BillingAdapter, BaseContext, BadRequestException, AuthorizationError, AuthUser, AuthTokens, AuthInstance, AuthEntityProxy, AuthDbClient, AuthContext, AuthConfig, AuthCallbackContext, AuthApi, AppConfig2 as AppConfig, AppBuilder2 as AppBuilder, AddOnRequires, AclClaim, AccumulateProvides, AccessWsData, AccessSet, AccessRule2 as AccessRule, AccessInstance, AccessEventBroadcasterConfig, AccessEventBroadcaster, AccessEvent, AccessDefinition, AccessContextConfig, AccessContext, AccessConfig, AccessCheckResult, AccessCheckData, AUTH_TABLE_NAMES };