@tonbo/cli 0.15.0 → 0.16.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 +1 -1
- package/dist/bin/tonbo.js +526 -161
- package/package.json +2 -2
package/dist/bin/tonbo.js
CHANGED
|
@@ -174,6 +174,42 @@ var managementOperations = {
|
|
|
174
174
|
"idempotencyKey": false,
|
|
175
175
|
"cli": "tonbo account show"
|
|
176
176
|
},
|
|
177
|
+
"listBudgets": {
|
|
178
|
+
"method": "GET",
|
|
179
|
+
"path": "/budgets",
|
|
180
|
+
"pathParams": [],
|
|
181
|
+
"query": [],
|
|
182
|
+
"idempotencyKey": false,
|
|
183
|
+
"cli": "tonbo budget list"
|
|
184
|
+
},
|
|
185
|
+
"createBudget": {
|
|
186
|
+
"method": "POST",
|
|
187
|
+
"path": "/budgets",
|
|
188
|
+
"pathParams": [],
|
|
189
|
+
"query": [],
|
|
190
|
+
"idempotencyKey": true,
|
|
191
|
+
"cli": "tonbo budget add"
|
|
192
|
+
},
|
|
193
|
+
"getBudget": {
|
|
194
|
+
"method": "GET",
|
|
195
|
+
"path": "/budgets/{budget_id}",
|
|
196
|
+
"pathParams": [
|
|
197
|
+
"budget_id"
|
|
198
|
+
],
|
|
199
|
+
"query": [],
|
|
200
|
+
"idempotencyKey": false,
|
|
201
|
+
"cli": "tonbo budget show"
|
|
202
|
+
},
|
|
203
|
+
"changeBudget": {
|
|
204
|
+
"method": "PATCH",
|
|
205
|
+
"path": "/budgets/{budget_id}",
|
|
206
|
+
"pathParams": [
|
|
207
|
+
"budget_id"
|
|
208
|
+
],
|
|
209
|
+
"query": [],
|
|
210
|
+
"idempotencyKey": false,
|
|
211
|
+
"cli": "tonbo budget set"
|
|
212
|
+
},
|
|
177
213
|
"listRegions": {
|
|
178
214
|
"method": "GET",
|
|
179
215
|
"path": "/regions",
|
|
@@ -438,6 +474,13 @@ var managementOperations = {
|
|
|
438
474
|
"query": [],
|
|
439
475
|
"idempotencyKey": false
|
|
440
476
|
},
|
|
477
|
+
"renewHandoffHolds": {
|
|
478
|
+
"method": "POST",
|
|
479
|
+
"path": "/yield/handoffs/holds",
|
|
480
|
+
"pathParams": [],
|
|
481
|
+
"query": [],
|
|
482
|
+
"idempotencyKey": false
|
|
483
|
+
},
|
|
441
484
|
"showPersonalHandoff": {
|
|
442
485
|
"method": "GET",
|
|
443
486
|
"path": "/yield/handoffs/{id}",
|
|
@@ -1179,42 +1222,6 @@ var managementOperations = {
|
|
|
1179
1222
|
}
|
|
1180
1223
|
};
|
|
1181
1224
|
var plannedManagementOperations = {
|
|
1182
|
-
"listBudgets": {
|
|
1183
|
-
"method": "GET",
|
|
1184
|
-
"path": "/budgets",
|
|
1185
|
-
"pathParams": [],
|
|
1186
|
-
"query": [],
|
|
1187
|
-
"idempotencyKey": false,
|
|
1188
|
-
"cli": "tonbo budget list"
|
|
1189
|
-
},
|
|
1190
|
-
"createBudget": {
|
|
1191
|
-
"method": "POST",
|
|
1192
|
-
"path": "/budgets",
|
|
1193
|
-
"pathParams": [],
|
|
1194
|
-
"query": [],
|
|
1195
|
-
"idempotencyKey": true,
|
|
1196
|
-
"cli": "tonbo budget add"
|
|
1197
|
-
},
|
|
1198
|
-
"getBudget": {
|
|
1199
|
-
"method": "GET",
|
|
1200
|
-
"path": "/budgets/{budget_id}",
|
|
1201
|
-
"pathParams": [
|
|
1202
|
-
"budget_id"
|
|
1203
|
-
],
|
|
1204
|
-
"query": [],
|
|
1205
|
-
"idempotencyKey": false,
|
|
1206
|
-
"cli": "tonbo budget show"
|
|
1207
|
-
},
|
|
1208
|
-
"changeBudget": {
|
|
1209
|
-
"method": "PATCH",
|
|
1210
|
-
"path": "/budgets/{budget_id}",
|
|
1211
|
-
"pathParams": [
|
|
1212
|
-
"budget_id"
|
|
1213
|
-
],
|
|
1214
|
-
"query": [],
|
|
1215
|
-
"idempotencyKey": false,
|
|
1216
|
-
"cli": "tonbo budget set"
|
|
1217
|
-
},
|
|
1218
1225
|
"getMachineActivation": {
|
|
1219
1226
|
"method": "GET",
|
|
1220
1227
|
"path": "/machines/{name}/activations/{activation}",
|
|
@@ -1983,19 +1990,29 @@ var MANAGEMENT_AUDIENCE = cliSessionContract.audiences.management;
|
|
|
1983
1990
|
function isAccountBearer(bearer) {
|
|
1984
1991
|
return typeof bearer !== "string";
|
|
1985
1992
|
}
|
|
1993
|
+
function retriesAfterRefusal(bearer, audience, code) {
|
|
1994
|
+
if (!isAccountBearer(bearer))
|
|
1995
|
+
return false;
|
|
1996
|
+
if (bearer.kind === "key")
|
|
1997
|
+
return bearer.refused(audience, code);
|
|
1998
|
+
bearer.invalidate(audience);
|
|
1999
|
+
return true;
|
|
2000
|
+
}
|
|
1986
2001
|
function managementBearer(bearer) {
|
|
1987
2002
|
return isAccountBearer(bearer) ? bearer.token(MANAGEMENT_AUDIENCE) : Promise.resolve(bearer);
|
|
1988
2003
|
}
|
|
1989
2004
|
|
|
1990
2005
|
// ../../packages/tonbo-cli-core/dist/src/access-token.js
|
|
1991
2006
|
var DEFAULT_LOGIN_HINT = credentialsContract.reader.login_hint;
|
|
2007
|
+
var STREAMS_AUDIENCE = "https://streams.tonbo.dev";
|
|
1992
2008
|
var GIT_AUDIENCE = "https://git.tonbo.dev";
|
|
2009
|
+
var REFRESH_MARGIN_MS = 6e4;
|
|
1993
2010
|
var AccessTokenError = class extends Error {
|
|
1994
2011
|
status;
|
|
1995
2012
|
retryAfterMs;
|
|
1996
2013
|
code;
|
|
1997
|
-
constructor(message, status, retryAfterMs = null, code = null) {
|
|
1998
|
-
super(message);
|
|
2014
|
+
constructor(message, status, retryAfterMs = null, code = null, options) {
|
|
2015
|
+
super(message, options);
|
|
1999
2016
|
this.status = status;
|
|
2000
2017
|
this.retryAfterMs = retryAfterMs;
|
|
2001
2018
|
this.code = code;
|
|
@@ -2004,6 +2021,116 @@ var AccessTokenError = class extends Error {
|
|
|
2004
2021
|
return this.status === null || this.status === 429 || this.status >= 500;
|
|
2005
2022
|
}
|
|
2006
2023
|
};
|
|
2024
|
+
var AccessTokens = class {
|
|
2025
|
+
fetcher;
|
|
2026
|
+
userOrigin;
|
|
2027
|
+
audience;
|
|
2028
|
+
loginHint;
|
|
2029
|
+
now;
|
|
2030
|
+
cached = null;
|
|
2031
|
+
/** The account bearer the last token was of, which `invalidate` forgets. */
|
|
2032
|
+
last = null;
|
|
2033
|
+
constructor(fetcher, userOrigin, audience = STREAMS_AUDIENCE, loginHint = DEFAULT_LOGIN_HINT, now = () => Date.now()) {
|
|
2034
|
+
this.fetcher = fetcher;
|
|
2035
|
+
this.userOrigin = userOrigin;
|
|
2036
|
+
this.audience = audience;
|
|
2037
|
+
this.loginHint = loginHint;
|
|
2038
|
+
this.now = now;
|
|
2039
|
+
}
|
|
2040
|
+
async token(key) {
|
|
2041
|
+
if (isAccountBearer(key)) {
|
|
2042
|
+
this.last = key;
|
|
2043
|
+
return key.token(this.audience);
|
|
2044
|
+
}
|
|
2045
|
+
if (this.cached && this.cached.key === key && this.cached.refreshAt > this.now())
|
|
2046
|
+
return this.cached.token;
|
|
2047
|
+
const issuedAt = this.now();
|
|
2048
|
+
const what = this.audience === GIT_AUDIENCE ? "git" : this.audience === MANAGEMENT_AUDIENCE ? "Management" : "Streams";
|
|
2049
|
+
let response;
|
|
2050
|
+
try {
|
|
2051
|
+
response = await this.fetcher(`${this.userOrigin}/api/iam/token`, {
|
|
2052
|
+
method: "POST",
|
|
2053
|
+
redirect: "manual",
|
|
2054
|
+
signal: AbortSignal.timeout(3e4),
|
|
2055
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
2056
|
+
body: JSON.stringify({ audience: this.audience })
|
|
2057
|
+
});
|
|
2058
|
+
} catch (error) {
|
|
2059
|
+
throw new AccessTokenError(`The ${what} token exchange failed: ${error instanceof Error ? error.message : String(error)}`, null, null, null, { cause: error });
|
|
2060
|
+
}
|
|
2061
|
+
if (!response.ok) {
|
|
2062
|
+
const problem = await response.json().catch(() => null);
|
|
2063
|
+
const retryAfter = Number(response.headers.get("retry-after") ?? "");
|
|
2064
|
+
throw new AccessTokenError(response.status === 401 ? `Your Tonbo login was refused. Run \`${this.loginHint}\`.` : `The ${what} token exchange answered ${response.status}.`, response.status, Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1e3 : null, typeof problem?.code === "string" ? problem.code : null);
|
|
2065
|
+
}
|
|
2066
|
+
const body = await response.json().catch(() => null);
|
|
2067
|
+
if (typeof body?.access_token !== "string" || !body.access_token || typeof body.expires_in !== "number" || !(body.expires_in > 0))
|
|
2068
|
+
throw new AccessTokenError(`The ${what} token exchange answered no token.`, response.status);
|
|
2069
|
+
this.cached = {
|
|
2070
|
+
key,
|
|
2071
|
+
token: body.access_token,
|
|
2072
|
+
refreshAt: issuedAt + Math.max(0, body.expires_in * 1e3 - REFRESH_MARGIN_MS)
|
|
2073
|
+
};
|
|
2074
|
+
return body.access_token;
|
|
2075
|
+
}
|
|
2076
|
+
/** Forgets the token, after the resource server refused it. */
|
|
2077
|
+
invalidate() {
|
|
2078
|
+
this.cached = null;
|
|
2079
|
+
this.last?.invalidate(this.audience);
|
|
2080
|
+
}
|
|
2081
|
+
/** Sends a request with the token; a 401 drops the token and sends once
|
|
2082
|
+
* more with a newly exchanged one, whose answer is final. */
|
|
2083
|
+
async send(key, request) {
|
|
2084
|
+
const response = await request(await this.token(key));
|
|
2085
|
+
if (response.status !== 401)
|
|
2086
|
+
return response;
|
|
2087
|
+
await response.body?.cancel();
|
|
2088
|
+
if (isAccountBearer(key))
|
|
2089
|
+
key.invalidate(this.audience);
|
|
2090
|
+
else
|
|
2091
|
+
this.invalidate();
|
|
2092
|
+
return request(await this.token(key));
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
function apiKeyBearer(key, fetcher, userOrigin, now = () => Date.now()) {
|
|
2096
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
2097
|
+
let exchanging = false;
|
|
2098
|
+
const tokensOf = (audience) => {
|
|
2099
|
+
let found = tokens.get(audience);
|
|
2100
|
+
if (!found) {
|
|
2101
|
+
found = new AccessTokens(fetcher, userOrigin, audience, void 0, now);
|
|
2102
|
+
tokens.set(audience, found);
|
|
2103
|
+
}
|
|
2104
|
+
return found;
|
|
2105
|
+
};
|
|
2106
|
+
return {
|
|
2107
|
+
kind: "key",
|
|
2108
|
+
async token(audience) {
|
|
2109
|
+
if (audience === MANAGEMENT_AUDIENCE && !exchanging)
|
|
2110
|
+
return key;
|
|
2111
|
+
try {
|
|
2112
|
+
return await tokensOf(audience).token(key);
|
|
2113
|
+
} catch (error) {
|
|
2114
|
+
if (!(error instanceof AccessTokenError) || error.status !== 401)
|
|
2115
|
+
throw error;
|
|
2116
|
+
throw new AccessTokenError("TONBO_API_KEY was refused (HTTP 401). Check that the key exists and is not revoked or expired.", 401, null, error.code);
|
|
2117
|
+
}
|
|
2118
|
+
},
|
|
2119
|
+
invalidate(audience) {
|
|
2120
|
+
tokens.get(audience)?.invalidate();
|
|
2121
|
+
},
|
|
2122
|
+
refused(audience, code) {
|
|
2123
|
+
if (audience === MANAGEMENT_AUDIENCE && !exchanging) {
|
|
2124
|
+
if (code !== "exchange_required")
|
|
2125
|
+
return false;
|
|
2126
|
+
exchanging = true;
|
|
2127
|
+
return true;
|
|
2128
|
+
}
|
|
2129
|
+
tokens.get(audience)?.invalidate();
|
|
2130
|
+
return true;
|
|
2131
|
+
}
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2007
2134
|
|
|
2008
2135
|
// ../../packages/tonbo-cli-core/dist/src/http.js
|
|
2009
2136
|
var HttpError = class extends Error {
|
|
@@ -2029,11 +2156,11 @@ async function requestJson(fetcher, url, init = {}) {
|
|
|
2029
2156
|
if (response.ok)
|
|
2030
2157
|
return body;
|
|
2031
2158
|
const record = body && typeof body === "object" ? body : {};
|
|
2032
|
-
const
|
|
2159
|
+
const reason2 = [record.detail, record.title, record.error].find((value) => typeof value === "string" && value.length > 0);
|
|
2033
2160
|
const requestId = response.headers.get("x-request-id") ?? (typeof record.request_id === "string" ? record.request_id : null);
|
|
2034
2161
|
const issues = Array.isArray(record.errors) ? record.errors.filter((issue) => typeof issue?.field === "string" && typeof issue.code === "string").map((issue) => `${issue.field}: ${issue.code}`) : [];
|
|
2035
2162
|
const message = [
|
|
2036
|
-
|
|
2163
|
+
reason2 ?? `Request failed with HTTP ${response.status}.`,
|
|
2037
2164
|
...issues.length ? [`(${issues.join(", ")})`] : []
|
|
2038
2165
|
].join(" ");
|
|
2039
2166
|
throw new HttpError(requestId ? `${message} (request ${requestId})` : message, response.status, body, requestId);
|
|
@@ -2051,9 +2178,9 @@ async function presented(key) {
|
|
|
2051
2178
|
try {
|
|
2052
2179
|
return await managementBearer(key);
|
|
2053
2180
|
} catch (error) {
|
|
2054
|
-
if (!(error instanceof AccessTokenError))
|
|
2181
|
+
if (!(error instanceof AccessTokenError) || error.status === null)
|
|
2055
2182
|
throw error;
|
|
2056
|
-
throw new ExchangeRefusal(error.message, error.status
|
|
2183
|
+
throw new ExchangeRefusal(error.message, error.status, { code: error.code }, null);
|
|
2057
2184
|
}
|
|
2058
2185
|
}
|
|
2059
2186
|
var ManagementApi = class {
|
|
@@ -2081,15 +2208,14 @@ var ManagementApi = class {
|
|
|
2081
2208
|
try {
|
|
2082
2209
|
return await attempt();
|
|
2083
2210
|
} catch (error) {
|
|
2084
|
-
if (!
|
|
2211
|
+
if (!(error instanceof HttpError) || error.status !== 401 || error instanceof ExchangeRefusal || !retriesAfterRefusal(key, MANAGEMENT_AUDIENCE, error.code))
|
|
2085
2212
|
throw error;
|
|
2086
|
-
key.invalidate(MANAGEMENT_AUDIENCE);
|
|
2087
2213
|
return attempt();
|
|
2088
2214
|
}
|
|
2089
2215
|
}
|
|
2090
2216
|
async requestWith(operationId, key, { params = {}, query = {}, body, idempotencyKey, ifMatch }, seen) {
|
|
2091
2217
|
const operation = managementOperation(operationId);
|
|
2092
|
-
const
|
|
2218
|
+
const path5 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
2093
2219
|
const value = params[name];
|
|
2094
2220
|
if (value === void 0)
|
|
2095
2221
|
throw new Error(`${operationId} needs the path parameter ${name}.`);
|
|
@@ -2113,7 +2239,7 @@ var ManagementApi = class {
|
|
|
2113
2239
|
if (ifMatch)
|
|
2114
2240
|
headers["if-match"] = ifMatch;
|
|
2115
2241
|
const queryString = search.toString();
|
|
2116
|
-
const url = `${this.apiOrigin}${managementApiBasePath}${
|
|
2242
|
+
const url = `${this.apiOrigin}${managementApiBasePath}${path5}${queryString ? `?${queryString}` : ""}`;
|
|
2117
2243
|
try {
|
|
2118
2244
|
const fetcher = seen ? async (input, init) => {
|
|
2119
2245
|
const response = await this.fetcher(input, init);
|
|
@@ -2781,17 +2907,20 @@ function accountLogins(login) {
|
|
|
2781
2907
|
});
|
|
2782
2908
|
return entries;
|
|
2783
2909
|
}
|
|
2910
|
+
function personalLogin(login) {
|
|
2911
|
+
return accountLogins(login).find((entry) => entry.kind === "user") ?? null;
|
|
2912
|
+
}
|
|
2784
2913
|
async function preparePrivateDirectory(directory) {
|
|
2785
|
-
const
|
|
2786
|
-
if (
|
|
2914
|
+
const stat2 = await lstat(directory);
|
|
2915
|
+
if (stat2.isSymbolicLink() || !stat2.isDirectory())
|
|
2787
2916
|
throw new Error("Tonbo config directory must be a real directory.");
|
|
2788
2917
|
if (process.platform !== "win32")
|
|
2789
2918
|
await chmod(directory, DIRECTORY_MODE);
|
|
2790
2919
|
}
|
|
2791
2920
|
async function assertExistingDestinationIsSafe(filename) {
|
|
2792
2921
|
try {
|
|
2793
|
-
const
|
|
2794
|
-
if (
|
|
2922
|
+
const stat2 = await lstat(filename);
|
|
2923
|
+
if (stat2.isSymbolicLink() || !stat2.isFile())
|
|
2795
2924
|
throw new Error("Tonbo credential path must be a regular file.");
|
|
2796
2925
|
} catch (error) {
|
|
2797
2926
|
if (error.code !== "ENOENT")
|
|
@@ -2799,17 +2928,17 @@ async function assertExistingDestinationIsSafe(filename) {
|
|
|
2799
2928
|
}
|
|
2800
2929
|
}
|
|
2801
2930
|
async function assertPrivateRegularFile(filename) {
|
|
2802
|
-
const
|
|
2803
|
-
if (
|
|
2931
|
+
const stat2 = await lstat(filename);
|
|
2932
|
+
if (stat2.isSymbolicLink() || !stat2.isFile())
|
|
2804
2933
|
throw new Error("Tonbo credential path must be a regular file.");
|
|
2805
|
-
if (process.platform !== "win32" && (
|
|
2934
|
+
if (process.platform !== "win32" && (stat2.mode & REFUSED_MODE_BITS) !== 0)
|
|
2806
2935
|
throw new Error(`Tonbo credential file must have mode ${credentialsContract.permissions.file_mode}.`);
|
|
2807
2936
|
}
|
|
2808
2937
|
|
|
2809
2938
|
// ../../packages/tonbo-cli-core/dist/src/device.js
|
|
2810
2939
|
import { execFile } from "node:child_process";
|
|
2811
2940
|
import { createHash, randomUUID as randomUUID2 } from "node:crypto";
|
|
2812
|
-
import { link, mkdir as mkdir2, open as open2, readFile as readFile2, rename as rename2, rm as rm2 } from "node:fs/promises";
|
|
2941
|
+
import { link, mkdir as mkdir2, open as open2, readFile as readFile2, rename as rename2, rm as rm2, stat } from "node:fs/promises";
|
|
2813
2942
|
import os2 from "node:os";
|
|
2814
2943
|
import path2 from "node:path";
|
|
2815
2944
|
import { promisify } from "node:util";
|
|
@@ -2836,6 +2965,14 @@ async function readMachineId(platform = process.platform) {
|
|
|
2836
2965
|
function machineIdHash(machineId) {
|
|
2837
2966
|
return createHash("sha256").update(machineId).digest("hex");
|
|
2838
2967
|
}
|
|
2968
|
+
var NO_HARD_LINKS = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EMLINK", "EXDEV"]);
|
|
2969
|
+
var CREATE_POLL_MS = 50;
|
|
2970
|
+
var CREATE_STALE_MS = 5e3;
|
|
2971
|
+
var exists = (file) => stat(file).then(() => true, (error) => {
|
|
2972
|
+
if (error.code === "ENOENT")
|
|
2973
|
+
return false;
|
|
2974
|
+
throw error;
|
|
2975
|
+
});
|
|
2839
2976
|
var DeviceStore = class {
|
|
2840
2977
|
filename;
|
|
2841
2978
|
machineId;
|
|
@@ -2913,13 +3050,49 @@ var DeviceStore = class {
|
|
|
2913
3050
|
await link(temporary, this.filename);
|
|
2914
3051
|
return true;
|
|
2915
3052
|
} catch (error) {
|
|
2916
|
-
|
|
3053
|
+
const code = error.code;
|
|
3054
|
+
if (!replace && code === "EEXIST")
|
|
2917
3055
|
return false;
|
|
3056
|
+
if (!replace && code && NO_HARD_LINKS.has(code))
|
|
3057
|
+
return await this.create(temporary);
|
|
2918
3058
|
throw error;
|
|
2919
3059
|
} finally {
|
|
2920
3060
|
await rm2(temporary, { force: true });
|
|
2921
3061
|
}
|
|
2922
3062
|
}
|
|
3063
|
+
/** The first record on a filesystem without hard links (some FUSE and
|
|
3064
|
+
* network volumes), from the complete, synced `temporary`: renamed into
|
|
3065
|
+
* place, so the file is never seen empty or partial, while holding a lock
|
|
3066
|
+
* directory made exclusively, so of two first runs at once only one
|
|
3067
|
+
* writes it and the other waits for it. A lock left by a run that died
|
|
3068
|
+
* holding it is taken over after a while. */
|
|
3069
|
+
async create(temporary) {
|
|
3070
|
+
const lock = `${this.filename}.lock`;
|
|
3071
|
+
for (let waited = 0; ; waited += CREATE_POLL_MS) {
|
|
3072
|
+
try {
|
|
3073
|
+
await mkdir2(lock, { mode: 448 });
|
|
3074
|
+
break;
|
|
3075
|
+
} catch (error) {
|
|
3076
|
+
if (error.code !== "EEXIST")
|
|
3077
|
+
throw error;
|
|
3078
|
+
}
|
|
3079
|
+
if (await exists(this.filename))
|
|
3080
|
+
return false;
|
|
3081
|
+
if (waited >= CREATE_STALE_MS) {
|
|
3082
|
+
await rm2(lock, { recursive: true, force: true });
|
|
3083
|
+
waited = -CREATE_POLL_MS;
|
|
3084
|
+
} else
|
|
3085
|
+
await new Promise((resolve) => setTimeout(resolve, CREATE_POLL_MS));
|
|
3086
|
+
}
|
|
3087
|
+
try {
|
|
3088
|
+
if (await exists(this.filename))
|
|
3089
|
+
return false;
|
|
3090
|
+
await rename2(temporary, this.filename);
|
|
3091
|
+
return true;
|
|
3092
|
+
} finally {
|
|
3093
|
+
await rm2(lock, { recursive: true, force: true });
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
2923
3096
|
};
|
|
2924
3097
|
|
|
2925
3098
|
// ../../packages/tonbo-cli-core/dist/src/session.js
|
|
@@ -2964,7 +3137,7 @@ var FileSessionStore = class {
|
|
|
2964
3137
|
await removePrivateFile(this.filename);
|
|
2965
3138
|
}
|
|
2966
3139
|
};
|
|
2967
|
-
var
|
|
3140
|
+
var REFRESH_MARGIN_MS2 = 6e4;
|
|
2968
3141
|
var clients = /* @__PURE__ */ new WeakMap();
|
|
2969
3142
|
var SessionClient = class _SessionClient {
|
|
2970
3143
|
stored;
|
|
@@ -3059,7 +3232,7 @@ var SessionClient = class _SessionClient {
|
|
|
3059
3232
|
}, what, accountId);
|
|
3060
3233
|
this.tokens.set(`${audience} ${accountId}`, {
|
|
3061
3234
|
token: body.access_token,
|
|
3062
|
-
refreshAt: issuedAt + Math.max(0, body.expires_in * 1e3 -
|
|
3235
|
+
refreshAt: issuedAt + Math.max(0, body.expires_in * 1e3 - REFRESH_MARGIN_MS2)
|
|
3063
3236
|
});
|
|
3064
3237
|
return body.access_token;
|
|
3065
3238
|
}
|
|
@@ -3077,7 +3250,7 @@ var SessionClient = class _SessionClient {
|
|
|
3077
3250
|
body: JSON.stringify(fields2)
|
|
3078
3251
|
});
|
|
3079
3252
|
} catch (error) {
|
|
3080
|
-
throw new AccessTokenError(`The ${what} token exchange failed: ${error instanceof Error ? error.message : String(error)}`, null);
|
|
3253
|
+
throw new AccessTokenError(`The ${what} token exchange failed: ${error instanceof Error ? error.message : String(error)}`, null, null, null, { cause: error });
|
|
3081
3254
|
}
|
|
3082
3255
|
if (!response.ok)
|
|
3083
3256
|
throw await this.refusal(response, what, accountId);
|
|
@@ -3118,7 +3291,7 @@ var SessionClient = class _SessionClient {
|
|
|
3118
3291
|
headers: { authorization: `Bearer ${token}`, accept: "application/json" }
|
|
3119
3292
|
});
|
|
3120
3293
|
} catch (error) {
|
|
3121
|
-
throw new AccessTokenError(`Reading ${this.accountName(accountId)}'s git workspaces failed: ${error instanceof Error ? error.message : String(error)}`, null);
|
|
3294
|
+
throw new AccessTokenError(`Reading ${this.accountName(accountId)}'s git workspaces failed: ${error instanceof Error ? error.message : String(error)}`, null, null, null, { cause: error });
|
|
3122
3295
|
}
|
|
3123
3296
|
const body = await response.json().catch(() => null);
|
|
3124
3297
|
if (!response.ok)
|
|
@@ -3143,7 +3316,7 @@ var SessionClient = class _SessionClient {
|
|
|
3143
3316
|
}
|
|
3144
3317
|
});
|
|
3145
3318
|
} catch (error) {
|
|
3146
|
-
throw new AccessTokenError(`Reading your accounts failed: ${error instanceof Error ? error.message : String(error)}`, null);
|
|
3319
|
+
throw new AccessTokenError(`Reading your accounts failed: ${error instanceof Error ? error.message : String(error)}`, null, null, null, { cause: error });
|
|
3147
3320
|
}
|
|
3148
3321
|
if (!response.ok)
|
|
3149
3322
|
throw await this.refusal(response, "session", "");
|
|
@@ -3225,17 +3398,23 @@ var LoginStore = class {
|
|
|
3225
3398
|
await this.sessions.remove();
|
|
3226
3399
|
await this.legacy.remove();
|
|
3227
3400
|
}
|
|
3228
|
-
/** The person's accounts now, saved as the login's cache of names.
|
|
3401
|
+
/** The person's accounts now, saved as the login's cache of names. `client`
|
|
3402
|
+
* holds them before the save, so when the file cannot be written (an
|
|
3403
|
+
* agent's sandbox) `current(client)` still answers with them. */
|
|
3229
3404
|
async refreshAccounts(client) {
|
|
3230
3405
|
const accounts = await client.accounts();
|
|
3231
3406
|
const latest = await this.sessions.load() ?? client.stored;
|
|
3232
3407
|
if (latest.session.id === client.stored.session.id) {
|
|
3233
3408
|
const saved = withAccounts(latest, accounts);
|
|
3234
|
-
await this.sessions.save(saved);
|
|
3235
3409
|
client.stored = saved;
|
|
3410
|
+
await this.sessions.save(saved);
|
|
3236
3411
|
}
|
|
3237
3412
|
return accounts;
|
|
3238
3413
|
}
|
|
3414
|
+
/** The logins `client` holds in this process, without reading the file again. */
|
|
3415
|
+
current(client) {
|
|
3416
|
+
return this.view(client);
|
|
3417
|
+
}
|
|
3239
3418
|
/** Forgets an account the person no longer belongs to (they left it, or
|
|
3240
3419
|
* were removed): the login's cache of names is read again without it, it
|
|
3241
3420
|
* is no longer the default account, and this process's tokens of it are
|
|
@@ -3316,11 +3495,21 @@ var LoginStore = class {
|
|
|
3316
3495
|
const named = this.options.account;
|
|
3317
3496
|
if (!named)
|
|
3318
3497
|
return;
|
|
3319
|
-
const
|
|
3498
|
+
const read = async () => this.fetcher(`${this.options.apiOrigin}${managementApiBasePath}${managementOperations.getAccount.path}`, {
|
|
3320
3499
|
redirect: "manual",
|
|
3321
3500
|
signal: AbortSignal.timeout(3e4),
|
|
3322
|
-
headers: {
|
|
3501
|
+
headers: {
|
|
3502
|
+
authorization: `Bearer ${await managementBearer(key)}`,
|
|
3503
|
+
accept: "application/json"
|
|
3504
|
+
}
|
|
3323
3505
|
});
|
|
3506
|
+
let response = await read();
|
|
3507
|
+
if (response.status === 401) {
|
|
3508
|
+
const refusal = await response.json().catch(() => null);
|
|
3509
|
+
const code = typeof refusal?.code === "string" ? refusal.code : null;
|
|
3510
|
+
if (retriesAfterRefusal(key, MANAGEMENT_AUDIENCE, code))
|
|
3511
|
+
response = await read();
|
|
3512
|
+
}
|
|
3324
3513
|
const body = await response.json().catch(() => null);
|
|
3325
3514
|
if (!response.ok)
|
|
3326
3515
|
throw new Error(`TONBO_API_KEY was refused (HTTP ${response.status}).`);
|
|
@@ -3353,9 +3542,11 @@ var DEFAULT_LOGIN_HINT3 = credentialsContract.reader.login_hint;
|
|
|
3353
3542
|
var LOOPBACK_HOST = "127.0.0.1";
|
|
3354
3543
|
var BrowserUnavailableError = class extends Error {
|
|
3355
3544
|
};
|
|
3356
|
-
function prefersDeviceLogin(env = process.env) {
|
|
3545
|
+
function prefersDeviceLogin(env = process.env, platform = process.platform) {
|
|
3546
|
+
if (env.DISPLAY || env.WAYLAND_DISPLAY)
|
|
3547
|
+
return false;
|
|
3357
3548
|
const ssh = Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY);
|
|
3358
|
-
return ssh
|
|
3549
|
+
return ssh || platform !== "darwin" && platform !== "win32";
|
|
3359
3550
|
}
|
|
3360
3551
|
function loginHostname(hostname = os4.hostname()) {
|
|
3361
3552
|
const cleaned = hostname.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 64);
|
|
@@ -3374,7 +3565,8 @@ var AuthClient = class {
|
|
|
3374
3565
|
entryLine;
|
|
3375
3566
|
env;
|
|
3376
3567
|
facts;
|
|
3377
|
-
|
|
3568
|
+
/** TONBO_API_KEY's bearer, made once per key so its tokens and its exchange outlive a call. */
|
|
3569
|
+
injectedBearer = null;
|
|
3378
3570
|
constructor(credentials, fetcher, loginOrigin2 = "https://tonbo.dev", browserOpener = openBrowser, hostname = loginHostname(), loginHint = DEFAULT_LOGIN_HINT3, entryLine = "cli", env = process.env, facts = {}) {
|
|
3379
3571
|
this.credentials = credentials;
|
|
3380
3572
|
this.fetcher = fetcher;
|
|
@@ -3388,18 +3580,25 @@ var AuthClient = class {
|
|
|
3388
3580
|
}
|
|
3389
3581
|
/**
|
|
3390
3582
|
* The credential of the account a command acts in: TONBO_API_KEY (whose
|
|
3391
|
-
* own account any account the command named must be),
|
|
3392
|
-
*
|
|
3393
|
-
* account
|
|
3583
|
+
* own account any account the command named must be), presented as the
|
|
3584
|
+
* Tonbo SDK presents a key (apiKeyBearer), or the CLI login's bearer of the
|
|
3585
|
+
* named account, else of its default, else of the personal account; with
|
|
3586
|
+
* only a legacy login, that file's key.
|
|
3394
3587
|
*/
|
|
3395
3588
|
async apiKey() {
|
|
3396
3589
|
const injected = this.env.TONBO_API_KEY?.trim();
|
|
3397
3590
|
if (injected) {
|
|
3398
|
-
if (this.
|
|
3399
|
-
|
|
3400
|
-
|
|
3591
|
+
if (this.injectedBearer?.key !== injected)
|
|
3592
|
+
this.injectedBearer = {
|
|
3593
|
+
key: injected,
|
|
3594
|
+
bearer: apiKeyBearer(injected, this.fetcher, this.loginOrigin),
|
|
3595
|
+
checked: false
|
|
3596
|
+
};
|
|
3597
|
+
if (!this.injectedBearer.checked) {
|
|
3598
|
+
await this.credentials.checkKeyAccount(this.injectedBearer.bearer);
|
|
3599
|
+
this.injectedBearer.checked = true;
|
|
3401
3600
|
}
|
|
3402
|
-
return
|
|
3601
|
+
return this.injectedBearer.bearer;
|
|
3403
3602
|
}
|
|
3404
3603
|
const login = await this.credentials.load();
|
|
3405
3604
|
if (!login)
|
|
@@ -3412,17 +3611,20 @@ var AuthClient = class {
|
|
|
3412
3611
|
* Logs this computer in (ADR-0018): the browser handoff of /cli/login, PKCE
|
|
3413
3612
|
* S256 with a loopback redirect on a free port, asks for a session
|
|
3414
3613
|
* (`grant=cli_session`); the approval names this computer and asks no
|
|
3415
|
-
* account. With `device`, in an SSH session without a display, or when no
|
|
3614
|
+
* account. With `device`, in an SSH session or on Linux without a display, or when no
|
|
3416
3615
|
* browser can be opened, the same PKCE challenge goes to a device-code
|
|
3417
3616
|
* login instead: the person enters a short code at /cli/device in any
|
|
3418
3617
|
* browser and the CLI polls. The redeemed session, tied to this
|
|
3419
3618
|
* computer's device id, is saved as `login.json` and replaces this
|
|
3420
3619
|
* computer's earlier session; the legacy `credentials.json` is left as it
|
|
3421
3620
|
* is. With `account` (a name or id), commands act there by default unless
|
|
3422
|
-
* `current` is false.
|
|
3621
|
+
* `current` is false. With `openBrowser` false the browser login opens no
|
|
3622
|
+
* browser: the person opens the `authorization-url` they are shown, as
|
|
3623
|
+
* when an agent shows it with what the approval consents to. Answers the
|
|
3624
|
+
* login of the account commands act in.
|
|
3423
3625
|
*/
|
|
3424
3626
|
async login(progress = () => {
|
|
3425
|
-
}, account, { current = true, device } = {}) {
|
|
3627
|
+
}, account, { current = true, device, openBrowser: openBrowser2 = true } = {}) {
|
|
3426
3628
|
const deviceId = await (this.facts.devices ?? new DeviceStore()).deviceId();
|
|
3427
3629
|
const verifier = randomBytes(48).toString("base64url");
|
|
3428
3630
|
const challenge = createHash3("sha256").update(verifier).digest("base64url");
|
|
@@ -3433,7 +3635,7 @@ var AuthClient = class {
|
|
|
3433
3635
|
...clientFact(this.facts.clientVersion) ? { client_version: clientFact(this.facts.clientVersion) } : {},
|
|
3434
3636
|
...clientFact(process.platform) ? { os: process.platform } : {}
|
|
3435
3637
|
};
|
|
3436
|
-
const redeemed = device ?? prefersDeviceLogin(this.env) ? await this.deviceLogin(challenge, verifier, redeem, progress) : await this.browserLogin(challenge, verifier, redeem, progress).catch((error) => {
|
|
3638
|
+
const redeemed = device ?? prefersDeviceLogin(this.env) ? await this.deviceLogin(challenge, verifier, redeem, progress) : await this.browserLogin(challenge, verifier, redeem, progress, openBrowser2).catch((error) => {
|
|
3437
3639
|
if (!(error instanceof BrowserUnavailableError))
|
|
3438
3640
|
throw error;
|
|
3439
3641
|
progress({ status: "failed", step: "browser", reason: error.message });
|
|
@@ -3469,7 +3671,7 @@ var AuthClient = class {
|
|
|
3469
3671
|
throw new Error("The saved login could not be read back.");
|
|
3470
3672
|
return login;
|
|
3471
3673
|
}
|
|
3472
|
-
async browserLogin(challenge, verifier, redeem, progress) {
|
|
3674
|
+
async browserLogin(challenge, verifier, redeem, progress, openBrowser2) {
|
|
3473
3675
|
const state = randomBytes(24).toString("base64url");
|
|
3474
3676
|
const code = await this.listenForCode(state, (redirectUri) => {
|
|
3475
3677
|
const authorization = new URL("/cli/login", this.loginOrigin);
|
|
@@ -3483,7 +3685,7 @@ var AuthClient = class {
|
|
|
3483
3685
|
[cliSessionContract.grantParameter]: cliSessionContract.grant
|
|
3484
3686
|
}).toString();
|
|
3485
3687
|
return authorization.toString();
|
|
3486
|
-
}, progress);
|
|
3688
|
+
}, progress, openBrowser2);
|
|
3487
3689
|
progress({ status: "started", step: "key-redemption" });
|
|
3488
3690
|
const redeemed = await requestJson(this.fetcher, new URL("/api/cli/login/token", this.loginOrigin).toString(), {
|
|
3489
3691
|
method: "POST",
|
|
@@ -3561,7 +3763,7 @@ var AuthClient = class {
|
|
|
3561
3763
|
}
|
|
3562
3764
|
}
|
|
3563
3765
|
}
|
|
3564
|
-
listenForCode(expectedState, authorizationUrl, progress) {
|
|
3766
|
+
listenForCode(expectedState, authorizationUrl, progress, openBrowser2) {
|
|
3565
3767
|
return new Promise((resolve, reject) => {
|
|
3566
3768
|
let settled = false;
|
|
3567
3769
|
const sockets = /* @__PURE__ */ new Set();
|
|
@@ -3636,6 +3838,8 @@ var AuthClient = class {
|
|
|
3636
3838
|
progress({ status: "completed", step: "callback-server" });
|
|
3637
3839
|
const url = authorizationUrl(`http://${LOOPBACK_HOST}:${port}/callback`);
|
|
3638
3840
|
progress({ status: "authorization-url", step: "browser", url });
|
|
3841
|
+
if (!openBrowser2)
|
|
3842
|
+
return;
|
|
3639
3843
|
progress({ status: "started", step: "browser" });
|
|
3640
3844
|
void Promise.resolve().then(() => this.browserOpener(url)).then(() => startAuthorization(), (error) => {
|
|
3641
3845
|
if (settled || authorizationStarted)
|
|
@@ -3681,6 +3885,150 @@ async function openBrowser(url) {
|
|
|
3681
3885
|
}
|
|
3682
3886
|
}
|
|
3683
3887
|
|
|
3888
|
+
// ../../packages/tonbo-cli-core/dist/src/capture-filter.js
|
|
3889
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3890
|
+
import { existsSync } from "node:fs";
|
|
3891
|
+
import { mkdir as mkdir3, open as open3, readFile as readFile3, rename as rename3, rm as rm3 } from "node:fs/promises";
|
|
3892
|
+
import os5 from "node:os";
|
|
3893
|
+
import path4 from "node:path";
|
|
3894
|
+
var KEPT_WINDOWS = 200;
|
|
3895
|
+
function yieldStateDirectory(env = process.env, home = os5.homedir()) {
|
|
3896
|
+
return path4.join(env.XDG_STATE_HOME || path4.join(home, ".local", "state"), "tonbo-yield");
|
|
3897
|
+
}
|
|
3898
|
+
function emptyFilter() {
|
|
3899
|
+
return { version: 1, pause: null, past: [], excluded: [] };
|
|
3900
|
+
}
|
|
3901
|
+
function activePause(file, now) {
|
|
3902
|
+
const pause = file.pause;
|
|
3903
|
+
return pause && (pause.until === null || now < pause.until) ? pause : null;
|
|
3904
|
+
}
|
|
3905
|
+
function pauseForLogout(file, now, person) {
|
|
3906
|
+
const current = activePause(file, now);
|
|
3907
|
+
if (current?.logout)
|
|
3908
|
+
return file;
|
|
3909
|
+
const after = current ? { after: current.until } : {};
|
|
3910
|
+
if (current)
|
|
3911
|
+
current.until = now;
|
|
3912
|
+
settle(file, now);
|
|
3913
|
+
file.pause = { since: now, until: null, logout: { ...after, person } };
|
|
3914
|
+
return file;
|
|
3915
|
+
}
|
|
3916
|
+
function endLogoutPause(file, now, person) {
|
|
3917
|
+
const current = activePause(file, now);
|
|
3918
|
+
if (!current?.logout)
|
|
3919
|
+
return file;
|
|
3920
|
+
const { after, person: before } = current.logout;
|
|
3921
|
+
if (person === null || before == null || before !== person)
|
|
3922
|
+
current.since = 0;
|
|
3923
|
+
current.until = now;
|
|
3924
|
+
settle(file, now);
|
|
3925
|
+
if (after === null || after !== void 0 && after > now)
|
|
3926
|
+
file.pause = { since: now, until: after };
|
|
3927
|
+
return file;
|
|
3928
|
+
}
|
|
3929
|
+
function settle(file, now) {
|
|
3930
|
+
if (file.pause && !activePause(file, now)) {
|
|
3931
|
+
file.past = [...file.past, file.pause].slice(-KEPT_WINDOWS);
|
|
3932
|
+
file.pause = null;
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
async function durableWrite(filename, body) {
|
|
3936
|
+
const parent = path4.dirname(filename);
|
|
3937
|
+
const created = await mkdir3(parent, { recursive: true, mode: 448 });
|
|
3938
|
+
if (created) {
|
|
3939
|
+
const state = await open3(path4.dirname(parent), "r");
|
|
3940
|
+
try {
|
|
3941
|
+
await state.sync();
|
|
3942
|
+
} finally {
|
|
3943
|
+
await state.close();
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
const temporary = path4.join(parent, `.${randomUUID3()}.tmp`);
|
|
3947
|
+
const file = await open3(temporary, "wx", 384);
|
|
3948
|
+
try {
|
|
3949
|
+
await file.writeFile(JSON.stringify(body));
|
|
3950
|
+
await file.sync();
|
|
3951
|
+
} finally {
|
|
3952
|
+
await file.close();
|
|
3953
|
+
}
|
|
3954
|
+
try {
|
|
3955
|
+
await rename3(temporary, filename);
|
|
3956
|
+
const dir = await open3(parent, "r");
|
|
3957
|
+
try {
|
|
3958
|
+
await dir.sync();
|
|
3959
|
+
} finally {
|
|
3960
|
+
await dir.close();
|
|
3961
|
+
}
|
|
3962
|
+
} catch (error) {
|
|
3963
|
+
await rm3(temporary, { force: true });
|
|
3964
|
+
throw error;
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
var CaptureFilterStore = class {
|
|
3968
|
+
filename;
|
|
3969
|
+
constructor(filename = path4.join(yieldStateDirectory(), "capture-filter.json")) {
|
|
3970
|
+
this.filename = filename;
|
|
3971
|
+
}
|
|
3972
|
+
/** A missing file filters nothing; an unreadable one throws, and the
|
|
3973
|
+
* daemon then captures nothing (fail closed). */
|
|
3974
|
+
async load() {
|
|
3975
|
+
let body;
|
|
3976
|
+
try {
|
|
3977
|
+
body = await readFile3(this.filename, "utf8");
|
|
3978
|
+
} catch (error) {
|
|
3979
|
+
if (error.code === "ENOENT")
|
|
3980
|
+
return emptyFilter();
|
|
3981
|
+
throw error;
|
|
3982
|
+
}
|
|
3983
|
+
const parsed = JSON.parse(body);
|
|
3984
|
+
const window = (value) => !!value && Number.isFinite(value.since) && (value.until === null || Number.isFinite(value.until));
|
|
3985
|
+
if (parsed?.version !== 1 || !(parsed.pause === null || window(parsed.pause)) || !Array.isArray(parsed.past) || !parsed.past.every(window) || !Array.isArray(parsed.excluded) || !parsed.excluded.every((folder) => typeof folder === "string" && path4.isAbsolute(folder)))
|
|
3986
|
+
throw new Error(`${this.filename} is not a Yield capture filter file.`);
|
|
3987
|
+
return parsed;
|
|
3988
|
+
}
|
|
3989
|
+
async save(file) {
|
|
3990
|
+
await durableWrite(this.filename, file);
|
|
3991
|
+
}
|
|
3992
|
+
async update(change) {
|
|
3993
|
+
const file = await this.load();
|
|
3994
|
+
change(file);
|
|
3995
|
+
await this.save(file);
|
|
3996
|
+
return file;
|
|
3997
|
+
}
|
|
3998
|
+
};
|
|
3999
|
+
var CaptureNotStoppedError = class extends Error {
|
|
4000
|
+
};
|
|
4001
|
+
var reason = (error) => error instanceof Error ? error.message : String(error);
|
|
4002
|
+
async function stopCaptureForLogout(person, store = new CaptureFilterStore(), now = Date.now()) {
|
|
4003
|
+
if (!existsSync(path4.dirname(store.filename)))
|
|
4004
|
+
return null;
|
|
4005
|
+
let file;
|
|
4006
|
+
try {
|
|
4007
|
+
file = await store.load();
|
|
4008
|
+
} catch (error) {
|
|
4009
|
+
return `Yield's capture stays stopped on this computer: ${reason(error)}`;
|
|
4010
|
+
}
|
|
4011
|
+
pauseForLogout(file, now, person);
|
|
4012
|
+
try {
|
|
4013
|
+
await store.save(file);
|
|
4014
|
+
} catch (error) {
|
|
4015
|
+
throw new CaptureNotStoppedError(`Could not stop Yield's capture on this computer (${reason(error)}), so nothing was logged out. Run the logout again where it can write ${store.filename}, outside an agent's sandbox.`, { cause: error });
|
|
4016
|
+
}
|
|
4017
|
+
return null;
|
|
4018
|
+
}
|
|
4019
|
+
async function resumeCaptureAfterLogin(person, store = new CaptureFilterStore(), now = Date.now()) {
|
|
4020
|
+
try {
|
|
4021
|
+
const file = await store.load();
|
|
4022
|
+
if (!activePause(file, now)?.logout)
|
|
4023
|
+
return null;
|
|
4024
|
+
endLogoutPause(file, now, await person().catch(() => null));
|
|
4025
|
+
await store.save(file);
|
|
4026
|
+
return null;
|
|
4027
|
+
} catch (error) {
|
|
4028
|
+
return `Yield's capture stays stopped on this computer: ${reason(error)}`;
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
|
|
3684
4032
|
// ../../packages/tonbo-cli-core/dist/src/cell.js
|
|
3685
4033
|
function cellOrigin(region, configured = process.env.TONBO_CELL_API_ORIGIN) {
|
|
3686
4034
|
if (!/^[a-z0-9-]+$/.test(region))
|
|
@@ -3698,7 +4046,7 @@ var CellApi = class {
|
|
|
3698
4046
|
}
|
|
3699
4047
|
url(operationId, region, request) {
|
|
3700
4048
|
const operation = managementOperations[operationId];
|
|
3701
|
-
const
|
|
4049
|
+
const path5 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
3702
4050
|
const value = request.params[name];
|
|
3703
4051
|
if (value === void 0)
|
|
3704
4052
|
throw new Error(`${operationId} needs the path parameter ${name}.`);
|
|
@@ -3709,7 +4057,7 @@ var CellApi = class {
|
|
|
3709
4057
|
if (value !== void 0)
|
|
3710
4058
|
search.set(name, value);
|
|
3711
4059
|
const query = search.toString();
|
|
3712
|
-
return `${cellOrigin(region)}${managementApiBasePath}${
|
|
4060
|
+
return `${cellOrigin(region)}${managementApiBasePath}${path5}${query ? `?${query}` : ""}`;
|
|
3713
4061
|
}
|
|
3714
4062
|
async init(operationId, key, request) {
|
|
3715
4063
|
const headers = {
|
|
@@ -3787,14 +4135,22 @@ function when(timestamp, absent = "never") {
|
|
|
3787
4135
|
}
|
|
3788
4136
|
|
|
3789
4137
|
// ../../packages/tonbo-cli-core/dist/src/login.js
|
|
4138
|
+
function captureAfterLogin(deps) {
|
|
4139
|
+
return resumeCaptureAfterLogin(async () => {
|
|
4140
|
+
const file = await deps.credentials.load();
|
|
4141
|
+
return file ? personalLogin(file)?.account_id ?? null : null;
|
|
4142
|
+
}, deps.captureFilter);
|
|
4143
|
+
}
|
|
3790
4144
|
async function loginCommand(deps, options = {}) {
|
|
3791
4145
|
try {
|
|
3792
4146
|
const login = await deps.auth.login((event) => reportLoginProgress(deps.progress, event), options.account, { current: options.current, device: options.device });
|
|
4147
|
+
const stopped = await captureAfterLogin(deps);
|
|
3793
4148
|
const file = await deps.credentials.load();
|
|
3794
4149
|
const names = file ? accountLogins(file).map((entry) => entry.kind === "user" ? `${entry.name ?? "you"} (personal)` : entry.name ?? entry.account_id) : [];
|
|
3795
4150
|
deps.output([
|
|
3796
4151
|
`Logged in. This computer can act in every account you belong to, with your role in each${names.length ? `: ${names.join(", ")}` : ""}.`,
|
|
3797
|
-
`Commands act in ${login.kind === "user" ? "your personal account" : login.name ?? login.account_id} unless you pass --account.${login.expires_at ? ` The login ends ${login.expires_at.slice(0, 10)}, or after 30 days unused.` : ""}
|
|
4152
|
+
`Commands act in ${login.kind === "user" ? "your personal account" : login.name ?? login.account_id} unless you pass --account.${login.expires_at ? ` The login ends ${login.expires_at.slice(0, 10)}, or after 30 days unused.` : ""}`,
|
|
4153
|
+
...stopped ? [stopped] : []
|
|
3798
4154
|
].join("\n"));
|
|
3799
4155
|
return login;
|
|
3800
4156
|
} catch (error) {
|
|
@@ -3866,11 +4222,13 @@ async function logoutCommand(deps) {
|
|
|
3866
4222
|
failures.push(`${what}: ${failure.message}`);
|
|
3867
4223
|
};
|
|
3868
4224
|
let session = null;
|
|
4225
|
+
let unusable = false;
|
|
3869
4226
|
try {
|
|
3870
4227
|
session = await deps.credentials.session({ anyDevice: true });
|
|
3871
4228
|
} catch (error) {
|
|
3872
4229
|
if (!(error instanceof UnusableLoginError))
|
|
3873
4230
|
throw error;
|
|
4231
|
+
unusable = true;
|
|
3874
4232
|
}
|
|
3875
4233
|
let legacy = null;
|
|
3876
4234
|
try {
|
|
@@ -3878,6 +4236,13 @@ async function logoutCommand(deps) {
|
|
|
3878
4236
|
} catch (error) {
|
|
3879
4237
|
if (!(error instanceof UnusableLoginError))
|
|
3880
4238
|
throw error;
|
|
4239
|
+
unusable = true;
|
|
4240
|
+
}
|
|
4241
|
+
if (session || legacy || unusable) {
|
|
4242
|
+
const person = session?.stored.personal_account_id ?? (legacy && personalLogin(legacy)?.account_id) ?? null;
|
|
4243
|
+
const stopped = await stopCaptureForLogout(person, deps.captureFilter);
|
|
4244
|
+
if (stopped)
|
|
4245
|
+
deps.output(stopped);
|
|
3881
4246
|
}
|
|
3882
4247
|
if (session)
|
|
3883
4248
|
await session.logout().catch((error) => failed("the CLI login", error));
|
|
@@ -3986,7 +4351,7 @@ var TerminalProgress = class {
|
|
|
3986
4351
|
};
|
|
3987
4352
|
|
|
3988
4353
|
// src/commands.ts
|
|
3989
|
-
import { randomUUID as
|
|
4354
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
3990
4355
|
|
|
3991
4356
|
// src/agent-name.ts
|
|
3992
4357
|
var AGENT_NAME_RULE = agentNameContract.$defs.name;
|
|
@@ -4002,7 +4367,7 @@ function validateAgentName(name) {
|
|
|
4002
4367
|
}
|
|
4003
4368
|
|
|
4004
4369
|
// src/budget.ts
|
|
4005
|
-
import { randomUUID as
|
|
4370
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
4006
4371
|
var BUDGETS_NOT_AVAILABLE = "Budgets are not available yet.";
|
|
4007
4372
|
function parseDollars(value) {
|
|
4008
4373
|
const match = /^\$?([0-9]+)(?:\.([0-9]{1,2}))?$/.exec(value.trim());
|
|
@@ -4094,7 +4459,7 @@ async function budgetAddCommand(deps, name, options) {
|
|
|
4094
4459
|
const key = await deps.auth.apiKey();
|
|
4095
4460
|
const limit = parseDollars(options.limit);
|
|
4096
4461
|
const principals = options.member?.length ? await Promise.all(options.member.map((member) => resolveMemberSubject(deps, key, member))) : [(await deps.api.getAccount(key)).member.subject];
|
|
4097
|
-
const created = await deps.api.createBudget(key, { scope: "principals", name, limit_micros: limit, principals },
|
|
4462
|
+
const created = await deps.api.createBudget(key, { scope: "principals", name, limit_micros: limit, principals }, randomUUID4()).catch((error) => consoleOr(deps, error));
|
|
4098
4463
|
print(
|
|
4099
4464
|
deps,
|
|
4100
4465
|
options.json,
|
|
@@ -4127,7 +4492,7 @@ async function attachBudget(deps, key, subject, value, name, created) {
|
|
|
4127
4492
|
const budget = await deps.api.createBudget(
|
|
4128
4493
|
key,
|
|
4129
4494
|
{ scope: "principals", name, limit_micros: limit, principals: [subject] },
|
|
4130
|
-
|
|
4495
|
+
randomUUID4()
|
|
4131
4496
|
).catch((error) => consoleOr(deps, error));
|
|
4132
4497
|
deps.notice(`Budget ${budget.name ?? name}: ${usd(limit)} a month for ${subject}.`);
|
|
4133
4498
|
return;
|
|
@@ -4136,8 +4501,8 @@ async function attachBudget(deps, key, subject, value, name, created) {
|
|
|
4136
4501
|
await deps.api.changeBudget(key, found.id, { add_principals: [subject] }).catch((error) => consoleOr(deps, error));
|
|
4137
4502
|
deps.notice(`Budget ${found.name ?? found.id} now covers ${subject}.`);
|
|
4138
4503
|
} catch (error) {
|
|
4139
|
-
const
|
|
4140
|
-
throw new Error(`${created} was created, but its budget was not set: ${
|
|
4504
|
+
const reason2 = error instanceof Error ? error.message : String(error);
|
|
4505
|
+
throw new Error(`${created} was created, but its budget was not set: ${reason2}`);
|
|
4141
4506
|
}
|
|
4142
4507
|
}
|
|
4143
4508
|
function accountBudgetFields(account) {
|
|
@@ -4246,7 +4611,7 @@ async function agentCreateCommand(deps, name, options = {}) {
|
|
|
4246
4611
|
const agent = await deps.api.createAgent(
|
|
4247
4612
|
key,
|
|
4248
4613
|
{ name, region, ...options.role === void 0 ? {} : { role: options.role } },
|
|
4249
|
-
options.idempotencyKey ??
|
|
4614
|
+
options.idempotencyKey ?? randomUUID5()
|
|
4250
4615
|
);
|
|
4251
4616
|
print(
|
|
4252
4617
|
deps,
|
|
@@ -4554,7 +4919,7 @@ async function bucketCreateCommand(deps, name, options = {}) {
|
|
|
4554
4919
|
const bucket = await deps.api.createBucket(
|
|
4555
4920
|
await deps.auth.apiKey(),
|
|
4556
4921
|
{ name, region: options.region },
|
|
4557
|
-
options.idempotencyKey ??
|
|
4922
|
+
options.idempotencyKey ?? randomUUID5()
|
|
4558
4923
|
);
|
|
4559
4924
|
print(
|
|
4560
4925
|
deps,
|
|
@@ -4685,7 +5050,7 @@ async function databaseCreateCommand(deps, name, options = {}) {
|
|
|
4685
5050
|
const database = await deps.api.createDatabase(
|
|
4686
5051
|
await deps.auth.apiKey(),
|
|
4687
5052
|
{ name, region: options.region, expires_at: expiresAt },
|
|
4688
|
-
options.idempotencyKey ??
|
|
5053
|
+
options.idempotencyKey ?? randomUUID5()
|
|
4689
5054
|
);
|
|
4690
5055
|
print(
|
|
4691
5056
|
deps,
|
|
@@ -4873,7 +5238,7 @@ async function streamCreateCommand(deps, name, options = {}) {
|
|
|
4873
5238
|
content_type: options.contentType,
|
|
4874
5239
|
expires_at: expiresAt
|
|
4875
5240
|
},
|
|
4876
|
-
options.idempotencyKey ??
|
|
5241
|
+
options.idempotencyKey ?? randomUUID5()
|
|
4877
5242
|
);
|
|
4878
5243
|
print(
|
|
4879
5244
|
deps,
|
|
@@ -5097,11 +5462,11 @@ async function accountUseCommand(deps, account, options = {}) {
|
|
|
5097
5462
|
import { Help } from "commander";
|
|
5098
5463
|
|
|
5099
5464
|
// src/machine.ts
|
|
5100
|
-
import { randomUUID as
|
|
5465
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
5101
5466
|
import { readFileSync } from "node:fs";
|
|
5102
5467
|
|
|
5103
5468
|
// src/volume.ts
|
|
5104
|
-
import { randomUUID as
|
|
5469
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
5105
5470
|
var VOLUME_DURABILITY_LINK = "https://tonbo.dev/docs/machines/guarantees/data#volume";
|
|
5106
5471
|
var VOLUME_ERROR_HINTS = {
|
|
5107
5472
|
payment_required: "Add credit at <billing>, then create the Volume again.",
|
|
@@ -5199,7 +5564,7 @@ async function volumeCreateCommand(deps, name, options = {}) {
|
|
|
5199
5564
|
const volume = await deps.api.createVolume(
|
|
5200
5565
|
key,
|
|
5201
5566
|
{ name, region, size_gib: sizeGib },
|
|
5202
|
-
options.idempotencyKey ??
|
|
5567
|
+
options.idempotencyKey ?? randomUUID6()
|
|
5203
5568
|
);
|
|
5204
5569
|
print(
|
|
5205
5570
|
deps,
|
|
@@ -5355,10 +5720,10 @@ function duration(seconds) {
|
|
|
5355
5720
|
return seconds % 60 === 0 && seconds >= 120 ? `${seconds / 60} min` : `${seconds} s`;
|
|
5356
5721
|
}
|
|
5357
5722
|
function sleepText(machine) {
|
|
5358
|
-
const { action, reason } = machine.status.sleep;
|
|
5723
|
+
const { action, reason: reason2 } = machine.status.sleep;
|
|
5359
5724
|
const when2 = `after about ${duration(earlyReclaimSeconds)} idle (early) or at most ${machine.sleep_after_seconds} s without a connection`;
|
|
5360
5725
|
if (action !== "release") return `${when2}; it then ${action}s`;
|
|
5361
|
-
return `${when2}; the disk resets at every sleep${
|
|
5726
|
+
return `${when2}; the disk resets at every sleep${reason2 === "suspend_unavailable" ? " until suspend ships" : ""}`;
|
|
5362
5727
|
}
|
|
5363
5728
|
function activationFields(machine) {
|
|
5364
5729
|
const { activation } = machine;
|
|
@@ -5793,9 +6158,9 @@ async function waitForCreate(deps, key, machine, name, firstWait, timeout, json)
|
|
|
5793
6158
|
}
|
|
5794
6159
|
print(deps, json, settled, machineFields(settled, billing));
|
|
5795
6160
|
if (settled.state === "failed") {
|
|
5796
|
-
const { reason, hint } = creationFailureText(settled, billing);
|
|
6161
|
+
const { reason: reason2, hint } = creationFailureText(settled, billing);
|
|
5797
6162
|
throw new Error(
|
|
5798
|
-
`Machine ${settled.name} failed to be created${
|
|
6163
|
+
`Machine ${settled.name} failed to be created${reason2}; delete it with \`tonbo machine delete ${settled.name}\` and create it again.${hint}`
|
|
5799
6164
|
);
|
|
5800
6165
|
}
|
|
5801
6166
|
throw new Error(`Machine ${settled.name} is ${settled.state}; it was not created.`);
|
|
@@ -5837,7 +6202,7 @@ async function machineCreateCommand(deps, name, command, options = {}) {
|
|
|
5837
6202
|
...database ? { database: database.name } : {},
|
|
5838
6203
|
...options.database === false ? { database: null } : {}
|
|
5839
6204
|
},
|
|
5840
|
-
options.idempotencyKey ??
|
|
6205
|
+
options.idempotencyKey ?? randomUUID7()
|
|
5841
6206
|
);
|
|
5842
6207
|
} catch (error) {
|
|
5843
6208
|
if (error instanceof HttpError && error.code === "name_taken")
|
|
@@ -5849,9 +6214,9 @@ async function machineCreateCommand(deps, name, command, options = {}) {
|
|
|
5849
6214
|
if (machine.state === "failed") {
|
|
5850
6215
|
const billing = await billingUrl(deps);
|
|
5851
6216
|
print(deps, options.json, machine, machineFields(machine, billing));
|
|
5852
|
-
const { reason, hint } = creationFailureText(machine, billing);
|
|
6217
|
+
const { reason: reason2, hint } = creationFailureText(machine, billing);
|
|
5853
6218
|
throw new Error(
|
|
5854
|
-
`Machine ${machine.name} failed to be created${
|
|
6219
|
+
`Machine ${machine.name} failed to be created${reason2}; delete it with \`tonbo machine delete ${machine.name}\` and create it again.${hint}`
|
|
5855
6220
|
);
|
|
5856
6221
|
}
|
|
5857
6222
|
if (options.wait && machine.state === "creating")
|
|
@@ -5895,9 +6260,9 @@ async function lastActivationEnd(deps, key, name) {
|
|
|
5895
6260
|
const page = await deps.api.listMachineActivations(key, name, { limit: 2 });
|
|
5896
6261
|
return page.data.find((entry) => entry.end !== null)?.end ?? null;
|
|
5897
6262
|
} catch (error) {
|
|
5898
|
-
if (!(error instanceof HttpError)) throw error;
|
|
6263
|
+
if (!(error instanceof HttpError || error instanceof AccessTokenError)) throw error;
|
|
5899
6264
|
deps.notice(
|
|
5900
|
-
`Could not read how its last activation ended (${error.status}); \`tonbo machine activations ${name}\` lists them.`
|
|
6265
|
+
`Could not read how its last activation ended (${error.status ?? "no answer"}); \`tonbo machine activations ${name}\` lists them.`
|
|
5901
6266
|
);
|
|
5902
6267
|
return null;
|
|
5903
6268
|
}
|
|
@@ -6226,7 +6591,7 @@ var TOPICS = [
|
|
|
6226
6591
|
title: "The login, accounts and keys",
|
|
6227
6592
|
text: [
|
|
6228
6593
|
"Log in once per computer: `tonbo login` signs this computer in as you, and every account you belong to is reachable with `--account`, with your role in each. Joining an organization needs no new login, and leaving one ends this computer's access to it at once. A command acts in one account: `--account`, else `TONBO_ACCOUNT`, else the default `tonbo account use` set, else your personal account. For each request the CLI exchanges the login for a short-lived token of that account; what a command may do is what your role there allows, and the CLI never decides it.",
|
|
6229
|
-
"`TONBO_API_KEY`, when it is set, is used instead: an API key reaches only its own account
|
|
6594
|
+
"`TONBO_API_KEY`, when it is set, is used instead: an API key reaches only its own account, as narrowed by the key. A personal key is sent as the Bearer credential. An Agent key, which the API never accepts directly, is exchanged at `POST https://tonbo.dev/api/iam/token` for a token of at most 10 minutes, kept in memory and exchanged again shortly before it expires; neither the key nor the token is ever printed. Agents and CI should use an Agent key or an API key of one account, never your login, which reaches every account you belong to.",
|
|
6230
6595
|
"The login is saved at `${XDG_CONFIG_HOME:-$HOME/.config}/tonbo/login.json`, which `yield` shares: either CLI's `login` signs both in, and either `logout` signs both out. It works only on the computer that logged in, ends after 30 days unused or 180 days at most, and is listed and revoked on the console's Sign-in sessions page. The CLI replaces the file atomically with owner-only permissions and refuses a symlink or a file other users can read."
|
|
6231
6596
|
]
|
|
6232
6597
|
},
|
|
@@ -6272,7 +6637,7 @@ var COMMAND_HELP = {
|
|
|
6272
6637
|
},
|
|
6273
6638
|
"tonbo logout": {
|
|
6274
6639
|
about: [
|
|
6275
|
-
"Ends this computer's login for every account and removes the file, which signs `yield` out too. A login an earlier release saved (one key per account in `credentials.json`) has each key revoked as well; one already revoked or expired is skipped. If a revocation fails, the files are still removed, and the CLI says what it could not revoke and exits 1, so you can revoke it on the console's Sign-in sessions page or under API keys. `TONBO_API_KEY` is never touched."
|
|
6640
|
+
"Ends this computer's login for every account and removes the file, which signs `yield` out too. Before anything is revoked, Yield's capture on this computer stops until the next login of either CLI, and nothing written meanwhile is ever recorded; when it cannot write Yield's folder to stop it, nothing is logged out. A login an earlier release saved (one key per account in `credentials.json`) has each key revoked as well; one already revoked or expired is skipped. If a revocation fails, the files are still removed, and the CLI says what it could not revoke and exits 1, so you can revoke it on the console's Sign-in sessions page or under API keys. `TONBO_API_KEY` is never touched."
|
|
6276
6641
|
],
|
|
6277
6642
|
examples: ["tonbo logout"]
|
|
6278
6643
|
},
|
|
@@ -6889,19 +7254,19 @@ function optionDescription(option) {
|
|
|
6889
7254
|
function applyHelp(program) {
|
|
6890
7255
|
const named = /* @__PURE__ */ new Set();
|
|
6891
7256
|
const walk = (command, words) => {
|
|
6892
|
-
const
|
|
7257
|
+
const path5 = words.join(" ");
|
|
6893
7258
|
command.configureHelp({ ...command.configureHelp(), optionDescription });
|
|
6894
|
-
const help = COMMAND_HELP[
|
|
7259
|
+
const help = COMMAND_HELP[path5];
|
|
6895
7260
|
if (help) {
|
|
6896
|
-
named.add(
|
|
7261
|
+
named.add(path5);
|
|
6897
7262
|
for (const [name, text] of Object.entries(help.arguments ?? {})) {
|
|
6898
7263
|
const argument = command.registeredArguments.find((entry) => entry.name() === name);
|
|
6899
|
-
if (!argument) throw new Error(`${
|
|
7264
|
+
if (!argument) throw new Error(`${path5} has no argument ${name}`);
|
|
6900
7265
|
argument.description = text;
|
|
6901
7266
|
}
|
|
6902
7267
|
for (const [flag, text] of Object.entries(help.defaults ?? {})) {
|
|
6903
7268
|
const option = command.options.find((entry) => entry.long === flag);
|
|
6904
|
-
if (!option) throw new Error(`${
|
|
7269
|
+
if (!option) throw new Error(`${path5} has no option ${flag}`);
|
|
6905
7270
|
option.defaultValueDescription = text;
|
|
6906
7271
|
}
|
|
6907
7272
|
command.addHelpText("after", commandHelpText(help));
|
|
@@ -6909,15 +7274,15 @@ function applyHelp(program) {
|
|
|
6909
7274
|
for (const child of command.commands) walk(child, [...words, child.name()]);
|
|
6910
7275
|
};
|
|
6911
7276
|
walk(program, [program.name()]);
|
|
6912
|
-
for (const
|
|
6913
|
-
if (!named.has(
|
|
7277
|
+
for (const path5 of Object.keys(COMMAND_HELP))
|
|
7278
|
+
if (!named.has(path5)) throw new Error(`The help of ${path5} names no command`);
|
|
6914
7279
|
program.addHelpText("after", programHelpText());
|
|
6915
7280
|
return program;
|
|
6916
7281
|
}
|
|
6917
7282
|
|
|
6918
7283
|
// src/machine-cell.ts
|
|
6919
|
-
import { readFile as
|
|
6920
|
-
import { randomUUID as
|
|
7284
|
+
import { readFile as readFile4, writeFile } from "node:fs/promises";
|
|
7285
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
6921
7286
|
var EXEC_EXIT_CODES = {
|
|
6922
7287
|
timeout: 124,
|
|
6923
7288
|
output_limit: 125,
|
|
@@ -7102,7 +7467,7 @@ async function machineProcessStartCommand(deps, name, command, options = {}) {
|
|
|
7102
7467
|
params: { name },
|
|
7103
7468
|
headers: {
|
|
7104
7469
|
"content-type": "application/json",
|
|
7105
|
-
"idempotency-key": options.idempotencyKey ??
|
|
7470
|
+
"idempotency-key": options.idempotencyKey ?? randomUUID8()
|
|
7106
7471
|
},
|
|
7107
7472
|
body: JSON.stringify(body)
|
|
7108
7473
|
});
|
|
@@ -7255,13 +7620,13 @@ async function machineProcessSummary(cell, key, region, name) {
|
|
|
7255
7620
|
return null;
|
|
7256
7621
|
}
|
|
7257
7622
|
}
|
|
7258
|
-
async function machineFileReadCommand(deps, name,
|
|
7623
|
+
async function machineFileReadCommand(deps, name, path5, options = {}) {
|
|
7259
7624
|
try {
|
|
7260
7625
|
const key = await deps.auth.apiKey();
|
|
7261
7626
|
const region = await regionOf(deps, key, name);
|
|
7262
7627
|
const response = await cellApi(deps).fetch("readMachineFile", key, region, {
|
|
7263
7628
|
params: { name },
|
|
7264
|
-
query: { path:
|
|
7629
|
+
query: { path: path5 },
|
|
7265
7630
|
headers: { range: options.range, "tonbo-expect-activation": options.expectActivation }
|
|
7266
7631
|
});
|
|
7267
7632
|
const bytes2 = new Uint8Array(await response.arrayBuffer());
|
|
@@ -7271,21 +7636,21 @@ async function machineFileReadCommand(deps, name, path4, options = {}) {
|
|
|
7271
7636
|
throw explainMachineError(error);
|
|
7272
7637
|
}
|
|
7273
7638
|
}
|
|
7274
|
-
async function machineFileWriteCommand(deps, name,
|
|
7639
|
+
async function machineFileWriteCommand(deps, name, path5, options = {}) {
|
|
7275
7640
|
try {
|
|
7276
|
-
const bytes2 = options.from ? new Uint8Array(await
|
|
7641
|
+
const bytes2 = options.from ? new Uint8Array(await readFile4(options.from)) : await readAllStdin(deps);
|
|
7277
7642
|
const key = await deps.auth.apiKey();
|
|
7278
7643
|
const region = await regionOf(deps, key, name);
|
|
7279
7644
|
const written = await cellApi(deps).json("writeMachineFile", key, region, {
|
|
7280
7645
|
params: { name },
|
|
7281
|
-
query: { path:
|
|
7646
|
+
query: { path: path5 },
|
|
7282
7647
|
headers: {
|
|
7283
7648
|
"content-type": "application/octet-stream",
|
|
7284
7649
|
"if-match": options.ifMatch,
|
|
7285
7650
|
"if-none-match": options.create ? "*" : void 0,
|
|
7286
7651
|
"tonbo-file-mode": options.mode,
|
|
7287
7652
|
"tonbo-expect-activation": options.expectActivation,
|
|
7288
|
-
"idempotency-key":
|
|
7653
|
+
"idempotency-key": randomUUID8()
|
|
7289
7654
|
},
|
|
7290
7655
|
body: new Uint8Array(bytes2)
|
|
7291
7656
|
});
|
|
@@ -7300,28 +7665,28 @@ async function machineFileWriteCommand(deps, name, path4, options = {}) {
|
|
|
7300
7665
|
throw explainMachineError(error);
|
|
7301
7666
|
}
|
|
7302
7667
|
}
|
|
7303
|
-
async function machineFileStatCommand(deps, name,
|
|
7668
|
+
async function machineFileStatCommand(deps, name, path5, options = {}) {
|
|
7304
7669
|
try {
|
|
7305
7670
|
const key = await deps.auth.apiKey();
|
|
7306
7671
|
const region = await regionOf(deps, key, name);
|
|
7307
|
-
const
|
|
7672
|
+
const stat2 = await cellApi(deps).json(
|
|
7308
7673
|
"statMachineFile",
|
|
7309
7674
|
key,
|
|
7310
7675
|
region,
|
|
7311
7676
|
{
|
|
7312
7677
|
params: { name },
|
|
7313
|
-
query: { path:
|
|
7678
|
+
query: { path: path5 },
|
|
7314
7679
|
headers: { "tonbo-expect-activation": options.expectActivation }
|
|
7315
7680
|
}
|
|
7316
7681
|
);
|
|
7317
|
-
noticeWake(deps,
|
|
7682
|
+
noticeWake(deps, stat2.machine, name);
|
|
7318
7683
|
print(
|
|
7319
7684
|
deps,
|
|
7320
7685
|
options.json,
|
|
7321
|
-
|
|
7686
|
+
stat2,
|
|
7322
7687
|
fields(
|
|
7323
7688
|
["path", "type", "size", "mode", "uid", "gid", "mtime", "etag", "target", "durability"].map(
|
|
7324
|
-
(field) => [field, cellText(
|
|
7689
|
+
(field) => [field, cellText(stat2[field])]
|
|
7325
7690
|
)
|
|
7326
7691
|
)
|
|
7327
7692
|
);
|
|
@@ -7329,13 +7694,13 @@ async function machineFileStatCommand(deps, name, path4, options = {}) {
|
|
|
7329
7694
|
throw explainMachineError(error);
|
|
7330
7695
|
}
|
|
7331
7696
|
}
|
|
7332
|
-
async function machineFileListCommand(deps, name,
|
|
7697
|
+
async function machineFileListCommand(deps, name, path5, options = {}) {
|
|
7333
7698
|
try {
|
|
7334
7699
|
const key = await deps.auth.apiKey();
|
|
7335
7700
|
const region = await regionOf(deps, key, name);
|
|
7336
7701
|
const listing = await cellApi(deps).json("listMachineFiles", key, region, {
|
|
7337
7702
|
params: { name },
|
|
7338
|
-
query: { path:
|
|
7703
|
+
query: { path: path5 },
|
|
7339
7704
|
headers: { "tonbo-expect-activation": options.expectActivation }
|
|
7340
7705
|
});
|
|
7341
7706
|
noticeWake(deps, listing.machine, name);
|
|
@@ -7359,19 +7724,19 @@ async function machineFileListCommand(deps, name, path4, options = {}) {
|
|
|
7359
7724
|
throw explainMachineError(error);
|
|
7360
7725
|
}
|
|
7361
7726
|
}
|
|
7362
|
-
async function machineFileDeleteCommand(deps, name,
|
|
7727
|
+
async function machineFileDeleteCommand(deps, name, path5, options = {}) {
|
|
7363
7728
|
try {
|
|
7364
7729
|
const key = await deps.auth.apiKey();
|
|
7365
7730
|
const region = await regionOf(deps, key, name);
|
|
7366
7731
|
await cellApi(deps).fetch("deleteMachineFile", key, region, {
|
|
7367
7732
|
params: { name },
|
|
7368
|
-
query: { path:
|
|
7733
|
+
query: { path: path5, recursive: options.recursive ? "true" : void 0 },
|
|
7369
7734
|
headers: {
|
|
7370
7735
|
"tonbo-expect-activation": options.expectActivation,
|
|
7371
|
-
"idempotency-key":
|
|
7736
|
+
"idempotency-key": randomUUID8()
|
|
7372
7737
|
}
|
|
7373
7738
|
});
|
|
7374
|
-
print(deps, options.json, { path:
|
|
7739
|
+
print(deps, options.json, { path: path5 }, `Deleted ${path5}.`);
|
|
7375
7740
|
} catch (error) {
|
|
7376
7741
|
throw explainMachineError(error);
|
|
7377
7742
|
}
|
|
@@ -7474,8 +7839,8 @@ async function terminalAsk(question) {
|
|
|
7474
7839
|
|
|
7475
7840
|
// src/ssh.ts
|
|
7476
7841
|
import { spawn, spawnSync } from "node:child_process";
|
|
7477
|
-
import { existsSync } from "node:fs";
|
|
7478
|
-
import { chmod as chmod2, mkdir as
|
|
7842
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
7843
|
+
import { chmod as chmod2, mkdir as mkdir4, readFile as readFile5, readdir, rename as rename4, writeFile as writeFile2 } from "node:fs/promises";
|
|
7479
7844
|
import { homedir } from "node:os";
|
|
7480
7845
|
import { join } from "node:path";
|
|
7481
7846
|
function sshDirectory(env = process.env) {
|
|
@@ -7499,9 +7864,9 @@ function cellOrigin2(host) {
|
|
|
7499
7864
|
}
|
|
7500
7865
|
async function ensureKey(directory) {
|
|
7501
7866
|
const key = join(directory, "id_ed25519");
|
|
7502
|
-
await
|
|
7867
|
+
await mkdir4(directory, { recursive: true, mode: 448 });
|
|
7503
7868
|
await chmod2(directory, 448);
|
|
7504
|
-
if (!
|
|
7869
|
+
if (!existsSync2(key)) {
|
|
7505
7870
|
const made = spawnSync(
|
|
7506
7871
|
"ssh-keygen",
|
|
7507
7872
|
["-q", "-t", "ed25519", "-N", "", "-C", "tonbo", "-f", key],
|
|
@@ -7517,10 +7882,10 @@ async function ensureKey(directory) {
|
|
|
7517
7882
|
}
|
|
7518
7883
|
return key;
|
|
7519
7884
|
}
|
|
7520
|
-
async function writeAtomic(
|
|
7521
|
-
const temporary = `${
|
|
7885
|
+
async function writeAtomic(path5, data) {
|
|
7886
|
+
const temporary = `${path5}.${process.pid}.tmp`;
|
|
7522
7887
|
await writeFile2(temporary, data, { mode: 384 });
|
|
7523
|
-
await
|
|
7888
|
+
await rename4(temporary, path5);
|
|
7524
7889
|
}
|
|
7525
7890
|
function certificatePath(directory, user) {
|
|
7526
7891
|
return join(directory, `${user}-cert.pub`);
|
|
@@ -7533,7 +7898,7 @@ function hostCAPath(directory, host) {
|
|
|
7533
7898
|
}
|
|
7534
7899
|
async function trustHostCA(directory, host, lines) {
|
|
7535
7900
|
const cells = join(directory, "host-ca");
|
|
7536
|
-
await
|
|
7901
|
+
await mkdir4(cells, { recursive: true, mode: 448 });
|
|
7537
7902
|
await writeAtomic(hostCAPath(directory, host), `${lines.join("\n")}
|
|
7538
7903
|
`);
|
|
7539
7904
|
const trusted = /* @__PURE__ */ new Set();
|
|
@@ -7541,7 +7906,7 @@ async function trustHostCA(directory, host, lines) {
|
|
|
7541
7906
|
if (name.endsWith(".tmp")) {
|
|
7542
7907
|
continue;
|
|
7543
7908
|
}
|
|
7544
|
-
for (const line of (await
|
|
7909
|
+
for (const line of (await readFile5(join(cells, name), "utf8")).split("\n")) {
|
|
7545
7910
|
if (line.trim() !== "") {
|
|
7546
7911
|
trusted.add(line.trim());
|
|
7547
7912
|
}
|
|
@@ -7553,7 +7918,7 @@ async function trustHostCA(directory, host, lines) {
|
|
|
7553
7918
|
async function heldUntil(directory, user) {
|
|
7554
7919
|
try {
|
|
7555
7920
|
const sidecar = JSON.parse(
|
|
7556
|
-
await
|
|
7921
|
+
await readFile5(`${certificatePath(directory, user)}.json`, "utf8")
|
|
7557
7922
|
);
|
|
7558
7923
|
return sidecar.valid_before ? Date.parse(sidecar.valid_before) : 0;
|
|
7559
7924
|
} catch {
|
|
@@ -7572,11 +7937,9 @@ async function cellRequest(bearer, method, url, body) {
|
|
|
7572
7937
|
},
|
|
7573
7938
|
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
7574
7939
|
});
|
|
7575
|
-
if (response.status === 401 && attempt === 0 && isAccountBearer(bearer)) {
|
|
7576
|
-
bearer.invalidate(MANAGEMENT_AUDIENCE);
|
|
7577
|
-
continue;
|
|
7578
|
-
}
|
|
7579
7940
|
const answer = await response.json().catch(() => null);
|
|
7941
|
+
if (response.status === 401 && attempt === 0 && retriesAfterRefusal(bearer, MANAGEMENT_AUDIENCE, answer?.code ?? null))
|
|
7942
|
+
continue;
|
|
7580
7943
|
if (!response.ok) {
|
|
7581
7944
|
throw new Error(
|
|
7582
7945
|
`${url} answered ${response.status}${answer?.code ? ` ${answer.code}` : ""}${answer?.detail ? `: ${answer.detail}` : ""}`
|
|
@@ -7589,11 +7952,11 @@ async function refreshCertificate(bearer, destination, now = Date.now, directory
|
|
|
7589
7952
|
const key = await ensureKey(directory);
|
|
7590
7953
|
const certificate = certificatePath(directory, destination.user);
|
|
7591
7954
|
const knownHosts = join(directory, "known_hosts");
|
|
7592
|
-
if (await heldUntil(directory, destination.user) - now() > RENEW_BEFORE_MS &&
|
|
7955
|
+
if (await heldUntil(directory, destination.user) - now() > RENEW_BEFORE_MS && existsSync2(hostCAPath(directory, destination.host)) && existsSync2(knownHosts)) {
|
|
7593
7956
|
return { key, certificate, knownHosts };
|
|
7594
7957
|
}
|
|
7595
7958
|
const origin = cellOrigin2(destination.host);
|
|
7596
|
-
const publicKey = (await
|
|
7959
|
+
const publicKey = (await readFile5(`${key}.pub`, "utf8")).trim();
|
|
7597
7960
|
const issued = await cellRequest(bearer, "POST", `${origin}/v1/ssh/certificates`, {
|
|
7598
7961
|
public_key: publicKey,
|
|
7599
7962
|
ttl_seconds: REQUESTED_TTL_SECONDS
|
|
@@ -7685,7 +8048,7 @@ async function sshHostCaCommand(deps, options) {
|
|
|
7685
8048
|
print(deps, options.json, data, data.known_hosts.join("\n"));
|
|
7686
8049
|
}
|
|
7687
8050
|
async function sshCertificateIssueCommand(deps, options) {
|
|
7688
|
-
const publicKey = (await
|
|
8051
|
+
const publicKey = (await readFile5(options.publicKey, "utf8")).trim();
|
|
7689
8052
|
const key = await deps.auth.apiKey();
|
|
7690
8053
|
const data = await (deps.cell ?? new CellApi(fetch)).json(
|
|
7691
8054
|
"issueSshCertificate",
|
|
@@ -7762,7 +8125,7 @@ var repeated = (value, previous = []) => [...previous, value];
|
|
|
7762
8125
|
var budgetOption = (whom) => new Option(
|
|
7763
8126
|
"--budget <budget>",
|
|
7764
8127
|
`put ${whom} under a monthly budget: an amount in dollars such as 50 creates one, a budget's name adds to it`
|
|
7765
|
-
)
|
|
8128
|
+
);
|
|
7766
8129
|
var roleOption = (description) => new Option("--role <role>", description).choices(accountRoles);
|
|
7767
8130
|
function machineSpecOptions(command, update) {
|
|
7768
8131
|
const later = update ? "; applies at the next boot" : "";
|
|
@@ -7828,7 +8191,9 @@ function createProgram(makeDependencies = createDependencies) {
|
|
|
7828
8191
|
const account2 = program.opts().account;
|
|
7829
8192
|
await loginCommand(dependencies(), { ...options, account: account2 });
|
|
7830
8193
|
});
|
|
7831
|
-
program.command("logout").description(
|
|
8194
|
+
program.command("logout").description(
|
|
8195
|
+
"end this computer's CLI login and remove the saved login; Yield's capture here stops until the next login"
|
|
8196
|
+
).action(async () => logoutCommand(dependencies()));
|
|
7832
8197
|
const account = program.command("account").description("the accounts you belong to and the one commands act in");
|
|
7833
8198
|
operationOptions(
|
|
7834
8199
|
account.command("show").description("show the account, your role in it and its limits"),
|
|
@@ -7919,7 +8284,7 @@ function createProgram(makeDependencies = createDependencies) {
|
|
|
7919
8284
|
).action(
|
|
7920
8285
|
async (keyId, options) => apiKeyRevokeCommand(dependencies(), keyId, options)
|
|
7921
8286
|
);
|
|
7922
|
-
const budget = program.command("budget"
|
|
8287
|
+
const budget = program.command("budget").description("the account's monthly spend budgets");
|
|
7923
8288
|
operationOptions(
|
|
7924
8289
|
budget.command("list").description("list the budgets, the account budget first"),
|
|
7925
8290
|
"listBudgets"
|
|
@@ -8225,31 +8590,31 @@ function createProgram(makeDependencies = createDependencies) {
|
|
|
8225
8590
|
file.command("read <name> <path>").description("print a file of a Machine, waking it first").option("--output <file>", "write the bytes to this local file instead of stdout").option("--range <range>", "one byte range, such as bytes=0-1023").option("--expect-activation <activation>", "read only in this activation"),
|
|
8226
8591
|
"readMachineFile"
|
|
8227
8592
|
).action(
|
|
8228
|
-
async (name,
|
|
8593
|
+
async (name, path5, options) => machineFileReadCommand(dependencies(), name, path5, options)
|
|
8229
8594
|
);
|
|
8230
8595
|
operationOptions(
|
|
8231
8596
|
file.command("write <name> <path>").description("replace a file of a Machine with stdin or --from, atomically").option("--from <file>", "the local file to send instead of stdin").option("--mode <mode>", "the file's mode, four octal digits such as 0755").option("--if-match <etag>", "replace only the version with this ETag").option("--create", "only create the file; refuse when it exists").option("--expect-activation <activation>", "write only in this activation"),
|
|
8232
8597
|
"writeMachineFile"
|
|
8233
8598
|
).action(
|
|
8234
|
-
async (name,
|
|
8599
|
+
async (name, path5, options) => machineFileWriteCommand(dependencies(), name, path5, options)
|
|
8235
8600
|
);
|
|
8236
8601
|
operationOptions(
|
|
8237
8602
|
file.command("stat <name> <path>").description("show a path's type, size, mode, owner and ETag").option("--expect-activation <activation>", "stat only in this activation"),
|
|
8238
8603
|
"statMachineFile"
|
|
8239
8604
|
).action(
|
|
8240
|
-
async (name,
|
|
8605
|
+
async (name, path5, options) => machineFileStatCommand(dependencies(), name, path5, options)
|
|
8241
8606
|
);
|
|
8242
8607
|
operationOptions(
|
|
8243
8608
|
file.command("list <name> <path>").description("list a directory of a Machine").option("--expect-activation <activation>", "list only in this activation"),
|
|
8244
8609
|
"listMachineFiles"
|
|
8245
8610
|
).action(
|
|
8246
|
-
async (name,
|
|
8611
|
+
async (name, path5, options) => machineFileListCommand(dependencies(), name, path5, options)
|
|
8247
8612
|
);
|
|
8248
8613
|
operationOptions(
|
|
8249
8614
|
file.command("delete <name> <path>").description("delete a file, a symbolic link or an empty directory of a Machine").option("--recursive", "delete a directory and everything in it").option("--expect-activation <activation>", "delete only in this activation"),
|
|
8250
8615
|
"deleteMachineFile"
|
|
8251
8616
|
).action(
|
|
8252
|
-
async (name,
|
|
8617
|
+
async (name, path5, options) => machineFileDeleteCommand(dependencies(), name, path5, options)
|
|
8253
8618
|
);
|
|
8254
8619
|
const hold = machine.command("hold").description("keep a Machine awake for work that has no open connection");
|
|
8255
8620
|
operationOptions(
|