@bakery-framework/core 2.0.0-alpha.9 → 2.0.0-rc.1

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 (78) hide show
  1. package/README.md +8 -8
  2. package/package.json +2 -5
  3. package/src/cache/lru.ts +2 -2
  4. package/src/cache/shared-db.ts +81 -11
  5. package/src/cache/string.ts +4 -0
  6. package/src/cache/tiered.ts +11 -5
  7. package/src/client/globals.d.ts +7 -7
  8. package/src/client/livereload.ts +2 -2
  9. package/src/client/utils.ts +11 -6
  10. package/src/compiler/compiler.ts +48 -44
  11. package/src/compiler/dev-service.ts +29 -30
  12. package/src/compiler/prompt-tracker.ts +2 -2
  13. package/src/compiler/tsconfig-sync.ts +130 -59
  14. package/src/core/bakery.ts +6 -3
  15. package/src/core/cache-version.ts +68 -18
  16. package/src/core/config.ts +16 -16
  17. package/src/core/context.ts +8 -8
  18. package/src/core/define-route.ts +5 -6
  19. package/src/core/index.ts +19 -7
  20. package/src/core/init.ts +38 -35
  21. package/src/core/jsx.ts +19 -4
  22. package/src/core/paths.ts +3 -1
  23. package/src/core/plugins.ts +15 -4
  24. package/src/core/port.ts +6 -6
  25. package/src/global.d.ts +36 -18
  26. package/src/handlers/assets/google-font.ts +6 -6
  27. package/src/handlers/assets/image.ts +36 -3
  28. package/src/handlers/assets/nm.ts +7 -7
  29. package/src/handlers/assets/static.ts +55 -12
  30. package/src/handlers/assets/ts.ts +3 -3
  31. package/src/handlers/assets/tsx.ts +1 -1
  32. package/src/handlers/assets/virtual-asset.ts +1 -1
  33. package/src/handlers/core/$base.ts +24 -8
  34. package/src/handlers/core/$dynamic.ts +66 -23
  35. package/src/handlers/core/$error.ts +29 -17
  36. package/src/handlers/core/$middleware.ts +6 -6
  37. package/src/handlers/core/$mounts.ts +2 -2
  38. package/src/handlers/core/$registry.ts +16 -6
  39. package/src/handlers/core/$routing.ts +18 -19
  40. package/src/handlers/core/$static.ts +11 -12
  41. package/src/handlers/routes/api.ts +5 -5
  42. package/src/handlers/routes/html.ts +2 -2
  43. package/src/handlers/routes/proxy.ts +1 -1
  44. package/src/logger/clients.ts +2 -2
  45. package/src/logger/logger.ts +4 -4
  46. package/src/logger/serve-log.ts +32 -18
  47. package/src/plugins/index.ts +1 -1
  48. package/src/plugins/routes.ts +7 -7
  49. package/src/plugins/types.ts +13 -11
  50. package/src/router.ts +12 -12
  51. package/src/session.ts +41 -23
  52. package/src/shared.d.ts +5 -5
  53. package/src/startup.ts +3 -3
  54. package/src/types.d.ts +10 -10
  55. package/src/utils/common/case.ts +1 -1
  56. package/src/utils/common/match.ts +1 -1
  57. package/src/utils/common/misc.ts +1 -1
  58. package/src/utils/constants.ts +15 -15
  59. package/src/utils/fs.ts +97 -39
  60. package/src/utils/http/authorize.ts +24 -17
  61. package/src/utils/http/cors.ts +6 -6
  62. package/src/utils/http/credential.ts +4 -4
  63. package/src/utils/http/csrf.ts +6 -7
  64. package/src/utils/http/dom.ts +9 -9
  65. package/src/utils/http/etag.ts +54 -13
  66. package/src/utils/http/html.ts +29 -20
  67. package/src/utils/http/ip.ts +1 -1
  68. package/src/utils/http/sse.ts +11 -11
  69. package/src/utils/http/url.ts +4 -4
  70. package/src/utils/http/validate.ts +3 -4
  71. package/src/utils/isomorphic/escape.ts +1 -1
  72. package/src/utils/isomorphic/is.ts +2 -2
  73. package/src/utils/isomorphic/misc.ts +1 -1
  74. package/src/utils/isomorphic/stringify.ts +2 -3
  75. package/src/utils/isomorphic/try.ts +1 -1
  76. package/src/utils/shared-pool.ts +23 -16
  77. package/tsconfig.app.json +3 -3
  78. package/tsconfig.vue.json +3 -3
package/src/utils/fs.ts CHANGED
@@ -15,7 +15,7 @@ import { gzip as zlibGzip } from 'node:zlib'
15
15
  import { LRUCache } from '../cache/lru'
16
16
  // A cycle on paper (`core/context` imports this file for `getAppVersion`),
17
17
  // harmless in practice: neither module touches the other's bindings during
18
- // evaluation — `hostStore` is only read inside `isForbidden`, at request time.
18
+ // evaluation. `hostStore` is only read inside `isForbidden`, at request time.
19
19
  import { hostStore } from '../core/context'
20
20
  import type { MixedPromise } from '../types'
21
21
  import { is, Try } from './common'
