@veloxts/router 0.7.5 → 0.7.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @veloxts/router
2
2
 
3
+ ## 0.7.6
4
+
5
+ ### Patch Changes
6
+
7
+ - feat(router): custom access levels for the Resource API + advanced Architectural Patterns
8
+ - Updated dependencies
9
+ - @veloxts/core@0.7.6
10
+ - @veloxts/validation@0.7.6
11
+
3
12
  ## 0.7.5
4
13
 
5
14
  ### Patch Changes
package/GUIDE.md CHANGED
@@ -28,8 +28,8 @@ export const userProcedures = procedures('users', {
28
28
  ## Procedure API
29
29
 
30
30
  - `.input(schema)` - Validate input with Zod
31
- - `.output(schema)` - Validate output with Zod (same fields for all users)
32
- - `.resource(schema)` - Context-dependent output with field visibility
31
+ - `.output(schema)` - Validate output with Zod
32
+ - `.expose(schema)` - Context-dependent output with resource schema
33
33
  - `.use(middleware)` - Add middleware
34
34
  - `.guard(guard)` - Add authorization guard
35
35
  - `.guardNarrow(guard)` - Add guard with TypeScript type narrowing
@@ -430,7 +430,7 @@ console.table(routes);
430
430
 
431
431
  ## Resource API (Context-Dependent Outputs)
432
432
 
433
- The Resource API provides context-dependent output types using phantom types. Unlike `.output()` which returns the same fields to all users, `.resource()` lets you define field visibility per access level.
433
+ The Resource API provides context-dependent output types using phantom types. Pass a resource schema to `.output()` instead of a Zod schema to define field visibility per access level.
434
434
 
435
435
  ### Defining a Resource Schema
436
436
 
@@ -450,7 +450,7 @@ const UserSchema = resourceSchema()
450
450
 
451
451
  ### Automatic Projection (Simple Cases)
452
452
 
453
- The most elegant approach is to chain `.resource()` with a narrowing guard. The procedure executor automatically projects fields based on the guard's access level:
453
+ The most elegant approach is to chain `.expose()` with a narrowing guard. The procedure executor automatically projects fields based on the guard's access level:
454
454
 
455
455
  ```typescript
456
456
  import { authenticatedNarrow, adminNarrow } from '@veloxts/auth';
@@ -459,7 +459,7 @@ export const userProcedures = procedures('users', {
459
459
  // Authenticated endpoint - auto-projects { id, name, email, createdAt }
460
460
  getProfile: procedure()
461
461
  .guardNarrow(authenticatedNarrow)
462
- .resource(UserSchema)
462
+ .expose(UserSchema)
463
463
  .input(z.object({ id: z.string().uuid() }))
464
464
  .query(async ({ input, ctx }) => {
465
465
  // Just return the full data - projection is automatic!
@@ -469,7 +469,7 @@ export const userProcedures = procedures('users', {
469
469
  // Admin endpoint - auto-projects all fields
470
470
  getFullProfile: procedure()
471
471
  .guardNarrow(adminNarrow)
472
- .resource(UserSchema)
472
+ .expose(UserSchema)
473
473
  .input(z.object({ id: z.string().uuid() }))
474
474
  .query(async ({ input, ctx }) => {
475
475
  return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
@@ -544,7 +544,7 @@ For arrays of items, use `resourceCollection()`:
544
544
  // Automatic projection (simple cases)
545
545
  const listUsers = procedure()
546
546
  .guardNarrow(authenticatedNarrow)
547
- .resource(UserSchema)
547
+ .expose(UserSchema)
548
548
  .query(async ({ ctx }) => {
549
549
  return ctx.db.user.findMany({ take: 50 });
550
550
  });
@@ -590,7 +590,7 @@ The Resource API provides compile-time type safety:
590
590
  // Automatic projection - type inferred from guard
591
591
  const getProfile = procedure()
592
592
  .guardNarrow(authenticatedNarrow)
593
- .resource(UserSchema)
593
+ .expose(UserSchema)
594
594
  .query(async ({ ctx }) => {
595
595
  return ctx.db.user.findFirst();
596
596
  });
@@ -611,7 +611,7 @@ const adminResult = resource(user, UserSchema.admin);
611
611
 
612
612
  | Scenario | Approach |
613
613
  |----------|----------|
614
- | Guard determines access level | **Automatic** (`.guardNarrow().resource()`) |
614
+ | Guard determines access level | **Automatic** (`.guardNarrow().output()`) |
615
615
  | Public endpoints (no guard) | Tagged view (`UserSchema.public`) |
616
616
  | Conditional/dynamic projection | Tagged view or `.for(ctx)` in handler |
617
617
  | Simple, declarative code | **Automatic** |
package/dist/index.d.ts CHANGED
@@ -39,7 +39,7 @@
39
39
  */
40
40
  /** Router package version */
41
41
  export declare const ROUTER_VERSION: string;
42
- export type { CompiledProcedure, ContextExtensions, ContextFactory, ExtendedContext, GuardLike, HttpMethod, InferProcedureContext, InferProcedureInput, InferProcedureOutput, MiddlewareArgs, MiddlewareFunction, MiddlewareNext, MiddlewareResult, ParentResourceChain, ParentResourceConfig, ProcedureCollection, ProcedureHandler, ProcedureHandlerArgs, ProcedureRecord, ProcedureType, RestRouteOverride, } from './types.js';
42
+ export type { CompiledProcedure, ContextExtensions, ContextFactory, ExtendedContext, GuardLike, HttpMethod, InferProcedureContext, InferProcedureInput, InferProcedureOutput, Middleware, MiddlewareArgs, MiddlewareFunction, MiddlewareNext, MiddlewareResult, ParentResourceChain, ParentResourceConfig, ProcedureCollection, ProcedureHandler, ProcedureHandlerArgs, ProcedureRecord, ProcedureType, RestRouteOverride, } from './types.js';
43
43
  export { PROCEDURE_METHOD_MAP, } from './types.js';
44
44
  export type { GuardErrorResponse, RouterErrorCode } from './errors.js';
45
45
  export { GuardError, isGuardError } from './errors.js';
@@ -83,7 +83,7 @@ export { serve } from './expose.js';
83
83
  */
84
84
  export type { BuildParametersOptions, BuildParametersResult, GuardMappingOptions, JSONSchema, OpenAPIComponents, OpenAPIContact, OpenAPIEncoding, OpenAPIExample, OpenAPIExternalDocs, OpenAPIGeneratorOptions, OpenAPIHeader, OpenAPIHttpMethod, OpenAPIInfo, OpenAPILicense, OpenAPILink, OpenAPIMediaType, OpenAPIOAuthFlow, OpenAPIOAuthFlows, OpenAPIOperation, OpenAPIParameter, OpenAPIPathItem, OpenAPIRequestBody, OpenAPIResponse, OpenAPISecurityRequirement, OpenAPISecurityScheme, OpenAPIServer, OpenAPISpec, OpenAPITag, ParameterIn, QueryParamExtractionOptions, RouteInfo, SchemaConversionOptions, SecuritySchemeType, SwaggerUIConfig, SwaggerUIHtmlOptions, SwaggerUIPluginOptions, } from './openapi/index.js';
85
85
  export { buildParameters, convertFromOpenAPIPath, convertToOpenAPIPath, createSecurityRequirement, createStringSchema, createSwaggerUI, DEFAULT_GUARD_MAPPINGS, DEFAULT_SECURITY_SCHEMES, DEFAULT_UI_CONFIG, escapeHtml, extractGuardScopes, extractPathParamNames, extractQueryParameters, extractResourceFromPath, extractSchemaProperties, extractUsedSecuritySchemes, filterUsedSecuritySchemes, generateOpenApiSpec, generateSwaggerUIHtml, getOpenApiRouteSummary, getOpenApiSpec, guardsRequireAuth, guardsToSecurity, hasPathParameters, joinPaths, mapGuardToSecurity, mergeSchemas, mergeSecuritySchemes, normalizePath, parsePathParameters, registerDocs, removeSchemaProperties, SWAGGER_UI_CDN, schemaHasProperties, swaggerUIPlugin, validateOpenApiSpec, zodSchemaToJsonSchema, } from './openapi/index.js';
86
- export type { AccessLevel, ADMIN, AdminOutput, AdminTaggedContext, ANONYMOUS, AnonymousOutput, AnonymousTaggedContext, AnyResourceOutput, AUTHENTICATED, AuthenticatedOutput, AuthenticatedTaggedContext, BuilderField, ContextTag, ExtractTag, HasTag, IfAdmin, IfAuthenticated, InferResourceData, InferResourceOutput, IsVisibleToTag, OutputForTag, RelationField, ResourceField, ResourceSchema, RuntimeField, TaggedContext, VisibilityLevel, WithTag, } from './resource/index.js';
86
+ export type { AccessLevel, AccessLevelConfig, ADMIN, AdminOutput, AdminTaggedContext, ANONYMOUS, AnonymousOutput, AnonymousTaggedContext, AnyResourceOutput, AUTHENTICATED, AuthenticatedOutput, AuthenticatedTaggedContext, BuilderField, ContextTag, CustomResourceSchemaWithViews, CustomSchemaBuilder, ExtractTag, FilterFieldsByLevel, HasTag, IfAdmin, IfAuthenticated, InferResourceData, InferResourceOutput, IsVisibleToTag, LevelToTag, OutputForLevel, OutputForTag, PUBLIC, PublicOutput, PublicTaggedContext, RelationField, ResourceField, ResourceSchema, ResourceSchemaWithViews, RuntimeField, TaggedContext, TagToLevel, VisibilityLevel, WithTag, } from './resource/index.js';
87
87
  /**
88
88
  * Resource API for context-dependent output types using phantom types.
89
89
  *
@@ -109,9 +109,9 @@ export type { AccessLevel, ADMIN, AdminOutput, AdminTaggedContext, ANONYMOUS, An
109
109
  * .input(z.object({ id: z.string() }))
110
110
  * .query(async ({ input, ctx }) => {
111
111
  * const user = await ctx.db.user.findUnique({ where: { id: input.id } });
112
- * return resource(user, UserSchema).forAnonymous();
112
+ * return resource(user, UserSchema).forPublic();
113
113
  * }),
114
114
  * });
115
115
  * ```
116
116
  */
117
- export { getAccessibleLevels, getVisibilityForTag, isResourceSchema, isVisibleAtLevel, Resource, ResourceCollection, ResourceSchemaBuilder, resource, resourceCollection, resourceSchema, } from './resource/index.js';
117
+ export { defineAccessLevels, getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isResourceSchema, isVisibleAtLevel, Resource, ResourceCollection, ResourceSchemaBuilder, resource, resourceCollection, resourceSchema, } from './resource/index.js';
package/dist/index.js CHANGED
@@ -103,12 +103,14 @@ generateSwaggerUIHtml, getOpenApiRouteSummary, getOpenApiSpec, guardsRequireAuth
103
103
  * .input(z.object({ id: z.string() }))
104
104
  * .query(async ({ input, ctx }) => {
105
105
  * const user = await ctx.db.user.findUnique({ where: { id: input.id } });
106
- * return resource(user, UserSchema).forAnonymous();
106
+ * return resource(user, UserSchema).forPublic();
107
107
  * }),
108
108
  * });
