yedra 0.20.11 → 0.20.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/routing/rest.d.ts +2 -0
  2. package/dist/routing/rest.js +2 -0
  3. package/dist/routing/websocket.d.ts +1 -0
  4. package/dist/routing/websocket.js +1 -0
  5. package/dist/src/index.d.ts +9 -0
  6. package/dist/src/index.js +7 -0
  7. package/dist/src/lib.d.ts +10 -0
  8. package/dist/src/lib.js +16 -0
  9. package/dist/src/routing/app.d.ts +165 -0
  10. package/dist/src/routing/app.js +537 -0
  11. package/dist/src/routing/env.d.ts +4 -0
  12. package/dist/src/routing/env.js +13 -0
  13. package/dist/src/routing/errors.d.ts +50 -0
  14. package/dist/src/routing/errors.js +64 -0
  15. package/dist/src/routing/log.d.ts +22 -0
  16. package/dist/src/routing/log.js +30 -0
  17. package/dist/src/routing/path.d.ts +44 -0
  18. package/dist/src/routing/path.js +110 -0
  19. package/dist/src/routing/rest.d.ts +103 -0
  20. package/dist/src/routing/rest.js +181 -0
  21. package/dist/src/routing/websocket.d.ts +60 -0
  22. package/dist/src/routing/websocket.js +132 -0
  23. package/dist/src/schema-lib.d.ts +17 -0
  24. package/dist/src/schema-lib.js +20 -0
  25. package/dist/src/schema.d.ts +2 -0
  26. package/dist/src/schema.js +4 -0
  27. package/dist/src/util/counter.d.ts +7 -0
  28. package/dist/src/util/counter.js +24 -0
  29. package/dist/src/util/docs.d.ts +9 -0
  30. package/dist/src/util/docs.js +57 -0
  31. package/dist/src/util/security.d.ts +14 -0
  32. package/dist/src/util/security.js +6 -0
  33. package/dist/src/util/stream.d.ts +2 -0
  34. package/dist/src/util/stream.js +7 -0
  35. package/dist/src/validation/body.d.ts +46 -0
  36. package/dist/src/validation/body.js +15 -0
  37. package/dist/src/validation/boolean.d.ts +10 -0
  38. package/dist/src/validation/boolean.js +27 -0
  39. package/dist/src/validation/date.d.ts +11 -0
  40. package/dist/src/validation/date.js +29 -0
  41. package/dist/src/validation/doc.d.ts +10 -0
  42. package/dist/src/validation/doc.js +22 -0
  43. package/dist/src/validation/either.d.ts +15 -0
  44. package/dist/src/validation/either.js +38 -0
  45. package/dist/src/validation/enum.d.ts +19 -0
  46. package/dist/src/validation/enum.js +44 -0
  47. package/dist/src/validation/error.d.ts +21 -0
  48. package/dist/src/validation/error.js +32 -0
  49. package/dist/src/validation/integer.d.ts +23 -0
  50. package/dist/src/validation/integer.js +64 -0
  51. package/dist/src/validation/json.d.ts +12 -0
  52. package/dist/src/validation/json.js +33 -0
  53. package/dist/src/validation/lazy.d.ts +48 -0
  54. package/dist/src/validation/lazy.js +78 -0
  55. package/dist/src/validation/modifiable.d.ts +105 -0
  56. package/dist/src/validation/modifiable.js +223 -0
  57. package/dist/src/validation/none.d.ts +10 -0
  58. package/dist/src/validation/none.js +14 -0
  59. package/dist/src/validation/null.d.ts +10 -0
  60. package/dist/src/validation/null.js +21 -0
  61. package/dist/src/validation/number.d.ts +23 -0
  62. package/dist/src/validation/number.js +58 -0
  63. package/dist/src/validation/object.d.ts +38 -0
  64. package/dist/src/validation/object.js +87 -0
  65. package/dist/src/validation/raw.d.ts +13 -0
  66. package/dist/src/validation/raw.js +21 -0
  67. package/dist/src/validation/record.d.ts +16 -0
  68. package/dist/src/validation/record.js +51 -0
  69. package/dist/src/validation/schema.d.ts +35 -0
  70. package/dist/src/validation/schema.js +76 -0
  71. package/dist/src/validation/stream.d.ts +13 -0
  72. package/dist/src/validation/stream.js +26 -0
  73. package/dist/src/validation/string.d.ts +29 -0
  74. package/dist/src/validation/string.js +51 -0
  75. package/dist/src/validation/union.d.ts +16 -0
  76. package/dist/src/validation/union.js +36 -0
  77. package/dist/src/validation/unknown.d.ts +10 -0
  78. package/dist/src/validation/unknown.js +13 -0
  79. package/dist/src/validation/uuid.d.ts +11 -0
  80. package/dist/src/validation/uuid.js +23 -0
  81. package/package.json +3 -4
