@runappsai/sdk 0.7.7 → 0.7.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runappsai/sdk",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
4
4
  "description": "JavaScript / TypeScript client for the RunApps AI Gateway",
5
5
  "license": "MIT",
6
6
  "author": "RunApps",
@@ -115,8 +115,17 @@ const TOKEN_REFRESH_MARGIN_S = 60;
115
115
  // unsuffixed key, which preserves back-compat for already-persisted
116
116
  // tokens after upgrade.
117
117
  const STORAGE_KEY_BASE = "__runapps_auth_v1__";
118
- /** window.name of the sign-in popup; the page loaded in it closes itself. */
118
+ /** window.name of the sign-in popup. */
119
119
  const POPUP_NAME = "runapps-signin";
120
+ /** Query parameter marking the app URL the popup lands on when the host
121
+ * has no callback page (window.name is reset by a COOP swap, so it
122
+ * cannot carry the mark). */
123
+ const POPUP_PARAM = "runapps_popup";
124
+ /** The callback page every app host on the platform serves: it stores the
125
+ * token for the app and closes, without loading the app. */
126
+ const POPUP_CALLBACK_PATH = "/__runapps/signin.html";
127
+ /** Hosts whose backend serves POPUP_CALLBACK_PATH. */
128
+ const hasCallbackPage = (hostname: string) => /\.runapps\.dev$/i.test(hostname) || /\.localhost$/i.test(hostname);
120
129
  // Written by SDK 0.3 and earlier; read once and migrated, so an upgrade
121
130
  // keeps the user signed in.
122
131
  const LEGACY_STORAGE_KEY_BASE = "__runjobs_auth_v1__";
