@bakery-framework/core 2.0.0-alpha.6 → 2.0.0-alpha.8
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/package.json +1 -1
- package/src/core/index.ts +17 -0
- package/src/handlers/core/$registry.ts +31 -2
- package/src/handlers/core/$routing.ts +25 -3
- package/src/utils/fs.ts +29 -0
package/package.json
CHANGED
package/src/core/index.ts
CHANGED
|
@@ -4,6 +4,10 @@ import { Case, is, Math2, match, Try } from '../utils/common'
|
|
|
4
4
|
import { encodeSSE, response, sse } from '../utils/http'
|
|
5
5
|
import Bakery, { getHostname, hostKey, hostStore } from './bakery'
|
|
6
6
|
import { getConfig, NOOP } from './config'
|
|
7
|
+
// `./context` is already in this barrel's graph — line 5 reaches it through
|
|
8
|
+
// `./bakery`, which re-exports `hostStore` from exactly here — so naming it
|
|
9
|
+
// adds no module edge, only a name.
|
|
10
|
+
import { getFrameworkVersion } from './context'
|
|
7
11
|
import { createElement, Fragment, html } from './jsx'
|
|
8
12
|
|
|
9
13
|
export const defineConfig = <T extends AppConfig>(config: T): T => config
|
|
@@ -63,6 +67,19 @@ export {
|
|
|
63
67
|
encodeSSE,
|
|
64
68
|
Fragment,
|
|
65
69
|
getConfig,
|
|
70
|
+
/**
|
|
71
|
+
* The version of `@bakery-framework/core` itself, read from its own manifest.
|
|
72
|
+
*
|
|
73
|
+
* **Not the app's version**, which is what `import.meta.env.BAKERY_VERSION`
|
|
74
|
+
* and `getAppVersion()` report — the compiler reads those from
|
|
75
|
+
* `<cwd>/package.json`, so in an application they answer with the
|
|
76
|
+
* application's number. The name is a long-standing misnomer and this is the
|
|
77
|
+
* one that means what "Bakery version" sounds like it means.
|
|
78
|
+
*
|
|
79
|
+
* Exported because a plugin rendering framework chrome has no other way to
|
|
80
|
+
* ask. The dashboard's footer showed a hardcoded `v3` for want of it.
|
|
81
|
+
*/
|
|
82
|
+
getFrameworkVersion,
|
|
66
83
|
// Multi-host helpers. Documented in docs/configuration/multi-host.md, and
|
|
67
84
|
// the only reason `./core/bakery` had to be a subpath of its own.
|
|
68
85
|
getHostname,
|
|
@@ -30,11 +30,16 @@ export class HandlerMap<T extends typeof Handler = typeof Handler> extends Map<
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
/** Drop the derived views. Every mutation has to call this, not just `set`. */
|
|
34
|
+
private invalidate(): void {
|
|
35
35
|
this.cachedList = null
|
|
36
36
|
this.cachedGates = null
|
|
37
37
|
this.cachedOrder = null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
set(handlerClass: any, priority: number = 10): this {
|
|
41
|
+
super.set(handlerClass, priority)
|
|
42
|
+
this.invalidate()
|
|
38
43
|
return this
|
|
39
44
|
}
|
|
40
45
|
|
|
@@ -42,6 +47,30 @@ export class HandlerMap<T extends typeof Handler = typeof Handler> extends Map<
|
|
|
42
47
|
return this.set(handlerClass, priority)
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
/**
|
|
51
|
+
* `delete` and `clear` invalidate too, and neither used to.
|
|
52
|
+
*
|
|
53
|
+
* `list()` memoizes the sorted handler array and only `set` cleared it, so a
|
|
54
|
+
* removed handler stayed in the list — and therefore stayed *in the request
|
|
55
|
+
* pipeline* — until something happened to add one. Registration is
|
|
56
|
+
* add-only in a served process, which is why this never bit: it is reachable
|
|
57
|
+
* only by code that unregisters, and the first thing to do that was a test.
|
|
58
|
+
*
|
|
59
|
+
* A cache that survives the removal of its input is wrong regardless of who
|
|
60
|
+
* currently calls it, and "nothing removes handlers today" is a property of
|
|
61
|
+
* the callers rather than of this class.
|
|
62
|
+
*/
|
|
63
|
+
override delete(handlerClass: any): boolean {
|
|
64
|
+
const removed = super.delete(handlerClass)
|
|
65
|
+
if (removed) this.invalidate()
|
|
66
|
+
return removed
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override clear(): void {
|
|
70
|
+
super.clear()
|
|
71
|
+
this.invalidate()
|
|
72
|
+
}
|
|
73
|
+
|
|
45
74
|
list(): T[] {
|
|
46
75
|
if (this.cachedList) {
|
|
47
76
|
return this.cachedList
|
|
@@ -21,15 +21,37 @@ export type RouteScanOptions = {
|
|
|
21
21
|
const catchAllGlob = (ext: string) => new Bun.Glob(`[[]...*${ext || '.*'}`)
|
|
22
22
|
|
|
23
23
|
// The single-param route forms, in the order they are tried: `[name].ext`
|
|
24
|
-
// first, then the
|
|
24
|
+
// first, then the literal-asterisk `*.ext`. Built here rather than twice inside
|
|
25
25
|
// `routeGlobs` — the `dynamicOnly` branch and the combined branch returned
|
|
26
26
|
// character-identical pairs, and the two must stay in step or a route form
|
|
27
27
|
// resolves under one caller and not the other. A function, not a hoisted
|
|
28
28
|
// constant: `ext` varies per handler, and `staticOnly` returns before it needs
|
|
29
29
|
// them at all.
|
|
30
|
-
|
|
30
|
+
//
|
|
31
|
+
// **The literal asterisk is a character class, `[*]`, never the escape `\*`.**
|
|
32
|
+
// A backslash is the obvious spelling and is unusable on Windows, where `\` is
|
|
33
|
+
// a path separator: Bun read `\*.*` as a drive-absolute pattern, ignored the
|
|
34
|
+
// `cwd` in `GETFILE` entirely, and matched files at `C:\`. A route lookup under
|
|
35
|
+
// a serve root six levels down returned `C:\$WINRE_BACKUP_PARTITION.MARKER`,
|
|
36
|
+
// and `fs.isForbidden` — whose walk is bounded by `startsWith(root)` — waved it
|
|
37
|
+
// through because an out-of-root path skipped the loop and answered "allowed".
|
|
38
|
+
// That clamp now fails closed, so this is belt and braces; both halves are
|
|
39
|
+
// pinned, and neither test can see the other's bug.
|
|
40
|
+
//
|
|
41
|
+
// Measured with a scan whose cwd was a temp directory holding one file: `[*]`
|
|
42
|
+
// yields nothing, `\*` yields four files from the drive root. The escape is
|
|
43
|
+
// also unreachable on Windows in the direction it was meant for — `*` is a
|
|
44
|
+
// reserved character in a Windows filename, so a route file literally named
|
|
45
|
+
// `*.ts` can only exist on POSIX, where both spellings match it identically
|
|
46
|
+
// (verified on Linux). The character class costs nothing and means the same
|
|
47
|
+
// thing on both platforms.
|
|
48
|
+
//
|
|
49
|
+
// Exported only as a test seam: with the clamp in place no test driving
|
|
50
|
+
// `getRoute` can tell the two spellings apart — verified by reverting this line
|
|
51
|
+
// and watching all 17 pass — so the pattern has to be asserted directly.
|
|
52
|
+
export const dynamicGlobs = (ext: string) => [
|
|
31
53
|
new Bun.Glob(`[[][!.]*${ext || '.*'}`),
|
|
32
|
-
new Bun.Glob(
|
|
54
|
+
new Bun.Glob(`[*]${ext || '.*'}`),
|
|
33
55
|
]
|
|
34
56
|
|
|
35
57
|
const routeGlobs = (
|
package/src/utils/fs.ts
CHANGED
|
@@ -395,6 +395,35 @@ export namespace FileSystem {
|
|
|
395
395
|
let curr = safeResolve(pathToCheck)
|
|
396
396
|
const normalizedRoot = safeResolve(root)
|
|
397
397
|
|
|
398
|
+
// A path outside `root` is forbidden, and this is the clause that says so.
|
|
399
|
+
//
|
|
400
|
+
// The walk below is bounded by `curr.startsWith(normalizedRoot)`, so an
|
|
401
|
+
// 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
|
|
403
|
+
// one input it most needs to refuse, and it made every caller that relied
|
|
404
|
+
// on this for containment (all of them: `constants.ts` said so in as many
|
|
405
|
+
// words) incapable of catching an escape.
|
|
406
|
+
//
|
|
407
|
+
// Not theoretical. `$routing.ts` builds its route globs with `\*` to mean a
|
|
408
|
+
// literal asterisk; on Windows a backslash is a path *separator*, so Bun
|
|
409
|
+
// read the pattern as drive-absolute, ignored `cwd`, and matched files at
|
|
410
|
+
// `C:\`. `getRoute` resolved one, asked this function, was told "allowed",
|
|
411
|
+
// 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
|
|
413
|
+
// outside the root has to be refused here whatever produced it.
|
|
414
|
+
//
|
|
415
|
+
// The separator suffix matters: a bare `startsWith` would also accept a
|
|
416
|
+
// 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 —
|
|
418
|
+
// `C://` matches nothing, which would make every path under `C:/` read as
|
|
419
|
+
// an escape. `fs.test.ts` drives both through its root corpus.
|
|
420
|
+
const rootPrefix = normalizedRoot.endsWith('/')
|
|
421
|
+
? normalizedRoot
|
|
422
|
+
: `${normalizedRoot}/`
|
|
423
|
+
if (curr !== normalizedRoot && !curr.startsWith(rootPrefix)) {
|
|
424
|
+
return true
|
|
425
|
+
}
|
|
426
|
+
|
|
398
427
|
const store = hostStore.getStore()
|
|
399
428
|
const seen = store ? (store.forbiddenProbes ??= new Map()) : null
|
|
400
429
|
|