@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.d.cts
CHANGED
|
@@ -201,6 +201,55 @@ interface Discovery {
|
|
|
201
201
|
declare class SsoError extends Error {
|
|
202
202
|
constructor(message: string);
|
|
203
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* F095.2 — the user must log in again: there is no live session, no stored
|
|
206
|
+
* tokens, or the renewal was refused. `reason` says which; "no_session" means
|
|
207
|
+
* there never was a login to recover, the others mean there was one and it is
|
|
208
|
+
* spent. Thrown by getAccessToken.
|
|
209
|
+
*/
|
|
210
|
+
declare class SsoReauthError extends SsoError {
|
|
211
|
+
readonly reason: "no_session" | "no_tokens" | "expired" | "refresh_failed";
|
|
212
|
+
constructor(message: string, reason: "no_session" | "no_tokens" | "expired" | "refresh_failed");
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* F095.2 — BID's app API answered with an error, or did not answer at all.
|
|
216
|
+
* `status` is BID's HTTP status (null when nothing came back: timeout, network);
|
|
217
|
+
* `code` is BID's own `error` field when it sent one. The message carries the
|
|
218
|
+
* URL, the status and the code — never the body, never a token.
|
|
219
|
+
*/
|
|
220
|
+
declare class SsoAppApiError extends SsoError {
|
|
221
|
+
readonly status: number | null;
|
|
222
|
+
readonly code: string | undefined;
|
|
223
|
+
constructor(message: string, status: number | null, code: string | undefined);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* F095.2 — BID refused because the user's token lacks a scope (403
|
|
227
|
+
* insufficient_scope). For the profile writes that is `profile:write`: the app
|
|
228
|
+
* must request it at login, and a user who logged in before it did must log in
|
|
229
|
+
* again. That is the one actionable fact, so it gets its own class.
|
|
230
|
+
*/
|
|
231
|
+
declare class SsoInsufficientScopeError extends SsoAppApiError {
|
|
232
|
+
readonly scope: string;
|
|
233
|
+
constructor(message: string, scope: string);
|
|
234
|
+
}
|
|
235
|
+
/** F095.2 — an avatar refused BEFORE it was sent: too big, or not PNG/JPEG/WebP. */
|
|
236
|
+
declare class SsoAvatarRejectedError extends SsoError {
|
|
237
|
+
readonly reason: "too_large" | "unsupported_type";
|
|
238
|
+
constructor(message: string, reason: "too_large" | "unsupported_type");
|
|
239
|
+
}
|
|
240
|
+
/** BID's limit for a profile picture (llms.txt step 7a: "max 2 MB", read as 2 MiB). */
|
|
241
|
+
declare const MAX_AVATAR_BYTES: number;
|
|
242
|
+
/** The picture types BID accepts. */
|
|
243
|
+
declare const AVATAR_TYPES: readonly string[];
|
|
244
|
+
/** The signed-in user's own profile, as BID's GET /api/app/profile returns it. */
|
|
245
|
+
interface BidProfile {
|
|
246
|
+
sub: string;
|
|
247
|
+
name: string | null;
|
|
248
|
+
picture: string | null;
|
|
249
|
+
email: string | null;
|
|
250
|
+
/** BID's own account page — mail, password, passkeys, 2FA and sessions live there. */
|
|
251
|
+
account_url: string;
|
|
252
|
+
}
|
|
204
253
|
/**
|
|
205
254
|
* What `beginLogin` produces. Every field except `url` must survive the round
|
|
206
255
|
* trip to BID and back — the adapter stores them in a short-lived cookie.
|
|
@@ -239,6 +288,8 @@ interface LoginResult {
|
|
|
239
288
|
idToken: string;
|
|
240
289
|
accessToken?: string;
|
|
241
290
|
refreshToken?: string;
|
|
291
|
+
/** Seconds the access token lives, when BID said so (`expires_in`). */
|
|
292
|
+
expiresIn?: number;
|
|
242
293
|
}
|
|
243
294
|
/**
|
|
244
295
|
* `prompt=none` asks BID to answer WITHOUT showing anything (components-F084.6).
|
|
@@ -278,6 +329,16 @@ interface SsoClient {
|
|
|
278
329
|
* answers — see the implementation for why that must never become "unverified".
|
|
279
330
|
*/
|
|
280
331
|
addressOwnership(accessToken: string, address: string): Promise<AddressOwnership>;
|
|
332
|
+
/**
|
|
333
|
+
* F095.1 — trade a refresh token for a new access token (`grant_type=refresh_token`).
|
|
334
|
+
* Throws SsoError on refusal; the message carries the status and BID's error
|
|
335
|
+
* code, never a token.
|
|
336
|
+
*/
|
|
337
|
+
refreshTokens(refreshToken: string): Promise<{
|
|
338
|
+
accessToken: string;
|
|
339
|
+
refreshToken?: string;
|
|
340
|
+
expiresIn?: number;
|
|
341
|
+
}>;
|
|
281
342
|
/**
|
|
282
343
|
* Invite users to BID on behalf of this app (the app's OWN key, `bidk_…`).
|
|
283
344
|
* One result per email. Throws SsoError on anything BID did not clearly say.
|
|
@@ -294,6 +355,21 @@ interface SsoClient {
|
|
|
294
355
|
* Throws SsoError on anything that is not a valid, sub-only logout token.
|
|
295
356
|
*/
|
|
296
357
|
verifyLogoutToken(token: string): Promise<LogoutToken>;
|
|
358
|
+
/**
|
|
359
|
+
* F095.2 — the signed-in user's own profile (scope `profile`), with the user's
|
|
360
|
+
* access token. Throws SsoAppApiError on any refusal.
|
|
361
|
+
*/
|
|
362
|
+
getProfile(accessToken: string): Promise<BidProfile>;
|
|
363
|
+
/** F095.2 — change the user's own name (scope `profile:write`). Returns the new profile. */
|
|
364
|
+
updateProfile(accessToken: string, name: string): Promise<BidProfile>;
|
|
365
|
+
/**
|
|
366
|
+
* F095.2 — replace the user's picture (scope `profile:write`). Refuses more than
|
|
367
|
+
* MAX_AVATAR_BYTES or a type outside AVATAR_TYPES with SsoAvatarRejectedError
|
|
368
|
+
* BEFORE anything is sent.
|
|
369
|
+
*/
|
|
370
|
+
uploadAvatar(accessToken: string, bytes: Uint8Array | ArrayBuffer, contentType: string): Promise<BidProfile>;
|
|
371
|
+
/** F095.2 — remove the user's picture (scope `profile:write`). */
|
|
372
|
+
removeAvatar(accessToken: string): Promise<BidProfile>;
|
|
297
373
|
/** Exposed for tests and for a health check; not needed in normal use. */
|
|
298
374
|
readonly jwks: JwksCache;
|
|
299
375
|
}
|
|
@@ -371,6 +447,12 @@ interface SessionPayload {
|
|
|
371
447
|
* older than any logout.
|
|
372
448
|
*/
|
|
373
449
|
iat?: number;
|
|
450
|
+
/**
|
|
451
|
+
* F095.1 — the key to this session's server-side tokens (ssoRoutes'
|
|
452
|
+
* `tokenStore`). Random, 128 bits. The tokens themselves are NEVER in the
|
|
453
|
+
* cookie: it is signed, not encrypted, and the browser can read it.
|
|
454
|
+
*/
|
|
455
|
+
sid?: string;
|
|
374
456
|
/** Optional convenience copies; never authorisation data. */
|
|
375
457
|
email?: string;
|
|
376
458
|
name?: string;
|
|
@@ -449,4 +531,50 @@ declare function cookieHeader(name: string, value: string, opts: {
|
|
|
449
531
|
}): string;
|
|
450
532
|
declare function readCookie(header: string | null | undefined, name: string): string | undefined;
|
|
451
533
|
|
|
452
|
-
|
|
534
|
+
/**
|
|
535
|
+
* Server-side tokens for calling Broberg ID on the user's behalf (F095.1).
|
|
536
|
+
*
|
|
537
|
+
* The login already RECEIVES an access token and a refresh token; before 0.9.0
|
|
538
|
+
* ssoRoutes dropped both, so an app could not call BID's app API (the account
|
|
539
|
+
* page: read and change your own name and picture) as the user.
|
|
540
|
+
*
|
|
541
|
+
* ── WHY NOT IN THE COOKIE ────────────────────────────────────────────────
|
|
542
|
+
* The session cookie is SIGNED, not encrypted, and the browser can read it. A
|
|
543
|
+
* refresh token is the longest-lived, highest-value credential in the whole
|
|
544
|
+
* flow: whoever holds it can mint access tokens until it is revoked. So the
|
|
545
|
+
* tokens live server-side, keyed by a random `sid` that is the only thing the
|
|
546
|
+
* cookie carries. A stolen cookie still needs the server to be useful.
|
|
547
|
+
*/
|
|
548
|
+
interface TokenSet {
|
|
549
|
+
/** The BID subject the tokens belong to — lets «Log ud overalt» find them. */
|
|
550
|
+
sub: string;
|
|
551
|
+
accessToken: string;
|
|
552
|
+
refreshToken?: string;
|
|
553
|
+
/** Unix seconds. */
|
|
554
|
+
expiresAt: number;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Where the tokens live. The APP provides it in production, backed by
|
|
558
|
+
* something every instance shares (SQLite on one machine, a database or Redis
|
|
559
|
+
* on several) — the same reason BackchannelStore has no in-memory default.
|
|
560
|
+
*/
|
|
561
|
+
interface TokenStore {
|
|
562
|
+
get(sid: string): Promise<TokenSet | undefined>;
|
|
563
|
+
set(sid: string, tokens: TokenSet): Promise<void>;
|
|
564
|
+
delete(sid: string): Promise<void>;
|
|
565
|
+
/** Drop every token set for `sub` — what «Log ud overalt» calls. */
|
|
566
|
+
deleteSub(sub: string): Promise<void>;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* An in-process TokenStore. Fine for tests and a single dev process. In
|
|
570
|
+
* production it LOSES every user's tokens on a restart (they must log in again
|
|
571
|
+
* before the account page works) and is not shared between machines — so it
|
|
572
|
+
* says so, once, when created.
|
|
573
|
+
*/
|
|
574
|
+
declare function memoryTokenStore(options?: {
|
|
575
|
+
quiet?: boolean;
|
|
576
|
+
}): TokenStore & {
|
|
577
|
+
readonly size: number;
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
export { AVATAR_TYPES, type AddressOwnership, type BeginLoginOptions, type BidProfile, type CreateSsoClientOptions, DEFAULT_SESSION_MAX_AGE, type Discovery, type InvitationOutcome, type InvitationResult, type InviteUsersInput, type JwksCache, type JwksCacheOptions, JwksError, JwksUnavailableError, JwksUnknownKeyError, type LoginResult, type LoginStart, type LogoutToken, MAX_AVATAR_BYTES, MAX_INVITATIONS, type MigrationState, type MigrationStatus, type SessionPayload, SsoAppApiError, SsoAvatarRejectedError, type SsoClaims, type SsoClient, type SsoConfig, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, type TokenSet, type TokenStore, cookieHeader, createJwksCache, createSsoClient, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue };
|
package/dist/index.d.ts
CHANGED
|
@@ -201,6 +201,55 @@ interface Discovery {
|
|
|
201
201
|
declare class SsoError extends Error {
|
|
202
202
|
constructor(message: string);
|
|
203
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* F095.2 — the user must log in again: there is no live session, no stored
|
|
206
|
+
* tokens, or the renewal was refused. `reason` says which; "no_session" means
|
|
207
|
+
* there never was a login to recover, the others mean there was one and it is
|
|
208
|
+
* spent. Thrown by getAccessToken.
|
|
209
|
+
*/
|
|
210
|
+
declare class SsoReauthError extends SsoError {
|
|
211
|
+
readonly reason: "no_session" | "no_tokens" | "expired" | "refresh_failed";
|
|
212
|
+
constructor(message: string, reason: "no_session" | "no_tokens" | "expired" | "refresh_failed");
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* F095.2 — BID's app API answered with an error, or did not answer at all.
|
|
216
|
+
* `status` is BID's HTTP status (null when nothing came back: timeout, network);
|
|
217
|
+
* `code` is BID's own `error` field when it sent one. The message carries the
|
|
218
|
+
* URL, the status and the code — never the body, never a token.
|
|
219
|
+
*/
|
|
220
|
+
declare class SsoAppApiError extends SsoError {
|
|
221
|
+
readonly status: number | null;
|
|
222
|
+
readonly code: string | undefined;
|
|
223
|
+
constructor(message: string, status: number | null, code: string | undefined);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* F095.2 — BID refused because the user's token lacks a scope (403
|
|
227
|
+
* insufficient_scope). For the profile writes that is `profile:write`: the app
|
|
228
|
+
* must request it at login, and a user who logged in before it did must log in
|
|
229
|
+
* again. That is the one actionable fact, so it gets its own class.
|
|
230
|
+
*/
|
|
231
|
+
declare class SsoInsufficientScopeError extends SsoAppApiError {
|
|
232
|
+
readonly scope: string;
|
|
233
|
+
constructor(message: string, scope: string);
|
|
234
|
+
}
|
|
235
|
+
/** F095.2 — an avatar refused BEFORE it was sent: too big, or not PNG/JPEG/WebP. */
|
|
236
|
+
declare class SsoAvatarRejectedError extends SsoError {
|
|
237
|
+
readonly reason: "too_large" | "unsupported_type";
|
|
238
|
+
constructor(message: string, reason: "too_large" | "unsupported_type");
|
|
239
|
+
}
|
|
240
|
+
/** BID's limit for a profile picture (llms.txt step 7a: "max 2 MB", read as 2 MiB). */
|
|
241
|
+
declare const MAX_AVATAR_BYTES: number;
|
|
242
|
+
/** The picture types BID accepts. */
|
|
243
|
+
declare const AVATAR_TYPES: readonly string[];
|
|
244
|
+
/** The signed-in user's own profile, as BID's GET /api/app/profile returns it. */
|
|
245
|
+
interface BidProfile {
|
|
246
|
+
sub: string;
|
|
247
|
+
name: string | null;
|
|
248
|
+
picture: string | null;
|
|
249
|
+
email: string | null;
|
|
250
|
+
/** BID's own account page — mail, password, passkeys, 2FA and sessions live there. */
|
|
251
|
+
account_url: string;
|
|
252
|
+
}
|
|
204
253
|
/**
|
|
205
254
|
* What `beginLogin` produces. Every field except `url` must survive the round
|
|
206
255
|
* trip to BID and back — the adapter stores them in a short-lived cookie.
|
|
@@ -239,6 +288,8 @@ interface LoginResult {
|
|
|
239
288
|
idToken: string;
|
|
240
289
|
accessToken?: string;
|
|
241
290
|
refreshToken?: string;
|
|
291
|
+
/** Seconds the access token lives, when BID said so (`expires_in`). */
|
|
292
|
+
expiresIn?: number;
|
|
242
293
|
}
|
|
243
294
|
/**
|
|
244
295
|
* `prompt=none` asks BID to answer WITHOUT showing anything (components-F084.6).
|
|
@@ -278,6 +329,16 @@ interface SsoClient {
|
|
|
278
329
|
* answers — see the implementation for why that must never become "unverified".
|
|
279
330
|
*/
|
|
280
331
|
addressOwnership(accessToken: string, address: string): Promise<AddressOwnership>;
|
|
332
|
+
/**
|
|
333
|
+
* F095.1 — trade a refresh token for a new access token (`grant_type=refresh_token`).
|
|
334
|
+
* Throws SsoError on refusal; the message carries the status and BID's error
|
|
335
|
+
* code, never a token.
|
|
336
|
+
*/
|
|
337
|
+
refreshTokens(refreshToken: string): Promise<{
|
|
338
|
+
accessToken: string;
|
|
339
|
+
refreshToken?: string;
|
|
340
|
+
expiresIn?: number;
|
|
341
|
+
}>;
|
|
281
342
|
/**
|
|
282
343
|
* Invite users to BID on behalf of this app (the app's OWN key, `bidk_…`).
|
|
283
344
|
* One result per email. Throws SsoError on anything BID did not clearly say.
|
|
@@ -294,6 +355,21 @@ interface SsoClient {
|
|
|
294
355
|
* Throws SsoError on anything that is not a valid, sub-only logout token.
|
|
295
356
|
*/
|
|
296
357
|
verifyLogoutToken(token: string): Promise<LogoutToken>;
|
|
358
|
+
/**
|
|
359
|
+
* F095.2 — the signed-in user's own profile (scope `profile`), with the user's
|
|
360
|
+
* access token. Throws SsoAppApiError on any refusal.
|
|
361
|
+
*/
|
|
362
|
+
getProfile(accessToken: string): Promise<BidProfile>;
|
|
363
|
+
/** F095.2 — change the user's own name (scope `profile:write`). Returns the new profile. */
|
|
364
|
+
updateProfile(accessToken: string, name: string): Promise<BidProfile>;
|
|
365
|
+
/**
|
|
366
|
+
* F095.2 — replace the user's picture (scope `profile:write`). Refuses more than
|
|
367
|
+
* MAX_AVATAR_BYTES or a type outside AVATAR_TYPES with SsoAvatarRejectedError
|
|
368
|
+
* BEFORE anything is sent.
|
|
369
|
+
*/
|
|
370
|
+
uploadAvatar(accessToken: string, bytes: Uint8Array | ArrayBuffer, contentType: string): Promise<BidProfile>;
|
|
371
|
+
/** F095.2 — remove the user's picture (scope `profile:write`). */
|
|
372
|
+
removeAvatar(accessToken: string): Promise<BidProfile>;
|
|
297
373
|
/** Exposed for tests and for a health check; not needed in normal use. */
|
|
298
374
|
readonly jwks: JwksCache;
|
|
299
375
|
}
|
|
@@ -371,6 +447,12 @@ interface SessionPayload {
|
|
|
371
447
|
* older than any logout.
|
|
372
448
|
*/
|
|
373
449
|
iat?: number;
|
|
450
|
+
/**
|
|
451
|
+
* F095.1 — the key to this session's server-side tokens (ssoRoutes'
|
|
452
|
+
* `tokenStore`). Random, 128 bits. The tokens themselves are NEVER in the
|
|
453
|
+
* cookie: it is signed, not encrypted, and the browser can read it.
|
|
454
|
+
*/
|
|
455
|
+
sid?: string;
|
|
374
456
|
/** Optional convenience copies; never authorisation data. */
|
|
375
457
|
email?: string;
|
|
376
458
|
name?: string;
|
|
@@ -449,4 +531,50 @@ declare function cookieHeader(name: string, value: string, opts: {
|
|
|
449
531
|
}): string;
|
|
450
532
|
declare function readCookie(header: string | null | undefined, name: string): string | undefined;
|
|
451
533
|
|
|
452
|
-
|
|
534
|
+
/**
|
|
535
|
+
* Server-side tokens for calling Broberg ID on the user's behalf (F095.1).
|
|
536
|
+
*
|
|
537
|
+
* The login already RECEIVES an access token and a refresh token; before 0.9.0
|
|
538
|
+
* ssoRoutes dropped both, so an app could not call BID's app API (the account
|
|
539
|
+
* page: read and change your own name and picture) as the user.
|
|
540
|
+
*
|
|
541
|
+
* ── WHY NOT IN THE COOKIE ────────────────────────────────────────────────
|
|
542
|
+
* The session cookie is SIGNED, not encrypted, and the browser can read it. A
|
|
543
|
+
* refresh token is the longest-lived, highest-value credential in the whole
|
|
544
|
+
* flow: whoever holds it can mint access tokens until it is revoked. So the
|
|
545
|
+
* tokens live server-side, keyed by a random `sid` that is the only thing the
|
|
546
|
+
* cookie carries. A stolen cookie still needs the server to be useful.
|
|
547
|
+
*/
|
|
548
|
+
interface TokenSet {
|
|
549
|
+
/** The BID subject the tokens belong to — lets «Log ud overalt» find them. */
|
|
550
|
+
sub: string;
|
|
551
|
+
accessToken: string;
|
|
552
|
+
refreshToken?: string;
|
|
553
|
+
/** Unix seconds. */
|
|
554
|
+
expiresAt: number;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Where the tokens live. The APP provides it in production, backed by
|
|
558
|
+
* something every instance shares (SQLite on one machine, a database or Redis
|
|
559
|
+
* on several) — the same reason BackchannelStore has no in-memory default.
|
|
560
|
+
*/
|
|
561
|
+
interface TokenStore {
|
|
562
|
+
get(sid: string): Promise<TokenSet | undefined>;
|
|
563
|
+
set(sid: string, tokens: TokenSet): Promise<void>;
|
|
564
|
+
delete(sid: string): Promise<void>;
|
|
565
|
+
/** Drop every token set for `sub` — what «Log ud overalt» calls. */
|
|
566
|
+
deleteSub(sub: string): Promise<void>;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* An in-process TokenStore. Fine for tests and a single dev process. In
|
|
570
|
+
* production it LOSES every user's tokens on a restart (they must log in again
|
|
571
|
+
* before the account page works) and is not shared between machines — so it
|
|
572
|
+
* says so, once, when created.
|
|
573
|
+
*/
|
|
574
|
+
declare function memoryTokenStore(options?: {
|
|
575
|
+
quiet?: boolean;
|
|
576
|
+
}): TokenStore & {
|
|
577
|
+
readonly size: number;
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
export { AVATAR_TYPES, type AddressOwnership, type BeginLoginOptions, type BidProfile, type CreateSsoClientOptions, DEFAULT_SESSION_MAX_AGE, type Discovery, type InvitationOutcome, type InvitationResult, type InviteUsersInput, type JwksCache, type JwksCacheOptions, JwksError, JwksUnavailableError, JwksUnknownKeyError, type LoginResult, type LoginStart, type LogoutToken, MAX_AVATAR_BYTES, MAX_INVITATIONS, type MigrationState, type MigrationStatus, type SessionPayload, SsoAppApiError, SsoAvatarRejectedError, type SsoClaims, type SsoClient, type SsoConfig, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, type TokenSet, type TokenStore, cookieHeader, createJwksCache, createSsoClient, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue };
|
package/dist/index.js
CHANGED
|
@@ -171,6 +171,45 @@ var SsoError = class extends Error {
|
|
|
171
171
|
this.name = "SsoError";
|
|
172
172
|
}
|
|
173
173
|
};
|
|
174
|
+
var SsoReauthError = class extends SsoError {
|
|
175
|
+
constructor(message, reason) {
|
|
176
|
+
super(message);
|
|
177
|
+
this.reason = reason;
|
|
178
|
+
this.name = "SsoReauthError";
|
|
179
|
+
}
|
|
180
|
+
reason;
|
|
181
|
+
};
|
|
182
|
+
var SsoAppApiError = class extends SsoError {
|
|
183
|
+
constructor(message, status, code) {
|
|
184
|
+
super(message);
|
|
185
|
+
this.status = status;
|
|
186
|
+
this.code = code;
|
|
187
|
+
this.name = "SsoAppApiError";
|
|
188
|
+
}
|
|
189
|
+
status;
|
|
190
|
+
code;
|
|
191
|
+
};
|
|
192
|
+
var SsoInsufficientScopeError = class extends SsoAppApiError {
|
|
193
|
+
constructor(message, scope) {
|
|
194
|
+
super(message, 403, "insufficient_scope");
|
|
195
|
+
this.scope = scope;
|
|
196
|
+
this.name = "SsoInsufficientScopeError";
|
|
197
|
+
}
|
|
198
|
+
scope;
|
|
199
|
+
};
|
|
200
|
+
var SsoAvatarRejectedError = class extends SsoError {
|
|
201
|
+
constructor(message, reason) {
|
|
202
|
+
super(message);
|
|
203
|
+
this.reason = reason;
|
|
204
|
+
this.name = "SsoAvatarRejectedError";
|
|
205
|
+
}
|
|
206
|
+
reason;
|
|
207
|
+
};
|
|
208
|
+
var MAX_AVATAR_BYTES = 2 * 1024 * 1024;
|
|
209
|
+
var AVATAR_TYPES = ["image/png", "image/jpeg", "image/webp"];
|
|
210
|
+
function mediaType(contentType) {
|
|
211
|
+
return (contentType ?? "").split(";")[0].trim().toLowerCase();
|
|
212
|
+
}
|
|
174
213
|
var b64url = (bytes) => {
|
|
175
214
|
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
176
215
|
let s = "";
|
|
@@ -262,6 +301,68 @@ function createSsoClient(config, options = {}) {
|
|
|
262
301
|
clearTimeout(timer);
|
|
263
302
|
}
|
|
264
303
|
}
|
|
304
|
+
async function appRequest(path, bearer, init, what) {
|
|
305
|
+
const url = new URL(path, config.issuer).toString();
|
|
306
|
+
const ctrl = new AbortController();
|
|
307
|
+
let timer;
|
|
308
|
+
const timedOut = new Promise((_, reject) => {
|
|
309
|
+
timer = setTimeout(() => {
|
|
310
|
+
reject(new SsoAppApiError(`${url} did not answer within ${timeoutMs} ms \u2014 ${what} is unknown.`, null, void 0));
|
|
311
|
+
ctrl.abort();
|
|
312
|
+
}, timeoutMs);
|
|
313
|
+
});
|
|
314
|
+
const headers = { authorization: `Bearer ${bearer}` };
|
|
315
|
+
let body;
|
|
316
|
+
if (init.json !== void 0) {
|
|
317
|
+
headers["content-type"] = "application/json";
|
|
318
|
+
body = JSON.stringify(init.json);
|
|
319
|
+
} else if (init.bytes !== void 0) {
|
|
320
|
+
headers["content-type"] = init.contentType ?? "application/octet-stream";
|
|
321
|
+
body = init.bytes;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const res = await Promise.race([
|
|
325
|
+
fetchImpl(url, { method: init.method, headers, ...body !== void 0 ? { body } : {}, signal: ctrl.signal }),
|
|
326
|
+
timedOut
|
|
327
|
+
]);
|
|
328
|
+
const raw = await Promise.race([res.text(), timedOut]);
|
|
329
|
+
let parsed;
|
|
330
|
+
try {
|
|
331
|
+
parsed = raw.trim() === "" ? void 0 : JSON.parse(raw);
|
|
332
|
+
} catch {
|
|
333
|
+
parsed = void 0;
|
|
334
|
+
}
|
|
335
|
+
const fields = parsed && typeof parsed === "object" ? parsed : {};
|
|
336
|
+
const code = typeof fields.error === "string" ? fields.error : void 0;
|
|
337
|
+
if (!res.ok) {
|
|
338
|
+
if (res.status === 403 && code === "insufficient_scope") {
|
|
339
|
+
const scope = typeof fields.scope === "string" && fields.scope ? fields.scope : init.method === "GET" ? "profile" : "profile:write";
|
|
340
|
+
throw new SsoInsufficientScopeError(
|
|
341
|
+
`${url} answered 403 insufficient_scope \u2014 the user's token lacks scope "${scope}"; log in again with it requested.`,
|
|
342
|
+
scope
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
throw new SsoAppApiError(`${url} answered ${res.status}${code ? ` (${code})` : ""} \u2014 ${what} is unknown.`, res.status, code);
|
|
346
|
+
}
|
|
347
|
+
if (parsed === void 0) {
|
|
348
|
+
throw new SsoAppApiError(`${url} answered ${res.status} with a body that is not JSON \u2014 ${what} is unknown.`, res.status, void 0);
|
|
349
|
+
}
|
|
350
|
+
return parsed;
|
|
351
|
+
} catch (e) {
|
|
352
|
+
if (e instanceof SsoError) throw e;
|
|
353
|
+
throw new SsoAppApiError(`${url} could not be reached (${e instanceof Error ? e.name : "error"}) \u2014 ${what} is unknown.`, null, void 0);
|
|
354
|
+
} finally {
|
|
355
|
+
clearTimeout(timer);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function asProfile(body, path) {
|
|
359
|
+
const b = body ?? {};
|
|
360
|
+
if (typeof b.sub !== "string" || b.sub === "" || typeof b.account_url !== "string") {
|
|
361
|
+
throw new SsoAppApiError(`${path} returned no "sub" or "account_url" \u2014 the profile is unknown.`, 200, void 0);
|
|
362
|
+
}
|
|
363
|
+
const opt = (v) => typeof v === "string" ? v : null;
|
|
364
|
+
return { sub: b.sub, name: opt(b.name), picture: opt(b.picture), email: opt(b.email), account_url: b.account_url };
|
|
365
|
+
}
|
|
265
366
|
let discoveryPromise = null;
|
|
266
367
|
let jwksCache = null;
|
|
267
368
|
async function discovery() {
|
|
@@ -445,7 +546,36 @@ function createSsoClient(config, options = {}) {
|
|
|
445
546
|
unverifiedClaims: merged.unverifiedClaims,
|
|
446
547
|
idToken: body.id_token,
|
|
447
548
|
...body.access_token ? { accessToken: body.access_token } : {},
|
|
448
|
-
...body.refresh_token ? { refreshToken: body.refresh_token } : {}
|
|
549
|
+
...body.refresh_token ? { refreshToken: body.refresh_token } : {},
|
|
550
|
+
...typeof body.expires_in === "number" ? { expiresIn: body.expires_in } : {}
|
|
551
|
+
};
|
|
552
|
+
},
|
|
553
|
+
async refreshTokens(refreshToken) {
|
|
554
|
+
const { token_endpoint } = await discovery();
|
|
555
|
+
const res = await fetchImpl(token_endpoint, {
|
|
556
|
+
method: "POST",
|
|
557
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
558
|
+
body: new URLSearchParams({
|
|
559
|
+
grant_type: "refresh_token",
|
|
560
|
+
refresh_token: refreshToken,
|
|
561
|
+
client_id: config.clientId,
|
|
562
|
+
...config.clientSecret ? { client_secret: config.clientSecret } : {}
|
|
563
|
+
})
|
|
564
|
+
});
|
|
565
|
+
const raw = await res.text();
|
|
566
|
+
let body;
|
|
567
|
+
try {
|
|
568
|
+
body = raw.trim() === "" ? {} : JSON.parse(raw);
|
|
569
|
+
} catch {
|
|
570
|
+
throw new SsoError(`token refresh failed (${res.status}): the response body is not JSON`);
|
|
571
|
+
}
|
|
572
|
+
if (!res.ok || typeof body.access_token !== "string" || body.access_token === "") {
|
|
573
|
+
throw new SsoError(`token refresh failed (${res.status}): ${body.error ?? "no access_token in response"}`);
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
accessToken: body.access_token,
|
|
577
|
+
...typeof body.refresh_token === "string" && body.refresh_token ? { refreshToken: body.refresh_token } : {},
|
|
578
|
+
...typeof body.expires_in === "number" ? { expiresIn: body.expires_in } : {}
|
|
449
579
|
};
|
|
450
580
|
},
|
|
451
581
|
async logoutUrl(opts = {}) {
|
|
@@ -476,6 +606,35 @@ function createSsoClient(config, options = {}) {
|
|
|
476
606
|
return status;
|
|
477
607
|
},
|
|
478
608
|
verifyLogoutToken,
|
|
609
|
+
async getProfile(accessToken) {
|
|
610
|
+
const path = "/api/app/profile";
|
|
611
|
+
return asProfile(await appRequest(path, accessToken, { method: "GET" }, "the profile"), path);
|
|
612
|
+
},
|
|
613
|
+
async updateProfile(accessToken, name) {
|
|
614
|
+
const path = "/api/app/profile";
|
|
615
|
+
return asProfile(await appRequest(path, accessToken, { method: "POST", json: { name } }, "the new name"), path);
|
|
616
|
+
},
|
|
617
|
+
async uploadAvatar(accessToken, bytes, contentType) {
|
|
618
|
+
const type = mediaType(contentType);
|
|
619
|
+
if (!AVATAR_TYPES.includes(type)) {
|
|
620
|
+
throw new SsoAvatarRejectedError(
|
|
621
|
+
`uploadAvatar: "${type}" is not accepted \u2014 BID takes ${AVATAR_TYPES.join(", ")}.`,
|
|
622
|
+
"unsupported_type"
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
if (bytes.byteLength > MAX_AVATAR_BYTES) {
|
|
626
|
+
throw new SsoAvatarRejectedError(
|
|
627
|
+
`uploadAvatar: ${bytes.byteLength} bytes \u2014 BID takes at most ${MAX_AVATAR_BYTES}.`,
|
|
628
|
+
"too_large"
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
const path = "/api/app/profile/avatar";
|
|
632
|
+
return asProfile(await appRequest(path, accessToken, { method: "POST", bytes, contentType: type }, "the new picture"), path);
|
|
633
|
+
},
|
|
634
|
+
async removeAvatar(accessToken) {
|
|
635
|
+
const path = "/api/app/profile/avatar/remove";
|
|
636
|
+
return asProfile(await appRequest(path, accessToken, { method: "POST" }, "the removed picture"), path);
|
|
637
|
+
},
|
|
479
638
|
async inviteUsers(appKey, input) {
|
|
480
639
|
if (input.users.length > MAX_INVITATIONS) {
|
|
481
640
|
throw new SsoError(`inviteUsers: ${input.users.length} users, BID accepts at most ${MAX_INVITATIONS} per call.`);
|
|
@@ -596,6 +755,35 @@ function readCookie(header, name) {
|
|
|
596
755
|
return void 0;
|
|
597
756
|
}
|
|
598
757
|
|
|
599
|
-
|
|
758
|
+
// src/tokens.ts
|
|
759
|
+
var warned = false;
|
|
760
|
+
function memoryTokenStore(options = {}) {
|
|
761
|
+
if (!options.quiet && !warned) {
|
|
762
|
+
warned = true;
|
|
763
|
+
console.warn(
|
|
764
|
+
"@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."
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
const rows = /* @__PURE__ */ new Map();
|
|
768
|
+
return {
|
|
769
|
+
get size() {
|
|
770
|
+
return rows.size;
|
|
771
|
+
},
|
|
772
|
+
async get(sid) {
|
|
773
|
+
return rows.get(sid);
|
|
774
|
+
},
|
|
775
|
+
async set(sid, tokens) {
|
|
776
|
+
rows.set(sid, { ...tokens });
|
|
777
|
+
},
|
|
778
|
+
async delete(sid) {
|
|
779
|
+
rows.delete(sid);
|
|
780
|
+
},
|
|
781
|
+
async deleteSub(sub) {
|
|
782
|
+
for (const [sid, t] of rows) if (t.sub === sub) rows.delete(sid);
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export { AVATAR_TYPES, DEFAULT_SESSION_MAX_AGE, JwksError, JwksUnavailableError, JwksUnknownKeyError, MAX_AVATAR_BYTES, MAX_INVITATIONS, SsoAppApiError, SsoAvatarRejectedError, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, cookieHeader, createJwksCache, createSsoClient, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue };
|
|
600
788
|
//# sourceMappingURL=index.js.map
|
|
601
789
|
//# sourceMappingURL=index.js.map
|