@vertz/server 0.2.15 → 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 +401 -199
  2. package/dist/index.js +1302 -382
  3. package/package.json +5 -4
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
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
7
  * rules.* builders — declarative access rule data structures.
7
8
  *
@@ -280,15 +281,101 @@ declare class InMemoryRoleAssignmentStore implements RoleAssignmentStore {
280
281
  * Used by Layer 1 of access context to gate entitlements on feature flags.
281
282
  */
282
283
  interface FlagStore {
283
- setFlag(orgId: string, flag: string, enabled: boolean): void;
284
- getFlag(orgId: string, flag: string): boolean;
285
- 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>;
286
287
  }
287
288
  declare class InMemoryFlagStore implements FlagStore {
288
289
  private flags;
289
- setFlag(orgId: string, flag: string, enabled: boolean): void;
290
- getFlag(orgId: string, flag: string): boolean;
291
- 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>;
293
+ }
294
+ /** Per-tenant limit override. Only affects the cap, not the billing period. */
295
+ interface LimitOverride {
296
+ max: number;
297
+ }
298
+ interface Subscription {
299
+ tenantId: string;
300
+ planId: string;
301
+ startedAt: Date;
302
+ expiresAt: Date | null;
303
+ overrides: Record<string, LimitOverride>;
304
+ }
305
+ interface SubscriptionStore {
306
+ /**
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().
309
+ */
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[]>;
322
+ dispose(): void;
323
+ }
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[]>;
335
+ dispose(): void;
336
+ }
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
+ /**
348
+ * WalletStore — consumption tracking for plan-limited entitlements.
349
+ *
350
+ * Tracks per-tenant usage within billing periods with atomic
351
+ * check-and-increment operations.
352
+ */
353
+ interface WalletEntry {
354
+ tenantId: string;
355
+ entitlement: string;
356
+ periodStart: Date;
357
+ periodEnd: Date;
358
+ consumed: number;
359
+ }
360
+ interface ConsumeResult {
361
+ success: boolean;
362
+ consumed: number;
363
+ limit: number;
364
+ remaining: number;
365
+ }
366
+ interface WalletStore {
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>;
370
+ dispose(): void;
371
+ }
372
+ declare class InMemoryWalletStore implements WalletStore {
373
+ private entries;
374
+ private key;
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;
292
379
  }
293
380
  /** Limit override: add (additive) or max (hard cap) */
