brustjs 0.1.49-alpha → 0.1.51-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/package.json +39 -15
  2. package/runtime/cache-sync.ts +291 -0
  3. package/runtime/cache.ts +4 -0
  4. package/runtime/cli/build.ts +7 -0
  5. package/runtime/cli/dev.ts +7 -0
  6. package/runtime/cli/native-routes-emit.ts +147 -1
  7. package/runtime/cli/ssg.ts +94 -23
  8. package/runtime/config.ts +42 -0
  9. package/runtime/index.d.ts +63 -0
  10. package/runtime/index.js +57 -52
  11. package/runtime/index.ts +114 -9
  12. package/runtime/islands/page-cache.ts +32 -2
  13. package/runtime/native/runtime.ts +220 -7
  14. package/runtime/render/fragment.ts +87 -0
  15. package/runtime/routes.ts +482 -95
  16. package/runtime/templates.ts +47 -0
  17. package/runtime/treaty.ts +24 -1
  18. package/types/action-error.d.ts +18 -0
  19. package/types/cache-sync.d.ts +42 -0
  20. package/types/cache.d.ts +20 -0
  21. package/types/cli/help.d.ts +28 -0
  22. package/types/cli/jinja-staleness.d.ts +14 -0
  23. package/types/cli/native-routes-emit.d.ts +217 -0
  24. package/types/cli/new.d.ts +30 -0
  25. package/types/cli/templates.d.ts +39 -0
  26. package/types/client/index.d.ts +14 -0
  27. package/types/config.d.ts +42 -0
  28. package/types/cookies.d.ts +25 -0
  29. package/types/create.d.ts +1 -0
  30. package/types/css/build.d.ts +11 -0
  31. package/types/css/component-build.d.ts +17 -0
  32. package/types/css/component-loader.d.ts +8 -0
  33. package/types/css/manifest.d.ts +21 -0
  34. package/types/css/process-modules.d.ts +31 -0
  35. package/types/css/route-deps.d.ts +20 -0
  36. package/types/css/scan-imports.d.ts +13 -0
  37. package/types/css.d.ts +16 -0
  38. package/types/define-actions.d.ts +133 -0
  39. package/types/dev/client.d.ts +8 -0
  40. package/types/dev/coordinator.d.ts +33 -0
  41. package/types/dev/inject.d.ts +6 -0
  42. package/types/dev/jinja-reload.d.ts +7 -0
  43. package/types/dev/tui.d.ts +35 -0
  44. package/types/dev/watcher.d.ts +34 -0
  45. package/types/dev/worker-registry.d.ts +17 -0
  46. package/types/dev/ws-channel.d.ts +39 -0
  47. package/types/generator.d.ts +23 -0
  48. package/types/index.d.ts +222 -0
  49. package/types/islands/brust-page.d.ts +74 -0
  50. package/types/islands/build.d.ts +49 -0
  51. package/types/islands/chunk-id.d.ts +10 -0
  52. package/types/islands/importmap.d.ts +2 -0
  53. package/types/islands/island.d.ts +65 -0
  54. package/types/islands/isr-jsx.d.ts +31 -0
  55. package/types/islands/native-render.d.ts +89 -0
  56. package/types/loader-cache.d.ts +18 -0
  57. package/types/mcp/extractor.d.ts +14 -0
  58. package/types/mcp/manifest.d.ts +23 -0
  59. package/types/mcp/schema.d.ts +19 -0
  60. package/types/mcp/server.d.ts +15 -0
  61. package/types/md/emit.d.ts +72 -0
  62. package/types/md/render.d.ts +80 -0
  63. package/types/md/routes.d.ts +119 -0
  64. package/types/md/scan.d.ts +34 -0
  65. package/types/md/slug.d.ts +1 -0
  66. package/types/native/build.d.ts +30 -0
  67. package/types/native/index.d.ts +2 -0
  68. package/types/native/runtime.d.ts +52 -0
  69. package/types/navigation/active-nav.d.ts +2 -0
  70. package/types/navigation/index.d.ts +5 -0
  71. package/types/navigation/navigate.d.ts +14 -0
  72. package/types/navigation/react.d.ts +15 -0
  73. package/types/navigation/store.d.ts +44 -0
  74. package/types/render/fragment.d.ts +20 -0
  75. package/types/render/inject-action-prefix.d.ts +9 -0
  76. package/types/render/inject-css-link.d.ts +8 -0
  77. package/types/render/inject-dev-client.d.ts +6 -0
  78. package/types/render/inject-generator.d.ts +7 -0
  79. package/types/render/inject-store.d.ts +9 -0
  80. package/types/render/stream.d.ts +45 -0
  81. package/types/request-context.d.ts +16 -0
  82. package/types/routes.d.ts +506 -0
  83. package/types/sse/handler.d.ts +22 -0
  84. package/types/standard-schema.d.ts +31 -0
  85. package/types/store/define-store.d.ts +31 -0
  86. package/types/store/index.d.ts +5 -0
  87. package/types/store/react.d.ts +2 -0
  88. package/types/store/serialize.d.ts +5 -0
  89. package/types/store/server-context.d.ts +4 -0
  90. package/types/store/signal.d.ts +18 -0
  91. package/types/templates.d.ts +18 -0
  92. package/types/treaty.d.ts +70 -0
  93. package/types/ws/handler.d.ts +26 -0