@@ -182,6 +191,7 @@ export class BrowserAuth {
182
191
  private expiresAt = 0;
183
192
  private signInMode: "redirect" | "popup";
184
193
  private popupWait: Promise<void> | null = null;
194
+ private popupOpenedAt = 0;
185
195
  private userInfo: BrowserUser | null = null;
186
196
  private listeners: Array<(token: string) => void> = [];
187
197
  private parentHandshake: Promise<void> | null = null;
@@ -216,8 +226,9 @@ export class BrowserAuth {
216
226
  // sticky flag — the user explicitly signed in again.
217
227
  if (this.consumeFragment()) {
218
228
  this.clearSignedOut();
219
- if (window.name === POPUP_NAME) {
220
- // We are the sign-in popup: the token is in localStorage, which
229
+ if (new URLSearchParams(location.search).has(POPUP_PARAM)) {
230
+ // We are the sign-in popup, landed on the app itself (no callback
231
+ // page on this host): the token is already in localStorage, which
221
232
  // the opener (same origin) sees through its `storage` event.
222
233
  this.finishPopup();
223
234
  return;
@@ -399,8 +410,18 @@ export class BrowserAuth {
399
410
  this.signingIn = true;
400
411
  location.href = this.grantUrlForThisPage();
401
412
  }
402
- private grantUrlForThisPage(): string {
403
- const ret = location.href.split("#")[0] ?? location.href;
413
+ private grantUrlForThisPage(popup = false): string {
414
+ let ret = location.href.split("#")[0] ?? location.href;
415
+ if (popup) {
416
+ if (hasCallbackPage(location.hostname)) {
417
+ const q = new URLSearchParams({ key: storageKey(this.project), origin: this.origin });
418
+ ret = location.origin + POPUP_CALLBACK_PATH + "?" + q.toString();
419
+ } else {
420
+ const u = new URL(ret);
421
+ u.searchParams.set(POPUP_PARAM, "1");
422
+ ret = u.toString();
423
+ }
424
+ }
404
425
  const scheme = window.matchMedia?.("(prefers-color-scheme: dark)")
405
426
  .matches
406
427
  ? "dark"
@@ -414,55 +435,62 @@ export class BrowserAuth {
414
435
  }
415
436
  /** Popup grant flow. One popup at a time; concurrent callers share it. */
416
437
  private signInPopup(): Promise<void> {
417
- if (this.popupWait) return this.popupWait;
438
+ // One popup at a time; a click a few seconds later (the user closed
439
+ // the first one, say) opens a fresh one and joins the same wait.
440
+ const now = Date.now();
441
+ if (this.popupWait && now - this.popupOpenedAt < 3000) return this.popupWait;
418
442
  this.clearSignedOut();
419
443
  const key = storageKey(this.project);
420
- const win = window.open(this.grantUrlForThisPage(), POPUP_NAME, "popup=yes,width=520,height=700");
444
+ const win = window.open(this.grantUrlForThisPage(true), POPUP_NAME, "popup=yes,width=520,height=700");
421
445
  if (!win) {
422
446
  // Popup blocked: the redirect is the only way left.
423
447
  this.signingIn = true;
424
448
  location.href = this.grantUrlForThisPage();
425
449
  return new Promise(() => {});
426
450
  }
427
- this.popupWait = new Promise<void>((resolve, reject) => {
451
+ this.popupOpenedAt = now;
452
+ if (this.popupWait) return this.popupWait;
453
+ // Under COOP the popup's handle is cut off as soon as it leaves this
454
+ // origin (`win.closed` reads true), so a closed popup cannot be told
455
+ // from one still signing in: the promise stays pending until a token
456
+ // arrives, like Puter's own sign-in.
457
+ this.popupWait = new Promise<void>((resolve) => {
428
458
  let timer = 0;
429
- const done = (err?: Error) => {
430
- window.removeEventListener("storage", onStorage);
431
- window.clearInterval(timer);
432
- this.popupWait = null;
433
- if (err) reject(err); else resolve();
434
- };
435
459
  const adopt = (): boolean => {
436
460
  const before = this.token;
437
461
  this.loadPersisted();
438
462
  if (!this.tokenIsFresh()) return false;
439
463
  if (this.token !== before) this.setToken(this.token as string, this.expiresAt, this.userInfo ?? undefined);
440
464
  else if (this.userInfo && !this.hideBadge) this.renderBadge();
441
- try { win.close(); } catch { /* already gone */ }
442
- done();
465
+ window.removeEventListener("storage", onStorage);
466
+ window.clearInterval(timer);
467
+ this.popupWait = null;
468
+ resolve();
443
469
  return true;
444
470
  };
445
471
  const onStorage = (e: StorageEvent) => {
446
472
  if (e.key === key && e.newValue) adopt();
447
473
  };
448
474
  window.addEventListener("storage", onStorage);
449
- timer = window.setInterval(() => {
450
- if (adopt()) return;
451
- if (win.closed) done(new Error("RunApps: sign-in was cancelled"));
452
- }, 500);
475
+ timer = window.setInterval(adopt, 1000);
453
476
  });
454
477
  return this.popupWait;
455
478
  }
456
479
  /** Inside the popup, after the token landed: close, or say so. */
457
480
  private finishPopup() {
458
481
  try { window.close(); } catch { /* not script-closable */ }
459
- window.setTimeout(() => {
460
- if (window.closed) return;
461
- document.documentElement.innerHTML =
462
- '<head><meta charset="utf-8"><title>Signed in</title></head>' +
463
- '<body style="margin:0;display:flex;align-items:center;justify-content:center;height:100vh;font:15px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;color:#333;background:#fafafa">' +
464
- "<p>Signed in. You can close this window and go back to the app.</p></body>";
465
- }, 400);
482
+ if (window.closed) return;
483
+ // Still open: stop the app from loading and show a plain page instead.
484
+ try { window.stop(); } catch { /* ignore */ }
485
+ try {
486
+ document.open();
487
+ document.write(
488
+ '<!doctype html><html><head><meta charset="utf-8"><title>Signed in</title></head>' +
489
+ '<body style="margin:0;display:flex;align-items:center;justify-content:center;height:100vh;font:15px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;color:#333;background:#fafafa;text-align:center">' +
490
+ '<div><p>Signed in. You can go back to the app.</p><button onclick="window.close()" style="padding:8px 16px;font:inherit;border:1px solid #ccc;border-radius:8px;background:#fff;cursor:pointer">Close this window</button></div></body></html>',
491
+ );
492
+ document.close();
493
+ } catch { /* ignore */ }
466
494
  }
467
495
 
468
496
  /**