@broberg/sso 0.20.0 → 0.21.1
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 +23 -0
- package/dist/{chunk-ZDNBHM2G.js → chunk-GK3QCKXB.js} +290 -7
- package/dist/chunk-GK3QCKXB.js.map +1 -0
- package/dist/{chunk-KEBSZBHL.cjs → chunk-XQEHSHLR.cjs} +302 -5
- package/dist/chunk-XQEHSHLR.cjs.map +1 -0
- package/dist/hono.cjs +38 -37
- package/dist/hono.cjs.map +1 -1
- package/dist/hono.d.cts +7 -1
- package/dist/hono.d.ts +7 -1
- package/dist/hono.js +3 -2
- package/dist/hono.js.map +1 -1
- package/dist/index.cjs +112 -325
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -260
- package/dist/index.js.map +1 -1
- package/dist/{session-AgUJ6U_7.d.cts → session-DnotfVNd.d.cts} +17 -1
- package/dist/{session-AgUJ6U_7.d.ts → session-DnotfVNd.d.ts} +17 -1
- package/package.json +1 -1
- package/dist/chunk-KEBSZBHL.cjs.map +0 -1
- package/dist/chunk-ZDNBHM2G.js.map +0 -1
package/dist/hono.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkXQEHSHLR_cjs = require('./chunk-XQEHSHLR.cjs');
|
|
4
4
|
var hono = require('hono');
|
|
5
5
|
|
|
6
6
|
var SESSION_KEY = "bidSession";
|
|
@@ -31,8 +31,9 @@ function safeReturnTo(raw, fallback) {
|
|
|
31
31
|
return normalised;
|
|
32
32
|
}
|
|
33
33
|
function ssoRoutes(options = {}) {
|
|
34
|
-
const
|
|
35
|
-
const
|
|
34
|
+
const baseConfig = options.config ?? chunkXQEHSHLR_cjs.loadSsoConfig();
|
|
35
|
+
const config = options.clientAuth ? { ...baseConfig, clientAuth: options.clientAuth } : baseConfig;
|
|
36
|
+
const client = options.client ?? chunkXQEHSHLR_cjs.createSsoClient(config);
|
|
36
37
|
const loginPath = options.loginPath ?? "/auth";
|
|
37
38
|
const defaultReturnTo = options.defaultReturnTo ?? "/";
|
|
38
39
|
const txCookie = `${config.cookieName}_tx`;
|
|
@@ -46,7 +47,7 @@ function ssoRoutes(options = {}) {
|
|
|
46
47
|
const callbackErrors = options.callbackErrors ?? "single";
|
|
47
48
|
const onCallbackRefused = options.onCallbackRefused ?? ((cause) => console.warn(`[@broberg/sso] /callback refused: ${cause}`));
|
|
48
49
|
async function liveSession(c) {
|
|
49
|
-
const session = await
|
|
50
|
+
const session = await chunkXQEHSHLR_cjs.verifySession(chunkXQEHSHLR_cjs.readCookie(c.req.header("cookie"), config.cookieName), config.cookieSecret);
|
|
50
51
|
if (!session || !backchannel) return session;
|
|
51
52
|
try {
|
|
52
53
|
const before = await backchannel.store.revokedBefore(session.sub);
|
|
@@ -72,7 +73,7 @@ function ssoRoutes(options = {}) {
|
|
|
72
73
|
try {
|
|
73
74
|
verified = await client.verifyLogoutToken(token);
|
|
74
75
|
} catch (e) {
|
|
75
|
-
const msg = e instanceof
|
|
76
|
+
const msg = e instanceof chunkXQEHSHLR_cjs.SsoError ? e.message : "logout_token could not be verified";
|
|
76
77
|
return c.json({ error: "invalid_request", error_description: msg }, 400);
|
|
77
78
|
}
|
|
78
79
|
try {
|
|
@@ -101,7 +102,7 @@ function ssoRoutes(options = {}) {
|
|
|
101
102
|
// The signed stamp carries the SERVER's window; the cookie carries the
|
|
102
103
|
// longer browser one, so an expired transaction still reaches us and can
|
|
103
104
|
// be named instead of vanishing.
|
|
104
|
-
|
|
105
|
+
chunkXQEHSHLR_cjs.cookieHeader(txCookie, await chunkXQEHSHLR_cjs.signValue(tx, config.cookieSecret, { maxAgeSeconds: TRANSACTION_MAX_AGE }), {
|
|
105
106
|
maxAge: TRANSACTION_COOKIE_MAX_AGE,
|
|
106
107
|
secure: isSecure(c)
|
|
107
108
|
})
|
|
@@ -110,7 +111,7 @@ function ssoRoutes(options = {}) {
|
|
|
110
111
|
});
|
|
111
112
|
app.get("/callback", async (c) => {
|
|
112
113
|
const parsed = await parseTransaction(
|
|
113
|
-
|
|
114
|
+
chunkXQEHSHLR_cjs.readCookie(c.req.header("cookie"), txCookie),
|
|
114
115
|
config.cookieSecret
|
|
115
116
|
);
|
|
116
117
|
if (!parsed.ok) {
|
|
@@ -142,7 +143,7 @@ function ssoRoutes(options = {}) {
|
|
|
142
143
|
message: "This login could not be completed. Begin again from the login page."
|
|
143
144
|
} : failures[parsed.reason];
|
|
144
145
|
const { status, ...body } = shown;
|
|
145
|
-
c.header("Set-Cookie",
|
|
146
|
+
c.header("Set-Cookie", chunkXQEHSHLR_cjs.cookieHeader(txCookie, "", { maxAge: 0, secure: isSecure(c) }));
|
|
146
147
|
return c.json(body, status);
|
|
147
148
|
}
|
|
148
149
|
const result = await client.completeLogin({
|
|
@@ -155,12 +156,12 @@ function ssoRoutes(options = {}) {
|
|
|
155
156
|
const exp = nowS + config.sessionMaxAge;
|
|
156
157
|
let sid;
|
|
157
158
|
if (tokenStore && result.accessToken) {
|
|
158
|
-
sid =
|
|
159
|
+
sid = chunkXQEHSHLR_cjs.newSid();
|
|
159
160
|
await tokenStore.set(sid, {
|
|
160
161
|
sub: result.claims.sub,
|
|
161
162
|
accessToken: result.accessToken,
|
|
162
163
|
...result.refreshToken ? { refreshToken: result.refreshToken } : {},
|
|
163
|
-
expiresAt:
|
|
164
|
+
expiresAt: chunkXQEHSHLR_cjs.expiresAtFrom(result.expiresIn, result.accessToken, nowS)
|
|
164
165
|
});
|
|
165
166
|
}
|
|
166
167
|
const session = {
|
|
@@ -176,14 +177,14 @@ function ssoRoutes(options = {}) {
|
|
|
176
177
|
};
|
|
177
178
|
c.header(
|
|
178
179
|
"Set-Cookie",
|
|
179
|
-
|
|
180
|
+
chunkXQEHSHLR_cjs.cookieHeader(config.cookieName, await chunkXQEHSHLR_cjs.signSession(session, config.cookieSecret), {
|
|
180
181
|
maxAge: config.sessionMaxAge,
|
|
181
182
|
secure: isSecure(c)
|
|
182
183
|
})
|
|
183
184
|
);
|
|
184
185
|
c.header(
|
|
185
186
|
"Set-Cookie",
|
|
186
|
-
|
|
187
|
+
chunkXQEHSHLR_cjs.cookieHeader(idTokenCookie, await chunkXQEHSHLR_cjs.signValue(result.idToken, config.cookieSecret), {
|
|
187
188
|
maxAge: config.sessionMaxAge,
|
|
188
189
|
secure: isSecure(c)
|
|
189
190
|
}),
|
|
@@ -191,27 +192,27 @@ function ssoRoutes(options = {}) {
|
|
|
191
192
|
);
|
|
192
193
|
c.header(
|
|
193
194
|
"Set-Cookie",
|
|
194
|
-
|
|
195
|
+
chunkXQEHSHLR_cjs.cookieHeader(txCookie, "", { maxAge: 0, secure: isSecure(c) }),
|
|
195
196
|
{ append: true }
|
|
196
197
|
);
|
|
197
198
|
return c.redirect(parsed.tx.returnTo, 302);
|
|
198
199
|
});
|
|
199
200
|
app.get("/logout", async (c) => {
|
|
200
|
-
const idTokenHint = await
|
|
201
|
-
|
|
201
|
+
const idTokenHint = await chunkXQEHSHLR_cjs.verifyValue(
|
|
202
|
+
chunkXQEHSHLR_cjs.readCookie(c.req.header("cookie"), idTokenCookie),
|
|
202
203
|
config.cookieSecret
|
|
203
204
|
);
|
|
204
205
|
if (tokenStore) {
|
|
205
|
-
const leaving = await
|
|
206
|
+
const leaving = await chunkXQEHSHLR_cjs.verifySession(chunkXQEHSHLR_cjs.readCookie(c.req.header("cookie"), config.cookieName), config.cookieSecret);
|
|
206
207
|
if (leaving?.sid) await tokenStore.delete(leaving.sid);
|
|
207
208
|
}
|
|
208
209
|
c.header(
|
|
209
210
|
"Set-Cookie",
|
|
210
|
-
|
|
211
|
+
chunkXQEHSHLR_cjs.cookieHeader(config.cookieName, "", { maxAge: 0, secure: isSecure(c) })
|
|
211
212
|
);
|
|
212
213
|
c.header(
|
|
213
214
|
"Set-Cookie",
|
|
214
|
-
|
|
215
|
+
chunkXQEHSHLR_cjs.cookieHeader(idTokenCookie, "", { maxAge: 0, secure: isSecure(c) }),
|
|
215
216
|
{ append: true }
|
|
216
217
|
);
|
|
217
218
|
if ((options.logout ?? "app") === "app") {
|
|
@@ -238,23 +239,23 @@ function ssoRoutes(options = {}) {
|
|
|
238
239
|
const refreshing = /* @__PURE__ */ new Map();
|
|
239
240
|
async function getAccessToken(c) {
|
|
240
241
|
if (!tokenStore) {
|
|
241
|
-
throw new
|
|
242
|
+
throw new chunkXQEHSHLR_cjs.SsoError("getAccessToken needs ssoRoutes({ tokenStore }) \u2014 without one the login's tokens are not kept.");
|
|
242
243
|
}
|
|
243
|
-
const raw = await
|
|
244
|
+
const raw = await chunkXQEHSHLR_cjs.verifySession(chunkXQEHSHLR_cjs.readCookie(c.req.header("cookie"), config.cookieName), config.cookieSecret);
|
|
244
245
|
const live = await liveSession(c);
|
|
245
246
|
if (!live) {
|
|
246
247
|
if (raw?.sid) await tokenStore.delete(raw.sid);
|
|
247
|
-
throw new
|
|
248
|
+
throw new chunkXQEHSHLR_cjs.SsoReauthError("no live session \u2014 log in again.", "no_session");
|
|
248
249
|
}
|
|
249
250
|
const sid = live.sid;
|
|
250
|
-
if (!sid) throw new
|
|
251
|
+
if (!sid) throw new chunkXQEHSHLR_cjs.SsoReauthError("this session has no stored tokens (it began before tokenStore was configured) \u2014 log in again.", "no_tokens");
|
|
251
252
|
const tokens = await tokenStore.get(sid);
|
|
252
|
-
if (!tokens) throw new
|
|
253
|
+
if (!tokens) throw new chunkXQEHSHLR_cjs.SsoReauthError("no tokens stored for this session \u2014 log in again.", "no_tokens");
|
|
253
254
|
const now = Math.floor(Date.now() / 1e3);
|
|
254
255
|
if (tokens.expiresAt - 30 > now) return tokens.accessToken;
|
|
255
256
|
if (!tokens.refreshToken) {
|
|
256
257
|
await tokenStore.delete(sid);
|
|
257
|
-
throw new
|
|
258
|
+
throw new chunkXQEHSHLR_cjs.SsoReauthError("the access token has expired and there is no refresh token \u2014 log in again.", "expired");
|
|
258
259
|
}
|
|
259
260
|
const inFlight = refreshing.get(sid);
|
|
260
261
|
if (inFlight) return inFlight;
|
|
@@ -264,14 +265,14 @@ function ssoRoutes(options = {}) {
|
|
|
264
265
|
next = await client.refreshTokens(tokens.refreshToken);
|
|
265
266
|
} catch (e) {
|
|
266
267
|
await tokenStore.delete(sid);
|
|
267
|
-
throw new
|
|
268
|
+
throw new chunkXQEHSHLR_cjs.SsoReauthError(e instanceof chunkXQEHSHLR_cjs.SsoError ? e.message : "token refresh failed", "refresh_failed");
|
|
268
269
|
}
|
|
269
270
|
await tokenStore.set(sid, {
|
|
270
271
|
sub: tokens.sub,
|
|
271
272
|
accessToken: next.accessToken,
|
|
272
273
|
// Rotation: use the new one if BID issued one, else keep the old.
|
|
273
274
|
...next.refreshToken ?? tokens.refreshToken ? { refreshToken: next.refreshToken ?? tokens.refreshToken } : {},
|
|
274
|
-
expiresAt:
|
|
275
|
+
expiresAt: chunkXQEHSHLR_cjs.expiresAtFrom(next.expiresIn, next.accessToken, Math.floor(Date.now() / 1e3))
|
|
275
276
|
});
|
|
276
277
|
return next.accessToken;
|
|
277
278
|
})();
|
|
@@ -295,7 +296,7 @@ function ssoRoutes(options = {}) {
|
|
|
295
296
|
if (left <= 0) return;
|
|
296
297
|
c.header(
|
|
297
298
|
"Set-Cookie",
|
|
298
|
-
|
|
299
|
+
chunkXQEHSHLR_cjs.cookieHeader(config.cookieName, await chunkXQEHSHLR_cjs.signSession(next, config.cookieSecret), { maxAge: left, secure: isSecure(c) }),
|
|
299
300
|
{ append: true }
|
|
300
301
|
);
|
|
301
302
|
}
|
|
@@ -303,9 +304,9 @@ function ssoRoutes(options = {}) {
|
|
|
303
304
|
}
|
|
304
305
|
async function parseTransaction(raw, secret) {
|
|
305
306
|
if (raw === void 0) return { ok: false, reason: "absent" };
|
|
306
|
-
const signed = await
|
|
307
|
+
const signed = await chunkXQEHSHLR_cjs.verifyValue(raw, secret);
|
|
307
308
|
if (signed === null) return { ok: false, reason: "unreadable" };
|
|
308
|
-
const fresh = await
|
|
309
|
+
const fresh = await chunkXQEHSHLR_cjs.verifyValue(raw, secret, { maxAgeSeconds: TRANSACTION_MAX_AGE });
|
|
309
310
|
if (fresh === null) return { ok: false, reason: "expired" };
|
|
310
311
|
try {
|
|
311
312
|
const tx = JSON.parse(fresh);
|
|
@@ -375,16 +376,16 @@ function accountRoutes(sso) {
|
|
|
375
376
|
});
|
|
376
377
|
app.post("/profile/avatar", async (c) => {
|
|
377
378
|
const token = await sso.getAccessToken(c);
|
|
378
|
-
const type =
|
|
379
|
-
if (!
|
|
380
|
-
return c.json({ error: "unsupported_type", accepted:
|
|
379
|
+
const type = chunkXQEHSHLR_cjs.mediaType(c.req.header("content-type"));
|
|
380
|
+
if (!chunkXQEHSHLR_cjs.AVATAR_TYPES.includes(type)) {
|
|
381
|
+
return c.json({ error: "unsupported_type", accepted: chunkXQEHSHLR_cjs.AVATAR_TYPES }, 415);
|
|
381
382
|
}
|
|
382
383
|
const declared = Number(c.req.header("content-length"));
|
|
383
|
-
if (Number.isFinite(declared) && declared >
|
|
384
|
-
return c.json({ error: "too_large", max_bytes:
|
|
384
|
+
if (Number.isFinite(declared) && declared > chunkXQEHSHLR_cjs.MAX_AVATAR_BYTES) {
|
|
385
|
+
return c.json({ error: "too_large", max_bytes: chunkXQEHSHLR_cjs.MAX_AVATAR_BYTES }, 413);
|
|
385
386
|
}
|
|
386
|
-
const bytes = await readCapped(c,
|
|
387
|
-
if (!bytes) return c.json({ error: "too_large", max_bytes:
|
|
387
|
+
const bytes = await readCapped(c, chunkXQEHSHLR_cjs.MAX_AVATAR_BYTES);
|
|
388
|
+
if (!bytes) return c.json({ error: "too_large", max_bytes: chunkXQEHSHLR_cjs.MAX_AVATAR_BYTES }, 413);
|
|
388
389
|
return answer(c, await client.uploadAvatar(token, bytes, type));
|
|
389
390
|
});
|
|
390
391
|
app.post("/profile/avatar/remove", async (c) => answer(c, await client.removeAvatar(await sso.getAccessToken(c))));
|
|
@@ -397,7 +398,7 @@ function accountRoutes(sso) {
|
|
|
397
398
|
case "SsoInsufficientScopeError":
|
|
398
399
|
return c.json({ error: "insufficient_scope", scope: e.scope }, 403);
|
|
399
400
|
case "SsoAvatarRejectedError":
|
|
400
|
-
return e.reason === "too_large" ? c.json({ error: "too_large", max_bytes:
|
|
401
|
+
return e.reason === "too_large" ? c.json({ error: "too_large", max_bytes: chunkXQEHSHLR_cjs.MAX_AVATAR_BYTES }, 413) : c.json({ error: "unsupported_type", accepted: chunkXQEHSHLR_cjs.AVATAR_TYPES }, 415);
|
|
401
402
|
case "SsoAppApiError": {
|
|
402
403
|
if (e.status === 401) return c.json({ error: "reauth" }, 401);
|
|
403
404
|
if (typeof e.status === "number" && e.status >= 400 && e.status < 500) {
|
package/dist/hono.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hono.ts"],"names":["loadSsoConfig","createSsoClient","verifySession","readCookie","Hono","SsoError","cookieHeader","signValue","newSid","expiresAtFrom","signSession","verifyValue","require","SsoReauthError","mediaType","AVATAR_TYPES","MAX_AVATAR_BYTES"],"mappings":"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","file":"hono.cjs","sourcesContent":["/**\n * The Hono adapter — mounting, and nothing else.\n *\n * Every rule about what a valid session IS lives in the core (client.ts,\n * session.ts). This file only turns those into routes. F084.5 adds a Next.js\n * adapter on the SAME core, because a rule implemented twice is a rule that\n * eventually says two things.\n */\nimport { Hono, type Context, type MiddlewareHandler } from \"hono\";\nimport { loadSsoConfig, type SsoConfig } from \"./config.js\";\nimport {\n AVATAR_TYPES,\n createSsoClient,\n MAX_AVATAR_BYTES,\n mediaType,\n SsoError,\n SsoReauthError,\n type BidProfile,\n type SsoClient,\n} from \"./client.js\";\nimport { expiresAtFrom, newSid, type TokenStore } from \"./tokens.js\";\nimport {\n cookieHeader,\n readCookie,\n signSession,\n signValue,\n verifySession,\n verifyValue,\n type SessionPayload,\n} from \"./session.js\";\n\n/** Where the adapter parks the session on the request, for `getSession`. */\nconst SESSION_KEY = \"bidSession\";\n\n/**\n * How long the SERVER will accept an in-flight login. Five minutes is plenty\n * for a person to type a password; longer just widens the window. This is the\n * real limit: it is enforced in `parseTransaction`, on a timestamp inside the\n * signature, so no client can talk its way past it.\n */\nconst TRANSACTION_MAX_AGE = 300;\n\n/**\n * How long the BROWSER keeps the cookie — deliberately LONGER than the window\n * above, and the two numbers must never be collapsed back into one.\n *\n * A cookie the browser has already dropped NEVER ARRIVES. With one number for\n * both, the moment the window passes there is simply nothing in the request,\n * and the server cannot tell \"she took too long\" from \"she never started a\n * login here\". Those are different things that deserve different answers, and\n * collapsing them cost a diagnosis: this adapter used to answer every failed\n * callback with one message containing the word \"or\".\n *\n * Reported by helpdesk (2026-09-22, components-F084.54), who run 3x in\n * production for exactly this reason. The extra time is INERT — `parseTransaction`\n * refuses anything past TRANSACTION_MAX_AGE and /callback clears the cookie, so\n * a verifier that cannot be exchanged is not a key. It buys diagnosis, not\n * lifetime.\n */\nconst TRANSACTION_COOKIE_MAX_AGE = TRANSACTION_MAX_AGE * 3;\n\n/**\n * Where «Log ud overalt» is remembered (F084.151). The APP provides it, backed by\n * something every instance shares — apps run on two machines, so a per-process\n * list would log the user out on one machine and not the other. There is\n * deliberately no in-memory default.\n */\nexport interface BackchannelStore {\n /** Every session for `sub` minted at or before `iat` is over. Keep the LATEST iat. */\n revokeSubBefore(sub: string, iat: number): Promise<void>;\n /** The iat recorded for `sub`, or null. */\n revokedBefore(sub: string): Promise<number | null>;\n /** Record a logout token id; true the FIRST time, false for a replay. Keep it until `until` (unix s). */\n useJti(jti: string, until: number): Promise<boolean>;\n}\n\nexport interface BackchannelOptions {\n store: BackchannelStore;\n /**\n * When the store cannot answer: \"reject\" (default) treats the user as logged\n * out; \"allow\" lets the session through unchecked. «reject» is the secure\n * choice — a user who pressed «Log ud overalt» must not stay in because a\n * database hiccupped — at the price of everyone being out while it lasts.\n */\n onStoreError?: \"reject\" | \"allow\";\n}\n\nexport interface SsoRoutesOptions {\n /**\n * What «Log ud» in the app does (F084.152).\n * \"app\" (default): end THIS app's session and send the user to BID's login\n * dialog (prompt=login). They stay signed in to BID — its own «Log ud» is\n * a separate action (decision D-376ffa) — and BID shows the dialog anyway.\n * \"central\": the pre-0.7.0 behaviour — RP-initiated logout at BID's\n * end-session endpoint, which also ends the BID session.\n */\n logout?: \"app\" | \"central\";\n /**\n * Receive «Log ud overalt» from BID at POST <mount>/backchannel-logout.\n * Nothing arrives until BID has registered that URL for this app (ship dark),\n * so mounting it early is safe.\n */\n backchannel?: BackchannelOptions;\n config?: SsoConfig;\n client?: SsoClient;\n /** Path prefix these routes are mounted under. Used to build the return URL. */\n loginPath?: string;\n /** Where to send a user after a successful login when no returnTo is given. */\n defaultReturnTo?: string;\n /**\n * F095.1 — keep the user's access and refresh token SERVER-SIDE, so the app\n * can call BID's app API (the account page) as the user via getAccessToken.\n * Without it, login behaves exactly as in 0.8.0 and the tokens are dropped.\n */\n tokenStore?: TokenStore;\n /**\n * What the BROWSER is told when /callback refuses a login (F084.55, 0.12.0).\n * \"single\" (default): an expired login and an unreadable cookie get the SAME\n * answer, `login_failed`. The two differ only in whether the cookie's\n * signature held against the current secret, and a stranger holding a\n * FOUND cookie must not be able to ask that.\n * \"granular\": the 0.3.0–0.11.0 behaviour — `login_expired` and\n * `bad_login_cookie` told apart. For an app that knows its threat model\n * (e.g. internal, behind a VPN) and wants «dit login udløb» in the browser.\n * `no_login_in_progress` is its own answer either way: it depends on no secret.\n */\n callbackErrors?: \"single\" | \"granular\";\n /**\n * Receives the PRECISE cause of every refused callback, whatever the browser\n * was told — this is the operator's channel. `unreadable` on a real user's\n * browser is what a rotated SSO_COOKIE_SECRET looks like from outside.\n * Without it, the cause is written with console.warn so the log still has it.\n */\n onCallbackRefused?: (cause: CallbackRefusal, c: Context) => void | Promise<void>;\n}\n\n/** Why /callback refused: no login started here · too old · signature did not hold. */\nexport type CallbackRefusal = \"absent\" | \"expired\" | \"unreadable\";\n\nfunction isSecure(c: Context): boolean {\n // localhost over http is the one case where a Secure cookie would simply\n // never be stored, making local development impossible for a reason no error\n // would explain.\n return new URL(c.req.url).protocol === \"https:\";\n}\n\n/**\n * Only same-origin, path-only return targets are honoured.\n *\n * Without this, `/auth/login?returnTo=https://evil.example` turns this app's\n * own login into an open redirect that arrives wearing our domain — the\n * classic phishing lever, and it is one line to close.\n */\n/**\n * An origin nothing can resolve to. Only used as a yardstick: if a candidate\n * still sits on THIS origin after parsing, it is same-origin wherever the app\n * actually lives.\n */\nconst YARDSTICK = \"https://sso.invalid\";\n\n/**\n * Only same-origin, path-only return targets are honoured.\n *\n * ── WHY THIS ASKS A PARSER INSTEAD OF INSPECTING CHARACTERS ───────────────\n *\n * The first version of this function rejected a leading `//` and required a\n * leading `/`. That is the obvious rule, it reads as correct, and it was\n * EXPLOITABLE — found in the first full security review of this package\n * (2026-09-20), not by any diff review, because this code never appeared in a\n * diff:\n *\n * returnTo=/\\evil.dk → passed the guard\n * → resolves to https://evil.dk/ in a browser\n *\n * A backslash is not a path separator to RFC 3986 and IS one to the WHATWG URL\n * spec, which is what browsers implement. So the string looked like a path to\n * us and was an authority to the thing that acts on it.\n *\n * AND THE OBVIOUS FIX WAS ALSO LEAKY. Parsing once and comparing origins closes\n * the backslash and still lets `/..//evil.dk` through: `..` NORMALISES the path\n * to `//evil.dk`, which is protocol-relative when it is later used as a\n * Location. So the result is re-checked — a fixpoint — rather than trusted\n * because the input parsed cleanly.\n *\n * The rule is therefore: let the SAME parser the browser uses decide, twice.\n * A list of dangerous characters is always one trick behind; the first version\n * knew `//` and not `\\`, and the second knew `\\` and not `..`.\n */\nfunction safeReturnTo(raw: string | undefined, fallback: string): string {\n if (!raw || !raw.startsWith(\"/\")) return fallback;\n\n let once: URL;\n try {\n once = new URL(raw, YARDSTICK);\n } catch {\n return fallback;\n }\n if (once.origin !== YARDSTICK) return fallback;\n\n const normalised = once.pathname + once.search + once.hash;\n let twice: URL;\n try {\n twice = new URL(normalised, YARDSTICK);\n } catch {\n return fallback;\n }\n if (twice.origin !== YARDSTICK) return fallback;\n // Belt and braces: a normalised path must still be a path.\n if (!normalised.startsWith(\"/\") || normalised.startsWith(\"//\")) return fallback;\n\n return normalised;\n}\n\nexport function ssoRoutes(options: SsoRoutesOptions = {}) {\n const config = options.config ?? loadSsoConfig();\n const client = options.client ?? createSsoClient(config);\n const loginPath = options.loginPath ?? \"/auth\";\n const defaultReturnTo = options.defaultReturnTo ?? \"/\";\n const txCookie = `${config.cookieName}_tx`;\n /**\n * The ID token, kept ONLY so logout can prove who is leaving.\n *\n * Its own cookie rather than a field on SessionPayload, for three reasons:\n * that type is public and deliberately small (session.ts says why), a JWT is\n * ~1 kB that would otherwise ride along on every single request to the app,\n * and the two are cleared at different moments.\n */\n const idTokenCookie = `${config.cookieName}_idt`;\n const backchannel = options.backchannel;\n if (backchannel && !backchannel.store) {\n throw new Error(\"ssoRoutes: backchannel needs a store shared by every instance of the app — there is no in-memory default.\");\n }\n const onStoreError = backchannel?.onStoreError ?? \"reject\";\n const tokenStore = options.tokenStore;\n const callbackErrors = options.callbackErrors ?? \"single\";\n const onCallbackRefused =\n options.onCallbackRefused ?? ((cause: CallbackRefusal) => console.warn(`[@broberg/sso] /callback refused: ${cause}`));\n\n /** A verified session, or null — also null when «Log ud overalt» has ended it. */\n async function liveSession(c: Context): Promise<SessionPayload | null> {\n const session = await verifySession(readCookie(c.req.header(\"cookie\"), config.cookieName), config.cookieSecret);\n if (!session || !backchannel) return session;\n try {\n const before = await backchannel.store.revokedBefore(session.sub);\n if (before !== null && (session.iat ?? 0) <= before) return null;\n return session;\n } catch {\n return onStoreError === \"allow\" ? session : null;\n }\n }\n\n const app = new Hono();\n\n if (backchannel) {\n app.post(\"/backchannel-logout\", async (c) => {\n c.header(\"Cache-Control\", \"no-store\");\n let token: string | undefined;\n try {\n const form = await c.req.parseBody();\n token = typeof form.logout_token === \"string\" ? form.logout_token : undefined;\n } catch {\n token = undefined;\n }\n if (!token) return c.json({ error: \"invalid_request\", error_description: \"logout_token is missing\" }, 400);\n let verified;\n try {\n verified = await client.verifyLogoutToken(token);\n } catch (e) {\n const msg = e instanceof SsoError ? e.message : \"logout_token could not be verified\";\n return c.json({ error: \"invalid_request\", error_description: msg }, 400);\n }\n try {\n // Revoke FIRST, then record the jti. The other order loses a logout: if the\n // revoke failed after the jti was recorded, the retry would be read as a\n // replay and do nothing. Revoking twice is harmless (the store keeps the\n // latest iat), so a replay only costs one idempotent write.\n await backchannel.store.revokeSubBefore(verified.sub, verified.iat);\n // F095.1 — the user's tokens go with the sessions. Inside the same try:\n // not deleted = not done, so BID reports this app as failed.\n if (tokenStore) await tokenStore.deleteSub(verified.sub);\n await backchannel.store.useJti(verified.jti, verified.exp);\n } catch {\n // Not stored = not done. 5xx so BID reports this app as failed to the user.\n return c.json({ error: \"store_unavailable\" }, 503);\n }\n return c.body(null, 200);\n });\n }\n\n app.get(\"/login\", async (c) => {\n const prompt = c.req.query(\"prompt\");\n // Only the two prompts an app has a reason to ask for; anything else is dropped.\n const start = await client.beginLogin(\n prompt === \"none\" || prompt === \"login\" ? { prompt } : {},\n );\n const tx = JSON.stringify({\n state: start.state,\n codeVerifier: start.codeVerifier,\n nonce: start.nonce,\n returnTo: safeReturnTo(c.req.query(\"returnTo\"), defaultReturnTo),\n });\n c.header(\n \"Set-Cookie\",\n // The signed stamp carries the SERVER's window; the cookie carries the\n // longer browser one, so an expired transaction still reaches us and can\n // be named instead of vanishing.\n cookieHeader(txCookie, await signValue(tx, config.cookieSecret, { maxAgeSeconds: TRANSACTION_MAX_AGE }), {\n maxAge: TRANSACTION_COOKIE_MAX_AGE,\n secure: isSecure(c),\n }),\n );\n // A FULL top-level redirect. Never a hidden iframe — see client.ts.\n return c.redirect(start.url, 302);\n });\n\n app.get(\"/callback\", async (c) => {\n const parsed = await parseTransaction(\n readCookie(c.req.header(\"cookie\"), txCookie),\n config.cookieSecret,\n );\n if (!parsed.ok) {\n /**\n * THREE CAUSES, THREE ANSWERS (components-F084.54).\n *\n * THE ORDER IN `parseTransaction` IS LOAD-BEARING AND NOT OBVIOUS:\n * `expired` is reached ONLY AFTER the signature has verified. So\n * `login_expired` is a POSITIVE CONFIRMATION that the HMAC held against\n * the CURRENT secret, and `bad_login_cookie` is the denial of that.\n *\n * WHICH MAKES THE PAIR A ONE-BIT ORACLE, and the justification that stood\n * here was wrong. It said an attacker can produce all three himself, so\n * naming them tells him nothing. That holds only for an attacker who\n * CONSTRUCTED the input and therefore knows what he is holding. The\n * interesting one has a value he FOUND — in a log, on a shared machine, in\n * a referrer leak, in a backup — and he cannot mint a\n * validly-signed-but-expired value himself. For him the answer separates\n * two things he could not otherwise learn: whether a found value is still\n * live, when the secret rotates (the same value changes code the second it\n * does), and whether two environments share a secret.\n *\n * Reported by helpdesk, 2026-09-22, measured against the published 0.3.0\n * dist rather than argued. It is NOT a hole: it does not help him forge\n * anything — one bit per attempt against an HMAC-SHA256 key he never\n * reaches. It is a property the server gives away for free.\n *\n * SO THE TWO AUDIENCES ARE SPLIT (components-F084.55, Christian\n * 2026-10-07: «ét svar» as the default). The app and the log get all\n * three causes through `onCallbackRefused`, always. The browser gets ONE\n * answer for expired + unreadable unless the app opts into \"granular\".\n * `absent` stays its own code: it depends on no secret, so it leaks\n * nothing. Do NOT reorder parseTransaction to hide the oracle instead —\n * signature-before-age is what keeps the operator's two causes apart.\n *\n * The operator half stands unchanged and is the reason the codes exist at\n * all: `bad_login_cookie` on a REAL user's browser is what a rotated\n * SSO_COOKIE_SECRET looks like from the outside.\n */\n const failures = {\n absent: {\n status: 400 as const,\n error: \"no_login_in_progress\",\n message: \"This browser did not start a login here. Begin again from the login page.\",\n },\n expired: {\n status: 400 as const,\n error: \"login_expired\",\n message: `This login took longer than ${TRANSACTION_MAX_AGE} seconds. Begin again from the login page.`,\n },\n unreadable: {\n status: 400 as const,\n error: \"bad_login_cookie\",\n message: \"This browser's login cookie could not be read. Begin again from the login page.\",\n },\n };\n // The operator's channel must never turn a refusal into a 500: a hook that\n // throws is reported, and the browser still gets its answer.\n try {\n await onCallbackRefused(parsed.reason, c);\n } catch (err) {\n console.error(\"[@broberg/sso] onCallbackRefused threw; the refusal stands\", err);\n }\n const shown =\n callbackErrors === \"single\" && parsed.reason !== \"absent\"\n ? {\n status: 400 as const,\n error: \"login_failed\",\n message: \"This login could not be completed. Begin again from the login page.\",\n }\n : failures[parsed.reason];\n const { status, ...body } = shown;\n // Clear it either way: a transaction we refuse must not sit in the browser\n // waiting to be refused again on every retry.\n c.header(\"Set-Cookie\", cookieHeader(txCookie, \"\", { maxAge: 0, secure: isSecure(c) }));\n return c.json(body, status);\n }\n\n const result = await client.completeLogin({\n params: new URL(c.req.url).searchParams,\n state: parsed.tx.state,\n codeVerifier: parsed.tx.codeVerifier,\n nonce: parsed.tx.nonce,\n });\n\n const nowS = Math.floor(Date.now() / 1000);\n const exp = nowS + config.sessionMaxAge;\n // F095.1 — tokens to the server-side store, keyed by a fresh sid; only the\n // sid rides in the cookie.\n let sid: string | undefined;\n if (tokenStore && result.accessToken) {\n sid = newSid();\n await tokenStore.set(sid, {\n sub: result.claims.sub,\n accessToken: result.accessToken,\n ...(result.refreshToken ? { refreshToken: result.refreshToken } : {}),\n expiresAt: expiresAtFrom(result.expiresIn, result.accessToken, nowS),\n });\n }\n const session: SessionPayload = {\n sub: result.claims.sub,\n exp,\n iat: nowS,\n ...(sid ? { sid } : {}),\n ...(result.claims.email ? { email: result.claims.email } : {}),\n ...(result.claims.name ? { name: result.claims.name } : {}),\n ...(result.claims.picture ? { picture: result.claims.picture } : {}),\n // Only when BID actually said so — absent means \"not stated\", never false.\n ...(typeof result.claims.email_verified === \"boolean\" ? { email_verified: result.claims.email_verified } : {}),\n };\n\n c.header(\n \"Set-Cookie\",\n cookieHeader(config.cookieName, await signSession(session, config.cookieSecret), {\n maxAge: config.sessionMaxAge,\n secure: isSecure(c),\n }),\n );\n // The logout hint. Without it the issuer MUST ask the user to confirm —\n // that is RP-initiated logout per spec, and the plugin says so itself\n // (\"User confirmation is required to complete logout\"). The result was an\n // unstyled English confirmation page with a bare browser button in the\n // middle of our own product.\n c.header(\n \"Set-Cookie\",\n cookieHeader(idTokenCookie, await signValue(result.idToken, config.cookieSecret), {\n maxAge: config.sessionMaxAge,\n secure: isSecure(c),\n }),\n { append: true },\n );\n // Clear the transaction cookie: it has done its job, and a replayed one is\n // only ever useful to someone who should not have it.\n c.header(\n \"Set-Cookie\",\n cookieHeader(txCookie, \"\", { maxAge: 0, secure: isSecure(c) }),\n { append: true },\n );\n return c.redirect(parsed.tx.returnTo, 302);\n });\n\n app.get(\"/logout\", async (c) => {\n // Read the hint BEFORE clearing, obviously — but note what happens when it\n // is absent: a session minted by 0.1.0 has no such cookie, and that session\n // is still live in every app that upgrades. So a missing hint falls back to\n // the old behaviour (the issuer asks) rather than throwing. The upgrade path\n // is the one that fails in production; it gets its own test.\n const idTokenHint = await verifyValue(\n readCookie(c.req.header(\"cookie\"), idTokenCookie),\n config.cookieSecret,\n );\n // F095.1 — the tokens die with the session. Read the sid before the cookie goes.\n if (tokenStore) {\n const leaving = await verifySession(readCookie(c.req.header(\"cookie\"), config.cookieName), config.cookieSecret);\n if (leaving?.sid) await tokenStore.delete(leaving.sid);\n }\n\n c.header(\n \"Set-Cookie\",\n cookieHeader(config.cookieName, \"\", { maxAge: 0, secure: isSecure(c) }),\n );\n c.header(\n \"Set-Cookie\",\n cookieHeader(idTokenCookie, \"\", { maxAge: 0, secure: isSecure(c) }),\n { append: true },\n );\n // F084.152 — the default: this app is logged out, BID is not, and the user\n // lands on BID's login dialog (prompt=login makes BID show it even with a\n // live BID session). Through our own /login, so PKCE and state still apply.\n if ((options.logout ?? \"app\") === \"app\") {\n return c.redirect(`${loginPath}/login?prompt=login`, 302);\n }\n // Local cookies cleared FIRST, then central logout. If the redirect to BID\n // fails or the user closes the tab, the worst case is \"signed out here but\n // not everywhere\" — never the reverse, which would leave this app trusting\n // a session the user believes is gone.\n return c.redirect(await client.logoutUrl(idTokenHint ? { idTokenHint } : {}), 302);\n });\n\n /** Reads the session and puts it on the context. Never blocks. */\n const attach: MiddlewareHandler = async (c, next) => {\n c.set(SESSION_KEY, await liveSession(c));\n await next();\n };\n\n /** Blocks and redirects to login, preserving where the user was going. */\n const require: MiddlewareHandler = async (c, next) => {\n const session = await liveSession(c);\n if (!session) {\n const url = new URL(c.req.url);\n return c.redirect(\n `${loginPath}/login?returnTo=${encodeURIComponent(url.pathname + url.search)}`,\n 302,\n );\n }\n c.set(SESSION_KEY, session);\n await next();\n };\n\n /**\n * F095.1 — the user's access token for calling BID's app API, renewed with the\n * refresh token when it has expired (or is within 30 s of it). Throws SsoError\n * — never returns a stale or empty token — when there is no live session, no\n * stored tokens, or the renewal is refused; a refused renewal also deletes the\n * dead pair so the next call does not try it again.\n */\n const refreshing = new Map<string, Promise<string>>();\n async function getAccessToken(c: Context): Promise<string> {\n if (!tokenStore) {\n throw new SsoError(\"getAccessToken needs ssoRoutes({ tokenStore }) — without one the login's tokens are not kept.\");\n }\n const raw = await verifySession(readCookie(c.req.header(\"cookie\"), config.cookieName), config.cookieSecret);\n const live = await liveSession(c);\n if (!live) {\n if (raw?.sid) await tokenStore.delete(raw.sid);\n throw new SsoReauthError(\"no live session — log in again.\", \"no_session\");\n }\n const sid = live.sid;\n if (!sid) throw new SsoReauthError(\"this session has no stored tokens (it began before tokenStore was configured) — log in again.\", \"no_tokens\");\n const tokens = await tokenStore.get(sid);\n if (!tokens) throw new SsoReauthError(\"no tokens stored for this session — log in again.\", \"no_tokens\");\n const now = Math.floor(Date.now() / 1000);\n if (tokens.expiresAt - 30 > now) return tokens.accessToken;\n if (!tokens.refreshToken) {\n await tokenStore.delete(sid);\n throw new SsoReauthError(\"the access token has expired and there is no refresh token — log in again.\", \"expired\");\n }\n // One renewal per session at a time: with refresh-token rotation, a second\n // parallel renewal would present an already-spent token and be refused.\n const inFlight = refreshing.get(sid);\n if (inFlight) return inFlight;\n const p = (async () => {\n let next;\n try {\n next = await client.refreshTokens(tokens.refreshToken!);\n } catch (e) {\n await tokenStore.delete(sid);\n throw new SsoReauthError(e instanceof SsoError ? e.message : \"token refresh failed\", \"refresh_failed\");\n }\n await tokenStore.set(sid, {\n sub: tokens.sub,\n accessToken: next.accessToken,\n // Rotation: use the new one if BID issued one, else keep the old.\n ...(next.refreshToken ?? tokens.refreshToken ? { refreshToken: next.refreshToken ?? tokens.refreshToken } : {}),\n expiresAt: expiresAtFrom(next.expiresIn, next.accessToken, Math.floor(Date.now() / 1000)),\n });\n return next.accessToken;\n })();\n refreshing.set(sid, p);\n try {\n return await p;\n } finally {\n refreshing.delete(sid);\n }\n }\n\n /**\n * F095.5 — after the user changed her own name or picture, re-sign the\n * session cookie with the new values, so the user menu shows them on the next\n * page load without a new login. Only the convenience copies change: sub,\n * sid, iat and exp are kept as they are (the cookie's lifetime is NOT\n * extended — Max-Age is what is left of it). No live session → nothing.\n */\n async function refreshSessionProfile(c: Context, profile: { name: string | null; picture: string | null }): Promise<void> {\n const live = await liveSession(c);\n if (!live) return;\n const next: SessionPayload = { ...live };\n if (profile.name) next.name = profile.name;\n else delete next.name;\n if (profile.picture) next.picture = profile.picture;\n else delete next.picture;\n if (next.name === live.name && next.picture === live.picture) return;\n const left = live.exp - Math.floor(Date.now() / 1000);\n if (left <= 0) return;\n c.header(\n \"Set-Cookie\",\n cookieHeader(config.cookieName, await signSession(next, config.cookieSecret), { maxAge: left, secure: isSecure(c) }),\n { append: true },\n );\n }\n\n return { app, attach, require, client, config, getAccessToken, refreshSessionProfile };\n}\n\n/** Why a login transaction could not be read back. Three states, three answers. */\nexport type TransactionFailure = \"absent\" | \"expired\" | \"unreadable\";\n\ntype TransactionResult =\n | { ok: true; tx: { state: string; codeVerifier: string; nonce: string; returnTo: string } }\n | { ok: false; reason: TransactionFailure };\n\n/**\n * Reads the login transaction back out of its signed cookie.\n *\n * RETURNS A REASON, NOT `null` (components-F084.54). It used to collapse three\n * causes into one empty answer, and /callback then told the user \"you did not\n * start a login here, OR it expired\" — a sentence that admits in its own wording\n * that it does not know. The word \"or\" was the bug.\n *\n * Age-checked against TRANSACTION_MAX_AGE, not only against the cookie's own\n * Max-Age (components-F084.53) — Max-Age is the browser's promise, and a client\n * can decline to make it. It still does NOT go through verifySession: that\n * envelope carries an `exp`, and the first version of this file used `exp: 0`,\n * which the expiry check rejected every time.\n *\n * NO FUTURE-STAMP GUARD, and that is a deliberate divergence from helpdesk's\n * implementation, which rejects a negative age so a runaway clock cannot widen\n * the window. Ours cannot be widened that way: the stamp is inside the HMAC, so\n * only OUR OWN clock could produce a future one — and refusing it would mean a\n * second app instance whose clock is a few seconds behind starts rejecting\n * perfectly good logins. That trades a theoretical gain for a real outage.\n */\nasync function parseTransaction(raw: string | undefined, secret: string): Promise<TransactionResult> {\n if (raw === undefined) return { ok: false, reason: \"absent\" };\n\n // Signature FIRST, without the age limit, so the two questions stay separate.\n // Asking them together is what produced one answer for three causes: a single\n // `verifyValue(..., { maxAgeSeconds })` returns null for a forged cookie and\n // for an honest late one alike.\n const signed = await verifyValue(raw, secret);\n if (signed === null) return { ok: false, reason: \"unreadable\" };\n\n const fresh = await verifyValue(raw, secret, { maxAgeSeconds: TRANSACTION_MAX_AGE });\n if (fresh === null) return { ok: false, reason: \"expired\" };\n\n try {\n const tx = JSON.parse(fresh) as {\n state: string;\n codeVerifier: string;\n nonce: string;\n returnTo: string;\n };\n // A correctly signed cookie whose contents are not a transaction is not an\n // expiry and not an absence — it is ours and it is wrong.\n if (!tx.state || !tx.codeVerifier || !tx.nonce) return { ok: false, reason: \"unreadable\" };\n return { ok: true, tx };\n } catch {\n return { ok: false, reason: \"unreadable\" };\n }\n}\n\n/** Read the session a middleware attached. */\nexport function getSession(c: Context): SessionPayload | null {\n return (c.get(SESSION_KEY) as SessionPayload | null) ?? null;\n}\n\n/* ── F095.2 — the user's own profile, as an API the app's account page calls ── */\n\n/** What accountRoutes needs from ssoRoutes — pass its return value as is. */\nexport interface AccountRoutesInput {\n client: Pick<SsoClient, \"getProfile\" | \"updateProfile\" | \"uploadAvatar\" | \"removeAvatar\">;\n getAccessToken(c: Context): Promise<string>;\n /**\n * F095.5 — re-sign the session cookie with the profile BID answered, so the\n * user menu shows the new name and picture on the next page load. ssoRoutes\n * provides it; optional so an older or hand-built input still works.\n */\n refreshSessionProfile?(c: Context, profile: { name: string | null; picture: string | null }): Promise<void>;\n}\n\n/**\n * Errors are matched by NAME, not instanceof. The ./hono entry is a separate\n * bundle with its own copy of the error classes, and an app may hand ssoRoutes a\n * client built from the main entry — instanceof would then miss every one of\n * them and a 403 insufficient_scope would come out as a 500.\n */\ntype Named = Error & { reason?: string; scope?: string; status?: number | null; code?: string };\nfunction errName(e: unknown): string | undefined {\n return e instanceof Error ? e.name : undefined;\n}\n\n/**\n * Read a request body, but stop at `max` bytes — Content-Length is the client's\n * claim, and a chunked upload has none. Returns null when the body is larger.\n */\nasync function readCapped(c: Context, max: number): Promise<Uint8Array | null> {\n const stream = c.req.raw.body;\n if (!stream) return new Uint8Array(0);\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n size += value.byteLength;\n if (size > max) {\n await reader.cancel().catch(() => {});\n return null;\n }\n chunks.push(value);\n }\n const out = new Uint8Array(size);\n let at = 0;\n for (const ch of chunks) {\n out.set(ch, at);\n at += ch.byteLength;\n }\n return out;\n}\n\n/**\n * The signed-in user's own name and picture, for the app's account page.\n * Mount it on /api/account:\n *\n * const sso = ssoRoutes({ tokenStore });\n * app.route(\"/api/account\", accountRoutes(sso));\n *\n * GET /profile · POST /profile {name} · POST /profile/avatar (raw image body) ·\n * POST /profile/avatar/remove. Every answer is JSON, refusals included — it is\n * an API, so never a login redirect:\n * 401 {error:\"unauthenticated\"} no live session\n * 401 {error:\"reauth\"} the session's tokens are spent — log in again\n * 403 {error:\"insufficient_scope\", scope} log in again with that scope requested\n * 413 / 415 picture too large / not PNG, JPEG or WebP\n * 4xx {error:<BID's code>} BID refused the change (e.g. an invalid name)\n * 502 {error:\"bid_unavailable\"} BID did not answer usably\n */\nexport function accountRoutes(sso: AccountRoutesInput) {\n const app = new Hono();\n const { client } = sso;\n\n // Personal data: never cached by a browser or a proxy.\n app.use(\"*\", async (c, next) => {\n c.header(\"Cache-Control\", \"no-store\");\n await next();\n });\n\n // A write from ANOTHER site is refused. SameSite=Lax stops a foreign domain,\n // but not a sibling subdomain (same site), and /profile/avatar/remove needs\n // no body at all — a plain <form> would do. Browsers send Sec-Fetch-Site on\n // every request; a non-browser caller sends none and is let through.\n app.use(\"*\", async (c, next) => {\n const site = c.req.header(\"sec-fetch-site\");\n if (c.req.method !== \"GET\" && site !== undefined && site !== \"same-origin\" && site !== \"none\") {\n return c.json({ error: \"cross_site\" }, 403);\n }\n await next();\n });\n\n // Every answer carries the profile AS BID NOW HOLDS IT; the session cookie\n // follows it (F095.5), so the user menu is right after a reload too — also\n // when the name was changed in BID itself.\n const answer = async (c: Context, profile: BidProfile) => {\n if (sso.refreshSessionProfile) await sso.refreshSessionProfile(c, profile);\n return c.json(profile);\n };\n\n // The session is checked FIRST on every route, before any body is looked at.\n app.get(\"/profile\", async (c) => answer(c, await client.getProfile(await sso.getAccessToken(c))));\n\n app.post(\"/profile\", async (c) => {\n const token = await sso.getAccessToken(c);\n const body = (await c.req.json().catch(() => null)) as { name?: unknown } | null;\n if (typeof body?.name !== \"string\") {\n return c.json({ error: \"invalid_request\", error_description: '\"name\" must be a string' }, 400);\n }\n return answer(c, await client.updateProfile(token, body.name));\n });\n\n app.post(\"/profile/avatar\", async (c) => {\n const token = await sso.getAccessToken(c);\n const type = mediaType(c.req.header(\"content-type\"));\n if (!AVATAR_TYPES.includes(type)) {\n return c.json({ error: \"unsupported_type\", accepted: AVATAR_TYPES }, 415);\n }\n // Early: a declared size over the limit is refused without reading a byte.\n const declared = Number(c.req.header(\"content-length\"));\n if (Number.isFinite(declared) && declared > MAX_AVATAR_BYTES) {\n return c.json({ error: \"too_large\", max_bytes: MAX_AVATAR_BYTES }, 413);\n }\n // And a body without (or lying about) its length is cut off at the limit.\n const bytes = await readCapped(c, MAX_AVATAR_BYTES);\n if (!bytes) return c.json({ error: \"too_large\", max_bytes: MAX_AVATAR_BYTES }, 413);\n return answer(c, await client.uploadAvatar(token, bytes, type));\n });\n\n app.post(\"/profile/avatar/remove\", async (c) => answer(c, await client.removeAvatar(await sso.getAccessToken(c))));\n\n app.onError((err, c) => {\n c.header(\"Cache-Control\", \"no-store\");\n const e = err as Named;\n switch (errName(err)) {\n case \"SsoReauthError\":\n return c.json({ error: e.reason === \"no_session\" ? \"unauthenticated\" : \"reauth\" }, 401);\n case \"SsoInsufficientScopeError\":\n return c.json({ error: \"insufficient_scope\", scope: e.scope }, 403);\n case \"SsoAvatarRejectedError\":\n return e.reason === \"too_large\"\n ? c.json({ error: \"too_large\", max_bytes: MAX_AVATAR_BYTES }, 413)\n : c.json({ error: \"unsupported_type\", accepted: AVATAR_TYPES }, 415);\n case \"SsoAppApiError\": {\n // BID no longer accepts the token (revoked, or the session ended in BID) → log in again.\n if (e.status === 401) return c.json({ error: \"reauth\" }, 401);\n if (typeof e.status === \"number\" && e.status >= 400 && e.status < 500) {\n return c.json({ error: e.code ?? \"bid_refused\" }, e.status as 400);\n }\n return c.json({ error: \"bid_unavailable\" }, 502);\n }\n // A plain SsoError here is a setup mistake (e.g. ssoRoutes without tokenStore).\n case \"SsoError\":\n return c.json({ error: \"sso_misconfigured\" }, 500);\n default:\n return c.json({ error: \"server_error\" }, 500);\n }\n });\n\n return app;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/hono.ts"],"names":["loadSsoConfig","createSsoClient","verifySession","readCookie","Hono","SsoError","cookieHeader","signValue","newSid","expiresAtFrom","signSession","verifyValue","require","SsoReauthError","mediaType","AVATAR_TYPES","MAX_AVATAR_BYTES"],"mappings":"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","file":"hono.cjs","sourcesContent":["/**\n * The Hono adapter — mounting, and nothing else.\n *\n * Every rule about what a valid session IS lives in the core (client.ts,\n * session.ts). This file only turns those into routes. F084.5 adds a Next.js\n * adapter on the SAME core, because a rule implemented twice is a rule that\n * eventually says two things.\n */\nimport { Hono, type Context, type MiddlewareHandler } from \"hono\";\nimport { loadSsoConfig, type SsoConfig } from \"./config.js\";\nimport {\n AVATAR_TYPES,\n createSsoClient,\n MAX_AVATAR_BYTES,\n mediaType,\n SsoError,\n SsoReauthError,\n type BidProfile,\n type SsoClient,\n} from \"./client.js\";\nimport { expiresAtFrom, newSid, type TokenStore } from \"./tokens.js\";\nimport {\n cookieHeader,\n readCookie,\n signSession,\n signValue,\n verifySession,\n verifyValue,\n type SessionPayload,\n} from \"./session.js\";\n\n/** Where the adapter parks the session on the request, for `getSession`. */\nconst SESSION_KEY = \"bidSession\";\n\n/**\n * How long the SERVER will accept an in-flight login. Five minutes is plenty\n * for a person to type a password; longer just widens the window. This is the\n * real limit: it is enforced in `parseTransaction`, on a timestamp inside the\n * signature, so no client can talk its way past it.\n */\nconst TRANSACTION_MAX_AGE = 300;\n\n/**\n * How long the BROWSER keeps the cookie — deliberately LONGER than the window\n * above, and the two numbers must never be collapsed back into one.\n *\n * A cookie the browser has already dropped NEVER ARRIVES. With one number for\n * both, the moment the window passes there is simply nothing in the request,\n * and the server cannot tell \"she took too long\" from \"she never started a\n * login here\". Those are different things that deserve different answers, and\n * collapsing them cost a diagnosis: this adapter used to answer every failed\n * callback with one message containing the word \"or\".\n *\n * Reported by helpdesk (2026-09-22, components-F084.54), who run 3x in\n * production for exactly this reason. The extra time is INERT — `parseTransaction`\n * refuses anything past TRANSACTION_MAX_AGE and /callback clears the cookie, so\n * a verifier that cannot be exchanged is not a key. It buys diagnosis, not\n * lifetime.\n */\nconst TRANSACTION_COOKIE_MAX_AGE = TRANSACTION_MAX_AGE * 3;\n\n/**\n * Where «Log ud overalt» is remembered (F084.151). The APP provides it, backed by\n * something every instance shares — apps run on two machines, so a per-process\n * list would log the user out on one machine and not the other. There is\n * deliberately no in-memory default.\n */\nexport interface BackchannelStore {\n /** Every session for `sub` minted at or before `iat` is over. Keep the LATEST iat. */\n revokeSubBefore(sub: string, iat: number): Promise<void>;\n /** The iat recorded for `sub`, or null. */\n revokedBefore(sub: string): Promise<number | null>;\n /** Record a logout token id; true the FIRST time, false for a replay. Keep it until `until` (unix s). */\n useJti(jti: string, until: number): Promise<boolean>;\n}\n\nexport interface BackchannelOptions {\n store: BackchannelStore;\n /**\n * When the store cannot answer: \"reject\" (default) treats the user as logged\n * out; \"allow\" lets the session through unchecked. «reject» is the secure\n * choice — a user who pressed «Log ud overalt» must not stay in because a\n * database hiccupped — at the price of everyone being out while it lasts.\n */\n onStoreError?: \"reject\" | \"allow\";\n}\n\nexport interface SsoRoutesOptions {\n /**\n * components-F084.181 — \"fly\": prove this app at BID's token endpoint with its\n * Fly identity instead of a client_secret (see SsoConfig.clientAuth). Overrides\n * SSO_CLIENT_AUTH. Ignored when you pass your own `client`.\n */\n clientAuth?: \"secret\" | \"fly\";\n /**\n * What «Log ud» in the app does (F084.152).\n * \"app\" (default): end THIS app's session and send the user to BID's login\n * dialog (prompt=login). They stay signed in to BID — its own «Log ud» is\n * a separate action (decision D-376ffa) — and BID shows the dialog anyway.\n * \"central\": the pre-0.7.0 behaviour — RP-initiated logout at BID's\n * end-session endpoint, which also ends the BID session.\n */\n logout?: \"app\" | \"central\";\n /**\n * Receive «Log ud overalt» from BID at POST <mount>/backchannel-logout.\n * Nothing arrives until BID has registered that URL for this app (ship dark),\n * so mounting it early is safe.\n */\n backchannel?: BackchannelOptions;\n config?: SsoConfig;\n client?: SsoClient;\n /** Path prefix these routes are mounted under. Used to build the return URL. */\n loginPath?: string;\n /** Where to send a user after a successful login when no returnTo is given. */\n defaultReturnTo?: string;\n /**\n * F095.1 — keep the user's access and refresh token SERVER-SIDE, so the app\n * can call BID's app API (the account page) as the user via getAccessToken.\n * Without it, login behaves exactly as in 0.8.0 and the tokens are dropped.\n */\n tokenStore?: TokenStore;\n /**\n * What the BROWSER is told when /callback refuses a login (F084.55, 0.12.0).\n * \"single\" (default): an expired login and an unreadable cookie get the SAME\n * answer, `login_failed`. The two differ only in whether the cookie's\n * signature held against the current secret, and a stranger holding a\n * FOUND cookie must not be able to ask that.\n * \"granular\": the 0.3.0–0.11.0 behaviour — `login_expired` and\n * `bad_login_cookie` told apart. For an app that knows its threat model\n * (e.g. internal, behind a VPN) and wants «dit login udløb» in the browser.\n * `no_login_in_progress` is its own answer either way: it depends on no secret.\n */\n callbackErrors?: \"single\" | \"granular\";\n /**\n * Receives the PRECISE cause of every refused callback, whatever the browser\n * was told — this is the operator's channel. `unreadable` on a real user's\n * browser is what a rotated SSO_COOKIE_SECRET looks like from outside.\n * Without it, the cause is written with console.warn so the log still has it.\n */\n onCallbackRefused?: (cause: CallbackRefusal, c: Context) => void | Promise<void>;\n}\n\n/** Why /callback refused: no login started here · too old · signature did not hold. */\nexport type CallbackRefusal = \"absent\" | \"expired\" | \"unreadable\";\n\nfunction isSecure(c: Context): boolean {\n // localhost over http is the one case where a Secure cookie would simply\n // never be stored, making local development impossible for a reason no error\n // would explain.\n return new URL(c.req.url).protocol === \"https:\";\n}\n\n/**\n * Only same-origin, path-only return targets are honoured.\n *\n * Without this, `/auth/login?returnTo=https://evil.example` turns this app's\n * own login into an open redirect that arrives wearing our domain — the\n * classic phishing lever, and it is one line to close.\n */\n/**\n * An origin nothing can resolve to. Only used as a yardstick: if a candidate\n * still sits on THIS origin after parsing, it is same-origin wherever the app\n * actually lives.\n */\nconst YARDSTICK = \"https://sso.invalid\";\n\n/**\n * Only same-origin, path-only return targets are honoured.\n *\n * ── WHY THIS ASKS A PARSER INSTEAD OF INSPECTING CHARACTERS ───────────────\n *\n * The first version of this function rejected a leading `//` and required a\n * leading `/`. That is the obvious rule, it reads as correct, and it was\n * EXPLOITABLE — found in the first full security review of this package\n * (2026-09-20), not by any diff review, because this code never appeared in a\n * diff:\n *\n * returnTo=/\\evil.dk → passed the guard\n * → resolves to https://evil.dk/ in a browser\n *\n * A backslash is not a path separator to RFC 3986 and IS one to the WHATWG URL\n * spec, which is what browsers implement. So the string looked like a path to\n * us and was an authority to the thing that acts on it.\n *\n * AND THE OBVIOUS FIX WAS ALSO LEAKY. Parsing once and comparing origins closes\n * the backslash and still lets `/..//evil.dk` through: `..` NORMALISES the path\n * to `//evil.dk`, which is protocol-relative when it is later used as a\n * Location. So the result is re-checked — a fixpoint — rather than trusted\n * because the input parsed cleanly.\n *\n * The rule is therefore: let the SAME parser the browser uses decide, twice.\n * A list of dangerous characters is always one trick behind; the first version\n * knew `//` and not `\\`, and the second knew `\\` and not `..`.\n */\nfunction safeReturnTo(raw: string | undefined, fallback: string): string {\n if (!raw || !raw.startsWith(\"/\")) return fallback;\n\n let once: URL;\n try {\n once = new URL(raw, YARDSTICK);\n } catch {\n return fallback;\n }\n if (once.origin !== YARDSTICK) return fallback;\n\n const normalised = once.pathname + once.search + once.hash;\n let twice: URL;\n try {\n twice = new URL(normalised, YARDSTICK);\n } catch {\n return fallback;\n }\n if (twice.origin !== YARDSTICK) return fallback;\n // Belt and braces: a normalised path must still be a path.\n if (!normalised.startsWith(\"/\") || normalised.startsWith(\"//\")) return fallback;\n\n return normalised;\n}\n\nexport function ssoRoutes(options: SsoRoutesOptions = {}) {\n const baseConfig = options.config ?? loadSsoConfig();\n const config = options.clientAuth ? { ...baseConfig, clientAuth: options.clientAuth } : baseConfig;\n const client = options.client ?? createSsoClient(config);\n const loginPath = options.loginPath ?? \"/auth\";\n const defaultReturnTo = options.defaultReturnTo ?? \"/\";\n const txCookie = `${config.cookieName}_tx`;\n /**\n * The ID token, kept ONLY so logout can prove who is leaving.\n *\n * Its own cookie rather than a field on SessionPayload, for three reasons:\n * that type is public and deliberately small (session.ts says why), a JWT is\n * ~1 kB that would otherwise ride along on every single request to the app,\n * and the two are cleared at different moments.\n */\n const idTokenCookie = `${config.cookieName}_idt`;\n const backchannel = options.backchannel;\n if (backchannel && !backchannel.store) {\n throw new Error(\"ssoRoutes: backchannel needs a store shared by every instance of the app — there is no in-memory default.\");\n }\n const onStoreError = backchannel?.onStoreError ?? \"reject\";\n const tokenStore = options.tokenStore;\n const callbackErrors = options.callbackErrors ?? \"single\";\n const onCallbackRefused =\n options.onCallbackRefused ?? ((cause: CallbackRefusal) => console.warn(`[@broberg/sso] /callback refused: ${cause}`));\n\n /** A verified session, or null — also null when «Log ud overalt» has ended it. */\n async function liveSession(c: Context): Promise<SessionPayload | null> {\n const session = await verifySession(readCookie(c.req.header(\"cookie\"), config.cookieName), config.cookieSecret);\n if (!session || !backchannel) return session;\n try {\n const before = await backchannel.store.revokedBefore(session.sub);\n if (before !== null && (session.iat ?? 0) <= before) return null;\n return session;\n } catch {\n return onStoreError === \"allow\" ? session : null;\n }\n }\n\n const app = new Hono();\n\n if (backchannel) {\n app.post(\"/backchannel-logout\", async (c) => {\n c.header(\"Cache-Control\", \"no-store\");\n let token: string | undefined;\n try {\n const form = await c.req.parseBody();\n token = typeof form.logout_token === \"string\" ? form.logout_token : undefined;\n } catch {\n token = undefined;\n }\n if (!token) return c.json({ error: \"invalid_request\", error_description: \"logout_token is missing\" }, 400);\n let verified;\n try {\n verified = await client.verifyLogoutToken(token);\n } catch (e) {\n const msg = e instanceof SsoError ? e.message : \"logout_token could not be verified\";\n return c.json({ error: \"invalid_request\", error_description: msg }, 400);\n }\n try {\n // Revoke FIRST, then record the jti. The other order loses a logout: if the\n // revoke failed after the jti was recorded, the retry would be read as a\n // replay and do nothing. Revoking twice is harmless (the store keeps the\n // latest iat), so a replay only costs one idempotent write.\n await backchannel.store.revokeSubBefore(verified.sub, verified.iat);\n // F095.1 — the user's tokens go with the sessions. Inside the same try:\n // not deleted = not done, so BID reports this app as failed.\n if (tokenStore) await tokenStore.deleteSub(verified.sub);\n await backchannel.store.useJti(verified.jti, verified.exp);\n } catch {\n // Not stored = not done. 5xx so BID reports this app as failed to the user.\n return c.json({ error: \"store_unavailable\" }, 503);\n }\n return c.body(null, 200);\n });\n }\n\n app.get(\"/login\", async (c) => {\n const prompt = c.req.query(\"prompt\");\n // Only the two prompts an app has a reason to ask for; anything else is dropped.\n const start = await client.beginLogin(\n prompt === \"none\" || prompt === \"login\" ? { prompt } : {},\n );\n const tx = JSON.stringify({\n state: start.state,\n codeVerifier: start.codeVerifier,\n nonce: start.nonce,\n returnTo: safeReturnTo(c.req.query(\"returnTo\"), defaultReturnTo),\n });\n c.header(\n \"Set-Cookie\",\n // The signed stamp carries the SERVER's window; the cookie carries the\n // longer browser one, so an expired transaction still reaches us and can\n // be named instead of vanishing.\n cookieHeader(txCookie, await signValue(tx, config.cookieSecret, { maxAgeSeconds: TRANSACTION_MAX_AGE }), {\n maxAge: TRANSACTION_COOKIE_MAX_AGE,\n secure: isSecure(c),\n }),\n );\n // A FULL top-level redirect. Never a hidden iframe — see client.ts.\n return c.redirect(start.url, 302);\n });\n\n app.get(\"/callback\", async (c) => {\n const parsed = await parseTransaction(\n readCookie(c.req.header(\"cookie\"), txCookie),\n config.cookieSecret,\n );\n if (!parsed.ok) {\n /**\n * THREE CAUSES, THREE ANSWERS (components-F084.54).\n *\n * THE ORDER IN `parseTransaction` IS LOAD-BEARING AND NOT OBVIOUS:\n * `expired` is reached ONLY AFTER the signature has verified. So\n * `login_expired` is a POSITIVE CONFIRMATION that the HMAC held against\n * the CURRENT secret, and `bad_login_cookie` is the denial of that.\n *\n * WHICH MAKES THE PAIR A ONE-BIT ORACLE, and the justification that stood\n * here was wrong. It said an attacker can produce all three himself, so\n * naming them tells him nothing. That holds only for an attacker who\n * CONSTRUCTED the input and therefore knows what he is holding. The\n * interesting one has a value he FOUND — in a log, on a shared machine, in\n * a referrer leak, in a backup — and he cannot mint a\n * validly-signed-but-expired value himself. For him the answer separates\n * two things he could not otherwise learn: whether a found value is still\n * live, when the secret rotates (the same value changes code the second it\n * does), and whether two environments share a secret.\n *\n * Reported by helpdesk, 2026-09-22, measured against the published 0.3.0\n * dist rather than argued. It is NOT a hole: it does not help him forge\n * anything — one bit per attempt against an HMAC-SHA256 key he never\n * reaches. It is a property the server gives away for free.\n *\n * SO THE TWO AUDIENCES ARE SPLIT (components-F084.55, Christian\n * 2026-10-07: «ét svar» as the default). The app and the log get all\n * three causes through `onCallbackRefused`, always. The browser gets ONE\n * answer for expired + unreadable unless the app opts into \"granular\".\n * `absent` stays its own code: it depends on no secret, so it leaks\n * nothing. Do NOT reorder parseTransaction to hide the oracle instead —\n * signature-before-age is what keeps the operator's two causes apart.\n *\n * The operator half stands unchanged and is the reason the codes exist at\n * all: `bad_login_cookie` on a REAL user's browser is what a rotated\n * SSO_COOKIE_SECRET looks like from the outside.\n */\n const failures = {\n absent: {\n status: 400 as const,\n error: \"no_login_in_progress\",\n message: \"This browser did not start a login here. Begin again from the login page.\",\n },\n expired: {\n status: 400 as const,\n error: \"login_expired\",\n message: `This login took longer than ${TRANSACTION_MAX_AGE} seconds. Begin again from the login page.`,\n },\n unreadable: {\n status: 400 as const,\n error: \"bad_login_cookie\",\n message: \"This browser's login cookie could not be read. Begin again from the login page.\",\n },\n };\n // The operator's channel must never turn a refusal into a 500: a hook that\n // throws is reported, and the browser still gets its answer.\n try {\n await onCallbackRefused(parsed.reason, c);\n } catch (err) {\n console.error(\"[@broberg/sso] onCallbackRefused threw; the refusal stands\", err);\n }\n const shown =\n callbackErrors === \"single\" && parsed.reason !== \"absent\"\n ? {\n status: 400 as const,\n error: \"login_failed\",\n message: \"This login could not be completed. Begin again from the login page.\",\n }\n : failures[parsed.reason];\n const { status, ...body } = shown;\n // Clear it either way: a transaction we refuse must not sit in the browser\n // waiting to be refused again on every retry.\n c.header(\"Set-Cookie\", cookieHeader(txCookie, \"\", { maxAge: 0, secure: isSecure(c) }));\n return c.json(body, status);\n }\n\n const result = await client.completeLogin({\n params: new URL(c.req.url).searchParams,\n state: parsed.tx.state,\n codeVerifier: parsed.tx.codeVerifier,\n nonce: parsed.tx.nonce,\n });\n\n const nowS = Math.floor(Date.now() / 1000);\n const exp = nowS + config.sessionMaxAge;\n // F095.1 — tokens to the server-side store, keyed by a fresh sid; only the\n // sid rides in the cookie.\n let sid: string | undefined;\n if (tokenStore && result.accessToken) {\n sid = newSid();\n await tokenStore.set(sid, {\n sub: result.claims.sub,\n accessToken: result.accessToken,\n ...(result.refreshToken ? { refreshToken: result.refreshToken } : {}),\n expiresAt: expiresAtFrom(result.expiresIn, result.accessToken, nowS),\n });\n }\n const session: SessionPayload = {\n sub: result.claims.sub,\n exp,\n iat: nowS,\n ...(sid ? { sid } : {}),\n ...(result.claims.email ? { email: result.claims.email } : {}),\n ...(result.claims.name ? { name: result.claims.name } : {}),\n ...(result.claims.picture ? { picture: result.claims.picture } : {}),\n // Only when BID actually said so — absent means \"not stated\", never false.\n ...(typeof result.claims.email_verified === \"boolean\" ? { email_verified: result.claims.email_verified } : {}),\n };\n\n c.header(\n \"Set-Cookie\",\n cookieHeader(config.cookieName, await signSession(session, config.cookieSecret), {\n maxAge: config.sessionMaxAge,\n secure: isSecure(c),\n }),\n );\n // The logout hint. Without it the issuer MUST ask the user to confirm —\n // that is RP-initiated logout per spec, and the plugin says so itself\n // (\"User confirmation is required to complete logout\"). The result was an\n // unstyled English confirmation page with a bare browser button in the\n // middle of our own product.\n c.header(\n \"Set-Cookie\",\n cookieHeader(idTokenCookie, await signValue(result.idToken, config.cookieSecret), {\n maxAge: config.sessionMaxAge,\n secure: isSecure(c),\n }),\n { append: true },\n );\n // Clear the transaction cookie: it has done its job, and a replayed one is\n // only ever useful to someone who should not have it.\n c.header(\n \"Set-Cookie\",\n cookieHeader(txCookie, \"\", { maxAge: 0, secure: isSecure(c) }),\n { append: true },\n );\n return c.redirect(parsed.tx.returnTo, 302);\n });\n\n app.get(\"/logout\", async (c) => {\n // Read the hint BEFORE clearing, obviously — but note what happens when it\n // is absent: a session minted by 0.1.0 has no such cookie, and that session\n // is still live in every app that upgrades. So a missing hint falls back to\n // the old behaviour (the issuer asks) rather than throwing. The upgrade path\n // is the one that fails in production; it gets its own test.\n const idTokenHint = await verifyValue(\n readCookie(c.req.header(\"cookie\"), idTokenCookie),\n config.cookieSecret,\n );\n // F095.1 — the tokens die with the session. Read the sid before the cookie goes.\n if (tokenStore) {\n const leaving = await verifySession(readCookie(c.req.header(\"cookie\"), config.cookieName), config.cookieSecret);\n if (leaving?.sid) await tokenStore.delete(leaving.sid);\n }\n\n c.header(\n \"Set-Cookie\",\n cookieHeader(config.cookieName, \"\", { maxAge: 0, secure: isSecure(c) }),\n );\n c.header(\n \"Set-Cookie\",\n cookieHeader(idTokenCookie, \"\", { maxAge: 0, secure: isSecure(c) }),\n { append: true },\n );\n // F084.152 — the default: this app is logged out, BID is not, and the user\n // lands on BID's login dialog (prompt=login makes BID show it even with a\n // live BID session). Through our own /login, so PKCE and state still apply.\n if ((options.logout ?? \"app\") === \"app\") {\n return c.redirect(`${loginPath}/login?prompt=login`, 302);\n }\n // Local cookies cleared FIRST, then central logout. If the redirect to BID\n // fails or the user closes the tab, the worst case is \"signed out here but\n // not everywhere\" — never the reverse, which would leave this app trusting\n // a session the user believes is gone.\n return c.redirect(await client.logoutUrl(idTokenHint ? { idTokenHint } : {}), 302);\n });\n\n /** Reads the session and puts it on the context. Never blocks. */\n const attach: MiddlewareHandler = async (c, next) => {\n c.set(SESSION_KEY, await liveSession(c));\n await next();\n };\n\n /** Blocks and redirects to login, preserving where the user was going. */\n const require: MiddlewareHandler = async (c, next) => {\n const session = await liveSession(c);\n if (!session) {\n const url = new URL(c.req.url);\n return c.redirect(\n `${loginPath}/login?returnTo=${encodeURIComponent(url.pathname + url.search)}`,\n 302,\n );\n }\n c.set(SESSION_KEY, session);\n await next();\n };\n\n /**\n * F095.1 — the user's access token for calling BID's app API, renewed with the\n * refresh token when it has expired (or is within 30 s of it). Throws SsoError\n * — never returns a stale or empty token — when there is no live session, no\n * stored tokens, or the renewal is refused; a refused renewal also deletes the\n * dead pair so the next call does not try it again.\n */\n const refreshing = new Map<string, Promise<string>>();\n async function getAccessToken(c: Context): Promise<string> {\n if (!tokenStore) {\n throw new SsoError(\"getAccessToken needs ssoRoutes({ tokenStore }) — without one the login's tokens are not kept.\");\n }\n const raw = await verifySession(readCookie(c.req.header(\"cookie\"), config.cookieName), config.cookieSecret);\n const live = await liveSession(c);\n if (!live) {\n if (raw?.sid) await tokenStore.delete(raw.sid);\n throw new SsoReauthError(\"no live session — log in again.\", \"no_session\");\n }\n const sid = live.sid;\n if (!sid) throw new SsoReauthError(\"this session has no stored tokens (it began before tokenStore was configured) — log in again.\", \"no_tokens\");\n const tokens = await tokenStore.get(sid);\n if (!tokens) throw new SsoReauthError(\"no tokens stored for this session — log in again.\", \"no_tokens\");\n const now = Math.floor(Date.now() / 1000);\n if (tokens.expiresAt - 30 > now) return tokens.accessToken;\n if (!tokens.refreshToken) {\n await tokenStore.delete(sid);\n throw new SsoReauthError(\"the access token has expired and there is no refresh token — log in again.\", \"expired\");\n }\n // One renewal per session at a time: with refresh-token rotation, a second\n // parallel renewal would present an already-spent token and be refused.\n const inFlight = refreshing.get(sid);\n if (inFlight) return inFlight;\n const p = (async () => {\n let next;\n try {\n next = await client.refreshTokens(tokens.refreshToken!);\n } catch (e) {\n await tokenStore.delete(sid);\n throw new SsoReauthError(e instanceof SsoError ? e.message : \"token refresh failed\", \"refresh_failed\");\n }\n await tokenStore.set(sid, {\n sub: tokens.sub,\n accessToken: next.accessToken,\n // Rotation: use the new one if BID issued one, else keep the old.\n ...(next.refreshToken ?? tokens.refreshToken ? { refreshToken: next.refreshToken ?? tokens.refreshToken } : {}),\n expiresAt: expiresAtFrom(next.expiresIn, next.accessToken, Math.floor(Date.now() / 1000)),\n });\n return next.accessToken;\n })();\n refreshing.set(sid, p);\n try {\n return await p;\n } finally {\n refreshing.delete(sid);\n }\n }\n\n /**\n * F095.5 — after the user changed her own name or picture, re-sign the\n * session cookie with the new values, so the user menu shows them on the next\n * page load without a new login. Only the convenience copies change: sub,\n * sid, iat and exp are kept as they are (the cookie's lifetime is NOT\n * extended — Max-Age is what is left of it). No live session → nothing.\n */\n async function refreshSessionProfile(c: Context, profile: { name: string | null; picture: string | null }): Promise<void> {\n const live = await liveSession(c);\n if (!live) return;\n const next: SessionPayload = { ...live };\n if (profile.name) next.name = profile.name;\n else delete next.name;\n if (profile.picture) next.picture = profile.picture;\n else delete next.picture;\n if (next.name === live.name && next.picture === live.picture) return;\n const left = live.exp - Math.floor(Date.now() / 1000);\n if (left <= 0) return;\n c.header(\n \"Set-Cookie\",\n cookieHeader(config.cookieName, await signSession(next, config.cookieSecret), { maxAge: left, secure: isSecure(c) }),\n { append: true },\n );\n }\n\n return { app, attach, require, client, config, getAccessToken, refreshSessionProfile };\n}\n\n/** Why a login transaction could not be read back. Three states, three answers. */\nexport type TransactionFailure = \"absent\" | \"expired\" | \"unreadable\";\n\ntype TransactionResult =\n | { ok: true; tx: { state: string; codeVerifier: string; nonce: string; returnTo: string } }\n | { ok: false; reason: TransactionFailure };\n\n/**\n * Reads the login transaction back out of its signed cookie.\n *\n * RETURNS A REASON, NOT `null` (components-F084.54). It used to collapse three\n * causes into one empty answer, and /callback then told the user \"you did not\n * start a login here, OR it expired\" — a sentence that admits in its own wording\n * that it does not know. The word \"or\" was the bug.\n *\n * Age-checked against TRANSACTION_MAX_AGE, not only against the cookie's own\n * Max-Age (components-F084.53) — Max-Age is the browser's promise, and a client\n * can decline to make it. It still does NOT go through verifySession: that\n * envelope carries an `exp`, and the first version of this file used `exp: 0`,\n * which the expiry check rejected every time.\n *\n * NO FUTURE-STAMP GUARD, and that is a deliberate divergence from helpdesk's\n * implementation, which rejects a negative age so a runaway clock cannot widen\n * the window. Ours cannot be widened that way: the stamp is inside the HMAC, so\n * only OUR OWN clock could produce a future one — and refusing it would mean a\n * second app instance whose clock is a few seconds behind starts rejecting\n * perfectly good logins. That trades a theoretical gain for a real outage.\n */\nasync function parseTransaction(raw: string | undefined, secret: string): Promise<TransactionResult> {\n if (raw === undefined) return { ok: false, reason: \"absent\" };\n\n // Signature FIRST, without the age limit, so the two questions stay separate.\n // Asking them together is what produced one answer for three causes: a single\n // `verifyValue(..., { maxAgeSeconds })` returns null for a forged cookie and\n // for an honest late one alike.\n const signed = await verifyValue(raw, secret);\n if (signed === null) return { ok: false, reason: \"unreadable\" };\n\n const fresh = await verifyValue(raw, secret, { maxAgeSeconds: TRANSACTION_MAX_AGE });\n if (fresh === null) return { ok: false, reason: \"expired\" };\n\n try {\n const tx = JSON.parse(fresh) as {\n state: string;\n codeVerifier: string;\n nonce: string;\n returnTo: string;\n };\n // A correctly signed cookie whose contents are not a transaction is not an\n // expiry and not an absence — it is ours and it is wrong.\n if (!tx.state || !tx.codeVerifier || !tx.nonce) return { ok: false, reason: \"unreadable\" };\n return { ok: true, tx };\n } catch {\n return { ok: false, reason: \"unreadable\" };\n }\n}\n\n/** Read the session a middleware attached. */\nexport function getSession(c: Context): SessionPayload | null {\n return (c.get(SESSION_KEY) as SessionPayload | null) ?? null;\n}\n\n/* ── F095.2 — the user's own profile, as an API the app's account page calls ── */\n\n/** What accountRoutes needs from ssoRoutes — pass its return value as is. */\nexport interface AccountRoutesInput {\n client: Pick<SsoClient, \"getProfile\" | \"updateProfile\" | \"uploadAvatar\" | \"removeAvatar\">;\n getAccessToken(c: Context): Promise<string>;\n /**\n * F095.5 — re-sign the session cookie with the profile BID answered, so the\n * user menu shows the new name and picture on the next page load. ssoRoutes\n * provides it; optional so an older or hand-built input still works.\n */\n refreshSessionProfile?(c: Context, profile: { name: string | null; picture: string | null }): Promise<void>;\n}\n\n/**\n * Errors are matched by NAME, not instanceof. The ./hono entry is a separate\n * bundle with its own copy of the error classes, and an app may hand ssoRoutes a\n * client built from the main entry — instanceof would then miss every one of\n * them and a 403 insufficient_scope would come out as a 500.\n */\ntype Named = Error & { reason?: string; scope?: string; status?: number | null; code?: string };\nfunction errName(e: unknown): string | undefined {\n return e instanceof Error ? e.name : undefined;\n}\n\n/**\n * Read a request body, but stop at `max` bytes — Content-Length is the client's\n * claim, and a chunked upload has none. Returns null when the body is larger.\n */\nasync function readCapped(c: Context, max: number): Promise<Uint8Array | null> {\n const stream = c.req.raw.body;\n if (!stream) return new Uint8Array(0);\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n size += value.byteLength;\n if (size > max) {\n await reader.cancel().catch(() => {});\n return null;\n }\n chunks.push(value);\n }\n const out = new Uint8Array(size);\n let at = 0;\n for (const ch of chunks) {\n out.set(ch, at);\n at += ch.byteLength;\n }\n return out;\n}\n\n/**\n * The signed-in user's own name and picture, for the app's account page.\n * Mount it on /api/account:\n *\n * const sso = ssoRoutes({ tokenStore });\n * app.route(\"/api/account\", accountRoutes(sso));\n *\n * GET /profile · POST /profile {name} · POST /profile/avatar (raw image body) ·\n * POST /profile/avatar/remove. Every answer is JSON, refusals included — it is\n * an API, so never a login redirect:\n * 401 {error:\"unauthenticated\"} no live session\n * 401 {error:\"reauth\"} the session's tokens are spent — log in again\n * 403 {error:\"insufficient_scope\", scope} log in again with that scope requested\n * 413 / 415 picture too large / not PNG, JPEG or WebP\n * 4xx {error:<BID's code>} BID refused the change (e.g. an invalid name)\n * 502 {error:\"bid_unavailable\"} BID did not answer usably\n */\nexport function accountRoutes(sso: AccountRoutesInput) {\n const app = new Hono();\n const { client } = sso;\n\n // Personal data: never cached by a browser or a proxy.\n app.use(\"*\", async (c, next) => {\n c.header(\"Cache-Control\", \"no-store\");\n await next();\n });\n\n // A write from ANOTHER site is refused. SameSite=Lax stops a foreign domain,\n // but not a sibling subdomain (same site), and /profile/avatar/remove needs\n // no body at all — a plain <form> would do. Browsers send Sec-Fetch-Site on\n // every request; a non-browser caller sends none and is let through.\n app.use(\"*\", async (c, next) => {\n const site = c.req.header(\"sec-fetch-site\");\n if (c.req.method !== \"GET\" && site !== undefined && site !== \"same-origin\" && site !== \"none\") {\n return c.json({ error: \"cross_site\" }, 403);\n }\n await next();\n });\n\n // Every answer carries the profile AS BID NOW HOLDS IT; the session cookie\n // follows it (F095.5), so the user menu is right after a reload too — also\n // when the name was changed in BID itself.\n const answer = async (c: Context, profile: BidProfile) => {\n if (sso.refreshSessionProfile) await sso.refreshSessionProfile(c, profile);\n return c.json(profile);\n };\n\n // The session is checked FIRST on every route, before any body is looked at.\n app.get(\"/profile\", async (c) => answer(c, await client.getProfile(await sso.getAccessToken(c))));\n\n app.post(\"/profile\", async (c) => {\n const token = await sso.getAccessToken(c);\n const body = (await c.req.json().catch(() => null)) as { name?: unknown } | null;\n if (typeof body?.name !== \"string\") {\n return c.json({ error: \"invalid_request\", error_description: '\"name\" must be a string' }, 400);\n }\n return answer(c, await client.updateProfile(token, body.name));\n });\n\n app.post(\"/profile/avatar\", async (c) => {\n const token = await sso.getAccessToken(c);\n const type = mediaType(c.req.header(\"content-type\"));\n if (!AVATAR_TYPES.includes(type)) {\n return c.json({ error: \"unsupported_type\", accepted: AVATAR_TYPES }, 415);\n }\n // Early: a declared size over the limit is refused without reading a byte.\n const declared = Number(c.req.header(\"content-length\"));\n if (Number.isFinite(declared) && declared > MAX_AVATAR_BYTES) {\n return c.json({ error: \"too_large\", max_bytes: MAX_AVATAR_BYTES }, 413);\n }\n // And a body without (or lying about) its length is cut off at the limit.\n const bytes = await readCapped(c, MAX_AVATAR_BYTES);\n if (!bytes) return c.json({ error: \"too_large\", max_bytes: MAX_AVATAR_BYTES }, 413);\n return answer(c, await client.uploadAvatar(token, bytes, type));\n });\n\n app.post(\"/profile/avatar/remove\", async (c) => answer(c, await client.removeAvatar(await sso.getAccessToken(c))));\n\n app.onError((err, c) => {\n c.header(\"Cache-Control\", \"no-store\");\n const e = err as Named;\n switch (errName(err)) {\n case \"SsoReauthError\":\n return c.json({ error: e.reason === \"no_session\" ? \"unauthenticated\" : \"reauth\" }, 401);\n case \"SsoInsufficientScopeError\":\n return c.json({ error: \"insufficient_scope\", scope: e.scope }, 403);\n case \"SsoAvatarRejectedError\":\n return e.reason === \"too_large\"\n ? c.json({ error: \"too_large\", max_bytes: MAX_AVATAR_BYTES }, 413)\n : c.json({ error: \"unsupported_type\", accepted: AVATAR_TYPES }, 415);\n case \"SsoAppApiError\": {\n // BID no longer accepts the token (revoked, or the session ended in BID) → log in again.\n if (e.status === 401) return c.json({ error: \"reauth\" }, 401);\n if (typeof e.status === \"number\" && e.status >= 400 && e.status < 500) {\n return c.json({ error: e.code ?? \"bid_refused\" }, e.status as 400);\n }\n return c.json({ error: \"bid_unavailable\" }, 502);\n }\n // A plain SsoError here is a setup mistake (e.g. ssoRoutes without tokenStore).\n case \"SsoError\":\n return c.json({ error: \"sso_misconfigured\" }, 500);\n default:\n return c.json({ error: \"server_error\" }, 500);\n }\n });\n\n return app;\n}\n"]}
|
package/dist/hono.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as hono_types from 'hono/types';
|
|
2
2
|
import { Context, Hono, MiddlewareHandler } from 'hono';
|
|
3
|
-
import { S as SsoClient, a as SsoConfig, T as TokenStore, b as SessionPayload } from './session-
|
|
3
|
+
import { S as SsoClient, a as SsoConfig, T as TokenStore, b as SessionPayload } from './session-DnotfVNd.cjs';
|
|
4
4
|
import 'jose';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -28,6 +28,12 @@ interface BackchannelOptions {
|
|
|
28
28
|
onStoreError?: "reject" | "allow";
|
|
29
29
|
}
|
|
30
30
|
interface SsoRoutesOptions {
|
|
31
|
+
/**
|
|
32
|
+
* components-F084.181 — "fly": prove this app at BID's token endpoint with its
|
|
33
|
+
* Fly identity instead of a client_secret (see SsoConfig.clientAuth). Overrides
|
|
34
|
+
* SSO_CLIENT_AUTH. Ignored when you pass your own `client`.
|
|
35
|
+
*/
|
|
36
|
+
clientAuth?: "secret" | "fly";
|
|
31
37
|
/**
|
|
32
38
|
* What «Log ud» in the app does (F084.152).
|
|
33
39
|
* "app" (default): end THIS app's session and send the user to BID's login
|
package/dist/hono.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as hono_types from 'hono/types';
|
|
2
2
|
import { Context, Hono, MiddlewareHandler } from 'hono';
|
|
3
|
-
import { S as SsoClient, a as SsoConfig, T as TokenStore, b as SessionPayload } from './session-
|
|
3
|
+
import { S as SsoClient, a as SsoConfig, T as TokenStore, b as SessionPayload } from './session-DnotfVNd.js';
|
|
4
4
|
import 'jose';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -28,6 +28,12 @@ interface BackchannelOptions {
|
|
|
28
28
|
onStoreError?: "reject" | "allow";
|
|
29
29
|
}
|
|
30
30
|
interface SsoRoutesOptions {
|
|
31
|
+
/**
|
|
32
|
+
* components-F084.181 — "fly": prove this app at BID's token endpoint with its
|
|
33
|
+
* Fly identity instead of a client_secret (see SsoConfig.clientAuth). Overrides
|
|
34
|
+
* SSO_CLIENT_AUTH. Ignored when you pass your own `client`.
|
|
35
|
+
*/
|
|
36
|
+
clientAuth?: "secret" | "fly";
|
|
31
37
|
/**
|
|
32
38
|
* What «Log ud» in the app does (F084.152).
|
|
33
39
|
* "app" (default): end THIS app's session and send the user to BID's login
|
package/dist/hono.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadSsoConfig, createSsoClient, SsoError, cookieHeader, signValue, readCookie, newSid, expiresAtFrom, signSession, verifyValue, verifySession, mediaType, AVATAR_TYPES, MAX_AVATAR_BYTES, SsoReauthError } from './chunk-
|
|
1
|
+
import { loadSsoConfig, createSsoClient, SsoError, cookieHeader, signValue, readCookie, newSid, expiresAtFrom, signSession, verifyValue, verifySession, mediaType, AVATAR_TYPES, MAX_AVATAR_BYTES, SsoReauthError } from './chunk-GK3QCKXB.js';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
3
|
|
|
4
4
|
var SESSION_KEY = "bidSession";
|
|
@@ -29,7 +29,8 @@ function safeReturnTo(raw, fallback) {
|
|
|
29
29
|
return normalised;
|
|
30
30
|
}
|
|
31
31
|
function ssoRoutes(options = {}) {
|
|
32
|
-
const
|
|
32
|
+
const baseConfig = options.config ?? loadSsoConfig();
|
|
33
|
+
const config = options.clientAuth ? { ...baseConfig, clientAuth: options.clientAuth } : baseConfig;
|
|
33
34
|
const client = options.client ?? createSsoClient(config);
|
|
34
35
|
const loginPath = options.loginPath ?? "/auth";
|
|
35
36
|
const defaultReturnTo = options.defaultReturnTo ?? "/";
|