@@ -147,6 +147,14 @@ export function collectStaticPaths(flatRoutes: FlatRouteLike[]): SsgRouteDecisio
147
147
  /** Reserved sentinel param value (Phase B fallback shell crawl). */
148
148
  export const SSG_FALLBACK_SENTINEL = '__brust_fallback__'
149
149
 
150
+ /** Definitely-unmatched URL path used to crawl the GLOBAL catch-all into
151
+ * 404.html: the dist server's NotFound tier renders the global catch-all at
152
+ * status 404 for any path no real route matches. Deliberately bogus — a
153
+ * double-underscore-namespaced segment no app route declares. NOT `/_brust/`-
154
+ * prefixed: those resolve to brust-internal handlers BEFORE route matching, so
155
+ * the catch-all tier would never see them. */
156
+ export const SSG_NOT_FOUND_SENTINEL_PATH = '/__brust_not_found_sentinel__'
157
+
150
158
  /** Structural view of the leaf's ssg config (mirrors RouteSsgConfig). */
151
159
  export interface RouteSsgLike {
152
160
  params?: () => Array<Record<string, string>> | Promise<Array<Record<string, string>>>
@@ -263,24 +271,24 @@ export function hasClientLoaderExport(source: string): boolean {
263
271
  return /export\s*\{[^}]*\bclientLoader\b[^}]*\}/.test(code)
264
272
  }
265
273
 
