@bakery-framework/plugin-db-explorer 2.0.0-alpha.4 → 2.0.0-alpha.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakery-framework/plugin-db-explorer",
3
- "version": "2.0.0-alpha.4",
3
+ "version": "2.0.0-alpha.5",
4
4
  "description": "Bakery database explorer plugin — read-only browsing of the app database.",
5
5
  "keywords": [
6
6
  "bakery",
@@ -33,8 +33,8 @@
33
33
  "!src/tests"
34
34
  ],
35
35
  "dependencies": {
36
- "@bakery-framework/core": "^2.0.0-alpha.4",
37
- "@bakery-framework/orm": "^2.0.0-alpha.4"
36
+ "@bakery-framework/core": "^2.0.0-alpha.5",
37
+ "@bakery-framework/orm": "^2.0.0-alpha.5"
38
38
  },
39
39
  "engines": {
40
40
  "bun": ">=1.3.14"
@@ -0,0 +1,26 @@
1
+ import { requestHasCredential } from '@bakery-framework/core/utils/http'
2
+
3
+ /**
4
+ * Shared-credential access: `dbExplorerPlugin({ credential: import.meta.env
5
+ * .DB_EXPLORER_KEY })`, presented as `x-db-key`, a Bearer token, or a
6
+ * one-time `?db-key=` query the client strips from the URL. The comparison
7
+ * lives in core (`requestHasCredential`) — one copy, shared with analytics;
8
+ * this only names the key, `db-key`.
9
+ *
10
+ * Named `hasDbKey`, not `credentialMatches`, deliberately. Core exports a
11
+ * `credentialMatches` of its own from the very barrel this file imports
12
+ * (`@bakery-framework/core/utils/http`), and it takes
13
+ * `(configured, presented: string | null | undefined)` where this takes
14
+ * `(credential, req: Request)`. Two same-named functions with the same arity
15
+ * and a different second parameter, one of them reachable by wildcard from a
16
+ * module every caller here already imports, is a trap: the compiler catches
17
+ * the direct substitution today only because `Request` is not assignable to
18
+ * `string | null | undefined`, which is a coincidence of the current types
19
+ * rather than a guarantee. The distinct name removes the question.
20
+ */
21
+ export function hasDbKey(
22
+ credential: string | undefined,
23
+ req: Request,
24
+ ): boolean {
25
+ return requestHasCredential(req, credential, 'db-key')
26
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  import { definePlugin } from '@bakery-framework/core/plugins'
2
- import type { AuthorizeFn } from './authorize'
2
+ import type { AuthorizeFn } from '@bakery-framework/core/utils/http'
3
3
 
4
- export type { AuthorizeFn } from './authorize'
4
+ /**
5
+ * Part of this plugin's public surface, and re-exported rather than declared:
6
+ * the predicate type is core's now, shared with the dashboard and analytics,
7
+ * so an application can write one `AuthorizeFn` and hand it to all three.
8
+ */
9
+ export type { AuthorizeFn } from '@bakery-framework/core/utils/http'
5
10
 
6
11
  export interface DbExplorerPluginOptions {
7
12
  /**
package/src/setup.ts CHANGED
@@ -5,14 +5,14 @@ import { errorMsg } from '@bakery-framework/core/logger'
5
5
  import type { PluginRouteTable } from '@bakery-framework/core/plugins'
6
6
  import { routeTable } from '@bakery-framework/core/plugins'
7
7
  import { fs } from '@bakery-framework/core/utils'
8
- import { response } from '@bakery-framework/core/utils/http'
9
8
  import {
10
9
  type AuthorizeFn,
11
- credentialMatches,
12
10
  defaultAuthorize,
13
11
  isAuthorized,
14
12
  resolveAuthorize,
15
- } from './authorize'
13
+ response,
14
+ } from '@bakery-framework/core/utils/http'
15
+ import { hasDbKey } from './credential'
16
16
  import { handleSchema, handleTableData } from './endpoints'
17
17
 
18
18
  /**
@@ -136,7 +136,7 @@ export class DbExplorerHandler extends Handler {
136
136
  // dashboard. Everything else fails closed. Either door admits: the
137
137
  // shared credential (constant-time, off when unset) or the predicate.
138
138
  const admitted =
139
- credentialMatches(credential, req) || (await isAuthorized(authorize, req))
139
+ hasDbKey(credential, req) || (await isAuthorized(authorize, req))
140
140
  if (!/\.(css|js)$/.test(path) && !admitted) {
141
141
  return path.startsWith('/api/')
142
142
  ? response.error('Unauthorized', 401)
package/src/authorize.ts DELETED
@@ -1,82 +0,0 @@
1
- import {
2
- getClientIp,
3
- requestHasCredential,
4
- } from '@bakery-framework/core/utils/http'
5
-
6
- /**
7
- * Decides whether a request may use the explorer.
8
- *
9
- * Same design as the dashboard's guard, for the same reason: the explorer
10
- * authenticates nobody itself. The host application, which already knows who
11
- * its users are, supplies a predicate; without one, access is loopback-only
12
- * in development and denied outright in production, so an unconfigured
13
- * explorer is never exposed.
14
- */
15
- export type AuthorizeFn = (req: Request) => boolean | Promise<boolean>
16
-
17
- /**
18
- * Addresses only, never hostnames: a peer address is not the string
19
- * `localhost`, and matching the request's hostname would trust a header the
20
- * client chooses (`Host: localhost` from anywhere on the LAN, with the dev
21
- * server listening on 0.0.0.0).
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
- // getClientIp reads config and the live server, either of which may be
29
- // absent (tests, early boot). An address that cannot be determined is
30
- // indeterminate, and per convention 2 an indeterminate answer is a denial —
31
- // not a reason to consult something the requester controls.
32
- let ip = ''
33
- try {
34
- ip = getClientIp(req)
35
- } catch {
36
- return false
37
- }
38
- return LOOPBACK.has(ip)
39
- }
40
-
41
- /**
42
- * The default: loopback in dev, nothing in prod. `import.meta.env.PROD` read
43
- * at call time — the flag is process state, and tests flip it.
44
- */
45
- export function defaultAuthorize(req: Request): boolean {
46
- if (import.meta.env.PROD) return false
47
- return isLoopback(req)
48
- }
49
-
50
- export function resolveAuthorize(fn?: AuthorizeFn): AuthorizeFn {
51
- return fn ?? defaultAuthorize
52
- }
53
-
54
- /**
55
- * Guard semantics per convention 2: the *authorizer* may throw or hang-fail;
56
- * the answer to any indeterminate state is denial.
57
- */
58
- export async function isAuthorized(
59
- authorize: AuthorizeFn,
60
- req: Request,
61
- ): Promise<boolean> {
62
- try {
63
- return (await authorize(req)) === true
64
- } catch {
65
- // A predicate that throws is indeterminate, and indeterminate is denied.
66
- return false
67
- }
68
- }
69
-
70
- /**
71
- * Shared-credential access: `dbExplorerPlugin({ credential: import.meta.env
72
- * .DB_EXPLORER_KEY })`, presented as `x-db-key`, a Bearer token, or a
73
- * one-time `?db-key=` query the client strips from the URL. The comparison
74
- * lives in core (`requestHasCredential`) — one copy, shared with analytics;
75
- * this only names the key, `db-key`.
76
- */
77
- export function credentialMatches(
78
- credential: string | undefined,
79
- req: Request,
80
- ): boolean {
81
- return requestHasCredential(req, credential, 'db-key')
82
- }