@biowiki/auth-sdk 0.1.0 → 0.3.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0
4
+
5
+ - Add `watchWebSession` for BFF-owned browser sessions, with visible-page polling,
6
+ focus checks, idempotent HTTP 401 handling and cleanup of listeners.
7
+ - Add `endWebSession` for same-origin BFF logout followed by a validated,
8
+ top-level Identity end-session navigation.
9
+ - Document Web single logout and its separation from native App sessions.
10
+ - Keep existing direct OAuth/PKCE client APIs compatible.
11
+ - Add tests for expired sessions, temporary outages, listener cleanup and trusted
12
+ logout redirects.
package/README.md CHANGED
@@ -5,19 +5,88 @@ It also maintains a stable per-application browser device ID in `localStorage` a
5
5
 
6
6
  ## Install
7
7
 
8
- The package is published only to the company private npm Registry. Configure the scope registry in the consuming project's `.npmrc`:
9
-
10
- ```ini
11
- @biowiki:registry=https://npm.company.example/repository/npm-private/
12
- ```
8
+ The package is published publicly to the npm registry at `https://registry.npmjs.org/`.
13
9
 
14
10
  Then install a pinned version:
15
11
 
16
12
  ```bash
17
- npm install @biowiki/auth-sdk@0.1.0
13
+ npm install @biowiki/auth-sdk@0.3.0
14
+ ```
15
+
16
+ ## Web BFF sessions (recommended for Tide / Collector / Dev)
17
+
18
+ Version 0.3.0 adds `watchWebSession` and `endWebSession` for applications whose
19
+ same-origin BFF owns the HttpOnly session. These helpers do not use browser
20
+ access-token storage or perform frontend authorization-code exchange. Existing
21
+ `BioWikiAuthClient` APIs remain available for older integrations.
22
+
23
+ ```ts
24
+ import { watchWebSession, endWebSession } from '@biowiki/auth-sdk';
25
+
26
+ const stopWatching = watchWebSession({
27
+ // Tide: '/auth/session'; Collector / Dev: '/proxy-api/auth/session'
28
+ sessionUrl: '/proxy-api/auth/session',
29
+ intervalMs: 15_000,
30
+ onSignedOut() {
31
+ clearCurrentUserAndPermissions();
32
+ showSignedOutPage(); // Idempotent; do not automatically start login again.
33
+ },
34
+ });
35
+
36
+ // Call from the application's logout button. Show a pending/disabled state.
37
+ async function logout() {
38
+ try {
39
+ await endWebSession('/proxy-api/auth/logout', 'https://api.biowiki.io', {
40
+ onSignedOut: clearCurrentUserAndPermissions,
41
+ });
42
+ } catch (error) {
43
+ clearCurrentUserAndPermissions();
44
+ showLogoutRetry(error); // Current BFF may have exited; unified logout may be incomplete.
45
+ }
46
+ }
47
+
48
+ // Dispose on application/component teardown.
49
+ // e.g. Vue: onUnmounted(stopWatching);
50
+ // or call stopWatching() from your teardown hook.
18
51
  ```
19
52
 
20
- ## Use
53
+ Replace the example UI callbacks with your application store/router functions.
54
+ Do not call `stopWatching()` immediately after initialization in real code; call
55
+ it when the application or owning component is destroyed.
56
+
57
+ The watcher queries only a same-origin session endpoint. It checks visible pages
58
+ every 15 seconds and on focus/visibility changes. HTTP 401 triggers signed-out
59
+ handling once until a later successful check. Network errors and 5xx responses
60
+ are retried instead of clearing valid login state. Also handle 401 from protected
61
+ business requests in the application's shared request interceptor.
62
+
63
+ `endWebSession` posts to the same-origin BFF and, when the response supplies
64
+ `X-SSO-End-Session`, performs a top-level navigation to the trusted Identity
65
+ origin's `/oauth/end-session`. The server uses a short-lived signed ticket to
66
+ clear the matching SSO cookie and return to the product with `logged_out=1`.
67
+ Do not replace that navigation with fetch or an iframe. On a `logged_out=1`
68
+ landing page, do not automatically restart authorization.
69
+
70
+ For Tide, retain its session-version consistency header and clear UI state before
71
+ navigation:
72
+
73
+ ```ts
74
+ await endWebSession('/auth/logout', 'https://api.biowiki.io', {
75
+ headers: { 'X-Tide-Session': sessionVersion },
76
+ onSignedOut: clearCurrentUserAndPermissions,
77
+ });
78
+ ```
79
+
80
+ `onSignedOut` runs after a successful BFF response and before Identity navigation.
81
+ For a failed response, handle the error and offer retry; the BFF may already have
82
+ cleared the local session. Both callbacks should be idempotent.
83
+
84
+ This requires the deployed Identity/BFF Web-sid logout implementation. Web
85
+ logout affects only the current browser's associated Web sessions; native App
86
+ sessions and other browser sessions remain independent. Use the BFF login URL
87
+ (`/auth/login` for Tide, `/proxy-api/auth/login` for Collector/Dev) for login.
88
+
89
+ ## Use (existing direct OAuth client)
21
90
 
