@tonbo/cli 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -7
- package/dist/bin/tonbo.js +1064 -524
- package/package.json +7 -7
package/dist/bin/tonbo.js
CHANGED
|
@@ -4,8 +4,150 @@
|
|
|
4
4
|
import { Command, InvalidArgumentError, Option } from "commander";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
|
|
7
|
-
// src/
|
|
7
|
+
// ../../packages/tonbo-cli-core/dist/src/auth.js
|
|
8
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
9
|
+
import { createServer } from "node:http";
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
11
|
+
import os2 from "node:os";
|
|
12
|
+
import { promisify } from "node:util";
|
|
13
|
+
|
|
14
|
+
// ../../packages/tonbo-cli-core/dist/src/callback-page.js
|
|
15
|
+
var WORDMARK = [
|
|
16
|
+
"\u2588\u2588\u2588\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
|
|
17
|
+
"\u2588\u2588\u2584 \u2584\u2588\u2588\u2580\u2584 \u2588\u2588 \u2584\u2580\u2588\u2588 \u2584 \u2588\u2588 \u2584\u2580\u2588",
|
|
18
|
+
"\u2588\u2588\u2588\u2584\u2584\u2588\u2588\u2584\u2584\u2588\u2588\u2588\u2584\u2588\u2584\u2588\u2588\u2584\u2584\u2588\u2588\u2588\u2588\u2584\u2584\u2588"
|
|
19
|
+
].join("\n");
|
|
20
|
+
var OUTCOMES = {
|
|
21
|
+
complete: {
|
|
22
|
+
status: 200,
|
|
23
|
+
title: "You are signed in.",
|
|
24
|
+
body: "Return to your terminal. This tab can be closed."
|
|
25
|
+
},
|
|
26
|
+
denied: {
|
|
27
|
+
status: 400,
|
|
28
|
+
title: "Login was not completed.",
|
|
29
|
+
body: "Nothing was authorized. Return to your terminal to try again."
|
|
30
|
+
},
|
|
31
|
+
invalid: {
|
|
32
|
+
// The state did not match, the code was missing, or the path was wrong.
|
|
33
|
+
// Say that it could not be verified rather than which check failed: the
|
|
34
|
+
// person who can read this page is not necessarily the one who started
|
|
35
|
+
// the login.
|
|
36
|
+
status: 400,
|
|
37
|
+
title: "This callback could not be verified.",
|
|
38
|
+
body: "Return to your terminal and start the login again."
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
function renderCallbackPage(outcome) {
|
|
42
|
+
const { title, body } = OUTCOMES[outcome];
|
|
43
|
+
return `<!doctype html>
|
|
44
|
+
<html lang="en">
|
|
45
|
+
<head>
|
|
46
|
+
<meta charset="utf-8">
|
|
47
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
48
|
+
<meta name="robots" content="noindex">
|
|
49
|
+
<title>Tonbo CLI</title>
|
|
50
|
+
<style>
|
|
51
|
+
:root {
|
|
52
|
+
--paper: #f3f2ee;
|
|
53
|
+
--ink: #1d1b17;
|
|
54
|
+
--rule: #c9c5ba;
|
|
55
|
+
--quiet: #5c564a;
|
|
56
|
+
--signal: #d64a03;
|
|
57
|
+
}
|
|
58
|
+
@media (prefers-color-scheme: dark) {
|
|
59
|
+
:root {
|
|
60
|
+
--paper: #1d1b17;
|
|
61
|
+
--ink: #f3f2ee;
|
|
62
|
+
--rule: #3a362e;
|
|
63
|
+
--quiet: #a49e8f;
|
|
64
|
+
--signal: #f08b4b;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
* { box-sizing: border-box; }
|
|
68
|
+
body {
|
|
69
|
+
margin: 0;
|
|
70
|
+
min-height: 100vh;
|
|
71
|
+
display: grid;
|
|
72
|
+
grid-template-rows: auto 1fr;
|
|
73
|
+
padding: 0 clamp(20px, 5vw, 72px);
|
|
74
|
+
background: var(--paper);
|
|
75
|
+
color: var(--ink);
|
|
76
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
77
|
+
-webkit-font-smoothing: antialiased;
|
|
78
|
+
}
|
|
79
|
+
header { padding: 28px 0; }
|
|
80
|
+
/* Same metrics as .auth-wordmark in the app: the glyphs are the ground
|
|
81
|
+
and the letters are the gaps, so the 8px/10px/scaleY(0.8) combination is
|
|
82
|
+
what makes the rows meet without seams. Iosevka is not available here,
|
|
83
|
+
so it falls back to the platform monospace. */
|
|
84
|
+
pre.wordmark {
|
|
85
|
+
margin: 0;
|
|
86
|
+
font-family: 'Iosevka', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
|
|
87
|
+
font-size: 8px;
|
|
88
|
+
font-weight: 400;
|
|
89
|
+
line-height: 10px;
|
|
90
|
+
letter-spacing: 0;
|
|
91
|
+
white-space: pre;
|
|
92
|
+
color: var(--signal);
|
|
93
|
+
transform: scaleY(0.8);
|
|
94
|
+
transform-origin: left top;
|
|
95
|
+
}
|
|
96
|
+
main {
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
justify-content: center;
|
|
100
|
+
max-width: 46ch;
|
|
101
|
+
padding-bottom: 12vh;
|
|
102
|
+
}
|
|
103
|
+
h1 {
|
|
104
|
+
margin: 0;
|
|
105
|
+
font-size: 26px;
|
|
106
|
+
font-weight: 300;
|
|
107
|
+
line-height: 1.16;
|
|
108
|
+
letter-spacing: -0.018em;
|
|
109
|
+
}
|
|
110
|
+
p {
|
|
111
|
+
margin: 14px 0 0;
|
|
112
|
+
font-size: 16px;
|
|
113
|
+
line-height: 1.55;
|
|
114
|
+
color: var(--quiet);
|
|
115
|
+
}
|
|
116
|
+
hr {
|
|
117
|
+
width: 40px;
|
|
118
|
+
margin: 28px 0 0;
|
|
119
|
+
border: 0;
|
|
120
|
+
border-top: 1px solid var(--rule);
|
|
121
|
+
}
|
|
122
|
+
</style>
|
|
123
|
+
</head>
|
|
124
|
+
<body>
|
|
125
|
+
<header><pre class="wordmark" aria-hidden="true">${WORDMARK}</pre></header>
|
|
126
|
+
<main>
|
|
127
|
+
<h1>${title}</h1>
|
|
128
|
+
<p>${body}</p>
|
|
129
|
+
<hr>
|
|
130
|
+
</main>
|
|
131
|
+
</body>
|
|
132
|
+
</html>
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
function callbackResponse(outcome) {
|
|
136
|
+
return {
|
|
137
|
+
status: OUTCOMES[outcome].status,
|
|
138
|
+
headers: {
|
|
139
|
+
"content-type": "text/html; charset=utf-8",
|
|
140
|
+
"cache-control": "no-store"
|
|
141
|
+
},
|
|
142
|
+
body: renderCallbackPage(outcome)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ../../packages/tonbo-cli-core/dist/src/http.js
|
|
8
147
|
var HttpError = class extends Error {
|
|
148
|
+
status;
|
|
149
|
+
body;
|
|
150
|
+
requestId;
|
|
9
151
|
constructor(message, status, body, requestId) {
|
|
10
152
|
super(message);
|
|
11
153
|
this.status = status;
|
|
@@ -22,26 +164,26 @@ var HttpError = class extends Error {
|
|
|
22
164
|
async function requestJson(fetcher, url, init = {}) {
|
|
23
165
|
const response = await fetcher(url, init);
|
|
24
166
|
const body = await response.json().catch(() => null);
|
|
25
|
-
if (response.ok)
|
|
167
|
+
if (response.ok)
|
|
168
|
+
return body;
|
|
26
169
|
const record = body && typeof body === "object" ? body : {};
|
|
27
|
-
const reason = [record.detail, record.title, record.error].find(
|
|
28
|
-
(value) => typeof value === "string" && value.length > 0
|
|
29
|
-
);
|
|
170
|
+
const reason = [record.detail, record.title, record.error].find((value) => typeof value === "string" && value.length > 0);
|
|
30
171
|
const requestId = response.headers.get("x-request-id") ?? (typeof record.request_id === "string" ? record.request_id : null);
|
|
31
172
|
const issues = Array.isArray(record.errors) ? record.errors.filter((issue) => typeof issue?.field === "string" && typeof issue.code === "string").map((issue) => `${issue.field}: ${issue.code}`) : [];
|
|
32
173
|
const message = [
|
|
33
174
|
reason ?? `Request failed with HTTP ${response.status}.`,
|
|
34
175
|
...issues.length ? [`(${issues.join(", ")})`] : []
|
|
35
176
|
].join(" ");
|
|
36
|
-
throw new HttpError(
|
|
37
|
-
requestId ? `${message} (request ${requestId})` : message,
|
|
38
|
-
response.status,
|
|
39
|
-
body,
|
|
40
|
-
requestId
|
|
41
|
-
);
|
|
177
|
+
throw new HttpError(requestId ? `${message} (request ${requestId})` : message, response.status, body, requestId);
|
|
42
178
|
}
|
|
43
179
|
|
|
44
|
-
// src/
|
|
180
|
+
// ../../packages/tonbo-cli-core/dist/src/credentials.js
|
|
181
|
+
import { randomUUID } from "node:crypto";
|
|
182
|
+
import { chmod, lstat, mkdir, open, readFile, rename, rm } from "node:fs/promises";
|
|
183
|
+
import os from "node:os";
|
|
184
|
+
import path from "node:path";
|
|
185
|
+
|
|
186
|
+
// ../../packages/tonbo-cli-core/dist/src/generated/contracts.js
|
|
45
187
|
var agentNameContract = {
|
|
46
188
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
47
189
|
"$id": "https://contracts.tonbo.dev/agents/agent-name-v1.json",
|
|
@@ -343,6 +485,70 @@ var managementOperations = {
|
|
|
343
485
|
],
|
|
344
486
|
"idempotencyKey": false,
|
|
345
487
|
"cli": "tonbo usage show"
|
|
488
|
+
},
|
|
489
|
+
"listDatabases": {
|
|
490
|
+
"method": "GET",
|
|
491
|
+
"path": "/databases",
|
|
492
|
+
"pathParams": [],
|
|
493
|
+
"query": [
|
|
494
|
+
"prefix",
|
|
495
|
+
"cursor",
|
|
496
|
+
"limit"
|
|
497
|
+
],
|
|
498
|
+
"idempotencyKey": false,
|
|
499
|
+
"cli": "tonbo database list"
|
|
500
|
+
},
|
|
501
|
+
"createDatabase": {
|
|
502
|
+
"method": "POST",
|
|
503
|
+
"path": "/databases",
|
|
504
|
+
"pathParams": [],
|
|
505
|
+
"query": [],
|
|
506
|
+
"idempotencyKey": true,
|
|
507
|
+
"cli": "tonbo database create"
|
|
508
|
+
},
|
|
509
|
+
"getDatabase": {
|
|
510
|
+
"method": "GET",
|
|
511
|
+
"path": "/databases/{name}",
|
|
512
|
+
"pathParams": [
|
|
513
|
+
"name"
|
|
514
|
+
],
|
|
515
|
+
"query": [],
|
|
516
|
+
"idempotencyKey": false,
|
|
517
|
+
"cli": "tonbo database show"
|
|
518
|
+
},
|
|
519
|
+
"updateDatabase": {
|
|
520
|
+
"method": "PATCH",
|
|
521
|
+
"path": "/databases/{name}",
|
|
522
|
+
"pathParams": [
|
|
523
|
+
"name"
|
|
524
|
+
],
|
|
525
|
+
"query": [],
|
|
526
|
+
"idempotencyKey": false,
|
|
527
|
+
"cli": "tonbo database update"
|
|
528
|
+
},
|
|
529
|
+
"deleteDatabase": {
|
|
530
|
+
"method": "DELETE",
|
|
531
|
+
"path": "/databases/{name}",
|
|
532
|
+
"pathParams": [
|
|
533
|
+
"name"
|
|
534
|
+
],
|
|
535
|
+
"query": [],
|
|
536
|
+
"idempotencyKey": false,
|
|
537
|
+
"cli": "tonbo database delete"
|
|
538
|
+
},
|
|
539
|
+
"listDatabaseUsage": {
|
|
540
|
+
"method": "GET",
|
|
541
|
+
"path": "/usage/databases",
|
|
542
|
+
"pathParams": [],
|
|
543
|
+
"query": [
|
|
544
|
+
"period",
|
|
545
|
+
"sort",
|
|
546
|
+
"prefix",
|
|
547
|
+
"cursor",
|
|
548
|
+
"limit"
|
|
549
|
+
],
|
|
550
|
+
"idempotencyKey": false,
|
|
551
|
+
"cli": "tonbo database usage"
|
|
346
552
|
}
|
|
347
553
|
};
|
|
348
554
|
var accountRoles = [
|
|
@@ -357,298 +563,305 @@ var apiKeyExpirations = [
|
|
|
357
563
|
"1_year",
|
|
358
564
|
"never"
|
|
359
565
|
];
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
566
|
+
var credentialsContract = {
|
|
567
|
+
"version": 1,
|
|
568
|
+
"description": "The login `tonbo login` (and `yield login`) saves: a personal key of one account. @tonbo/cli-core writes and reads it; other readers, such as @tonbo/sqlite-ursula/tonbo, follow `reader`.",
|
|
569
|
+
"path": {
|
|
570
|
+
"description": "`${XDG_CONFIG_HOME}/tonbo/credentials.json` when XDG_CONFIG_HOME is set and not empty, else `${HOME}/.config/tonbo/credentials.json`.",
|
|
571
|
+
"base_env": "XDG_CONFIG_HOME",
|
|
572
|
+
"fallback_base": [
|
|
573
|
+
"$HOME",
|
|
574
|
+
".config"
|
|
575
|
+
],
|
|
576
|
+
"relative": [
|
|
577
|
+
"tonbo",
|
|
578
|
+
"credentials.json"
|
|
579
|
+
]
|
|
580
|
+
},
|
|
581
|
+
"permissions": {
|
|
582
|
+
"description": "POSIX only; a reader on Windows skips the mode checks.",
|
|
583
|
+
"directory_mode": "0700",
|
|
584
|
+
"file_mode": "0600",
|
|
585
|
+
"writer": "The writer creates the directory 0700, writes a temporary file 0600 beside the target, syncs it and renames it over the target, so a crash never leaves a partial key. It refuses to replace or remove a symlink or anything that is not a regular file."
|
|
586
|
+
},
|
|
587
|
+
"key_sources": {
|
|
588
|
+
"description": "Where a client takes its key, first match wins. A library takes an explicit option first; the CLIs have none.",
|
|
589
|
+
"order": [
|
|
590
|
+
"option",
|
|
591
|
+
"env",
|
|
592
|
+
"file"
|
|
593
|
+
],
|
|
594
|
+
"env": "TONBO_API_KEY"
|
|
595
|
+
},
|
|
596
|
+
"reader": {
|
|
597
|
+
"missing": "No file is not logged in: ask the person to run `tonbo login`.",
|
|
598
|
+
"refuse_symlink": true,
|
|
599
|
+
"refuse_non_regular_file": true,
|
|
600
|
+
"refuse_mode_bits": "0077",
|
|
601
|
+
"refused": "A symlink, a non-regular file, or a file any group or other permission bit is set on is an error and is never read; do not fall back to another source.",
|
|
602
|
+
"unknown_version": "A `version` other than `current_version`, or content that does not match `schema`, is unusable: ask the person to run `tonbo login`. It is never migrated.",
|
|
603
|
+
"expired": "A login whose `expires_at` is at or before now is expired: ask the person to run `tonbo login`. `expires_at: null` never expires.",
|
|
604
|
+
"login_hint": "tonbo login"
|
|
605
|
+
},
|
|
606
|
+
"current_version": 1,
|
|
607
|
+
"schema": {
|
|
608
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
609
|
+
"$id": "https://tonbo.dev/contracts/cli/credentials-v1.json",
|
|
610
|
+
"title": "Tonbo CLI login",
|
|
611
|
+
"type": "object",
|
|
612
|
+
"required": [
|
|
613
|
+
"version",
|
|
614
|
+
"account_id",
|
|
615
|
+
"key_id",
|
|
616
|
+
"key",
|
|
617
|
+
"expires_at"
|
|
618
|
+
],
|
|
619
|
+
"additionalProperties": true,
|
|
620
|
+
"properties": {
|
|
621
|
+
"version": {
|
|
622
|
+
"const": 1
|
|
623
|
+
},
|
|
624
|
+
"account_id": {
|
|
625
|
+
"type": "string",
|
|
626
|
+
"minLength": 1,
|
|
627
|
+
"description": "The account the key belongs to (a UUID)."
|
|
628
|
+
},
|
|
629
|
+
"key_id": {
|
|
630
|
+
"type": "string",
|
|
631
|
+
"minLength": 1,
|
|
632
|
+
"description": "The key's id, its `client_id` at the exchange (a UUID)."
|
|
633
|
+
},
|
|
634
|
+
"key": {
|
|
635
|
+
"type": "string",
|
|
636
|
+
"pattern": "^tbo_",
|
|
637
|
+
"description": "The personal key's secret (contracts/iam/access-token-v1.json `personal_key`)."
|
|
638
|
+
},
|
|
639
|
+
"expires_at": {
|
|
640
|
+
"type": [
|
|
641
|
+
"string",
|
|
642
|
+
"null"
|
|
643
|
+
],
|
|
644
|
+
"format": "date-time",
|
|
645
|
+
"description": "When the key expires, ISO 8601; null for a key that never expires."
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
"examples": {
|
|
650
|
+
"usable": [
|
|
651
|
+
{
|
|
652
|
+
"version": 1,
|
|
653
|
+
"account_id": "10000000-0000-4000-8000-000000000001",
|
|
654
|
+
"key_id": "20000000-0000-4000-8000-000000000002",
|
|
655
|
+
"key": "tbo_secret",
|
|
656
|
+
"expires_at": "2027-04-24T00:00:00.000Z"
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
"version": 1,
|
|
660
|
+
"account_id": "10000000-0000-4000-8000-000000000001",
|
|
661
|
+
"key_id": "20000000-0000-4000-8000-000000000002",
|
|
662
|
+
"key": "tbo_secret",
|
|
663
|
+
"expires_at": null
|
|
664
|
+
}
|
|
665
|
+
],
|
|
666
|
+
"unusable": [
|
|
667
|
+
{
|
|
668
|
+
"access_token": "access",
|
|
669
|
+
"refresh_token": "refresh",
|
|
670
|
+
"token_type": "bearer"
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
"version": 2,
|
|
674
|
+
"account_id": "10000000-0000-4000-8000-000000000001",
|
|
675
|
+
"key_id": "20000000-0000-4000-8000-000000000002",
|
|
676
|
+
"key": "tbo_secret",
|
|
677
|
+
"expires_at": null
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
"version": 1,
|
|
681
|
+
"account_id": "10000000-0000-4000-8000-000000000001",
|
|
682
|
+
"key_id": "20000000-0000-4000-8000-000000000002",
|
|
683
|
+
"key": "not-a-personal-key",
|
|
684
|
+
"expires_at": null
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
"version": 1,
|
|
688
|
+
"account_id": "",
|
|
689
|
+
"key_id": "20000000-0000-4000-8000-000000000002",
|
|
690
|
+
"key": "tbo_secret",
|
|
691
|
+
"expires_at": null
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
"version": 1,
|
|
695
|
+
"account_id": "10000000-0000-4000-8000-000000000001",
|
|
696
|
+
"key_id": "20000000-0000-4000-8000-000000000002",
|
|
697
|
+
"key": "tbo_secret",
|
|
698
|
+
"expires_at": "not a date"
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
"version": 1,
|
|
702
|
+
"account_id": "10000000-0000-4000-8000-000000000001",
|
|
703
|
+
"key_id": "20000000-0000-4000-8000-000000000002",
|
|
704
|
+
"key": "tbo_secret"
|
|
705
|
+
}
|
|
706
|
+
]
|
|
371
707
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
// ../../packages/tonbo-cli-core/dist/src/credentials.js
|
|
711
|
+
var DIRECTORY_MODE = Number.parseInt(credentialsContract.permissions.directory_mode, 8);
|
|
712
|
+
var FILE_MODE = Number.parseInt(credentialsContract.permissions.file_mode, 8);
|
|
713
|
+
var REFUSED_MODE_BITS = Number.parseInt(credentialsContract.reader.refuse_mode_bits, 8);
|
|
714
|
+
function defaultCredentialsPath(env = process.env) {
|
|
715
|
+
const rule = credentialsContract.path;
|
|
716
|
+
const base = env[rule.base_env] || path.join(...rule.fallback_base.map((part) => part === "$HOME" ? os.homedir() : part));
|
|
717
|
+
return path.join(base, ...rule.relative);
|
|
718
|
+
}
|
|
719
|
+
function isExpiredLogin(login, now = Date.now()) {
|
|
720
|
+
return login.expires_at !== null && Date.parse(login.expires_at) <= now;
|
|
721
|
+
}
|
|
722
|
+
var UnusableLoginError = class extends Error {
|
|
723
|
+
};
|
|
724
|
+
var FileCredentialStore = class {
|
|
725
|
+
filename;
|
|
726
|
+
loginHint;
|
|
727
|
+
constructor(filename = defaultCredentialsPath(), loginHint = credentialsContract.reader.login_hint) {
|
|
728
|
+
this.filename = filename;
|
|
729
|
+
this.loginHint = loginHint;
|
|
375
730
|
}
|
|
376
|
-
async
|
|
377
|
-
|
|
378
|
-
const path2 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
379
|
-
const value = params[name];
|
|
380
|
-
if (value === void 0) throw new Error(`${operationId} needs the path parameter ${name}.`);
|
|
381
|
-
return encodeURIComponent(value);
|
|
382
|
-
});
|
|
383
|
-
const search = new URLSearchParams();
|
|
384
|
-
for (const [name, value] of Object.entries(query)) {
|
|
385
|
-
if (value === void 0) continue;
|
|
386
|
-
if (!operation.query.includes(name))
|
|
387
|
-
throw new Error(`${operationId} takes no query parameter ${name}.`);
|
|
388
|
-
search.set(name, String(value));
|
|
389
|
-
}
|
|
390
|
-
if (operation.idempotencyKey && !idempotencyKey)
|
|
391
|
-
throw new Error(`${operationId} needs an Idempotency-Key.`);
|
|
392
|
-
const headers = { authorization: `Bearer ${key}` };
|
|
393
|
-
if (body !== void 0) headers["content-type"] = "application/json";
|
|
394
|
-
if (idempotencyKey) headers["idempotency-key"] = idempotencyKey;
|
|
395
|
-
const queryString = search.toString();
|
|
396
|
-
const url = `${this.apiOrigin}${managementApiBasePath}${path2}${queryString ? `?${queryString}` : ""}`;
|
|
731
|
+
async load() {
|
|
732
|
+
let parsed;
|
|
397
733
|
try {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
headers,
|
|
401
|
-
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
402
|
-
});
|
|
734
|
+
await assertPrivateRegularFile(this.filename);
|
|
735
|
+
parsed = JSON.parse(await readFile(this.filename, "utf8"));
|
|
403
736
|
} catch (error) {
|
|
404
|
-
if (error
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
error.status,
|
|
408
|
-
error.body,
|
|
409
|
-
error.requestId
|
|
410
|
-
);
|
|
411
|
-
throw error;
|
|
737
|
+
if (error.code === "ENOENT")
|
|
738
|
+
return null;
|
|
739
|
+
throw new Error(`Could not read Tonbo credentials at ${this.filename}.`, { cause: error });
|
|
412
740
|
}
|
|
741
|
+
if (!isStoredLogin(parsed))
|
|
742
|
+
throw new UnusableLoginError(`The saved login at ${this.filename} is not usable by this CLI. Run \`${this.loginHint}\`.`);
|
|
743
|
+
return parsed;
|
|
413
744
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
* return it, and a blind retry of a live name answers 409. */
|
|
439
|
-
createApiKey(key, request) {
|
|
440
|
-
return this.call("createApiKey", key, { body: withoutUndefined(request) });
|
|
441
|
-
}
|
|
442
|
-
getApiKey(key, keyId) {
|
|
443
|
-
return this.call("getApiKey", key, { params: { key_id: keyId } });
|
|
444
|
-
}
|
|
445
|
-
revokeApiKey(key, keyId) {
|
|
446
|
-
return this.call("revokeApiKey", key, {
|
|
447
|
-
params: { key_id: keyId }
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
listBuckets(key, page) {
|
|
451
|
-
return this.page("listBuckets", key, page);
|
|
452
|
-
}
|
|
453
|
-
/** A replay with the same idempotency key returns the bucket the first
|
|
454
|
-
* request created. */
|
|
455
|
-
createBucket(key, request, idempotencyKey) {
|
|
456
|
-
return this.call("createBucket", key, {
|
|
457
|
-
body: withoutUndefined(request),
|
|
458
|
-
idempotencyKey
|
|
459
|
-
}).catch(unconfirmed("Bucket creation", idempotencyKey, "the same name and region"));
|
|
460
|
-
}
|
|
461
|
-
getBucket(key, name) {
|
|
462
|
-
return this.call("getBucket", key, { params: { name } });
|
|
463
|
-
}
|
|
464
|
-
updateBucket(key, name, change) {
|
|
465
|
-
return this.call("updateBucket", key, { params: { name }, body: change });
|
|
466
|
-
}
|
|
467
|
-
deleteBucket(key, name) {
|
|
468
|
-
return this.call("deleteBucket", key, { params: { name } });
|
|
745
|
+
async save(login) {
|
|
746
|
+
if (!isStoredLogin(login))
|
|
747
|
+
throw new Error("Refusing to store a malformed Tonbo login.");
|
|
748
|
+
const directory = path.dirname(this.filename);
|
|
749
|
+
await mkdir(directory, { mode: DIRECTORY_MODE, recursive: true });
|
|
750
|
+
await preparePrivateDirectory(directory);
|
|
751
|
+
await assertExistingDestinationIsSafe(this.filename);
|
|
752
|
+
const temporary = path.join(directory, `.${path.basename(this.filename)}.${process.pid}.${randomUUID()}.tmp`);
|
|
753
|
+
let handle = null;
|
|
754
|
+
try {
|
|
755
|
+
handle = await open(temporary, "wx", FILE_MODE);
|
|
756
|
+
await handle.writeFile(`${JSON.stringify(login, null, 2)}
|
|
757
|
+
`, "utf8");
|
|
758
|
+
await handle.sync();
|
|
759
|
+
await handle.close();
|
|
760
|
+
handle = null;
|
|
761
|
+
if (process.platform !== "win32")
|
|
762
|
+
await chmod(temporary, FILE_MODE);
|
|
763
|
+
await rename(temporary, this.filename);
|
|
764
|
+
} catch (error) {
|
|
765
|
+
await handle?.close().catch(() => void 0);
|
|
766
|
+
await rm(temporary, { force: true }).catch(() => void 0);
|
|
767
|
+
throw new Error(`Could not store Tonbo credentials at ${this.filename}.`, { cause: error });
|
|
768
|
+
}
|
|
469
769
|
}
|
|
470
|
-
|
|
471
|
-
|
|
770
|
+
async remove() {
|
|
771
|
+
await assertExistingDestinationIsSafe(this.filename);
|
|
772
|
+
await rm(this.filename, { force: true });
|
|
472
773
|
}
|
|
473
774
|
};
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
775
|
+
var RULE_KEYWORDS = /* @__PURE__ */ new Set(["const", "type", "minLength", "pattern", "format", "description"]);
|
|
776
|
+
function matchesRule(value, rule) {
|
|
777
|
+
if (Object.keys(rule).some((keyword) => !RULE_KEYWORDS.has(keyword)))
|
|
778
|
+
return false;
|
|
779
|
+
if ("const" in rule)
|
|
780
|
+
return value === rule.const;
|
|
781
|
+
const types = typeof rule.type === "string" ? [rule.type] : rule.type ?? [];
|
|
782
|
+
if (value === null)
|
|
783
|
+
return types.includes("null");
|
|
784
|
+
if (typeof value !== "string" || !types.includes("string"))
|
|
785
|
+
return false;
|
|
786
|
+
if (rule.minLength !== void 0 && value.length < rule.minLength)
|
|
787
|
+
return false;
|
|
788
|
+
if (rule.pattern !== void 0 && !new RegExp(rule.pattern, "u").test(value))
|
|
789
|
+
return false;
|
|
790
|
+
if (rule.format === "date-time" && Number.isNaN(Date.parse(value)))
|
|
791
|
+
return false;
|
|
792
|
+
return true;
|
|
478
793
|
}
|
|
479
|
-
function
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
794
|
+
function isStoredLogin(value) {
|
|
795
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
796
|
+
return false;
|
|
797
|
+
const login = value;
|
|
798
|
+
const schema = credentialsContract.schema;
|
|
799
|
+
if (login.version !== credentialsContract.current_version)
|
|
800
|
+
return false;
|
|
801
|
+
if (!schema.required.every((name) => Object.hasOwn(login, name)))
|
|
802
|
+
return false;
|
|
803
|
+
const properties = schema.properties;
|
|
804
|
+
return Object.entries(properties).every(([name, rule]) => !Object.hasOwn(login, name) || matchesRule(login[name], rule));
|
|
487
805
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
complete: {
|
|
504
|
-
status: 200,
|
|
505
|
-
title: "You are signed in.",
|
|
506
|
-
body: "Return to your terminal. This tab can be closed."
|
|
507
|
-
},
|
|
508
|
-
denied: {
|
|
509
|
-
status: 400,
|
|
510
|
-
title: "Login was not completed.",
|
|
511
|
-
body: "Nothing was authorized. Return to your terminal to try again."
|
|
512
|
-
},
|
|
513
|
-
invalid: {
|
|
514
|
-
// The state did not match, the code was missing, or the path was wrong.
|
|
515
|
-
// Say that it could not be verified rather than which check failed: the
|
|
516
|
-
// person who can read this page is not necessarily the one who started
|
|
517
|
-
// the login.
|
|
518
|
-
status: 400,
|
|
519
|
-
title: "This callback could not be verified.",
|
|
520
|
-
body: "Return to your terminal and start the login again."
|
|
521
|
-
}
|
|
522
|
-
};
|
|
523
|
-
function renderCallbackPage(outcome) {
|
|
524
|
-
const { title, body } = OUTCOMES[outcome];
|
|
525
|
-
return `<!doctype html>
|
|
526
|
-
<html lang="en">
|
|
527
|
-
<head>
|
|
528
|
-
<meta charset="utf-8">
|
|
529
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
530
|
-
<meta name="robots" content="noindex">
|
|
531
|
-
<title>Tonbo CLI</title>
|
|
532
|
-
<style>
|
|
533
|
-
:root {
|
|
534
|
-
--paper: #f3f2ee;
|
|
535
|
-
--ink: #1d1b17;
|
|
536
|
-
--rule: #c9c5ba;
|
|
537
|
-
--quiet: #5c564a;
|
|
538
|
-
--signal: #d64a03;
|
|
539
|
-
}
|
|
540
|
-
@media (prefers-color-scheme: dark) {
|
|
541
|
-
:root {
|
|
542
|
-
--paper: #1d1b17;
|
|
543
|
-
--ink: #f3f2ee;
|
|
544
|
-
--rule: #3a362e;
|
|
545
|
-
--quiet: #a49e8f;
|
|
546
|
-
--signal: #f08b4b;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
* { box-sizing: border-box; }
|
|
550
|
-
body {
|
|
551
|
-
margin: 0;
|
|
552
|
-
min-height: 100vh;
|
|
553
|
-
display: grid;
|
|
554
|
-
grid-template-rows: auto 1fr;
|
|
555
|
-
padding: 0 clamp(20px, 5vw, 72px);
|
|
556
|
-
background: var(--paper);
|
|
557
|
-
color: var(--ink);
|
|
558
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
559
|
-
-webkit-font-smoothing: antialiased;
|
|
560
|
-
}
|
|
561
|
-
header { padding: 28px 0; }
|
|
562
|
-
/* Same metrics as .auth-wordmark in the app: the glyphs are the ground
|
|
563
|
-
and the letters are the gaps, so the 8px/10px/scaleY(0.8) combination is
|
|
564
|
-
what makes the rows meet without seams. Iosevka is not available here,
|
|
565
|
-
so it falls back to the platform monospace. */
|
|
566
|
-
pre.wordmark {
|
|
567
|
-
margin: 0;
|
|
568
|
-
font-family: 'Iosevka', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
|
|
569
|
-
font-size: 8px;
|
|
570
|
-
font-weight: 400;
|
|
571
|
-
line-height: 10px;
|
|
572
|
-
letter-spacing: 0;
|
|
573
|
-
white-space: pre;
|
|
574
|
-
color: var(--signal);
|
|
575
|
-
transform: scaleY(0.8);
|
|
576
|
-
transform-origin: left top;
|
|
577
|
-
}
|
|
578
|
-
main {
|
|
579
|
-
display: flex;
|
|
580
|
-
flex-direction: column;
|
|
581
|
-
justify-content: center;
|
|
582
|
-
max-width: 46ch;
|
|
583
|
-
padding-bottom: 12vh;
|
|
584
|
-
}
|
|
585
|
-
h1 {
|
|
586
|
-
margin: 0;
|
|
587
|
-
font-size: 26px;
|
|
588
|
-
font-weight: 300;
|
|
589
|
-
line-height: 1.16;
|
|
590
|
-
letter-spacing: -0.018em;
|
|
591
|
-
}
|
|
592
|
-
p {
|
|
593
|
-
margin: 14px 0 0;
|
|
594
|
-
font-size: 16px;
|
|
595
|
-
line-height: 1.55;
|
|
596
|
-
color: var(--quiet);
|
|
597
|
-
}
|
|
598
|
-
hr {
|
|
599
|
-
width: 40px;
|
|
600
|
-
margin: 28px 0 0;
|
|
601
|
-
border: 0;
|
|
602
|
-
border-top: 1px solid var(--rule);
|
|
806
|
+
async function preparePrivateDirectory(directory) {
|
|
807
|
+
const stat = await lstat(directory);
|
|
808
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
809
|
+
throw new Error("Tonbo config directory must be a real directory.");
|
|
810
|
+
if (process.platform !== "win32")
|
|
811
|
+
await chmod(directory, DIRECTORY_MODE);
|
|
812
|
+
}
|
|
813
|
+
async function assertExistingDestinationIsSafe(filename) {
|
|
814
|
+
try {
|
|
815
|
+
const stat = await lstat(filename);
|
|
816
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
817
|
+
throw new Error("Tonbo credential path must be a regular file.");
|
|
818
|
+
} catch (error) {
|
|
819
|
+
if (error.code !== "ENOENT")
|
|
820
|
+
throw error;
|
|
603
821
|
}
|
|
604
|
-
</style>
|
|
605
|
-
</head>
|
|
606
|
-
<body>
|
|
607
|
-
<header><pre class="wordmark" aria-hidden="true">${WORDMARK}</pre></header>
|
|
608
|
-
<main>
|
|
609
|
-
<h1>${title}</h1>
|
|
610
|
-
<p>${body}</p>
|
|
611
|
-
<hr>
|
|
612
|
-
</main>
|
|
613
|
-
</body>
|
|
614
|
-
</html>
|
|
615
|
-
`;
|
|
616
822
|
}
|
|
617
|
-
function
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
},
|
|
624
|
-
body: renderCallbackPage(outcome)
|
|
625
|
-
};
|
|
823
|
+
async function assertPrivateRegularFile(filename) {
|
|
824
|
+
const stat = await lstat(filename);
|
|
825
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
826
|
+
throw new Error("Tonbo credential path must be a regular file.");
|
|
827
|
+
if (process.platform !== "win32" && (stat.mode & REFUSED_MODE_BITS) !== 0)
|
|
828
|
+
throw new Error(`Tonbo credential file must have mode ${credentialsContract.permissions.file_mode}.`);
|
|
626
829
|
}
|
|
627
830
|
|
|
628
|
-
// src/auth.
|
|
831
|
+
// ../../packages/tonbo-cli-core/dist/src/auth.js
|
|
629
832
|
var exec = promisify(execFile);
|
|
833
|
+
var DEFAULT_LOGIN_HINT = credentialsContract.reader.login_hint;
|
|
630
834
|
var LOOPBACK_HOST = "127.0.0.1";
|
|
631
|
-
function loginHostname(hostname =
|
|
835
|
+
function loginHostname(hostname = os2.hostname()) {
|
|
632
836
|
const cleaned = hostname.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 64);
|
|
633
837
|
return cleaned || "computer";
|
|
634
838
|
}
|
|
635
839
|
var AuthClient = class {
|
|
636
|
-
|
|
840
|
+
credentials;
|
|
841
|
+
fetcher;
|
|
842
|
+
loginOrigin;
|
|
843
|
+
browserOpener;
|
|
844
|
+
hostname;
|
|
845
|
+
loginHint;
|
|
846
|
+
constructor(credentials, fetcher, loginOrigin2 = "https://tonbo.dev", browserOpener = openBrowser, hostname = loginHostname(), loginHint = DEFAULT_LOGIN_HINT) {
|
|
637
847
|
this.credentials = credentials;
|
|
638
848
|
this.fetcher = fetcher;
|
|
639
849
|
this.loginOrigin = loginOrigin2;
|
|
640
850
|
this.browserOpener = browserOpener;
|
|
641
851
|
this.hostname = hostname;
|
|
852
|
+
this.loginHint = loginHint;
|
|
642
853
|
}
|
|
643
854
|
/** The personal key requests are made with: TONBO_API_KEY, or the saved
|
|
644
855
|
* login while it has not expired. */
|
|
645
856
|
async apiKey() {
|
|
646
857
|
const injected = process.env.TONBO_API_KEY?.trim();
|
|
647
|
-
if (injected)
|
|
858
|
+
if (injected)
|
|
859
|
+
return injected;
|
|
648
860
|
const login = await this.credentials.load();
|
|
649
|
-
if (!login)
|
|
650
|
-
|
|
651
|
-
|
|
861
|
+
if (!login)
|
|
862
|
+
throw new Error(`Not logged in. Run \`${this.loginHint}\`.`);
|
|
863
|
+
if (isExpiredLogin(login))
|
|
864
|
+
throw new Error(`Your Tonbo login has expired. Run \`${this.loginHint}\`.`);
|
|
652
865
|
return login.key;
|
|
653
866
|
}
|
|
654
867
|
/**
|
|
@@ -661,32 +874,24 @@ var AuthClient = class {
|
|
|
661
874
|
const verifier = randomBytes(48).toString("base64url");
|
|
662
875
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
663
876
|
const state = randomBytes(24).toString("base64url");
|
|
664
|
-
const code = await this.listenForCode(
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
return this.browserOpener(authorization.toString());
|
|
677
|
-
},
|
|
678
|
-
progress
|
|
679
|
-
);
|
|
877
|
+
const code = await this.listenForCode(state, (redirectUri) => {
|
|
878
|
+
const authorization = new URL("/cli/login", this.loginOrigin);
|
|
879
|
+
authorization.search = new URLSearchParams({
|
|
880
|
+
code_challenge: challenge,
|
|
881
|
+
code_challenge_method: "S256",
|
|
882
|
+
redirect_uri: redirectUri,
|
|
883
|
+
state,
|
|
884
|
+
hostname: this.hostname,
|
|
885
|
+
...account ? { account } : {}
|
|
886
|
+
}).toString();
|
|
887
|
+
return this.browserOpener(authorization.toString());
|
|
888
|
+
}, progress);
|
|
680
889
|
progress({ status: "started", step: "key-redemption" });
|
|
681
|
-
const redeemed = await requestJson(
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
{
|
|
685
|
-
|
|
686
|
-
headers: { "content-type": "application/json" },
|
|
687
|
-
body: JSON.stringify({ code, code_verifier: verifier })
|
|
688
|
-
}
|
|
689
|
-
);
|
|
890
|
+
const redeemed = await requestJson(this.fetcher, new URL("/api/cli/login/token", this.loginOrigin).toString(), {
|
|
891
|
+
method: "POST",
|
|
892
|
+
headers: { "content-type": "application/json" },
|
|
893
|
+
body: JSON.stringify({ code, code_verifier: verifier })
|
|
894
|
+
});
|
|
690
895
|
progress({ status: "completed", step: "key-redemption" });
|
|
691
896
|
const login = {
|
|
692
897
|
version: 1,
|
|
@@ -706,39 +911,42 @@ var AuthClient = class {
|
|
|
706
911
|
const sockets = /* @__PURE__ */ new Set();
|
|
707
912
|
let authorizationStarted = false;
|
|
708
913
|
const startAuthorization = () => {
|
|
709
|
-
if (authorizationStarted)
|
|
914
|
+
if (authorizationStarted)
|
|
915
|
+
return;
|
|
710
916
|
authorizationStarted = true;
|
|
711
917
|
progress({ status: "completed", step: "browser" });
|
|
712
918
|
progress({ status: "started", step: "browser-authorization" });
|
|
713
919
|
};
|
|
714
920
|
const finish = (result, completedResponseSocket) => {
|
|
715
|
-
if (settled)
|
|
921
|
+
if (settled)
|
|
922
|
+
return;
|
|
716
923
|
settled = true;
|
|
717
924
|
clearTimeout(timeout);
|
|
718
925
|
const complete = (closeError) => {
|
|
719
|
-
if (closeError)
|
|
926
|
+
if (closeError)
|
|
927
|
+
reject(closeError);
|
|
720
928
|
else if ("code" in result) {
|
|
721
929
|
progress({ status: "completed", step: "callback-close" });
|
|
722
930
|
resolve(result.code);
|
|
723
|
-
} else
|
|
931
|
+
} else
|
|
932
|
+
reject(result.error);
|
|
724
933
|
};
|
|
725
|
-
if ("code" in result)
|
|
934
|
+
if ("code" in result)
|
|
935
|
+
progress({ status: "started", step: "callback-close" });
|
|
726
936
|
if (!server.listening) {
|
|
727
937
|
complete();
|
|
728
938
|
return;
|
|
729
939
|
}
|
|
730
940
|
server.close(complete);
|
|
731
941
|
for (const socket of sockets) {
|
|
732
|
-
if (socket !== completedResponseSocket)
|
|
942
|
+
if (socket !== completedResponseSocket)
|
|
943
|
+
socket.destroy();
|
|
733
944
|
}
|
|
734
945
|
server.closeIdleConnections();
|
|
735
946
|
};
|
|
736
|
-
const timeout = setTimeout(
|
|
737
|
-
()
|
|
738
|
-
|
|
739
|
-
},
|
|
740
|
-
5 * 60 * 1e3
|
|
741
|
-
);
|
|
947
|
+
const timeout = setTimeout(() => {
|
|
948
|
+
finish({ error: new Error("Timed out waiting for browser login.") });
|
|
949
|
+
}, 5 * 60 * 1e3);
|
|
742
950
|
const server = createServer((request, response) => {
|
|
743
951
|
const url = new URL(request.url ?? "/", `http://${LOOPBACK_HOST}`);
|
|
744
952
|
if (url.pathname !== "/callback" || url.searchParams.get("state") !== expectedState) {
|
|
@@ -748,11 +956,7 @@ var AuthClient = class {
|
|
|
748
956
|
const denied = url.searchParams.get("error");
|
|
749
957
|
if (denied) {
|
|
750
958
|
const responseSocket2 = response.socket;
|
|
751
|
-
respond(
|
|
752
|
-
response,
|
|
753
|
-
"denied",
|
|
754
|
-
() => finish({ error: new Error(`The login was not approved (${denied}).`) }, responseSocket2)
|
|
755
|
-
);
|
|
959
|
+
respond(response, "denied", () => finish({ error: new Error(`The login was not approved (${denied}).`) }, responseSocket2));
|
|
756
960
|
return;
|
|
757
961
|
}
|
|
758
962
|
const code = url.searchParams.get("code");
|
|
@@ -775,141 +979,184 @@ var AuthClient = class {
|
|
|
775
979
|
const { port } = server.address();
|
|
776
980
|
progress({ status: "completed", step: "callback-server" });
|
|
777
981
|
progress({ status: "started", step: "browser" });
|
|
778
|
-
void ready(`http://${LOOPBACK_HOST}:${port}/callback`).then(startAuthorization).catch(
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
})
|
|
782
|
-
);
|
|
982
|
+
void ready(`http://${LOOPBACK_HOST}:${port}/callback`).then(startAuthorization).catch((error) => finish({
|
|
983
|
+
error: error instanceof Error ? error : new Error("Could not open the login browser.")
|
|
984
|
+
}));
|
|
783
985
|
});
|
|
784
986
|
});
|
|
785
987
|
}
|
|
786
988
|
};
|
|
787
989
|
function respond(response, outcome, finished) {
|
|
788
990
|
const page = callbackResponse(outcome);
|
|
789
|
-
if (finished)
|
|
991
|
+
if (finished)
|
|
992
|
+
response.once("finish", finished);
|
|
790
993
|
response.writeHead(page.status, { ...page.headers, connection: "close" });
|
|
791
994
|
response.end(page.body);
|
|
792
995
|
}
|
|
793
996
|
async function openBrowser(url) {
|
|
794
|
-
if (process.platform === "darwin")
|
|
997
|
+
if (process.platform === "darwin")
|
|
998
|
+
return void await exec("open", [url]);
|
|
795
999
|
if (process.platform === "win32")
|
|
796
1000
|
return void await exec("rundll32", ["url.dll,FileProtocolHandler", url]);
|
|
797
1001
|
return void await exec("xdg-open", [url]);
|
|
798
1002
|
}
|
|
799
1003
|
|
|
800
|
-
// src/
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
return path.join(base, "tonbo");
|
|
811
|
-
}
|
|
812
|
-
var UnusableLoginError = class extends Error {
|
|
813
|
-
};
|
|
814
|
-
var FileCredentialStore = class {
|
|
815
|
-
constructor(filename = path.join(defaultConfigDirectory(), "credentials.json")) {
|
|
816
|
-
this.filename = filename;
|
|
1004
|
+
// ../../packages/tonbo-cli-core/dist/src/api.js
|
|
1005
|
+
var DEFAULT_API_ORIGIN = "https://api.tonbo.dev";
|
|
1006
|
+
var ManagementApi = class {
|
|
1007
|
+
fetcher;
|
|
1008
|
+
apiOrigin;
|
|
1009
|
+
loginHint;
|
|
1010
|
+
constructor(fetcher, apiOrigin2 = DEFAULT_API_ORIGIN, loginHint = DEFAULT_LOGIN_HINT) {
|
|
1011
|
+
this.fetcher = fetcher;
|
|
1012
|
+
this.apiOrigin = apiOrigin2;
|
|
1013
|
+
this.loginHint = loginHint;
|
|
817
1014
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
await assertPrivateRegularFile(this.filename);
|
|
822
|
-
parsed = JSON.parse(await readFile(this.filename, "utf8"));
|
|
823
|
-
} catch (error) {
|
|
824
|
-
if (error.code === "ENOENT") return null;
|
|
825
|
-
throw new Error(`Could not read Tonbo credentials at ${this.filename}.`, { cause: error });
|
|
826
|
-
}
|
|
827
|
-
if (!isStoredLogin(parsed))
|
|
828
|
-
throw new UnusableLoginError(
|
|
829
|
-
`The saved login at ${this.filename} is not usable by this CLI. Run \`tonbo login\`.`
|
|
830
|
-
);
|
|
831
|
-
return parsed;
|
|
1015
|
+
/** The answer's `data`. A refusal throws an HttpError naming the request id. */
|
|
1016
|
+
async call(operationId, key, request = {}) {
|
|
1017
|
+
return (await this.request(operationId, key, request)).data;
|
|
832
1018
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1019
|
+
/** One page of a list operation: its `data` and `next_cursor`, with the
|
|
1020
|
+
* operation's other query parameters, if any. */
|
|
1021
|
+
page(operationId, key, { limit, cursor } = {}, query = {}) {
|
|
1022
|
+
return this.request(operationId, key, { query: { ...query, limit, cursor } });
|
|
1023
|
+
}
|
|
1024
|
+
async request(operationId, key, { params = {}, query = {}, body, idempotencyKey }) {
|
|
1025
|
+
const operation = managementOperations[operationId];
|
|
1026
|
+
const path2 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
1027
|
+
const value = params[name];
|
|
1028
|
+
if (value === void 0)
|
|
1029
|
+
throw new Error(`${operationId} needs the path parameter ${name}.`);
|
|
1030
|
+
return encodeURIComponent(value);
|
|
1031
|
+
});
|
|
1032
|
+
const search = new URLSearchParams();
|
|
1033
|
+
for (const [name, value] of Object.entries(query)) {
|
|
1034
|
+
if (value === void 0)
|
|
1035
|
+
continue;
|
|
1036
|
+
if (!operation.query.includes(name))
|
|
1037
|
+
throw new Error(`${operationId} takes no query parameter ${name}.`);
|
|
1038
|
+
search.set(name, String(value));
|
|
1039
|
+
}
|
|
1040
|
+
if (operation.idempotencyKey && !idempotencyKey)
|
|
1041
|
+
throw new Error(`${operationId} needs an Idempotency-Key.`);
|
|
1042
|
+
const headers = { authorization: `Bearer ${key}` };
|
|
1043
|
+
if (body !== void 0)
|
|
1044
|
+
headers["content-type"] = "application/json";
|
|
1045
|
+
if (idempotencyKey)
|
|
1046
|
+
headers["idempotency-key"] = idempotencyKey;
|
|
1047
|
+
const queryString = search.toString();
|
|
1048
|
+
const url = `${this.apiOrigin}${managementApiBasePath}${path2}${queryString ? `?${queryString}` : ""}`;
|
|
844
1049
|
try {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
handle = null;
|
|
851
|
-
if (process.platform !== "win32") await chmod(temporary, 384);
|
|
852
|
-
await rename(temporary, this.filename);
|
|
1050
|
+
return await requestJson(this.fetcher, url, {
|
|
1051
|
+
method: operation.method,
|
|
1052
|
+
headers,
|
|
1053
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
1054
|
+
});
|
|
853
1055
|
} catch (error) {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
throw
|
|
1056
|
+
if (error instanceof HttpError && error.status === 401)
|
|
1057
|
+
throw new HttpError(`${error.message} Run \`${this.loginHint}\`.`, error.status, error.body, error.requestId);
|
|
1058
|
+
throw error;
|
|
857
1059
|
}
|
|
858
1060
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
await rm(this.filename, { force: true });
|
|
1061
|
+
getAccount(key) {
|
|
1062
|
+
return this.call("getAccount", key);
|
|
862
1063
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
866
|
-
const login = value;
|
|
867
|
-
return login.version === 1 && typeof login.account_id === "string" && login.account_id.length > 0 && typeof login.key_id === "string" && login.key_id.length > 0 && typeof login.key === "string" && login.key.startsWith("tbo_") && (login.expires_at === null || typeof login.expires_at === "string" && !Number.isNaN(Date.parse(login.expires_at)));
|
|
868
|
-
}
|
|
869
|
-
async function preparePrivateDirectory(directory) {
|
|
870
|
-
const stat = await lstat(directory);
|
|
871
|
-
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
872
|
-
throw new Error("Tonbo config directory must be a real directory.");
|
|
873
|
-
if (process.platform !== "win32") await chmod(directory, 448);
|
|
874
|
-
}
|
|
875
|
-
async function assertExistingDestinationIsSafe(filename) {
|
|
876
|
-
try {
|
|
877
|
-
const stat = await lstat(filename);
|
|
878
|
-
if (stat.isSymbolicLink() || !stat.isFile())
|
|
879
|
-
throw new Error("Tonbo credential path must be a regular file.");
|
|
880
|
-
} catch (error) {
|
|
881
|
-
if (error.code !== "ENOENT") throw error;
|
|
1064
|
+
listRegions(key) {
|
|
1065
|
+
return this.call("listRegions", key);
|
|
882
1066
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1067
|
+
listMembers(key, page) {
|
|
1068
|
+
return this.page("listMembers", key, page);
|
|
1069
|
+
}
|
|
1070
|
+
updateMember(key, subject, change) {
|
|
1071
|
+
return this.call("updateMember", key, { params: { subject }, body: change });
|
|
1072
|
+
}
|
|
1073
|
+
/** A replay with the same idempotency key returns the Agent the first
|
|
1074
|
+
* request created, so an unconfirmed creation is retried with that key. */
|
|
1075
|
+
createAgent(key, request, idempotencyKey) {
|
|
1076
|
+
return this.call("createAgent", key, {
|
|
1077
|
+
body: withoutUndefined(request),
|
|
1078
|
+
idempotencyKey
|
|
1079
|
+
}).catch(unconfirmed("Agent creation", idempotencyKey, "the same name, role and region"));
|
|
1080
|
+
}
|
|
1081
|
+
listApiKeys(key, page) {
|
|
1082
|
+
return this.page("listApiKeys", key, page);
|
|
1083
|
+
}
|
|
1084
|
+
/** Not idempotent: the secret is never stored, so a replay could not
|
|
1085
|
+
* return it, and a blind retry of a live name answers 409. */
|
|
1086
|
+
createApiKey(key, request) {
|
|
1087
|
+
return this.call("createApiKey", key, { body: withoutUndefined(request) });
|
|
1088
|
+
}
|
|
1089
|
+
getApiKey(key, keyId) {
|
|
1090
|
+
return this.call("getApiKey", key, { params: { key_id: keyId } });
|
|
1091
|
+
}
|
|
1092
|
+
revokeApiKey(key, keyId) {
|
|
1093
|
+
return this.call("revokeApiKey", key, {
|
|
1094
|
+
params: { key_id: keyId }
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
listBuckets(key, page) {
|
|
1098
|
+
return this.page("listBuckets", key, page);
|
|
1099
|
+
}
|
|
1100
|
+
/** A replay with the same idempotency key returns the bucket the first
|
|
1101
|
+
* request created. */
|
|
1102
|
+
createBucket(key, request, idempotencyKey) {
|
|
1103
|
+
return this.call("createBucket", key, {
|
|
1104
|
+
body: withoutUndefined(request),
|
|
1105
|
+
idempotencyKey
|
|
1106
|
+
}).catch(unconfirmed("Bucket creation", idempotencyKey, "the same name and region"));
|
|
1107
|
+
}
|
|
1108
|
+
getBucket(key, name) {
|
|
1109
|
+
return this.call("getBucket", key, { params: { name } });
|
|
1110
|
+
}
|
|
1111
|
+
updateBucket(key, name, change) {
|
|
1112
|
+
return this.call("updateBucket", key, { params: { name }, body: change });
|
|
1113
|
+
}
|
|
1114
|
+
deleteBucket(key, name) {
|
|
1115
|
+
return this.call("deleteBucket", key, { params: { name } });
|
|
1116
|
+
}
|
|
1117
|
+
listDatabases(key, page, prefix) {
|
|
1118
|
+
return this.page("listDatabases", key, page, { prefix });
|
|
1119
|
+
}
|
|
1120
|
+
/** A replay with the same idempotency key returns the database the first
|
|
1121
|
+
* request created. */
|
|
1122
|
+
createDatabase(key, request, idempotencyKey) {
|
|
1123
|
+
return this.call("createDatabase", key, {
|
|
1124
|
+
body: withoutUndefined(request),
|
|
1125
|
+
idempotencyKey
|
|
1126
|
+
}).catch(unconfirmed("Database creation", idempotencyKey, "the same name and region"));
|
|
1127
|
+
}
|
|
1128
|
+
getDatabase(key, name) {
|
|
1129
|
+
return this.call("getDatabase", key, { params: { name } });
|
|
1130
|
+
}
|
|
1131
|
+
updateDatabase(key, name, change) {
|
|
1132
|
+
return this.call("updateDatabase", key, { params: { name }, body: change });
|
|
1133
|
+
}
|
|
1134
|
+
deleteDatabase(key, name) {
|
|
1135
|
+
return this.call("deleteDatabase", key, { params: { name } });
|
|
1136
|
+
}
|
|
1137
|
+
/** The whole usage page: its rows, `other`, prices and checkpoint. */
|
|
1138
|
+
listDatabaseUsage(key, page, query = {}) {
|
|
1139
|
+
return this.page("listDatabaseUsage", key, page, query);
|
|
1140
|
+
}
|
|
1141
|
+
getUsage(key, period) {
|
|
1142
|
+
return this.call("getUsage", key, { query: { period } });
|
|
902
1143
|
}
|
|
1144
|
+
};
|
|
1145
|
+
function withoutUndefined(value) {
|
|
1146
|
+
return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
|
|
1147
|
+
}
|
|
1148
|
+
function unconfirmed(what, idempotencyKey, same) {
|
|
1149
|
+
return (cause) => {
|
|
1150
|
+
if (cause instanceof HttpError && cause.status < 500)
|
|
1151
|
+
throw cause;
|
|
1152
|
+
throw new Error(`${what} was not confirmed: ${cause instanceof Error ? cause.message : String(cause)} Retry with --idempotency-key ${idempotencyKey} and ${same}.`, { cause });
|
|
1153
|
+
};
|
|
903
1154
|
}
|
|
904
1155
|
|
|
905
|
-
// src/format.
|
|
1156
|
+
// ../../packages/tonbo-cli-core/dist/src/format.js
|
|
906
1157
|
function table(headings, rows) {
|
|
907
|
-
const widths = headings.map(
|
|
908
|
-
|
|
909
|
-
);
|
|
910
|
-
return [headings, ...rows].map(
|
|
911
|
-
(row) => row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ").trimEnd()
|
|
912
|
-
).join("\n");
|
|
1158
|
+
const widths = headings.map((heading, column) => Math.max(heading.length, ...rows.map((row) => (row[column] ?? "").length)));
|
|
1159
|
+
return [headings, ...rows].map((row) => row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ").trimEnd()).join("\n");
|
|
913
1160
|
}
|
|
914
1161
|
function fields(pairs) {
|
|
915
1162
|
const width = Math.max(...pairs.map(([label]) => label.length + 1));
|
|
@@ -924,34 +1171,31 @@ var USD = new Intl.NumberFormat("en-US", {
|
|
|
924
1171
|
function usd(micros) {
|
|
925
1172
|
return USD.format(micros / 1e6);
|
|
926
1173
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
throw new Error(`Refusing to ${action} without confirmation: pass --yes.`);
|
|
943
|
-
if (await deps.ask(await question()) !== expected)
|
|
944
|
-
throw new Error("Cancelled; nothing was changed.");
|
|
1174
|
+
var BYTE_UNITS = ["KiB", "MiB", "GiB", "TiB"];
|
|
1175
|
+
function bytes(count, absent = "unknown") {
|
|
1176
|
+
if (count === null)
|
|
1177
|
+
return absent;
|
|
1178
|
+
if (count < 1024)
|
|
1179
|
+
return `${count} B`;
|
|
1180
|
+
let value = count;
|
|
1181
|
+
let unit = "B";
|
|
1182
|
+
for (const next of BYTE_UNITS) {
|
|
1183
|
+
if (value < 1024)
|
|
1184
|
+
break;
|
|
1185
|
+
value /= 1024;
|
|
1186
|
+
unit = next;
|
|
1187
|
+
}
|
|
1188
|
+
return `${value.toLocaleString("en-US", { maximumFractionDigits: 1 })} ${unit}`;
|
|
945
1189
|
}
|
|
1190
|
+
function when(timestamp, absent = "never") {
|
|
1191
|
+
return timestamp ?? absent;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// ../../packages/tonbo-cli-core/dist/src/login.js
|
|
946
1195
|
async function loginCommand(deps, options = {}) {
|
|
947
1196
|
try {
|
|
948
|
-
const login = await deps.auth.login(
|
|
949
|
-
|
|
950
|
-
options.account
|
|
951
|
-
);
|
|
952
|
-
deps.output(
|
|
953
|
-
`Logged in to account ${login.account_id}.${login.expires_at ? ` The login expires ${login.expires_at}.` : ""}`
|
|
954
|
-
);
|
|
1197
|
+
const login = await deps.auth.login((event) => reportLoginProgress(deps.progress, event), options.account);
|
|
1198
|
+
deps.output(`Logged in to account ${login.account_id}.${login.expires_at ? ` The login expires ${login.expires_at}.` : ""}`);
|
|
955
1199
|
} catch (error) {
|
|
956
1200
|
deps.progress.fail();
|
|
957
1201
|
throw error;
|
|
@@ -985,15 +1229,18 @@ var LOGIN_PROGRESS_MESSAGES = {
|
|
|
985
1229
|
};
|
|
986
1230
|
function reportLoginProgress(progress, event) {
|
|
987
1231
|
const messages = LOGIN_PROGRESS_MESSAGES[event.step];
|
|
988
|
-
if (event.status === "started")
|
|
989
|
-
|
|
1232
|
+
if (event.status === "started")
|
|
1233
|
+
progress.start(messages.started);
|
|
1234
|
+
else
|
|
1235
|
+
progress.succeed(messages.completed);
|
|
990
1236
|
}
|
|
991
1237
|
async function logoutCommand(deps) {
|
|
992
1238
|
let login;
|
|
993
1239
|
try {
|
|
994
1240
|
login = await deps.credentials.load();
|
|
995
1241
|
} catch (error) {
|
|
996
|
-
if (!(error instanceof UnusableLoginError))
|
|
1242
|
+
if (!(error instanceof UnusableLoginError))
|
|
1243
|
+
throw error;
|
|
997
1244
|
await deps.credentials.remove();
|
|
998
1245
|
deps.output("Removed a saved login this CLI cannot use; it had no key to revoke.");
|
|
999
1246
|
return;
|
|
@@ -1011,11 +1258,114 @@ async function logoutCommand(deps) {
|
|
|
1011
1258
|
}
|
|
1012
1259
|
await deps.credentials.remove();
|
|
1013
1260
|
if (failure)
|
|
1261
|
+
throw new Error(`Removed the saved login, but could not revoke its key ${login.key_id}: ${failure.message} Revoke it under API keys in the Tonbo console.`, { cause: failure });
|
|
1262
|
+
deps.output("Logged out. The CLI key is revoked.");
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// ../../packages/tonbo-cli-core/dist/src/origin.js
|
|
1266
|
+
function apiOrigin(configured = process.env.TONBO_API_ORIGIN) {
|
|
1267
|
+
const url = new URL(configured?.trim() || DEFAULT_API_ORIGIN);
|
|
1268
|
+
const loopback = ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
|
|
1269
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback))
|
|
1270
|
+
throw new Error("TONBO_API_ORIGIN must be an https origin.");
|
|
1271
|
+
return url.origin;
|
|
1272
|
+
}
|
|
1273
|
+
function loginOrigin(api) {
|
|
1274
|
+
const url = new URL(api);
|
|
1275
|
+
if (url.hostname.startsWith("api."))
|
|
1276
|
+
url.hostname = url.hostname.slice("api.".length);
|
|
1277
|
+
return url.origin;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// ../../packages/tonbo-cli-core/dist/src/progress.js
|
|
1281
|
+
var FRAMES = ["|", "/", "-", "\\"];
|
|
1282
|
+
var TerminalProgress = class {
|
|
1283
|
+
stream;
|
|
1284
|
+
intervalMs;
|
|
1285
|
+
activeMessage;
|
|
1286
|
+
frame = 0;
|
|
1287
|
+
lastWidth = 0;
|
|
1288
|
+
timer;
|
|
1289
|
+
constructor(stream, intervalMs = 80) {
|
|
1290
|
+
this.stream = stream;
|
|
1291
|
+
this.intervalMs = intervalMs;
|
|
1292
|
+
}
|
|
1293
|
+
start(message) {
|
|
1294
|
+
if (this.activeMessage)
|
|
1295
|
+
this.succeed();
|
|
1296
|
+
this.activeMessage = message;
|
|
1297
|
+
this.frame = 0;
|
|
1298
|
+
if (!this.stream.isTTY) {
|
|
1299
|
+
this.stream.write(`[..] ${message}
|
|
1300
|
+
`);
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
this.render(`[${FRAMES[this.frame]}] ${message}`);
|
|
1304
|
+
this.timer = setInterval(() => {
|
|
1305
|
+
this.frame = (this.frame + 1) % FRAMES.length;
|
|
1306
|
+
this.render(`[${FRAMES[this.frame]}] ${this.activeMessage}`);
|
|
1307
|
+
}, this.intervalMs);
|
|
1308
|
+
this.timer.unref();
|
|
1309
|
+
}
|
|
1310
|
+
succeed(message = this.activeMessage) {
|
|
1311
|
+
this.finish("ok", message);
|
|
1312
|
+
}
|
|
1313
|
+
fail(message = this.activeMessage) {
|
|
1314
|
+
this.finish("!!", message);
|
|
1315
|
+
}
|
|
1316
|
+
finish(marker, message) {
|
|
1317
|
+
if (this.timer)
|
|
1318
|
+
clearInterval(this.timer);
|
|
1319
|
+
this.timer = void 0;
|
|
1320
|
+
this.activeMessage = void 0;
|
|
1321
|
+
if (!message)
|
|
1322
|
+
return;
|
|
1323
|
+
const line = `[${marker}] ${message}`;
|
|
1324
|
+
if (this.stream.isTTY) {
|
|
1325
|
+
this.render(line);
|
|
1326
|
+
this.stream.write("\n");
|
|
1327
|
+
this.lastWidth = 0;
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
this.stream.write(`${line}
|
|
1331
|
+
`);
|
|
1332
|
+
}
|
|
1333
|
+
render(value) {
|
|
1334
|
+
this.stream.write(`\r${value.padEnd(this.lastWidth)}`);
|
|
1335
|
+
this.lastWidth = value.length;
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
// src/commands.ts
|
|
1340
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1341
|
+
|
|
1342
|
+
// src/agent-name.ts
|
|
1343
|
+
var AGENT_NAME_RULE = agentNameContract.$defs.name;
|
|
1344
|
+
var AGENT_NAME_PATTERN = new RegExp(AGENT_NAME_RULE.pattern);
|
|
1345
|
+
var RESERVED_AGENT_NAMES = new Set(AGENT_NAME_RULE.not.enum);
|
|
1346
|
+
function validateAgentName(name) {
|
|
1347
|
+
const valid = name.length >= AGENT_NAME_RULE.minLength && name.length <= AGENT_NAME_RULE.maxLength && AGENT_NAME_PATTERN.test(name) && !RESERVED_AGENT_NAMES.has(name);
|
|
1348
|
+
if (!valid) {
|
|
1014
1349
|
throw new Error(
|
|
1015
|
-
`
|
|
1016
|
-
{ cause: failure }
|
|
1350
|
+
`Agent name must be ${AGENT_NAME_RULE.minLength}-${AGENT_NAME_RULE.maxLength} lowercase letters, digits, or single hyphens, start with a letter, and not be reserved.`
|
|
1017
1351
|
);
|
|
1018
|
-
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// src/commands.ts
|
|
1356
|
+
function print(deps, json, data, human) {
|
|
1357
|
+
deps.output(json ? JSON.stringify(data) : human);
|
|
1358
|
+
}
|
|
1359
|
+
function printPage(deps, json, page, human) {
|
|
1360
|
+
print(deps, json, page.data, human(page.data));
|
|
1361
|
+
if (page.next_cursor) deps.notice(`More results: run again with --cursor ${page.next_cursor}`);
|
|
1362
|
+
}
|
|
1363
|
+
async function confirm(deps, yes, action, question, expected) {
|
|
1364
|
+
if (yes) return;
|
|
1365
|
+
if (!deps.interactive())
|
|
1366
|
+
throw new Error(`Refusing to ${action} without confirmation: pass --yes.`);
|
|
1367
|
+
if (await deps.ask(await question()) !== expected)
|
|
1368
|
+
throw new Error("Cancelled; nothing was changed.");
|
|
1019
1369
|
}
|
|
1020
1370
|
async function regionListCommand(deps, options = {}) {
|
|
1021
1371
|
const regions = await deps.api.listRegions(await deps.auth.apiKey());
|
|
@@ -1024,8 +1374,8 @@ async function regionListCommand(deps, options = {}) {
|
|
|
1024
1374
|
options.json,
|
|
1025
1375
|
regions,
|
|
1026
1376
|
regions.length ? table(
|
|
1027
|
-
["REGION", "URL"],
|
|
1028
|
-
regions.map((region) => [region.id, region.url])
|
|
1377
|
+
["REGION", "URL", "DATABASE DEFAULT"],
|
|
1378
|
+
regions.map((region) => [region.id, region.url, region.default ? "yes" : ""])
|
|
1029
1379
|
) : "No region accepts new resources."
|
|
1030
1380
|
);
|
|
1031
1381
|
}
|
|
@@ -1145,8 +1495,19 @@ async function memberUpdateCommand(deps, member, options = {}) {
|
|
|
1145
1495
|
function keyAccess(apiKey) {
|
|
1146
1496
|
return apiKey.read_only ? "read-only" : "read-write";
|
|
1147
1497
|
}
|
|
1498
|
+
function narrowedByDatabase(apiKey) {
|
|
1499
|
+
return apiKey.databases.length + apiKey.database_prefixes.length > 0;
|
|
1500
|
+
}
|
|
1148
1501
|
function keyBuckets(apiKey) {
|
|
1149
|
-
|
|
1502
|
+
if (apiKey.buckets.length) return apiKey.buckets.join(", ");
|
|
1503
|
+
return narrowedByDatabase(apiKey) ? "none" : "all";
|
|
1504
|
+
}
|
|
1505
|
+
function keyDatabases(apiKey) {
|
|
1506
|
+
if (narrowedByDatabase(apiKey))
|
|
1507
|
+
return [...apiKey.databases, ...apiKey.database_prefixes.map((prefix) => `${prefix}*`)].join(
|
|
1508
|
+
", "
|
|
1509
|
+
);
|
|
1510
|
+
return apiKey.buckets.length ? "none" : "all";
|
|
1150
1511
|
}
|
|
1151
1512
|
function apiKeyFields(apiKey) {
|
|
1152
1513
|
return fields([
|
|
@@ -1156,6 +1517,7 @@ function apiKeyFields(apiKey) {
|
|
|
1156
1517
|
["Ends in", apiKey.last4],
|
|
1157
1518
|
["Access", keyAccess(apiKey)],
|
|
1158
1519
|
["Buckets", keyBuckets(apiKey)],
|
|
1520
|
+
["Databases", keyDatabases(apiKey)],
|
|
1159
1521
|
["Expires", when(apiKey.expires_at)],
|
|
1160
1522
|
["Last used", when(apiKey.last_used_at)],
|
|
1161
1523
|
["Created", apiKey.created_at]
|
|
@@ -1171,7 +1533,17 @@ async function apiKeyListCommand(deps, options = {}) {
|
|
|
1171
1533
|
options.json,
|
|
1172
1534
|
page,
|
|
1173
1535
|
(keys) => keys.length ? table(
|
|
1174
|
-
[
|
|
1536
|
+
[
|
|
1537
|
+
"ID",
|
|
1538
|
+
"NAME",
|
|
1539
|
+
"MEMBER",
|
|
1540
|
+
"ENDS IN",
|
|
1541
|
+
"ACCESS",
|
|
1542
|
+
"BUCKETS",
|
|
1543
|
+
"DATABASES",
|
|
1544
|
+
"EXPIRES",
|
|
1545
|
+
"LAST USED"
|
|
1546
|
+
],
|
|
1175
1547
|
keys.map((apiKey) => [
|
|
1176
1548
|
apiKey.id,
|
|
1177
1549
|
apiKey.name,
|
|
@@ -1179,6 +1551,7 @@ async function apiKeyListCommand(deps, options = {}) {
|
|
|
1179
1551
|
apiKey.last4,
|
|
1180
1552
|
keyAccess(apiKey),
|
|
1181
1553
|
keyBuckets(apiKey),
|
|
1554
|
+
keyDatabases(apiKey),
|
|
1182
1555
|
when(apiKey.expires_at),
|
|
1183
1556
|
when(apiKey.last_used_at)
|
|
1184
1557
|
])
|
|
@@ -1193,6 +1566,8 @@ async function apiKeyCreateCommand(deps, name, options = {}) {
|
|
|
1193
1566
|
member,
|
|
1194
1567
|
read_only: options.readOnly,
|
|
1195
1568
|
buckets: options.bucket,
|
|
1569
|
+
databases: options.database,
|
|
1570
|
+
database_prefixes: options.databasePrefix,
|
|
1196
1571
|
expires_in: options.expiresIn
|
|
1197
1572
|
});
|
|
1198
1573
|
print(
|
|
@@ -1204,12 +1579,20 @@ async function apiKeyCreateCommand(deps, name, options = {}) {
|
|
|
1204
1579
|
"",
|
|
1205
1580
|
` ${created.key}`,
|
|
1206
1581
|
"",
|
|
1582
|
+
"For your application's environment (.env or secret store):",
|
|
1583
|
+
"",
|
|
1584
|
+
` TONBO_API_KEY=${created.key}`,
|
|
1585
|
+
"",
|
|
1207
1586
|
apiKeyFields(created)
|
|
1208
1587
|
].join("\n")
|
|
1209
1588
|
);
|
|
1210
1589
|
deps.notice(
|
|
1211
1590
|
"This is the only time the key's secret is shown: store it now. Anyone holding it acts as its member."
|
|
1212
1591
|
);
|
|
1592
|
+
if (!options.json)
|
|
1593
|
+
deps.notice(
|
|
1594
|
+
"Do not set TONBO_API_KEY in your own shell: it overrides your `tonbo login`, for this CLI too."
|
|
1595
|
+
);
|
|
1213
1596
|
}
|
|
1214
1597
|
async function apiKeyShowCommand(deps, keyId, options = {}) {
|
|
1215
1598
|
const apiKey = await deps.api.getApiKey(await deps.auth.apiKey(), keyId);
|
|
@@ -1312,6 +1695,164 @@ async function bucketDeleteCommand(deps, name, options = {}) {
|
|
|
1312
1695
|
`Bucket ${bucket.name} is ${bucket.state}; its streams are being purged.`
|
|
1313
1696
|
);
|
|
1314
1697
|
}
|
|
1698
|
+
function databaseAccess(database) {
|
|
1699
|
+
return database.public_read ? "public" : "private";
|
|
1700
|
+
}
|
|
1701
|
+
function databaseSize(database) {
|
|
1702
|
+
const { log, snapshot } = database.stored_bytes;
|
|
1703
|
+
if (log === null && snapshot === null) return "unknown";
|
|
1704
|
+
return `${bytes(log)} log, ${bytes(snapshot, "no")} snapshot`;
|
|
1705
|
+
}
|
|
1706
|
+
function databaseOwner(database) {
|
|
1707
|
+
return database.owner_id ? `${database.kind} ${database.owner_id}` : database.kind;
|
|
1708
|
+
}
|
|
1709
|
+
function databaseFields(database) {
|
|
1710
|
+
return fields([
|
|
1711
|
+
["Name", database.name],
|
|
1712
|
+
["Kind", databaseOwner(database)],
|
|
1713
|
+
["Region", database.region],
|
|
1714
|
+
["State", database.state],
|
|
1715
|
+
["Read access", databaseAccess(database)],
|
|
1716
|
+
["URL", database.url],
|
|
1717
|
+
["Incarnation", database.incarnation ?? "none yet"],
|
|
1718
|
+
["Last write", when(database.last_write_at)],
|
|
1719
|
+
["Stored", databaseSize(database)],
|
|
1720
|
+
["Created", `${database.created_at} by ${database.created_by}`]
|
|
1721
|
+
]);
|
|
1722
|
+
}
|
|
1723
|
+
function noticeUnavailable(deps, regions, what) {
|
|
1724
|
+
if (regions.length)
|
|
1725
|
+
deps.notice(
|
|
1726
|
+
`Could not read ${what} in ${regions.join(", ")}; they show as unknown or are missing.`
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
async function databaseListCommand(deps, options = {}) {
|
|
1730
|
+
const page = await deps.api.listDatabases(
|
|
1731
|
+
await deps.auth.apiKey(),
|
|
1732
|
+
{ limit: options.limit, cursor: options.cursor },
|
|
1733
|
+
options.prefix
|
|
1734
|
+
);
|
|
1735
|
+
printPage(
|
|
1736
|
+
deps,
|
|
1737
|
+
options.json,
|
|
1738
|
+
page,
|
|
1739
|
+
(databases) => databases.length ? table(
|
|
1740
|
+
["NAME", "KIND", "REGION", "STATE", "READ ACCESS", "LAST WRITE", "STORED", "URL"],
|
|
1741
|
+
databases.map((database) => [
|
|
1742
|
+
database.name,
|
|
1743
|
+
databaseOwner(database),
|
|
1744
|
+
database.region,
|
|
1745
|
+
database.state,
|
|
1746
|
+
databaseAccess(database),
|
|
1747
|
+
when(database.last_write_at),
|
|
1748
|
+
databaseSize(database),
|
|
1749
|
+
database.url
|
|
1750
|
+
])
|
|
1751
|
+
) : "No databases."
|
|
1752
|
+
);
|
|
1753
|
+
noticeUnavailable(deps, page.unavailable_regions, "sizes");
|
|
1754
|
+
}
|
|
1755
|
+
async function databaseCreateCommand(deps, name, options = {}) {
|
|
1756
|
+
const database = await deps.api.createDatabase(
|
|
1757
|
+
await deps.auth.apiKey(),
|
|
1758
|
+
{ name, region: options.region },
|
|
1759
|
+
options.idempotencyKey ?? randomUUID2()
|
|
1760
|
+
);
|
|
1761
|
+
print(
|
|
1762
|
+
deps,
|
|
1763
|
+
options.json,
|
|
1764
|
+
database,
|
|
1765
|
+
[
|
|
1766
|
+
database.state === "active" ? `Created database ${database.name}.` : `Database ${database.name} is being created; \`tonbo database show ${database.name}\` reports it active when it is ready.`,
|
|
1767
|
+
databaseFields(database)
|
|
1768
|
+
].join("\n")
|
|
1769
|
+
);
|
|
1770
|
+
if (!options.json) deps.notice(databaseNextStep(database.name));
|
|
1771
|
+
}
|
|
1772
|
+
function databaseNextStep(name) {
|
|
1773
|
+
return [
|
|
1774
|
+
"Next: npm i @tonbo/sqlite-ursula",
|
|
1775
|
+
` open(${JSON.stringify(name)}) from "@tonbo/sqlite-ursula/tonbo"`
|
|
1776
|
+
].join("\n");
|
|
1777
|
+
}
|
|
1778
|
+
async function databaseShowCommand(deps, name, options = {}) {
|
|
1779
|
+
const database = await deps.api.getDatabase(await deps.auth.apiKey(), name);
|
|
1780
|
+
print(deps, options.json, database, databaseFields(database));
|
|
1781
|
+
}
|
|
1782
|
+
async function databaseUpdateCommand(deps, name, options = {}) {
|
|
1783
|
+
if (options.public === options.private)
|
|
1784
|
+
throw new Error("Pass exactly one of --public and --private.");
|
|
1785
|
+
const database = await deps.api.updateDatabase(await deps.auth.apiKey(), name, {
|
|
1786
|
+
public_read: options.public === true
|
|
1787
|
+
});
|
|
1788
|
+
print(
|
|
1789
|
+
deps,
|
|
1790
|
+
options.json,
|
|
1791
|
+
database,
|
|
1792
|
+
database.public_read ? `Database ${database.name} is public: anyone can read it at ${database.url}.` : `Database ${database.name} is private: only the account's keys can read it.`
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
async function databaseDeleteCommand(deps, name, options = {}) {
|
|
1796
|
+
await confirm(
|
|
1797
|
+
deps,
|
|
1798
|
+
options.yes,
|
|
1799
|
+
`delete database ${name}`,
|
|
1800
|
+
() => Promise.resolve(
|
|
1801
|
+
`Delete database ${name} and everything in it? This cannot be undone. Type the database's name to confirm:`
|
|
1802
|
+
),
|
|
1803
|
+
name
|
|
1804
|
+
);
|
|
1805
|
+
const database = await deps.api.deleteDatabase(await deps.auth.apiKey(), name);
|
|
1806
|
+
print(
|
|
1807
|
+
deps,
|
|
1808
|
+
options.json,
|
|
1809
|
+
database,
|
|
1810
|
+
database.state === "deleted" ? `Deleted database ${database.name}; its name is free again.` : `Database ${database.name} is ${database.state}; its name is free again once it is deleted.`
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
function databaseUsageSort(value) {
|
|
1814
|
+
if (value === "-list_value" || value === "list_value") return "-list_value";
|
|
1815
|
+
if (value === "name") return "name";
|
|
1816
|
+
throw new Error("--sort is list_value or name.");
|
|
1817
|
+
}
|
|
1818
|
+
async function databaseUsageCommand(deps, options = {}) {
|
|
1819
|
+
const usage = await deps.api.listDatabaseUsage(
|
|
1820
|
+
await deps.auth.apiKey(),
|
|
1821
|
+
{ limit: options.limit, cursor: options.cursor },
|
|
1822
|
+
{ period: options.period, sort: options.sort, prefix: options.prefix }
|
|
1823
|
+
);
|
|
1824
|
+
if (options.json) deps.output(JSON.stringify(usage));
|
|
1825
|
+
else
|
|
1826
|
+
deps.output(
|
|
1827
|
+
[
|
|
1828
|
+
`Database usage in ${usage.period} at list price, before the monthly allowance and credit (${usage.currency.toUpperCase()}; metered up to ${when(usage.as_of, "not yet metered")})`,
|
|
1829
|
+
usage.data.length ? table(
|
|
1830
|
+
[
|
|
1831
|
+
"NAME",
|
|
1832
|
+
"REGION",
|
|
1833
|
+
"STATE",
|
|
1834
|
+
"WRITE UNITS",
|
|
1835
|
+
"LOG GIB-MONTHS",
|
|
1836
|
+
"SNAPSHOT GIB-MONTHS",
|
|
1837
|
+
"LIST VALUE"
|
|
1838
|
+
],
|
|
1839
|
+
usage.data.map((row) => [
|
|
1840
|
+
row.name,
|
|
1841
|
+
row.region,
|
|
1842
|
+
row.state,
|
|
1843
|
+
row.write_units,
|
|
1844
|
+
row.retention_gib_months,
|
|
1845
|
+
row.snapshot_gib_months ?? "",
|
|
1846
|
+
usd(row.list_value_micros)
|
|
1847
|
+
])
|
|
1848
|
+
) : "No database usage.",
|
|
1849
|
+
`Other metered usage: ${usd(usage.other.list_value_micros)}`,
|
|
1850
|
+
"Charges, after the allowance and credit, are in `tonbo usage show`."
|
|
1851
|
+
].join("\n")
|
|
1852
|
+
);
|
|
1853
|
+
if (usage.next_cursor) deps.notice(`More results: run again with --cursor ${usage.next_cursor}`);
|
|
1854
|
+
noticeUnavailable(deps, usage.unavailable_regions, "usage");
|
|
1855
|
+
}
|
|
1315
1856
|
async function usageShowCommand(deps, options = {}) {
|
|
1316
1857
|
const usage = await deps.api.getUsage(await deps.auth.apiKey(), options.period);
|
|
1317
1858
|
print(
|
|
@@ -1336,60 +1877,6 @@ async function usageShowCommand(deps, options = {}) {
|
|
|
1336
1877
|
);
|
|
1337
1878
|
}
|
|
1338
1879
|
|
|
1339
|
-
// src/progress.ts
|
|
1340
|
-
var FRAMES = ["|", "/", "-", "\\"];
|
|
1341
|
-
var TerminalProgress = class {
|
|
1342
|
-
constructor(stream, intervalMs = 80) {
|
|
1343
|
-
this.stream = stream;
|
|
1344
|
-
this.intervalMs = intervalMs;
|
|
1345
|
-
}
|
|
1346
|
-
activeMessage;
|
|
1347
|
-
frame = 0;
|
|
1348
|
-
lastWidth = 0;
|
|
1349
|
-
timer;
|
|
1350
|
-
start(message) {
|
|
1351
|
-
if (this.activeMessage) this.succeed();
|
|
1352
|
-
this.activeMessage = message;
|
|
1353
|
-
this.frame = 0;
|
|
1354
|
-
if (!this.stream.isTTY) {
|
|
1355
|
-
this.stream.write(`[..] ${message}
|
|
1356
|
-
`);
|
|
1357
|
-
return;
|
|
1358
|
-
}
|
|
1359
|
-
this.render(`[${FRAMES[this.frame]}] ${message}`);
|
|
1360
|
-
this.timer = setInterval(() => {
|
|
1361
|
-
this.frame = (this.frame + 1) % FRAMES.length;
|
|
1362
|
-
this.render(`[${FRAMES[this.frame]}] ${this.activeMessage}`);
|
|
1363
|
-
}, this.intervalMs);
|
|
1364
|
-
this.timer.unref();
|
|
1365
|
-
}
|
|
1366
|
-
succeed(message = this.activeMessage) {
|
|
1367
|
-
this.finish("ok", message);
|
|
1368
|
-
}
|
|
1369
|
-
fail(message = this.activeMessage) {
|
|
1370
|
-
this.finish("!!", message);
|
|
1371
|
-
}
|
|
1372
|
-
finish(marker, message) {
|
|
1373
|
-
if (this.timer) clearInterval(this.timer);
|
|
1374
|
-
this.timer = void 0;
|
|
1375
|
-
this.activeMessage = void 0;
|
|
1376
|
-
if (!message) return;
|
|
1377
|
-
const line = `[${marker}] ${message}`;
|
|
1378
|
-
if (this.stream.isTTY) {
|
|
1379
|
-
this.render(line);
|
|
1380
|
-
this.stream.write("\n");
|
|
1381
|
-
this.lastWidth = 0;
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
|
-
this.stream.write(`${line}
|
|
1385
|
-
`);
|
|
1386
|
-
}
|
|
1387
|
-
render(value) {
|
|
1388
|
-
this.stream.write(`\r${value.padEnd(this.lastWidth)}`);
|
|
1389
|
-
this.lastWidth = value.length;
|
|
1390
|
-
}
|
|
1391
|
-
};
|
|
1392
|
-
|
|
1393
1880
|
// src/prompt.ts
|
|
1394
1881
|
import select from "@inquirer/select";
|
|
1395
1882
|
import { createInterface } from "node:readline/promises";
|
|
@@ -1419,18 +1906,6 @@ async function terminalAsk(question) {
|
|
|
1419
1906
|
var packageVersion = JSON.parse(
|
|
1420
1907
|
readFileSync(new URL("../../package.json", import.meta.url), "utf8")
|
|
1421
1908
|
);
|
|
1422
|
-
function apiOrigin(configured = process.env.TONBO_API_ORIGIN) {
|
|
1423
|
-
const url = new URL(configured?.trim() || DEFAULT_API_ORIGIN);
|
|
1424
|
-
const loopback = ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
|
|
1425
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback))
|
|
1426
|
-
throw new Error("TONBO_API_ORIGIN must be an https origin.");
|
|
1427
|
-
return url.origin;
|
|
1428
|
-
}
|
|
1429
|
-
function loginOrigin(api) {
|
|
1430
|
-
const url = new URL(api);
|
|
1431
|
-
if (url.hostname.startsWith("api.")) url.hostname = url.hostname.slice("api.".length);
|
|
1432
|
-
return url.origin;
|
|
1433
|
-
}
|
|
1434
1909
|
function createDependencies() {
|
|
1435
1910
|
const api = apiOrigin();
|
|
1436
1911
|
const credentials = new FileCredentialStore();
|
|
@@ -1458,6 +1933,7 @@ function operationOptions(command, operationId) {
|
|
|
1458
1933
|
command.option("--cursor <cursor>", "continue from the cursor a previous page printed");
|
|
1459
1934
|
return command.option("--json", "print the API data as JSON");
|
|
1460
1935
|
}
|
|
1936
|
+
var repeated = (value, previous = []) => [...previous, value];
|
|
1461
1937
|
var roleOption = (description) => new Option("--role <role>", description).choices(accountRoles);
|
|
1462
1938
|
function createProgram(dependencies = createDependencies) {
|
|
1463
1939
|
const program = new Command().name("tonbo").description("Sign in to Tonbo and manage account resources.").version(packageVersion.version);
|
|
@@ -1513,7 +1989,15 @@ function createProgram(dependencies = createDependencies) {
|
|
|
1513
1989
|
).option("--read-only", "narrow the key to read-only actions, for its whole life").option(
|
|
1514
1990
|
"--bucket <name>",
|
|
1515
1991
|
"narrow the key to this bucket, for its whole life; repeat for several",
|
|
1516
|
-
|
|
1992
|
+
repeated
|
|
1993
|
+
).option(
|
|
1994
|
+
"--database <name>",
|
|
1995
|
+
"narrow the key to this database, for its whole life; repeat for several; never with --bucket",
|
|
1996
|
+
repeated
|
|
1997
|
+
).option(
|
|
1998
|
+
"--database-prefix <prefix>",
|
|
1999
|
+
"narrow the key to the databases whose name starts with this, for its whole life; repeat for several; never with --bucket",
|
|
2000
|
+
repeated
|
|
1517
2001
|
).addOption(
|
|
1518
2002
|
new Option("--expires-in <period>", "when the key expires; never by default").choices(
|
|
1519
2003
|
apiKeyExpirations
|
|
@@ -1563,6 +2047,62 @@ function createProgram(dependencies = createDependencies) {
|
|
|
1563
2047
|
).action(
|
|
1564
2048
|
async (name, options) => bucketDeleteCommand(dependencies(), name, options)
|
|
1565
2049
|
);
|
|
2050
|
+
const database = program.command("database").description("the account's databases");
|
|
2051
|
+
operationOptions(
|
|
2052
|
+
database.command("list").description("list the databases in every region").option("--prefix <prefix>", "only databases whose name starts with this"),
|
|
2053
|
+
"listDatabases"
|
|
2054
|
+
).action(
|
|
2055
|
+
async (options) => databaseListCommand(dependencies(), options)
|
|
2056
|
+
);
|
|
2057
|
+
operationOptions(
|
|
2058
|
+
database.command("create <name>").description("create a database; its region is fixed for its life").option(
|
|
2059
|
+
"--region <region>",
|
|
2060
|
+
"the database's region (see tonbo region list); the database default region by default"
|
|
2061
|
+
).option(
|
|
2062
|
+
"--idempotency-key <key>",
|
|
2063
|
+
"reuse the key of an unconfirmed creation to get the database it created"
|
|
2064
|
+
),
|
|
2065
|
+
"createDatabase"
|
|
2066
|
+
).action(
|
|
2067
|
+
async (name, options) => databaseCreateCommand(dependencies(), name, options)
|
|
2068
|
+
);
|
|
2069
|
+
operationOptions(
|
|
2070
|
+
database.command("show <name>").description("show a database, its URL and size"),
|
|
2071
|
+
"getDatabase"
|
|
2072
|
+
).action(
|
|
2073
|
+
async (name, options) => databaseShowCommand(dependencies(), name, options)
|
|
2074
|
+
);
|
|
2075
|
+
operationOptions(
|
|
2076
|
+
database.command("update <name>").description("publish a database for anonymous reads, or take it private (owners and admins)").addOption(new Option("--public", "serve anonymous reads").conflicts("private")).addOption(new Option("--private", "serve only the account's keys").conflicts("public")),
|
|
2077
|
+
"updateDatabase"
|
|
2078
|
+
).action(
|
|
2079
|
+
async (name, options) => databaseUpdateCommand(dependencies(), name, options)
|
|
2080
|
+
);
|
|
2081
|
+
operationOptions(
|
|
2082
|
+
database.command("delete <name>").description("delete a database and everything in it").option("--yes", "delete without asking; required when stdin is not a terminal"),
|
|
2083
|
+
"deleteDatabase"
|
|
2084
|
+
).action(
|
|
2085
|
+
async (name, options) => databaseDeleteCommand(dependencies(), name, options)
|
|
2086
|
+
);
|
|
2087
|
+
operationOptions(
|
|
2088
|
+
database.command("usage").description("each database's usage of one month at list price, before allowance and credit").option(
|
|
2089
|
+
"--period <month>",
|
|
2090
|
+
"the calendar month in UTC, as YYYY-MM; the current one by default"
|
|
2091
|
+
).option(
|
|
2092
|
+
"--sort <order>",
|
|
2093
|
+
"list_value (highest first, the default) or name",
|
|
2094
|
+
(value) => {
|
|
2095
|
+
try {
|
|
2096
|
+
return databaseUsageSort(value);
|
|
2097
|
+
} catch (error) {
|
|
2098
|
+
throw new InvalidArgumentError(error.message);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
).option("--prefix <prefix>", "only databases whose name starts with this"),
|
|
2102
|
+
"listDatabaseUsage"
|
|
2103
|
+
).action(
|
|
2104
|
+
async (options) => databaseUsageCommand(dependencies(), options)
|
|
2105
|
+
);
|
|
1566
2106
|
operationOptions(
|
|
1567
2107
|
program.command("usage").description("the account's rated usage").command("show").description("show one month's rated usage").option(
|
|
1568
2108
|
"--period <month>",
|