@broberg/sso 0.10.1 → 0.11.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 CHANGED
@@ -474,5 +474,13 @@ access token itself (a 401 from BID). It is never a 500.
474
474
  `SsoAppApiError` (`.status`, BID's `.code`; `status` is null when BID did not
475
475
  answer). All extend `SsoError`. No message ever contains a token.
476
476
 
477
+ **The user menu follows (since 0.11.0, F095.5).** Every successful
478
+ `accountRoutes` answer re-signs the session cookie with the name and picture BID
479
+ returned (`ssoRoutes().refreshSessionProfile`). sub, sid, iat and exp are kept,
480
+ and Max-Age is what is left of the session: the lifetime is never extended.
481
+ Nothing is written when nothing changed, and nothing is written on a refusal.
482
+ A reload therefore shows the new name in the menu without a new login. A GET also
483
+ picks up a change the user made in BID itself.
484
+
477
485
  Mail, password, passkeys, two-factor and sessions stay in BID: link to the
478
486
  profile's `account_url` in a new tab.
package/dist/hono.cjs CHANGED
@@ -1048,7 +1048,24 @@ function ssoRoutes(options = {}) {
1048
1048
  refreshing.delete(sid);
1049
1049
  }
1050
1050
  }
1051
- return { app, attach, require: require2, client, config, getAccessToken };
1051
+ async function refreshSessionProfile(c, profile) {
1052
+ const live = await liveSession(c);
1053
+ if (!live) return;
1054
+ const next = { ...live };
1055
+ if (profile.name) next.name = profile.name;
1056
+ else delete next.name;
1057
+ if (profile.picture) next.picture = profile.picture;
1058
+ else delete next.picture;
1059
+ if (next.name === live.name && next.picture === live.picture) return;
1060
+ const left = live.exp - Math.floor(Date.now() / 1e3);
1061
+ if (left <= 0) return;
1062
+ c.header(
1063
+ "Set-Cookie",
1064
+ cookieHeader(config.cookieName, await signSession(next, config.cookieSecret), { maxAge: left, secure: isSecure(c) }),
1065
+ { append: true }
1066
+ );
1067
+ }
1068
+ return { app, attach, require: require2, client, config, getAccessToken, refreshSessionProfile };
1052
1069
  }
1053
1070
  async function parseTransaction(raw, secret) {
1054
1071
  if (raw === void 0) return { ok: false, reason: "absent" };
@@ -1109,14 +1126,18 @@ function accountRoutes(sso) {
1109
1126
  }
1110
1127
  await next();
1111
1128
  });
1112
- app.get("/profile", async (c) => c.json(await client.getProfile(await sso.getAccessToken(c))));
1129
+ const answer = async (c, profile) => {
1130
+ if (sso.refreshSessionProfile) await sso.refreshSessionProfile(c, profile);
1131
+ return c.json(profile);
1132
+ };
1133
+ app.get("/profile", async (c) => answer(c, await client.getProfile(await sso.getAccessToken(c))));
1113
1134
  app.post("/profile", async (c) => {
1114
1135
  const token = await sso.getAccessToken(c);
1115
1136
  const body = await c.req.json().catch(() => null);
1116
1137
  if (typeof body?.name !== "string") {
1117
1138
  return c.json({ error: "invalid_request", error_description: '"name" must be a string' }, 400);
1118
1139
  }
1119
- return c.json(await client.updateProfile(token, body.name));
1140
+ return answer(c, await client.updateProfile(token, body.name));
1120
1141
  });
1121
1142
  app.post("/profile/avatar", async (c) => {
1122
1143
  const token = await sso.getAccessToken(c);
@@ -1130,9 +1151,9 @@ function accountRoutes(sso) {
1130
1151
  }
1131
1152
  const bytes = await readCapped(c, MAX_AVATAR_BYTES);
1132
1153
  if (!bytes) return c.json({ error: "too_large", max_bytes: MAX_AVATAR_BYTES }, 413);
1133
- return c.json(await client.uploadAvatar(token, bytes, type));
1154
+ return answer(c, await client.uploadAvatar(token, bytes, type));
1134
1155
  });
1135
- app.post("/profile/avatar/remove", async (c) => c.json(await client.removeAvatar(await sso.getAccessToken(c))));
1156
+ app.post("/profile/avatar/remove", async (c) => answer(c, await client.removeAvatar(await sso.getAccessToken(c))));
1136
1157
  app.onError((err, c) => {
1137
1158
  c.header("Cache-Control", "no-store");
1138
1159
  const e = err;