@broberg/sso 0.9.0 → 0.10.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/dist/index.cjs CHANGED
@@ -173,6 +173,45 @@ var SsoError = class extends Error {
173
173
  this.name = "SsoError";
174
174
  }
175
175
  };
176
+ var SsoReauthError = class extends SsoError {
177
+ constructor(message, reason) {
178
+ super(message);
179
+ this.reason = reason;
180
+ this.name = "SsoReauthError";
181
+ }
182
+ reason;
183
+ };
184
+ var SsoAppApiError = class extends SsoError {
185
+ constructor(message, status, code) {
186
+ super(message);
187
+ this.status = status;
188
+ this.code = code;
189
+ this.name = "SsoAppApiError";
190
+ }
191
+ status;
192
+ code;
193
+ };
194
+ var SsoInsufficientScopeError = class extends SsoAppApiError {
195
+ constructor(message, scope) {
196
+ super(message, 403, "insufficient_scope");
197
+ this.scope = scope;
198
+ this.name = "SsoInsufficientScopeError";
199
+ }
200
+ scope;
201
+ };
202
+ var SsoAvatarRejectedError = class extends SsoError {
203
+ constructor(message, reason) {
204
+ super(message);
205
+ this.reason = reason;
206
+ this.name = "SsoAvatarRejectedError";
207
+ }
208
+ reason;
209
+ };
210
+ var MAX_AVATAR_BYTES = 2 * 1024 * 1024;
211
+ var AVATAR_TYPES = ["image/png", "image/jpeg", "image/webp"];
212
+ function mediaType(contentType) {
213
+ return (contentType ?? "").split(";")[0].trim().toLowerCase();
214
+ }
176
215
  var b64url = (bytes) => {
177
216
  const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
178
217
  let s = "";
@@ -264,6 +303,68 @@ function createSsoClient(config, options = {}) {
264
303
  clearTimeout(timer);
265
304
  }
266
305
  }
306
+ async function appRequest(path, bearer, init, what) {
307
+ const url = new URL(path, config.issuer).toString();
308
+ const ctrl = new AbortController();
309
+ let timer;
310
+ const timedOut = new Promise((_, reject) => {
311
+ timer = setTimeout(() => {
312
+ reject(new SsoAppApiError(`${url} did not answer within ${timeoutMs} ms \u2014 ${what} is unknown.`, null, void 0));
313
+ ctrl.abort();
314
+ }, timeoutMs);
315
+ });
316
+ const headers = { authorization: `Bearer ${bearer}` };
317
+ let body;
318
+ if (init.json !== void 0) {
319
+ headers["content-type"] = "application/json";
320
+ body = JSON.stringify(init.json);
321
+ } else if (init.bytes !== void 0) {
322
+ headers["content-type"] = init.contentType ?? "application/octet-stream";
323
+ body = init.bytes;
324
+ }
325
+ try {
326
+ const res = await Promise.race([
327
+ fetchImpl(url, { method: init.method, headers, ...body !== void 0 ? { body } : {}, signal: ctrl.signal }),
328
+ timedOut
329
+ ]);
330
+ const raw = await Promise.race([res.text(), timedOut]);
331
+ let parsed;
332
+ try {
333
+ parsed = raw.trim() === "" ? void 0 : JSON.parse(raw);
334
+ } catch {
335
+ parsed = void 0;
336
+ }
337
+ const fields = parsed && typeof parsed === "object" ? parsed : {};
338
+ const code = typeof fields.error === "string" ? fields.error : void 0;
339
+ if (!res.ok) {
340
+ if (res.status === 403 && code === "insufficient_scope") {
341
+ const scope = typeof fields.scope === "string" && fields.scope ? fields.scope : init.method === "GET" ? "profile" : "profile:write";
342
+ throw new SsoInsufficientScopeError(
343
+ `${url} answered 403 insufficient_scope \u2014 the user's token lacks scope "${scope}"; log in again with it requested.`,
344
+ scope
345
+ );
346
+ }
347
+ throw new SsoAppApiError(`${url} answered ${res.status}${code ? ` (${code})` : ""} \u2014 ${what} is unknown.`, res.status, code);
348
+ }
349
+ if (parsed === void 0) {
350
+ throw new SsoAppApiError(`${url} answered ${res.status} with a body that is not JSON \u2014 ${what} is unknown.`, res.status, void 0);
351
+ }
352
+ return parsed;
353
+ } catch (e) {
354
+ if (e instanceof SsoError) throw e;
355
+ throw new SsoAppApiError(`${url} could not be reached (${e instanceof Error ? e.name : "error"}) \u2014 ${what} is unknown.`, null, void 0);
356
+ } finally {
357
+ clearTimeout(timer);
358
+ }
359
+ }
360
+ function asProfile(body, path) {
361
+ const b = body ?? {};
362
+ if (typeof b.sub !== "string" || b.sub === "" || typeof b.account_url !== "string") {
363
+ throw new SsoAppApiError(`${path} returned no "sub" or "account_url" \u2014 the profile is unknown.`, 200, void 0);
364
+ }
365
+ const opt = (v) => typeof v === "string" ? v : null;
366
+ return { sub: b.sub, name: opt(b.name), picture: opt(b.picture), email: opt(b.email), account_url: b.account_url };
367
+ }
267
368
  let discoveryPromise = null;
