@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 +1 -1
- package/roamer-device-auth.mjs +62 -5
package/package.json
CHANGED
package/roamer-device-auth.mjs
CHANGED
|
@@ -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
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
|
|
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
|
};
|