yedra 0.20.10 → 0.20.12

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/app.d.ts +12 -0
  2. package/dist/routing/app.js +64 -22
  3. package/dist/routing/rest.d.ts +2 -0
  4. package/dist/routing/rest.js +2 -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
@@ -15,6 +15,8 @@ declare class Context {
15
15
  type ServeFile = {
16
16
  data: Buffer;
17
17
  mime: string;
18
+ etag: string;
19
+ cacheControl: string;
18
20
  };
19
21
  type ServeResponse = {
20
22
  status?: number;
@@ -27,6 +29,16 @@ type ServeFallback = (req: {
27
29
  type ServeConfig = {
28
30
  dir: string;
29
31
  fallback?: string | ServeFallback;
32
+ /**
33
+ * Files matching `pattern` are served with `Cache-Control: public,
34
+ * max-age=<maxAge>, immutable`. Intended for content-addressed assets
35
+ * (e.g. `app.abc12345.js`) whose URL changes when the content changes.
36
+ * Never matches the fallback file.
37
+ */
38
+ immutable?: {
39
+ pattern: RegExp;
40
+ maxAge: number;
41
+ };
30
42
  };
31
43
  type ServeData = {
32
44
  files: Map<string, ServeFile>;
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { readdir, readFile, stat } from 'node:fs/promises';
2
3
  import { createServer as createHttpServer } from 'node:http';
3
4
  import { createServer as createHttpsServer } from 'node:https';
@@ -135,11 +136,27 @@ class BuiltApp {
135
136
  const staticFile = this.serveData.files.get(req.url.pathname) ??
136
137
  this.serveData.files.get('__fallback');
137
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
+ }
138
153
  return {
139
154
  status: 200,
140
155
  body: staticFile.data,
141
156
  headers: {
142
157
  'content-type': staticFile.mime,
158
+ etag: staticFile.etag,
159
+ 'cache-control': staticFile.cacheControl,
143
160
  },
144
161
  };
145
162
  }
@@ -275,29 +292,45 @@ yedra_request_duration_sum{method="${method}",status="${status}"} ${data?.durati
275
292
  }));
276
293
  });
277
294
  const wss = new WebSocketServer({ server });
278
- wss.on('connection', async (ws, req) => {
279
- const url = new URL(req.url, 'http://localhost');
280
- const { result } = this.matchWsRoute(url.pathname);
281
- if (!result) {
282
- ws.close(4404);
283
- return;
284
- }
285
- try {
286
- const headers = Object.fromEntries(Object.entries(req.headers).map(([key, value]) => [
287
- key,
288
- Array.isArray(value) ? value.join(',') : (value ?? ''),
289
- ]));
290
- await result.endpoint.handle(url, result.params, headers, ws);
291
- }
292
- catch (error) {
293
- if (error instanceof HttpError) {
294
- ws.close(4000 + error.status, error.message);
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;
295
316
  }
296
- else {
297
- console.error(error);
298
- ws.close(1011, 'Internal Error');
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);
299
323
  }
300
- }
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
+ }));
301
334
  });