@@ -42,7 +42,7 @@ const gzipAsync = promisify(zlibGzip) as (
42
42
  * 1MB | 5804us | 6480us | 14469us | 17114us
43
43
  * (32KB gzip: median of 5 x 200-iteration runs; single runs were noisy)
44
44
  *
45
- * At 32KB the sync cost crosses the dispatch overhead for zstd — the
45
+ * At 32KB the sync cost crosses the dispatch overhead for zstd, the
46
46
  * first-preference codec. gzip's raw-latency break-even sits nearer 64KB,
47
47
  * but the sync figure is a *stall* shared by every concurrent request while
48
48
  * the async figure is latency private to one caller, so the tie goes to
@@ -54,8 +54,8 @@ export const ASYNC_COMPRESSION_MIN = 32 * 1024
54
54
  * `compress` is the sync variant (blocks the event loop for the durations
55
55
  * above); `compressAsync` runs on a worker pool and only parks the caller.
56
56
  * zstd: `Bun.zstdCompress` (native async, Bun >= 1.1.21). gzip: Bun 1.3.14
57
- * has no async `Bun.gzip`, so `node:zlib`'s callback `gzip` — which runs on
58
- * the libuv pool — is promisified instead. Both async variants were verified
57
+ * has no async `Bun.gzip`, so `node:zlib`'s callback `gzip` (which runs on
58
+ * the libuv pool) is promisified instead. Both async variants were verified
59
59
  * byte-identical to their sync counterparts on this Bun, so either path
60
60
  * yields the same cached artifacts and the same response bodies.
61
61
  */
@@ -82,13 +82,13 @@ function toArray<T>(val?: MixedArray<T>): T[] {
82
82
  }
83
83
 
84
84
  /**
85
- * Memoised: `resolve` measures ~16-20us on Bun/Windows (see `isForbidden`) and
85
+ * Memoized: `resolve` measures ~16-20us on Bun/Windows (see `isForbidden`) and
86
86
  * a CPU profile of the request path put `safeResolve` + `path.resolve` at over
87
87
  * 40% of per-request self time. Caching is safe because the function is purely
88
- * lexical — `path.resolve` reads no filesystem state, only `process.cwd()`.
88
+ * lexical: `path.resolve` reads no filesystem state, only `process.cwd()`.
89
89
  * Relative arguments make entries cwd-dependent, and while nothing in the
90
90
  * runtime calls `chdir`, tests do (config.broken.test.ts boots from temp
91
- * fixture dirs) — so the cache is invalidated wholesale when cwd moves. The
91
+ * fixture dirs), so the cache is invalidated wholesale when cwd moves. The
92
92
  * check is one `process.cwd()` read per call, measured at 12ns in Bun (it is
93
93
  * cached natively), against the ~16us a resolve costs. The cache is an LRU,
94
94
  * not a Map, because arguments derive from request paths (convention 6);
@@ -238,7 +238,7 @@ export namespace Glob {
238
238
  }
239
239
 
240
240
  export namespace FileSystem {
241
- // Naming, not constraint — `string & {}` is `string`. These document what a
241
+ // Naming, not constraint: `string & {}` is `string`. These document what a
242
242
  // parameter means at its declaration, which earns their keep. Four more
243
243
  // (`RequestPath`, `DirectoryPath`, `FileName`, `FileExtension`) documented
244
244
  // nothing, because nothing ever referenced them.
@@ -267,7 +267,7 @@ export namespace FileSystem {
267
267
 
268
268
  /**
269
269
  * Reads as UTF-8 text. Without the encoding this returns a Buffer, which
270
- * every caller then hands to `JSON.parse` — legal at runtime because it
270
+ * every caller then hands to `JSON.parse`: legal at runtime because it
271
271
  * coerces, but a type error at each call site, papered over in one of them
272
272
  * with `as any`.
273
273
  */
@@ -281,9 +281,29 @@ export namespace FileSystem {
281
281
  return (await Try(() => Bun.file(path).stat()))?.isDirectory() || false
282
282
  }
283
283
 
284
+ /**
285
+ * Handed a `BunFile`, this used to take the path back off it and build a
286
+ * **second** one to stat. A `BunFile` caches its own stat, so the instance
287
+ * the caller already has is free to read and the fresh one is a syscall:
288
+ * one question about one file cost two stats and an allocation.
289
+ *
290
+ * It shows up wherever a caller asks both questions, which is the common
291
+ * shape - `validCache` below, `nm.ts`'s `exists(nmFile) ? nmFile.lastModified`,
292
+ * and both mtime comparisons in `image.ts`. Measured on this machine, four
293
+ * interleaved rounds against a CPU-bound control flat at 27-31 ms:
294
+ *
295
+ * validCache, file present 33.0 us -> 17.3 us 1.9x
296
+ * validCache, file absent 34.9 us -> 17.4 us 2.0x
297
+ *
298
+ * The predicate is unchanged, and the two clauses in it are not
299
+ * interchangeable. `lastModified` on a **missing** file answers roughly
300
+ * `Date.now()` rather than 0, which is what the `< Date.now()` guard is
301
+ * for; dropping it reports every absent file as present. The `size > 0`
302
+ * fallback then covers a file whose mtime is in the future, which clock
303
+ * skew on a network share produces.
304
+ */
284
305
  export function exists(path: string | Bun.BunFile): boolean {
285
- path = typeof path === 'string' ? path : path.name || ''
286
- const file = Bun.file(path)
306
+ const file = typeof path === 'string' ? Bun.file(path) : path
287
307
  const lastMod = file.lastModified
288
308
  return Boolean((lastMod && lastMod < Date.now()) || file.size > 0)
289
309
  }
