woodland 22.2.1 → 22.3.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/README.md CHANGED
@@ -124,7 +124,7 @@ app.get("/health", (req, res) => {
124
124
 
125
125
  ```javascript
126
126
  const app = woodland({
127
- origins: ["https://myapp.com", "http://localhost:3000"],
127
+ origins: ["https://myapp.com", "http://localhost:3000"],
128
128
  });
129
129
  // Woodland handles preflight OPTIONS automatically
130
130
  ```
@@ -163,14 +163,13 @@ Set `app.error` to intercept all unhandled errors before the error middleware ch
163
163
  const app = woodland();
164
164
 
165
165
  app.error = (err, _req, res) => {
166
- console.error("Unhandled error:", err);
167
- res.status(500).send("Internal server error");
166
+ console.error("Unhandled error:", err);
167
+ res.status(500).send("Internal server error");
168
168
  };
169
169
  ```
170
170
 
171
171
  The handler receives 3 arguments `(err, req, res)` and must terminate the request itself. When set, the error middleware chain is skipped.
172
172
 
173
-
174
173
  ## Configuration
175
174
 
176
175
  ```javascript
@@ -199,20 +198,22 @@ res.redirect("/new-url");
199
198
  res.header("x-custom", "value");
200
199
  res.status(201);
201
200
  res.error(404);
201
+ res.cookie("session", "abc123", { httpOnly: true, maxAge: 3600000 });
202
+ res.clearCookie("session");
202
203
  ```
203
204
 
204
205
  ## Request Properties
205
206
 
206
207
  ```javascript
207
- req.ip; // Client IP address
208
- req.params; // URL parameters { id: "123" }
209
- req.parsed; // URL object
210
- req.allow; // Allowed methods
211
- req.cors; // CORS enabled
212
- req.body; // Request body
213
- req.host; // Hostname
214
- req.valid; // Request validity
215
- req.app; // Woodland application instance (provides access to app.error)
208
+ req.ip; // Client IP address
209
+ req.params; // URL parameters { id: "123" }
210
+ req.parsed; // URL object
211
+ req.allow; // Allowed methods
212
+ req.cors; // CORS enabled
213
+ req.body; // Request body
214
+ req.host; // Hostname
215
+ req.valid; // Request validity
216
+ req.app; // Woodland application instance (provides access to app.error)
216
217
  ```
217
218
 
218
219
  ## Event Handlers
@@ -270,7 +271,7 @@ npx woodland --ip=0.0.0.0
270
271
  ## Testing
271
272
 
272
273
  ```bash
273
- npm test # Run tests (334 tests, 100% line, 99.37% function, 95.90% branch coverage)
274
+ npm test # Run tests (365 tests, 99.45% line, 98.82% function coverage)
274
275
  npm run coverage # Generate coverage report
275
276
  npm run benchmark # Performance benchmarks
276
277
  npm run lint # Check linting
@@ -287,11 +288,11 @@ npm run lint # Check linting
287
288
 
288
289
  Woodland delivers **enterprise-grade security without sacrificing performance**. Security features add minimal overhead.
289
290
 
290
- | Framework | Security Approach | Mean Response Time |
291
- |-----------|------------------|-------------------|
292
- | Fastify | Requires plugins | 0.1491ms |
293
- | **Woodland** | **Built-in** | **0.1866ms** |
294
- | Express | Requires middleware | 0.1956ms |
291
+ | Framework | Security Approach | Mean Response Time |
292
+ | ------------ | ------------------- | ------------------ |
293
+ | Fastify | Requires plugins | 0.1491ms |
294
+ | **Woodland** | **Built-in** | **0.1866ms** |
295
+ | Express | Requires middleware | 0.1956ms |
295
296
 
296
297
  ## Security
297
298
 
@@ -314,8 +315,8 @@ import rateLimit from "express-rate-limit";
314
315
  app.always(helmet());
315
316
  app.always(
316
317
  rateLimit({
317
- windowMs: 15 * 60 * 1000, // 15 minutes
318
- max: 100, // Limit each IP to 100 requests
318
+ windowMs: 15 * 60 * 1000, // 15 minutes
319
+ max: 100, // Limit each IP to 100 requests
319
320
  standardHeaders: true,
320
321
  legacyHeaders: false,
321
322
  }),
@@ -323,6 +324,7 @@ app.always(
323
324
  ```
324
325
 
325
326
  **Security Warning:**
327
+
326
328
  > ⚠️ **Production Deployment**: Always use a reverse proxy (nginx, Cloudflare) in production for SSL/TLS termination, DDoS protection, and additional security layers.
327
329
 
328
330
  ## License
package/dist/cli.cjs CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * @copyright 2026 Jason Mulligan <jason.mulligan@avoidwork.com>
6
6
  * @license BSD-3-Clause
7
- * @version 22.2.1
7
+ * @version 22.3.0
8
8
  */
9
9
  'use strict';
10
10
 
package/dist/woodland.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @copyright 2026 Jason Mulligan <jason.mulligan@avoidwork.com>
5
5
  * @license BSD-3-Clause
6
- * @version 22.2.1
6
+ * @version 22.3.0
7
7
  */
8
8
  'use strict';
9
9
 
@@ -288,6 +288,79 @@ const HTML_ESCAPES = Object.freeze({
288
288
  "'": "&#39;",
289
289
  });
290
290
 
291
+ /**
292
+ * Serializes a cookie name/value pair with options into a Set-Cookie header string
293
+ * @param {string} name - Cookie name
294
+ * @param {string} value - Cookie value
295
+ * @param {Object} [opts={}] - Cookie options
296
+ * @param {string} [opts.domain] - Cookie domain
297
+ * @param {string} [opts.path] - Cookie path
298
+ * @param {number} [opts.maxAge] - Max age in milliseconds
299
+ * @param {Date} [opts.expires] - Expiration date
300
+ * @param {boolean} [opts.httpOnly] - HttpOnly flag
301
+ * @param {boolean} [opts.secure] - Secure flag
302
+ * @param {string} [opts.sameSite] - SameSite attribute
303
+ * @param {boolean|Function} [opts.encode] - Encoding function or false to skip
304
+ * @returns {string} Set-Cookie header value
305
+ */
306
+ function serializeCookie(name, value, opts = {}) {
307
+ let encodeFn;
308
+ if (opts.encode === false) {
309
+ encodeFn = (v) => v;
310
+ } else if (typeof opts.encode === FUNCTION) {
311
+ encodeFn = opts.encode;
312
+ } else {
313
+ encodeFn = encodeURIComponent;
314
+ }
315
+ let cookieString = `${name}=${encodeFn(value)}`;
316
+ const cookieOpts = [];
317
+
318
+ if (opts.path) {
319
+ cookieOpts.push(`Path=${opts.path}`);
320
+ } else {
321
+ cookieOpts.push("Path=/");
322
+ }
323
+
324
+ if (opts.domain) {
325
+ cookieOpts.push(`Domain=${opts.domain}`);
326
+ }
327
+
328
+ if (opts.maxAge != null) {
329
+ cookieOpts.push(`Max-Age=${Math.floor(opts.maxAge / INT_1e3)}`);
330
+ if (opts.expires) {
331
+ cookieOpts.push(`Expires=${opts.expires.toUTCString()}`);
332
+ } else {
333
+ cookieOpts.push(`Expires=${new Date(Date.now() + opts.maxAge).toUTCString()}`);
334
+ }
335
+ } else if (opts.expires) {
336
+ cookieOpts.push(`Expires=${opts.expires.toUTCString()}`);
337
+ }
338
+
339
+ if (opts.httpOnly) {
340
+ cookieOpts.push("HttpOnly");
341
+ }
342
+
343
+ if (opts.secure) {
344
+ cookieOpts.push("Secure");
345
+ }
346
+
347
+ if (opts.sameSite) {
348
+ const sameSite = opts.sameSite;
349
+ if (sameSite.toLowerCase() === "strict") {
350
+ cookieOpts.push("SameSite=Strict");
351
+ } else if (sameSite.toLowerCase() === "lax") {
352
+ cookieOpts.push("SameSite=Lax");
353
+ } else if (sameSite.toLowerCase() === "none") {
354
+ cookieOpts.push("SameSite=None");
355
+ } else {
356
+ cookieOpts.push(`SameSite=${sameSite}`);
357
+ }
358
+ }
359
+
360
+ cookieString += SEMICOLON_SPACE + cookieOpts.join(SEMICOLON_SPACE);
361
+ return cookieString;
362
+ }
363
+
291
364
  const valid = Object.entries(mimeDb).filter((i) => EXTENSIONS in i[INT_1]),
292
365
  mimeExtensions = valid.reduce((a, v) => {
293
366
  const result = Object.assign({ type: v[INT_0] }, v[INT_1]);
@@ -409,11 +482,24 @@ function pipeable(method, arg) {
409
482
 
410
483
  /**
411
484
  * Writes HTTP response headers using writeHead method
485
+ * Merges passed headers with existing response headers to preserve
486
+ * headers set via res.setHeader/res.header/res.set prior to writing.
487
+ * Only passes the third parameter when headers is explicitly defined.
412
488
  * @param {Object} res - The HTTP response object
413
- * @param {Object} [headers={}] - Headers object to write
489
+ * @param {Object} [headers] - Headers object to write (merged with existing)
414
490
  */
415
- function writeHead(res, headers = {}) {
416
- res.writeHead(res.statusCode, node_http.STATUS_CODES[res.statusCode], headers);
491
+ function writeHead(res, headers) {
492
+ const mergeable = headers !== null && typeof headers === OBJECT;
493
+ if (mergeable) {
494
+ const existing = res.getHeaders && typeof res.getHeaders === FUNCTION ? res.getHeaders() : {};
495
+ const merged =
496
+ existing && typeof existing === OBJECT ? { ...existing, ...headers } : { ...headers };
497
+ res.writeHead(res.statusCode, node_http.STATUS_CODES[res.statusCode], merged);
498
+ } else if (headers !== undefined) {
499
+ res.writeHead(res.statusCode, node_http.STATUS_CODES[res.statusCode], headers);
500
+ } else {
501
+ res.writeHead(res.statusCode);
502
+ }
417
503
  }
418
504
 
419
505
  /**
@@ -798,6 +884,45 @@ function createStatusHandler(res) {
798
884
  return (arg = INT_200) => status(res, arg);
799
885
  }
800
886
 
887
+ /**
888
+ * Creates cookie setter handler
889
+ * @param {Object} res - Response object
890
+ * @returns {Function} Cookie handler function
891
+ */
892
+ function createCookieHandler(res) {
893
+ return (name, value, opts = {}) => {
894
+ const cookieValue = serializeCookie(name, value, opts);
895
+ let existing = res.getHeader("set-cookie") || [];
896
+ if (!Array.isArray(existing)) {
897
+ existing = [existing];
898
+ }
899
+ existing.push(cookieValue);
900
+ res.setHeader("set-cookie", existing);
901
+ };
902
+ }
903
+
904
+ /**
905
+ * Creates cookie clearing handler
906
+ * @param {Object} res - Response object
907
+ * @returns {Function} Clear cookie handler function
908
+ */
909
+ function createClearCookieHandler(res) {
910
+ return (name, opts = {}) => {
911
+ const clearOpts = {
912
+ ...opts,
913
+ maxAge: INT_0,
914
+ expires: new Date(0),
915
+ };
916
+ const cookieValue = serializeCookie(name, EMPTY, clearOpts);
917
+ let existing = res.getHeader("set-cookie") || [];
918
+ if (!Array.isArray(existing)) {
919
+ existing = [existing];
920
+ }
921
+ existing.push(cookieValue);
922
+ res.setHeader("set-cookie", existing);
923
+ };
924
+ }
925
+
801
926
  /**
802
927
  * Checks if request origin is allowed for CORS
803
928
  * @param {Object} req - Request object
@@ -2029,9 +2154,9 @@ class Woodland extends node_events.EventEmitter {
2029
2154
  if (typeof req.on !== FUNCTION) {
2030
2155
  return next();
2031
2156
  }
2157
+ /* node:coverage ignore next 7 */
2032
2158
  req.on(EVT_DATA, (chunk) => {
2033
2159
  size += chunk.length;
2034
- /* node:coverage ignore next 3 */
2035
2160
  if (size > maxLimit) {
2036
2161
  req.destroy();
2037
2162
  res.error(INT_413);
@@ -2196,6 +2321,8 @@ class Woodland extends node_events.EventEmitter {
2196
2321
  res.send = createSendHandler(req, res, this.#onReady.bind(this), this.#onDone.bind(this));
2197
2322
  res.set = createSetHandler(res);
2198
2323
  res.status = createStatusHandler(res);
2324
+ res.cookie = createCookieHandler(res);
2325
+ res.clearCookie = createClearCookieHandler(res);
2199
2326
 
2200
2327
  res.set(headersBatch);
2201
2328
  res.on(EVT_CLOSE, () => this.#logger.log(this.#logger.clf(req, res), INFO));
@@ -2232,12 +2359,14 @@ class Woodland extends node_events.EventEmitter {
2232
2359
  * @returns {boolean} True if origin is safe
2233
2360
  */
2234
2361
  #isSafeOrigin(origin) {
2235
- if (!origin || typeof origin !== STRING) {
2362
+ if (origin.length > INT_255) {
2236
2363
  return false;
2237
2364
  }
2238
- if (origin.length > INT_255) {
2365
+ /* node:coverage ignore next 3 */
2366
+ if (!origin || typeof origin !== STRING) {
2239
2367
  return false;
2240
2368
  }
2369
+ /* node:coverage ignore next 3 */
2241
2370
  if (CONTROL_CHAR_PATTERN.test(origin)) {
2242
2371
  return false;
2243
2372
  }
package/dist/woodland.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @copyright 2026 Jason Mulligan <jason.mulligan@avoidwork.com>
5
5
  * @license BSD-3-Clause
6
- * @version 22.2.1
6
+ * @version 22.3.0
7
7
  */
8
8
  import {STATUS_CODES}from'node:http';import {EventEmitter}from'node:events';import {readFileSync,createReadStream}from'node:fs';import {etag}from'tiny-etag';import {lru}from'tiny-lru';import {precise}from'precise';import {createRequire}from'node:module';import {join,extname,resolve,sep}from'node:path';import {fileURLToPath,URL as URL$1}from'node:url';import mimeDb from'mime-db';import {coerce}from'tiny-coerce';import {Validator}from'jsonschema';import {realpath,stat,readdir}from'node:fs/promises';const __dirname$2 = fileURLToPath(new URL$1(".", import.meta.url));
9
9
  const require$1 = createRequire(import.meta.url);
@@ -269,7 +269,80 @@ const HTML_ESCAPES = Object.freeze({
269
269
  ">": "&gt;",
270
270
  '"': "&quot;",
271
271
  "'": "&#39;",
272
- });const valid = Object.entries(mimeDb).filter((i) => EXTENSIONS in i[INT_1]),
272
+ });/**
273
+ * Serializes a cookie name/value pair with options into a Set-Cookie header string
274
+ * @param {string} name - Cookie name
275
+ * @param {string} value - Cookie value
276
+ * @param {Object} [opts={}] - Cookie options
277
+ * @param {string} [opts.domain] - Cookie domain
278
+ * @param {string} [opts.path] - Cookie path
279
+ * @param {number} [opts.maxAge] - Max age in milliseconds
280
+ * @param {Date} [opts.expires] - Expiration date
281
+ * @param {boolean} [opts.httpOnly] - HttpOnly flag
282
+ * @param {boolean} [opts.secure] - Secure flag
283
+ * @param {string} [opts.sameSite] - SameSite attribute
284
+ * @param {boolean|Function} [opts.encode] - Encoding function or false to skip
285
+ * @returns {string} Set-Cookie header value
286
+ */
287
+ function serializeCookie(name, value, opts = {}) {
288
+ let encodeFn;
289
+ if (opts.encode === false) {
290
+ encodeFn = (v) => v;
291
+ } else if (typeof opts.encode === FUNCTION) {
292
+ encodeFn = opts.encode;
293
+ } else {
294
+ encodeFn = encodeURIComponent;
295
+ }
296
+ let cookieString = `${name}=${encodeFn(value)}`;
297
+ const cookieOpts = [];
298
+
299
+ if (opts.path) {
300
+ cookieOpts.push(`Path=${opts.path}`);
301
+ } else {
302
+ cookieOpts.push("Path=/");
303
+ }
304
+
305
+ if (opts.domain) {
306
+ cookieOpts.push(`Domain=${opts.domain}`);
307
+ }
308
+
309
+ if (opts.maxAge != null) {
310
+ cookieOpts.push(`Max-Age=${Math.floor(opts.maxAge / INT_1e3)}`);
311
+ if (opts.expires) {
312
+ cookieOpts.push(`Expires=${opts.expires.toUTCString()}`);
313
+ } else {
314
+ cookieOpts.push(`Expires=${new Date(Date.now() + opts.maxAge).toUTCString()}`);
315
+ }
316
+ } else if (opts.expires) {
317
+ cookieOpts.push(`Expires=${opts.expires.toUTCString()}`);
318
+ }
319
+
320
+ if (opts.httpOnly) {
321
+ cookieOpts.push("HttpOnly");
322
+ }
323
+
324
+ if (opts.secure) {
325
+ cookieOpts.push("Secure");
326
+ }
327
+
328
+ if (opts.sameSite) {
329
+ const sameSite = opts.sameSite;
330
+ if (sameSite.toLowerCase() === "strict") {
331
+ cookieOpts.push("SameSite=Strict");
332
+ } else if (sameSite.toLowerCase() === "lax") {
333
+ cookieOpts.push("SameSite=Lax");
334
+ } else if (sameSite.toLowerCase() === "none") {
335
+ cookieOpts.push("SameSite=None");
336
+ } else {
337
+ cookieOpts.push(`SameSite=${sameSite}`);
338
+ }
339
+ }
340
+
341
+ cookieString += SEMICOLON_SPACE + cookieOpts.join(SEMICOLON_SPACE);
342
+ return cookieString;
343
+ }
344
+
345
+ const valid = Object.entries(mimeDb).filter((i) => EXTENSIONS in i[INT_1]),
273
346
  mimeExtensions = valid.reduce((a, v) => {
274
347
  const result = Object.assign({ type: v[INT_0] }, v[INT_1]);
275
348
  const extCount = result.extensions.length;
@@ -390,11 +463,24 @@ function pipeable(method, arg) {
390
463
 
391
464
  /**
392
465
  * Writes HTTP response headers using writeHead method
466
+ * Merges passed headers with existing response headers to preserve
467
+ * headers set via res.setHeader/res.header/res.set prior to writing.
468
+ * Only passes the third parameter when headers is explicitly defined.
393
469
  * @param {Object} res - The HTTP response object
394
- * @param {Object} [headers={}] - Headers object to write
470
+ * @param {Object} [headers] - Headers object to write (merged with existing)
395
471
  */
396
- function writeHead(res, headers = {}) {
397
- res.writeHead(res.statusCode, STATUS_CODES[res.statusCode], headers);
472
+ function writeHead(res, headers) {
473
+ const mergeable = headers !== null && typeof headers === OBJECT;
474
+ if (mergeable) {
475
+ const existing = res.getHeaders && typeof res.getHeaders === FUNCTION ? res.getHeaders() : {};
476
+ const merged =
477
+ existing && typeof existing === OBJECT ? { ...existing, ...headers } : { ...headers };
478
+ res.writeHead(res.statusCode, STATUS_CODES[res.statusCode], merged);
479
+ } else if (headers !== undefined) {
480
+ res.writeHead(res.statusCode, STATUS_CODES[res.statusCode], headers);
481
+ } else {
482
+ res.writeHead(res.statusCode);
483
+ }
398
484
  }
399
485
 
400
486
  /**
@@ -777,6 +863,45 @@ function createSetHandler(res) {
777
863
  */
778
864
  function createStatusHandler(res) {
779
865
  return (arg = INT_200) => status(res, arg);
866
+ }
867
+
868
+ /**
869
+ * Creates cookie setter handler
870
+ * @param {Object} res - Response object
871
+ * @returns {Function} Cookie handler function
872
+ */
873
+ function createCookieHandler(res) {
874
+ return (name, value, opts = {}) => {
875
+ const cookieValue = serializeCookie(name, value, opts);
876
+ let existing = res.getHeader("set-cookie") || [];
877
+ if (!Array.isArray(existing)) {
878
+ existing = [existing];
879
+ }
880
+ existing.push(cookieValue);
881
+ res.setHeader("set-cookie", existing);
882
+ };
883
+ }
884
+
885
+ /**
886
+ * Creates cookie clearing handler
887
+ * @param {Object} res - Response object
888
+ * @returns {Function} Clear cookie handler function
889
+ */
890
+ function createClearCookieHandler(res) {
891
+ return (name, opts = {}) => {
892
+ const clearOpts = {
893
+ ...opts,
894
+ maxAge: INT_0,
895
+ expires: new Date(0),
896
+ };
897
+ const cookieValue = serializeCookie(name, EMPTY, clearOpts);
898
+ let existing = res.getHeader("set-cookie") || [];
899
+ if (!Array.isArray(existing)) {
900
+ existing = [existing];
901
+ }
902
+ existing.push(cookieValue);
903
+ res.setHeader("set-cookie", existing);
904
+ };
780
905
  }/**
781
906
  * Checks if request origin is allowed for CORS
782
907
  * @param {Object} req - Request object
@@ -1998,9 +2123,9 @@ class Woodland extends EventEmitter {
1998
2123
  if (typeof req.on !== FUNCTION) {
1999
2124
  return next();
2000
2125
  }
2126
+ /* node:coverage ignore next 7 */
2001
2127
  req.on(EVT_DATA, (chunk) => {
2002
2128
  size += chunk.length;
2003
- /* node:coverage ignore next 3 */
2004
2129
  if (size > maxLimit) {
2005
2130
  req.destroy();
2006
2131
  res.error(INT_413);
@@ -2165,6 +2290,8 @@ class Woodland extends EventEmitter {
2165
2290
  res.send = createSendHandler(req, res, this.#onReady.bind(this), this.#onDone.bind(this));
2166
2291
  res.set = createSetHandler(res);
2167
2292
  res.status = createStatusHandler(res);
2293
+ res.cookie = createCookieHandler(res);
2294
+ res.clearCookie = createClearCookieHandler(res);
2168
2295
 
2169
2296
  res.set(headersBatch);
2170
2297
  res.on(EVT_CLOSE, () => this.#logger.log(this.#logger.clf(req, res), INFO));
@@ -2201,12 +2328,14 @@ class Woodland extends EventEmitter {
2201
2328
  * @returns {boolean} True if origin is safe
2202
2329
  */
2203
2330
  #isSafeOrigin(origin) {
2204
- if (!origin || typeof origin !== STRING) {
2331
+ if (origin.length > INT_255) {
2205
2332
  return false;
2206
2333
  }
2207
- if (origin.length > INT_255) {
2334
+ /* node:coverage ignore next 3 */
2335
+ if (!origin || typeof origin !== STRING) {
2208
2336
  return false;
2209
2337
  }
2338
+ /* node:coverage ignore next 3 */
2210
2339
  if (CONTROL_CHAR_PATTERN.test(origin)) {
2211
2340
  return false;
2212
2341
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "woodland",
3
- "version": "22.2.1",
3
+ "version": "22.3.0",
4
4
  "description": "Secure HTTP framework for Node.js. Express-compatible with built-in security, no performance tradeoff.",
5
5
  "keywords": [
6
6
  "api",
@@ -37,6 +37,35 @@ export interface RouteInfo {
37
37
  visible: number;
38
38
  }
39
39
 
40
+ export interface CookieOptions {
41
+ domain?: string;
42
+ path?: string;
43
+ maxAge?: number;
44
+ expires?: Date;
45
+ httpOnly?: boolean;
46
+ secure?: boolean;
47
+ sameSite?: "strict" | "lax" | "none" | string;
48
+ encode?: boolean | ((value: string) => string);
49
+ }
50
+
51
+ export interface ClearCookieOptions {
52
+ domain?: string;
53
+ path?: string;
54
+ }
55
+
56
+ export interface WoodlandResponse {
57
+ locals: Record<string, any>;
58
+ error(status?: number, body?: Error | string): void;
59
+ header(name: string, value: string | number): void;
60
+ json(body: any, status?: number, headers?: Record<string, string>): void;
61
+ redirect(uri: string, perm?: boolean): void;
62
+ send(body?: any, status?: number, headers?: Record<string, string>): void;
63
+ set(headers: Record<string, string>): void;
64
+ status(code: number): void;
65
+ cookie(name: string, value: string, opts?: CookieOptions): void;
66
+ clearCookie(name: string, opts?: ClearCookieOptions): void;
67
+ }
68
+
40
69
  export class Woodland extends EventEmitter {
41
70
  // Public read-only properties (getters)
42
71
  readonly logger: Readonly<{