@reddb-io/cli 1.0.1
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/LICENSE +661 -0
- package/README.md +635 -0
- package/drivers/js/cli-postinstall.js +109 -0
- package/drivers/js/package.json +50 -0
- package/drivers/js/src/binary.js +66 -0
- package/drivers/js/src/cache.js +137 -0
- package/drivers/js/src/cli.js +25 -0
- package/drivers/js/src/config.js +66 -0
- package/drivers/js/src/http.js +200 -0
- package/drivers/js/src/index.js +432 -0
- package/drivers/js/src/internal/asset-fetcher/asset-name.js +37 -0
- package/drivers/js/src/internal/asset-fetcher/checksum.js +23 -0
- package/drivers/js/src/internal/asset-fetcher/download.js +89 -0
- package/drivers/js/src/internal/asset-fetcher/index.js +52 -0
- package/drivers/js/src/internal/bin-resolver/index.js +57 -0
- package/drivers/js/src/internal/version-compare/index.js +163 -0
- package/drivers/js/src/kv.js +70 -0
- package/drivers/js/src/protocol.js +157 -0
- package/drivers/js/src/redwire.js +723 -0
- package/drivers/js/src/spawn.js +177 -0
- package/drivers/js/src/url.js +271 -0
- package/drivers/js/src/vault.js +58 -0
- package/package.json +39 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-agnostic subprocess spawn.
|
|
3
|
+
*
|
|
4
|
+
* Returns a uniform handle exposing:
|
|
5
|
+
* - `stdin` WritableStream-like (has `write(buf)`, `end()`)
|
|
6
|
+
* - `stdout` AsyncIterable<Uint8Array> for line-buffered reading
|
|
7
|
+
* - `kill()` terminate the process
|
|
8
|
+
* - `wait()` resolve when the process exits
|
|
9
|
+
*
|
|
10
|
+
* Detects Bun and Deno first because they ship native, Node-incompatible
|
|
11
|
+
* spawn APIs that are faster than their `node:child_process` shims.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const isBun = typeof globalThis.Bun !== 'undefined' && typeof globalThis.Bun.spawn === 'function'
|
|
15
|
+
const isDeno = typeof globalThis.Deno !== 'undefined' && typeof globalThis.Deno.Command === 'function'
|
|
16
|
+
|
|
17
|
+
/** @returns {Promise<RedProcess>} */
|
|
18
|
+
export async function spawnRed(binary, args) {
|
|
19
|
+
if (isBun) return spawnBun(binary, args)
|
|
20
|
+
if (isDeno) return spawnDeno(binary, args)
|
|
21
|
+
return spawnNode(binary, args)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Node
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
async function spawnNode(binary, args) {
|
|
29
|
+
const { spawn } = await import('node:child_process')
|
|
30
|
+
const child = spawn(binary, args, { stdio: ['pipe', 'pipe', 'inherit'] })
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
runtime: 'node',
|
|
34
|
+
stdin: {
|
|
35
|
+
write(buf) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
child.stdin.write(buf, (err) => (err ? reject(err) : resolve()))
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
end() {
|
|
41
|
+
child.stdin.end()
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
stdout: child.stdout, // already AsyncIterable<Buffer>
|
|
45
|
+
kill() {
|
|
46
|
+
child.kill('SIGTERM')
|
|
47
|
+
},
|
|
48
|
+
wait() {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
child.on('exit', (code) => resolve(code ?? 0))
|
|
51
|
+
})
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Bun
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
function spawnBun(binary, args) {
|
|
61
|
+
const child = globalThis.Bun.spawn({
|
|
62
|
+
cmd: [binary, ...args],
|
|
63
|
+
stdin: 'pipe',
|
|
64
|
+
stdout: 'pipe',
|
|
65
|
+
stderr: 'inherit',
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const writer = child.stdin.getWriter ? child.stdin.getWriter() : null
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
runtime: 'bun',
|
|
72
|
+
stdin: {
|
|
73
|
+
async write(buf) {
|
|
74
|
+
if (writer) {
|
|
75
|
+
await writer.write(buf)
|
|
76
|
+
} else {
|
|
77
|
+
// Older Bun: stdin is a FileSink
|
|
78
|
+
child.stdin.write(buf)
|
|
79
|
+
await child.stdin.flush()
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
end() {
|
|
83
|
+
if (writer) {
|
|
84
|
+
writer.close()
|
|
85
|
+
} else {
|
|
86
|
+
child.stdin.end()
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
stdout: bunStdoutToAsyncIterable(child.stdout),
|
|
91
|
+
kill() {
|
|
92
|
+
child.kill()
|
|
93
|
+
},
|
|
94
|
+
wait() {
|
|
95
|
+
return child.exited
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function* bunStdoutToAsyncIterable(stream) {
|
|
101
|
+
const reader = stream.getReader()
|
|
102
|
+
try {
|
|
103
|
+
while (true) {
|
|
104
|
+
const { value, done } = await reader.read()
|
|
105
|
+
if (done) return
|
|
106
|
+
yield value
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
reader.releaseLock()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Deno
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
async function spawnDeno(binary, args) {
|
|
118
|
+
const cmd = new globalThis.Deno.Command(binary, {
|
|
119
|
+
args,
|
|
120
|
+
stdin: 'piped',
|
|
121
|
+
stdout: 'piped',
|
|
122
|
+
stderr: 'inherit',
|
|
123
|
+
})
|
|
124
|
+
const child = cmd.spawn()
|
|
125
|
+
const writer = child.stdin.getWriter()
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
runtime: 'deno',
|
|
129
|
+
stdin: {
|
|
130
|
+
async write(buf) {
|
|
131
|
+
await writer.write(buf)
|
|
132
|
+
},
|
|
133
|
+
end() {
|
|
134
|
+
try {
|
|
135
|
+
writer.close()
|
|
136
|
+
} catch {
|
|
137
|
+
// already closed
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
stdout: denoStdoutToAsyncIterable(child.stdout),
|
|
142
|
+
kill() {
|
|
143
|
+
try {
|
|
144
|
+
child.kill('SIGTERM')
|
|
145
|
+
} catch {
|
|
146
|
+
// process may already be gone
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
async wait() {
|
|
150
|
+
const status = await child.status
|
|
151
|
+
return status.code ?? 0
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function* denoStdoutToAsyncIterable(stream) {
|
|
157
|
+
const reader = stream.getReader()
|
|
158
|
+
try {
|
|
159
|
+
while (true) {
|
|
160
|
+
const { value, done } = await reader.read()
|
|
161
|
+
if (done) return
|
|
162
|
+
yield value
|
|
163
|
+
}
|
|
164
|
+
} finally {
|
|
165
|
+
reader.releaseLock()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @typedef {{
|
|
171
|
+
* runtime: 'node' | 'bun' | 'deno',
|
|
172
|
+
* stdin: { write(buf: Uint8Array): Promise<void>, end(): void },
|
|
173
|
+
* stdout: AsyncIterable<Uint8Array>,
|
|
174
|
+
* kill(): void,
|
|
175
|
+
* wait(): Promise<number>,
|
|
176
|
+
* }} RedProcess
|
|
177
|
+
*/
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `red://` connection-string parser.
|
|
3
|
+
*
|
|
4
|
+
* One URL covers every transport RedDB speaks:
|
|
5
|
+
*
|
|
6
|
+
* red:// embedded in-memory
|
|
7
|
+
* red:///abs/path/data.rdb embedded persistent
|
|
8
|
+
* red://user:pass@host:5050 remote, default proto=red (wire)
|
|
9
|
+
* red://host:8080?proto=https remote HTTPS
|
|
10
|
+
* red://host:5432?proto=pg PostgreSQL wire
|
|
11
|
+
* red://host:5055?proto=grpc&token=sk-abc remote gRPC w/ bearer
|
|
12
|
+
* red://host:8080?proto=https&apiKey=ak-xyz remote HTTPS w/ api key
|
|
13
|
+
*
|
|
14
|
+
* Backwards-compat: legacy `memory://`, `file://`, `grpc://` URLs
|
|
15
|
+
* still work via `parseLegacyUrl`. New code should prefer `red://`
|
|
16
|
+
* because it carries auth + protocol selection in one place.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { RedDBError } from './protocol.js'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} ParsedUri
|
|
23
|
+
* @property {'embedded' | 'http' | 'https' | 'red' | 'reds' | 'grpc' | 'grpcs' | 'pg'} kind
|
|
24
|
+
* @property {string} [host]
|
|
25
|
+
* @property {number} [port]
|
|
26
|
+
* @property {string} [path] // for embedded `file://`-equivalent
|
|
27
|
+
* @property {string} [username]
|
|
28
|
+
* @property {string} [password]
|
|
29
|
+
* @property {string} [token]
|
|
30
|
+
* @property {string} [apiKey]
|
|
31
|
+
* @property {string} [loginUrl] // explicit override for login flow
|
|
32
|
+
* @property {URLSearchParams} [params] // remaining query params
|
|
33
|
+
* @property {string} originalUri
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse any URI string into a normalised `ParsedUri`.
|
|
38
|
+
* Accepts `red://`, `memory://`, `file://`, `grpc://` (the latter
|
|
39
|
+
* three for backwards compat).
|
|
40
|
+
*
|
|
41
|
+
* @param {string} uri
|
|
42
|
+
* @returns {ParsedUri}
|
|
43
|
+
*/
|
|
44
|
+
export function parseUri(uri) {
|
|
45
|
+
if (typeof uri !== 'string' || uri.length === 0) {
|
|
46
|
+
throw new TypeError(
|
|
47
|
+
"connect() requires a URI string (e.g. 'red://localhost:5050' or 'red:///data.rdb')",
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
if (uri.startsWith('red://') || uri === 'red:' || uri === 'red:/') {
|
|
51
|
+
return parseRedUrl(uri)
|
|
52
|
+
}
|
|
53
|
+
return parseLegacyUrl(uri)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse a `red://` URL.
|
|
58
|
+
*
|
|
59
|
+
* Authority shape: `[user[:pass]@]host[:port]`
|
|
60
|
+
* Path: optional, used as filesystem path when `host` is absent or
|
|
61
|
+
* is the special token `localhost-embedded` (rare).
|
|
62
|
+
* Query: `proto`, `token`, `apiKey`, `loginUrl`.
|
|
63
|
+
*/
|
|
64
|
+
export function parseRedUrl(uri) {
|
|
65
|
+
// The host might be missing (`red:///path`), the URL constructor
|
|
66
|
+
// requires *something* there. Re-write to a parse-friendly shape:
|
|
67
|
+
// - `red:///x` → `red://embedded.local/x` (embedded with path)
|
|
68
|
+
// - `red://memory` → `red://embedded.local` (embedded in-memory)
|
|
69
|
+
// - `red://` → `red://embedded.local` (embedded in-memory)
|
|
70
|
+
let normalised = uri
|
|
71
|
+
if (uri === 'red:' || uri === 'red:/' || uri === 'red://') {
|
|
72
|
+
normalised = 'red://embedded.local'
|
|
73
|
+
} else if (uri.startsWith('red:///')) {
|
|
74
|
+
normalised = `red://embedded.local${uri.slice('red://'.length)}`
|
|
75
|
+
} else if (
|
|
76
|
+
uri === 'red://memory'
|
|
77
|
+
|| uri === 'red://memory/'
|
|
78
|
+
|| uri === 'red://:memory'
|
|
79
|
+
|| uri === 'red://:memory:' // SQLite-style ":memory:" alias
|
|
80
|
+
) {
|
|
81
|
+
normalised = 'red://embedded.local'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let parsed
|
|
85
|
+
try {
|
|
86
|
+
parsed = new URL(normalised)
|
|
87
|
+
} catch (err) {
|
|
88
|
+
throw new RedDBError('UNPARSEABLE_URI', `failed to parse '${uri}': ${err.message}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const params = parsed.searchParams
|
|
92
|
+
const proto = (params.get('proto') || '').toLowerCase()
|
|
93
|
+
const path = parsed.pathname && parsed.pathname !== '/' ? parsed.pathname : ''
|
|
94
|
+
|
|
95
|
+
// Embedded: special host, OR `proto=embedded`, OR no proto + has path
|
|
96
|
+
// and the user clearly meant a file path (red:///abs/path).
|
|
97
|
+
if (parsed.hostname === 'embedded.local') {
|
|
98
|
+
if (path) {
|
|
99
|
+
return {
|
|
100
|
+
kind: 'embedded',
|
|
101
|
+
path,
|
|
102
|
+
params,
|
|
103
|
+
originalUri: uri,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
kind: 'embedded',
|
|
108
|
+
params,
|
|
109
|
+
originalUri: uri,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Remote — default proto is red (wire).
|
|
114
|
+
const kind = resolveKind(proto)
|
|
115
|
+
const port = parsed.port ? Number(parsed.port) : defaultPortFor(kind)
|
|
116
|
+
const username = parsed.username ? decodeURIComponent(parsed.username) : undefined
|
|
117
|
+
const password = parsed.password ? decodeURIComponent(parsed.password) : undefined
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
kind,
|
|
121
|
+
host: parsed.hostname,
|
|
122
|
+
port,
|
|
123
|
+
path: path || undefined,
|
|
124
|
+
username,
|
|
125
|
+
password,
|
|
126
|
+
token: params.get('token') ?? undefined,
|
|
127
|
+
apiKey: params.get('apiKey') ?? params.get('api_key') ?? undefined,
|
|
128
|
+
loginUrl: params.get('loginUrl') ?? params.get('login_url') ?? undefined,
|
|
129
|
+
params,
|
|
130
|
+
originalUri: uri,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Backwards-compat parser for the legacy URL shapes the driver
|
|
136
|
+
* accepted before `red://` existed. Returns the same `ParsedUri`
|
|
137
|
+
* shape so downstream code is uniform.
|
|
138
|
+
*/
|
|
139
|
+
export function parseLegacyUrl(uri) {
|
|
140
|
+
if (uri === 'memory://' || uri === 'memory:') {
|
|
141
|
+
return { kind: 'embedded', originalUri: uri }
|
|
142
|
+
}
|
|
143
|
+
if (uri.startsWith('file://')) {
|
|
144
|
+
const path = uri.slice('file://'.length)
|
|
145
|
+
if (!path) {
|
|
146
|
+
throw new TypeError(`invalid file:// URI: missing path in '${uri}'`)
|
|
147
|
+
}
|
|
148
|
+
return { kind: 'embedded', path, originalUri: uri }
|
|
149
|
+
}
|
|
150
|
+
if (
|
|
151
|
+
uri.startsWith('grpc://')
|
|
152
|
+
|| uri.startsWith('grpcs://')
|
|
153
|
+
|| uri.startsWith('reds://')
|
|
154
|
+
) {
|
|
155
|
+
const scheme = uri.split('://', 1)[0]
|
|
156
|
+
const stripped = uri.slice(`${scheme}://`.length)
|
|
157
|
+
const [hostPort] = stripped.split(/[/?]/, 1)
|
|
158
|
+
const [host, portStr] = hostPort.split(':')
|
|
159
|
+
if (!host) {
|
|
160
|
+
throw new TypeError(`invalid ${scheme}:// URI: missing host in '${uri}'`)
|
|
161
|
+
}
|
|
162
|
+
const legacyKind = scheme === 'reds' ? 'reds' : scheme === 'grpcs' ? 'grpcs' : scheme === 'grpc' ? 'grpc' : 'red'
|
|
163
|
+
return {
|
|
164
|
+
kind: legacyKind,
|
|
165
|
+
host,
|
|
166
|
+
port: portStr ? Number(portStr) : defaultPortFor(legacyKind),
|
|
167
|
+
originalUri: uri,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (uri.startsWith('http://') || uri.startsWith('https://')) {
|
|
171
|
+
let parsed
|
|
172
|
+
try {
|
|
173
|
+
parsed = new URL(uri)
|
|
174
|
+
} catch (err) {
|
|
175
|
+
throw new RedDBError('UNPARSEABLE_URI', `failed to parse '${uri}': ${err.message}`)
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
kind: parsed.protocol === 'https:' ? 'https' : 'http',
|
|
179
|
+
host: parsed.hostname,
|
|
180
|
+
port: parsed.port ? Number(parsed.port) : defaultPortFor(parsed.protocol === 'https:' ? 'https' : 'http'),
|
|
181
|
+
path: parsed.pathname !== '/' ? parsed.pathname : undefined,
|
|
182
|
+
username: parsed.username ? decodeURIComponent(parsed.username) : undefined,
|
|
183
|
+
password: parsed.password ? decodeURIComponent(parsed.password) : undefined,
|
|
184
|
+
token: parsed.searchParams.get('token') ?? undefined,
|
|
185
|
+
apiKey: parsed.searchParams.get('apiKey') ?? undefined,
|
|
186
|
+
params: parsed.searchParams,
|
|
187
|
+
originalUri: uri,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
throw new RedDBError(
|
|
191
|
+
'UNSUPPORTED_SCHEME',
|
|
192
|
+
`unsupported URI: '${uri}'. Use 'red://...' or one of memory://, file://, grpc://, http(s)://`,
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function resolveKind(protoQueryParam) {
|
|
197
|
+
switch (protoQueryParam) {
|
|
198
|
+
case '':
|
|
199
|
+
case 'red':
|
|
200
|
+
return 'red'
|
|
201
|
+
case 'reds':
|
|
202
|
+
return 'reds'
|
|
203
|
+
case 'grpc':
|
|
204
|
+
return 'grpc'
|
|
205
|
+
case 'grpcs':
|
|
206
|
+
return 'grpcs'
|
|
207
|
+
case 'http':
|
|
208
|
+
return 'http'
|
|
209
|
+
case 'https':
|
|
210
|
+
return 'https'
|
|
211
|
+
case 'pg':
|
|
212
|
+
case 'postgres':
|
|
213
|
+
case 'postgresql':
|
|
214
|
+
return 'pg'
|
|
215
|
+
default:
|
|
216
|
+
throw new RedDBError(
|
|
217
|
+
'UNSUPPORTED_PROTO',
|
|
218
|
+
`unknown proto='${protoQueryParam}'. Supported: red | reds | grpc | grpcs | http | https | pg`,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function defaultPortFor(kind) {
|
|
224
|
+
switch (kind) {
|
|
225
|
+
case 'http':
|
|
226
|
+
return 8080
|
|
227
|
+
case 'https':
|
|
228
|
+
return 8443
|
|
229
|
+
case 'red':
|
|
230
|
+
case 'reds':
|
|
231
|
+
case 'redwire':
|
|
232
|
+
return 5050
|
|
233
|
+
case 'grpc':
|
|
234
|
+
return 5055
|
|
235
|
+
case 'grpcs':
|
|
236
|
+
return 5056
|
|
237
|
+
case 'pg':
|
|
238
|
+
case 'postgres':
|
|
239
|
+
case 'postgresql':
|
|
240
|
+
return 5432
|
|
241
|
+
default:
|
|
242
|
+
return undefined
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Derive the HTTP login URL (`/auth/login`) from a parsed URI.
|
|
248
|
+
* Used by the auto-login flow when the user supplies `username:password@`
|
|
249
|
+
* but not an explicit `loginUrl`.
|
|
250
|
+
*
|
|
251
|
+
* Strategy: if proto is already http/https, just append `/auth/login`.
|
|
252
|
+
* For grpc/grpcs/pg, default to https://host:443 — operators that
|
|
253
|
+
* don't want that should pass `loginUrl=` explicitly.
|
|
254
|
+
*/
|
|
255
|
+
export function deriveLoginUrl(parsed) {
|
|
256
|
+
if (parsed.loginUrl) return parsed.loginUrl
|
|
257
|
+
if (!parsed.host) {
|
|
258
|
+
throw new RedDBError(
|
|
259
|
+
'AUTH_LOGIN_NEEDS_HOST',
|
|
260
|
+
'cannot derive loginUrl without a host; pass it explicitly via loginUrl=...',
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
if (parsed.kind === 'http' || parsed.kind === 'https') {
|
|
264
|
+
const scheme = parsed.kind
|
|
265
|
+
const port = parsed.port ?? defaultPortFor(parsed.kind)
|
|
266
|
+
return `${scheme}://${parsed.host}:${port}/auth/login`
|
|
267
|
+
}
|
|
268
|
+
// Non-HTTP transports — default to HTTPS on 443 unless the user
|
|
269
|
+
// tells us otherwise.
|
|
270
|
+
return `https://${parsed.host}/auth/login`
|
|
271
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export class VaultClient {
|
|
2
|
+
constructor(client, collection = 'red.vault') {
|
|
3
|
+
this.client = client
|
|
4
|
+
this.collection = collection
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
put(key, value, options = {}) {
|
|
8
|
+
rejectVolatileOptions(options, 'vault')
|
|
9
|
+
const collection = options.collection ?? this.collection
|
|
10
|
+
const tags = Array.isArray(options.tags) && options.tags.length > 0
|
|
11
|
+
? ` TAGS [${options.tags.map(keyedStringLiteral).join(', ')}]`
|
|
12
|
+
: ''
|
|
13
|
+
return this.client.call('query', {
|
|
14
|
+
sql: `VAULT PUT ${keyedIdentifier(collection)}.${keyedIdentifier(key)} = ${keyedValueLiteral(value)}${tags}`,
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get(key, options = {}) {
|
|
19
|
+
const collection = options.collection ?? this.collection
|
|
20
|
+
return this.client.call('query', {
|
|
21
|
+
sql: `VAULT GET ${keyedIdentifier(collection)}.${keyedIdentifier(key)}`,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
unseal(key, options = {}) {
|
|
26
|
+
const collection = options.collection ?? this.collection
|
|
27
|
+
return this.client.call('query', {
|
|
28
|
+
sql: `UNSEAL VAULT ${keyedIdentifier(collection)}.${keyedIdentifier(key)}`,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function rejectVolatileOptions(options, domain) {
|
|
34
|
+
for (const field of ['ttl', 'ttlMs', 'ttl_ms', 'expireMs', 'expire_ms', 'expiresAt']) {
|
|
35
|
+
if (options[field] != null) {
|
|
36
|
+
throw new TypeError(`${domain} does not support TTL or expiration options`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function keyedIdentifier(value) {
|
|
42
|
+
const out = String(value)
|
|
43
|
+
if (!/^[A-Za-z0-9_.]+$/.test(out)) {
|
|
44
|
+
throw new TypeError('keyed collection and key names must use letters, numbers, underscores, or dots')
|
|
45
|
+
}
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function keyedValueLiteral(value) {
|
|
50
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
|
|
51
|
+
if (value == null) return 'NULL'
|
|
52
|
+
if (Array.isArray(value) || typeof value === 'object') return JSON.stringify(value)
|
|
53
|
+
return keyedStringLiteral(value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function keyedStringLiteral(value) {
|
|
57
|
+
return `'${String(value).replace(/'/g, "''")}'`
|
|
58
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reddb-io/cli",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "CLI launcher for RedDB. The JS/TS app driver is published as @reddb-io/sdk.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"reddb-cli": "./drivers/js/src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"drivers/js/src/",
|
|
11
|
+
"drivers/js/cli-postinstall.js",
|
|
12
|
+
"drivers/js/package.json"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"packageManager": "pnpm@9.15.0",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"postinstall": "node drivers/js/cli-postinstall.js",
|
|
20
|
+
"test": "node drivers/js/test/smoke.test.mjs",
|
|
21
|
+
"version": "node scripts/sync-version.js"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"database",
|
|
25
|
+
"multi-model",
|
|
26
|
+
"vector-search",
|
|
27
|
+
"graph",
|
|
28
|
+
"document-store",
|
|
29
|
+
"key-value",
|
|
30
|
+
"rust",
|
|
31
|
+
"embedded"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/reddb-io/reddb"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/reddb-io/reddb"
|
|
39
|
+
}
|