109
109
  * ```
110
110
  */
111
- export { getAccessibleLevels, getVisibilityForTag, isResourceSchema,
111
+ export {
112
+ // Access level configuration
113
+ defineAccessLevels, getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isResourceSchema,
112
114
  // Visibility
113
115
  isVisibleAtLevel, Resource, ResourceCollection, ResourceSchemaBuilder,
114
116
  // Resource instances
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { generateRestRoutes } from '../rest/adapter.js';
9
9
  import { buildParameters, convertToOpenAPIPath, joinPaths } from './path-extractor.js';
10
- import { removeSchemaProperties, zodSchemaToJsonSchema } from './schema-converter.js';
10
+ import { removeSchemaProperties, resourceSchemaToJsonSchema, zodSchemaToJsonSchema, } from './schema-converter.js';
11
11
  import { extractUsedSecuritySchemes, filterUsedSecuritySchemes, guardsToSecurity, mergeSecuritySchemes, } from './security-mapper.js';
12
12
  // ============================================================================
13
13
  // Main Generator
@@ -110,9 +110,14 @@ function generateOperation(route, namespace, options) {
110
110
  const inputSchema = procedure.inputSchema
111
111
  ? zodSchemaToJsonSchema(procedure.inputSchema)
112
112
  : undefined;
113
- const outputSchema = procedure.outputSchema
114
- ? zodSchemaToJsonSchema(procedure.outputSchema)
115
- : undefined;
113
+ let outputSchema;
114
+ if (procedure.outputSchema) {
115
+ outputSchema = zodSchemaToJsonSchema(procedure.outputSchema);
116
+ }
117
+ else if (procedure._resourceSchema) {
118
+ const level = procedure._resourceLevel ?? 'public';
119
+ outputSchema = resourceSchemaToJsonSchema(procedure._resourceSchema, level);
120
+ }
116
121
  // Build parameters
117
122
  const { pathParams, queryParams, pathParamNames } = buildParameters({
118
123
  path,
@@ -6,6 +6,7 @@
6
6
  * @module @veloxts/router/openapi/schema-converter
7
7
  */
8
8
  import { type ZodType } from 'zod';
9
+ import type { ResourceSchema } from '../resource/schema.js';
9
10
  import type { JSONSchema } from './types.js';
10
11
  /**
11
12
  * Options for Zod to JSON Schema conversion
@@ -100,3 +101,15 @@ export declare function createStringSchema(format?: string): JSONSchema;
100
101
  * Checks if a schema has any properties
101
102
  */
102
103
  export declare function schemaHasProperties(schema: JSONSchema | undefined): boolean;
104
+ /**
105
+ * Converts a ResourceSchema to JSON Schema for OpenAPI, filtered by visibility level
106
+ *
107
+ * Iterates the resource's field definitions and includes only fields
108
+ * visible at the given access level. Nested resource schemas are
109
+ * converted recursively.
110
+ *
111
+ * @param schema - The resource schema
112
+ * @param level - The access level to generate documentation for (defaults to 'public')
113
+ * @returns JSON Schema with only the fields visible at the given level
114
+ */
115
+ export declare function resourceSchemaToJsonSchema(schema: ResourceSchema, level?: string): JSONSchema;
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { createLogger } from '@veloxts/core';
9
9
  import { z } from 'zod';
10
+ import { isFieldVisibleToLevel } from '../resource/visibility.js';
10
11
  const log = createLogger('router');
11
12
  /**
12
13
  * Maps our target names to Zod 4's native `z.toJSONSchema()` target names.
@@ -257,3 +258,53 @@ export function schemaHasProperties(schema) {
257
258
  return false;
258
259
  return Object.keys(schema.properties).length > 0;
259
260
  }
261
+ // ============================================================================
262
+ // Resource Schema to JSON Schema
263
+ // ============================================================================
264
+ /**
265
+ * Converts a ResourceSchema to JSON Schema for OpenAPI, filtered by visibility level
266
+ *
267
+ * Iterates the resource's field definitions and includes only fields
268
+ * visible at the given access level. Nested resource schemas are
269
+ * converted recursively.
270
+ *
271
+ * @param schema - The resource schema
272
+ * @param level - The access level to generate documentation for (defaults to 'public')
273
+ * @returns JSON Schema with only the fields visible at the given level
274
+ */
275
+ export function resourceSchemaToJsonSchema(schema, level = 'public') {
276
+ const properties = {};
277
+ const required = [];
278
+ for (const field of schema.fields) {
279
+ if (!isFieldVisibleToLevel(field, level)) {
280
+ continue;
281
+ }
282
+ if (field.nestedSchema) {
283
+ // Nested relation — recurse
284
+ const nestedJsonSchema = resourceSchemaToJsonSchema(field.nestedSchema, level);
285
+ if (field.cardinality === 'many') {
286
+ properties[field.name] = { type: 'array', items: nestedJsonSchema };
287
+ }
288
+ else {
289
+ properties[field.name] = { ...nestedJsonSchema, nullable: true };
290
+ }
291
+ }
292
+ else if (field.schema) {
293
+ // Scalar field with Zod schema
294
+ const fieldJsonSchema = zodSchemaToJsonSchema(field.schema);
295
+ if (fieldJsonSchema) {
296
+ properties[field.name] = fieldJsonSchema;
297
+ }
298
+ }
299
+ else {
300
+ // Field without schema — generic
301
+ properties[field.name] = {};
302
+ }
303
+ required.push(field.name);
304
+ }
305
+ return {
306
+ type: 'object',
307
+ properties,
308
+ ...(required.length > 0 ? { required } : {}),
309
+ };
310
+ }
@@ -85,15 +85,26 @@ function createBuilder(state) {
85
85
  });
86
86
  },
87
87
  /**
88
- * Sets the output validation schema
88
+ * Sets the output validation schema (Zod-only)
89
89
  */
90
90
  output(schema) {
91
- // Return new builder with updated output schema
92
91
  return createBuilder({
93
92
  ...state,
94
93
  outputSchema: schema,
95
94
  });
96
95
  },
96
+ /**
97
+ * Sets field-level visibility via a resource schema
98
+ */
99
+ expose(schema) {
100
+ const level = isTaggedResourceSchema(schema) ? schema._level : undefined;
101
+ return createBuilder({
102
+ ...state,
103
+ resourceSchema: schema,
104
+ resourceLevel: level,
105
+ outputSchema: undefined,
106
+ });
107
+ },
97
108
  /**
98
109
  * Adds middleware to the chain
99
110
  */
@@ -151,6 +162,16 @@ function createBuilder(state) {
151
162
  restOverride: config,
152
163
  });
153
164
  },
165
+ /**
166
+ * Configures the procedure as a webhook endpoint
167
+ */
168
+ webhook(path) {
169
+ return createBuilder({
170
+ ...state,
171
+ restOverride: { method: 'POST', path },
172
+ isWebhook: true,
173
+ });
174
+ },
154
175
  /**
155
176
  * Marks the procedure as deprecated
156
177
  */
@@ -200,10 +221,7 @@ function createBuilder(state) {
200
221
  return compileProcedure('mutation', handler, state);
201
222
  },
202
223
  /**
203
- * Sets the output type based on a resource schema
204
- *
205
- * Accepts either a plain `ResourceSchema` or a tagged schema
206
- * (e.g., `UserSchema.authenticated`) for declarative auto-projection.
224
+ * @deprecated Use `.expose()` instead. `.resource()` will be removed in v1.0.
207
225
  */
208
226
  resource(schema) {
209
227
  const level = isTaggedResourceSchema(schema) ? schema._level : undefined;
@@ -211,6 +229,7 @@ function createBuilder(state) {
211
229
  ...state,
212
230
  resourceSchema: schema,
213
231
  resourceLevel: level,
232
+ outputSchema: undefined,
214
233
  });
215
234
  },
216
235
  };
@@ -240,6 +259,7 @@ function compileProcedure(type, handler, state) {
240
259
  restOverride: state.restOverride,
241
260
  deprecated: state.deprecated,
242
261
  deprecationMessage: state.deprecationMessage,
262
+ isWebhook: state.isWebhook,
243
263
  parentResource: state.parentResource,
244
264
  parentResources: state.parentResources,
245
265
  // Store pre-compiled executor for performance
@@ -413,14 +433,14 @@ export async function executeProcedure(procedure, rawInput, ctx) {
413
433
  const message = guard.message ?? `Guard "${guard.name}" check failed`;
414
434
  throw new GuardError(guard.name, message, statusCode);
415
435
  }
416
- // Track highest access level from narrowing guards
436
+ // Track access level from narrowing guards.
437
+ // IMPORTANT: last guard's accessLevel wins. With custom levels that
438
+ // have no inherent hierarchy, ordering of guards matters.
439
+ // Guards without accessLevel (e.g. plain `authenticated`) do NOT
440
+ // update the level — it stays at 'public' for .expose() projection.
417
441
  const guardWithLevel = guard;
418
442
  if (guardWithLevel.accessLevel) {
419
- // Admin > authenticated > public
420
- if (guardWithLevel.accessLevel === 'admin' ||
421
- (guardWithLevel.accessLevel === 'authenticated' && accessLevel === 'public')) {
422
- accessLevel = guardWithLevel.accessLevel;
423
- }
443
+ accessLevel = guardWithLevel.accessLevel;
424
444
  }
425
445
  }
426
446
  }
@@ -451,15 +471,7 @@ export async function executeProcedure(procedure, rawInput, ctx) {
451
471
  // Prefer explicit level from tagged schema over guard-derived level
452
472
  const finalLevel = procedure._resourceLevel ?? accessLevel;
453
473
  const projectOne = (item) => {
454
- const r = new Resource(item, schema);
455
- switch (finalLevel) {
456
- case 'admin':
457
- return r.forAdmin();
458
- case 'authenticated':
459
- return r.forAuthenticated();
460
- default:
461
- return r.forAnonymous();
462
- }
474
+ return new Resource(item, schema).forLevel(finalLevel);
463
475
  };
464
476
  if (Array.isArray(result)) {
465
477
  result = result.map((item) => projectOne(item));
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import type { BaseContext } from '@veloxts/core';
13
13
  import type { ZodType } from 'zod';
14
- import type { OutputForTag, ResourceSchema, TaggedResourceSchema } from '../resource/index.js';
14
+ import type { FilterFieldsByLevel, OutputForTag, ResourceSchema, TaggedResourceSchema } from '../resource/index.js';
15
15
  import type { ContextTag, ExtractTag, LevelToTag, TaggedContext } from '../resource/tags.js';
16
16
  import type { CompiledProcedure, GuardLike, MiddlewareFunction, ParentResourceConfig, ProcedureHandler, RestRouteOverride } from '../types.js';
17
17
  /**
@@ -94,10 +94,13 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
94
94
  */
95
95
  input<TSchema extends ValidSchema>(schema: TSchema): ProcedureBuilder<InferSchemaOutput<TSchema>, TOutput, TContext>;
96
96
  /**
97
- * Defines the output validation schema for the procedure
97
+ * Defines the output validation schema (Zod)
98
98
  *
99
- * The output type is automatically inferred from the Zod schema.
100
- * The handler return type will be validated against this schema.
99
+ * Sets a Zod schema that validates the handler's return value.
100
+ * All callers receive the same fields.
101
+ *
102
+ * For field-level visibility (different fields per access level),
103
+ * use `.expose()` with a resource schema instead.
101
104
  *
102
105
  * @template TSchema - The Zod schema type
103
106
  * @param schema - Zod schema for output validation
@@ -106,14 +109,39 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
106
109
  * @example
107
110
  * ```typescript
