@alxia/core 0.1.0 → 0.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.
Files changed (59) hide show
  1. package/README.md +103 -3
  2. package/dist/app/alxia.d.ts +53 -5
  3. package/dist/app/alxia.d.ts.map +1 -1
  4. package/dist/app/chain.d.ts +5 -8
  5. package/dist/app/chain.d.ts.map +1 -1
  6. package/dist/app/context.d.ts +12 -0
  7. package/dist/app/context.d.ts.map +1 -0
  8. package/dist/app/definition.d.ts +22 -1
  9. package/dist/app/definition.d.ts.map +1 -1
  10. package/dist/app/refusal.d.ts +15 -0
  11. package/dist/app/refusal.d.ts.map +1 -0
  12. package/dist/app/send.d.ts +8 -1
  13. package/dist/app/send.d.ts.map +1 -1
  14. package/dist/app/socket.d.ts +1 -1
  15. package/dist/app/socket.d.ts.map +1 -1
  16. package/dist/app/types/index.d.ts +1 -0
  17. package/dist/app/types/index.d.ts.map +1 -1
  18. package/dist/app/types/refusal.d.ts +94 -0
  19. package/dist/app/types/refusal.d.ts.map +1 -0
  20. package/dist/app/types/route-table.d.ts +3 -2
  21. package/dist/app/types/route-table.d.ts.map +1 -1
  22. package/dist/app/types/schema.d.ts +7 -0
  23. package/dist/app/types/schema.d.ts.map +1 -1
  24. package/dist/app/types/valid-schema.d.ts +1 -1
  25. package/dist/app/types/valid-schema.d.ts.map +1 -1
  26. package/dist/errors/errors.d.ts +47 -1
  27. package/dist/errors/errors.d.ts.map +1 -1
  28. package/dist/index.d.ts +5 -3
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +395 -97
  31. package/dist/index.js.map +20 -13
  32. package/dist/reply/problem.d.ts +33 -0
  33. package/dist/reply/problem.d.ts.map +1 -0
  34. package/dist/reply/reply.d.ts.map +1 -1
  35. package/dist/request/limit.d.ts +13 -0
  36. package/dist/request/limit.d.ts.map +1 -0
  37. package/dist/request/read.d.ts +3 -2
  38. package/dist/request/read.d.ts.map +1 -1
  39. package/dist/sse/async-iterable.d.ts +6 -0
  40. package/dist/sse/async-iterable.d.ts.map +1 -0
  41. package/dist/sse/event-stream.d.ts +29 -6
  42. package/dist/sse/event-stream.d.ts.map +1 -1
  43. package/dist/sse/frame.d.ts +35 -0
  44. package/dist/sse/frame.d.ts.map +1 -0
  45. package/dist/sse/named-events.d.ts +70 -0
  46. package/dist/sse/named-events.d.ts.map +1 -0
  47. package/docs/README.md +3 -3
  48. package/docs/guide/groups-and-plugins.md +8 -1
  49. package/docs/guide/hooks.md +128 -3
  50. package/docs/guide/replies.md +40 -0
  51. package/docs/guide/routes.md +115 -1
  52. package/docs/guide/server-sent-events.md +177 -5
  53. package/docs/guide/serving.md +1 -1
  54. package/docs/guide/types.md +3 -1
  55. package/docs/guide/websockets.md +1 -1
  56. package/docs/guide/writing-a-plugin.md +1 -1
  57. package/docs/roadmap.md +35 -2
  58. package/docs/troubleshooting.md +303 -6
  59. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,3 +1,78 @@
