@broberg/sso 0.8.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/README.md +113 -0
- package/dist/hono.cjs +341 -4
- package/dist/hono.cjs.map +1 -1
- package/dist/hono.d.cts +108 -1
- package/dist/hono.d.ts +108 -1
- package/dist/hono.js +341 -5
- package/dist/hono.js.map +1 -1
- package/dist/index.cjs +196 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +129 -1
- package/dist/index.d.ts +129 -1
- package/dist/index.js +190 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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() {
|
|
@@ -447,7 +548,36 @@ function createSsoClient(config, options = {}) {
|
|
|
447
548
|
unverifiedClaims: merged.unverifiedClaims,
|
|
448
549
|
idToken: body.id_token,
|
|
449
550
|
...body.access_token ? { accessToken: body.access_token } : {},
|
|
450
|
-
...body.refresh_token ? { refreshToken: body.refresh_token } : {}
|
|
551
|
+
...body.refresh_token ? { refreshToken: body.refresh_token } : {},
|
|
552
|
+
...typeof body.expires_in === "number" ? { expiresIn: body.expires_in } : {}
|
|
553
|
+
};
|
|
554
|
+
},
|
|
555
|
+
async refreshTokens(refreshToken) {
|
|
556
|
+
const { token_endpoint } = await discovery();
|
|
557
|
+
const res = await fetchImpl(token_endpoint, {
|
|
558
|
+
method: "POST",
|
|
559
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
560
|
+
body: new URLSearchParams({
|
|
561
|
+
grant_type: "refresh_token",
|
|
562
|
+
refresh_token: refreshToken,
|
|
563
|
+
client_id: config.clientId,
|
|
564
|
+
...config.clientSecret ? { client_secret: config.clientSecret } : {}
|
|
565
|
+
})
|
|
566
|
+
});
|
|
567
|
+
const raw = await res.text();
|
|
568
|
+
let body;
|
|
569
|
+
try {
|
|
570
|
+
body = raw.trim() === "" ? {} : JSON.parse(raw);
|
|
571
|
+
} catch {
|
|
572
|
+
throw new SsoError(`token refresh failed (${res.status}): the response body is not JSON`);
|
|
573
|
+
}
|
|
574
|
+
if (!res.ok || typeof body.access_token !== "string" || body.access_token === "") {
|
|
575
|
+
throw new SsoError(`token refresh failed (${res.status}): ${body.error ?? "no access_token in response"}`);
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
accessToken: body.access_token,
|
|
579
|
+
...typeof body.refresh_token === "string" && body.refresh_token ? { refreshToken: body.refresh_token } : {},
|
|
580
|
+
...typeof body.expires_in === "number" ? { expiresIn: body.expires_in } : {}
|
|
451
581
|
};
|
|
452
582
|
},
|
|
453
583
|
async logoutUrl(opts = {}) {
|
|
@@ -478,6 +608,35 @@ function createSsoClient(config, options = {}) {
|
|
|
478
608
|
return status;
|
|
479
609
|
},
|
|
480
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
|
+
},
|
|
481
640
|
async inviteUsers(appKey, input) {
|
|
482
641
|
if (input.users.length > MAX_INVITATIONS) {
|
|
483
642
|
throw new SsoError(`inviteUsers: ${input.users.length} users, BID accepts at most ${MAX_INVITATIONS} per call.`);
|
|
@@ -598,17 +757,53 @@ function readCookie(header, name) {
|
|
|
598
757
|
return void 0;
|
|
599
758
|
}
|
|
600
759
|
|
|
760
|
+
// src/tokens.ts
|
|
761
|
+
var warned = false;
|
|
762
|
+
function memoryTokenStore(options = {}) {
|
|
763
|
+
if (!options.quiet && !warned) {
|
|
764
|
+
warned = true;
|
|
765
|
+
console.warn(
|
|
766
|
+
"@broberg/sso: memoryTokenStore() keeps tokens in this process only \u2014 they are lost on restart and not shared between instances. Give ssoRoutes a TokenStore backed by your database in production."
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
const rows = /* @__PURE__ */ new Map();
|
|
770
|
+
return {
|
|
771
|
+
get size() {
|
|
772
|
+
return rows.size;
|
|
773
|
+
},
|
|
774
|
+
async get(sid) {
|
|
775
|
+
return rows.get(sid);
|
|
776
|
+
},
|
|
777
|
+
async set(sid, tokens) {
|
|
778
|
+
rows.set(sid, { ...tokens });
|
|
779
|
+
},
|
|
780
|
+
async delete(sid) {
|
|
781
|
+
rows.delete(sid);
|
|
782
|
+
},
|
|
783
|
+
async deleteSub(sub) {
|
|
784
|
+
for (const [sid, t] of rows) if (t.sub === sub) rows.delete(sid);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
exports.AVATAR_TYPES = AVATAR_TYPES;
|
|
601
790
|
exports.DEFAULT_SESSION_MAX_AGE = DEFAULT_SESSION_MAX_AGE;
|
|
602
791
|
exports.JwksError = JwksError;
|
|
603
792
|
exports.JwksUnavailableError = JwksUnavailableError;
|
|
604
793
|
exports.JwksUnknownKeyError = JwksUnknownKeyError;
|
|
794
|
+
exports.MAX_AVATAR_BYTES = MAX_AVATAR_BYTES;
|
|
605
795
|
exports.MAX_INVITATIONS = MAX_INVITATIONS;
|
|
796
|
+
exports.SsoAppApiError = SsoAppApiError;
|
|
797
|
+
exports.SsoAvatarRejectedError = SsoAvatarRejectedError;
|
|
606
798
|
exports.SsoConfigError = SsoConfigError;
|
|
607
799
|
exports.SsoError = SsoError;
|
|
800
|
+
exports.SsoInsufficientScopeError = SsoInsufficientScopeError;
|
|
801
|
+
exports.SsoReauthError = SsoReauthError;
|
|
608
802
|
exports.cookieHeader = cookieHeader;
|
|
609
803
|
exports.createJwksCache = createJwksCache;
|
|
610
804
|
exports.createSsoClient = createSsoClient;
|
|
611
805
|
exports.loadSsoConfig = loadSsoConfig;
|
|
806
|
+
exports.memoryTokenStore = memoryTokenStore;
|
|
612
807
|
exports.readCookie = readCookie;
|
|
613
808
|
exports.signSession = signSession;
|
|
614
809
|
exports.signValue = signValue;
|