@mathismeadows/roamer-device-auth 1.5.3 → 1.5.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mathismeadows/roamer-device-auth",
3
- "version": "1.5.3",
3
+ "version": "1.5.5",
4
4
  "private": false,
5
5
  "mcpName": "com.mathismeadows/roamer-mcp",
6
6
  "type": "module",
@@ -639,10 +639,12 @@ async function doGetValidTokens(clientSlug, forceRefresh, forceFreshLogin = fals
639
639
  }
640
640
 
641
641
  log(`Go to ${device.verification_uri} and enter code: ${device.user_code}`);
642
- // Fire-and-forget: the dialog is how the human actually sees this on macOS; polling below
643
- // must not wait on it being dismissed. Skip re-showing it on a resume — the human already
644
- // saw it (or is mid-flow in the browser) from the process that started this same code.
645
- if (!resuming) showDeviceCodeDialog(device.verification_uri, device.user_code);
642
+ // AUTH-61: launch the browser first, then show the fallback dialog both are best-effort
643
+ // OS-level UI activations, and requesting them in the same instant can leave Safari
644
+ // intermittently stuck showing "Application Not Responding" on launch (macOS-only; see
645
+ // thread AUTH-safari-device-flow-hang). The dialog stays fire-and-forget: polling below
646
+ // must not wait on it being dismissed. Skip both on a resume — the human already saw them
647
+ // (or is mid-flow in the browser) from the process that started this same code.
646
648
  if (!resuming) {
647
649
  try {
648
650
  const { default: open } = await import("open");
@@ -651,6 +653,7 @@ async function doGetValidTokens(clientSlug, forceRefresh, forceFreshLogin = fals
651
653
  // Best-effort only — the dialog and QR code below already carry the URL and code.
652
654
  }
653
655
  }
656
+ if (!resuming) showDeviceCodeDialog(device.verification_uri, device.user_code);
654
657
  // Terminal-visible for any client where a human sees this process's stderr/log output
655
658
  // (Claude Code CLI, VS Code's integrated terminal) — QR codes are a real usability win
656
659
  // for CLI auth over typing an 8-character code by hand.
@@ -906,6 +909,37 @@ async function forwardLine(transport, line, getTokens, setTokens, getFreshTokens
906
909
  }
907
910
  }
908
911
 
912
+ // AUTH-60: a reserved, locally-answered resource — never forwarded to the real upstream
913
+ // server. Lets the exact connection asking report its own resolved clientSlug/identityLabel
914
+ // back through the MCP session itself, with no external file or PID correlation required
915
+ // (see AUTH-60's notes for why a file-based "list live connections" design was rejected).
916
+ function isWhoamiResourceRead(line) {
917
+ try {
918
+ const message = JSON.parse(line);
919
+ return message?.method === "resources/read" && message?.params?.uri === "roamer://whoami";
920
+ } catch {
921
+ return false;
922
+ }
923
+ }
924
+
925
+ function respondWhoami(line, clientSlug, identityLabel) {
926
+ let id = null;
927
+ try {
928
+ id = JSON.parse(line)?.id ?? null;
929
+ } catch {
930
+ // Malformed input never had a usable id anyway — respond with null per JSON-RPC convention.
931
+ }
932
+ const payload = { clientSlug: clientSlug ?? null, identityLabel: identityLabel ?? null };
933
+ const response = {
934
+ jsonrpc: "2.0",
935
+ id,
936
+ result: {
937
+ contents: [{ uri: "roamer://whoami", mimeType: "application/json", text: JSON.stringify(payload, null, 2) }],
938
+ },
939
+ };
940
+ process.stdout.write(`${JSON.stringify(response)}\n`);
941
+ }
942
+
909
943
  // AUTH-28: a hard sign-in failure (denied/expired device code) must tell the host exactly
910
944
  // that, per request, rather than leaving requests unanswered for the host to time out on.
911
945
  function respondWithSignInError(line, err) {
@@ -934,6 +968,13 @@ async function main() {
934
968
  let ready = false;
935
969
  const pendingLines = [];
936
970
 
971
+ // AUTH-60: identityLabel is resolved independently of `ready` (well before transport
972
+ // connects — see identityResolved below), so roamer://whoami can answer even while the
973
+ // real upstream connection is still coming up.
974
+ let identityLabel = null;
975
+ let identityResolved = false;
976
+ const pendingWhoamiLines = [];
977
+
937
978
  // AUTH-51: which client is asking, resolved once from the first stdin line (the MCP
938
979
  // `initialize` request, always message #1 by protocol) before any credential is touched.
939
980
  let clientSlug = null;
@@ -970,6 +1011,12 @@ async function main() {
970
1011
  clientSlug = clientSlugFromInitializeLine(line);
971
1012
  resolveFirstLine();
972
1013
  }
1014
+ // AUTH-60: answered locally from this process's own state, never forwarded upstream.
1015
+ if (isWhoamiResourceRead(line)) {
1016
+ if (identityResolved) respondWhoami(line, clientSlug, identityLabel);
1017
+ else pendingWhoamiLines.push(line);
1018
+ continue;
1019
+ }
973
1020
  if (ready) {
974
1021
  forwardLine(transport, line, getTokens, setTokens, freshTokensForForward);
975
1022
  } else {
@@ -1013,7 +1060,6 @@ async function main() {
1013
1060
  freshTokensForForward = (force) => getFreshTokens(clientSlug, force).then((result) => result.tokens);
1014
1061
 
1015
1062
  let reusedSilently = false;
1016
- let identityLabel = null;
1017
1063
  try {
1018
1064
  const result = await getFreshTokens(clientSlug);
1019
1065
  tokens = result.tokens;
@@ -1028,6 +1074,9 @@ async function main() {
1028
1074
  // explicit failure instead of a process it has to silently time out on.
1029
1075
  log(`Sign-in failed: ${err.message}`);
1030
1076
  for (const line of pendingLines) respondWithSignInError(line, err);
1077
+ // AUTH-60: a whoami read queued during a sign-in that then fails can never get a real
1078
+ // answer — same failure treatment as every other queued line, not left hanging.
1079
+ for (const line of pendingWhoamiLines) respondWithSignInError(line, err);
1031
1080
  process.exit(1);
1032
1081
  }
1033
1082
 
@@ -1037,6 +1086,11 @@ async function main() {
1037
1086
  // instant a transient OS notification fired.
1038
1087
  log(identityLabel ? `Authenticated as ${identityLabel} for client "${clientSlug}".` : `Authenticated for client "${clientSlug}" (no identity claim found on the token).`);
1039
1088
 
1089
+ // AUTH-60: identityLabel is now final — answer any roamer://whoami reads that arrived
1090
+ // before sign-in completed, the same way pendingLines get flushed once ready flips true.
1091
+ identityResolved = true;
1092
+ for (const line of pendingWhoamiLines) respondWhoami(line, clientSlug, identityLabel);
1093
+
1040
1094
  // AUTH-51: the moment that used to be completely silent — a process starting up and
1041
1095
  // immediately using a credential it never interactively obtained this run.
1042
1096
  if (reusedSilently) await notifySessionReused(clientSlug, identityLabel);
@@ -1240,4 +1294,7 @@ export {
1240
1294
  loginCommand,
1241
1295
  logoutCommand,
1242
1296
  statusCommand,
1297
+ // AUTH-60
1298
+ isWhoamiResourceRead,
1299
+ respondWhoami,
1243
1300
  };