1
1
import { createHmac } from "node:crypto" ;
2
- import { readFile } from "node:fs/promises" ;
3
- import { homedir } from "node:os" ;
4
- import { join } from "node:path" ;
5
- import { type CellPiece } from "./markdown.js" ;
2
+ import type { CellPiece } from "./markdown.js" ;
6
3
7
- export type CellResolver = ( cell : CellPiece ) => CellPiece ;
8
-
9
- export interface ResolvedDatabaseReference {
10
- name : string ;
11
- origin : string ;
12
- token : string ;
13
- type : string ;
14
- }
4
+ const DEFAULT_DATABASE_TOKEN_DURATION = 60 * 60 * 1000 * 36 ; // 36 hours in ms
15
5
16
- interface DatabaseProxyItem {
17
- secret : string ;
18
- }
19
-
20
- type DatabaseProxyConfig = Record < string , DatabaseProxyItem > ;
21
-
22
- interface ObservableConfig {
23
- "database-proxy" : DatabaseProxyConfig ;
24
- }
6
+ export type CellResolver = ( cell : CellPiece ) => CellPiece ;
25
7
26
8
interface DatabaseConfig {
27
9
host : string ;
@@ -31,59 +13,49 @@ interface DatabaseConfig {
31
13
secret : string ;
32
14
ssl : "disabled" | "enabled" ;
33
15
type : string ;
34
- url : string ;
35
16
}
36
17
37
- const configFile = join ( homedir ( ) , ".observablehq" ) ;
38
- const key = `database-proxy` ;
39
-
40
- export async function readDatabaseProxyConfig ( ) : Promise < DatabaseProxyConfig | null > {
41
- let observableConfig ;
42
- try {
43
- observableConfig = JSON . parse ( await readFile ( configFile , "utf-8" ) ) as ObservableConfig | null ;
44
- } catch {
45
- // Ignore missing config file
18
+ function getDatabaseProxyConfig ( env : typeof process . env , name : string ) : DatabaseConfig | null {
19
+ const property = `OBSERVABLEHQ_DB_SECRET_${ name } ` ;
20
+ if ( env [ property ] ) {
21
+ const config = JSON . parse ( Buffer . from ( env [ property ] ! , "base64" ) . toString ( "utf8" ) ) as DatabaseConfig ;
22
+ if ( ! config . host || ! config . port || ! config . secret ) {
23
+ throw new Error ( `Invalid database config: ${ property } ` ) ;
24
+ }
25
+ return config ;
46
26
}
47
- return observableConfig && observableConfig [ key ] ;
48
- }
49
-
50
- function readDatabaseConfig ( config : DatabaseProxyConfig | null , name ) : DatabaseConfig {
51
- if ( ! config ) throw new Error ( `Missing database configuration file "${ configFile } "` ) ;
52
- if ( ! name ) throw new Error ( `No database name specified` ) ;
53
- const raw = ( config && config [ name ] ) as DatabaseConfig | null ;
54
- if ( ! raw ) throw new Error ( `No configuration found for "${ name } "` ) ;
55
- return {
56
- ...decodeSecret ( raw . secret ) ,
57
- url : raw . url
58
- } as DatabaseConfig ;
27
+ return null ;
59
28
}
60
29
61
- function decodeSecret ( secret : string ) : Record < string , string > {
62
- return JSON . parse ( Buffer . from ( secret , "base64" ) . toString ( "utf8" ) ) ;
63
- }
64
-
65
- function encodeToken ( payload : { name : string } , secret ) : string {
30
+ function encodeToken ( payload : { name : string ; exp : number } , secret : string ) : string {
66
31
const data = JSON . stringify ( payload ) ;
67
32
const hmac = createHmac ( "sha256" , Buffer . from ( secret , "hex" ) ) . update ( data ) . digest ( ) ;
68
33
return `${ Buffer . from ( data ) . toString ( "base64" ) } .${ Buffer . from ( hmac ) . toString ( "base64" ) } ` ;
69
34
}
70
35
71
- export async function makeCLIResolver ( ) : Promise < CellResolver > {
72
- const config = await readDatabaseProxyConfig ( ) ;
36
+ interface ResolverOptions {
37
+ databaseTokenDuration ?: number ;
38
+ env ?: typeof process . env ;
39
+ }
40
+
41
+ export async function makeCLIResolver ( {
42
+ databaseTokenDuration = DEFAULT_DATABASE_TOKEN_DURATION ,
43
+ env = process . env
44
+ } : ResolverOptions = { } ) : Promise < CellResolver > {
73
45
return ( cell : CellPiece ) : CellPiece => {
74
46
if ( cell . databases !== undefined ) {
75
47
cell = {
76
48
...cell ,
77
49
databases : cell . databases . map ( ( ref ) => {
78
- const db = readDatabaseConfig ( config , ref . name ) ;
50
+ const db = getDatabaseProxyConfig ( env , ref . name ) ;
79
51
if ( db ) {
80
52
const url = new URL ( "http://localhost" ) ;
81
53
url . protocol = db . ssl !== "disabled" ? "https:" : "http:" ;
82
54
url . host = db . host ;
83
55
url . port = String ( db . port ) ;
84
56
return {
85
57
...ref ,
86
- token : encodeToken ( ref , db . secret ) ,
58
+ token : encodeToken ( { ... ref , exp : Date . now ( ) + databaseTokenDuration } , db . secret ) ,
87
59
type : db . type ,
88
60
url : url . toString ( )
89
61
} ;
0 commit comments