@runappsai/sdk 0.7.6 → 0.7.7
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 +14 -0
- package/dist/browser-auth.d.ts +21 -2
- package/dist/browser-auth.d.ts.map +1 -1
- package/dist/browser-auth.js +98 -3
- package/dist/browser-auth.js.map +1 -1
- package/dist/client.d.ts +9 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -1
- package/dist/client.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 +89 -4
- package/src/client.ts +11 -2
package/package.json
CHANGED
package/src/browser-auth.ts
CHANGED
|
@@ -41,6 +41,16 @@ export interface BrowserAuthOptions {
|
|
|
41
41
|
* through `RunApps`.
|
|
42
42
|
*/
|
|
43
43
|
hideBadge?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* How a missing token is obtained. `"redirect"` (default) navigates
|
|
46
|
+
* the page to the grant flow and back, which discards unsaved work in
|
|
47
|
+
* the page. `"popup"` opens the grant flow in a popup window and waits:
|
|
48
|
+
* the popup lands back on this origin with the token, which it stores,
|
|
49
|
+
* and this page picks it up through the `storage` event — so it works
|
|
50
|
+
* under COOP, where popups lose their `window.opener`. A blocked popup
|
|
51
|
+
* falls back to the redirect.
|
|
52
|
+
*/
|
|
53
|
+
signInMode?: "redirect" | "popup";
|
|
44
54
|
/**
|
|
45
55
|
* Pin the grant flow to a named runapps.ai project. When set, the
|
|
46
56
|
* SDK passes `project_id=<value>` to `/api/sdk/grant`, asking the
|
|
@@ -105,6 +115,8 @@ const TOKEN_REFRESH_MARGIN_S = 60;
|
|
|
105
115
|
// unsuffixed key, which preserves back-compat for already-persisted
|
|
106
116
|
// tokens after upgrade.
|
|
107
117
|
const STORAGE_KEY_BASE = "__runapps_auth_v1__";
|
|
118
|
+
/** window.name of the sign-in popup; the page loaded in it closes itself. */
|
|
119
|
+
const POPUP_NAME = "runapps-signin";
|
|
108
120
|
// Written by SDK 0.3 and earlier; read once and migrated, so an upgrade
|
|
109
121
|
// keeps the user signed in.
|
|
110
122
|
const LEGACY_STORAGE_KEY_BASE = "__runjobs_auth_v1__";
|
|
@@ -168,6 +180,8 @@ export class BrowserAuth {
|
|
|
168
180
|
private readonly badgePosition: BadgePosition;
|
|
169
181
|
private token: string | null = null;
|
|
170
182
|
private expiresAt = 0;
|
|
183
|
+
private signInMode: "redirect" | "popup";
|
|
184
|
+
private popupWait: Promise<void> | null = null;
|
|
171
185
|
private userInfo: BrowserUser | null = null;
|
|
172
186
|
private listeners: Array<(token: string) => void> = [];
|
|
173
187
|
private parentHandshake: Promise<void> | null = null;
|
|
@@ -179,6 +193,7 @@ export class BrowserAuth {
|
|
|
179
193
|
this.project = opts.project ?? null;
|
|
180
194
|
this.events = opts.events ?? null;
|
|
181
195
|
this.badgePosition = opts.badgePosition ?? "bottom-right";
|
|
196
|
+
this.signInMode = opts.signInMode ?? "redirect";
|
|
182
197
|
if (this.events && !this.hideBadge) {
|
|
183
198
|
this.tracker = new ActivityTracker();
|
|
184
199
|
this.tracker.attach(this.events);
|
|
@@ -201,6 +216,12 @@ export class BrowserAuth {
|
|
|
201
216
|
// sticky flag — the user explicitly signed in again.
|
|
202
217
|
if (this.consumeFragment()) {
|
|
203
218
|
this.clearSignedOut();
|
|
219
|
+
if (window.name === POPUP_NAME) {
|
|
220
|
+
// We are the sign-in popup: the token is in localStorage, which
|
|
221
|
+
// the opener (same origin) sees through its `storage` event.
|
|
222
|
+
this.finishPopup();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
204
225
|
}
|
|
205
226
|
|
|
206
227
|
if (this.isSignedOut()) {
|
|
@@ -277,6 +298,11 @@ export class BrowserAuth {
|
|
|
277
298
|
// No fresh token + can't get one silently → redirect to grant.
|
|
278
299
|
// The page is about to unload; return a never-resolving Promise so
|
|
279
300
|
// the caller doesn't proceed with a half-issued request.
|
|
301
|
+
if (this.signInMode === "popup") {
|
|
302
|
+
await this.signInPopup();
|
|
303
|
+
if (this.tokenIsFresh()) return this.token as string;
|
|
304
|
+
throw new Error("RunApps: sign-in did not complete");
|
|
305
|
+
}
|
|
280
306
|
this.signIn();
|
|
281
307
|
return new Promise(() => {});
|
|
282
308
|
};
|
|
@@ -360,25 +386,84 @@ export class BrowserAuth {
|
|
|
360
386
|
this.clearPersisted();
|
|
361
387
|
}
|
|
362
388
|
|
|
363
|
-
/**
|
|
364
|
-
|
|
365
|
-
|
|
389
|
+
/** Start the grant flow: a redirect (default) or, in popup mode, a
|
|
390
|
+
* popup window — then the returned promise resolves once the token
|
|
391
|
+
* has arrived (and rejects if the popup is closed first). */
|
|
392
|
+
signIn(): Promise<void> | void {
|
|
393
|
+
if (typeof window === "undefined") return;
|
|
394
|
+
if (this.signInMode === "popup") return this.signInPopup();
|
|
395
|
+
if (this.signingIn) return;
|
|
366
396
|
// Explicit signIn clears the "signed out" flag so getToken() can
|
|
367
397
|
// proceed with the redirect-grant flow.
|
|
368
398
|
this.clearSignedOut();
|
|
369
399
|
this.signingIn = true;
|
|
400
|
+
location.href = this.grantUrlForThisPage();
|
|
401
|
+
}
|
|
402
|
+
private grantUrlForThisPage(): string {
|
|
370
403
|
const ret = location.href.split("#")[0] ?? location.href;
|
|
371
404
|
const scheme = window.matchMedia?.("(prefers-color-scheme: dark)")
|
|
372
405
|
.matches
|
|
373
406
|
? "dark"
|
|
374
407
|
: "light";
|
|
375
|
-
|
|
408
|
+
return this.buildGrantUrl({
|
|
376
409
|
pageOrigin: location.origin,
|
|
377
410
|
app: location.host,
|
|
378
411
|
redirectTo: ret,
|
|
379
412
|
scheme,
|
|
380
413
|
});
|
|
381
414
|
}
|
|
415
|
+
/** Popup grant flow. One popup at a time; concurrent callers share it. */
|
|
416
|
+
private signInPopup(): Promise<void> {
|
|
417
|
+
if (this.popupWait) return this.popupWait;
|
|
418
|
+
this.clearSignedOut();
|
|
419
|
+
const key = storageKey(this.project);
|
|
420
|
+
const win = window.open(this.grantUrlForThisPage(), POPUP_NAME, "popup=yes,width=520,height=700");
|
|
421
|
+
if (!win) {
|
|
422
|
+
// Popup blocked: the redirect is the only way left.
|
|
423
|
+
this.signingIn = true;
|
|
424
|
+
location.href = this.grantUrlForThisPage();
|
|
425
|
+
return new Promise(() => {});
|
|
426
|
+
}
|
|
427
|
+
this.popupWait = new Promise<void>((resolve, reject) => {
|
|
428
|
+
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
|
+
const adopt = (): boolean => {
|
|
436
|
+
const before = this.token;
|
|
437
|
+
this.loadPersisted();
|
|
438
|
+
if (!this.tokenIsFresh()) return false;
|
|
439
|
+
if (this.token !== before) this.setToken(this.token as string, this.expiresAt, this.userInfo ?? undefined);
|
|
440
|
+
else if (this.userInfo && !this.hideBadge) this.renderBadge();
|
|
441
|
+
try { win.close(); } catch { /* already gone */ }
|
|
442
|
+
done();
|
|
443
|
+
return true;
|
|
444
|
+
};
|
|
445
|
+
const onStorage = (e: StorageEvent) => {
|
|
446
|
+
if (e.key === key && e.newValue) adopt();
|
|
447
|
+
};
|
|
448
|
+
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);
|
|
453
|
+
});
|
|
454
|
+
return this.popupWait;
|
|
455
|
+
}
|
|
456
|
+
/** Inside the popup, after the token landed: close, or say so. */
|
|
457
|
+
private finishPopup() {
|
|
458
|
+
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);
|
|
466
|
+
}
|
|
382
467
|
|
|
383
468
|
/**
|
|
384
469
|
* Build the `/api/sdk/grant?…` URL the user is redirected to. Split
|
package/src/client.ts
CHANGED
|
@@ -60,6 +60,14 @@ export interface ClientOptions {
|
|
|
60
60
|
* badge in the corner.
|
|
61
61
|
*/
|
|
62
62
|
showIdentityBadge?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* How the browser auth flow obtains a token when there is none:
|
|
65
|
+
* `"redirect"` (default) navigates the page away and back, losing
|
|
66
|
+
* unsaved state; `"popup"` signs in through a popup window and
|
|
67
|
+
* resumes the pending call, so the page keeps its state. See
|
|
68
|
+
* `BrowserAuthOptions.signInMode`.
|
|
69
|
+
*/
|
|
70
|
+
signInMode?: "redirect" | "popup";
|
|
63
71
|
/**
|
|
64
72
|
* Which corner the identity badge floats in. Defaults to
|
|
65
73
|
* `"bottom-right"` — the canonical tray-app position. Switch to
|
|
@@ -213,6 +221,7 @@ export class RunApps {
|
|
|
213
221
|
events: this.events,
|
|
214
222
|
...(options.badgePosition !== undefined && { badgePosition: options.badgePosition }),
|
|
215
223
|
...(options.project !== undefined && { project: options.project }),
|
|
224
|
+
...(options.signInMode !== undefined && { signInMode: options.signInMode }),
|
|
216
225
|
});
|
|
217
226
|
(this as { auth: BrowserAuth | null }).auth = auth;
|
|
218
227
|
// The browser auth resolver wins over any caller-supplied one
|
|
@@ -278,8 +287,8 @@ export class RunApps {
|
|
|
278
287
|
|
|
279
288
|
/** Force a redirect to the runapps.ai grant page. No-op in static
|
|
280
289
|
* auth mode or in Node. */
|
|
281
|
-
signIn(): void {
|
|
282
|
-
this.auth?.signIn();
|
|
290
|
+
signIn(): Promise<void> | void {
|
|
291
|
+
return this.auth?.signIn();
|
|
283
292
|
}
|
|
284
293
|
|
|
285
294
|
/**
|