@@ -0,0 +1,537 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readdir, readFile, stat } from 'node:fs/promises';
3
+ import { createServer as createHttpServer } from 'node:http';
4
+ import { createServer as createHttpsServer } from 'node:https';
5
+ import { extname, join } from 'node:path';
6
+ import { URL } from 'node:url';
7
+ import { isUint8Array } from 'node:util/types';
8
+ import { context, propagation, SpanKind, trace } from '@opentelemetry/api';
9
+ import mime from 'mime';
10
+ import { WebSocketServer } from 'ws';
11
+ import { Counter } from '../util/counter.js';
12
+ import { collectLazySchemas } from '../validation/lazy.js';
13
+ import { HttpError } from './errors.js';
14
+ import { Path } from './path.js';
15
+ import { RestEndpoint } from './rest.js';
16
+ import { WsEndpoint } from './websocket.js';
17
+ class Context {
18
+ constructor(server, wss, counter, docs) {
19
+ this.server = server;
20
+ this.wss = wss;
21
+ this.counter = counter;
22
+ this.docs = docs;
23
+ }
24
+ stop() {
25
+ return new Promise((resolve) => {
26
+ // don't accept any new connections
27
+ this.wss.close();
28
+ this.server.close();
29
+ for (const client of this.wss.clients) {
30
+ // send shutdown message to all WebSocket clients
31
+ client.close(1000, 'Server Shutdown');
32
+ }
33
+ // wait until all connections are done
34
+ this.counter.wait().then(() => {
35
+ resolve();
36
+ });
37
+ });
38
+ }
39
+ }
40
+ class BuiltApp {
41
+ constructor(options) {
42
+ this.restRoutes = [];
43
+ this.wsRoutes = [];
44
+ this.requestData = {};
45
+ this.serveData = options.serveData;
46
+ this.docs = options.docs;
47
+ this.generatedDocs = JSON.stringify(options.docs);
48
+ this.connectMiddlewares = options.connectMiddlewares;
49
+ this.quiet = options.quiet;
50
+ this.restRoutes = options.restRoutes;
51
+ this.wsRoutes = options.wsRoutes;
52
+ }
53
+ static middlewareNext(req, res, middlewares, last) {
54
+ if (middlewares.length > 0) {
55
+ // apply the next middleware
56
+ middlewares[0](req, res, () => BuiltApp.middlewareNext(req, res, middlewares.slice(1), last));
57
+ }
58
+ else {
59
+ // no middlewares left, invoke yedra
60
+ last();
61
+ }
62
+ }
63
+ handle(req, res) {
64
+ // first, invoke the middleware chain
65
+ BuiltApp.middlewareNext(req, res, this.connectMiddlewares, async () => {
66
+ // this will be called after all middlewares are done
67
+ const url = new URL(req.url, 'http://localhost');
68
+ const begin = Date.now();
69
+ const response = await this.performRequest({
70
+ method: req.method ?? 'GET',
71
+ url,
72
+ body: req,
73
+ headers: req.headers,
74
+ });
75
+ const status = response.status ?? 200;
76
+ if (response.body instanceof ReadableStream) {
77
+ res.writeHead(status, response.headers);
78
+ try {
79
+ for await (const chunk of response.body) {
80
+ const canContinue = res.write(chunk);
81
+ if (!canContinue) {
82
+ // Buffer full - wait for drain before continuing
83
+ await new Promise((resolve) => res.once('drain', resolve));
84
+ }
85
+ }
86
+ }
87
+ catch (_error) {
88
+ res.destroy();
89
+ }
90
+ }
91
+ else if (response.body instanceof Uint8Array) {
92
+ res.writeHead(status, response.headers);
93
+ res.write(response.body);
94
+ }
95
+ else {
96
+ res.writeHead(status, {
97
+ 'content-type': 'application/json',
98
+ ...response.headers,
99
+ });
100
+ res.write(JSON.stringify(response.body));
101
+ }
102
+ res.end();
103
+ const duration = Date.now() - begin;
104
+ if (this.quiet !== true) {
105
+ console.log(`${req.method} ${url.pathname} -> ${status} (${duration}ms)`);
106
+ }
107
+ this.track(req.method, status, duration / 1000);
108
+ });
109
+ }
110
+ async performRequest(req) {
111
+ if (req.method !== 'GET' &&
112
+ req.method !== 'POST' &&
113
+ req.method !== 'PUT' &&
114
+ req.method !== 'PATCH' &&
115
+ req.method !== 'DELETE') {
116
+ return BuiltApp.errorResponse(405, `Method \`${req.method}\` not allowed.`);
117
+ }
118
+ if (req.method === 'GET' && req.url.pathname === '/openapi.json') {
119
+ if (this.generatedDocs === undefined) {
120
+ console.error('Docs were not generated correctly.');
121
+ return BuiltApp.errorResponse(500, 'Internal Server Error');
122
+ }
123
+ return {
124
+ status: 200,
125
+ body: Buffer.from(this.generatedDocs ?? '{}', 'utf-8'),
126
+ headers: {
127
+ 'content-type': 'application/json',
128
+ },
129
+ };
130
+ }
131
+ const match = this.matchRestRoute(req.url.pathname, req.method);
132
+ if (!match.result) {
133
+ // no matching route found
134
+ if (req.method === 'GET') {
135
+ // look for a static file
136
+ const staticFile = this.serveData.files.get(req.url.pathname) ??
137
+ this.serveData.files.get('__fallback');
138
+ if (staticFile !== undefined) {
139
+ const ifNoneMatch = req.headers['if-none-match'];
140
+ const clientEtag = Array.isArray(ifNoneMatch)
141
+ ? ifNoneMatch[0]
142
+ : ifNoneMatch;
143
+ if (clientEtag === staticFile.etag) {
144
+ return {
145
+ status: 304,
146
+ body: Buffer.alloc(0),
147
+ headers: {
148
+ etag: staticFile.etag,
149
+ 'cache-control': staticFile.cacheControl,
150
+ },
151
+ };
152
+ }
153
+ return {
154
+ status: 200,
155
+ body: staticFile.data,
156
+ headers: {
157
+ 'content-type': staticFile.mime,
158
+ etag: staticFile.etag,
159
+ 'cache-control': staticFile.cacheControl,
160
+ },
161
+ };
162
+ }
163
+ if (this.serveData.fallback !== undefined) {
164
+ try {
165
+ const response = await this.serveData.fallback({
166
+ href: req.url.href,
167
+ });
168
+ return {
169
+ status: response.status ?? 200,
170
+ body: isUint8Array(response.body)
171
+ ? response.body
172
+ : Buffer.from(response.body, 'utf-8'),
173
+ headers: response.headers,
174
+ };
175
+ }
176
+ catch (error) {
177
+ if (error instanceof HttpError) {
178
+ return BuiltApp.errorResponse(error.status, error.message, error.code);
179
+ }
180
+ console.error(error);
181
+ return BuiltApp.errorResponse(500, 'Internal Server Error.');
182
+ }
183
+ }
184
+ }
185
+ if (match.invalidMethod) {
186
+ // we found a route, but it did not match the request method
187
+ return BuiltApp.errorResponse(405, `Method ${req.method} not allowed for path \`${req.url.pathname}\`.`);
188
+ }
189
+ return BuiltApp.errorResponse(404, `Path \`${req.url.pathname}\` not found.`);
190
+ }
191
+ try {
192
+ return await match.result.endpoint.handle({
193
+ url: req.url.pathname,
194
+ body: req.body,
195
+ params: match.result.params,
196
+ query: Object.fromEntries(req.url.searchParams),
197
+ headers: Object.fromEntries(Object.entries(req.headers).map(([key, value]) => [
198
+ key,
199
+ Array.isArray(value) ? value.join(',') : (value ?? ''),
200
+ ])),
201
+ });
202
+ }
203
+ catch (error) {
204
+ if (error instanceof HttpError) {
205
+ return BuiltApp.errorResponse(error.status, error.message, error.code);
206
+ }
207
+ console.error(error);
208
+ return BuiltApp.errorResponse(500, 'Internal Server Error.');
209
+ }
210
+ }
211
+ static errorResponse(status, errorMessage, code) {
212
+ const defaultCodes = new Map([
213
+ [400, 'bad_request'],
214
+ [401, 'unauthorized'],
215
+ [402, 'payment_required'],
216
+ [403, 'forbidden'],
217
+ [404, 'not_found'],
218
+ [405, 'method_not_allowed'],
219
+ [409, 'conflict'],
220
+ [500, 'internal_server_error'],
221
+ ]);
222
+ return {
223
+ status,
224
+ body: {
225
+ status,
226
+ errorMessage,
227
+ code: code ?? defaultCodes.get(status) ?? 'unknown_error',
228
+ },
229
+ };
230
+ }
231
+ matchRestRoute(url, method) {
232
+ let invalidMethod = false;
233
+ let result;
234
+ for (const route of this.restRoutes) {
235
+ const match = route.path.match(url);
236
+ if (match === undefined) {
237
+ continue;
238
+ }
239
+ const { params, score } = match;
240
+ if (route.endpoint.method !== method) {
241
+ invalidMethod = true;
242
+ continue;
243
+ }
244
+ const previous = result?.score;
245
+ if (previous === undefined || score < previous) {
246
+ // if there was no previous match or this one is better, use it
247
+ result = { endpoint: route.endpoint, params, score };
248
+ }
249
+ }
250
+ return { invalidMethod, result };
251
+ }
252
+ track(method, status, duration) {
253
+ const id = `${method}-${status}`;
254
+ const data = this.requestData[id] ?? { count: 0, duration: 0 };
255
+ this.requestData[id] = {
256
+ count: data.count + 1,
257
+ duration: data.duration + duration,
258
+ };
259
+ }
260
+ metrics() {
261
+ return Object.entries(this.requestData)
262
+ .map(([key, data]) => {
263
+ const [method, status] = key.split('-');
264
+ return `yedra_requests_total{method="${method}",status="${status}"} ${data?.count}
265
+ yedra_request_duration_sum{method="${method}",status="${status}"} ${data?.duration}
266
+ `;
267
+ })
268
+ .join('');
269
+ }
270
+ listen(port, options) {
271
+ const server = options?.tls === undefined
272
+ ? createHttpServer()
273
+ : createHttpsServer({
274
+ key: options.tls.key,
275
+ cert: options.tls.cert,
276
+ });
277
+ const counter = new Counter();
278
+ server.on('request', (req, res) => {
279
+ counter.increment();
280
+ const extractedContext = propagation.extract(context.active(), req.headers);
281
+ context.with(extractedContext, () => trace.getTracer('yedra').startActiveSpan('incoming_request', {
282
+ kind: SpanKind.SERVER,
283
+ }, (span) => {
284
+ this.handle(req, res);
285
+ res.on('close', () => {
286
+ span.setAttribute('http.method', req.method ?? 'UNKNOWN');
287
+ span.setAttribute('http.url', req.url ?? 'UNKNOWN');
288
+ span.setAttribute('http.status_code', res.statusCode);
289
+ span.end();
290
+ counter.decrement();
291
+ });
292
+ }));
293
+ });
294
+ const wss = new WebSocketServer({ server });
295
+ wss.on('connection', (ws, req) => {
296
+ const extractedContext = propagation.extract(context.active(), req.headers);
297
+ context.with(extractedContext, () => trace.getTracer('yedra').startActiveSpan('incoming_ws_connection', {
298
+ kind: SpanKind.SERVER,
299
+ }, async (span) => {
300
+ span.setAttribute('http.url', req.url ?? 'UNKNOWN');
301
+ let ended = false;
302
+ const end = () => {
303
+ if (ended) {
304
+ return;
305
+ }
306
+ ended = true;
307
+ span.end();
308
+ };
309
+ ws.once('close', end);
310
+ const url = new URL(req.url, 'http://localhost');
311
+ const { result } = this.matchWsRoute(url.pathname);
312
+ if (!result) {
313
+ ws.close(4404);
314
+ end();
315
+ return;
316
+ }
317
+ try {
318
+ const headers = Object.fromEntries(Object.entries(req.headers).map(([key, value]) => [
319
+ key,
320
+ Array.isArray(value) ? value.join(',') : (value ?? ''),
321
+ ]));
322
+ await result.endpoint.handle(url, result.params, headers, ws);
323
+ }
324
+ catch (error) {
325
+ if (error instanceof HttpError) {
326
+ ws.close(4000 + error.status, error.message);
327
+ }
328
+ else {
329
+ console.error(error);
330
+ ws.close(1011, 'Internal Error');
331
+ }
332
+ }
333
+ }));
334
+ });
335
+ server.listen(port, () => {
336
+ if (this.quiet !== true) {
337
+ console.log(`yedra listening on http://localhost:${port}`);
338
+ }
339
+ });
340
+ if (options?.metrics !== undefined) {
341
+ const metricsEndpoint = options.metrics;
342
+ const metricsServer = createHttpServer();
343
+ metricsServer.on('request', async (req, res) => {
344
+ if (req.method === 'GET' && req.url === metricsEndpoint.path) {
345
+ res.writeHead(200, {
346
+ 'content-type': 'text/plain; version=0.0.4; charset=utf-8; escaping=underscores',
347
+ });
348
+ res.write(this.metrics());
349
+ if (metricsEndpoint.get !== undefined) {
350
+ res.write(await metricsEndpoint.get());
351
+ }
352
+ res.end();
353
+ }
354
+ else {
355
+ res.writeHead(404);
356
+ res.end('Not found');
357
+ }
358
+ });
359
+ metricsServer.listen(metricsEndpoint.port, () => {
360
+ if (this.quiet !== true) {
361
+ console.log(`yedra metrics on http://localhost:${metricsEndpoint.port}${metricsEndpoint.path}`);
362
+ }
363
+ });
364
+ }
365
+ return new Context(server, wss, counter, this.docs);
366
+ }
367
+ matchWsRoute(url) {
368
+ let result;
369
+ for (const route of this.wsRoutes) {
370
+ const match = route.path.match(url);
371
+ if (match === undefined) {
372
+ continue;
373
+ }
374
+ const { params, score } = match;
375
+ const previous = result?.score;
376
+ if (previous === undefined || score < previous) {
377
+ // if there was no previous match or this one is better, use it
378
+ result = { endpoint: route.endpoint, params, score };
379
+ }
380
+ }
381
+ return { result };
382
+ }
383
+ }
384
+ export class Yedra {
385
+ constructor() {
386
+ this.restRoutes = [];
387
+ this.wsRoutes = [];
388
+ }
389
+ use(path, endpoint) {
390
+ if (endpoint instanceof Yedra) {
391
+ for (const route of endpoint.restRoutes) {
392
+ const newPath = route.path.withPrefix(path);
393
+ this.restRoutes.push({
394
+ path: newPath,
395
+ endpoint: route.endpoint,
396
+ });
397
+ }
398
+ for (const route of endpoint.wsRoutes) {
399
+ const newPath = route.path.withPrefix(path);
400
+ this.wsRoutes.push({ path: newPath, endpoint: route.endpoint });
401
+ }
402
+ }
403
+ else if (endpoint instanceof RestEndpoint) {
404
+ this.restRoutes.push({ path: new Path(path), endpoint });
405
+ }
406
+ else if (endpoint instanceof WsEndpoint) {
407
+ this.wsRoutes.push({ path: new Path(path), endpoint });
408
+ }
409
+ else {
410
+ throw new Error('Invalid endpoint argument.');
411
+ }
412
+ return this;
413
+ }
414
+ generateDocs(options) {
415
+ // this set will be filled with the security schemes from all endpoints
416
+ const securitySchemes = new Set();
417
+ // Wrap path generation in collectLazySchemas so that any LazySchema
418
+ // whose documentation() is called will register its full definition.
419
+ const { result: paths, schemas } = collectLazySchemas(() => {
420
+ const paths = {};
421
+ for (const route of this.restRoutes) {
422
+ if (route.endpoint.isHidden()) {
423
+ // do not include hidden endpoints in the documentation
424
+ continue;
425
+ }
426
+ const path = route.path.toString();
427
+ const methods = paths[path] ?? {};
428
+ methods[route.endpoint.method.toLowerCase()] =
429
+ route.endpoint.documentation(path, securitySchemes);
430
+ paths[path] = methods;
431
+ }
432
+ return paths;
433
+ });
434
+ return {
435
+ openapi: '3.0.2',
436
+ info: {
437
+ title: options?.title ?? 'Yedra API',
438
+ description: options?.description ??
439
+ 'This is an OpenAPI documentation generated automatically by Yedra.',
440
+ version: options?.version ?? '0.1.0',
441
+ },
442
+ components: {
443
+ securitySchemes: Object.fromEntries(securitySchemes
444
+ .values()
445
+ .map((scheme) => [scheme.name, scheme.scheme])),
446
+ schemas: Object.fromEntries(schemas),
447
+ },
448
+ servers: options?.servers ?? [],
449
+ paths,
450
+ };
451
+ }
452
+ static async loadServe(config) {
453
+ if (config === undefined) {
454
+ return {
455
+ files: new Map(),
456
+ fallback: undefined,
457
+ };
458
+ }
459
+ const staticFiles = new Map();
460
+ let files;
461
+ try {
462
+ files = await readdir(config.dir, { recursive: true });
463
+ }
464
+ catch (_error) {
465
+ files = [];
466
+ }
467
+ const revalidate = 'public, max-age=0, must-revalidate';
468
+ await Promise.all(files.map(async (file) => {
469
+ const absolute = join(config.dir, file);
470
+ if (!(await stat(absolute)).isFile()) {
471
+ return;
472
+ }
473
+ const data = await readFile(absolute);
474
+ const urlPath = `/${file}`;
475
+ const cacheControl = config.immutable !== undefined && config.immutable.pattern.test(file)
476
+ ? `public, max-age=${config.immutable.maxAge}, immutable`
477
+ : revalidate;
478
+ staticFiles.set(urlPath, {
479
+ data,
480
+ mime: mime.getType(extname(file)) ?? 'application/octet-stream',
481
+ etag: `"${createHash('sha1').update(data).digest('hex')}"`,
482
+ cacheControl,
483
+ });
484
+ }));
485
+ if (config.fallback) {
486
+ if (typeof config.fallback === 'string') {
487
+ const data = await readFile(config.fallback);
488
+ staticFiles.set('__fallback', {
489
+ data,
490
+ mime: mime.getType(extname(config.fallback)) ??
491
+ 'application/octet-stream',
492
+ etag: `"${createHash('sha1').update(data).digest('hex')}"`,
493
+ cacheControl: revalidate,
494
+ });
495
+ return {
496
+ files: staticFiles,
497
+ fallback: undefined,
498
+ };
499
+ }
500
+ return {
501
+ files: staticFiles,
502
+ fallback: config.fallback,
503
+ };
504
+ }
505
+ return {
506
+ files: staticFiles,
507
+ fallback: undefined,
508
+ };
509
+ }
510
+ async build(options) {
511
+ const serveData = await Yedra.loadServe(options?.serve);
512
+ const docs = this.generateDocs(options?.docs);
513
+ return new BuiltApp({
514
+ serveData,
515
+ docs,
516
+ connectMiddlewares: options?.connectMiddlewares ?? [],
517
+ quiet: options?.quiet ?? false,
518
+ restRoutes: this.restRoutes,
519
+ wsRoutes: this.wsRoutes,
520
+ });
521
+ }
522
+ async listen(port, options) {
523
+ const app = await this.build({
524
+ docs: options?.docs,
525
+ serve: options?.serve,
526
+ quiet: options?.quiet,
527
+ connectMiddlewares: options?.connectMiddlewares,
528
+ });
529
+ return app.listen(port, options);
530
+ }
531
+ }
532
+ /**
533
+ * TODO: how do we add OpenTelemetry instrumentation here?
534
+ * We need two things:
535
+ * 1. Start active span for each incoming request, and end after response is sent.
536
+ * 2. Extract context from incoming requests to propagate traces.
537
+ */
@@ -0,0 +1,4 @@
1
+ import type { Typeof } from '../validation/body.js';
2
+ import { type ObjectSchema } from '../validation/object.js';
3
+ import type { Schema } from '../validation/schema.js';
4
+ export declare const parseEnv: <T extends Record<string, Schema<unknown>>>(shape: T) => Typeof<ObjectSchema<T>>;
@@ -0,0 +1,13 @@
1
+ import { ValidationError } from '../validation/error.js';
2
+ import { laxObject } from '../validation/object.js';
3
+ export const parseEnv = (shape) => {
4
+ try {
5
+ return laxObject(shape).parse(process.env);
6
+ }
7
+ catch (error) {
8
+ if (error instanceof ValidationError) {
9
+ console.error(`error: env validation failed: ${error.format()}`);
10
+ }
11
+ process.exit(1);
12
+ }
13
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Base class for errors that will be handled as HTTP status codes.
3
+ */
4
+ export declare class HttpError extends Error {
5
+ readonly status: number;
6
+ readonly code: string | undefined;
7
+ constructor(status: number, message: string, code?: string);
8
+ }
9
+ /**
10
+ * Indicates a malformed request.
11
+ * Corresponds to HTTP status code 400 Bad Request.
12
+ */
13
+ export declare class BadRequestError extends HttpError {
14
+ constructor(message: string, code?: string);
15
+ }
16
+ /**
17
+ * Indicates missing or invalid credentials.
18
+ * Corresponds to HTTP status code 401 Unauthorized.
19
+ */
20
+ export declare class UnauthorizedError extends HttpError {
21
+ constructor(message: string, code?: string);
22
+ }
23
+ /**
24
+ * Indicates that some kind of payment is required.
25
+ * Corresponds to HTTP status code 402 Payment Required.
26
+ */
27
+ export declare class PaymentRequiredError extends HttpError {
28
+ constructor(message: string, code?: string);
29
+ }
30
+ /**
31
+ * Indicates that the user is not allowed to do something.
32
+ * Corresponds to HTTP status code 403 Forbidden.
33
+ */
34
+ export declare class ForbiddenError extends HttpError {
35
+ constructor(message: string, code?: string);
36
+ }
37
+ /**
38
+ * Indicates that the requested resource does not exist.
39
+ * Corresponds to HTTP status code 404 Not Found.
40
+ */
41
+ export declare class NotFoundError extends HttpError {
42
+ constructor(message: string, code?: string);
43
+ }
44
+ /**
45
+ * Indicates that the action conflicts with the current state.
46
+ * Corresponds to HTTP status code 409 Conflict.
47
+ */
48
+ export declare class ConflictError extends HttpError {
49
+ constructor(message: string, code?: string);
50
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Base class for errors that will be handled as HTTP status codes.
3
+ */
4
+ export class HttpError extends Error {
5
+ constructor(status, message, code) {
6
+ super(message);
7
+ this.status = status;
8
+ this.code = code;
9
+ }
10
+ }
11
+ /**
12
+ * Indicates a malformed request.
13
+ * Corresponds to HTTP status code 400 Bad Request.
14
+ */
15
+ export class BadRequestError extends HttpError {
16
+ constructor(message, code) {
17
+ super(400, message, code);
18
+ }
19
+ }
20
+ /**
21
+ * Indicates missing or invalid credentials.
22
+ * Corresponds to HTTP status code 401 Unauthorized.
23
+ */
24
+ export class UnauthorizedError extends HttpError {
25
+ constructor(message, code) {
26
+ super(401, message, code);
27
+ }
28
+ }
29
+ /**
30
+ * Indicates that some kind of payment is required.
31
+ * Corresponds to HTTP status code 402 Payment Required.
32
+ */
33
+ export class PaymentRequiredError extends HttpError {
34
+ constructor(message, code) {
35
+ super(402, message, code);
36
+ }
37
+ }
38
+ /**
39
+ * Indicates that the user is not allowed to do something.
40
+ * Corresponds to HTTP status code 403 Forbidden.
41
+ */
42
+ export class ForbiddenError extends HttpError {
43
+ constructor(message, code) {
44
+ super(403, message, code);
45
+ }
46
+ }
47
+ /**
48
+ * Indicates that the requested resource does not exist.
49
+ * Corresponds to HTTP status code 404 Not Found.
50
+ */
51
+ export class NotFoundError extends HttpError {
52
+ constructor(message, code) {
53
+ super(404, message, code);
54
+ }
55
+ }
56
+ /**
57
+ * Indicates that the action conflicts with the current state.
58
+ * Corresponds to HTTP status code 409 Conflict.
59
+ */
60
+ export class ConflictError extends HttpError {
61
+ constructor(message, code) {
62
+ super(409, message, code);
63
+ }
64
+ }
@@ -0,0 +1,22 @@
1
+ export declare class Log {
2
+ /**
3
+ * Output a debug message.
4
+ * @param args - The message.
5
+ */
6
+ debug(...args: unknown[]): void;
7
+ /**
8
+ * Output an informational message.
9
+ * @param args - The message.
10
+ */
11
+ info(...args: unknown[]): void;
12
+ /**
13
+ * Output a warning message.
14
+ * @param args - The message.
15
+ */
16
+ warning(...args: unknown[]): void;
17
+ /**
18
+ * Output an error message.
19
+ * @param args - The message.
20
+ */
21
+ error(...args: unknown[]): void;
22
+ }