@bakery-framework/plugin-db-explorer 2.0.0-alpha.3 → 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 +3 -3
- package/src/credential.ts +26 -0
- package/src/index.ts +7 -2
- package/src/setup.ts +4 -4
- package/src/authorize.ts +0 -82
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakery-framework/plugin-db-explorer",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
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.
|
|
37
|
-
"@bakery-framework/orm": "^2.0.0-alpha.
|
|
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 '
|
|
2
|
+
import type { AuthorizeFn } from '@bakery-framework/core/utils/http'
|
|
3
3
|
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|