@reddb-io/client 1.23.4 → 1.24.0
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/README.md +4 -15
- package/index.browser.d.ts +13 -3
- package/index.d.ts +13 -3
- package/package.json +2 -4
- package/src/cache.js +4 -4
- package/src/core/result.js +76 -0
- package/src/grpc.js +7 -4
- package/src/http.js +17 -9
- package/src/redwire-core.js +10 -1
- package/src/vault.js +5 -1
- package/postinstall.js +0 -139
- package/src/internal/asset-fetcher/asset-name.js +0 -37
- package/src/internal/asset-fetcher/checksum.js +0 -23
- package/src/internal/asset-fetcher/download.js +0 -170
- package/src/internal/asset-fetcher/index.js +0 -130
package/README.md
CHANGED
|
@@ -30,12 +30,10 @@ pnpm add @reddb-io/client
|
|
|
30
30
|
npm install @reddb-io/client
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`
|
|
37
|
-
manually. The driver itself does **not** need the binary for `connect()`
|
|
38
|
-
— it speaks the wire protocols directly from JS.
|
|
33
|
+
This package is pure JavaScript: it has no install scripts and downloads no
|
|
34
|
+
binary, so it installs the same way on every platform and works with
|
|
35
|
+
`--ignore-scripts`. It speaks the wire protocols directly from JS. For the
|
|
36
|
+
`red` / `red_client` CLI, install `@reddb-io/cli`.
|
|
39
37
|
|
|
40
38
|
## Quickstart
|
|
41
39
|
|
|
@@ -186,15 +184,6 @@ await connect('red://host:5050?token=sk-abc')
|
|
|
186
184
|
await connect('red://user:pass@host:5050')
|
|
187
185
|
```
|
|
188
186
|
|
|
189
|
-
## Environment overrides
|
|
190
|
-
|
|
191
|
-
| Variable | Effect |
|
|
192
|
-
| ---------------------------- | ----------------------------------------------------- |
|
|
193
|
-
| `REDDB_CLIENT_BIN` | Override path to `red_client` for spawn-style helpers |
|
|
194
|
-
| `REDDB_SKIP_POSTINSTALL=1` | Don't download the binary on install |
|
|
195
|
-
| `REDDB_POSTINSTALL_VERSION` | Pull a specific release tag |
|
|
196
|
-
| `REDDB_POSTINSTALL_REPO` | Pull from a fork (default `reddb-io/reddb`) |
|
|
197
|
-
|
|
198
187
|
## License
|
|
199
188
|
|
|
200
189
|
MIT.
|
package/index.browser.d.ts
CHANGED
|
@@ -35,6 +35,12 @@ export interface QueryResult {
|
|
|
35
35
|
affected: number
|
|
36
36
|
columns: string[]
|
|
37
37
|
rows: Array<Record<string, unknown>>
|
|
38
|
+
/** Present when the server reports it. */
|
|
39
|
+
ok?: boolean
|
|
40
|
+
/** Causal-consistency token returned by a write; pass it to a later read. */
|
|
41
|
+
bookmark?: string
|
|
42
|
+
/** Execution statistics, when the server reports them. */
|
|
43
|
+
stats?: Record<string, unknown>
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
export type QueryParam =
|
|
@@ -95,9 +101,12 @@ export interface CachePutOptions {
|
|
|
95
101
|
ttl_ms?: number
|
|
96
102
|
tags?: string[]
|
|
97
103
|
policy?: {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
ttl_ms?: number
|
|
105
|
+
expires_at_unix_ms?: number
|
|
106
|
+
max_blob_bytes?: number
|
|
107
|
+
l1_admission?: 'auto' | 'always' | 'never'
|
|
108
|
+
priority?: number
|
|
109
|
+
version?: number
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
|
|
@@ -329,6 +338,7 @@ export class VaultClient {
|
|
|
329
338
|
options?: { collection?: string; tags?: string[] },
|
|
330
339
|
): Promise<QueryResult>
|
|
331
340
|
get(key: string, options?: { collection?: string }): Promise<QueryResult>
|
|
341
|
+
reveal(key: string, options?: { collection?: string }): Promise<QueryResult>
|
|
332
342
|
unseal(key: string, options?: { collection?: string }): Promise<QueryResult>
|
|
333
343
|
}
|
|
334
344
|
|
package/index.d.ts
CHANGED
|
@@ -30,6 +30,12 @@ export interface QueryResult {
|
|
|
30
30
|
affected: number
|
|
31
31
|
columns: string[]
|
|
32
32
|
rows: Array<Record<string, unknown>>
|
|
33
|
+
/** Present when the server reports it. */
|
|
34
|
+
ok?: boolean
|
|
35
|
+
/** Causal-consistency token returned by a write; pass it to a later read. */
|
|
36
|
+
bookmark?: string
|
|
37
|
+
/** Execution statistics, when the server reports them. */
|
|
38
|
+
stats?: Record<string, unknown>
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
export type QueryParam =
|
|
@@ -91,9 +97,12 @@ export interface CachePutOptions {
|
|
|
91
97
|
ttl_ms?: number
|
|
92
98
|
tags?: string[]
|
|
93
99
|
policy?: {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
ttl_ms?: number
|
|
101
|
+
expires_at_unix_ms?: number
|
|
102
|
+
max_blob_bytes?: number
|
|
103
|
+
l1_admission?: 'auto' | 'always' | 'never'
|
|
104
|
+
priority?: number
|
|
105
|
+
version?: number
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
108
|
|
|
@@ -325,6 +334,7 @@ export class VaultClient {
|
|
|
325
334
|
options?: { collection?: string; tags?: string[] },
|
|
326
335
|
): Promise<QueryResult>
|
|
327
336
|
get(key: string, options?: { collection?: string }): Promise<QueryResult>
|
|
337
|
+
reveal(key: string, options?: { collection?: string }): Promise<QueryResult>
|
|
328
338
|
unseal(key: string, options?: { collection?: string }): Promise<QueryResult>
|
|
329
339
|
}
|
|
330
340
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reddb-io/client",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Thin remote-only RedDB driver.
|
|
3
|
+
"version": "1.24.0",
|
|
4
|
+
"description": "Thin remote-only RedDB driver. Pure JavaScript — no install scripts, no native binary. Speaks RedWire/gRPC/HTTP. Embedded URIs (memory://, file://, red:///path) are rejected — use @reddb-io/sdk for those.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"browser": "./src/index.browser.js",
|
|
@@ -51,7 +51,6 @@
|
|
|
51
51
|
"src/",
|
|
52
52
|
"index.d.ts",
|
|
53
53
|
"index.browser.d.ts",
|
|
54
|
-
"postinstall.js",
|
|
55
54
|
"README.md",
|
|
56
55
|
"LICENSE"
|
|
57
56
|
],
|
|
@@ -80,7 +79,6 @@
|
|
|
80
79
|
"url": "https://github.com/reddb-io/reddb/issues"
|
|
81
80
|
},
|
|
82
81
|
"scripts": {
|
|
83
|
-
"postinstall": "node postinstall.js",
|
|
84
82
|
"test": "node --test test/*.test.mjs"
|
|
85
83
|
}
|
|
86
84
|
}
|
package/src/cache.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Cache client — exposes cache.{get,put,exists,invalidate,invalidatePrefix,
|
|
3
3
|
* invalidateTags,flushNamespace} via the underlying HTTP transport.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* The HTTP data-plane endpoints under /cache/ns/* authorize the caller's
|
|
6
|
+
* tenant and cache namespace. Invalidation is local to the addressed server.
|
|
7
|
+
* Use the HTTP transport for remote cache operations.
|
|
8
8
|
*
|
|
9
9
|
* Values are base64-encoded in transit so binary payloads survive JSON.
|
|
10
10
|
*/
|
|
@@ -93,7 +93,7 @@ export class CacheClient {
|
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
95
|
* Remove all entries in a namespace.
|
|
96
|
-
*
|
|
96
|
+
* Uses the caller-scoped POST /cache/ns/:namespace/flush endpoint.
|
|
97
97
|
* @param {string} namespace
|
|
98
98
|
* @returns {Promise<void>}
|
|
99
99
|
*/
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical query result.
|
|
3
|
+
*
|
|
4
|
+
* Every transport must hand callers the same shape,
|
|
5
|
+
* `{ statement, affected, columns, rows }`, with `rows` an array of plain
|
|
6
|
+
* objects keyed by column name. The wire does not: the HTTP and RedWire
|
|
7
|
+
* (`QueryWithParams`) replies are an envelope
|
|
8
|
+
* `{ ok, statement, affected_rows, result: { columns, records: [{ values, meta }], stats } }`,
|
|
9
|
+
* and the gRPC reply carries `records` as well. This turns any of them into
|
|
10
|
+
* the canonical shape. A value that is already canonical normalizes to an
|
|
11
|
+
* equivalent value, so the function is safe to apply more than once.
|
|
12
|
+
*
|
|
13
|
+
* Pure: no `node:` imports, no I/O.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function isObject(value) {
|
|
17
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function firstNumber(...candidates) {
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
if (typeof candidate === 'number' || typeof candidate === 'bigint') return candidate
|
|
23
|
+
}
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A record is `{ values, meta, ... }`; the row a caller wants is `values`. */
|
|
28
|
+
function recordToRow(record) {
|
|
29
|
+
return isObject(record) && isObject(record.values) ? record.values : record
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function columnNames(raw, body) {
|
|
33
|
+
if (Array.isArray(body.columns)) return body.columns
|
|
34
|
+
if (Array.isArray(raw.columns)) return raw.columns
|
|
35
|
+
const described = raw.descriptor?.columns
|
|
36
|
+
if (Array.isArray(described)) {
|
|
37
|
+
return described.map((column) => (isObject(column) ? column.name : column))
|
|
38
|
+
}
|
|
39
|
+
return []
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {unknown} raw a reply from any transport
|
|
44
|
+
* @returns {{ statement: string, affected: number | bigint, columns: string[], rows: object[] }}
|
|
45
|
+
* plus `ok`, `bookmark` and `stats` when the server supplied them
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeQueryResult(raw) {
|
|
48
|
+
if (!isObject(raw)) return raw
|
|
49
|
+
const body = isObject(raw.result) ? raw.result : raw
|
|
50
|
+
const records = Array.isArray(body.records) ? body.records : null
|
|
51
|
+
const hasRows = Array.isArray(body.rows) || Array.isArray(raw.rows)
|
|
52
|
+
if (!records && !hasRows && !('affected_rows' in raw) && !('affected' in raw) && !isObject(raw.result)) {
|
|
53
|
+
// Not a query reply at all (e.g. a bare health/version object): leave it alone.
|
|
54
|
+
return raw
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const rows = Array.isArray(body.rows)
|
|
58
|
+
? body.rows
|
|
59
|
+
: Array.isArray(raw.rows)
|
|
60
|
+
? raw.rows
|
|
61
|
+
: records
|
|
62
|
+
? records.map(recordToRow)
|
|
63
|
+
: []
|
|
64
|
+
|
|
65
|
+
const result = {
|
|
66
|
+
statement: raw.statement_type ?? raw.statement ?? body.statement ?? '',
|
|
67
|
+
affected: firstNumber(raw.affected, raw.affected_rows, body.affected, body.affected_rows) ?? 0,
|
|
68
|
+
columns: columnNames(raw, body),
|
|
69
|
+
rows,
|
|
70
|
+
}
|
|
71
|
+
if (raw.ok !== undefined) result.ok = raw.ok
|
|
72
|
+
if (raw.bookmark !== undefined) result.bookmark = raw.bookmark
|
|
73
|
+
const stats = body.stats ?? raw.stats
|
|
74
|
+
if (stats !== undefined) result.stats = stats
|
|
75
|
+
return result
|
|
76
|
+
}
|
package/src/grpc.js
CHANGED
|
@@ -10,6 +10,7 @@ import { connect as connectHttp2 } from 'node:http2'
|
|
|
10
10
|
import { Buffer } from 'node:buffer'
|
|
11
11
|
|
|
12
12
|
import { RedDBError } from './protocol.js'
|
|
13
|
+
import { normalizeQueryResult } from './core/result.js'
|
|
13
14
|
import { normalizeExactNumbers, serializeJsonValue } from './core/serialization.js'
|
|
14
15
|
|
|
15
16
|
const SERVICE = '/reddb.v1.RedDb'
|
|
@@ -155,12 +156,14 @@ function normalizeQueryReply(reply) {
|
|
|
155
156
|
throw new RedDBError('QUERY_ERROR', `bad gRPC query JSON: ${err.message}`)
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
|
-
|
|
159
|
+
// `records` are `{ values, meta }` objects; the caller wants the `values`.
|
|
160
|
+
const canonical = normalizeQueryResult(parsed)
|
|
161
|
+
const rows = normalizeExactNumbers(Array.isArray(canonical?.rows) ? canonical.rows : [])
|
|
159
162
|
return {
|
|
160
163
|
ok: reply.ok,
|
|
161
|
-
statement:
|
|
162
|
-
affected:
|
|
163
|
-
columns:
|
|
164
|
+
statement: canonical?.statement || reply.statement || '',
|
|
165
|
+
affected: canonical?.affected ?? 0,
|
|
166
|
+
columns: canonical?.columns?.length ? canonical.columns : (reply.columns ?? []),
|
|
164
167
|
rows,
|
|
165
168
|
}
|
|
166
169
|
}
|
package/src/http.js
CHANGED
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
* cache.exists → GET /cache/ns/:ns/:key/exists
|
|
28
28
|
* cache.invalidate → DELETE /cache/ns/:ns/:key
|
|
29
29
|
* cache.invalidate_prefix → DELETE /cache/ns/:ns?prefix=...
|
|
30
|
-
* cache.invalidate_tags →
|
|
31
|
-
* cache.flush_namespace → POST /
|
|
30
|
+
* cache.invalidate_tags → POST /cache/ns/:ns/invalidate-tags (body: {tags})
|
|
31
|
+
* cache.flush_namespace → POST /cache/ns/:ns/flush
|
|
32
32
|
*
|
|
33
33
|
* Auth: every request after construction carries `Authorization:
|
|
34
34
|
* Bearer <token>` (when set). `setToken(token)` updates it in
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
import { RedDBError } from './protocol.js'
|
|
40
40
|
import { classifyNdjsonFrame, splitLines } from './core/ndjson.js'
|
|
41
|
+
import { normalizeQueryResult } from './core/result.js'
|
|
41
42
|
import { normalizeExactNumbers, serializeJsonValue } from './core/serialization.js'
|
|
42
43
|
|
|
43
44
|
export class HttpRpcClient {
|
|
@@ -76,6 +77,11 @@ export class HttpRpcClient {
|
|
|
76
77
|
}
|
|
77
78
|
const { url, init } = route(this.baseUrl, params)
|
|
78
79
|
const response = await fetch(url, this.attachAuth(init))
|
|
80
|
+
if (method === 'query') {
|
|
81
|
+
// Keep the whole envelope: `statement` and `affected_rows` sit next to
|
|
82
|
+
// `result`, which is all the generic unwrap below would return.
|
|
83
|
+
return normalizeQueryResult(await parseResponse(response, { envelope: true }))
|
|
84
|
+
}
|
|
79
85
|
return parseResponse(response)
|
|
80
86
|
}
|
|
81
87
|
|
|
@@ -281,7 +287,7 @@ async function readInputTerminal(response) {
|
|
|
281
287
|
return end
|
|
282
288
|
}
|
|
283
289
|
|
|
284
|
-
async function parseResponse(response) {
|
|
290
|
+
async function parseResponse(response, { envelope = false } = {}) {
|
|
285
291
|
const text = await response.text()
|
|
286
292
|
let body = null
|
|
287
293
|
if (text) {
|
|
@@ -303,7 +309,7 @@ async function parseResponse(response) {
|
|
|
303
309
|
const code = body.error_code || 'RPC_ERROR'
|
|
304
310
|
throw new RedDBError(code, body.error || 'unknown error', body)
|
|
305
311
|
}
|
|
306
|
-
return normalizeExactNumbers(body.result ?? body)
|
|
312
|
+
return normalizeExactNumbers(envelope ? body : (body.result ?? body))
|
|
307
313
|
}
|
|
308
314
|
return normalizeExactNumbers(body)
|
|
309
315
|
}
|
|
@@ -326,7 +332,9 @@ const ROUTES = {
|
|
|
326
332
|
}),
|
|
327
333
|
insert: (base, { collection, payload }) => ({
|
|
328
334
|
url: `${base}/collections/${encodeURIComponent(collection)}/rows`,
|
|
329
|
-
|
|
335
|
+
// The server reads the row from `fields` (`row create payload must contain
|
|
336
|
+
// an object field named 'fields'`); a bare object is a 400.
|
|
337
|
+
init: { method: 'POST', body: JSON.stringify({ fields: serializeJsonValue(payload) }) },
|
|
330
338
|
}),
|
|
331
339
|
bulk_insert: (base, { collection, payloads }) => ({
|
|
332
340
|
url: `${base}/collections/${encodeURIComponent(collection)}/bulk/rows`,
|
|
@@ -387,11 +395,11 @@ const ROUTES = {
|
|
|
387
395
|
init: { method: 'DELETE' },
|
|
388
396
|
}),
|
|
389
397
|
'cache.invalidate_tags': (base, { namespace, tags }) => ({
|
|
390
|
-
url: `${base}/cache/ns/${encodeURIComponent(namespace)}/tags`,
|
|
391
|
-
init: { method: '
|
|
398
|
+
url: `${base}/cache/ns/${encodeURIComponent(namespace)}/invalidate-tags`,
|
|
399
|
+
init: { method: 'POST', body: JSON.stringify({ tags }) },
|
|
392
400
|
}),
|
|
393
401
|
'cache.flush_namespace': (base, { namespace }) => ({
|
|
394
|
-
url: `${base}/
|
|
395
|
-
init: { method: 'POST'
|
|
402
|
+
url: `${base}/cache/ns/${encodeURIComponent(namespace)}/flush`,
|
|
403
|
+
init: { method: 'POST' },
|
|
396
404
|
}),
|
|
397
405
|
}
|
package/src/redwire-core.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { RedDBError } from './protocol.js'
|
|
20
|
+
import { normalizeQueryResult } from './core/result.js'
|
|
20
21
|
|
|
21
22
|
const MAGIC = 0xfe
|
|
22
23
|
const SUPPORTED_VERSION = 0x01
|
|
@@ -344,6 +345,14 @@ export class RedWireClient {
|
|
|
344
345
|
}
|
|
345
346
|
kind = MessageKind.QueryWithParams
|
|
346
347
|
payload = encodeQueryWithParams(sql, params)
|
|
348
|
+
} else if (this.supportsParams()) {
|
|
349
|
+
// Always QueryWithParams, with an empty list when there is nothing to
|
|
350
|
+
// bind. The legacy Query frame answers a bare summary with no rows for
|
|
351
|
+
// anything but a binary SELECT, and QueryBinary mis-decodes some
|
|
352
|
+
// replies (a one-row table came back as all NULLs on 1.23.4);
|
|
353
|
+
// QueryWithParams returns the full envelope for every statement.
|
|
354
|
+
kind = MessageKind.QueryWithParams
|
|
355
|
+
payload = encodeQueryWithParams(sql, [])
|
|
347
356
|
} else {
|
|
348
357
|
kind = isSelectQuery(sql) ? MessageKind.QueryBinary : MessageKind.Query
|
|
349
358
|
payload = new TextEncoder().encode(sql)
|
|
@@ -351,7 +360,7 @@ export class RedWireClient {
|
|
|
351
360
|
await writeFrame(this.socket, kind, corr, payload)
|
|
352
361
|
const resp = await this.reader.next()
|
|
353
362
|
if (resp.kind === MessageKind.Result) {
|
|
354
|
-
return decodeResultPayload(resp.payload)
|
|
363
|
+
return normalizeQueryResult(decodeResultPayload(resp.payload))
|
|
355
364
|
}
|
|
356
365
|
if (resp.kind === MessageKind.Error) {
|
|
357
366
|
throw new RedDBError(
|
package/src/vault.js
CHANGED
|
@@ -23,9 +23,13 @@ export class VaultClient {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
unseal(key, options = {}) {
|
|
26
|
+
return this.reveal(key, options)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
reveal(key, options = {}) {
|
|
26
30
|
const collection = options.collection ?? this.collection
|
|
27
31
|
return this.client.call('query', {
|
|
28
|
-
sql: `
|
|
32
|
+
sql: `VAULT REVEAL ${keyedIdentifier(collection)}.${keyedIdentifier(key)}`,
|
|
29
33
|
})
|
|
30
34
|
}
|
|
31
35
|
}
|
package/postinstall.js
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* postinstall.js — download the matching `red_client` binary from
|
|
3
|
-
* GitHub Releases.
|
|
4
|
-
*
|
|
5
|
-
* Behaviour:
|
|
6
|
-
* - Resolves `red_client-<platform>-<arch>` from process.platform +
|
|
7
|
-
* process.arch via the vendored asset fetcher.
|
|
8
|
-
* - Targets the GitHub release matching this package's version.
|
|
9
|
-
* - Drops the binary at `<package>/bin/red_client[.exe]` and chmods
|
|
10
|
-
* +x on Unix.
|
|
11
|
-
* - On any failure, prints a warning to stderr but exits 0 — npm
|
|
12
|
-
* install never breaks because of this script. The user gets a
|
|
13
|
-
* clear error later when they call `connect()` if the binary is
|
|
14
|
-
* missing AND the runtime needs to spawn it. (The remote-only
|
|
15
|
-
* transports in @reddb-io/client don't actually need the binary
|
|
16
|
-
* for `connect()` itself; the binary is provided as a CLI helper
|
|
17
|
-
* for ad-hoc REPL / one-shot SQL.)
|
|
18
|
-
*
|
|
19
|
-
* Override hooks (env vars):
|
|
20
|
-
* REDDB_SKIP_POSTINSTALL=1 do nothing
|
|
21
|
-
* REDDB_POSTINSTALL_VERSION=… pull a different release tag
|
|
22
|
-
* REDDB_POSTINSTALL_REPO=… pull from a fork (default: reddb-io/reddb)
|
|
23
|
-
* REDDB_CLIENT_BIN=/path runtime override consulted by callers
|
|
24
|
-
* that spawn the binary; postinstall
|
|
25
|
-
* still downloads to the package dir
|
|
26
|
-
* unless REDDB_SKIP_POSTINSTALL=1.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { createRequire } from 'node:module'
|
|
30
|
-
import { fileURLToPath } from 'node:url'
|
|
31
|
-
import { dirname, join, sep } from 'node:path'
|
|
32
|
-
import { existsSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs'
|
|
33
|
-
|
|
34
|
-
import { fetchReleaseAsset } from './src/internal/asset-fetcher/index.js'
|
|
35
|
-
|
|
36
|
-
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
37
|
-
const require = createRequire(import.meta.url)
|
|
38
|
-
const pkg = require('./package.json')
|
|
39
|
-
|
|
40
|
-
const DEFAULT_REPO = 'reddb-io/reddb'
|
|
41
|
-
const BIN_NAME = 'red_client'
|
|
42
|
-
|
|
43
|
-
if (process.env.REDDB_SKIP_POSTINSTALL === '1') {
|
|
44
|
-
process.stdout.write('@reddb-io/client: postinstall skipped (REDDB_SKIP_POSTINSTALL=1)\n')
|
|
45
|
-
process.exit(0)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Workspace-local install detection — see drivers/js/postinstall.js for
|
|
49
|
-
// the matching guard and rationale. connect() in this package doesn't need
|
|
50
|
-
// the binary anyway; the helper CLI is the only consumer.
|
|
51
|
-
if (!HERE.includes(`${sep}node_modules${sep}`)) {
|
|
52
|
-
process.stdout.write(
|
|
53
|
-
'@reddb-io/client: skipping postinstall — running from a workspace checkout (no node_modules/).\n' +
|
|
54
|
-
' connect() works without the binary. If you also want the red_client CLI:\n' +
|
|
55
|
-
' cargo build --release --bin red_client -p reddb-io-client --no-default-features\n' +
|
|
56
|
-
' export REDDB_CLIENT_BIN="$PWD/target/release/red_client"\n' +
|
|
57
|
-
' Set REDDB_SKIP_POSTINSTALL=1 to silence this message.\n',
|
|
58
|
-
)
|
|
59
|
-
process.exit(0)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
main().catch((err) => {
|
|
63
|
-
process.stderr.write(formatFailure(err))
|
|
64
|
-
process.exit(0)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
function formatFailure(err) {
|
|
68
|
-
const repo = process.env.REDDB_POSTINSTALL_REPO || DEFAULT_REPO
|
|
69
|
-
if (err && err.code === 'UNSUPPORTED_PLATFORM') {
|
|
70
|
-
return (
|
|
71
|
-
`@reddb-io/client: no prebuilt red_client binary for ${process.platform}/${process.arch}.\n`
|
|
72
|
-
+ ` Note: connect() itself does not need the binary — it is only used as a CLI helper.\n`
|
|
73
|
-
+ ` Options:\n`
|
|
74
|
-
+ ` - build from source: https://github.com/${repo}#build-from-source\n`
|
|
75
|
-
+ ` - or set REDDB_CLIENT_BIN=/path/to/red_client.\n`
|
|
76
|
-
)
|
|
77
|
-
}
|
|
78
|
-
if (err && err.code === 'ASSET_NOT_FOUND') {
|
|
79
|
-
return (
|
|
80
|
-
`@reddb-io/client: release asset not found at ${err.url}\n`
|
|
81
|
-
+ ` The GitHub Release for this client version likely has not been\n`
|
|
82
|
-
+ ` published yet. The driver still installs and connect() works\n`
|
|
83
|
-
+ ` without the binary; this only affects the bundled CLI helper.\n`
|
|
84
|
-
+ ` To unblock:\n`
|
|
85
|
-
+ ` 1. Pull a specific tag and rebuild:\n`
|
|
86
|
-
+ ` REDDB_POSTINSTALL_VERSION=v1.0.5 npm rebuild @reddb-io/client\n`
|
|
87
|
-
+ ` 2. Or download red_client manually and export REDDB_CLIENT_BIN.\n`
|
|
88
|
-
+ ` Releases: https://github.com/${repo}/releases\n`
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
return (
|
|
92
|
-
`@reddb-io/client: postinstall could not download red_client (${err && err.message}).\n`
|
|
93
|
-
+ ` The package itself still installs (connect() does not need the binary).\n`
|
|
94
|
-
+ ` To get the bundled CLI working:\n`
|
|
95
|
-
+ ` - set REDDB_CLIENT_BIN=/path/to/red_client\n`
|
|
96
|
-
+ ` - or install it manually from https://github.com/${repo}/releases\n`
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function main() {
|
|
101
|
-
const repo = process.env.REDDB_POSTINSTALL_REPO || DEFAULT_REPO
|
|
102
|
-
const tag = process.env.REDDB_POSTINSTALL_VERSION
|
|
103
|
-
? normalizeTag(process.env.REDDB_POSTINSTALL_VERSION)
|
|
104
|
-
: `v${pkg.version}`
|
|
105
|
-
|
|
106
|
-
const binDir = join(HERE, 'bin')
|
|
107
|
-
const binaryPath = join(binDir, defaultBinaryName())
|
|
108
|
-
|
|
109
|
-
if (existsSync(binaryPath)) {
|
|
110
|
-
process.stdout.write(`@reddb-io/client: binary already present at ${binaryPath}\n`)
|
|
111
|
-
return
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
process.stdout.write(
|
|
115
|
-
`@reddb-io/client: downloading ${BIN_NAME} ${tag} for ${process.platform}/${process.arch} from ${repo}\n`,
|
|
116
|
-
)
|
|
117
|
-
const body = await fetchReleaseAsset({
|
|
118
|
-
repo,
|
|
119
|
-
tag,
|
|
120
|
-
platform: process.platform,
|
|
121
|
-
arch: process.arch,
|
|
122
|
-
binName: BIN_NAME,
|
|
123
|
-
})
|
|
124
|
-
mkdirSync(binDir, { recursive: true })
|
|
125
|
-
writeFileSync(binaryPath, body)
|
|
126
|
-
if (process.platform !== 'win32') {
|
|
127
|
-
chmodSync(binaryPath, 0o755)
|
|
128
|
-
}
|
|
129
|
-
process.stdout.write(`@reddb-io/client: installed binary at ${binaryPath}\n`)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function defaultBinaryName() {
|
|
133
|
-
return process.platform === 'win32' ? `${BIN_NAME}.exe` : BIN_NAME
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function normalizeTag(value) {
|
|
137
|
-
const v = String(value).trim()
|
|
138
|
-
return v.startsWith('v') ? v : `v${v}`
|
|
139
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { request as httpsRequest } from 'node:https'
|
|
2
|
-
import { request as httpRequest } from 'node:http'
|
|
3
|
-
|
|
4
|
-
const MAX_REDIRECTS = 5
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Hosts a release download may be served from. The postinstall script writes
|
|
8
|
-
* the response to disk and then executes it, so an unrestricted redirect
|
|
9
|
-
* chain (or a plain-http hop) turns any network position into code
|
|
10
|
-
* execution on the installing machine. GitHub serves release assets from
|
|
11
|
-
* `github.com` and redirects to its object storage.
|
|
12
|
-
*/
|
|
13
|
-
const ALLOWED_HOSTS = new Set([
|
|
14
|
-
'github.com',
|
|
15
|
-
'objects.githubusercontent.com',
|
|
16
|
-
'release-assets.githubusercontent.com',
|
|
17
|
-
'raw.githubusercontent.com',
|
|
18
|
-
])
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Ceiling on a downloaded asset. The `red` binary is tens of megabytes; a
|
|
22
|
-
* response without this cap is buffered in full whatever its size.
|
|
23
|
-
*/
|
|
24
|
-
const MAX_BODY_BYTES = 512 * 1024 * 1024
|
|
25
|
-
|
|
26
|
-
export class DisallowedHostError extends Error {
|
|
27
|
-
constructor(url) {
|
|
28
|
-
super(`refusing to download from a non-allowlisted host: ${url}`)
|
|
29
|
-
this.name = 'DisallowedHostError'
|
|
30
|
-
this.code = 'DISALLOWED_HOST'
|
|
31
|
-
this.url = url
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class ResponseTooLargeError extends Error {
|
|
36
|
-
constructor(url, limit) {
|
|
37
|
-
super(`response from ${url} exceeds the ${limit}-byte limit`)
|
|
38
|
-
this.name = 'ResponseTooLargeError'
|
|
39
|
-
this.code = 'RESPONSE_TOO_LARGE'
|
|
40
|
-
this.url = url
|
|
41
|
-
this.limit = limit
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Reject anything that is not https on an allowlisted host. Applied to the
|
|
47
|
-
* initial URL and to every redirect target, since a redirect is exactly how
|
|
48
|
-
* an attacker would leave the allowlist.
|
|
49
|
-
*
|
|
50
|
-
* `allowedHosts` exists so the test suite can point the transport at a local
|
|
51
|
-
* server; production callers never pass it and get {@link ALLOWED_HOSTS}
|
|
52
|
-
* plus the https requirement.
|
|
53
|
-
*/
|
|
54
|
-
export function assertDownloadableUrl(url, allowedHosts = ALLOWED_HOSTS) {
|
|
55
|
-
let parsed
|
|
56
|
-
try {
|
|
57
|
-
parsed = new URL(url)
|
|
58
|
-
} catch {
|
|
59
|
-
throw new DisallowedHostError(url)
|
|
60
|
-
}
|
|
61
|
-
const isDefaultPolicy = allowedHosts === ALLOWED_HOSTS
|
|
62
|
-
if (isDefaultPolicy && parsed.protocol !== 'https:') {
|
|
63
|
-
throw new DisallowedHostError(url)
|
|
64
|
-
}
|
|
65
|
-
if (!allowedHosts.has(parsed.hostname)) {
|
|
66
|
-
throw new DisallowedHostError(url)
|
|
67
|
-
}
|
|
68
|
-
return parsed
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export class AssetNotFoundError extends Error {
|
|
72
|
-
constructor(url) {
|
|
73
|
-
super(`asset not found (HTTP 404) at ${url}`)
|
|
74
|
-
this.name = 'AssetNotFoundError'
|
|
75
|
-
this.code = 'ASSET_NOT_FOUND'
|
|
76
|
-
this.url = url
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export class HttpError extends Error {
|
|
81
|
-
constructor(status, url) {
|
|
82
|
-
super(`HTTP ${status} fetching ${url}`)
|
|
83
|
-
this.name = 'HttpError'
|
|
84
|
-
this.code = 'HTTP_ERROR'
|
|
85
|
-
this.status = status
|
|
86
|
-
this.url = url
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export class TooManyRedirectsError extends Error {
|
|
91
|
-
constructor(url) {
|
|
92
|
-
super(`too many redirects (>${MAX_REDIRECTS}) starting at ${url}`)
|
|
93
|
-
this.name = 'TooManyRedirectsError'
|
|
94
|
-
this.code = 'TOO_MANY_REDIRECTS'
|
|
95
|
-
this.url = url
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function resolveLocation(currentUrl, location) {
|
|
100
|
-
if (/^https?:\/\//i.test(location)) return location
|
|
101
|
-
return new URL(location, currentUrl).toString()
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function downloadFollowingRedirects(
|
|
105
|
-
url,
|
|
106
|
-
{ userAgent, originalUrl, allowedHosts } = {},
|
|
107
|
-
depth = 0,
|
|
108
|
-
) {
|
|
109
|
-
const startUrl = originalUrl || url
|
|
110
|
-
if (depth > MAX_REDIRECTS) {
|
|
111
|
-
return Promise.reject(new TooManyRedirectsError(startUrl))
|
|
112
|
-
}
|
|
113
|
-
let parsed
|
|
114
|
-
try {
|
|
115
|
-
parsed = assertDownloadableUrl(url, allowedHosts)
|
|
116
|
-
} catch (err) {
|
|
117
|
-
return Promise.reject(err)
|
|
118
|
-
}
|
|
119
|
-
const request = parsed.protocol === 'http:' ? httpRequest : httpsRequest
|
|
120
|
-
return new Promise((resolve, reject) => {
|
|
121
|
-
const req = request(
|
|
122
|
-
url,
|
|
123
|
-
{
|
|
124
|
-
method: 'GET',
|
|
125
|
-
headers: {
|
|
126
|
-
'User-Agent': userAgent || 'reddb-internal-asset-fetcher',
|
|
127
|
-
Accept: 'application/octet-stream',
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
(res) => {
|
|
131
|
-
const status = res.statusCode || 0
|
|
132
|
-
if (status >= 300 && status < 400 && res.headers.location) {
|
|
133
|
-
res.resume()
|
|
134
|
-
const next = resolveLocation(url, res.headers.location)
|
|
135
|
-
downloadFollowingRedirects(
|
|
136
|
-
next,
|
|
137
|
-
{ userAgent, originalUrl: startUrl, allowedHosts },
|
|
138
|
-
depth + 1,
|
|
139
|
-
).then(resolve, reject)
|
|
140
|
-
return
|
|
141
|
-
}
|
|
142
|
-
if (status === 404) {
|
|
143
|
-
res.resume()
|
|
144
|
-
reject(new AssetNotFoundError(startUrl))
|
|
145
|
-
return
|
|
146
|
-
}
|
|
147
|
-
if (status < 200 || status >= 300) {
|
|
148
|
-
res.resume()
|
|
149
|
-
reject(new HttpError(status, url))
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
const chunks = []
|
|
153
|
-
let received = 0
|
|
154
|
-
res.on('data', (chunk) => {
|
|
155
|
-
received += chunk.length
|
|
156
|
-
if (received > MAX_BODY_BYTES) {
|
|
157
|
-
res.destroy()
|
|
158
|
-
reject(new ResponseTooLargeError(startUrl, MAX_BODY_BYTES))
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
chunks.push(chunk)
|
|
162
|
-
})
|
|
163
|
-
res.on('end', () => resolve(Buffer.concat(chunks)))
|
|
164
|
-
res.on('error', reject)
|
|
165
|
-
},
|
|
166
|
-
)
|
|
167
|
-
req.on('error', reject)
|
|
168
|
-
req.end()
|
|
169
|
-
})
|
|
170
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @reddb-io/internal-asset-fetcher — fetch a `red`/`red_client` binary
|
|
3
|
-
* from a GitHub release.
|
|
4
|
-
*
|
|
5
|
-
* Public surface: one function.
|
|
6
|
-
*
|
|
7
|
-
* fetchReleaseAsset({ repo, tag, platform, arch, binName, sha256? }) → Buffer
|
|
8
|
-
*
|
|
9
|
-
* Steps:
|
|
10
|
-
* 1. Map (platform, arch, binName) → asset filename.
|
|
11
|
-
* 2. Compose the GitHub download URL: `https://github.com/<repo>/releases/download/<tag>/<asset>`.
|
|
12
|
-
* 3. Follow up to 5 redirects, returning the final body as a Buffer.
|
|
13
|
-
* 4. If `sha256` was supplied, verify before returning.
|
|
14
|
-
*
|
|
15
|
-
* Errors carry distinct `.code` values so callers can differentiate:
|
|
16
|
-
* - UNSUPPORTED_PLATFORM — no asset for this platform/arch
|
|
17
|
-
* - ASSET_NOT_FOUND — HTTP 404 (release/tag/asset name wrong)
|
|
18
|
-
* - CHECKSUM_MISMATCH — body downloaded but sha256 mismatched
|
|
19
|
-
* - HTTP_ERROR — any other non-2xx status
|
|
20
|
-
* - TOO_MANY_REDIRECTS — redirect chain longer than 5 hops
|
|
21
|
-
*
|
|
22
|
-
* Internal modules (`./asset-name.js`, `./download.js`, `./checksum.js`)
|
|
23
|
-
* are not part of the public contract — only `fetchReleaseAsset` is.
|
|
24
|
-
* They are imported directly in tests for focused coverage.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { composeAssetName } from './asset-name.js'
|
|
28
|
-
import { downloadFollowingRedirects } from './download.js'
|
|
29
|
-
import { sha256Hex, verifySha256, ChecksumMismatchError } from './checksum.js'
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* A GitHub `owner/name` pair. `repo` reaches this function from
|
|
33
|
-
* `REDDB_POSTINSTALL_REPO` / `REDDB_MCP_REPO`, and is interpolated into the
|
|
34
|
-
* download URL, so a value containing a path or authority would point the
|
|
35
|
-
* download somewhere else entirely.
|
|
36
|
-
*/
|
|
37
|
-
const REPO_PATTERN = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/
|
|
38
|
-
|
|
39
|
-
/** A release tag, as it appears in the download URL path. */
|
|
40
|
-
const TAG_PATTERN = /^[A-Za-z0-9._+-]+$/
|
|
41
|
-
|
|
42
|
-
export class ChecksumUnavailableError extends Error {
|
|
43
|
-
constructor(assetName) {
|
|
44
|
-
super(
|
|
45
|
-
`no SHA256SUMS entry for ${assetName}; refusing to install an unverified binary ` +
|
|
46
|
-
`(set REDDB_POSTINSTALL_ALLOW_UNVERIFIED=1 to override)`,
|
|
47
|
-
)
|
|
48
|
-
this.name = 'ChecksumUnavailableError'
|
|
49
|
-
this.code = 'CHECKSUM_UNAVAILABLE'
|
|
50
|
-
this.assetName = assetName
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Parse a `SHA256SUMS` file (`<hex> <filename>` per line) and return the
|
|
56
|
-
* digest recorded for `assetName`, or `null` when it is not listed.
|
|
57
|
-
*/
|
|
58
|
-
export function sha256FromSumsFile(text, assetName) {
|
|
59
|
-
for (const line of String(text).split('\n')) {
|
|
60
|
-
const match = line.trim().match(/^([0-9a-fA-F]{64})\s+\*?(.+)$/)
|
|
61
|
-
if (match && match[2].trim() === assetName) {
|
|
62
|
-
return match[1].toLowerCase()
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return null
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function fetchReleaseAsset({ repo, tag, platform, arch, binName, sha256 } = {}) {
|
|
69
|
-
if (typeof repo !== 'string' || repo === '') {
|
|
70
|
-
throw new TypeError('fetchReleaseAsset: `repo` must be a non-empty string (e.g. "reddb-io/reddb")')
|
|
71
|
-
}
|
|
72
|
-
if (typeof tag !== 'string' || tag === '') {
|
|
73
|
-
throw new TypeError('fetchReleaseAsset: `tag` must be a non-empty string (e.g. "v0.2.9")')
|
|
74
|
-
}
|
|
75
|
-
if (typeof platform !== 'string' || platform === '') {
|
|
76
|
-
throw new TypeError('fetchReleaseAsset: `platform` must be a non-empty string')
|
|
77
|
-
}
|
|
78
|
-
if (typeof arch !== 'string' || arch === '') {
|
|
79
|
-
throw new TypeError('fetchReleaseAsset: `arch` must be a non-empty string')
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!REPO_PATTERN.test(repo)) {
|
|
83
|
-
throw new TypeError(`fetchReleaseAsset: \`repo\` must be "owner/name", got ${JSON.stringify(repo)}`)
|
|
84
|
-
}
|
|
85
|
-
if (!TAG_PATTERN.test(tag)) {
|
|
86
|
-
throw new TypeError(`fetchReleaseAsset: \`tag\` contains unsupported characters: ${JSON.stringify(tag)}`)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const assetName = composeAssetName({ platform, arch, binName })
|
|
90
|
-
const base = `https://github.com/${repo}/releases/download/${tag}`
|
|
91
|
-
const body = await downloadFollowingRedirects(`${base}/${assetName}`)
|
|
92
|
-
|
|
93
|
-
// The caller may pin a digest; otherwise fall back to the `SHA256SUMS`
|
|
94
|
-
// file the release publishes beside the assets. The downloaded bytes are
|
|
95
|
-
// written to disk and executed, so installing them unverified is the one
|
|
96
|
-
// outcome worth failing the install over — `install.sh` has verified since
|
|
97
|
-
// it shipped, and this path had not.
|
|
98
|
-
if (sha256) {
|
|
99
|
-
verifySha256(body, sha256)
|
|
100
|
-
return body
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
let expected = null
|
|
104
|
-
try {
|
|
105
|
-
const sums = await downloadFollowingRedirects(`${base}/SHA256SUMS`)
|
|
106
|
-
expected = sha256FromSumsFile(sums.toString('utf8'), assetName)
|
|
107
|
-
} catch {
|
|
108
|
-
expected = null
|
|
109
|
-
}
|
|
110
|
-
if (expected) {
|
|
111
|
-
if (sha256Hex(body) !== expected) {
|
|
112
|
-
throw new ChecksumMismatchError(expected, sha256Hex(body))
|
|
113
|
-
}
|
|
114
|
-
return body
|
|
115
|
-
}
|
|
116
|
-
if (allowUnverified()) {
|
|
117
|
-
return body
|
|
118
|
-
}
|
|
119
|
-
throw new ChecksumUnavailableError(assetName)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Escape hatch for installing from a release that predates `SHA256SUMS`, or
|
|
124
|
-
* from a fork that does not publish one. Opt-in and loud rather than the
|
|
125
|
-
* silent default it used to be.
|
|
126
|
-
*/
|
|
127
|
-
function allowUnverified() {
|
|
128
|
-
const raw = process.env.REDDB_POSTINSTALL_ALLOW_UNVERIFIED
|
|
129
|
-
return raw === '1' || raw === 'true' || raw === 'yes'
|
|
130
|
-
}
|