@bakery-framework/plugin-dashboard 2.0.0-alpha.1 → 2.0.0-alpha.11

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/src/shell.tsx CHANGED
@@ -1,3 +1,5 @@
1
+ import { getFrameworkVersion } from '@bakery-framework/core'
2
+ import { Bakery } from '@bakery-framework/core/core/bakery'
1
3
  import { renderDatabaseBrowser } from './components/DBBrowser'
2
4
  import { renderLogsPanel } from './components/LogsPanel'
3
5
  import { renderSessionsPanel } from './components/SessionsPanel'
@@ -10,25 +12,78 @@ import { renderTopPagesPanel } from './components/TopPagesPanel'
10
12
  * Keeping that contract lets the chrome be replaced without touching the ~3k
11
13
  * lines of panel client code.
12
14
  */
13
- const NAV = [
14
- {
15
- group: 'Observability',
16
- items: [
17
- { id: 'stats', label: 'Overview' },
18
- { id: 'top-pages', label: 'Traffic' },
19
- { id: 'logs', label: 'Logs' },
20
- ],
21
- },
22
- {
23
- group: 'Data',
24
- items: [
25
- { id: 'database', label: 'Database' },
26
- { id: 'sessions', label: 'Sessions' },
27
- ],
28
- },
29
- ]
15
+ /**
16
+ * Is anything serving `/_db`?
17
+ *
18
+ * Asked of the registry's *declarations*, never by importing db-explorer: a
19
+ * plugin-to-plugin import is a package-graph edge, and
20
+ * `tests/conventions.test.ts` allows exactly one of those (this package →
21
+ * analytics). `Handler.namespace` is the declaration — any handler that owns
22
+ * `/_db` as a surface gets the link, including an application serving its own
23
+ * explorer there.
24
+ *
25
+ * This replaces a behavioural probe that called every handler's
26
+ * `canHandle('/_db')` with a control path to exclude the priority-0 catch-all.
27
+ * The probe worked and its `as any` was the tell: the registry could not say
28
+ * what a handler serves, so the question had to be asked by experiment. Now it
29
+ * can — `list()` is typed `typeof Handler[]`, so this reads with no cast, and
30
+ * a second plugin wanting a console entry declares a namespace rather than
31
+ * copying a probe.
32
+ */
33
+ function explorerIsMounted(): boolean {
34
+ return Bakery.handlers.fetch.list().some(h => h.namespace === '/_db')
35
+ }
36
+
37
+ interface NavEntry {
38
+ id: string
39
+ label: string
40
+ /** Present when this entry leaves the console rather than switching a tab. */
41
+ href?: string
42
+ }
43
+
44
+ function navSections(explorerMounted: boolean): {
45
+ group: string
46
+ items: NavEntry[]
47
+ }[] {
48
+ return [
49
+ {
50
+ group: 'Observability',
51
+ items: [
52
+ { id: 'stats', label: 'Overview' },
53
+ { id: 'top-pages', label: 'Traffic' },
54
+ { id: 'logs', label: 'Logs' },
55
+ ],
56
+ },
57
+ {
58
+ group: 'Data',
59
+ items: [
60
+ // The console does not browse the database any more, so Database is a
61
+ // way *out* of it when the explorer is mounted — a link, not a tab.
62
+ // Without it the entry stays a tab showing the panel that explains
63
+ // where the editor went and how to get it back; an entry that silently
64
+ // navigates to a 404 would be worse than either.
65
+ explorerMounted
66
+ ? { id: 'database', label: 'Database', href: '/_db' }
67
+ : { id: 'database', label: 'Database' },
68
+ { id: 'sessions', label: 'Sessions' },
69
+ ],
70
+ },
71
+ ]
72
+ }
73
+
74
+ function NavItem({ id, label, href }: NavEntry) {
75
+ if (href) {
76
+ return (
77
+ <a class="tab-btn" href={href}>
78
+ <span class="nav-dot"></span>
79
+ <span>{label}</span>
80
+ <span class="nav-external" aria-hidden="true">
81
+
82
+ </span>
83
+ </a>
84
+ )
85
+ }
30
86
 
31
- function NavItem({ id, label }: { id: string; label: string }) {
32
87
  return (
33
88
  <button
34
89
  type="button"
@@ -41,6 +96,9 @@ function NavItem({ id, label }: { id: string; label: string }) {
41
96
  }
42
97
 
43
98
  export default function Dashboard() {
99
+ const explorerMounted = explorerIsMounted()
100
+ const NAV = navSections(explorerMounted)
101
+
44
102
  return (
45
103
  <html lang="en">
46
104
  <head>
@@ -62,14 +120,20 @@ export default function Dashboard() {
62
120
  <nav class="rail-group">
63
121
  <div class="rail-group-label">{section.group}</div>
64
122
  {section.items.map(item => (
65
- <NavItem id={item.id} label={item.label} />
123
+ <NavItem id={item.id} label={item.label} href={item.href} />
66
124
  ))}
67
125
  </nav>
68
126
  ))}
69
127
 
70
128
  <div class="rail-foot">
71
129
  <span>Bakery</span>
72
- <span id="rail-version">v3</span>
130
+ {/* Was the literal `v3`, which was never any version of anything
131
+ — nothing filled the id, and the framework was on 1.x when it
132
+ was written. `getFrameworkVersion()` reads core's own
133
+ manifest, which is the number this label claims to be; the
134
+ app's version is a different question and `BAKERY_VERSION`
135
+ answers that one despite its name. */}
136
+ <span id="rail-version">v{getFrameworkVersion()}</span>
73
137
  </div>
74
138
  </aside>
75
139
 
@@ -125,7 +189,7 @@ export default function Dashboard() {
125
189
  {renderStatsPanel()}
126
190
  {renderTopPagesPanel()}
127
191
  {renderSessionsPanel()}
128
- {renderDatabaseBrowser()}
192
+ {explorerMounted ? null : renderDatabaseBrowser()}
129
193
  {renderLogsPanel()}
130
194
  </main>
131
195
  </div>
package/src/authorize.ts DELETED
@@ -1,76 +0,0 @@
1
- import { getClientIp } from '@bakery-framework/core/utils/http'
2
-
3
- /**
4
- * Decides whether a request may use the console.
5
- *
6
- * The dashboard used to run its own identity system: a shared `DASHPASS`
7
- * secret, a login form, a session flag, a constant-time compare and a
8
- * failed-attempt backoff map. That is a lot of security-sensitive surface for
9
- * a framework to own, and it composed with nothing — an app with real users
10
- * and roles still had to hand out a second, shared password.
11
- *
12
- * So the dashboard no longer authenticates anyone. The host application, which
13
- * already knows who its users are, supplies a predicate.
14
- */
15
- export type AuthorizeFn = (req: Request) => boolean | Promise<boolean>
16
-
17
- /**
18
- * Addresses only. `'localhost'` used to be a member because the request's
19
- * *hostname* was compared against this set as well — see below for why that is
20
- * gone. A peer address is never the string `localhost`, and accepting it would
21
- * mean an `X-Forwarded-For: localhost` counted as loopback under `trustProxy`.
22
- */
23
- const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1'])
24
-
25
- /** True when the request came from this machine. */
26
- export function isLoopback(req: Request): boolean {
27
- // The peer address is the only evidence here the client does not choose.
28
- // This used to fall back to `new URL(req.url).hostname`, which Bun builds
29
- // from the client's own `Host` header — and `DEFAULT_HOST` is 0.0.0.0, so a
30
- // dev server listens on every interface. Any peer on the LAN could send
31
- // `Host: localhost` and be handed the database browser.
32
- //
33
- // getClientIp reads config and the live server, either of which may be
34
- // absent (tests, early boot). An address that cannot be determined is
35
- // indeterminate, and an indeterminate answer is a denial — not a reason to
36
- // consult something the requester controls.
37
- let ip = ''
38
- try {
39
- ip = getClientIp(req)
40
- } catch {
41
- // See above: no server and no config means no evidence, which is a denial.
42
- ip = ''
43
- }
44
-
45
- return LOOPBACK.has(ip)
46
- }
47
-
48
- /**
49
- * Fail closed. With no predicate configured the console is reachable only from
50
- * loopback in development, and from nowhere in production — so forgetting to
51
- * configure it cannot expose a database browser to the internet.
52
- */
53
- export function defaultAuthorize(req: Request): boolean {
54
- if (!import.meta.env.DEV) return false
55
- return isLoopback(req)
56
- }
57
-
58
- export function resolveAuthorize(authorize?: AuthorizeFn): AuthorizeFn {
59
- return authorize ?? defaultAuthorize
60
- }
61
-
62
- /**
63
- * Run a predicate without letting a throwing one grant access.
64
- */
65
- export async function isAuthorized(
66
- authorize: AuthorizeFn,
67
- req: Request,
68
- ): Promise<boolean> {
69
- try {
70
- return Boolean(await authorize(req))
71
- } catch {
72
- // An authorization check that errors is indeterminate, and an
73
- // indeterminate answer is a denial.
74
- return false
75
- }
76
- }