yaxa-svelte 2.1.0 → 2.2.0

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 CHANGED
@@ -241,13 +241,46 @@ export const POST = createPolarWebhookHandler();
241
241
 
242
242
  ---
243
243
 
244
+ ## ⚡ Type-Safe Endpoint Pipeline (`yaxaEndpoint` & Declarative Guards)
245
+
246
+ Eliminate endpoint boilerplate in `+server.ts` with declarative auth guards, sliding-window rate limiting, multi-engine validation (Zod, Valibot, Standard Schema), and RFC 7807 Problem Details:
247
+
248
+ ```ts
249
+ // src/routes/api/projects/+server.ts
250
+ import { yaxaEndpoint, requireAuth, requireRole, rateLimit } from 'yaxa-svelte/server';
251
+ import { z } from 'zod';
252
+
253
+ const CreateProjectSchema = z.object({
254
+ name: z.string().min(3),
255
+ description: z.string().optional()
256
+ });
257
+
258
+ export const POST = yaxaEndpoint({
259
+ status: 201,
260
+ guards: [
261
+ requireAuth(),
262
+ requireRole(['admin', 'owner']),
263
+ rateLimit({ points: 20, windowSeconds: 60 })
264
+ ],
265
+ schema: {
266
+ body: CreateProjectSchema
267
+ },
268
+ handle: async ({ body, user, orgId }) => {
269
+ // Guaranteed type-safe body, authenticated user, and active orgId
270
+ return { id: 'proj_123', name: body.name, orgId };
271
+ }
272
+ });
273
+ ```
274
+
275
+ ---
276
+
244
277
  ## 📦 Subpath Exports Architecture
245
278
 
246
279
  | Subpath | Description |
247
280
  | :---------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- |
248
281
  | `yaxa-svelte` | Core UI Primitives, SEO Components, `<Gate />`, Theme System & Composables (`useGate`, `useUpload`, `useToast`, `useClipboard`, etc.) |
249
282
  | `yaxa-svelte/admin` | Drizzle ORM Schema Introspection, `<AdminDashboard />`, `createYaxaAdminHook`, `createDrizzleAdmin`, `<DevSandbox />`, `<RecordDrawer />` |
250
- | `yaxa-svelte/server` | Universal Server Hooks, SEO endpoints, Drizzle DB drivers, Cloud Storage handlers, and Email dispatch |
283
+ | `yaxa-svelte/server` | `yaxaEndpoint` Pipeline, Declarative Guards, RFC 7807, Universal Server Hooks, SEO endpoints, Drizzle DB, Storage & Email |
251
284
  | `yaxa-svelte/auth` | Better-Auth client & server integration (`useAuth`, `<AuthCard />`, `<PasskeyUI />`, `<TwoFactorModal />`, `createYaxaAuth`) |
252
285
  | `yaxa-svelte/db` | Multi-dialect Drizzle ORM clients & schemas for Neon (PostgreSQL) and Turso (SQLite/LibSQL) |
