@mathismeadows/roamer-device-auth 1.5.3 → 1.5.4

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.4",
4
4
  "private": false,
5
5
  "mcpName": "com.mathismeadows/roamer-mcp",
6
6
  "type": "module",
@@ -906,6 +906,37 @@ async function forwardLine(transport, line, getTokens, setTokens, getFreshTokens
906
906
  }
907
907
  }
908
908
 
909
+ // AUTH-60: a reserved, locally-answered resource — never forwarded to the real upstream
910
+ // server. Lets the exact connection asking report its own resolved clientSlug/identityLabel
911
+ // back through the MCP session itself, with no external file or PID correlation required
912
+ // (see AUTH-60's notes for why a file-based "list live connections" design was rejected).
913
+ function isWhoamiResourceRead(line) {
914
+ try {
915
+ const message = JSON.parse(line);
916
+ return message?.method === "resources/read" && message?.params?.uri === "roamer://whoami";
917
+ } catch {
918
+ return false;
919
+ }
920
+ }
921
+
922
+ function respondWhoami(line, clientSlug, identityLabel) {
923
+ let id = null;
924
+ try {
925
+ id = JSON.parse(line)?.id ?? null;
926
+ } catch {
927
+ // Malformed input never had a usable id anyway — respond with null per JSON-RPC convention.
928
+ }
929
+ const payload = { clientSlug: clientSlug ?? null, identityLabel: identityLabel ?? null };
930
+ const response = {
931
+ jsonrpc: "2.0",
932
+ id,
933
+ result: {
934
+ contents: [{ uri: "roamer://whoami", mimeType: "application/json", text: JSON.stringify(payload, null, 2) }],
935
+ },
936
+ };
937
+ process.stdout.write(`${JSON.stringify(response)}\n`);
938
+ }
939
+
909
940
  // AUTH-28: a hard sign-in failure (denied/expired device code) must tell the host exactly
910
941
  // that, per request, rather than leaving requests unanswered for the host to time out on.
911
942
  function respondWithSignInError(line, err) {
@@ -934,6 +965,13 @@ async function main() {
934
965
  let ready = false;
935
966
  const pendingLines = [];
936
967
 
968
+ // AUTH-60: identityLabel is resolved independently of `ready` (well before transport
969
+ // connects — see identityResolved below), so roamer://whoami can answer even while the
970
+ // real upstream connection is still coming up.
971
+ let identityLabel = null;
972
+ let identityResolved = false;
973
+ const pendingWhoamiLines = [];
974
+
937
975
  // AUTH-51: which client is asking, resolved once from the first stdin line (the MCP
938
976
  // `initialize` request, always message #1 by protocol) before any credential is touched.
939
977
  let clientSlug = null;
@@ -970,6 +1008,12 @@ async function main() {
970
1008
  clientSlug = clientSlugFromInitializeLine(line);
971
1009
  resolveFirstLine();
972
1010
  }
1011
+ // AUTH-60: answered locally from this process's own state, never forwarded upstream.
1012
+ if (isWhoamiResourceRead(line)) {
1013
+ if (identityResolved) respondWhoami(line, clientSlug, identityLabel);
1014
+ else pendingWhoamiLines.push(line);
1015
+ continue;
1016
+ }
973
1017
  if (ready) {
974
1018
  forwardLine(transport, line, getTokens, setTokens, freshTokensForForward);
975
1019
  } else {
@@ -1013,7 +1057,6 @@ async function main() {
1013
1057
  freshTokensForForward = (force) => getFreshTokens(clientSlug, force).then((result) => result.tokens);
1014
1058
 
1015
1059
  let reusedSilently = false;
1016
- let identityLabel = null;
1017
1060
  try {
1018
1061
  const result = await getFreshTokens(clientSlug);
1019
1062
  tokens = result.tokens;
@@ -1028,6 +1071,9 @@ async function main() {
1028
1071
  // explicit failure instead of a process it has to silently time out on.
1029
1072
  log(`Sign-in failed: ${err.message}`);
1030
1073
  for (const line of pendingLines) respondWithSignInError(line, err);
1074
+ // AUTH-60: a whoami read queued during a sign-in that then fails can never get a real
1075
+ // answer — same failure treatment as every other queued line, not left hanging.
1076
+ for (const line of pendingWhoamiLines) respondWithSignInError(line, err);
1031
1077
  process.exit(1);
1032
1078
  }
1033
1079
 
@@ -1037,6 +1083,11 @@ async function main() {
1037
1083
  // instant a transient OS notification fired.
1038
1084
  log(identityLabel ? `Authenticated as ${identityLabel} for client "${clientSlug}".` : `Authenticated for client "${clientSlug}" (no identity claim found on the token).`);
1039
1085
 
1086
+ // AUTH-60: identityLabel is now final — answer any roamer://whoami reads that arrived
1087
+ // before sign-in completed, the same way pendingLines get flushed once ready flips true.
1088
+ identityResolved = true;
1089
+ for (const line of pendingWhoamiLines) respondWhoami(line, clientSlug, identityLabel);
1090
+
1040
1091
  // AUTH-51: the moment that used to be completely silent — a process starting up and
1041
1092
  // immediately using a credential it never interactively obtained this run.
1042
1093
  if (reusedSilently) await notifySessionReused(clientSlug, identityLabel);
@@ -1240,4 +1291,7 @@ export {
1240
1291
  loginCommand,
1241
1292
  logoutCommand,
1242
1293
  statusCommand,
1294
+ // AUTH-60
1295
+ isWhoamiResourceRead,
1296
+ respondWhoami,
1243
1297
  };