@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.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * cli-postinstall.js — install/upgrade/skip for `@reddb-io/cli`.
3
+ *
4
+ * Asymmetry vs SDK postinstall (`drivers/js/postinstall.js`):
5
+ * - SDK always downloads — wire format is version-coupled to the driver.
6
+ * - CLI compares this package's version against `red --version` on PATH:
7
+ * absent → install
8
+ * older → upgrade (overwrite + one-line log)
9
+ * equal → skip
10
+ * newer → skip ("PATH binary is newer; leaving in place")
11
+ * garbage → install (warn, treat as absent)
12
+ *
13
+ * See ADR 0007 for the rationale.
14
+ *
15
+ * Override hooks (env vars):
16
+ * REDDB_SKIP_POSTINSTALL=1 do nothing
17
+ * REDDB_BIN=/path/to/red skip — caller already has a binary
18
+ * REDDB_POSTINSTALL_VERSION=… pull a different release tag
19
+ * REDDB_POSTINSTALL_REPO=… pull from a fork (default: reddb-io/reddb)
20
+ */
21
+
22
+ import { createRequire } from 'node:module'
23
+ import { fileURLToPath } from 'node:url'
24
+ import { dirname, join } from 'node:path'
25
+ import { existsSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs'
26
+ import { execSync } from 'node:child_process'
27
+
28
+ import { fetchReleaseAsset } from './src/internal/asset-fetcher/index.js'
29
+ import { compareInstalled } from './src/internal/version-compare/index.js'
30
+
31
+ const HERE = dirname(fileURLToPath(import.meta.url))
32
+ const require = createRequire(import.meta.url)
33
+ // CLI manifest lives at the repo root (../../package.json), and that is the
34
+ // `@reddb-io/cli` package — not the SDK manifest next to this file.
35
+ const pkg = require('../../package.json')
36
+
37
+ const DEFAULT_REPO = 'reddb-io/reddb'
38
+
39
+ if (process.env.REDDB_SKIP_POSTINSTALL === '1') {
40
+ process.stdout.write('reddb-cli: postinstall skipped (REDDB_SKIP_POSTINSTALL=1)\n')
41
+ process.exit(0)
42
+ }
43
+
44
+ if (typeof process.env.REDDB_BIN === 'string' && process.env.REDDB_BIN !== '') {
45
+ process.stdout.write(
46
+ `reddb-cli: postinstall skipped (REDDB_BIN=${process.env.REDDB_BIN})\n`,
47
+ )
48
+ process.exit(0)
49
+ }
50
+
51
+ main().catch((err) => {
52
+ process.stderr.write(
53
+ `reddb-cli: postinstall could not download the binary (${err.message}).\n` +
54
+ ` The package will still install. To use the CLI you can:\n` +
55
+ ` - set REDDB_BIN=/path/to/red\n` +
56
+ ` - or install the binary manually from https://github.com/${DEFAULT_REPO}/releases\n`,
57
+ )
58
+ process.exit(0)
59
+ })
60
+
61
+ async function main() {
62
+ const verdict = compareInstalled({
63
+ packageVersion: pkg.version,
64
+ exec: () => execSync('red --version', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }),
65
+ })
66
+
67
+ if (verdict.action === 'skip') {
68
+ process.stdout.write(`reddb-cli: ${verdict.reason} — skipping download\n`)
69
+ return
70
+ }
71
+
72
+ const repo = process.env.REDDB_POSTINSTALL_REPO || DEFAULT_REPO
73
+ const tag = process.env.REDDB_POSTINSTALL_VERSION
74
+ ? normalizeTag(process.env.REDDB_POSTINSTALL_VERSION)
75
+ : `v${pkg.version}`
76
+
77
+ const binDir = join(HERE, 'bin')
78
+ const binaryPath = join(binDir, defaultBinaryName())
79
+
80
+ if (verdict.action === 'install') {
81
+ process.stdout.write(`reddb-cli: ${verdict.reason} — installing red ${tag}\n`)
82
+ } else {
83
+ // upgrade
84
+ process.stdout.write(`reddb-cli: ${verdict.reason} — upgrading red to ${tag}\n`)
85
+ }
86
+
87
+ const body = await fetchReleaseAsset({
88
+ repo,
89
+ tag,
90
+ platform: process.platform,
91
+ arch: process.arch,
92
+ binName: 'red',
93
+ })
94
+ mkdirSync(binDir, { recursive: true })
95
+ writeFileSync(binaryPath, body)
96
+ if (process.platform !== 'win32') {
97
+ chmodSync(binaryPath, 0o755)
98
+ }
99
+ process.stdout.write(`reddb-cli: installed binary at ${binaryPath}\n`)
100
+ }
101
+
102
+ function defaultBinaryName() {
103
+ return process.platform === 'win32' ? 'red.exe' : 'red'
104
+ }
105
+
106
+ function normalizeTag(value) {
107
+ const v = String(value).trim()
108
+ return v.startsWith('v') ? v : `v${v}`
109
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@reddb-io/sdk",
3
+ "version": "1.0.1",
4
+ "description": "Official RedDB driver — talks the native RedWire TCP protocol (mTLS), HTTP, gRPC bridge, or embedded stdio JSON-RPC. Works in Node 18+, Bun and Deno.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./src/index.js",
10
+ "types": "./index.d.ts"
11
+ }
12
+ },
13
+ "types": "./index.d.ts",
14
+ "files": [
15
+ "src/",
16
+ "index.d.ts",
17
+ "postinstall.js",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "postinstall": "node postinstall.js",
23
+ "test": "node --test test/cache.test.mjs && node test/smoke.test.mjs"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "keywords": [
29
+ "reddb",
30
+ "database",
31
+ "client",
32
+ "driver",
33
+ "json-rpc",
34
+ "multi-model",
35
+ "vector",
36
+ "graph",
37
+ "document",
38
+ "key-value"
39
+ ],
40
+ "license": "MIT",
41
+ "homepage": "https://github.com/reddb-io/reddb",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/reddb-io/reddb.git",
45
+ "directory": "drivers/js"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/reddb-io/reddb/issues"
49
+ }
50
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Locate the `red` binary for SDK / CLI use.
3
+ *
4
+ * SDK lookup (`resolveSdkBinary`):
5
+ * 1. `REDDB_BIN` env var (the canonical override per ADR 0006).
6
+ * 2. `REDDB_BINARY_PATH` env var (legacy alias, deprecation window).
7
+ * 3. `<package>/bin/red[.exe]` — where postinstall.js dropped it.
8
+ * 4. Otherwise throw an actionable error.
9
+ *
10
+ * PATH is **never** consulted. The wire-format coupling between the
11
+ * SDK and the embedded engine is too tight to silently bind to
12
+ * whatever `red` happens to be on PATH (see ADR 0006).
13
+ *
14
+ * CLI lookup (`resolveCliBinary`):
15
+ * 1. `REDDB_BIN` env var.
16
+ * 2. `<package>/bin/red[.exe]`.
17
+ * 3. PATH-resolved bare `red[.exe]` — appropriate for the CLI which
18
+ * *targets* PATH.
19
+ */
20
+
21
+ import { existsSync } from 'node:fs'
22
+ import { fileURLToPath } from 'node:url'
23
+ import { dirname, resolve, join } from 'node:path'
24
+
25
+ import { resolveBin } from './internal/bin-resolver/index.js'
26
+
27
+ const HERE = dirname(fileURLToPath(import.meta.url))
28
+ const PACKAGE_ROOT = resolve(HERE, '..')
29
+
30
+ function defaultBinaryName() {
31
+ if (typeof process !== 'undefined' && process.platform === 'win32') {
32
+ return 'red.exe'
33
+ }
34
+ return 'red'
35
+ }
36
+
37
+ /** SDK runtime lookup. Throws actionable error when binary cannot be located. */
38
+ export function resolveSdkBinary() {
39
+ const legacy = process.env?.REDDB_BINARY_PATH
40
+ if (typeof legacy === 'string' && legacy !== '' && !process.env?.REDDB_BIN) {
41
+ return legacy
42
+ }
43
+ return resolveBin({
44
+ name: defaultBinaryName(),
45
+ packageRoot: PACKAGE_ROOT,
46
+ envVar: 'REDDB_BIN',
47
+ })
48
+ }
49
+
50
+ /** CLI runtime lookup. Allowed to fall back to PATH per ADR 0006. */
51
+ export function resolveCliBinary() {
52
+ const override = process.env?.REDDB_BIN
53
+ if (typeof override === 'string' && override !== '') {
54
+ return override
55
+ }
56
+ const local = join(PACKAGE_ROOT, 'bin', defaultBinaryName())
57
+ if (existsSync(local)) {
58
+ return local
59
+ }
60
+ return defaultBinaryName()
61
+ }
62
+
63
+ /** Used by postinstall.js to know where to drop the downloaded binary. */
64
+ export function packageBinaryDir() {
65
+ return join(PACKAGE_ROOT, 'bin')
66
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Cache client — exposes cache.{get,put,exists,invalidate,invalidatePrefix,
3
+ * invalidateTags,flushNamespace} via the underlying HTTP transport.
4
+ *
5
+ * NOTE: These methods require server-side HTTP endpoints under /cache/ns/*.
6
+ * flushNamespace routes to the existing POST /admin/blob_cache/flush_namespace.
7
+ * All others target endpoints planned for a future server release.
8
+ *
9
+ * Values are base64-encoded in transit so binary payloads survive JSON.
10
+ */
11
+
12
+ export class CacheClient {
13
+ /** @param {{ call: Function }} client */
14
+ constructor(client) {
15
+ this._client = client
16
+ }
17
+
18
+ /**
19
+ * Fetch a cached value. Returns a Uint8Array on hit, null on miss.
20
+ * @param {string} namespace
21
+ * @param {string} key
22
+ * @returns {Promise<Uint8Array | null>}
23
+ */
24
+ async get(namespace, key) {
25
+ const result = await this._client.call('cache.get', { namespace, key })
26
+ if (result == null || result.value == null) return null
27
+ return base64ToBytes(result.value)
28
+ }
29
+
30
+ /**
31
+ * Store a value in the cache.
32
+ * @param {string} namespace
33
+ * @param {string} key
34
+ * @param {Uint8Array | Buffer | string} value String is UTF-8 encoded.
35
+ * @param {object} [opts]
36
+ * @param {number} [opts.ttl_ms]
37
+ * @param {string[]} [opts.tags]
38
+ * @param {object} [opts.policy]
39
+ * @returns {Promise<void>}
40
+ */
41
+ async put(namespace, key, value, opts = {}) {
42
+ const encoded = bytesToBase64(value)
43
+ await this._client.call('cache.put', {
44
+ namespace,
45
+ key,
46
+ value: encoded,
47
+ ...opts,
48
+ })
49
+ }
50
+
51
+ /**
52
+ * Check whether a key is present.
53
+ * @param {string} namespace
54
+ * @param {string} key
55
+ * @returns {Promise<'present' | 'absent' | 'maybe'>}
56
+ */
57
+ async exists(namespace, key) {
58
+ const result = await this._client.call('cache.exists', { namespace, key })
59
+ return result?.status ?? 'maybe'
60
+ }
61
+
62
+ /**
63
+ * Remove a single entry.
64
+ * @param {string} namespace
65
+ * @param {string} key
66
+ * @returns {Promise<void>}
67
+ */
68
+ async invalidate(namespace, key) {
69
+ await this._client.call('cache.invalidate', { namespace, key })
70
+ }
71
+
72
+ /**
73
+ * Remove all entries whose key starts with `prefix`.
74
+ * @param {string} namespace
75
+ * @param {string} prefix
76
+ * @returns {Promise<number>} Number of entries removed.
77
+ */
78
+ async invalidatePrefix(namespace, prefix) {
79
+ const result = await this._client.call('cache.invalidate_prefix', { namespace, prefix })
80
+ return result?.removed ?? 0
81
+ }
82
+
83
+ /**
84
+ * Remove all entries tagged with any of the given tags.
85
+ * @param {string} namespace
86
+ * @param {string[]} tags
87
+ * @returns {Promise<number>} Number of entries removed.
88
+ */
89
+ async invalidateTags(namespace, tags) {
90
+ const result = await this._client.call('cache.invalidate_tags', { namespace, tags })
91
+ return result?.removed ?? 0
92
+ }
93
+
94
+ /**
95
+ * Remove all entries in a namespace.
96
+ * Routes to POST /admin/blob_cache/flush_namespace (live endpoint).
97
+ * @param {string} namespace
98
+ * @returns {Promise<void>}
99
+ */
100
+ async flushNamespace(namespace) {
101
+ await this._client.call('cache.flush_namespace', { namespace })
102
+ }
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Helpers
107
+ // ---------------------------------------------------------------------------
108
+
109
+ function bytesToBase64(value) {
110
+ if (typeof value === 'string') {
111
+ const bytes = new TextEncoder().encode(value)
112
+ return bufToBase64(bytes)
113
+ }
114
+ if (value instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(value))) {
115
+ return bufToBase64(value)
116
+ }
117
+ throw new TypeError('cache value must be a string, Uint8Array, or Buffer')
118
+ }
119
+
120
+ function bufToBase64(bytes) {
121
+ if (typeof Buffer !== 'undefined') {
122
+ return Buffer.from(bytes).toString('base64')
123
+ }
124
+ let bin = ''
125
+ for (const b of bytes) bin += String.fromCharCode(b)
126
+ return btoa(bin)
127
+ }
128
+
129
+ function base64ToBytes(b64) {
130
+ if (typeof Buffer !== 'undefined') {
131
+ return new Uint8Array(Buffer.from(b64, 'base64'))
132
+ }
133
+ const bin = atob(b64)
134
+ const out = new Uint8Array(bin.length)
135
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)
136
+ return out
137
+ }
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'node:child_process'
4
+ import { resolveCliBinary } from './binary.js'
5
+
6
+ const binary = resolveCliBinary()
7
+ const args = process.argv.slice(2)
8
+
9
+ const child = spawn(binary, args, {
10
+ cwd: process.cwd(),
11
+ env: process.env,
12
+ stdio: 'inherit',
13
+ })
14
+
15
+ child.on('error', (err) => {
16
+ process.stderr.write(`reddb-cli: ${err.message}\n`)
17
+ process.exit(1)
18
+ })
19
+
20
+ child.on('exit', (code, signal) => {
21
+ if (signal) {
22
+ process.exit(1)
23
+ }
24
+ process.exit(code ?? 0)
25
+ })
@@ -0,0 +1,66 @@
1
+ export class ConfigClient {
2
+ constructor(client, collection = 'red.config') {
3
+ this.client = client
4
+ this.collection = collection
5
+ }
6
+
7
+ put(key, value, options = {}) {
8
+ rejectVolatileOptions(options, 'config')
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: `PUT CONFIG ${keyedIdentifier(collection)} ${keyedIdentifier(key)} = ${configValueLiteral(value, options)}${tags}`,
15
+ })
16
+ }
17
+
18
+ get(key, options = {}) {
19
+ const collection = options.collection ?? this.collection
20
+ return this.client.call('query', {
21
+ sql: `GET CONFIG ${keyedIdentifier(collection)} ${keyedIdentifier(key)}`,
22
+ })
23
+ }
24
+
25
+ resolve(key, options = {}) {
26
+ const collection = options.collection ?? this.collection
27
+ return this.client.call('query', {
28
+ sql: `RESOLVE CONFIG ${keyedIdentifier(collection)} ${keyedIdentifier(key)}`,
29
+ })
30
+ }
31
+ }
32
+
33
+ function configValueLiteral(value, options) {
34
+ if (options.secretRef) {
35
+ const { collection, key } = options.secretRef
36
+ return `SECRET_REF(vault, ${keyedIdentifier(collection)}.${keyedIdentifier(key)})`
37
+ }
38
+ return keyedValueLiteral(value)
39
+ }
40
+
41
+ function rejectVolatileOptions(options, domain) {
42
+ for (const field of ['ttl', 'ttlMs', 'ttl_ms', 'expireMs', 'expire_ms', 'expiresAt']) {
43
+ if (options[field] != null) {
44
+ throw new TypeError(`${domain} does not support TTL or expiration options`)
45
+ }
46
+ }
47
+ }
48
+
49
+ function keyedIdentifier(value) {
50
+ const out = String(value)
51
+ if (!/^[A-Za-z0-9_.]+$/.test(out)) {
52
+ throw new TypeError('keyed collection and key names must use letters, numbers, underscores, or dots')
53
+ }
54
+ return out
55
+ }
56
+
57
+ function keyedValueLiteral(value) {
58
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
59
+ if (value == null) return 'NULL'
60
+ if (Array.isArray(value) || typeof value === 'object') return JSON.stringify(value)
61
+ return keyedStringLiteral(value)
62
+ }
63
+
64
+ function keyedStringLiteral(value) {
65
+ return `'${String(value).replace(/'/g, "''")}'`
66
+ }
@@ -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
+ }