@runappsai/sdk 0.7.7 → 0.7.9
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/browser-auth.d.ts +4 -0
- package/dist/browser-auth.d.ts.map +1 -1
- package/dist/browser-auth.js +100 -41
- package/dist/browser-auth.js.map +1 -1
- package/dist/sdk.umd.js +3 -3
- package/dist/sdk.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/browser-auth.ts +90 -32
package/package.json
CHANGED
package/src/browser-auth.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
220
|
-
// We are the sign-in popup
|
|
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;
|
|
@@ -250,7 +261,10 @@ export class BrowserAuth {
|
|
|
250
261
|
// user to click signIn. Without this guard, the backend's
|
|
251
262
|
// auto-grant flow would re-issue a token on every page reload
|
|
252
263
|
// (the cookie on the gateway domain is still valid).
|
|
253
|
-
|
|
264
|
+
// In popup mode a signed-out user is simply asked to sign in again:
|
|
265
|
+
// the popup only ever opens on the user's own action, so there is no
|
|
266
|
+
// page to lose and no reason to refuse the call.
|
|
267
|
+
if (this.isSignedOut() && !(this.signInMode === "popup" && !this.inIframe())) {
|
|
254
268
|
throw new Error("RunApps: signed out — call client.signIn() to authenticate");
|
|
255
269
|
}
|
|
256
270
|
|
|
@@ -399,8 +413,18 @@ export class BrowserAuth {
|
|
|
399
413
|
this.signingIn = true;
|
|
400
414
|
location.href = this.grantUrlForThisPage();
|
|
401
415
|
}
|
|
402
|
-
private grantUrlForThisPage(): string {
|
|
403
|
-
|
|
416
|
+
private grantUrlForThisPage(popup = false): string {
|
|
417
|
+
let ret = location.href.split("#")[0] ?? location.href;
|
|
418
|
+
if (popup) {
|
|
419
|
+
if (hasCallbackPage(location.hostname)) {
|
|
420
|
+
const q = new URLSearchParams({ key: storageKey(this.project), origin: this.origin });
|
|
421
|
+
ret = location.origin + POPUP_CALLBACK_PATH + "?" + q.toString();
|
|
422
|
+
} else {
|
|
423
|
+
const u = new URL(ret);
|
|
424
|
+
u.searchParams.set(POPUP_PARAM, "1");
|
|
425
|
+
ret = u.toString();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
404
428
|
const scheme = window.matchMedia?.("(prefers-color-scheme: dark)")
|
|
405
429
|
.matches
|
|
406
430
|
? "dark"
|
|
@@ -414,55 +438,89 @@ export class BrowserAuth {
|
|
|
414
438
|
}
|
|
415
439
|
/** Popup grant flow. One popup at a time; concurrent callers share it. */
|
|
416
440
|
private signInPopup(): Promise<void> {
|
|
417
|
-
|
|
441
|
+
// One popup at a time; a click a few seconds later (the user closed
|
|
442
|
+
// the first one, say) opens a fresh one and joins the same wait.
|
|
443
|
+
const now = Date.now();
|
|
444
|
+
if (this.popupWait && now - this.popupOpenedAt < 3000) return this.popupWait;
|
|
418
445
|
this.clearSignedOut();
|
|
419
446
|
const key = storageKey(this.project);
|
|
420
|
-
const
|
|
447
|
+
const open = () => window.open(this.grantUrlForThisPage(true), POPUP_NAME, "popup=yes,width=520,height=700");
|
|
448
|
+
let win = open();
|
|
421
449
|
if (!win) {
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
let timer = 0;
|
|
429
|
-
const done = (err?: Error) => {
|
|
430
|
-
window.removeEventListener("storage", onStorage);
|
|
431
|
-
window.clearInterval(timer);
|
|
450
|
+
// Blocked — usually because no user gesture is active (a call made
|
|
451
|
+
// at load, or after a long await). Open it on the user's next click
|
|
452
|
+
// or key press instead; only a popup blocked on a real gesture falls
|
|
453
|
+
// back to the redirect.
|
|
454
|
+
if (this.popupWait) return this.popupWait;
|
|
455
|
+
this.popupWait = this.waitForUserGesture().then(() => {
|
|
432
456
|
this.popupWait = null;
|
|
433
|
-
|
|
457
|
+
win = open();
|
|
458
|
+
if (!win) {
|
|
459
|
+
this.signingIn = true;
|
|
460
|
+
location.href = this.grantUrlForThisPage();
|
|
461
|
+
return new Promise<void>(() => {});
|
|
462
|
+
}
|
|
463
|
+
return this.waitForPopupToken(key, Date.now());
|
|
464
|
+
});
|
|
465
|
+
return this.popupWait;
|
|
466
|
+
}
|
|
467
|
+
if (this.popupWait) { this.popupOpenedAt = now; return this.popupWait; }
|
|
468
|
+
return this.waitForPopupToken(key, now);
|
|
469
|
+
}
|
|
470
|
+
/** Resolves on the next click, tap or key press anywhere in the page. */
|
|
471
|
+
private waitForUserGesture(): Promise<void> {
|
|
472
|
+
return new Promise((resolve) => {
|
|
473
|
+
const types = ["pointerdown", "keydown", "touchend"];
|
|
474
|
+
const handler = () => {
|
|
475
|
+
for (const t of types) window.removeEventListener(t, handler, true);
|
|
476
|
+
resolve();
|
|
434
477
|
};
|
|
478
|
+
for (const t of types) window.addEventListener(t, handler, true);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
private waitForPopupToken(key: string, openedAt: number): Promise<void> {
|
|
482
|
+
this.popupOpenedAt = openedAt;
|
|
483
|
+
// Under COOP the popup's handle is cut off as soon as it leaves this
|
|
484
|
+
// origin (`win.closed` reads true), so a closed popup cannot be told
|
|
485
|
+
// from one still signing in: the promise stays pending until a token
|
|
486
|
+
// arrives, like Puter's own sign-in.
|
|
487
|
+
this.popupWait = new Promise<void>((resolve) => {
|
|
488
|
+
let timer = 0;
|
|
435
489
|
const adopt = (): boolean => {
|
|
436
490
|
const before = this.token;
|
|
437
491
|
this.loadPersisted();
|
|
438
492
|
if (!this.tokenIsFresh()) return false;
|
|
439
493
|
if (this.token !== before) this.setToken(this.token as string, this.expiresAt, this.userInfo ?? undefined);
|
|
440
494
|
else if (this.userInfo && !this.hideBadge) this.renderBadge();
|
|
441
|
-
|
|
442
|
-
|
|
495
|
+
window.removeEventListener("storage", onStorage);
|
|
496
|
+
window.clearInterval(timer);
|
|
497
|
+
this.popupWait = null;
|
|
498
|
+
resolve();
|
|
443
499
|
return true;
|
|
444
500
|
};
|
|
445
501
|
const onStorage = (e: StorageEvent) => {
|
|
446
502
|
if (e.key === key && e.newValue) adopt();
|
|
447
503
|
};
|
|
448
504
|
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);
|
|
505
|
+
timer = window.setInterval(adopt, 1000);
|
|
453
506
|
});
|
|
454
507
|
return this.popupWait;
|
|
455
508
|
}
|
|
456
509
|
/** Inside the popup, after the token landed: close, or say so. */
|
|
457
510
|
private finishPopup() {
|
|
458
511
|
try { window.close(); } catch { /* not script-closable */ }
|
|
459
|
-
window.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
512
|
+
if (window.closed) return;
|
|
513
|
+
// Still open: stop the app from loading and show a plain page instead.
|
|
514
|
+
try { window.stop(); } catch { /* ignore */ }
|
|
515
|
+
try {
|
|
516
|
+
document.open();
|
|
517
|
+
document.write(
|
|
518
|
+
'<!doctype html><html><head><meta charset="utf-8"><title>Signed in</title></head>' +
|
|
519
|
+
'<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">' +
|
|
520
|
+
'<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>',
|
|
521
|
+
);
|
|
522
|
+
document.close();
|
|
523
|
+
} catch { /* ignore */ }
|
|
466
524
|
}
|
|
467
525
|
|
|
468
526
|
/**
|