268
369
  let jwksCache = null;
269
370
  async function discovery() {
@@ -507,6 +608,35 @@ function createSsoClient(config, options = {}) {
507
608
  return status;
508
609
  },
509
610
  verifyLogoutToken,
611
+ async getProfile(accessToken) {
612
+ const path = "/api/app/profile";
613
+ return asProfile(await appRequest(path, accessToken, { method: "GET" }, "the profile"), path);
614
+ },
615
+ async updateProfile(accessToken, name) {
616
+ const path = "/api/app/profile";
617
+ return asProfile(await appRequest(path, accessToken, { method: "POST", json: { name } }, "the new name"), path);
618
+ },
619
+ async uploadAvatar(accessToken, bytes, contentType) {
620
+ const type = mediaType(contentType);
621
+ if (!AVATAR_TYPES.includes(type)) {
622
+ throw new SsoAvatarRejectedError(
623
+ `uploadAvatar: "${type}" is not accepted \u2014 BID takes ${AVATAR_TYPES.join(", ")}.`,
624
+ "unsupported_type"
625
+ );
626
+ }
627
+ if (bytes.byteLength > MAX_AVATAR_BYTES) {
628
+ throw new SsoAvatarRejectedError(
629
+ `uploadAvatar: ${bytes.byteLength} bytes \u2014 BID takes at most ${MAX_AVATAR_BYTES}.`,
630
+ "too_large"
631
+ );
632
+ }
633
+ const path = "/api/app/profile/avatar";
634
+ return asProfile(await appRequest(path, accessToken, { method: "POST", bytes, contentType: type }, "the new picture"), path);
635
+ },
636
+ async removeAvatar(accessToken) {
637
+ const path = "/api/app/profile/avatar/remove";
638
+ return asProfile(await appRequest(path, accessToken, { method: "POST" }, "the removed picture"), path);
639
+ },
510
640
  async inviteUsers(appKey, input) {
511
641
  if (input.users.length > MAX_INVITATIONS) {
512
642
  throw new SsoError(`inviteUsers: ${input.users.length} users, BID accepts at most ${MAX_INVITATIONS} per call.`);
@@ -656,13 +786,19 @@ function memoryTokenStore(options = {}) {
656
786
  };
657
787
  }
658
788
 
789
+ exports.AVATAR_TYPES = AVATAR_TYPES;
659
790
  exports.DEFAULT_SESSION_MAX_AGE = DEFAULT_SESSION_MAX_AGE;
660
791
  exports.JwksError = JwksError;
661
792
  exports.JwksUnavailableError = JwksUnavailableError;
662
793
  exports.JwksUnknownKeyError = JwksUnknownKeyError;
794
+ exports.MAX_AVATAR_BYTES = MAX_AVATAR_BYTES;
663
795
  exports.MAX_INVITATIONS = MAX_INVITATIONS;
796
+ exports.SsoAppApiError = SsoAppApiError;
797
+ exports.SsoAvatarRejectedError = SsoAvatarRejectedError;
664
798
  exports.SsoConfigError = SsoConfigError;
665
799
  exports.SsoError = SsoError;
800
+ exports.SsoInsufficientScopeError = SsoInsufficientScopeError;
801
+ exports.SsoReauthError = SsoReauthError;
666
802
  exports.cookieHeader = cookieHeader;
667
803
  exports.createJwksCache = createJwksCache;
668
804
  exports.createSsoClient = createSsoClient;