bitfab-cli 0.2.44 → 0.2.46
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/dist/index.js +209 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6972,6 +6972,79 @@ async function pollLoginEvents(opts) {
|
|
|
6972
6972
|
}
|
|
6973
6973
|
throw new Error("Login polling aborted");
|
|
6974
6974
|
}
|
|
6975
|
+
var STUDIO_SESSION_SCOPE_HEADER = "x-bitfab-auth-scope";
|
|
6976
|
+
var STUDIO_SESSION_SCOPE_VALUE = "session";
|
|
6977
|
+
async function requestSessionReauth(opts) {
|
|
6978
|
+
try {
|
|
6979
|
+
const res = await fetch(`${opts.serviceUrl}/api/studio/events`, {
|
|
6980
|
+
method: "POST",
|
|
6981
|
+
headers: {
|
|
6982
|
+
"Content-Type": "application/json",
|
|
6983
|
+
[STUDIO_SESSION_SCOPE_HEADER]: STUDIO_SESSION_SCOPE_VALUE
|
|
6984
|
+
},
|
|
6985
|
+
body: JSON.stringify({
|
|
6986
|
+
session: opts.sessionId,
|
|
6987
|
+
type: "agent:request-reauth",
|
|
6988
|
+
data: { redirectPath: opts.redirectPath }
|
|
6989
|
+
})
|
|
6990
|
+
});
|
|
6991
|
+
if (!res.ok) {
|
|
6992
|
+
return { ok: false };
|
|
6993
|
+
}
|
|
6994
|
+
const body = await res.json();
|
|
6995
|
+
return {
|
|
6996
|
+
ok: true,
|
|
6997
|
+
pushedEventId: body.id,
|
|
6998
|
+
signInUrl: `${opts.serviceUrl}${body.signInPath}`
|
|
6999
|
+
};
|
|
7000
|
+
} catch {
|
|
7001
|
+
return { ok: false };
|
|
7002
|
+
}
|
|
7003
|
+
}
|
|
7004
|
+
async function pollSessionAuthEvents(opts) {
|
|
7005
|
+
const interval = opts.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
7006
|
+
let since = opts.startCursor ?? null;
|
|
7007
|
+
while (!opts.abortSignal.aborted) {
|
|
7008
|
+
try {
|
|
7009
|
+
const url2 = new URL(`${opts.serviceUrl}/api/studio/events`);
|
|
7010
|
+
url2.searchParams.set("session", opts.sessionId);
|
|
7011
|
+
if (since) {
|
|
7012
|
+
url2.searchParams.set("since", since);
|
|
7013
|
+
}
|
|
7014
|
+
const res = await fetch(url2.toString(), {
|
|
7015
|
+
headers: {
|
|
7016
|
+
[STUDIO_SESSION_SCOPE_HEADER]: STUDIO_SESSION_SCOPE_VALUE
|
|
7017
|
+
},
|
|
7018
|
+
signal: opts.abortSignal
|
|
7019
|
+
});
|
|
7020
|
+
if (res.ok) {
|
|
7021
|
+
const body = await res.json();
|
|
7022
|
+
for (const event of body.events) {
|
|
7023
|
+
since = event.id;
|
|
7024
|
+
opts.onEvent(event);
|
|
7025
|
+
}
|
|
7026
|
+
if (body.cursor) {
|
|
7027
|
+
since = body.cursor;
|
|
7028
|
+
}
|
|
7029
|
+
}
|
|
7030
|
+
} catch {
|
|
7031
|
+
if (opts.abortSignal.aborted) {
|
|
7032
|
+
return;
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
7035
|
+
await new Promise((resolve) => {
|
|
7036
|
+
const onAbort = () => {
|
|
7037
|
+
clearTimeout(timer);
|
|
7038
|
+
resolve();
|
|
7039
|
+
};
|
|
7040
|
+
const timer = setTimeout(() => {
|
|
7041
|
+
opts.abortSignal.removeEventListener("abort", onAbort);
|
|
7042
|
+
resolve();
|
|
7043
|
+
}, interval);
|
|
7044
|
+
opts.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
7045
|
+
});
|
|
7046
|
+
}
|
|
7047
|
+
}
|
|
6975
7048
|
async function fetchStreamTip(client) {
|
|
6976
7049
|
const PAGE_LIMIT = 500;
|
|
6977
7050
|
const MAX_PAGES = 10;
|
|
@@ -7964,6 +8037,7 @@ async function waitForSessionReady(opts) {
|
|
|
7964
8037
|
|
|
7965
8038
|
// ../bitfab-plugin-lib/dist/commands/openStudioTo.js
|
|
7966
8039
|
var LOGIN_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
8040
|
+
var REAUTH_ACK_TIMEOUT_MS = 2e4;
|
|
7967
8041
|
var StudioNavigationError = class extends Error {
|
|
7968
8042
|
reason;
|
|
7969
8043
|
blockedReason;
|
|
@@ -7979,14 +8053,34 @@ var StudioNavigationError = class extends Error {
|
|
|
7979
8053
|
async function openStudioTo(path15, opts = {}) {
|
|
7980
8054
|
const config2 = getConfig();
|
|
7981
8055
|
const serviceUrl = config2.serviceUrl;
|
|
7982
|
-
const apiKey = hasCredentials() ? config2.apiKey : null;
|
|
8056
|
+
const apiKey = !opts.forceLogin && hasCredentials() ? config2.apiKey : null;
|
|
7983
8057
|
const noop = () => {
|
|
7984
8058
|
};
|
|
7985
8059
|
const restoreFocus = recordFocus();
|
|
7986
8060
|
if (!apiKey) {
|
|
8061
|
+
const existing2 = readActiveStudioSession();
|
|
8062
|
+
if (existing2) {
|
|
8063
|
+
const reauth = await requestSessionReauth({
|
|
8064
|
+
serviceUrl: existing2.serviceUrl,
|
|
8065
|
+
sessionId: existing2.sessionId,
|
|
8066
|
+
redirectPath: path15
|
|
8067
|
+
});
|
|
8068
|
+
if (!reauth.ok) {
|
|
8069
|
+
throw new StudioNavigationError("push-failed", void 0, existing2.sessionId);
|
|
8070
|
+
}
|
|
8071
|
+
return loginViaExistingWindow({
|
|
8072
|
+
serviceUrl: existing2.serviceUrl,
|
|
8073
|
+
sessionId: existing2.sessionId,
|
|
8074
|
+
startCursor: reauth.pushedEventId,
|
|
8075
|
+
signInUrl: reauth.signInUrl,
|
|
8076
|
+
restoreFocus,
|
|
8077
|
+
opts
|
|
8078
|
+
});
|
|
8079
|
+
}
|
|
7987
8080
|
const sessionId2 = crypto4.randomUUID();
|
|
7988
|
-
const
|
|
7989
|
-
const
|
|
8081
|
+
const freshPath = opts.freshWindowPath ?? path15;
|
|
8082
|
+
const separator = freshPath.includes("?") ? "&" : "?";
|
|
8083
|
+
const redirectPath = `${freshPath}${separator}session=${encodeURIComponent(sessionId2)}&pluginLogin=true`;
|
|
7990
8084
|
const signInPath = `/studio/sign-in?redirect_url=${encodeURIComponent(redirectPath)}`;
|
|
7991
8085
|
const signInUrl = `${serviceUrl}${signInPath}&session=${encodeURIComponent(sessionId2)}`;
|
|
7992
8086
|
const windowPid = openChromelessWindow(signInUrl);
|
|
@@ -8077,6 +8171,97 @@ async function openStudioTo(path15, opts = {}) {
|
|
|
8077
8171
|
...poller
|
|
8078
8172
|
};
|
|
8079
8173
|
}
|
|
8174
|
+
async function loginViaExistingWindow(args) {
|
|
8175
|
+
const { serviceUrl, sessionId } = args;
|
|
8176
|
+
const noop = () => {
|
|
8177
|
+
};
|
|
8178
|
+
const keepalive = setInterval(() => process.stderr.write(""), 3e4);
|
|
8179
|
+
let loginApiKey;
|
|
8180
|
+
try {
|
|
8181
|
+
loginApiKey = await awaitReauthLogin({
|
|
8182
|
+
serviceUrl,
|
|
8183
|
+
sessionId,
|
|
8184
|
+
startCursor: args.startCursor,
|
|
8185
|
+
// Only fired once the sign-in page proved the window alive AND is
|
|
8186
|
+
// waiting on the user; a still-valid browser session re-auths silently
|
|
8187
|
+
// and the caller never surfaces a sign-in prompt.
|
|
8188
|
+
onAck: () => args.opts.onLoginRequired?.(sessionId, args.signInUrl)
|
|
8189
|
+
});
|
|
8190
|
+
} finally {
|
|
8191
|
+
clearInterval(keepalive);
|
|
8192
|
+
}
|
|
8193
|
+
saveCredentials(loginApiKey);
|
|
8194
|
+
args.opts.onAuthenticated?.(sessionId);
|
|
8195
|
+
if (args.opts.onEvent) {
|
|
8196
|
+
await waitForSessionReady({
|
|
8197
|
+
serviceUrl,
|
|
8198
|
+
apiKey: loginApiKey,
|
|
8199
|
+
sessionId
|
|
8200
|
+
});
|
|
8201
|
+
const poller = startEventPoller(serviceUrl, loginApiKey, sessionId, args.opts.onEvent, args.restoreFocus);
|
|
8202
|
+
return {
|
|
8203
|
+
sessionId,
|
|
8204
|
+
serviceUrl,
|
|
8205
|
+
apiKey: loginApiKey,
|
|
8206
|
+
opened: false,
|
|
8207
|
+
...poller
|
|
8208
|
+
};
|
|
8209
|
+
}
|
|
8210
|
+
args.restoreFocus();
|
|
8211
|
+
return {
|
|
8212
|
+
sessionId,
|
|
8213
|
+
serviceUrl,
|
|
8214
|
+
apiKey: loginApiKey,
|
|
8215
|
+
opened: false,
|
|
8216
|
+
abort: noop,
|
|
8217
|
+
done: Promise.resolve()
|
|
8218
|
+
};
|
|
8219
|
+
}
|
|
8220
|
+
function awaitReauthLogin(args) {
|
|
8221
|
+
return new Promise((resolve, reject) => {
|
|
8222
|
+
const abortController = new AbortController();
|
|
8223
|
+
let settled = false;
|
|
8224
|
+
let acked = false;
|
|
8225
|
+
const settle = (fn) => {
|
|
8226
|
+
if (settled) {
|
|
8227
|
+
return;
|
|
8228
|
+
}
|
|
8229
|
+
settled = true;
|
|
8230
|
+
clearTimeout(ackTimer);
|
|
8231
|
+
clearTimeout(loginTimer);
|
|
8232
|
+
abortController.abort();
|
|
8233
|
+
fn();
|
|
8234
|
+
};
|
|
8235
|
+
const ackTimer = setTimeout(() => {
|
|
8236
|
+
settle(() => reject(new StudioNavigationError("timeout", void 0, args.sessionId)));
|
|
8237
|
+
}, REAUTH_ACK_TIMEOUT_MS);
|
|
8238
|
+
const loginTimer = setTimeout(() => {
|
|
8239
|
+
settle(() => reject(new Error("Login polling aborted")));
|
|
8240
|
+
}, LOGIN_TIMEOUT_MS);
|
|
8241
|
+
pollSessionAuthEvents({
|
|
8242
|
+
serviceUrl: args.serviceUrl,
|
|
8243
|
+
sessionId: args.sessionId,
|
|
8244
|
+
startCursor: args.startCursor,
|
|
8245
|
+
abortSignal: abortController.signal,
|
|
8246
|
+
onEvent: (event) => {
|
|
8247
|
+
if (event.type === "studio:auth-required" && !acked) {
|
|
8248
|
+
acked = true;
|
|
8249
|
+
clearTimeout(ackTimer);
|
|
8250
|
+
args.onAck();
|
|
8251
|
+
return;
|
|
8252
|
+
}
|
|
8253
|
+
if (event.type === "studio:authenticated") {
|
|
8254
|
+
const token = event.data.token;
|
|
8255
|
+
if (typeof token === "string") {
|
|
8256
|
+
settle(() => resolve(token));
|
|
8257
|
+
}
|
|
8258
|
+
}
|
|
8259
|
+
}
|
|
8260
|
+
}).catch((err) => {
|
|
8261
|
+
settle(() => reject(err instanceof Error ? err : new Error(String(err))));
|
|
8262
|
+
});
|
|
8263
|
+
});
|
|
8264
|
+
}
|
|
8080
8265
|
function startEventPoller(serviceUrl, apiKey, sessionId, onEvent, restoreFocus) {
|
|
8081
8266
|
const abortController = new AbortController();
|
|
8082
8267
|
let endedNotified = false;
|
|
@@ -8160,11 +8345,25 @@ async function verifyToken(serviceUrl, token) {
|
|
|
8160
8345
|
}
|
|
8161
8346
|
async function runLogin(platform2, pluginVersion, options) {
|
|
8162
8347
|
const exitOnComplete = options?.exitOnComplete ?? true;
|
|
8348
|
+
const force = options?.force ?? process.argv.includes("--force");
|
|
8163
8349
|
const config2 = getConfig();
|
|
8350
|
+
if (!force && hasCredentials() && config2.apiKey) {
|
|
8351
|
+
const who = await verifyToken(config2.serviceUrl, config2.apiKey);
|
|
8352
|
+
if (who) {
|
|
8353
|
+
const identity = who.user.email ?? "your account";
|
|
8354
|
+
console.log(`
|
|
8355
|
+
Already logged in as ${identity}. Run the login command with --force to re-authenticate.`);
|
|
8356
|
+
if (exitOnComplete) {
|
|
8357
|
+
process.exit(0);
|
|
8358
|
+
}
|
|
8359
|
+
return;
|
|
8360
|
+
}
|
|
8361
|
+
}
|
|
8164
8362
|
console.log("\nOpening Studio to sign in...");
|
|
8165
|
-
const closePath = `/studio/close?autoClose=true&message=${encodeURIComponent("Login complete")}`;
|
|
8166
8363
|
try {
|
|
8167
|
-
const result = await openStudioTo(
|
|
8364
|
+
const result = await openStudioTo("/studio", {
|
|
8365
|
+
forceLogin: true,
|
|
8366
|
+
freshWindowPath: `/studio/close?autoClose=true&message=${encodeURIComponent("Login complete")}`,
|
|
8168
8367
|
onLoginRequired: (_sessionId, signInUrl) => {
|
|
8169
8368
|
console.log(`
|
|
8170
8369
|
If the browser didn't open, visit this URL manually:
|
|
@@ -8191,8 +8390,12 @@ ${greeting}`);
|
|
|
8191
8390
|
}
|
|
8192
8391
|
} catch (err) {
|
|
8193
8392
|
if (exitOnComplete) {
|
|
8194
|
-
|
|
8393
|
+
if (err instanceof StudioNavigationError && err.staleSessionId) {
|
|
8394
|
+
console.error("\nA Studio window is recorded as open but is not responding. Close it (or run the clearStudioSession command), then try again.");
|
|
8395
|
+
} else {
|
|
8396
|
+
console.error(`
|
|
8195
8397
|
${err.message}`);
|
|
8398
|
+
}
|
|
8196
8399
|
process.exit(1);
|
|
8197
8400
|
}
|
|
8198
8401
|
throw err;
|