1
+ // src/errors/errors.ts
2
+ class HttpError extends Error {
3
+ name = "HttpError";
4
+ status;
5
+ body;
6
+ constructor(status, body, message) {
7
+ super(message ?? `HTTP ${status}`);
8
+ this.status = status;
9
+ this.body = body;
10
+ }
11
+ }
12
+
13
+ class ContentTooLargeError extends HttpError {
14
+ name = "ContentTooLargeError";
15
+ limit;
16
+ constructor(limit) {
17
+ super(413, { error: "content_too_large", limit }, `The request body is larger than the route's limit of ${limit} bytes`);
18
+ this.limit = limit;
19
+ }
20
+ }
21
+
22
+ class ResponseValidationError extends Error {
23
+ name = "ResponseValidationError";
24
+ status;
25
+ issues;
26
+ constructor(method, path, status, issues, message) {
27
+ super(message ?? `${method} ${path}: the ${status} reply does not match its schema: ${issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ")}`);
28
+ this.status = status;
29
+ this.issues = issues;
30
+ }
31
+ static undeclared(method, path, status) {
32
+ return new ResponseValidationError(method, path, status, [
33
+ {
34
+ target: "body",
35
+ path: [],
36
+ code: "undeclared_status",
37
+ message: `the route declares no ${status} reply`
38
+ }
39
+ ], `${method} ${path} declares no ${status} reply`);
40
+ }
41
+ }
42
+
43
+ // src/request/limit.ts
44
+ function limitBody(request, limit) {
45
+ const source = request.body;
46
+ if (source === null || request.bodyUsed || source.locked)
47
+ return request;
48
+ const declared = Number(request.headers.get("content-length") ?? Number.NaN);
49
+ let body;
50
+ if (declared > limit) {
51
+ source.cancel().catch(() => {});
52
+ body = new ReadableStream({
53
+ start: (controller) => controller.error(new ContentTooLargeError(limit))
54
+ });
55
+ } else {
56
+ let count = 0;
57
+ body = source.pipeThrough(new TransformStream({
58
+ transform(chunk, controller) {
59
+ count += chunk.byteLength;
60
+ if (count > limit) {
61
+ controller.error(new ContentTooLargeError(limit));
62
+ } else
63
+ controller.enqueue(chunk);
64
+ }
65
+ }));
66
+ }
67
+ return new Request(request, { body, duplex: "half" });
68
+ }
69
+ function checkLimit(limit, where) {
70
+ if (!Number.isSafeInteger(limit) || limit < 0) {
71
+ throw new TypeError(`${where}: bodyLimit must be a whole number of bytes, 0 or more; got ${limit}`);
72
+ }
73
+ return limit;
74
+ }
75
+
1
76
  // src/router/compile.ts
2
77
  var LITERAL = 0;
3
78
  var PARAMETER = 1;
