@bubblebrain-ai/bubble 0.0.45 → 0.0.46
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/dist/oauth/grok.js +53 -27
- package/dist/oauth/openai-codex.js +13 -0
- package/package.json +1 -1
package/dist/oauth/grok.js
CHANGED
|
@@ -34,11 +34,28 @@ function openBrowser(url) {
|
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* xAI's OAuth client allows dynamic loopback ports, so listen on an ephemeral
|
|
37
|
-
* port
|
|
37
|
+
* port. Awaiting this returns only once the socket is actually bound, so the
|
|
38
|
+
* caller can never build an authorization URL around an empty redirect URI.
|
|
38
39
|
*/
|
|
39
|
-
async function startCallbackServer(timeoutMs,
|
|
40
|
-
|
|
40
|
+
async function startCallbackServer(timeoutMs, onStatus) {
|
|
41
|
+
let settle = () => { };
|
|
42
|
+
let fail = () => { };
|
|
43
|
+
const result = new Promise((resolve, reject) => {
|
|
44
|
+
settle = resolve;
|
|
45
|
+
fail = reject;
|
|
46
|
+
});
|
|
47
|
+
// Nothing awaits `result` until the caller has a redirect URI, and an early
|
|
48
|
+
// rejection (bind failure) would otherwise be an unhandled rejection.
|
|
49
|
+
result.catch(() => { });
|
|
50
|
+
return new Promise((resolveListening, rejectListening) => {
|
|
41
51
|
let resolved = false;
|
|
52
|
+
const finish = (action) => {
|
|
53
|
+
if (resolved)
|
|
54
|
+
return;
|
|
55
|
+
resolved = true;
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
server.close(action);
|
|
58
|
+
};
|
|
42
59
|
const server = createServer((req, res) => {
|
|
43
60
|
try {
|
|
44
61
|
const port = server.address().port;
|
|
@@ -51,19 +68,13 @@ async function startCallbackServer(timeoutMs, onListening, onStatus) {
|
|
|
51
68
|
if (error) {
|
|
52
69
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
53
70
|
res.end(`<html><body><h1>Authorization failed</h1><p>${errorDesc || error}</p></body></html>`);
|
|
54
|
-
|
|
55
|
-
resolved = true;
|
|
56
|
-
server.close(() => reject(new Error(`OAuth error: ${errorDesc || error}`)));
|
|
57
|
-
}
|
|
71
|
+
finish(() => fail(new Error(`OAuth error: ${errorDesc || error}`)));
|
|
58
72
|
return;
|
|
59
73
|
}
|
|
60
74
|
if (code) {
|
|
61
75
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
62
76
|
res.end("<html><body><h1>Authorization successful</h1><p>You can close this window and return to the terminal.</p></body></html>");
|
|
63
|
-
|
|
64
|
-
resolved = true;
|
|
65
|
-
server.close(() => resolve({ code, state: state || "" }));
|
|
66
|
-
}
|
|
77
|
+
finish(() => settle({ code, state: state || "" }));
|
|
67
78
|
return;
|
|
68
79
|
}
|
|
69
80
|
}
|
|
@@ -75,17 +86,32 @@ async function startCallbackServer(timeoutMs, onListening, onStatus) {
|
|
|
75
86
|
res.end("Error");
|
|
76
87
|
}
|
|
77
88
|
});
|
|
89
|
+
// Unref'd so a pending login never keeps the process alive on its own,
|
|
90
|
+
// and cleared on completion so a successful login does not leave the
|
|
91
|
+
// event loop holding a five-minute timer.
|
|
92
|
+
const timer = setTimeout(() => {
|
|
93
|
+
finish(() => fail(new Error(`OAuth login timed out after ${timeoutMs / 1000}s`)));
|
|
94
|
+
}, timeoutMs);
|
|
95
|
+
timer.unref?.();
|
|
96
|
+
// Without this, a failed bind (port exhausted, sandbox denies listening)
|
|
97
|
+
// never rejects anything — the login just hangs until the timeout.
|
|
98
|
+
server.on("error", (error) => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
if (resolved)
|
|
101
|
+
return;
|
|
102
|
+
resolved = true;
|
|
103
|
+
rejectListening(error instanceof Error ? error : new Error(String(error)));
|
|
104
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
105
|
+
});
|
|
78
106
|
server.listen(0, "127.0.0.1", () => {
|
|
79
107
|
const port = server.address().port;
|
|
80
108
|
onStatus?.(`Local server listening on http://127.0.0.1:${port}`);
|
|
81
|
-
|
|
109
|
+
// Resolve only once the socket is genuinely bound. The caller used to
|
|
110
|
+
// race this with a single setImmediate tick and declare failure if the
|
|
111
|
+
// listen callback had not fired yet — which it frequently has not, since
|
|
112
|
+
// binding is real async I/O.
|
|
113
|
+
resolveListening({ redirectUri: `http://127.0.0.1:${port}/callback`, result });
|
|
82
114
|
});
|
|
83
|
-
setTimeout(() => {
|
|
84
|
-
if (!resolved) {
|
|
85
|
-
resolved = true;
|
|
86
|
-
server.close(() => reject(new Error(`OAuth login timed out after ${timeoutMs / 1000}s`)));
|
|
87
|
-
}
|
|
88
|
-
}, timeoutMs);
|
|
89
115
|
});
|
|
90
116
|
}
|
|
91
117
|
function parseTokenResponse(data, fallbackRefreshToken) {
|
|
@@ -109,15 +135,15 @@ export async function loginGrok(callbacks, options = {}) {
|
|
|
109
135
|
const pkce = generatePKCE();
|
|
110
136
|
const state = generateOpaqueId();
|
|
111
137
|
const nonce = generateOpaqueId();
|
|
112
|
-
let redirectUri
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
throw new Error(
|
|
138
|
+
let redirectUri;
|
|
139
|
+
let serverPromise;
|
|
140
|
+
try {
|
|
141
|
+
const server = await startCallbackServer(5 * 60 * 1000, callbacks?.onStatus);
|
|
142
|
+
redirectUri = server.redirectUri;
|
|
143
|
+
serverPromise = server.result;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
throw new Error(`The local OAuth callback server could not start: ${error instanceof Error ? error.message : String(error)}`);
|
|
121
147
|
}
|
|
122
148
|
const params = new URLSearchParams({
|
|
123
149
|
response_type: "code",
|
|
@@ -63,6 +63,19 @@ async function startCallbackServer(port, timeoutMs, onStatus) {
|
|
|
63
63
|
res.end("Error");
|
|
64
64
|
}
|
|
65
65
|
});
|
|
66
|
+
// This flow binds a FIXED port (the provider only whitelists one redirect
|
|
67
|
+
// URI), so EADDRINUSE is a realistic outcome — a stale login, or any other
|
|
68
|
+
// process on the port. Without this handler nothing rejects and the user
|
|
69
|
+
// stares at "waiting for authorization" for the full timeout.
|
|
70
|
+
server.on("error", (error) => {
|
|
71
|
+
if (resolved)
|
|
72
|
+
return;
|
|
73
|
+
resolved = true;
|
|
74
|
+
const detail = error?.code === "EADDRINUSE"
|
|
75
|
+
? `port ${port} is already in use — close any other sign-in still running and retry`
|
|
76
|
+
: error instanceof Error ? error.message : String(error);
|
|
77
|
+
reject(new Error(`The local OAuth callback server could not start: ${detail}`));
|
|
78
|
+
});
|
|
66
79
|
server.listen(port, "127.0.0.1", () => {
|
|
67
80
|
onStatus?.(`Local server listening on http://127.0.0.1:${port}`);
|
|
68
81
|
});
|