266
- /** Static-host 404 document for `fallback: 'client'` routes: inlines the
267
- * [{pattern, doc}] manifest pairs; a path matching a fallback pattern stashes
268
- * the REAL url in sessionStorage (the takeover runtime restores it via
269
- * history.replaceState) and redirects to the prerendered fallback shell.
270
- * No match → plain 404 text. Pure string fn so the script/inline-JSON
271
- * contract is unit-testable. Escapes for the <script> context: `<`/`>` (no
272
- * `</script>`/`<!--`/`-->` sequences) and U+2028/U+2029 (legal in JSON,
273
- * illegal in pre-ES2019-parsed JS string literals). Patterns are
274
- * author-controlled belt-and-braces, not a trust boundary. */
275
- export function fallback404Html(pairs: Array<{ pattern: string; doc: string }>): string {
274
+ /** The redirect-only `<script>` for `fallback: 'client'` routes (no surrounding
275
+ * document): inlines the [{pattern, doc}] manifest pairs; a path matching a
276
+ * fallback pattern stashes the REAL url in sessionStorage (the takeover runtime
277
+ * restores it via history.replaceState) and redirects to the prerendered
278
+ * fallback shell. No match → no-op (the surrounding document is the 404 body).
279
+ * Extracted so it can be wrapped in the minimal `fallback404Html` shell OR
280
+ * injected into a crawled global-404 page (compose404Html). Pure string fn so
281
+ * the script/inline-JSON contract is unit-testable. Escapes for the <script>
282
+ * context: `<`/`>` (no `</script>`/`<!--`/`-->` sequences) and U+2028/U+2029
283
+ * (legal in JSON, illegal in pre-ES2019-parsed JS string literals). Patterns
284
+ * are author-controlled — belt-and-braces, not a trust boundary. */
285
+ export function fallback404Script(pairs: Array<{ pattern: string; doc: string }>): string {
276
286
  const inlineJson = JSON.stringify(pairs)
277
287
  .replace(/</g, '\\u003c')
278
288
  .replace(/>/g, '\\u003e')
279
289
  .replace(/\u2028/g, '\\u2028')
280
290
  .replace(/\u2029/g, '\\u2029')
281
- return `<!doctype html><html><head><meta charset="utf-8"><title>404</title></head><body>
282
- <p>Not found.</p>
283
- <script>
291
+ return `<script>
284
292
  (function () {
285
293
  var MANIFEST = ${inlineJson};
286
294
  function match(pattern, path) {
@@ -300,10 +308,43 @@ export function fallback404Html(pairs: Array<{ pattern: string; doc: string }>):
300
308
  }
301
309
  }
302
310
  })()
303
- </script></body></html>
311
+ </script>`
312
+ }
313
+
314
+ /** Static-host 404 document for `fallback: 'client'` routes when NO global
315
+ * catch-all page exists: the minimal shell (`<p>Not found.</p>`) wrapping the
316
+ * redirect `<script>` from `fallback404Script`. When a global catch-all DOES
317
+ * exist its crawled page is the document and the script is injected instead
318
+ * (compose404Html) — this shell is unused in that case. */
319
+ export function fallback404Html(pairs: Array<{ pattern: string; doc: string }>): string {
320
+ return `<!doctype html><html><head><meta charset="utf-8"><title>404</title></head><body>
321
+ <p>Not found.</p>
322
+ ${fallback404Script(pairs)}</body></html>
304
323
  `
305
324
  }
306
325
 
326
+ /** Inject `<script>…</script>` into `html` just before the closing `</body>`
327
+ * (last occurrence, case-insensitive). If the document has no `</body>` the
328
+ * script is appended — a static host still parses a trailing script. Used to
329
+ * compose the fallback redirect into a crawled global-404 page. */
330
+ export function injectBeforeBodyClose(html: string, script: string): string {
331
+ const idx = html.toLowerCase().lastIndexOf('</body>')
332
+ if (idx === -1) return `${html}\n${script}`
333
+ return `${html.slice(0, idx)}${script}\n${html.slice(idx)}`
334
+ }
335
+
336
+ /** Compose the final SSG `404.html` from a crawled global-catch-all page and,
337
+ * when `fallback: 'client'` routes also exist, the fallback redirect `<script>`
338
+ * injected before `</body>`. With no fallback pairs the crawled page is
339
+ * returned verbatim (pure rendered 404 page). */
340
+ export function compose404Html(
341
+ crawled: string,
342
+ fallbackPairs: Array<{ pattern: string; doc: string }>,
343
+ ): string {
344
+ if (fallbackPairs.length === 0) return crawled
345
+ return injectBeforeBodyClose(crawled, fallback404Script(fallbackPairs))
346
+ }
347
+
307
348
  // ----- static export -----
308
349
 
309
350
  const READY_LINE = '[brust] listening on' // println! in brust-core server/mod.rs
@@ -415,13 +456,19 @@ export async function exportStatic(opts: {
415
456
  routes: SsgRouteDecision[]
416
457
  /** `fallback: 'client'` routes (pattern + built chunk URL) to emit shells for. */
417
458
  fallbacks?: Array<{ pattern: string; chunk: string }>
459
+ /** True when the app declares a GLOBAL catch-all (notFoundPrefix === ''). The
460
+ * crawler fetches an unmatched sentinel path (→ NotFound tier renders the
461
+ * catch-all at 404) and writes its HTML to staticOut/404.html, composing the
462
+ * fallback redirect script when fallbacks also exist. An app public/404.html
463
+ * still wins. */
464
+ globalNotFound?: boolean
418
465
  }): Promise<{
419
466
  written: string[]
420
467
  navWritten: string[]
421
468
  fallbackWritten: string[]
422
469
  skipped: SsgRouteDecision[]
423
470
  }> {
424
- const { distDir, entryDir, staticOut, routes, fallbacks = [] } = opts
471
+ const { distDir, entryDir, staticOut, routes, fallbacks = [], globalNotFound = false } = opts
425
472
  const included = routes.filter((r) => r.include)
426
473
  const skipped = routes.filter((r) => !r.include)
427
474
 
@@ -431,7 +478,7 @@ export async function exportStatic(opts: {
431
478
  const written: string[] = []
432
479
  const navWritten: string[] = []
433
480
  const fallbackWritten: string[] = []
434
- if (included.length > 0 || fallbacks.length > 0) {
481
+ if (included.length > 0 || fallbacks.length > 0 || globalNotFound) {
435
482
  const port = await freePort()
436
483
  const proc = Bun.spawn(['bun', join(distDir, 'index.js')], {
437
484
  env: { ...process.env, BRUST_PORT: String(port), BRUST_WORKERS: '1' },
@@ -541,6 +588,13 @@ export async function exportStatic(opts: {
541
588
  fallbackWritten.push(payloadFile)
542
589
  }
543
590
 
591
+ // Fallback {pattern, doc} pairs for the 404.html redirect script — also
592
+ // inlined in the routes.json manifest. Empty when no fallback routes.
593
+ const fallbackPairs = fallbacks.map((f) => ({
594
+ pattern: f.pattern,
595
+ doc: `/_brust/fallback/${fallbackDiskPath(f.pattern)}/`,
596
+ }))
597
+
544
598
  if (fallbacks.length > 0) {
545
599
  // Manifest the client takeover runtime fetches to map a 404'd path to
546
600
  // its fallback shell. doc/payload are directory-index URLs (trailing
@@ -557,19 +611,36 @@ export async function exportStatic(opts: {
557
611
  const manifestPath = join(staticOut, '_brust', 'routes.json')
558
612
  await mkdir(dirname(manifestPath), { recursive: true })
559
613
  await Bun.write(manifestPath, JSON.stringify(manifest))
614
+ }
560
615
 
561
- // 404.html redirects non-prerendered fallback paths to their shell.
562
- // An app-authored public/404.html wins it lands via the public/
563
- // copy below, and the author owns the redirect contract.
616
+ // 404.html, single static-host slot. Resolution:
617
+ // - app public/404.html present author owns it (lands via the public/
618
+ // copy below); never overwrite, warn only when it would have mattered.
619
+ // - GLOBAL catch-all → crawl an unmatched sentinel (NotFound tier renders
620
+ // the catch-all at 404), use its HTML as the document; inject the
621
+ // fallback redirect <script> when fallbacks ALSO exist (compose404Html).
622
+ // - fallbacks only → the minimal fallback404Html shell (unchanged).
623
+ // - neither → no framework 404.html (byte-identical-today).
624
+ if (globalNotFound || fallbacks.length > 0) {
564
625
  if (existsSync(join(entryDir, 'public', '404.html'))) {
565
626
  console.warn(
566
627
  '[brust build] ssg: public/404.html exists — NOT overwriting; your 404 page must redirect fallback routes itself (see docs)',
567
628
  )
629
+ } else if (globalNotFound) {
630
+ // Crawl the global catch-all via an unmatched path: the dist server's
631
+ // NotFound tier renders it at status 404. A non-404 here means the
632
+ // catch-all isn't wired — fail the export rather than ship a wrong
633
+ // (or 200) page as the 404.
634
+ const resp = await fetch(`http://127.0.0.1:${port}${SSG_NOT_FOUND_SENTINEL_PATH}`)
635
+ const body = await resp.text()
636
+ if (resp.status !== 404) {
637
+ throw new Error(
638
+ `global catch-all crawl GET ${SSG_NOT_FOUND_SENTINEL_PATH} → ${resp.status} (expected 404)\n${body.slice(0, 500)}`,
639
+ )
640
+ }
641
+ await Bun.write(join(staticOut, '404.html'), compose404Html(body, fallbackPairs))
568
642
  } else {
569
- await Bun.write(
570
- join(staticOut, '404.html'),
571
- fallback404Html(manifest.fallbacks.map(({ pattern, doc }) => ({ pattern, doc }))),
572
- )
643
+ await Bun.write(join(staticOut, '404.html'), fallback404Html(fallbackPairs))
573
644
  }
