bitfab-cli 0.2.45 → 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.
Files changed (2) hide show
  1. package/dist/index.js +209 -6
  2. 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 separator = path15.includes("?") ? "&" : "?";
7989
- const redirectPath = `${path15}${separator}session=${encodeURIComponent(sessionId2)}&pluginLogin=true`;
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(closePath, {
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
- console.error(`
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitfab-cli",
3
- "version": "0.2.45",
3
+ "version": "0.2.46",
4
4
  "description": "Install and configure the Bitfab plugin in Claude Code, Codex, or Cursor.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",