@vertz/server 0.2.11 → 0.2.13
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.
- package/README.md +76 -0
- package/dist/index.d.ts +1001 -156
- package/dist/index.js +3491 -512
- package/package.json +6 -6
package/dist/index.d.ts
CHANGED
|
@@ -1,167 +1,315 @@
|
|
|
1
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";
|
|
2
2
|
import { BadRequestException, ConflictException, createEnv, createImmutableProxy, createMiddleware, deepFreeze, ForbiddenException, InternalServerErrorException, makeImmutable, NotFoundException, ServiceUnavailableException, UnauthorizedException, ValidationException, VertzException, vertz } from "@vertz/core";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
import { ModelEntry } from "@vertz/db";
|
|
4
|
+
import { AuthError, Result } from "@vertz/errors";
|
|
5
|
+
/**
|
|
6
|
+
* defineAccess() — Phase 6 RBAC & Access Control configuration
|
|
7
|
+
*
|
|
8
|
+
* Replaces createAccess() with hierarchical RBAC: resources form trees,
|
|
9
|
+
* roles inherit down the hierarchy, and entitlements map to roles/plans/flags.
|
|
10
|
+
*/
|
|
11
|
+
/** Denial reasons ordered by actionability (most actionable first) */
|
|
12
|
+
type DenialReason = "plan_required" | "role_required" | "limit_reached" | "flag_disabled" | "hierarchy_denied" | "step_up_required" | "not_authenticated";
|
|
13
|
+
/** Metadata attached to denial reasons */
|
|
14
|
+
interface DenialMeta {
|
|
15
|
+
requiredPlans?: string[];
|
|
16
|
+
requiredRoles?: string[];
|
|
17
|
+
disabledFlags?: string[];
|
|
18
|
+
limit?: {
|
|
19
|
+
max: number;
|
|
20
|
+
consumed: number;
|
|
21
|
+
remaining: number;
|
|
22
|
+
};
|
|
23
|
+
fvaMaxAge?: number;
|
|
14
24
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
/** Result of a full access check (all layers evaluated) */
|
|
26
|
+
interface AccessCheckResult {
|
|
27
|
+
allowed: boolean;
|
|
28
|
+
reasons: DenialReason[];
|
|
29
|
+
reason?: DenialReason;
|
|
30
|
+
meta?: DenialMeta;
|
|
18
31
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
delete(ctx: EntityContext, id: string): Promise<Result<CrudResult<null>, EntityError>>;
|
|
32
|
+
/** Entitlement definition — maps to roles, optional plans and flags */
|
|
33
|
+
interface EntitlementDef {
|
|
34
|
+
roles: string[];
|
|
35
|
+
plans?: string[];
|
|
36
|
+
flags?: string[];
|
|
25
37
|
}
|
|
26
|
-
|
|
38
|
+
/** Billing period for plan limits */
|
|
39
|
+
type BillingPeriod = "month" | "day" | "hour";
|
|
40
|
+
/** Limit definition within a plan */
|
|
41
|
+
interface LimitDef {
|
|
42
|
+
per: BillingPeriod;
|
|
43
|
+
max: number;
|
|
44
|
+
}
|
|
45
|
+
/** Plan definition — which entitlements are included and their usage limits */
|
|
46
|
+
interface PlanDef {
|
|
47
|
+
entitlements: readonly string[] | string[];
|
|
48
|
+
limits?: Record<string, LimitDef>;
|
|
49
|
+
}
|
|
50
|
+
/** The input config for defineAccess() */
|
|
51
|
+
interface DefineAccessInput {
|
|
52
|
+
hierarchy: string[];
|
|
53
|
+
roles: Record<string, string[]>;
|
|
54
|
+
inheritance?: Record<string, Record<string, string>>;
|
|
55
|
+
entitlements: Record<string, EntitlementDef>;
|
|
56
|
+
plans?: Record<string, PlanDef>;
|
|
57
|
+
/** Fallback plan name when an org's plan expires. Defaults to 'free'. */
|
|
58
|
+
defaultPlan?: string;
|
|
59
|
+
}
|
|
60
|
+
/** The frozen config returned by defineAccess() */
|
|
61
|
+
interface AccessDefinition {
|
|
62
|
+
readonly hierarchy: readonly string[];
|
|
63
|
+
readonly roles: Readonly<Record<string, readonly string[]>>;
|
|
64
|
+
readonly inheritance: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
65
|
+
readonly entitlements: Readonly<Record<string, Readonly<EntitlementDef>>>;
|
|
66
|
+
readonly plans?: Readonly<Record<string, Readonly<PlanDef>>>;
|
|
67
|
+
/** Fallback plan name when an org's plan expires. Defaults to 'free'. */
|
|
68
|
+
readonly defaultPlan?: string;
|
|
69
|
+
}
|
|
70
|
+
declare function defineAccess(input: DefineAccessInput): AccessDefinition;
|
|
27
71
|
/**
|
|
28
|
-
*
|
|
72
|
+
* InMemoryClosureStore — closure table for resource hierarchy.
|
|
29
73
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
74
|
+
* Stores ancestor/descendant relationships with depth tracking.
|
|
75
|
+
* Self-reference rows (depth 0) are created for every resource.
|
|
76
|
+
* Hierarchy depth is capped at 4 levels.
|
|
32
77
|
*/
|
|
33
|
-
interface
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
78
|
+
interface ClosureRow {
|
|
79
|
+
ancestorType: string;
|
|
80
|
+
ancestorId: string;
|
|
81
|
+
descendantType: string;
|
|
82
|
+
descendantId: string;
|
|
83
|
+
depth: number;
|
|
84
|
+
}
|
|
85
|
+
interface ClosureEntry {
|
|
86
|
+
type: string;
|
|
87
|
+
id: string;
|
|
88
|
+
depth: number;
|
|
89
|
+
}
|
|
90
|
+
interface ParentRef {
|
|
91
|
+
parentType: string;
|
|
92
|
+
parentId: string;
|
|
93
|
+
}
|
|
94
|
+
interface ClosureStore {
|
|
95
|
+
addResource(type: string, id: string, parent?: ParentRef): Promise<void>;
|
|
96
|
+
removeResource(type: string, id: string): Promise<void>;
|
|
97
|
+
getAncestors(type: string, id: string): Promise<ClosureEntry[]>;
|
|
98
|
+
getDescendants(type: string, id: string): Promise<ClosureEntry[]>;
|
|
99
|
+
hasPath(ancestorType: string, ancestorId: string, descendantType: string, descendantId: string): Promise<boolean>;
|
|
100
|
+
dispose(): void;
|
|
101
|
+
}
|
|
102
|
+
declare class InMemoryClosureStore implements ClosureStore {
|
|
103
|
+
private rows;
|
|
104
|
+
addResource(type: string, id: string, parent?: ParentRef): Promise<void>;
|
|
105
|
+
removeResource(type: string, id: string): Promise<void>;
|
|
106
|
+
getAncestors(type: string, id: string): Promise<ClosureEntry[]>;
|
|
107
|
+
getDescendants(type: string, id: string): Promise<ClosureEntry[]>;
|
|
108
|
+
hasPath(ancestorType: string, ancestorId: string, descendantType: string, descendantId: string): Promise<boolean>;
|
|
109
|
+
dispose(): void;
|
|
110
|
+
}
|
|
111
|
+
interface RoleAssignment {
|
|
112
|
+
userId: string;
|
|
113
|
+
resourceType: string;
|
|
114
|
+
resourceId: string;
|
|
115
|
+
role: string;
|
|
116
|
+
}
|
|
117
|
+
interface RoleAssignmentStore {
|
|
118
|
+
assign(userId: string, resourceType: string, resourceId: string, role: string): Promise<void>;
|
|
119
|
+
revoke(userId: string, resourceType: string, resourceId: string, role: string): Promise<void>;
|
|
120
|
+
getRoles(userId: string, resourceType: string, resourceId: string): Promise<string[]>;
|
|
121
|
+
getRolesForUser(userId: string): Promise<RoleAssignment[]>;
|
|
122
|
+
getEffectiveRole(userId: string, resourceType: string, resourceId: string, accessDef: AccessDefinition, closureStore: ClosureStore): Promise<string | null>;
|
|
123
|
+
dispose(): void;
|
|
124
|
+
}
|
|
125
|
+
declare class InMemoryRoleAssignmentStore implements RoleAssignmentStore {
|
|
126
|
+
private assignments;
|
|
127
|
+
assign(userId: string, resourceType: string, resourceId: string, role: string): Promise<void>;
|
|
128
|
+
revoke(userId: string, resourceType: string, resourceId: string, role: string): Promise<void>;
|
|
129
|
+
getRoles(userId: string, resourceType: string, resourceId: string): Promise<string[]>;
|
|
130
|
+
getRolesForUser(userId: string): Promise<RoleAssignment[]>;
|
|
131
|
+
/**
|
|
132
|
+
* Compute the effective role for a user on a resource.
|
|
133
|
+
* Considers direct assignments + inherited roles from ancestors.
|
|
134
|
+
* Most permissive role wins (additive model).
|
|
135
|
+
*/
|
|
136
|
+
getEffectiveRole(userId: string, resourceType: string, resourceId: string, accessDef: AccessDefinition, closureStore: ClosureStore): Promise<string | null>;
|
|
137
|
+
dispose(): void;
|
|
39
138
|
}
|
|
40
|
-
/** Extracts the model type from an EntityDefinition */
|
|
41
|
-
type ExtractModel<T> = T extends EntityDefinition<infer M> ? M : ModelDef2;
|
|
42
139
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
140
|
+
* Feature Flag Store — per-tenant boolean feature flags.
|
|
141
|
+
*
|
|
142
|
+
* Pluggable interface with in-memory default.
|
|
143
|
+
* Used by Layer 1 of access context to gate entitlements on feature flags.
|
|
45
144
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
tenant(): boolean;
|
|
51
|
-
role(...roles: string[]): boolean;
|
|
145
|
+
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>;
|
|
52
149
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
readonly entity: EntityOperations<TModel>;
|
|
59
|
-
/** Typed access to injected entities only */
|
|
60
|
-
readonly entities: InjectToOperations<TInject>;
|
|
61
|
-
}
|
|
62
|
-
type AccessRule = false | ((ctx: BaseContext, row: Record<string, unknown>) => boolean | Promise<boolean>);
|
|
63
|
-
interface EntityBeforeHooks<
|
|
64
|
-
TCreateInput = unknown,
|
|
65
|
-
TUpdateInput = unknown
|
|
66
|
-
> {
|
|
67
|
-
readonly create?: (data: TCreateInput, ctx: EntityContext) => TCreateInput | Promise<TCreateInput>;
|
|
68
|
-
readonly update?: (data: TUpdateInput, ctx: EntityContext) => TUpdateInput | Promise<TUpdateInput>;
|
|
150
|
+
declare class InMemoryFlagStore implements FlagStore {
|
|
151
|
+
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>;
|
|
69
155
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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. */
|
|
163
|
+
interface LimitOverride {
|
|
164
|
+
max: number;
|
|
74
165
|
}
|
|
75
|
-
interface
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
readonly method?: string;
|
|
82
|
-
readonly path?: string;
|
|
83
|
-
readonly body: SchemaLike<TInput>;
|
|
84
|
-
readonly response: SchemaLike<TOutput>;
|
|
85
|
-
readonly handler: (input: TInput, ctx: TCtx, row: TResponse | null) => Promise<TOutput>;
|
|
166
|
+
interface OrgPlan {
|
|
167
|
+
orgId: string;
|
|
168
|
+
planId: string;
|
|
169
|
+
startedAt: Date;
|
|
170
|
+
expiresAt: Date | null;
|
|
171
|
+
overrides: Record<string, LimitOverride>;
|
|
86
172
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
readonly access?: Partial<Record<"list" | "get" | "create" | "update" | "delete" | Extract<keyof NoInfer<TActions>, string>, AccessRule>>;
|
|
98
|
-
readonly before?: {
|
|
99
|
-
readonly create?: (data: TModel["table"]["$create_input"], ctx: EntityContext<TModel, TInject>) => TModel["table"]["$create_input"] | Promise<TModel["table"]["$create_input"]>;
|
|
100
|
-
readonly update?: (data: TModel["table"]["$update_input"], ctx: EntityContext<TModel, TInject>) => TModel["table"]["$update_input"] | Promise<TModel["table"]["$update_input"]>;
|
|
101
|
-
};
|
|
102
|
-
readonly after?: {
|
|
103
|
-
readonly create?: (result: TModel["table"]["$response"], ctx: EntityContext<TModel, TInject>) => void | Promise<void>;
|
|
104
|
-
readonly update?: (prev: TModel["table"]["$response"], next: TModel["table"]["$response"], ctx: EntityContext<TModel, TInject>) => void | Promise<void>;
|
|
105
|
-
readonly delete?: (row: TModel["table"]["$response"], ctx: EntityContext<TModel, TInject>) => void | Promise<void>;
|
|
106
|
-
};
|
|
107
|
-
readonly actions?: { readonly [K in keyof TActions] : TActions[K] };
|
|
108
|
-
readonly relations?: EntityRelationsConfig<TModel["relations"]>;
|
|
173
|
+
interface PlanStore {
|
|
174
|
+
/**
|
|
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().
|
|
177
|
+
*/
|
|
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>;
|
|
182
|
+
dispose(): void;
|
|
109
183
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
readonly after: EntityAfterHooks;
|
|
118
|
-
readonly actions: Record<string, EntityActionDef>;
|
|
119
|
-
readonly relations: EntityRelationsConfig<TModel["relations"]>;
|
|
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>;
|
|
190
|
+
dispose(): void;
|
|
120
191
|
}
|
|
121
|
-
import { SchemaLike as SchemaLike2 } from "@vertz/db";
|
|
122
|
-
/** Extracts the model type from an EntityDefinition */
|
|
123
|
-
type ExtractModel2<T> = T extends EntityDefinition<infer M> ? M : never;
|
|
124
192
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
193
|
+
* WalletStore — consumption tracking for plan-limited entitlements.
|
|
194
|
+
*
|
|
195
|
+
* Tracks per-org usage within billing periods with atomic
|
|
196
|
+
* check-and-increment operations.
|
|
127
197
|
*/
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
198
|
+
interface WalletEntry {
|
|
199
|
+
orgId: string;
|
|
200
|
+
entitlement: string;
|
|
201
|
+
periodStart: Date;
|
|
202
|
+
periodEnd: Date;
|
|
203
|
+
consumed: number;
|
|
132
204
|
}
|
|
133
|
-
interface
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
readonly method?: string;
|
|
139
|
-
readonly path?: string;
|
|
140
|
-
readonly body: SchemaLike2<TInput>;
|
|
141
|
-
readonly response: SchemaLike2<TOutput>;
|
|
142
|
-
readonly handler: (input: TInput, ctx: TCtx) => Promise<TOutput>;
|
|
205
|
+
interface ConsumeResult {
|
|
206
|
+
success: boolean;
|
|
207
|
+
consumed: number;
|
|
208
|
+
limit: number;
|
|
209
|
+
remaining: number;
|
|
143
210
|
}
|
|
144
|
-
interface
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
readonly access?: Partial<Record<Extract<keyof NoInfer<TActions>, string>, AccessRule>>;
|
|
150
|
-
readonly actions: { readonly [K in keyof TActions] : TActions[K] };
|
|
211
|
+
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>;
|
|
215
|
+
dispose(): void;
|
|
151
216
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
217
|
+
declare class InMemoryWalletStore implements WalletStore {
|
|
218
|
+
private entries;
|
|
219
|
+
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>;
|
|
223
|
+
dispose(): void;
|
|
158
224
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
225
|
+
interface ResourceRef {
|
|
226
|
+
type: string;
|
|
227
|
+
id: string;
|
|
228
|
+
}
|
|
229
|
+
interface AccessContextConfig {
|
|
230
|
+
userId: string | null;
|
|
231
|
+
accessDef: AccessDefinition;
|
|
232
|
+
closureStore: ClosureStore;
|
|
233
|
+
roleStore: RoleAssignmentStore;
|
|
234
|
+
/** Factor verification age — seconds since last MFA. undefined if no MFA done. */
|
|
235
|
+
fva?: number;
|
|
236
|
+
/** Flag store — required for Layer 1 feature flag checks */
|
|
237
|
+
flagStore?: FlagStore;
|
|
238
|
+
/** Plan store — required for Layer 4 plan checks */
|
|
239
|
+
planStore?: PlanStore;
|
|
240
|
+
/** Wallet store — required for Layer 5 wallet checks and canAndConsume() */
|
|
241
|
+
walletStore?: WalletStore;
|
|
242
|
+
/** Resolves an org ID from a resource. Required for plan/wallet checks. */
|
|
243
|
+
orgResolver?: (resource?: ResourceRef) => Promise<string | null>;
|
|
244
|
+
}
|
|
245
|
+
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>;
|
|
249
|
+
canAll(checks: Array<{
|
|
250
|
+
entitlement: string;
|
|
251
|
+
resource?: ResourceRef;
|
|
252
|
+
}>): Promise<Map<string, boolean>>;
|
|
253
|
+
/** Atomic check + consume. Runs full can() then increments wallet if all layers pass. */
|
|
254
|
+
canAndConsume(entitlement: string, resource?: ResourceRef, amount?: number): Promise<boolean>;
|
|
255
|
+
/** Rollback a previous canAndConsume(). Use when the operation fails after consumption. */
|
|
256
|
+
unconsume(entitlement: string, resource?: ResourceRef, amount?: number): Promise<void>;
|
|
257
|
+
}
|
|
258
|
+
declare function createAccessContext(config: AccessContextConfig): AccessContext;
|
|
259
|
+
interface AccessCheckData {
|
|
260
|
+
allowed: boolean;
|
|
261
|
+
reasons: DenialReason[];
|
|
262
|
+
reason?: DenialReason;
|
|
263
|
+
meta?: DenialMeta;
|
|
264
|
+
}
|
|
265
|
+
interface AccessSet {
|
|
266
|
+
entitlements: Record<string, AccessCheckData>;
|
|
267
|
+
flags: Record<string, boolean>;
|
|
268
|
+
plan: string | null;
|
|
269
|
+
computedAt: string;
|
|
270
|
+
}
|
|
271
|
+
interface ComputeAccessSetConfig {
|
|
272
|
+
userId: string | null;
|
|
273
|
+
accessDef: AccessDefinition;
|
|
274
|
+
roleStore: RoleAssignmentStore;
|
|
275
|
+
closureStore: ClosureStore;
|
|
276
|
+
plan?: string | null;
|
|
277
|
+
/** Flag store — for feature flag state in access set */
|
|
278
|
+
flagStore?: FlagStore;
|
|
279
|
+
/** Plan store — for limit info in access set */
|
|
280
|
+
planStore?: PlanStore;
|
|
281
|
+
/** Wallet store — for consumption info in access set */
|
|
282
|
+
walletStore?: WalletStore;
|
|
283
|
+
/** Org resolver — for plan/wallet lookups */
|
|
284
|
+
orgResolver?: (resource?: ResourceRef) => Promise<string | null>;
|
|
285
|
+
/** Org ID — direct org ID for global access set (bypass orgResolver) */
|
|
286
|
+
orgId?: string | null;
|
|
287
|
+
}
|
|
288
|
+
declare function computeAccessSet(config: ComputeAccessSetConfig): Promise<AccessSet>;
|
|
289
|
+
/** Sparse encoding for JWT. Only includes allowed + denied-with-meta entries. */
|
|
290
|
+
interface EncodedAccessSet {
|
|
291
|
+
entitlements: Record<string, EncodedAccessCheckData>;
|
|
292
|
+
flags: Record<string, boolean>;
|
|
293
|
+
plan: string | null;
|
|
294
|
+
computedAt: string;
|
|
295
|
+
}
|
|
296
|
+
interface EncodedAccessCheckData {
|
|
297
|
+
allowed: boolean;
|
|
298
|
+
reasons?: DenialReason[];
|
|
299
|
+
reason?: DenialReason;
|
|
300
|
+
meta?: DenialMeta;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Encode an AccessSet for JWT embedding.
|
|
304
|
+
* Sparse: only includes allowed entitlements and denied entries with meta.
|
|
305
|
+
* Strips requiredRoles and requiredPlans from meta (organizational info).
|
|
306
|
+
*/
|
|
307
|
+
declare function encodeAccessSet(set: AccessSet): EncodedAccessSet;
|
|
308
|
+
/**
|
|
309
|
+
* Decode a sparse encoded AccessSet back to full form.
|
|
310
|
+
* Missing entitlements default to denied with 'role_required'.
|
|
311
|
+
*/
|
|
312
|
+
declare function decodeAccessSet(encoded: EncodedAccessSet, accessDef: AccessDefinition): AccessSet;
|
|
165
313
|
type SessionStrategy = "jwt" | "database" | "hybrid";
|
|
166
314
|
interface CookieConfig {
|
|
167
315
|
name?: string;
|
|
@@ -174,8 +322,10 @@ interface CookieConfig {
|
|
|
174
322
|
interface SessionConfig {
|
|
175
323
|
strategy: SessionStrategy;
|
|
176
324
|
ttl: string | number;
|
|
325
|
+
refreshTtl?: string | number;
|
|
177
326
|
refreshable?: boolean;
|
|
178
327
|
cookie?: CookieConfig;
|
|
328
|
+
refreshName?: string;
|
|
179
329
|
}
|
|
180
330
|
interface PasswordRequirements {
|
|
181
331
|
minLength?: number;
|
|
@@ -192,11 +342,168 @@ interface RateLimitConfig {
|
|
|
192
342
|
window: string;
|
|
193
343
|
maxAttempts: number;
|
|
194
344
|
}
|
|
345
|
+
interface SessionStore {
|
|
346
|
+
createSessionWithId(id: string, data: {
|
|
347
|
+
userId: string;
|
|
348
|
+
refreshTokenHash: string;
|
|
349
|
+
ipAddress: string;
|
|
350
|
+
userAgent: string;
|
|
351
|
+
expiresAt: Date;
|
|
352
|
+
currentTokens?: AuthTokens;
|
|
353
|
+
}): Promise<StoredSession>;
|
|
354
|
+
findByRefreshHash(hash: string): Promise<StoredSession | null>;
|
|
355
|
+
findByPreviousRefreshHash(hash: string): Promise<StoredSession | null>;
|
|
356
|
+
revokeSession(id: string): Promise<void>;
|
|
357
|
+
listActiveSessions(userId: string): Promise<StoredSession[]>;
|
|
358
|
+
countActiveSessions(userId: string): Promise<number>;
|
|
359
|
+
getCurrentTokens(sessionId: string): Promise<AuthTokens | null>;
|
|
360
|
+
updateSession(id: string, data: {
|
|
361
|
+
refreshTokenHash: string;
|
|
362
|
+
previousRefreshHash: string;
|
|
363
|
+
lastActiveAt: Date;
|
|
364
|
+
currentTokens?: AuthTokens;
|
|
365
|
+
}): Promise<void>;
|
|
366
|
+
dispose(): void;
|
|
367
|
+
}
|
|
368
|
+
interface StoredSession {
|
|
369
|
+
id: string;
|
|
370
|
+
userId: string;
|
|
371
|
+
refreshTokenHash: string;
|
|
372
|
+
previousRefreshHash: string | null;
|
|
373
|
+
ipAddress: string;
|
|
374
|
+
userAgent: string;
|
|
375
|
+
createdAt: Date;
|
|
376
|
+
lastActiveAt: Date;
|
|
377
|
+
expiresAt: Date;
|
|
378
|
+
revokedAt: Date | null;
|
|
379
|
+
}
|
|
380
|
+
interface RateLimitStore {
|
|
381
|
+
check(key: string, maxAttempts: number, windowMs: number): Promise<RateLimitResult>;
|
|
382
|
+
dispose(): void;
|
|
383
|
+
}
|
|
384
|
+
interface UserStore {
|
|
385
|
+
createUser(user: AuthUser, passwordHash: string | null): Promise<void>;
|
|
386
|
+
findByEmail(email: string): Promise<{
|
|
387
|
+
user: AuthUser;
|
|
388
|
+
passwordHash: string | null;
|
|
389
|
+
} | null>;
|
|
390
|
+
findById(id: string): Promise<AuthUser | null>;
|
|
391
|
+
updatePasswordHash(userId: string, passwordHash: string): Promise<void>;
|
|
392
|
+
updateEmailVerified(userId: string, verified: boolean): Promise<void>;
|
|
393
|
+
}
|
|
394
|
+
interface MfaConfig {
|
|
395
|
+
enabled?: boolean;
|
|
396
|
+
issuer?: string;
|
|
397
|
+
backupCodeCount?: number;
|
|
398
|
+
}
|
|
399
|
+
interface MFAStore {
|
|
400
|
+
enableMfa(userId: string, encryptedSecret: string): Promise<void>;
|
|
401
|
+
disableMfa(userId: string): Promise<void>;
|
|
402
|
+
getSecret(userId: string): Promise<string | null>;
|
|
403
|
+
isMfaEnabled(userId: string): Promise<boolean>;
|
|
404
|
+
setBackupCodes(userId: string, hashedCodes: string[]): Promise<void>;
|
|
405
|
+
getBackupCodes(userId: string): Promise<string[]>;
|
|
406
|
+
consumeBackupCode(userId: string, hashedCode: string): Promise<void>;
|
|
407
|
+
dispose(): void;
|
|
408
|
+
}
|
|
409
|
+
interface MfaSetupData {
|
|
410
|
+
secret: string;
|
|
411
|
+
uri: string;
|
|
412
|
+
}
|
|
413
|
+
interface MfaChallengeData {
|
|
414
|
+
userId: string;
|
|
415
|
+
sessionId?: string;
|
|
416
|
+
expiresAt: number;
|
|
417
|
+
}
|
|
418
|
+
interface OAuthProviderConfig {
|
|
419
|
+
clientId: string;
|
|
420
|
+
clientSecret: string;
|
|
421
|
+
redirectUrl?: string;
|
|
422
|
+
scopes?: string[];
|
|
423
|
+
}
|
|
424
|
+
interface OAuthTokens {
|
|
425
|
+
accessToken: string;
|
|
426
|
+
refreshToken?: string;
|
|
427
|
+
expiresIn?: number;
|
|
428
|
+
idToken?: string;
|
|
429
|
+
}
|
|
430
|
+
interface OAuthUserInfo {
|
|
431
|
+
providerId: string;
|
|
432
|
+
email: string;
|
|
433
|
+
emailVerified: boolean;
|
|
434
|
+
name?: string;
|
|
435
|
+
avatarUrl?: string;
|
|
436
|
+
}
|
|
437
|
+
interface OAuthProvider {
|
|
438
|
+
id: string;
|
|
439
|
+
name: string;
|
|
440
|
+
scopes: string[];
|
|
441
|
+
trustEmail: boolean;
|
|
442
|
+
getAuthorizationUrl: (state: string, codeChallenge?: string, nonce?: string) => string;
|
|
443
|
+
exchangeCode: (code: string, codeVerifier?: string) => Promise<OAuthTokens>;
|
|
444
|
+
getUserInfo: (accessToken: string, idToken?: string, nonce?: string) => Promise<OAuthUserInfo>;
|
|
445
|
+
}
|
|
446
|
+
interface OAuthAccountStore {
|
|
447
|
+
linkAccount(userId: string, provider: string, providerId: string, email?: string): Promise<void>;
|
|
448
|
+
findByProviderAccount(provider: string, providerId: string): Promise<string | null>;
|
|
449
|
+
findByUserId(userId: string): Promise<{
|
|
450
|
+
provider: string;
|
|
451
|
+
providerId: string;
|
|
452
|
+
}[]>;
|
|
453
|
+
unlinkAccount(userId: string, provider: string): Promise<void>;
|
|
454
|
+
dispose(): void;
|
|
455
|
+
}
|
|
456
|
+
interface EmailVerificationConfig {
|
|
457
|
+
enabled: boolean;
|
|
458
|
+
tokenTtl?: string | number;
|
|
459
|
+
onSend: (user: AuthUser, token: string) => Promise<void>;
|
|
460
|
+
}
|
|
461
|
+
interface StoredEmailVerification {
|
|
462
|
+
id: string;
|
|
463
|
+
userId: string;
|
|
464
|
+
tokenHash: string;
|
|
465
|
+
expiresAt: Date;
|
|
466
|
+
createdAt: Date;
|
|
467
|
+
}
|
|
468
|
+
interface EmailVerificationStore {
|
|
469
|
+
createVerification(data: {
|
|
470
|
+
userId: string;
|
|
471
|
+
tokenHash: string;
|
|
472
|
+
expiresAt: Date;
|
|
473
|
+
}): Promise<StoredEmailVerification>;
|
|
474
|
+
findByTokenHash(tokenHash: string): Promise<StoredEmailVerification | null>;
|
|
475
|
+
deleteByUserId(userId: string): Promise<void>;
|
|
476
|
+
deleteByTokenHash(tokenHash: string): Promise<void>;
|
|
477
|
+
dispose(): void;
|
|
478
|
+
}
|
|
479
|
+
interface PasswordResetConfig {
|
|
480
|
+
enabled: boolean;
|
|
481
|
+
tokenTtl?: string | number;
|
|
482
|
+
revokeSessionsOnReset?: boolean;
|
|
483
|
+
onSend: (user: AuthUser, token: string) => Promise<void>;
|
|
484
|
+
}
|
|
485
|
+
interface StoredPasswordReset {
|
|
486
|
+
id: string;
|
|
487
|
+
userId: string;
|
|
488
|
+
tokenHash: string;
|
|
489
|
+
expiresAt: Date;
|
|
490
|
+
createdAt: Date;
|
|
491
|
+
}
|
|
492
|
+
interface PasswordResetStore {
|
|
493
|
+
createReset(data: {
|
|
494
|
+
userId: string;
|
|
495
|
+
tokenHash: string;
|
|
496
|
+
expiresAt: Date;
|
|
497
|
+
}): Promise<StoredPasswordReset>;
|
|
498
|
+
findByTokenHash(tokenHash: string): Promise<StoredPasswordReset | null>;
|
|
499
|
+
deleteByUserId(userId: string): Promise<void>;
|
|
500
|
+
dispose(): void;
|
|
501
|
+
}
|
|
195
502
|
interface AuthConfig {
|
|
196
503
|
session: SessionConfig;
|
|
197
504
|
emailPassword?: EmailPasswordConfig;
|
|
198
505
|
jwtSecret?: string;
|
|
199
|
-
jwtAlgorithm?: "HS256" | "HS384" | "HS512"
|
|
506
|
+
jwtAlgorithm?: "HS256" | "HS384" | "HS512";
|
|
200
507
|
/** Custom claims function for JWT payload */
|
|
201
508
|
claims?: (user: AuthUser) => Record<string, unknown>;
|
|
202
509
|
/**
|
|
@@ -211,12 +518,50 @@ interface AuthConfig {
|
|
|
211
518
|
* Only used in non-production mode when jwtSecret is not provided.
|
|
212
519
|
*/
|
|
213
520
|
devSecretPath?: string;
|
|
521
|
+
/** Pluggable session store — defaults to InMemorySessionStore */
|
|
522
|
+
sessionStore?: SessionStore;
|
|
523
|
+
/** Pluggable rate limit store — defaults to InMemoryRateLimitStore */
|
|
524
|
+
rateLimitStore?: RateLimitStore;
|
|
525
|
+
/** Pluggable user store — defaults to InMemoryUserStore */
|
|
526
|
+
userStore?: UserStore;
|
|
527
|
+
/** OAuth provider instances */
|
|
528
|
+
providers?: OAuthProvider[];
|
|
529
|
+
/** Pluggable OAuth account store — required when providers are configured */
|
|
530
|
+
oauthAccountStore?: OAuthAccountStore;
|
|
531
|
+
/** Encryption key for OAuth state cookies — required when providers are configured */
|
|
532
|
+
oauthEncryptionKey?: string;
|
|
533
|
+
/** Redirect URL after successful OAuth (default '/') */
|
|
534
|
+
oauthSuccessRedirect?: string;
|
|
535
|
+
/** Redirect URL on OAuth error (default '/auth/error') */
|
|
536
|
+
oauthErrorRedirect?: string;
|
|
537
|
+
/** MFA configuration */
|
|
538
|
+
mfa?: MfaConfig;
|
|
539
|
+
/** Pluggable MFA store — defaults to InMemoryMFAStore */
|
|
540
|
+
mfaStore?: MFAStore;
|
|
541
|
+
/** Email verification configuration */
|
|
542
|
+
emailVerification?: EmailVerificationConfig;
|
|
543
|
+
/** Pluggable email verification store — defaults to InMemoryEmailVerificationStore */
|
|
544
|
+
emailVerificationStore?: EmailVerificationStore;
|
|
545
|
+
/** Password reset configuration */
|
|
546
|
+
passwordReset?: PasswordResetConfig;
|
|
547
|
+
/** Pluggable password reset store — defaults to InMemoryPasswordResetStore */
|
|
548
|
+
passwordResetStore?: PasswordResetStore;
|
|
549
|
+
/** Access control configuration — enables ACL claim in JWT */
|
|
550
|
+
access?: AuthAccessConfig;
|
|
551
|
+
}
|
|
552
|
+
/** Access control configuration for JWT acl claim computation. */
|
|
553
|
+
interface AuthAccessConfig {
|
|
554
|
+
definition: AccessDefinition;
|
|
555
|
+
roleStore: RoleAssignmentStore;
|
|
556
|
+
closureStore: ClosureStore;
|
|
557
|
+
flagStore?: FlagStore;
|
|
214
558
|
}
|
|
215
559
|
interface AuthUser {
|
|
216
560
|
id: string;
|
|
217
561
|
email: string;
|
|
218
562
|
role: string;
|
|
219
563
|
plan?: string;
|
|
564
|
+
emailVerified?: boolean;
|
|
220
565
|
createdAt: Date;
|
|
221
566
|
updatedAt: Date;
|
|
222
567
|
[key: string]: unknown;
|
|
@@ -227,12 +572,41 @@ interface SessionPayload {
|
|
|
227
572
|
role: string;
|
|
228
573
|
iat: number;
|
|
229
574
|
exp: number;
|
|
575
|
+
jti: string;
|
|
576
|
+
sid: string;
|
|
230
577
|
claims?: Record<string, unknown>;
|
|
578
|
+
fva?: number;
|
|
579
|
+
acl?: AclClaim;
|
|
580
|
+
}
|
|
581
|
+
/** JWT acl claim — embedded access set with overflow strategy. */
|
|
582
|
+
interface AclClaim {
|
|
583
|
+
/** Full sparse set when fits within 2KB budget */
|
|
584
|
+
set?: EncodedAccessSet;
|
|
585
|
+
/** SHA-256 hex of canonical JSON — always present */
|
|
586
|
+
hash: string;
|
|
587
|
+
/** True when set omitted due to size */
|
|
588
|
+
overflow: boolean;
|
|
589
|
+
}
|
|
590
|
+
interface AuthTokens {
|
|
591
|
+
jwt: string;
|
|
592
|
+
refreshToken: string;
|
|
231
593
|
}
|
|
232
594
|
interface Session {
|
|
233
595
|
user: AuthUser;
|
|
234
596
|
expiresAt: Date;
|
|
235
597
|
payload: SessionPayload;
|
|
598
|
+
tokens?: AuthTokens;
|
|
599
|
+
}
|
|
600
|
+
interface SessionInfo {
|
|
601
|
+
id: string;
|
|
602
|
+
userId: string;
|
|
603
|
+
ipAddress: string;
|
|
604
|
+
userAgent: string;
|
|
605
|
+
deviceName: string;
|
|
606
|
+
createdAt: Date;
|
|
607
|
+
lastActiveAt: Date;
|
|
608
|
+
expiresAt: Date;
|
|
609
|
+
isCurrent: boolean;
|
|
236
610
|
}
|
|
237
611
|
interface SignUpInput {
|
|
238
612
|
email: string;
|
|
@@ -245,11 +619,22 @@ interface SignInInput {
|
|
|
245
619
|
password: string;
|
|
246
620
|
}
|
|
247
621
|
interface AuthApi {
|
|
248
|
-
signUp: (data: SignUpInput
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
622
|
+
signUp: (data: SignUpInput, ctx?: {
|
|
623
|
+
headers: Headers;
|
|
624
|
+
}) => Promise<Result<Session, AuthError>>;
|
|
625
|
+
signIn: (data: SignInInput, ctx?: {
|
|
626
|
+
headers: Headers;
|
|
627
|
+
}) => Promise<Result<Session, AuthError>>;
|
|
628
|
+
signOut: (ctx: {
|
|
629
|
+
headers: Headers;
|
|
630
|
+
}) => Promise<Result<void, AuthError>>;
|
|
631
|
+
getSession: (headers: Headers) => Promise<Result<Session | null, AuthError>>;
|
|
632
|
+
refreshSession: (ctx: {
|
|
633
|
+
headers: Headers;
|
|
634
|
+
}) => Promise<Result<Session, AuthError>>;
|
|
635
|
+
listSessions: (headers: Headers) => Promise<Result<SessionInfo[], AuthError>>;
|
|
636
|
+
revokeSession: (sessionId: string, headers: Headers) => Promise<Result<void, AuthError>>;
|
|
637
|
+
revokeAllSessions: (headers: Headers) => Promise<Result<void, AuthError>>;
|
|
253
638
|
}
|
|
254
639
|
interface AuthInstance {
|
|
255
640
|
/** HTTP handler for auth routes */
|
|
@@ -257,9 +642,11 @@ interface AuthInstance {
|
|
|
257
642
|
/** Server-side API */
|
|
258
643
|
api: AuthApi;
|
|
259
644
|
/** Session middleware that injects ctx.user */
|
|
260
|
-
middleware: () =>
|
|
645
|
+
middleware: () => (ctx: Record<string, unknown>, next: () => Promise<void>) => Promise<void>;
|
|
261
646
|
/** Initialize auth (create tables, etc.) */
|
|
262
647
|
initialize: () => Promise<void>;
|
|
648
|
+
/** Dispose stores and cleanup intervals */
|
|
649
|
+
dispose: () => void;
|
|
263
650
|
}
|
|
264
651
|
interface AuthContext {
|
|
265
652
|
headers: Headers;
|
|
@@ -271,6 +658,51 @@ interface RateLimitResult {
|
|
|
271
658
|
remaining: number;
|
|
272
659
|
resetAt: Date;
|
|
273
660
|
}
|
|
661
|
+
interface UserTableEntry extends ModelEntry<any, any> {
|
|
662
|
+
table: {
|
|
663
|
+
id: {
|
|
664
|
+
type: string;
|
|
665
|
+
};
|
|
666
|
+
email: {
|
|
667
|
+
type: string;
|
|
668
|
+
};
|
|
669
|
+
passwordHash: {
|
|
670
|
+
type: string;
|
|
671
|
+
};
|
|
672
|
+
role: {
|
|
673
|
+
type: string;
|
|
674
|
+
};
|
|
675
|
+
plan?: {
|
|
676
|
+
type: string;
|
|
677
|
+
};
|
|
678
|
+
createdAt: {
|
|
679
|
+
type: Date;
|
|
680
|
+
};
|
|
681
|
+
updatedAt: {
|
|
682
|
+
type: Date;
|
|
683
|
+
};
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
interface RoleAssignmentTableEntry extends ModelEntry<any, any> {
|
|
687
|
+
table: {
|
|
688
|
+
id: {
|
|
689
|
+
type: string;
|
|
690
|
+
};
|
|
691
|
+
userId: {
|
|
692
|
+
type: string;
|
|
693
|
+
};
|
|
694
|
+
role: {
|
|
695
|
+
type: string;
|
|
696
|
+
};
|
|
697
|
+
createdAt: {
|
|
698
|
+
type: Date;
|
|
699
|
+
};
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
import { AuthValidationError } from "@vertz/errors";
|
|
703
|
+
declare function hashPassword(password: string): Promise<string>;
|
|
704
|
+
declare function verifyPassword(password: string, hash: string): Promise<boolean>;
|
|
705
|
+
declare function validatePassword(password: string, requirements?: PasswordRequirements): AuthValidationError | null;
|
|
274
706
|
type Entitlement = string;
|
|
275
707
|
interface RoleDefinition {
|
|
276
708
|
entitlements: Entitlement[];
|
|
@@ -316,17 +748,426 @@ declare class AuthorizationError extends Error {
|
|
|
316
748
|
}
|
|
317
749
|
declare function createAccess(config: AccessConfig): AccessInstance;
|
|
318
750
|
declare const defaultAccess: AccessInstance;
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
751
|
+
type AccessEvent = {
|
|
752
|
+
type: "access:flag_toggled";
|
|
753
|
+
orgId: string;
|
|
754
|
+
flag: string;
|
|
755
|
+
enabled: boolean;
|
|
756
|
+
} | {
|
|
757
|
+
type: "access:limit_updated";
|
|
758
|
+
orgId: string;
|
|
759
|
+
entitlement: string;
|
|
760
|
+
consumed: number;
|
|
761
|
+
remaining: number;
|
|
762
|
+
max: number;
|
|
763
|
+
} | {
|
|
764
|
+
type: "access:role_changed";
|
|
765
|
+
userId: string;
|
|
766
|
+
} | {
|
|
767
|
+
type: "access:plan_changed";
|
|
768
|
+
orgId: string;
|
|
769
|
+
};
|
|
770
|
+
interface AccessWsData {
|
|
771
|
+
userId: string;
|
|
772
|
+
orgId: string;
|
|
773
|
+
}
|
|
774
|
+
interface AccessEventBroadcasterConfig {
|
|
775
|
+
jwtSecret: string;
|
|
776
|
+
jwtAlgorithm?: string;
|
|
777
|
+
/** WebSocket upgrade path. Defaults to '/api/auth/access-events'. */
|
|
778
|
+
path?: string;
|
|
779
|
+
/** Cookie name for session JWT. Defaults to 'vertz.sid'. */
|
|
780
|
+
cookieName?: string;
|
|
781
|
+
}
|
|
782
|
+
interface AccessEventBroadcaster {
|
|
783
|
+
handleUpgrade(request: Request, server: BunServer): Promise<boolean>;
|
|
784
|
+
websocket: {
|
|
785
|
+
open(ws: BunWebSocket<AccessWsData>): void;
|
|
786
|
+
message(ws: BunWebSocket<AccessWsData>, msg: string | Buffer): void;
|
|
787
|
+
close(ws: BunWebSocket<AccessWsData>): void;
|
|
788
|
+
};
|
|
789
|
+
broadcastFlagToggle(orgId: string, flag: string, enabled: boolean): void;
|
|
790
|
+
broadcastLimitUpdate(orgId: string, entitlement: string, consumed: number, remaining: number, max: number): void;
|
|
791
|
+
broadcastRoleChange(userId: string): void;
|
|
792
|
+
broadcastPlanChange(orgId: string): void;
|
|
793
|
+
getConnectionCount: number;
|
|
794
|
+
}
|
|
795
|
+
/** Minimal Bun.Server interface for WebSocket upgrade */
|
|
796
|
+
interface BunServer {
|
|
797
|
+
upgrade(request: Request, options?: {
|
|
798
|
+
data?: AccessWsData;
|
|
799
|
+
}): boolean;
|
|
800
|
+
}
|
|
801
|
+
/** Minimal Bun.ServerWebSocket interface */
|
|
802
|
+
interface BunWebSocket<T> {
|
|
803
|
+
data: T;
|
|
804
|
+
send(data: string): void;
|
|
805
|
+
close(): void;
|
|
806
|
+
ping(): void;
|
|
807
|
+
}
|
|
808
|
+
declare function createAccessEventBroadcaster(config: AccessEventBroadcasterConfig): AccessEventBroadcaster;
|
|
809
|
+
interface Period {
|
|
810
|
+
periodStart: Date;
|
|
811
|
+
periodEnd: Date;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Calculate the billing period that contains `now`, anchored to `startedAt`.
|
|
815
|
+
*
|
|
816
|
+
* For 'month': walks from startedAt adding 1 month at a time until we find
|
|
817
|
+
* the period containing now. For 'day'/'hour': uses fixed-duration periods.
|
|
818
|
+
*/
|
|
819
|
+
declare function calculateBillingPeriod(startedAt: Date, per: BillingPeriod, now?: Date): Period;
|
|
820
|
+
declare class InMemoryEmailVerificationStore implements EmailVerificationStore {
|
|
821
|
+
private byId;
|
|
822
|
+
private byTokenHash;
|
|
823
|
+
private byUserId;
|
|
824
|
+
createVerification(data: {
|
|
825
|
+
userId: string;
|
|
826
|
+
tokenHash: string;
|
|
827
|
+
expiresAt: Date;
|
|
828
|
+
}): Promise<StoredEmailVerification>;
|
|
829
|
+
findByTokenHash(tokenHash: string): Promise<StoredEmailVerification | null>;
|
|
830
|
+
deleteByUserId(userId: string): Promise<void>;
|
|
831
|
+
deleteByTokenHash(tokenHash: string): Promise<void>;
|
|
832
|
+
dispose(): void;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Compute access metadata for a specific entity.
|
|
836
|
+
*
|
|
837
|
+
* @param entitlements - Entitlement names to check
|
|
838
|
+
* @param entity - The resource to check against
|
|
839
|
+
* @param accessContext - The server-side access context
|
|
840
|
+
* @returns Record of entitlement name to AccessCheckData (client-compatible shape)
|
|
841
|
+
*/
|
|
842
|
+
declare function computeEntityAccess(entitlements: string[], entity: {
|
|
843
|
+
type: string;
|
|
844
|
+
id: string;
|
|
845
|
+
}, accessContext: AccessContext): Promise<Record<string, AccessCheckData>>;
|
|
846
|
+
/**
|
|
847
|
+
* Check if the session's factor verification is still fresh.
|
|
848
|
+
* Returns true if `fva` exists and (now - fva) < maxAgeSeconds.
|
|
849
|
+
*/
|
|
850
|
+
declare function checkFva(payload: SessionPayload, maxAgeSeconds: number): boolean;
|
|
851
|
+
declare class InMemoryMFAStore implements MFAStore {
|
|
852
|
+
private secrets;
|
|
853
|
+
private backupCodes;
|
|
854
|
+
private enabled;
|
|
855
|
+
enableMfa(userId: string, encryptedSecret: string): Promise<void>;
|
|
856
|
+
disableMfa(userId: string): Promise<void>;
|
|
857
|
+
getSecret(userId: string): Promise<string | null>;
|
|
858
|
+
isMfaEnabled(userId: string): Promise<boolean>;
|
|
859
|
+
setBackupCodes(userId: string, hashedCodes: string[]): Promise<void>;
|
|
860
|
+
getBackupCodes(userId: string): Promise<string[]>;
|
|
861
|
+
consumeBackupCode(userId: string, hashedCode: string): Promise<void>;
|
|
862
|
+
dispose(): void;
|
|
863
|
+
}
|
|
864
|
+
declare class InMemoryOAuthAccountStore implements OAuthAccountStore {
|
|
865
|
+
private byProviderAccount;
|
|
866
|
+
private byUserId;
|
|
867
|
+
private providerKey;
|
|
868
|
+
linkAccount(userId: string, provider: string, providerId: string, email?: string): Promise<void>;
|
|
869
|
+
findByProviderAccount(provider: string, providerId: string): Promise<string | null>;
|
|
870
|
+
findByUserId(userId: string): Promise<{
|
|
871
|
+
provider: string;
|
|
872
|
+
providerId: string;
|
|
873
|
+
}[]>;
|
|
874
|
+
unlinkAccount(userId: string, provider: string): Promise<void>;
|
|
875
|
+
dispose(): void;
|
|
876
|
+
}
|
|
877
|
+
declare class InMemoryPasswordResetStore implements PasswordResetStore {
|
|
878
|
+
private byId;
|
|
879
|
+
private byTokenHash;
|
|
880
|
+
private byUserId;
|
|
881
|
+
createReset(data: {
|
|
882
|
+
userId: string;
|
|
883
|
+
tokenHash: string;
|
|
884
|
+
expiresAt: Date;
|
|
885
|
+
}): Promise<StoredPasswordReset>;
|
|
886
|
+
findByTokenHash(tokenHash: string): Promise<StoredPasswordReset | null>;
|
|
887
|
+
deleteByUserId(userId: string): Promise<void>;
|
|
888
|
+
dispose(): void;
|
|
889
|
+
}
|
|
890
|
+
declare function discord(config: OAuthProviderConfig): OAuthProvider;
|
|
891
|
+
declare function github(config: OAuthProviderConfig): OAuthProvider;
|
|
892
|
+
declare function google(config: OAuthProviderConfig): OAuthProvider;
|
|
893
|
+
declare class InMemoryRateLimitStore implements RateLimitStore {
|
|
894
|
+
private store;
|
|
895
|
+
private cleanupTimer;
|
|
896
|
+
constructor();
|
|
897
|
+
check(key: string, maxAttempts: number, windowMs: number): Promise<RateLimitResult>;
|
|
898
|
+
dispose(): void;
|
|
899
|
+
private cleanup;
|
|
900
|
+
}
|
|
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
|
+
declare class InMemorySessionStore implements SessionStore {
|
|
960
|
+
private sessions;
|
|
961
|
+
private currentTokens;
|
|
962
|
+
private cleanupTimer;
|
|
963
|
+
private maxSessionsPerUser;
|
|
964
|
+
constructor(maxSessionsPerUser?: number);
|
|
965
|
+
createSession(data: {
|
|
966
|
+
userId: string;
|
|
967
|
+
refreshTokenHash: string;
|
|
968
|
+
ipAddress: string;
|
|
969
|
+
userAgent: string;
|
|
970
|
+
expiresAt: Date;
|
|
971
|
+
}): Promise<StoredSession>;
|
|
972
|
+
createSessionWithId(id: string, data: {
|
|
973
|
+
userId: string;
|
|
974
|
+
refreshTokenHash: string;
|
|
975
|
+
ipAddress: string;
|
|
976
|
+
userAgent: string;
|
|
977
|
+
expiresAt: Date;
|
|
978
|
+
currentTokens?: AuthTokens;
|
|
979
|
+
}): Promise<StoredSession>;
|
|
980
|
+
findByRefreshHash(hash: string): Promise<StoredSession | null>;
|
|
981
|
+
findByPreviousRefreshHash(hash: string): Promise<StoredSession | null>;
|
|
982
|
+
revokeSession(id: string): Promise<void>;
|
|
983
|
+
listActiveSessions(userId: string): Promise<StoredSession[]>;
|
|
984
|
+
countActiveSessions(userId: string): Promise<number>;
|
|
985
|
+
getCurrentTokens(sessionId: string): Promise<AuthTokens | null>;
|
|
986
|
+
updateSession(id: string, data: {
|
|
987
|
+
refreshTokenHash: string;
|
|
988
|
+
previousRefreshHash: string;
|
|
989
|
+
lastActiveAt: Date;
|
|
990
|
+
currentTokens?: AuthTokens;
|
|
991
|
+
}): Promise<void>;
|
|
992
|
+
dispose(): void;
|
|
993
|
+
private cleanup;
|
|
994
|
+
}
|
|
995
|
+
declare class InMemoryUserStore implements UserStore {
|
|
996
|
+
private byEmail;
|
|
997
|
+
private byId;
|
|
998
|
+
createUser(user: AuthUser, passwordHash: string | null): Promise<void>;
|
|
999
|
+
findByEmail(email: string): Promise<{
|
|
1000
|
+
user: AuthUser;
|
|
1001
|
+
passwordHash: string | null;
|
|
1002
|
+
} | null>;
|
|
1003
|
+
findById(id: string): Promise<AuthUser | null>;
|
|
1004
|
+
updatePasswordHash(userId: string, passwordHash: string): Promise<void>;
|
|
1005
|
+
updateEmailVerified(userId: string, verified: boolean): Promise<void>;
|
|
1006
|
+
}
|
|
322
1007
|
declare function createAuth(config: AuthConfig): AuthInstance;
|
|
323
1008
|
import { AppBuilder, AppConfig } from "@vertz/core";
|
|
324
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";
|
|
1014
|
+
import { EntityDbAdapter as EntityDbAdapter2, ListOptions as ListOptions2 } from "@vertz/db";
|
|
1015
|
+
interface ListResult<T = Record<string, unknown>> {
|
|
1016
|
+
items: T[];
|
|
1017
|
+
total: number;
|
|
1018
|
+
limit: number;
|
|
1019
|
+
nextCursor: string | null;
|
|
1020
|
+
hasNextPage: boolean;
|
|
1021
|
+
}
|
|
1022
|
+
interface CrudResult<T = unknown> {
|
|
1023
|
+
status: number;
|
|
1024
|
+
body: T;
|
|
1025
|
+
}
|
|
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;
|
|
1034
|
+
/**
|
|
1035
|
+
* EntityOperations — typed CRUD facade for a single entity.
|
|
1036
|
+
*
|
|
1037
|
+
* When used as `ctx.entity`, TModel fills in actual column types.
|
|
1038
|
+
* When used as `ctx.entities.*`, TModel defaults to `ModelDef` (loose typing).
|
|
1039
|
+
*/
|
|
1040
|
+
interface EntityOperations<TModel extends ModelDef = ModelDef> {
|
|
1041
|
+
get(id: string): Promise<TModel["table"]["$response"]>;
|
|
1042
|
+
list(options?: ListOptions2): Promise<ListResult<TModel["table"]["$response"]>>;
|
|
1043
|
+
create(data: TModel["table"]["$create_input"]): Promise<TModel["table"]["$response"]>;
|
|
1044
|
+
update(id: string, data: TModel["table"]["$update_input"]): Promise<TModel["table"]["$response"]>;
|
|
1045
|
+
delete(id: string): Promise<void>;
|
|
1046
|
+
}
|
|
1047
|
+
/** Extracts the model type from an EntityDefinition */
|
|
1048
|
+
type ExtractModel<T> = T extends EntityDefinition<infer M> ? M : ModelDef2;
|
|
1049
|
+
/**
|
|
1050
|
+
* Maps an inject config `{ key: EntityDefinition<TModel> }` to
|
|
1051
|
+
* `{ key: EntityOperations<TModel> }` for typed ctx.entities access.
|
|
1052
|
+
*/
|
|
1053
|
+
type InjectToOperations<TInject extends Record<string, EntityDefinition> = {}> = { readonly [K in keyof TInject] : EntityOperations<ExtractModel<TInject[K]>> };
|
|
1054
|
+
interface BaseContext {
|
|
1055
|
+
readonly userId: string | null;
|
|
1056
|
+
authenticated(): boolean;
|
|
1057
|
+
tenant(): boolean;
|
|
1058
|
+
role(...roles: string[]): boolean;
|
|
1059
|
+
}
|
|
1060
|
+
interface EntityContext<
|
|
1061
|
+
TModel extends ModelDef2 = ModelDef2,
|
|
1062
|
+
TInject extends Record<string, EntityDefinition> = {}
|
|
1063
|
+
> extends BaseContext {
|
|
1064
|
+
/** Typed CRUD on the current entity */
|
|
1065
|
+
readonly entity: EntityOperations<TModel>;
|
|
1066
|
+
/** Typed access to injected entities only */
|
|
1067
|
+
readonly entities: InjectToOperations<TInject>;
|
|
1068
|
+
}
|
|
1069
|
+
type AccessRule2 = false | ((ctx: BaseContext, row: Record<string, unknown>) => boolean | Promise<boolean>);
|
|
1070
|
+
interface EntityBeforeHooks<
|
|
1071
|
+
TCreateInput = unknown,
|
|
1072
|
+
TUpdateInput = unknown
|
|
1073
|
+
> {
|
|
1074
|
+
readonly create?: (data: TCreateInput, ctx: EntityContext) => TCreateInput | Promise<TCreateInput>;
|
|
1075
|
+
readonly update?: (data: TUpdateInput, ctx: EntityContext) => TUpdateInput | Promise<TUpdateInput>;
|
|
1076
|
+
}
|
|
1077
|
+
interface EntityAfterHooks<TResponse = unknown> {
|
|
1078
|
+
readonly create?: (result: TResponse, ctx: EntityContext) => void | Promise<void>;
|
|
1079
|
+
readonly update?: (prev: TResponse, next: TResponse, ctx: EntityContext) => void | Promise<void>;
|
|
1080
|
+
readonly delete?: (row: TResponse, ctx: EntityContext) => void | Promise<void>;
|
|
1081
|
+
}
|
|
1082
|
+
interface EntityActionDef<
|
|
1083
|
+
TInput = unknown,
|
|
1084
|
+
TOutput = unknown,
|
|
1085
|
+
TResponse = unknown,
|
|
1086
|
+
TCtx extends EntityContext = EntityContext
|
|
1087
|
+
> {
|
|
1088
|
+
readonly method?: string;
|
|
1089
|
+
readonly path?: string;
|
|
1090
|
+
readonly body: SchemaLike<TInput>;
|
|
1091
|
+
readonly response: SchemaLike<TOutput>;
|
|
1092
|
+
readonly handler: (input: TInput, ctx: TCtx, row: TResponse | null) => Promise<TOutput>;
|
|
1093
|
+
}
|
|
1094
|
+
/** 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 } };
|
|
1097
|
+
interface EntityConfig<
|
|
1098
|
+
TModel extends ModelDef2 = ModelDef2,
|
|
1099
|
+
TActions extends Record<string, EntityActionDef<any, any, any, any>> = {},
|
|
1100
|
+
TInject extends Record<string, EntityDefinition> = {}
|
|
1101
|
+
> {
|
|
1102
|
+
readonly model: TModel;
|
|
1103
|
+
readonly inject?: TInject;
|
|
1104
|
+
readonly access?: Partial<Record<"list" | "get" | "create" | "update" | "delete" | Extract<keyof NoInfer<TActions>, string>, AccessRule2>>;
|
|
1105
|
+
readonly before?: {
|
|
1106
|
+
readonly create?: (data: TModel["table"]["$create_input"], ctx: EntityContext<TModel, TInject>) => TModel["table"]["$create_input"] | Promise<TModel["table"]["$create_input"]>;
|
|
1107
|
+
readonly update?: (data: TModel["table"]["$update_input"], ctx: EntityContext<TModel, TInject>) => TModel["table"]["$update_input"] | Promise<TModel["table"]["$update_input"]>;
|
|
1108
|
+
};
|
|
1109
|
+
readonly after?: {
|
|
1110
|
+
readonly create?: (result: TModel["table"]["$response"], ctx: EntityContext<TModel, TInject>) => void | Promise<void>;
|
|
1111
|
+
readonly update?: (prev: TModel["table"]["$response"], next: TModel["table"]["$response"], ctx: EntityContext<TModel, TInject>) => void | Promise<void>;
|
|
1112
|
+
readonly delete?: (row: TModel["table"]["$response"], ctx: EntityContext<TModel, TInject>) => void | Promise<void>;
|
|
1113
|
+
};
|
|
1114
|
+
readonly actions?: { readonly [K in keyof TActions] : TActions[K] };
|
|
1115
|
+
readonly relations?: EntityRelationsConfig<TModel["relations"]>;
|
|
1116
|
+
}
|
|
1117
|
+
interface EntityDefinition<TModel extends ModelDef2 = ModelDef2> {
|
|
1118
|
+
readonly kind: "entity";
|
|
1119
|
+
readonly name: string;
|
|
1120
|
+
readonly model: TModel;
|
|
1121
|
+
readonly inject: Record<string, EntityDefinition>;
|
|
1122
|
+
readonly access: Partial<Record<string, AccessRule2>>;
|
|
1123
|
+
readonly before: EntityBeforeHooks;
|
|
1124
|
+
readonly after: EntityAfterHooks;
|
|
1125
|
+
readonly actions: Record<string, EntityActionDef>;
|
|
1126
|
+
readonly relations: EntityRelationsConfig<TModel["relations"]>;
|
|
1127
|
+
}
|
|
1128
|
+
import { SchemaLike as SchemaLike2 } from "@vertz/db";
|
|
1129
|
+
/** Extracts the model type from an EntityDefinition */
|
|
1130
|
+
type ExtractModel2<T> = T extends EntityDefinition<infer M> ? M : never;
|
|
1131
|
+
/**
|
|
1132
|
+
* Maps an inject config `{ key: EntityDefinition<TModel> }` to
|
|
1133
|
+
* `{ key: EntityOperations<TModel> }` for typed ctx.entities access.
|
|
1134
|
+
*/
|
|
1135
|
+
type InjectToOperations2<TInject extends Record<string, EntityDefinition> = {}> = { readonly [K in keyof TInject] : EntityOperations<ExtractModel2<TInject[K]>> };
|
|
1136
|
+
interface ServiceContext<TInject extends Record<string, EntityDefinition> = {}> extends BaseContext {
|
|
1137
|
+
/** Typed access to injected entities only */
|
|
1138
|
+
readonly entities: InjectToOperations2<TInject>;
|
|
1139
|
+
}
|
|
1140
|
+
interface ServiceActionDef<
|
|
1141
|
+
TInput = unknown,
|
|
1142
|
+
TOutput = unknown,
|
|
1143
|
+
TCtx extends ServiceContext = ServiceContext
|
|
1144
|
+
> {
|
|
1145
|
+
readonly method?: string;
|
|
1146
|
+
readonly path?: string;
|
|
1147
|
+
readonly body: SchemaLike2<TInput>;
|
|
1148
|
+
readonly response: SchemaLike2<TOutput>;
|
|
1149
|
+
readonly handler: (input: TInput, ctx: TCtx) => Promise<TOutput>;
|
|
1150
|
+
}
|
|
1151
|
+
interface ServiceConfig<
|
|
1152
|
+
TActions extends Record<string, ServiceActionDef<any, any, any>> = Record<string, ServiceActionDef<any, any, any>>,
|
|
1153
|
+
TInject extends Record<string, EntityDefinition> = {}
|
|
1154
|
+
> {
|
|
1155
|
+
readonly inject?: TInject;
|
|
1156
|
+
readonly access?: Partial<Record<Extract<keyof NoInfer<TActions>, string>, AccessRule2>>;
|
|
1157
|
+
readonly actions: { readonly [K in keyof TActions] : TActions[K] };
|
|
1158
|
+
}
|
|
1159
|
+
interface ServiceDefinition {
|
|
1160
|
+
readonly kind: "service";
|
|
1161
|
+
readonly name: string;
|
|
1162
|
+
readonly inject: Record<string, EntityDefinition>;
|
|
1163
|
+
readonly access: Partial<Record<string, AccessRule2>>;
|
|
1164
|
+
readonly actions: Record<string, ServiceActionDef>;
|
|
1165
|
+
}
|
|
325
1166
|
interface ServerConfig extends Omit<AppConfig, "_entityDbFactory" | "entities"> {
|
|
326
1167
|
/** Entity definitions created via entity() from @vertz/server */
|
|
327
1168
|
entities?: EntityDefinition[];
|
|
328
|
-
/** Standalone
|
|
329
|
-
|
|
1169
|
+
/** Standalone service definitions created via service() from @vertz/server */
|
|
1170
|
+
services?: ServiceDefinition[];
|
|
330
1171
|
/**
|
|
331
1172
|
* Database for entity CRUD operations.
|
|
332
1173
|
* Accepts either:
|
|
@@ -347,13 +1188,13 @@ import { EntityForbiddenError, Result as Result3 } from "@vertz/errors";
|
|
|
347
1188
|
* Evaluates an access rule for the given operation.
|
|
348
1189
|
* Returns err(EntityForbiddenError) if access is denied.
|
|
349
1190
|
*
|
|
350
|
-
* Accepts BaseContext so both EntityContext and
|
|
1191
|
+
* Accepts BaseContext so both EntityContext and ServiceContext can use it.
|
|
351
1192
|
*
|
|
352
1193
|
* - No rule defined → deny (deny by default)
|
|
353
1194
|
* - Rule is false → operation is disabled
|
|
354
1195
|
* - Rule is a function → evaluate and deny if returns false
|
|
355
1196
|
*/
|
|
356
|
-
declare function enforceAccess(operation: string, accessRules: Partial<Record<string,
|
|
1197
|
+
declare function enforceAccess(operation: string, accessRules: Partial<Record<string, AccessRule2>>, ctx: BaseContext, row?: Record<string, unknown>): Promise<Result3<void, EntityForbiddenError>>;
|
|
357
1198
|
import { ModelDef as ModelDef3 } from "@vertz/db";
|
|
358
1199
|
/**
|
|
359
1200
|
* Request info extracted from HTTP context / auth middleware.
|
|
@@ -426,4 +1267,8 @@ interface EntityRouteOptions {
|
|
|
426
1267
|
* Operations explicitly disabled (access: false) get a 405 handler.
|
|
427
1268
|
*/
|
|
428
1269
|
declare function generateEntityRoutes(def: EntityDefinition, registry: EntityRegistry, db: EntityDbAdapter2, options?: EntityRouteOptions): EntityRouteEntry[];
|
|
429
|
-
|
|
1270
|
+
declare function service<
|
|
1271
|
+
TInject extends Record<string, EntityDefinition> = {},
|
|
1272
|
+
TActions extends Record<string, ServiceActionDef<any, any, any>> = Record<string, ServiceActionDef<any, any, any>>
|
|
1273
|
+
>(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 };
|