nolo-cli 0.8.0 → 0.9.1
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/_executeModel-UD44KGZ4.js +203 -0
- package/action-ZJWE3Y5S.js +28 -0
- package/addContentAction-TVDBNJM5.js +26 -0
- package/addDialogAgentAction-EKH774VT.js +68 -0
- package/agentDeleteCommand-TCPEFJTF.js +327 -0
- package/agentEmailCommands-CZ3UILZ4.js +626 -0
- package/agentListCommands-SJKO2FFB.js +223 -0
- package/agentMachineCommands-PSNABISZ.js +425 -0
- package/agentPullCommand-O2MTUAFR.js +122 -0
- package/agentRecordCommands-MH2JKFHS.js +94 -0
- package/agentRunCommand-4KH6MIZR.js +110 -0
- package/agentRunControl-K4VKNEHF.js +57 -0
- package/agentRuntimeLocal-SKHJUWSS.js +104 -0
- package/agentSlice-3WXXN4VK.js +42 -0
- package/authCommand-PXFX6JDT.js +581 -0
- package/authCommands-356AYWEK.js +35 -0
- package/chunk-2EISRLRC.js +238 -0
- package/chunk-2FTTDCD4.js +675 -0
- package/chunk-2K2ECKSG.js +222 -0
- package/chunk-2PUGVR6E.js +19 -0
- package/chunk-3ASDTTEI.js +1774 -0
- package/chunk-3EUW3WEO.js +8 -0
- package/chunk-3LY4F7SV.js +9293 -0
- package/chunk-3TZDXYSF.js +71 -0
- package/chunk-3WYQEUBI.js +34 -0
- package/chunk-4CQLVGRV.js +50 -0
- package/chunk-4IJYA27C.js +342 -0
- package/chunk-4K2KKCSD.js +577 -0
- package/chunk-4SN6J65J.js +38 -0
- package/chunk-4VNS5WPM.js +42 -0
- package/chunk-4ZDM73SD.js +34 -0
- package/chunk-5CS33IBL.js +301 -0
- package/chunk-5KLGBAAR.js +386 -0
- package/chunk-5MIDPSSK.js +206 -0
- package/chunk-5MMAK75Q.js +15 -0
- package/chunk-5PJ3VWCS.js +360 -0
- package/chunk-63SLEOLK.js +175 -0
- package/chunk-63W5N6P3.js +150 -0
- package/chunk-6GANEXVO.js +231 -0
- package/chunk-6HYVJV7B.js +102 -0
- package/chunk-6WD3OOJS.js +20 -0
- package/chunk-7OOXPE3N.js +628 -0
- package/chunk-7QRKCBOP.js +1078 -0
- package/chunk-ATNTHIZI.js +13 -0
- package/chunk-AUQ747NZ.js +902 -0
- package/chunk-AVMKZHBC.js +85 -0
- package/chunk-AZPCASRE.js +36 -0
- package/chunk-BBYVOSU2.js +792 -0
- package/chunk-C3RUNRHT.js +51 -0
- package/chunk-CF2WZF4N.js +134 -0
- package/chunk-CF7PE763.js +10 -0
- package/chunk-CKOJQDKR.js +198 -0
- package/chunk-COMXDWNM.js +807 -0
- package/chunk-CPB2YM4D.js +171 -0
- package/chunk-CWMSB67P.js +34 -0
- package/chunk-DCOA4QSN.js +105 -0
- package/chunk-DFFIZ25G.js +30 -0
- package/chunk-DWLEOOGR.js +8 -0
- package/chunk-DZ3XJVET.js +231 -0
- package/chunk-EHLHSX7R.js +1433 -0
- package/chunk-ELRRLDWI.js +478 -0
- package/chunk-EXTIQUCI.js +482 -0
- package/chunk-EYEEHCU4.js +3066 -0
- package/chunk-G3SUCT4W.js +8 -0
- package/chunk-G43KEVSC.js +1447 -0
- package/chunk-GAKEBQD6.js +30 -0
- package/chunk-GBRG75BQ.js +86 -0
- package/chunk-GG4AJPSR.js +191 -0
- package/chunk-H33743XH.js +25 -0
- package/chunk-HNMXYYJR.js +12 -0
- package/chunk-I42VC5KR.js +15 -0
- package/chunk-IDZTNRP2.js +85 -0
- package/chunk-ISNCLGUZ.js +146 -0
- package/chunk-IWX4UW2E.js +548 -0
- package/chunk-JBTJAPUF.js +35 -0
- package/chunk-JEXVIMYZ.js +55 -0
- package/chunk-JRKKFRAV.js +537 -0
- package/chunk-JZPBVBIY.js +94 -0
- package/chunk-KKY4B2Q2.js +21 -0
- package/chunk-KOCA5FCS.js +4750 -0
- package/chunk-KQHGZWSV.js +63 -0
- package/chunk-KTVVPRJS.js +138 -0
- package/chunk-KUG52IAP.js +8 -0
- package/chunk-KX6YBIHG.js +8 -0
- package/chunk-LARL2V6G.js +9017 -0
- package/chunk-LCBAMY2M.js +60 -0
- package/chunk-LM6IYQ3F.js +16 -0
- package/chunk-M76ECQRD.js +13658 -0
- package/chunk-MGOJ5R6J.js +113 -0
- package/chunk-MKMWT7SA.js +27196 -0
- package/chunk-MRC4Q34M.js +188 -0
- package/chunk-OLZBQWZU.js +6 -0
- package/chunk-OPZYGHUC.js +89 -0
- package/chunk-OWOHNGCN.js +50 -0
- package/chunk-PAKN26FP.js +239 -0
- package/chunk-PCWC3JTN.js +91 -0
- package/chunk-PG5PNA7Y.js +939 -0
- package/chunk-Q2YZULBH.js +104 -0
- package/chunk-QSJ3HFEF.js +8 -0
- package/chunk-QYXLLLGL.js +4519 -0
- package/chunk-ROWQEJRP.js +139 -0
- package/chunk-S7Z2HMCN.js +25 -0
- package/chunk-SBR32ZZS.js +1901 -0
- package/chunk-SH2CFQOH.js +3391 -0
- package/chunk-SL35K7AH.js +226 -0
- package/chunk-TILFE6LV.js +381 -0
- package/chunk-TSS3XJY4.js +2052 -0
- package/chunk-U4TRT5V4.js +246 -0
- package/chunk-U6VC4MU4.js +8 -0
- package/chunk-UAD2WT54.js +64 -0
- package/chunk-UUJDGGAK.js +421 -0
- package/chunk-VF4K2QE6.js +21 -0
- package/chunk-VHBWHNVT.js +59 -0
- package/chunk-VIDPVFP3.js +20 -0
- package/chunk-VP4WJWKW.js +8882 -0
- package/chunk-W4NVOSFF.js +37 -0
- package/chunk-WKM6FH35.js +190 -0
- package/chunk-WWC7I6TA.js +8 -0
- package/chunk-X7W4TNB4.js +1550 -0
- package/chunk-XE635NI4.js +1948 -0
- package/chunk-XKIMRZO2.js +221 -0
- package/chunk-XS466O7S.js +933 -0
- package/chunk-Y5GCAMGP.js +9 -0
- package/chunk-YBWOP46I.js +3272 -0
- package/chunk-YPRZSPF3.js +148 -0
- package/chunk-YYUCRDOK.js +69 -0
- package/chunk-Z4NR32XR.js +50 -0
- package/chunk-ZIG5LJ7X.js +24765 -0
- package/chunk-ZNFJWEN4.js +56 -0
- package/chunk-ZQ2UM4KG.js +2878 -0
- package/cliExecutor-DNE7TCC4.js +36 -0
- package/clipboardy-BVE3LVEH.js +12001 -0
- package/createAgentAutomationAction-63OIP6X5.js +866 -0
- package/createDialogAction-ZLYZDYBB.js +54 -0
- package/createPageAction-AL5FSDCK.js +171 -0
- package/db-HCJ5PRB5.js +271 -0
- package/dbSlice-PR44QPXY.js +60 -0
- package/deleteFile-4SKGPHOZ.js +41 -0
- package/dialogCommands-F6YI5742.js +63 -0
- package/dialogSlice-JSBNZRLR.js +148 -0
- package/docCreateCommands-4CTO7ERT.js +219 -0
- package/docDeleteCommands-VBWERDRG.js +180 -0
- package/docReadCommands-KNP4MFYB.js +133 -0
- package/docStore-ADP5VYY4.js +98 -0
- package/docUpdateCommands-4BCBJQHA.js +317 -0
- package/fetchMessages-U6ZHN56A.js +17 -0
- package/getPricing-3E6UNPKL.js +30 -0
- package/handleSendMessageAction-IJDQ6ARA.js +185 -0
- package/index.js +188 -112943
- package/lib-FYYBQIXS.js +19839 -0
- package/localDialogRead-Z5UTAJPU.js +26 -0
- package/localFirstLog-DHNOY2NA.js +19 -0
- package/localRuntimeAuthority-TEGCOWX4.js +471 -0
- package/localRuntimeDb-LGDF6FQP.js +12 -0
- package/machineCommands-EA3M4XTN.js +779 -0
- package/machineInfo-M6KB6HZN.js +10 -0
- package/machineWsRunDispatch-NAPKE5ZA.js +110 -0
- package/memoryCommands-O4MIPIWQ.js +476 -0
- package/models-4OPOOLFS.js +9 -0
- package/offlineMarxistsAgentCommand-BCPJITJL.js +242 -0
- package/package.json +3 -25
- package/read-RIJCIVKX.js +17 -0
- package/readDocTool-GZD2N3LO.js +16 -0
- package/readlineWorkspace-KMWT4T4S.js +5509 -0
- package/removeDialogAgentAction-GJQJNDW6.js +73 -0
- package/runAgentClientLoop-U5Q6NDTJ.js +291 -0
- package/runtimeDoctorCommands-LRHLPDRC.js +289 -0
- package/setDialogExtraReferencesAction-M7OQG54N.js +60 -0
- package/setPrimaryDialogAgentAction-46UNFJDL.js +80 -0
- package/settingSlice-FVUUH4JA.js +199 -0
- package/skillSummaryMarker-42HKSXYY.js +9 -0
- package/slateToRenderMarkdown-Y5IYN73U.js +7 -0
- package/slateUtils-2M7MUTWX.js +19 -0
- package/spaceCommands-E2CFRPZJ.js +454 -0
- package/spaceSlice-L3R6IMXE.js +147 -0
- package/streamAgentChatTurn-UBDAOQWE.js +4528 -0
- package/tableCommands-GOFP7QKA.js +1286 -0
- package/toolApiClient-QI3YBNWB.js +26 -0
- package/tools-TMJXZTCA.js +85 -0
- package/tuiSummaryLlmCaller-RV3THLZF.js +78 -0
- package/understanding-GQKNDDWM.js +271 -0
- package/updateCommands-VOSPKXFF.js +31 -0
- package/updateDialogTitleAction-ZTWRDXI2.js +188 -0
|
@@ -0,0 +1,1433 @@
|
|
|
1
|
+
import {
|
|
2
|
+
writeOAuthCredential
|
|
3
|
+
} from "./chunk-JZPBVBIY.js";
|
|
4
|
+
import {
|
|
5
|
+
toErrorMessage
|
|
6
|
+
} from "./chunk-HNMXYYJR.js";
|
|
7
|
+
import {
|
|
8
|
+
asOptionalFiniteNumber
|
|
9
|
+
} from "./chunk-WWC7I6TA.js";
|
|
10
|
+
import {
|
|
11
|
+
asTrimmedString
|
|
12
|
+
} from "./chunk-3EUW3WEO.js";
|
|
13
|
+
import {
|
|
14
|
+
isRecord
|
|
15
|
+
} from "./chunk-KUG52IAP.js";
|
|
16
|
+
|
|
17
|
+
// packages/cli/oauth/callback-server.ts
|
|
18
|
+
import { createServer } from "node:http";
|
|
19
|
+
var DEFAULT_CALLBACK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
20
|
+
function parseQuery(url) {
|
|
21
|
+
const questionIndex = url.indexOf("?");
|
|
22
|
+
if (questionIndex < 0) return {};
|
|
23
|
+
const search = url.slice(questionIndex + 1);
|
|
24
|
+
const params = new URLSearchParams(search);
|
|
25
|
+
const result = {};
|
|
26
|
+
for (const [key, value] of params) {
|
|
27
|
+
result[key] = value;
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
var SUCCESS_HTML = `<!doctype html>
|
|
32
|
+
<html><head><meta charset="utf-8"><title>Authorization complete</title></head>
|
|
33
|
+
<body><h2>Authorization complete</h2><p>You can close this tab and return to the terminal.</p></body></html>`;
|
|
34
|
+
var ERROR_HTML = `<!doctype html>
|
|
35
|
+
<html><head><meta charset="utf-8"><title>Authorization error</title></head>
|
|
36
|
+
<body><h2>Authorization error</h2><p>No authorization code was received. Please try again.</p></body></html>`;
|
|
37
|
+
function startCallbackServerOnPort(options) {
|
|
38
|
+
const hostname = options.hostname ?? "localhost";
|
|
39
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_CALLBACK_TIMEOUT_MS;
|
|
40
|
+
const now = options.now ?? Date.now;
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const deadline = now() + timeoutMs;
|
|
43
|
+
let result = null;
|
|
44
|
+
let resultResolve = null;
|
|
45
|
+
let resultReject = null;
|
|
46
|
+
let settled = false;
|
|
47
|
+
let listening = false;
|
|
48
|
+
const codePromise = new Promise((res, rej) => {
|
|
49
|
+
resultResolve = res;
|
|
50
|
+
resultReject = rej;
|
|
51
|
+
});
|
|
52
|
+
const timeoutTimer = setTimeout(() => {
|
|
53
|
+
if (settled) return;
|
|
54
|
+
settled = true;
|
|
55
|
+
const err = new Error(`OAuth callback timed out after ${Math.round(timeoutMs / 1e3)}s`);
|
|
56
|
+
server.close();
|
|
57
|
+
resultReject?.(err);
|
|
58
|
+
}, Math.max(0, deadline - now()));
|
|
59
|
+
const server = createServer((req, res) => {
|
|
60
|
+
if (!req.url) {
|
|
61
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
62
|
+
res.end(ERROR_HTML);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const query = parseQuery(req.url);
|
|
66
|
+
const code = query.code;
|
|
67
|
+
if (!code) {
|
|
68
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
69
|
+
res.end(ERROR_HTML);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
result = { code, ...query.state ? { state: query.state } : {} };
|
|
73
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
74
|
+
res.end(SUCCESS_HTML);
|
|
75
|
+
if (!settled) {
|
|
76
|
+
settled = true;
|
|
77
|
+
clearTimeout(timeoutTimer);
|
|
78
|
+
server.close();
|
|
79
|
+
resultResolve?.(result);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
server.on("error", (err) => {
|
|
83
|
+
if (settled) return;
|
|
84
|
+
settled = true;
|
|
85
|
+
clearTimeout(timeoutTimer);
|
|
86
|
+
server.close();
|
|
87
|
+
if (listening) {
|
|
88
|
+
resultReject?.(err);
|
|
89
|
+
} else {
|
|
90
|
+
reject(err);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
server.listen(options.port, hostname, () => {
|
|
94
|
+
listening = true;
|
|
95
|
+
const address = server.address();
|
|
96
|
+
if (!address || typeof address === "string") {
|
|
97
|
+
const err = new Error("OAuth callback server did not bind a TCP port.");
|
|
98
|
+
settled = true;
|
|
99
|
+
clearTimeout(timeoutTimer);
|
|
100
|
+
server.close();
|
|
101
|
+
reject(err);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
resolve({
|
|
105
|
+
server,
|
|
106
|
+
port: address.port,
|
|
107
|
+
waitForCode: () => codePromise,
|
|
108
|
+
close: () => {
|
|
109
|
+
clearTimeout(timeoutTimer);
|
|
110
|
+
settled = true;
|
|
111
|
+
return new Promise((res) => server.close(() => res()));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function isAddressInUse(error) {
|
|
118
|
+
return !!error && typeof error === "object" && "code" in error && error.code === "EADDRINUSE";
|
|
119
|
+
}
|
|
120
|
+
async function startCallbackServer(options) {
|
|
121
|
+
try {
|
|
122
|
+
return await startCallbackServerOnPort(options);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (!options.fallbackToRandomPort || options.port === 0 || !isAddressInUse(error)) {
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
return startCallbackServerOnPort({ ...options, port: 0 });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// packages/cli/oauth/pkce.ts
|
|
132
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
133
|
+
var PKCE_VERIFIER_LENGTH = 64;
|
|
134
|
+
function generatePkceVerifier(length = PKCE_VERIFIER_LENGTH) {
|
|
135
|
+
if (length < 43 || length > 128) {
|
|
136
|
+
throw new Error("PKCE verifier length must be between 43 and 128 characters.");
|
|
137
|
+
}
|
|
138
|
+
return randomBytes(length).toString("base64url").slice(0, length);
|
|
139
|
+
}
|
|
140
|
+
function computePkceChallenge(verifier) {
|
|
141
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
142
|
+
}
|
|
143
|
+
function generatePkcePair(length = PKCE_VERIFIER_LENGTH) {
|
|
144
|
+
const verifier = generatePkceVerifier(length);
|
|
145
|
+
return {
|
|
146
|
+
verifier,
|
|
147
|
+
challenge: computePkceChallenge(verifier),
|
|
148
|
+
method: "S256"
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// packages/cli/oauth/flows/antigravity.ts
|
|
153
|
+
var decode = (s) => Buffer.from(s, "base64").toString("utf8");
|
|
154
|
+
var CLIENT_ID = decode(
|
|
155
|
+
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ=="
|
|
156
|
+
);
|
|
157
|
+
var CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
|
|
158
|
+
var CALLBACK_PORT = 51121;
|
|
159
|
+
var CALLBACK_PATH = "/oauth-callback";
|
|
160
|
+
var SCOPES = [
|
|
161
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
162
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
163
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
164
|
+
"https://www.googleapis.com/auth/cclog",
|
|
165
|
+
"https://www.googleapis.com/auth/experimentsandconfigs"
|
|
166
|
+
];
|
|
167
|
+
var AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
168
|
+
var TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
169
|
+
var USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json";
|
|
170
|
+
var CLOUD_CODE_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
171
|
+
var TIER_LEGACY = "legacy-tier";
|
|
172
|
+
var PROJECT_ONBOARD_MAX_ATTEMPTS = 5;
|
|
173
|
+
var PROJECT_ONBOARD_INTERVAL_MS = 2e3;
|
|
174
|
+
var ACCESS_TOKEN_CLIENT_SKEW_MS = 5 * 60 * 1e3;
|
|
175
|
+
var ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA = Object.freeze({
|
|
176
|
+
ideType: "ANTIGRAVITY",
|
|
177
|
+
platform: "PLATFORM_UNSPECIFIED",
|
|
178
|
+
pluginType: "GEMINI"
|
|
179
|
+
});
|
|
180
|
+
function readProjectId(value) {
|
|
181
|
+
if (typeof value === "string" && value.length > 0) {
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
if (value && typeof value === "object" && typeof value.id === "string" && value.id.length > 0) {
|
|
185
|
+
return value.id;
|
|
186
|
+
}
|
|
187
|
+
return void 0;
|
|
188
|
+
}
|
|
189
|
+
function getDefaultTierId(allowedTiers) {
|
|
190
|
+
if (!allowedTiers || allowedTiers.length === 0) {
|
|
191
|
+
return TIER_LEGACY;
|
|
192
|
+
}
|
|
193
|
+
const defaultTier = allowedTiers.find(
|
|
194
|
+
(tier) => tier.isDefault && typeof tier.id === "string" && tier.id.length > 0
|
|
195
|
+
);
|
|
196
|
+
if (defaultTier?.id) {
|
|
197
|
+
return defaultTier.id;
|
|
198
|
+
}
|
|
199
|
+
return TIER_LEGACY;
|
|
200
|
+
}
|
|
201
|
+
async function sleep(ms) {
|
|
202
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
203
|
+
setTimeout(resolve, ms);
|
|
204
|
+
return promise;
|
|
205
|
+
}
|
|
206
|
+
async function onboardProjectWithRetries(fetchImpl, endpoint, headers, onboardBody, onProgress) {
|
|
207
|
+
for (let attempt = 1; attempt <= PROJECT_ONBOARD_MAX_ATTEMPTS; attempt += 1) {
|
|
208
|
+
if (attempt > 1) {
|
|
209
|
+
onProgress?.(
|
|
210
|
+
`Waiting for project provisioning (attempt ${attempt}/${PROJECT_ONBOARD_MAX_ATTEMPTS})...`
|
|
211
|
+
);
|
|
212
|
+
await sleep(PROJECT_ONBOARD_INTERVAL_MS);
|
|
213
|
+
}
|
|
214
|
+
const onboardResponse = await fetchImpl(
|
|
215
|
+
`${endpoint}/v1internal:onboardUser`,
|
|
216
|
+
{
|
|
217
|
+
method: "POST",
|
|
218
|
+
headers,
|
|
219
|
+
body: JSON.stringify(onboardBody)
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
if (!onboardResponse.ok) {
|
|
223
|
+
const errorText = await onboardResponse.text();
|
|
224
|
+
throw new Error(
|
|
225
|
+
`onboardUser failed: ${onboardResponse.status} ${onboardResponse.statusText}: ${errorText}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const operation = await onboardResponse.json();
|
|
229
|
+
if (!operation.done) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const projectId = readProjectId(operation.response?.cloudaicompanionProject);
|
|
233
|
+
if (projectId) {
|
|
234
|
+
return projectId;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
throw new Error(
|
|
238
|
+
`onboardUser did not return a provisioned project id after ${PROJECT_ONBOARD_MAX_ATTEMPTS} attempts`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
async function discoverProject(fetchImpl, accessToken, onProgress) {
|
|
242
|
+
const headers = {
|
|
243
|
+
Authorization: `Bearer ${accessToken}`,
|
|
244
|
+
"Content-Type": "application/json"
|
|
245
|
+
};
|
|
246
|
+
onProgress?.("Checking for existing project...");
|
|
247
|
+
const endpoint = CLOUD_CODE_ENDPOINT;
|
|
248
|
+
const loadResponse = await fetchImpl(`${endpoint}/v1internal:loadCodeAssist`, {
|
|
249
|
+
method: "POST",
|
|
250
|
+
headers,
|
|
251
|
+
body: JSON.stringify({
|
|
252
|
+
metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA
|
|
253
|
+
})
|
|
254
|
+
});
|
|
255
|
+
if (!loadResponse.ok) {
|
|
256
|
+
const errorText = await loadResponse.text();
|
|
257
|
+
throw new Error(
|
|
258
|
+
`loadCodeAssist failed: ${loadResponse.status} ${loadResponse.statusText}: ${errorText}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
const loadPayload = await loadResponse.json();
|
|
262
|
+
const existingProject = readProjectId(loadPayload.cloudaicompanionProject);
|
|
263
|
+
if (existingProject) {
|
|
264
|
+
return existingProject;
|
|
265
|
+
}
|
|
266
|
+
const tierId = getDefaultTierId(loadPayload.allowedTiers);
|
|
267
|
+
onProgress?.("Provisioning project...");
|
|
268
|
+
const onboardBody = {
|
|
269
|
+
tierId,
|
|
270
|
+
metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA
|
|
271
|
+
};
|
|
272
|
+
return onboardProjectWithRetries(
|
|
273
|
+
fetchImpl,
|
|
274
|
+
endpoint,
|
|
275
|
+
headers,
|
|
276
|
+
onboardBody,
|
|
277
|
+
onProgress
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
async function getUserEmail(fetchImpl, accessToken) {
|
|
281
|
+
try {
|
|
282
|
+
const response = await fetchImpl(USERINFO_URL, {
|
|
283
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
284
|
+
});
|
|
285
|
+
if (!response.ok) return void 0;
|
|
286
|
+
const data = await response.json();
|
|
287
|
+
return typeof data.email === "string" ? data.email : void 0;
|
|
288
|
+
} catch {
|
|
289
|
+
return void 0;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function generateState() {
|
|
293
|
+
const bytes = new Uint8Array(16);
|
|
294
|
+
crypto.getRandomValues(bytes);
|
|
295
|
+
return Array.from(bytes).map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
296
|
+
}
|
|
297
|
+
async function runAntigravityOAuthLogin(deps = {}) {
|
|
298
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
299
|
+
const output = deps.output ?? console;
|
|
300
|
+
const error = deps.error ?? console;
|
|
301
|
+
const redirectUri = `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
302
|
+
let handle;
|
|
303
|
+
try {
|
|
304
|
+
handle = await startCallbackServer({
|
|
305
|
+
port: CALLBACK_PORT,
|
|
306
|
+
hostname: "127.0.0.1",
|
|
307
|
+
timeoutMs: 5 * 6e4
|
|
308
|
+
});
|
|
309
|
+
} catch (err) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`Failed to start Antigravity OAuth callback server on ${CALLBACK_PORT}: ${toErrorMessage(err)}`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
const state = generateState();
|
|
316
|
+
const pkce = await generatePkcePair();
|
|
317
|
+
const authParams = new URLSearchParams({
|
|
318
|
+
client_id: CLIENT_ID,
|
|
319
|
+
response_type: "code",
|
|
320
|
+
redirect_uri: redirectUri,
|
|
321
|
+
scope: SCOPES.join(" "),
|
|
322
|
+
state,
|
|
323
|
+
access_type: "offline",
|
|
324
|
+
prompt: "consent"
|
|
325
|
+
});
|
|
326
|
+
const authUrl = `${AUTH_URL}?${authParams.toString()}`;
|
|
327
|
+
output.log(
|
|
328
|
+
`Open the following URL in your browser to log in to Antigravity:
|
|
329
|
+
${authUrl}`
|
|
330
|
+
);
|
|
331
|
+
if (deps.openBrowser) {
|
|
332
|
+
try {
|
|
333
|
+
await deps.openBrowser(authUrl);
|
|
334
|
+
} catch (err) {
|
|
335
|
+
error.error(
|
|
336
|
+
`Failed to open browser automatically: ${toErrorMessage(err)}`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const callback = await handle.waitForCode();
|
|
341
|
+
const tokenResponse = await fetchImpl(TOKEN_URL, {
|
|
342
|
+
method: "POST",
|
|
343
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
344
|
+
body: new URLSearchParams({
|
|
345
|
+
client_id: CLIENT_ID,
|
|
346
|
+
client_secret: CLIENT_SECRET,
|
|
347
|
+
code: callback.code,
|
|
348
|
+
grant_type: "authorization_code",
|
|
349
|
+
redirect_uri: redirectUri
|
|
350
|
+
})
|
|
351
|
+
});
|
|
352
|
+
if (!tokenResponse.ok) {
|
|
353
|
+
const body = await tokenResponse.text();
|
|
354
|
+
throw new Error(`Antigravity token exchange failed: ${body}`);
|
|
355
|
+
}
|
|
356
|
+
const tokenData = await tokenResponse.json();
|
|
357
|
+
if (!tokenData.refresh_token) {
|
|
358
|
+
throw new Error("No refresh token received. Please try again.");
|
|
359
|
+
}
|
|
360
|
+
const email = await getUserEmail(fetchImpl, tokenData.access_token);
|
|
361
|
+
const projectId = await discoverProject(
|
|
362
|
+
fetchImpl,
|
|
363
|
+
tokenData.access_token,
|
|
364
|
+
(message) => output.log(message)
|
|
365
|
+
);
|
|
366
|
+
const now = (deps.now ?? Date.now)();
|
|
367
|
+
return {
|
|
368
|
+
provider: "antigravity",
|
|
369
|
+
accessToken: tokenData.access_token,
|
|
370
|
+
refreshToken: tokenData.refresh_token,
|
|
371
|
+
expiresAt: now + tokenData.expires_in * 1e3 - ACCESS_TOKEN_CLIENT_SKEW_MS,
|
|
372
|
+
scope: tokenData.scope,
|
|
373
|
+
obtainedAt: now,
|
|
374
|
+
metadata: {
|
|
375
|
+
projectId,
|
|
376
|
+
email
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
} finally {
|
|
380
|
+
await handle.close();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async function refreshAntigravityOAuthToken(credential) {
|
|
384
|
+
if (!credential.refreshToken) {
|
|
385
|
+
throw new Error("Antigravity credential has no refresh_token");
|
|
386
|
+
}
|
|
387
|
+
const fetchImpl = fetch;
|
|
388
|
+
const projectId = credential.metadata?.projectId ?? void 0;
|
|
389
|
+
const response = await fetchImpl(TOKEN_URL, {
|
|
390
|
+
method: "POST",
|
|
391
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
392
|
+
body: new URLSearchParams({
|
|
393
|
+
client_id: CLIENT_ID,
|
|
394
|
+
client_secret: CLIENT_SECRET,
|
|
395
|
+
refresh_token: credential.refreshToken,
|
|
396
|
+
grant_type: "refresh_token"
|
|
397
|
+
})
|
|
398
|
+
});
|
|
399
|
+
if (!response.ok) {
|
|
400
|
+
const body = await response.text();
|
|
401
|
+
throw new Error(`Antigravity token refresh failed: ${body}`);
|
|
402
|
+
}
|
|
403
|
+
const data = await response.json();
|
|
404
|
+
const now = Date.now();
|
|
405
|
+
return {
|
|
406
|
+
...credential,
|
|
407
|
+
accessToken: data.access_token,
|
|
408
|
+
refreshToken: data.refresh_token || credential.refreshToken,
|
|
409
|
+
expiresAt: now + data.expires_in * 1e3 - ACCESS_TOKEN_CLIENT_SKEW_MS,
|
|
410
|
+
scope: data.scope ?? credential.scope,
|
|
411
|
+
obtainedAt: now
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// packages/cli/oauth/flows/openai-codex.ts
|
|
416
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
417
|
+
var OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
418
|
+
var OPENAI_CODEX_SCOPES = [
|
|
419
|
+
"openid",
|
|
420
|
+
"profile",
|
|
421
|
+
"email",
|
|
422
|
+
"offline_access",
|
|
423
|
+
"api.connectors.read",
|
|
424
|
+
"api.connectors.invoke"
|
|
425
|
+
];
|
|
426
|
+
var OPENAI_CODEX_DEVICE_AUTH_URL = "https://auth.openai.com/api/accounts/deviceauth/usercode";
|
|
427
|
+
var OPENAI_CODEX_DEVICE_TOKEN_URL = "https://auth.openai.com/api/accounts/deviceauth/token";
|
|
428
|
+
var OPENAI_CODEX_DEVICE_VERIFICATION_URL = "https://auth.openai.com/codex/device";
|
|
429
|
+
var OPENAI_CODEX_DEVICE_REDIRECT_URI = "https://auth.openai.com/deviceauth/callback";
|
|
430
|
+
var OPENAI_CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
431
|
+
var OPENAI_CODEX_AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
432
|
+
var OPENAI_CODEX_ORIGINATOR = "codex_cli_rs";
|
|
433
|
+
var OPENAI_CODEX_CALLBACK_PORT = 1455;
|
|
434
|
+
var OPENAI_CODEX_CALLBACK_PATH = "/auth/callback";
|
|
435
|
+
var OPENAI_CODEX_CALLBACK_URL = `http://localhost:${OPENAI_CODEX_CALLBACK_PORT}${OPENAI_CODEX_CALLBACK_PATH}`;
|
|
436
|
+
var AUTHORIZATION_CODE_GRANT_TYPE = "authorization_code";
|
|
437
|
+
var REFRESH_TOKEN_GRANT_TYPE = "refresh_token";
|
|
438
|
+
var DEVICE_CODE_DEFAULT_INTERVAL_MS = 5e3;
|
|
439
|
+
var DEVICE_POLL_SAFETY_MARGIN_MS = 3e3;
|
|
440
|
+
var DEVICE_MAX_POLLS = 120;
|
|
441
|
+
async function postJson(fetchImpl, url, body, deps) {
|
|
442
|
+
const response = await fetchImpl(url, {
|
|
443
|
+
method: "POST",
|
|
444
|
+
headers: {
|
|
445
|
+
"Content-Type": "application/json",
|
|
446
|
+
Accept: "application/json"
|
|
447
|
+
},
|
|
448
|
+
body: JSON.stringify(body)
|
|
449
|
+
});
|
|
450
|
+
deps?.output?.log?.(`[oauth:openai-codex] POST ${url} -> ${response.status}`);
|
|
451
|
+
return response;
|
|
452
|
+
}
|
|
453
|
+
async function postForm(fetchImpl, url, body, deps) {
|
|
454
|
+
const response = await fetchImpl(url, {
|
|
455
|
+
method: "POST",
|
|
456
|
+
headers: {
|
|
457
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
458
|
+
Accept: "application/json"
|
|
459
|
+
},
|
|
460
|
+
body: new URLSearchParams(body).toString()
|
|
461
|
+
});
|
|
462
|
+
deps?.output?.log?.(`[oauth:openai-codex] POST ${url} -> ${response.status}`);
|
|
463
|
+
return response;
|
|
464
|
+
}
|
|
465
|
+
async function parseJsonResponse(response) {
|
|
466
|
+
const text = await response.text().catch(() => "");
|
|
467
|
+
if (!text) return {};
|
|
468
|
+
try {
|
|
469
|
+
return JSON.parse(text);
|
|
470
|
+
} catch {
|
|
471
|
+
return {};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function scopeString(scopes = OPENAI_CODEX_SCOPES) {
|
|
475
|
+
return scopes.join(" ");
|
|
476
|
+
}
|
|
477
|
+
function base64UrlDecode(value) {
|
|
478
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
479
|
+
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
|
|
480
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
481
|
+
}
|
|
482
|
+
var OPENAI_AUTH_CLAIM = "https://api.openai.com/auth";
|
|
483
|
+
var OPENAI_PROFILE_CLAIM = "https://api.openai.com/profile";
|
|
484
|
+
function readClaimString(claim, key) {
|
|
485
|
+
if (!claim || typeof claim !== "object") return void 0;
|
|
486
|
+
const value = claim[key];
|
|
487
|
+
return typeof value === "string" && value ? value : void 0;
|
|
488
|
+
}
|
|
489
|
+
function decodeOpenAiIdToken(idToken) {
|
|
490
|
+
if (!idToken) return {};
|
|
491
|
+
const parts = idToken.split(".");
|
|
492
|
+
if (parts.length < 2) return {};
|
|
493
|
+
try {
|
|
494
|
+
const payload = JSON.parse(base64UrlDecode(parts[1]));
|
|
495
|
+
const authClaim = payload[OPENAI_AUTH_CLAIM];
|
|
496
|
+
const profileClaim = payload[OPENAI_PROFILE_CLAIM];
|
|
497
|
+
return {
|
|
498
|
+
chatgptAccountId: readClaimString(authClaim, "chatgpt_account_id") ?? (typeof payload["chatgpt_account_id"] === "string" ? payload["chatgpt_account_id"] : void 0),
|
|
499
|
+
email: readClaimString(profileClaim, "email") ?? (typeof payload["email"] === "string" ? payload["email"] : void 0),
|
|
500
|
+
payload
|
|
501
|
+
};
|
|
502
|
+
} catch {
|
|
503
|
+
return {};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function tokenResponseToCredential(token, now) {
|
|
507
|
+
const fromAccess = decodeOpenAiIdToken(token.accessToken);
|
|
508
|
+
const fromId = decodeOpenAiIdToken(token.idToken);
|
|
509
|
+
const decoded = {
|
|
510
|
+
chatgptAccountId: fromAccess.chatgptAccountId ?? fromId.chatgptAccountId,
|
|
511
|
+
email: fromAccess.email ?? fromId.email
|
|
512
|
+
};
|
|
513
|
+
const expiresAt = typeof token.expiresIn === "number" && token.expiresIn > 0 ? now + token.expiresIn * 1e3 : void 0;
|
|
514
|
+
return {
|
|
515
|
+
provider: "chatgpt",
|
|
516
|
+
accessToken: token.accessToken,
|
|
517
|
+
...token.refreshToken ? { refreshToken: token.refreshToken } : {},
|
|
518
|
+
...expiresAt ? { expiresAt } : {},
|
|
519
|
+
...token.scope ? { scope: token.scope } : {},
|
|
520
|
+
...token.idToken ? { idToken: token.idToken } : {},
|
|
521
|
+
...decoded.chatgptAccountId ? { accountId: decoded.chatgptAccountId } : {},
|
|
522
|
+
obtainedAt: now
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function extractTokenResponse(data) {
|
|
526
|
+
const accessToken = data["access_token"];
|
|
527
|
+
if (typeof accessToken !== "string" || !accessToken) return null;
|
|
528
|
+
const expiresInRaw = data["expires_in"];
|
|
529
|
+
return {
|
|
530
|
+
accessToken,
|
|
531
|
+
...typeof data["refresh_token"] === "string" ? { refreshToken: data["refresh_token"] } : {},
|
|
532
|
+
...typeof expiresInRaw === "number" ? { expiresIn: expiresInRaw } : {},
|
|
533
|
+
...typeof data["scope"] === "string" ? { scope: data["scope"] } : {},
|
|
534
|
+
...typeof data["id_token"] === "string" ? { idToken: data["id_token"] } : {}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
async function startDeviceCodeFlow(deps) {
|
|
538
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
539
|
+
const response = await postJson(
|
|
540
|
+
fetchImpl,
|
|
541
|
+
OPENAI_CODEX_DEVICE_AUTH_URL,
|
|
542
|
+
{ client_id: OPENAI_CODEX_CLIENT_ID },
|
|
543
|
+
deps
|
|
544
|
+
);
|
|
545
|
+
if (!response.ok) {
|
|
546
|
+
const data2 = await parseJsonResponse(response);
|
|
547
|
+
throw new Error(
|
|
548
|
+
`OpenAI device-code start failed (HTTP ${response.status}): ${JSON.stringify(data2)}`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
const data = await parseJsonResponse(response);
|
|
552
|
+
const deviceAuthId = data["device_auth_id"];
|
|
553
|
+
const userCode = data["user_code"];
|
|
554
|
+
if (typeof deviceAuthId !== "string" || !deviceAuthId) {
|
|
555
|
+
throw new Error("OpenAI device-code start response did not include a device_auth_id.");
|
|
556
|
+
}
|
|
557
|
+
if (typeof userCode !== "string" || !userCode) {
|
|
558
|
+
throw new Error("OpenAI device-code start response did not include a user_code.");
|
|
559
|
+
}
|
|
560
|
+
const intervalRaw = data["interval"];
|
|
561
|
+
const intervalSec = typeof intervalRaw === "number" ? intervalRaw : Number.parseInt(String(intervalRaw ?? "5"), 10) || 5;
|
|
562
|
+
return {
|
|
563
|
+
deviceAuthId,
|
|
564
|
+
userCode,
|
|
565
|
+
intervalMs: Math.max(1, intervalSec) * 1e3 + DEVICE_POLL_SAFETY_MARGIN_MS
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
async function pollDeviceCodeToken(args) {
|
|
569
|
+
const fetchImpl = args.deps?.fetchImpl ?? fetch;
|
|
570
|
+
const sleep3 = args.sleep ?? args.deps?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
571
|
+
const intervalMs = args.intervalMs ?? DEVICE_CODE_DEFAULT_INTERVAL_MS;
|
|
572
|
+
const maxPolls = args.maxPolls ?? DEVICE_MAX_POLLS;
|
|
573
|
+
for (let poll = 0; poll < maxPolls; poll++) {
|
|
574
|
+
await sleep3(poll === 0 ? Math.min(intervalMs, DEVICE_CODE_DEFAULT_INTERVAL_MS) : intervalMs);
|
|
575
|
+
const response = await postJson(
|
|
576
|
+
fetchImpl,
|
|
577
|
+
OPENAI_CODEX_DEVICE_TOKEN_URL,
|
|
578
|
+
{ device_auth_id: args.deviceAuthId, user_code: args.userCode },
|
|
579
|
+
args.deps
|
|
580
|
+
);
|
|
581
|
+
if (response.status === 403 || response.status === 404) {
|
|
582
|
+
args.onPending?.();
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
const data = await parseJsonResponse(response);
|
|
586
|
+
if (!response.ok) {
|
|
587
|
+
throw new Error(
|
|
588
|
+
`OpenAI device-code poll failed (HTTP ${response.status}): ${JSON.stringify(data)}`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
const authorizationCode = data["authorization_code"];
|
|
592
|
+
const codeVerifier = data["code_verifier"];
|
|
593
|
+
if (typeof authorizationCode !== "string" || !authorizationCode) {
|
|
594
|
+
throw new Error("OpenAI device-code poll response missing authorization_code.");
|
|
595
|
+
}
|
|
596
|
+
if (typeof codeVerifier !== "string" || !codeVerifier) {
|
|
597
|
+
throw new Error("OpenAI device-code poll response missing code_verifier.");
|
|
598
|
+
}
|
|
599
|
+
return { authorizationCode, codeVerifier };
|
|
600
|
+
}
|
|
601
|
+
throw new Error("OpenAI device-code flow timed out waiting for user authorization.");
|
|
602
|
+
}
|
|
603
|
+
async function runOpenAiCodexDeviceCode(deps = {}) {
|
|
604
|
+
const now = deps.now ?? Date.now;
|
|
605
|
+
const start = await startDeviceCodeFlow(deps);
|
|
606
|
+
deps.output?.log?.("Authorize nolo-cli for ChatGPT / OpenAI Codex:");
|
|
607
|
+
deps.output?.log?.(OPENAI_CODEX_DEVICE_VERIFICATION_URL);
|
|
608
|
+
deps.output?.log?.(`Enter code: ${start.userCode}`);
|
|
609
|
+
if (deps.openBrowser) {
|
|
610
|
+
const opened = await deps.openBrowser(OPENAI_CODEX_DEVICE_VERIFICATION_URL);
|
|
611
|
+
if (!opened) deps.output?.log?.("Could not open a browser automatically. Open the URL above.");
|
|
612
|
+
} else {
|
|
613
|
+
deps.output?.log?.("Open the URL above in a browser and enter the code to approve.");
|
|
614
|
+
}
|
|
615
|
+
const { authorizationCode, codeVerifier } = await pollDeviceCodeToken({
|
|
616
|
+
deviceAuthId: start.deviceAuthId,
|
|
617
|
+
userCode: start.userCode,
|
|
618
|
+
deps,
|
|
619
|
+
intervalMs: start.intervalMs,
|
|
620
|
+
sleep: deps.sleep,
|
|
621
|
+
onPending: () => {
|
|
622
|
+
deps.output?.log?.("[oauth:openai-codex] Waiting for authorization...");
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
const credential = await exchangeCodexAuthorizationCode({
|
|
626
|
+
code: authorizationCode,
|
|
627
|
+
codeVerifier,
|
|
628
|
+
redirectUri: OPENAI_CODEX_DEVICE_REDIRECT_URI,
|
|
629
|
+
deps,
|
|
630
|
+
now
|
|
631
|
+
});
|
|
632
|
+
writeOAuthCredential("chatgpt", credential);
|
|
633
|
+
deps.output?.log?.(
|
|
634
|
+
`Saved ChatGPT OAuth credential (account=${credential.accountId ?? "unknown"}).`
|
|
635
|
+
);
|
|
636
|
+
return credential;
|
|
637
|
+
}
|
|
638
|
+
async function exchangeCodexAuthorizationCode(args) {
|
|
639
|
+
const fetchImpl = args.deps?.fetchImpl ?? fetch;
|
|
640
|
+
const now = args.now ?? Date.now;
|
|
641
|
+
const response = await postForm(
|
|
642
|
+
fetchImpl,
|
|
643
|
+
OPENAI_CODEX_TOKEN_URL,
|
|
644
|
+
{
|
|
645
|
+
grant_type: AUTHORIZATION_CODE_GRANT_TYPE,
|
|
646
|
+
code: args.code,
|
|
647
|
+
redirect_uri: args.redirectUri,
|
|
648
|
+
client_id: OPENAI_CODEX_CLIENT_ID,
|
|
649
|
+
code_verifier: args.codeVerifier
|
|
650
|
+
},
|
|
651
|
+
args.deps
|
|
652
|
+
);
|
|
653
|
+
const data = await parseJsonResponse(response);
|
|
654
|
+
if (!response.ok) {
|
|
655
|
+
throw new Error(
|
|
656
|
+
`OpenAI authorization-code exchange failed (HTTP ${response.status}): ${JSON.stringify(data)}`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
const token = extractTokenResponse(data);
|
|
660
|
+
if (!token) {
|
|
661
|
+
throw new Error(`OpenAI authorization-code exchange returned no token: ${JSON.stringify(data)}`);
|
|
662
|
+
}
|
|
663
|
+
return tokenResponseToCredential(token, now());
|
|
664
|
+
}
|
|
665
|
+
async function exchangeAuthorizationCode(args) {
|
|
666
|
+
return exchangeCodexAuthorizationCode({
|
|
667
|
+
code: args.code,
|
|
668
|
+
codeVerifier: args.pkce.verifier,
|
|
669
|
+
redirectUri: OPENAI_CODEX_CALLBACK_URL,
|
|
670
|
+
deps: args.deps,
|
|
671
|
+
now: args.now
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
async function runOpenAiCodexBrowserPkce(deps = {}) {
|
|
675
|
+
const now = deps.now ?? Date.now;
|
|
676
|
+
const pkce = generatePkcePair();
|
|
677
|
+
const state = createHash2("sha256").update(`${now()}-${Math.random()}`).digest("base64url");
|
|
678
|
+
const callback = await startCallbackServer({
|
|
679
|
+
port: OPENAI_CODEX_CALLBACK_PORT,
|
|
680
|
+
now
|
|
681
|
+
});
|
|
682
|
+
try {
|
|
683
|
+
const authorizeUrl = new URL(OPENAI_CODEX_AUTHORIZE_URL);
|
|
684
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
685
|
+
authorizeUrl.searchParams.set("client_id", OPENAI_CODEX_CLIENT_ID);
|
|
686
|
+
authorizeUrl.searchParams.set("redirect_uri", OPENAI_CODEX_CALLBACK_URL);
|
|
687
|
+
authorizeUrl.searchParams.set("scope", scopeString());
|
|
688
|
+
authorizeUrl.searchParams.set("code_challenge", pkce.challenge);
|
|
689
|
+
authorizeUrl.searchParams.set("code_challenge_method", pkce.method);
|
|
690
|
+
authorizeUrl.searchParams.set("id_token_add_organizations", "true");
|
|
691
|
+
authorizeUrl.searchParams.set("codex_cli_simplified_flow", "true");
|
|
692
|
+
authorizeUrl.searchParams.set("originator", OPENAI_CODEX_ORIGINATOR);
|
|
693
|
+
authorizeUrl.searchParams.set("state", state);
|
|
694
|
+
const authorizeUrlString = authorizeUrl.toString();
|
|
695
|
+
deps.output?.log?.("Authorize nolo-cli for ChatGPT / OpenAI Codex:");
|
|
696
|
+
deps.output?.log?.(authorizeUrlString);
|
|
697
|
+
if (deps.openBrowser) {
|
|
698
|
+
const opened = await deps.openBrowser(authorizeUrlString);
|
|
699
|
+
if (!opened) deps.output?.log?.("Could not open a browser automatically. Open the URL above.");
|
|
700
|
+
}
|
|
701
|
+
const result = await callback.waitForCode();
|
|
702
|
+
if (result.state && result.state !== state) {
|
|
703
|
+
throw new Error("OpenAI browser PKCE flow state mismatch.");
|
|
704
|
+
}
|
|
705
|
+
const credential = await exchangeAuthorizationCode({
|
|
706
|
+
code: result.code,
|
|
707
|
+
state,
|
|
708
|
+
pkce,
|
|
709
|
+
deps,
|
|
710
|
+
now
|
|
711
|
+
});
|
|
712
|
+
writeOAuthCredential("chatgpt", credential);
|
|
713
|
+
deps.output?.log?.(
|
|
714
|
+
`Saved ChatGPT OAuth credential (account=${credential.accountId ?? "unknown"}).`
|
|
715
|
+
);
|
|
716
|
+
return credential;
|
|
717
|
+
} finally {
|
|
718
|
+
await callback.close().catch(() => {
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
var refreshOpenAiCodexToken = async (credential, deps = {}) => {
|
|
723
|
+
if (!credential.refreshToken) {
|
|
724
|
+
throw new Error("Cannot refresh ChatGPT OAuth token without a refresh_token.");
|
|
725
|
+
}
|
|
726
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
727
|
+
const now = deps.now ?? Date.now;
|
|
728
|
+
const response = await postForm(
|
|
729
|
+
fetchImpl,
|
|
730
|
+
OPENAI_CODEX_TOKEN_URL,
|
|
731
|
+
{
|
|
732
|
+
grant_type: REFRESH_TOKEN_GRANT_TYPE,
|
|
733
|
+
refresh_token: credential.refreshToken,
|
|
734
|
+
client_id: OPENAI_CODEX_CLIENT_ID,
|
|
735
|
+
scope: scopeString()
|
|
736
|
+
},
|
|
737
|
+
deps
|
|
738
|
+
);
|
|
739
|
+
const data = await parseJsonResponse(response);
|
|
740
|
+
if (!response.ok) {
|
|
741
|
+
throw new Error(
|
|
742
|
+
`OpenAI token refresh failed (HTTP ${response.status}): ${JSON.stringify(data)}`
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
const token = extractTokenResponse(data);
|
|
746
|
+
if (!token) {
|
|
747
|
+
throw new Error(`OpenAI token refresh returned no token: ${JSON.stringify(data)}`);
|
|
748
|
+
}
|
|
749
|
+
const refreshed = tokenResponseToCredential(token, now());
|
|
750
|
+
return {
|
|
751
|
+
...refreshed,
|
|
752
|
+
refreshToken: refreshed.refreshToken ?? credential.refreshToken,
|
|
753
|
+
accountId: refreshed.accountId ?? credential.accountId
|
|
754
|
+
};
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// packages/cli/oauth/flows/xai.ts
|
|
758
|
+
var XAI_OAUTH_ISSUER = "https://auth.x.ai";
|
|
759
|
+
var XAI_OAUTH_DISCOVERY_URL = `${XAI_OAUTH_ISSUER}/.well-known/openid-configuration`;
|
|
760
|
+
var XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
761
|
+
var XAI_OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
|
|
762
|
+
var XAI_OAUTH_REDIRECT_HOST = "127.0.0.1";
|
|
763
|
+
var XAI_OAUTH_REDIRECT_PORT = 56121;
|
|
764
|
+
var XAI_OAUTH_REDIRECT_PATH = "/callback";
|
|
765
|
+
var XAI_OAUTH_DOCS_URL = "https://hermes-agent.nousresearch.com/docs/guides/xai-grok-oauth";
|
|
766
|
+
var XAI_DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
767
|
+
var XAI_DEVICE_CODE_DEFAULT_INTERVAL_MS = 5e3;
|
|
768
|
+
var XAI_DEVICE_CODE_MIN_INTERVAL_MS = 1e3;
|
|
769
|
+
var XAI_DEVICE_CODE_SLOW_DOWN_INCREMENT_MS = 5e3;
|
|
770
|
+
var XAI_DEVICE_CODE_DEFAULT_EXPIRES_MS = 5 * 6e4;
|
|
771
|
+
var ACCESS_TOKEN_CLIENT_SKEW_MS2 = 5 * 60 * 1e3;
|
|
772
|
+
var DISCOVERY_TIMEOUT_MS = 15e3;
|
|
773
|
+
var TOKEN_REQUEST_TIMEOUT_MS = 2e4;
|
|
774
|
+
function validateXAIEndpoint(url, field) {
|
|
775
|
+
let parsed;
|
|
776
|
+
try {
|
|
777
|
+
parsed = new URL(url);
|
|
778
|
+
} catch {
|
|
779
|
+
throw new Error(`Invalid xAI ${field}: ${url}`);
|
|
780
|
+
}
|
|
781
|
+
if (parsed.protocol !== "https:") {
|
|
782
|
+
throw new Error(`Invalid xAI ${field}: ${url}`);
|
|
783
|
+
}
|
|
784
|
+
const host = parsed.hostname.toLowerCase();
|
|
785
|
+
if (!host || host !== "x.ai" && !host.endsWith(".x.ai")) {
|
|
786
|
+
throw new Error(`Invalid xAI ${field}: ${url}`);
|
|
787
|
+
}
|
|
788
|
+
return url;
|
|
789
|
+
}
|
|
790
|
+
function isLikelyHtmlOrCloudflareChallenge(response, bodyText) {
|
|
791
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
792
|
+
return response.headers.get("cf-mitigated") === "challenge" || /text\/html/i.test(contentType) || /<!doctype html|<html\b/i.test(bodyText) || /\b(?:cloudflare|attention required|just a moment|enable javascript and cookies|challenge-platform)\b/i.test(
|
|
793
|
+
bodyText
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
async function readJsonBody(response, context) {
|
|
797
|
+
let text = "";
|
|
798
|
+
try {
|
|
799
|
+
text = await response.text();
|
|
800
|
+
} catch (error) {
|
|
801
|
+
throw new Error(
|
|
802
|
+
`${context}: failed to read response body: ${toErrorMessage(error)}`
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
if (!text.trim()) {
|
|
806
|
+
if (!response.ok) {
|
|
807
|
+
throw new Error(`${context} failed (${response.status}): empty response body`);
|
|
808
|
+
}
|
|
809
|
+
return {};
|
|
810
|
+
}
|
|
811
|
+
try {
|
|
812
|
+
const parsed = JSON.parse(text);
|
|
813
|
+
if (!isRecord(parsed)) {
|
|
814
|
+
throw new Error(`${context} returned non-object JSON`);
|
|
815
|
+
}
|
|
816
|
+
return parsed;
|
|
817
|
+
} catch (error) {
|
|
818
|
+
if (error instanceof Error && error.message.startsWith(context)) {
|
|
819
|
+
throw error;
|
|
820
|
+
}
|
|
821
|
+
if (isLikelyHtmlOrCloudflareChallenge(response, text)) {
|
|
822
|
+
throw new Error(
|
|
823
|
+
`${context} failed (${response.status}): xAI returned an HTML/Cloudflare challenge instead of OAuth JSON`
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
throw new Error(
|
|
827
|
+
`${context} returned invalid JSON: ${toErrorMessage(error)}`
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async function xaiOAuthDiscovery(fetchImpl, options = {}) {
|
|
832
|
+
const timeoutMs = options.timeoutMs ?? DISCOVERY_TIMEOUT_MS;
|
|
833
|
+
let response;
|
|
834
|
+
try {
|
|
835
|
+
response = await fetchImpl(XAI_OAUTH_DISCOVERY_URL, {
|
|
836
|
+
method: "GET",
|
|
837
|
+
headers: { Accept: "application/json" },
|
|
838
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
839
|
+
});
|
|
840
|
+
} catch (error) {
|
|
841
|
+
throw new Error(
|
|
842
|
+
`xAI OIDC discovery failed: ${toErrorMessage(error)}`
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
if (response.status !== 200) {
|
|
846
|
+
throw new Error(`xAI OIDC discovery returned status ${response.status}.`);
|
|
847
|
+
}
|
|
848
|
+
const obj = await readJsonBody(response, "xAI OIDC discovery");
|
|
849
|
+
const authorizationEndpoint = asTrimmedString(obj.authorization_endpoint);
|
|
850
|
+
const tokenEndpoint = asTrimmedString(obj.token_endpoint);
|
|
851
|
+
if (!authorizationEndpoint || !tokenEndpoint) {
|
|
852
|
+
throw new Error("xAI OIDC discovery response was missing required endpoints.");
|
|
853
|
+
}
|
|
854
|
+
validateXAIEndpoint(authorizationEndpoint, "authorization_endpoint");
|
|
855
|
+
validateXAIEndpoint(tokenEndpoint, "token_endpoint");
|
|
856
|
+
const deviceAuthorizationEndpoint = asTrimmedString(
|
|
857
|
+
obj.device_authorization_endpoint
|
|
858
|
+
);
|
|
859
|
+
if (options.requireDeviceAuthorization) {
|
|
860
|
+
if (!deviceAuthorizationEndpoint) {
|
|
861
|
+
throw new Error(
|
|
862
|
+
"xAI OIDC discovery response was missing device_authorization_endpoint."
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
validateXAIEndpoint(deviceAuthorizationEndpoint, "device_authorization_endpoint");
|
|
866
|
+
} else if (deviceAuthorizationEndpoint) {
|
|
867
|
+
validateXAIEndpoint(deviceAuthorizationEndpoint, "device_authorization_endpoint");
|
|
868
|
+
}
|
|
869
|
+
return {
|
|
870
|
+
authorization_endpoint: authorizationEndpoint,
|
|
871
|
+
token_endpoint: tokenEndpoint,
|
|
872
|
+
...deviceAuthorizationEndpoint ? { device_authorization_endpoint: deviceAuthorizationEndpoint } : {}
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
function buildXAIAuthorizeUrl(opts) {
|
|
876
|
+
const params = new URLSearchParams({
|
|
877
|
+
response_type: "code",
|
|
878
|
+
client_id: XAI_OAUTH_CLIENT_ID,
|
|
879
|
+
redirect_uri: opts.redirectUri,
|
|
880
|
+
scope: XAI_OAUTH_SCOPE,
|
|
881
|
+
code_challenge: opts.codeChallenge,
|
|
882
|
+
code_challenge_method: "S256",
|
|
883
|
+
state: opts.state,
|
|
884
|
+
nonce: opts.nonce,
|
|
885
|
+
plan: "generic",
|
|
886
|
+
referrer: "nolo-cli"
|
|
887
|
+
});
|
|
888
|
+
return `${opts.authorizationEndpoint}?${params.toString()}`;
|
|
889
|
+
}
|
|
890
|
+
function generateState2() {
|
|
891
|
+
const bytes = new Uint8Array(16);
|
|
892
|
+
crypto.getRandomValues(bytes);
|
|
893
|
+
return Array.from(bytes).map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
894
|
+
}
|
|
895
|
+
function parsePositiveSecondsToMs(value, fallbackMs) {
|
|
896
|
+
const sec = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value, 10) : NaN;
|
|
897
|
+
if (!Number.isFinite(sec) || sec <= 0) return fallbackMs;
|
|
898
|
+
return Math.max(1, Math.floor(sec * 1e3));
|
|
899
|
+
}
|
|
900
|
+
function parseOAuthTokenPayload(data, context, options = {}) {
|
|
901
|
+
if (typeof data.access_token !== "string" || !data.access_token) {
|
|
902
|
+
throw new Error(`${context} missing access_token`);
|
|
903
|
+
}
|
|
904
|
+
const refreshToken = typeof data.refresh_token === "string" && data.refresh_token ? data.refresh_token : void 0;
|
|
905
|
+
if (options.requireRefreshToken && !refreshToken) {
|
|
906
|
+
throw new Error(`${context} missing refresh_token`);
|
|
907
|
+
}
|
|
908
|
+
const expiresIn = asOptionalFiniteNumber(data.expires_in);
|
|
909
|
+
if (expiresIn === void 0) {
|
|
910
|
+
throw new Error(`${context} missing expires_in`);
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
accessToken: data.access_token,
|
|
914
|
+
...refreshToken ? { refreshToken } : {},
|
|
915
|
+
expiresIn,
|
|
916
|
+
scope: typeof data.scope === "string" ? data.scope : void 0,
|
|
917
|
+
idToken: typeof data.id_token === "string" ? data.id_token : void 0
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function tokenToCredential(token, now) {
|
|
921
|
+
const expiresIn = token.expiresIn;
|
|
922
|
+
return {
|
|
923
|
+
provider: "xai",
|
|
924
|
+
accessToken: token.accessToken,
|
|
925
|
+
refreshToken: token.refreshToken,
|
|
926
|
+
expiresAt: now + expiresIn * 1e3 - ACCESS_TOKEN_CLIENT_SKEW_MS2,
|
|
927
|
+
scope: token.scope,
|
|
928
|
+
idToken: token.idToken,
|
|
929
|
+
obtainedAt: now
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
async function exchangeXAIToken(fetchImpl, code, redirectUri, verifier) {
|
|
933
|
+
const discovery = await xaiOAuthDiscovery(fetchImpl);
|
|
934
|
+
const tokenEndpoint = validateXAIEndpoint(discovery.token_endpoint, "token_endpoint");
|
|
935
|
+
const body = new URLSearchParams({
|
|
936
|
+
grant_type: "authorization_code",
|
|
937
|
+
client_id: XAI_OAUTH_CLIENT_ID,
|
|
938
|
+
code,
|
|
939
|
+
redirect_uri: redirectUri,
|
|
940
|
+
code_verifier: verifier
|
|
941
|
+
});
|
|
942
|
+
let response;
|
|
943
|
+
try {
|
|
944
|
+
response = await fetchImpl(tokenEndpoint, {
|
|
945
|
+
method: "POST",
|
|
946
|
+
headers: {
|
|
947
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
948
|
+
Accept: "application/json"
|
|
949
|
+
},
|
|
950
|
+
body,
|
|
951
|
+
signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS)
|
|
952
|
+
});
|
|
953
|
+
} catch (error) {
|
|
954
|
+
throw new Error(
|
|
955
|
+
`xAI token exchange failed: ${toErrorMessage(error)}`
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
const data = await readJsonBody(response, "xAI token exchange");
|
|
959
|
+
if (!response.ok) {
|
|
960
|
+
const detail = typeof data.error_description === "string" && data.error_description || typeof data.error === "string" && data.error || "";
|
|
961
|
+
throw new Error(
|
|
962
|
+
`xAI token exchange failed: ${response.status}${detail ? ` ${detail}` : ""}`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
return parseOAuthTokenPayload(data, "xAI token exchange response", {
|
|
966
|
+
requireRefreshToken: true
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
async function requestXaiDeviceCode(fetchImpl, deviceAuthorizationEndpoint) {
|
|
970
|
+
const endpoint = validateXAIEndpoint(
|
|
971
|
+
deviceAuthorizationEndpoint,
|
|
972
|
+
"device_authorization_endpoint"
|
|
973
|
+
);
|
|
974
|
+
let response;
|
|
975
|
+
try {
|
|
976
|
+
response = await fetchImpl(endpoint, {
|
|
977
|
+
method: "POST",
|
|
978
|
+
headers: {
|
|
979
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
980
|
+
Accept: "application/json"
|
|
981
|
+
},
|
|
982
|
+
body: new URLSearchParams({
|
|
983
|
+
client_id: XAI_OAUTH_CLIENT_ID,
|
|
984
|
+
scope: XAI_OAUTH_SCOPE
|
|
985
|
+
}),
|
|
986
|
+
signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS)
|
|
987
|
+
});
|
|
988
|
+
} catch (error) {
|
|
989
|
+
throw new Error(
|
|
990
|
+
`xAI device code request failed: ${toErrorMessage(error)}`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
const data = await readJsonBody(response, "xAI device code request");
|
|
994
|
+
if (!response.ok) {
|
|
995
|
+
const detail = typeof data.error_description === "string" && data.error_description || typeof data.error === "string" && data.error || "";
|
|
996
|
+
throw new Error(
|
|
997
|
+
`xAI device code request failed (${response.status})${detail ? `: ${detail}` : ""}`
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
const deviceCode = asTrimmedString(data.device_code);
|
|
1001
|
+
const userCode = asTrimmedString(data.user_code);
|
|
1002
|
+
const verificationUri = asTrimmedString(data.verification_uri);
|
|
1003
|
+
const verificationUriComplete = asTrimmedString(
|
|
1004
|
+
data.verification_uri_complete
|
|
1005
|
+
);
|
|
1006
|
+
if (!deviceCode || !userCode || !verificationUri) {
|
|
1007
|
+
throw new Error(
|
|
1008
|
+
"xAI device code response is missing device_code, user_code, or verification_uri"
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
validateXAIEndpoint(verificationUri, "verification_uri");
|
|
1012
|
+
if (verificationUriComplete) {
|
|
1013
|
+
validateXAIEndpoint(verificationUriComplete, "verification_uri_complete");
|
|
1014
|
+
}
|
|
1015
|
+
return {
|
|
1016
|
+
deviceCode,
|
|
1017
|
+
userCode,
|
|
1018
|
+
verificationUri,
|
|
1019
|
+
...verificationUriComplete ? { verificationUriComplete } : {},
|
|
1020
|
+
expiresInMs: parsePositiveSecondsToMs(
|
|
1021
|
+
data.expires_in,
|
|
1022
|
+
XAI_DEVICE_CODE_DEFAULT_EXPIRES_MS
|
|
1023
|
+
),
|
|
1024
|
+
intervalMs: Math.max(
|
|
1025
|
+
parsePositiveSecondsToMs(data.interval, XAI_DEVICE_CODE_DEFAULT_INTERVAL_MS),
|
|
1026
|
+
XAI_DEVICE_CODE_MIN_INTERVAL_MS
|
|
1027
|
+
)
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
async function pollXaiDeviceCodeToken(args) {
|
|
1031
|
+
const tokenEndpoint = validateXAIEndpoint(args.tokenEndpoint, "token_endpoint");
|
|
1032
|
+
const deadlineMs = args.now() + args.expiresInMs;
|
|
1033
|
+
let intervalMs = args.intervalMs;
|
|
1034
|
+
while (args.now() < deadlineMs) {
|
|
1035
|
+
const remainingMs = Math.max(0, deadlineMs - args.now());
|
|
1036
|
+
const delayMs = Math.min(Math.max(intervalMs, XAI_DEVICE_CODE_MIN_INTERVAL_MS), remainingMs);
|
|
1037
|
+
if (delayMs > 0) {
|
|
1038
|
+
await args.sleep(delayMs);
|
|
1039
|
+
}
|
|
1040
|
+
if (args.now() >= deadlineMs) break;
|
|
1041
|
+
let response;
|
|
1042
|
+
try {
|
|
1043
|
+
response = await args.fetchImpl(tokenEndpoint, {
|
|
1044
|
+
method: "POST",
|
|
1045
|
+
headers: {
|
|
1046
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1047
|
+
Accept: "application/json"
|
|
1048
|
+
},
|
|
1049
|
+
body: new URLSearchParams({
|
|
1050
|
+
grant_type: XAI_DEVICE_CODE_GRANT_TYPE,
|
|
1051
|
+
client_id: XAI_OAUTH_CLIENT_ID,
|
|
1052
|
+
device_code: args.deviceCode
|
|
1053
|
+
}),
|
|
1054
|
+
signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS)
|
|
1055
|
+
});
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
const message = toErrorMessage(error);
|
|
1058
|
+
if (/abort|timeout/i.test(message)) {
|
|
1059
|
+
throw new Error(`xAI device token poll aborted or timed out: ${message}`);
|
|
1060
|
+
}
|
|
1061
|
+
throw new Error(`xAI device token poll failed: ${message}`);
|
|
1062
|
+
}
|
|
1063
|
+
let data;
|
|
1064
|
+
try {
|
|
1065
|
+
data = await readJsonBody(response, "xAI device token exchange");
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
throw error;
|
|
1068
|
+
}
|
|
1069
|
+
if (response.ok) {
|
|
1070
|
+
return parseOAuthTokenPayload(data, "xAI device token exchange response", {
|
|
1071
|
+
requireRefreshToken: true
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
const oauthError = typeof data.error === "string" ? data.error : "";
|
|
1075
|
+
if (oauthError === "authorization_pending") {
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
if (oauthError === "slow_down") {
|
|
1079
|
+
intervalMs += XAI_DEVICE_CODE_SLOW_DOWN_INCREMENT_MS;
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
if (oauthError === "access_denied" || oauthError === "authorization_denied") {
|
|
1083
|
+
throw new Error("xAI device authorization was denied");
|
|
1084
|
+
}
|
|
1085
|
+
if (oauthError === "expired_token") {
|
|
1086
|
+
throw new Error("xAI device code expired. Re-run the login.");
|
|
1087
|
+
}
|
|
1088
|
+
const detail = typeof data.error_description === "string" && data.error_description || oauthError || "";
|
|
1089
|
+
throw new Error(
|
|
1090
|
+
`xAI device token exchange failed (${response.status})${detail ? `: ${detail}` : ""}`
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
throw new Error("xAI device authorization timed out");
|
|
1094
|
+
}
|
|
1095
|
+
async function runXaiOAuthLogin(deps = {}) {
|
|
1096
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
1097
|
+
const output = deps.output ?? console;
|
|
1098
|
+
const error = deps.error ?? console;
|
|
1099
|
+
const redirectUri = `http://${XAI_OAUTH_REDIRECT_HOST}:${XAI_OAUTH_REDIRECT_PORT}${XAI_OAUTH_REDIRECT_PATH}`;
|
|
1100
|
+
let handle;
|
|
1101
|
+
try {
|
|
1102
|
+
handle = await startCallbackServer({
|
|
1103
|
+
port: XAI_OAUTH_REDIRECT_PORT,
|
|
1104
|
+
hostname: XAI_OAUTH_REDIRECT_HOST,
|
|
1105
|
+
timeoutMs: 5 * 6e4
|
|
1106
|
+
});
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
throw new Error(
|
|
1109
|
+
`Failed to start xAI OAuth callback server on ${XAI_OAUTH_REDIRECT_PORT}: ${toErrorMessage(err)}`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
try {
|
|
1113
|
+
const state = generateState2();
|
|
1114
|
+
const pkce = await generatePkcePair();
|
|
1115
|
+
const nonce = crypto.randomUUID().replace(/-/g, "");
|
|
1116
|
+
const discovery = await xaiOAuthDiscovery(fetchImpl);
|
|
1117
|
+
const authUrl = buildXAIAuthorizeUrl({
|
|
1118
|
+
authorizationEndpoint: discovery.authorization_endpoint,
|
|
1119
|
+
redirectUri,
|
|
1120
|
+
codeChallenge: pkce.challenge,
|
|
1121
|
+
state,
|
|
1122
|
+
nonce
|
|
1123
|
+
});
|
|
1124
|
+
output.log(
|
|
1125
|
+
`Open the following URL in your browser to log in to xAI Grok (SuperGrok):
|
|
1126
|
+
${authUrl}
|
|
1127
|
+
|
|
1128
|
+
Docs: ${XAI_OAUTH_DOCS_URL}`
|
|
1129
|
+
);
|
|
1130
|
+
if (deps.openBrowser) {
|
|
1131
|
+
try {
|
|
1132
|
+
await deps.openBrowser(authUrl);
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
error.error(
|
|
1135
|
+
`Failed to open browser automatically: ${toErrorMessage(err)}`
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const callback = await handle.waitForCode();
|
|
1140
|
+
const token = await exchangeXAIToken(
|
|
1141
|
+
fetchImpl,
|
|
1142
|
+
callback.code,
|
|
1143
|
+
redirectUri,
|
|
1144
|
+
pkce.verifier
|
|
1145
|
+
);
|
|
1146
|
+
const now = (deps.now ?? Date.now)();
|
|
1147
|
+
return tokenToCredential(token, now);
|
|
1148
|
+
} finally {
|
|
1149
|
+
await handle.close();
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
async function runXaiOAuthDeviceCode(deps = {}) {
|
|
1153
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
1154
|
+
const output = deps.output ?? console;
|
|
1155
|
+
const error = deps.error ?? console;
|
|
1156
|
+
const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1157
|
+
const now = deps.now ?? Date.now;
|
|
1158
|
+
const discovery = await xaiOAuthDiscovery(fetchImpl, {
|
|
1159
|
+
requireDeviceAuthorization: true
|
|
1160
|
+
});
|
|
1161
|
+
const deviceAuthorizationEndpoint = discovery.device_authorization_endpoint;
|
|
1162
|
+
if (!deviceAuthorizationEndpoint) {
|
|
1163
|
+
throw new Error("xAI OIDC discovery missing device_authorization_endpoint");
|
|
1164
|
+
}
|
|
1165
|
+
const start = await requestXaiDeviceCode(fetchImpl, deviceAuthorizationEndpoint);
|
|
1166
|
+
const browserUrl = start.verificationUriComplete ?? start.verificationUri;
|
|
1167
|
+
const expiresMinutes = Math.max(1, Math.round(start.expiresInMs / 6e4));
|
|
1168
|
+
output.log("Authorize nolo-cli for xAI Grok (device code / headless):");
|
|
1169
|
+
output.log(` URL: ${start.verificationUri}`);
|
|
1170
|
+
if (start.verificationUriComplete) {
|
|
1171
|
+
output.log(` Complete URL: ${start.verificationUriComplete}`);
|
|
1172
|
+
}
|
|
1173
|
+
output.log(` Code: ${start.userCode}`);
|
|
1174
|
+
output.log(
|
|
1175
|
+
` Code expires in ~${expiresMinutes} minute(s). Never share the code or tokens.`
|
|
1176
|
+
);
|
|
1177
|
+
if (deps.openBrowser) {
|
|
1178
|
+
try {
|
|
1179
|
+
const opened = await deps.openBrowser(browserUrl);
|
|
1180
|
+
if (!opened) {
|
|
1181
|
+
output.log("Could not open a browser automatically. Open the URL above.");
|
|
1182
|
+
}
|
|
1183
|
+
} catch (err) {
|
|
1184
|
+
error.error(
|
|
1185
|
+
`Failed to open browser automatically: ${toErrorMessage(err)}`
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
} else {
|
|
1189
|
+
output.log("Open the URL above in a browser and enter the code to approve.");
|
|
1190
|
+
}
|
|
1191
|
+
const token = await pollXaiDeviceCodeToken({
|
|
1192
|
+
fetchImpl,
|
|
1193
|
+
tokenEndpoint: discovery.token_endpoint,
|
|
1194
|
+
deviceCode: start.deviceCode,
|
|
1195
|
+
expiresInMs: start.expiresInMs,
|
|
1196
|
+
intervalMs: start.intervalMs,
|
|
1197
|
+
sleep: sleep3,
|
|
1198
|
+
now
|
|
1199
|
+
});
|
|
1200
|
+
return tokenToCredential(token, now());
|
|
1201
|
+
}
|
|
1202
|
+
async function refreshXaiOAuthToken(credential) {
|
|
1203
|
+
if (!credential.refreshToken) {
|
|
1204
|
+
throw new Error("xAI credential has no refresh_token");
|
|
1205
|
+
}
|
|
1206
|
+
const fetchImpl = fetch;
|
|
1207
|
+
const discovery = await xaiOAuthDiscovery(fetchImpl);
|
|
1208
|
+
const tokenEndpoint = validateXAIEndpoint(discovery.token_endpoint, "token_endpoint");
|
|
1209
|
+
const body = new URLSearchParams({
|
|
1210
|
+
grant_type: "refresh_token",
|
|
1211
|
+
client_id: XAI_OAUTH_CLIENT_ID,
|
|
1212
|
+
refresh_token: credential.refreshToken
|
|
1213
|
+
});
|
|
1214
|
+
let response;
|
|
1215
|
+
try {
|
|
1216
|
+
response = await fetchImpl(tokenEndpoint, {
|
|
1217
|
+
method: "POST",
|
|
1218
|
+
headers: {
|
|
1219
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1220
|
+
Accept: "application/json"
|
|
1221
|
+
},
|
|
1222
|
+
body,
|
|
1223
|
+
signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS)
|
|
1224
|
+
});
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
throw new Error(
|
|
1227
|
+
`xAI token refresh failed: ${toErrorMessage(error)}`
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
const data = await readJsonBody(response, "xAI token refresh");
|
|
1231
|
+
if (!response.ok) {
|
|
1232
|
+
const detail = typeof data.error_description === "string" && data.error_description || typeof data.error === "string" && data.error || "";
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
`xAI token refresh failed: ${response.status}${detail ? ` ${detail}` : ""}`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
if (typeof data.access_token !== "string" || !data.access_token) {
|
|
1238
|
+
throw new Error("xAI token refresh response missing access_token");
|
|
1239
|
+
}
|
|
1240
|
+
const expiresIn = asOptionalFiniteNumber(data.expires_in);
|
|
1241
|
+
if (expiresIn === void 0) {
|
|
1242
|
+
throw new Error("xAI token refresh response missing expires_in");
|
|
1243
|
+
}
|
|
1244
|
+
const newRefresh = typeof data.refresh_token === "string" && data.refresh_token ? data.refresh_token : credential.refreshToken;
|
|
1245
|
+
const now = Date.now();
|
|
1246
|
+
return {
|
|
1247
|
+
...credential,
|
|
1248
|
+
accessToken: data.access_token,
|
|
1249
|
+
refreshToken: newRefresh,
|
|
1250
|
+
expiresAt: now + expiresIn * 1e3 - ACCESS_TOKEN_CLIENT_SKEW_MS2,
|
|
1251
|
+
obtainedAt: now
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// packages/cli/oauth/flows/cursor.ts
|
|
1256
|
+
var CURSOR_LOGIN_URL = "https://cursor.com/loginDeepControl";
|
|
1257
|
+
var CURSOR_POLL_URL = "https://api2.cursor.sh/auth/poll";
|
|
1258
|
+
var CURSOR_REFRESH_URL = "https://api2.cursor.sh/auth/exchange_user_api_key";
|
|
1259
|
+
var POLL_MAX_ATTEMPTS = 150;
|
|
1260
|
+
var POLL_BASE_DELAY_MS = 1e3;
|
|
1261
|
+
var POLL_MAX_DELAY_MS = 1e4;
|
|
1262
|
+
var POLL_BACKOFF_MULTIPLIER = 1.2;
|
|
1263
|
+
var POLL_MAX_CONSECUTIVE_ERRORS = 3;
|
|
1264
|
+
var ACCESS_TOKEN_CLIENT_SKEW_MS3 = 5 * 60 * 1e3;
|
|
1265
|
+
function decodeJwtExp(token) {
|
|
1266
|
+
try {
|
|
1267
|
+
const parts = token.split(".");
|
|
1268
|
+
if (parts.length !== 3) return void 0;
|
|
1269
|
+
const payload = parts[1];
|
|
1270
|
+
if (!payload) return void 0;
|
|
1271
|
+
const decoded = JSON.parse(
|
|
1272
|
+
Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8")
|
|
1273
|
+
);
|
|
1274
|
+
if (decoded && typeof decoded === "object" && typeof decoded.exp === "number") {
|
|
1275
|
+
return decoded.exp * 1e3 - ACCESS_TOKEN_CLIENT_SKEW_MS3;
|
|
1276
|
+
}
|
|
1277
|
+
} catch {
|
|
1278
|
+
}
|
|
1279
|
+
return void 0;
|
|
1280
|
+
}
|
|
1281
|
+
function sleep2(ms, signal) {
|
|
1282
|
+
if (signal) {
|
|
1283
|
+
return new Promise((resolve, reject) => {
|
|
1284
|
+
if (signal.aborted) {
|
|
1285
|
+
reject(new Error("Aborted"));
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
const timer = setTimeout(resolve, ms);
|
|
1289
|
+
signal.addEventListener(
|
|
1290
|
+
"abort",
|
|
1291
|
+
() => {
|
|
1292
|
+
clearTimeout(timer);
|
|
1293
|
+
reject(new Error("Aborted"));
|
|
1294
|
+
},
|
|
1295
|
+
{ once: true }
|
|
1296
|
+
);
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1300
|
+
}
|
|
1301
|
+
async function pollCursorAuth(uuid, verifier, deps = {}) {
|
|
1302
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
1303
|
+
const sleepFn = deps.sleepFn ?? ((ms) => sleep2(ms));
|
|
1304
|
+
let delay = POLL_BASE_DELAY_MS;
|
|
1305
|
+
let consecutiveErrors = 0;
|
|
1306
|
+
for (let attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) {
|
|
1307
|
+
await sleepFn(delay);
|
|
1308
|
+
let response;
|
|
1309
|
+
try {
|
|
1310
|
+
response = await fetchImpl(
|
|
1311
|
+
`${CURSOR_POLL_URL}?uuid=${encodeURIComponent(uuid)}&verifier=${encodeURIComponent(verifier)}`
|
|
1312
|
+
);
|
|
1313
|
+
} catch {
|
|
1314
|
+
consecutiveErrors++;
|
|
1315
|
+
if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS) {
|
|
1316
|
+
throw new Error("Too many consecutive network errors during Cursor auth polling");
|
|
1317
|
+
}
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
if (response.status === 404) {
|
|
1321
|
+
consecutiveErrors = 0;
|
|
1322
|
+
delay = Math.min(delay * POLL_BACKOFF_MULTIPLIER, POLL_MAX_DELAY_MS);
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
if (response.ok) {
|
|
1326
|
+
const data = await response.json();
|
|
1327
|
+
if (typeof data.accessToken !== "string" || typeof data.refreshToken !== "string") {
|
|
1328
|
+
throw new Error("Cursor poll response missing accessToken or refreshToken");
|
|
1329
|
+
}
|
|
1330
|
+
return { accessToken: data.accessToken, refreshToken: data.refreshToken };
|
|
1331
|
+
}
|
|
1332
|
+
consecutiveErrors++;
|
|
1333
|
+
if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS) {
|
|
1334
|
+
throw new Error(`Cursor auth poll failed: ${response.status}`);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
throw new Error("Cursor authentication polling timed out");
|
|
1338
|
+
}
|
|
1339
|
+
async function runCursorOAuthLogin(deps = {}) {
|
|
1340
|
+
const output = deps.output ?? console;
|
|
1341
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
1342
|
+
const pkce = generatePkcePair();
|
|
1343
|
+
const uuid = crypto.randomUUID();
|
|
1344
|
+
const params = new URLSearchParams({
|
|
1345
|
+
challenge: pkce.challenge,
|
|
1346
|
+
uuid,
|
|
1347
|
+
mode: "login",
|
|
1348
|
+
redirectTarget: "cli"
|
|
1349
|
+
});
|
|
1350
|
+
const loginUrl = `${CURSOR_LOGIN_URL}?${params.toString()}`;
|
|
1351
|
+
output.log(
|
|
1352
|
+
`
|
|
1353
|
+
Open this URL in your browser to authorize Cursor:
|
|
1354
|
+
${loginUrl}
|
|
1355
|
+
|
|
1356
|
+
Waiting for browser authentication... (polling ${CURSOR_POLL_URL})`
|
|
1357
|
+
);
|
|
1358
|
+
const openBrowser = deps.openBrowser;
|
|
1359
|
+
if (openBrowser) {
|
|
1360
|
+
try {
|
|
1361
|
+
await openBrowser(loginUrl);
|
|
1362
|
+
} catch {
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
const { accessToken, refreshToken } = await pollCursorAuth(uuid, pkce.verifier, {
|
|
1366
|
+
fetchImpl,
|
|
1367
|
+
...deps.sleep ? { sleepFn: deps.sleep } : {}
|
|
1368
|
+
});
|
|
1369
|
+
const now = deps.now?.() ?? Date.now();
|
|
1370
|
+
const expiresAt = decodeJwtExp(accessToken) ?? now + 3600 * 1e3 - ACCESS_TOKEN_CLIENT_SKEW_MS3;
|
|
1371
|
+
return {
|
|
1372
|
+
provider: "cursor",
|
|
1373
|
+
accessToken,
|
|
1374
|
+
refreshToken,
|
|
1375
|
+
expiresAt,
|
|
1376
|
+
obtainedAt: now
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
var cursorRefresh = async (credential, deps = {}) => {
|
|
1380
|
+
if (!credential.refreshToken) {
|
|
1381
|
+
throw new Error("Cursor credential has no refresh_token");
|
|
1382
|
+
}
|
|
1383
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
1384
|
+
const response = await fetchImpl(CURSOR_REFRESH_URL, {
|
|
1385
|
+
method: "POST",
|
|
1386
|
+
headers: {
|
|
1387
|
+
Authorization: `Bearer ${credential.refreshToken}`,
|
|
1388
|
+
"Content-Type": "application/json"
|
|
1389
|
+
},
|
|
1390
|
+
body: "{}",
|
|
1391
|
+
signal: AbortSignal.timeout(3e4)
|
|
1392
|
+
});
|
|
1393
|
+
if (!response.ok) {
|
|
1394
|
+
let detail = "";
|
|
1395
|
+
try {
|
|
1396
|
+
detail = (await response.text()).trim();
|
|
1397
|
+
} catch {
|
|
1398
|
+
}
|
|
1399
|
+
throw new Error(
|
|
1400
|
+
`Cursor token refresh failed: ${response.status}${detail ? ` ${detail}` : ""}`
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
const data = await response.json();
|
|
1404
|
+
if (typeof data.accessToken !== "string" || !data.accessToken) {
|
|
1405
|
+
throw new Error("Cursor token refresh response missing accessToken");
|
|
1406
|
+
}
|
|
1407
|
+
const now = deps.now?.() ?? Date.now();
|
|
1408
|
+
const expiresAt = decodeJwtExp(data.accessToken) ?? now + 3600 * 1e3 - ACCESS_TOKEN_CLIENT_SKEW_MS3;
|
|
1409
|
+
const newRefresh = typeof data.refreshToken === "string" && data.refreshToken ? data.refreshToken : credential.refreshToken;
|
|
1410
|
+
return {
|
|
1411
|
+
...credential,
|
|
1412
|
+
provider: "cursor",
|
|
1413
|
+
accessToken: data.accessToken,
|
|
1414
|
+
refreshToken: newRefresh,
|
|
1415
|
+
expiresAt,
|
|
1416
|
+
obtainedAt: now
|
|
1417
|
+
};
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
export {
|
|
1421
|
+
startCallbackServer,
|
|
1422
|
+
generatePkcePair,
|
|
1423
|
+
runAntigravityOAuthLogin,
|
|
1424
|
+
refreshAntigravityOAuthToken,
|
|
1425
|
+
runOpenAiCodexDeviceCode,
|
|
1426
|
+
runOpenAiCodexBrowserPkce,
|
|
1427
|
+
refreshOpenAiCodexToken,
|
|
1428
|
+
runXaiOAuthLogin,
|
|
1429
|
+
runXaiOAuthDeviceCode,
|
|
1430
|
+
refreshXaiOAuthToken,
|
|
1431
|
+
runCursorOAuthLogin,
|
|
1432
|
+
cursorRefresh
|
|
1433
|
+
};
|