294
381
  interface LimitOverrideDef {
@@ -324,59 +411,6 @@ declare class InMemoryOverrideStore implements OverrideStore {
324
411
  * Throws on invalid limit keys, invalid feature names, or invalid max values.
325
412
  */
326
413
  declare function validateOverrides(accessDef: AccessDefinition, overrides: TenantOverrides): void;
327
- /** Per-customer limit override. Only affects the cap, not the billing period. */
328
- interface LimitOverride {
329
- max: number;
330
- }
331
- interface OrgPlan {
332
- orgId: string;
333
- planId: string;
334
- startedAt: Date;
335
- expiresAt: Date | null;
336
- overrides: Record<string, LimitOverride>;
337
- }
338
- interface PlanStore {
339
- /**
340
- * Assign a plan to an org. Resets per-customer overrides (overrides are plan-specific).
341
- * To preserve overrides across plan changes, re-apply them after calling assignPlan().
342
- */
343
- assignPlan(orgId: string, planId: string, startedAt?: Date, expiresAt?: Date | null): Promise<void>;
344
- getPlan(orgId: string): Promise<OrgPlan | null>;
345
- updateOverrides(orgId: string, overrides: Record<string, LimitOverride>): Promise<void>;
346
- removePlan(orgId: string): Promise<void>;
347
- /** Attach an add-on to an org. */
348
- attachAddOn?(orgId: string, addOnId: string): Promise<void>;
349
- /** Detach an add-on from an org. */
350
- detachAddOn?(orgId: string, addOnId: string): Promise<void>;
351
- /** Get all active add-on IDs for an org. */
352
- getAddOns?(orgId: string): Promise<string[]>;
353
- /** List all org IDs assigned to a specific plan. */
354
- listByPlan?(planId: string): Promise<string[]>;
355
- dispose(): void;
356
- }
357
- declare class InMemoryPlanStore implements PlanStore {
358
- private plans;
359
- private addOns;
360
- assignPlan(orgId: string, planId: string, startedAt?: Date, expiresAt?: Date | null): Promise<void>;
361
- getPlan(orgId: string): Promise<OrgPlan | null>;
362
- updateOverrides(orgId: string, overrides: Record<string, LimitOverride>): Promise<void>;
363
- removePlan(orgId: string): Promise<void>;
364
- attachAddOn(orgId: string, addOnId: string): Promise<void>;
365
- detachAddOn(orgId: string, addOnId: string): Promise<void>;
366
- getAddOns(orgId: string): Promise<string[]>;
367
- listByPlan(planId: string): Promise<string[]>;
368
- dispose(): void;
369
- }
370
- /**
371
- * Check if an add-on is compatible with a given base plan.
372
- * Returns true if the add-on has no `requires` or if the plan is in the requires list.
373
- */
374
- declare function checkAddOnCompatibility(accessDef: AccessDefinition, addOnId: string, currentPlanId: string): boolean;
375
- /**
376
- * Get add-ons that are incompatible with a target plan.
377
- * Used to flag incompatible add-ons when a tenant downgrades.
378
- */
379
- declare function getIncompatibleAddOns(accessDef: AccessDefinition, activeAddOnIds: string[], targetPlanId: string): string[];
380
414
  /**
381
415
  * Plan Version Store — tracks versioned snapshots of plan configurations.
382
416
  *
@@ -425,39 +459,6 @@ declare class InMemoryPlanVersionStore implements PlanVersionStore {
425
459
  getCurrentHash(planId: string): Promise<string | null>;
426
460
  dispose(): void;
427
461
  }
428
- /**
429
- * WalletStore — consumption tracking for plan-limited entitlements.
430
- *
431
- * Tracks per-org usage within billing periods with atomic
432
- * check-and-increment operations.
433
- */
434
- interface WalletEntry {
435
- orgId: string;
436
- entitlement: string;
437
- periodStart: Date;
438
- periodEnd: Date;
439
- consumed: number;
440
- }
441
- interface ConsumeResult {
442
- success: boolean;
443
- consumed: number;
444
- limit: number;
445
- remaining: number;
446
- }
447
- interface WalletStore {
448
- consume(orgId: string, entitlement: string, periodStart: Date, periodEnd: Date, limit: number, amount?: number): Promise<ConsumeResult>;
449
- unconsume(orgId: string, entitlement: string, periodStart: Date, periodEnd: Date, amount?: number): Promise<void>;
450
- getConsumption(orgId: string, entitlement: string, periodStart: Date, periodEnd: Date): Promise<number>;
451
- dispose(): void;
452
- }
453
- declare class InMemoryWalletStore implements WalletStore {
454
- private entries;
455
- private key;
456
- consume(orgId: string, entitlement: string, periodStart: Date, periodEnd: Date, limit: number, amount?: number): Promise<ConsumeResult>;
457
- unconsume(orgId: string, entitlement: string, periodStart: Date, _periodEnd: Date, amount?: number): Promise<void>;
458
- getConsumption(orgId: string, entitlement: string, periodStart: Date, _periodEnd: Date): Promise<number>;
459
- dispose(): void;
460
- }
461
462
  interface ResourceRef {
462
463
  type: string;
463
464
  id: string;
@@ -471,8 +472,8 @@ interface AccessContextConfig {
471
472
  fva?: number;
472
473
  /** Flag store — required for Layer 1 feature flag checks */
473
474
  flagStore?: FlagStore;
474
- /** Plan store — required for Layer 4 plan checks */
475
- planStore?: PlanStore;
475
+ /** Subscription store — required for Layer 4 plan checks */
476
+ subscriptionStore?: SubscriptionStore;
476
477
  /** Wallet store — required for Layer 5 wallet checks and canAndConsume() */
477
478
  walletStore?: WalletStore;
478
479
  /** Override store — per-tenant feature and limit overrides */
@@ -482,20 +483,28 @@ interface AccessContextConfig {
482
483
  /** Plan version store — required for versioned plan resolution (grandfathered tenants) */
483
484
  planVersionStore?: PlanVersionStore;
484
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>;
485
494
  interface AccessContext {
486
- can(entitlement: string, resource?: ResourceRef): Promise<boolean>;
487
- check(entitlement: string, resource?: ResourceRef): Promise<AccessCheckResult>;
488
- 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>;
489
498
  canAll(checks: Array<{
490
- entitlement: string;
499
+ entitlement: Entitlement;
491
500
  resource?: ResourceRef;
492
501
  }>): Promise<Map<string, boolean>>;
493
502
  /** Batch check: single entitlement across multiple entities. Returns Map<entityId, boolean>. */
494
- canBatch(entitlement: string, resources: ResourceRef[]): Promise<Map<string, boolean>>;
503
+ canBatch(entitlement: Entitlement, resources: ResourceRef[]): Promise<Map<string, boolean>>;
495
504
  /** Atomic check + consume. Runs full can() then increments wallet if all layers pass. */
496
- canAndConsume(entitlement: string, resource?: ResourceRef, amount?: number): Promise<boolean>;
505
+ canAndConsume(entitlement: Entitlement, resource?: ResourceRef, amount?: number): Promise<boolean>;
497
506
  /** Rollback a previous canAndConsume(). Use when the operation fails after consumption. */
498
- unconsume(entitlement: string, resource?: ResourceRef, amount?: number): Promise<void>;
507
+ unconsume(entitlement: Entitlement, resource?: ResourceRef, amount?: number): Promise<void>;
499
508
  }
500
509
  declare function createAccessContext(config: AccessContextConfig): AccessContext;
501
510
  interface AccessCheckData {
@@ -515,17 +524,16 @@ interface ComputeAccessSetConfig {
515
524
  accessDef: AccessDefinition;
516
525
  roleStore: RoleAssignmentStore;
517
526
  closureStore: ClosureStore;
518
- plan?: string | null;
519
527
  /** Flag store — for feature flag state in access set */
520
528
  flagStore?: FlagStore;
521
- /** Plan store — for limit info in access set */
522
- planStore?: PlanStore;
529
+ /** Subscription store — for limit info in access set */
530
+ subscriptionStore?: SubscriptionStore;
523
531
  /** Wallet store — for consumption info in access set */
524
532
  walletStore?: WalletStore;
525
533
  /** Org resolver — for plan/wallet lookups */
526
534
  orgResolver?: (resource?: ResourceRef) => Promise<string | null>;
527
- /** Org ID — direct org ID for global access set (bypass orgResolver) */
528
- orgId?: string | null;
535
+ /** Tenant ID — direct tenant ID for global access set (bypass orgResolver) */
536
+ tenantId?: string | null;
529
537
  }
530
538
  declare function computeAccessSet(config: ComputeAccessSetConfig): Promise<AccessSet>;
531
539
  /** Sparse encoding for JWT. Only includes allowed + denied-with-meta entries. */
@@ -593,6 +601,7 @@ interface SessionStore {
593
601
  expiresAt: Date;
594
602
  currentTokens?: AuthTokens;
595
603
  }): Promise<StoredSession>;
604
+ findActiveSessionById(id: string): Promise<StoredSession | null>;
596
605
  findByRefreshHash(hash: string): Promise<StoredSession | null>;
597
606
  findByPreviousRefreshHash(hash: string): Promise<StoredSession | null>;
598
607
  revokeSession(id: string): Promise<void>;
@@ -632,6 +641,8 @@ interface UserStore {
632
641
  findById(id: string): Promise<AuthUser | null>;
633
642
  updatePasswordHash(userId: string, passwordHash: string): Promise<void>;
634
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>;
635
646
  }
636
647
  interface MfaConfig {
637
648
  enabled?: boolean;
@@ -673,8 +684,7 @@ interface OAuthUserInfo {
673
684
  providerId: string;
674
685
  email: string;
675
686
  emailVerified: boolean;
676
- name?: string;
677
- avatarUrl?: string;
687
+ raw: Record<string, unknown>;
678
688
  }
679
689
  interface OAuthProvider {
680
690
  id: string;
@@ -695,6 +705,45 @@ interface OAuthAccountStore {
695
705
  unlinkAccount(userId: string, provider: string): Promise<void>;
696
706
  dispose(): void;
697
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
+ };
698
747
  interface EmailVerificationConfig {
699
748
  enabled: boolean;
700
749
  tokenTtl?: string | number;
@@ -790,6 +839,21 @@ interface AuthConfig {
790
839
  passwordResetStore?: PasswordResetStore;
791
840
  /** Access control configuration — enables ACL claim in JWT */
792
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>;
793
857
  }
794
858
  /** Access control configuration for JWT acl claim computation. */
795
859
  interface AuthAccessConfig {
@@ -797,16 +861,16 @@ interface AuthAccessConfig {
797
861
  roleStore: RoleAssignmentStore;
798
862
  closureStore: ClosureStore;
799
863
  flagStore?: FlagStore;
864
+ subscriptionStore?: SubscriptionStore;
865
+ walletStore?: WalletStore;
800
866
  }
801
867
  interface AuthUser {
802
868
  id: string;
803
869
  email: string;
804
870
  role: string;
805
- plan?: string;
806
871
  emailVerified?: boolean;
807
872
  createdAt: Date;
808
873
  updatedAt: Date;
809
- [key: string]: unknown;
810
874
  }
811
875
  interface SessionPayload {
812
876
  sub: string;
@@ -816,6 +880,7 @@ interface SessionPayload {
816
880
  exp: number;
817
881
  jti: string;
818
882
  sid: string;
883
+ tenantId?: string;
819
884
  claims?: Record<string, unknown>;
820
885
  fva?: number;
821
886
  acl?: AclClaim;
@@ -850,16 +915,18 @@ interface SessionInfo {
850
915
  expiresAt: Date;
851
916
  isCurrent: boolean;
852
917
  }
853
- interface SignUpInput {
854
- email: string;
855
- password: string;
856
- role?: string;
857
- [key: string]: unknown;
858
- }
859
- interface SignInInput {
860
- email: string;
861
- password: string;
862
- }
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>;
863
930
  interface AuthApi {
864
931
  signUp: (data: SignUpInput, ctx?: {
865
932
  headers: Headers;
@@ -889,6 +956,19 @@ interface AuthInstance {
889
956
  initialize: () => Promise<void>;
890
957
  /** Dispose stores and cleanup intervals */
891
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>;
892
972
  }
893
973
  interface AuthContext {
894
974
  headers: Headers;
@@ -914,9 +994,6 @@ interface UserTableEntry extends ModelEntry<any, any> {
914
994
  role: {
915
995
  type: string;
916
996
  };
917
- plan?: {
918
- type: string;
919
- };
920
997
  createdAt: {
921
998
  type: Date;
922
999
  };
@@ -945,9 +1022,8 @@ import { AuthValidationError } from "@vertz/errors";
945
1022
  declare function hashPassword(password: string): Promise<string>;
946
1023
  declare function verifyPassword(password: string, hash: string): Promise<boolean>;
947
1024
  declare function validatePassword(password: string, requirements?: PasswordRequirements): AuthValidationError | null;
948
- type Entitlement = string;
949
1025
  interface RoleDefinition {
950
- entitlements: Entitlement[];
1026
+ entitlements: string[];
951
1027
  }
952
1028
  interface EntitlementDefinition {
953
1029
  roles: string[];
@@ -960,20 +1036,20 @@ interface AccessConfig {
960
1036
  }
961
1037
  interface AccessInstance {
962
1038
  /** Check if user has a specific entitlement */
963
- can(entitlement: Entitlement, user: AuthUser | null): Promise<boolean>;
1039
+ can(entitlement: string, user: AuthUser | null): Promise<boolean>;
964
1040
  /** Check with resource context */
965
- canWithResource(entitlement: Entitlement, resource: Resource, user: AuthUser | null): Promise<boolean>;
1041
+ canWithResource(entitlement: string, resource: Resource, user: AuthUser | null): Promise<boolean>;
966
1042
  /** Throws if not authorized */
967
- authorize(entitlement: Entitlement, user: AuthUser | null): Promise<void>;
1043
+ authorize(entitlement: string, user: AuthUser | null): Promise<void>;
968
1044
  /** Authorize with resource context */
969
- authorizeWithResource(entitlement: Entitlement, resource: Resource, user: AuthUser | null): Promise<void>;
1045
+ authorizeWithResource(entitlement: string, resource: Resource, user: AuthUser | null): Promise<void>;
970
1046
  /** Check multiple entitlements at once */
971
1047
  canAll(checks: Array<{
972
- entitlement: Entitlement;
1048
+ entitlement: string;
973
1049
  resource?: Resource;
974
1050
  }>, user: AuthUser | null): Promise<Map<string, boolean>>;
975
1051
  /** Get all entitlements for a role */
976
- getEntitlementsForRole(role: string): Entitlement[];
1052
+ getEntitlementsForRole(role: string): string[];
977
1053
  /** Middleware that adds ctx.can() and ctx.authorize() to context */
978
1054
  middleware: () => any;
979
1055
  }
@@ -984,9 +1060,9 @@ interface Resource {
984
1060
  [key: string]: unknown;
985
1061
  }
986
1062
  declare class AuthorizationError extends Error {
987
- readonly entitlement: Entitlement;
1063
+ readonly entitlement: string;
988
1064
  readonly userId?: string | undefined;
989
- constructor(message: string, entitlement: Entitlement, userId?: string | undefined);
1065
+ constructor(message: string, entitlement: string, userId?: string | undefined);
990
1066
  }
991
1067
  declare function createAccess(config: AccessConfig): AccessInstance;
992
1068
  declare const defaultAccess: AccessInstance;
@@ -1071,22 +1147,17 @@ interface BunWebSocket<T> {
1071
1147
  declare function createAccessEventBroadcaster(config: AccessEventBroadcasterConfig): AccessEventBroadcaster;
1072
1148
  import { ModelDef } from "@vertz/db";
1073
1149
  declare const authModels: Record<string, ModelDef>;
1074
- import { QueryResult, ReadError } from "@vertz/db";
1075
- import { SqlFragment } from "@vertz/db/sql";
1076
- import { Result as Result2 } from "@vertz/errors";
1150
+ import { DatabaseClient } from "@vertz/db";
1151
+ type AuthModels = typeof authModels;
1077
1152
  /**
1078
1153
  * Minimal database interface for auth stores.
1079
- * Only requires raw query capability and dialect info.
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.
1080
1159
  */
1081
- interface AuthDbClient {
1082
- query<T = Record<string, unknown>>(fragment: SqlFragment): Promise<Result2<QueryResult<T>, ReadError>>;
1083
- _internals: {
1084
- readonly models: Record<string, unknown>;
1085
- readonly dialect: {
1086
- readonly name: "postgres" | "sqlite";
1087
- };
1088
- };
1089
- }
1160
+ type AuthDbClient = Pick<DatabaseClient<AuthModels>, "auth_sessions" | "query" | "_internals" | "transaction">;
1090
1161
  /**
1091
1162
  * Dialect-aware DDL helpers for auth table creation.
1092
1163
  *
@@ -1218,7 +1289,7 @@ interface StripeBillingAdapterConfig {
1218
1289
  }
1219
1290
  declare function createStripeBillingAdapter(config: StripeBillingAdapterConfig): BillingAdapter;
1220
1291
  interface WebhookHandlerConfig {
1221
- planStore: PlanStore;
1292
+ subscriptionStore: SubscriptionStore;
1222
1293
  emitter: BillingEventEmitter;
1223
1294
  defaultPlan: string;
1224
1295
  webhookSecret: string;
@@ -1269,19 +1340,6 @@ declare class DbOAuthAccountStore implements OAuthAccountStore {
1269
1340
  unlinkAccount(userId: string, provider: string): Promise<void>;
1270
1341
  dispose(): void;
1271
1342
  }
1272
- declare class DbPlanStore implements PlanStore {
1273
- private db;
1274
- constructor(db: AuthDbClient);
1275
- assignPlan(orgId: string, planId: string, startedAt?: Date, expiresAt?: Date | null): Promise<void>;
1276
- getPlan(orgId: string): Promise<OrgPlan | null>;
1277
- updateOverrides(orgId: string, overrides: Record<string, LimitOverride>): Promise<void>;
1278
- removePlan(orgId: string): Promise<void>;
1279
- attachAddOn(orgId: string, addOnId: string): Promise<void>;
1280
- detachAddOn(orgId: string, addOnId: string): Promise<void>;
1281
- getAddOns(orgId: string): Promise<string[]>;
1282
- dispose(): void;
1283
- private loadOverrides;
1284
- }
1285
1343
  declare class DbRoleAssignmentStore implements RoleAssignmentStore {
1286
1344
  private db;
1287
1345
  constructor(db: AuthDbClient);
@@ -1304,6 +1362,7 @@ declare class DbSessionStore implements SessionStore {
1304
1362
  currentTokens?: AuthTokens;
1305
1363
  }): Promise<StoredSession>;
1306
1364
  findByRefreshHash(hash: string): Promise<StoredSession | null>;
1365
+ findActiveSessionById(id: string): Promise<StoredSession | null>;
1307
1366
  findByPreviousRefreshHash(hash: string): Promise<StoredSession | null>;
1308
1367
  revokeSession(id: string): Promise<void>;
1309
1368
  listActiveSessions(userId: string): Promise<StoredSession[]>;
@@ -1316,7 +1375,23 @@ declare class DbSessionStore implements SessionStore {
1316
1375
  currentTokens?: AuthTokens;
1317
1376
  }): Promise<void>;
1318
1377
  dispose(): void;
1378
+ /** Map a raw SQL row (snake_case) to StoredSession. */
1319
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;
1320
1395
  }
1321
1396
  declare class DbUserStore implements UserStore {
1322
1397
  private db;
@@ -1329,6 +1404,7 @@ declare class DbUserStore implements UserStore {
1329
1404
  findById(id: string): Promise<AuthUser | null>;
1330
1405
  updatePasswordHash(userId: string, passwordHash: string): Promise<void>;
1331
1406
  updateEmailVerified(userId: string, verified: boolean): Promise<void>;
1407
+ deleteUser(id: string): Promise<void>;
1332
1408
  private rowToUser;
1333
1409
  }
1334
1410
  declare class InMemoryEmailVerificationStore implements EmailVerificationStore {
@@ -1476,7 +1552,7 @@ interface PlanManagerConfig {
1476
1552
  plans: Record<string, PlanDef>;
1477
1553
  versionStore: PlanVersionStore;
1478
1554
  grandfatheringStore: GrandfatheringStore;
1479
- planStore: PlanStore;
1555
+ subscriptionStore: SubscriptionStore;
1480
1556
  clock?: () => Date;
1481
1557
  }
1482
1558
  interface PlanManager {
@@ -1531,6 +1607,7 @@ declare class InMemorySessionStore implements SessionStore {
1531
1607
  currentTokens?: AuthTokens;
1532
1608
  }): Promise<StoredSession>;
1533
1609
  findByRefreshHash(hash: string): Promise<StoredSession | null>;
1610
+ findActiveSessionById(id: string): Promise<StoredSession | null>;
1534
1611
  findByPreviousRefreshHash(hash: string): Promise<StoredSession | null>;
1535
1612
  revokeSession(id: string): Promise<void>;
1536
1613
  listActiveSessions(userId: string): Promise<StoredSession[]>;
@@ -1556,14 +1633,78 @@ declare class InMemoryUserStore implements UserStore {
1556
1633
  findById(id: string): Promise<AuthUser | null>;
1557
1634
  updatePasswordHash(userId: string, passwordHash: string): Promise<void>;
1558
1635
  updateEmailVerified(userId: string, verified: boolean): Promise<void>;
1636
+ deleteUser(id: string): Promise<void>;
1559
1637
  }
1560
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
+ };
1561
1664
  import { AppBuilder, AppConfig } from "@vertz/core";
1562
- import { DatabaseClient, EntityDbAdapter as EntityDbAdapter3, ModelEntry as ModelEntry2 } from "@vertz/db";
1563
- import { ModelDef as ModelDef3, RelationDef, SchemaLike, TableDef } from "@vertz/db";
1564
- import { ModelDef as ModelDef2 } from "@vertz/db";
1565
- import { EntityDbAdapter, ListOptions } from "@vertz/db";
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";
1566
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;
1567
1708
  import { EntityDbAdapter as EntityDbAdapter2, ListOptions as ListOptions2 } from "@vertz/db";
1568
1709
  interface ListResult<T = Record<string, unknown>> {
1569
1710
  items: T[];
@@ -1576,29 +1717,38 @@ interface CrudResult<T = unknown> {
1576
1717
  status: number;
1577
1718
  body: T;
1578
1719
  }
1579
- interface CrudHandlers {
1580
- list(ctx: EntityContext, options?: ListOptions): Promise<Result3<CrudResult<ListResult>, EntityError>>;
1581
- get(ctx: EntityContext, id: string): Promise<Result3<CrudResult<Record<string, unknown>>, EntityError>>;
1582
- create(ctx: EntityContext, data: Record<string, unknown>): Promise<Result3<CrudResult<Record<string, unknown>>, EntityError>>;
1583
- update(ctx: EntityContext, id: string, data: Record<string, unknown>): Promise<Result3<CrudResult<Record<string, unknown>>, EntityError>>;
1584
- delete(ctx: EntityContext, id: string): Promise<Result3<CrudResult<null>, EntityError>>;
1585
- }
1586
- 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>;
1587
1737
  /**
1588
1738
  * EntityOperations — typed CRUD facade for a single entity.
1589
1739
  *
1590
1740
  * When used as `ctx.entity`, TModel fills in actual column types.
1591
1741
  * When used as `ctx.entities.*`, TModel defaults to `ModelDef` (loose typing).
1592
1742
  */
1593
- interface EntityOperations<TModel extends ModelDef2 = ModelDef2> {
1743
+ interface EntityOperations<TModel extends ModelDef3 = ModelDef3> {
1594
1744
  get(id: string): Promise<TModel["table"]["$response"]>;
1595
- list(options?: ListOptions2): Promise<ListResult<TModel["table"]["$response"]>>;
1745
+ list(options?: ListOptions3<TModel>): Promise<ListResult<TModel["table"]["$response"]>>;
1596
1746
  create(data: TModel["table"]["$create_input"]): Promise<TModel["table"]["$response"]>;
1597
1747
  update(id: string, data: TModel["table"]["$update_input"]): Promise<TModel["table"]["$response"]>;
1598
1748
  delete(id: string): Promise<void>;
1599
1749
  }
1600
1750
  /** Extracts the model type from an EntityDefinition */
1601
- type ExtractModel<T> = T extends EntityDefinition<infer M> ? M : ModelDef3;
1751
+ type ExtractModel<T> = T extends EntityDefinition<infer M> ? M : ModelDef4;
1602
1752
  /**
1603
1753
  * Maps an inject config `{ key: EntityDefinition<TModel> }` to
1604
1754
  * `{ key: EntityOperations<TModel> }` for typed ctx.entities access.
@@ -1606,12 +1756,13 @@ type ExtractModel<T> = T extends EntityDefinition<infer M> ? M : ModelDef3;
1606
1756
  type InjectToOperations<TInject extends Record<string, EntityDefinition> = {}> = { readonly [K in keyof TInject] : EntityOperations<ExtractModel<TInject[K]>> };
1607
1757
  interface BaseContext {
1608
1758
  readonly userId: string | null;
1759
+ readonly tenantId: string | null;
1609
1760
  authenticated(): boolean;
1610
1761
  tenant(): boolean;
1611
1762
  role(...roles: string[]): boolean;
1612
1763
  }
1613
1764
  interface EntityContext<
1614
- TModel extends ModelDef3 = ModelDef3,
1765
+ TModel extends ModelDef4 = ModelDef4,
1615
1766
  TInject extends Record<string, EntityDefinition> = {}
1616
1767
  > extends BaseContext {
1617
1768
  /** Typed CRUD on the current entity */
@@ -1619,7 +1770,7 @@ interface EntityContext<
1619
1770
  /** Typed access to injected entities only */
1620
1771
  readonly entities: InjectToOperations<TInject>;
1621
1772
  }
1622
- type AccessRule2 = false | PublicRule | ((ctx: BaseContext, row: Record<string, unknown>) => boolean | Promise<boolean>);
1773
+ type AccessRule2 = false | AccessRule | ((ctx: BaseContext, row: Record<string, unknown>) => boolean | Promise<boolean>);
1623
1774
  interface EntityBeforeHooks<
1624
1775
  TCreateInput = unknown,
1625
1776
  TUpdateInput = unknown
@@ -1640,20 +1791,35 @@ interface EntityActionDef<
1640
1791
  > {
1641
1792
  readonly method?: string;
1642
1793
  readonly path?: string;
1643
- readonly body: SchemaLike<TInput>;
1644
- readonly response: SchemaLike<TOutput>;
1794
+ readonly body: SchemaLike2<TInput>;
1795
+ readonly response: SchemaLike2<TOutput>;
1645
1796
  readonly handler: (input: TInput, ctx: TCtx, row: TResponse | null) => Promise<TOutput>;
1646
1797
  }
1647
1798
  /** Extract column keys from a RelationDef's target table. */
1648
- type RelationColumnKeys<R> = R extends RelationDef<infer TTarget> ? TTarget extends TableDef<infer TCols> ? Extract<keyof TCols, string> : string : string;
1649
- 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]>> };
1650
1812
  interface EntityConfig<
1651
- TModel extends ModelDef3 = ModelDef3,
1813
+ TModel extends ModelDef4 = ModelDef4,
1652
1814
  TActions extends Record<string, EntityActionDef<any, any, any, any>> = {},
1653
1815
  TInject extends Record<string, EntityDefinition> = {}
1654
1816
  > {
1655
1817
  readonly model: TModel;
1656
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;
1657
1823
  readonly access?: Partial<Record<"list" | "get" | "create" | "update" | "delete" | Extract<keyof NoInfer<TActions>, string>, AccessRule2>>;
1658
1824
  readonly before?: {
1659
1825
  readonly create?: (data: TModel["table"]["$create_input"], ctx: EntityContext<TModel, TInject>) => TModel["table"]["$create_input"] | Promise<TModel["table"]["$create_input"]>;
@@ -1667,7 +1833,7 @@ interface EntityConfig<
1667
1833
  readonly actions?: { readonly [K in keyof TActions] : TActions[K] };
1668
1834
  readonly relations?: EntityRelationsConfig<TModel["relations"]>;
1669
1835
  }
1670
- interface EntityDefinition<TModel extends ModelDef3 = ModelDef3> {
1836
+ interface EntityDefinition<TModel extends ModelDef4 = ModelDef4> {
1671
1837
  readonly kind: "entity";
1672
1838
  readonly name: string;
1673
1839
  readonly model: TModel;
@@ -1677,8 +1843,14 @@ interface EntityDefinition<TModel extends ModelDef3 = ModelDef3> {
1677
1843
  readonly after: EntityAfterHooks;
1678
1844
  readonly actions: Record<string, EntityActionDef>;
1679
1845
  readonly relations: EntityRelationsConfig<TModel["relations"]>;
1680
- }
1681
- 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";
1682
1854
  /** Extracts the model type from an EntityDefinition */
1683
1855
  type ExtractModel2<T> = T extends EntityDefinition<infer M> ? M : never;
1684
1856
  /**
@@ -1686,9 +1858,18 @@ type ExtractModel2<T> = T extends EntityDefinition<infer M> ? M : never;
1686
1858
  * `{ key: EntityOperations<TModel> }` for typed ctx.entities access.
1687
1859
  */
1688
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
+ }
1689
1868
  interface ServiceContext<TInject extends Record<string, EntityDefinition> = {}> extends BaseContext {
1690
1869
  /** Typed access to injected entities only */
1691
1870
  readonly entities: InjectToOperations2<TInject>;
1871
+ /** Raw request metadata — URL, method, headers, pre-parsed body */
1872
+ readonly request: ServiceRequestInfo;
1692
1873
  }
1693
1874
  interface ServiceActionDef<
1694
1875
  TInput = unknown,
@@ -1697,8 +1878,8 @@ interface ServiceActionDef<
1697
1878
  > {
1698
1879
  readonly method?: string;
1699
1880
  readonly path?: string;
1700
- readonly body: SchemaLike2<TInput>;
1701
- readonly response: SchemaLike2<TOutput>;
1881
+ readonly body?: SchemaLike3<TInput>;
1882
+ readonly response: SchemaLike3<TOutput>;
1702
1883
  readonly handler: (input: TInput, ctx: TCtx) => Promise<TOutput>;
1703
1884
  }
1704
1885
  interface ServiceConfig<
@@ -1719,6 +1900,8 @@ interface ServiceDefinition {
1719
1900
  interface ServerInstance extends AppBuilder {
1720
1901
  auth: AuthInstance;
1721
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>;
1722
1905
  }
1723
1906
  interface ServerConfig extends Omit<AppConfig, "_entityDbFactory" | "entities"> {
1724
1907
  /** Entity definitions created via entity() from @vertz/server */
@@ -1731,9 +1914,13 @@ interface ServerConfig extends Omit<AppConfig, "_entityDbFactory" | "entities">
1731
1914
  * - A DatabaseClient from createDb() (recommended — auto-bridged per entity)
1732
1915
  * - An EntityDbAdapter (deprecated — simple adapter with get/list/create/update/delete)
1733
1916
  */
1734
- db?: DatabaseClient<Record<string, ModelEntry2>> | EntityDbAdapter3;
1917
+ db?: DatabaseClient2<Record<string, ModelEntry2>> | EntityDbAdapter3;
1735
1918
  /** @internal Factory to create a DB adapter for each entity. Prefer `db` instead. */
1736
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>;
1737
1924
  /** Auth configuration — when combined with db, auto-wires DB-backed stores */
1738
1925
  auth?: AuthConfig;
1739
1926
  }
@@ -1747,11 +1934,17 @@ interface ServerConfig extends Omit<AppConfig, "_entityDbFactory" | "entities">
1747
1934
  * - Returns ServerInstance with `.auth` and `.initialize()`
1748
1935
  */
1749
1936
  declare function createServer(config: ServerConfig & {
1750
- db: DatabaseClient<Record<string, ModelEntry2>>;
1937
+ db: DatabaseClient2<Record<string, ModelEntry2>>;
1751
1938
  auth: AuthConfig;
1752
1939
  }): ServerInstance;
1753
1940
  declare function createServer(config: ServerConfig): AppBuilder;
1754
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
+ }
1755
1948
  /**
1756
1949
  * Evaluates an access rule for the given operation.
1757
1950
  * Returns err(EntityForbiddenError) if access is denied.
@@ -1760,11 +1953,16 @@ import { EntityForbiddenError, Result as Result4 } from "@vertz/errors";
1760
1953
  *
1761
1954
  * - No rule defined → deny (deny by default)
1762
1955
  * - Rule is false → operation is disabled
1763
- * - Rule is { type: 'public' } → always allow
1764
- * - Rule is a function → evaluate and deny if returns false
1956
+ * - Descriptor rule dispatch 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.
1765
1961
  */
1766
- declare function enforceAccess(operation: string, accessRules: Partial<Record<string, AccessRule2>>, ctx: BaseContext, row?: Record<string, unknown>): Promise<Result4<void, EntityForbiddenError>>;
1767
- import { ModelDef as ModelDef4 } 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";
1768
1966
  /**
1769
1967
  * Request info extracted from HTTP context / auth middleware.
1770
1968
  */
@@ -1776,10 +1974,10 @@ interface RequestInfo {
1776
1974
  /**
1777
1975
  * Creates an EntityContext from request info, entity operations, and registry proxy.
1778
1976
  */
1779
- declare function createEntityContext<TModel extends ModelDef4 = ModelDef4>(request: RequestInfo, entityOps: EntityOperations<TModel>, registryProxy: Record<string, EntityOperations>): EntityContext<TModel>;
1780
- import { ModelDef as ModelDef5 } 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";
1781
1979
  declare function entity<
1782
- TModel extends ModelDef5,
1980
+ TModel extends ModelDef7,
1783
1981
  TInject extends Record<string, EntityDefinition> = {},
1784
1982
  TActions extends Record<string, EntityActionDef<any, any, TModel["table"]["$response"], EntityContext<TModel, TInject>>> = {}
1785
1983
  >(name: string, config: EntityConfig<TModel, TActions, TInject>): EntityDefinition<TModel>;
@@ -1827,6 +2025,10 @@ declare function stripReadOnlyFields(table: TableDef2, data: Record<string, unkn
1827
2025
  import { EntityRouteEntry } from "@vertz/core";
1828
2026
  interface EntityRouteOptions {
1829
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;
1830
2032
  }
1831
2033
  /**
1832
2034
  * Generates HTTP route entries for a single entity definition.
@@ -1840,4 +2042,4 @@ declare function service<
1840
2042
  TInject extends Record<string, EntityDefinition> = {},
1841
2043
  TActions extends Record<string, ServiceActionDef<any, any, any>> = Record<string, ServiceActionDef<any, any, any>>
1842
2044
  >(name: string, config: ServiceConfig<TActions, TInject>): ServiceDefinition;
1843
- export { vertz, verifyPassword, validatePassword, validateOverrides, validateAuthModels, stripReadOnlyFields, stripHiddenFields, service, rules, makeImmutable, 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, computePlanHash, computeOverage, computeEntityAccess, computeAccessSet, checkFva, checkAddOnCompatibility, calculateBillingPeriod, authModels, WebhookHandlerConfig, WalletStore, WalletEntry, VertzException, ValidationException, UserTableEntry, UserStore, UnauthorizedException, TenantOverrides, StripeProduct, StripePrice, StripeClient, StripeBillingAdapterConfig, StoredSession, StoredPasswordReset, StoredEmailVerification, SignUpInput, SignInInput, SessionStrategy, SessionStore, SessionPayload, SessionInfo, SessionConfig, Session, ServiceUnavailableException, ServiceDefinition, ServiceContext, ServiceConfig, ServiceActionDef, ServerInstance, ServerHandle, ServerConfig, ServerAdapter, RuleContext, RoleAssignmentTableEntry, RoleAssignmentStore, RoleAssignment, ResourceRef, Resource, RequestInfo, RawRequest, RateLimitStore, RateLimitResult, RateLimitConfig, PriceInterval, PlanVersionStore, PlanVersionInfo, PlanStore, PlanSnapshot, PlanPrice, PlanHashInput, PlanDef, Period, PasswordResetStore, PasswordResetConfig, PasswordRequirements, ParentRef, OverrideStore, OverageInput, OverageConfig, OrgPlan, OAuthUserInfo, OAuthTokens, OAuthProviderConfig, OAuthProvider, OAuthAccountStore, NotFoundException, NamedMiddlewareDef, MiddlewareDef, MfaSetupData, MfaConfig, MfaChallengeData, MFAStore, ListenOptions, ListResult, ListOptions2 as ListOptions, LimitOverrideDef, LimitOverride, LimitDef, InternalServerErrorException, InferSchema, Infer, InMemoryWalletStore, InMemoryUserStore, InMemorySessionStore, InMemoryRoleAssignmentStore, InMemoryRateLimitStore, InMemoryPlanVersionStore, InMemoryPlanStore, 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, EntitlementDefinition, EntitlementDef, Entitlement, EncodedAccessSet, EmailVerificationStore, EmailVerificationConfig, EmailPasswordConfig, Deps, DenialReason, DenialMeta, DefineAccessInput, DeepReadonly, DbUserStore, DbSessionStore, DbRoleAssignmentStore, DbPlanStore, DbOAuthAccountStore, DbFlagStore, DbDialectName, DbClosureStore, Ctx, CrudResult, CrudHandlers, CorsConfig, CookieConfig, ConsumeResult, ConflictException, ComputeAccessSetConfig, ClosureStore, ClosureRow, ClosureEntry, BillingPeriod, BillingEventType, BillingEventHandler, BillingEventEmitter, BillingEvent, BillingAdapter, BaseContext, BadRequestException, AuthorizationError, AuthUser, AuthTokens, AuthInstance, AuthDbClient, AuthContext, AuthConfig, 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 };
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 };