302
335
  server.listen(port, () => {
303
336
  if (this.quiet !== true) {
@@ -431,15 +464,22 @@ export class Yedra {
431
464
  catch (_error) {
432
465
  files = [];
433
466
  }
467
+ const revalidate = 'public, max-age=0, must-revalidate';
434
468
  await Promise.all(files.map(async (file) => {
435
469
  const absolute = join(config.dir, file);
436
470
  if (!(await stat(absolute)).isFile()) {
437
471
  return;
438
472
  }
439
473
  const data = await readFile(absolute);
440
- staticFiles.set(`/${file}`, {
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, {
441
479
  data,
442
480
  mime: mime.getType(extname(file)) ?? 'application/octet-stream',
481
+ etag: `"${createHash('sha1').update(data).digest('hex')}"`,
482
+ cacheControl,
443
483
  });
444
484
  }));
445
485
  if (config.fallback) {
@@ -449,6 +489,8 @@ export class Yedra {
449
489
  data,
450
490
  mime: mime.getType(extname(config.fallback)) ??
451
491
  'application/octet-stream',
492
+ etag: `"${createHash('sha1').update(data).digest('hex')}"`,
493
+ cacheControl: revalidate,
452
494
  });
453
495
  return {
454
496
  files: staticFiles,
@@ -6,9 +6,11 @@ import { type ObjectSchema } from '../validation/object.js';
6
6
  import type { Schema } from '../validation/schema.js';
7
7
  type ReqObject<Params, Query, Headers, Body> = {
8
8
  url: string;
9
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
9
10
  params: Params;
10
11
  query: Query;
11
12
  headers: Headers;
13
+ rawHeaders: Record<string, string>;
12
14
  body: Body;
13
15
  };
14
16
  type ResObject<Body> = Promise<{
@@ -84,12 +84,14 @@ class ConcreteRestEndpoint extends RestEndpoint {
84
84
  }
85
85
  return await this.options.do({
86
86
  url: req.url,
87
+ method: this._method,
87
88
  // biome-ignore lint/style/noNonNullAssertion: this is required to convince TypeScript that this is initialized
88
89
  params: parsedParams,
89
90
  // biome-ignore lint/style/noNonNullAssertion: this is required to convince TypeScript that this is initialized
90
91
  query: parsedQuery,
91
92
  // biome-ignore lint/style/noNonNullAssertion: this is required to convince TypeScript that this is initialized
92
93
  headers: parsedHeaders,
94
+ rawHeaders: req.headers,
93
95
  // biome-ignore lint/style/noNonNullAssertion: this is required to convince TypeScript that this is initialized
94
96
  body: parsedBody,
95
97
  });
@@ -0,0 +1,9 @@
1
+ import * as y from './lib.js';
2
+ export { y };
3
+ export type { ConnectMiddleware } from './routing/app.js';
4
+ export { Yedra } from './routing/app.js';
5
+ export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotFoundError, PaymentRequiredError, UnauthorizedError, } from './routing/errors.js';
6
+ export { Delete, Get, Patch, Post, Put } from './routing/rest.js';
7
+ export type { YedraWebSocket } from './routing/websocket.js';
8
+ export { Ws } from './routing/websocket.js';
9
+ export { SecurityScheme } from './util/security.js';
@@ -0,0 +1,7 @@
1
+ import * as y from './lib.js';
2
+ export { y };
3
+ export { Yedra } from './routing/app.js';
4
+ export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotFoundError, PaymentRequiredError, UnauthorizedError, } from './routing/errors.js';
5
+ export { Delete, Get, Patch, Post, Put } from './routing/rest.js';
6
+ export { Ws } from './routing/websocket.js';
7
+ export { SecurityScheme } from './util/security.js';
@@ -0,0 +1,10 @@
1
+ export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotFoundError, PaymentRequiredError, UnauthorizedError, } from './routing/errors.js';
2
+ export { Log } from './routing/log.js';
3
+ export * from './schema-lib.js';
4
+ export declare const validatePath: (path: string) => void;
5
+ export { parseEnv } from './routing/env.js';
6
+ export { SecurityScheme } from './util/security.js';
7
+ export { either } from './validation/either.js';
8
+ export { json } from './validation/json.js';
9
+ export { raw } from './validation/raw.js';
10
+ export { stream } from './validation/stream.js';
@@ -0,0 +1,16 @@
1
+ import { Path } from './routing/path.js';
2
+ // routing
3
+ export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotFoundError, PaymentRequiredError, UnauthorizedError, } from './routing/errors.js';
4
+ export { Log } from './routing/log.js';
5
+ // validation (browser-safe, shared with yedra/schema)
6
+ export * from './schema-lib.js';
7
+ export const validatePath = (path) => {
8
+ new Path(path);
9
+ };
10
+ export { parseEnv } from './routing/env.js';
11
+ export { SecurityScheme } from './util/security.js';
12
+ // Node-only body types
13
+ export { either } from './validation/either.js';
14
+ export { json } from './validation/json.js';
15
+ export { raw } from './validation/raw.js';
16
+ export { stream } from './validation/stream.js';
@@ -0,0 +1,165 @@
1
+ import type { IncomingMessage, Server, ServerResponse } from 'node:http';
2
+ import { WebSocketServer } from 'ws';
3
+ import { Counter } from '../util/counter.js';
4
+ import { Path } from './path.js';
5
+ import { RestEndpoint } from './rest.js';
6
+ import { WsEndpoint } from './websocket.js';
7
+ declare class Context {
8
+ private readonly server;
9
+ private readonly wss;
10
+ private readonly counter;
11
+ readonly docs: object;
12
+ constructor(server: Server, wss: WebSocketServer, counter: Counter, docs: object);
13
+ stop(): Promise<void>;
14
+ }
15
+ type ServeFile = {
16
+ data: Buffer;
17
+ mime: string;
18
+ etag: string;
19
+ cacheControl: string;
20
+ };
21
+ type ServeResponse = {
22
+ status?: number;
23
+ body: Uint8Array | string;
24
+ headers?: Record<string, string>;
25
+ };
26
+ type ServeFallback = (req: {
27
+ href: string;
28
+ }) => ServeResponse | Promise<ServeResponse>;
29
+ type ServeConfig = {
30
+ dir: string;
31
+ fallback?: string | ServeFallback;
32
+ /**
33
+ * Files matching `pattern` are served with `Cache-Control: public,
34
+ * max-age=<maxAge>, immutable`. Intended for content-addressed assets
35
+ * (e.g. `app.abc12345.js`) whose URL changes when the content changes.
36
+ * Never matches the fallback file.
37
+ */
38
+ immutable?: {
39
+ pattern: RegExp;
40
+ maxAge: number;
41
+ };
42
+ };
43
+ type ServeData = {
44
+ files: Map<string, ServeFile>;
45
+ fallback: ServeFallback | undefined;
46
+ };
47
+ type DocsData = {
48
+ /**
49
+ * The title of your API.
50
+ */
51
+ title: string;
52
+ /**
53
+ * The description of your API.
54
+ */
55
+ description: string;
56
+ /**
57
+ * The current version of your API.
58
+ */
59
+ version: string;
60
+ /**
61
+ * The list of servers your API is reachable under.
62
+ */
63
+ servers?: {
64
+ description: string;
65
+ url: string;
66
+ }[];
67
+ };
68
+ export type ConnectMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
69
+ type RestRoute = {
70
+ path: Path;
71
+ endpoint: RestEndpoint;
72
+ };
73
+ type WsRoute = {
74
+ path: Path;
75
+ endpoint: WsEndpoint;
76
+ };
77
+ declare class BuiltApp {
78
+ private serveData;
79
+ private docs;
80
+ private generatedDocs;
81
+ private connectMiddlewares;
82
+ private quiet;
83
+ private restRoutes;
84
+ private wsRoutes;
85
+ private requestData;
86
+ constructor(options: {
87
+ serveData: ServeData;
88
+ docs: object;
89
+ connectMiddlewares: ConnectMiddleware[];
90
+ quiet: boolean;
91
+ restRoutes: RestRoute[];
92
+ wsRoutes: WsRoute[];
93
+ });
94
+ private static middlewareNext;
95
+ handle(req: IncomingMessage, res: ServerResponse): void;
96
+ private performRequest;
97
+ private static errorResponse;
98
+ private matchRestRoute;
99
+ private track;
100
+ metrics(): string;
101
+ listen(port: number, options?: {
102
+ tls?: {
103
+ key: string;
104
+ cert: string;
105
+ };
106
+ metrics?: {
107
+ port: number;
108
+ path: string;
109
+ get?: () => Promise<string> | string;
110
+ };
111
+ }): Context;
112
+ private matchWsRoute;
113
+ }
114
+ export declare class Yedra {
115
+ private restRoutes;
116
+ private wsRoutes;
117
+ use(path: string, endpoint: RestEndpoint | WsEndpoint | Yedra): Yedra;
118
+ private generateDocs;
119
+ private static loadServe;
120
+ build(options?: {
121
+ /**
122
+ * Configuration for the `/openapi.json` endpoint, which generates
123
+ * OpenAPI documentation.
124
+ */
125
+ docs?: DocsData;
126
+ quiet?: boolean;
127
+ serve?: ServeConfig;
128
+ /**
129
+ * Add Express/Connect compatible middleware.
130
+ */
131
+ connectMiddlewares?: ConnectMiddleware[];
132
+ }): Promise<BuiltApp>;
133
+ listen(port: number, options?: {
134
+ tls?: {
135
+ key: string;
136
+ cert: string;
137
+ };
138
+ metrics?: {
139
+ port: number;
140
+ path: string;
141
+ get?: () => Promise<string> | string;
142
+ };
143
+ /**
144
+ * Configuration for the `/openapi.json` endpoint, which generates
145
+ * OpenAPI documentation.
146
+ */
147
+ docs?: DocsData;
148
+ serve?: ServeConfig;
149
+ /**
150
+ * Prevents all normal output from Yedra. Mostly useful for tests.
151
+ */
152
+ quiet?: boolean;
153
+ /**
154
+ * Add Express/Connect compatible middleware.
155
+ */
156
+ connectMiddlewares?: ConnectMiddleware[];
157
+ }): Promise<Context>;
158
+ }
159
+ export {};
160
+ /**
161
+ * TODO: how do we add OpenTelemetry instrumentation here?
162
+ * We need two things:
163
+ * 1. Start active span for each incoming request, and end after response is sent.
164
+ * 2. Extract context from incoming requests to propagate traces.
165
+ */