yaxa-svelte 2.2.0 → 2.4.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.
Files changed (39) hide show
  1. package/README.md +85 -14
  2. package/dist/server/db/schema-pg.d.ts +262 -0
  3. package/dist/server/db/schema-pg.js +20 -0
  4. package/dist/server/db/schema-sqlite.d.ts +276 -0
  5. package/dist/server/db/schema-sqlite.js +20 -0
  6. package/dist/server/db/types.d.ts +18 -0
  7. package/dist/server/endpoint/guards.d.ts +5 -0
  8. package/dist/server/endpoint/guards.js +19 -3
  9. package/dist/server/endpoint/index.js +4 -0
  10. package/dist/server/endpoint/types.d.ts +7 -0
  11. package/dist/server/index.d.ts +4 -0
  12. package/dist/server/index.js +4 -0
  13. package/dist/server/jobs/events.d.ts +29 -0
  14. package/dist/server/jobs/events.js +74 -0
  15. package/dist/server/jobs/index.d.ts +4 -0
  16. package/dist/server/jobs/index.js +3 -0
  17. package/dist/server/jobs/jobs.test.d.ts +1 -0
  18. package/dist/server/jobs/jobs.test.js +189 -0
  19. package/dist/server/jobs/queue.d.ts +14 -0
  20. package/dist/server/jobs/queue.js +90 -0
  21. package/dist/server/jobs/runner.d.ts +12 -0
  22. package/dist/server/jobs/runner.js +176 -0
  23. package/dist/server/jobs/types.d.ts +136 -0
  24. package/dist/server/jobs/types.js +1 -0
  25. package/dist/server/openapi/generator.d.ts +5 -0
  26. package/dist/server/openapi/generator.js +317 -0
  27. package/dist/server/openapi/handler.d.ts +7 -0
  28. package/dist/server/openapi/handler.js +117 -0
  29. package/dist/server/openapi/index.d.ts +5 -0
  30. package/dist/server/openapi/index.js +4 -0
  31. package/dist/server/openapi/openapi.test.d.ts +1 -0
  32. package/dist/server/openapi/openapi.test.js +219 -0
  33. package/dist/server/openapi/registry.d.ts +35 -0
  34. package/dist/server/openapi/registry.js +51 -0
  35. package/dist/server/openapi/schema.d.ts +4 -0
  36. package/dist/server/openapi/schema.js +59 -0
  37. package/dist/server/openapi/types.d.ts +127 -0
  38. package/dist/server/openapi/types.js +1 -0
  39. package/package.json +1 -1