574
645
  }
575
646
  } catch (err) {
package/runtime/config.ts CHANGED
@@ -14,6 +14,12 @@ export interface BrustConfig {
14
14
  cacheMaxEntries?: number
15
15
  /** L2 page-cache capacity (entries). Undefined → Rust default of 1000. */
16
16
  cachePageMaxEntries?: number
17
+ /** R9 cross-process cache invalidation: redis/dragonfly URL. Absent →
18
+ * feature disabled (current single-process behavior). */
19
+ cacheSyncUrl?: string
20
+ /** Pub/sub channel for cache invalidation. Undefined → module default
21
+ * (`brust:cache:invalidate`). */
22
+ cacheSyncChannel?: string
17
23
  }
18
24
 
19
25
  /** Caller-supplied fallbacks (e.g. `brust.run({ address, port })`) applied
@@ -85,6 +91,8 @@ export async function loadConfig(
85
91
  workers,
86
92
  cacheMaxEntries: fromToml.cacheMaxEntries,
87
93
  cachePageMaxEntries: fromToml.cachePageMaxEntries,
94
+ cacheSyncUrl: fromEnv.cacheSyncUrl ?? fromToml.cacheSyncUrl,
95
+ cacheSyncChannel: fromEnv.cacheSyncChannel ?? fromToml.cacheSyncChannel,
88
96
  }
89
97
  }
90
98
 
@@ -168,6 +176,26 @@ function extractFromToml(parsed: unknown, file: string): Partial<BrustConfig> {
168
176
  }
169
177
  out.cachePageMaxEntries = pageMaxEntries
170
178
  }
179
+ const syncUrl = (cache as Record<string, unknown>).sync_url
180
+ if (syncUrl !== undefined) {
181
+ if (typeof syncUrl !== 'string' || syncUrl.trim() === '') {
182
+ throw new BrustConfigError(
183
+ `${file}: cache.sync_url must be a non-empty string (got ${JSON.stringify(syncUrl)})`,
184
+ file,
185
+ )
186
+ }
187
+ out.cacheSyncUrl = syncUrl.trim()
188
+ }
189
+ const syncChannel = (cache as Record<string, unknown>).sync_channel
190
+ if (syncChannel !== undefined) {
191
+ if (typeof syncChannel !== 'string' || syncChannel.trim() === '') {
192
+ throw new BrustConfigError(
193
+ `${file}: cache.sync_channel must be a non-empty string (got ${JSON.stringify(syncChannel)})`,
194
+ file,
195
+ )
196
+ }
197
+ out.cacheSyncChannel = syncChannel.trim()
198
+ }
171
199
  }
172
200
 
173
201
  return out
@@ -202,5 +230,19 @@ function extractFromEnv(): Partial<BrustConfig> {
202
230
  }
203
231
  out.workers = n
204
232
  }
233
+ if (process.env.BRUST_CACHE_SYNC_URL) {
234
+ const url = process.env.BRUST_CACHE_SYNC_URL.trim()
235
+ if (url === '') {
236
+ throw new BrustConfigError('BRUST_CACHE_SYNC_URL must be a non-empty string', null)
237
+ }
238
+ out.cacheSyncUrl = url
239
+ }
240
+ if (process.env.BRUST_CACHE_SYNC_CHANNEL) {
241
+ const channel = process.env.BRUST_CACHE_SYNC_CHANNEL.trim()
242
+ if (channel === '') {
243
+ throw new BrustConfigError('BRUST_CACHE_SYNC_CHANNEL must be a non-empty string', null)
244
+ }
245
+ out.cacheSyncChannel = channel
246
+ }
205
247
  return out
206
248
  }
@@ -75,6 +75,38 @@ export interface NapiCompiledJsx {
75
75
  warnings: Array<string>
76
76
  }
77
77
 
78
+ /**
79
+ * Global CORS policy (maps onto `brust_core::CorsConfig`). napi-rs camelCases
80
+ * the fields, so the TS runtime sends `origins`, `methods`, `headers`,
81
+ * `exposeHeaders`, `credentials`, `maxAgeSeconds`.
82
+ */
83
+ export interface NapiCorsOptions {
84
+ /**
85
+ * Allowed origins (exact scheme+host+port match). `['*']` — or any list
86
+ * CONTAINING `'*'` — allows every origin (echoed as the literal `*`).
87
+ */
88
+ origins: Array<string>
89
+ /**
90
+ * Preflight `Access-Control-Allow-Methods`. Omit for the default
91
+ * `GET,POST,PUT,PATCH,DELETE,OPTIONS`.
92
+ */
93
+ methods?: Array<string>
94
+ /**
95
+ * Preflight `Access-Control-Allow-Headers`. Omit to echo the request's
96
+ * `Access-Control-Request-Headers`.
97
+ */
98
+ headers?: Array<string>
99
+ /** `Access-Control-Expose-Headers` on actual responses. Omit for none. */
100
+ exposeHeaders?: Array<string>
101
+ /**
102
+ * Emit `Access-Control-Allow-Credentials: true`. INVALID with a wildcard
103
+ * origin — serve() throws at boot.
104
+ */
105
+ credentials?: boolean
106
+ /** Preflight `Access-Control-Max-Age` seconds. Omit for 600. */
107
+ maxAgeSeconds?: number
108
+ }
109
+
78
110
  /**
79
111
  * Dev-mode: broadcast one JSON control frame (building / reload / css-update /
80
112
  * error / ok) to every connected `/_brust/dev` client. Called from the main
@@ -87,6 +119,12 @@ export interface NapiCompiledJsx {
87
119
  */
88
120
  export declare function napiDevBroadcast(json: string): void
89
121
 
122
+ /** R1 — true when `name` resolves in either tier (dynamic first, then boot). */
123
+ export declare function napiHasTemplate(name: string): boolean
124
+
125
+ /** R1 — names of runtime-registered templates (dynamic tier only). */
126
+ export declare function napiListDynamicTemplates(): Array<string>
127
+
90
128
  /**
91
129
  * Sub-project J — boot-time listing of registered minijinja templates.
92
130
  * JS dispatcher uses this to validate every `native: true` route's
@@ -110,6 +148,13 @@ export declare function napiLoadJinjaTemplates(dir: string): Array<string>
110
148
  */
111
149
  export declare function napiRegisterSsePaths(paths: Array<string>): NapiResult<undefined>
112
150
 
151
+ /**
152
+ * R1 dynamic template registry — register (or replace) a runtime template.
153
+ * Errors (name validation / jinja syntax) surface as a thrown JS Error with
154
+ * the minijinja message (includes line info).
155
+ */
156
+ export declare function napiRegisterTemplate(name: string, source: string): NapiResult<undefined>
157
+
113
158
  /**
114
159
  * Boot-time registry of literal WS paths. Mirror of napi_register_sse_paths.
115
160
  * Call once before begin_serve; exact-match only (parameterized routes are a
@@ -117,6 +162,12 @@ export declare function napiRegisterSsePaths(paths: Array<string>): NapiResult<u
117
162
  */
118
163
  export declare function napiRegisterWsPaths(paths: Array<string>): NapiResult<undefined>
119
164
 
165
+ /**
166
+ * R1 — remove a runtime-registered template. Returns whether it existed.
167
+ * Boot-tier (directory-loaded) templates are not removable.
168
+ */
169
+ export declare function napiRemoveTemplate(name: string): boolean
170
+
120
171
  /**
121
172
  * Worker-driven render chunk delivery. Worker calls this once per chunk
122
173
  * it wants to emit; final call uses `len = 0` to close the channel.
@@ -177,6 +228,13 @@ export declare function napiRenderChunkFinal(workerId: number, slot: number, len
177
228
  */
178
229
  export declare function napiRenderJinja(workerId: number, slot: number, dataLen: number, templateName: string, status?: number | undefined | null): NapiResult<number>
179
230
 
231
+ /**
232
+ * R1 — render a template (either tier) to an HTML string. NOT the request
233
+ * hot path (allocates a JS string per call; the fast lane is
234
+ * napi_render_jinja via SAB) — intended for handlers/loaders/tooling.
235
+ */
236
+ export declare function napiRenderTemplate(name: string, dataJson: string): NapiResult<string>
237
+
180
238
  /**
181
239
  * Drop the connection's sender, which signals the per-conn task to exit
182
240
  * and close the TCP socket. Idempotent — a missing conn is a no-op.
@@ -300,6 +358,11 @@ export interface ServeOptions {
300
358
  * configured (cert + key present). An unrecognized value is rejected.
301
359
  */
302
360
  tlsMinVersion?: string
361
+ /**
362
+ * Optional global CORS policy. Omit to keep CORS disabled (byte-identical
363
+ * default behavior: no `Access-Control-*` headers, OPTIONS still 405).
364
+ */
365
+ cors?: NapiCorsOptions
303
366
  }
304
367
 
305
368
  /**