@canopy-io/browser 0.0.0-stage → 0.1.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,7 @@
1
+ # @canopy-io/browser
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0a59da5: First release. `createCanopyBrowserClient` signs a single-page app in through Canopy's hosted login as a public OAuth client: PKCE with no secret, tokens kept in memory, a reload resumed with `prompt=none`, refreshes shared so a rotated token is never presented twice, and a sign-out that ends Canopy's session too. Zero runtime dependencies.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Canopy Identity Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,70 @@
1
- # Temporary Holding Version
1
+ # @canopy-io/browser
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Sign a single-page app in through [Canopy](https://canopy-io.com)'s hosted login, with no backend and no secret.
4
+
5
+ The app registers as a **public** OAuth client: it has a `client_id` and nothing else to keep, and PKCE binds each sign-in to the browser that started it. This package runs that flow and keeps the tokens in memory, out of reach of anything reading `localStorage`. Zero runtime dependencies.
6
+
7
+ An app that has a backend does not need this package: its server exchanges the code as a confidential client, and the browser never holds a token. Use [`@canopy-io/node`](https://www.npmjs.com/package/@canopy-io/node) there.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @canopy-io/browser
13
+ ```
14
+
15
+ ## Before the first sign-in
16
+
17
+ 1. Register an OAuth client in the Developer Console (Integrations → OAuth Clients) with **Client Type** set to **Public**, your callback URL as a redirect URI, and the page `signOut()` lands on as a post-logout redirect URL.
18
+ 2. Add your app's origin, such as `https://app.example.com`, to the Environment's **Allowed web origins** (Tenant → Authentication → Sign-in methods). Without it the browser cannot read Canopy's token responses.
19
+
20
+ ## Use
21
+
22
+ ```ts
23
+ import { createCanopyBrowserClient } from "@canopy-io/browser";
24
+
25
+ const canopy = createCanopyBrowserClient({
26
+ issuer: "https://canopy-io.com/environments/<environment_id>",
27
+ clientId: "<public client_id>",
28
+ redirectUri: `${location.origin}/callback`,
29
+ postLogoutRedirectUri: `${location.origin}/`,
30
+ onSessionEnd: () => showSignedOut(),
31
+ });
32
+
33
+ // Once per page load.
34
+ const result = await canopy.start();
35
+
36
+ if (result.status === "signed-in") {
37
+ showSignedIn(canopy.getClaims());
38
+ } else if (result.status !== "redirecting") {
39
+ showSignedOut();
40
+ }
41
+
42
+ signInButton.onclick = () => canopy.signIn();
43
+ signOutButton.onclick = () => canopy.signOut();
44
+
45
+ // On every call to your API.
46
+ const token = await canopy.getAccessToken();
47
+ ```
48
+
49
+ - `start()` finishes a sign-in Canopy has just answered. With nothing in memory (a first visit, a reload) it asks Canopy with `prompt=none`, which returns with tokens while the person's Canopy session lasts and with `login_required` otherwise. Show the sign-in button on `signed-out`, and on `error` (`result.error.needsSignIn` tells the expected answers from a fault).
50
+ - `signIn({ prompt, organization, loginHint, returnTo })` sends the browser to the hosted sign-in. `returnTo` comes back on the result.
51
+ - `getAccessToken()` returns a current access token, refreshed first when it is within a minute of expiring, or `null` once the session has ended. Concurrent calls share one refresh.
52
+ - `getClaims()` returns the ID token's claims: `sub`, and `email` or `name` with those scopes.
53
+ - `signOut()` revokes the refresh token and sends the browser to Canopy's end-session endpoint, so the next `start()` does not sign the person back in.
54
+
55
+ Your API verifies the access token as it would any Hosted Login token, with `audience` set to the public `client_id` (`TokenVerifier` in `@canopy-io/node`). This package does not verify signatures: it checks the ID token's issuer, audience, nonce and expiry, as OpenID Connect asks of a token received straight from the token endpoint.
56
+
57
+ ## Sessions
58
+
59
+ A public client's session ends a fixed time after sign-in, however often it refreshes: 30 days by default, set per Environment from 1 to 90 days (**Public app session lifetime**). Then `getAccessToken()` returns `null` and `onSessionEnd` is called.
60
+
61
+ ## Errors
62
+
63
+ - `CanopyAuthError`: Canopy answered with an OAuth error. Branch on `code` (`login_required`, `interaction_required`, `invalid_grant`, `state_mismatch`).
64
+ - `CanopyConnectionError`: no response could be read. A browser reports a cross-origin refusal the same way as a network failure; on a page that has never signed in, check **Allowed web origins**.
65
+
66
+ Full guide: [Hosted Login, single-page and mobile apps](https://canopy-io.com/docs/authentication/hosted-login#single-page-and-mobile-apps).
67
+
68
+ ## License
69
+
70
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,505 @@
1
+ 'use strict';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __typeError = (msg) => {
5
+ throw TypeError(msg);
6
+ };
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
9
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
10
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
11
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
12
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value);
13
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
14
+
15
+ // src/errors.ts
16
+ var NEEDS_SIGN_IN = /* @__PURE__ */ new Set([
17
+ "login_required",
18
+ "interaction_required",
19
+ "consent_required"
20
+ ]);
21
+ var CanopyAuthError = class extends Error {
22
+ constructor(code, description, status) {
23
+ super(description ? `${code}: ${description}` : code);
24
+ __publicField(this, "name", "CanopyAuthError");
25
+ /** The OAuth `error`, e.g. `login_required` or `invalid_grant`. */
26
+ __publicField(this, "code");
27
+ /** The `error_description`, for logging. Not meant for end users. */
28
+ __publicField(this, "description");
29
+ /** The HTTP status, when the error came from an endpoint rather than the redirect. */
30
+ __publicField(this, "status");
31
+ this.code = code;
32
+ this.description = description;
33
+ this.status = status;
34
+ }
35
+ /**
36
+ * True when the answer means the person has to sign in: a silent attempt
37
+ * found no session, or the session needs them for something. Show your
38
+ * sign-in button rather than trying again silently.
39
+ */
40
+ get needsSignIn() {
41
+ return NEEDS_SIGN_IN.has(this.code);
42
+ }
43
+ };
44
+ var CanopyConnectionError = class extends Error {
45
+ constructor(url, cause) {
46
+ super(
47
+ `Could not read a response from ${url}. If this page has never signed in, add its origin (${currentOrigin()}) to the Environment's Allowed web origins in the Developer Console; the browser blocks the response otherwise.`,
48
+ { cause }
49
+ );
50
+ __publicField(this, "name", "CanopyConnectionError");
51
+ /** The URL that could not be read. */
52
+ __publicField(this, "url");
53
+ this.url = url;
54
+ }
55
+ };
56
+ function currentOrigin() {
57
+ return typeof location === "undefined" ? "this origin" : location.origin;
58
+ }
59
+ function isCanopyAuthError(error) {
60
+ return error instanceof CanopyAuthError;
61
+ }
62
+ function isCanopyConnectionError(error) {
63
+ return error instanceof CanopyConnectionError;
64
+ }
65
+
66
+ // src/pkce.ts
67
+ function base64UrlEncode(bytes) {
68
+ let binary = "";
69
+ for (const byte of bytes) {
70
+ binary += String.fromCharCode(byte);
71
+ }
72
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
73
+ }
74
+ function base64UrlDecode(value) {
75
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/");
76
+ const binary = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
77
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
78
+ return new TextDecoder().decode(bytes);
79
+ }
80
+ function randomString() {
81
+ const bytes = new Uint8Array(32);
82
+ crypto.getRandomValues(bytes);
83
+ return base64UrlEncode(bytes);
84
+ }
85
+ async function createPkcePair() {
86
+ const verifier = randomString();
87
+ const digest = await crypto.subtle.digest(
88
+ "SHA-256",
89
+ new TextEncoder().encode(verifier)
90
+ );
91
+ return { verifier, challenge: base64UrlEncode(new Uint8Array(digest)) };
92
+ }
93
+
94
+ // src/id-token.ts
95
+ function decodeJwtPayload(token) {
96
+ const payload = token.split(".")[1];
97
+ if (!payload) {
98
+ throw new CanopyAuthError("invalid_token", "The token is not a JWT.");
99
+ }
100
+ const parsed = JSON.parse(base64UrlDecode(payload));
101
+ if (typeof parsed !== "object" || parsed === null) {
102
+ throw new CanopyAuthError("invalid_token", "The token has no claims.");
103
+ }
104
+ return parsed;
105
+ }
106
+ function checkIdToken(token, expected, nowSeconds) {
107
+ const claims = decodeJwtPayload(token);
108
+ const audience = claims["aud"];
109
+ const audiences = Array.isArray(audience) ? audience : [audience];
110
+ if (claims["iss"] !== expected.issuer) {
111
+ throw new CanopyAuthError(
112
+ "invalid_token",
113
+ `The ID token's issuer is ${String(claims["iss"])}, not ${expected.issuer}.`
114
+ );
115
+ }
116
+ if (!audiences.includes(expected.clientId)) {
117
+ throw new CanopyAuthError(
118
+ "invalid_token",
119
+ "The ID token was not issued to this client."
120
+ );
121
+ }
122
+ if (expected.nonce !== void 0 && claims["nonce"] !== expected.nonce) {
123
+ throw new CanopyAuthError(
124
+ "invalid_token",
125
+ "The ID token answers another sign-in: its nonce does not match."
126
+ );
127
+ }
128
+ if (typeof claims["exp"] !== "number" || claims["exp"] <= nowSeconds) {
129
+ throw new CanopyAuthError("invalid_token", "The ID token has expired.");
130
+ }
131
+ return claims;
132
+ }
133
+
134
+ // src/client.ts
135
+ var DEFAULT_SCOPES = ["openid", "profile", "email"];
136
+ var RESPONSE_PARAMS = [
137
+ "code",
138
+ "state",
139
+ "error",
140
+ "error_description",
141
+ "error_uri",
142
+ "iss"
143
+ ];
144
+ function createCanopyBrowserClient(options) {
145
+ return new CanopyBrowserClient(options);
146
+ }
147
+ var _issuer, _clientId, _redirectUri, _scopes, _postLogoutRedirectUri, _leewayMs, _onSessionEnd, _storage, _navigate, _replaceUrl, _fetch, _now, _discovery, _session, _refreshing, _CanopyBrowserClient_instances, refresh_fn, keep_fn, token_fn, post_fn, discover_fn, readDiscovery_fn, pendingKey_fn, takePending_fn;
148
+ var CanopyBrowserClient = class {
149
+ constructor(options) {
150
+ __privateAdd(this, _CanopyBrowserClient_instances);
151
+ __privateAdd(this, _issuer);
152
+ __privateAdd(this, _clientId);
153
+ __privateAdd(this, _redirectUri);
154
+ __privateAdd(this, _scopes);
155
+ __privateAdd(this, _postLogoutRedirectUri);
156
+ __privateAdd(this, _leewayMs);
157
+ __privateAdd(this, _onSessionEnd);
158
+ __privateAdd(this, _storage);
159
+ __privateAdd(this, _navigate);
160
+ __privateAdd(this, _replaceUrl);
161
+ __privateAdd(this, _fetch);
162
+ __privateAdd(this, _now);
163
+ __privateAdd(this, _discovery);
164
+ __privateAdd(this, _session);
165
+ __privateAdd(this, _refreshing);
166
+ if (!options.issuer || !options.clientId || !options.redirectUri) {
167
+ throw new TypeError(
168
+ "createCanopyBrowserClient needs an issuer, a clientId and a redirectUri."
169
+ );
170
+ }
171
+ __privateSet(this, _issuer, withoutTrailingSlashes(options.issuer));
172
+ __privateSet(this, _clientId, options.clientId);
173
+ __privateSet(this, _redirectUri, options.redirectUri);
174
+ __privateSet(this, _scopes, options.scopes ?? DEFAULT_SCOPES);
175
+ __privateSet(this, _postLogoutRedirectUri, options.postLogoutRedirectUri);
176
+ __privateSet(this, _leewayMs, (options.refreshLeewaySeconds ?? 60) * 1e3);
177
+ __privateSet(this, _onSessionEnd, options.onSessionEnd);
178
+ __privateSet(this, _storage, options.storage ?? sessionStorage);
179
+ __privateSet(this, _navigate, options.navigate ?? ((url) => location.assign(url)));
180
+ __privateSet(this, _replaceUrl, options.replaceUrl ?? ((url) => history.replaceState(history.state, "", url)));
181
+ __privateSet(this, _fetch, options.fetch ?? globalThis.fetch.bind(globalThis));
182
+ __privateSet(this, _now, options.now ?? Date.now);
183
+ }
184
+ /**
185
+ * Run once when the app loads. Finishes a sign-in when Canopy has just sent
186
+ * the browser back, and otherwise tries to resume Canopy's session without
187
+ * showing a page. Show your sign-in button on `signed-out`, and on `error`
188
+ * when `error.needsSignIn`.
189
+ */
190
+ async start(url = location.href) {
191
+ if (hasResponse(url)) {
192
+ return this.handleRedirect(url);
193
+ }
194
+ if (__privateGet(this, _session)) {
195
+ return { status: "signed-in", returnTo: void 0 };
196
+ }
197
+ await this.signIn({ prompt: "none" });
198
+ return { status: "redirecting" };
199
+ }
200
+ /** Send the browser to Canopy's sign-in. */
201
+ async signIn(signIn = {}) {
202
+ const discovery = await __privateMethod(this, _CanopyBrowserClient_instances, discover_fn).call(this);
203
+ const { verifier, challenge } = await createPkcePair();
204
+ const state = randomString();
205
+ const nonce = randomString();
206
+ const pending = { verifier, nonce };
207
+ if (signIn.returnTo !== void 0) {
208
+ pending.returnTo = signIn.returnTo;
209
+ }
210
+ __privateGet(this, _storage).setItem(__privateMethod(this, _CanopyBrowserClient_instances, pendingKey_fn).call(this, state), JSON.stringify(pending));
211
+ const authorize = new URL(discovery.authorization_endpoint);
212
+ authorize.search = new URLSearchParams({
213
+ response_type: "code",
214
+ client_id: __privateGet(this, _clientId),
215
+ redirect_uri: __privateGet(this, _redirectUri),
216
+ scope: __privateGet(this, _scopes).join(" "),
217
+ state,
218
+ nonce,
219
+ code_challenge: challenge,
220
+ code_challenge_method: "S256",
221
+ ...signIn.prompt ? { prompt: signIn.prompt } : {},
222
+ ...signIn.organization ? { organization: signIn.organization } : {},
223
+ ...signIn.loginHint ? { login_hint: signIn.loginHint } : {}
224
+ }).toString();
225
+ __privateGet(this, _navigate).call(this, authorize.toString());
226
+ }
227
+ /**
228
+ * Finish the sign-in Canopy just sent the browser back from: check `state`
229
+ * against this browser's own request, exchange the code with the client id
230
+ * and the PKCE verifier, and take the response parameters out of the URL.
231
+ */
232
+ async handleRedirect(url = location.href) {
233
+ const current = new URL(url);
234
+ const params = current.searchParams;
235
+ const state = params.get("state");
236
+ if (!hasResponse(url)) {
237
+ return { status: "signed-out" };
238
+ }
239
+ __privateGet(this, _replaceUrl).call(this, withoutResponse(current));
240
+ const pending = state ? __privateMethod(this, _CanopyBrowserClient_instances, takePending_fn).call(this, state) : void 0;
241
+ if (!pending) {
242
+ return {
243
+ status: "error",
244
+ error: new CanopyAuthError(
245
+ "state_mismatch",
246
+ "The response does not answer a sign-in this browser started."
247
+ ),
248
+ returnTo: void 0
249
+ };
250
+ }
251
+ const error = params.get("error");
252
+ if (error) {
253
+ return {
254
+ status: "error",
255
+ error: new CanopyAuthError(
256
+ error,
257
+ params.get("error_description") ?? void 0
258
+ ),
259
+ returnTo: pending.returnTo
260
+ };
261
+ }
262
+ try {
263
+ const tokens = await __privateMethod(this, _CanopyBrowserClient_instances, token_fn).call(this, {
264
+ grant_type: "authorization_code",
265
+ code: params.get("code") ?? "",
266
+ redirect_uri: __privateGet(this, _redirectUri),
267
+ code_verifier: pending.verifier
268
+ });
269
+ __privateMethod(this, _CanopyBrowserClient_instances, keep_fn).call(this, tokens, pending.nonce);
270
+ return { status: "signed-in", returnTo: pending.returnTo };
271
+ } catch (caught) {
272
+ if (caught instanceof CanopyAuthError) {
273
+ return { status: "error", error: caught, returnTo: pending.returnTo };
274
+ }
275
+ throw caught;
276
+ }
277
+ }
278
+ /** Whether tokens are in memory. */
279
+ isSignedIn() {
280
+ return __privateGet(this, _session) !== void 0;
281
+ }
282
+ /**
283
+ * A current access token for `Authorization: Bearer`, refreshed first when
284
+ * it is about to expire. `null` when nobody is signed in, or the session has
285
+ * ended (its lifetime ran out or it was revoked).
286
+ */
287
+ async getAccessToken() {
288
+ if (!__privateGet(this, _session)) {
289
+ return null;
290
+ }
291
+ if (__privateGet(this, _session).expiresAt - __privateGet(this, _leewayMs) <= __privateGet(this, _now).call(this)) {
292
+ try {
293
+ await this.refresh();
294
+ } catch (caught) {
295
+ if (caught instanceof CanopyAuthError) {
296
+ return null;
297
+ }
298
+ throw caught;
299
+ }
300
+ }
301
+ return __privateGet(this, _session)?.accessToken ?? null;
302
+ }
303
+ /**
304
+ * Trade the refresh token for new tokens now. Concurrent calls share one
305
+ * request, so the rotated token is never presented twice from this page.
306
+ * A refused refresh ends the session in memory and calls `onSessionEnd`.
307
+ */
308
+ refresh() {
309
+ __privateGet(this, _refreshing) ?? __privateSet(this, _refreshing, __privateMethod(this, _CanopyBrowserClient_instances, refresh_fn).call(this).finally(() => {
310
+ __privateSet(this, _refreshing, void 0);
311
+ }));
312
+ return __privateGet(this, _refreshing);
313
+ }
314
+ /** The signed-in person's ID token claims, or `null`. */
315
+ getClaims() {
316
+ return __privateGet(this, _session)?.claims ?? null;
317
+ }
318
+ /**
319
+ * Sign out of the app and of Canopy: revoke the refresh token, forget the
320
+ * tokens, then send the browser to Canopy's end-session endpoint with the
321
+ * ID token as the hint. Skipping the second half would let the next
322
+ * `start()` sign the person straight back in.
323
+ */
324
+ async signOut() {
325
+ const session = __privateGet(this, _session);
326
+ const discovery = await __privateMethod(this, _CanopyBrowserClient_instances, discover_fn).call(this);
327
+ __privateSet(this, _session, void 0);
328
+ if (session?.refreshToken && discovery.revocation_endpoint) {
329
+ try {
330
+ await __privateMethod(this, _CanopyBrowserClient_instances, post_fn).call(this, discovery.revocation_endpoint, {
331
+ token: session.refreshToken,
332
+ token_type_hint: "refresh_token"
333
+ });
334
+ } catch {
335
+ }
336
+ }
337
+ if (!discovery.end_session_endpoint) {
338
+ return;
339
+ }
340
+ const endSession = new URL(discovery.end_session_endpoint);
341
+ const query = new URLSearchParams({ client_id: __privateGet(this, _clientId) });
342
+ if (session) {
343
+ query.set("id_token_hint", session.idToken);
344
+ }
345
+ if (__privateGet(this, _postLogoutRedirectUri)) {
346
+ query.set("post_logout_redirect_uri", __privateGet(this, _postLogoutRedirectUri));
347
+ }
348
+ endSession.search = query.toString();
349
+ __privateGet(this, _navigate).call(this, endSession.toString());
350
+ }
351
+ };
352
+ _issuer = new WeakMap();
353
+ _clientId = new WeakMap();
354
+ _redirectUri = new WeakMap();
355
+ _scopes = new WeakMap();
356
+ _postLogoutRedirectUri = new WeakMap();
357
+ _leewayMs = new WeakMap();
358
+ _onSessionEnd = new WeakMap();
359
+ _storage = new WeakMap();
360
+ _navigate = new WeakMap();
361
+ _replaceUrl = new WeakMap();
362
+ _fetch = new WeakMap();
363
+ _now = new WeakMap();
364
+ _discovery = new WeakMap();
365
+ _session = new WeakMap();
366
+ _refreshing = new WeakMap();
367
+ _CanopyBrowserClient_instances = new WeakSet();
368
+ refresh_fn = async function() {
369
+ var _a;
370
+ const session = __privateGet(this, _session);
371
+ if (!session?.refreshToken) {
372
+ throw new CanopyAuthError(
373
+ "invalid_grant",
374
+ "There is no refresh token to use."
375
+ );
376
+ }
377
+ try {
378
+ const tokens = await __privateMethod(this, _CanopyBrowserClient_instances, token_fn).call(this, {
379
+ grant_type: "refresh_token",
380
+ refresh_token: session.refreshToken
381
+ });
382
+ __privateMethod(this, _CanopyBrowserClient_instances, keep_fn).call(this, tokens, void 0, session);
383
+ } catch (caught) {
384
+ if (caught instanceof CanopyAuthError) {
385
+ __privateSet(this, _session, void 0);
386
+ (_a = __privateGet(this, _onSessionEnd)) == null ? void 0 : _a.call(this, caught);
387
+ }
388
+ throw caught;
389
+ }
390
+ };
391
+ keep_fn = function(tokens, nonce, previous) {
392
+ const idToken = tokens.id_token ?? previous?.idToken;
393
+ if (!idToken) {
394
+ throw new CanopyAuthError(
395
+ "invalid_token",
396
+ "The token response carried no ID token. Ask for the openid scope."
397
+ );
398
+ }
399
+ const claims = tokens.id_token ? checkIdToken(
400
+ tokens.id_token,
401
+ { issuer: __privateGet(this, _issuer), clientId: __privateGet(this, _clientId), nonce },
402
+ Math.floor(__privateGet(this, _now).call(this) / 1e3)
403
+ ) : previous.claims;
404
+ __privateSet(this, _session, {
405
+ accessToken: tokens.access_token,
406
+ expiresAt: __privateGet(this, _now).call(this) + (tokens.expires_in ?? 0) * 1e3,
407
+ refreshToken: tokens.refresh_token ?? previous?.refreshToken,
408
+ idToken,
409
+ claims
410
+ });
411
+ };
412
+ token_fn = async function(body) {
413
+ const discovery = await __privateMethod(this, _CanopyBrowserClient_instances, discover_fn).call(this);
414
+ return await __privateMethod(this, _CanopyBrowserClient_instances, post_fn).call(this, discovery.token_endpoint, body);
415
+ };
416
+ post_fn = async function(url, body) {
417
+ let response;
418
+ try {
419
+ response = await __privateGet(this, _fetch).call(this, url, {
420
+ method: "POST",
421
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
422
+ body: new URLSearchParams({ ...body, client_id: __privateGet(this, _clientId) })
423
+ });
424
+ } catch (caught) {
425
+ throw new CanopyConnectionError(url, caught);
426
+ }
427
+ const text = await response.text();
428
+ const parsed = text ? JSON.parse(text) : {};
429
+ if (!response.ok) {
430
+ const failure = parsed;
431
+ throw new CanopyAuthError(
432
+ failure.error ?? "server_error",
433
+ failure.error_description,
434
+ response.status
435
+ );
436
+ }
437
+ return parsed;
438
+ };
439
+ discover_fn = function() {
440
+ __privateGet(this, _discovery) ?? __privateSet(this, _discovery, __privateMethod(this, _CanopyBrowserClient_instances, readDiscovery_fn).call(this).catch((caught) => {
441
+ __privateSet(this, _discovery, void 0);
442
+ throw caught;
443
+ }));
444
+ return __privateGet(this, _discovery);
445
+ };
446
+ readDiscovery_fn = async function() {
447
+ const url = `${__privateGet(this, _issuer)}/.well-known/openid-configuration`;
448
+ let response;
449
+ try {
450
+ response = await __privateGet(this, _fetch).call(this, url);
451
+ } catch (caught) {
452
+ throw new CanopyConnectionError(url, caught);
453
+ }
454
+ if (!response.ok) {
455
+ throw new CanopyAuthError(
456
+ "invalid_issuer",
457
+ `${url} answered ${response.status}. Check the issuer.`,
458
+ response.status
459
+ );
460
+ }
461
+ const discovery = await response.json();
462
+ if (discovery.issuer !== __privateGet(this, _issuer)) {
463
+ throw new CanopyAuthError(
464
+ "invalid_issuer",
465
+ `The discovery document names ${discovery.issuer}, not ${__privateGet(this, _issuer)}.`
466
+ );
467
+ }
468
+ return discovery;
469
+ };
470
+ pendingKey_fn = function(state) {
471
+ return `canopy.browser.${__privateGet(this, _clientId)}.${state}`;
472
+ };
473
+ takePending_fn = function(state) {
474
+ const key = __privateMethod(this, _CanopyBrowserClient_instances, pendingKey_fn).call(this, state);
475
+ const raw = __privateGet(this, _storage).getItem(key);
476
+ __privateGet(this, _storage).removeItem(key);
477
+ return raw ? JSON.parse(raw) : void 0;
478
+ };
479
+ function withoutTrailingSlashes(value) {
480
+ let end = value.length;
481
+ while (end > 0 && value[end - 1] === "/") {
482
+ end -= 1;
483
+ }
484
+ return value.slice(0, end);
485
+ }
486
+ function hasResponse(url) {
487
+ const params = new URL(url).searchParams;
488
+ return params.has("state") && (params.has("code") || params.has("error"));
489
+ }
490
+ function withoutResponse(url) {
491
+ const clean = new URL(url.toString());
492
+ for (const name of RESPONSE_PARAMS) {
493
+ clean.searchParams.delete(name);
494
+ }
495
+ return clean.toString();
496
+ }
497
+
498
+ exports.CanopyAuthError = CanopyAuthError;
499
+ exports.CanopyBrowserClient = CanopyBrowserClient;
500
+ exports.CanopyConnectionError = CanopyConnectionError;
501
+ exports.createCanopyBrowserClient = createCanopyBrowserClient;
502
+ exports.isCanopyAuthError = isCanopyAuthError;
503
+ exports.isCanopyConnectionError = isCanopyConnectionError;
504
+ //# sourceMappingURL=index.cjs.map
505
+ //# sourceMappingURL=index.cjs.map