@wirecat/cli-core 0.541.0 → 0.542.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/CHANGELOG.md CHANGED
@@ -7,6 +7,15 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.542.0 — 02.11.2026
11
+
12
+ ### Added
13
+
14
+ - **`redirect: { kind: "loopback", host: "localhost" }` signs in through `http://localhost:<port>/callback`.** Microsoft
15
+ Entra ignores the port only for `localhost` redirect URIs, so a public-client app registered as
16
+ `http://localhost/callback` needs it. The listener then takes `127.0.0.1` and, when the host has IPv6, `::1`, because
17
+ the browser may resolve `localhost` to either. Without `host` nothing changes: Google and the rest keep `127.0.0.1`.
18
+
10
19
  ## 0.541.0 — 02.11.2026
11
20
 
12
21
  ### Added
@@ -46,7 +46,7 @@ export const authorize = async (options) => {
46
46
  else
47
47
  fail(new CliError("authentication_error", `${provider.name} sign-in was declined`, { reason: "oauth.declined" }));
48
48
  };
49
- const server = createServer((request, response) => {
49
+ const handler = (request, response) => {
50
50
  let url;
51
51
  try {
52
52
  url = new URL(request.url ?? "/", "http://127.0.0.1");
@@ -69,7 +69,11 @@ export const authorize = async (options) => {
69
69
  }
70
70
  response.end("Sign-in received; you can close this tab");
71
71
  settle(url.searchParams);
72
- });
72
+ };
73
+ const server = createServer(handler);
74
+ const localhost = provider.redirect.kind === "loopback" && provider.redirect.host === "localhost";
75
+ // `localhost` may resolve to ::1 in the browser; the IPv6 listener is best effort, as some hosts have no IPv6.
76
+ const server6 = localhost ? createServer(handler) : undefined;
73
77
  const onAbort = () => fail(cancelled());
74
78
  options.signal?.addEventListener("abort", onAbort, { once: true });
75
79
  const timer = setTimeout(() => fail(new CliError("timeout", `${provider.name} sign-in timed out`, { reason: "oauth.timeout" })), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
@@ -81,10 +85,19 @@ export const authorize = async (options) => {
81
85
  const address = server.address();
82
86
  if (!address || typeof address === "string")
83
87
  throw new CliError("configuration_error", "could not start the local sign-in listener");
88
+ if (server6) {
89
+ server6.on("error", () => { });
90
+ await new Promise((resolve) => {
91
+ server6.once("error", () => resolve());
92
+ server6.listen(address.port, "::1", () => resolve());
93
+ });
94
+ }
84
95
  const random = randomBytes(32).toString("base64url");
85
96
  const relay = provider.redirect.kind === "relay";
86
97
  state = relay ? `${address.port}.${random}` : random;
87
- const redirectUri = provider.redirect.kind === "relay" ? provider.redirect.url : `http://127.0.0.1:${address.port}/callback`;
98
+ const redirectUri = provider.redirect.kind === "relay"
99
+ ? provider.redirect.url
100
+ : `http://${localhost ? "localhost" : "127.0.0.1"}:${address.port}/callback`;
88
101
  const url = new URL(provider.authorizeUrl);
89
102
  for (const [key, value] of Object.entries({
90
103
  ...provider.authorizeParams,
@@ -124,7 +137,10 @@ export const authorize = async (options) => {
124
137
  clearTimeout(timer);
125
138
  options.signal?.removeEventListener("abort", onAbort);
126
139
  paste.abort();
127
- server.closeAllConnections();
128
- await new Promise((resolve) => server.close(() => resolve()));
140
+ for (const each of server6 ? [server, server6] : [server]) {
141
+ each.closeAllConnections();
142
+ if (each.listening)
143
+ await new Promise((resolve) => each.close(() => resolve()));
144
+ }
129
145
  }
130
146
  };
@@ -39,11 +39,13 @@ export interface OAuthProvider {
39
39
  revokeUrl?: string;
40
40
  client: OAuthClient;
41
41
  /**
42
- * `loopback` sends the browser to `http://127.0.0.1:<port>/callback`. `relay` sends it to a fixed page that
42
+ * `loopback` sends the browser to `http://127.0.0.1:<port>/callback`, or to `http://localhost:<port>/callback` with
43
+ * `host: "localhost"` (the listener then takes `127.0.0.1` and `::1`). `relay` sends it to a fixed page that
43
44
  * forwards to the loopback port, which it reads from `state` (`<port>.<random>`).
44
45
  */
45
46
  redirect: {
46
47
  kind: "loopback";
48
+ host?: "localhost";
47
49
  } | {
48
50
  kind: "relay";
49
51
  url: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.541.0",
3
+ "version": "0.542.0",
4
4
  "description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",