@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/hono.d.cts
CHANGED
|
@@ -139,6 +139,15 @@ interface Discovery {
|
|
|
139
139
|
userinfo_endpoint?: string;
|
|
140
140
|
end_session_endpoint?: string;
|
|
141
141
|
}
|
|
142
|
+
/** The signed-in user's own profile, as BID's GET /api/app/profile returns it. */
|
|
143
|
+
interface BidProfile {
|
|
144
|
+
sub: string;
|
|
145
|
+
name: string | null;
|
|
146
|
+
picture: string | null;
|
|
147
|
+
email: string | null;
|
|
148
|
+
/** BID's own account page — mail, password, passkeys, 2FA and sessions live there. */
|
|
149
|
+
account_url: string;
|
|
150
|
+
}
|
|
142
151
|
/**
|
|
143
152
|
* What `beginLogin` produces. Every field except `url` must survive the round
|
|
144
153
|
* trip to BID and back — the adapter stores them in a short-lived cookie.
|
|
@@ -177,6 +186,8 @@ interface LoginResult {
|
|
|
177
186
|
idToken: string;
|
|
178
187
|
accessToken?: string;
|
|
179
188
|
refreshToken?: string;
|
|
189
|
+
/** Seconds the access token lives, when BID said so (`expires_in`). */
|
|
190
|
+
expiresIn?: number;
|
|
180
191
|
}
|
|
181
192
|
/**
|
|
182
193
|
* `prompt=none` asks BID to answer WITHOUT showing anything (components-F084.6).
|
|
@@ -216,6 +227,16 @@ interface SsoClient {
|
|
|
216
227
|
* answers — see the implementation for why that must never become "unverified".
|
|
217
228
|
*/
|
|
218
229
|
addressOwnership(accessToken: string, address: string): Promise<AddressOwnership>;
|
|
230
|
+
/**
|
|
231
|
+
* F095.1 — trade a refresh token for a new access token (`grant_type=refresh_token`).
|
|
232
|
+
* Throws SsoError on refusal; the message carries the status and BID's error
|
|
233
|
+
* code, never a token.
|
|
234
|
+
*/
|
|
235
|
+
refreshTokens(refreshToken: string): Promise<{
|
|
236
|
+
accessToken: string;
|
|
237
|
+
refreshToken?: string;
|
|
238
|
+
expiresIn?: number;
|
|
239
|
+
}>;
|
|
219
240
|
/**
|
|
220
241
|
* Invite users to BID on behalf of this app (the app's OWN key, `bidk_…`).
|
|
221
242
|
* One result per email. Throws SsoError on anything BID did not clearly say.
|
|
@@ -232,6 +253,21 @@ interface SsoClient {
|
|
|
232
253
|
* Throws SsoError on anything that is not a valid, sub-only logout token.
|
|
233
254
|
*/
|
|
234
255
|
verifyLogoutToken(token: string): Promise<LogoutToken>;
|
|
256
|
+
/**
|
|
257
|
+
* F095.2 — the signed-in user's own profile (scope `profile`), with the user's
|
|
258
|
+
* access token. Throws SsoAppApiError on any refusal.
|
|
259
|
+
*/
|
|
260
|
+
getProfile(accessToken: string): Promise<BidProfile>;
|
|
261
|
+
/** F095.2 — change the user's own name (scope `profile:write`). Returns the new profile. */
|
|
262
|
+
updateProfile(accessToken: string, name: string): Promise<BidProfile>;
|
|
263
|
+
/**
|
|
264
|
+
* F095.2 — replace the user's picture (scope `profile:write`). Refuses more than
|
|
265
|
+
* MAX_AVATAR_BYTES or a type outside AVATAR_TYPES with SsoAvatarRejectedError
|
|
266
|
+
* BEFORE anything is sent.
|
|
267
|
+
*/
|
|
268
|
+
uploadAvatar(accessToken: string, bytes: Uint8Array | ArrayBuffer, contentType: string): Promise<BidProfile>;
|
|
269
|
+
/** F095.2 — remove the user's picture (scope `profile:write`). */
|
|
270
|
+
removeAvatar(accessToken: string): Promise<BidProfile>;
|
|
235
271
|
/** Exposed for tests and for a health check; not needed in normal use. */
|
|
236
272
|
readonly jwks: JwksCache;
|
|
237
273
|
}
|
|
@@ -271,6 +307,41 @@ interface LogoutToken {
|
|
|
271
307
|
exp: number;
|
|
272
308
|
}
|
|
273
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Server-side tokens for calling Broberg ID on the user's behalf (F095.1).
|
|
312
|
+
*
|
|
313
|
+
* The login already RECEIVES an access token and a refresh token; before 0.9.0
|
|
314
|
+
* ssoRoutes dropped both, so an app could not call BID's app API (the account
|
|
315
|
+
* page: read and change your own name and picture) as the user.
|
|
316
|
+
*
|
|
317
|
+
* ── WHY NOT IN THE COOKIE ────────────────────────────────────────────────
|
|
318
|
+
* The session cookie is SIGNED, not encrypted, and the browser can read it. A
|
|
319
|
+
* refresh token is the longest-lived, highest-value credential in the whole
|
|
320
|
+
* flow: whoever holds it can mint access tokens until it is revoked. So the
|
|
321
|
+
* tokens live server-side, keyed by a random `sid` that is the only thing the
|
|
322
|
+
* cookie carries. A stolen cookie still needs the server to be useful.
|
|
323
|
+
*/
|
|
324
|
+
interface TokenSet {
|
|
325
|
+
/** The BID subject the tokens belong to — lets «Log ud overalt» find them. */
|
|
326
|
+
sub: string;
|
|
327
|
+
accessToken: string;
|
|
328
|
+
refreshToken?: string;
|
|
329
|
+
/** Unix seconds. */
|
|
330
|
+
expiresAt: number;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Where the tokens live. The APP provides it in production, backed by
|
|
334
|
+
* something every instance shares (SQLite on one machine, a database or Redis
|
|
335
|
+
* on several) — the same reason BackchannelStore has no in-memory default.
|
|
336
|
+
*/
|
|
337
|
+
interface TokenStore {
|
|
338
|
+
get(sid: string): Promise<TokenSet | undefined>;
|
|
339
|
+
set(sid: string, tokens: TokenSet): Promise<void>;
|
|
340
|
+
delete(sid: string): Promise<void>;
|
|
341
|
+
/** Drop every token set for `sub` — what «Log ud overalt» calls. */
|
|
342
|
+
deleteSub(sub: string): Promise<void>;
|
|
343
|
+
}
|
|
344
|
+
|
|
274
345
|
/**
|
|
275
346
|
* The app's OWN session cookie — signed, not encrypted.
|
|
276
347
|
*
|
|
@@ -296,6 +367,12 @@ interface SessionPayload {
|
|
|
296
367
|
* older than any logout.
|
|
297
368
|
*/
|
|
298
369
|
iat?: number;
|
|
370
|
+
/**
|
|
371
|
+
* F095.1 — the key to this session's server-side tokens (ssoRoutes'
|
|
372
|
+
* `tokenStore`). Random, 128 bits. The tokens themselves are NEVER in the
|
|
373
|
+
* cookie: it is signed, not encrypted, and the browser can read it.
|
|
374
|
+
*/
|
|
375
|
+
sid?: string;
|
|
299
376
|
/** Optional convenience copies; never authorisation data. */
|
|
300
377
|
email?: string;
|
|
301
378
|
name?: string;
|
|
@@ -356,6 +433,12 @@ interface SsoRoutesOptions {
|
|
|
356
433
|
loginPath?: string;
|
|
357
434
|
/** Where to send a user after a successful login when no returnTo is given. */
|
|
358
435
|
defaultReturnTo?: string;
|
|
436
|
+
/**
|
|
437
|
+
* F095.1 — keep the user's access and refresh token SERVER-SIDE, so the app
|
|
438
|
+
* can call BID's app API (the account page) as the user via getAccessToken.
|
|
439
|
+
* Without it, login behaves exactly as in 0.8.0 and the tokens are dropped.
|
|
440
|
+
*/
|
|
441
|
+
tokenStore?: TokenStore;
|
|
359
442
|
}
|
|
360
443
|
declare function ssoRoutes(options?: SsoRoutesOptions): {
|
|
361
444
|
app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
|
|
@@ -363,10 +446,34 @@ declare function ssoRoutes(options?: SsoRoutesOptions): {
|
|
|
363
446
|
require: MiddlewareHandler;
|
|
364
447
|
client: SsoClient;
|
|
365
448
|
config: SsoConfig;
|
|
449
|
+
getAccessToken: (c: Context) => Promise<string>;
|
|
366
450
|
};
|
|
367
451
|
/** Why a login transaction could not be read back. Three states, three answers. */
|
|
368
452
|
type TransactionFailure = "absent" | "expired" | "unreadable";
|
|
369
453
|
/** Read the session a middleware attached. */
|
|
370
454
|
declare function getSession(c: Context): SessionPayload | null;
|
|
455
|
+
/** What accountRoutes needs from ssoRoutes — pass its return value as is. */
|
|
456
|
+
interface AccountRoutesInput {
|
|
457
|
+
client: Pick<SsoClient, "getProfile" | "updateProfile" | "uploadAvatar" | "removeAvatar">;
|
|
458
|
+
getAccessToken(c: Context): Promise<string>;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* The signed-in user's own name and picture, for the app's account page.
|
|
462
|
+
* Mount it on /api/account:
|
|
463
|
+
*
|
|
464
|
+
* const sso = ssoRoutes({ tokenStore });
|
|
465
|
+
* app.route("/api/account", accountRoutes(sso));
|
|
466
|
+
*
|
|
467
|
+
* GET /profile · POST /profile {name} · POST /profile/avatar (raw image body) ·
|
|
468
|
+
* POST /profile/avatar/remove. Every answer is JSON, refusals included — it is
|
|
469
|
+
* an API, so never a login redirect:
|
|
470
|
+
* 401 {error:"unauthenticated"} no live session
|
|
471
|
+
* 401 {error:"reauth"} the session's tokens are spent — log in again
|
|
472
|
+
* 403 {error:"insufficient_scope", scope} log in again with that scope requested
|
|
473
|
+
* 413 / 415 picture too large / not PNG, JPEG or WebP
|
|
474
|
+
* 4xx {error:<BID's code>} BID refused the change (e.g. an invalid name)
|
|
475
|
+
* 502 {error:"bid_unavailable"} BID did not answer usably
|
|
476
|
+
*/
|
|
477
|
+
declare function accountRoutes(sso: AccountRoutesInput): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
|
|
371
478
|
|
|
372
|
-
export { type BackchannelOptions, type BackchannelStore, type SsoRoutesOptions, type TransactionFailure, getSession, ssoRoutes };
|
|
479
|
+
export { type AccountRoutesInput, type BackchannelOptions, type BackchannelStore, type SsoRoutesOptions, type TransactionFailure, accountRoutes, getSession, ssoRoutes };
|
package/dist/hono.d.ts
CHANGED
|
@@ -139,6 +139,15 @@ interface Discovery {
|
|
|
139
139
|
userinfo_endpoint?: string;
|
|
140
140
|
end_session_endpoint?: string;
|
|
141
141
|
}
|
|
142
|
+
/** The signed-in user's own profile, as BID's GET /api/app/profile returns it. */
|
|
143
|
+
interface BidProfile {
|
|
144
|
+
sub: string;
|
|
145
|
+
name: string | null;
|
|
146
|
+
picture: string | null;
|
|
147
|
+
email: string | null;
|
|
148
|
+
/** BID's own account page — mail, password, passkeys, 2FA and sessions live there. */
|
|
149
|
+
account_url: string;
|
|
150
|
+
}
|
|
142
151
|
/**
|
|
143
152
|
* What `beginLogin` produces. Every field except `url` must survive the round
|
|
144
153
|
* trip to BID and back — the adapter stores them in a short-lived cookie.
|
|
@@ -177,6 +186,8 @@ interface LoginResult {
|
|
|
177
186
|
idToken: string;
|
|
178
187
|
accessToken?: string;
|
|
179
188
|
refreshToken?: string;
|
|
189
|
+
/** Seconds the access token lives, when BID said so (`expires_in`). */
|
|
190
|
+
expiresIn?: number;
|
|
180
191
|
}
|
|
181
192
|
/**
|
|
182
193
|
* `prompt=none` asks BID to answer WITHOUT showing anything (components-F084.6).
|
|
@@ -216,6 +227,16 @@ interface SsoClient {
|
|
|
216
227
|
* answers — see the implementation for why that must never become "unverified".
|
|
217
228
|
*/
|
|
218
229
|
addressOwnership(accessToken: string, address: string): Promise<AddressOwnership>;
|
|
230
|
+
/**
|
|
231
|
+
* F095.1 — trade a refresh token for a new access token (`grant_type=refresh_token`).
|
|
232
|
+
* Throws SsoError on refusal; the message carries the status and BID's error
|
|
233
|
+
* code, never a token.
|
|
234
|
+
*/
|
|
235
|
+
refreshTokens(refreshToken: string): Promise<{
|
|
236
|
+
accessToken: string;
|
|
237
|
+
refreshToken?: string;
|
|
238
|
+
expiresIn?: number;
|
|
239
|
+
}>;
|
|
219
240
|
/**
|
|
220
241
|
* Invite users to BID on behalf of this app (the app's OWN key, `bidk_…`).
|
|
221
242
|
* One result per email. Throws SsoError on anything BID did not clearly say.
|
|
@@ -232,6 +253,21 @@ interface SsoClient {
|
|
|
232
253
|
* Throws SsoError on anything that is not a valid, sub-only logout token.
|
|
233
254
|
*/
|
|
234
255
|
verifyLogoutToken(token: string): Promise<LogoutToken>;
|
|
256
|
+
/**
|
|
257
|
+
* F095.2 — the signed-in user's own profile (scope `profile`), with the user's
|
|
258
|
+
* access token. Throws SsoAppApiError on any refusal.
|
|
259
|
+
*/
|
|
260
|
+
getProfile(accessToken: string): Promise<BidProfile>;
|
|
261
|
+
/** F095.2 — change the user's own name (scope `profile:write`). Returns the new profile. */
|
|
262
|
+
updateProfile(accessToken: string, name: string): Promise<BidProfile>;
|
|
263
|
+
/**
|
|
264
|
+
* F095.2 — replace the user's picture (scope `profile:write`). Refuses more than
|
|
265
|
+
* MAX_AVATAR_BYTES or a type outside AVATAR_TYPES with SsoAvatarRejectedError
|
|
266
|
+
* BEFORE anything is sent.
|
|
267
|
+
*/
|
|
268
|
+
uploadAvatar(accessToken: string, bytes: Uint8Array | ArrayBuffer, contentType: string): Promise<BidProfile>;
|
|
269
|
+
/** F095.2 — remove the user's picture (scope `profile:write`). */
|
|
270
|
+
removeAvatar(accessToken: string): Promise<BidProfile>;
|
|
235
271
|
/** Exposed for tests and for a health check; not needed in normal use. */
|
|
236
272
|
readonly jwks: JwksCache;
|
|
237
273
|
}
|
|
@@ -271,6 +307,41 @@ interface LogoutToken {
|
|
|
271
307
|
exp: number;
|
|
272
308
|
}
|
|
273
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Server-side tokens for calling Broberg ID on the user's behalf (F095.1).
|
|
312
|
+
*
|
|
313
|
+
* The login already RECEIVES an access token and a refresh token; before 0.9.0
|
|
314
|
+
* ssoRoutes dropped both, so an app could not call BID's app API (the account
|
|
315
|
+
* page: read and change your own name and picture) as the user.
|
|
316
|
+
*
|
|
317
|
+
* ── WHY NOT IN THE COOKIE ────────────────────────────────────────────────
|
|
318
|
+
* The session cookie is SIGNED, not encrypted, and the browser can read it. A
|
|
319
|
+
* refresh token is the longest-lived, highest-value credential in the whole
|
|
320
|
+
* flow: whoever holds it can mint access tokens until it is revoked. So the
|
|
321
|
+
* tokens live server-side, keyed by a random `sid` that is the only thing the
|
|
322
|
+
* cookie carries. A stolen cookie still needs the server to be useful.
|
|
323
|
+
*/
|
|
324
|
+
interface TokenSet {
|
|
325
|
+
/** The BID subject the tokens belong to — lets «Log ud overalt» find them. */
|
|
326
|
+
sub: string;
|
|
327
|
+
accessToken: string;
|
|
328
|
+
refreshToken?: string;
|
|
329
|
+
/** Unix seconds. */
|
|
330
|
+
expiresAt: number;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Where the tokens live. The APP provides it in production, backed by
|
|
334
|
+
* something every instance shares (SQLite on one machine, a database or Redis
|
|
335
|
+
* on several) — the same reason BackchannelStore has no in-memory default.
|
|
336
|
+
*/
|
|
337
|
+
interface TokenStore {
|
|
338
|
+
get(sid: string): Promise<TokenSet | undefined>;
|
|
339
|
+
set(sid: string, tokens: TokenSet): Promise<void>;
|
|
340
|
+
delete(sid: string): Promise<void>;
|
|
341
|
+
/** Drop every token set for `sub` — what «Log ud overalt» calls. */
|
|
342
|
+
deleteSub(sub: string): Promise<void>;
|
|
343
|
+
}
|
|
344
|
+
|
|
274
345
|
/**
|
|
275
346
|
* The app's OWN session cookie — signed, not encrypted.
|
|
276
347
|
*
|
|
@@ -296,6 +367,12 @@ interface SessionPayload {
|
|
|
296
367
|
* older than any logout.
|
|
297
368
|
*/
|
|
298
369
|
iat?: number;
|
|
370
|
+
/**
|
|
371
|
+
* F095.1 — the key to this session's server-side tokens (ssoRoutes'
|
|
372
|
+
* `tokenStore`). Random, 128 bits. The tokens themselves are NEVER in the
|
|
373
|
+
* cookie: it is signed, not encrypted, and the browser can read it.
|
|
374
|
+
*/
|
|
375
|
+
sid?: string;
|
|
299
376
|
/** Optional convenience copies; never authorisation data. */
|
|
300
377
|
email?: string;
|
|
301
378
|
name?: string;
|
|
@@ -356,6 +433,12 @@ interface SsoRoutesOptions {
|
|
|
356
433
|
loginPath?: string;
|
|
357
434
|
/** Where to send a user after a successful login when no returnTo is given. */
|
|
358
435
|
defaultReturnTo?: string;
|
|
436
|
+
/**
|
|
437
|
+
* F095.1 — keep the user's access and refresh token SERVER-SIDE, so the app
|
|
438
|
+
* can call BID's app API (the account page) as the user via getAccessToken.
|
|
439
|
+
* Without it, login behaves exactly as in 0.8.0 and the tokens are dropped.
|
|
440
|
+
*/
|
|
441
|
+
tokenStore?: TokenStore;
|
|
359
442
|
}
|
|
360
443
|
declare function ssoRoutes(options?: SsoRoutesOptions): {
|
|
361
444
|
app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
|
|
@@ -363,10 +446,34 @@ declare function ssoRoutes(options?: SsoRoutesOptions): {
|
|
|
363
446
|
require: MiddlewareHandler;
|
|
364
447
|
client: SsoClient;
|
|
365
448
|
config: SsoConfig;
|
|
449
|
+
getAccessToken: (c: Context) => Promise<string>;
|
|
366
450
|
};
|
|
367
451
|
/** Why a login transaction could not be read back. Three states, three answers. */
|
|
368
452
|
type TransactionFailure = "absent" | "expired" | "unreadable";
|
|
369
453
|
/** Read the session a middleware attached. */
|
|
370
454
|
declare function getSession(c: Context): SessionPayload | null;
|
|
455
|
+
/** What accountRoutes needs from ssoRoutes — pass its return value as is. */
|
|
456
|
+
interface AccountRoutesInput {
|
|
457
|
+
client: Pick<SsoClient, "getProfile" | "updateProfile" | "uploadAvatar" | "removeAvatar">;
|
|
458
|
+
getAccessToken(c: Context): Promise<string>;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* The signed-in user's own name and picture, for the app's account page.
|
|
462
|
+
* Mount it on /api/account:
|
|
463
|
+
*
|
|
464
|
+
* const sso = ssoRoutes({ tokenStore });
|
|
465
|
+
* app.route("/api/account", accountRoutes(sso));
|
|
466
|
+
*
|
|
467
|
+
* GET /profile · POST /profile {name} · POST /profile/avatar (raw image body) ·
|
|
468
|
+
* POST /profile/avatar/remove. Every answer is JSON, refusals included — it is
|
|
469
|
+
* an API, so never a login redirect:
|
|
470
|
+
* 401 {error:"unauthenticated"} no live session
|
|
471
|
+
* 401 {error:"reauth"} the session's tokens are spent — log in again
|
|
472
|
+
* 403 {error:"insufficient_scope", scope} log in again with that scope requested
|
|
473
|
+
* 413 / 415 picture too large / not PNG, JPEG or WebP
|
|
474
|
+
* 4xx {error:<BID's code>} BID refused the change (e.g. an invalid name)
|
|
475
|
+
* 502 {error:"bid_unavailable"} BID did not answer usably
|
|
476
|
+
*/
|
|
477
|
+
declare function accountRoutes(sso: AccountRoutesInput): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
|
|
371
478
|
|
|
372
|
-
export { type BackchannelOptions, type BackchannelStore, type SsoRoutesOptions, type TransactionFailure, getSession, ssoRoutes };
|
|
479
|
+
export { type AccountRoutesInput, type BackchannelOptions, type BackchannelStore, type SsoRoutesOptions, type TransactionFailure, accountRoutes, getSession, ssoRoutes };
|