@@ -293,7 +313,7 @@ export namespace FileSystem {
293
313
  }
294
314
 
295
315
  /**
296
- * True only for an existing regular *file* — a directory answers false.
316
+ * True only for an existing regular *file*: a directory answers false.
297
317
  * Sync because `findDynamicRoute` (a sync hot-path scan) is a caller; one
298
318
  * stat, no BunFile allocation.
299
319
  */
@@ -302,12 +322,12 @@ export namespace FileSystem {
302
322
  }
303
323
 
304
324
  /**
305
- * Parent directory of an **already-normalised** absolute path — the output
325
+ * Parent directory of an **already-normalized** absolute path: the output
306
326
  * shape `safeResolve` produces: forward slashes, no `.`/`..` segments, no
307
327
  * trailing slash except at a root.
308
328
  *
309
329
  * Only correct for that input shape, which is why it is not exported.
310
- * `isForbidden` is the sole caller and it normalises before the first call.
330
+ * `isForbidden` is the sole caller and it normalizes before the first call.
311
331
  */
312
332
  function parentOf(path: string): string {
313
333
  const idx = path.lastIndexOf('/')
@@ -319,7 +339,7 @@ export namespace FileSystem {
319
339
  // Both of these branches are defensive rather than covered. `isForbidden`
320
340
  // stops as soon as the walk leaves `root`, and every root it is called
321
341
  // with is a real serve directory well below a filesystem root, so the walk
322
- // never reaches one — dropping them both leaves the equivalence test in
342
+ // never reaches one: dropping them both leaves the equivalence test in
323
343
  // `fs.test.ts` green. They are kept so `parentOf` is correct as written
324
344
  // rather than correct only for its current caller.
325
345
  if (path.charCodeAt(idx - 1) === 58 /* ':' */) return path.slice(0, idx + 1)
@@ -330,7 +350,7 @@ export namespace FileSystem {
330
350
  * True when `pathToCheck`, or any directory between it and `root`, holds a
331
351
  * `.forbidden` marker.
332
352
  *
333
- * This is a security guard, and it runs several times per request — from
353
+ * This is a security guard, and it runs several times per request, from
334
354
  * `router.ts` before anything else, then again from `getStatic`, `getRoute`
335
355
  * (once per level of a nested route) and the dynamic-route cache. Each call
336
356
  * walks every directory between the target and the serve root, so the
@@ -338,26 +358,26 @@ export namespace FileSystem {
338
358
  *
339
359
  * The walk used to call `safeResolve` twice per level, once to build
340
360
  * `<dir>/.forbidden` and once to step to the parent. Both are `node:path`
341
- * `resolve`, which on Bun/Windows measures **~16-20us** — roughly 100x
361
+ * `resolve`, which on Bun/Windows measures **~16-20us**: roughly 100x
342
362
  * `path.dirname` (~0.2us), independent of the filesystem and of where the
343
363
  * cwd lives, and not explained by native-call overhead (`Bun.nanoseconds()`
344
364
  * is 0.045us). A depth-4 walk therefore paid ten `resolve` calls.
345
365
  *
346
- * Since `curr` is normalised once on entry, every value it takes afterwards
366
+ * Since `curr` is normalized once on entry, every value it takes afterwards
347
367
  * is already in `resolve`'s own output shape, so both operations are exact
348
368
  * string edits. That takes a depth-4 walk from ten `resolve` calls to two
349
369
  * and leaves one `existsSync` per level as the only remaining syscall.
350
370
  * `fs.test.ts` pins the two implementations as equivalent.
351
371
  *
352
- * The entry normalisation stays: `pathToCheck` reaches here straight from a
372
+ * The entry normalization stays: `pathToCheck` reaches here straight from a
353
373
  * URL pathname in `router.ts`, before any containment check, and collapsing
354
374
  * its dot segments is what makes the rest of the walk safe.
355
375
  *
356
- * The remaining `existsSync` per level is deliberately **not** memoised
376
+ * The remaining `existsSync` per level is deliberately **not** memoized
357
377
  * *across requests*, which is the opposite call to the one `getMimeType`
358
- * below makes. The difference is not the key — both are ultimately derived
378
+ * below makes. The difference is not the key: both are ultimately derived
359
379
  * from a request path and both would need an `LRUCache` to stay bounded
360
- * under convention 6 — it is what a stale entry costs. A stale MIME type
380
+ * under convention 6: it is what a stale entry costs. A stale MIME type
361
381
  * serves the wrong `Content-Type`; a stale *negative* here serves a file an
362
382
  * operator has marked forbidden, which is the exact outcome the marker
363
383
  * exists to prevent.
@@ -367,7 +387,7 @@ export namespace FileSystem {
367
387
  * a cached "not forbidden" would persist for the life of the process; a TTL
368
388
  * would only bound that window, not close it, and an operator dropping a
369
389
  * `.forbidden` file to pull content offline has no way to learn how long the
370
- * window is. The LRU also fails exactly when it would matter — a client
390
+ * window is. The LRU also fails exactly when it would matter: a client
371
391
  * requesting varied paths evicts the useful upper-level entries, so the
372
392
  * hit rate collapses under the load that motivated caching in the first
373
393
  * place. `fs.test.ts` has a regression test that fails if this is added.
@@ -376,8 +396,8 @@ export namespace FileSystem {
376
396
  * 4-6x per request over overlapping subtrees (router, `getStatic`, every
377
397
  * level of `getRoute`, the dynamic-route cache), each walk re-stating the
378
398
  * same directories at ~50us per miss on Bun/Windows. A marker dropped
379
- * *mid-request* has no meaningful semantics — the operator's guarantee is
380
- * "the next request sees it" — so the raw probe results live on the current
399
+ * *mid-request* has no meaningful semantics (the operator's guarantee is
400
+ * "the next request sees it"), so the raw probe results live on the current
381
401
  * `hostStore` value (`HostContext.forbiddenProbes`), which worker.ts creates
382
402
  * per request and never reuses. No store (tests, direct calls, startup)
383
403
  * means no caching, bit-for-bit today's behavior.
@@ -399,7 +419,7 @@ export namespace FileSystem {
399
419
  //
400
420
  // The walk below is bounded by `curr.startsWith(normalizedRoot)`, so an
401
421
  // out-of-root path skipped the loop body entirely and fell through to
402
- // `return false` — "not forbidden". That is a guard failing *open* on the
422
+ // `return false`, "not forbidden". That is a guard failing *open* on the
403
423
  // one input it most needs to refuse, and it made every caller that relied
404
424
  // on this for containment (all of them: `constants.ts` said so in as many
405
425
  // words) incapable of catching an escape.
@@ -409,12 +429,12 @@ export namespace FileSystem {
409
429
  // read the pattern as drive-absolute, ignored `cwd`, and matched files at
410
430
  // `C:\`. `getRoute` resolved one, asked this function, was told "allowed",
411
431
  // and returned an `Info` pointing six levels above the serve root. The glob
412
- // is fixed too, but the glob was only how it was reached — a resolved file
432
+ // is fixed too, but the glob was only how it was reached: a resolved file
413
433
  // outside the root has to be refused here whatever produced it.
414
434
  //
415
435
  // The separator suffix matters: a bare `startsWith` would also accept a
416
436
  // sibling directory whose name merely begins with the root's. A root that
417
- // is already a filesystem root ends in `/` and must not gain a second one —
437
+ // is already a filesystem root ends in `/` and must not gain a second one:
418
438
  // `C://` matches nothing, which would make every path under `C:/` read as
419
439
  // an escape. `fs.test.ts` drives both through its root corpus.
420
440
  const rootPrefix = normalizedRoot.endsWith('/')
@@ -452,19 +472,53 @@ export namespace FileSystem {
452
472
  return false
453
473
  }
454
474
 
455
- let probeForbidden: (path: string) => boolean = nodeExistsSync
475
+ /**
476
+ * `statSync` with ENOENT suppressed, not `existsSync`.
477
+ *
478
+ * Node's `existsSync` is a `stat` with a `try`/`catch` around it, and on
479
+ * Bun/Windows the throw-and-catch for a path that is not there is most of
480
+ * what it costs. Asking for the stat directly and reading `undefined` for a
481
+ * miss skips that. Measured on this machine, four interleaved rounds
482
+ * against a CPU-bound control that stayed flat at 27-30 ms:
483
+ *
484
+ * existsSync 30.00 us guarded statSync 17.52 us 1.71x
485
+ *
486
+ * A miss is the case that matters: almost no directory holds a `.forbidden`
487
+ * marker, so every level of every walk takes this path. At four unique
488
+ * levels per request that is 50 us, at six 75 us.
489
+ *
490
+ * The `catch` is load-bearing rather than defensive habit. `throwIfNoEntry:
491
+ * false` suppresses **ENOENT only**, so a directory the process cannot stat
492
+ * still throws EACCES or EPERM where `existsSync` answers false - and a
493
+ * throw here would escape through `isForbidden` into the request. Answering
494
+ * false matches the behavior this replaces exactly; the two forms agree on
495
+ * a missing marker, a present one, a marker that is a *directory*, a path
496
+ * several levels below anything that exists, a directory, and the empty
497
+ * string.
498
+ */
499
+ function statProbe(path: string): boolean {
500
+ try {
501
+ return nodeStatSync(path, { throwIfNoEntry: false }) !== undefined
502
+ } catch {
503
+ // See above: a stat that fails for any reason other than absence is
504
+ // reported as absent, which is what `existsSync` did.
505
+ return false
506
+ }
507
+ }
508
+
509
+ let probeForbidden: (path: string) => boolean = statProbe
456
510
 
457
511
  /**
458
512
  * Test seam (convention 9): lets `fs.test.ts` count the syscalls the
459
513
  * per-request dedup above is supposed to be saving, without module-mocking
460
- * `node:fs` — which is process-global and never unwinds.
514
+ * `node:fs`, which is process-global and never unwinds.
461
515
  */
462
516
  export function __setForbiddenProbe(fn: (path: string) => boolean) {
463
517
  probeForbidden = fn
464
518
  }
465
519
 
466
520
  export function __resetForbiddenProbe() {
467
- probeForbidden = nodeExistsSync
521
+ probeForbidden = statProbe
468
522
  }
469
523
 
470
524
  export async function mkdir(path: string) {
@@ -508,6 +562,10 @@ export namespace FileSystem {
508
562
  return compressable.has(cleanExt)
509
563
  }
510
564
 
565
+ /**
566
+ * Two reads of one `BunFile`, which is one stat: `exists` no longer
567
+ * re-wraps the instance, so the second read comes off the cached stat.
568
+ */
511
569
  function validCache(file: Bun.BunFile, sourceMtime: number | null): boolean {
512
570
  return exists(file) && (!sourceMtime || sourceMtime <= file.lastModified)
513
571
  }
@@ -521,11 +579,11 @@ export namespace FileSystem {
521
579
  * so N concurrent first-hits on a cold cache each saw "not cached", each ran
522
580
  * `compiler()`, and each wrote the same three paths. Measured: 8 concurrent
523
581
  * callers produced 8 compiler invocations. Every `.ts`/`.tsx`/`.vue` asset
524
- * goes through here, so a traffic burst on a cold cache — or a cluster whose
525
- * workers share one cache directory — paid the full transpile *plus* gzip
582
+ * goes through here, so a traffic burst on a cold cache (or a cluster whose
583
+ * workers share one cache directory) paid the full transpile *plus* gzip
526
584
  * *and* zstd once per concurrent request.
527
585
  *
528
- * Same memoised-promise idiom as `initDB`, `setupPlugins` and
586
+ * Same memoized-promise idiom as `initDB`, `setupPlugins` and
529
587
  * `getTranspiler`, keyed rather than singular, and deleted in a `finally` so
530
588
  * a failed build is retried by the next caller rather than remembered. It is
531
589
  * bounded by construction (convention 6): an entry exists only while its own
@@ -533,7 +591,7 @@ export namespace FileSystem {
533
591
  *
534
592
  * The key is the path alone, not the path plus `sourceMtime`. A caller that
535
593
  * joins mid-build can therefore receive a build started for a marginally
536
- * older mtime — a window only as wide as one compile, which the next request
594
+ * older mtime: a window only as wide as one compile, which the next request
537
595
  * re-validates and rebuilds. Including the mtime would close that window by
538
596
  * letting two builds of the same file race each other onto the same three
539
597
  * output paths, which is the worse trade.
@@ -590,7 +648,7 @@ export namespace FileSystem {
590
648
  if (content == null) return null
591
649
 
592
650
  // Only on the build path: a warm hit was paying this stat on every
593
- // request for nothing — the directory necessarily exists if the cached
651
+ // request for nothing, the directory necessarily exists if the cached
594
652
  // file it holds just validated.
595
653
  await mkdir(cacheDir)
596
654
 
@@ -617,7 +675,7 @@ export namespace FileSystem {
617
675
 
618
676
  // The mirror of the compressed branch's check above. Without it a
619
677
  // non-compressible entry (a .woff2 from the gstatic proxy, say) was
620
- // rebuilt — for fonts, re-fetched upstream — on every single request.
678
+ // rebuilt (for fonts, re-fetched upstream) on every single request.
621
679
  if (validCache(rawFile, sourceMtime)) return rawFile
622
680
 
623
681
  const content = await compiler()
@@ -629,8 +687,8 @@ export namespace FileSystem {
629
687
 
630
688
  /**
631
689
  * The extension→MIME mapping is static, but resolving it allocates a
632
- * `Bun.BunFile` every call — ~1.4us, on the path of every static-file and
633
- * compressed-asset response. Memoised behind an `LRUCache` rather than a
690
+ * `Bun.BunFile` every call, ~1.4us, on the path of every static-file and
691
+ * compressed-asset response. Memoized behind an `LRUCache` rather than a
634
692
  * plain Map because `ext` is ultimately derived from the request path, and
635
693
  * an unbounded map keyed on client-supplied input is exactly what
636
694
  * convention 6 forbids.
@@ -4,13 +4,13 @@
4
4
  * One implementation, deliberately in core, for the same reason
5
5
  * `credential.ts` is here: the dashboard, db-explorer and analytics plugins
6
6
  * each need this guard, and per-plugin copies of security code drift. These
7
- * three had already drifted — one coerced its predicate's return with
7
+ * three had already drifted: one coerced its predicate's return with
8
8
  * `Boolean`, one gated on `!DEV` where the others read `PROD`, one swallowed a
9
9
  * `getClientIp` throw into an empty string and carried on. Each divergence is
10
10
  * resolved below toward the fail-closed reading, and each is marked as a
11
11
  * decision rather than left to look like a style choice.
12
12
  *
13
- * It owns *only* the predicate — no logins, no sessions, no backoff. The shared
13
+ * It owns *only* the predicate: no logins, no sessions, no backoff. The shared
14
14
  * key path is `credential.ts`; a plugin that offers both doors composes them.
15
15
  *
16
16
  * `getClientIp` is imported from `./ip` directly, never through this
@@ -37,7 +37,7 @@ export type AuthorizeFn = (req: Request) => boolean | Promise<boolean>
37
37
  * `'localhost'` used to be a member, back when the request's *hostname* was
38
38
  * compared against this set as well. A peer address is never the string
39
39
  * `localhost`, and accepting it meant `X-Forwarded-For: localhost` counted as
40
- * loopback under `trustProxy` — and, worse, that `new URL(req.url).hostname`
40
+ * loopback under `trustProxy`, and, worse, that `new URL(req.url).hostname`
41
41
  * did too. Bun builds that from the client's own `Host` header and
42
42
  * `DEFAULT_HOST` is 0.0.0.0, so any peer on the LAN could send
43
43
  * `Host: localhost` and be handed a database browser.
@@ -51,7 +51,7 @@ export function isLoopback(req: Request): boolean {
51
51
  // DECISION (divergence 3): a throw returns `false` immediately rather than
52
52
  // falling through with `ip = ''`. `getClientIp` reads config and the live
53
53
  // server, either of which may be absent (tests, early boot). The two spell
54
- // the same answer today, because `LOOPBACK.has('')` is false — but only by
54
+ // the same answer today, because `LOOPBACK.has('')` is false, but only by
55
55
  // coincidence, and the fall-through invites a later edit that adds a second
56
56
  // source of evidence below this line and silently consults it on the
57
57
  // indeterminate path. Returning here says the answer is settled: no address
@@ -71,31 +71,38 @@ export function isLoopback(req: Request): boolean {
71
71
  */
72
72
  export function defaultAuthorize(req: Request): boolean {
73
73
  // DECISION (divergence 2): gate on `PROD`, not `!DEV`, and read it at call
74
- // time — the mode flags are accessors on `process.env` (`core/init.ts`), so
74
+ // time. The mode flags are accessors on `process.env` (`core/init.ts`), so
75
75
  // they are process state and tests flip them.
76
76
  //
77
77
  // The two spellings look interchangeable because `init.ts` derives them as
78
78
  // complements (`PROD = !isDev && !--dev-worker`). They are not, because they
79
79
  // are *independently settable* accessors and the test fixtures set them one
80
- // at a time: `asDev` flips `DEV` alone and leaves the ambient `PROD` — which
81
- // under `bun test` is `true` — in place. In that state `!DEV` admits a
80
+ // at a time: `asDev` flips `DEV` alone and leaves the ambient `PROD` (which
81
+ // under `bun test` is `true`) in place. In that state `!DEV` admits a
82
82
  // loopback caller and `PROD` denies, so `PROD` is both the fail-closed
83
83
  // reading and the one that literally names the condition it means, rather
84
84
  // than inferring production from the absence of development.
85
85
  //
86
- // Hence `!== false` rather than a plain truthiness test, which is the one
87
- // place a bare `PROD` gate would be *weaker* than `!DEV`. The flag does not
88
- // exist until `core/init.ts` has run, and `if (undefined)` falls straight
89
- // through to the loopback check — so an uninitialised process would open the
90
- // door that an initialised production one keeps shut. `init.ts` defines
91
- // `PROD` as a real boolean (`accessor(!isDev && !hasDevWorkerArg)`), so
92
- // "explicitly false" is a statement only a booted development server can
93
- // make. Unset is not evidence of development; it is no evidence at all.
86
+ // Hence a test for the *positive* development marker rather than a truthiness
87
+ // test on `PROD`, which is the one place a bare `PROD` gate would be weaker
88
+ // than `!DEV`. The flags do not exist until `core/init.ts` has run, and
89
+ // `if (undefined)` falls straight through to the loopback check, so an
90
+ // uninitialized process would open the door an initialized production one
91
+ // keeps shut. Unset is not evidence of development; it is no evidence at all.
92
+ //
93
+ // This read `PROD !== false` while the flags were real booleans. Bun 1.4
94
+ // rejects accessor descriptors on `process.env`, so they are `'1'`/`''`
95
+ // strings now (see `core/init.ts`), and `!== false` is true for *every*
96
+ // string, including the `''` a development server sets, so the gate would
97
+ // have denied on loopback in development while still looking correct.
98
+ // `DEV === '1'` is the same statement in the encoding that survives: only a
99
+ // booted development server sets it, and production (`''`) and never-booted
100
+ // (`undefined`) both fail it.
94
101
  //
95
102
  // `isLoopback` would very likely deny on its own there, having no config or
96
103
  // server to read an address from. That is a second line of defence, not this
97
104
  // one's excuse: a guard should not depend on another guard's failure mode.
98
- if (import.meta.env.PROD !== false) return false
105
+ if (import.meta.env.DEV !== '1') return false
99
106
  return isLoopback(req)
100
107
  }
101
108
 
@@ -116,7 +123,7 @@ export async function isAuthorized(
116
123
  try {
117
124
  // DECISION (divergence 1): `=== true`, never `Boolean(...)`. The predicate
118
125
  // comes from application code and `AuthorizeFn`'s return type is only
119
- // advice — an untyped, transpiled or `as any` predicate can hand back
126
+ // advice: an untyped, transpiled or `as any` predicate can hand back
120
127
  // anything. `Boolean` admits every truthy non-boolean, so a check that
121
128
  // answers with a status string denies on `""` and *grants* on `"no"`, and
122
129
  // one that answers with a count grants on any non-zero. Admission is the
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Cross-origin resource sharing.
3
3
  *
4
- * Bakery already ships the pieces of an API server — `ApiHandler`, sessions,
5
- * CSRF, rate limiting — and had no way to serve one to a browser on another
4
+ * Bakery already ships the pieces of an API server (`ApiHandler`, sessions,
5
+ * CSRF, rate limiting), and had no way to serve one to a browser on another
6
6
  * origin. This is that.
7
7
  *
8
8
  * Two halves, and both are needed: a preflight `OPTIONS` has to be answered
@@ -22,8 +22,8 @@ export interface CorsOptions {
22
22
  /**
23
23
  * Origins allowed to read responses.
24
24
  *
25
- * `'*'` is honoured literally, and is refused in combination with
26
- * `credentials` — see `resolveOrigin`. A function receives the request's
25
+ * `'*'` is honored literally, and is refused in combination with
26
+ * `credentials`. See `resolveOrigin`. A function receives the request's
27
27
  * `Origin` and returns the value to echo, or `null` to deny.
28
28
  */
29
29
  origin: string | string[] | ((origin: string) => string | null)
@@ -45,7 +45,7 @@ const DEFAULT_METHODS = ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE']
45
45
  * The value for `Access-Control-Allow-Origin`, or `null` to send nothing.
46
46
  *
47
47
  * **`'*'` with credentials is refused rather than silently downgraded.** The
48
- * browser rejects that pairing anyway, so honouring it would produce a request
48
+ * browser rejects that pairing anyway, so honoring it would produce a request
49
49
  * that fails in the client with a CORS error and a server that believes it
50
50
  * allowed the call. Echoing the origin instead would be a *quiet widening* of
51
51
  * what the app asked for. Returning null makes the misconfiguration visible as
@@ -97,7 +97,7 @@ export function corsHeaders(
97
97
  /**
98
98
  * The response to a preflight, or `null` if this is not one.
99
99
  *
100
- * A preflight is `OPTIONS` *with* `Access-Control-Request-Method` — plain
100
+ * A preflight is `OPTIONS` *with* `Access-Control-Request-Method`: plain
101
101
  * `OPTIONS` is a normal request and must fall through to routing, or an app
102
102
  * with its own OPTIONS route would find it shadowed.
103
103
  *
@@ -4,14 +4,14 @@
4
4
  *
5
5
  * One implementation, deliberately in core: the db-explorer and analytics
6
6
  * plugins each need it and per-plugin copies of security code drift. It owns
7
- * *only* the comparison — no logins, no sessions, no backoff.
7
+ * *only* the comparison: no logins, no sessions, no backoff.
8
8
  *
9
9
  * A credential is presented three ways, in this order of preference:
10
10
  *
11
11
  * - `x-<name>` header (scriptable, never logged by default)
12
12
  * - `Authorization: Bearer <credential>`
13
13
  * - `?<name>=<credential>` query, for a human opening a URL in a browser
14
- * who cannot set a header — the caller is expected to strip it from the
14
+ * who cannot set a header. The caller is expected to strip it from the
15
15
  * URL client-side; it *does* reach server logs, which is documented, and
16
16
  * the header forms exist for anything automated.
17
17
  *
@@ -20,7 +20,7 @@
20
20
  * variable unset must mean "off", not "open".
21
21
  *
22
22
  * The compare is constant-time. A plain `===` on a secret is a timing oracle,
23
- * and `timingSafeEqual` costs one line — but it throws on length mismatch, so
23
+ * and `timingSafeEqual` costs one line, but it throws on length mismatch, so
24
24
  * the length is checked first (which leaks only the length, never the bytes).
25
25
  */
26
26
 
@@ -41,7 +41,7 @@ export function credentialMatches(
41
41
  /**
42
42
  * Pull a presented credential off a request: the `x-<name>` header, a Bearer
43
43
  * token, or the `<name>` query parameter, in that order. Returns `null` when
44
- * none is present. `name` is the header/query key — `db-key`, `analytics-key`.
44
+ * none is present. `name` is the header/query key: `db-key`, `analytics-key`.
45
45
  */
46
46
  export function readCredential(req: Request, name: string): string | null {
47
47
  const header = req.headers.get(`x-${name}`)
@@ -2,7 +2,7 @@
2
2
  * Cross-site request forgery guards.
3
3
  *
4
4
  * A `SameSite=Lax` cookie is still sent on top-level cross-site *navigations*,
5
- * so it is not on its own sufficient to protect a state-changing endpoint —
5
+ * so it is not on its own sufficient to protect a state-changing endpoint:
6
6
  * `<a href="/api/admin/delete?id=5">` or a cross-origin `<form>` POST both
7
7
  * arrive with the victim's session attached.
8
8
  */
@@ -17,7 +17,7 @@ const SAME_SITE_VALUES = new Set(['same-origin', 'same-site', 'none'])
17
17
  /**
18
18
  * Returns a reason string when the request looks cross-site, or `null` when it
19
19
  * is safe to process. Requests with neither `Origin` nor `Sec-Fetch-Site` (curl,
20
- * server-to-server, older clients) are allowed — browsers always send at least
20
+ * server-to-server, older clients) are allowed: browsers always send at least
21
21
  * one of them on a cross-origin request.
22
22
  */
23
23
  export function checkSameOrigin(req: Request, url: URL): string | null {
@@ -46,7 +46,7 @@ export function checkCsrf(req: Request, url: URL): string | null {
46
46
  /**
47
47
  * An attacker-supplied header value, made safe to put in a reason string.
48
48
  *
49
- * Reason strings are logged, and the logger renders one record per line — so
49
+ * Reason strings are logged, and the logger renders one record per line, so
50
50
  * an `Origin` carrying a newline could forge additional log entries. Anything
51
51
  * outside printable ASCII goes, and the result is capped: a rejection reason
52
52
  * is a diagnostic, not a transcript.
@@ -57,14 +57,13 @@ function safeEcho(value: string): string {
57
57
  }
58
58
 
59
59
  /**
60
- * Origin guard for WebSocket upgrades — deliberately stricter than
60
+ * Origin guard for WebSocket upgrades: deliberately stricter than
61
61
  * `checkSameOrigin`, and applied in `upgradeWebsocket` so every socket
62
62
  * inherits it rather than each handler having to remember.
63
63
  *
64
64
  * WebSockets are exempt from the same-origin policy. Any page the developer
65
65
  * visits can open `ws://localhost:3000/_livereload`, and until this existed
66
- * one `{"type":"subscribe_logger"}` bought a live feed of the server log —
67
- * failing SQL, table and column names, absolute source paths — plus the
66
+ * one `{"type":"subscribe_logger"}` bought a live feed of the server log ( * failing SQL, table and column names, absolute source paths) plus the
68
67
  * ability to reload every open browser. The only thing that separates the
69
68
  * app's own page from an attacker's is `Origin`, which the browser sets on
70
69
  * every handshake and page script cannot override.
@@ -89,7 +88,7 @@ function safeEcho(value: string): string {
89
88
  *
90
89
  * An **absent** `Origin` is allowed, and that is a decision rather than an
91
90
  * oversight. A browser never omits it on a handshake, so its absence means the
92
- * peer is not a browser — curl, a test, a CLI, a health check. Such a client
91
+ * peer is not a browser: curl, a test, a CLI, a health check. Such a client
93
92
  * can also set `Origin` to whatever it likes, so refusing the absent case buys
94
93
  * no security at all while breaking every non-browser client. This check is
95
94
  * worth exactly what a browser's inability to forge the header is worth, and
@@ -7,7 +7,7 @@ import { Try } from '../common/try'
7
7
  import { fs } from '../fs'
8
8
  import { escapeScriptJson } from '../isomorphic/escape'
9
9
 
10
- // Keyed by hostname, which comes from the Host header — bounded so a client
10
+ // Keyed by hostname, which comes from the Host header: bounded so a client
11
11
  // cannot grow it without limit by varying that header per request.
12
12
  const headBodyCache = new LRUCache<string, { head: string; body: string }>(64)
13
13
 
@@ -22,12 +22,12 @@ let depMap = ''
22
22
  const hostDepMaps = new Map<string, string>()
23
23
 
24
24
  /**
25
- * Normalise one import-map entry, for both the process-level map and the
25
+ * Normalize one import-map entry, for both the process-level map and the
26
26
  * per-host maps.
27
27
  *
28
28
  * One helper, two callers, deliberately: they used to be separate copies that
29
- * had drifted into testing different things — one switched on the entry key,
30
- * the other on the entry value — so the same input normalised two ways
29
+ * had drifted into testing different things (one switched on the entry key,
30
+ * the other on the entry value), so the same input normalized two ways
31
31
  * depending on which path saw it.
32
32
  *
33
33
  * `@client/utils` is keyed on the *key*: it is a live alias, the default
@@ -75,12 +75,12 @@ let installedCache: Promise<string[]> | null = null
75
75
  *
76
76
  * Read from `node_modules` rather than from `dependencies`, and the difference
77
77
  * is the whole point: a package can be installed and imported without being
78
- * declared — a transitive one, or a dependency someone forgot to add — and the
78
+ * declared (a transitive one, or a dependency someone forgot to add), and the
79
79
  * browser's failure for a specifier the map misses names its own rule rather
80
80
  * than the missing entry: *"Failed to resolve module specifier 'pkg'. Relative
81
81
  * references must start with either "/", "./", or "../"."*
82
82
  *
83
- * Cheap — one `readdir` per scope, no `package.json` reads — and memoised per
83
+ * Cheap: one `readdir` per scope, no `package.json` reads, and memoized per
84
84
  * process besides: the import map reads it at boot, `bundleModule` on every
85
85
  * bundle (as its `external` list), and a `readdir` sweep per bundle is pure
86
86
  * waste. A dev restart is a new process, so an install still shows up.
@@ -122,7 +122,7 @@ async function readInstalledPackages(): Promise<string[]> {
122
122
  *
123
123
  * **Resolution happens at the other end of the URL, and rewriting happens
124
124
  * nowhere.** An entry maps a package to `/_nm/<name>`; `NMHandler` hands that to
125
- * `Bun.build`, which applies real browser resolution — `exports` maps,
125
+ * `Bun.build`, which applies real browser resolution: `exports` maps,
126
126
  * conditions, the `browser` field. Naming an entry file here instead would mean
127
127
  * reimplementing all of that, badly.
128
128
  *
@@ -168,7 +168,7 @@ export namespace DOMTools {
168
168
 
169
169
  // Was a hand-rolled `JSON.stringify(...).replace(/</g, ...)`, which is the
170
170
  // same job `escapeScriptJson` already does for every other inline-script
171
- // payload — its docstring even said "Mirrors utils/http/dom.ts". The two
171
+ // payload: its docstring even said "Mirrors utils/http/dom.ts". The two
172
172
  // had drifted: this copy never escaped U+2028/U+2029.
173
173
  return `<script>window.__PAGE_PARAMS__ = ${escapeScriptJson(newParams)}</script>`
174
174
  }
@@ -190,7 +190,7 @@ export namespace DOMTools {
190
190
  }
191
191
 
192
192
  /**
193
- * ResponseInit carrying `data`'s status and headers, minus Content-Length —
193
+ * ResponseInit carrying `data`'s status and headers, minus Content-Length:
194
194
  * injection changes the body length, so a stale value must not survive into
195
195
  * the rebuilt Response. Shared by the buffered and streamed injection paths
196
196
  * so the two cannot drift on which headers survive.