22
91
  ```ts
23
92
  import { BioWikiAuthClient } from '@biowiki/auth-sdk';
@@ -42,18 +111,12 @@ In the callback route, call `completeLogin()` exactly once. For protected pages,
42
111
 
43
112
  ## Logout
44
113
 
45
- First revoke the application's current API session through its same-origin BFF,
46
- then call `logout()`. The SDK clears its local access token and navigates to
47
- the Identity Service logout endpoint to clear the SSO session before returning
48
- to the registered application origin.
49
-
50
- ```ts
51
- try {
52
- await fetch('/api/bff/auth/logout', { method: 'POST' });
53
- } finally {
54
- await auth.logout();
55
- }
56
- ```
114
+ Call `logout()` after the application has revoked its current API session. The
115
+ SDK clears its local access token and performs a top-level navigation to the
116
+ Identity Service logout endpoint. Identity then clears the SSO cookies and
117
+ redirects only to the registered client's origin. This front-channel flow works
118
+ for local development as well as production without relying on third-party
119
+ cookies.
57
120
 
58
121
  ## Registration
59
122
 
@@ -94,3 +157,57 @@ Set `NPM_REGISTRY` and `NPM_TOKEN` in CI secrets. Copy `.npmrc.example` to a use
94
157
  - Register the exact callback URL for each environment in Platform Console.
95
158
  - Configure Identity Service `CORS_ORIGIN` to include the application Origin.
96
159
  - The SDK does not create Tenants, Applications, OAuth Clients, or API credentials. Use `getSession()`, `isAuthenticated()`, and `requireAuth()` for browser session checks.
160
+
161
+ ## Popup login
162
+
163
+ Call from a user's click handler so browsers can open the window synchronously:
164
+
165
+ ```ts
166
+ const result = await auth.login({ mode: 'popup', returnTo: '/dashboard' });
167
+ if (result) {
168
+ // Update the application's session store or load the authenticated view.
169
+ window.location.assign(result.returnTo);
170
+ }
171
+ ```
172
+
173
+ Registration also supports `auth.beginRegistration({ mode: 'popup' })`.
174
+ A blocked popup automatically uses the existing full-page redirect flow, so the
175
+ callback route must support both modes:
176
+
177
+ ```ts
178
+ import { relayPopupCallback } from '@biowiki/auth-sdk';
179
+
180
+ if (!relayPopupCallback()) {
181
+ const { returnTo } = await auth.completeLogin();
182
+ window.location.replace(returnTo);
183
+ }
184
+ ```
185
+
186
+ Run the relay before session restoration, route access checks, or ordinary
187
+ callback code exchange. Only SDK-created popup transactions are relayed. The
188
+ callback sends code/state (or an OAuth error) to its own origin; the opener checks
189
+ origin, window identity, and state before redeeming the code with its own PKCE
190
+ verifier. Tokens and verifiers are never sent through `postMessage`. Each
191
+ transaction has its own storage key, and its callback is consumed once.
192
+
193
+ Requirements:
194
+
195
+ - `redirectUri` must have the same origin as the window initiating popup login.
196
+ Register that exact callback URI and the application's origin in OAuth config.
197
+ - Keep redirect login (`auth.login('/path')`) for URL-driven login routes, mobile
198
+ redirects, or providers that break opener communication.
199
+ - The application must use no COOP isolation or `same-origin-allow-popups`;
200
+ `Cross-Origin-Opener-Policy: same-origin` severs cross-origin opener access.
201
+ Identity emits `unsafe-none` on its OAuth/login/registration routes. Check
202
+ ingress/CDN headers and provider redirects too.
203
+ - Current browser refresh still uses Identity's HttpOnly `SameSite=Lax` cookie.
204
+ Unrelated application sites should use a BFF with a first-party session; this
205
+ popup change does not solve third-party cookie restrictions.
206
+ - Closing the popup rejects the promise; waiting more than two minutes also
207
+ rejects and closes it. Token exchange has a 15-second request timeout. Handle
208
+ rejection in the application's UI and allow retry.
209
+
210
+ Validate with `npm test` and `npm run typecheck`. Tests cover message validation,
211
+ PKCE exchange, replay, cancellation, timeout, blocked windows, registration,
212
+ OAuth errors, callback relay, and safe return paths. Live password/provider SSO
213
+ still needs staging acceptance against the deployed Identity and Gateway.
package/dist/index.d.ts CHANGED
@@ -1,9 +1,34 @@
1
+ /** BFF session synchronization. No access/refresh token is stored in the browser. */
2
+ declare function watchWebSession(options: {
3
+ sessionUrl: string;
4
+ onSignedOut: () => void;
5
+ intervalMs?: number;
6
+ }): () => void;
7
+ /** Completes browser SSO cookie cleanup after the BFF has revoked its web sid. */
8
+ declare function endWebSession(logoutUrl: string, identityOrigin: string, options?: {
9
+ headers?: HeadersInit;
10
+ onSignedOut?: () => void;
11
+ }): Promise<void>;
12
+
1
13
  interface AuthSdkConfig {
2
14
  authUrl: string;
3
15
  clientId: string;
4
16
  redirectUri: string;
5
17
  scope?: string;
6
18
  }
19
+ interface LoginOptions {
20
+ mode?: 'redirect' | 'popup';
21
+ returnTo?: string;
22
+ }
23
+ interface PopupCallbackMessage {
24
+ type: 'biowiki.oauth.callback';
25
+ code?: string;
26
+ state?: string;
27
+ error?: string;
28
+ errorDescription?: string;
29
+ }
30
+ /** Relays an OAuth callback from a popup to the window that opened it. */
31
+ declare function relayPopupCallback(url?: string): boolean;
7
32
  interface TokenSet {
8
33
  accessToken: string;
9
34
  expiresIn: number;
@@ -33,9 +58,17 @@ declare class BioWikiAuthClient {
33
58
  message: string;
34
59
  }>;
35
60
  register<TUser = unknown>(input: RegistrationInput): Promise<RegistrationResult<TUser>>;
36
- login(returnTo?: string): Promise<void>;
37
- beginRegistration(returnTo?: string): Promise<void>;
61
+ login(options?: LoginOptions | string): Promise<{
62
+ returnTo: string;
63
+ token: TokenSet;
64
+ } | void>;
65
+ beginRegistration(options?: LoginOptions | string): Promise<{
66
+ returnTo: string;
67
+ token: TokenSet;
68
+ } | void>;
69
+ private loginWithPopup;
38
70
  private startAuthorization;
71
+ private prepareAuthorization;
39
72
  completeLogin(url?: string): Promise<{
40
73
  returnTo: string;
41
74
  token: TokenSet;
@@ -48,4 +81,4 @@ declare class BioWikiAuthClient {
48
81
  private authRequest;
49
82
  }
50
83
 
51
- export { type AuthSdkConfig, BioWikiAuthClient, type RegistrationCodeInput, type RegistrationInput, type RegistrationResult, type TokenSet };
84
+ export { type AuthSdkConfig, BioWikiAuthClient, type LoginOptions, type PopupCallbackMessage, type RegistrationCodeInput, type RegistrationInput, type RegistrationResult, type TokenSet, endWebSession, relayPopupCallback, watchWebSession };
package/dist/index.js CHANGED
@@ -1,4 +1,73 @@
1
+ // src/web-session.ts
2
+ function watchWebSession(options) {
3
+ let authenticated;
4
+ let pending = false;
5
+ let stopped = false;
6
+ const sessionUrl = new URL(options.sessionUrl, window.location.origin);
7
+ if (sessionUrl.origin !== window.location.origin) throw new Error("Session endpoint must be same-origin");
8
+ const check = async () => {
9
+ if (pending || stopped || document.visibilityState === "hidden") return;
10
+ pending = true;
11
+ try {
12
+ const response = await fetch(sessionUrl, { credentials: "same-origin", cache: "no-store" });
13
+ if (stopped) return;
14
+ if (response.ok) authenticated = true;
15
+ else if (response.status === 401 && authenticated !== false) {
16
+ authenticated = false;
17
+ options.onSignedOut();
18
+ }
19
+ } catch {
20
+ } finally {
21
+ pending = false;
22
+ }
23
+ };
24
+ const wake = () => {
25
+ void check();
26
+ };
27
+ const timer = window.setInterval(wake, options.intervalMs ?? 15e3);
28
+ window.addEventListener("focus", wake);
29
+ document.addEventListener("visibilitychange", wake);
30
+ wake();
31
+ return () => {
32
+ stopped = true;
33
+ clearInterval(timer);
34
+ window.removeEventListener("focus", wake);
35
+ document.removeEventListener("visibilitychange", wake);
36
+ };
37
+ }
38
+ async function endWebSession(logoutUrl, identityOrigin, options = {}) {
39
+ const endpoint = new URL(logoutUrl, window.location.origin);
40
+ if (endpoint.origin !== window.location.origin) throw new Error("Logout endpoint must be same-origin");
41
+ const response = await fetch(endpoint, { method: "POST", credentials: "same-origin", cache: "no-store", headers: options.headers });
42
+ if (!response.ok) throw new Error("Unified logout is incomplete. Please retry.");
43
+ options.onSignedOut?.();
44
+ const next = response.headers.get("X-SSO-End-Session");
45
+ if (!next) return;
46
+ const url = new URL(next);
47
+ if (url.origin !== new URL(identityOrigin).origin || url.pathname !== "/oauth/end-session") {
48
+ throw new Error("Unexpected logout redirect");
49
+ }
50
+ window.location.assign(url.toString());
51
+ }
52
+
1
53
  // src/index.ts
54
+ function relayPopupCallback(url = window.location.href) {
55
+ if (!window.opener || window.opener === window) return false;
56
+ const callback = new URL(url);
57
+ const state = callback.searchParams.get("state");
58
+ const marker = `biowiki.auth-sdk:popup:${state}`;
59
+ if (!state || sessionStorage.getItem(marker) !== "1") return false;
60
+ sessionStorage.removeItem(marker);
61
+ const payload = {
62
+ type: "biowiki.oauth.callback",
63
+ code: callback.searchParams.get("code") ?? void 0,
64
+ state: callback.searchParams.get("state") ?? void 0,
65
+ error: callback.searchParams.get("error") ?? void 0,
66
+ errorDescription: callback.searchParams.get("error_description") ?? void 0
67
+ };
68
+ window.opener.postMessage(payload, window.location.origin);
69
+ return true;
70
+ }
2
71
  var storagePrefix = "biowiki.auth-sdk";
3
72
  function base64Url(bytes) {
4
73
  return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
@@ -180,18 +249,92 @@ var BioWikiAuthClient = class {
180
249
  verification_code: input.verificationCode
181
250
  });
182
251
  }
183
- async login(returnTo = "/") {
184
- await this.startAuthorization(returnTo);
252
+ async login(options = "/") {
253
+ const normalized = typeof options === "string" ? { returnTo: options, mode: "redirect" } : options;
254
+ if (normalized.mode === "popup") return this.loginWithPopup(normalized.returnTo ?? "/");
255
+ await this.startAuthorization(normalized.returnTo ?? "/");
256
+ }
257
+ async beginRegistration(options = "/") {
258
+ const normalized = typeof options === "string" ? { returnTo: options, mode: "redirect" } : options;
259
+ if (normalized.mode === "popup") return this.loginWithPopup(normalized.returnTo ?? "/", "signup");
260
+ await this.startAuthorization(normalized.returnTo ?? "/", "signup");
185
261
  }
186
- async beginRegistration(returnTo = "/") {
187
- await this.startAuthorization(returnTo, "signup");
262
+ async loginWithPopup(returnTo, screenHint) {
263
+ if (new URL(this.config.redirectUri).origin !== window.location.origin) {
264
+ throw new Error("Popup redirect URI must use the current application origin.");
265
+ }
266
+ const popup = window.open("about:blank", `biowiki-login-${randomValue(12)}`, "popup,width=520,height=720,resizable=yes,scrollbars=yes");
267
+ let authorizationUrl;
268
+ try {
269
+ authorizationUrl = await this.prepareAuthorization(returnTo, screenHint);
270
+ } catch (error) {
271
+ popup?.close();
272
+ throw error;
273
+ }
274
+ if (!popup) {
275
+ window.location.assign(authorizationUrl);
276
+ return;
277
+ }
278
+ const expectedState = new URL(authorizationUrl).searchParams.get("state");
279
+ return new Promise((resolve, reject) => {
280
+ const expectedOrigin = new URL(this.config.redirectUri).origin;
281
+ let timer;
282
+ let timeout;
283
+ const cleanup = () => {
284
+ window.removeEventListener("message", onMessage);
285
+ if (timer) clearInterval(timer);
286
+ if (timeout) clearTimeout(timeout);
287
+ };
288
+ const fail = (error) => {
289
+ cleanup();
290
+ sessionStorage.removeItem(this.key(`transaction:${expectedState}`));
291
+ if (!popup.closed) popup.close();
292
+ reject(error);
293
+ };
294
+ const onMessage = async (event) => {
295
+ if (event.origin !== expectedOrigin || event.source !== popup || event.data?.type !== "biowiki.oauth.callback") return;
296
+ if (event.data.state !== expectedState) return;
297
+ cleanup();
298
+ if (!popup.closed) popup.close();
299
+ if (event.data.error) {
300
+ if (!popup.closed) popup.close();
301
+ sessionStorage.removeItem(this.key(`transaction:${expectedState}`));
302
+ reject(new Error(event.data.errorDescription || event.data.error));
303
+ return;
304
+ }
305
+ try {
306
+ const callback = new URL(this.config.redirectUri);
307
+ if (!event.data.code || !event.data.state) throw new Error("Invalid OAuth popup callback.");
308
+ callback.searchParams.set("code", event.data.code);
309
+ callback.searchParams.set("state", event.data.state);
310
+ const result = await this.completeLogin(callback.toString());
311
+ if (!popup.closed) popup.close();
312
+ resolve(result);
313
+ } catch (error) {
314
+ if (!popup.closed) popup.close();
315
+ reject(error instanceof Error ? error : new Error("OAuth popup login failed."));
316
+ }
317
+ };
318
+ window.addEventListener("message", onMessage);
319
+ timer = setInterval(() => {
320
+ if (popup.closed) fail(new Error("Login popup was closed."));
321
+ }, 500);
322
+ timeout = setTimeout(() => fail(new Error("Login popup timed out.")), 12e4);
323
+ try {
324
+ popup.sessionStorage.setItem(`biowiki.auth-sdk:popup:${expectedState}`, "1");
325
+ popup.location.assign(authorizationUrl);
326
+ } catch (error) {
327
+ fail(error instanceof Error ? error : new Error("Unable to open login popup."));
328
+ }
329
+ });
188
330
  }
189
331
  async startAuthorization(returnTo, screenHint) {
332
+ window.location.assign(await this.prepareAuthorization(returnTo, screenHint));
333
+ }
334
+ async prepareAuthorization(returnTo, screenHint) {
190
335
  const state = randomValue();
191
336
  const verifier = randomValue(48);
192
- sessionStorage.setItem(this.key("state"), state);
193
- sessionStorage.setItem(this.key("verifier"), verifier);
194
- sessionStorage.setItem(this.key("return-to"), returnTo);
337
+ sessionStorage.setItem(this.key(`transaction:${state}`), JSON.stringify({ verifier, returnTo, createdAt: Date.now() }));
195
338
  const authorize = new URL("/oauth/authorize", this.config.authUrl);
196
339
  authorize.search = new URLSearchParams({
197
340
  response_type: "code",
@@ -203,18 +346,21 @@ var BioWikiAuthClient = class {
203
346
  code_challenge_method: "S256"
204
347
  }).toString();
205
348
  if (screenHint) authorize.searchParams.set("screen_hint", screenHint);
206
- window.location.assign(authorize.toString());
349
+ return authorize.toString();
207
350
  }
208
351
  async completeLogin(url = window.location.href) {
209
352
  const callback = new URL(url);
210
353
  const code = callback.searchParams.get("code");
211
354
  const state = callback.searchParams.get("state");
212
- const expectedState = sessionStorage.getItem(this.key("state"));
213
- const verifier = sessionStorage.getItem(this.key("verifier"));
214
- if (!code || !state || state !== expectedState || !verifier) {
355
+ const transactionKey = this.key(`transaction:${state}`);
356
+ const rawTransaction = sessionStorage.getItem(transactionKey);
357
+ const transaction = rawTransaction ? JSON.parse(rawTransaction) : null;
358
+ if (!code || !state || !transaction?.verifier || Date.now() - transaction.createdAt > 10 * 60 * 1e3) {
215
359
  throw new Error("Invalid OAuth callback state.");
216
360
  }
361
+ sessionStorage.removeItem(transactionKey);
217
362
  const response = await fetch(new URL("/oauth/token", this.config.authUrl), {
363
+ signal: AbortSignal.timeout(15e3),
218
364
  method: "POST",
219
365
  credentials: "include",
220
366
  headers: {
@@ -226,17 +372,18 @@ var BioWikiAuthClient = class {
226
372
  code,
227
373
  client_id: this.config.clientId,
228
374
  redirect_uri: this.config.redirectUri,
229
- code_verifier: verifier
375
+ code_verifier: transaction.verifier
230
376
  })
231
377
  });
232
378
  if (!response.ok) throw new Error("OAuth authorization code exchange failed.");
233
379
  const payload = await response.json();
380
+ if (typeof payload.access_token !== "string" || !payload.access_token) {
381
+ throw new Error("OAuth response did not contain an access token.");
382
+ }
234
383
  const token = { accessToken: payload.access_token, expiresIn: payload.expires_in };
235
384
  sessionStorage.setItem(this.key("access-token"), token.accessToken);
236
- const returnTo = sessionStorage.getItem(this.key("return-to")) ?? "/";
237
- sessionStorage.removeItem(this.key("state"));
238
- sessionStorage.removeItem(this.key("verifier"));
239
- sessionStorage.removeItem(this.key("return-to"));
385
+ const candidate = transaction.returnTo;
386
+ const returnTo = typeof candidate === "string" && candidate.startsWith("/") && !candidate.startsWith("//") && !candidate.includes("\\") ? candidate : "/";
240
387
  return { returnTo, token };
241
388
  }
242
389
  async refresh() {
@@ -251,6 +398,9 @@ var BioWikiAuthClient = class {
251
398
  });
252
399
  if (!response.ok) throw new Error("OAuth refresh failed.");
253
400
  const payload = await response.json();
401
+ if (typeof payload.access_token !== "string" || !payload.access_token) {
402
+ throw new Error("OAuth response did not contain an access token.");
403
+ }
254
404
  const token = { accessToken: payload.access_token, expiresIn: payload.expires_in };
255
405
  sessionStorage.setItem(this.key("access-token"), token.accessToken);
256
406
  return token;
@@ -300,6 +450,9 @@ var BioWikiAuthClient = class {
300
450
  }
301
451
  };
302
452
  export {
303
- BioWikiAuthClient
453
+ BioWikiAuthClient,
454
+ endWebSession,
455
+ relayPopupCallback,
456
+ watchWebSession
304
457
  };
305
458
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export interface AuthSdkConfig {\n authUrl: string;\n clientId: string;\n redirectUri: string;\n scope?: string;\n}\n\nexport interface TokenSet {\n accessToken: string;\n expiresIn: number;\n}\n\nexport interface RegistrationInput {\n email: string;\n password: string;\n verificationCode: string;\n}\n\nexport interface RegistrationResult<TUser = unknown> {\n user: TUser;\n registered: true;\n}\n\nexport interface RegistrationCodeInput {\n email: string;\n botToken?: string;\n}\n\nconst storagePrefix = 'biowiki.auth-sdk';\n\nfunction base64Url(bytes: Uint8Array) {\n return btoa(String.fromCharCode(...bytes))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replaceAll('=', '');\n}\n\nfunction randomValue(bytes = 32) {\n const value = new Uint8Array(bytes);\n crypto.getRandomValues(value);\n return base64Url(value);\n}\n\nasync function challenge(verifier: string) {\n const input = new TextEncoder().encode(verifier);\n if (crypto.subtle) {\n const digest = await crypto.subtle.digest('SHA-256', input);\n return base64Url(new Uint8Array(digest));\n }\n // Browsers do not expose SubtleCrypto for HTTP pages served from a LAN IP.\n return base64Url(sha256(input));\n}\n\nfunction sha256(input: Uint8Array) {\n const bitLength = input.length * 8;\n const paddedLength = Math.ceil((input.length + 9) / 64) * 64;\n const bytes = new Uint8Array(paddedLength);\n bytes.set(input);\n bytes[input.length] = 0x80;\n\n let length = bitLength;\n for (let index = 0; index < 8; index += 1) {\n bytes[paddedLength - 1 - index] = length & 0xff;\n length = Math.floor(length / 256);\n }\n\n const words = new Uint32Array(64);\n const hash = new Uint32Array([\n 0x6a09e667,\n 0xbb67ae85,\n 0x3c6ef372,\n 0xa54ff53a,\n 0x510e527f,\n 0x9b05688c,\n 0x1f83d9ab,\n 0x5be0cd19\n ]);\n\n for (let offset = 0; offset < bytes.length; offset += 64) {\n for (let index = 0; index < 16; index += 1) {\n const position = offset + index * 4;\n words[index] =\n (bytes[position] << 24) |\n (bytes[position + 1] << 16) |\n (bytes[position + 2] << 8) |\n bytes[position + 3];\n }\n for (let index = 16; index < 64; index += 1) {\n const a = words[index - 15];\n const b = words[index - 2];\n const sigma0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);\n const sigma1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);\n words[index] = (words[index - 16] + sigma0 + words[index - 7] + sigma1) >>> 0;\n }\n\n let [a, b, c, d, e, f, g, h] = hash;\n for (let index = 0; index < 64; index += 1) {\n const sigma1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));\n const choice = (e & f) ^ (~e & g);\n const temp1 = (h + sigma1 + choice + SHA256_CONSTANTS[index] + words[index]) >>> 0;\n const sigma0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));\n const majority = (a & b) ^ (a & c) ^ (b & c);\n const temp2 = (sigma0 + majority) >>> 0;\n [h, g, f, e, d, c, b, a] = [g, f, e, (d + temp1) >>> 0, c, b, a, (temp1 + temp2) >>> 0];\n }\n\n hash[0] = (hash[0] + a) >>> 0;\n hash[1] = (hash[1] + b) >>> 0;\n hash[2] = (hash[2] + c) >>> 0;\n hash[3] = (hash[3] + d) >>> 0;\n hash[4] = (hash[4] + e) >>> 0;\n hash[5] = (hash[5] + f) >>> 0;\n hash[6] = (hash[6] + g) >>> 0;\n hash[7] = (hash[7] + h) >>> 0;\n }\n\n const digest = new Uint8Array(32);\n for (let index = 0; index < hash.length; index += 1) {\n digest[index * 4] = hash[index] >>> 24;\n digest[index * 4 + 1] = hash[index] >>> 16;\n digest[index * 4 + 2] = hash[index] >>> 8;\n digest[index * 4 + 3] = hash[index];\n }\n return digest;\n}\n\nconst SHA256_CONSTANTS = new Uint32Array([\n 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,\n 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,\n 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,\n 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,\n 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,\n 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,\n 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,\n 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2\n]);\n\nexport class BioWikiAuthClient {\n constructor(private readonly config: AuthSdkConfig) {}\n\n private key(name: string) {\n return `${storagePrefix}:${this.config.clientId}:${name}`;\n }\n\n getToken() {\n return sessionStorage.getItem(this.key('access-token')) ?? '';\n }\n\n getDeviceId() {\n const key = this.key('device-id');\n const existing = localStorage.getItem(key);\n if (existing) return existing;\n const deviceId = `web-${randomValue(24)}`;\n localStorage.setItem(key, deviceId);\n return deviceId;\n }\n\n isAuthenticated() {\n return Boolean(this.getToken());\n }\n\n async requestRegistrationCode({ email, botToken }: RegistrationCodeInput) {\n return this.authRequest('/api/v1/auth/email/send-code', {\n email,\n purpose: 'register',\n ...(botToken ? { bot_token: botToken } : {})\n });\n }\n\n async register<TUser = unknown>(input: RegistrationInput): Promise<RegistrationResult<TUser>> {\n return this.authRequest<RegistrationResult<TUser>>('/api/v1/auth/register', {\n email: input.email,\n password: input.password,\n verification_code: input.verificationCode\n });\n }\n\n async login(returnTo = '/') {\n await this.startAuthorization(returnTo);\n }\n\n async beginRegistration(returnTo = '/') {\n await this.startAuthorization(returnTo, 'signup');\n }\n\n private async startAuthorization(returnTo: string, screenHint?: 'signup') {\n const state = randomValue();\n const verifier = randomValue(48);\n sessionStorage.setItem(this.key('state'), state);\n sessionStorage.setItem(this.key('verifier'), verifier);\n sessionStorage.setItem(this.key('return-to'), returnTo);\n const authorize = new URL('/oauth/authorize', this.config.authUrl);\n authorize.search = new URLSearchParams({\n response_type: 'code',\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n scope: this.config.scope ?? 'openid profile email',\n state,\n code_challenge: await challenge(verifier),\n code_challenge_method: 'S256'\n }).toString();\n if (screenHint) authorize.searchParams.set('screen_hint', screenHint);\n window.location.assign(authorize.toString());\n }\n\n async completeLogin(url = window.location.href): Promise<{ returnTo: string; token: TokenSet }> {\n const callback = new URL(url);\n const code = callback.searchParams.get('code');\n const state = callback.searchParams.get('state');\n const expectedState = sessionStorage.getItem(this.key('state'));\n const verifier = sessionStorage.getItem(this.key('verifier'));\n if (!code || !state || state !== expectedState || !verifier) {\n throw new Error('Invalid OAuth callback state.');\n }\n const response = await fetch(new URL('/oauth/token', this.config.authUrl), {\n method: 'POST',\n credentials: 'include',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n 'x-device-id': this.getDeviceId()\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n code_verifier: verifier\n })\n });\n if (!response.ok) throw new Error('OAuth authorization code exchange failed.');\n const payload = await response.json();\n const token = { accessToken: payload.access_token, expiresIn: payload.expires_in };\n sessionStorage.setItem(this.key('access-token'), token.accessToken);\n const returnTo = sessionStorage.getItem(this.key('return-to')) ?? '/';\n sessionStorage.removeItem(this.key('state'));\n sessionStorage.removeItem(this.key('verifier'));\n sessionStorage.removeItem(this.key('return-to'));\n return { returnTo, token };\n }\n\n async refresh(): Promise<TokenSet> {\n const response = await fetch(new URL('/oauth/token', this.config.authUrl), {\n method: 'POST',\n credentials: 'include',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n 'x-device-id': this.getDeviceId()\n },\n body: new URLSearchParams({ grant_type: 'refresh_token', client_id: this.config.clientId })\n });\n if (!response.ok) throw new Error('OAuth refresh failed.');\n const payload = await response.json();\n const token = { accessToken: payload.access_token, expiresIn: payload.expires_in };\n sessionStorage.setItem(this.key('access-token'), token.accessToken);\n return token;\n }\n\n async getUser<T>() {\n const token = this.getToken() || (await this.refresh()).accessToken;\n const response = await fetch(new URL('/oauth/userinfo', this.config.authUrl), {\n headers: { authorization: `Bearer ${token}` },\n credentials: 'include'\n });\n if (!response.ok) throw new Error('OAuth userinfo request failed.');\n return response.json() as Promise<T>;\n }\n\n async getSession<TUser = unknown>(): Promise<TUser | null> {\n try {\n return await this.getUser<TUser>();\n } catch {\n return null;\n }\n }\n\n async requireAuth(returnTo = window.location.pathname + window.location.search) {\n if (this.isAuthenticated()) return true;\n await this.login(returnTo);\n return false;\n }\n\n async logout(returnTo = `${window.location.origin}/`) {\n const logout = new URL('/oauth/logout', this.config.authUrl);\n logout.search = new URLSearchParams({\n client_id: this.config.clientId,\n post_logout_redirect_uri: returnTo\n }).toString();\n sessionStorage.removeItem(this.key('access-token'));\n window.location.assign(logout.toString());\n }\n\n private async authRequest<T = { success: true; message: string }>(path: string, body: Record<string, string>) {\n const response = await fetch(new URL(path, this.config.authUrl), {\n method: 'POST',\n credentials: 'include',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body)\n });\n const payload = await response.json().catch(() => null);\n if (!response.ok) {\n throw new Error(payload?.error?.message ?? 'Identity registration request failed.');\n }\n return (payload?.data ?? payload) as T;\n }\n}\n"],"mappings":"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","names":["a","b"]}
1
+ {"version":3,"sources":["../src/web-session.ts","../src/index.ts"],"sourcesContent":["/** BFF session synchronization. No access/refresh token is stored in the browser. */\nexport function watchWebSession(options: {\n sessionUrl: string;\n onSignedOut: () => void;\n intervalMs?: number;\n}) {\n let authenticated: boolean | undefined;\n let pending = false;\n let stopped = false;\n const sessionUrl = new URL(options.sessionUrl, window.location.origin);\n if (sessionUrl.origin !== window.location.origin) throw new Error('Session endpoint must be same-origin');\n const check = async () => {\n if (pending || stopped || document.visibilityState === 'hidden') return;\n pending = true;\n try {\n const response = await fetch(sessionUrl, { credentials: 'same-origin', cache: 'no-store' });\n if (stopped) return;\n if (response.ok) authenticated = true;\n else if (response.status === 401 && authenticated !== false) {\n authenticated = false;\n options.onSignedOut();\n }\n // Temporary network/service failures must not be treated as logout.\n } catch { /* Retry on next focus or interval. */ }\n finally { pending = false; }\n };\n const wake = () => { void check(); };\n const timer = window.setInterval(wake, options.intervalMs ?? 15000);\n window.addEventListener('focus', wake);\n document.addEventListener('visibilitychange', wake);\n wake();\n return () => {\n stopped = true;\n clearInterval(timer);\n window.removeEventListener('focus', wake);\n document.removeEventListener('visibilitychange', wake);\n };\n}\n\n/** Completes browser SSO cookie cleanup after the BFF has revoked its web sid. */\nexport async function endWebSession(\n logoutUrl: string,\n identityOrigin: string,\n options: { headers?: HeadersInit; onSignedOut?: () => void } = {},\n) {\n const endpoint = new URL(logoutUrl, window.location.origin);\n if (endpoint.origin !== window.location.origin) throw new Error('Logout endpoint must be same-origin');\n const response = await fetch(endpoint, { method: 'POST', credentials: 'same-origin', cache: 'no-store', headers: options.headers });\n if (!response.ok) throw new Error('Unified logout is incomplete. Please retry.');\n options.onSignedOut?.();\n const next = response.headers.get('X-SSO-End-Session');\n if (!next) return;\n const url = new URL(next);\n if (url.origin !== new URL(identityOrigin).origin || url.pathname !== '/oauth/end-session') {\n throw new Error('Unexpected logout redirect');\n }\n window.location.assign(url.toString());\n}\n","export interface AuthSdkConfig {\n authUrl: string;\n clientId: string;\n redirectUri: string;\n scope?: string;\n}\n\nexport interface LoginOptions {\n mode?: 'redirect' | 'popup';\n returnTo?: string;\n}\n\nexport interface PopupCallbackMessage {\n type: 'biowiki.oauth.callback';\n code?: string;\n state?: string;\n error?: string;\n errorDescription?: string;\n}\n\n/** Relays an OAuth callback from a popup to the window that opened it. */\nexport function relayPopupCallback(url = window.location.href) {\n if (!window.opener || window.opener === window) return false;\n const callback = new URL(url);\n const state = callback.searchParams.get('state');\n const marker = `biowiki.auth-sdk:popup:${state}`;\n if (!state || sessionStorage.getItem(marker) !== '1') return false;\n sessionStorage.removeItem(marker);\n const payload: PopupCallbackMessage = {\n type: 'biowiki.oauth.callback',\n code: callback.searchParams.get('code') ?? undefined,\n state: callback.searchParams.get('state') ?? undefined,\n error: callback.searchParams.get('error') ?? undefined,\n errorDescription: callback.searchParams.get('error_description') ?? undefined\n };\n window.opener.postMessage(payload, window.location.origin);\n // The opener closes this window after receiving the message, avoiding a\n // race between delivery and its popup-closed polling timer.\n return true;\n}\n\nexport interface TokenSet {\n accessToken: string;\n expiresIn: number;\n}\n\nexport interface RegistrationInput {\n email: string;\n password: string;\n verificationCode: string;\n}\n\nexport interface RegistrationResult<TUser = unknown> {\n user: TUser;\n registered: true;\n}\n\nexport interface RegistrationCodeInput {\n email: string;\n botToken?: string;\n}\n\nconst storagePrefix = 'biowiki.auth-sdk';\n\nfunction base64Url(bytes: Uint8Array) {\n return btoa(String.fromCharCode(...bytes))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replaceAll('=', '');\n}\n\nfunction randomValue(bytes = 32) {\n const value = new Uint8Array(bytes);\n crypto.getRandomValues(value);\n return base64Url(value);\n}\n\nasync function challenge(verifier: string) {\n const input = new TextEncoder().encode(verifier);\n if (crypto.subtle) {\n const digest = await crypto.subtle.digest('SHA-256', input);\n return base64Url(new Uint8Array(digest));\n }\n // Browsers do not expose SubtleCrypto for HTTP pages served from a LAN IP.\n return base64Url(sha256(input));\n}\n\nfunction sha256(input: Uint8Array) {\n const bitLength = input.length * 8;\n const paddedLength = Math.ceil((input.length + 9) / 64) * 64;\n const bytes = new Uint8Array(paddedLength);\n bytes.set(input);\n bytes[input.length] = 0x80;\n\n let length = bitLength;\n for (let index = 0; index < 8; index += 1) {\n bytes[paddedLength - 1 - index] = length & 0xff;\n length = Math.floor(length / 256);\n }\n\n const words = new Uint32Array(64);\n const hash = new Uint32Array([\n 0x6a09e667,\n 0xbb67ae85,\n 0x3c6ef372,\n 0xa54ff53a,\n 0x510e527f,\n 0x9b05688c,\n 0x1f83d9ab,\n 0x5be0cd19\n ]);\n\n for (let offset = 0; offset < bytes.length; offset += 64) {\n for (let index = 0; index < 16; index += 1) {\n const position = offset + index * 4;\n words[index] =\n (bytes[position] << 24) |\n (bytes[position + 1] << 16) |\n (bytes[position + 2] << 8) |\n bytes[position + 3];\n }\n for (let index = 16; index < 64; index += 1) {\n const a = words[index - 15];\n const b = words[index - 2];\n const sigma0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);\n const sigma1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);\n words[index] = (words[index - 16] + sigma0 + words[index - 7] + sigma1) >>> 0;\n }\n\n let [a, b, c, d, e, f, g, h] = hash;\n for (let index = 0; index < 64; index += 1) {\n const sigma1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));\n const choice = (e & f) ^ (~e & g);\n const temp1 = (h + sigma1 + choice + SHA256_CONSTANTS[index] + words[index]) >>> 0;\n const sigma0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));\n const majority = (a & b) ^ (a & c) ^ (b & c);\n const temp2 = (sigma0 + majority) >>> 0;\n [h, g, f, e, d, c, b, a] = [g, f, e, (d + temp1) >>> 0, c, b, a, (temp1 + temp2) >>> 0];\n }\n\n hash[0] = (hash[0] + a) >>> 0;\n hash[1] = (hash[1] + b) >>> 0;\n hash[2] = (hash[2] + c) >>> 0;\n hash[3] = (hash[3] + d) >>> 0;\n hash[4] = (hash[4] + e) >>> 0;\n hash[5] = (hash[5] + f) >>> 0;\n hash[6] = (hash[6] + g) >>> 0;\n hash[7] = (hash[7] + h) >>> 0;\n }\n\n const digest = new Uint8Array(32);\n for (let index = 0; index < hash.length; index += 1) {\n digest[index * 4] = hash[index] >>> 24;\n digest[index * 4 + 1] = hash[index] >>> 16;\n digest[index * 4 + 2] = hash[index] >>> 8;\n digest[index * 4 + 3] = hash[index];\n }\n return digest;\n}\n\nconst SHA256_CONSTANTS = new Uint32Array([\n 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,\n 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,\n 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,\n 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,\n 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,\n 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,\n 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,\n 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2\n]);\n\nexport class BioWikiAuthClient {\n constructor(private readonly config: AuthSdkConfig) {}\n\n private key(name: string) {\n return `${storagePrefix}:${this.config.clientId}:${name}`;\n }\n\n getToken() {\n return sessionStorage.getItem(this.key('access-token')) ?? '';\n }\n\n getDeviceId() {\n const key = this.key('device-id');\n const existing = localStorage.getItem(key);\n if (existing) return existing;\n const deviceId = `web-${randomValue(24)}`;\n localStorage.setItem(key, deviceId);\n return deviceId;\n }\n\n isAuthenticated() {\n return Boolean(this.getToken());\n }\n\n async requestRegistrationCode({ email, botToken }: RegistrationCodeInput) {\n return this.authRequest('/api/v1/auth/email/send-code', {\n email,\n purpose: 'register',\n ...(botToken ? { bot_token: botToken } : {})\n });\n }\n\n async register<TUser = unknown>(input: RegistrationInput): Promise<RegistrationResult<TUser>> {\n return this.authRequest<RegistrationResult<TUser>>('/api/v1/auth/register', {\n email: input.email,\n password: input.password,\n verification_code: input.verificationCode\n });\n }\n\n async login(options: LoginOptions | string = '/'): Promise<{ returnTo: string; token: TokenSet } | void> {\n const normalized = typeof options === 'string' ? { returnTo: options, mode: 'redirect' as const } : options;\n if (normalized.mode === 'popup') return this.loginWithPopup(normalized.returnTo ?? '/');\n await this.startAuthorization(normalized.returnTo ?? '/');\n }\n\n async beginRegistration(options: LoginOptions | string = '/'): Promise<{ returnTo: string; token: TokenSet } | void> {\n const normalized = typeof options === 'string' ? { returnTo: options, mode: 'redirect' as const } : options;\n if (normalized.mode === 'popup') return this.loginWithPopup(normalized.returnTo ?? '/', 'signup');\n await this.startAuthorization(normalized.returnTo ?? '/', 'signup');\n }\n\n private async loginWithPopup(returnTo: string, screenHint?: 'signup') {\n if (new URL(this.config.redirectUri).origin !== window.location.origin) {\n throw new Error('Popup redirect URI must use the current application origin.');\n }\n const popup = window.open('about:blank', `biowiki-login-${randomValue(12)}`, 'popup,width=520,height=720,resizable=yes,scrollbars=yes');\n let authorizationUrl: string;\n try {\n authorizationUrl = await this.prepareAuthorization(returnTo, screenHint);\n } catch (error) {\n popup?.close();\n throw error;\n }\n if (!popup) {\n window.location.assign(authorizationUrl);\n return;\n }\n const expectedState = new URL(authorizationUrl).searchParams.get('state')!;\n return new Promise<{ returnTo: string; token: TokenSet }>((resolve, reject) => {\n const expectedOrigin = new URL(this.config.redirectUri).origin;\n let timer: ReturnType<typeof setInterval> | undefined;\n let timeout: ReturnType<typeof setTimeout> | undefined;\n const cleanup = () => {\n window.removeEventListener('message', onMessage);\n if (timer) clearInterval(timer);\n if (timeout) clearTimeout(timeout);\n };\n const fail = (error: Error) => {\n cleanup();\n sessionStorage.removeItem(this.key(`transaction:${expectedState}`));\n if (!popup.closed) popup.close();\n reject(error);\n };\n const onMessage = async (event: MessageEvent<PopupCallbackMessage>) => {\n if (event.origin !== expectedOrigin || event.source !== popup || event.data?.type !== 'biowiki.oauth.callback') return;\n if (event.data.state !== expectedState) return;\n cleanup();\n if (!popup.closed) popup.close();\n if (event.data.error) {\n if (!popup.closed) popup.close();\n sessionStorage.removeItem(this.key(`transaction:${expectedState}`));\n reject(new Error(event.data.errorDescription || event.data.error));\n return;\n }\n try {\n const callback = new URL(this.config.redirectUri);\n if (!event.data.code || !event.data.state) throw new Error('Invalid OAuth popup callback.');\n callback.searchParams.set('code', event.data.code);\n callback.searchParams.set('state', event.data.state);\n const result = await this.completeLogin(callback.toString());\n if (!popup.closed) popup.close();\n resolve(result);\n } catch (error) {\n if (!popup.closed) popup.close();\n reject(error instanceof Error ? error : new Error('OAuth popup login failed.'));\n }\n };\n window.addEventListener('message', onMessage);\n timer = setInterval(() => {\n if (popup.closed) fail(new Error('Login popup was closed.'));\n }, 500);\n timeout = setTimeout(() => fail(new Error('Login popup timed out.')), 120_000);\n try {\n // about:blank inherits the opener origin. This marker remains local to\n // the popup's tab storage when it returns to the application callback.\n popup.sessionStorage.setItem(`biowiki.auth-sdk:popup:${expectedState}`, '1');\n popup.location.assign(authorizationUrl);\n } catch (error) {\n fail(error instanceof Error ? error : new Error('Unable to open login popup.'));\n }\n });\n }\n\n private async startAuthorization(returnTo: string, screenHint?: 'signup') {\n window.location.assign(await this.prepareAuthorization(returnTo, screenHint));\n }\n\n private async prepareAuthorization(returnTo: string, screenHint?: 'signup') {\n const state = randomValue();\n const verifier = randomValue(48);\n sessionStorage.setItem(this.key(`transaction:${state}`), JSON.stringify({ verifier, returnTo, createdAt: Date.now() }));\n const authorize = new URL('/oauth/authorize', this.config.authUrl);\n authorize.search = new URLSearchParams({\n response_type: 'code',\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n scope: this.config.scope ?? 'openid profile email',\n state,\n code_challenge: await challenge(verifier),\n code_challenge_method: 'S256'\n }).toString();\n if (screenHint) authorize.searchParams.set('screen_hint', screenHint);\n return authorize.toString();\n }\n\n async completeLogin(url = window.location.href): Promise<{ returnTo: string; token: TokenSet }> {\n const callback = new URL(url);\n const code = callback.searchParams.get('code');\n const state = callback.searchParams.get('state');\n const transactionKey = this.key(`transaction:${state}`);\n const rawTransaction = sessionStorage.getItem(transactionKey);\n const transaction = rawTransaction ? JSON.parse(rawTransaction) : null;\n if (!code || !state || !transaction?.verifier || Date.now() - transaction.createdAt > 10 * 60 * 1000) {\n throw new Error('Invalid OAuth callback state.');\n }\n sessionStorage.removeItem(transactionKey);\n const response = await fetch(new URL('/oauth/token', this.config.authUrl), {\n signal: AbortSignal.timeout(15000),\n method: 'POST',\n credentials: 'include',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n 'x-device-id': this.getDeviceId()\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n code_verifier: transaction.verifier\n })\n });\n if (!response.ok) throw new Error('OAuth authorization code exchange failed.');\n const payload = await response.json();\n if (typeof payload.access_token !== 'string' || !payload.access_token) {\n throw new Error('OAuth response did not contain an access token.');\n }\n const token = { accessToken: payload.access_token, expiresIn: payload.expires_in };\n sessionStorage.setItem(this.key('access-token'), token.accessToken);\n const candidate = transaction.returnTo;\n const returnTo = typeof candidate === 'string' && candidate.startsWith('/') && !candidate.startsWith('//') && !candidate.includes('\\\\') ? candidate : '/';\n return { returnTo, token };\n }\n\n async refresh(): Promise<TokenSet> {\n const response = await fetch(new URL('/oauth/token', this.config.authUrl), {\n method: 'POST',\n credentials: 'include',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n 'x-device-id': this.getDeviceId()\n },\n body: new URLSearchParams({ grant_type: 'refresh_token', client_id: this.config.clientId })\n });\n if (!response.ok) throw new Error('OAuth refresh failed.');\n const payload = await response.json();\n if (typeof payload.access_token !== 'string' || !payload.access_token) {\n throw new Error('OAuth response did not contain an access token.');\n }\n const token = { accessToken: payload.access_token, expiresIn: payload.expires_in };\n sessionStorage.setItem(this.key('access-token'), token.accessToken);\n return token;\n }\n\n async getUser<T>() {\n const token = this.getToken() || (await this.refresh()).accessToken;\n const response = await fetch(new URL('/oauth/userinfo', this.config.authUrl), {\n headers: { authorization: `Bearer ${token}` },\n credentials: 'include'\n });\n if (!response.ok) throw new Error('OAuth userinfo request failed.');\n return response.json() as Promise<T>;\n }\n\n async getSession<TUser = unknown>(): Promise<TUser | null> {\n try {\n return await this.getUser<TUser>();\n } catch {\n return null;\n }\n }\n\n async requireAuth(returnTo = window.location.pathname + window.location.search) {\n if (this.isAuthenticated()) return true;\n await this.login(returnTo);\n return false;\n }\n\n async logout(returnTo = `${window.location.origin}/`) {\n const logout = new URL('/oauth/logout', this.config.authUrl);\n logout.search = new URLSearchParams({\n client_id: this.config.clientId,\n post_logout_redirect_uri: returnTo\n }).toString();\n sessionStorage.removeItem(this.key('access-token'));\n window.location.assign(logout.toString());\n }\n\n private async authRequest<T = { success: true; message: string }>(path: string, body: Record<string, string>) {\n const response = await fetch(new URL(path, this.config.authUrl), {\n method: 'POST',\n credentials: 'include',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body)\n });\n const payload = await response.json().catch(() => null);\n if (!response.ok) {\n throw new Error(payload?.error?.message ?? 'Identity registration request failed.');\n }\n return (payload?.data ?? payload) as T;\n }\n}\n\nexport { watchWebSession, endWebSession } from \"./web-session.js\";\n"],"mappings":"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","names":["a","b"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biowiki/auth-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Browser OAuth 2.0 Authorization Code + PKCE client for BioWiki applications.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -14,16 +14,18 @@
14
14
  "files": [
15
15
  "dist",
16
16
  "README.md",
17
+ "CHANGELOG.md",
17
18
  "LICENSE"
18
19
  ],
19
20
  "scripts": {
21
+ "test": "npm run build && node --test test/*.test.js",
20
22
  "build": "tsup",
21
23
  "typecheck": "tsc --noEmit",
22
24
  "prepack": "npm run typecheck && npm run build",
23
25
  "prepublishOnly": "npm run typecheck && npm run build"
24
26
  },
25
27
  "publishConfig": {
26
- "access": "restricted"
28
+ "access": "public"
27
29
  },
28
30
  "engines": {
29
31
  "node": ">=24 <25"