@openbat/cli 0.1.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 +292 -0
- package/bin/openbat +3 -0
- package/dist/api-client.d.mts +41 -0
- package/dist/api-client.d.ts +41 -0
- package/dist/api-client.js +175 -0
- package/dist/api-client.mjs +6 -0
- package/dist/chunk-CRJZM45P.mjs +152 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1221 -0
- package/dist/index.mjs +1051 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# @openbat/cli
|
|
2
|
+
|
|
3
|
+
Command-line tool for managing OpenBat chatbots end-to-end — read
|
|
4
|
+
analytics + conversations, manage settings + keys + webhooks + workflows
|
|
5
|
+
+ reports, run experiments, and help install the SDK in a target app.
|
|
6
|
+
|
|
7
|
+
> **Companion docs**: [`@openbat/mcp`](../mcp/README.md) (same surface
|
|
8
|
+
> over MCP), [`lib/openbat-tools/`](../../lib/openbat-tools/README.md)
|
|
9
|
+
> (tool registry that powers both), and the
|
|
10
|
+
> [A-Z test guide](../../docs/agent-surface-testing.md).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm i -g @openbat/cli
|
|
16
|
+
# or run on demand without installing:
|
|
17
|
+
npx @openbat/cli --help
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Authentication — four key kinds
|
|
23
|
+
|
|
24
|
+
The CLI accepts any read-capable OpenBat key. Pick the smallest scope
|
|
25
|
+
that works:
|
|
26
|
+
|
|
27
|
+
| Prefix | Kind | Scope | What you can do |
|
|
28
|
+
|---|---|---|---|
|
|
29
|
+
| `ob_read_*` | read | one chatbot, **read-only** | Listing, analytics, conversations, export |
|
|
30
|
+
| `ob_admin_*` | admin | one chatbot, **read+write** | All of read, plus webhooks / workflows / reports / settings / mint read keys |
|
|
31
|
+
| `ob_pat_*` | PAT | one user across multiple chatbots and orgs | All of admin, plus `chatbots create/delete`, `org`, mint admin keys, multi-chatbot inventory |
|
|
32
|
+
| `ob_live_*` | ingest | (rejected by the CLI) | SDK-only — capture endpoint. Never use here. |
|
|
33
|
+
|
|
34
|
+
PATs also carry a sub-scope on the row (`read` or `admin`). A read-scope
|
|
35
|
+
PAT can list across all your chatbots but mutate nothing.
|
|
36
|
+
|
|
37
|
+
Generate keys in the dashboard:
|
|
38
|
+
- **Read key** → `Settings → API Keys → Generate Read key`
|
|
39
|
+
- **Admin key** → `Settings → API Keys → Generate Admin key`
|
|
40
|
+
- **PAT** → `Settings → Personal Access Tokens`
|
|
41
|
+
|
|
42
|
+
Each plaintext is shown exactly once.
|
|
43
|
+
|
|
44
|
+
### Save the key
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Recommended — stdin keeps the plaintext out of shell history:
|
|
48
|
+
echo "ob_pat_..." | openbat config set-key --from-stdin
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Stored at `~/.openbatrc` with mode `0600`. The CLI refuses to load if
|
|
52
|
+
the perms are looser than that.
|
|
53
|
+
|
|
54
|
+
**Auth resolution order** (each falls through to the next):
|
|
55
|
+
|
|
56
|
+
1. `--api-key <key>` flag — convenient but leaks into shell history;
|
|
57
|
+
discouraged.
|
|
58
|
+
2. `$OPENBAT_API_KEY` env var — best for CI.
|
|
59
|
+
3. `~/.openbatrc` — persistent local default.
|
|
60
|
+
|
|
61
|
+
If you accidentally try to set an ingest key (`ob_live_*`), the CLI
|
|
62
|
+
rejects it with a helpful error.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Command tree
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
openbat
|
|
70
|
+
├── config
|
|
71
|
+
│ ├── set-key Store / replace the key in ~/.openbatrc
|
|
72
|
+
│ ├── set-url <baseUrl> Override the API base URL
|
|
73
|
+
│ └── show Print the resolved config (key prefix only)
|
|
74
|
+
│
|
|
75
|
+
├── auth [any kind]
|
|
76
|
+
│ ├── whoami Show kind, chatbots in scope, orgs (PAT only)
|
|
77
|
+
│ └── audit-log [--days] Recent api_audit_log entries (stub today)
|
|
78
|
+
│
|
|
79
|
+
├── org [pat]
|
|
80
|
+
│ ├── list List user's orgs
|
|
81
|
+
│ ├── show Active org + members
|
|
82
|
+
│ ├── rename --id ORG --name "..." Owner only
|
|
83
|
+
│ ├── members list --id ORG
|
|
84
|
+
│ ├── members invite --id ORG --email --role member|admin
|
|
85
|
+
│ ├── members set-role --id ORG --member M --role admin|member
|
|
86
|
+
│ ├── members remove --id ORG --member M
|
|
87
|
+
│ └── invitations list --id ORG
|
|
88
|
+
│
|
|
89
|
+
├── chatbots [any kind]
|
|
90
|
+
│ ├── list Every chatbot in scope
|
|
91
|
+
│ ├── create --name --website [--docs-url] [--mcp-url] [pat] mint chatbot + ingest key
|
|
92
|
+
│ └── delete <id> [admin/pat] cascade delete
|
|
93
|
+
│
|
|
94
|
+
├── chatbot (legacy alias)
|
|
95
|
+
│ └── info [any kind] current chatbot row
|
|
96
|
+
│
|
|
97
|
+
├── conversations [any kind]
|
|
98
|
+
│ ├── list [--days N] [--from ISO] [--to ISO] [--limit N]
|
|
99
|
+
│ └── show <id>
|
|
100
|
+
│
|
|
101
|
+
├── users [any kind]
|
|
102
|
+
│ └── list --chatbot ID [--days] [--search] External users + health
|
|
103
|
+
│
|
|
104
|
+
├── settings [admin or pat]
|
|
105
|
+
│ ├── update --chatbot ID [--description] [--website-url] [--language]
|
|
106
|
+
│ └── keys
|
|
107
|
+
│ ├── list-admin --chatbot ID
|
|
108
|
+
│ ├── rotate-ingest --chatbot ID New ob_live_* (shown once)
|
|
109
|
+
│ ├── generate-read --chatbot ID New ob_read_* (shown once)
|
|
110
|
+
│ ├── generate-admin --chatbot ID --name N [--expires-in-days] [pat] new ob_admin_*
|
|
111
|
+
│ └── revoke-admin --chatbot ID --key KEYID [pat] revoke
|
|
112
|
+
│
|
|
113
|
+
├── webhooks [admin or pat]
|
|
114
|
+
│ ├── list --chatbot ID
|
|
115
|
+
│ ├── create --chatbot ID --name --url --type discord|slack|custom Returns signing secret (once)
|
|
116
|
+
│ └── delete --chatbot ID --webhook WHID
|
|
117
|
+
│
|
|
118
|
+
├── workflows [admin or pat]
|
|
119
|
+
│ ├── list --chatbot ID
|
|
120
|
+
│ └── create --chatbot ID --name --template T --trigger-value V --webhook WHID [--message TPL]
|
|
121
|
+
│ Templates: flag-to-webhook | outcome-to-webhook | sentiment-drop-to-webhook
|
|
122
|
+
│
|
|
123
|
+
├── reports [admin or pat]
|
|
124
|
+
│ ├── list --chatbot ID
|
|
125
|
+
│ └── create --chatbot ID [--name "..."] Returns org-private dashboard URL
|
|
126
|
+
│
|
|
127
|
+
├── analysis [admin or pat]
|
|
128
|
+
│ ├── list --chatbot ID [--type] [--pending]
|
|
129
|
+
│ └── add --chatbot ID --type intent|flag|assistant_outcome|assistant_issue
|
|
130
|
+
│ --name SLUG --display-name "..." --description "..."
|
|
131
|
+
│
|
|
132
|
+
├── analytics [any kind]
|
|
133
|
+
│ ├── overview
|
|
134
|
+
│ └── sentiment [--days N]
|
|
135
|
+
│
|
|
136
|
+
├── export --format json|csv [--out FILE] [any kind] streaming export
|
|
137
|
+
│
|
|
138
|
+
└── sdk [any kind]
|
|
139
|
+
├── install-instructions [--framework next|node|vercel-ai-sdk] [--chatbot ID]
|
|
140
|
+
└── verify --chatbot ID [--timeout N] Polls until first event
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Every command supports `--json` for raw output (default for non-TTY
|
|
144
|
+
stdout, so piping into `jq` always works).
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Write commands — what to expect
|
|
149
|
+
|
|
150
|
+
Mutating commands follow a consistent output convention so scripts
|
|
151
|
+
and humans can both consume them cleanly:
|
|
152
|
+
|
|
153
|
+
- **Plaintext secrets go to stderr**, inside a "shown ONCE" banner:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
────────────────────────────────────────────────────────────
|
|
157
|
+
Webhook signing secret (shown ONCE — store this now)
|
|
158
|
+
|
|
159
|
+
whsec_abcdef…
|
|
160
|
+
────────────────────────────────────────────────────────────
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- **Structured response goes to stdout** without the plaintext, so
|
|
164
|
+
`... | jq` keeps working:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
openbat webhooks create --chatbot $CB --name foo --url ... --type slack > webhook.json
|
|
168
|
+
jq .id webhook.json
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- **Errors go to stderr with a non-zero exit code.** API key plaintext
|
|
172
|
+
is automatically redacted from any error message (`ob_admin_<16 chars>…<hidden>`).
|
|
173
|
+
|
|
174
|
+
- **HTTP 401 vs 403**: a 401 means "key invalid / expired / wrong kind
|
|
175
|
+
for this endpoint" — usually fixed by `openbat auth whoami` + a fresh
|
|
176
|
+
key. A 403 means "key valid but lacks permission" — e.g. a read-scope
|
|
177
|
+
PAT trying to mutate, or a member trying to do an owner-only action.
|
|
178
|
+
|
|
179
|
+
- **HTTP 429** includes a `Retry-After` header. The CLI surfaces it in
|
|
180
|
+
the error message.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Quickstart — chatbot zero to first event
|
|
185
|
+
|
|
186
|
+
End-to-end in under 5 minutes. The full A-Z (including SDK install in a
|
|
187
|
+
real Next.js app) lives in
|
|
188
|
+
[`docs/agent-surface-testing.md`](../../docs/agent-surface-testing.md).
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# 0. Configure a PAT (you mint this in the dashboard).
|
|
192
|
+
echo "ob_pat_..." | openbat config set-key --from-stdin
|
|
193
|
+
|
|
194
|
+
# 1. Verify scope.
|
|
195
|
+
openbat auth whoami
|
|
196
|
+
|
|
197
|
+
# 2. Create a chatbot.
|
|
198
|
+
openbat chatbots create --name "My Bot" --website https://example.com
|
|
199
|
+
# stderr: Ingest API key (shown ONCE)
|
|
200
|
+
# stdout: { chatbot: { id, ... }, dashboardUrl }
|
|
201
|
+
CB=<chatbot id from stdout>
|
|
202
|
+
|
|
203
|
+
# 3. Mint an admin key so we can manage it without the PAT.
|
|
204
|
+
openbat settings keys generate-admin --chatbot $CB --name "dev" --expires-in-days 30
|
|
205
|
+
# stderr: ob_admin_... (shown ONCE)
|
|
206
|
+
|
|
207
|
+
# 4. Add a webhook + a workflow that fires it on a flag.
|
|
208
|
+
openbat webhooks create --chatbot $CB --name "ops" \
|
|
209
|
+
--url https://hooks.slack.com/services/T.../B.../X --type slack
|
|
210
|
+
# stdout: { id: WH_ID, ... }
|
|
211
|
+
|
|
212
|
+
openbat analysis add --chatbot $CB --type flag --name billing_issue \
|
|
213
|
+
--display-name "Billing Issue" --description "Customer raises a billing concern"
|
|
214
|
+
|
|
215
|
+
openbat workflows create --chatbot $CB \
|
|
216
|
+
--name "billing → slack" \
|
|
217
|
+
--template flag-to-webhook \
|
|
218
|
+
--trigger-value billing_issue \
|
|
219
|
+
--webhook $WH_ID
|
|
220
|
+
|
|
221
|
+
# 5. Help an agent install the SDK in a target app.
|
|
222
|
+
openbat sdk install-instructions --framework next --chatbot $CB
|
|
223
|
+
# Markdown to stdout — agent (or you) follows the steps.
|
|
224
|
+
|
|
225
|
+
# 6. After SDK is wired up + a real chat sent, verify ingestion:
|
|
226
|
+
openbat sdk verify --chatbot $CB --timeout 60
|
|
227
|
+
# Exits 0 on first event, 2 on timeout.
|
|
228
|
+
|
|
229
|
+
# 7. Read your data.
|
|
230
|
+
openbat conversations list --days 7
|
|
231
|
+
openbat analytics overview
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Override the API base URL
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
openbat config set-url https://staging.openbat.dev
|
|
240
|
+
# or per-invocation:
|
|
241
|
+
openbat --base-url http://localhost:3000 auth whoami
|
|
242
|
+
# or via env:
|
|
243
|
+
OPENBAT_BASE_URL=http://localhost:3000 openbat auth whoami
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The CLI **refuses non-HTTPS base URLs** unless they point at `localhost`
|
|
247
|
+
or `127.0.0.1`. There is no `--insecure` escape hatch.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Security properties
|
|
252
|
+
|
|
253
|
+
- API key plaintext never lands in any error message — auto-redacted to
|
|
254
|
+
`ob_<kind>_<first 16>…<hidden>`.
|
|
255
|
+
- HTTPS-only base URL (localhost exception only).
|
|
256
|
+
- `~/.openbatrc` enforced at `mode 0600`; the loader refuses looser perms.
|
|
257
|
+
- Mint commands print plaintext to stderr only, with a "shown ONCE"
|
|
258
|
+
banner. Pipe stderr to `/dev/null` if you don't want it on screen
|
|
259
|
+
(you'll lose the secret).
|
|
260
|
+
- Every authenticated call lands in `api_audit_log` server-side
|
|
261
|
+
(migration 036) — operators can answer "who did what when" via
|
|
262
|
+
Supabase Studio.
|
|
263
|
+
|
|
264
|
+
## Rate limits
|
|
265
|
+
|
|
266
|
+
Per-tool buckets keyed by credential. The strict ones to know about:
|
|
267
|
+
|
|
268
|
+
| Operation | Limit |
|
|
269
|
+
|---|---|
|
|
270
|
+
| Create chatbot | 5 per hour per PAT |
|
|
271
|
+
| Mint or rotate any key | 10 per hour per credential |
|
|
272
|
+
| Create backtest | 10 per hour per PAT |
|
|
273
|
+
| Invite org member | 20 per hour per PAT |
|
|
274
|
+
| Chat with an AI report | 30 per minute per credential |
|
|
275
|
+
| Generic write | 60 per minute per credential |
|
|
276
|
+
| Generic read | 600 per minute per credential |
|
|
277
|
+
| Export | 30 per hour per credential |
|
|
278
|
+
|
|
279
|
+
429 responses include `Retry-After` (seconds).
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## See also
|
|
284
|
+
|
|
285
|
+
- [`@openbat/mcp`](../mcp/README.md) — the same surface to Claude /
|
|
286
|
+
Cursor / any MCP client.
|
|
287
|
+
- [`@openbat/sdk`](../sdk/README.md) — capture conversations from your
|
|
288
|
+
app (uses the ingest key only).
|
|
289
|
+
- [`lib/openbat-tools/`](../../lib/openbat-tools/README.md) — the
|
|
290
|
+
registry that powers every CLI command + MCP tool + v1 route.
|
|
291
|
+
- [A-Z test guide](../../docs/agent-surface-testing.md) — clone the
|
|
292
|
+
repo and exercise every key kind, CLI, MCP, SDK in ~30 minutes.
|
package/bin/openbat
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal fetch wrapper used by both the CLI and the MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Two design properties worth preserving:
|
|
5
|
+
*
|
|
6
|
+
* • **HTTPS-only.** The constructor refuses non-HTTPS base URLs unless
|
|
7
|
+
* they point at localhost. There is no `--insecure` escape hatch —
|
|
8
|
+
* if you need to test against a self-signed cert, fix the cert.
|
|
9
|
+
*
|
|
10
|
+
* • **Key redaction.** The client never emits the plaintext API key in
|
|
11
|
+
* any error message. If a request fails, only the response status
|
|
12
|
+
* and the server's generic error string are exposed.
|
|
13
|
+
*/
|
|
14
|
+
type ApiClientOptions = {
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
apiKey: string;
|
|
17
|
+
};
|
|
18
|
+
declare class ApiClient {
|
|
19
|
+
#private;
|
|
20
|
+
readonly baseUrl: string;
|
|
21
|
+
constructor(opts: ApiClientOptions);
|
|
22
|
+
/**
|
|
23
|
+
* Issue a GET against `path` (must begin with `/`). Returns the parsed
|
|
24
|
+
* JSON on 200. Throws an Error whose message is safe to print (key
|
|
25
|
+
* redacted, status included).
|
|
26
|
+
*/
|
|
27
|
+
get<T = unknown>(path: string): Promise<T>;
|
|
28
|
+
/** Issue a POST with a JSON body. */
|
|
29
|
+
post<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
30
|
+
/** Issue a PATCH with a JSON body. */
|
|
31
|
+
patch<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
32
|
+
/** Issue a DELETE. Body is rarely used; we still allow it. */
|
|
33
|
+
delete<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
34
|
+
/** Pass-through for streaming endpoints (export). Returns the raw body. */
|
|
35
|
+
getRaw(path: string): Promise<{
|
|
36
|
+
body: ReadableStream<Uint8Array>;
|
|
37
|
+
contentType: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { ApiClient, type ApiClientOptions };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal fetch wrapper used by both the CLI and the MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Two design properties worth preserving:
|
|
5
|
+
*
|
|
6
|
+
* • **HTTPS-only.** The constructor refuses non-HTTPS base URLs unless
|
|
7
|
+
* they point at localhost. There is no `--insecure` escape hatch —
|
|
8
|
+
* if you need to test against a self-signed cert, fix the cert.
|
|
9
|
+
*
|
|
10
|
+
* • **Key redaction.** The client never emits the plaintext API key in
|
|
11
|
+
* any error message. If a request fails, only the response status
|
|
12
|
+
* and the server's generic error string are exposed.
|
|
13
|
+
*/
|
|
14
|
+
type ApiClientOptions = {
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
apiKey: string;
|
|
17
|
+
};
|
|
18
|
+
declare class ApiClient {
|
|
19
|
+
#private;
|
|
20
|
+
readonly baseUrl: string;
|
|
21
|
+
constructor(opts: ApiClientOptions);
|
|
22
|
+
/**
|
|
23
|
+
* Issue a GET against `path` (must begin with `/`). Returns the parsed
|
|
24
|
+
* JSON on 200. Throws an Error whose message is safe to print (key
|
|
25
|
+
* redacted, status included).
|
|
26
|
+
*/
|
|
27
|
+
get<T = unknown>(path: string): Promise<T>;
|
|
28
|
+
/** Issue a POST with a JSON body. */
|
|
29
|
+
post<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
30
|
+
/** Issue a PATCH with a JSON body. */
|
|
31
|
+
patch<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
32
|
+
/** Issue a DELETE. Body is rarely used; we still allow it. */
|
|
33
|
+
delete<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
34
|
+
/** Pass-through for streaming endpoints (export). Returns the raw body. */
|
|
35
|
+
getRaw(path: string): Promise<{
|
|
36
|
+
body: ReadableStream<Uint8Array>;
|
|
37
|
+
contentType: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { ApiClient, type ApiClientOptions };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __typeError = (msg) => {
|
|
7
|
+
throw TypeError(msg);
|
|
8
|
+
};
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
23
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
24
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
25
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
26
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
27
|
+
|
|
28
|
+
// src/api-client.ts
|
|
29
|
+
var api_client_exports = {};
|
|
30
|
+
__export(api_client_exports, {
|
|
31
|
+
ApiClient: () => ApiClient
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(api_client_exports);
|
|
34
|
+
var import_node_url = require("url");
|
|
35
|
+
var KEY_REGEX = /ob_(?:live|read|admin|pat)_[0-9a-f]{32}/g;
|
|
36
|
+
function redact(s) {
|
|
37
|
+
return s.replace(KEY_REGEX, (k) => `${k.slice(0, 16)}\u2026<hidden>`);
|
|
38
|
+
}
|
|
39
|
+
function assertHttpsOrLocalhost(baseUrl) {
|
|
40
|
+
let url;
|
|
41
|
+
try {
|
|
42
|
+
url = new import_node_url.URL(baseUrl);
|
|
43
|
+
} catch {
|
|
44
|
+
throw new Error(`Invalid base URL: ${redact(baseUrl)}`);
|
|
45
|
+
}
|
|
46
|
+
if (url.protocol === "https:") return;
|
|
47
|
+
if (url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1")) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
throw new Error(
|
|
51
|
+
"Refusing to use a non-HTTPS base URL. localhost / 127.0.0.1 are allowed for dev."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
var _apiKey, _ApiClient_instances, mutate_fn;
|
|
55
|
+
var ApiClient = class {
|
|
56
|
+
constructor(opts) {
|
|
57
|
+
__privateAdd(this, _ApiClient_instances);
|
|
58
|
+
__privateAdd(this, _apiKey);
|
|
59
|
+
assertHttpsOrLocalhost(opts.baseUrl);
|
|
60
|
+
this.baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
61
|
+
__privateSet(this, _apiKey, opts.apiKey);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Issue a GET against `path` (must begin with `/`). Returns the parsed
|
|
65
|
+
* JSON on 200. Throws an Error whose message is safe to print (key
|
|
66
|
+
* redacted, status included).
|
|
67
|
+
*/
|
|
68
|
+
async get(path) {
|
|
69
|
+
const url = `${this.baseUrl}${path}`;
|
|
70
|
+
let res;
|
|
71
|
+
try {
|
|
72
|
+
res = await fetch(url, {
|
|
73
|
+
method: "GET",
|
|
74
|
+
headers: {
|
|
75
|
+
"x-openbat-key": __privateGet(this, _apiKey),
|
|
76
|
+
accept: "application/json"
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
81
|
+
throw new Error(`Request to ${redact(url)} failed: ${redact(msg)}`);
|
|
82
|
+
}
|
|
83
|
+
return parseResponse(res, url);
|
|
84
|
+
}
|
|
85
|
+
/** Issue a POST with a JSON body. */
|
|
86
|
+
async post(path, body) {
|
|
87
|
+
return __privateMethod(this, _ApiClient_instances, mutate_fn).call(this, "POST", path, body);
|
|
88
|
+
}
|
|
89
|
+
/** Issue a PATCH with a JSON body. */
|
|
90
|
+
async patch(path, body) {
|
|
91
|
+
return __privateMethod(this, _ApiClient_instances, mutate_fn).call(this, "PATCH", path, body);
|
|
92
|
+
}
|
|
93
|
+
/** Issue a DELETE. Body is rarely used; we still allow it. */
|
|
94
|
+
async delete(path, body) {
|
|
95
|
+
return __privateMethod(this, _ApiClient_instances, mutate_fn).call(this, "DELETE", path, body);
|
|
96
|
+
}
|
|
97
|
+
/** Pass-through for streaming endpoints (export). Returns the raw body. */
|
|
98
|
+
async getRaw(path) {
|
|
99
|
+
const url = `${this.baseUrl}${path}`;
|
|
100
|
+
const res = await fetch(url, {
|
|
101
|
+
method: "GET",
|
|
102
|
+
headers: {
|
|
103
|
+
"x-openbat-key": __privateGet(this, _apiKey)
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
if (!res.ok || !res.body) {
|
|
107
|
+
const errText = await res.text().catch(() => "");
|
|
108
|
+
throw new Error(
|
|
109
|
+
`GET ${redact(url)} \u2192 ${res.status} ${res.statusText}${errText ? `: ${redact(errText.slice(0, 200))}` : ""}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
body: res.body,
|
|
114
|
+
contentType: res.headers.get("content-type") ?? "application/octet-stream"
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
_apiKey = new WeakMap();
|
|
119
|
+
_ApiClient_instances = new WeakSet();
|
|
120
|
+
mutate_fn = async function(method, path, body) {
|
|
121
|
+
const url = `${this.baseUrl}${path}`;
|
|
122
|
+
let res;
|
|
123
|
+
try {
|
|
124
|
+
res = await fetch(url, {
|
|
125
|
+
method,
|
|
126
|
+
headers: {
|
|
127
|
+
"x-openbat-key": __privateGet(this, _apiKey),
|
|
128
|
+
"content-type": "application/json",
|
|
129
|
+
accept: "application/json"
|
|
130
|
+
},
|
|
131
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
132
|
+
});
|
|
133
|
+
} catch (err) {
|
|
134
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
135
|
+
throw new Error(`Request to ${redact(url)} failed: ${redact(msg)}`);
|
|
136
|
+
}
|
|
137
|
+
return parseResponse(res, url);
|
|
138
|
+
};
|
|
139
|
+
async function parseResponse(res, url) {
|
|
140
|
+
if (res.status === 401) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
"Unauthorized. The API key was rejected (invalid, wrong kind for this endpoint, expired, or revoked)."
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (res.status === 403) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
"Forbidden. The credential is valid but lacks permission for this operation (e.g. read-scope PAT can't mutate)."
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (res.status === 429) {
|
|
151
|
+
const retry = res.headers.get("retry-after");
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Rate limited.${retry ? ` Retry after ${retry}s.` : ""}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
let errBody = null;
|
|
158
|
+
try {
|
|
159
|
+
errBody = await res.json();
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
throw new Error(
|
|
163
|
+
`GET ${redact(url)} \u2192 ${res.status} ${res.statusText}${errBody?.error ? `: ${redact(errBody.error)}` : ""}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
return await res.json();
|
|
168
|
+
} catch {
|
|
169
|
+
throw new Error(`Response from ${redact(url)} was not valid JSON`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
173
|
+
0 && (module.exports = {
|
|
174
|
+
ApiClient
|
|
175
|
+
});
|