108
111
  * procedure()
109
- * .output(z.object({
110
- * id: z.string(),
111
- * name: z.string(),
112
- * }))
113
- * // handler must return { id: string; name: string }
112
+ * .output(z.object({ id: z.string(), name: z.string() }))
113
+ * .query(handler) // handler must return { id: string; name: string }
114
114
  * ```
115
115
  */
116
116
  output<TSchema extends ValidSchema>(schema: TSchema): ProcedureBuilder<TInput, InferSchemaOutput<TSchema>, TContext>;
117
+ /**
118
+ * Sets field-level visibility via a resource schema
119
+ *
120
+ * Accepts two resource schema variants:
121
+ * 1. **Tagged resource schema** (e.g., `UserSchema.authenticated`) — explicit field projection by access level
122
+ * 2. **Plain resource schema** (e.g., `UserSchema`) — context-derived field projection from `guardNarrow`
123
+ *
124
+ * @template TSchema - The resource schema type
125
+ * @param schema - Resource schema for field projection
126
+ * @returns New builder with updated output type
127
+ *
128
+ * @example Tagged resource schema — explicit projection level
129
+ * ```typescript
130
+ * procedure()
131
+ * .guard(authenticated)
132
+ * .expose(UserSchema.authenticated) // returns { id, name, email }
133
+ * .query(handler)
134
+ * ```
135
+ *
136
+ * @example Plain resource schema — derives level from guardNarrow
137
+ * ```typescript
138
+ * procedure()
139
+ * .guardNarrow(authenticatedNarrow)
140
+ * .expose(UserSchema) // auto-projects based on guard's accessLevel
141
+ * .query(handler)
142
+ * ```
143
+ */
144
+ expose<TSchema extends ResourceSchema>(schema: TSchema): ProcedureBuilder<TInput, TSchema extends TaggedResourceSchema<infer TFields, infer TLevel> ? TLevel extends 'admin' | 'authenticated' | 'public' ? OutputForTag<ResourceSchema<TFields>, LevelToTag<TLevel>> : FilterFieldsByLevel<TFields, TLevel> : TContext extends TaggedContext<infer TTag> ? TTag extends ContextTag ? OutputForTag<TSchema, TTag> : OutputForTag<TSchema, ExtractTag<TContext>> : OutputForTag<TSchema, ExtractTag<TContext>>, TContext>;
117
145
  /**
118
146
  * Adds middleware to the procedure chain
119
147
  *
@@ -255,6 +283,26 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
255
283
  * ```