@@ -233,6 +308,11 @@ class Router {
233
308
  // src/static/serve.ts
234
309
  import { extname as extname2, resolve, sep } from "node:path";
235
310
 
311
+ // src/sse/async-iterable.ts
312
+ function isAsyncIterable(value) {
313
+ return value !== null && typeof value === "object" && Symbol.asyncIterator in value && !(value instanceof ReadableStream);
314
+ }
315
+
236
316
  // src/schema/standard-schema.ts
237
317
  async function check(schema, value, target) {
238
318
  let result = schema["~standard"].validate(value);
@@ -254,8 +334,143 @@ async function check(schema, value, target) {
254
334
  };
255
335
  }
256
336
 
337
+ // src/sse/frame.ts
338
+ class Frame {
339
+ event;
340
+ data;
341
+ id;
342
+ retry;
343
+ constructor(event, data, id, retry) {
344
+ this.event = event;
345
+ this.data = data;
346
+ this.id = id;
347
+ this.retry = retry;
348
+ }
349
+ }
350
+ function frameText(value) {
351
+ const lines = [];
352
+ let data = value;
353
+ if (value instanceof Frame) {
354
+ lines.push(`event: ${value.event}`);
355
+ if (value.id !== undefined)
356
+ lines.push(`id: ${value.id}`);
357
+ if (value.retry !== undefined)
358
+ lines.push(`retry: ${value.retry}`);
359
+ data = value.data;
360
+ }
361
+ const json = JSON.stringify(data) ?? "null";
362
+ for (const line of json.split(/\r\n|\r|\n/))
363
+ lines.push(`data: ${line}`);
364
+ return `${lines.join(`
365
+ `)}
366
+
367
+ `;
368
+ }
369
+ function mismatch(issues, name) {
370
+ return new TypeError(`An event does not match its schema: ${issues.map((issue) => {
371
+ const path = name === undefined ? issue.path : [name, ...issue.path];
372
+ return `${path.join(".") || "(root)"}: ${issue.message}`;
373
+ }).join("; ")}`);
374
+ }
375
+ var BREAK = /[\r\n\0]/;
376
+ function refuseName(name) {
377
+ if (name === "")
378
+ return "An event name must not be empty";
379
+ if (BREAK.test(name)) {
380
+ return `An event name must not hold a line break or a NUL: ${JSON.stringify(name)}`;
381
+ }
382
+ return;
383
+ }
384
+ function refuseId(id) {
385
+ if (id === undefined)
386
+ return;
387
+ if (typeof id !== "string")
388
+ return "An event id must be a string";
389
+ if (BREAK.test(id)) {
390
+ return `An event id must not hold a line break or a NUL: ${JSON.stringify(id)}`;
391
+ }
392
+ return;
393
+ }
394
+ function refuseRetry(retry) {
395
+ if (retry === undefined)
396
+ return;
397
+ if (typeof retry !== "number" || !Number.isSafeInteger(retry) || retry < 0) {
398
+ return `An event retry must be a whole number of milliseconds, 0 or more: ${String(retry)}`;
399
+ }
400
+ return;
401
+ }
402
+
403
+ // src/sse/named-events.ts
404
+ function namedEventStream(events) {
405
+ const names = Object.keys(events);
406
+ if (names.length === 0) {
407
+ throw new TypeError("A named event stream declares at least one event");
408
+ }
409
+ for (const name of names) {
410
+ const refused = refuseName(name);
411
+ if (refused !== undefined)
412
+ throw new TypeError(refused);
413
+ const schema = events[name];
414
+ if (schema === null || typeof schema !== "object" || !("~standard" in schema)) {
415
+ throw new TypeError(`The event ${JSON.stringify(name)} is not a Standard Schema`);
416
+ }
417
+ }
418
+ return {
419
+ "~events": events,
420
+ event: (event, data, fields) => ({
421
+ ...fields,
422
+ event,
423
+ data
424
+ }),
425
+ "~standard": {
426
+ version: 1,
427
+ vendor: "alxia",
428
+ validate: (value) => {
429
+ if (!isAsyncIterable(value)) {
430
+ return {
431
+ issues: [
432
+ { message: "An event stream replies with an async iterable" }
433
+ ]
434
+ };
435
+ }
436
+ return { value: toFrames(events, value, true) };
437
+ }
438
+ }
439
+ };
440
+ }
441
+ function isNamedEventStreamSchema(schema) {
442
+ return "~events" in schema;
443
+ }
444
+ async function* toFrames(events, values, validate) {
445
+ for await (const value of values) {
446
+ if (value === null || typeof value !== "object" || !("event" in value)) {
447
+ throw new TypeError("An event of a named stream is an object { event, data }");
448
+ }
449
+ const { event, data, id, retry } = value;
450
+ const schema = typeof event === "string" && Object.hasOwn(events, event) ? events[event] : undefined;
451
+ if (schema === undefined) {
452
+ throw new TypeError(`The event ${JSON.stringify(event)} is not declared: ${Object.keys(events).join(", ")}`);
453
+ }
454
+ const refused = refuseId(id) ?? refuseRetry(retry);
455
+ if (refused !== undefined)
456
+ throw new TypeError(refused);
457
+ let output = data;
458
+ if (validate) {
459
+ const checked = await check(schema, data, "body");
460
+ if (!checked.ok) {
461
+ throw mismatch(checked.issues, event);
462
+ }
463
+ output = checked.value;
464
+ }
465
+ yield new Frame(event, output, id, retry);
466
+ }
467
+ }
468
+
257
469
  // src/sse/event-stream.ts
258
- function eventStream(item) {
470
+ function eventStream(schema) {
471
+ if (!isStandardSchema(schema))
472
+ return namedEventStream(schema);
473
+ const item = schema;
259
474
  return {
260
475
  "~eventStream": item,
261
476
  "~standard": {
@@ -277,14 +492,14 @@ function eventStream(item) {
277
492
  async function* checkEach(item, values) {
278
493
  for await (const value of values) {
279
494
  const checked = await check(item, value, "body");
280
- if (!checked.ok) {
281
- throw new TypeError(`An event does not match its schema: ${checked.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ")}`);
282
- }
495
+ if (!checked.ok)
496
+ throw mismatch(checked.issues);
283
497
  yield checked.value;
284
498
  }
285
499
  }
286
- function isAsyncIterable(value) {
287
- return value !== null && typeof value === "object" && Symbol.asyncIterator in value && !(value instanceof ReadableStream);
500
+ function isStandardSchema(value) {
501
+ const standard = value["~standard"];
502
+ return standard?.version === 1 && typeof standard.vendor === "string";
288
503
  }
289
504
  function isEventStreamSchema(schema) {
290
505
  return "~eventStream" in schema;
@@ -327,12 +542,7 @@ function toEventStream(values, signal) {
327
542
  controller.close();
328
543
  return;
329
544
  }
330
- const data = JSON.stringify(next.value) ?? "null";
331
- controller.enqueue(encoder.encode(`${data.split(`
332
- `).map((line) => `data: ${line}`).join(`
333
- `)}
334
-
335
- `));
545
+ controller.enqueue(encoder.encode(frameText(next.value)));
336
546
  } catch (error) {
337
547
  stop();
338
548
  console.error(error);
@@ -661,39 +871,6 @@ function isBunFile(blob) {
661
871
  return typeof blob.exists === "function";
662
872
  }
663
873
 
664
- // src/errors/errors.ts
665
- class HttpError extends Error {
666
- name = "HttpError";
667
- status;
668
- body;
669
- constructor(status, body, message) {
670
- super(message ?? `HTTP ${status}`);
671
- this.status = status;
672
- this.body = body;
673
- }
674
- }
675
-
676
- class ResponseValidationError extends Error {
677
- name = "ResponseValidationError";
678
- status;
679
- issues;
680
- constructor(method, path, status, issues, message) {
681
- super(message ?? `${method} ${path}: the ${status} reply does not match its schema: ${issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ")}`);
682
- this.status = status;
683
- this.issues = issues;
684
- }
685
- static undeclared(method, path, status) {
686
- return new ResponseValidationError(method, path, status, [
687
- {
688
- target: "body",
689
- path: [],
690
- code: "undeclared_status",
691
- message: `the route declares no ${status} reply`
692
- }
693
- ], `${method} ${path} declares no ${status} reply`);
694
- }
695
- }
696
-
697
874
  // src/request/read.ts
698
875
  function readQuery(url) {
699
876
  const query = {};
@@ -730,6 +907,8 @@ async function readBody(request, parsers = []) {
730
907
  try {
731
908
  return { ok: true, value: await custom.parse(request) };
732
909
  } catch (error) {
910
+ if (error instanceof ContentTooLargeError)
911
+ throw error;
733
912
  return {
734
913
  ok: false,
735
914
  issue: {
@@ -780,6 +959,33 @@ async function readBody(request, parsers = []) {
780
959
  return { ok: true, value: await request.arrayBuffer() };
781
960
  }
782
961
 
962
+ // src/app/context.ts
963
+ var redirect = (location, status) => new Reply(status ?? 302, undefined, {
964
+ headers: { location: String(location) }
965
+ });
966
+ function routeContext(definition, request, pathParams) {
967
+ let cookies;
968
+ const set = {
969
+ headers: new Headers,
970
+ get cookies() {
971
+ cookies ??= new Bun.CookieMap;
972
+ return cookies;
973
+ },
974
+ touched: () => cookies !== undefined
975
+ };
976
+ const limit = "bodyLimit" in definition ? definition.bodyLimit : undefined;
977
+ const ctx = {
978
+ ...request,
979
+ ...limit === undefined ? {} : { request: limitBody(request.request, limit) },
980
+ route: definition.path,
981
+ pathParams,
982
+ set,
983
+ reply: createReply,
984
+ redirect
985
+ };
986
+ return { ctx, set };
987
+ }
988
+
783
989
  // src/app/send.ts
784
990
  var internal = { error: "internal" };
785
991
  function internalError() {
@@ -810,17 +1016,23 @@ async function sendDeclared(route, reply, set, signal, validateResponses) {
810
1016
  if (responses === undefined || isRedirect(reply)) {
811
1017
  return send2(reply, set, signal);
812
1018
  }
1019
+ const checked = await checkReply(route.method, route.path, responses, reply, validateResponses);
1020
+ return send2(checked, set, signal);
1021
+ }
1022
+ async function checkReply(method, path, responses, reply, validateResponses) {
813
1023
  const schema = responses[reply.status];
814
1024
  if (schema === undefined) {
815
- throw ResponseValidationError.undeclared(route.method, route.path, reply.status);
1025
+ throw ResponseValidationError.undeclared(method, path, reply.status);
816
1026
  }
817
1027
  if (!validateResponses)
818
- return send2(reply, set, signal);
1028
+ return framed(schema, reply);
819
1029
  const checked = await check(schema, reply.body, "body");
820
1030
  if (!checked.ok) {
821
- throw new ResponseValidationError(route.method, route.path, reply.status, checked.issues);
1031
+ throw new ResponseValidationError(method, path, reply.status, checked.issues);
822
1032
  }
823
- return send2(new Reply(reply.status, checked.value, { headers: reply.headers ?? {} }), set, signal);
1033
+ return new Reply(reply.status, checked.value, {
1034
+ headers: reply.headers ?? {}
1035
+ });
824
1036
  }
825
1037
  function routingError(status, error, allowed) {
826
1038
  const headers = new Headers;
@@ -829,42 +1041,73 @@ function routingError(status, error, allowed) {
829
1041
  const body = { error };
830
1042
  return toResponse(status, body, headers);
831
1043
  }
1044
+ function framed(schema, reply) {
1045
+ if (!isNamedEventStreamSchema(schema) || !isAsyncIterable(reply.body)) {
1046
+ return reply;
1047
+ }
1048
+ return new Reply(reply.status, toFrames(schema["~events"], reply.body, false), { headers: reply.headers ?? {} });
1049
+ }
832
1050
  function isRedirect(reply) {
833
1051
  return reply.status >= 300 && reply.status < 400 && reply.body === undefined;
834
1052
  }
835
1053
 
836
- // src/app/chain.ts
837
- var redirect = (location, status) => new Reply(status ?? 302, undefined, {
838
- headers: { location: String(location) }
839
- });
840
- function routeContext(definition, request, pathParams) {
841
- let cookies;
842
- const set = {
843
- headers: new Headers,
844
- get cookies() {
845
- cookies ??= new Bun.CookieMap;
846
- return cookies;
847
- },
848
- touched: () => cookies !== undefined
849
- };
850
- const ctx = {
851
- ...request,
852
- route: definition.path,
853
- pathParams,
854
- set,
855
- reply: createReply,
856
- redirect
857
- };
858
- return { ctx, set };
1054
+ // src/app/refusal.ts
1055
+ async function refuse(definition, refusal, set, ctx, validateResponses) {
1056
+ const handler = definition.refusal;
1057
+ if (handler !== undefined) {
1058
+ let reply = handler.hook(refusal, ctx);
1059
+ if (reply instanceof Promise)
1060
+ reply = await reply;
1061
+ const method = "method" in definition ? definition.method : "WS";
1062
+ if (reply instanceof Reply) {
1063
+ if (handler.response !== undefined) {
1064
+ reply = await checkReply(method, definition.path, handler.response, reply, validateResponses);
1065
+ }
1066
+ return send2(withContentType2(reply, handler.contentType), set);
1067
+ }
1068
+ if (reply !== undefined) {
1069
+ throw new TypeError(`${method} ${definition.path}: the onRefusal hook returned neither a reply nor nothing.`);
1070
+ }
1071
+ }
1072
+ return send2(byDefault(refusal), set);
1073
+ }
1074
+ function byDefault(refusal) {
1075
+ switch (refusal.kind) {
1076
+ case "validation": {
1077
+ const body = {
1078
+ error: "validation",
1079
+ issues: refusal.issues
1080
+ };
1081
+ return new Reply(400, body);
1082
+ }
1083
+ case "body_limit": {
1084
+ const body = {
1085
+ error: "content_too_large",
1086
+ limit: refusal.limit
1087
+ };
1088
+ return new Reply(413, body);
1089
+ }
1090
+ }
1091
+ }
1092
+ function withContentType2(reply, type) {
1093
+ if (type === undefined)
1094
+ return reply;
1095
+ const headers = new Headers(reply.headers);
1096
+ if (headers.has("content-type"))
1097
+ return reply;
1098
+ headers.set("content-type", type);
1099
+ return new Reply(reply.status, reply.body, { headers });
859
1100
  }
860
- async function chain(definition, request, rawParams, set, ctx, parsers, last) {
1101
+
1102
+ // src/app/chain.ts
1103
+ async function chain(definition, request, rawParams, set, ctx, parsers, validateResponses, last) {
861
1104
  const hooks = definition.derive;
862
1105
  const socket = !("method" in definition);
863
1106
  const signal = request.request.signal;
864
1107
  const step = async (index) => {
865
1108
  const hook = hooks[index];
866
1109
  if (hook === undefined) {
867
- const refused = await validate(definition, request, rawParams, set, ctx, parsers);
1110
+ const refused = await validate(definition, request, rawParams, set, ctx, parsers, validateResponses);
868
1111
  return refused ?? last();
869
1112
  }
870
1113
  if (hook.kind === "wrap") {
@@ -887,9 +1130,10 @@ async function chain(definition, request, rawParams, set, ctx, parsers, last) {
887
1130
  };
888
1131
  return step(0);
889
1132
  }
890
- async function validate(definition, request, rawParams, set, ctx, parsers) {
1133
+ async function validate(definition, request, rawParams, set, ctx, parsers, validateResponses) {
891
1134
  const { schema } = definition;
892
1135
  const issues = [];
1136
+ let part;
893
1137
  const parts = [
894
1138
  ["params", schema.params, () => rawParams],
895
1139
  ["query", schema.query, () => readQuery(request.url)],
@@ -905,33 +1149,36 @@ async function validate(definition, request, rawParams, set, ctx, parsers) {
905
1149
  const checked = await check(partSchema, raw, target);
906
1150
  if (checked.ok)
907
1151
  ctx[target] = checked.value;
908
- else
1152
+ else {
1153
+ part ??= target;
909
1154
  issues.push(...checked.issues);
1155
+ }
910
1156
  }
911
1157
  ctx["body"] = undefined;
912
1158
  const bodySchema = "body" in schema ? schema.body : undefined;
913
1159
  if (bodySchema !== undefined) {
914
- const body = await readBody(request.request, parsers);
915
- if (!body.ok)
1160
+ const body = await readBody(ctx.request, parsers);
1161
+ if (!body.ok) {
1162
+ part ??= "body";
916
1163
  issues.push(body.issue);
917
- else {
1164
+ } else {
918
1165
  const checked = await check(bodySchema, body.value, "body");
919
1166
  if (checked.ok)
920
1167
  ctx["body"] = checked.value;
921
- else
1168
+ else {
1169
+ part ??= "body";
922
1170
  issues.push(...checked.issues);
1171
+ }
923
1172
  }
924
1173
  }
925
- if (issues.length > 0) {
926
- const body = { error: "validation", issues };
927
- return send2(new Reply(400, body), set);
928
- }
929
- return;
1174
+ if (part === undefined)
1175
+ return;
1176
+ return refuse(definition, { kind: "validation", part, issues }, set, ctx, validateResponses);
930
1177
  }
931
1178
  async function handle(route, request, rawParams, parsers, validateResponses) {
932
1179
  const { ctx, set } = routeContext(route, request, rawParams);
933
1180
  try {
934
- return await chain(route, request, rawParams, set, ctx, parsers, async () => {
1181
+ return await chain(route, request, rawParams, set, ctx, parsers, validateResponses, async () => {
935
1182
  let reply = route.handler(ctx);
936
1183
  if (reply instanceof Promise)
937
1184
  reply = await reply;
@@ -942,10 +1189,18 @@ async function handle(route, request, rawParams, parsers, validateResponses) {
942
1189
  });
943
1190
  } catch (error) {
944
1191
  request.error = error;
945
- return fail(route, error, ctx);
1192
+ return fail(route, error, ctx, validateResponses);
946
1193
  }
947
1194
  }
948
- async function fail(definition, error, ctx) {
1195
+ async function fail(definition, error, ctx, validateResponses = true) {
1196
+ if (error instanceof ContentTooLargeError) {
1197
+ try {
1198
+ return await refuse(definition, { kind: "body_limit", limit: error.limit }, ctx.set, ctx, validateResponses);
1199
+ } catch (thrown) {
1200
+ console.error(thrown);
1201
+ return internalError();
1202
+ }
1203
+ }
949
1204
  for (const hook of definition.onError) {
950
1205
  let handled = hook(error, ctx);
951
1206
  if (handled instanceof Promise)
@@ -962,11 +1217,11 @@ async function fail(definition, error, ctx) {
962
1217
 
963
1218
  // src/app/socket.ts
964
1219
  var UPGRADED = Symbol("upgraded");
965
- async function upgradeSocket(definition, request, rawParams, parsers) {
1220
+ async function upgradeSocket(definition, request, rawParams, parsers, validateResponses) {
966
1221
  const { ctx, set } = routeContext(definition, request, rawParams);
967
1222
  const server = request.server;
968
1223
  try {
969
- return await chain(definition, request, rawParams, set, ctx, parsers, async () => {
1224
+ return await chain(definition, request, rawParams, set, ctx, parsers, validateResponses, async () => {
970
1225
  if (server === undefined) {
971
1226
  return routingError(426, "upgrade_required");
972
1227
  }
@@ -1158,7 +1413,7 @@ async function route(runtime, ctx, path) {
1158
1413
  const socket = find("WS");
1159
1414
  if (socket !== undefined && "value" in socket && socket.value.kind === "ws") {
1160
1415
  ctx.route = socket.value.path;
1161
- return upgradeSocket(socket.value, ctx, socket.params, globals.parsers);
1416
+ return upgradeSocket(socket.value, ctx, socket.params, globals.parsers, runtime.validateResponses);
1162
1417
  }
1163
1418
  }
1164
1419
  let match = find(request.method);
@@ -1197,6 +1452,8 @@ class Alxia {
1197
1452
  #sockets = [];
1198
1453
  #derive = [];
1199
1454
  #onError = [];
1455
+ #refusal;
1456
+ #bodyLimit;
1200
1457
  #server;
1201
1458
  #websocket;
1202
1459
  constructor(options = {}) {
@@ -1253,7 +1510,8 @@ class Alxia {
1253
1510
  schema,
1254
1511
  handlers,
1255
1512
  derive: [...this.#derive],
1256
- onError: [...this.#onError]
1513
+ onError: [...this.#onError],
1514
+ refusal: this.#refusal
1257
1515
  };
1258
1516
  this.#mount(definition);
1259
1517
  return this;
@@ -1270,10 +1528,29 @@ class Alxia {
1270
1528
  this.#derive.push({ kind: "wrap", run: hook });
1271
1529
  return this;
1272
1530
  }
1531
+ bodyLimit(bytes) {
1532
+ this.#bodyLimit = checkLimit(bytes, "bodyLimit()");
1533
+ return this;
1534
+ }
1273
1535
  onError(hook) {
1274
1536
  this.#onError.push(hook);
1275
1537
  return this;
1276
1538
  }
1539
+ onRefusal(schemaOrHook, maybeHook) {
1540
+ if (typeof schemaOrHook === "function") {
1541
+ this.#refusal = { hook: schemaOrHook };
1542
+ return this;
1543
+ }
1544
+ if (typeof maybeHook !== "function") {
1545
+ throw new TypeError("onRefusal(): the hook is missing");
1546
+ }
1547
+ this.#refusal = {
1548
+ hook: maybeHook,
1549
+ response: schemaOrHook.response,
1550
+ ...schemaOrHook.contentType === undefined ? {} : { contentType: schemaOrHook.contentType }
1551
+ };
1552
+ return this;
1553
+ }
1277
1554
  onRequest(hook) {
1278
1555
  this.#runtime.globals.onRequest.push(hook);
1279
1556
  return this;
@@ -1308,6 +1585,8 @@ class Alxia {
1308
1585
  });
1309
1586
  child.#derive = [...this.#derive];
1310
1587
  child.#onError = [...this.#onError];
1588
+ child.#refusal = this.#refusal;
1589
+ child.#bodyLimit = this.#bodyLimit;
1311
1590
  child.#runtime = { ...child.#runtime, globals: this.#runtime.globals };
1312
1591
  const before = new Set(this.#runtime.globals.pages.keys());
1313
1592
  const built = build(child);
@@ -1330,7 +1609,8 @@ class Alxia {
1330
1609
  ...route,
1331
1610
  path: joinPath(this.#prefix, route.path),
1332
1611
  derive: [...this.#derive, ...route.derive],
1333
- onError: [...route.onError, ...this.#onError]
1612
+ onError: [...route.onError, ...this.#onError],
1613
+ refusal: route.refusal ?? this.#refusal
1334
1614
  });
1335
1615
  }
1336
1616
  for (const socket of plugin.sockets) {
@@ -1338,11 +1618,14 @@ class Alxia {
1338
1618
  ...socket,
1339
1619
  path: joinPath(this.#prefix, socket.path),
1340
1620
  derive: [...this.#derive, ...socket.derive],
1341
- onError: [...socket.onError, ...this.#onError]
1621
+ onError: [...socket.onError, ...this.#onError],
1622
+ refusal: socket.refusal ?? this.#refusal
1342
1623
  });
1343
1624
  }
1344
1625
  this.#derive = [...this.#derive, ...plugin.#derive];
1345
1626
  this.#onError = [...plugin.#onError, ...this.#onError];
1627
+ this.#refusal = plugin.#refusal ?? this.#refusal;
1628
+ this.#bodyLimit = plugin.#bodyLimit ?? this.#bodyLimit;
1346
1629
  if (plugin.#runtime.globals !== this.#runtime.globals) {
1347
1630
  const globals = plugin.#runtime.globals;
1348
1631
  this.#runtime.globals.around.push(...globals.around);
@@ -1410,13 +1693,17 @@ class Alxia {
1410
1693
  if (typeof handler !== "function") {
1411
1694
  throw new TypeError(`${method} ${path}: the handler is missing`);
1412
1695
  }
1696
+ const full = joinPath(this.#prefix, path);
1697
+ const bodyLimit = schema.bodyLimit === undefined ? this.#bodyLimit : checkLimit(schema.bodyLimit, `${method} ${full}`);
1413
1698
  this.#register({
1414
1699
  method,
1415
- path: joinPath(this.#prefix, path),
1700
+ path: full,
1416
1701
  schema,
1702
+ ...bodyLimit === undefined ? {} : { bodyLimit },
1417
1703
  handler,
1418
1704
  derive: [...this.#derive],
1419
- onError: [...this.#onError]
1705
+ onError: [...this.#onError],
1706
+ refusal: this.#refusal
1420
1707
  });
1421
1708
  return this;
1422
1709
  };
@@ -1455,8 +1742,17 @@ function alxia(options = {}) {
1455
1742
  function definePlugin() {
1456
1743
  return (build) => build(alxia());
1457
1744
  }
1745
+ // src/reply/problem.ts
1746
+ function problem(body, init) {
1747
+ const headers = new Headers(init?.headers);
1748
+ if (!headers.has("content-type")) {
1749
+ headers.set("content-type", "application/problem+json");
1750
+ }
1751
+ return new Reply(body.status, body, { ...init, headers });
1752
+ }
1458
1753
  export {
1459
1754
  Alxia,
1755
+ ContentTooLargeError,
1460
1756
  HttpError,
1461
1757
  Reply,
1462
1758
  ResponseValidationError,
@@ -1466,12 +1762,14 @@ export {
1466
1762
  definePlugin,
1467
1763
  eventStream,
1468
1764
  isEventStreamSchema,
1765
+ isNamedEventStreamSchema,
1469
1766
  joinPath,
1470
1767
  parseRange,
1768
+ problem,
1471
1769
  shapeOf,
1472
1770
  vary,
1473
1771
  withHeaders
1474
1772
  };
1475
1773
 
1476
- //# debugId=F86708E1572B31A764756E2164756E21
1774
+ //# debugId=62C2FCF982C60F6964756E2164756E21
1477
1775
  //# sourceMappingURL=index.js.map