@reddb-io/client 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.
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Embedded-URI rejection for the thin remote-only client.
3
+ *
4
+ * The `@reddb-io/client` package ships only the `red_client` binary,
5
+ * which has no embedded engine. Any URI that asks for an in-memory
6
+ * or file-backed database must be rejected at parse time with the
7
+ * same wording the Rust `red_client` binary prints, so users get a
8
+ * uniform error across language drivers and the CLI.
9
+ *
10
+ * Mirrors `is_embedded_uri` in
11
+ * `crates/reddb-client/src/bin/red_client.rs` (the rejected forms):
12
+ *
13
+ * "red://" — bare red:// with no host
14
+ * "red:" — degenerate
15
+ * "red:///" — explicit empty path
16
+ * "red:///<path>" — any red:// URL with a leading-slash path
17
+ * "red://:memory" — SQLite-style alias
18
+ * "red://:memory:" — SQLite-style alias
19
+ *
20
+ * The legacy `memory://`, `memory:`, and `file://<path>` schemes are
21
+ * also rejected because they were always shorthand for the embedded
22
+ * engine.
23
+ */
24
+
25
+ import { RedDBError } from './protocol.js'
26
+
27
+ /** Wording is intentionally identical to the Rust binary stderr message. */
28
+ export const EMBEDDED_REJECTION_MESSAGE =
29
+ 'embedded schemes (memory:// / file://) are not supported.\n'
30
+ + 'Use the full `red` binary for in-memory or file-backed engines.'
31
+
32
+ /**
33
+ * Specialised error raised when an embedded URI is passed to the
34
+ * thin client. Always carries `code === 'EmbeddedNotSupported'` and
35
+ * the wording from `EMBEDDED_REJECTION_MESSAGE`, surfacing the same
36
+ * actionable hint as the underlying Rust binary.
37
+ */
38
+ export class EmbeddedNotSupported extends RedDBError {
39
+ constructor(uri) {
40
+ super('EmbeddedNotSupported', EMBEDDED_REJECTION_MESSAGE, { uri })
41
+ this.name = 'EmbeddedNotSupported'
42
+ this.uri = uri
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Return true when `uri` selects the embedded engine.
48
+ *
49
+ * @param {string} uri
50
+ * @returns {boolean}
51
+ */
52
+ export function isEmbeddedUri(uri) {
53
+ if (typeof uri !== 'string') return false
54
+ const trimmed = uri.trim()
55
+ if (
56
+ trimmed === 'red://'
57
+ || trimmed === 'red:'
58
+ || trimmed === 'red:/'
59
+ || trimmed === 'red:///'
60
+ || trimmed === 'red://:memory'
61
+ || trimmed === 'red://:memory:'
62
+ ) {
63
+ return true
64
+ }
65
+ // Any `red:///<path>` form is the embedded persistent engine.
66
+ if (trimmed.startsWith('red:///')) return true
67
+ // Legacy shorthands that always meant embedded.
68
+ if (trimmed === 'memory://' || trimmed === 'memory:') return true
69
+ if (trimmed.startsWith('file://')) return true
70
+ return false
71
+ }
72
+
73
+ /**
74
+ * Throws `EmbeddedNotSupported` if `uri` is an embedded shape.
75
+ * Otherwise returns the trimmed URI for downstream consumption.
76
+ *
77
+ * @param {string} uri
78
+ * @returns {string}
79
+ */
80
+ export function rejectEmbeddedUri(uri) {
81
+ if (typeof uri !== 'string' || uri.length === 0) {
82
+ throw new TypeError(
83
+ "connect() requires a URI string (e.g. 'red://localhost:5050' or 'grpc://host:5055')",
84
+ )
85
+ }
86
+ if (isEmbeddedUri(uri)) {
87
+ throw new EmbeddedNotSupported(uri)
88
+ }
89
+ return uri.trim()
90
+ }
package/src/http.js ADDED
@@ -0,0 +1,200 @@
1
+ /**
2
+ * HTTP transport for the JS driver.
3
+ *
4
+ * Mirrors the public surface of `RpcClient` (call, close) but talks
5
+ * straight to the RedDB HTTP server via fetch() — no binary spawn.
6
+ * Each `RedDB` method is mapped to a REST endpoint; method names
7
+ * stay identical to the JSON-RPC ones so `RedDB` doesn't need to
8
+ * know which transport it's using.
9
+ *
10
+ * Endpoint mapping (server-side defined in src/server/routing.rs):
11
+ *
12
+ * query / explain → POST /query, POST /query/explain
13
+ * insert → POST /collections/:name/rows
14
+ * bulk_insert → POST /collections/:name/bulk/rows
15
+ * get → GET /collections/:name/{id} (entity scan + filter)
16
+ * delete → DELETE /collections/:name/{id}
17
+ * health → GET /health
18
+ * version → GET /admin/version
19
+ * auth.login → POST /auth/login
20
+ * auth.whoami → GET /auth/whoami
21
+ * auth.create_api_key → POST /auth/api-keys
22
+ * auth.revoke_api_key → DELETE /auth/api-keys/:key
23
+ * auth.change_password→ POST /auth/change-password
24
+ *
25
+ * cache.get → GET /cache/ns/:ns/:key
26
+ * cache.put → PUT /cache/ns/:ns/:key
27
+ * cache.exists → GET /cache/ns/:ns/:key/exists
28
+ * cache.invalidate → DELETE /cache/ns/:ns/:key
29
+ * cache.invalidate_prefix → DELETE /cache/ns/:ns?prefix=...
30
+ * cache.invalidate_tags → DELETE /cache/ns/:ns/tags (body: {tags})
31
+ * cache.flush_namespace → POST /admin/blob_cache/flush_namespace
32
+ *
33
+ * Auth: every request after construction carries `Authorization:
34
+ * Bearer <token>` (when set). `setToken(token)` updates it in
35
+ * place — used by the login flow that exchanges credentials for
36
+ * a fresh bearer.
37
+ */
38
+
39
+ import { RedDBError } from './protocol.js'
40
+
41
+ export class HttpRpcClient {
42
+ /**
43
+ * @param {object} opts
44
+ * @param {string} opts.baseUrl Server origin, e.g. 'https://reddb.example.com:8443'
45
+ * @param {string} [opts.token] Bearer token / API key
46
+ */
47
+ constructor({ baseUrl, token }) {
48
+ if (typeof baseUrl !== 'string' || baseUrl.length === 0) {
49
+ throw new TypeError('HttpRpcClient: baseUrl required')
50
+ }
51
+ this.baseUrl = baseUrl.replace(/\/$/, '')
52
+ this.token = token ?? null
53
+ }
54
+
55
+ setToken(token) {
56
+ this.token = token
57
+ }
58
+
59
+ async close() {
60
+ // HTTP is stateless — nothing to close.
61
+ }
62
+
63
+ /**
64
+ * Generic RPC entry point. Routes the named method to the
65
+ * corresponding HTTP endpoint and returns the parsed JSON body.
66
+ */
67
+ async call(method, params = {}) {
68
+ const route = ROUTES[method]
69
+ if (!route) {
70
+ throw new RedDBError(
71
+ 'UNKNOWN_METHOD',
72
+ `HTTP transport has no route for method '${method}'`,
73
+ )
74
+ }
75
+ const { url, init } = route(this.baseUrl, params)
76
+ const response = await fetch(url, this.attachAuth(init))
77
+ return parseResponse(response)
78
+ }
79
+
80
+ attachAuth(init) {
81
+ const headers = new Headers(init.headers || {})
82
+ if (this.token && !headers.has('authorization')) {
83
+ headers.set('authorization', `Bearer ${this.token}`)
84
+ }
85
+ if (init.body && !headers.has('content-type')) {
86
+ headers.set('content-type', 'application/json')
87
+ }
88
+ return { ...init, headers }
89
+ }
90
+ }
91
+
92
+ async function parseResponse(response) {
93
+ const text = await response.text()
94
+ let body = null
95
+ if (text) {
96
+ try {
97
+ body = JSON.parse(text)
98
+ } catch {
99
+ body = { raw: text }
100
+ }
101
+ }
102
+ if (!response.ok) {
103
+ const code = body?.error_code || `HTTP_${response.status}`
104
+ const message = body?.error || body?.message || `request failed with status ${response.status}`
105
+ throw new RedDBError(code, message, body)
106
+ }
107
+ // RedDB envelope is `{ ok, result, error? }` for some endpoints
108
+ // and bare JSON for others. Unwrap the envelope when present.
109
+ if (body && typeof body === 'object' && 'ok' in body) {
110
+ if (body.ok === false) {
111
+ const code = body.error_code || 'RPC_ERROR'
112
+ throw new RedDBError(code, body.error || 'unknown error', body)
113
+ }
114
+ return body.result ?? body
115
+ }
116
+ return body
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Method → HTTP route mapping
121
+ // ---------------------------------------------------------------------------
122
+
123
+ const ROUTES = {
124
+ health: (base) => ({ url: `${base}/health`, init: { method: 'GET' } }),
125
+ version: (base) => ({ url: `${base}/admin/version`, init: { method: 'GET' } }),
126
+ query: (base, { sql }) => ({
127
+ url: `${base}/query`,
128
+ init: { method: 'POST', body: JSON.stringify({ query: sql }) },
129
+ }),
130
+ insert: (base, { collection, payload }) => ({
131
+ url: `${base}/collections/${encodeURIComponent(collection)}/rows`,
132
+ init: { method: 'POST', body: JSON.stringify(payload) },
133
+ }),
134
+ bulk_insert: (base, { collection, payloads }) => ({
135
+ url: `${base}/collections/${encodeURIComponent(collection)}/bulk/rows`,
136
+ init: { method: 'POST', body: JSON.stringify({ rows: payloads }) },
137
+ }),
138
+ get: (base, { collection, id }) => ({
139
+ url: `${base}/collections/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`,
140
+ init: { method: 'GET' },
141
+ }),
142
+ delete: (base, { collection, id }) => ({
143
+ url: `${base}/collections/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`,
144
+ init: { method: 'DELETE' },
145
+ }),
146
+ 'auth.login': (base, { username, password }) => ({
147
+ url: `${base}/auth/login`,
148
+ init: { method: 'POST', body: JSON.stringify({ username, password }) },
149
+ }),
150
+ 'auth.whoami': (base) => ({
151
+ url: `${base}/auth/whoami`,
152
+ init: { method: 'GET' },
153
+ }),
154
+ 'auth.create_api_key': (base, params = {}) => ({
155
+ url: `${base}/auth/api-keys`,
156
+ init: { method: 'POST', body: JSON.stringify(params) },
157
+ }),
158
+ 'auth.revoke_api_key': (base, { key }) => ({
159
+ url: `${base}/auth/api-keys/${encodeURIComponent(key)}`,
160
+ init: { method: 'DELETE' },
161
+ }),
162
+ 'auth.change_password': (base, { current_password, new_password }) => ({
163
+ url: `${base}/auth/change-password`,
164
+ init: {
165
+ method: 'POST',
166
+ body: JSON.stringify({
167
+ current_password,
168
+ new_password,
169
+ }),
170
+ },
171
+ }),
172
+ 'cache.get': (base, { namespace, key }) => ({
173
+ url: `${base}/cache/ns/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`,
174
+ init: { method: 'GET' },
175
+ }),
176
+ 'cache.put': (base, { namespace, key, value, ttl_ms, tags, policy }) => ({
177
+ url: `${base}/cache/ns/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`,
178
+ init: { method: 'PUT', body: JSON.stringify({ value, ttl_ms, tags, policy }) },
179
+ }),
180
+ 'cache.exists': (base, { namespace, key }) => ({
181
+ url: `${base}/cache/ns/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}/exists`,
182
+ init: { method: 'GET' },
183
+ }),
184
+ 'cache.invalidate': (base, { namespace, key }) => ({
185
+ url: `${base}/cache/ns/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`,
186
+ init: { method: 'DELETE' },
187
+ }),
188
+ 'cache.invalidate_prefix': (base, { namespace, prefix }) => ({
189
+ url: `${base}/cache/ns/${encodeURIComponent(namespace)}?prefix=${encodeURIComponent(prefix)}`,
190
+ init: { method: 'DELETE' },
191
+ }),
192
+ 'cache.invalidate_tags': (base, { namespace, tags }) => ({
193
+ url: `${base}/cache/ns/${encodeURIComponent(namespace)}/tags`,
194
+ init: { method: 'DELETE', body: JSON.stringify({ tags }) },
195
+ }),
196
+ 'cache.flush_namespace': (base, { namespace }) => ({
197
+ url: `${base}/admin/blob_cache/flush_namespace`,
198
+ init: { method: 'POST', body: JSON.stringify({ namespace }) },
199
+ }),
200
+ }
package/src/index.js ADDED
@@ -0,0 +1,336 @@
1
+ /**
2
+ * @reddb-io/client — thin remote-only RedDB driver.
3
+ *
4
+ * Public API:
5
+ * import { connect } from '@reddb-io/client'
6
+ * const db = await connect('red://reddb.example.com:5050')
7
+ * const result = await db.query('SELECT * FROM users LIMIT 10')
8
+ * await db.close()
9
+ *
10
+ * Accepted URIs:
11
+ * - 'red://host:port' — RedWire TCP (default)
12
+ * - 'reds://host:port' — RedWire over TLS
13
+ * - 'grpc://host:port' — gRPC
14
+ * - 'grpcs://host:port' — gRPC over TLS
15
+ * - 'http://host:port' — HTTP JSON-RPC
16
+ * - 'https://host:port' — HTTPS JSON-RPC
17
+ *
18
+ * Rejected URIs (use @reddb-io/sdk for these):
19
+ * - 'memory://', 'memory:' — in-memory embedded engine
20
+ * - 'file:///abs/path' — file-backed embedded engine
21
+ * - 'red://', 'red:///path' — same shapes via the unified scheme
22
+ * - 'red://:memory[:]' — SQLite-style embedded alias
23
+ *
24
+ * The thin `red_client` binary does not bundle the storage engine —
25
+ * the package is roughly 10x smaller than `@reddb-io/sdk`. If you
26
+ * need an embedded engine, install `@reddb-io/sdk` instead.
27
+ */
28
+
29
+ import { RedDBError } from './protocol.js'
30
+ import { HttpRpcClient } from './http.js'
31
+ import { connectRedwire } from './redwire.js'
32
+ import { parseUri, deriveLoginUrl } from './url.js'
33
+ import {
34
+ EmbeddedNotSupported,
35
+ EMBEDDED_REJECTION_MESSAGE,
36
+ isEmbeddedUri,
37
+ rejectEmbeddedUri,
38
+ } from './embedded-rejection.js'
39
+ import { CacheClient } from './cache.js'
40
+ import { KvClient } from './kv.js'
41
+ import { ConfigClient } from './config.js'
42
+ import { VaultClient } from './vault.js'
43
+
44
+ export { RedDBError, EmbeddedNotSupported, EMBEDDED_REJECTION_MESSAGE, isEmbeddedUri }
45
+ export { CacheClient } from './cache.js'
46
+ export { KvClient } from './kv.js'
47
+ export { ConfigClient } from './config.js'
48
+ export { VaultClient } from './vault.js'
49
+ export { parseUri, deriveLoginUrl } from './url.js'
50
+
51
+ /**
52
+ * Connect to a remote RedDB instance.
53
+ *
54
+ * @param {string} uri Connection URI. See module docstring for accepted schemes.
55
+ * @param {object} [options]
56
+ * @param {object} [options.auth] Authentication credentials.
57
+ * @param {string} [options.auth.token] Bearer / API-key token.
58
+ * @param {string} [options.auth.apiKey] Alias for `token`.
59
+ * @param {string} [options.auth.username] Username for password login.
60
+ * @param {string} [options.auth.password] Password for password login.
61
+ * @param {string} [options.auth.loginUrl] Override URL for the password
62
+ * exchange (defaults to deriving `/auth/login` from `uri`).
63
+ * @param {object} [options.tls] TLS / mTLS options for redwire(s)://.
64
+ * @returns {Promise<RedDB>}
65
+ */
66
+ export async function connect(uri, options = {}) {
67
+ // Reject embedded shapes upfront with the same wording the Rust
68
+ // binary uses, before the URL parser would map them to kind=embedded.
69
+ rejectEmbeddedUri(uri)
70
+
71
+ const parsed = parseUri(uri)
72
+
73
+ // Belt-and-braces: if the parser still produced an embedded kind
74
+ // (e.g. via a URI shape we forgot to enumerate above), reject it.
75
+ if (parsed.kind === 'embedded') {
76
+ throw new EmbeddedNotSupported(uri)
77
+ }
78
+
79
+ const merged = mergeAuthFromUri(parsed, options.auth)
80
+
81
+ if (parsed.kind === 'http' || parsed.kind === 'https') {
82
+ const baseUrl = `${parsed.kind}://${parsed.host}:${parsed.port}`
83
+ let token = merged.token
84
+ if (!token && merged.username && merged.password) {
85
+ const loginUrl = merged.loginUrl ?? `${baseUrl}/auth/login`
86
+ const session = await login(loginUrl, {
87
+ username: merged.username,
88
+ password: merged.password,
89
+ })
90
+ token = session.token
91
+ }
92
+ const client = new HttpRpcClient({ baseUrl, token })
93
+ await client.call('health', {})
94
+ return new RedDB(client)
95
+ }
96
+
97
+ if (
98
+ parsed.kind === 'red'
99
+ || parsed.kind === 'reds'
100
+ || parsed.kind === 'grpc'
101
+ || parsed.kind === 'grpcs'
102
+ ) {
103
+ let token = merged.token
104
+ if (!token && merged.username && merged.password) {
105
+ const loginUrl = merged.loginUrl ?? deriveLoginUrl(parsed)
106
+ const session = await login(loginUrl, {
107
+ username: merged.username,
108
+ password: merged.password,
109
+ })
110
+ token = session.token
111
+ }
112
+ const auth = token ? { kind: 'bearer', token } : { kind: 'anonymous' }
113
+ const tls = buildTlsOpts(parsed, options.tls)
114
+ const client = await connectRedwire({
115
+ host: parsed.host,
116
+ port: parsed.port,
117
+ auth,
118
+ ...(tls ? { tls } : {}),
119
+ })
120
+ return new RedDB(client)
121
+ }
122
+
123
+ if (parsed.kind === 'pg') {
124
+ throw new RedDBError(
125
+ 'PG_TRANSPORT_NOT_WIRED',
126
+ "PostgreSQL wire (proto=pg) requires a node-pg-style client; "
127
+ + "the JS thin client doesn't bundle one. Use a separate `pg` "
128
+ + 'package against the same host:port.',
129
+ )
130
+ }
131
+
132
+ throw new RedDBError(
133
+ 'UNSUPPORTED_KIND',
134
+ `internal: parsed kind '${parsed.kind}' has no transport`,
135
+ )
136
+ }
137
+
138
+ /**
139
+ * Resolve TLS options for a redwire(s) connection. Source order:
140
+ * 1. caller-supplied `options.tls` object.
141
+ * 2. `parsed.kind === 'reds' | 'grpcs'`.
142
+ * 3. `?tls=true` URL param.
143
+ * 4. `?ca=`, `?cert=`, `?key=`, `?servername=`, `?rejectUnauthorized=false`
144
+ * URL params (path or PEM string).
145
+ */
146
+ function buildTlsOpts(parsed, callerTls) {
147
+ if (callerTls && typeof callerTls === 'object') {
148
+ return callerTls
149
+ }
150
+ const params = parsed.params
151
+ const wantsTls =
152
+ parsed.kind === 'reds'
153
+ || parsed.kind === 'grpcs'
154
+ || params?.get?.('tls') === 'true'
155
+ || params?.get?.('tls') === '1'
156
+ if (!wantsTls) return null
157
+ return {
158
+ ca: params?.get?.('ca') ?? undefined,
159
+ cert: params?.get?.('cert') ?? undefined,
160
+ key: params?.get?.('key') ?? undefined,
161
+ servername: params?.get?.('servername') ?? undefined,
162
+ rejectUnauthorized:
163
+ params?.get?.('rejectUnauthorized') === 'false' ? false : true,
164
+ }
165
+ }
166
+
167
+ function mergeAuthFromUri(parsed, optionAuth) {
168
+ const out = {
169
+ token: parsed.token ?? parsed.apiKey ?? null,
170
+ username: parsed.username ?? null,
171
+ password: parsed.password ?? null,
172
+ loginUrl: parsed.loginUrl ?? null,
173
+ }
174
+ if (optionAuth == null) return out
175
+ if (typeof optionAuth !== 'object') {
176
+ throw new TypeError('options.auth must be an object')
177
+ }
178
+ if (optionAuth.token != null) {
179
+ if (typeof optionAuth.token !== 'string' || optionAuth.token.length === 0) {
180
+ throw new TypeError('options.auth.token must be a non-empty string')
181
+ }
182
+ out.token = optionAuth.token
183
+ }
184
+ if (optionAuth.apiKey != null) {
185
+ if (typeof optionAuth.apiKey !== 'string' || optionAuth.apiKey.length === 0) {
186
+ throw new TypeError('options.auth.apiKey must be a non-empty string')
187
+ }
188
+ out.token = optionAuth.apiKey
189
+ }
190
+ if (optionAuth.username != null) {
191
+ if (typeof optionAuth.username !== 'string' || optionAuth.username.length === 0) {
192
+ throw new TypeError('options.auth.username must be a non-empty string')
193
+ }
194
+ out.username = optionAuth.username
195
+ }
196
+ if (optionAuth.password != null) {
197
+ if (typeof optionAuth.password !== 'string' || optionAuth.password.length === 0) {
198
+ throw new TypeError('options.auth.password must be a non-empty string')
199
+ }
200
+ out.password = optionAuth.password
201
+ }
202
+ if (optionAuth.loginUrl != null) {
203
+ out.loginUrl = optionAuth.loginUrl
204
+ }
205
+ return out
206
+ }
207
+
208
+ /**
209
+ * Exchange username + password for a bearer token via the server's
210
+ * `POST /auth/login` HTTP endpoint. Same flow used by `connect()` when
211
+ * the caller passes `auth: { username, password }`.
212
+ *
213
+ * @param {string} loginUrl Full URL of the server's auth endpoint.
214
+ * @param {{ username: string, password: string }} credentials
215
+ * @returns {Promise<{ token: string, username: string, role: string, expires_at: number }>}
216
+ */
217
+ export async function login(loginUrl, { username, password }) {
218
+ if (typeof loginUrl !== 'string' || !loginUrl.startsWith('http')) {
219
+ throw new TypeError("login() requires an http(s):// URL pointing at /auth/login")
220
+ }
221
+ if (typeof username !== 'string' || username.length === 0) {
222
+ throw new TypeError('login() requires a non-empty username')
223
+ }
224
+ if (typeof password !== 'string' || password.length === 0) {
225
+ throw new TypeError('login() requires a non-empty password')
226
+ }
227
+ const response = await fetch(loginUrl, {
228
+ method: 'POST',
229
+ headers: { 'content-type': 'application/json' },
230
+ body: JSON.stringify({ username, password }),
231
+ })
232
+ const body = await response.json().catch(() => ({}))
233
+ if (!response.ok || body.ok === false) {
234
+ const code = body.error_code || `HTTP_${response.status}`
235
+ const message = body.error || `auth/login returned ${response.status}`
236
+ throw new RedDBError(code, message, body)
237
+ }
238
+ if (typeof body.token !== 'string') {
239
+ throw new RedDBError(
240
+ 'AUTH_LOGIN_BAD_RESPONSE',
241
+ 'auth/login response missing string token',
242
+ body,
243
+ )
244
+ }
245
+ return body
246
+ }
247
+
248
+ /**
249
+ * Connection handle. Methods map 1:1 to JSON-RPC methods on the server.
250
+ * Identical surface to `@reddb-io/sdk`'s `RedDB`, minus the local-spawn
251
+ * lifecycle.
252
+ */
253
+ export class RedDB {
254
+ /** @param {HttpRpcClient | import('./redwire.js').RedWireClient} client */
255
+ constructor(client) {
256
+ this.client = client
257
+ this.cache = new CacheClient(client)
258
+ const defaultKv = new KvClient(client)
259
+ this.kv = Object.assign((collection = 'kv_default') => new KvClient(client, collection), {
260
+ put: defaultKv.put.bind(defaultKv),
261
+ invalidateTags: defaultKv.invalidateTags.bind(defaultKv),
262
+ watch: defaultKv.watch.bind(defaultKv),
263
+ watchPrefix: defaultKv.watchPrefix.bind(defaultKv),
264
+ })
265
+ this.config = (collection = 'red.config') => new ConfigClient(client, collection)
266
+ this.vault = (collection = 'red.vault') => new VaultClient(client, collection)
267
+ }
268
+
269
+ /** Execute a SQL query. Returns `{ statement, affected, columns, rows }`. */
270
+ query(sql) {
271
+ return this.client.call('query', { sql })
272
+ }
273
+
274
+ /** Insert one row. Returns `{ affected, id? }`. */
275
+ insert(collection, payload) {
276
+ return this.client.call('insert', { collection, payload })
277
+ }
278
+
279
+ /** Insert many rows in one call. Returns `{ affected }`. */
280
+ bulkInsert(collection, payloads) {
281
+ return this.client.call('bulk_insert', { collection, payloads })
282
+ }
283
+
284
+ /** Get an entity by id. Returns `{ entity }` (entity is `null` if not found). */
285
+ get(collection, id) {
286
+ return this.client.call('get', { collection, id: String(id) })
287
+ }
288
+
289
+ /** Delete an entity by id. Returns `{ affected }`. */
290
+ delete(collection, id) {
291
+ return this.client.call('delete', { collection, id: String(id) })
292
+ }
293
+
294
+ /** Probe the server. Returns `{ ok: true, version }`. */
295
+ health() {
296
+ return this.client.call('health', {})
297
+ }
298
+
299
+ /** Server version + protocol version. */
300
+ version() {
301
+ return this.client.call('version', {})
302
+ }
303
+
304
+ /** Exchange username + password for a bearer token. */
305
+ login(username, password) {
306
+ return this.client.call('auth.login', { username, password })
307
+ }
308
+
309
+ /** Identify the current caller. */
310
+ whoami() {
311
+ return this.client.call('auth.whoami', {})
312
+ }
313
+
314
+ /** Change the current caller's password. */
315
+ changePassword(currentPassword, newPassword) {
316
+ return this.client.call('auth.change_password', {
317
+ current_password: currentPassword,
318
+ new_password: newPassword,
319
+ })
320
+ }
321
+
322
+ /** Mint a long-lived API key. */
323
+ createApiKey({ username, role } = {}) {
324
+ return this.client.call('auth.create_api_key', { username, role })
325
+ }
326
+
327
+ /** Revoke an API key by its public id. */
328
+ revokeApiKey(key) {
329
+ return this.client.call('auth.revoke_api_key', { key })
330
+ }
331
+
332
+ /** Close the underlying transport. */
333
+ close() {
334
+ return this.client.close()
335
+ }
336
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Resolve the GitHub release asset filename for a given Node
3
+ * `process.platform` / `process.arch` pair and binary base name.
4
+ *
5
+ * Mapping is the existing scheme from `drivers/js/postinstall.js`:
6
+ * linux + x64 → <bin>-linux-x86_64
7
+ * linux + arm64 → <bin>-linux-aarch64
8
+ * linux + arm/armv7l→ <bin>-linux-armv7
9
+ * darwin + x64 → <bin>-macos-x86_64
10
+ * darwin + arm64 → <bin>-macos-aarch64
11
+ * win32 + x64 → <bin>-windows-x86_64.exe
12
+ *
13
+ * Throws `UnsupportedPlatformError` for any other combination.
14
+ */
15
+
16
+ export class UnsupportedPlatformError extends Error {
17
+ constructor(platform, arch) {
18
+ super(`unsupported platform/arch combination: ${platform}/${arch}`)
19
+ this.name = 'UnsupportedPlatformError'
20
+ this.code = 'UNSUPPORTED_PLATFORM'
21
+ this.platform = platform
22
+ this.arch = arch
23
+ }
24
+ }
25
+
26
+ export function composeAssetName({ platform, arch, binName }) {
27
+ if (typeof binName !== 'string' || binName === '') {
28
+ throw new TypeError('composeAssetName: `binName` must be a non-empty string')
29
+ }
30
+ if (platform === 'linux' && arch === 'x64') return `${binName}-linux-x86_64`
31
+ if (platform === 'linux' && arch === 'arm64') return `${binName}-linux-aarch64`
32
+ if (platform === 'linux' && (arch === 'arm' || arch === 'armv7l')) return `${binName}-linux-armv7`
33
+ if (platform === 'darwin' && arch === 'x64') return `${binName}-macos-x86_64`
34
+ if (platform === 'darwin' && arch === 'arm64') return `${binName}-macos-aarch64`
35
+ if (platform === 'win32' && arch === 'x64') return `${binName}-windows-x86_64.exe`
36
+ throw new UnsupportedPlatformError(platform, arch)
37
+ }
@@ -0,0 +1,23 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ export class ChecksumMismatchError extends Error {
4
+ constructor(expected, actual) {
5
+ super(`checksum mismatch: expected sha256=${expected}, got sha256=${actual}`)
6
+ this.name = 'ChecksumMismatchError'
7
+ this.code = 'CHECKSUM_MISMATCH'
8
+ this.expected = expected
9
+ this.actual = actual
10
+ }
11
+ }
12
+
13
+ export function sha256Hex(buf) {
14
+ return createHash('sha256').update(buf).digest('hex')
15
+ }
16
+
17
+ export function verifySha256(buf, expected) {
18
+ const expectedNorm = String(expected).trim().toLowerCase()
19
+ const actual = sha256Hex(buf)
20
+ if (actual !== expectedNorm) {
21
+ throw new ChecksumMismatchError(expectedNorm, actual)
22
+ }
23
+ }