256
284
  */
257
285
  rest(config: RestRouteOverride): ProcedureBuilder<TInput, TOutput, TContext>;
286
+ /**
287
+ * Configures the procedure as a webhook endpoint
288
+ *
289
+ * Sugar for `.rest({ method: 'POST', path })` with a webhook flag.
290
+ * Webhook procedures expect raw body access for signature verification.
291
+ *
292
+ * @param path - The webhook endpoint path (e.g., '/webhooks/stripe')
293
+ * @returns New builder with webhook configuration
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * procedure()
298
+ * .webhook('/webhooks/stripe')
299
+ * .mutation(async ({ input, ctx }) => {
300
+ * const isValid = verifySignature(ctx.request.rawBody, ctx.request.headers['stripe-signature']);
301
+ * // ...
302
+ * })
303
+ * ```
304
+ */
305
+ webhook(path: string): ProcedureBuilder<TInput, TOutput, TContext>;
258
306
  /**
259
307
  * Marks the procedure as deprecated
260
308
  *
@@ -377,28 +425,17 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
377
425
  */
378
426
  mutation(handler: ProcedureHandler<TInput, TOutput, TContext>): CompiledProcedure<TInput, TOutput, TContext, 'mutation'>;
379
427
  /**
380
- * Sets the output type based on a resource schema
428
+ * @deprecated Use `.expose()` instead. `.resource()` will be removed in v1.0.
381
429
  *
382
- * Accepts either a tagged schema (e.g., `UserSchema.authenticated`) for
383
- * explicit auto-projection, or a plain schema for backward compatibility.
430
+ * Sets field-level visibility via a resource schema.
384
431
  *
385
- * When a tagged schema is used, the output type is computed from the
386
- * tag's access level. When a plain schema is used, the output type is
387
- * derived from the context's phantom tag (set by `guardNarrow`).
388
- *
389
- * @example
432
+ * @example Migration
390
433
  * ```typescript
391
- * // Tagged schema — explicit projection level (recommended)
392
- * procedure()
393
- * .guard(authenticated)
394
- * .resource(UserSchema.authenticated)
395
- * .query(async ({ ctx }) => ctx.db.user.findUnique(...));
434
+ * // Before
435
+ * procedure().resource(UserSchema.authenticated).query(handler)
396
436
  *
397
- * // Plain schema — derives level from guardNarrow or defaults to public
398
- * procedure()
399
- * .guardNarrow(authenticatedNarrow)
400
- * .resource(UserSchema)
401
- * .query(async ({ ctx }) => ctx.db.user.findUnique(...));
437
+ * // After
438
+ * procedure().expose(UserSchema.authenticated).query(handler)
402
439
  * ```
403
440
  */