253
286
  | `yaxa-svelte/polar` | Polar.sh billing, customer portal sessions, and webhook synchronization |
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,364 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { z } from 'zod';
3
+ import { createMockRequestEvent } from '../../testing/index.js';
4
+ import { yaxaEndpoint, createProblemDetails, problemResponse, isProblemDetails, requireAuth, requireRole, rateLimit, composeGuards, validateData } from './index.js';
5
+ describe('RFC 7807 Problem Details', () => {
6
+ it('creates structured problem details object with standard defaults', () => {
7
+ const problem = createProblemDetails({
8
+ status: 404,
9
+ detail: 'Project proj_999 does not exist.'
10
+ });
11
+ expect(problem.status).toBe(404);
12
+ expect(problem.title).toBe('Not Found');
13
+ expect(problem.type).toBe('https://httpstatuses.com/404');
14
+ expect(problem.detail).toBe('Project proj_999 does not exist.');
15
+ expect(isProblemDetails(problem)).toBe(true);
16
+ });
17
+ it('returns Response with application/problem+json content-type and custom fields', async () => {
18
+ const response = problemResponse({
19
+ status: 422,
20
+ title: 'Unprocessable Entity',
21
+ detail: 'Validation error',
22
+ invalidParams: [{ name: 'email', reason: 'Must be a valid email' }],
23
+ traceId: 'trace_abc123'
24
+ });
25
+ expect(response.status).toBe(422);
26
+ expect(response.headers.get('content-type')).toContain('application/problem+json');
27
+ const json = await response.json();
28
+ expect(json.status).toBe(422);
29
+ expect(json.invalidParams).toHaveLength(1);
30
+ expect(json.invalidParams[0].name).toBe('email');
31
+ expect(json.traceId).toBe('trace_abc123');
32
+ });
33
+ });
34
+ describe('Declarative Guards', () => {
35
+ describe('requireAuth', () => {
36
+ it('returns 401 Problem Details when user is unauthenticated', async () => {
37
+ const guard = requireAuth();
38
+ const event = createMockRequestEvent({ locals: { user: null } });
39
+ const result = await guard(event);
40
+ expect(result).toBeInstanceOf(Response);
41
+ if (result instanceof Response) {
42
+ expect(result.status).toBe(401);
43
+ const json = await result.json();
44
+ expect(json.title).toBe('Unauthorized');
45
+ }
46
+ });
47
+ it('throws 303 redirect when redirect option is set', async () => {
48
+ const guard = requireAuth({ redirect: '/login' });
49
+ const event = createMockRequestEvent({
50
+ url: 'http://localhost:5173/dashboard?tab=billing',
51
+ locals: { user: null }
52
+ });
53
+ try {
54
+ await guard(event);
55
+ expect.fail('Expected redirect to be thrown');
56
+ }
57
+ catch (err) {
58
+ expect(err.status).toBe(303);
59
+ expect(err.location).toContain('/login?returnTo=');
60
+ expect(err.location).toContain(encodeURIComponent('/dashboard?tab=billing'));
61
+ }
62
+ });
63
+ it('passes cleanly when user exists in locals', async () => {
64
+ const guard = requireAuth();
65
+ const event = createMockRequestEvent({
66
+ locals: { user: { id: 'usr_1', role: 'admin' } }
67
+ });
68
+ const result = await guard(event);
69
+ expect(result).toBeUndefined();
70
+ });
71
+ });
72
+ describe('requireRole', () => {
73
+ it('returns 401 if user is not logged in', async () => {
74
+ const guard = requireRole(['admin']);
75
+ const event = createMockRequestEvent({ locals: { user: null } });
76
+ const result = (await guard(event));
77
+ expect(result.status).toBe(401);
78
+ });
79
+ it('returns 403 Forbidden when user role is not allowed', async () => {
80
+ const guard = requireRole(['admin', 'owner']);
81
+ const event = createMockRequestEvent({
82
+ locals: { user: { id: 'usr_1', role: 'member' } }
83
+ });
84
+ const result = (await guard(event));
85
+ expect(result.status).toBe(403);
86
+ const json = await result.json();
87
+ expect(json.title).toBe('Forbidden');
88
+ expect(json.requiredRoles).toEqual(['admin', 'owner']);
89
+ });
90
+ it('passes when user has one of the allowed roles', async () => {
91
+ const guard = requireRole(['admin', 'owner']);
92
+ const event = createMockRequestEvent({
93
+ locals: { user: { id: 'usr_1', role: 'owner' } }
94
+ });
95
+ const result = await guard(event);
96
+ expect(result).toBeUndefined();
97
+ });
98
+ it('passes when member role matches org role in multi-tenant setup', async () => {
99
+ const guard = requireRole(['billing_admin']);
100
+ const event = createMockRequestEvent({
101
+ locals: {
102
+ user: { id: 'usr_1', role: 'member' },
103
+ member: { role: 'billing_admin' }
104
+ }
105
+ });
106
+ const result = await guard(event);
107
+ expect(result).toBeUndefined();
108
+ });
109
+ });
110
+ describe('rateLimit', () => {
111
+ it('allows requests up to points threshold then returns 429', async () => {
112
+ const guard = rateLimit({
113
+ points: 3,
114
+ windowSeconds: 10,
115
+ keyGenerator: () => 'test_client_key'
116
+ });
117
+ const event = createMockRequestEvent();
118
+ // First 3 calls succeed
119
+ expect(await guard(event)).toBeUndefined();
120
+ expect(await guard(event)).toBeUndefined();
121
+ expect(await guard(event)).toBeUndefined();
122
+ // 4th call is rate-limited
123
+ const result = (await guard(event));
124
+ expect(result.status).toBe(429);
125
+ expect(result.headers.get('Retry-After')).toBeDefined();
126
+ expect(result.headers.get('X-RateLimit-Remaining')).toBe('0');
127
+ const json = await result.json();
128
+ expect(json.title).toBe('Too Many Requests');
129
+ });
130
+ });
131
+ describe('composeGuards', () => {
132
+ it('executes guards sequentially and halts on first failure', async () => {
133
+ const guard = composeGuards(requireAuth(), requireRole(['admin']));
134
+ // Unauthenticated -> fails on requireAuth with 401
135
+ const eventUnauth = createMockRequestEvent({ locals: { user: null } });
136
+ const res1 = (await guard(eventUnauth));
137
+ expect(res1.status).toBe(401);
138
+ // Authenticated but wrong role -> fails on requireRole with 403
139
+ const eventForbidden = createMockRequestEvent({
140
+ locals: { user: { id: 'usr_1', role: 'viewer' } }
141
+ });
142
+ const res2 = (await guard(eventForbidden));
143
+ expect(res2.status).toBe(403);
144
+ // Both pass
145
+ const eventOk = createMockRequestEvent({
146
+ locals: { user: { id: 'usr_1', role: 'admin' } }
147
+ });
148
+ const res3 = await guard(eventOk);
149
+ expect(res3).toBeUndefined();
150
+ });
151
+ });
152
+ });
153
+ describe('Multi-Engine Validation Engine', () => {
154
+ it('validates with Zod schema and extracts parsed data', async () => {
155
+ const schema = z.object({
156
+ email: z.string().email(),
157
+ age: z.number().min(18)
158
+ });
159
+ const valid = await validateData(schema, { email: 'test@example.com', age: 25 });
160
+ expect(valid.success).toBe(true);
161
+ if (valid.success) {
162
+ expect(valid.data.email).toBe('test@example.com');
163
+ expect(valid.data.age).toBe(25);
164
+ }
165
+ const invalid = await validateData(schema, { email: 'bad-email', age: 16 }, 'body');
166
+ expect(invalid.success).toBe(false);
167
+ if (!invalid.success) {
168
+ expect(invalid.invalidParams.length).toBeGreaterThanOrEqual(2);
169
+ expect(invalid.invalidParams[0].name).toContain('body.');
170
+ }
171
+ });
172
+ it('validates with Standard Schema v1 (~standard)', async () => {
173
+ // Mock Standard Schema specification
174
+ const mockStandardSchema = {
175
+ '~standard': {
176
+ version: 1,
177
+ vendor: 'custom',
178
+ validate: (val) => {
179
+ if (typeof val === 'string' && val.length > 3) {
180
+ return { value: val.toUpperCase() };
181
+ }
182
+ return {
183
+ issues: [{ path: ['name'], message: 'Name must exceed 3 characters' }]
184
+ };
185
+ }
186
+ }
187
+ };
188
+ const ok = await validateData(mockStandardSchema, 'alice');
189
+ expect(ok.success).toBe(true);
190
+ if (ok.success)
191
+ expect(ok.data).toBe('ALICE');
192
+ const bad = await validateData(mockStandardSchema, 'al', 'body');
193
+ expect(bad.success).toBe(false);
194
+ if (!bad.success) {
195
+ expect(bad.invalidParams[0].name).toBe('body.name');
196
+ expect(bad.invalidParams[0].reason).toContain('exceed 3 characters');
197
+ }
198
+ });
199
+ it('validates with custom function validator', async () => {
200
+ const validator = (val) => {
201
+ if (!val || val.length === 0) {
202
+ return { success: false, error: 'Cannot be empty' };
203
+ }
204
+ return { success: true, data: val };
205
+ };
206
+ const ok = await validateData(validator, 'hello');
207
+ expect(ok.success).toBe(true);
208
+ const bad = await validateData(validator, '');
209
+ expect(bad.success).toBe(false);
210
+ });
211
+ });
212
+ describe('yaxaEndpoint Higher-Order Pipeline', () => {
213
+ const ItemSchema = z.object({
214
+ title: z.string().min(2),
215
+ price: z.number().positive()
216
+ });
217
+ const QuerySchema = z.object({
218
+ search: z.string().optional()
219
+ });
220
+ it('enforces guards before executing handler', async () => {
221
+ const endpoint = yaxaEndpoint({
222
+ guards: [requireAuth()],
223
+ handle: () => ({ success: true })
224
+ });
225
+ const eventUnauth = createMockRequestEvent({ locals: { user: null } });
226
+ const response = await endpoint(eventUnauth);
227
+ expect(response.status).toBe(401);
228
+ const json = await response.json();
229
+ expect(json.title).toBe('Unauthorized');
230
+ });
231
+ it('returns 422 Problem Details on invalid request body', async () => {
232
+ const endpoint = yaxaEndpoint({
233
+ schema: { body: ItemSchema },
234
+ handle: ({ body }) => body
235
+ });
236
+ const event = createMockRequestEvent({
237
+ method: 'POST',
238
+ body: { title: 'a', price: -5 }
239
+ });
240
+ const response = await endpoint(event);
241
+ expect(response.status).toBe(422);
242
+ expect(response.headers.get('content-type')).toContain('application/problem+json');
243
+ const json = await response.json();
244
+ expect(json.status).toBe(422);
245
+ expect(json.title).toBe('Unprocessable Entity');
246
+ expect(json.invalidParams.length).toBeGreaterThanOrEqual(2);
247
+ });
248
+ it('handles malformed JSON body gracefully with 400 Bad Request', async () => {
249
+ const endpoint = yaxaEndpoint({
250
+ handle: ({ body }) => body
251
+ });
252
+ const event = createMockRequestEvent({
253
+ method: 'POST',
254
+ body: '{ broken json: ',
255
+ headers: { 'content-type': 'application/json' }
256
+ });
257
+ const response = await endpoint(event);
258
+ expect(response.status).toBe(400);
259
+ const json = await response.json();
260
+ expect(json.title).toBe('Bad Request');
261
+ });
262
+ it('executes handler and auto-serializes JSON with typed body, query, and params', async () => {
263
+ const endpoint = yaxaEndpoint({
264
+ status: 201,
265
+ schema: {
266
+ body: ItemSchema,
267
+ query: QuerySchema
268
+ },
269
+ handle: ({ body, query, params, user }) => {
270
+ return {
271
+ created: true,
272
+ item: body,
273
+ q: query.search,
274
+ id: params.id,
275
+ userId: user?.id
276
+ };
277
+ }
278
+ });
279
+ const event = createMockRequestEvent({
280
+ method: 'POST',
281
+ url: 'http://localhost:5173/api/items/item_42?search=svelte',
282
+ params: { id: 'item_42' },
283
+ body: { title: 'Keyboard', price: 99 },
284
+ locals: { user: { id: 'usr_77', role: 'admin' } }
285
+ });
286
+ const response = await endpoint(event);
287
+ expect(response.status).toBe(201);
288
+ expect(response.headers.get('content-type')).toContain('application/json');
289
+ const json = await response.json();
290
+ expect(json.created).toBe(true);
291
+ expect(json.item.title).toBe('Keyboard');
292
+ expect(json.item.price).toBe(99);
293
+ expect(json.q).toBe('svelte');
294
+ expect(json.id).toBe('item_42');
295
+ expect(json.userId).toBe('usr_77');
296
+ });
297
+ it('supports string returns as text/plain and null/void as 204 No Content', async () => {
298
+ const stringEndpoint = yaxaEndpoint({
299
+ handle: () => 'PONG'
300
+ });
301
+ const res1 = await stringEndpoint(createMockRequestEvent());
302
+ expect(res1.status).toBe(200);
303
+ expect(await res1.text()).toBe('PONG');
304
+ expect(res1.headers.get('content-type')).toContain('text/plain');
305
+ const voidEndpoint = yaxaEndpoint({
306
+ handle: () => undefined
307
+ });
308
+ const res2 = await voidEndpoint(createMockRequestEvent());
309
+ expect(res2.status).toBe(204);
310
+ });
311
+ it('passes through custom Response instances without modification', async () => {
312
+ const customEndpoint = yaxaEndpoint({
313
+ handle: () => new Response('Custom CSV Content', {
314
+ status: 200,
315
+ headers: { 'content-type': 'text/csv' }
316
+ })
317
+ });
318
+ const response = await customEndpoint(createMockRequestEvent());
319
+ expect(response.status).toBe(200);
320
+ expect(response.headers.get('content-type')).toBe('text/csv');
321
+ expect(await response.text()).toBe('Custom CSV Content');
322
+ });
323
+ it('supports interceptors for request timing and header enrichment', async () => {
324
+ const endpoint = yaxaEndpoint({
325
+ interceptors: [
326
+ async (_ctx, next) => {
327
+ const res = await next();
328
+ res.headers.set('x-yaxa-intercepted', 'true');
329
+ return res;
330
+ }
331
+ ],
332
+ handle: () => ({ ok: true })
333
+ });
334
+ const response = await endpoint(createMockRequestEvent());
335
+ expect(response.headers.get('x-yaxa-intercepted')).toBe('true');
336
+ });
337
+ it('sanitizes unexpected internal 500 errors without leaking stack traces or sensitive strings', async () => {
338
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
339
+ const failingEndpoint = yaxaEndpoint({
340
+ handle: () => {
341
+ throw new Error('Database password leak: secret123456');
342
+ }
343
+ });
344
+ const response = await failingEndpoint(createMockRequestEvent());
345
+ expect(response.status).toBe(500);
346
+ expect(response.headers.get('content-type')).toContain('application/problem+json');
347
+ const json = await response.json();
348
+ expect(json.title).toBe('Internal Server Error');
349
+ expect(json.detail).not.toContain('secret123456');
350
+ errorSpy.mockRestore();
351
+ });
352
+ it('attaches _yaxaConfig metadata for runtime reflection and OpenAPI discovery', () => {
353
+ const config = {
354
+ summary: 'Create Project',
355
+ description: 'Creates a new user project',
356
+ tags: ['Projects'],
357
+ handle: () => ({})
358
+ };
359
+ const endpoint = yaxaEndpoint(config);
360
+ expect(endpoint._yaxaConfig).toBeDefined();
361
+ expect(endpoint._yaxaConfig.summary).toBe('Create Project');
362
+ expect(endpoint._yaxaConfig.tags).toEqual(['Projects']);
363
+ });
364
+ });
@@ -0,0 +1,52 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ import type { Guard } from './types';
3
+ export interface RequireAuthOptions {
4
+ /**
5
+ * If provided, unauthorized users are redirected with 303 instead of receiving 401 JSON.
6
+ * (Useful for dashboard/form routes).
7
+ */
8
+ redirect?: string;
9
+ /**
10
+ * Custom error message for the RFC 7807 detail.
11
+ */
12
+ detail?: string;
13
+ }
14
+ /**
15
+ * Declarative Guard: Enforces that the request has an active authenticated user in `event.locals.user`.
16
+ */
17
+ export declare function requireAuth(options?: RequireAuthOptions): Guard;
18
+ export interface RequireRoleOptions {
19
+ detail?: string;
20
+ }
21
+ /**
22
+ * Declarative Guard: Enforces that the active user/member possesses one of the allowed roles.
23
+ * Checks `event.locals.user.role`, `event.locals.member.role`, and `event.locals.orgRole`.
24
+ */
25
+ export declare function requireRole(roles: string | string[], options?: RequireRoleOptions): Guard;
26
+ export interface RateLimitOptions {
27
+ /**
28
+ * Maximum allowed requests within the time window. Default: 60.
29
+ */
30
+ points?: number;
31
+ /**
32
+ * Window duration in seconds. Default: 60.
33
+ */
34
+ windowSeconds?: number;
35
+ /**
36
+ * Custom cache key generator. Defaults to IP address or authenticated user ID.
37
+ */
38
+ keyGenerator?: (event: RequestEvent) => string;
39
+ /**
40
+ * Custom error message.
41
+ */
42
+ detail?: string;
43
+ }
44
+ /**
45
+ * Declarative Guard: In-memory sliding-window rate limiter.
46
+ * Returns 429 Too Many Requests with a `Retry-After` header when exceeded.
47
+ */
48
+ export declare function rateLimit(options?: RateLimitOptions): Guard;
49
+ /**
50
+ * Combines multiple guards into a single composite guard.
51
+ */
52
+ export declare function composeGuards(...guards: Guard[]): Guard;
@@ -0,0 +1,137 @@
1
+ import { redirect } from '@sveltejs/kit';
2
+ import { problemResponse } from './problem';
3
+ /**
4
+ * Declarative Guard: Enforces that the request has an active authenticated user in `event.locals.user`.
5
+ */
6
+ export function requireAuth(options = {}) {
7
+ return (event) => {
8
+ const user = event.locals?.user;
9
+ if (!user) {
10
+ if (options.redirect) {
11
+ const returnTo = encodeURIComponent(event.url.pathname + event.url.search);
12
+ const target = options.redirect.includes('?')
13
+ ? `${options.redirect}&returnTo=${returnTo}`
14
+ : `${options.redirect}?returnTo=${returnTo}`;
15
+ throw redirect(303, target);
16
+ }
17
+ return problemResponse({
18
+ status: 401,
19
+ title: 'Unauthorized',
20
+ detail: options.detail || 'Authentication required to access this endpoint.',
21
+ type: 'https://yaxa.dev/errors/unauthorized'
22
+ });
23
+ }
24
+ };
25
+ }
26
+ /**
27
+ * Declarative Guard: Enforces that the active user/member possesses one of the allowed roles.
28
+ * Checks `event.locals.user.role`, `event.locals.member.role`, and `event.locals.orgRole`.
29
+ */
30
+ export function requireRole(roles, options = {}) {
31
+ const allowedRoles = Array.isArray(roles) ? roles : [roles];
32
+ return (event) => {
33
+ const locals = event.locals;
34
+ const user = locals?.user;
35
+ if (!user) {
36
+ return problemResponse({
37
+ status: 401,
38
+ title: 'Unauthorized',
39
+ detail: 'Authentication required before role verification.'
40
+ });
41
+ }
42
+ const userRole = user.role;
43
+ const memberRole = locals.member?.role || locals.orgRole;
44
+ const hasRole = (userRole && allowedRoles.includes(userRole)) ||
45
+ (memberRole && allowedRoles.includes(memberRole));
46
+ if (!hasRole) {
47
+ return problemResponse({
48
+ status: 403,
49
+ title: 'Forbidden',
50
+ detail: options.detail ||
51
+ `Insufficient permissions. Requires one of: [${allowedRoles.join(', ')}].`,
52
+ type: 'https://yaxa.dev/errors/forbidden',
53
+ requiredRoles: allowedRoles
54
+ });
55
+ }
56
+ };
57
+ }
58
+ const rateLimitStore = new Map();
59
+ // Periodic cleanup of stale rate-limit buckets (every 5 minutes)
60
+ if (typeof setInterval !== 'undefined') {
61
+ const timer = setInterval(() => {
62
+ const now = Date.now();
63
+ for (const [key, bucket] of rateLimitStore.entries()) {
64
+ if (bucket.resetAt <= now) {
65
+ rateLimitStore.delete(key);
66
+ }
67
+ }
68
+ }, 5 * 60 * 1000);
69
+ if (timer && typeof timer === 'object' && 'unref' in timer) {
70
+ timer.unref();
71
+ }
72
+ }
73
+ /**
74
+ * Declarative Guard: In-memory sliding-window rate limiter.
75
+ * Returns 429 Too Many Requests with a `Retry-After` header when exceeded.
76
+ */
77
+ export function rateLimit(options = {}) {
78
+ const points = options.points ?? 60;
79
+ const windowMs = (options.windowSeconds ?? 60) * 1000;
80
+ return (event) => {
81
+ let key;
82
+ if (options.keyGenerator) {
83
+ key = options.keyGenerator(event);
84
+ }
85
+ else {
86
+ const userId = event.locals?.user?.id;
87
+ let clientIp = '127.0.0.1';
88
+ try {
89
+ if (typeof event.getClientAddress === 'function') {
90
+ clientIp = event.getClientAddress();
91
+ }
92
+ }
93
+ catch {
94
+ // Fallback when running in environments without getClientAddress
95
+ }
96
+ const headerIp = event.request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
97
+ event.request.headers.get('x-real-ip');
98
+ const ip = headerIp || clientIp;
99
+ key = userId ? `user:${userId}` : `ip:${ip}`;
100
+ }
101
+ const now = Date.now();
102
+ let bucket = rateLimitStore.get(key);
103
+ if (!bucket || bucket.resetAt <= now) {
104
+ bucket = { tokens: points - 1, resetAt: now + windowMs };
105
+ rateLimitStore.set(key, bucket);
106
+ return;
107
+ }
108
+ if (bucket.tokens <= 0) {
109
+ const retryAfterSeconds = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000));
110
+ return problemResponse({
111
+ status: 429,
112
+ title: 'Too Many Requests',
113
+ detail: options.detail || `Rate limit exceeded. Try again in ${retryAfterSeconds} seconds.`,
114
+ type: 'https://yaxa.dev/errors/rate-limit-exceeded',
115
+ headers: {
116
+ 'Retry-After': String(retryAfterSeconds),
117
+ 'X-RateLimit-Limit': String(points),
118
+ 'X-RateLimit-Remaining': '0',
119
+ 'X-RateLimit-Reset': String(Math.ceil(bucket.resetAt / 1000))
120
+ }
121
+ });
122
+ }
123
+ bucket.tokens -= 1;
124
+ };
125
+ }
126
+ /**
127
+ * Combines multiple guards into a single composite guard.
128
+ */
129
+ export function composeGuards(...guards) {
130
+ return async (event) => {
131
+ for (const guard of guards) {
132
+ const result = await guard(event);
133
+ if (result)
134
+ return result;
135
+ }
136
+ };
137
+ }
@@ -0,0 +1,14 @@
1
+ import type { RequestHandler } from '@sveltejs/kit';
2
+ import type { YaxaEndpointConfig } from './types';
3
+ /**
4
+ * Wraps SvelteKit +server.ts request handlers into an opinionated,
5
+ * declarative, type-safe execution pipeline with Guards, Multi-Engine
6
+ * Validation, and RFC 7807 Problem Details.
7
+ */
8
+ export declare function yaxaEndpoint<TBodySchema = any, TQuerySchema = any, TParamsSchema = any, THeadersSchema = any, TResult = any>(config: YaxaEndpointConfig<TBodySchema, TQuerySchema, TParamsSchema, THeadersSchema, TResult>): RequestHandler & {
9
+ _yaxaConfig: typeof config;
10
+ };
11
+ export * from './types';
12
+ export * from './problem';
13
+ export * from './guards';
14
+ export * from './validate';
@@ -0,0 +1,238 @@
1
+ import { isProblemDetails, problemResponse } from './problem';
2
+ import { validateData } from './validate';
3
+ /**
4
+ * Parses URLSearchParams into a plain key-value object.
5
+ * Duplicate keys are parsed into arrays.
6
+ */
7
+ function parseSearchParamsToObject(searchParams) {
8
+ const result = {};
9
+ for (const key of searchParams.keys()) {
10
+ const allValues = searchParams.getAll(key);
11
+ if (allValues.length > 1) {
12
+ result[key] = allValues;
13
+ }
14
+ else if (allValues.length === 1) {
15
+ result[key] = allValues[0];
16
+ }
17
+ }
18
+ return result;
19
+ }
20
+ /**
21
+ * Safely parses request body based on Content-Type.
22
+ */
23
+ async function parseRequestBody(request) {
24
+ const contentType = request.headers.get('content-type') || '';
25
+ if (contentType.includes('application/json')) {
26
+ try {
27
+ const text = await request.text();
28
+ if (!text.trim()) {
29
+ return { ok: true, body: undefined };
30
+ }
31
+ return { ok: true, body: JSON.parse(text) };
32
+ }
33
+ catch {
34
+ return {
35
+ ok: false,
36
+ response: problemResponse({
37
+ status: 400,
38
+ title: 'Bad Request',
39
+ detail: 'Malformed JSON payload in request body.',
40
+ type: 'https://yaxa.dev/errors/malformed-json'
41
+ })
42
+ };
43
+ }
44
+ }
45
+ if (contentType.includes('application/x-www-form-urlencoded') ||
46
+ contentType.includes('multipart/form-data')) {
47
+ try {
48
+ const formData = await request.formData();
49
+ const formObj = {};
50
+ formData.forEach((val, key) => {
51
+ if (key in formObj) {
52
+ if (Array.isArray(formObj[key])) {
53
+ formObj[key].push(val);
54
+ }
55
+ else {
56
+ formObj[key] = [formObj[key], val];
57
+ }
58
+ }
59
+ else {
60
+ formObj[key] = val;
61
+ }
62
+ });
63
+ return { ok: true, body: formObj };
64
+ }
65
+ catch {
66
+ return {
67
+ ok: false,
68
+ response: problemResponse({
69
+ status: 400,
70
+ title: 'Bad Request',
71
+ detail: 'Malformed form-data in request body.'
72
+ })
73
+ };
74
+ }
75
+ }
76
+ // Plain text or other payloads
77
+ try {
78
+ const text = await request.text();
79
+ return { ok: true, body: text ? text : undefined };
80
+ }
81
+ catch {
82
+ return { ok: true, body: undefined };
83
+ }
84
+ }
85
+ /**
86
+ * Wraps SvelteKit +server.ts request handlers into an opinionated,
87
+ * declarative, type-safe execution pipeline with Guards, Multi-Engine
88
+ * Validation, and RFC 7807 Problem Details.
89
+ */
90
+ export function yaxaEndpoint(config) {
91
+ const handler = async (event) => {
92
+ const { request, url, locals, params } = event;
93
+ // 1. Execute Declarative Guards in Series
94
+ if (config.guards && config.guards.length > 0) {
95
+ for (const guard of config.guards) {
96
+ const result = await guard(event);
97
+ if (result instanceof Response) {
98
+ return result;
99
+ }
100
+ if (isProblemDetails(result)) {
101
+ return problemResponse(result);
102
+ }
103
+ if (result === false) {
104
+ return problemResponse({
105
+ status: 403,
106
+ title: 'Forbidden',
107
+ detail: 'Access denied by endpoint guard.'
108
+ });
109
+ }
110
+ }
111
+ }
112
+ // 2. Parse Raw Inputs
113
+ const rawParams = params || {};
114
+ const rawQuery = parseSearchParamsToObject(url.searchParams);
115
+ const rawHeaders = {};
116
+ request.headers.forEach((val, key) => {
117
+ rawHeaders[key.toLowerCase()] = val;
118
+ });
119
+ let rawBody;
120
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
121
+ const parsed = await parseRequestBody(request);
122
+ if (!parsed.ok) {
123
+ return parsed.response;
124
+ }
125
+ rawBody = parsed.body;
126
+ }
127
+ // 3. Multi-Engine Schema Validation (Standard Schema, Zod, etc.)
128
+ const allInvalidParams = [];
129
+ let validatedBody = rawBody;
130
+ let validatedQuery = rawQuery;
131
+ let validatedParams = rawParams;
132
+ let validatedHeaders = rawHeaders;
133
+ if (config.schema) {
134
+ if (config.schema.params) {
135
+ const res = await validateData(config.schema.params, rawParams, 'params');
136
+ if (res.success)
137
+ validatedParams = res.data;
138
+ else
139
+ allInvalidParams.push(...res.invalidParams);
140
+ }
141
+ if (config.schema.query) {
142
+ const res = await validateData(config.schema.query, rawQuery, 'query');
143
+ if (res.success)
144
+ validatedQuery = res.data;
145
+ else
146
+ allInvalidParams.push(...res.invalidParams);
147
+ }
148
+ if (config.schema.headers) {
149
+ const res = await validateData(config.schema.headers, rawHeaders, 'headers');
150
+ if (res.success)
151
+ validatedHeaders = res.data;
152
+ else
153
+ allInvalidParams.push(...res.invalidParams);
154
+ }
155
+ if (config.schema.body && request.method !== 'GET' && request.method !== 'HEAD') {
156
+ const res = await validateData(config.schema.body, rawBody, 'body');
157
+ if (res.success)
158
+ validatedBody = res.data;
159
+ else
160
+ allInvalidParams.push(...res.invalidParams);
161
+ }
162
+ }
163
+ if (allInvalidParams.length > 0) {
164
+ return problemResponse({
165
+ status: 422,
166
+ title: 'Unprocessable Entity',
167
+ detail: `Validation failed for ${allInvalidParams.length} parameter(s).`,
168
+ type: 'https://yaxa.dev/errors/validation-failed',
169
+ invalidParams: allInvalidParams
170
+ });
171
+ }
172
+ // 4. Construct Rich Endpoint Context
173
+ const context = {
174
+ event,
175
+ request,
176
+ url,
177
+ locals,
178
+ body: validatedBody,
179
+ query: validatedQuery,
180
+ params: validatedParams,
181
+ headers: validatedHeaders,
182
+ user: locals?.user || null,
183
+ session: locals?.session || null,
184
+ orgId: locals?.orgId || null
185
+ };
186
+ // 5. Execute Handler wrapped in Interceptors & Error Boundary
187
+ const runHandler = async () => {
188
+ const defaultStatus = config.status || 200;
189
+ let result;
190
+ try {
191
+ result = await config.handle(context);
192
+ }
193
+ catch (err) {
194
+ // Allow SvelteKit redirects and HTTP errors to bubble up
195
+ if (err && typeof err === 'object' && ('status' in err || 'location' in err)) {
196
+ throw err;
197
+ }
198
+ if (err instanceof Response) {
199
+ return err;
200
+ }
201
+ console.error('[Yaxa Endpoint Error]', err);
202
+ return problemResponse({
203
+ status: 500,
204
+ title: 'Internal Server Error',
205
+ detail: 'An unexpected internal error occurred.',
206
+ type: 'https://yaxa.dev/errors/internal-server-error'
207
+ });
208
+ }
209
+ // 6. Response Auto-Serialization
210
+ if (result instanceof Response) {
211
+ return result;
212
+ }
213
+ if (result === undefined || result === null) {
214
+ return new Response(null, { status: 204 });
215
+ }
216
+ if (typeof result === 'string') {
217
+ return new Response(result, {
218
+ status: defaultStatus,
219
+ headers: { 'content-type': 'text/plain; charset=utf-8' }
220
+ });
221
+ }
222
+ return Response.json(result, { status: defaultStatus });
223
+ };
224
+ // 7. Apply Interceptors (if any)
225
+ if (config.interceptors && config.interceptors.length > 0) {
226
+ const pipeline = config.interceptors.reduceRight((next, interceptor) => () => interceptor(context, next), runHandler);
227
+ return pipeline();
228
+ }
229
+ return runHandler();
230
+ };
231
+ // Attach configuration metadata for runtime reflection and OpenAPI generation (Milestone 3)
232
+ handler._yaxaConfig = config;
233
+ return handler;
234
+ }
235
+ export * from './types';
236
+ export * from './problem';
237
+ export * from './guards';
238
+ export * from './validate';
@@ -0,0 +1,26 @@
1
+ import type { ProblemDetails } from './types';
2
+ export interface CreateProblemOptions {
3
+ status: number;
4
+ title?: string;
5
+ detail?: string;
6
+ type?: string;
7
+ instance?: string;
8
+ invalidParams?: Array<{
9
+ name: string;
10
+ reason: string;
11
+ }>;
12
+ headers?: HeadersInit;
13
+ [key: string]: unknown;
14
+ }
15
+ /**
16
+ * Creates an RFC 7807 Problem Details object.
17
+ */
18
+ export declare function createProblemDetails(options: CreateProblemOptions): ProblemDetails;
19
+ /**
20
+ * Creates a standard SvelteKit Response formatted with application/problem+json.
21
+ */
22
+ export declare function problemResponse(options: CreateProblemOptions): Response;
23
+ /**
24
+ * Type-guard checking if an object conforms to ProblemDetails.
25
+ */
26
+ export declare function isProblemDetails(value: unknown): value is ProblemDetails;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Standard HTTP error status code to human-readable RFC 7807 titles.
3
+ */
4
+ const STATUS_TITLES = {
5
+ 400: 'Bad Request',
6
+ 401: 'Unauthorized',
7
+ 403: 'Forbidden',
8
+ 404: 'Not Found',
9
+ 405: 'Method Not Allowed',
10
+ 409: 'Conflict',
11
+ 422: 'Unprocessable Entity',
12
+ 429: 'Too Many Requests',
13
+ 500: 'Internal Server Error',
14
+ 502: 'Bad Gateway',
15
+ 503: 'Service Unavailable',
16
+ 504: 'Gateway Timeout'
17
+ };
18
+ /**
19
+ * Creates an RFC 7807 Problem Details object.
20
+ */
21
+ export function createProblemDetails(options) {
22
+ const status = options.status || 500;
23
+ const title = options.title || STATUS_TITLES[status] || 'Internal Error';
24
+ const type = options.type || `https://httpstatuses.com/${status}`;
25
+ const problem = {
26
+ type,
27
+ title,
28
+ status
29
+ };
30
+ for (const [key, value] of Object.entries(options)) {
31
+ if (key !== 'headers' && key !== 'status' && key !== 'title' && key !== 'type') {
32
+ problem[key] = value;
33
+ }
34
+ }
35
+ return problem;
36
+ }
37
+ /**
38
+ * Creates a standard SvelteKit Response formatted with application/problem+json.
39
+ */
40
+ export function problemResponse(options) {
41
+ const status = options.status || 500;
42
+ const problem = createProblemDetails(options);
43
+ const customHeaders = new Headers(options.headers || {});
44
+ if (!customHeaders.has('content-type')) {
45
+ customHeaders.set('content-type', 'application/problem+json; charset=utf-8');
46
+ }
47
+ return new Response(JSON.stringify(problem), {
48
+ status,
49
+ headers: customHeaders
50
+ });
51
+ }
52
+ /**
53
+ * Type-guard checking if an object conforms to ProblemDetails.
54
+ */
55
+ export function isProblemDetails(value) {
56
+ return (typeof value === 'object' &&
57
+ value !== null &&
58
+ 'status' in value &&
59
+ typeof value.status === 'number' &&
60
+ 'title' in value &&
61
+ 'type' in value);
62
+ }
@@ -0,0 +1,144 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ /**
3
+ * Standard RFC 7807 Problem Details representation.
4
+ * @see https://datatracker.ietf.org/doc/html/rfc7807
5
+ */
6
+ export interface ProblemDetails {
7
+ type: string;
8
+ title: string;
9
+ status: number;
10
+ detail?: string;
11
+ instance?: string;
12
+ invalidParams?: Array<{
13
+ name: string;
14
+ reason: string;
15
+ }>;
16
+ [key: string]: unknown;
17
+ }
18
+ /**
19
+ * Generic guard return value.
20
+ * - Return `Response` or `ProblemDetails`: Immediately stops pipeline and sends HTTP response.
21
+ * - Return `void` or `undefined` or `true`: Guard passed; continues to next step.
22
+ */
23
+ export type GuardResult = Response | ProblemDetails | void | undefined | boolean;
24
+ /**
25
+ * Guard function signature.
26
+ */
27
+ export type Guard = (event: RequestEvent) => Promise<GuardResult> | GuardResult;
28
+ /**
29
+ * Standard Schema or validator representation.
30
+ * Supports:
31
+ * 1. Standard Schema v1 (`~standard` property, e.g. Zod v3.24+, Zod v4, Valibot, ArkType)
32
+ * 2. `.safeParse()` (Zod, Yup, etc.)
33
+ * 3. Functional validator: `(data: unknown) => { success: boolean; data?: T; error?: any }`
34
+ */
35
+ export type AnyValidator<T = any> = {
36
+ '~standard': {
37
+ validate: (value: unknown) => Promise<{
38
+ value?: T;
39
+ issues?: ReadonlyArray<any>;
40
+ }> | {
41
+ value?: T;
42
+ issues?: ReadonlyArray<any>;
43
+ };
44
+ };
45
+ } | {
46
+ safeParse: (value: unknown) => {
47
+ success: true;
48
+ data: T;
49
+ } | {
50
+ success: false;
51
+ error: any;
52
+ };
53
+ } | {
54
+ parse: (value: unknown) => T;
55
+ } | ((value: unknown) => {
56
+ success: boolean;
57
+ data?: T;
58
+ error?: any;
59
+ });
60
+ /**
61
+ * Infer output type from a validator.
62
+ */
63
+ export type InferValidatorOutput<V> = V extends {
64
+ '~standard': {
65
+ validate: (...args: any[]) => Promise<{
66
+ value?: infer T;
67
+ }> | {
68
+ value?: infer T;
69
+ };
70
+ };
71
+ } ? T : V extends {
72
+ safeParse: (...args: any[]) => {
73
+ success: true;
74
+ data: infer T;
75
+ } | {
76
+ success: false;
77
+ error: any;
78
+ };
79
+ } ? T : V extends {
80
+ parse: (...args: any[]) => infer T;
81
+ } ? T : V extends (value: unknown) => {
82
+ success: true;
83
+ data: infer T;
84
+ } ? T : any;
85
+ export interface YaxaEndpointSchemaConfig<TBody = any, TQuery = any, TParams = any, THeaders = any> {
86
+ body?: TBody;
87
+ query?: TQuery;
88
+ params?: TParams;
89
+ headers?: THeaders;
90
+ }
91
+ /**
92
+ * Rich context passed to business handler in yaxaEndpoint.
93
+ */
94
+ export interface YaxaEndpointContext<TBody = any, TQuery = any, TParams = any, THeaders = any> {
95
+ event: RequestEvent;
96
+ request: Request;
97
+ url: URL;
98
+ locals: App.Locals;
99
+ body: TBody;
100
+ query: TQuery;
101
+ params: TParams;
102
+ headers: THeaders;
103
+ user: any;
104
+ session: any;
105
+ orgId: string | null;
106
+ }
107
+ /**
108
+ * Interceptor function wrapping handler execution.
109
+ */
110
+ export type YaxaInterceptor = (context: YaxaEndpointContext, next: () => Promise<Response>) => Promise<Response>;
111
+ /**
112
+ * Endpoint configuration passed to yaxaEndpoint().
113
+ */
114
+ export interface YaxaEndpointConfig<TBodySchema = any, TQuerySchema = any, TParamsSchema = any, THeadersSchema = any, TResult = any> {
115
+ /**
116
+ * Sequence of declarative guards executed before handler body.
117
+ * (e.g. requireAuth(), requireRole(['admin']), rateLimit({ points: 10 }))
118
+ */
119
+ guards?: Guard[];
120
+ /**
121
+ * Schemas for input payload validation (body, query string, path params, headers).
122
+ */
123
+ schema?: YaxaEndpointSchemaConfig<TBodySchema, TQuerySchema, TParamsSchema, THeadersSchema>;
124
+ /**
125
+ * Optional status code to return for successful non-Response return values.
126
+ * Defaults to 200 (or 201 for POST when returning created object if desired).
127
+ */
128
+ status?: number;
129
+ /**
130
+ * Optional response interceptors (e.g. audit logging, timing headers).
131
+ */
132
+ interceptors?: YaxaInterceptor[];
133
+ /**
134
+ * OpenAPI metadata annotations (used by Milestone 3 auto-docs).
135
+ */
136
+ summary?: string;
137
+ description?: string;
138
+ tags?: string[];
139
+ deprecated?: boolean;
140
+ /**
141
+ * Core business logic handler.
142
+ */
143
+ handle: (context: YaxaEndpointContext<InferValidatorOutput<TBodySchema>, InferValidatorOutput<TQuerySchema>, InferValidatorOutput<TParamsSchema>, InferValidatorOutput<THeadersSchema>>) => Promise<TResult> | TResult;
144
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ export interface ValidationSuccess<T> {
2
+ success: true;
3
+ data: T;
4
+ }
5
+ export interface ValidationFailure {
6
+ success: false;
7
+ invalidParams: Array<{
8
+ name: string;
9
+ reason: string;
10
+ }>;
11
+ rawError?: any;
12
+ }
13
+ export type ValidationOutcome<T> = ValidationSuccess<T> | ValidationFailure;
14
+ /**
15
+ * Validates arbitrary input data against any supported validator:
16
+ * 1. Standard Schema v1 (`~standard` property, e.g. Zod v3.24+, Zod v4, Valibot, ArkType)
17
+ * 2. `.safeParse()` (Zod, Yup, etc.)
18
+ * 3. `.parse()`
19
+ * 4. Custom validation function: `(data) => { success, data, error }`
20
+ */
21
+ export declare function validateData<T = any>(validator: any, input: unknown, scopePrefix?: string): Promise<ValidationOutcome<T>>;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Validates arbitrary input data against any supported validator:
3
+ * 1. Standard Schema v1 (`~standard` property, e.g. Zod v3.24+, Zod v4, Valibot, ArkType)
4
+ * 2. `.safeParse()` (Zod, Yup, etc.)
5
+ * 3. `.parse()`
6
+ * 4. Custom validation function: `(data) => { success, data, error }`
7
+ */
8
+ export async function validateData(validator, input, scopePrefix = '') {
9
+ if (!validator) {
10
+ return { success: true, data: input };
11
+ }
12
+ // 1. Standard Schema v1 (~standard specification)
13
+ if (typeof validator === 'object' && validator !== null && '~standard' in validator) {
14
+ try {
15
+ const std = validator['~standard'];
16
+ const result = await std.validate(input);
17
+ if (result &&
18
+ 'issues' in result &&
19
+ Array.isArray(result.issues) &&
20
+ result.issues.length > 0) {
21
+ const invalidParams = result.issues.map((issue) => {
22
+ let pathStr = '';
23
+ if (Array.isArray(issue.path)) {
24
+ pathStr = issue.path
25
+ .map((seg) => typeof seg === 'object' && seg !== null && 'key' in seg ? seg.key : String(seg))
26
+ .join('.');
27
+ }
28
+ const name = scopePrefix
29
+ ? pathStr
30
+ ? `${scopePrefix}.${pathStr}`
31
+ : scopePrefix
32
+ : pathStr || 'value';
33
+ return {
34
+ name,
35
+ reason: issue.message || 'Invalid value'
36
+ };
37
+ });
38
+ return { success: false, invalidParams, rawError: result.issues };
39
+ }
40
+ return { success: true, data: result.value };
41
+ }
42
+ catch (err) {
43
+ return {
44
+ success: false,
45
+ invalidParams: [
46
+ { name: scopePrefix || 'value', reason: err?.message || 'Validation failed' }
47
+ ],
48
+ rawError: err
49
+ };
50
+ }
51
+ }
52
+ // 2. Standard .safeParse() (Zod, etc.)
53
+ if (typeof validator === 'object' &&
54
+ validator !== null &&
55
+ typeof validator.safeParse === 'function') {
56
+ const parsed = validator.safeParse(input);
57
+ if (parsed.success) {
58
+ return { success: true, data: parsed.data };
59
+ }
60
+ // ZodError or similar
61
+ const issues = parsed.error?.issues || parsed.error?.errors || [];
62
+ const invalidParams = issues.length > 0
63
+ ? issues.map((issue) => {
64
+ const pathStr = Array.isArray(issue.path) ? issue.path.join('.') : '';
65
+ const name = scopePrefix
66
+ ? pathStr
67
+ ? `${scopePrefix}.${pathStr}`
68
+ : scopePrefix
69
+ : pathStr || 'value';
70
+ return {
71
+ name,
72
+ reason: issue.message || 'Validation failed'
73
+ };
74
+ })
75
+ : [{ name: scopePrefix || 'value', reason: parsed.error?.message || 'Validation failed' }];
76
+ return { success: false, invalidParams, rawError: parsed.error };
77
+ }
78
+ // 3. Functional Validator
79
+ if (typeof validator === 'function') {
80
+ try {
81
+ const res = validator(input);
82
+ if (res && typeof res === 'object' && 'success' in res) {
83
+ if (res.success) {
84
+ return { success: true, data: (res.data !== undefined ? res.data : input) };
85
+ }
86
+ const reason = res.error?.message || (typeof res.error === 'string' ? res.error : 'Validation failed');
87
+ return {
88
+ success: false,
89
+ invalidParams: [{ name: scopePrefix || 'value', reason }],
90
+ rawError: res.error
91
+ };
92
+ }
93
+ // If function returned parsed data without error
94
+ return { success: true, data: res };
95
+ }
96
+ catch (err) {
97
+ return {
98
+ success: false,
99
+ invalidParams: [
100
+ { name: scopePrefix || 'value', reason: err?.message || 'Validation failed' }
101
+ ],
102
+ rawError: err
103
+ };
104
+ }
105
+ }
106
+ // 4. Standard .parse() throwing validator
107
+ if (typeof validator === 'object' &&
108
+ validator !== null &&
109
+ typeof validator.parse === 'function') {
110
+ try {
111
+ const data = validator.parse(input);
112
+ return { success: true, data: data };
113
+ }
114
+ catch (err) {
115
+ const issues = err?.issues || err?.errors || [];
116
+ const invalidParams = issues.length > 0
117
+ ? issues.map((issue) => ({
118
+ name: scopePrefix
119
+ ? `${scopePrefix}.${issue.path?.join('.')}`
120
+ : issue.path?.join('.') || 'value',
121
+ reason: issue.message || 'Validation failed'
122
+ }))
123
+ : [{ name: scopePrefix || 'value', reason: err?.message || 'Validation failed' }];
124
+ return { success: false, invalidParams, rawError: err };
125
+ }
126
+ }
127
+ return { success: true, data: input };
128
+ }
@@ -22,3 +22,5 @@ export { getResendClient, sendMagicLinkEmail, sendWelcomeEmail } from './email/i
22
22
  export type { ResendEmailConfig, SendMagicLinkOptions, SendWelcomeEmailOptions } from './email/index';
23
23
  export { getS3Client, createPresignedUploadUrl, createS3UploadHandler } from './storage/index';
24
24
  export type { S3StorageConfig, PresignedUploadOptions, PresignedUploadResult, S3UploadHandlerOptions } from './storage/index';
25
+ export { yaxaEndpoint, createProblemDetails, problemResponse, isProblemDetails, requireAuth, requireRole, rateLimit, composeGuards, validateData } from './endpoint/index';
26
+ export type { YaxaEndpointConfig, YaxaEndpointContext, YaxaEndpointSchemaConfig, YaxaInterceptor, Guard, GuardResult, ProblemDetails, CreateProblemOptions, RequireAuthOptions, RequireRoleOptions, RateLimitOptions, AnyValidator, InferValidatorOutput, ValidationOutcome, ValidationSuccess, ValidationFailure } from './endpoint/index';
@@ -18,3 +18,5 @@ export { getPolarClient, createPolarCheckout, createPolarCustomerPortalSession,
18
18
  export { getResendClient, sendMagicLinkEmail, sendWelcomeEmail } from './email/index';
19
19
  // Cloud Storage (AWS S3 & Cloudflare R2)
20
20
  export { getS3Client, createPresignedUploadUrl, createS3UploadHandler } from './storage/index';
21
+ // Endpoint Pipeline, Guards & RFC 7807 Validation (Milestone 1)
22
+ export { yaxaEndpoint, createProblemDetails, problemResponse, isProblemDetails, requireAuth, requireRole, rateLimit, composeGuards, validateData } from './endpoint/index';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaxa-svelte",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "The Intuitive Svelte UI Library — Nuxt UI v4 equivalent for SvelteKit 2 & 3 & Svelte 5 with automated SEO, dynamic OpenGraph cards, sitemaps, robots.txt, and Tailwind CSS v4.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://yaxa.vercel.app",