@@ -0,0 +1,176 @@
1
+ import { memoryJobStore } from './queue';
2
+ /**
3
+ * Declares a typed worker handler for a specific job name.
4
+ */
5
+ export function defineWorker(name, handler, options = {}) {
6
+ return {
7
+ name,
8
+ handler,
9
+ queue: options.queue || 'default'
10
+ };
11
+ }
12
+ /**
13
+ * Calculates exponential backoff delay with jitter.
14
+ */
15
+ function calculateBackoffDelay(attempt) {
16
+ const baseDelay = Math.min(3600000, Math.pow(2, attempt) * 1000);
17
+ const jitter = Math.floor(Math.random() * 500);
18
+ return baseDelay + jitter;
19
+ }
20
+ /**
21
+ * Creates an embedded job runner capable of executing pending jobs either on-demand
22
+ * (for serverless cron invocations) or continuously in the background (for Node.js servers).
23
+ */
24
+ export function createJobRunner(options) {
25
+ const queue = options.queue || 'default';
26
+ const batchSize = options.batchSize || 10;
27
+ const pollIntervalMs = options.pollIntervalMs || 3000;
28
+ const lockTimeoutMs = options.lockTimeoutMs || 5 * 60 * 1000;
29
+ const workerId = options.workerId || `worker_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
30
+ // Index worker handlers by name
31
+ const workerMap = new Map();
32
+ for (const worker of options.workers) {
33
+ workerMap.set(worker.name, worker.handler);
34
+ }
35
+ let activeTimer = null;
36
+ let isRunning = false;
37
+ let isProcessing = false;
38
+ /**
39
+ * Leases next batch of jobs from memory or database.
40
+ */
41
+ const leaseNextBatch = async () => {
42
+ const now = Date.now();
43
+ const leased = [];
44
+ // 1. Process from memory store
45
+ const candidates = Array.from(memoryJobStore.values()).filter((job) => {
46
+ if (job.queue !== queue)
47
+ return false;
48
+ const isReady = job.runAt.getTime() <= now;
49
+ const isPending = job.status === 'pending' && isReady;
50
+ const isStaleLocked = job.status === 'processing' && job.lockedAt && now - job.lockedAt.getTime() > lockTimeoutMs;
51
+ return isPending || isStaleLocked;
52
+ });
53
+ // Sort by priority DESC, runAt ASC
54
+ candidates.sort((a, b) => {
55
+ if (b.priority !== a.priority)
56
+ return b.priority - a.priority;
57
+ return a.runAt.getTime() - b.runAt.getTime();
58
+ });
59
+ const batch = candidates.slice(0, batchSize);
60
+ for (const job of batch) {
61
+ job.status = 'processing';
62
+ job.lockedAt = new Date(now);
63
+ job.lockedBy = workerId;
64
+ job.updatedAt = new Date(now);
65
+ leased.push(job);
66
+ }
67
+ return leased;
68
+ };
69
+ /**
70
+ * Processes the next batch of pending jobs.
71
+ */
72
+ const processNextBatch = async () => {
73
+ if (isProcessing) {
74
+ return { processed: 0, succeeded: 0, failed: 0 };
75
+ }
76
+ isProcessing = true;
77
+ let succeeded = 0;
78
+ let failed = 0;
79
+ try {
80
+ const batch = await leaseNextBatch();
81
+ for (const job of batch) {
82
+ const handler = workerMap.get(job.name);
83
+ if (!handler) {
84
+ console.warn(`[JobRunner] No worker handler registered for job "${job.name}" (${job.id}). Leaving pending.`);
85
+ job.status = 'pending';
86
+ job.lockedAt = null;
87
+ job.lockedBy = null;
88
+ continue;
89
+ }
90
+ const logger = {
91
+ info: (msg, ...args) => console.log(`[JobRunner:${job.name}:${job.id}] ${msg}`, ...args),
92
+ warn: (msg, ...args) => console.warn(`[JobRunner:${job.name}:${job.id}] ${msg}`, ...args),
93
+ error: (msg, ...args) => console.error(`[JobRunner:${job.name}:${job.id}] ${msg}`, ...args)
94
+ };
95
+ try {
96
+ await handler(job.payload, { job, logger });
97
+ // Mark completed
98
+ const now = new Date();
99
+ job.status = 'completed';
100
+ job.completedAt = now;
101
+ job.updatedAt = now;
102
+ job.lockedAt = null;
103
+ job.lockedBy = null;
104
+ succeeded++;
105
+ }
106
+ catch (err) {
107
+ const now = new Date();
108
+ job.attempts += 1;
109
+ job.lockedAt = null;
110
+ job.lockedBy = null;
111
+ job.failedReason = err?.message || String(err);
112
+ job.updatedAt = now;
113
+ if (job.attempts < job.maxAttempts) {
114
+ // Exponential backoff retry
115
+ const delayMs = calculateBackoffDelay(job.attempts);
116
+ job.status = 'pending';
117
+ job.runAt = new Date(Date.now() + delayMs);
118
+ logger.warn(`Job execution failed (Attempt ${job.attempts}/${job.maxAttempts}). Retrying in ${Math.round(delayMs / 1000)}s: ${job.failedReason}`);
119
+ }
120
+ else {
121
+ // Exceeded max attempts: Route to Dead Letter Queue (DLQ)
122
+ job.status = 'failed';
123
+ failed++;
124
+ logger.error(`Job failed permanently after ${job.attempts} attempts. Routed to Dead Letter Queue: ${job.failedReason}`);
125
+ }
126
+ }
127
+ }
128
+ return {
129
+ processed: batch.length,
130
+ succeeded,
131
+ failed
132
+ };
133
+ }
134
+ finally {
135
+ isProcessing = false;
136
+ }
137
+ };
138
+ /**
139
+ * Continuous background polling loop.
140
+ */
141
+ const poll = async () => {
142
+ if (!isRunning)
143
+ return;
144
+ try {
145
+ await processNextBatch();
146
+ }
147
+ catch (err) {
148
+ console.error('[JobRunner] Unexpected error during polling cycle:', err);
149
+ }
150
+ finally {
151
+ if (isRunning) {
152
+ activeTimer = setTimeout(poll, pollIntervalMs);
153
+ if (activeTimer && typeof activeTimer === 'object' && 'unref' in activeTimer) {
154
+ activeTimer.unref();
155
+ }
156
+ }
157
+ }
158
+ };
159
+ return {
160
+ processNextBatch,
161
+ start: () => {
162
+ if (isRunning)
163
+ return;
164
+ isRunning = true;
165
+ poll();
166
+ },
167
+ stop: () => {
168
+ isRunning = false;
169
+ if (activeTimer) {
170
+ clearTimeout(activeTimer);
171
+ activeTimer = null;
172
+ }
173
+ },
174
+ isRunning: () => isRunning
175
+ };
176
+ }
@@ -0,0 +1,136 @@
1
+ import type { AnyDb } from '../db';
2
+ import type { JobStatus } from '../db/types';
3
+ /**
4
+ * Persisted Job record structure.
5
+ */
6
+ export interface JobRecord<TPayload = any> {
7
+ id: string;
8
+ queue: string;
9
+ name: string;
10
+ payload: TPayload;
11
+ status: JobStatus;
12
+ priority: number;
13
+ attempts: number;
14
+ maxAttempts: number;
15
+ runAt: Date;
16
+ lockedAt?: Date | null;
17
+ lockedBy?: string | null;
18
+ failedReason?: string | null;
19
+ createdAt: Date;
20
+ updatedAt: Date;
21
+ completedAt?: Date | null;
22
+ }
23
+ /**
24
+ * Delay specifier:
25
+ * - String format: '30s', '5m', '2h', '1d'
26
+ * - Number: delay in milliseconds
27
+ * - Date: absolute execution timestamp
28
+ */
29
+ export type JobDelay = string | number | Date;
30
+ export interface EnqueueJobOptions {
31
+ /**
32
+ * Queue partition name (default: 'default').
33
+ */
34
+ queue?: string;
35
+ /**
36
+ * Execution delay or scheduled Date.
37
+ */
38
+ delay?: JobDelay;
39
+ /**
40
+ * Job execution priority (higher number = higher priority). Default: 0.
41
+ */
42
+ priority?: number;
43
+ /**
44
+ * Maximum retry attempts before routing to Dead Letter Queue (DLQ). Default: 3.
45
+ */
46
+ maxAttempts?: number;
47
+ /**
48
+ * Optional Drizzle transaction object for the Transactional Outbox pattern.
49
+ */
50
+ tx?: any;
51
+ /**
52
+ * Optional custom Drizzle DB client instance.
53
+ */
54
+ db?: AnyDb;
55
+ }
56
+ export interface WorkerContext<TPayload = any> {
57
+ job: JobRecord<TPayload>;
58
+ logger: {
59
+ info: (msg: string, ...args: any[]) => void;
60
+ warn: (msg: string, ...args: any[]) => void;
61
+ error: (msg: string, ...args: any[]) => void;
62
+ };
63
+ }
64
+ export type WorkerHandler<TPayload = any> = (payload: TPayload, context: WorkerContext<TPayload>) => Promise<void> | void;
65
+ export interface WorkerDefinition<TPayload = any> {
66
+ name: string;
67
+ handler: WorkerHandler<TPayload>;
68
+ queue?: string;
69
+ }
70
+ export interface JobRunnerOptions {
71
+ /**
72
+ * Drizzle database instance (Neon or Turso).
73
+ */
74
+ db?: AnyDb;
75
+ /**
76
+ * Database driver ('neon' | 'turso' | 'sqlite').
77
+ */
78
+ driver?: 'neon' | 'turso' | 'sqlite';
79
+ /**
80
+ * List of registered worker handlers.
81
+ */
82
+ workers: WorkerDefinition<any>[];
83
+ /**
84
+ * Target queue name to poll (default: 'default').
85
+ */
86
+ queue?: string;
87
+ /**
88
+ * Maximum jobs to lease and process per batch (default: 10).
89
+ */
90
+ batchSize?: number;
91
+ /**
92
+ * Polling interval in milliseconds for daemon mode (default: 3000ms).
93
+ */
94
+ pollIntervalMs?: number;
95
+ /**
96
+ * Lock timeout in milliseconds before a processing job is considered stale (default: 5 minutes).
97
+ */
98
+ lockTimeoutMs?: number;
99
+ /**
100
+ * Worker instance identifier (default: generated worker id).
101
+ */
102
+ workerId?: string;
103
+ }
104
+ export interface JobBatchResult {
105
+ processed: number;
106
+ succeeded: number;
107
+ failed: number;
108
+ }
109
+ export interface JobRunner {
110
+ /**
111
+ * Processes the next batch of pending jobs. Perfect for serverless cron endpoints (/api/cron/jobs).
112
+ */
113
+ processNextBatch: () => Promise<JobBatchResult>;
114
+ /**
115
+ * Starts the continuous background polling loop (for long-lived Node.js servers).
116
+ */
117
+ start: () => void;
118
+ /**
119
+ * Stops the background polling loop.
120
+ */
121
+ stop: () => void;
122
+ /**
123
+ * Returns true if the polling runner is currently active.
124
+ */
125
+ isRunning: () => boolean;
126
+ }
127
+ export type EventHandler<TData = any> = (data: TData) => Promise<void> | void;
128
+ export interface EventSubscription {
129
+ unsubscribe: () => void;
130
+ }
131
+ export interface IEventBus {
132
+ publish: <TData = any>(eventName: string, data: TData) => Promise<void>;
133
+ subscribe: <TData = any>(eventName: string, handler: EventHandler<TData>) => EventSubscription;
134
+ once: <TData = any>(eventName: string, handler: EventHandler<TData>) => EventSubscription;
135
+ clear: () => void;
136
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { GenerateOpenApiOptions, OpenApiSpec31 } from './types';
2
+ /**
3
+ * Generates an OpenAPI 3.1.0 document from registered yaxaEndpoints.
4
+ */
5
+ export declare function generateOpenApiSpec(options?: GenerateOpenApiOptions): OpenApiSpec31;
@@ -0,0 +1,317 @@
1
+ import { convertToJsonSchema } from './schema';
2
+ import { globalApiRegistry } from './registry';
3
+ /**
4
+ * Standard RFC 7807 Problem Details Schema for OpenAPI Components.
5
+ */
6
+ const PROBLEM_DETAILS_SCHEMA = {
7
+ type: 'object',
8
+ properties: {
9
+ type: { type: 'string', format: 'uri', example: 'https://yaxa.dev/errors/validation-failed' },
10
+ title: { type: 'string', example: 'Unprocessable Entity' },
11
+ status: { type: 'integer', example: 422 },
12
+ detail: { type: 'string', example: 'Validation failed for 1 parameter(s).' },
13
+ instance: { type: 'string' },
14
+ invalidParams: {
15
+ type: 'array',
16
+ items: {
17
+ type: 'object',
18
+ properties: {
19
+ name: { type: 'string', example: 'body.email' },
20
+ reason: { type: 'string', example: 'Invalid email address' }
21
+ },
22
+ required: ['name', 'reason']
23
+ }
24
+ }
25
+ },
26
+ required: ['type', 'title', 'status']
27
+ };
28
+ /**
29
+ * Extracts parameter list from a converted JSON schema.
30
+ */
31
+ function extractParametersFromSchema(jsonSchema, location) {
32
+ if (!jsonSchema || typeof jsonSchema !== 'object' || !jsonSchema.properties) {
33
+ return [];
34
+ }
35
+ const requiredList = Array.isArray(jsonSchema.required) ? jsonSchema.required : [];
36
+ const parameters = [];
37
+ for (const [key, propSchema] of Object.entries(jsonSchema.properties)) {
38
+ parameters.push({
39
+ name: key,
40
+ in: location,
41
+ required: location === 'path' ? true : requiredList.includes(key),
42
+ description: propSchema?.description,
43
+ deprecated: propSchema?.deprecated,
44
+ schema: propSchema
45
+ });
46
+ }
47
+ return parameters;
48
+ }
49
+ /**
50
+ * Resolves YaxaEndpointConfig from an endpoint function or config object.
51
+ */
52
+ function resolveConfig(endpoint) {
53
+ if (!endpoint)
54
+ return null;
55
+ if (typeof endpoint === 'function' && '_yaxaConfig' in endpoint) {
56
+ return endpoint._yaxaConfig;
57
+ }
58
+ if (typeof endpoint === 'object' && 'handle' in endpoint) {
59
+ return endpoint;
60
+ }
61
+ return null;
62
+ }
63
+ /**
64
+ * Generates an OpenAPI 3.1.0 document from registered yaxaEndpoints.
65
+ */
66
+ export function generateOpenApiSpec(options = {}) {
67
+ const title = options.title || 'Yaxa SvelteKit API';
68
+ const version = options.version || '1.0.0';
69
+ const description = options.description || 'Auto-generated OpenAPI 3.1 contract via yaxa-svelte.';
70
+ const registry = options.registry || globalApiRegistry;
71
+ const registeredEndpoints = registry.getEndpoints();
72
+ // Merge manual options.endpoints if provided
73
+ const mergedEndpoints = {
74
+ ...registeredEndpoints
75
+ };
76
+ if (options.endpoints) {
77
+ for (const [rawPath, methods] of Object.entries(options.endpoints)) {
78
+ const normalizedPath = registry.normalizePath(rawPath);
79
+ if (!mergedEndpoints[normalizedPath]) {
80
+ mergedEndpoints[normalizedPath] = {};
81
+ }
82
+ for (const [method, handler] of Object.entries(methods)) {
83
+ mergedEndpoints[normalizedPath][method.toUpperCase()] = handler;
84
+ }
85
+ }
86
+ }
87
+ const paths = {};
88
+ const securitySchemes = {};
89
+ let usesSecurity = false;
90
+ for (const [path, methods] of Object.entries(mergedEndpoints)) {
91
+ paths[path] = {};
92
+ for (const [method, endpoint] of Object.entries(methods)) {
93
+ const config = resolveConfig(endpoint);
94
+ const methodLower = method.toLowerCase();
95
+ if (!config) {
96
+ // Fallback for raw RequestHandler
97
+ paths[path][methodLower] = {
98
+ summary: `${method.toUpperCase()} ${path}`,
99
+ responses: {
100
+ '200': { description: 'Successful response' }
101
+ }
102
+ };
103
+ continue;
104
+ }
105
+ const parameters = [];
106
+ // 1. Path Params
107
+ if (config.schema?.params) {
108
+ const paramsSchema = convertToJsonSchema(config.schema.params);
109
+ parameters.push(...extractParametersFromSchema(paramsSchema, 'path'));
110
+ }
111
+ else {
112
+ // Detect {param} placeholders in path and add basic string parameters
113
+ const pathParamMatches = path.match(/\{(\w+)\}/g);
114
+ if (pathParamMatches) {
115
+ for (const match of pathParamMatches) {
116
+ const paramName = match.slice(1, -1);
117
+ parameters.push({
118
+ name: paramName,
119
+ in: 'path',
120
+ required: true,
121
+ schema: { type: 'string' }
122
+ });
123
+ }
124
+ }
125
+ }
126
+ // 2. Query Params
127
+ if (config.schema?.query) {
128
+ const querySchema = convertToJsonSchema(config.schema.query);
129
+ parameters.push(...extractParametersFromSchema(querySchema, 'query'));
130
+ }
131
+ // 3. Header Params
132
+ if (config.schema?.headers) {
133
+ const headersSchema = convertToJsonSchema(config.schema.headers);
134
+ parameters.push(...extractParametersFromSchema(headersSchema, 'header'));
135
+ }
136
+ // 4. Request Body
137
+ let requestBody = undefined;
138
+ if (config.schema?.body && method !== 'GET' && method !== 'HEAD') {
139
+ const bodySchema = convertToJsonSchema(config.schema.body);
140
+ requestBody = {
141
+ required: true,
142
+ content: {
143
+ 'application/json': {
144
+ schema: bodySchema
145
+ }
146
+ }
147
+ };
148
+ }
149
+ // 5. Responses
150
+ const responses = {};
151
+ const successStatus = config.status
152
+ ? String(config.status)
153
+ : method === 'POST'
154
+ ? '201'
155
+ : '200';
156
+ if (config.responses) {
157
+ for (const [statusCode, resp] of Object.entries(config.responses)) {
158
+ responses[String(statusCode)] = {
159
+ description: resp.description,
160
+ content: resp.schema
161
+ ? {
162
+ 'application/json': {
163
+ schema: convertToJsonSchema(resp.schema)
164
+ }
165
+ }
166
+ : undefined
167
+ };
168
+ }
169
+ }
170
+ if (!responses[successStatus]) {
171
+ responses[successStatus] = {
172
+ description: successStatus === '201'
173
+ ? 'Created'
174
+ : successStatus === '204'
175
+ ? 'No Content'
176
+ : 'Successful Operation',
177
+ content: successStatus !== '204'
178
+ ? {
179
+ 'application/json': {
180
+ schema: { type: 'object' }
181
+ }
182
+ }
183
+ : undefined
184
+ };
185
+ }
186
+ // 6. Guard Reflection (Security & Error Status Codes)
187
+ let hasAuthGuard = false;
188
+ let hasRoleGuard = false;
189
+ let roleGuardRoles = [];
190
+ let hasRateLimitGuard = false;
191
+ let rateLimitRetryAfter = 60;
192
+ if (config.guards && config.guards.length > 0) {
193
+ for (const guard of config.guards) {
194
+ const meta = guard._yaxaGuard;
195
+ if (meta) {
196
+ if (meta.type === 'auth') {
197
+ hasAuthGuard = true;
198
+ }
199
+ else if (meta.type === 'role') {
200
+ hasRoleGuard = true;
201
+ if (meta.roles)
202
+ roleGuardRoles = meta.roles;
203
+ }
204
+ else if (meta.type === 'rateLimit') {
205
+ hasRateLimitGuard = true;
206
+ if (meta.options?.windowSeconds) {
207
+ rateLimitRetryAfter = meta.options.windowSeconds;
208
+ }
209
+ }
210
+ }
211
+ }
212
+ }
213
+ const operationSecurity = [];
214
+ if (hasAuthGuard || hasRoleGuard) {
215
+ usesSecurity = true;
216
+ securitySchemes['BearerAuth'] = {
217
+ type: 'http',
218
+ scheme: 'bearer',
219
+ bearerFormat: 'JWT',
220
+ description: 'Standard Bearer Token Header (`Authorization: Bearer <token>`)'
221
+ };
222
+ securitySchemes['CookieAuth'] = {
223
+ type: 'apiKey',
224
+ in: 'cookie',
225
+ name: 'better-auth.session_token',
226
+ description: 'Session cookie via Better-Auth'
227
+ };
228
+ operationSecurity.push({ BearerAuth: [] }, { CookieAuth: [] });
229
+ responses['401'] = {
230
+ description: 'Unauthorized (RFC 7807 Problem Details)',
231
+ content: {
232
+ 'application/problem+json': {
233
+ schema: { $ref: '#/components/schemas/ProblemDetails' }
234
+ }
235
+ }
236
+ };
237
+ }
238
+ if (hasRoleGuard) {
239
+ responses['403'] = {
240
+ description: `Forbidden — Insufficient role permissions. Requires: [${roleGuardRoles.join(', ')}]`,
241
+ content: {
242
+ 'application/problem+json': {
243
+ schema: { $ref: '#/components/schemas/ProblemDetails' }
244
+ }
245
+ }
246
+ };
247
+ }
248
+ // 7. Schema Validation Error (422)
249
+ if (config.schema &&
250
+ (config.schema.body || config.schema.query || config.schema.params || config.schema.headers)) {
251
+ responses['422'] = {
252
+ description: 'Unprocessable Entity — Schema validation failed (RFC 7807)',
253
+ content: {
254
+ 'application/problem+json': {
255
+ schema: { $ref: '#/components/schemas/ProblemDetails' }
256
+ }
257
+ }
258
+ };
259
+ }
260
+ // 8. Rate Limit Error (429)
261
+ if (hasRateLimitGuard) {
262
+ responses['429'] = {
263
+ description: 'Too Many Requests — Rate limit exceeded (RFC 7807)',
264
+ headers: {
265
+ 'Retry-After': {
266
+ description: 'Seconds to wait before retrying',
267
+ schema: { type: 'integer', example: rateLimitRetryAfter }
268
+ }
269
+ },
270
+ content: {
271
+ 'application/problem+json': {
272
+ schema: { $ref: '#/components/schemas/ProblemDetails' }
273
+ }
274
+ }
275
+ };
276
+ }
277
+ // 9. Standard 500 Sanitized Error
278
+ responses['500'] = {
279
+ description: 'Internal Server Error — Sanitized exception (RFC 7807)',
280
+ content: {
281
+ 'application/problem+json': {
282
+ schema: { $ref: '#/components/schemas/ProblemDetails' }
283
+ }
284
+ }
285
+ };
286
+ const operation = {
287
+ summary: config.summary || `${method.toUpperCase()} ${path}`,
288
+ description: config.description,
289
+ operationId: config.operationId ||
290
+ `${methodLower}_${path.replace(/[/{}]+/g, '_').replace(/^_+|_+$/g, '')}`,
291
+ tags: config.tags || (path.split('/')[2] ? [path.split('/')[2]] : ['Default']),
292
+ deprecated: config.deprecated,
293
+ parameters: parameters.length > 0 ? parameters : undefined,
294
+ requestBody,
295
+ responses,
296
+ security: operationSecurity.length > 0 ? operationSecurity : undefined
297
+ };
298
+ paths[path][methodLower] = operation;
299
+ }
300
+ }
301
+ return {
302
+ openapi: '3.1.0',
303
+ info: {
304
+ title,
305
+ version,
306
+ description
307
+ },
308
+ servers: options.servers || [{ url: '/', description: 'Current Environment' }],
309
+ paths,
310
+ components: {
311
+ schemas: {
312
+ ProblemDetails: PROBLEM_DETAILS_SCHEMA
313
+ },
314
+ securitySchemes: usesSecurity ? securitySchemes : undefined
315
+ }
316
+ };
317
+ }
@@ -0,0 +1,7 @@
1
+ import type { RequestHandler } from '@sveltejs/kit';
2
+ import type { ApiDocsOptions } from './types';
3
+ /**
4
+ * Creates a turnkey SvelteKit RequestHandler that generates an interactive OpenAPI 3.1 documentation
5
+ * page (Scalar, Swagger UI, or Redoc) or returns raw OpenAPI 3.1 JSON when ?format=json is requested.
6
+ */
7
+ export declare function createApiDocsHandler(options?: ApiDocsOptions): RequestHandler;