404
441
  resource<TSchema extends ResourceSchema>(schema: TSchema): ProcedureBuilder<TInput, TSchema extends TaggedResourceSchema<infer TFields, infer TLevel> ? OutputForTag<ResourceSchema<TFields>, LevelToTag<TLevel>> : TContext extends TaggedContext<infer TTag> ? TTag extends ContextTag ? OutputForTag<TSchema, TTag> : OutputForTag<TSchema, ExtractTag<TContext>> : OutputForTag<TSchema, ExtractTag<TContext>>, TContext>;
@@ -417,7 +454,7 @@ export interface BuilderRuntimeState {
417
454
  /** Resource schema for context-dependent output */
418
455
  resourceSchema?: ResourceSchema;
419
456
  /** Explicit resource level from tagged schema (e.g., UserSchema.authenticated) */
420
- resourceLevel?: 'public' | 'authenticated' | 'admin';
457
+ resourceLevel?: string;
421
458
  /** Middleware chain */
422
459
  middlewares: MiddlewareFunction<unknown, BaseContext, BaseContext, unknown>[];
423
460
  /** Guards to execute before handler */
@@ -432,6 +469,8 @@ export interface BuilderRuntimeState {
432
469
  deprecated?: boolean;
433
470
  /** Deprecation message */
434
471
  deprecationMessage?: string;
472
+ /** Whether this procedure is a webhook endpoint (metadata marker) */
473
+ isWebhook?: boolean;
435
474
  }
436
475
  /**
437
476
  * Type for the procedures object passed to defineProcedures
@@ -34,7 +34,7 @@
34
34
  * .input(z.object({ id: z.string() }))
35
35
  * .query(async ({ input, ctx }) => {
36
36
  * const user = await ctx.db.user.findUnique({ where: { id: input.id } });
37
- * return resource(user, UserSchema).forAnonymous();
37
+ * return resource(user, UserSchema).forPublic();
38
38
  * }),
39
39
  *
40
40
  * // Authenticated endpoint → returns { id, name, email }
@@ -60,9 +60,11 @@
60
60
  * @module resource
61
61
  */
62
62
  export { Resource, ResourceCollection, resource, resourceCollection } from './instance.js';
63
- export type { AdminOutput, AnonymousOutput, AuthenticatedOutput, BuilderField, OutputForLevel, OutputForTag, RelationField, ResourceField, ResourceSchema, ResourceSchemaWithViews, RuntimeField, TaggedResourceSchema, } from './schema.js';
63
+ export type { AccessLevelConfig } from './levels.js';
64
+ export { defineAccessLevels } from './levels.js';
65
+ export type { AdminOutput, AnonymousOutput, AuthenticatedOutput, BuilderField, CustomResourceSchemaWithViews, CustomSchemaBuilder, FilterFieldsByLevel, OutputForLevel, OutputForTag, PublicOutput, RelationField, ResourceField, ResourceSchema, ResourceSchemaWithViews, RuntimeField, TaggedResourceSchema, } from './schema.js';
64
66
  export { isResourceSchema, isTaggedResourceSchema, ResourceSchemaBuilder, resourceSchema, } from './schema.js';
65
- export type { AccessLevel, ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, ExtractTag, HasTag, LevelToTag, TaggedContext, WithTag, } from './tags.js';
66
- export type { AdminTaggedContext, AnonymousTaggedContext, AnyResourceOutput, AuthenticatedTaggedContext, IfAdmin, IfAuthenticated, InferResourceData, InferResourceOutput, } from './types.js';
67
+ export type { AccessLevel, ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, ExtractTag, HasTag, LevelToTag, PUBLIC, TaggedContext, TagToLevel, WithTag, } from './tags.js';
68
+ export type { AdminTaggedContext, AnonymousTaggedContext, AnyResourceOutput, AuthenticatedTaggedContext, IfAdmin, IfAuthenticated, InferResourceData, InferResourceOutput, PublicTaggedContext, } from './types.js';
67
69
  export type { IsVisibleToTag, VisibilityLevel } from './visibility.js';
68
- export { getAccessibleLevels, getVisibilityForTag, isVisibleAtLevel } from './visibility.js';
70
+ export { getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isVisibleAtLevel, } from './visibility.js';
@@ -34,7 +34,7 @@
34
34
  * .input(z.object({ id: z.string() }))
35
35
  * .query(async ({ input, ctx }) => {
36
36
  * const user = await ctx.db.user.findUnique({ where: { id: input.id } });
37
- * return resource(user, UserSchema).forAnonymous();
37
+ * return resource(user, UserSchema).forPublic();
38
38
  * }),
39
39
  *
40
40
  * // Authenticated endpoint → returns { id, name, email }
@@ -64,7 +64,8 @@
64
64
  // ============================================================================
65
65
  // Resource instances
66
66
  export { Resource, ResourceCollection, resource, resourceCollection } from './instance.js';
67
+ export { defineAccessLevels } from './levels.js';
67
68
  // Schema builder
68
69
  export { isResourceSchema, isTaggedResourceSchema, ResourceSchemaBuilder, resourceSchema, } from './schema.js';
69
70
  // Visibility
70
- export { getAccessibleLevels, getVisibilityForTag, isVisibleAtLevel } from './visibility.js';
71
+ export { getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isVisibleAtLevel, } from './visibility.js';