@warlock.js/core 5.15.0 → 5.16.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.
- package/CHANGELOG.md +11 -0
- package/esm/generations/features/shared/merge-web-sitemap-config.mjs +118 -0
- package/esm/generations/features/shared/merge-web-sitemap-config.mjs.map +1 -0
- package/esm/generations/features/sitemap.feature.mjs +50 -47
- package/esm/generations/features/sitemap.feature.mjs.map +1 -1
- package/esm/http/index.d.mts +1 -0
- package/esm/http/parse-urlencoded-body.mjs +27 -0
- package/esm/http/parse-urlencoded-body.mjs.map +1 -0
- package/esm/http/plugins.d.mts.map +1 -1
- package/esm/http/plugins.mjs +8 -0
- package/esm/http/plugins.mjs.map +1 -1
- package/esm/http/response.d.mts +9 -2
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +12 -4
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/xmlable.d.mts +14 -0
- package/esm/http/xmlable.d.mts.map +1 -0
- package/esm/index.d.mts +2 -1
- package/llms-full.txt +30 -6
- package/llms.txt +1 -1
- package/package.json +11 -11
- package/skills/configure-app/SKILL.md +6 -6
- package/skills/create-controller/SKILL.md +14 -0
- package/skills/health-checks/SKILL.md +8 -0
- package/skills/send-response/SKILL.md +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
> ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an _Upgrading_ section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
|
|
8
8
|
|
|
9
|
+
## 5.16.0 - 2026-09-18
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Core now parses `application/x-www-form-urlencoded` request bodies, so plain HTML forms and OAuth `form_post` callbacks (Apple) reach their routes instead of failing with `FST_ERR_CTP_INVALID_MEDIA_TYPE`. These bodies have the same `http.bodyLimit` as JSON. A key sent more than once becomes an array.
|
|
14
|
+
- `response.xml()` accepts a raw string or any value with a `toXML(): string` method (such as a `@warlock.js/sitemap` `Sitemap`), and sends `application/xml`.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `warlock add sitemap` now merges a disabled `sitemap` section into `src/config/web.ts`, and creates that file when it is missing. It no longer writes `src/config/sitemap.ts` or registers `sitemapConnector()`. The merge edits only a `sitemap` key directly on the exported config object, and the result is re-parsed before it is written.
|
|
19
|
+
|
|
9
20
|
## 5.15.0 - 2026-09-18
|
|
10
21
|
|
|
11
22
|
### Added
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/generations/features/shared/merge-web-sitemap-config.ts
|
|
4
|
+
/**
|
|
5
|
+
* Merge a disabled `sitemap` section into `src/config/web.ts` SOURCE TEXT.
|
|
6
|
+
*
|
|
7
|
+
* **String surgery, never parse-and-print** — same rationale as
|
|
8
|
+
* `insertConnectorEntry`: `web.ts` is app-owned and may carry any formatting
|
|
9
|
+
* or comments a parse-and-print would discard. The TypeScript AST is used to
|
|
10
|
+
* *find where to cut*, never to regenerate the file — the splice below still
|
|
11
|
+
* copies the original source's bytes verbatim.
|
|
12
|
+
*
|
|
13
|
+
* Recognises the exported config object two ways: an inline `export default
|
|
14
|
+
* {...}`, or `export default <identifier>;` pointing back at a `const
|
|
15
|
+
* <identifier> = {...}` (or `const <identifier>: SomeType = {...}`) declared
|
|
16
|
+
* earlier in the file — the shape this generator itself scaffolds, and the
|
|
17
|
+
* shape every other generated config file in this project uses. Any other
|
|
18
|
+
* shape (a function call, a re-export, no default export at all) returns
|
|
19
|
+
* `"unrecognised"` rather than guessing at where to cut — clobbering an
|
|
20
|
+
* app-owned file the merge could not actually understand is worse than
|
|
21
|
+
* refusing.
|
|
22
|
+
*
|
|
23
|
+
* A `sitemap` property already present anywhere in the source (by key, so one
|
|
24
|
+
* a human has since hand-edited is still found) is left unchanged — that is
|
|
25
|
+
* what makes a second `warlock add sitemap` a no-op instead of a duplicate
|
|
26
|
+
* block. Detection walks the AST rather than regex-matching `sitemap\s*:`, so
|
|
27
|
+
* a comment like `// sitemap: TODO` — text, not a property — does not count.
|
|
28
|
+
*
|
|
29
|
+
* Before returning a merge, the resulting text is re-parsed and checked for
|
|
30
|
+
* syntax errors; a merge that would not itself parse is refused (as
|
|
31
|
+
* `"unrecognised"`) instead of written, since a matched-but-corrupt splice is
|
|
32
|
+
* worse than doing nothing.
|
|
33
|
+
*
|
|
34
|
+
* @param source The config file's current text.
|
|
35
|
+
* @returns What happened, and the new text when there is any.
|
|
36
|
+
*/
|
|
37
|
+
function mergeWebSitemapConfig(source) {
|
|
38
|
+
const sourceFile = ts.createSourceFile("web.ts", source, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS);
|
|
39
|
+
const objectLiteral = findExportedConfigObjectLiteral(sourceFile);
|
|
40
|
+
if (!objectLiteral) return { status: "unrecognised" };
|
|
41
|
+
if (hasTopLevelSitemapProperty(objectLiteral)) return { status: "already-present" };
|
|
42
|
+
const bodyStart = objectLiteral.getStart(sourceFile) + 1;
|
|
43
|
+
const closingBraceIndex = objectLiteral.getEnd() - 1;
|
|
44
|
+
const body = source.slice(bodyStart, closingBraceIndex);
|
|
45
|
+
const indentMatch = /\n([ \t]*)\S/.exec(body);
|
|
46
|
+
const indent = indentMatch ? indentMatch[1] : " ";
|
|
47
|
+
const block = `${indent}sitemap: {\n${indent}${indent}enabled: false,\n${indent}${indent}path: "/sitemap.xml",\n${indent}${indent}defaults: { changefreq: "weekly", priority: 0.5 },\n${indent}},\n`;
|
|
48
|
+
const { index: insertionPoint, needsComma } = lastPropertyEnd(source, closingBraceIndex);
|
|
49
|
+
const next = `${source.slice(0, insertionPoint)}${needsComma ? ",\n" : ""}${block}${source.slice(closingBraceIndex)}`;
|
|
50
|
+
if (hasSyntaxErrors(next)) return { status: "unrecognised" };
|
|
51
|
+
return {
|
|
52
|
+
status: "merged",
|
|
53
|
+
next
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** Whether TEXT fails to parse as TypeScript source. */
|
|
57
|
+
function hasSyntaxErrors(text) {
|
|
58
|
+
const { diagnostics } = ts.transpileModule(text, {
|
|
59
|
+
compilerOptions: {
|
|
60
|
+
module: ts.ModuleKind.ESNext,
|
|
61
|
+
target: ts.ScriptTarget.ESNext
|
|
62
|
+
},
|
|
63
|
+
reportDiagnostics: true
|
|
64
|
+
});
|
|
65
|
+
return Boolean(diagnostics && diagnostics.length > 0);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether the exported config object literal already has a `sitemap`
|
|
69
|
+
* property directly on it — a `sitemap` key nested under some other
|
|
70
|
+
* property (e.g. `seo: { sitemap: true }`) is a different feature's field
|
|
71
|
+
* and must not block adding the top-level one.
|
|
72
|
+
*/
|
|
73
|
+
function hasTopLevelSitemapProperty(objectLiteral) {
|
|
74
|
+
return objectLiteral.properties.some((property) => (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && propertyKeyName(property.name) === "sitemap");
|
|
75
|
+
}
|
|
76
|
+
/** The literal text of a property name node, when it is one of the plain shapes we care about. */
|
|
77
|
+
function propertyKeyName(name) {
|
|
78
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Find the object literal a source file's default export ultimately points
|
|
82
|
+
* at, whether the export is inline or an identifier declared earlier in the
|
|
83
|
+
* file.
|
|
84
|
+
*/
|
|
85
|
+
function findExportedConfigObjectLiteral(sourceFile) {
|
|
86
|
+
const exportAssignment = sourceFile.statements.find((statement) => ts.isExportAssignment(statement) && !statement.isExportEquals);
|
|
87
|
+
if (!exportAssignment) return void 0;
|
|
88
|
+
const { expression } = exportAssignment;
|
|
89
|
+
if (ts.isObjectLiteralExpression(expression)) return expression;
|
|
90
|
+
if (!ts.isIdentifier(expression)) return void 0;
|
|
91
|
+
const identifier = expression.text;
|
|
92
|
+
for (const statement of sourceFile.statements) {
|
|
93
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
94
|
+
for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.name.text === identifier && declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)) return declaration.initializer;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Where to insert a new property right before a closing brace, and whether
|
|
99
|
+
* the property immediately before it (if any) needs a trailing comma added —
|
|
100
|
+
* an object literal with two properties and no comma between them does not
|
|
101
|
+
* parse.
|
|
102
|
+
*/
|
|
103
|
+
function lastPropertyEnd(source, closingBraceIndex) {
|
|
104
|
+
let cursor = closingBraceIndex - 1;
|
|
105
|
+
while (cursor >= 0 && /\s/.test(source.charAt(cursor))) cursor--;
|
|
106
|
+
if (cursor < 0 || source[cursor] === "{" || source[cursor] === ",") return {
|
|
107
|
+
index: closingBraceIndex,
|
|
108
|
+
needsComma: false
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
index: cursor + 1,
|
|
112
|
+
needsComma: true
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
export { mergeWebSitemapConfig };
|
|
118
|
+
//# sourceMappingURL=merge-web-sitemap-config.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-web-sitemap-config.mjs","names":[],"sources":["../../../../../../../../../core/src/generations/features/shared/merge-web-sitemap-config.ts"],"sourcesContent":["import ts from \"typescript\";\n\n/** What {@link mergeWebSitemapConfig} did, or could not do, to the source it was given. */\nexport type WebSitemapMergeResult =\n { status: \"merged\"; next: string } | { status: \"already-present\" } | { status: \"unrecognised\" };\n\n/**\n * Merge a disabled `sitemap` section into `src/config/web.ts` SOURCE TEXT.\n *\n * **String surgery, never parse-and-print** — same rationale as\n * `insertConnectorEntry`: `web.ts` is app-owned and may carry any formatting\n * or comments a parse-and-print would discard. The TypeScript AST is used to\n * *find where to cut*, never to regenerate the file — the splice below still\n * copies the original source's bytes verbatim.\n *\n * Recognises the exported config object two ways: an inline `export default\n * {...}`, or `export default <identifier>;` pointing back at a `const\n * <identifier> = {...}` (or `const <identifier>: SomeType = {...}`) declared\n * earlier in the file — the shape this generator itself scaffolds, and the\n * shape every other generated config file in this project uses. Any other\n * shape (a function call, a re-export, no default export at all) returns\n * `\"unrecognised\"` rather than guessing at where to cut — clobbering an\n * app-owned file the merge could not actually understand is worse than\n * refusing.\n *\n * A `sitemap` property already present anywhere in the source (by key, so one\n * a human has since hand-edited is still found) is left unchanged — that is\n * what makes a second `warlock add sitemap` a no-op instead of a duplicate\n * block. Detection walks the AST rather than regex-matching `sitemap\\s*:`, so\n * a comment like `// sitemap: TODO` — text, not a property — does not count.\n *\n * Before returning a merge, the resulting text is re-parsed and checked for\n * syntax errors; a merge that would not itself parse is refused (as\n * `\"unrecognised\"`) instead of written, since a matched-but-corrupt splice is\n * worse than doing nothing.\n *\n * @param source The config file's current text.\n * @returns What happened, and the new text when there is any.\n */\nexport function mergeWebSitemapConfig(source: string): WebSitemapMergeResult {\n const sourceFile = ts.createSourceFile(\n \"web.ts\",\n source,\n ts.ScriptTarget.ESNext,\n /* setParentNodes */ true,\n ts.ScriptKind.TS,\n );\n\n const objectLiteral = findExportedConfigObjectLiteral(sourceFile);\n\n if (!objectLiteral) {\n return { status: \"unrecognised\" };\n }\n\n if (hasTopLevelSitemapProperty(objectLiteral)) {\n return { status: \"already-present\" };\n }\n\n const bodyStart = objectLiteral.getStart(sourceFile) + 1;\n const closingBraceIndex = objectLiteral.getEnd() - 1;\n const body = source.slice(bodyStart, closingBraceIndex);\n\n const indentMatch = /\\n([ \\t]*)\\S/.exec(body);\n const indent = indentMatch ? indentMatch[1] : \" \";\n\n const block =\n `${indent}sitemap: {\\n` +\n `${indent}${indent}enabled: false,\\n` +\n `${indent}${indent}path: \"/sitemap.xml\",\\n` +\n `${indent}${indent}defaults: { changefreq: \"weekly\", priority: 0.5 },\\n` +\n `${indent}},\\n`;\n\n const { index: insertionPoint, needsComma } = lastPropertyEnd(source, closingBraceIndex);\n\n const next =\n `${source.slice(0, insertionPoint)}` +\n `${needsComma ? \",\\n\" : \"\"}` +\n `${block}` +\n `${source.slice(closingBraceIndex)}`;\n\n if (hasSyntaxErrors(next)) {\n return { status: \"unrecognised\" };\n }\n\n return { status: \"merged\", next };\n}\n\n/** Whether TEXT fails to parse as TypeScript source. */\nfunction hasSyntaxErrors(text: string): boolean {\n const { diagnostics } = ts.transpileModule(text, {\n compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ESNext },\n reportDiagnostics: true,\n });\n\n return Boolean(diagnostics && diagnostics.length > 0);\n}\n\n/**\n * Whether the exported config object literal already has a `sitemap`\n * property directly on it — a `sitemap` key nested under some other\n * property (e.g. `seo: { sitemap: true }`) is a different feature's field\n * and must not block adding the top-level one.\n */\nfunction hasTopLevelSitemapProperty(objectLiteral: ts.ObjectLiteralExpression): boolean {\n return objectLiteral.properties.some(\n (property) =>\n (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&\n propertyKeyName(property.name) === \"sitemap\",\n );\n}\n\n/** The literal text of a property name node, when it is one of the plain shapes we care about. */\nfunction propertyKeyName(name: ts.PropertyName): string | undefined {\n if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {\n return name.text;\n }\n\n return undefined;\n}\n\n/**\n * Find the object literal a source file's default export ultimately points\n * at, whether the export is inline or an identifier declared earlier in the\n * file.\n */\nfunction findExportedConfigObjectLiteral(\n sourceFile: ts.SourceFile,\n): ts.ObjectLiteralExpression | undefined {\n const exportAssignment = sourceFile.statements.find(\n (statement): statement is ts.ExportAssignment =>\n ts.isExportAssignment(statement) && !statement.isExportEquals,\n );\n\n if (!exportAssignment) return undefined;\n\n const { expression } = exportAssignment;\n\n if (ts.isObjectLiteralExpression(expression)) {\n return expression;\n }\n\n if (!ts.isIdentifier(expression)) return undefined;\n\n const identifier = expression.text;\n\n for (const statement of sourceFile.statements) {\n if (!ts.isVariableStatement(statement)) continue;\n\n for (const declaration of statement.declarationList.declarations) {\n if (\n ts.isIdentifier(declaration.name) &&\n declaration.name.text === identifier &&\n declaration.initializer &&\n ts.isObjectLiteralExpression(declaration.initializer)\n ) {\n return declaration.initializer;\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Where to insert a new property right before a closing brace, and whether\n * the property immediately before it (if any) needs a trailing comma added —\n * an object literal with two properties and no comma between them does not\n * parse.\n */\nfunction lastPropertyEnd(\n source: string,\n closingBraceIndex: number,\n): { index: number; needsComma: boolean } {\n let cursor = closingBraceIndex - 1;\n\n while (cursor >= 0 && /\\s/.test(source.charAt(cursor))) {\n cursor--;\n }\n\n if (cursor < 0 || source[cursor] === \"{\" || source[cursor] === \",\") {\n return { index: closingBraceIndex, needsComma: false };\n }\n\n return { index: cursor + 1, needsComma: true };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,sBAAsB,QAAuC;CAC3E,MAAM,aAAa,GAAG,iBACpB,UACA,QACA,GAAG,aAAa,QACK,MACrB,GAAG,WAAW,EAChB;CAEA,MAAM,gBAAgB,gCAAgC,UAAU;CAEhE,IAAI,CAAC,eACH,OAAO,EAAE,QAAQ,eAAe;CAGlC,IAAI,2BAA2B,aAAa,GAC1C,OAAO,EAAE,QAAQ,kBAAkB;CAGrC,MAAM,YAAY,cAAc,SAAS,UAAU,IAAI;CACvD,MAAM,oBAAoB,cAAc,OAAO,IAAI;CACnD,MAAM,OAAO,OAAO,MAAM,WAAW,iBAAiB;CAEtD,MAAM,cAAc,eAAe,KAAK,IAAI;CAC5C,MAAM,SAAS,cAAc,YAAY,KAAK;CAE9C,MAAM,QACJ,GAAG,OAAO,cACP,SAAS,OAAO,mBAChB,SAAS,OAAO,yBAChB,SAAS,OAAO,sDAChB,OAAO;CAEZ,MAAM,EAAE,OAAO,gBAAgB,eAAe,gBAAgB,QAAQ,iBAAiB;CAEvF,MAAM,OACJ,GAAG,OAAO,MAAM,GAAG,cAAc,IAC9B,aAAa,QAAQ,KACrB,QACA,OAAO,MAAM,iBAAiB;CAEnC,IAAI,gBAAgB,IAAI,GACtB,OAAO,EAAE,QAAQ,eAAe;CAGlC,OAAO;EAAE,QAAQ;EAAU;CAAK;AAClC;;AAGA,SAAS,gBAAgB,MAAuB;CAC9C,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,MAAM;EAC/C,iBAAiB;GAAE,QAAQ,GAAG,WAAW;GAAQ,QAAQ,GAAG,aAAa;EAAO;EAChF,mBAAmB;CACrB,CAAC;CAED,OAAO,QAAQ,eAAe,YAAY,SAAS,CAAC;AACtD;;;;;;;AAQA,SAAS,2BAA2B,eAAoD;CACtF,OAAO,cAAc,WAAW,MAC7B,cACE,GAAG,qBAAqB,QAAQ,KAAK,GAAG,8BAA8B,QAAQ,MAC/E,gBAAgB,SAAS,IAAI,MAAM,SACvC;AACF;;AAGA,SAAS,gBAAgB,MAA2C;CAClE,IAAI,GAAG,aAAa,IAAI,KAAK,GAAG,gBAAgB,IAAI,KAAK,GAAG,iBAAiB,IAAI,GAC/E,OAAO,KAAK;AAIhB;;;;;;AAOA,SAAS,gCACP,YACwC;CACxC,MAAM,mBAAmB,WAAW,WAAW,MAC5C,cACC,GAAG,mBAAmB,SAAS,KAAK,CAAC,UAAU,cACnD;CAEA,IAAI,CAAC,kBAAkB,OAAO;CAE9B,MAAM,EAAE,eAAe;CAEvB,IAAI,GAAG,0BAA0B,UAAU,GACzC,OAAO;CAGT,IAAI,CAAC,GAAG,aAAa,UAAU,GAAG,OAAO;CAEzC,MAAM,aAAa,WAAW;CAE9B,KAAK,MAAM,aAAa,WAAW,YAAY;EAC7C,IAAI,CAAC,GAAG,oBAAoB,SAAS,GAAG;EAExC,KAAK,MAAM,eAAe,UAAU,gBAAgB,cAClD,IACE,GAAG,aAAa,YAAY,IAAI,KAChC,YAAY,KAAK,SAAS,cAC1B,YAAY,eACZ,GAAG,0BAA0B,YAAY,WAAW,GAEpD,OAAO,YAAY;CAGzB;AAGF;;;;;;;AAQA,SAAS,gBACP,QACA,mBACwC;CACxC,IAAI,SAAS,oBAAoB;CAEjC,OAAO,UAAU,KAAK,KAAK,KAAK,OAAO,OAAO,MAAM,CAAC,GACnD;CAGF,IAAI,SAAS,KAAK,OAAO,YAAY,OAAO,OAAO,YAAY,KAC7D,OAAO;EAAE,OAAO;EAAmB,YAAY;CAAM;CAGvD,OAAO;EAAE,OAAO,SAAS;EAAG,YAAY;CAAK;AAC/C"}
|
|
@@ -1,72 +1,75 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { srcPath } from "../../utils/paths.mjs";
|
|
2
2
|
import "../../utils/index.mjs";
|
|
3
3
|
import { INSTALLED_WARLOCK_VERSION } from "./types.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { mergeWebSitemapConfig } from "./shared/merge-web-sitemap-config.mjs";
|
|
5
5
|
import { colors } from "@mongez/copper";
|
|
6
6
|
import { fileExistsAsync, getFileAsync, putFileAsync } from "@warlock.js/fs";
|
|
7
7
|
|
|
8
8
|
//#region ../core/src/generations/features/sitemap.feature.ts
|
|
9
|
-
const
|
|
10
|
-
|
|
9
|
+
const NEXT_STEPS = "Next: set `app.publicUrl` (src/config/app.ts) or the PUBLIC_APP_URL environment variable, then flip `sitemap.enabled` to true in src/config/web.ts.";
|
|
11
10
|
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
11
|
+
* Ships DISABLED because a sitemap needs this application's public origin and
|
|
12
|
+
* a freshly generated app has no way to know it. Two steps to turn it on:
|
|
13
|
+
*
|
|
14
|
+
* 1. set `app.publicUrl` in src/config/app.ts, or the PUBLIC_APP_URL
|
|
15
|
+
* environment variable;
|
|
16
|
+
* 2. flip `sitemap.enabled` to true here.
|
|
17
|
+
*
|
|
18
|
+
* With it enabled and no origin configured, generation REFUSES rather than
|
|
19
|
+
* serving absolute URLs built from a guessed host — a sitemap pointing at
|
|
20
|
+
* the wrong domain is worse than one that never starts, because nothing
|
|
21
|
+
* downstream reports it.
|
|
22
|
+
*/
|
|
23
|
+
const freshWebConfigStub = `import type { WebSitemapConfig } from "@warlock.js/web/sitemap";
|
|
24
|
+
|
|
25
|
+
const webConfig: { sitemap: WebSitemapConfig } = {
|
|
26
|
+
sitemap: {
|
|
27
|
+
enabled: false,
|
|
28
|
+
path: "/sitemap.xml",
|
|
29
|
+
defaults: { changefreq: "weekly", priority: 0.5 },
|
|
30
|
+
},
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
export default
|
|
33
|
+
export default webConfig;
|
|
33
34
|
`;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* `warlock add sitemap` writes its policy into the app's \`src/config/web.ts\`,
|
|
37
|
+
* under the \`sitemap\` key — there is no \`src/config/sitemap.ts\` and no
|
|
38
|
+
* standalone connector to register; \`@warlock.js/web\` (already installed via
|
|
39
|
+
* the \`web\` requirement) reads \`web.sitemap\` itself.
|
|
40
|
+
*
|
|
41
|
+
* Creates \`web.ts\` when the app does not have one yet; otherwise merges a
|
|
42
|
+
* \`sitemap\` section into whatever is already there rather than clobbering it.
|
|
43
|
+
*/
|
|
44
|
+
async function installSitemapConfig() {
|
|
45
|
+
const configPath = srcPath("config/web.ts");
|
|
37
46
|
if (!await fileExistsAsync(configPath)) {
|
|
38
|
-
|
|
47
|
+
await putFileAsync(configPath, freshWebConfigStub);
|
|
48
|
+
console.log(`${colors.green("✓")} Created src/config/web.ts with a disabled sitemap section`);
|
|
49
|
+
console.log(NEXT_STEPS);
|
|
39
50
|
return;
|
|
40
51
|
}
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
console.log(`${colors.yellowBright("
|
|
52
|
+
const merge = mergeWebSitemapConfig(await getFileAsync(configPath));
|
|
53
|
+
if (merge.status === "already-present") {
|
|
54
|
+
console.log(`${colors.yellowBright("sitemap")} already configured in src/config/web.ts, skipping...`);
|
|
44
55
|
return;
|
|
45
56
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (insertion.status === "added") next = insertion.next;
|
|
51
|
-
else if (next.includes("defineConfig({")) next = next.replace("defineConfig({", "defineConfig({\n connectors: [sitemapConnector()],\n");
|
|
52
|
-
else {
|
|
53
|
-
console.log(`${colors.yellowBright("warlock.config.ts")} has no recognisable defineConfig({...}) — add \`connectors: [sitemapConnector()]\` yourself.`);
|
|
57
|
+
if (merge.status === "unrecognised") {
|
|
58
|
+
console.log(`${colors.redBright("✗")} Could not safely add a \`sitemap\` section to ${colors.yellowBright("src/config/web.ts")} — its exported config object was not recognised, so nothing was written. Add it yourself:
|
|
59
|
+
sitemap: { enabled: false, path: "/sitemap.xml", defaults: { changefreq: "weekly", priority: 0.5 } },`);
|
|
60
|
+
process.exitCode = 1;
|
|
54
61
|
return;
|
|
55
62
|
}
|
|
56
|
-
await putFileAsync(configPath, next);
|
|
57
|
-
console.log(`${colors.green("✓")}
|
|
58
|
-
console.log(
|
|
63
|
+
await putFileAsync(configPath, merge.next);
|
|
64
|
+
console.log(`${colors.green("✓")} Added a disabled \`sitemap\` section to src/config/web.ts`);
|
|
65
|
+
console.log(NEXT_STEPS);
|
|
59
66
|
}
|
|
60
67
|
/** `warlock add sitemap` — runtime sitemap.xml generation, backed by the page registry. */
|
|
61
68
|
const sitemapFeature = {
|
|
62
|
-
description: "Installs @warlock.js/sitemap — runtime sitemap.xml generation from the page registry.
|
|
69
|
+
description: "Installs @warlock.js/sitemap — runtime sitemap.xml generation from the page registry. Adds a disabled `sitemap` section to src/config/web.ts, creating the file if it doesn't exist yet. Requires app.publicUrl (or PUBLIC_APP_URL) to be set.",
|
|
63
70
|
requires: ["web"],
|
|
64
71
|
dependencies: { "@warlock.js/sitemap": INSTALLED_WARLOCK_VERSION },
|
|
65
|
-
|
|
66
|
-
content: sitemapConfigStub,
|
|
67
|
-
name: "sitemap"
|
|
68
|
-
},
|
|
69
|
-
onExecuting: registerSitemapConnector
|
|
72
|
+
onExecuting: installSitemapConfig
|
|
70
73
|
};
|
|
71
74
|
|
|
72
75
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sitemap.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/sitemap.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport {
|
|
1
|
+
{"version":3,"file":"sitemap.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/sitemap.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport { srcPath } from \"../../utils\";\nimport { mergeWebSitemapConfig } from \"./shared/merge-web-sitemap-config\";\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\nconst NEXT_STEPS =\n \"Next: set `app.publicUrl` (src/config/app.ts) or the PUBLIC_APP_URL environment \" +\n \"variable, then flip `sitemap.enabled` to true in src/config/web.ts.\";\n\n/**\n * Ships DISABLED because a sitemap needs this application's public origin and\n * a freshly generated app has no way to know it. Two steps to turn it on:\n *\n * 1. set `app.publicUrl` in src/config/app.ts, or the PUBLIC_APP_URL\n * environment variable;\n * 2. flip `sitemap.enabled` to true here.\n *\n * With it enabled and no origin configured, generation REFUSES rather than\n * serving absolute URLs built from a guessed host — a sitemap pointing at\n * the wrong domain is worse than one that never starts, because nothing\n * downstream reports it.\n */\nconst freshWebConfigStub = `import type { WebSitemapConfig } from \"@warlock.js/web/sitemap\";\n\nconst webConfig: { sitemap: WebSitemapConfig } = {\n sitemap: {\n enabled: false,\n path: \"/sitemap.xml\",\n defaults: { changefreq: \"weekly\", priority: 0.5 },\n },\n};\n\nexport default webConfig;\n`;\n\n/**\n * `warlock add sitemap` writes its policy into the app's \\`src/config/web.ts\\`,\n * under the \\`sitemap\\` key — there is no \\`src/config/sitemap.ts\\` and no\n * standalone connector to register; \\`@warlock.js/web\\` (already installed via\n * the \\`web\\` requirement) reads \\`web.sitemap\\` itself.\n *\n * Creates \\`web.ts\\` when the app does not have one yet; otherwise merges a\n * \\`sitemap\\` section into whatever is already there rather than clobbering it.\n */\nasync function installSitemapConfig(): Promise<void> {\n const configPath = srcPath(\"config/web.ts\");\n\n if (!(await fileExistsAsync(configPath))) {\n await putFileAsync(configPath, freshWebConfigStub);\n console.log(`${colors.green(\"✓\")} Created src/config/web.ts with a disabled sitemap section`);\n console.log(NEXT_STEPS);\n\n return;\n }\n\n const current = await getFileAsync(configPath);\n const merge = mergeWebSitemapConfig(current);\n\n if (merge.status === \"already-present\") {\n console.log(\n `${colors.yellowBright(\"sitemap\")} already configured in src/config/web.ts, skipping...`,\n );\n\n return;\n }\n\n if (merge.status === \"unrecognised\") {\n console.log(\n `${colors.redBright(\"✗\")} Could not safely add a \\`sitemap\\` section to ` +\n `${colors.yellowBright(\"src/config/web.ts\")} — its exported config object was not ` +\n \"recognised, so nothing was written. Add it yourself:\\n\" +\n ' sitemap: { enabled: false, path: \"/sitemap.xml\", defaults: { changefreq: \"weekly\", priority: 0.5 } },',\n );\n\n process.exitCode = 1;\n\n return;\n }\n\n await putFileAsync(configPath, merge.next);\n console.log(`${colors.green(\"✓\")} Added a disabled \\`sitemap\\` section to src/config/web.ts`);\n console.log(NEXT_STEPS);\n}\n\n/** `warlock add sitemap` — runtime sitemap.xml generation, backed by the page registry. */\nexport const sitemapFeature: FeatureDefinition = {\n description:\n \"Installs @warlock.js/sitemap — runtime sitemap.xml generation from the page registry. \" +\n \"Adds a disabled `sitemap` section to src/config/web.ts, creating the file if it doesn't \" +\n \"exist yet. Requires app.publicUrl (or PUBLIC_APP_URL) to be set.\",\n requires: [\"web\"],\n dependencies: {\n \"@warlock.js/sitemap\": INSTALLED_WARLOCK_VERSION,\n },\n onExecuting: installSitemapConfig,\n};\n"],"mappings":";;;;;;;;AAMA,MAAM,aACJ;;;;;;;;;;;;;;AAgBF,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;AAsB3B,eAAe,uBAAsC;CACnD,MAAM,aAAa,QAAQ,eAAe;CAE1C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;EACxC,MAAM,aAAa,YAAY,kBAAkB;EACjD,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,2DAA2D;EAC5F,QAAQ,IAAI,UAAU;EAEtB;CACF;CAGA,MAAM,QAAQ,sBAAsB,MADd,aAAa,UAAU,CACF;CAE3C,IAAI,MAAM,WAAW,mBAAmB;EACtC,QAAQ,IACN,GAAG,OAAO,aAAa,SAAS,EAAE,sDACpC;EAEA;CACF;CAEA,IAAI,MAAM,WAAW,gBAAgB;EACnC,QAAQ,IACN,GAAG,OAAO,UAAU,GAAG,EAAE,iDACpB,OAAO,aAAa,mBAAmB,EAAE;wGAGhD;EAEA,QAAQ,WAAW;EAEnB;CACF;CAEA,MAAM,aAAa,YAAY,MAAM,IAAI;CACzC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,2DAA2D;CAC5F,QAAQ,IAAI,UAAU;AACxB;;AAGA,MAAa,iBAAoC;CAC/C,aACE;CAGF,UAAU,CAAC,KAAK;CAChB,cAAc,EACZ,uBAAuB,0BACzB;CACA,aAAa;AACf"}
|
package/esm/http/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PipeableReactStream, StreamReactResponseOptions, streamReactResponse } from "./stream-react-response.mjs";
|
|
2
|
+
import { XMLable } from "./xmlable.mjs";
|
|
2
3
|
import { CookieOptions, Response, ResponseStatus, SendBufferOptions, SendFileOptions } from "./response.mjs";
|
|
3
4
|
import { FileNamingStrategy, ImageTransformCallback, ImageTransformConfig, PrefixConfig, PrefixOptions, SaveAsOptions, SaveOptions, UploadedFileImageOptions, UploadsConfigurations } from "./uploads-types.mjs";
|
|
4
5
|
import { FileValidationOptions, UploadedFile, UploadedFileJson } from "./uploaded-file.mjs";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region ../core/src/http/parse-urlencoded-body.ts
|
|
2
|
+
/**
|
|
3
|
+
* Decode an `application/x-www-form-urlencoded` request body into a plain
|
|
4
|
+
* object, the shape `request.ts`'s `parseBody` already expects from JSON and
|
|
5
|
+
* multipart bodies.
|
|
6
|
+
*
|
|
7
|
+
* Repeated keys become arrays (`code=a&code=b` -> `{ code: ["a", "b"] }`);
|
|
8
|
+
* every other key stays a single string. Bracket-notation keys (`a[b]=1`) are
|
|
9
|
+
* NOT expanded here — they are handed through as literal keys and, like any
|
|
10
|
+
* JSON or query-string key of that shape, are expanded later by the shared
|
|
11
|
+
* `parseBody` nesting logic in `request.ts`. This function's only job is the
|
|
12
|
+
* urlencoded -> flat-object step; it does not duplicate that logic.
|
|
13
|
+
*/
|
|
14
|
+
function parseUrlencodedBody(raw) {
|
|
15
|
+
const params = new URLSearchParams(raw);
|
|
16
|
+
const body = {};
|
|
17
|
+
for (const key of params.keys()) {
|
|
18
|
+
if (Object.prototype.hasOwnProperty.call(body, key)) continue;
|
|
19
|
+
const values = params.getAll(key);
|
|
20
|
+
body[key] = values.length > 1 ? values : values[0] ?? "";
|
|
21
|
+
}
|
|
22
|
+
return body;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { parseUrlencodedBody };
|
|
27
|
+
//# sourceMappingURL=parse-urlencoded-body.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-urlencoded-body.mjs","names":[],"sources":["../../../../../../../core/src/http/parse-urlencoded-body.ts"],"sourcesContent":["/**\n * Decode an `application/x-www-form-urlencoded` request body into a plain\n * object, the shape `request.ts`'s `parseBody` already expects from JSON and\n * multipart bodies.\n *\n * Repeated keys become arrays (`code=a&code=b` -> `{ code: [\"a\", \"b\"] }`);\n * every other key stays a single string. Bracket-notation keys (`a[b]=1`) are\n * NOT expanded here — they are handed through as literal keys and, like any\n * JSON or query-string key of that shape, are expanded later by the shared\n * `parseBody` nesting logic in `request.ts`. This function's only job is the\n * urlencoded -> flat-object step; it does not duplicate that logic.\n */\nexport function parseUrlencodedBody(raw: string): Record<string, string | string[]> {\n const params = new URLSearchParams(raw);\n const body: Record<string, string | string[]> = {};\n\n for (const key of params.keys()) {\n if (Object.prototype.hasOwnProperty.call(body, key)) continue;\n\n const values = params.getAll(key);\n\n body[key] = values.length > 1 ? values : (values[0] ?? \"\");\n }\n\n return body;\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAgB,oBAAoB,KAAgD;CAClF,MAAM,SAAS,IAAI,gBAAgB,GAAG;CACtC,MAAM,OAA0C,CAAC;CAEjD,KAAK,MAAM,OAAO,OAAO,KAAK,GAAG;EAC/B,IAAI,OAAO,UAAU,eAAe,KAAK,MAAM,GAAG,GAAG;EAErD,MAAM,SAAS,OAAO,OAAO,GAAG;EAEhC,KAAK,OAAO,OAAO,SAAS,IAAI,SAAU,OAAO,MAAM;CACzD;CAEA,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugins.d.mts","names":[],"sources":["../../../../../../../core/src/http/plugins.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"plugins.d.mts","names":[],"sources":["../../../../../../../core/src/http/plugins.ts"],"mappings":";;;iBAQsB,mBAAA,CAAoB,MAAA,EAAQ,eAAA,GAAe,OAAA"}
|
package/esm/http/plugins.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { rootPath } from "../utils/paths.mjs";
|
|
2
2
|
import "../utils/index.mjs";
|
|
3
3
|
import { buildCorsOptions } from "./build-cors-options.mjs";
|
|
4
|
+
import { parseUrlencodedBody } from "./parse-urlencoded-body.mjs";
|
|
4
5
|
import config from "@mongez/config";
|
|
5
6
|
import fastifyMultipart from "@fastify/multipart";
|
|
6
7
|
|
|
@@ -15,6 +16,13 @@ async function registerHttpPlugins(server) {
|
|
|
15
16
|
attachFieldsToBody: true,
|
|
16
17
|
limits: { fileSize: config.get("http.fileUploadLimit", 10 * 1024 * 1024) }
|
|
17
18
|
});
|
|
19
|
+
server.addContentTypeParser("application/x-www-form-urlencoded", { parseAs: "string" }, (_request, body, done) => {
|
|
20
|
+
try {
|
|
21
|
+
done(null, parseUrlencodedBody(body));
|
|
22
|
+
} catch (error) {
|
|
23
|
+
done(error, void 0);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
18
26
|
server.register(import("@fastify/static"), {
|
|
19
27
|
root: config.get("storage.publicRoot", rootPath("public")),
|
|
20
28
|
prefix: config.get("storage.publicPrefix", "/public/")
|
package/esm/http/plugins.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugins.mjs","names":[],"sources":["../../../../../../../core/src/http/plugins.ts"],"sourcesContent":["import fastifyMultipart from \"@fastify/multipart\";\nimport config from \"@mongez/config\";\nimport { rootPath } from \"../utils\";\nimport { buildCorsOptions } from \"./build-cors-options\";\nimport type { FastifyInstance } from \"./server\";\n\nexport async function registerHttpPlugins(server: FastifyInstance) {\n // 👇🏻 register rate-limit plugin\n server.register(import(\"@fastify/rate-limit\"), {\n // max requests per time window\n max: config.get(\"http.rateLimit.max\", 60),\n // maximum time that is will allow max requests\n timeWindow: config.get(\"http.rateLimit.duration\", 60 * 1000),\n });\n\n // 👇🏻 register cors plugin\n server.register(import(\"@fastify/cors\"), buildCorsOptions());\n\n // 👇🏻 import multipart plugin\n server.register(fastifyMultipart, {\n attachFieldsToBody: true,\n limits: {\n // file size could be up to 10MB\n fileSize: config.get(\"http.fileUploadLimit\", 10 * 1024 * 1024),\n },\n });\n\n server.register(import(\"@fastify/static\"), {\n root: config.get(\"storage.publicRoot\", rootPath(\"public\")),\n prefix: config.get(\"storage.publicPrefix\", \"/public/\"),\n });\n\n // 👇🏻 register cookie plugin\n server.register(import(\"@fastify/cookie\"), {\n secret: config.get(\"http.cookies.secret\"), // Optional: allow signed cookies\n parseOptions: config.get(\"http.cookies.options\", {}),\n });\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugins.mjs","names":[],"sources":["../../../../../../../core/src/http/plugins.ts"],"sourcesContent":["import fastifyMultipart from \"@fastify/multipart\";\nimport config from \"@mongez/config\";\nimport type { FastifyRequest } from \"fastify\";\nimport { rootPath } from \"../utils\";\nimport { buildCorsOptions } from \"./build-cors-options\";\nimport { parseUrlencodedBody } from \"./parse-urlencoded-body\";\nimport type { FastifyInstance } from \"./server\";\n\nexport async function registerHttpPlugins(server: FastifyInstance) {\n // 👇🏻 register rate-limit plugin\n server.register(import(\"@fastify/rate-limit\"), {\n // max requests per time window\n max: config.get(\"http.rateLimit.max\", 60),\n // maximum time that is will allow max requests\n timeWindow: config.get(\"http.rateLimit.duration\", 60 * 1000),\n });\n\n // 👇🏻 register cors plugin\n server.register(import(\"@fastify/cors\"), buildCorsOptions());\n\n // 👇🏻 import multipart plugin\n server.register(fastifyMultipart, {\n attachFieldsToBody: true,\n limits: {\n // file size could be up to 10MB\n fileSize: config.get(\"http.fileUploadLimit\", 10 * 1024 * 1024),\n },\n });\n\n // 👇🏻 parse application/x-www-form-urlencoded bodies (Apple's OAuth\n // `form_post` callback, plain HTML forms). Fastify only parses JSON and\n // text natively, so without this every urlencoded POST is rejected with\n // FST_ERR_CTP_INVALID_MEDIA_TYPE before it reaches a route.\n //\n // No `bodyLimit` option here: Fastify falls back to the server's own\n // `bodyLimit` (`server.ts`, from `http.bodyLimit`) whenever a content-type\n // parser doesn't set one — the exact limit JSON bodies are already held to.\n // Reading `http.bodyLimit` again here would duplicate that lookup and risk\n // the two silently drifting.\n server.addContentTypeParser(\n \"application/x-www-form-urlencoded\",\n { parseAs: \"string\" },\n (_request: FastifyRequest, body: string, done: (err: Error | null, body?: any) => void) => {\n try {\n done(null, parseUrlencodedBody(body));\n } catch (error) {\n done(error as Error, undefined);\n }\n },\n );\n\n server.register(import(\"@fastify/static\"), {\n root: config.get(\"storage.publicRoot\", rootPath(\"public\")),\n prefix: config.get(\"storage.publicPrefix\", \"/public/\"),\n });\n\n // 👇🏻 register cookie plugin\n server.register(import(\"@fastify/cookie\"), {\n secret: config.get(\"http.cookies.secret\"), // Optional: allow signed cookies\n parseOptions: config.get(\"http.cookies.options\", {}),\n });\n}\n"],"mappings":";;;;;;;;AAQA,eAAsB,oBAAoB,QAAyB;CAEjE,OAAO,SAAS,OAAO,wBAAwB;EAE7C,KAAK,OAAO,IAAI,sBAAsB,EAAE;EAExC,YAAY,OAAO,IAAI,2BAA2B,KAAK,GAAI;CAC7D,CAAC;CAGD,OAAO,SAAS,OAAO,kBAAkB,iBAAiB,CAAC;CAG3D,OAAO,SAAS,kBAAkB;EAChC,oBAAoB;EACpB,QAAQ,EAEN,UAAU,OAAO,IAAI,wBAAwB,KAAK,OAAO,IAAI,EAC/D;CACF,CAAC;CAYD,OAAO,qBACL,qCACA,EAAE,SAAS,SAAS,IACnB,UAA0B,MAAc,SAAkD;EACzF,IAAI;GACF,KAAK,MAAM,oBAAoB,IAAI,CAAC;EACtC,SAAS,OAAO;GACd,KAAK,OAAgB,MAAS;EAChC;CACF,CACF;CAEA,OAAO,SAAS,OAAO,oBAAoB;EACzC,MAAM,OAAO,IAAI,sBAAsB,SAAS,QAAQ,CAAC;EACzD,QAAQ,OAAO,IAAI,wBAAwB,UAAU;CACvD,CAAC;CAGD,OAAO,SAAS,OAAO,oBAAoB;EACzC,QAAQ,OAAO,IAAI,qBAAqB;EACxC,cAAc,OAAO,IAAI,wBAAwB,CAAC,CAAC;CACrD,CAAC;AACH"}
|
package/esm/http/response.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { StorageFile } from "../storage/storage-file.mjs";
|
|
2
2
|
import { PipeableReactStream } from "./stream-react-response.mjs";
|
|
3
|
+
import { XMLable } from "./xmlable.mjs";
|
|
3
4
|
import { Request } from "./request.mjs";
|
|
4
5
|
import { ResponseEvent, ResponseSSEController, ResponseStreamController } from "./types.mjs";
|
|
5
6
|
import { Route } from "../router/types.mjs";
|
|
@@ -230,9 +231,15 @@ declare class Response {
|
|
|
230
231
|
*/
|
|
231
232
|
render(element: React.ReactElement | React.ComponentType, status?: number): Promise<Response>;
|
|
232
233
|
/**
|
|
233
|
-
* Send xml response
|
|
234
|
+
* Send an xml response.
|
|
235
|
+
*
|
|
236
|
+
* Accepts either a raw XML string or an `XMLable` — anything with a
|
|
237
|
+
* `toXML()` method, structurally typed so packages like
|
|
238
|
+
* `@warlock.js/sitemap` never need to import core. This is the BOUNDED
|
|
239
|
+
* path only: a very large document should be streamed from generated
|
|
240
|
+
* files instead of passed through here.
|
|
234
241
|
*/
|
|
235
|
-
xml(
|
|
242
|
+
xml(body: string | XMLable, statusCode?: number): Promise<Response>;
|
|
236
243
|
/**
|
|
237
244
|
* Send plain text response
|
|
238
245
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response.d.mts","names":[],"sources":["../../../../../../../core/src/http/response.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"response.d.mts","names":[],"sources":["../../../../../../../core/src/http/response.ts"],"mappings":"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"}
|
package/esm/http/response.mjs
CHANGED
|
@@ -304,10 +304,18 @@ var Response = class Response {
|
|
|
304
304
|
return this.setStatusCode(status).html(renderReact(element));
|
|
305
305
|
}
|
|
306
306
|
/**
|
|
307
|
-
* Send xml response
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
307
|
+
* Send an xml response.
|
|
308
|
+
*
|
|
309
|
+
* Accepts either a raw XML string or an `XMLable` — anything with a
|
|
310
|
+
* `toXML()` method, structurally typed so packages like
|
|
311
|
+
* `@warlock.js/sitemap` never need to import core. This is the BOUNDED
|
|
312
|
+
* path only: a very large document should be streamed from generated
|
|
313
|
+
* files instead of passed through here.
|
|
314
|
+
*/
|
|
315
|
+
xml(body, statusCode) {
|
|
316
|
+
if (typeof body === "string") return this.setContentType("application/xml").send(body, statusCode);
|
|
317
|
+
if (typeof body?.toXML !== "function") throw new TypeError("response.xml() expects a string or an XMLable value (an object with a toXML() method).");
|
|
318
|
+
return this.setContentType("application/xml").send(body.toXML(), statusCode);
|
|
311
319
|
}
|
|
312
320
|
/**
|
|
313
321
|
* Send plain text response
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response.mjs","names":[],"sources":["../../../../../../../core/src/http/response.ts"],"sourcesContent":["import type { CookieSerializeOptions } from \"@fastify/cookie\";\r\nimport type { OutgoingHttpHeaders } from \"node:http\";\r\nimport config from \"@mongez/config\";\r\nimport type { EventSubscription } from \"@mongez/events\";\r\nimport events from \"@mongez/events\";\r\nimport { fileExistsAsync } from \"@warlock.js/fs\";\r\nimport { isIterable, isPlainObject, isScalar } from \"@mongez/supportive-is\";\r\nimport type { LogLevel } from \"@warlock.js/logger\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport type { ValidationResult } from \"@warlock.js/seal\";\r\nimport type { FastifyReply } from \"fastify\";\r\nimport fs from \"fs\";\r\nimport mime from \"mime\";\r\nimport path from \"path\";\r\nimport type React from \"react\";\r\nimport { type ReactNode } from \"react\";\r\nimport { Application } from \"../application/application\";\r\nimport { LOCALE_COOKIE_NAME, resolveLocaleConfiguration } from \"../config/locale-configuration\";\r\nimport { UnknownLocaleError } from \"../errors/unknown-locale-error\";\r\nimport type { Route } from \"../router\";\r\nimport { StorageFile } from \"../storage\";\r\nimport { renderReact } from \"./../react\";\r\nimport type { Request } from \"./request\";\r\nimport { streamReactResponse, type PipeableReactStream } from \"./stream-react-response\";\r\nimport type { ResponseEvent, ResponseSSEController, ResponseStreamController } from \"./types\";\r\n\r\ntype CookieValue = string | number | boolean | Record<string, any> | Array<any>;\r\n\r\n/**\r\n * Cookie options accepted by `response.cookie()`.\r\n *\r\n * Extends Fastify's `CookieSerializeOptions` with `raw` — set to `true` to\r\n * skip the default `JSON.stringify` of the value and write it as-is. Use for\r\n * plain-string cookies (session tokens, opaque IDs) that shouldn't be JSON-quoted.\r\n *\r\n * When `raw: true`, non-string values are coerced via `String(value)`. The\r\n * read side (`request.cookie(name)`) tries `JSON.parse` first and falls back\r\n * to the raw string on parse failure, so round-tripping a raw string cookie\r\n * Just Works.\r\n */\r\nexport type CookieOptions = CookieSerializeOptions & {\r\n /**\r\n * Skip JSON.stringify and write the value as-is.\r\n *\r\n * @default false\r\n */\r\n raw?: boolean;\r\n};\r\n\r\nexport enum ResponseStatus {\r\n OK = 200,\r\n CREATED = 201,\r\n ACCEPTED = 202,\r\n MOVED_PERMANENTLY = 301,\r\n FOUND = 302,\r\n SEE_OTHER = 303,\r\n NOT_MODIFIED = 304,\r\n TEMPORARY_REDIRECT = 307,\r\n PERMANENT_REDIRECT = 308,\r\n NO_CONTENT = 204,\r\n BAD_REQUEST = 400,\r\n UNAUTHORIZED = 401,\r\n FORBIDDEN = 403,\r\n NOT_FOUND = 404,\r\n METHOD_NOT_ALLOWED = 405,\r\n CONFLICT = 409,\r\n TOO_MANY_REQUESTS = 429,\r\n INTERNAL_SERVER_ERROR = 500,\r\n SERVICE_UNAVAILABLE = 503,\r\n}\r\n\r\n/**\r\n * Options for sending files\r\n */\r\nexport type SendFileOptions = {\r\n cacheTime?: number;\r\n immutable?: boolean;\r\n inline?: boolean;\r\n filename?: string;\r\n};\r\n\r\n/**\r\n * Options for sending buffers\r\n */\r\nexport type SendBufferOptions = SendFileOptions & {\r\n contentType?: string;\r\n etag?: string;\r\n};\r\n\r\n/**\r\n * The cookie flags every response cookie gets unless something overrides them.\r\n *\r\n * Computed per call rather than hoisted to a constant because `secure` depends\r\n * on the environment, which is not known at module-evaluation time.\r\n *\r\n * `secure` is relaxed in development only: a `Secure` cookie is dropped by the\r\n * browser over plain http, which would silently break every local login. It\r\n * stays on everywhere else, including test and staging.\r\n */\r\nfunction secureCookieDefaults(): CookieSerializeOptions {\r\n return {\r\n httpOnly: true,\r\n sameSite: \"lax\",\r\n secure: !Application.isDevelopment,\r\n };\r\n}\r\n\r\nexport class Response {\r\n /**\r\n * Current route\r\n */\r\n protected route!: Route;\r\n\r\n /**\r\n * Underlying Fastify reply — a public escape hatch to capabilities the\r\n * framework's high-level helpers don't yet cover.\r\n *\r\n * **Prefer framework methods first**: `response.send()`, `response.header()`,\r\n * `response.cookie()`, `response.sendFile()`, `response.stream()`, etc.\r\n * They wire status codes, content-type detection, the event lifecycle, and\r\n * the cache-pattern replay path correctly.\r\n *\r\n * **Reach for `baseResponse` only** when the framework genuinely lacks a\r\n * helper for what you need — and when you do, file an issue so we can add\r\n * it. Streaming and SSE are the precedent here: they bypass `send()`\r\n * deliberately because the framework didn't ship chunked-write support\r\n * natively at the time. Reaching here for non-streaming work means a\r\n * missing helper, not an answer.\r\n */\r\n public baseResponse!: FastifyReply;\r\n\r\n /**\r\n * Current status code\r\n */\r\n protected currentStatusCode = 200;\r\n\r\n /**\r\n * Current response body\r\n */\r\n protected currentBody: any;\r\n\r\n /**\r\n * Request object\r\n */\r\n public request!: Request;\r\n\r\n /**\r\n * Internal events related to this particular response object\r\n */\r\n protected events = new Map<string, any[]>();\r\n\r\n /**\r\n * Parsed body\r\n * This will return the parsed body of the response\r\n * Please note that if this property is called before the response is sent, it will return undefined\r\n */\r\n public parsedBody: any;\r\n\r\n /**\r\n * Get raw response\r\n */\r\n public get raw() {\r\n return this.baseResponse.raw;\r\n }\r\n\r\n /**\r\n * Get Current response body\r\n */\r\n public get body() {\r\n return this.currentBody;\r\n }\r\n\r\n /**\r\n * Set response body\r\n */\r\n public set body(body: any) {\r\n this.currentBody = body;\r\n }\r\n\r\n /**\r\n * Add event on sending response\r\n */\r\n public onSending(callback: any) {\r\n this.events.set(\"sending\", [...(this.events.get(\"sending\") || []), callback]);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Add event on sent response\r\n */\r\n public onSent(callback: any) {\r\n this.events.set(\"sent\", [...(this.events.get(\"sent\") || []), callback]);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set the Fastify response object\r\n */\r\n public setResponse(response: FastifyReply) {\r\n this.baseResponse = response;\r\n\r\n // Listen to the 'finish' event to track when response is fully sent\r\n // This works for all response types: JSON, streams, buffers, files, etc.\r\n this.baseResponse.raw.once(\"finish\", () => {\r\n this.request.endTime = Date.now();\r\n });\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Reset the response state\r\n */\r\n public reset() {\r\n this.route = {} as Route;\r\n this.currentBody = null;\r\n this.currentStatusCode = 200;\r\n }\r\n\r\n /**\r\n * Set current route\r\n */\r\n public setRoute(route: Route) {\r\n this.route = route;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get the content type\r\n */\r\n public get contentType() {\r\n return this.baseResponse.getHeader(\"Content-Type\");\r\n }\r\n\r\n /**\r\n * Set the content type\r\n */\r\n public setContentType(contentType: string) {\r\n this.baseResponse.header(\"Content-Type\", contentType);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get the status code\r\n */\r\n public get statusCode(): number {\r\n return this.currentStatusCode ?? this.baseResponse.statusCode;\r\n }\r\n\r\n /**\r\n * Check if response status is ok\r\n */\r\n public get isOk() {\r\n return this.currentStatusCode >= 200 && this.currentStatusCode < 300;\r\n }\r\n\r\n /**\r\n * Check if the response has been sent\r\n */\r\n public get sent() {\r\n return this.baseResponse.sent;\r\n }\r\n\r\n /**\r\n * Add a listener to the response event\r\n */\r\n public static on(\r\n event: ResponseEvent,\r\n listener: (response: Response) => void,\r\n ): EventSubscription {\r\n return events.subscribe(`response.${event}`, listener);\r\n }\r\n\r\n /**\r\n * Trigger the response event\r\n */\r\n protected static async trigger(event: ResponseEvent, ...args: any[]) {\r\n // make a timeout to make sure the request events is executed first\r\n return new Promise((resolve) => {\r\n setTimeout(async () => {\r\n await events.triggerAllAsync(`response.${event}`, ...args);\r\n resolve(true);\r\n }, 0);\r\n });\r\n }\r\n\r\n /**\r\n * Parse body\r\n */\r\n protected async parseBody() {\r\n return await this.parse(this.currentBody);\r\n }\r\n\r\n /**\r\n * Parse the given value\r\n */\r\n public async parse(value: any): Promise<any> {\r\n // if it is a falsy value, return it\r\n if (!value || isScalar(value)) return value;\r\n\r\n // if it has a `toJSON` method, call it and await the result then return it\r\n if (value.toJSON) {\r\n value.request = this.request;\r\n return await value.toJSON();\r\n }\r\n\r\n // if it is iterable, an array or array-like object then parse each item\r\n if (isIterable(value)) {\r\n const values = Array.from(value);\r\n\r\n return Promise.all(\r\n values.map(async (item: any) => {\r\n return await this.parse(item);\r\n }),\r\n );\r\n }\r\n\r\n // if not plain object, then return it\r\n if (!isPlainObject(value)) {\r\n return value;\r\n }\r\n\r\n // loop over the object and check if the value and call `parse` on it\r\n for (const key in value) {\r\n const subValue = value[key];\r\n\r\n value[key] = await this.parse(subValue);\r\n }\r\n\r\n return value;\r\n }\r\n\r\n /**\r\n * Make a log message\r\n */\r\n public log(message: string, level: LogLevel = \"info\") {\r\n if (!config.get(\"http.log\")) return;\r\n\r\n log.log({\r\n module: \"response\",\r\n action: this.route.method + \" \" + this.route.path.replace(\"/*\", \"\") + `:${this.request.id}`,\r\n message,\r\n type: level,\r\n context: {\r\n request: this.request,\r\n response: this,\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Check if returning response is json\r\n */\r\n public get isJson() {\r\n return this.getHeader(\"Content-Type\") === \"application/json\";\r\n }\r\n\r\n /**\r\n * Send the response\r\n * @param data - Response data\r\n * @param statusCode - HTTP status code\r\n * @param triggerEvents - Whether to trigger response events (default: true)\r\n */\r\n public async send(data?: any, statusCode?: number, triggerEvents = true): Promise<Response> {\r\n // Defensive guard against double-send. The underlying Fastify reply silently\r\n // ignores subsequent sends once `sent === true`, which has historically hidden\r\n // middleware bugs (cache-pattern replay paths that returned `baseResponse.send`\r\n // ended up re-entering `Response.send` with the FastifyReply as the body).\r\n // Surfacing the misuse via `error`-level log makes the bug loud without\r\n // crashing production traffic.\r\n if (this.baseResponse.sent) {\r\n log.error(\r\n \"response\",\r\n \"send\",\r\n `send() called on already-sent response (request:${this.request?.id ?? \"unknown\"}) — likely a middleware bug`,\r\n );\r\n\r\n return this;\r\n }\r\n\r\n if (statusCode) {\r\n this.currentStatusCode = statusCode;\r\n }\r\n\r\n if (data === this) return this;\r\n\r\n if (data) {\r\n this.currentBody = data;\r\n }\r\n\r\n if (!this.currentStatusCode) {\r\n this.currentStatusCode = 200;\r\n }\r\n\r\n this.log(\"Sending response\");\r\n // Auto-pick `application/json` only when no content-type was set by the caller.\r\n // This preserves explicit overrides (e.g. `application/vnd.api+json` from a\r\n // cache replay, `application/problem+json` from an RFC 7807 error response)\r\n // while keeping the convenience default for the common object-body path.\r\n if (Array.isArray(this.currentBody) || isPlainObject(this.currentBody)) {\r\n if (!this.baseResponse.getHeader(\"Content-Type\")) {\r\n this.setContentType(\"application/json\");\r\n }\r\n }\r\n\r\n if (triggerEvents) {\r\n await Response.trigger(\"sending\", this);\r\n\r\n for (const callback of this.events.get(\"sending\") || []) {\r\n await callback(this);\r\n }\r\n\r\n if (this.isJson) {\r\n await Response.trigger(\"sendingJson\", this);\r\n for (const callback of this.events.get(\"sendingJson\") || []) {\r\n await callback(this);\r\n }\r\n\r\n if (this.isOk) {\r\n await Response.trigger(\"sendingSuccessJson\", this);\r\n for (const callback of this.events.get(\"sendingSuccessJson\") || []) {\r\n await callback(this);\r\n }\r\n }\r\n }\r\n }\r\n\r\n // parse the body and make sure it is transformed to sync data instead of async data\r\n if (typeof this.currentBody !== \"string\") {\r\n this.parsedBody = await this.parseBody();\r\n } else {\r\n this.parsedBody = data;\r\n }\r\n\r\n // Set the status first\r\n this.baseResponse.status(this.currentStatusCode);\r\n\r\n // Then send the response with the parsed body\r\n await this.baseResponse.send(this.parsedBody);\r\n\r\n this.log(\"Response sent\");\r\n\r\n if (triggerEvents) {\r\n // trigger the sent event\r\n Response.trigger(\"sent\", this);\r\n\r\n for (const callback of this.events.get(\"sent\") || []) {\r\n callback(this);\r\n }\r\n\r\n // trigger the success event if the status code is 2xx\r\n if (this.currentStatusCode >= 200 && this.currentStatusCode < 300) {\r\n Response.trigger(\"success\", this);\r\n }\r\n\r\n // trigger the successCreate event if the status code is 201\r\n if (this.currentStatusCode === 201) {\r\n Response.trigger(\"successCreate\", this);\r\n }\r\n\r\n // trigger the badRequest event if the status code is 400\r\n if (this.currentStatusCode === 400) {\r\n Response.trigger(\"badRequest\", this);\r\n }\r\n\r\n // trigger the unauthorized event if the status code is 401\r\n if (this.currentStatusCode === 401) {\r\n Response.trigger(\"unauthorized\", this);\r\n }\r\n\r\n // trigger the forbidden event if the status code is 403\r\n if (this.currentStatusCode === 403) {\r\n Response.trigger(\"forbidden\", this);\r\n }\r\n\r\n // trigger the notFound event if the status code is 404\r\n if (this.currentStatusCode === 404) {\r\n Response.trigger(\"notFound\", this);\r\n }\r\n\r\n // trigger the content too large event if the status code is 413\r\n if (this.currentStatusCode === 413) {\r\n Response.trigger(\"contentTooLarge\", this);\r\n }\r\n\r\n // trigger the throttled event if the status code is 429\r\n if (this.currentStatusCode === 429) {\r\n Response.trigger(\"throttled\", this);\r\n }\r\n\r\n // trigger the serverError event if the status code is 500\r\n if (this.currentStatusCode === 500) {\r\n Response.trigger(\"serverError\", this);\r\n }\r\n\r\n // trigger the error event if the status code is 4xx or 5xx\r\n if (this.currentStatusCode >= 400) {\r\n Response.trigger(\"error\", this);\r\n }\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Replay a previously-captured response shape — used by cache-pattern\r\n * middlewares (idempotency, response cache) to send a cached response\r\n * without re-running the controller.\r\n *\r\n * Preserves the cached status code, content-type, and any extra headers,\r\n * then sends the body through the standard `send()` pipeline so the full\r\n * event lifecycle still fires (`sent`, `success`, status-specific events).\r\n * That keeps cross-cutting observers (logger, metrics, audit) consistent\r\n * between fresh and replayed responses.\r\n *\r\n * @example\r\n * // Inside a cache-pattern middleware on HIT:\r\n * return response.header(\"X-Cache\", \"HIT\").replay({\r\n * status: cached.status,\r\n * body: cached.body,\r\n * contentType: cached.contentType,\r\n * });\r\n */\r\n public replay(cached: {\r\n status: number;\r\n body: unknown;\r\n contentType?: string;\r\n headers?: Record<string, string>;\r\n }): Promise<Response> {\r\n this.setStatusCode(cached.status);\r\n\r\n if (cached.contentType) {\r\n this.setContentType(cached.contentType);\r\n }\r\n\r\n if (cached.headers) {\r\n for (const [name, value] of Object.entries(cached.headers)) {\r\n this.header(name, value);\r\n }\r\n }\r\n\r\n return this.send(cached.body);\r\n }\r\n\r\n /**\r\n * Send html response\r\n */\r\n public html(data: string, statusCode?: number) {\r\n return this.setContentType(\"text/html\").send(data, statusCode);\r\n }\r\n\r\n /**\r\n * Render the given react component\r\n */\r\n public render(element: React.ReactElement | React.ComponentType, status = 200) {\r\n return this.setStatusCode(status).html(renderReact(element));\r\n }\r\n\r\n /**\r\n * Send xml response\r\n */\r\n public xml(data: string, statusCode?: number) {\r\n return this.setContentType(\"text/xml\").send(data, statusCode);\r\n }\r\n\r\n /**\r\n * Send plain text response\r\n */\r\n public text(data: string, statusCode?: number) {\r\n return this.setContentType(\"text/plain\").send(data, statusCode);\r\n }\r\n\r\n /**\r\n * Create a streaming response for progressive/chunked data sending\r\n *\r\n * This method allows you to send data in chunks and control when the response ends.\r\n * Perfect for Server-Sent Events (SSE), progressive rendering, or streaming large responses.\r\n *\r\n * @example\r\n * ```ts\r\n * const stream = response.stream(\"text/html\");\r\n * stream.send(\"<html><body>\");\r\n * stream.send(\"<h1>Hello</h1>\");\r\n * stream.render(<MyComponent />);\r\n * stream.send(\"</body></html>\");\r\n * stream.end();\r\n * ```\r\n *\r\n * @param contentType - The content type for the stream (default: \"text/plain\")\r\n * @returns Stream controller with send(), render(), and end() methods\r\n */\r\n public stream(contentType = \"text/plain\"): ResponseStreamController {\r\n // Set headers using the response API\r\n this.setContentType(contentType);\r\n this.header(\"Transfer-Encoding\", \"chunked\");\r\n this.header(\"Cache-Control\", \"no-cache\");\r\n this.header(\"Connection\", \"keep-alive\");\r\n this.header(\"X-Content-Type-Options\", \"nosniff\");\r\n\r\n // Trigger sending events\r\n Response.trigger(\"sending\", this);\r\n for (const callback of this.events.get(\"sending\") || []) {\r\n callback(this);\r\n }\r\n\r\n this.log(\"Starting stream\");\r\n\r\n // Track stream state\r\n let isEnded = false;\r\n const chunks: any[] = [];\r\n\r\n // Write headers to start the stream\r\n // Note: We use raw here because we need chunked encoding control\r\n // This is the only valid use case for bypassing Fastify's abstraction\r\n this.baseResponse.raw.writeHead(this.statusCode, this.getHeaders() as any);\r\n\r\n return {\r\n /**\r\n * Send a chunk of data to the client\r\n * @param data - Data to send (string, Buffer, or any serializable data)\r\n */\r\n send: (data: any) => {\r\n if (isEnded) {\r\n throw new Error(\"Cannot send data: stream has already ended\");\r\n }\r\n\r\n this.baseResponse.raw.write(data);\r\n\r\n return this;\r\n },\r\n\r\n /**\r\n * Render a React component and send it as a chunk\r\n * @param element - React element or component to render\r\n */\r\n render: (element: ReactNode) => {\r\n if (isEnded) {\r\n throw new Error(\"Cannot render: stream has already ended\");\r\n }\r\n\r\n const html = renderReact(element);\r\n chunks.push(html);\r\n this.baseResponse.raw.write(html);\r\n\r\n return this;\r\n },\r\n\r\n /**\r\n * End the stream and trigger completion events\r\n */\r\n end: () => {\r\n if (isEnded) {\r\n return this;\r\n }\r\n\r\n isEnded = true;\r\n\r\n // Store the streamed content for logging/debugging\r\n this.currentBody = chunks;\r\n this.parsedBody = chunks;\r\n\r\n // End the response\r\n this.baseResponse.raw.end();\r\n\r\n this.log(\"Stream ended\");\r\n\r\n // Trigger sent events\r\n Response.trigger(\"sent\", this);\r\n for (const callback of this.events.get(\"sent\") || []) {\r\n callback(this);\r\n }\r\n\r\n // Trigger success event if status is 2xx\r\n if (this.isOk) {\r\n Response.trigger(\"success\", this);\r\n }\r\n\r\n // Trigger status-specific events\r\n if (this.currentStatusCode === 201) {\r\n Response.trigger(\"successCreate\", this);\r\n }\r\n\r\n return this;\r\n },\r\n\r\n /**\r\n * Check if the stream has ended\r\n */\r\n get ended() {\r\n return isEnded;\r\n },\r\n };\r\n }\r\n\r\n /**\r\n * Pipe a React server stream (`renderToPipeableStream`) onto this\r\n * response — the Stage 1 streaming SSR seam. Writes the already-committed\r\n * status and headers, then pipes; aborts the React render if the client\r\n * disconnects before the stream finishes.\r\n *\r\n * This is the ONLY sanctioned way for `@warlock.js/web` to put a React\r\n * stream on the wire: it never touches `response.raw` itself, it calls\r\n * this method, which does (`stream-react-response.ts`).\r\n *\r\n * @example\r\n * ```ts\r\n * const pipeableStream = await renderPageToPipeableStream(element);\r\n * await response.streamReact(pipeableStream);\r\n * ```\r\n */\r\n public streamReact(pipeableStream: PipeableReactStream): Promise<void> {\r\n Response.trigger(\"sending\", this);\r\n for (const callback of this.events.get(\"sending\") || []) {\r\n callback(this);\r\n }\r\n\r\n return streamReactResponse({\r\n raw: this.baseResponse.raw,\r\n statusCode: this.statusCode,\r\n headers: this.getHeaders() as OutgoingHttpHeaders,\r\n pipeableStream,\r\n }).then(() => {\r\n Response.trigger(\"sent\", this);\r\n for (const callback of this.events.get(\"sent\") || []) {\r\n callback(this);\r\n }\r\n\r\n if (this.isOk) {\r\n Response.trigger(\"success\", this);\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Create a Server-Sent Events (SSE) stream\r\n *\r\n * SSE is a standard for pushing real-time updates from server to client.\r\n * Perfect for live notifications, progress updates, or real-time data feeds.\r\n *\r\n * @example\r\n * ```ts\r\n * const sse = response.sse();\r\n *\r\n * // Send events\r\n * sse.send(\"message\", { text: \"Hello!\" });\r\n * sse.send(\"notification\", { type: \"info\", message: \"Update available\" }, \"msg-123\");\r\n *\r\n * // Keep connection alive\r\n * const keepAlive = setInterval(() => sse.comment(\"ping\"), 30000);\r\n *\r\n * // Clean up when done\r\n * clearInterval(keepAlive);\r\n * sse.end();\r\n * ```\r\n *\r\n * @returns SSE controller with send(), comment(), and end() methods\r\n */\r\n public sse(): ResponseSSEController {\r\n // Set SSE-specific headers\r\n this.setContentType(\"text/event-stream\");\r\n this.header(\"Cache-Control\", \"no-cache, no-store, must-revalidate\");\r\n this.header(\"Connection\", \"keep-alive\");\r\n this.header(\"X-Accel-Buffering\", \"no\"); // Disable nginx buffering\r\n\r\n // Trigger sending events\r\n Response.trigger(\"sending\", this);\r\n for (const callback of this.events.get(\"sending\") || []) {\r\n callback(this);\r\n }\r\n\r\n this.log(\"Starting SSE stream\");\r\n\r\n // Track stream state\r\n let isEnded = false;\r\n const events: any[] = [];\r\n const disconnectHandlers: Array<() => void> = [];\r\n\r\n // Write headers to start the stream\r\n this.baseResponse.raw.writeHead(this.statusCode, this.getHeaders() as any);\r\n\r\n // Detect client disconnect — set isEnded silently and invoke cleanup handlers.\r\n // Without this, background jobs keep writing to a dead socket after the client drops.\r\n this.baseResponse.raw.on(\"close\", () => {\r\n if (!isEnded) {\r\n isEnded = true;\r\n this.log(\"SSE client disconnected\");\r\n for (const handler of disconnectHandlers) {\r\n handler();\r\n }\r\n }\r\n });\r\n\r\n const controller: ResponseSSEController = {\r\n /**\r\n * Send an SSE event\r\n * @param event - Event name (e.g., \"message\", \"chunk\", \"done\")\r\n * @param data - Event data (will be JSON stringified)\r\n * @param id - Optional event ID for client-side Last-Event-ID tracking (reconnect support)\r\n */\r\n send: (event: string, data: any, id?: string): ResponseSSEController => {\r\n // Silent no-op after disconnect — background jobs should not crash when\r\n // the client drops mid-stream. The onDisconnect handler handles cleanup.\r\n if (isEnded) return controller;\r\n\r\n let message = \"\";\r\n if (id) message += `id: ${id}\\n`;\r\n message += `event: ${event}\\n`;\r\n message += `data: ${JSON.stringify(data)}\\n\\n`;\r\n\r\n events.push({ event, data, id });\r\n this.baseResponse.raw.write(message);\r\n\r\n return controller;\r\n },\r\n\r\n /**\r\n * Send a comment (keeps connection alive, invisible to client)\r\n * Useful for preventing timeout on long-lived connections\r\n * @param text - Comment text\r\n */\r\n comment: (text: string): ResponseSSEController => {\r\n // Silent no-op after disconnect\r\n if (isEnded) return controller;\r\n\r\n this.baseResponse.raw.write(`: ${text}\\n\\n`);\r\n\r\n return controller;\r\n },\r\n\r\n /**\r\n * End the SSE stream and trigger completion events\r\n */\r\n end: (): ResponseSSEController => {\r\n if (isEnded) return controller;\r\n\r\n isEnded = true;\r\n\r\n // Store the events for logging/debugging\r\n this.currentBody = events;\r\n this.parsedBody = events;\r\n\r\n // End the response\r\n this.baseResponse.raw.end();\r\n\r\n this.log(\"SSE stream ended\");\r\n\r\n // Trigger sent events\r\n Response.trigger(\"sent\", this);\r\n for (const callback of this.events.get(\"sent\") || []) {\r\n callback(this);\r\n }\r\n\r\n // Trigger success event if status is 2xx\r\n if (this.isOk) {\r\n Response.trigger(\"success\", this);\r\n }\r\n\r\n return controller;\r\n },\r\n\r\n /**\r\n * Register a handler to be called when the client disconnects.\r\n * Use this to clean up EventEmitter listeners, cancel background jobs, etc.\r\n *\r\n * @example\r\n * ```ts\r\n * const sse = response.sse();\r\n * const listener = (chunk) => sse.send(\"chunk\", { chunk });\r\n * eventBus.on(aiMessageId, listener);\r\n * sse.onDisconnect(() => eventBus.off(aiMessageId, listener));\r\n * ```\r\n */\r\n onDisconnect: (handler: () => void): ResponseSSEController => {\r\n disconnectHandlers.push(handler);\r\n return controller;\r\n },\r\n\r\n /**\r\n * Check if the stream has ended (either via end() or client disconnect)\r\n */\r\n get ended() {\r\n return isEnded;\r\n },\r\n };\r\n\r\n return controller;\r\n }\r\n\r\n /**\r\n * Set the status code\r\n */\r\n public setStatusCode(statusCode: number) {\r\n this.currentStatusCode = statusCode;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Redirect the user to another route\r\n */\r\n public redirect(url: string, statusCode = 302) {\r\n this.baseResponse.redirect(url, statusCode);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Permanent redirect\r\n */\r\n public permanentRedirect(url: string) {\r\n this.baseResponse.redirect(url, 301);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get the response time\r\n */\r\n public getResponseTime() {\r\n return this.baseResponse.elapsedTime;\r\n }\r\n\r\n /**\r\n * Remove a specific header\r\n */\r\n public removeHeader(key: string) {\r\n this.baseResponse.removeHeader(key);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get a specific header\r\n */\r\n public getHeader(key: string) {\r\n return this.baseResponse.getHeader(key);\r\n }\r\n\r\n /**\r\n * Get the response headers\r\n */\r\n public getHeaders() {\r\n return this.baseResponse.getHeaders();\r\n }\r\n\r\n /**\r\n * Set multiple headers\r\n */\r\n public headers(headers: Record<string, string>) {\r\n this.baseResponse.headers(headers);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set the response header\r\n */\r\n public header(key: string, value: any) {\r\n this.baseResponse.header(key, value);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set a cookie on the response.\r\n *\r\n * Values are JSON-stringified by default so structured cookies round-trip\r\n * cleanly with `request.cookie(name)`. Pass `{ raw: true }` to skip the\r\n * JSON wrapping for plain-string cookies (session tokens, opaque IDs).\r\n *\r\n * **Secure by default.** `httpOnly: true`, `sameSite: \"lax\"`, and — outside\r\n * development — `secure: true` are applied unless you override them. These\r\n * are the flags whose absence never fails a test and is fatal in production:\r\n * without `httpOnly` any injected script can read the cookie, without\r\n * `secure` it travels in cleartext, without `sameSite` it rides along on\r\n * cross-site requests. Opting out is explicit, per call or via\r\n * `http.cookies.options`.\r\n *\r\n * Precedence, lowest to highest: framework defaults → `http.cookies.options`\r\n * → the per-call `options` argument.\r\n *\r\n * @example\r\n * // JSON-wrapped (default) — round-trips with request.cookie()\r\n * response.cookie(\"prefs\", { theme: \"dark\" }, { maxAge: 3600 });\r\n *\r\n * @example\r\n * // Raw string — no JSON quoting; useful for tokens / opaque IDs\r\n * response.cookie(\"session\", \"abc.def.ghi\", { raw: true });\r\n *\r\n * @example\r\n * // Deliberately readable by client-side JS\r\n * response.cookie(\"theme\", \"dark\", { httpOnly: false });\r\n */\r\n public cookie(name: string, value: CookieValue, options: CookieOptions = {}) {\r\n const { raw, ...cookieOptions } = options;\r\n const defaultOptions = config.get(\"http.cookies.options\", {});\r\n const serializedValue = raw ? String(value) : JSON.stringify(value);\r\n\r\n this.baseResponse.setCookie(name, serializedValue, {\r\n ...secureCookieDefaults(),\r\n ...defaultOptions,\r\n ...cookieOptions,\r\n });\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set the active locale for subsequent requests.\r\n *\r\n * Writes the SAME cookie `request.locale` reads (`LOCALE_COOKIE_NAME`,\r\n * owned by the framework, never a string an app hardcodes) — the two sides\r\n * are read from one shared constant so they cannot name the cookie\r\n * differently. Written raw (no JSON quoting), matching how\r\n * `Request.resolveLocale()` reads it back.\r\n *\r\n * Any `Set-Cookie` this emits revokes public cacheability for the response,\r\n * including a page that opted into `public, max-age` — a per-visitor\r\n * locale cookie replayed from a shared cache would hand visitor A's locale\r\n * to visitor B.\r\n *\r\n * @throws {UnknownLocaleError} when `locale` is outside the app's\r\n * configured `app.localeCodes` allow-list. Silently accepting an\r\n * unconfigured locale would set a cookie the app can never actually serve.\r\n *\r\n * @example\r\n * response.setLocale(\"ar\");\r\n */\r\n public setLocale(locale: string) {\r\n const { localeCodes } = resolveLocaleConfiguration(\r\n config.get(\"app.localeCode\"),\r\n config.get(\"app.localeCodes\"),\r\n );\r\n\r\n if (localeCodes !== undefined && !localeCodes.includes(locale)) {\r\n throw new UnknownLocaleError(locale, localeCodes);\r\n }\r\n\r\n return this.cookie(LOCALE_COOKIE_NAME, locale, { raw: true });\r\n }\r\n\r\n /**\r\n * Clear a cookie from the response\r\n *\r\n * @example\r\n * response.clearCookie('token', { path: '/' });\r\n */\r\n public clearCookie(name: string, options?: CookieSerializeOptions) {\r\n const defaultOptions = config.get(\"http.cookies.options\", {});\r\n this.baseResponse.clearCookie(name, { ...defaultOptions, ...options });\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Clear every cookie NAME the current request sent — best-effort, not\r\n * exhaustive: HTTP gives the server no way to discover a cookie's `Path`\r\n * or `Domain`, only the name, so a cookie originally set on a `path` or\r\n * `domain` other than the one this call targets (the framework default,\r\n * or whatever is passed here / configured via `http.cookies.options`)\r\n * will NOT be deleted, and nothing will report that — the browser just\r\n * silently ignores a `Set-Cookie` whose scope doesn't match. Pass an\r\n * explicit `path` / `domain` for cookies the app knows it owns on a\r\n * non-default scope; call `clearCookie()` per name for anything else.\r\n *\r\n * @example\r\n * response.clearCookies();\r\n * response.clearCookies({ path: '/admin' });\r\n */\r\n public clearCookies(options?: CookieSerializeOptions) {\r\n for (const name of Object.keys(this.request.cookies)) {\r\n this.clearCookie(name, options);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Alias to header method\r\n */\r\n public setHeader(key: string, value: any) {\r\n return this.header(key, value);\r\n }\r\n\r\n /**\r\n * Send an error response with status code 500\r\n */\r\n public serverError(data: any) {\r\n return this.send(data, 500);\r\n }\r\n\r\n /**\r\n * Send a forbidden response with status code 403\r\n */\r\n public forbidden(\r\n data: any = {\r\n error: \"You are not allowed to access this resource, FORBIDDEN\",\r\n },\r\n ) {\r\n return this.send(data, 403);\r\n }\r\n\r\n /**\r\n * Send a service unavailable response with status code 503\r\n */\r\n public serviceUnavailable(data: any) {\r\n return this.send(data, 503);\r\n }\r\n\r\n /**\r\n * Send an unauthorized response with status code 401\r\n */\r\n public unauthorized(\r\n data: any = {\r\n error: \"unauthorized\",\r\n },\r\n ) {\r\n return this.send(data, 401);\r\n }\r\n\r\n /**\r\n * Send a not found response with status code 404\r\n */\r\n public notFound(\r\n data: any = {\r\n error: \"notFound\",\r\n },\r\n ) {\r\n return this.send(data, 404);\r\n }\r\n\r\n /**\r\n * Send a bad request response with status code 400\r\n */\r\n public badRequest(data: any) {\r\n return this.send(data, 400);\r\n }\r\n\r\n /**\r\n * Send a content too large response with status code 413\r\n */\r\n public contentTooLarge(data: any) {\r\n return this.send(data, 413);\r\n }\r\n\r\n /**\r\n * Send a success response with status code 201\r\n */\r\n public successCreate(data: any) {\r\n return this.send(data, 201);\r\n }\r\n\r\n /**\r\n * Send a success response\r\n */\r\n public success(data: any = { success: true }) {\r\n return this.send(data);\r\n }\r\n\r\n /**\r\n * Send a no content response with status code 204\r\n */\r\n public noContent() {\r\n return this.baseResponse.status(204).send();\r\n }\r\n\r\n /**\r\n * Send an accepted response with status code 202\r\n * Used for async operations that have been accepted but not yet processed\r\n */\r\n public accepted(data: any = { message: \"Request accepted for processing\" }) {\r\n return this.send(data, 202);\r\n }\r\n\r\n /**\r\n * Send a conflict response with status code 409\r\n */\r\n public conflict(data: any = { error: \"Resource conflict\" }) {\r\n return this.send(data, 409);\r\n }\r\n\r\n /**\r\n * Send a too many requests response with status code 429\r\n */\r\n public tooManyRequests(data: any) {\r\n return this.send(data, 429);\r\n }\r\n\r\n /**\r\n * Send an unprocessable entity response with status code 422\r\n * Used for semantic validation errors\r\n */\r\n public unprocessableEntity(data: any) {\r\n return this.send(data, 422);\r\n }\r\n\r\n /**\r\n * Apply response options (cache, disposition, etag)\r\n * Shared helper for sendFile and sendBuffer\r\n */\r\n /**\r\n * Build an RFC 6266–safe `Content-Disposition` value for a file name.\r\n *\r\n * HTTP header values must be ISO-8859-1; Node's `setHeader` throws\r\n * `ERR_INVALID_CHAR` on any other byte, so a raw `filename=\"تقرير.pdf\"` would\r\n * crash the response (a 500 the moment the name is non-ASCII). RFC 6266 solves\r\n * this with two parameters emitted together:\r\n * - `filename=\"<ascii>\"` — sanitised ASCII fallback for legacy clients\r\n * - `filename*=UTF-8''<pct>` — RFC 5987 ext-value; modern browsers restore\r\n * the real (e.g. Arabic) name.\r\n */\r\n private contentDisposition(type: \"inline\" | \"attachment\", rawName: string): string {\r\n // Strip CR/LF first so a crafted file name can never inject extra headers.\r\n const name = (rawName || \"file\").replace(/[\\r\\n]/g, \"\");\r\n\r\n // ASCII fallback: replace quote/backslash + every non-printable-ASCII byte\r\n // (covers all multibyte characters) so the quoted-string is always legal.\r\n const ascii =\r\n name\r\n .replace(/[\"\\\\]/g, \"_\")\r\n .replace(/[^\\x20-\\x7E]/g, \"_\")\r\n .trim() || \"file\";\r\n\r\n // Pure-ASCII name → the quoted form is enough; no ext-value needed.\r\n if (!/[^\\x20-\\x7E]/.test(name)) {\r\n return `${type}; filename=\"${ascii}\"`;\r\n }\r\n\r\n // RFC 5987 ext-value. `encodeURIComponent` leaves ' ( ) * unescaped, but they\r\n // are not valid `attr-char`, so percent-encode those too.\r\n const encoded = encodeURIComponent(name).replace(\r\n /['()*]/g,\r\n (char) => \"%\" + char.charCodeAt(0).toString(16).toUpperCase(),\r\n );\r\n\r\n return `${type}; filename=\"${ascii}\"; filename*=UTF-8''${encoded}`;\r\n }\r\n\r\n private applyResponseOptions(options: SendBufferOptions, defaultFilename?: string): boolean {\r\n // Set content type if provided\r\n if (options.contentType) {\r\n this.baseResponse.type(options.contentType);\r\n }\r\n\r\n // Set cache headers if specified\r\n if (options.cacheTime) {\r\n const cacheControl = options.immutable\r\n ? `public, max-age=${options.cacheTime}, immutable`\r\n : `public, max-age=${options.cacheTime}`;\r\n this.header(\"Cache-Control\", cacheControl);\r\n this.header(\"Expires\", new Date(Date.now() + options.cacheTime * 1000).toUTCString());\r\n }\r\n\r\n // Set ETag if provided (for conditional requests)\r\n if (options.etag) {\r\n this.header(\"ETag\", options.etag);\r\n\r\n // Check If-None-Match for conditional request\r\n const ifNoneMatch = this.request.header(\"if-none-match\");\r\n if (ifNoneMatch && ifNoneMatch === options.etag) {\r\n this.log(\"Content not modified (ETag match), sending 304\");\r\n this.baseResponse.status(304).send();\r\n return true; // Indicates 304 was sent\r\n }\r\n }\r\n\r\n // Set Content-Disposition if inline or filename is specified\r\n if (options.inline !== undefined || options.filename) {\r\n const disposition = options.inline ? \"inline\" : \"attachment\";\r\n const filename = options.filename || defaultFilename || \"file\";\r\n this.header(\"Content-Disposition\", this.contentDisposition(disposition, filename));\r\n }\r\n\r\n return false; // No 304 sent\r\n }\r\n\r\n /**\r\n * Send a file as a response\r\n */\r\n public async sendFile(filePath: string | StorageFile, options?: number | SendFileOptions) {\r\n if (filePath instanceof StorageFile) {\r\n filePath = filePath.absolutePath!;\r\n }\r\n\r\n this.log(`Sending file: ${filePath}`);\r\n\r\n // Check if file exists first\r\n if (!(await fileExistsAsync(filePath))) {\r\n return this.notFound({\r\n error: \"File Not Found\",\r\n });\r\n }\r\n\r\n try {\r\n // Normalize options to object format\r\n const opts = typeof options === \"number\" ? { cacheTime: options } : options || {};\r\n\r\n // Get file stats for ETag and Last-Modified\r\n const stats = await fs.promises.stat(filePath);\r\n const lastModified = stats.mtime;\r\n\r\n // Generate ETag based on file size and modification time\r\n const etag = `\"${stats.size}-${stats.mtime.getTime()}\"`;\r\n\r\n // Set Last-Modified header\r\n this.header(\"Last-Modified\", lastModified.toUTCString());\r\n this.header(\"ETag\", etag);\r\n\r\n // Set content type\r\n const contentType = this.getFileContentType(filePath);\r\n this.baseResponse.type(contentType);\r\n\r\n // Apply common response options (cache, disposition)\r\n const defaultFilename = path.basename(filePath);\r\n const sent304 = this.applyResponseOptions({ ...opts, etag, contentType }, defaultFilename);\r\n if (sent304) return this.baseResponse;\r\n\r\n // Check conditional request headers\r\n const ifNoneMatch = this.request.header(\"if-none-match\");\r\n const ifModifiedSince = this.request.header(\"if-modified-since\");\r\n\r\n // Handle If-None-Match (ETag validation)\r\n if (ifNoneMatch && ifNoneMatch === etag) {\r\n this.log(\"File not modified (ETag match), sending 304\");\r\n return this.baseResponse.status(304).send();\r\n }\r\n\r\n // Handle If-Modified-Since (Last-Modified validation)\r\n if (ifModifiedSince) {\r\n const modifiedSinceDate = new Date(ifModifiedSince);\r\n if (lastModified.getTime() <= modifiedSinceDate.getTime()) {\r\n this.log(\"File not modified (Last-Modified check), sending 304\");\r\n return this.baseResponse.status(304).send();\r\n }\r\n }\r\n\r\n // Use streaming for efficient file sending\r\n const stream = fs.createReadStream(filePath);\r\n\r\n // Handle stream errors\r\n stream.on(\"error\", (error) => {\r\n this.log(`Error reading file: ${error.message}`, \"error\");\r\n if (!this.baseResponse.sent) {\r\n this.serverError({\r\n error: \"Error reading file\",\r\n message: error.message,\r\n });\r\n }\r\n });\r\n\r\n // Send the stream (endTime will be set by finish event listener)\r\n return this.baseResponse.send(stream);\r\n } catch (error: any) {\r\n this.log(`Error sending file: ${error.message}`, \"error\");\r\n return this.serverError({\r\n error: \"Error sending file\",\r\n message: error.message,\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Send buffer as a response\r\n * Useful for dynamically generated content (e.g., resized images, generated PDFs)\r\n */\r\n public sendBuffer(buffer: Buffer, options?: number | SendBufferOptions) {\r\n this.log(\"Sending buffer\");\r\n\r\n // Normalize options to object format\r\n const opts = typeof options === \"number\" ? { cacheTime: options } : options || {};\r\n\r\n // Apply common response options (cache, disposition, etag)\r\n const sent304 = this.applyResponseOptions(opts);\r\n if (sent304) return this.baseResponse;\r\n\r\n // Note: endTime is set in the main send() method for non-streaming responses\r\n return this.baseResponse.send(buffer);\r\n }\r\n\r\n /**\r\n * Send an Image instance as a response\r\n * Automatically detects image format and sets content type\r\n */\r\n public async sendImage(\r\n image: any, // Type as 'any' to avoid circular dependency with Image class\r\n options?: number | (Omit<SendBufferOptions, \"contentType\"> & { contentType?: string }),\r\n ) {\r\n this.log(\"Sending image\");\r\n\r\n // Normalize options to object format\r\n const opts = typeof options === \"number\" ? { cacheTime: options } : options || {};\r\n\r\n // Get image metadata to determine format\r\n const metadata = await image.metadata();\r\n const format = metadata.format || \"jpeg\";\r\n\r\n // Convert image to buffer\r\n const buffer = await image.toBuffer();\r\n\r\n // Auto-set content type if not provided\r\n const contentType = opts.contentType || `image/${format}`;\r\n\r\n // Auto-generate ETag if not provided\r\n // Format: \"format-widthxheight-size\" (e.g., \"jpeg-800x600-45231\")\r\n // This catches changes in dimensions, quality, filters, and format\r\n if (!opts.etag) {\r\n const width = metadata.width || 0;\r\n const height = metadata.height || 0;\r\n opts.etag = `\"${format}-${width}x${height}-${buffer.length}\"`;\r\n }\r\n\r\n // Apply common response options with auto-detected content type\r\n const sent304 = this.applyResponseOptions({ ...opts, contentType });\r\n if (sent304) return this.baseResponse;\r\n\r\n // Note: endTime is set in the main send() method for non-streaming responses\r\n return this.baseResponse.send(buffer);\r\n }\r\n\r\n /**\r\n * Send file and cache it\r\n * Cache time in seconds\r\n * Cache time will be one year\r\n */\r\n public sendCachedFile(path: string | StorageFile, cacheTime = 31536000) {\r\n return this.sendFile(path, cacheTime);\r\n }\r\n\r\n /**\r\n * Download the given file path\r\n */\r\n public download(path: string, filename?: string) {\r\n return this.downloadFile(path, filename);\r\n }\r\n\r\n /**\r\n * Download the given file path\r\n */\r\n public async downloadFile(filePath: string, filename?: string) {\r\n // Check if file exists first\r\n if (!(await fileExistsAsync(filePath))) {\r\n return this.notFound({\r\n error: \"File Not Found\",\r\n });\r\n }\r\n\r\n try {\r\n if (!filename) {\r\n filename = path.basename(filePath);\r\n }\r\n\r\n this.baseResponse.header(\r\n \"Content-Disposition\",\r\n this.contentDisposition(\"attachment\", filename),\r\n );\r\n\r\n // this.baseResponse.header(\"Content-Type\", this.getFileContentType(filePath));\r\n this.baseResponse.header(\"Content-Type\", \"application/octet-stream\");\r\n\r\n const stream = fs.createReadStream(filePath);\r\n\r\n // Handle stream errors\r\n stream.on(\"error\", (error) => {\r\n this.log(`Error reading file for download: ${error.message}`, \"error\");\r\n if (!this.baseResponse.sent) {\r\n this.serverError({\r\n error: \"Error reading file\",\r\n message: error.message,\r\n });\r\n }\r\n });\r\n\r\n // Send the stream (endTime will be set by finish event listener)\r\n return this.baseResponse.send(stream);\r\n } catch (error: any) {\r\n this.log(`Error downloading file: ${error.message}`, \"error\");\r\n return this.serverError({\r\n error: \"Error downloading file\",\r\n message: error.message,\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Get content type of the given path\r\n */\r\n public getFileContentType(filePath: string) {\r\n const type = mime.getType(filePath) || \"application/octet-stream\";\r\n return type;\r\n }\r\n\r\n /**\r\n * Mark the response as failed\r\n */\r\n public failedSchema(result: ValidationResult) {\r\n const { errors, inputKey, inputError, status } = config.get(\"validation.response\", {\r\n errors: \"errors\",\r\n inputKey: \"input\",\r\n inputError: \"error\",\r\n status: 422,\r\n });\r\n\r\n log.error(\"request\", \"validation\", `${this.request.id} - Validation failed`);\r\n\r\n return this.send(\r\n {\r\n [errors]: result.errors.map((error) => ({\r\n [inputKey]: error.input,\r\n [inputError]: error.error,\r\n })),\r\n },\r\n status,\r\n );\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"response.mjs","names":[],"sources":["../../../../../../../core/src/http/response.ts"],"sourcesContent":["import type { CookieSerializeOptions } from \"@fastify/cookie\";\r\nimport type { OutgoingHttpHeaders } from \"node:http\";\r\nimport config from \"@mongez/config\";\r\nimport type { EventSubscription } from \"@mongez/events\";\r\nimport events from \"@mongez/events\";\r\nimport { fileExistsAsync } from \"@warlock.js/fs\";\r\nimport { isIterable, isPlainObject, isScalar } from \"@mongez/supportive-is\";\r\nimport type { LogLevel } from \"@warlock.js/logger\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport type { ValidationResult } from \"@warlock.js/seal\";\r\nimport type { FastifyReply } from \"fastify\";\r\nimport fs from \"fs\";\r\nimport mime from \"mime\";\r\nimport path from \"path\";\r\nimport type React from \"react\";\r\nimport { type ReactNode } from \"react\";\r\nimport { Application } from \"../application/application\";\r\nimport { LOCALE_COOKIE_NAME, resolveLocaleConfiguration } from \"../config/locale-configuration\";\r\nimport { UnknownLocaleError } from \"../errors/unknown-locale-error\";\r\nimport type { Route } from \"../router\";\r\nimport { StorageFile } from \"../storage\";\r\nimport { renderReact } from \"./../react\";\r\nimport type { Request } from \"./request\";\r\nimport { streamReactResponse, type PipeableReactStream } from \"./stream-react-response\";\r\nimport type { ResponseEvent, ResponseSSEController, ResponseStreamController } from \"./types\";\r\nimport type { XMLable } from \"./xmlable\";\r\n\r\ntype CookieValue = string | number | boolean | Record<string, any> | Array<any>;\r\n\r\n/**\r\n * Cookie options accepted by `response.cookie()`.\r\n *\r\n * Extends Fastify's `CookieSerializeOptions` with `raw` — set to `true` to\r\n * skip the default `JSON.stringify` of the value and write it as-is. Use for\r\n * plain-string cookies (session tokens, opaque IDs) that shouldn't be JSON-quoted.\r\n *\r\n * When `raw: true`, non-string values are coerced via `String(value)`. The\r\n * read side (`request.cookie(name)`) tries `JSON.parse` first and falls back\r\n * to the raw string on parse failure, so round-tripping a raw string cookie\r\n * Just Works.\r\n */\r\nexport type CookieOptions = CookieSerializeOptions & {\r\n /**\r\n * Skip JSON.stringify and write the value as-is.\r\n *\r\n * @default false\r\n */\r\n raw?: boolean;\r\n};\r\n\r\nexport enum ResponseStatus {\r\n OK = 200,\r\n CREATED = 201,\r\n ACCEPTED = 202,\r\n MOVED_PERMANENTLY = 301,\r\n FOUND = 302,\r\n SEE_OTHER = 303,\r\n NOT_MODIFIED = 304,\r\n TEMPORARY_REDIRECT = 307,\r\n PERMANENT_REDIRECT = 308,\r\n NO_CONTENT = 204,\r\n BAD_REQUEST = 400,\r\n UNAUTHORIZED = 401,\r\n FORBIDDEN = 403,\r\n NOT_FOUND = 404,\r\n METHOD_NOT_ALLOWED = 405,\r\n CONFLICT = 409,\r\n TOO_MANY_REQUESTS = 429,\r\n INTERNAL_SERVER_ERROR = 500,\r\n SERVICE_UNAVAILABLE = 503,\r\n}\r\n\r\n/**\r\n * Options for sending files\r\n */\r\nexport type SendFileOptions = {\r\n cacheTime?: number;\r\n immutable?: boolean;\r\n inline?: boolean;\r\n filename?: string;\r\n};\r\n\r\n/**\r\n * Options for sending buffers\r\n */\r\nexport type SendBufferOptions = SendFileOptions & {\r\n contentType?: string;\r\n etag?: string;\r\n};\r\n\r\n/**\r\n * The cookie flags every response cookie gets unless something overrides them.\r\n *\r\n * Computed per call rather than hoisted to a constant because `secure` depends\r\n * on the environment, which is not known at module-evaluation time.\r\n *\r\n * `secure` is relaxed in development only: a `Secure` cookie is dropped by the\r\n * browser over plain http, which would silently break every local login. It\r\n * stays on everywhere else, including test and staging.\r\n */\r\nfunction secureCookieDefaults(): CookieSerializeOptions {\r\n return {\r\n httpOnly: true,\r\n sameSite: \"lax\",\r\n secure: !Application.isDevelopment,\r\n };\r\n}\r\n\r\nexport class Response {\r\n /**\r\n * Current route\r\n */\r\n protected route!: Route;\r\n\r\n /**\r\n * Underlying Fastify reply — a public escape hatch to capabilities the\r\n * framework's high-level helpers don't yet cover.\r\n *\r\n * **Prefer framework methods first**: `response.send()`, `response.header()`,\r\n * `response.cookie()`, `response.sendFile()`, `response.stream()`, etc.\r\n * They wire status codes, content-type detection, the event lifecycle, and\r\n * the cache-pattern replay path correctly.\r\n *\r\n * **Reach for `baseResponse` only** when the framework genuinely lacks a\r\n * helper for what you need — and when you do, file an issue so we can add\r\n * it. Streaming and SSE are the precedent here: they bypass `send()`\r\n * deliberately because the framework didn't ship chunked-write support\r\n * natively at the time. Reaching here for non-streaming work means a\r\n * missing helper, not an answer.\r\n */\r\n public baseResponse!: FastifyReply;\r\n\r\n /**\r\n * Current status code\r\n */\r\n protected currentStatusCode = 200;\r\n\r\n /**\r\n * Current response body\r\n */\r\n protected currentBody: any;\r\n\r\n /**\r\n * Request object\r\n */\r\n public request!: Request;\r\n\r\n /**\r\n * Internal events related to this particular response object\r\n */\r\n protected events = new Map<string, any[]>();\r\n\r\n /**\r\n * Parsed body\r\n * This will return the parsed body of the response\r\n * Please note that if this property is called before the response is sent, it will return undefined\r\n */\r\n public parsedBody: any;\r\n\r\n /**\r\n * Get raw response\r\n */\r\n public get raw() {\r\n return this.baseResponse.raw;\r\n }\r\n\r\n /**\r\n * Get Current response body\r\n */\r\n public get body() {\r\n return this.currentBody;\r\n }\r\n\r\n /**\r\n * Set response body\r\n */\r\n public set body(body: any) {\r\n this.currentBody = body;\r\n }\r\n\r\n /**\r\n * Add event on sending response\r\n */\r\n public onSending(callback: any) {\r\n this.events.set(\"sending\", [...(this.events.get(\"sending\") || []), callback]);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Add event on sent response\r\n */\r\n public onSent(callback: any) {\r\n this.events.set(\"sent\", [...(this.events.get(\"sent\") || []), callback]);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set the Fastify response object\r\n */\r\n public setResponse(response: FastifyReply) {\r\n this.baseResponse = response;\r\n\r\n // Listen to the 'finish' event to track when response is fully sent\r\n // This works for all response types: JSON, streams, buffers, files, etc.\r\n this.baseResponse.raw.once(\"finish\", () => {\r\n this.request.endTime = Date.now();\r\n });\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Reset the response state\r\n */\r\n public reset() {\r\n this.route = {} as Route;\r\n this.currentBody = null;\r\n this.currentStatusCode = 200;\r\n }\r\n\r\n /**\r\n * Set current route\r\n */\r\n public setRoute(route: Route) {\r\n this.route = route;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get the content type\r\n */\r\n public get contentType() {\r\n return this.baseResponse.getHeader(\"Content-Type\");\r\n }\r\n\r\n /**\r\n * Set the content type\r\n */\r\n public setContentType(contentType: string) {\r\n this.baseResponse.header(\"Content-Type\", contentType);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get the status code\r\n */\r\n public get statusCode(): number {\r\n return this.currentStatusCode ?? this.baseResponse.statusCode;\r\n }\r\n\r\n /**\r\n * Check if response status is ok\r\n */\r\n public get isOk() {\r\n return this.currentStatusCode >= 200 && this.currentStatusCode < 300;\r\n }\r\n\r\n /**\r\n * Check if the response has been sent\r\n */\r\n public get sent() {\r\n return this.baseResponse.sent;\r\n }\r\n\r\n /**\r\n * Add a listener to the response event\r\n */\r\n public static on(\r\n event: ResponseEvent,\r\n listener: (response: Response) => void,\r\n ): EventSubscription {\r\n return events.subscribe(`response.${event}`, listener);\r\n }\r\n\r\n /**\r\n * Trigger the response event\r\n */\r\n protected static async trigger(event: ResponseEvent, ...args: any[]) {\r\n // make a timeout to make sure the request events is executed first\r\n return new Promise((resolve) => {\r\n setTimeout(async () => {\r\n await events.triggerAllAsync(`response.${event}`, ...args);\r\n resolve(true);\r\n }, 0);\r\n });\r\n }\r\n\r\n /**\r\n * Parse body\r\n */\r\n protected async parseBody() {\r\n return await this.parse(this.currentBody);\r\n }\r\n\r\n /**\r\n * Parse the given value\r\n */\r\n public async parse(value: any): Promise<any> {\r\n // if it is a falsy value, return it\r\n if (!value || isScalar(value)) return value;\r\n\r\n // if it has a `toJSON` method, call it and await the result then return it\r\n if (value.toJSON) {\r\n value.request = this.request;\r\n return await value.toJSON();\r\n }\r\n\r\n // if it is iterable, an array or array-like object then parse each item\r\n if (isIterable(value)) {\r\n const values = Array.from(value);\r\n\r\n return Promise.all(\r\n values.map(async (item: any) => {\r\n return await this.parse(item);\r\n }),\r\n );\r\n }\r\n\r\n // if not plain object, then return it\r\n if (!isPlainObject(value)) {\r\n return value;\r\n }\r\n\r\n // loop over the object and check if the value and call `parse` on it\r\n for (const key in value) {\r\n const subValue = value[key];\r\n\r\n value[key] = await this.parse(subValue);\r\n }\r\n\r\n return value;\r\n }\r\n\r\n /**\r\n * Make a log message\r\n */\r\n public log(message: string, level: LogLevel = \"info\") {\r\n if (!config.get(\"http.log\")) return;\r\n\r\n log.log({\r\n module: \"response\",\r\n action: this.route.method + \" \" + this.route.path.replace(\"/*\", \"\") + `:${this.request.id}`,\r\n message,\r\n type: level,\r\n context: {\r\n request: this.request,\r\n response: this,\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Check if returning response is json\r\n */\r\n public get isJson() {\r\n return this.getHeader(\"Content-Type\") === \"application/json\";\r\n }\r\n\r\n /**\r\n * Send the response\r\n * @param data - Response data\r\n * @param statusCode - HTTP status code\r\n * @param triggerEvents - Whether to trigger response events (default: true)\r\n */\r\n public async send(data?: any, statusCode?: number, triggerEvents = true): Promise<Response> {\r\n // Defensive guard against double-send. The underlying Fastify reply silently\r\n // ignores subsequent sends once `sent === true`, which has historically hidden\r\n // middleware bugs (cache-pattern replay paths that returned `baseResponse.send`\r\n // ended up re-entering `Response.send` with the FastifyReply as the body).\r\n // Surfacing the misuse via `error`-level log makes the bug loud without\r\n // crashing production traffic.\r\n if (this.baseResponse.sent) {\r\n log.error(\r\n \"response\",\r\n \"send\",\r\n `send() called on already-sent response (request:${this.request?.id ?? \"unknown\"}) — likely a middleware bug`,\r\n );\r\n\r\n return this;\r\n }\r\n\r\n if (statusCode) {\r\n this.currentStatusCode = statusCode;\r\n }\r\n\r\n if (data === this) return this;\r\n\r\n if (data) {\r\n this.currentBody = data;\r\n }\r\n\r\n if (!this.currentStatusCode) {\r\n this.currentStatusCode = 200;\r\n }\r\n\r\n this.log(\"Sending response\");\r\n // Auto-pick `application/json` only when no content-type was set by the caller.\r\n // This preserves explicit overrides (e.g. `application/vnd.api+json` from a\r\n // cache replay, `application/problem+json` from an RFC 7807 error response)\r\n // while keeping the convenience default for the common object-body path.\r\n if (Array.isArray(this.currentBody) || isPlainObject(this.currentBody)) {\r\n if (!this.baseResponse.getHeader(\"Content-Type\")) {\r\n this.setContentType(\"application/json\");\r\n }\r\n }\r\n\r\n if (triggerEvents) {\r\n await Response.trigger(\"sending\", this);\r\n\r\n for (const callback of this.events.get(\"sending\") || []) {\r\n await callback(this);\r\n }\r\n\r\n if (this.isJson) {\r\n await Response.trigger(\"sendingJson\", this);\r\n for (const callback of this.events.get(\"sendingJson\") || []) {\r\n await callback(this);\r\n }\r\n\r\n if (this.isOk) {\r\n await Response.trigger(\"sendingSuccessJson\", this);\r\n for (const callback of this.events.get(\"sendingSuccessJson\") || []) {\r\n await callback(this);\r\n }\r\n }\r\n }\r\n }\r\n\r\n // parse the body and make sure it is transformed to sync data instead of async data\r\n if (typeof this.currentBody !== \"string\") {\r\n this.parsedBody = await this.parseBody();\r\n } else {\r\n this.parsedBody = data;\r\n }\r\n\r\n // Set the status first\r\n this.baseResponse.status(this.currentStatusCode);\r\n\r\n // Then send the response with the parsed body\r\n await this.baseResponse.send(this.parsedBody);\r\n\r\n this.log(\"Response sent\");\r\n\r\n if (triggerEvents) {\r\n // trigger the sent event\r\n Response.trigger(\"sent\", this);\r\n\r\n for (const callback of this.events.get(\"sent\") || []) {\r\n callback(this);\r\n }\r\n\r\n // trigger the success event if the status code is 2xx\r\n if (this.currentStatusCode >= 200 && this.currentStatusCode < 300) {\r\n Response.trigger(\"success\", this);\r\n }\r\n\r\n // trigger the successCreate event if the status code is 201\r\n if (this.currentStatusCode === 201) {\r\n Response.trigger(\"successCreate\", this);\r\n }\r\n\r\n // trigger the badRequest event if the status code is 400\r\n if (this.currentStatusCode === 400) {\r\n Response.trigger(\"badRequest\", this);\r\n }\r\n\r\n // trigger the unauthorized event if the status code is 401\r\n if (this.currentStatusCode === 401) {\r\n Response.trigger(\"unauthorized\", this);\r\n }\r\n\r\n // trigger the forbidden event if the status code is 403\r\n if (this.currentStatusCode === 403) {\r\n Response.trigger(\"forbidden\", this);\r\n }\r\n\r\n // trigger the notFound event if the status code is 404\r\n if (this.currentStatusCode === 404) {\r\n Response.trigger(\"notFound\", this);\r\n }\r\n\r\n // trigger the content too large event if the status code is 413\r\n if (this.currentStatusCode === 413) {\r\n Response.trigger(\"contentTooLarge\", this);\r\n }\r\n\r\n // trigger the throttled event if the status code is 429\r\n if (this.currentStatusCode === 429) {\r\n Response.trigger(\"throttled\", this);\r\n }\r\n\r\n // trigger the serverError event if the status code is 500\r\n if (this.currentStatusCode === 500) {\r\n Response.trigger(\"serverError\", this);\r\n }\r\n\r\n // trigger the error event if the status code is 4xx or 5xx\r\n if (this.currentStatusCode >= 400) {\r\n Response.trigger(\"error\", this);\r\n }\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Replay a previously-captured response shape — used by cache-pattern\r\n * middlewares (idempotency, response cache) to send a cached response\r\n * without re-running the controller.\r\n *\r\n * Preserves the cached status code, content-type, and any extra headers,\r\n * then sends the body through the standard `send()` pipeline so the full\r\n * event lifecycle still fires (`sent`, `success`, status-specific events).\r\n * That keeps cross-cutting observers (logger, metrics, audit) consistent\r\n * between fresh and replayed responses.\r\n *\r\n * @example\r\n * // Inside a cache-pattern middleware on HIT:\r\n * return response.header(\"X-Cache\", \"HIT\").replay({\r\n * status: cached.status,\r\n * body: cached.body,\r\n * contentType: cached.contentType,\r\n * });\r\n */\r\n public replay(cached: {\r\n status: number;\r\n body: unknown;\r\n contentType?: string;\r\n headers?: Record<string, string>;\r\n }): Promise<Response> {\r\n this.setStatusCode(cached.status);\r\n\r\n if (cached.contentType) {\r\n this.setContentType(cached.contentType);\r\n }\r\n\r\n if (cached.headers) {\r\n for (const [name, value] of Object.entries(cached.headers)) {\r\n this.header(name, value);\r\n }\r\n }\r\n\r\n return this.send(cached.body);\r\n }\r\n\r\n /**\r\n * Send html response\r\n */\r\n public html(data: string, statusCode?: number) {\r\n return this.setContentType(\"text/html\").send(data, statusCode);\r\n }\r\n\r\n /**\r\n * Render the given react component\r\n */\r\n public render(element: React.ReactElement | React.ComponentType, status = 200) {\r\n return this.setStatusCode(status).html(renderReact(element));\r\n }\r\n\r\n /**\r\n * Send an xml response.\r\n *\r\n * Accepts either a raw XML string or an `XMLable` — anything with a\r\n * `toXML()` method, structurally typed so packages like\r\n * `@warlock.js/sitemap` never need to import core. This is the BOUNDED\r\n * path only: a very large document should be streamed from generated\r\n * files instead of passed through here.\r\n */\r\n public xml(body: string | XMLable, statusCode?: number) {\r\n if (typeof body === \"string\") {\r\n return this.setContentType(\"application/xml\").send(body, statusCode);\r\n }\r\n\r\n if (typeof body?.toXML !== \"function\") {\r\n throw new TypeError(\r\n \"response.xml() expects a string or an XMLable value (an object with a toXML() method).\",\r\n );\r\n }\r\n\r\n return this.setContentType(\"application/xml\").send(body.toXML(), statusCode);\r\n }\r\n\r\n /**\r\n * Send plain text response\r\n */\r\n public text(data: string, statusCode?: number) {\r\n return this.setContentType(\"text/plain\").send(data, statusCode);\r\n }\r\n\r\n /**\r\n * Create a streaming response for progressive/chunked data sending\r\n *\r\n * This method allows you to send data in chunks and control when the response ends.\r\n * Perfect for Server-Sent Events (SSE), progressive rendering, or streaming large responses.\r\n *\r\n * @example\r\n * ```ts\r\n * const stream = response.stream(\"text/html\");\r\n * stream.send(\"<html><body>\");\r\n * stream.send(\"<h1>Hello</h1>\");\r\n * stream.render(<MyComponent />);\r\n * stream.send(\"</body></html>\");\r\n * stream.end();\r\n * ```\r\n *\r\n * @param contentType - The content type for the stream (default: \"text/plain\")\r\n * @returns Stream controller with send(), render(), and end() methods\r\n */\r\n public stream(contentType = \"text/plain\"): ResponseStreamController {\r\n // Set headers using the response API\r\n this.setContentType(contentType);\r\n this.header(\"Transfer-Encoding\", \"chunked\");\r\n this.header(\"Cache-Control\", \"no-cache\");\r\n this.header(\"Connection\", \"keep-alive\");\r\n this.header(\"X-Content-Type-Options\", \"nosniff\");\r\n\r\n // Trigger sending events\r\n Response.trigger(\"sending\", this);\r\n for (const callback of this.events.get(\"sending\") || []) {\r\n callback(this);\r\n }\r\n\r\n this.log(\"Starting stream\");\r\n\r\n // Track stream state\r\n let isEnded = false;\r\n const chunks: any[] = [];\r\n\r\n // Write headers to start the stream\r\n // Note: We use raw here because we need chunked encoding control\r\n // This is the only valid use case for bypassing Fastify's abstraction\r\n this.baseResponse.raw.writeHead(this.statusCode, this.getHeaders() as any);\r\n\r\n return {\r\n /**\r\n * Send a chunk of data to the client\r\n * @param data - Data to send (string, Buffer, or any serializable data)\r\n */\r\n send: (data: any) => {\r\n if (isEnded) {\r\n throw new Error(\"Cannot send data: stream has already ended\");\r\n }\r\n\r\n this.baseResponse.raw.write(data);\r\n\r\n return this;\r\n },\r\n\r\n /**\r\n * Render a React component and send it as a chunk\r\n * @param element - React element or component to render\r\n */\r\n render: (element: ReactNode) => {\r\n if (isEnded) {\r\n throw new Error(\"Cannot render: stream has already ended\");\r\n }\r\n\r\n const html = renderReact(element);\r\n chunks.push(html);\r\n this.baseResponse.raw.write(html);\r\n\r\n return this;\r\n },\r\n\r\n /**\r\n * End the stream and trigger completion events\r\n */\r\n end: () => {\r\n if (isEnded) {\r\n return this;\r\n }\r\n\r\n isEnded = true;\r\n\r\n // Store the streamed content for logging/debugging\r\n this.currentBody = chunks;\r\n this.parsedBody = chunks;\r\n\r\n // End the response\r\n this.baseResponse.raw.end();\r\n\r\n this.log(\"Stream ended\");\r\n\r\n // Trigger sent events\r\n Response.trigger(\"sent\", this);\r\n for (const callback of this.events.get(\"sent\") || []) {\r\n callback(this);\r\n }\r\n\r\n // Trigger success event if status is 2xx\r\n if (this.isOk) {\r\n Response.trigger(\"success\", this);\r\n }\r\n\r\n // Trigger status-specific events\r\n if (this.currentStatusCode === 201) {\r\n Response.trigger(\"successCreate\", this);\r\n }\r\n\r\n return this;\r\n },\r\n\r\n /**\r\n * Check if the stream has ended\r\n */\r\n get ended() {\r\n return isEnded;\r\n },\r\n };\r\n }\r\n\r\n /**\r\n * Pipe a React server stream (`renderToPipeableStream`) onto this\r\n * response — the Stage 1 streaming SSR seam. Writes the already-committed\r\n * status and headers, then pipes; aborts the React render if the client\r\n * disconnects before the stream finishes.\r\n *\r\n * This is the ONLY sanctioned way for `@warlock.js/web` to put a React\r\n * stream on the wire: it never touches `response.raw` itself, it calls\r\n * this method, which does (`stream-react-response.ts`).\r\n *\r\n * @example\r\n * ```ts\r\n * const pipeableStream = await renderPageToPipeableStream(element);\r\n * await response.streamReact(pipeableStream);\r\n * ```\r\n */\r\n public streamReact(pipeableStream: PipeableReactStream): Promise<void> {\r\n Response.trigger(\"sending\", this);\r\n for (const callback of this.events.get(\"sending\") || []) {\r\n callback(this);\r\n }\r\n\r\n return streamReactResponse({\r\n raw: this.baseResponse.raw,\r\n statusCode: this.statusCode,\r\n headers: this.getHeaders() as OutgoingHttpHeaders,\r\n pipeableStream,\r\n }).then(() => {\r\n Response.trigger(\"sent\", this);\r\n for (const callback of this.events.get(\"sent\") || []) {\r\n callback(this);\r\n }\r\n\r\n if (this.isOk) {\r\n Response.trigger(\"success\", this);\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Create a Server-Sent Events (SSE) stream\r\n *\r\n * SSE is a standard for pushing real-time updates from server to client.\r\n * Perfect for live notifications, progress updates, or real-time data feeds.\r\n *\r\n * @example\r\n * ```ts\r\n * const sse = response.sse();\r\n *\r\n * // Send events\r\n * sse.send(\"message\", { text: \"Hello!\" });\r\n * sse.send(\"notification\", { type: \"info\", message: \"Update available\" }, \"msg-123\");\r\n *\r\n * // Keep connection alive\r\n * const keepAlive = setInterval(() => sse.comment(\"ping\"), 30000);\r\n *\r\n * // Clean up when done\r\n * clearInterval(keepAlive);\r\n * sse.end();\r\n * ```\r\n *\r\n * @returns SSE controller with send(), comment(), and end() methods\r\n */\r\n public sse(): ResponseSSEController {\r\n // Set SSE-specific headers\r\n this.setContentType(\"text/event-stream\");\r\n this.header(\"Cache-Control\", \"no-cache, no-store, must-revalidate\");\r\n this.header(\"Connection\", \"keep-alive\");\r\n this.header(\"X-Accel-Buffering\", \"no\"); // Disable nginx buffering\r\n\r\n // Trigger sending events\r\n Response.trigger(\"sending\", this);\r\n for (const callback of this.events.get(\"sending\") || []) {\r\n callback(this);\r\n }\r\n\r\n this.log(\"Starting SSE stream\");\r\n\r\n // Track stream state\r\n let isEnded = false;\r\n const events: any[] = [];\r\n const disconnectHandlers: Array<() => void> = [];\r\n\r\n // Write headers to start the stream\r\n this.baseResponse.raw.writeHead(this.statusCode, this.getHeaders() as any);\r\n\r\n // Detect client disconnect — set isEnded silently and invoke cleanup handlers.\r\n // Without this, background jobs keep writing to a dead socket after the client drops.\r\n this.baseResponse.raw.on(\"close\", () => {\r\n if (!isEnded) {\r\n isEnded = true;\r\n this.log(\"SSE client disconnected\");\r\n for (const handler of disconnectHandlers) {\r\n handler();\r\n }\r\n }\r\n });\r\n\r\n const controller: ResponseSSEController = {\r\n /**\r\n * Send an SSE event\r\n * @param event - Event name (e.g., \"message\", \"chunk\", \"done\")\r\n * @param data - Event data (will be JSON stringified)\r\n * @param id - Optional event ID for client-side Last-Event-ID tracking (reconnect support)\r\n */\r\n send: (event: string, data: any, id?: string): ResponseSSEController => {\r\n // Silent no-op after disconnect — background jobs should not crash when\r\n // the client drops mid-stream. The onDisconnect handler handles cleanup.\r\n if (isEnded) return controller;\r\n\r\n let message = \"\";\r\n if (id) message += `id: ${id}\\n`;\r\n message += `event: ${event}\\n`;\r\n message += `data: ${JSON.stringify(data)}\\n\\n`;\r\n\r\n events.push({ event, data, id });\r\n this.baseResponse.raw.write(message);\r\n\r\n return controller;\r\n },\r\n\r\n /**\r\n * Send a comment (keeps connection alive, invisible to client)\r\n * Useful for preventing timeout on long-lived connections\r\n * @param text - Comment text\r\n */\r\n comment: (text: string): ResponseSSEController => {\r\n // Silent no-op after disconnect\r\n if (isEnded) return controller;\r\n\r\n this.baseResponse.raw.write(`: ${text}\\n\\n`);\r\n\r\n return controller;\r\n },\r\n\r\n /**\r\n * End the SSE stream and trigger completion events\r\n */\r\n end: (): ResponseSSEController => {\r\n if (isEnded) return controller;\r\n\r\n isEnded = true;\r\n\r\n // Store the events for logging/debugging\r\n this.currentBody = events;\r\n this.parsedBody = events;\r\n\r\n // End the response\r\n this.baseResponse.raw.end();\r\n\r\n this.log(\"SSE stream ended\");\r\n\r\n // Trigger sent events\r\n Response.trigger(\"sent\", this);\r\n for (const callback of this.events.get(\"sent\") || []) {\r\n callback(this);\r\n }\r\n\r\n // Trigger success event if status is 2xx\r\n if (this.isOk) {\r\n Response.trigger(\"success\", this);\r\n }\r\n\r\n return controller;\r\n },\r\n\r\n /**\r\n * Register a handler to be called when the client disconnects.\r\n * Use this to clean up EventEmitter listeners, cancel background jobs, etc.\r\n *\r\n * @example\r\n * ```ts\r\n * const sse = response.sse();\r\n * const listener = (chunk) => sse.send(\"chunk\", { chunk });\r\n * eventBus.on(aiMessageId, listener);\r\n * sse.onDisconnect(() => eventBus.off(aiMessageId, listener));\r\n * ```\r\n */\r\n onDisconnect: (handler: () => void): ResponseSSEController => {\r\n disconnectHandlers.push(handler);\r\n return controller;\r\n },\r\n\r\n /**\r\n * Check if the stream has ended (either via end() or client disconnect)\r\n */\r\n get ended() {\r\n return isEnded;\r\n },\r\n };\r\n\r\n return controller;\r\n }\r\n\r\n /**\r\n * Set the status code\r\n */\r\n public setStatusCode(statusCode: number) {\r\n this.currentStatusCode = statusCode;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Redirect the user to another route\r\n */\r\n public redirect(url: string, statusCode = 302) {\r\n this.baseResponse.redirect(url, statusCode);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Permanent redirect\r\n */\r\n public permanentRedirect(url: string) {\r\n this.baseResponse.redirect(url, 301);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get the response time\r\n */\r\n public getResponseTime() {\r\n return this.baseResponse.elapsedTime;\r\n }\r\n\r\n /**\r\n * Remove a specific header\r\n */\r\n public removeHeader(key: string) {\r\n this.baseResponse.removeHeader(key);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get a specific header\r\n */\r\n public getHeader(key: string) {\r\n return this.baseResponse.getHeader(key);\r\n }\r\n\r\n /**\r\n * Get the response headers\r\n */\r\n public getHeaders() {\r\n return this.baseResponse.getHeaders();\r\n }\r\n\r\n /**\r\n * Set multiple headers\r\n */\r\n public headers(headers: Record<string, string>) {\r\n this.baseResponse.headers(headers);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set the response header\r\n */\r\n public header(key: string, value: any) {\r\n this.baseResponse.header(key, value);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set a cookie on the response.\r\n *\r\n * Values are JSON-stringified by default so structured cookies round-trip\r\n * cleanly with `request.cookie(name)`. Pass `{ raw: true }` to skip the\r\n * JSON wrapping for plain-string cookies (session tokens, opaque IDs).\r\n *\r\n * **Secure by default.** `httpOnly: true`, `sameSite: \"lax\"`, and — outside\r\n * development — `secure: true` are applied unless you override them. These\r\n * are the flags whose absence never fails a test and is fatal in production:\r\n * without `httpOnly` any injected script can read the cookie, without\r\n * `secure` it travels in cleartext, without `sameSite` it rides along on\r\n * cross-site requests. Opting out is explicit, per call or via\r\n * `http.cookies.options`.\r\n *\r\n * Precedence, lowest to highest: framework defaults → `http.cookies.options`\r\n * → the per-call `options` argument.\r\n *\r\n * @example\r\n * // JSON-wrapped (default) — round-trips with request.cookie()\r\n * response.cookie(\"prefs\", { theme: \"dark\" }, { maxAge: 3600 });\r\n *\r\n * @example\r\n * // Raw string — no JSON quoting; useful for tokens / opaque IDs\r\n * response.cookie(\"session\", \"abc.def.ghi\", { raw: true });\r\n *\r\n * @example\r\n * // Deliberately readable by client-side JS\r\n * response.cookie(\"theme\", \"dark\", { httpOnly: false });\r\n */\r\n public cookie(name: string, value: CookieValue, options: CookieOptions = {}) {\r\n const { raw, ...cookieOptions } = options;\r\n const defaultOptions = config.get(\"http.cookies.options\", {});\r\n const serializedValue = raw ? String(value) : JSON.stringify(value);\r\n\r\n this.baseResponse.setCookie(name, serializedValue, {\r\n ...secureCookieDefaults(),\r\n ...defaultOptions,\r\n ...cookieOptions,\r\n });\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set the active locale for subsequent requests.\r\n *\r\n * Writes the SAME cookie `request.locale` reads (`LOCALE_COOKIE_NAME`,\r\n * owned by the framework, never a string an app hardcodes) — the two sides\r\n * are read from one shared constant so they cannot name the cookie\r\n * differently. Written raw (no JSON quoting), matching how\r\n * `Request.resolveLocale()` reads it back.\r\n *\r\n * Any `Set-Cookie` this emits revokes public cacheability for the response,\r\n * including a page that opted into `public, max-age` — a per-visitor\r\n * locale cookie replayed from a shared cache would hand visitor A's locale\r\n * to visitor B.\r\n *\r\n * @throws {UnknownLocaleError} when `locale` is outside the app's\r\n * configured `app.localeCodes` allow-list. Silently accepting an\r\n * unconfigured locale would set a cookie the app can never actually serve.\r\n *\r\n * @example\r\n * response.setLocale(\"ar\");\r\n */\r\n public setLocale(locale: string) {\r\n const { localeCodes } = resolveLocaleConfiguration(\r\n config.get(\"app.localeCode\"),\r\n config.get(\"app.localeCodes\"),\r\n );\r\n\r\n if (localeCodes !== undefined && !localeCodes.includes(locale)) {\r\n throw new UnknownLocaleError(locale, localeCodes);\r\n }\r\n\r\n return this.cookie(LOCALE_COOKIE_NAME, locale, { raw: true });\r\n }\r\n\r\n /**\r\n * Clear a cookie from the response\r\n *\r\n * @example\r\n * response.clearCookie('token', { path: '/' });\r\n */\r\n public clearCookie(name: string, options?: CookieSerializeOptions) {\r\n const defaultOptions = config.get(\"http.cookies.options\", {});\r\n this.baseResponse.clearCookie(name, { ...defaultOptions, ...options });\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Clear every cookie NAME the current request sent — best-effort, not\r\n * exhaustive: HTTP gives the server no way to discover a cookie's `Path`\r\n * or `Domain`, only the name, so a cookie originally set on a `path` or\r\n * `domain` other than the one this call targets (the framework default,\r\n * or whatever is passed here / configured via `http.cookies.options`)\r\n * will NOT be deleted, and nothing will report that — the browser just\r\n * silently ignores a `Set-Cookie` whose scope doesn't match. Pass an\r\n * explicit `path` / `domain` for cookies the app knows it owns on a\r\n * non-default scope; call `clearCookie()` per name for anything else.\r\n *\r\n * @example\r\n * response.clearCookies();\r\n * response.clearCookies({ path: '/admin' });\r\n */\r\n public clearCookies(options?: CookieSerializeOptions) {\r\n for (const name of Object.keys(this.request.cookies)) {\r\n this.clearCookie(name, options);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Alias to header method\r\n */\r\n public setHeader(key: string, value: any) {\r\n return this.header(key, value);\r\n }\r\n\r\n /**\r\n * Send an error response with status code 500\r\n */\r\n public serverError(data: any) {\r\n return this.send(data, 500);\r\n }\r\n\r\n /**\r\n * Send a forbidden response with status code 403\r\n */\r\n public forbidden(\r\n data: any = {\r\n error: \"You are not allowed to access this resource, FORBIDDEN\",\r\n },\r\n ) {\r\n return this.send(data, 403);\r\n }\r\n\r\n /**\r\n * Send a service unavailable response with status code 503\r\n */\r\n public serviceUnavailable(data: any) {\r\n return this.send(data, 503);\r\n }\r\n\r\n /**\r\n * Send an unauthorized response with status code 401\r\n */\r\n public unauthorized(\r\n data: any = {\r\n error: \"unauthorized\",\r\n },\r\n ) {\r\n return this.send(data, 401);\r\n }\r\n\r\n /**\r\n * Send a not found response with status code 404\r\n */\r\n public notFound(\r\n data: any = {\r\n error: \"notFound\",\r\n },\r\n ) {\r\n return this.send(data, 404);\r\n }\r\n\r\n /**\r\n * Send a bad request response with status code 400\r\n */\r\n public badRequest(data: any) {\r\n return this.send(data, 400);\r\n }\r\n\r\n /**\r\n * Send a content too large response with status code 413\r\n */\r\n public contentTooLarge(data: any) {\r\n return this.send(data, 413);\r\n }\r\n\r\n /**\r\n * Send a success response with status code 201\r\n */\r\n public successCreate(data: any) {\r\n return this.send(data, 201);\r\n }\r\n\r\n /**\r\n * Send a success response\r\n */\r\n public success(data: any = { success: true }) {\r\n return this.send(data);\r\n }\r\n\r\n /**\r\n * Send a no content response with status code 204\r\n */\r\n public noContent() {\r\n return this.baseResponse.status(204).send();\r\n }\r\n\r\n /**\r\n * Send an accepted response with status code 202\r\n * Used for async operations that have been accepted but not yet processed\r\n */\r\n public accepted(data: any = { message: \"Request accepted for processing\" }) {\r\n return this.send(data, 202);\r\n }\r\n\r\n /**\r\n * Send a conflict response with status code 409\r\n */\r\n public conflict(data: any = { error: \"Resource conflict\" }) {\r\n return this.send(data, 409);\r\n }\r\n\r\n /**\r\n * Send a too many requests response with status code 429\r\n */\r\n public tooManyRequests(data: any) {\r\n return this.send(data, 429);\r\n }\r\n\r\n /**\r\n * Send an unprocessable entity response with status code 422\r\n * Used for semantic validation errors\r\n */\r\n public unprocessableEntity(data: any) {\r\n return this.send(data, 422);\r\n }\r\n\r\n /**\r\n * Apply response options (cache, disposition, etag)\r\n * Shared helper for sendFile and sendBuffer\r\n */\r\n /**\r\n * Build an RFC 6266–safe `Content-Disposition` value for a file name.\r\n *\r\n * HTTP header values must be ISO-8859-1; Node's `setHeader` throws\r\n * `ERR_INVALID_CHAR` on any other byte, so a raw `filename=\"تقرير.pdf\"` would\r\n * crash the response (a 500 the moment the name is non-ASCII). RFC 6266 solves\r\n * this with two parameters emitted together:\r\n * - `filename=\"<ascii>\"` — sanitised ASCII fallback for legacy clients\r\n * - `filename*=UTF-8''<pct>` — RFC 5987 ext-value; modern browsers restore\r\n * the real (e.g. Arabic) name.\r\n */\r\n private contentDisposition(type: \"inline\" | \"attachment\", rawName: string): string {\r\n // Strip CR/LF first so a crafted file name can never inject extra headers.\r\n const name = (rawName || \"file\").replace(/[\\r\\n]/g, \"\");\r\n\r\n // ASCII fallback: replace quote/backslash + every non-printable-ASCII byte\r\n // (covers all multibyte characters) so the quoted-string is always legal.\r\n const ascii =\r\n name\r\n .replace(/[\"\\\\]/g, \"_\")\r\n .replace(/[^\\x20-\\x7E]/g, \"_\")\r\n .trim() || \"file\";\r\n\r\n // Pure-ASCII name → the quoted form is enough; no ext-value needed.\r\n if (!/[^\\x20-\\x7E]/.test(name)) {\r\n return `${type}; filename=\"${ascii}\"`;\r\n }\r\n\r\n // RFC 5987 ext-value. `encodeURIComponent` leaves ' ( ) * unescaped, but they\r\n // are not valid `attr-char`, so percent-encode those too.\r\n const encoded = encodeURIComponent(name).replace(\r\n /['()*]/g,\r\n (char) => \"%\" + char.charCodeAt(0).toString(16).toUpperCase(),\r\n );\r\n\r\n return `${type}; filename=\"${ascii}\"; filename*=UTF-8''${encoded}`;\r\n }\r\n\r\n private applyResponseOptions(options: SendBufferOptions, defaultFilename?: string): boolean {\r\n // Set content type if provided\r\n if (options.contentType) {\r\n this.baseResponse.type(options.contentType);\r\n }\r\n\r\n // Set cache headers if specified\r\n if (options.cacheTime) {\r\n const cacheControl = options.immutable\r\n ? `public, max-age=${options.cacheTime}, immutable`\r\n : `public, max-age=${options.cacheTime}`;\r\n this.header(\"Cache-Control\", cacheControl);\r\n this.header(\"Expires\", new Date(Date.now() + options.cacheTime * 1000).toUTCString());\r\n }\r\n\r\n // Set ETag if provided (for conditional requests)\r\n if (options.etag) {\r\n this.header(\"ETag\", options.etag);\r\n\r\n // Check If-None-Match for conditional request\r\n const ifNoneMatch = this.request.header(\"if-none-match\");\r\n if (ifNoneMatch && ifNoneMatch === options.etag) {\r\n this.log(\"Content not modified (ETag match), sending 304\");\r\n this.baseResponse.status(304).send();\r\n return true; // Indicates 304 was sent\r\n }\r\n }\r\n\r\n // Set Content-Disposition if inline or filename is specified\r\n if (options.inline !== undefined || options.filename) {\r\n const disposition = options.inline ? \"inline\" : \"attachment\";\r\n const filename = options.filename || defaultFilename || \"file\";\r\n this.header(\"Content-Disposition\", this.contentDisposition(disposition, filename));\r\n }\r\n\r\n return false; // No 304 sent\r\n }\r\n\r\n /**\r\n * Send a file as a response\r\n */\r\n public async sendFile(filePath: string | StorageFile, options?: number | SendFileOptions) {\r\n if (filePath instanceof StorageFile) {\r\n filePath = filePath.absolutePath!;\r\n }\r\n\r\n this.log(`Sending file: ${filePath}`);\r\n\r\n // Check if file exists first\r\n if (!(await fileExistsAsync(filePath))) {\r\n return this.notFound({\r\n error: \"File Not Found\",\r\n });\r\n }\r\n\r\n try {\r\n // Normalize options to object format\r\n const opts = typeof options === \"number\" ? { cacheTime: options } : options || {};\r\n\r\n // Get file stats for ETag and Last-Modified\r\n const stats = await fs.promises.stat(filePath);\r\n const lastModified = stats.mtime;\r\n\r\n // Generate ETag based on file size and modification time\r\n const etag = `\"${stats.size}-${stats.mtime.getTime()}\"`;\r\n\r\n // Set Last-Modified header\r\n this.header(\"Last-Modified\", lastModified.toUTCString());\r\n this.header(\"ETag\", etag);\r\n\r\n // Set content type\r\n const contentType = this.getFileContentType(filePath);\r\n this.baseResponse.type(contentType);\r\n\r\n // Apply common response options (cache, disposition)\r\n const defaultFilename = path.basename(filePath);\r\n const sent304 = this.applyResponseOptions({ ...opts, etag, contentType }, defaultFilename);\r\n if (sent304) return this.baseResponse;\r\n\r\n // Check conditional request headers\r\n const ifNoneMatch = this.request.header(\"if-none-match\");\r\n const ifModifiedSince = this.request.header(\"if-modified-since\");\r\n\r\n // Handle If-None-Match (ETag validation)\r\n if (ifNoneMatch && ifNoneMatch === etag) {\r\n this.log(\"File not modified (ETag match), sending 304\");\r\n return this.baseResponse.status(304).send();\r\n }\r\n\r\n // Handle If-Modified-Since (Last-Modified validation)\r\n if (ifModifiedSince) {\r\n const modifiedSinceDate = new Date(ifModifiedSince);\r\n if (lastModified.getTime() <= modifiedSinceDate.getTime()) {\r\n this.log(\"File not modified (Last-Modified check), sending 304\");\r\n return this.baseResponse.status(304).send();\r\n }\r\n }\r\n\r\n // Use streaming for efficient file sending\r\n const stream = fs.createReadStream(filePath);\r\n\r\n // Handle stream errors\r\n stream.on(\"error\", (error) => {\r\n this.log(`Error reading file: ${error.message}`, \"error\");\r\n if (!this.baseResponse.sent) {\r\n this.serverError({\r\n error: \"Error reading file\",\r\n message: error.message,\r\n });\r\n }\r\n });\r\n\r\n // Send the stream (endTime will be set by finish event listener)\r\n return this.baseResponse.send(stream);\r\n } catch (error: any) {\r\n this.log(`Error sending file: ${error.message}`, \"error\");\r\n return this.serverError({\r\n error: \"Error sending file\",\r\n message: error.message,\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Send buffer as a response\r\n * Useful for dynamically generated content (e.g., resized images, generated PDFs)\r\n */\r\n public sendBuffer(buffer: Buffer, options?: number | SendBufferOptions) {\r\n this.log(\"Sending buffer\");\r\n\r\n // Normalize options to object format\r\n const opts = typeof options === \"number\" ? { cacheTime: options } : options || {};\r\n\r\n // Apply common response options (cache, disposition, etag)\r\n const sent304 = this.applyResponseOptions(opts);\r\n if (sent304) return this.baseResponse;\r\n\r\n // Note: endTime is set in the main send() method for non-streaming responses\r\n return this.baseResponse.send(buffer);\r\n }\r\n\r\n /**\r\n * Send an Image instance as a response\r\n * Automatically detects image format and sets content type\r\n */\r\n public async sendImage(\r\n image: any, // Type as 'any' to avoid circular dependency with Image class\r\n options?: number | (Omit<SendBufferOptions, \"contentType\"> & { contentType?: string }),\r\n ) {\r\n this.log(\"Sending image\");\r\n\r\n // Normalize options to object format\r\n const opts = typeof options === \"number\" ? { cacheTime: options } : options || {};\r\n\r\n // Get image metadata to determine format\r\n const metadata = await image.metadata();\r\n const format = metadata.format || \"jpeg\";\r\n\r\n // Convert image to buffer\r\n const buffer = await image.toBuffer();\r\n\r\n // Auto-set content type if not provided\r\n const contentType = opts.contentType || `image/${format}`;\r\n\r\n // Auto-generate ETag if not provided\r\n // Format: \"format-widthxheight-size\" (e.g., \"jpeg-800x600-45231\")\r\n // This catches changes in dimensions, quality, filters, and format\r\n if (!opts.etag) {\r\n const width = metadata.width || 0;\r\n const height = metadata.height || 0;\r\n opts.etag = `\"${format}-${width}x${height}-${buffer.length}\"`;\r\n }\r\n\r\n // Apply common response options with auto-detected content type\r\n const sent304 = this.applyResponseOptions({ ...opts, contentType });\r\n if (sent304) return this.baseResponse;\r\n\r\n // Note: endTime is set in the main send() method for non-streaming responses\r\n return this.baseResponse.send(buffer);\r\n }\r\n\r\n /**\r\n * Send file and cache it\r\n * Cache time in seconds\r\n * Cache time will be one year\r\n */\r\n public sendCachedFile(path: string | StorageFile, cacheTime = 31536000) {\r\n return this.sendFile(path, cacheTime);\r\n }\r\n\r\n /**\r\n * Download the given file path\r\n */\r\n public download(path: string, filename?: string) {\r\n return this.downloadFile(path, filename);\r\n }\r\n\r\n /**\r\n * Download the given file path\r\n */\r\n public async downloadFile(filePath: string, filename?: string) {\r\n // Check if file exists first\r\n if (!(await fileExistsAsync(filePath))) {\r\n return this.notFound({\r\n error: \"File Not Found\",\r\n });\r\n }\r\n\r\n try {\r\n if (!filename) {\r\n filename = path.basename(filePath);\r\n }\r\n\r\n this.baseResponse.header(\r\n \"Content-Disposition\",\r\n this.contentDisposition(\"attachment\", filename),\r\n );\r\n\r\n // this.baseResponse.header(\"Content-Type\", this.getFileContentType(filePath));\r\n this.baseResponse.header(\"Content-Type\", \"application/octet-stream\");\r\n\r\n const stream = fs.createReadStream(filePath);\r\n\r\n // Handle stream errors\r\n stream.on(\"error\", (error) => {\r\n this.log(`Error reading file for download: ${error.message}`, \"error\");\r\n if (!this.baseResponse.sent) {\r\n this.serverError({\r\n error: \"Error reading file\",\r\n message: error.message,\r\n });\r\n }\r\n });\r\n\r\n // Send the stream (endTime will be set by finish event listener)\r\n return this.baseResponse.send(stream);\r\n } catch (error: any) {\r\n this.log(`Error downloading file: ${error.message}`, \"error\");\r\n return this.serverError({\r\n error: \"Error downloading file\",\r\n message: error.message,\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Get content type of the given path\r\n */\r\n public getFileContentType(filePath: string) {\r\n const type = mime.getType(filePath) || \"application/octet-stream\";\r\n return type;\r\n }\r\n\r\n /**\r\n * Mark the response as failed\r\n */\r\n public failedSchema(result: ValidationResult) {\r\n const { errors, inputKey, inputError, status } = config.get(\"validation.response\", {\r\n errors: \"errors\",\r\n inputKey: \"input\",\r\n inputError: \"error\",\r\n status: 422,\r\n });\r\n\r\n log.error(\"request\", \"validation\", `${this.request.id} - Validation failed`);\r\n\r\n return this.send(\r\n {\r\n [errors]: result.errors.map((error) => ({\r\n [inputKey]: error.input,\r\n [inputError]: error.error,\r\n })),\r\n },\r\n status,\r\n );\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region ../core/src/http/xmlable.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A class contract for anything `response.xml()` can serialize.
|
|
4
|
+
*
|
|
5
|
+
* Structurally typed on purpose: `@warlock.js/sitemap` (or a future RSS/Atom
|
|
6
|
+
* feed package) never imports core — a `Sitemap` satisfies `XMLable` just by
|
|
7
|
+
* having a `toXML()` method.
|
|
8
|
+
*/
|
|
9
|
+
interface XMLable {
|
|
10
|
+
toXML(): string;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { XMLable };
|
|
14
|
+
//# sourceMappingURL=xmlable.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xmlable.d.mts","names":[],"sources":["../../../../../../../core/src/http/xmlable.ts"],"mappings":";;AAOA;;;;AACO;;UADU,OAAA;EACf,KAAK;AAAA"}
|
package/esm/index.d.mts
CHANGED
|
@@ -16,6 +16,7 @@ import { LocalDriver } from "./storage/drivers/local-driver.mjs";
|
|
|
16
16
|
import { R2Driver } from "./storage/drivers/r2-driver.mjs";
|
|
17
17
|
import { S3Driver } from "./storage/drivers/s3-driver.mjs";
|
|
18
18
|
import { PipeableReactStream, StreamReactResponseOptions, streamReactResponse } from "./http/stream-react-response.mjs";
|
|
19
|
+
import { XMLable } from "./http/xmlable.mjs";
|
|
19
20
|
import { CookieOptions, Response, ResponseStatus, SendBufferOptions, SendFileOptions } from "./http/response.mjs";
|
|
20
21
|
import { Image, ImageFormat, ImageInput, ImageTransformOptions, WatermarkConfig } from "./image/image.mjs";
|
|
21
22
|
import { FileNamingStrategy, ImageTransformCallback, ImageTransformConfig, PrefixConfig, PrefixOptions, SaveAsOptions, SaveOptions, UploadedFileImageOptions, UploadsConfigurations } from "./http/uploads-types.mjs";
|
|
@@ -167,7 +168,7 @@ import { WarlockConfigManager, isUnknownTsExtensionError, warlockConfigManager }
|
|
|
167
168
|
import { env } from "@mongez/dotenv";
|
|
168
169
|
import { colors } from "@mongez/copper";
|
|
169
170
|
export * from "@mongez/localization";
|
|
170
|
-
export { $registerUseCase, $unregisterUseCase, AccessConnector, AggregateExpressionInput, AggregateExpressions, AiConnector, AllRepositoryOptions, AppConfigurations, Application, BOOT_SIGNAL_ENV_KEY, BOOT_SIGNAL_VERSION, BadRequestError, BadSchemaUseCaseError, BaseConnector, type BenchmarkChannel, type BenchmarkConfigurations, type BenchmarkErrorResult, type BenchmarkOptions, BenchmarkProfiler, type BenchmarkProfilerOptions, type BenchmarkResult, BenchmarkSnapshots, type BenchmarkSnapshotsOptions, type BenchmarkStats, type BenchmarkSuccessResult, BootContext, BootListener, BootSignal, BootSignalType, BootValidator, CLICommand, type CLICommandAction, type CLICommandOption, type CLICommandOptions, type CLICommandPreload, type CLICommandSource, CacheConnector, type CacheMiddlewareOptions, CachedRepositoryOptions, type CapturedMail, CascadeAdapter, CascadeQueryBuilder, ChunkCallback, ClosableServer, CloudDriver, CloudStorageDriverContract, CloudStorageDriverOptions, CloudStorageFileData, type CommandActionData, type ConcurrencyLimitOptions, ConfigKey, ConfigKeyRegistry, ConfigName, ConfigRegistry, ConfigSpecialHandlers, ConflictError, Connector, ConnectorBuildContext, ConnectorBuildContribution, ConnectorBuildGenerateResult, ConnectorEsbuildPatch, ConnectorLifecyclePhase, ConnectorName, ConnectorPriority, ConnectorsManager, ConsoleChannel, ContainerTypes, CookieJarUnavailableError, CookieOptions, CspConfig, CursorPaginationOptions, CursorPaginationResult, DEFAULT_CSP_DIRECTIVES, DOSpacesDriver, DatabaseCacheDriver, type DatabaseCacheOptions, DatabaseConnector, DatabaseLog, DatabaseLogModel, DatabaseLogOptions, DecodedAccessToken, DefineResourceOptions, DeleteManyResult, EncryptionConfigurations, EncryptionPasswordConfigurations, Environment, EventSubscription, ExistsExceptCurrentIdRuleOptions, ExistsExceptCurrentUserRuleOptions, FastifyInstance, FileNamingStrategy, FileValidationOptions, FileValidator, FileVisibility, FilterFunction, FilterOperator, FilterOptions, FilterRule, FilterRules, ForbiddenError, GroupByFields, GroupedRoutesOptions, HealthCheck, HealthStatus, HeraldConnector, HttpConfigurations, HttpConnector, HttpContext, HttpError, HttpErrorCodes, HttpReadyReport, HttpTracingConfig, type IdempotencyOptions, Image, ImageFormat, ImageInput, ImageTransformCallback, ImageTransformConfig, ImageTransformOptions, InvalidCspDirectiveError, type IpFilterOptions, ListOptions, LocalDriver, LocalStorageDriverOptions, LocalizedObject, LogConfigurations, LoggerConnector, MAIL_EVENTS, Mail, type MailAddress, type MailAttachment, type MailConfigurations, MailError, type MailErrorCode, type MailEvents, type MailMode, type MailOptions, type MailPriority, type MailResult, MailerConnector, type MailersConfig, type MaintenanceOptions, Middleware, MiddlewareResponse, MimeTypes, NoopChannel, type NormalizedMail, NotAcceptableError, NotAllowedError, NotificationsConnector, PaginationMode, PaginationResult, PartialMiddleware, PartialPick, Path, PipeableReactStream, PipelineOptions, PortInUseError, PositionalHandlerSuspect, PrefixConfig, PrefixOptions, PresignedOptions, PresignedUploadOptions, PutDirectoryOptions, PutDirectoryResult, PutFromUrlOptions, PutOptions, QueryBuilderContract, Queue, R2Driver, R2StorageDriverOptions, type RateLimitOptions, RegisterConfiguredConnectorsOptions, RegisterResource, RegisteredUseCase, RepositoryAdapterContract, RepositoryConfigurations, RepositoryEvent, RepositoryManager, RepositoryOptions, RepositoryOptionsWithCursor, RepositoryOptionsWithPages, Request, RequestContextStore, RequestController, RequestControllerContract, RequestEvent, RequestHandler, RequestHandlerType, RequestHandlerValidation, RequestLocals, RequestLog, RequestMethod, RequestUserMovedError, type ResolvedCLICommandOption, Resource, ResourceArraySchema, ResourceCastType, ResourceConstructor, ResourceContract, ResourceFieldBuilder, ResourceFieldBuilderDateOutputOptions, ResourceFieldConfig, ResourceMethod, ResourceNotFoundError, ResourceOutputValueCastType, ResourceSchema, ResourceSelfReference, Response, ResponseBodyValue, ResponseEvent, ResponseSSEController, ResponseSchema, ResponseStatus, ResponseStreamController, Restful, RestfulMiddleware, ReturnedResponse, Route, RouteOptions, RouteRegistry, RouteResource, Router, RouterGroupCallback, RouterStacks, RuntimeStrategy, S3Driver, type SESConfigurations, type SMTPConfigurations, SafeFetchOptions, SafeFetchResult, SaveAsOptions, SaveMode, SaveOptions, ScopedStorage, ScopedStorageContract, SeedClock, SeedContext, SeedRecordRef, SeedResult, Seeder, SeederDependencyCycleError, SeederMetadata, SendBufferOptions, SendFileOptions, ServerError, ShutdownListener, SocketConnector, SocketOptions, Storage, StorageConfigurations, StorageConnector, StorageCopyEventPayload, StorageDriverConfig, StorageDriverContextStore, StorageDriverContract, StorageDriverName, StorageDriverRegistry, StorageDriverType, StorageError, StorageErrorOptions, StorageEventHandler, StorageEventPayload, StorageEventType, StorageFile, StorageFileData, StorageFileInfo, StorageManagerContract, StoragePutEventPayload, StreamReactResponseOptions, TemporaryTokenError, TemporaryTokenPayload, TemporaryTokenValidation, TracingContext, TracingContextSource, TracingHooks, TracingPhaseInfo, TracingRequestEndInfo, Track, TrackableModel, TypedAllRepositoryOptions, TypedRepositoryOptions, TypedRepositoryOptionsWithCursor, TypedRepositoryOptionsWithPages, UPLOADS_DEFAULTS, UnAuthorizedError, UniqueExceptCurrentIdRuleOptions, UniqueExceptCurrentUserRuleOptions, UnknownSeederDependencyError, UploadedFile, UploadedFileImageOptions, UploadedFileJson, UploadsConfigurations, UseCase, UseCaseAfterMiddleware, UseCaseBeforeMiddleware, UseCaseBroadcastChannel, UseCaseBroadcastEvent, UseCaseBroadcastOption, UseCaseConfigurations, UseCaseContext, UseCaseErrorResult, UseCaseEventsCallbacksMap, UseCaseGuard, UseCaseHandler, UseCaseOnExecutingContext, UseCaseResult, UseCaseRuntimeOptions, UseCaseWithSchema, ValidationConfiguration, WarlockConfig, WarlockConfigManager, WatermarkConfig, WhereOperator, addUseCaseHistory, anyMatch, app, appLog, appPath, applyCspHeader, assertConfiguredHttpPortIsFree, assertMailCount, assertMailSent, assertNoReservedConnectorNames, assertPortIsAvailable, assertUniqueConnectorNames, assetsUrl, bootstrap, broadcastUseCaseResult, buildCspHeaderValue, buildIdempotencyCacheKey, buildTracingContext, cachePath, captureMail, clearPositionalHandlerSuspects, clearTestMailbox, closeAllMailers, closeMailer, closeServerWithTimeout, colors, command, config, configPath, configSpecialHandlers, connectorsManager, container, createHttpApplication, createRequestStore, decrypt, defaultHttpConfigurations, defineConfig, defineResource, deriveTraceId, describePositionalHandlerSuspect, dispatchPhase, dispatchRequestEnd, dispatchRequestStart, displayEnvironmentMode, encrypt, ensureFatalIsVisible, env, environment, existsExceptCurrentIdRule, existsExceptCurrentUserRule, fetchLatestVersion, fileExtensionRule, fileRule, fileTypeRule, findMailsBySubject, findMailsTo, fireLifecycleEvent, forgetPositionalHandlerSuspects, generateMailId, getDefaultMailConfig, getHttpReadyReport, getHttpServer, getLastMail, getLocalized, getMailEventName, getMailMode, getMailboxSize, getMailer, getMailerConfig, getMimeType, getPoolStats, getPublicUrl, getSocketServer, getTestMailbox, getUseCase, getUseCaseHistory, getUseCases, globalEventsCallbacksMap, globalUseCasesEvents, hashBody, hashPassword, health, hmacHash, httpConfig, imageRule, increaseUseCaseFailedCalls, increaseUseCaseSuccessCalls, inspectHandlerSignature, ipMatches, isBootSignal, isDevelopmentMode, isNewerVersion, isPortAvailable, isPrivateOrReservedIp, isProductionMode, isTestMode, isTracingEnabled, isUnknownTsExtensionError, isValidIdempotencyKey, listPositionalHandlerSuspects, loadS3, logResponse, logsPath, looksLikePositionalHandler, mailEvents, matchesDerivedRouteName, measure, mergeCspDirectives, middleware, normalizeRequestPath, normalizeRoutePath, parseSize, parseTraceparentTraceId, paths, preflightConfiguredHttpPort, promiseAllObject, publicPath, publicUrl, registerAppConfig, registerConfiguredConnectors, registerHttpPlugins, remedyLines, renderReact, renderReactMail, reportPositionalHandlerSuspects, requestContext, requestMemo, resetHttpReadyReport, resetMailConfig, resetTracingConfigForTests, resolveCspConfig, resolveMailConfig, resolveTracingConfig, resolveWithinRoot, rootPath, routeNameMethodSuffix, router, runPipeline, safeFetchToBuffer, sanitizePath, seeder, sendBootSignal, sendMail, serializeCspDirectives, setBaseUrl, setConfig, setEnvironment, setHttpReadyReport, setLogConfigurations, setMailConfigurations, setMailMode, shouldPreflightHttpPort, sleep, sluggable, srcPath, startHttpServer, stopHttpApplication, storage, storageConfig, storageConfigurations, storageDriverContext, storagePath, streamReactResponse, t, tempPath, toJson, uniqueExceptCurrentIdRule, uniqueExceptCurrentUserRule, uploadedFileMetadataSchema, uploadsConfig, uploadsPath, uploadsUrl, url, useCase, useComputedModel, useComputedSlug, useCurrentUser, useHashedPassword, useRequest, useRequestStore, validateCspConfigAtBoot, validateCspDirectives, verifyMailer, verifyPassword, warlockConfigManager, warlockPath, wasMailSentTo, wasMailSentWithSubject, wrapResponseInDataKey };
|
|
171
|
+
export { $registerUseCase, $unregisterUseCase, AccessConnector, AggregateExpressionInput, AggregateExpressions, AiConnector, AllRepositoryOptions, AppConfigurations, Application, BOOT_SIGNAL_ENV_KEY, BOOT_SIGNAL_VERSION, BadRequestError, BadSchemaUseCaseError, BaseConnector, type BenchmarkChannel, type BenchmarkConfigurations, type BenchmarkErrorResult, type BenchmarkOptions, BenchmarkProfiler, type BenchmarkProfilerOptions, type BenchmarkResult, BenchmarkSnapshots, type BenchmarkSnapshotsOptions, type BenchmarkStats, type BenchmarkSuccessResult, BootContext, BootListener, BootSignal, BootSignalType, BootValidator, CLICommand, type CLICommandAction, type CLICommandOption, type CLICommandOptions, type CLICommandPreload, type CLICommandSource, CacheConnector, type CacheMiddlewareOptions, CachedRepositoryOptions, type CapturedMail, CascadeAdapter, CascadeQueryBuilder, ChunkCallback, ClosableServer, CloudDriver, CloudStorageDriverContract, CloudStorageDriverOptions, CloudStorageFileData, type CommandActionData, type ConcurrencyLimitOptions, ConfigKey, ConfigKeyRegistry, ConfigName, ConfigRegistry, ConfigSpecialHandlers, ConflictError, Connector, ConnectorBuildContext, ConnectorBuildContribution, ConnectorBuildGenerateResult, ConnectorEsbuildPatch, ConnectorLifecyclePhase, ConnectorName, ConnectorPriority, ConnectorsManager, ConsoleChannel, ContainerTypes, CookieJarUnavailableError, CookieOptions, CspConfig, CursorPaginationOptions, CursorPaginationResult, DEFAULT_CSP_DIRECTIVES, DOSpacesDriver, DatabaseCacheDriver, type DatabaseCacheOptions, DatabaseConnector, DatabaseLog, DatabaseLogModel, DatabaseLogOptions, DecodedAccessToken, DefineResourceOptions, DeleteManyResult, EncryptionConfigurations, EncryptionPasswordConfigurations, Environment, EventSubscription, ExistsExceptCurrentIdRuleOptions, ExistsExceptCurrentUserRuleOptions, FastifyInstance, FileNamingStrategy, FileValidationOptions, FileValidator, FileVisibility, FilterFunction, FilterOperator, FilterOptions, FilterRule, FilterRules, ForbiddenError, GroupByFields, GroupedRoutesOptions, HealthCheck, HealthStatus, HeraldConnector, HttpConfigurations, HttpConnector, HttpContext, HttpError, HttpErrorCodes, HttpReadyReport, HttpTracingConfig, type IdempotencyOptions, Image, ImageFormat, ImageInput, ImageTransformCallback, ImageTransformConfig, ImageTransformOptions, InvalidCspDirectiveError, type IpFilterOptions, ListOptions, LocalDriver, LocalStorageDriverOptions, LocalizedObject, LogConfigurations, LoggerConnector, MAIL_EVENTS, Mail, type MailAddress, type MailAttachment, type MailConfigurations, MailError, type MailErrorCode, type MailEvents, type MailMode, type MailOptions, type MailPriority, type MailResult, MailerConnector, type MailersConfig, type MaintenanceOptions, Middleware, MiddlewareResponse, MimeTypes, NoopChannel, type NormalizedMail, NotAcceptableError, NotAllowedError, NotificationsConnector, PaginationMode, PaginationResult, PartialMiddleware, PartialPick, Path, PipeableReactStream, PipelineOptions, PortInUseError, PositionalHandlerSuspect, PrefixConfig, PrefixOptions, PresignedOptions, PresignedUploadOptions, PutDirectoryOptions, PutDirectoryResult, PutFromUrlOptions, PutOptions, QueryBuilderContract, Queue, R2Driver, R2StorageDriverOptions, type RateLimitOptions, RegisterConfiguredConnectorsOptions, RegisterResource, RegisteredUseCase, RepositoryAdapterContract, RepositoryConfigurations, RepositoryEvent, RepositoryManager, RepositoryOptions, RepositoryOptionsWithCursor, RepositoryOptionsWithPages, Request, RequestContextStore, RequestController, RequestControllerContract, RequestEvent, RequestHandler, RequestHandlerType, RequestHandlerValidation, RequestLocals, RequestLog, RequestMethod, RequestUserMovedError, type ResolvedCLICommandOption, Resource, ResourceArraySchema, ResourceCastType, ResourceConstructor, ResourceContract, ResourceFieldBuilder, ResourceFieldBuilderDateOutputOptions, ResourceFieldConfig, ResourceMethod, ResourceNotFoundError, ResourceOutputValueCastType, ResourceSchema, ResourceSelfReference, Response, ResponseBodyValue, ResponseEvent, ResponseSSEController, ResponseSchema, ResponseStatus, ResponseStreamController, Restful, RestfulMiddleware, ReturnedResponse, Route, RouteOptions, RouteRegistry, RouteResource, Router, RouterGroupCallback, RouterStacks, RuntimeStrategy, S3Driver, type SESConfigurations, type SMTPConfigurations, SafeFetchOptions, SafeFetchResult, SaveAsOptions, SaveMode, SaveOptions, ScopedStorage, ScopedStorageContract, SeedClock, SeedContext, SeedRecordRef, SeedResult, Seeder, SeederDependencyCycleError, SeederMetadata, SendBufferOptions, SendFileOptions, ServerError, ShutdownListener, SocketConnector, SocketOptions, Storage, StorageConfigurations, StorageConnector, StorageCopyEventPayload, StorageDriverConfig, StorageDriverContextStore, StorageDriverContract, StorageDriverName, StorageDriverRegistry, StorageDriverType, StorageError, StorageErrorOptions, StorageEventHandler, StorageEventPayload, StorageEventType, StorageFile, StorageFileData, StorageFileInfo, StorageManagerContract, StoragePutEventPayload, StreamReactResponseOptions, TemporaryTokenError, TemporaryTokenPayload, TemporaryTokenValidation, TracingContext, TracingContextSource, TracingHooks, TracingPhaseInfo, TracingRequestEndInfo, Track, TrackableModel, TypedAllRepositoryOptions, TypedRepositoryOptions, TypedRepositoryOptionsWithCursor, TypedRepositoryOptionsWithPages, UPLOADS_DEFAULTS, UnAuthorizedError, UniqueExceptCurrentIdRuleOptions, UniqueExceptCurrentUserRuleOptions, UnknownSeederDependencyError, UploadedFile, UploadedFileImageOptions, UploadedFileJson, UploadsConfigurations, UseCase, UseCaseAfterMiddleware, UseCaseBeforeMiddleware, UseCaseBroadcastChannel, UseCaseBroadcastEvent, UseCaseBroadcastOption, UseCaseConfigurations, UseCaseContext, UseCaseErrorResult, UseCaseEventsCallbacksMap, UseCaseGuard, UseCaseHandler, UseCaseOnExecutingContext, UseCaseResult, UseCaseRuntimeOptions, UseCaseWithSchema, ValidationConfiguration, WarlockConfig, WarlockConfigManager, WatermarkConfig, WhereOperator, XMLable, addUseCaseHistory, anyMatch, app, appLog, appPath, applyCspHeader, assertConfiguredHttpPortIsFree, assertMailCount, assertMailSent, assertNoReservedConnectorNames, assertPortIsAvailable, assertUniqueConnectorNames, assetsUrl, bootstrap, broadcastUseCaseResult, buildCspHeaderValue, buildIdempotencyCacheKey, buildTracingContext, cachePath, captureMail, clearPositionalHandlerSuspects, clearTestMailbox, closeAllMailers, closeMailer, closeServerWithTimeout, colors, command, config, configPath, configSpecialHandlers, connectorsManager, container, createHttpApplication, createRequestStore, decrypt, defaultHttpConfigurations, defineConfig, defineResource, deriveTraceId, describePositionalHandlerSuspect, dispatchPhase, dispatchRequestEnd, dispatchRequestStart, displayEnvironmentMode, encrypt, ensureFatalIsVisible, env, environment, existsExceptCurrentIdRule, existsExceptCurrentUserRule, fetchLatestVersion, fileExtensionRule, fileRule, fileTypeRule, findMailsBySubject, findMailsTo, fireLifecycleEvent, forgetPositionalHandlerSuspects, generateMailId, getDefaultMailConfig, getHttpReadyReport, getHttpServer, getLastMail, getLocalized, getMailEventName, getMailMode, getMailboxSize, getMailer, getMailerConfig, getMimeType, getPoolStats, getPublicUrl, getSocketServer, getTestMailbox, getUseCase, getUseCaseHistory, getUseCases, globalEventsCallbacksMap, globalUseCasesEvents, hashBody, hashPassword, health, hmacHash, httpConfig, imageRule, increaseUseCaseFailedCalls, increaseUseCaseSuccessCalls, inspectHandlerSignature, ipMatches, isBootSignal, isDevelopmentMode, isNewerVersion, isPortAvailable, isPrivateOrReservedIp, isProductionMode, isTestMode, isTracingEnabled, isUnknownTsExtensionError, isValidIdempotencyKey, listPositionalHandlerSuspects, loadS3, logResponse, logsPath, looksLikePositionalHandler, mailEvents, matchesDerivedRouteName, measure, mergeCspDirectives, middleware, normalizeRequestPath, normalizeRoutePath, parseSize, parseTraceparentTraceId, paths, preflightConfiguredHttpPort, promiseAllObject, publicPath, publicUrl, registerAppConfig, registerConfiguredConnectors, registerHttpPlugins, remedyLines, renderReact, renderReactMail, reportPositionalHandlerSuspects, requestContext, requestMemo, resetHttpReadyReport, resetMailConfig, resetTracingConfigForTests, resolveCspConfig, resolveMailConfig, resolveTracingConfig, resolveWithinRoot, rootPath, routeNameMethodSuffix, router, runPipeline, safeFetchToBuffer, sanitizePath, seeder, sendBootSignal, sendMail, serializeCspDirectives, setBaseUrl, setConfig, setEnvironment, setHttpReadyReport, setLogConfigurations, setMailConfigurations, setMailMode, shouldPreflightHttpPort, sleep, sluggable, srcPath, startHttpServer, stopHttpApplication, storage, storageConfig, storageConfigurations, storageDriverContext, storagePath, streamReactResponse, t, tempPath, toJson, uniqueExceptCurrentIdRule, uniqueExceptCurrentUserRule, uploadedFileMetadataSchema, uploadsConfig, uploadsPath, uploadsUrl, url, useCase, useComputedModel, useComputedSlug, useCurrentUser, useHashedPassword, useRequest, useRequestStore, validateCspConfigAtBoot, validateCspDirectives, verifyMailer, verifyPassword, warlockConfigManager, warlockPath, wasMailSentTo, wasMailSentWithSubject, wrapResponseInDataKey };
|
|
171
172
|
import "./config/types.mjs";
|
|
172
173
|
import "./http/request.mjs";
|
|
173
174
|
import "./http/types.mjs";
|
package/llms-full.txt
CHANGED
|
@@ -964,7 +964,7 @@ Don't call `setBaseUrl` per request — it's process-global and races every othe
|
|
|
964
964
|
|
|
965
965
|
---
|
|
966
966
|
name: configure-app
|
|
967
|
-
description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app''s public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap
|
|
967
|
+
description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app''s public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap refusal on a missing origin — `@warlock.js/web/generate-sitemap/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.'
|
|
968
968
|
---
|
|
969
969
|
|
|
970
970
|
# Warlock — configure the app
|
|
@@ -1224,11 +1224,11 @@ falling back to the `PUBLIC_APP_URL` env var, or `undefined` when neither is
|
|
|
1224
1224
|
set.
|
|
1225
1225
|
|
|
1226
1226
|
`getPublicUrl()` never throws — it is a consumer's job to fail loudly when it
|
|
1227
|
-
requires the value. `@warlock.js/
|
|
1228
|
-
`sitemap.enabled: true` and no `app.publicUrl`/`PUBLIC_APP_URL` set,
|
|
1229
|
-
refuses
|
|
1230
|
-
back to a request-derived host — a sitemap
|
|
1231
|
-
worse than
|
|
1227
|
+
requires the value. `@warlock.js/web`'s sitemap is the first such consumer:
|
|
1228
|
+
with `web.sitemap.enabled: true` and no `app.publicUrl`/`PUBLIC_APP_URL` set,
|
|
1229
|
+
generation refuses (`MissingPublicUrlError`, naming both) rather than falling
|
|
1230
|
+
back to a request-derived host — a sitemap pointing at the wrong host is
|
|
1231
|
+
worse than none. See `@warlock.js/web/generate-sitemap/SKILL.md`.
|
|
1232
1232
|
|
|
1233
1233
|
## Common patterns
|
|
1234
1234
|
|
|
@@ -1309,6 +1309,20 @@ Scaffold with: `npx warlock generate.controller <module>/<action>` (add `--with-
|
|
|
1309
1309
|
|
|
1310
1310
|
Prefer `request.validated()` once a schema is attached — it's typed.
|
|
1311
1311
|
|
|
1312
|
+
### Request body content types
|
|
1313
|
+
|
|
1314
|
+
`request.input()` / `.all()` / `.validated()` read the same way regardless of how the body arrived — every content type below feeds the same parsed bag:
|
|
1315
|
+
|
|
1316
|
+
| Content type | Parsed by | Notes |
|
|
1317
|
+
| ------------------------------------ | -------------------------------- | --------------------------------------------------------------------- |
|
|
1318
|
+
| `application/json` | Fastify (built-in) | objects/arrays parsed as-is |
|
|
1319
|
+
| `multipart/form-data` | `@fastify/multipart` | fields + files; see [`upload-file`](../upload-file/SKILL.md) |
|
|
1320
|
+
| `application/x-www-form-urlencoded` | Warlock's own content-type parser (`http/parse-urlencoded-body.ts`) | plain HTML forms, OAuth `form_post` callbacks (e.g. Apple Sign in) |
|
|
1321
|
+
|
|
1322
|
+
For urlencoded bodies: fields decode via `URLSearchParams`. A key sent more than once (`tag=a&tag=b`) becomes an array (`request.input("tag")` → `["a", "b"]`); every other key is a plain string. Bracket-notation keys (`a[b]=1`) are **not** expanded by the urlencoded parser itself — nesting only happens through the same shared bracket-key logic every body type already goes through, so it behaves exactly like a JSON or query-string key of that shape, no differently than today.
|
|
1323
|
+
|
|
1324
|
+
All three content types are held to the same `http.bodyLimit` — an over-limit urlencoded body is rejected with the same `413` a JSON body would get.
|
|
1325
|
+
|
|
1312
1326
|
## Returning output
|
|
1313
1327
|
|
|
1314
1328
|
Pick the helper that matches the outcome. Full surface in [send-response](../send-response/SKILL.md). Quick map:
|
|
@@ -2416,6 +2430,8 @@ GET /ready → 200 {"status":"ok","checks":{"db":true}}
|
|
|
2416
2430
|
### Config
|
|
2417
2431
|
|
|
2418
2432
|
```ts title="src/config/http.ts"
|
|
2433
|
+
import type { HttpConfigurations } from "@warlock.js/core";
|
|
2434
|
+
|
|
2419
2435
|
const httpConfigurations: HttpConfigurations = {
|
|
2420
2436
|
health: {
|
|
2421
2437
|
enabled: true, // default; set false to remove both endpoints
|
|
@@ -2423,6 +2439,8 @@ const httpConfigurations: HttpConfigurations = {
|
|
|
2423
2439
|
readinessPath: "/ready", // readiness path
|
|
2424
2440
|
},
|
|
2425
2441
|
};
|
|
2442
|
+
|
|
2443
|
+
export default httpConfigurations;
|
|
2426
2444
|
```
|
|
2427
2445
|
|
|
2428
2446
|
## Readiness checks
|
|
@@ -2463,12 +2481,16 @@ On SIGINT/SIGTERM the framework tears down in order: **app `onShutdown` hooks
|
|
|
2463
2481
|
3. Draining is bounded by a timeout so one stuck request can't hang the deploy — after it, the server force-closes and a warning is logged.
|
|
2464
2482
|
|
|
2465
2483
|
```ts title="src/config/http.ts"
|
|
2484
|
+
import type { HttpConfigurations } from "@warlock.js/core";
|
|
2485
|
+
|
|
2466
2486
|
const httpConfigurations: HttpConfigurations = {
|
|
2467
2487
|
gracefulShutdown: {
|
|
2468
2488
|
timeout: 10_000, // ms to wait for in-flight drain (default 10s)
|
|
2469
2489
|
forceCloseConnections: "idle", // close idle keep-alives, let active finish (default)
|
|
2470
2490
|
},
|
|
2471
2491
|
};
|
|
2492
|
+
|
|
2493
|
+
export default httpConfigurations;
|
|
2472
2494
|
```
|
|
2473
2495
|
|
|
2474
2496
|
`forceCloseConnections`: `"idle"` (default) closes idle keep-alive connections and lets active requests finish; `true` force-closes everything immediately; `false` waits for every connection.
|
|
@@ -4489,6 +4511,8 @@ return response.sendBuffer(buffer, { contentType: "image/png" });
|
|
|
4489
4511
|
|
|
4490
4512
|
`SendFileOptions` lets you set `cacheTime`, `immutable`, `inline`, `filename` (download attachment name).
|
|
4491
4513
|
|
|
4514
|
+
`response.xml(string | XMLable, statusCode?)` sends `application/xml` — pass a raw XML string, or anything with a `toXML(): string` method (structural, so core never depends on `@warlock.js/sitemap`). This is for BOUNDED values only: a raw string, or a single `@warlock.js/sitemap` `Sitemap` under the sitemaps.org 50,000-URL / 50MB ceiling. `SitemapIndex` has no `toXML()` — it is the streaming path for larger URL sets and does not fit in one response body. It writes shards plus a master index as FILES via `saveTo`/`publish`, and those files are served directly rather than passed through `xml()`. In Warlock web this is the `web.sitemap` config: `enabled`, `outputDir` (default `storagePath("sitemap")`), and `path` (default `/sitemap.xml`) — the app serves the generated files from `outputDir` at that route, it never builds a `SitemapIndex` response inline.
|
|
4515
|
+
|
|
4492
4516
|
## Streams
|
|
4493
4517
|
|
|
4494
4518
|
```ts
|
package/llms.txt
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
- [benchmark-code](@warlock.js/core/benchmark-code/SKILL.md): Wrap a function with `measure(name, fn, options?)` to time it and classify the latency — onComplete/onError/onFinish hooks, `latencyRange` thresholds, `BenchmarkProfiler` for percentiles, `BenchmarkSnapshots` for raw captures. Triggers: `measure`, `BenchmarkProfiler`, `BenchmarkSnapshots`, `BenchmarkChannel`, `ConsoleChannel`, `latencyRange`, `shouldBenchmarkError`; "time this operation", "profile a slow service", "emit p50/p95/p99 metrics", "classify latency against thresholds"; typical import `import { measure, BenchmarkProfiler } from "@warlock.js/core"`. Skip: retry composition — `@warlock.js/core/retry-operation/SKILL.md`; benchmark config wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs `prom-client`, `pino`, `perf_hooks`, `console.time`.
|
|
11
11
|
- [build-restful](@warlock.js/core/build-restful/SKILL.md): Generate standard CRUD endpoints — via `router.route(path).list().show().create().update().destroy()` chain or the `Restful` base class. Pick the chain by default; reach for `Restful` when you want repository-bound defaults. Triggers: `router.route`, `Restful`, `router.restfulResource`, `RouteResource`, `.crud`, `.nest`, `beforeCreate`, `onCreate`; "build a CRUD API", "register list/show/create/update/destroy", "repository-bound default handlers", "override a single REST action"; typical import `import { router, Restful } from "@warlock.js/core"`. Skip: wider router surface — `@warlock.js/core/register-route/SKILL.md`; per-action controllers — `@warlock.js/core/create-controller/SKILL.md`; wire mapping — `@warlock.js/core/define-resource/SKILL.md`; competing pattern: hand-rolled controllers, `@nestjs/swagger` decorator-driven CRUD.
|
|
12
12
|
- [build-url](@warlock.js/core/build-url/SKILL.md): HTTP URL helpers — `url`, `publicUrl`, `assetsUrl`, `uploadsUrl`, anchored at `app.baseUrl`. Use to render `src` / `href` / API URLs in resources and responses. `setBaseUrl` is wired by the HTTP connector from `config.get("app.baseUrl")`. Triggers: `url`, `publicUrl`, `assetsUrl`, `uploadsUrl`, `setBaseUrl`, `BASE_URL`; "render an avatar src URL", "absolute download link", "embed asset URL in email", "URL helpers vs path helpers"; typical import `import { url, publicUrl, uploadsUrl } from "@warlock.js/core"`. Skip: filesystem paths — `@warlock.js/core/resolve-path/SKILL.md`; signed CDN URLs — `@warlock.js/core/store-file/SKILL.md`; resource output — `@warlock.js/core/define-resource/SKILL.md`; competing patterns: hand-rolled `${baseUrl}/...` template strings.
|
|
13
|
-
- [configure-app](@warlock.js/core/configure-app/SKILL.md): Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app's public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap
|
|
13
|
+
- [configure-app](@warlock.js/core/configure-app/SKILL.md): Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app's public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap refusal on a missing origin — `@warlock.js/web/generate-sitemap/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.
|
|
14
14
|
- [create-controller](@warlock.js/core/create-controller/SKILL.md): Author HTTP controllers in @warlock.js/core — RequestHandler signature, validated input via seal schemas, response helpers, attaching metadata. Controllers are thin functions; business logic moves to services or use-cases. Triggers: `RequestHandler`, `Request<TSchema>`, `GuardedRequestHandler`, `request.validated`, `request.input`, `controller.validation`, `response.success`, `response.successCreate`; "write a controller", "attach a schema to a handler", "thin controller pattern", "guarded request type"; typical import `import { type RequestHandler } from "@warlock.js/core"`. Skip: response helper menu — `@warlock.js/core/send-response/SKILL.md`; schema authoring — `@warlock.js/core/validate-input/SKILL.md`; URL wiring — `@warlock.js/core/register-route/SKILL.md`; competing patterns: `express` middleware functions, `@nestjs/common` `@Controller`/`@Get` decorators.
|
|
15
15
|
- [create-module](@warlock.js/core/create-module/SKILL.md): Scaffold a new feature module under `src/app/<name>/` via `warlock generate.module` and the follow-up generators for controllers, models, repositories, resources, and validation schemas. Triggers: `warlock generate.module`, `generate.controller`, `generate.service`, `generate.model`, `generate.repository`, `generate.resource`, `generate.migration`, `--minimal`, `gen.m`; "scaffold a new module", "create CRUD bootstrap", "add a controller to a module", "generate a model"; typical CLI `npx warlock generate.module <name>`. Skip: framework-wide layout rules — `@warlock.js/core/warlock-conventions/SKILL.md`; routes file shape — `@warlock.js/core/register-route/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing tooling: `@nestjs/cli`, `hygen`, hand-rolled folder layouts.
|
|
16
16
|
- [define-resource](@warlock.js/core/define-resource/SKILL.md): Map model fields to wire-shape via `defineResource()` or `Resource` subclasses. Output-only — never put business logic, hydration, or reconciliation in a resource. Triggers: `defineResource`, `Resource`, `RegisterResource`, `toJSON`, `"self"`, `"localized"`, `"uploadsUrl"`; "shape an API response", "nest related resources", "rename a field on output", "self-referential tree resource"; typical import `import { defineResource } from "@warlock.js/core"`. Skip: localized columns — `@warlock.js/core/use-localization/SKILL.md`; URL casting — `@warlock.js/core/build-url/SKILL.md`; controller side — `@warlock.js/core/create-controller/SKILL.md`; competing libs `@nestjs/swagger` `@ApiProperty`, `class-transformer`, hand-rolled DTO mappers.
|
package/package.json
CHANGED
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"@mongez/slug": "^1.0.7",
|
|
26
26
|
"@mongez/supportive-is": "^2.1.4",
|
|
27
27
|
"@mongez/time-wizard": "^1.0.6",
|
|
28
|
-
"@warlock.js/cache": "5.
|
|
29
|
-
"@warlock.js/cascade": "5.
|
|
30
|
-
"@warlock.js/context": "5.
|
|
31
|
-
"@warlock.js/logger": "5.
|
|
32
|
-
"@warlock.js/seal": "5.
|
|
33
|
-
"@warlock.js/fs": "5.
|
|
28
|
+
"@warlock.js/cache": "5.16.0",
|
|
29
|
+
"@warlock.js/cascade": "5.16.0",
|
|
30
|
+
"@warlock.js/context": "5.16.0",
|
|
31
|
+
"@warlock.js/logger": "5.16.0",
|
|
32
|
+
"@warlock.js/seal": "5.16.0",
|
|
33
|
+
"@warlock.js/fs": "5.16.0",
|
|
34
34
|
"chokidar": "^5.0.0",
|
|
35
35
|
"dayjs": "^1.11.19",
|
|
36
36
|
"es-module-lexer": "^2.0.0",
|
|
@@ -56,10 +56,10 @@
|
|
|
56
56
|
"react": "^19.2.3",
|
|
57
57
|
"react-dom": "^19.2.3",
|
|
58
58
|
"@react-email/render": "^2.0.5",
|
|
59
|
-
"@warlock.js/herald": "5.
|
|
60
|
-
"@warlock.js/ai": "5.
|
|
61
|
-
"@warlock.js/access": "5.
|
|
62
|
-
"@warlock.js/notifications": "5.
|
|
59
|
+
"@warlock.js/herald": "5.16.0",
|
|
60
|
+
"@warlock.js/ai": "5.16.0",
|
|
61
|
+
"@warlock.js/access": "5.16.0",
|
|
62
|
+
"@warlock.js/notifications": "5.16.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"sharp": {
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
],
|
|
123
123
|
"author": "hassanzohdy",
|
|
124
124
|
"license": "MIT",
|
|
125
|
-
"version": "5.
|
|
125
|
+
"version": "5.16.0",
|
|
126
126
|
"type": "module",
|
|
127
127
|
"main": "./esm/index.mjs",
|
|
128
128
|
"module": "./esm/index.mjs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: configure-app
|
|
3
|
-
description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app''s public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap
|
|
3
|
+
description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app''s public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap refusal on a missing origin — `@warlock.js/web/generate-sitemap/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Warlock — configure the app
|
|
@@ -260,11 +260,11 @@ falling back to the `PUBLIC_APP_URL` env var, or `undefined` when neither is
|
|
|
260
260
|
set.
|
|
261
261
|
|
|
262
262
|
`getPublicUrl()` never throws — it is a consumer's job to fail loudly when it
|
|
263
|
-
requires the value. `@warlock.js/
|
|
264
|
-
`sitemap.enabled: true` and no `app.publicUrl`/`PUBLIC_APP_URL` set,
|
|
265
|
-
refuses
|
|
266
|
-
back to a request-derived host — a sitemap
|
|
267
|
-
worse than
|
|
263
|
+
requires the value. `@warlock.js/web`'s sitemap is the first such consumer:
|
|
264
|
+
with `web.sitemap.enabled: true` and no `app.publicUrl`/`PUBLIC_APP_URL` set,
|
|
265
|
+
generation refuses (`MissingPublicUrlError`, naming both) rather than falling
|
|
266
|
+
back to a request-derived host — a sitemap pointing at the wrong host is
|
|
267
|
+
worse than none. See `@warlock.js/web/generate-sitemap/SKILL.md`.
|
|
268
268
|
|
|
269
269
|
## Common patterns
|
|
270
270
|
|
|
@@ -41,6 +41,20 @@ Scaffold with: `npx warlock generate.controller <module>/<action>` (add `--with-
|
|
|
41
41
|
|
|
42
42
|
Prefer `request.validated()` once a schema is attached — it's typed.
|
|
43
43
|
|
|
44
|
+
### Request body content types
|
|
45
|
+
|
|
46
|
+
`request.input()` / `.all()` / `.validated()` read the same way regardless of how the body arrived — every content type below feeds the same parsed bag:
|
|
47
|
+
|
|
48
|
+
| Content type | Parsed by | Notes |
|
|
49
|
+
| ------------------------------------ | -------------------------------- | --------------------------------------------------------------------- |
|
|
50
|
+
| `application/json` | Fastify (built-in) | objects/arrays parsed as-is |
|
|
51
|
+
| `multipart/form-data` | `@fastify/multipart` | fields + files; see [`upload-file`](../upload-file/SKILL.md) |
|
|
52
|
+
| `application/x-www-form-urlencoded` | Warlock's own content-type parser (`http/parse-urlencoded-body.ts`) | plain HTML forms, OAuth `form_post` callbacks (e.g. Apple Sign in) |
|
|
53
|
+
|
|
54
|
+
For urlencoded bodies: fields decode via `URLSearchParams`. A key sent more than once (`tag=a&tag=b`) becomes an array (`request.input("tag")` → `["a", "b"]`); every other key is a plain string. Bracket-notation keys (`a[b]=1`) are **not** expanded by the urlencoded parser itself — nesting only happens through the same shared bracket-key logic every body type already goes through, so it behaves exactly like a JSON or query-string key of that shape, no differently than today.
|
|
55
|
+
|
|
56
|
+
All three content types are held to the same `http.bodyLimit` — an over-limit urlencoded body is rejected with the same `413` a JSON body would get.
|
|
57
|
+
|
|
44
58
|
## Returning output
|
|
45
59
|
|
|
46
60
|
Pick the helper that matches the outcome. Full surface in [send-response](../send-response/SKILL.md). Quick map:
|
|
@@ -27,6 +27,8 @@ GET /ready → 200 {"status":"ok","checks":{"db":true}}
|
|
|
27
27
|
### Config
|
|
28
28
|
|
|
29
29
|
```ts title="src/config/http.ts"
|
|
30
|
+
import type { HttpConfigurations } from "@warlock.js/core";
|
|
31
|
+
|
|
30
32
|
const httpConfigurations: HttpConfigurations = {
|
|
31
33
|
health: {
|
|
32
34
|
enabled: true, // default; set false to remove both endpoints
|
|
@@ -34,6 +36,8 @@ const httpConfigurations: HttpConfigurations = {
|
|
|
34
36
|
readinessPath: "/ready", // readiness path
|
|
35
37
|
},
|
|
36
38
|
};
|
|
39
|
+
|
|
40
|
+
export default httpConfigurations;
|
|
37
41
|
```
|
|
38
42
|
|
|
39
43
|
## Readiness checks
|
|
@@ -74,12 +78,16 @@ On SIGINT/SIGTERM the framework tears down in order: **app `onShutdown` hooks
|
|
|
74
78
|
3. Draining is bounded by a timeout so one stuck request can't hang the deploy — after it, the server force-closes and a warning is logged.
|
|
75
79
|
|
|
76
80
|
```ts title="src/config/http.ts"
|
|
81
|
+
import type { HttpConfigurations } from "@warlock.js/core";
|
|
82
|
+
|
|
77
83
|
const httpConfigurations: HttpConfigurations = {
|
|
78
84
|
gracefulShutdown: {
|
|
79
85
|
timeout: 10_000, // ms to wait for in-flight drain (default 10s)
|
|
80
86
|
forceCloseConnections: "idle", // close idle keep-alives, let active finish (default)
|
|
81
87
|
},
|
|
82
88
|
};
|
|
89
|
+
|
|
90
|
+
export default httpConfigurations;
|
|
83
91
|
```
|
|
84
92
|
|
|
85
93
|
`forceCloseConnections`: `"idle"` (default) closes idle keep-alive connections and lets active requests finish; `true` force-closes everything immediately; `false` waits for every connection.
|
|
@@ -84,6 +84,8 @@ return response.sendBuffer(buffer, { contentType: "image/png" });
|
|
|
84
84
|
|
|
85
85
|
`SendFileOptions` lets you set `cacheTime`, `immutable`, `inline`, `filename` (download attachment name).
|
|
86
86
|
|
|
87
|
+
`response.xml(string | XMLable, statusCode?)` sends `application/xml` — pass a raw XML string, or anything with a `toXML(): string` method (structural, so core never depends on `@warlock.js/sitemap`). This is for BOUNDED values only: a raw string, or a single `@warlock.js/sitemap` `Sitemap` under the sitemaps.org 50,000-URL / 50MB ceiling. `SitemapIndex` has no `toXML()` — it is the streaming path for larger URL sets and does not fit in one response body. It writes shards plus a master index as FILES via `saveTo`/`publish`, and those files are served directly rather than passed through `xml()`. In Warlock web this is the `web.sitemap` config: `enabled`, `outputDir` (default `storagePath("sitemap")`), and `path` (default `/sitemap.xml`) — the app serves the generated files from `outputDir` at that route, it never builds a `SitemapIndex` response inline.
|
|
88
|
+
|
|
87
89
|
## Streams
|
|
88
90
|
|
|
89
91
|
```ts
|