@arkyn/server 3.0.9 → 3.0.10

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.
@@ -1,31 +1,31 @@
1
1
  import { flushDebugLogs as e } from "../../utilities/flushDebugLogs.js";
2
- import { LogService as t } from "../../services/logService.js";
3
- import { SENSITIVE_DATA_KEYS as n } from "../../utilities/sensitiveDataKeys.js";
2
+ import { SENSITIVE_DATA_KEYS as t } from "../../utilities/sensitiveDataKeys.js";
3
+ import { LogService as n } from "../../services/logService.js";
4
4
  import { formatJsonObject as r, parseSensitiveData as i } from "@arkyn/shared";
5
5
  //#region src/http/api/_logRequest.ts
6
6
  async function a(a) {
7
- let o = t.getConfig();
7
+ let o = n.getConfig();
8
8
  if (!o) return;
9
9
  let { userToken: s, apiUrl: c, trafficSourceId: l } = o;
10
10
  if (!c) return;
11
11
  let { elapsedTime: u, method: d, queryParams: f, requestBody: p, requestHeaders: m, responseBody: h, responseHeaders: g, status: _, rawUrl: v } = a;
12
12
  if (process.env.NODE_ENV !== "development") try {
13
- let t = new URL(v), a = "https";
14
- t.protocol === "http:" && (a = "http");
13
+ let n = new URL(v), a = "https";
14
+ n.protocol === "http:" && (a = "http");
15
15
  let o = JSON.stringify({
16
- domainUrl: `${t.protocol}//${t.host}`,
17
- pathnameUrl: t.pathname,
16
+ domainUrl: `${n.protocol}//${n.host}`,
17
+ pathnameUrl: n.pathname,
18
18
  trafficSourceId: l,
19
19
  status: _,
20
20
  protocol: a,
21
21
  method: d.toLowerCase(),
22
22
  trafficUserId: null,
23
23
  elapsedTime: u,
24
- requestHeaders: i(JSON.stringify(m), [...n]),
25
- requestBody: i(JSON.stringify(p), [...n]),
24
+ requestHeaders: i(JSON.stringify(m), [...t]),
25
+ requestBody: i(JSON.stringify(p), [...t]),
26
26
  queryParams: JSON.stringify(f),
27
- responseHeaders: i(JSON.stringify(g), [...n]),
28
- responseBody: i(JSON.stringify(h), [...n])
27
+ responseHeaders: i(JSON.stringify(g), [...t]),
28
+ responseBody: i(JSON.stringify(h), [...t])
29
29
  }), y = {
30
30
  "Content-Type": "application/json",
31
31
  Authorization: `Bearer ${s}`
@@ -8,6 +8,7 @@ var r = class {
8
8
  _status = 500;
9
9
  _statusText = "Unknown error";
10
10
  _debugColor = "red";
11
+ exposeCauseInProduction = !1;
11
12
  get cause() {
12
13
  return this._cause;
13
14
  }
@@ -52,10 +53,11 @@ var r = class {
52
53
  });
53
54
  }
54
55
  makeBody() {
56
+ let e = process.env.NODE_ENV === "production" && !this.exposeCauseInProduction;
55
57
  return {
56
58
  name: this._name,
57
59
  message: this._statusText,
58
- cause: this._cause
60
+ cause: e ? void 0 : this._cause
59
61
  };
60
62
  }
61
63
  };
@@ -1 +1 @@
1
- {"version":3,"file":"_badResponse.js","names":[],"sources":["../../../../src/http/badResponses/_badResponse.ts"],"sourcesContent":["import { formatJsonString } from \"@arkyn/shared\";\n\nimport { DebugService } from \"../../services/debugService\";\nimport { flushDebugLogs } from \"../../utilities/flushDebugLogs\";\n\nclass BadResponse {\n\t// biome-ignore lint/suspicious/noExplicitAny: intentional\n\tprivate _cause?: any;\n\tprivate _name: string = \"BadResponse\";\n\tprivate _status: number = 500;\n\tprivate _statusText: string = \"Unknown error\";\n\tprivate _debugColor: \"green\" | \"yellow\" | \"cyan\" | \"red\" = \"red\";\n\n\t// biome-ignore lint/suspicious/noExplicitAny: intentional\n\tget cause(): any {\n\t\treturn this._cause;\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: intentional\n\tset cause(value: any) {\n\t\tthis._cause = value;\n\t}\n\n\tget name(): string {\n\t\treturn this._name;\n\t}\n\n\tset name(value: string) {\n\t\tthis._name = value;\n\t}\n\n\tget status(): number {\n\t\treturn this._status;\n\t}\n\n\tset status(value: number) {\n\t\tthis._status = value;\n\t}\n\n\tget statusText(): string {\n\t\treturn this._statusText;\n\t}\n\n\tset statusText(value: string) {\n\t\tthis._statusText = value;\n\t}\n\n\tget debugColor(): \"green\" | \"yellow\" | \"cyan\" | \"red\" {\n\t\treturn this._debugColor;\n\t}\n\n\tset debugColor(value: \"green\" | \"yellow\" | \"cyan\" | \"red\") {\n\t\tif (![\"green\", \"yellow\", \"cyan\", \"red\"].includes(value)) return;\n\t\tthis._debugColor = value;\n\t}\n\n\tonDebug() {\n\t\tconst debugs: string[] = [];\n\t\tconst { callerInfo, functionName } = DebugService.getCaller();\n\n\t\tdebugs.push(`Caller Function: ${functionName}`);\n\t\tdebugs.push(`Caller Location: ${callerInfo}`);\n\n\t\tif (this._statusText) debugs.push(`Message: ${this._statusText}`);\n\t\tif (this._cause) {\n\t\t\tdebugs.push(`Cause: ${formatJsonString(JSON.stringify(this._cause))}`);\n\t\t}\n\n\t\tflushDebugLogs({ scheme: \"red\", name: this._name, debugs });\n\t}\n\n\tmakeBody() {\n\t\treturn {\n\t\t\tname: this._name,\n\t\t\tmessage: this._statusText,\n\t\t\tcause: this._cause,\n\t\t};\n\t}\n}\n\nexport { BadResponse };\n"],"mappings":";;;;AAKA,IAAM,IAAN,MAAkB;CAEjB;CACA,QAAwB;CACxB,UAA0B;CAC1B,cAA8B;CAC9B,cAA2D;CAG3D,IAAI,QAAa;EAChB,OAAO,KAAK;CACb;CAGA,IAAI,MAAM,GAAY;EACrB,KAAK,SAAS;CACf;CAEA,IAAI,OAAe;EAClB,OAAO,KAAK;CACb;CAEA,IAAI,KAAK,GAAe;EACvB,KAAK,QAAQ;CACd;CAEA,IAAI,SAAiB;EACpB,OAAO,KAAK;CACb;CAEA,IAAI,OAAO,GAAe;EACzB,KAAK,UAAU;CAChB;CAEA,IAAI,aAAqB;EACxB,OAAO,KAAK;CACb;CAEA,IAAI,WAAW,GAAe;EAC7B,KAAK,cAAc;CACpB;CAEA,IAAI,aAAkD;EACrD,OAAO,KAAK;CACb;CAEA,IAAI,WAAW,GAA4C;EACrD;GAAC;GAAS;GAAU;GAAQ;EAAK,CAAC,CAAC,SAAS,CAAK,MACtD,KAAK,cAAc;CACpB;CAEA,UAAU;EACT,IAAM,IAAmB,CAAC,GACpB,EAAE,eAAY,oBAAiB,EAAa,UAAU;EAU5D,AARA,EAAO,KAAK,oBAAoB,GAAc,GAC9C,EAAO,KAAK,oBAAoB,GAAY,GAExC,KAAK,eAAa,EAAO,KAAK,YAAY,KAAK,aAAa,GAC5D,KAAK,UACR,EAAO,KAAK,UAAU,EAAiB,KAAK,UAAU,KAAK,MAAM,CAAC,GAAG,GAGtE,EAAe;GAAE,QAAQ;GAAO,MAAM,KAAK;GAAO;EAAO,CAAC;CAC3D;CAEA,WAAW;EACV,OAAO;GACN,MAAM,KAAK;GACX,SAAS,KAAK;GACd,OAAO,KAAK;EACb;CACD;AACD"}
1
+ {"version":3,"file":"_badResponse.js","names":[],"sources":["../../../../src/http/badResponses/_badResponse.ts"],"sourcesContent":["import { formatJsonString } from \"@arkyn/shared\";\n\nimport { DebugService } from \"../../services/debugService\";\nimport { flushDebugLogs } from \"../../utilities/flushDebugLogs\";\n\nclass BadResponse {\n\t// biome-ignore lint/suspicious/noExplicitAny: intentional\n\tprivate _cause?: any;\n\tprivate _name: string = \"BadResponse\";\n\tprivate _status: number = 500;\n\tprivate _statusText: string = \"Unknown error\";\n\tprivate _debugColor: \"green\" | \"yellow\" | \"cyan\" | \"red\" = \"red\";\n\t/**\n\t * When `false` (the default), `makeBody()` omits `cause` in production so internal\n\t * details (stack traces, SQL, infra info) never reach the client. Subclasses whose\n\t * `cause` is safe, client-facing data (e.g. `UnprocessableEntity`'s form field errors)\n\t * set this to `true`.\n\t */\n\tprotected exposeCauseInProduction = false;\n\n\t// biome-ignore lint/suspicious/noExplicitAny: intentional\n\tget cause(): any {\n\t\treturn this._cause;\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: intentional\n\tset cause(value: any) {\n\t\tthis._cause = value;\n\t}\n\n\tget name(): string {\n\t\treturn this._name;\n\t}\n\n\tset name(value: string) {\n\t\tthis._name = value;\n\t}\n\n\tget status(): number {\n\t\treturn this._status;\n\t}\n\n\tset status(value: number) {\n\t\tthis._status = value;\n\t}\n\n\tget statusText(): string {\n\t\treturn this._statusText;\n\t}\n\n\tset statusText(value: string) {\n\t\tthis._statusText = value;\n\t}\n\n\tget debugColor(): \"green\" | \"yellow\" | \"cyan\" | \"red\" {\n\t\treturn this._debugColor;\n\t}\n\n\tset debugColor(value: \"green\" | \"yellow\" | \"cyan\" | \"red\") {\n\t\tif (![\"green\", \"yellow\", \"cyan\", \"red\"].includes(value)) return;\n\t\tthis._debugColor = value;\n\t}\n\n\tonDebug() {\n\t\tconst debugs: string[] = [];\n\t\tconst { callerInfo, functionName } = DebugService.getCaller();\n\n\t\tdebugs.push(`Caller Function: ${functionName}`);\n\t\tdebugs.push(`Caller Location: ${callerInfo}`);\n\n\t\tif (this._statusText) debugs.push(`Message: ${this._statusText}`);\n\t\tif (this._cause) {\n\t\t\tdebugs.push(`Cause: ${formatJsonString(JSON.stringify(this._cause))}`);\n\t\t}\n\n\t\tflushDebugLogs({ scheme: \"red\", name: this._name, debugs });\n\t}\n\n\tmakeBody() {\n\t\tconst isProduction = process.env.NODE_ENV === \"production\";\n\t\tconst hideCause = isProduction && !this.exposeCauseInProduction;\n\n\t\treturn {\n\t\t\tname: this._name,\n\t\t\tmessage: this._statusText,\n\t\t\tcause: hideCause ? undefined : this._cause,\n\t\t};\n\t}\n}\n\nexport { BadResponse };\n"],"mappings":";;;;AAKA,IAAM,IAAN,MAAkB;CAEjB;CACA,QAAwB;CACxB,UAA0B;CAC1B,cAA8B;CAC9B,cAA2D;CAO3D,0BAAoC;CAGpC,IAAI,QAAa;EAChB,OAAO,KAAK;CACb;CAGA,IAAI,MAAM,GAAY;EACrB,KAAK,SAAS;CACf;CAEA,IAAI,OAAe;EAClB,OAAO,KAAK;CACb;CAEA,IAAI,KAAK,GAAe;EACvB,KAAK,QAAQ;CACd;CAEA,IAAI,SAAiB;EACpB,OAAO,KAAK;CACb;CAEA,IAAI,OAAO,GAAe;EACzB,KAAK,UAAU;CAChB;CAEA,IAAI,aAAqB;EACxB,OAAO,KAAK;CACb;CAEA,IAAI,WAAW,GAAe;EAC7B,KAAK,cAAc;CACpB;CAEA,IAAI,aAAkD;EACrD,OAAO,KAAK;CACb;CAEA,IAAI,WAAW,GAA4C;EACrD;GAAC;GAAS;GAAU;GAAQ;EAAK,CAAC,CAAC,SAAS,CAAK,MACtD,KAAK,cAAc;CACpB;CAEA,UAAU;EACT,IAAM,IAAmB,CAAC,GACpB,EAAE,eAAY,oBAAiB,EAAa,UAAU;EAU5D,AARA,EAAO,KAAK,oBAAoB,GAAc,GAC9C,EAAO,KAAK,oBAAoB,GAAY,GAExC,KAAK,eAAa,EAAO,KAAK,YAAY,KAAK,aAAa,GAC5D,KAAK,UACR,EAAO,KAAK,UAAU,EAAiB,KAAK,UAAU,KAAK,MAAM,CAAC,GAAG,GAGtE,EAAe;GAAE,QAAQ;GAAO,MAAM,KAAK;GAAO;EAAO,CAAC;CAC3D;CAEA,WAAW;EAEV,IAAM,IAAA,QAAA,IAAA,aADwC,gBACZ,CAAC,KAAK;EAExC,OAAO;GACN,MAAM,KAAK;GACX,SAAS,KAAK;GACd,OAAO,IAAY,KAAA,IAAY,KAAK;EACrC;CACD;AACD"}
@@ -1,11 +1,21 @@
1
1
  import { BadResponse as e } from "./_badResponse.js";
2
+ import { SENSITIVE_DATA_KEYS as t } from "../../utilities/sensitiveDataKeys.js";
3
+ import { parseSensitiveData as n } from "@arkyn/shared";
2
4
  //#region src/http/badResponses/unprocessableEntity.ts
3
- var t = class extends e {
5
+ function r(e) {
6
+ if (e === void 0) return e;
7
+ try {
8
+ return JSON.parse(n(JSON.stringify(e), [...t]));
9
+ } catch {
10
+ return e;
11
+ }
12
+ }
13
+ var i = class extends e {
4
14
  constructor(e) {
5
- super(), this.name = "UnprocessableEntity", this.status = 422, this.statusText = e.message || "Unprocessable entity", this.debugColor = "yellow", this.cause = {
6
- data: e.data,
15
+ super(), this.name = "UnprocessableEntity", this.status = 422, this.statusText = e.message || "Unprocessable entity", this.debugColor = "yellow", this.exposeCauseInProduction = !0, this.cause = {
16
+ data: r(e.data),
7
17
  fieldErrors: e.fieldErrors,
8
- fields: e.fields
18
+ fields: r(e.fields)
9
19
  }, this.onDebug();
10
20
  }
11
21
  toResponse() {
@@ -25,6 +35,6 @@ var t = class extends e {
25
35
  }
26
36
  };
27
37
  //#endregion
28
- export { t as UnprocessableEntity };
38
+ export { i as UnprocessableEntity };
29
39
 
30
40
  //# sourceMappingURL=unprocessableEntity.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"unprocessableEntity.js","names":[],"sources":["../../../../src/http/badResponses/unprocessableEntity.ts"],"sourcesContent":["import { BadResponse } from \"./_badResponse\";\n\n/**\n * HTTP 422 Unprocessable Entity, the request is well-formed but contains semantic validation errors.\n * Typically used for form field validation failures.\n *\n * @example\n * ```typescript\n * throw new UnprocessableEntity({\n * message: \"Validation failed\",\n * fieldErrors: { email: \"Invalid email format\", age: \"Must be 18 or older\" },\n * fields: { email: \"not-an-email\", age: \"15\" },\n * });\n * ```\n */\nclass UnprocessableEntity extends BadResponse {\n\t/**\n\t * @param props.message - Human-readable description of the error.\n\t * @param props.fieldErrors - Per-field validation messages keyed by field name.\n\t * @param props.fields - Original submitted field values (useful for repopulating forms).\n\t * @param props.data - Any extra data to include in the response body.\n\t */\n\tconstructor(props: {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: intentional\n\t\tdata?: any;\n\t\tfieldErrors?: Record<string, string>;\n\t\tfields?: Record<string, string>;\n\t\tmessage?: string;\n\t}) {\n\t\tsuper();\n\n\t\tthis.name = \"UnprocessableEntity\";\n\t\tthis.status = 422;\n\t\tthis.statusText = props.message || \"Unprocessable entity\";\n\t\tthis.debugColor = \"yellow\";\n\t\tthis.cause = {\n\t\t\tdata: props.data,\n\t\t\tfieldErrors: props.fieldErrors,\n\t\t\tfields: props.fields,\n\t\t};\n\n\t\tthis.onDebug();\n\t}\n\n\t/** Converts to a `Response` with `Content-Type: application/json` header. */\n\ttoResponse(): Response {\n\t\tconst responseInit: ResponseInit = {\n\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\tstatus: this.status,\n\t\t\tstatusText: this.statusText,\n\t\t};\n\n\t\treturn new Response(JSON.stringify(this.makeBody()), responseInit);\n\t}\n\n\t/** Converts to a `Response` using `Response.json()`. Alternative to `toResponse()`. */\n\ttoJson(): Response {\n\t\tconst responseInit: ResponseInit = {\n\t\t\tstatus: this.status,\n\t\t\tstatusText: this.statusText,\n\t\t};\n\n\t\treturn Response.json(this.makeBody(), responseInit);\n\t}\n}\n\nexport { UnprocessableEntity };\n"],"mappings":";;AAeA,IAAM,IAAN,cAAkC,EAAY;CAO7C,YAAY,GAMT;EAaF,AAZA,MAAM,GAEN,KAAK,OAAO,uBACZ,KAAK,SAAS,KACd,KAAK,aAAa,EAAM,WAAW,wBACnC,KAAK,aAAa,UAClB,KAAK,QAAQ;GACZ,MAAM,EAAM;GACZ,aAAa,EAAM;GACnB,QAAQ,EAAM;EACf,GAEA,KAAK,QAAQ;CACd;CAGA,aAAuB;EACtB,IAAM,IAA6B;GAClC,SAAS,EAAE,gBAAgB,mBAAmB;GAC9C,QAAQ,KAAK;GACb,YAAY,KAAK;EAClB;EAEA,OAAO,IAAI,SAAS,KAAK,UAAU,KAAK,SAAS,CAAC,GAAG,CAAY;CAClE;CAGA,SAAmB;EAClB,IAAM,IAA6B;GAClC,QAAQ,KAAK;GACb,YAAY,KAAK;EAClB;EAEA,OAAO,SAAS,KAAK,KAAK,SAAS,GAAG,CAAY;CACnD;AACD"}
1
+ {"version":3,"file":"unprocessableEntity.js","names":[],"sources":["../../../../src/http/badResponses/unprocessableEntity.ts"],"sourcesContent":["import { parseSensitiveData } from \"@arkyn/shared\";\nimport { SENSITIVE_DATA_KEYS } from \"../../utilities/sensitiveDataKeys\";\nimport { BadResponse } from \"./_badResponse\";\n\n/**\n * Masks sensitive values (passwords, tokens, etc. — see `SENSITIVE_DATA_KEYS`) inside a\n * form-submission payload before it is echoed back to the client. `fields`/`data` on\n * `UnprocessableEntity` exist to repopulate a form, so submitted secrets must never be\n * returned in cleartext.\n */\nfunction redactSensitiveValues<T>(value: T): T {\n\tif (value === undefined) return value;\n\n\ttry {\n\t\treturn JSON.parse(\n\t\t\tparseSensitiveData(JSON.stringify(value), [...SENSITIVE_DATA_KEYS]),\n\t\t);\n\t} catch {\n\t\treturn value;\n\t}\n}\n\n/**\n * HTTP 422 Unprocessable Entity, the request is well-formed but contains semantic validation errors.\n * Typically used for form field validation failures.\n *\n * @example\n * ```typescript\n * throw new UnprocessableEntity({\n * message: \"Validation failed\",\n * fieldErrors: { email: \"Invalid email format\", age: \"Must be 18 or older\" },\n * fields: { email: \"not-an-email\", age: \"15\" },\n * });\n * ```\n */\nclass UnprocessableEntity extends BadResponse {\n\t/**\n\t * @param props.message - Human-readable description of the error.\n\t * @param props.fieldErrors - Per-field validation messages keyed by field name.\n\t * @param props.fields - Original submitted field values (useful for repopulating forms).\n\t * @param props.data - Any extra data to include in the response body.\n\t */\n\tconstructor(props: {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: intentional\n\t\tdata?: any;\n\t\tfieldErrors?: Record<string, string>;\n\t\tfields?: Record<string, string>;\n\t\tmessage?: string;\n\t}) {\n\t\tsuper();\n\n\t\tthis.name = \"UnprocessableEntity\";\n\t\tthis.status = 422;\n\t\tthis.statusText = props.message || \"Unprocessable entity\";\n\t\tthis.debugColor = \"yellow\";\n\t\t// `cause` here is structured, redacted form-validation data meant for the\n\t\t// client (to repopulate the form) — unlike other responses' `cause`, it stays\n\t\t// visible in production.\n\t\tthis.exposeCauseInProduction = true;\n\t\tthis.cause = {\n\t\t\tdata: redactSensitiveValues(props.data),\n\t\t\tfieldErrors: props.fieldErrors,\n\t\t\tfields: redactSensitiveValues(props.fields),\n\t\t};\n\n\t\tthis.onDebug();\n\t}\n\n\t/** Converts to a `Response` with `Content-Type: application/json` header. */\n\ttoResponse(): Response {\n\t\tconst responseInit: ResponseInit = {\n\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\tstatus: this.status,\n\t\t\tstatusText: this.statusText,\n\t\t};\n\n\t\treturn new Response(JSON.stringify(this.makeBody()), responseInit);\n\t}\n\n\t/** Converts to a `Response` using `Response.json()`. Alternative to `toResponse()`. */\n\ttoJson(): Response {\n\t\tconst responseInit: ResponseInit = {\n\t\t\tstatus: this.status,\n\t\t\tstatusText: this.statusText,\n\t\t};\n\n\t\treturn Response.json(this.makeBody(), responseInit);\n\t}\n}\n\nexport { UnprocessableEntity };\n"],"mappings":";;;;AAUA,SAAS,EAAyB,GAAa;CAC9C,IAAI,MAAU,KAAA,GAAW,OAAO;CAEhC,IAAI;EACH,OAAO,KAAK,MACX,EAAmB,KAAK,UAAU,CAAK,GAAG,CAAC,GAAG,CAAmB,CAAC,CACnE;CACD,QAAQ;EACP,OAAO;CACR;AACD;AAeA,IAAM,IAAN,cAAkC,EAAY;CAO7C,YAAY,GAMT;EAiBF,AAhBA,MAAM,GAEN,KAAK,OAAO,uBACZ,KAAK,SAAS,KACd,KAAK,aAAa,EAAM,WAAW,wBACnC,KAAK,aAAa,UAIlB,KAAK,0BAA0B,IAC/B,KAAK,QAAQ;GACZ,MAAM,EAAsB,EAAM,IAAI;GACtC,aAAa,EAAM;GACnB,QAAQ,EAAsB,EAAM,MAAM;EAC3C,GAEA,KAAK,QAAQ;CACd;CAGA,aAAuB;EACtB,IAAM,IAA6B;GAClC,SAAS,EAAE,gBAAgB,mBAAmB;GAC9C,QAAQ,KAAK;GACb,YAAY,KAAK;EAClB;EAEA,OAAO,IAAI,SAAS,KAAK,UAAU,KAAK,SAAS,CAAC,GAAG,CAAY;CAClE;CAGA,SAAmB;EAClB,IAAM,IAA6B;GAClC,QAAQ,KAAK;GACb,YAAY,KAAK;EAClB;EAEA,OAAO,SAAS,KAAK,KAAK,SAAS,GAAG,CAAY;CACnD;AACD"}
@@ -1,22 +1,27 @@
1
1
  import { BadRequest as e } from "../http/badResponses/badRequest.js";
2
2
  //#region src/utilities/decodeRequestBody.ts
3
- async function t(t) {
4
- let n, r = await t.arrayBuffer(), i = new TextDecoder().decode(r);
3
+ var t = 5 * 1024 * 1024;
4
+ async function n(n, r) {
5
+ let i, a = r?.maxBodySizeBytes ?? t, o = Number(n.headers.get("content-length"));
6
+ if (Number.isFinite(o) && o > a) throw new e("Request body exceeds the maximum allowed size");
7
+ let s = await n.arrayBuffer();
8
+ if (s.byteLength > a) throw new e("Request body exceeds the maximum allowed size");
9
+ let c = new TextDecoder().decode(s);
5
10
  try {
6
- n = JSON.parse(i);
11
+ i = JSON.parse(c);
7
12
  } catch {
8
13
  try {
9
- if (i.includes("=")) {
10
- let e = new URLSearchParams(i);
11
- n = Object.fromEntries(e.entries());
14
+ if (c.includes("=")) {
15
+ let e = new URLSearchParams(c);
16
+ i = Object.fromEntries(e.entries());
12
17
  } else throw new e("Invalid URLSearchParams format");
13
18
  } catch {
14
19
  throw new e("Failed to extract data from request");
15
20
  }
16
21
  }
17
- return n;
22
+ return i;
18
23
  }
19
24
  //#endregion
20
- export { t as decodeRequestBody };
25
+ export { n as decodeRequestBody };
21
26
 
22
27
  //# sourceMappingURL=decodeRequestBody.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"decodeRequestBody.js","names":[],"sources":["../../../src/utilities/decodeRequestBody.ts"],"sourcesContent":["import { BadRequest } from \"../http/badResponses/badRequest\";\n\n/**\n * Decodes a request body into a plain object, trying JSON first then URL-encoded form data.\n * Throws `BadRequest` if neither format can be parsed.\n *\n * @param request - The incoming request whose body will be decoded.\n * @returns The decoded body as a plain object.\n *\n * @example\n * ```typescript\n * export async function action({ request }: ActionFunctionArgs) {\n * const body = await decodeRequestBody(request);\n * // body is now a plain JS object\n * }\n * ```\n */\n\n// biome-ignore lint/suspicious/noExplicitAny: intentional\nasync function decodeRequestBody(request: Request): Promise<any> {\n\t// biome-ignore lint/suspicious/noExplicitAny: intentional\n\tlet data: any;\n\n\tconst arrayBuffer = await request.arrayBuffer();\n\tconst text = new TextDecoder().decode(arrayBuffer);\n\n\ttry {\n\t\tdata = JSON.parse(text);\n\t} catch (_jsonError) {\n\t\ttry {\n\t\t\tif (text.includes(\"=\")) {\n\t\t\t\tconst formData = new URLSearchParams(text);\n\t\t\t\tdata = Object.fromEntries(formData.entries());\n\t\t\t} else {\n\t\t\t\tthrow new BadRequest(\"Invalid URLSearchParams format\");\n\t\t\t}\n\t\t} catch (_formDataError) {\n\t\t\tthrow new BadRequest(\"Failed to extract data from request\");\n\t\t}\n\t}\n\n\treturn data;\n}\n\nexport { decodeRequestBody };\n"],"mappings":";;AAmBA,eAAe,EAAkB,GAAgC;CAEhE,IAAI,GAEE,IAAc,MAAM,EAAQ,YAAY,GACxC,IAAO,IAAI,YAAY,CAAC,CAAC,OAAO,CAAW;CAEjD,IAAI;EACH,IAAO,KAAK,MAAM,CAAI;CACvB,QAAqB;EACpB,IAAI;GACH,IAAI,EAAK,SAAS,GAAG,GAAG;IACvB,IAAM,IAAW,IAAI,gBAAgB,CAAI;IACzC,IAAO,OAAO,YAAY,EAAS,QAAQ,CAAC;GAC7C,OACC,MAAM,IAAI,EAAW,gCAAgC;EAEvD,QAAyB;GACxB,MAAM,IAAI,EAAW,qCAAqC;EAC3D;CACD;CAEA,OAAO;AACR"}
1
+ {"version":3,"file":"decodeRequestBody.js","names":[],"sources":["../../../src/utilities/decodeRequestBody.ts"],"sourcesContent":["import { BadRequest } from \"../http/badResponses/badRequest\";\n\n/** Default maximum accepted request body size (5 MB) when `maxBodySizeBytes` isn't set. */\nconst DEFAULT_MAX_BODY_SIZE_BYTES = 5 * 1024 * 1024;\n\ntype DecodeRequestBodyOptions = {\n\t/** Max accepted body size in bytes. Larger bodies are rejected with `BadRequest`. @default 5242880 (5 MB) */\n\tmaxBodySizeBytes?: number;\n};\n\n/**\n * Decodes a request body into a plain object, trying JSON first then URL-encoded form data.\n * Throws `BadRequest` if neither format can be parsed, or if the body exceeds `maxBodySizeBytes`.\n *\n * @param request - The incoming request whose body will be decoded.\n * @param options.maxBodySizeBytes - Max accepted body size in bytes. Default: 5 MB.\n * @returns The decoded body as a plain object.\n *\n * @example\n * ```typescript\n * export async function action({ request }: ActionFunctionArgs) {\n * const body = await decodeRequestBody(request);\n * // body is now a plain JS object\n * }\n * ```\n */\n\nasync function decodeRequestBody(\n\trequest: Request,\n\toptions?: DecodeRequestBodyOptions,\n\t// biome-ignore lint/suspicious/noExplicitAny: intentional\n): Promise<any> {\n\t// biome-ignore lint/suspicious/noExplicitAny: intentional\n\tlet data: any;\n\n\tconst maxBodySizeBytes =\n\t\toptions?.maxBodySizeBytes ?? DEFAULT_MAX_BODY_SIZE_BYTES;\n\n\tconst contentLength = Number(request.headers.get(\"content-length\"));\n\tif (Number.isFinite(contentLength) && contentLength > maxBodySizeBytes) {\n\t\tthrow new BadRequest(\"Request body exceeds the maximum allowed size\");\n\t}\n\n\tconst arrayBuffer = await request.arrayBuffer();\n\n\tif (arrayBuffer.byteLength > maxBodySizeBytes) {\n\t\tthrow new BadRequest(\"Request body exceeds the maximum allowed size\");\n\t}\n\n\tconst text = new TextDecoder().decode(arrayBuffer);\n\n\ttry {\n\t\tdata = JSON.parse(text);\n\t} catch (_jsonError) {\n\t\ttry {\n\t\t\tif (text.includes(\"=\")) {\n\t\t\t\tconst formData = new URLSearchParams(text);\n\t\t\t\tdata = Object.fromEntries(formData.entries());\n\t\t\t} else {\n\t\t\t\tthrow new BadRequest(\"Invalid URLSearchParams format\");\n\t\t\t}\n\t\t} catch (_formDataError) {\n\t\t\tthrow new BadRequest(\"Failed to extract data from request\");\n\t\t}\n\t}\n\n\treturn data;\n}\n\nexport { decodeRequestBody };\n"],"mappings":";;AAGA,IAAM,IAA8B,IAAI,OAAO;AAwB/C,eAAe,EACd,GACA,GAEe;CAEf,IAAI,GAEE,IACL,GAAS,oBAAoB,GAExB,IAAgB,OAAO,EAAQ,QAAQ,IAAI,gBAAgB,CAAC;CAClE,IAAI,OAAO,SAAS,CAAa,KAAK,IAAgB,GACrD,MAAM,IAAI,EAAW,+CAA+C;CAGrE,IAAM,IAAc,MAAM,EAAQ,YAAY;CAE9C,IAAI,EAAY,aAAa,GAC5B,MAAM,IAAI,EAAW,+CAA+C;CAGrE,IAAM,IAAO,IAAI,YAAY,CAAC,CAAC,OAAO,CAAW;CAEjD,IAAI;EACH,IAAO,KAAK,MAAM,CAAI;CACvB,QAAqB;EACpB,IAAI;GACH,IAAI,EAAK,SAAS,GAAG,GAAG;IACvB,IAAM,IAAW,IAAI,gBAAgB,CAAI;IACzC,IAAO,OAAO,YAAY,EAAS,QAAQ,CAAC;GAC7C,OACC,MAAM,IAAI,EAAW,gCAAgC;EAEvD,QAAyB;GACxB,MAAM,IAAI,EAAW,qCAAqC;EAC3D;CACD;CAEA,OAAO;AACR"}
@@ -31,26 +31,31 @@ var s = [
31
31
  "MX",
32
32
  "A",
33
33
  "AAAA"
34
- ];
35
- async function c(t, n) {
36
- try {
37
- return await e?.promises?.resolve(t, n), !0;
38
- } catch {
39
- return !1;
40
- }
41
- }
42
- async function l(e) {
43
- for (let t of s) if (await c(e, t)) return !0;
34
+ ], c = 5e3;
35
+ async function l(t, n, r) {
36
+ return new Promise((i) => {
37
+ let a = !1, o = setTimeout(() => {
38
+ a || (a = !0, i(!1));
39
+ }, r);
40
+ o.unref?.(), e.promises.resolve(t, n).then(() => {
41
+ a || (a = !0, clearTimeout(o), i(!0));
42
+ }).catch(() => {
43
+ a || (a = !0, clearTimeout(o), i(!1));
44
+ });
45
+ });
46
+ }
47
+ async function u(e, t) {
48
+ for (let n of s) if (await l(e, n, t)) return !0;
44
49
  return !1;
45
50
  }
46
- async function u(e) {
51
+ async function d(e, n) {
47
52
  if (!e || typeof e != "string") return !1;
48
- let n = e.trim();
49
- if (!t(n) || !a(n)) return !1;
50
- let r = o(n);
51
- return r ? await l(r) : !1;
53
+ let r = e.trim();
54
+ if (!t(r) || !a(r)) return !1;
55
+ let i = o(r);
56
+ return i ? await u(i, n?.dnsTimeoutMs ?? c) : !1;
52
57
  }
53
58
  //#endregion
54
- export { u as validateEmail };
59
+ export { d as validateEmail };
55
60
 
56
61
  //# sourceMappingURL=validateEmail.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"validateEmail.js","names":[],"sources":["../../../src/validations/validateEmail.ts"],"sourcesContent":["import dns from \"node:dns\";\n\nfunction isValidBasicFormat(email: string): boolean {\n\tconst emailRegex =\n\t\t/^[a-zA-Z0-9.!$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;\n\n\treturn emailRegex.test(email);\n}\n\nfunction isValidLocalPart(localPart: string): boolean {\n\tif (localPart.length === 0 || localPart.length > 64) return false;\n\tif (localPart.startsWith(\".\") || localPart.endsWith(\".\")) return false;\n\tif (localPart.includes(\"..\")) return false;\n\n\tconst validLocalRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+$/;\n\tif (!validLocalRegex.test(localPart)) return false;\n\n\treturn true;\n}\n\nfunction isValidDomainLabel(label: string): boolean {\n\tif (label.length === 0 || label.length > 63) return false;\n\tif (label.startsWith(\"-\") || label.endsWith(\"-\")) return false;\n\tif (!/^[a-zA-Z0-9-]+$/.test(label)) return false;\n\treturn true;\n}\n\nfunction isValidDomainPart(domainPart: string): boolean {\n\tif (domainPart.length === 0 || domainPart.length > 253) return false;\n\n\tif (\n\t\tdomainPart.startsWith(\".\") ||\n\t\tdomainPart.endsWith(\".\") ||\n\t\tdomainPart.startsWith(\"-\") ||\n\t\tdomainPart.endsWith(\"-\")\n\t) {\n\t\treturn false;\n\t}\n\n\tconst labels = domainPart.split(\".\");\n\tif (labels.length < 2) return false;\n\n\tfor (const label of labels) if (!isValidDomainLabel(label)) return false;\n\n\tconst tld = labels[labels.length - 1];\n\tif (tld.length < 2 || !/^[a-zA-Z]+$/.test(tld)) return false;\n\n\treturn true;\n}\n\nfunction isValidAdvancedSyntax(email: string): boolean {\n\tconst parts = email.split(\"@\");\n\tif (parts.length !== 2) return false;\n\n\tconst [localPart, domainPart] = parts;\n\n\tif (!isValidLocalPart(localPart)) return false;\n\tif (!isValidDomainPart(domainPart)) return false;\n\treturn true;\n}\n\nfunction extractDomain(email: string): string | null {\n\tconst parts = email.split(\"@\");\n\treturn parts.length === 2 ? parts[1].toLowerCase() : null;\n}\n\nconst DNS_RECORD_TYPES = [\"MX\", \"A\", \"AAAA\"] as const;\n\nasync function tryResolveDnsRecord(\n\tdomain: string,\n\trecordType: string,\n): Promise<boolean> {\n\ttry {\n\t\tawait dns?.promises?.resolve(domain, recordType);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nasync function isValidDns(domain: string): Promise<boolean> {\n\tfor (const recordType of DNS_RECORD_TYPES) {\n\t\tconst hasRecord = await tryResolveDnsRecord(domain, recordType);\n\t\tif (hasRecord) return true;\n\t}\n\treturn false;\n}\n\n/**\n * Validates if an email address is valid in all possible ways, including DNS validation.\n *\n * This function performs comprehensive email validation by:\n * - Checking basic email format and syntax\n * - Validating advanced RFC 5322 compliance rules\n * - Verifying that the domain has valid MX or A records in DNS\n *\n * @param rawEmail - The email address to validate.\n * @returns `true` if the email passes format checks and its domain resolves in DNS, otherwise `false`.\n *\n * @example\n * ```typescript\n * await validateEmail(\"user@gmail.com\"); // true\n * await validateEmail(\"user@gmil.com\"); // false (invalid domain)\n * await validateEmail(\"invalid-email\"); // false (invalid format)\n * ```\n */\n\nasync function validateEmail(rawEmail: string): Promise<boolean> {\n\tif (!rawEmail || typeof rawEmail !== \"string\") return false;\n\tconst email = rawEmail.trim();\n\n\tif (!isValidBasicFormat(email)) return false;\n\tif (!isValidAdvancedSyntax(email)) return false;\n\n\tconst domain = extractDomain(email);\n\tif (!domain) return false;\n\n\treturn await isValidDns(domain);\n}\n\nexport { validateEmail };\n"],"mappings":";;AAEA,SAAS,EAAmB,GAAwB;CAInD,OAAO,sIAAW,KAAK,CAAK;AAC7B;AAEA,SAAS,EAAiB,GAA4B;CAQrD,OAFA,EALI,EAAU,WAAW,KAAK,EAAU,SAAS,MAC7C,EAAU,WAAW,GAAG,KAAK,EAAU,SAAS,GAAG,KACnD,EAAU,SAAS,IAAI,KAGvB,CAAC,qCAAgB,KAAK,CAAS;AAGpC;AAEA,SAAS,EAAmB,GAAwB;CAInD,OADA,EAFI,EAAM,WAAW,KAAK,EAAM,SAAS,MACrC,EAAM,WAAW,GAAG,KAAK,EAAM,SAAS,GAAG,KAC3C,CAAC,kBAAkB,KAAK,CAAK;AAElC;AAEA,SAAS,EAAkB,GAA6B;CAGvD,IAFI,EAAW,WAAW,KAAK,EAAW,SAAS,OAGlD,EAAW,WAAW,GAAG,KACzB,EAAW,SAAS,GAAG,KACvB,EAAW,WAAW,GAAG,KACzB,EAAW,SAAS,GAAG,GAEvB,OAAO;CAGR,IAAM,IAAS,EAAW,MAAM,GAAG;CACnC,IAAI,EAAO,SAAS,GAAG,OAAO;CAE9B,KAAK,IAAM,KAAS,GAAQ,IAAI,CAAC,EAAmB,CAAK,GAAG,OAAO;CAEnE,IAAM,IAAM,EAAO,EAAO,SAAS;CAGnC,OAFA,EAAI,EAAI,SAAS,KAAK,CAAC,cAAc,KAAK,CAAG;AAG9C;AAEA,SAAS,EAAsB,GAAwB;CACtD,IAAM,IAAQ,EAAM,MAAM,GAAG;CAC7B,IAAI,EAAM,WAAW,GAAG,OAAO;CAE/B,IAAM,CAAC,GAAW,KAAc;CAIhC,OADA,EADI,CAAC,EAAiB,CAAS,KAC3B,CAAC,EAAkB,CAAU;AAElC;AAEA,SAAS,EAAc,GAA8B;CACpD,IAAM,IAAQ,EAAM,MAAM,GAAG;CAC7B,OAAO,EAAM,WAAW,IAAI,EAAM,EAAE,CAAC,YAAY,IAAI;AACtD;AAEA,IAAM,IAAmB;CAAC;CAAM;CAAK;AAAM;AAE3C,eAAe,EACd,GACA,GACmB;CACnB,IAAI;EAEH,OADA,MAAM,GAAK,UAAU,QAAQ,GAAQ,CAAU,GACxC;CACR,QAAQ;EACP,OAAO;CACR;AACD;AAEA,eAAe,EAAW,GAAkC;CAC3D,KAAK,IAAM,KAAc,GAExB,IAAI,MADoB,EAAoB,GAAQ,CAAU,GAC/C,OAAO;CAEvB,OAAO;AACR;AAqBA,eAAe,EAAc,GAAoC;CAChE,IAAI,CAAC,KAAY,OAAO,KAAa,UAAU,OAAO;CACtD,IAAM,IAAQ,EAAS,KAAK;CAG5B,IADI,CAAC,EAAmB,CAAK,KACzB,CAAC,EAAsB,CAAK,GAAG,OAAO;CAE1C,IAAM,IAAS,EAAc,CAAK;CAGlC,OAFK,IAEE,MAAM,EAAW,CAAM,IAFV;AAGrB"}
1
+ {"version":3,"file":"validateEmail.js","names":[],"sources":["../../../src/validations/validateEmail.ts"],"sourcesContent":["import dns from \"node:dns\";\n\nfunction isValidBasicFormat(email: string): boolean {\n\tconst emailRegex =\n\t\t/^[a-zA-Z0-9.!$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;\n\n\treturn emailRegex.test(email);\n}\n\nfunction isValidLocalPart(localPart: string): boolean {\n\tif (localPart.length === 0 || localPart.length > 64) return false;\n\tif (localPart.startsWith(\".\") || localPart.endsWith(\".\")) return false;\n\tif (localPart.includes(\"..\")) return false;\n\n\tconst validLocalRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+$/;\n\tif (!validLocalRegex.test(localPart)) return false;\n\n\treturn true;\n}\n\nfunction isValidDomainLabel(label: string): boolean {\n\tif (label.length === 0 || label.length > 63) return false;\n\tif (label.startsWith(\"-\") || label.endsWith(\"-\")) return false;\n\tif (!/^[a-zA-Z0-9-]+$/.test(label)) return false;\n\treturn true;\n}\n\nfunction isValidDomainPart(domainPart: string): boolean {\n\tif (domainPart.length === 0 || domainPart.length > 253) return false;\n\n\tif (\n\t\tdomainPart.startsWith(\".\") ||\n\t\tdomainPart.endsWith(\".\") ||\n\t\tdomainPart.startsWith(\"-\") ||\n\t\tdomainPart.endsWith(\"-\")\n\t) {\n\t\treturn false;\n\t}\n\n\tconst labels = domainPart.split(\".\");\n\tif (labels.length < 2) return false;\n\n\tfor (const label of labels) if (!isValidDomainLabel(label)) return false;\n\n\tconst tld = labels[labels.length - 1];\n\tif (tld.length < 2 || !/^[a-zA-Z]+$/.test(tld)) return false;\n\n\treturn true;\n}\n\nfunction isValidAdvancedSyntax(email: string): boolean {\n\tconst parts = email.split(\"@\");\n\tif (parts.length !== 2) return false;\n\n\tconst [localPart, domainPart] = parts;\n\n\tif (!isValidLocalPart(localPart)) return false;\n\tif (!isValidDomainPart(domainPart)) return false;\n\treturn true;\n}\n\nfunction extractDomain(email: string): string | null {\n\tconst parts = email.split(\"@\");\n\treturn parts.length === 2 ? parts[1].toLowerCase() : null;\n}\n\nconst DNS_RECORD_TYPES = [\"MX\", \"A\", \"AAAA\"] as const;\n\n/** Default time to wait for a single DNS lookup before treating it as unresolved. */\nconst DEFAULT_DNS_TIMEOUT_MS = 5000;\n\n/**\n * Resolves a single DNS record type for `domain`, racing it against `timeoutMs` so a slow\n * or unresponsive DNS server can never leave the caller waiting indefinitely. A timeout is\n * treated the same as \"no record found\" (`false`), not as a validation error.\n */\nasync function tryResolveDnsRecord(\n\tdomain: string,\n\trecordType: string,\n\ttimeoutMs: number,\n): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\tlet settled = false;\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tresolve(false);\n\t\t}, timeoutMs);\n\t\ttimer.unref?.();\n\n\t\tdns.promises\n\t\t\t.resolve(domain, recordType)\n\t\t\t.then(() => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tresolve(true);\n\t\t\t})\n\t\t\t.catch(() => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tresolve(false);\n\t\t\t});\n\t});\n}\n\nasync function isValidDns(domain: string, timeoutMs: number): Promise<boolean> {\n\tfor (const recordType of DNS_RECORD_TYPES) {\n\t\tconst hasRecord = await tryResolveDnsRecord(domain, recordType, timeoutMs);\n\t\tif (hasRecord) return true;\n\t}\n\treturn false;\n}\n\ntype ValidateEmailOptions = {\n\t/** Max time (ms) to wait for each DNS lookup before treating it as unresolved. @default 5000 */\n\tdnsTimeoutMs?: number;\n};\n\n/**\n * Validates if an email address is valid in all possible ways, including DNS validation.\n *\n * This function performs comprehensive email validation by:\n * - Checking basic email format and syntax\n * - Validating advanced RFC 5322 compliance rules\n * - Verifying that the domain has valid MX or A records in DNS\n *\n * DNS lookups are bounded by `options.dnsTimeoutMs` (default 5000ms) so a slow or\n * unresponsive DNS server can never leave the returned promise pending indefinitely.\n *\n * @param rawEmail - The email address to validate.\n * @param options.dnsTimeoutMs - Max time (ms) to wait for each DNS lookup. Default: 5000.\n * @returns `true` if the email passes format checks and its domain resolves in DNS, otherwise `false`.\n *\n * @example\n * ```typescript\n * await validateEmail(\"user@gmail.com\"); // true\n * await validateEmail(\"user@gmil.com\"); // false (invalid domain)\n * await validateEmail(\"invalid-email\"); // false (invalid format)\n * await validateEmail(\"user@slow-dns.com\", { dnsTimeoutMs: 2000 }); // bounded wait\n * ```\n */\n\nasync function validateEmail(\n\trawEmail: string,\n\toptions?: ValidateEmailOptions,\n): Promise<boolean> {\n\tif (!rawEmail || typeof rawEmail !== \"string\") return false;\n\tconst email = rawEmail.trim();\n\n\tif (!isValidBasicFormat(email)) return false;\n\tif (!isValidAdvancedSyntax(email)) return false;\n\n\tconst domain = extractDomain(email);\n\tif (!domain) return false;\n\n\tconst timeoutMs = options?.dnsTimeoutMs ?? DEFAULT_DNS_TIMEOUT_MS;\n\treturn await isValidDns(domain, timeoutMs);\n}\n\nexport { validateEmail };\n"],"mappings":";;AAEA,SAAS,EAAmB,GAAwB;CAInD,OAAO,sIAAW,KAAK,CAAK;AAC7B;AAEA,SAAS,EAAiB,GAA4B;CAQrD,OAFA,EALI,EAAU,WAAW,KAAK,EAAU,SAAS,MAC7C,EAAU,WAAW,GAAG,KAAK,EAAU,SAAS,GAAG,KACnD,EAAU,SAAS,IAAI,KAGvB,CAAC,qCAAgB,KAAK,CAAS;AAGpC;AAEA,SAAS,EAAmB,GAAwB;CAInD,OADA,EAFI,EAAM,WAAW,KAAK,EAAM,SAAS,MACrC,EAAM,WAAW,GAAG,KAAK,EAAM,SAAS,GAAG,KAC3C,CAAC,kBAAkB,KAAK,CAAK;AAElC;AAEA,SAAS,EAAkB,GAA6B;CAGvD,IAFI,EAAW,WAAW,KAAK,EAAW,SAAS,OAGlD,EAAW,WAAW,GAAG,KACzB,EAAW,SAAS,GAAG,KACvB,EAAW,WAAW,GAAG,KACzB,EAAW,SAAS,GAAG,GAEvB,OAAO;CAGR,IAAM,IAAS,EAAW,MAAM,GAAG;CACnC,IAAI,EAAO,SAAS,GAAG,OAAO;CAE9B,KAAK,IAAM,KAAS,GAAQ,IAAI,CAAC,EAAmB,CAAK,GAAG,OAAO;CAEnE,IAAM,IAAM,EAAO,EAAO,SAAS;CAGnC,OAFA,EAAI,EAAI,SAAS,KAAK,CAAC,cAAc,KAAK,CAAG;AAG9C;AAEA,SAAS,EAAsB,GAAwB;CACtD,IAAM,IAAQ,EAAM,MAAM,GAAG;CAC7B,IAAI,EAAM,WAAW,GAAG,OAAO;CAE/B,IAAM,CAAC,GAAW,KAAc;CAIhC,OADA,EADI,CAAC,EAAiB,CAAS,KAC3B,CAAC,EAAkB,CAAU;AAElC;AAEA,SAAS,EAAc,GAA8B;CACpD,IAAM,IAAQ,EAAM,MAAM,GAAG;CAC7B,OAAO,EAAM,WAAW,IAAI,EAAM,EAAE,CAAC,YAAY,IAAI;AACtD;AAEA,IAAM,IAAmB;CAAC;CAAM;CAAK;AAAM,GAGrC,IAAyB;AAO/B,eAAe,EACd,GACA,GACA,GACmB;CACnB,OAAO,IAAI,SAAS,MAAY;EAC/B,IAAI,IAAU,IAER,IAAQ,iBAAiB;GAC1B,MACJ,IAAU,IACV,EAAQ,EAAK;EACd,GAAG,CAAS;EAGZ,AAFA,EAAM,QAAQ,GAEd,EAAI,SACF,QAAQ,GAAQ,CAAU,CAAC,CAC3B,WAAW;GACP,MACJ,IAAU,IACV,aAAa,CAAK,GAClB,EAAQ,EAAI;EACb,CAAC,CAAC,CACD,YAAY;GACR,MACJ,IAAU,IACV,aAAa,CAAK,GAClB,EAAQ,EAAK;EACd,CAAC;CACH,CAAC;AACF;AAEA,eAAe,EAAW,GAAgB,GAAqC;CAC9E,KAAK,IAAM,KAAc,GAExB,IAAI,MADoB,EAAoB,GAAQ,GAAY,CAAS,GAC1D,OAAO;CAEvB,OAAO;AACR;AA+BA,eAAe,EACd,GACA,GACmB;CACnB,IAAI,CAAC,KAAY,OAAO,KAAa,UAAU,OAAO;CACtD,IAAM,IAAQ,EAAS,KAAK;CAG5B,IADI,CAAC,EAAmB,CAAK,KACzB,CAAC,EAAsB,CAAK,GAAG,OAAO;CAE1C,IAAM,IAAS,EAAc,CAAK;CAIlC,OAHK,IAGE,MAAM,EAAW,GADN,GAAS,gBAAgB,CACF,IAHrB;AAIrB"}
@@ -1,8 +1,13 @@
1
+ type DecodeRequestBodyOptions = {
2
+ /** Max accepted body size in bytes. Larger bodies are rejected with `BadRequest`. @default 5242880 (5 MB) */
3
+ maxBodySizeBytes?: number;
4
+ };
1
5
  /**
2
6
  * Decodes a request body into a plain object, trying JSON first then URL-encoded form data.
3
- * Throws `BadRequest` if neither format can be parsed.
7
+ * Throws `BadRequest` if neither format can be parsed, or if the body exceeds `maxBodySizeBytes`.
4
8
  *
5
9
  * @param request - The incoming request whose body will be decoded.
10
+ * @param options.maxBodySizeBytes - Max accepted body size in bytes. Default: 5 MB.
6
11
  * @returns The decoded body as a plain object.
7
12
  *
8
13
  * @example
@@ -13,6 +18,6 @@
13
18
  * }
14
19
  * ```
15
20
  */
16
- declare function decodeRequestBody(request: Request): Promise<any>;
21
+ declare function decodeRequestBody(request: Request, options?: DecodeRequestBodyOptions): Promise<any>;
17
22
  export { decodeRequestBody };
18
23
  //# sourceMappingURL=decodeRequestBody.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"decodeRequestBody.d.ts","sourceRoot":"","sources":["../../src/utilities/decodeRequestBody.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AAGH,iBAAe,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAuB/D;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
1
+ {"version":3,"file":"decodeRequestBody.d.ts","sourceRoot":"","sources":["../../src/utilities/decodeRequestBody.ts"],"names":[],"mappings":"AAKA,KAAK,wBAAwB,GAAG;IAC/B,6GAA6G;IAC7G,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AAEH,iBAAe,iBAAiB,CAC/B,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,wBAAwB,GAEhC,OAAO,CAAC,GAAG,CAAC,CAoCd;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
@@ -1,3 +1,7 @@
1
+ type ValidateEmailOptions = {
2
+ /** Max time (ms) to wait for each DNS lookup before treating it as unresolved. @default 5000 */
3
+ dnsTimeoutMs?: number;
4
+ };
1
5
  /**
2
6
  * Validates if an email address is valid in all possible ways, including DNS validation.
3
7
  *
@@ -6,7 +10,11 @@
6
10
  * - Validating advanced RFC 5322 compliance rules
7
11
  * - Verifying that the domain has valid MX or A records in DNS
8
12
  *
13
+ * DNS lookups are bounded by `options.dnsTimeoutMs` (default 5000ms) so a slow or
14
+ * unresponsive DNS server can never leave the returned promise pending indefinitely.
15
+ *
9
16
  * @param rawEmail - The email address to validate.
17
+ * @param options.dnsTimeoutMs - Max time (ms) to wait for each DNS lookup. Default: 5000.
10
18
  * @returns `true` if the email passes format checks and its domain resolves in DNS, otherwise `false`.
11
19
  *
12
20
  * @example
@@ -14,8 +22,9 @@
14
22
  * await validateEmail("user@gmail.com"); // true
15
23
  * await validateEmail("user@gmil.com"); // false (invalid domain)
16
24
  * await validateEmail("invalid-email"); // false (invalid format)
25
+ * await validateEmail("user@slow-dns.com", { dnsTimeoutMs: 2000 }); // bounded wait
17
26
  * ```
18
27
  */
19
- declare function validateEmail(rawEmail: string): Promise<boolean>;
28
+ declare function validateEmail(rawEmail: string, options?: ValidateEmailOptions): Promise<boolean>;
20
29
  export { validateEmail };
21
30
  //# sourceMappingURL=validateEmail.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validateEmail.d.ts","sourceRoot":"","sources":["../../src/validations/validateEmail.ts"],"names":[],"mappings":"AAwFA;;;;;;;;;;;;;;;;;GAiBG;AAEH,iBAAe,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAW/D;AAED,OAAO,EAAE,aAAa,EAAE,CAAC"}
1
+ {"version":3,"file":"validateEmail.d.ts","sourceRoot":"","sources":["../../src/validations/validateEmail.ts"],"names":[],"mappings":"AAoHA,KAAK,oBAAoB,GAAG;IAC3B,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,iBAAe,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,OAAO,CAAC,CAYlB;AAED,OAAO,EAAE,aAAa,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkyn/server",
3
- "version": "3.0.9",
3
+ "version": "3.0.10",
4
4
  "author": "Arkyn | Lucas Gonçalves",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -45,14 +45,14 @@
45
45
  },
46
46
  "scripts": {
47
47
  "audit": "bun audit",
48
- "build": "bunx vite build && bunx tsc --project tsconfig.json --emitDeclarationOnly && bun ./generate-exports.ts && rm -f dist/modules/index.js dist/modules/style.css",
48
+ "build": "bunx vite build && bunx tsc --project tsconfig.json --emitDeclarationOnly && bun ../../scripts/generate-exports.ts && rm -f dist/modules/index.js dist/modules/style.css",
49
49
  "prebuild": "rm -rf dist",
50
50
  "publish:beta": "bash ../../scripts/publish-idempotent.sh beta",
51
51
  "publish:latest": "bash ../../scripts/publish-idempotent.sh latest",
52
- "release:beta": "bun ./generate-version.ts beta",
53
- "release:patch": "bun ./generate-version.ts patch",
54
- "release:minor": "bun ./generate-version.ts minor",
55
- "release:major": "bun ./generate-version.ts major",
52
+ "release:beta": "bun ../../scripts/generate-version.ts beta",
53
+ "release:patch": "bun ../../scripts/generate-version.ts patch",
54
+ "release:minor": "bun ../../scripts/generate-version.ts minor",
55
+ "release:major": "bun ../../scripts/generate-version.ts major",
56
56
  "test": "bunx vitest --config vitest.config.ts --run",
57
57
  "test:watch": "bunx vitest --config vitest.config.ts --watch",
58
58
  "typecheck": "bunx tsc --project tsconfig.json --noEmit"