opensteer 0.6.13 → 0.7.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/README.md +256 -184
- package/dist/chunk-PQYA6IX2.js +32571 -0
- package/dist/chunk-PQYA6IX2.js.map +1 -0
- package/dist/cli/bin.cjs +38201 -0
- package/dist/cli/bin.cjs.map +1 -0
- package/dist/cli/bin.d.cts +1 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +5612 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/index.cjs +31309 -16009
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4440 -670
- package/dist/index.d.ts +4440 -670
- package/dist/index.js +438 -378
- package/dist/index.js.map +1 -0
- package/package.json +56 -62
- package/skills/README.md +21 -20
- package/skills/opensteer/SKILL.md +60 -194
- package/skills/opensteer/references/cli-reference.md +69 -113
- package/skills/opensteer/references/request-workflow.md +81 -0
- package/skills/opensteer/references/sdk-reference.md +101 -154
- package/CHANGELOG.md +0 -75
- package/bin/opensteer.mjs +0 -1423
- package/dist/browser-profile-client-CGXc0-P9.d.cts +0 -228
- package/dist/browser-profile-client-DHLzMf-K.d.ts +0 -228
- package/dist/chunk-2ES46WCO.js +0 -1437
- package/dist/chunk-3H5RRIMZ.js +0 -69
- package/dist/chunk-AVXUMEDG.js +0 -62
- package/dist/chunk-DN3GI5CH.js +0 -57
- package/dist/chunk-FAHE5DB2.js +0 -190
- package/dist/chunk-HBTSQ2V4.js +0 -15259
- package/dist/chunk-K5CL76MG.js +0 -81
- package/dist/chunk-U724TBY6.js +0 -1262
- package/dist/chunk-ZRCFF546.js +0 -77
- package/dist/cli/auth.cjs +0 -2022
- package/dist/cli/auth.d.cts +0 -114
- package/dist/cli/auth.d.ts +0 -114
- package/dist/cli/auth.js +0 -15
- package/dist/cli/local-profile.cjs +0 -197
- package/dist/cli/local-profile.d.cts +0 -18
- package/dist/cli/local-profile.d.ts +0 -18
- package/dist/cli/local-profile.js +0 -97
- package/dist/cli/profile.cjs +0 -18548
- package/dist/cli/profile.d.cts +0 -79
- package/dist/cli/profile.d.ts +0 -79
- package/dist/cli/profile.js +0 -1328
- package/dist/cli/server.cjs +0 -17232
- package/dist/cli/server.d.cts +0 -2
- package/dist/cli/server.d.ts +0 -2
- package/dist/cli/server.js +0 -977
- package/dist/cli/skills-installer.cjs +0 -230
- package/dist/cli/skills-installer.d.cts +0 -28
- package/dist/cli/skills-installer.d.ts +0 -28
- package/dist/cli/skills-installer.js +0 -201
- package/dist/extractor-4Q3TFZJB.js +0 -8
- package/dist/resolver-MGN64KCP.js +0 -7
- package/dist/types-Cr10igF3.d.cts +0 -345
- package/dist/types-Cr10igF3.d.ts +0 -345
- package/skills/electron/SKILL.md +0 -87
- package/skills/electron/references/opensteer-electron-recipes.md +0 -88
- package/skills/electron/references/opensteer-electron-workflow.md +0 -85
- package/skills/opensteer/references/examples.md +0 -118
package/dist/cli/auth.cjs
DELETED
|
@@ -1,2022 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
|
|
30
|
-
// src/cli/auth.ts
|
|
31
|
-
var auth_exports = {};
|
|
32
|
-
__export(auth_exports, {
|
|
33
|
-
ensureCloudCredentialsForCommand: () => ensureCloudCredentialsForCommand,
|
|
34
|
-
ensureCloudCredentialsForOpenCommand: () => ensureCloudCredentialsForOpenCommand,
|
|
35
|
-
isCloudModeEnabledForRootDir: () => isCloudModeEnabledForRootDir,
|
|
36
|
-
parseOpensteerAuthArgs: () => parseOpensteerAuthArgs,
|
|
37
|
-
runOpensteerAuthCli: () => runOpensteerAuthCli
|
|
38
|
-
});
|
|
39
|
-
module.exports = __toCommonJS(auth_exports);
|
|
40
|
-
var import_open = __toESM(require("open"), 1);
|
|
41
|
-
|
|
42
|
-
// src/cloud/cdp-client.ts
|
|
43
|
-
var import_playwright = require("playwright");
|
|
44
|
-
|
|
45
|
-
// src/cloud/contracts.ts
|
|
46
|
-
var cloudActionMethods = [
|
|
47
|
-
"goto",
|
|
48
|
-
"snapshot",
|
|
49
|
-
"screenshot",
|
|
50
|
-
"state",
|
|
51
|
-
"click",
|
|
52
|
-
"dblclick",
|
|
53
|
-
"rightclick",
|
|
54
|
-
"hover",
|
|
55
|
-
"input",
|
|
56
|
-
"select",
|
|
57
|
-
"scroll",
|
|
58
|
-
"tabs",
|
|
59
|
-
"newTab",
|
|
60
|
-
"switchTab",
|
|
61
|
-
"closeTab",
|
|
62
|
-
"getCookies",
|
|
63
|
-
"setCookie",
|
|
64
|
-
"clearCookies",
|
|
65
|
-
"pressKey",
|
|
66
|
-
"type",
|
|
67
|
-
"getElementText",
|
|
68
|
-
"getElementValue",
|
|
69
|
-
"getElementAttributes",
|
|
70
|
-
"getElementBoundingBox",
|
|
71
|
-
"getHtml",
|
|
72
|
-
"getTitle",
|
|
73
|
-
"uploadFile",
|
|
74
|
-
"exportCookies",
|
|
75
|
-
"importCookies",
|
|
76
|
-
"waitForText",
|
|
77
|
-
"extract",
|
|
78
|
-
"extractFromPlan",
|
|
79
|
-
"clearCache"
|
|
80
|
-
];
|
|
81
|
-
var cloudErrorCodes = [
|
|
82
|
-
"CLOUD_AUTH_FAILED",
|
|
83
|
-
"CLOUD_SESSION_NOT_FOUND",
|
|
84
|
-
"CLOUD_SESSION_CLOSED",
|
|
85
|
-
"CLOUD_UNSUPPORTED_METHOD",
|
|
86
|
-
"CLOUD_INVALID_REQUEST",
|
|
87
|
-
"CLOUD_MODEL_NOT_ALLOWED",
|
|
88
|
-
"CLOUD_ACTION_FAILED",
|
|
89
|
-
"CLOUD_CAPACITY_EXHAUSTED",
|
|
90
|
-
"CLOUD_RUNTIME_UNAVAILABLE",
|
|
91
|
-
"CLOUD_RUNTIME_MISMATCH",
|
|
92
|
-
"CLOUD_SESSION_STALE",
|
|
93
|
-
"CLOUD_CONTROL_PLANE_ERROR",
|
|
94
|
-
"CLOUD_CONTRACT_MISMATCH",
|
|
95
|
-
"CLOUD_PROXY_UNAVAILABLE",
|
|
96
|
-
"CLOUD_PROXY_REQUIRED",
|
|
97
|
-
"CLOUD_BILLING_LIMIT_REACHED",
|
|
98
|
-
"CLOUD_RATE_LIMITED",
|
|
99
|
-
"CLOUD_BROWSER_PROFILE_NOT_FOUND",
|
|
100
|
-
"CLOUD_BROWSER_PROFILE_BUSY",
|
|
101
|
-
"CLOUD_BROWSER_PROFILE_DISABLED",
|
|
102
|
-
"CLOUD_BROWSER_PROFILE_PROXY_UNAVAILABLE",
|
|
103
|
-
"CLOUD_BROWSER_PROFILE_SYNC_FAILED",
|
|
104
|
-
"CLOUD_INTERNAL"
|
|
105
|
-
];
|
|
106
|
-
var cloudSessionStatuses = [
|
|
107
|
-
"provisioning",
|
|
108
|
-
"active",
|
|
109
|
-
"closing",
|
|
110
|
-
"closed",
|
|
111
|
-
"failed"
|
|
112
|
-
];
|
|
113
|
-
var cloudSessionSourceTypes = [
|
|
114
|
-
"agent-thread",
|
|
115
|
-
"agent-run",
|
|
116
|
-
"project-agent-run",
|
|
117
|
-
"local-cloud",
|
|
118
|
-
"manual"
|
|
119
|
-
];
|
|
120
|
-
var cloudActionMethodSet = new Set(cloudActionMethods);
|
|
121
|
-
var cloudErrorCodeSet = new Set(cloudErrorCodes);
|
|
122
|
-
var cloudSessionSourceTypeSet = new Set(
|
|
123
|
-
cloudSessionSourceTypes
|
|
124
|
-
);
|
|
125
|
-
var cloudSessionStatusSet = new Set(cloudSessionStatuses);
|
|
126
|
-
|
|
127
|
-
// src/utils/strip-trailing-slashes.ts
|
|
128
|
-
function stripTrailingSlashes(value) {
|
|
129
|
-
let end = value.length;
|
|
130
|
-
while (end > 0 && value.charCodeAt(end - 1) === 47) {
|
|
131
|
-
end -= 1;
|
|
132
|
-
}
|
|
133
|
-
return end === value.length ? value : value.slice(0, end);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// src/cloud/http-client.ts
|
|
137
|
-
function normalizeCloudBaseUrl(baseUrl) {
|
|
138
|
-
return stripTrailingSlashes(baseUrl);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// src/cloud/runtime.ts
|
|
142
|
-
var DEFAULT_CLOUD_BASE_URL = "https://api.opensteer.com";
|
|
143
|
-
|
|
144
|
-
// src/config.ts
|
|
145
|
-
var import_fs = __toESM(require("fs"), 1);
|
|
146
|
-
var import_path = __toESM(require("path"), 1);
|
|
147
|
-
var import_dotenv = require("dotenv");
|
|
148
|
-
|
|
149
|
-
// src/error-normalization.ts
|
|
150
|
-
function extractErrorMessage(error, fallback = "Unknown error.") {
|
|
151
|
-
if (error instanceof Error) {
|
|
152
|
-
const message = error.message.trim();
|
|
153
|
-
if (message) return message;
|
|
154
|
-
const name = error.name.trim();
|
|
155
|
-
if (name) return name;
|
|
156
|
-
}
|
|
157
|
-
if (typeof error === "string" && error.trim()) {
|
|
158
|
-
return error.trim();
|
|
159
|
-
}
|
|
160
|
-
const record = asRecord(error);
|
|
161
|
-
const recordMessage = toNonEmptyString(record?.message) || toNonEmptyString(record?.error);
|
|
162
|
-
if (recordMessage) {
|
|
163
|
-
return recordMessage;
|
|
164
|
-
}
|
|
165
|
-
return fallback;
|
|
166
|
-
}
|
|
167
|
-
function asRecord(value) {
|
|
168
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
return value;
|
|
172
|
-
}
|
|
173
|
-
function toNonEmptyString(value) {
|
|
174
|
-
if (typeof value !== "string") return void 0;
|
|
175
|
-
const normalized = value.trim();
|
|
176
|
-
return normalized.length ? normalized : void 0;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// src/cloud/credential-selection.ts
|
|
180
|
-
function selectCloudCredential(options) {
|
|
181
|
-
const apiKey = normalizeNonEmptyString(options.apiKey);
|
|
182
|
-
const accessToken = normalizeNonEmptyString(options.accessToken);
|
|
183
|
-
if (apiKey) {
|
|
184
|
-
if (options.authScheme === "bearer") {
|
|
185
|
-
return {
|
|
186
|
-
apiKey,
|
|
187
|
-
authScheme: "bearer",
|
|
188
|
-
kind: "access-token",
|
|
189
|
-
token: apiKey,
|
|
190
|
-
compatibilityBearerApiKey: true
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
apiKey,
|
|
195
|
-
authScheme: "api-key",
|
|
196
|
-
kind: "api-key",
|
|
197
|
-
token: apiKey
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
if (accessToken) {
|
|
201
|
-
return {
|
|
202
|
-
accessToken,
|
|
203
|
-
authScheme: "bearer",
|
|
204
|
-
kind: "access-token",
|
|
205
|
-
token: accessToken
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
function selectCloudCredentialByPrecedence(layers, authScheme) {
|
|
211
|
-
for (const layer of layers) {
|
|
212
|
-
const hasApiKey = layer.hasApiKey ?? Object.prototype.hasOwnProperty.call(layer, "apiKey");
|
|
213
|
-
const hasAccessToken = layer.hasAccessToken ?? Object.prototype.hasOwnProperty.call(layer, "accessToken");
|
|
214
|
-
if (!hasApiKey && !hasAccessToken) {
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
return {
|
|
218
|
-
source: layer.source,
|
|
219
|
-
apiKey: layer.apiKey,
|
|
220
|
-
accessToken: layer.accessToken,
|
|
221
|
-
hasApiKey,
|
|
222
|
-
hasAccessToken,
|
|
223
|
-
credential: selectCloudCredential({
|
|
224
|
-
apiKey: layer.apiKey,
|
|
225
|
-
accessToken: layer.accessToken,
|
|
226
|
-
authScheme: layer.authScheme ?? authScheme
|
|
227
|
-
})
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
function normalizeNonEmptyString(value) {
|
|
233
|
-
if (typeof value !== "string") return void 0;
|
|
234
|
-
const normalized = value.trim();
|
|
235
|
-
return normalized.length ? normalized : void 0;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// src/config.ts
|
|
239
|
-
var DEFAULT_CONFIG = {
|
|
240
|
-
browser: {
|
|
241
|
-
headless: void 0,
|
|
242
|
-
executablePath: void 0,
|
|
243
|
-
slowMo: 0,
|
|
244
|
-
mode: void 0,
|
|
245
|
-
cdpUrl: void 0,
|
|
246
|
-
userDataDir: void 0,
|
|
247
|
-
profileDirectory: void 0
|
|
248
|
-
},
|
|
249
|
-
storage: {
|
|
250
|
-
rootDir: process.cwd()
|
|
251
|
-
},
|
|
252
|
-
cursor: {
|
|
253
|
-
enabled: false,
|
|
254
|
-
profile: "snappy"
|
|
255
|
-
},
|
|
256
|
-
model: "gpt-5.1",
|
|
257
|
-
debug: false
|
|
258
|
-
};
|
|
259
|
-
function dotenvFileOrder(nodeEnv) {
|
|
260
|
-
const normalized = nodeEnv?.trim() || "";
|
|
261
|
-
const files = [];
|
|
262
|
-
if (normalized) {
|
|
263
|
-
files.push(`.env.${normalized}.local`);
|
|
264
|
-
}
|
|
265
|
-
if (normalized !== "test") {
|
|
266
|
-
files.push(".env.local");
|
|
267
|
-
}
|
|
268
|
-
if (normalized) {
|
|
269
|
-
files.push(`.env.${normalized}`);
|
|
270
|
-
}
|
|
271
|
-
files.push(".env");
|
|
272
|
-
return files;
|
|
273
|
-
}
|
|
274
|
-
function loadDotenvValues(rootDir, baseEnv, options = {}) {
|
|
275
|
-
const values = {};
|
|
276
|
-
if (parseBool(baseEnv.OPENSTEER_DISABLE_DOTENV_AUTOLOAD) === true) {
|
|
277
|
-
return values;
|
|
278
|
-
}
|
|
279
|
-
const debug = options.debug ?? parseBool(baseEnv.OPENSTEER_DEBUG) === true;
|
|
280
|
-
const baseDir = import_path.default.resolve(rootDir);
|
|
281
|
-
const nodeEnv = baseEnv.NODE_ENV?.trim() || "";
|
|
282
|
-
for (const filename of dotenvFileOrder(nodeEnv)) {
|
|
283
|
-
const filePath = import_path.default.join(baseDir, filename);
|
|
284
|
-
if (!import_fs.default.existsSync(filePath)) continue;
|
|
285
|
-
try {
|
|
286
|
-
const raw = import_fs.default.readFileSync(filePath, "utf8");
|
|
287
|
-
const parsed = (0, import_dotenv.parse)(raw);
|
|
288
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
289
|
-
if (values[key] === void 0) {
|
|
290
|
-
values[key] = value;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
} catch (error) {
|
|
294
|
-
const message = extractErrorMessage(
|
|
295
|
-
error,
|
|
296
|
-
"Unable to read or parse dotenv file."
|
|
297
|
-
);
|
|
298
|
-
if (debug) {
|
|
299
|
-
console.warn(
|
|
300
|
-
`[opensteer] failed to load dotenv file "${filePath}": ${message}`
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return values;
|
|
307
|
-
}
|
|
308
|
-
function resolveEnv(rootDir, options = {}) {
|
|
309
|
-
const baseEnv = options.baseEnv ?? process.env;
|
|
310
|
-
const dotenvValues = loadDotenvValues(rootDir, baseEnv, options);
|
|
311
|
-
return {
|
|
312
|
-
...dotenvValues,
|
|
313
|
-
...baseEnv
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
function hasOwn(config, key) {
|
|
317
|
-
if (!config || typeof config !== "object") return false;
|
|
318
|
-
return Object.prototype.hasOwnProperty.call(config, key);
|
|
319
|
-
}
|
|
320
|
-
function hasLegacyAiConfig(config) {
|
|
321
|
-
return hasOwn(config, "ai");
|
|
322
|
-
}
|
|
323
|
-
function assertNoLegacyAiConfig(source, config) {
|
|
324
|
-
if (hasLegacyAiConfig(config)) {
|
|
325
|
-
throw new Error(
|
|
326
|
-
`Legacy "ai" config is no longer supported in ${source}. Use top-level "model" instead.`
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
function assertNoLegacyRuntimeConfig(source, config) {
|
|
331
|
-
if (!config || typeof config !== "object") return;
|
|
332
|
-
const configRecord = config;
|
|
333
|
-
if (hasOwn(configRecord, "runtime")) {
|
|
334
|
-
throw new Error(
|
|
335
|
-
`Legacy "runtime" config is no longer supported in ${source}. Use top-level "cloud" instead.`
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
if (hasOwn(configRecord, "mode")) {
|
|
339
|
-
throw new Error(
|
|
340
|
-
`Top-level "mode" config is no longer supported in ${source}. Use "cloud: true" to enable cloud mode.`
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
if (hasOwn(configRecord, "remote")) {
|
|
344
|
-
throw new Error(
|
|
345
|
-
`Top-level "remote" config is no longer supported in ${source}. Use "cloud" options instead.`
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
if (hasOwn(configRecord, "apiKey")) {
|
|
349
|
-
throw new Error(
|
|
350
|
-
`Top-level "apiKey" config is not supported in ${source}. Use "cloud.apiKey" instead.`
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
function loadConfigFile(rootDir, options = {}) {
|
|
355
|
-
const configPath = import_path.default.join(rootDir, ".opensteer", "config.json");
|
|
356
|
-
if (!import_fs.default.existsSync(configPath)) return {};
|
|
357
|
-
try {
|
|
358
|
-
const raw = import_fs.default.readFileSync(configPath, "utf8");
|
|
359
|
-
return JSON.parse(raw);
|
|
360
|
-
} catch (error) {
|
|
361
|
-
const message = extractErrorMessage(
|
|
362
|
-
error,
|
|
363
|
-
"Unable to read or parse config file."
|
|
364
|
-
);
|
|
365
|
-
if (options.debug) {
|
|
366
|
-
console.warn(
|
|
367
|
-
`[opensteer] failed to load config file "${configPath}": ${message}`
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
return {};
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
function mergeDeep(base, patch) {
|
|
374
|
-
const out = {
|
|
375
|
-
...base
|
|
376
|
-
};
|
|
377
|
-
for (const [key, value] of Object.entries(patch || {})) {
|
|
378
|
-
const currentValue = out[key];
|
|
379
|
-
if (value && typeof value === "object" && !Array.isArray(value) && currentValue && typeof currentValue === "object" && !Array.isArray(currentValue)) {
|
|
380
|
-
out[key] = mergeDeep(
|
|
381
|
-
currentValue,
|
|
382
|
-
value
|
|
383
|
-
);
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
if (value !== void 0) {
|
|
387
|
-
out[key] = value;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
return out;
|
|
391
|
-
}
|
|
392
|
-
function parseBool(value) {
|
|
393
|
-
if (value == null) return void 0;
|
|
394
|
-
const normalized = value.trim().toLowerCase();
|
|
395
|
-
if (normalized === "true" || normalized === "1") return true;
|
|
396
|
-
if (normalized === "false" || normalized === "0") return false;
|
|
397
|
-
return void 0;
|
|
398
|
-
}
|
|
399
|
-
function parseNumber(value) {
|
|
400
|
-
if (value == null || value.trim() === "") return void 0;
|
|
401
|
-
const parsed = Number(value);
|
|
402
|
-
if (!Number.isFinite(parsed)) return void 0;
|
|
403
|
-
return parsed;
|
|
404
|
-
}
|
|
405
|
-
function parseRuntimeMode(value, source) {
|
|
406
|
-
if (value == null) return void 0;
|
|
407
|
-
if (typeof value !== "string") {
|
|
408
|
-
throw new Error(
|
|
409
|
-
`Invalid ${source} value "${String(value)}". Use "local" or "cloud".`
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
const normalized = value.trim().toLowerCase();
|
|
413
|
-
if (!normalized) return void 0;
|
|
414
|
-
if (normalized === "local" || normalized === "cloud") {
|
|
415
|
-
return normalized;
|
|
416
|
-
}
|
|
417
|
-
throw new Error(
|
|
418
|
-
`Invalid ${source} value "${value}". Use "local" or "cloud".`
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
function parseAuthScheme(value, source) {
|
|
422
|
-
if (value == null) return void 0;
|
|
423
|
-
if (typeof value !== "string") {
|
|
424
|
-
throw new Error(
|
|
425
|
-
`Invalid ${source} value "${String(value)}". Use "api-key" or "bearer".`
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
const normalized = value.trim().toLowerCase();
|
|
429
|
-
if (!normalized) return void 0;
|
|
430
|
-
if (normalized === "api-key" || normalized === "bearer") {
|
|
431
|
-
return normalized;
|
|
432
|
-
}
|
|
433
|
-
throw new Error(
|
|
434
|
-
`Invalid ${source} value "${value}". Use "api-key" or "bearer".`
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
function parseCloudAnnounce(value, source) {
|
|
438
|
-
if (value == null) return void 0;
|
|
439
|
-
if (typeof value !== "string") {
|
|
440
|
-
throw new Error(
|
|
441
|
-
`Invalid ${source} value "${String(value)}". Use "always", "off", or "tty".`
|
|
442
|
-
);
|
|
443
|
-
}
|
|
444
|
-
const normalized = value.trim().toLowerCase();
|
|
445
|
-
if (!normalized) return void 0;
|
|
446
|
-
if (normalized === "always" || normalized === "off" || normalized === "tty") {
|
|
447
|
-
return normalized;
|
|
448
|
-
}
|
|
449
|
-
throw new Error(
|
|
450
|
-
`Invalid ${source} value "${value}". Use "always", "off", or "tty".`
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
function resolveOpensteerApiKey(env) {
|
|
454
|
-
const value = env.OPENSTEER_API_KEY?.trim();
|
|
455
|
-
if (!value) return void 0;
|
|
456
|
-
return value;
|
|
457
|
-
}
|
|
458
|
-
function resolveOpensteerAccessToken(env) {
|
|
459
|
-
const value = env.OPENSTEER_ACCESS_TOKEN?.trim();
|
|
460
|
-
if (!value) return void 0;
|
|
461
|
-
return value;
|
|
462
|
-
}
|
|
463
|
-
function resolveOpensteerBaseUrl(env) {
|
|
464
|
-
const value = env.OPENSTEER_BASE_URL?.trim();
|
|
465
|
-
if (!value) return void 0;
|
|
466
|
-
return value;
|
|
467
|
-
}
|
|
468
|
-
function resolveOpensteerAuthScheme(env) {
|
|
469
|
-
return parseAuthScheme(env.OPENSTEER_AUTH_SCHEME, "OPENSTEER_AUTH_SCHEME");
|
|
470
|
-
}
|
|
471
|
-
function resolveOpensteerCloudProfileId(env) {
|
|
472
|
-
const value = env.OPENSTEER_CLOUD_PROFILE_ID?.trim();
|
|
473
|
-
if (!value) return void 0;
|
|
474
|
-
return value;
|
|
475
|
-
}
|
|
476
|
-
function resolveOpensteerCloudProfileReuseIfActive(env) {
|
|
477
|
-
return parseBool(env.OPENSTEER_CLOUD_PROFILE_REUSE_IF_ACTIVE);
|
|
478
|
-
}
|
|
479
|
-
function parseCloudBrowserProfileReuseIfActive(value) {
|
|
480
|
-
if (value == null) return void 0;
|
|
481
|
-
if (typeof value !== "boolean") {
|
|
482
|
-
throw new Error(
|
|
483
|
-
`Invalid cloud.browserProfile.reuseIfActive value "${String(
|
|
484
|
-
value
|
|
485
|
-
)}". Use true or false.`
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
return value;
|
|
489
|
-
}
|
|
490
|
-
function normalizeCloudBrowserProfileOptions(value, source) {
|
|
491
|
-
if (value == null) {
|
|
492
|
-
return void 0;
|
|
493
|
-
}
|
|
494
|
-
if (typeof value !== "object" || Array.isArray(value)) {
|
|
495
|
-
throw new Error(
|
|
496
|
-
`Invalid ${source} value "${String(value)}". Use an object with profileId and optional reuseIfActive.`
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
const record = value;
|
|
500
|
-
const rawProfileId = record.profileId;
|
|
501
|
-
if (typeof rawProfileId !== "string" || !rawProfileId.trim()) {
|
|
502
|
-
throw new Error(
|
|
503
|
-
`${source}.profileId must be a non-empty string when browserProfile is provided.`
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
return {
|
|
507
|
-
profileId: rawProfileId.trim(),
|
|
508
|
-
reuseIfActive: parseCloudBrowserProfileReuseIfActive(record.reuseIfActive)
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
function resolveEnvCloudBrowserProfile(profileId, reuseIfActive) {
|
|
512
|
-
if (reuseIfActive !== void 0 && !profileId) {
|
|
513
|
-
throw new Error(
|
|
514
|
-
"OPENSTEER_CLOUD_PROFILE_REUSE_IF_ACTIVE requires OPENSTEER_CLOUD_PROFILE_ID."
|
|
515
|
-
);
|
|
516
|
-
}
|
|
517
|
-
if (!profileId) {
|
|
518
|
-
return void 0;
|
|
519
|
-
}
|
|
520
|
-
return {
|
|
521
|
-
profileId,
|
|
522
|
-
reuseIfActive
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
function normalizeCloudOptions(value) {
|
|
526
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
527
|
-
return void 0;
|
|
528
|
-
}
|
|
529
|
-
return value;
|
|
530
|
-
}
|
|
531
|
-
function parseCloudEnabled(value, source) {
|
|
532
|
-
if (value == null) return void 0;
|
|
533
|
-
if (typeof value === "boolean") return value;
|
|
534
|
-
if (typeof value === "object" && !Array.isArray(value)) return true;
|
|
535
|
-
throw new Error(
|
|
536
|
-
`Invalid ${source} value "${String(value)}". Use true, false, or a cloud options object.`
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
function resolveCloudCredentialFields(selectedLayer) {
|
|
540
|
-
const credential = selectedLayer?.credential;
|
|
541
|
-
if (!credential) return {};
|
|
542
|
-
if (credential.kind === "api-key" || credential.compatibilityBearerApiKey === true && selectedLayer?.source !== "env") {
|
|
543
|
-
return {
|
|
544
|
-
apiKey: credential.token
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
return {
|
|
548
|
-
accessToken: credential.token
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
function resolveCloudSelection(config, env = process.env) {
|
|
552
|
-
const configCloud = parseCloudEnabled(config.cloud, "cloud");
|
|
553
|
-
if (configCloud !== void 0) {
|
|
554
|
-
return {
|
|
555
|
-
cloud: configCloud,
|
|
556
|
-
source: "config.cloud"
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
const envMode = parseRuntimeMode(env.OPENSTEER_MODE, "OPENSTEER_MODE");
|
|
560
|
-
if (envMode) {
|
|
561
|
-
return {
|
|
562
|
-
cloud: envMode === "cloud",
|
|
563
|
-
source: "env.OPENSTEER_MODE"
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
return {
|
|
567
|
-
cloud: false,
|
|
568
|
-
source: "default"
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
function resolveConfigWithEnv(input = {}, options = {}) {
|
|
572
|
-
const processEnv = options.env ?? process.env;
|
|
573
|
-
const debugHint = typeof input.debug === "boolean" ? input.debug : parseBool(processEnv.OPENSTEER_DEBUG) === true;
|
|
574
|
-
const initialRootDir = input.storage?.rootDir ?? process.cwd();
|
|
575
|
-
const runtimeDefaults = mergeDeep(DEFAULT_CONFIG, {
|
|
576
|
-
storage: {
|
|
577
|
-
rootDir: initialRootDir
|
|
578
|
-
}
|
|
579
|
-
});
|
|
580
|
-
assertNoLegacyAiConfig("Opensteer constructor config", input);
|
|
581
|
-
assertNoLegacyRuntimeConfig("Opensteer constructor config", input);
|
|
582
|
-
const fileConfig = loadConfigFile(initialRootDir, {
|
|
583
|
-
debug: debugHint
|
|
584
|
-
});
|
|
585
|
-
assertNoLegacyAiConfig(".opensteer/config.json", fileConfig);
|
|
586
|
-
assertNoLegacyRuntimeConfig(".opensteer/config.json", fileConfig);
|
|
587
|
-
const fileCloudOptions = normalizeCloudOptions(fileConfig.cloud);
|
|
588
|
-
const fileHasCloudApiKey = hasOwn(fileCloudOptions, "apiKey");
|
|
589
|
-
const fileHasCloudAccessToken = hasOwn(fileCloudOptions, "accessToken");
|
|
590
|
-
const fileRootDir = typeof fileConfig.storage?.rootDir === "string" ? fileConfig.storage.rootDir : void 0;
|
|
591
|
-
assertNoRemovedBrowserConfig(input.browser, "Opensteer constructor config");
|
|
592
|
-
assertNoRemovedBrowserConfig(fileConfig.browser, ".opensteer/config.json");
|
|
593
|
-
const envRootDir = input.storage?.rootDir ?? fileRootDir ?? initialRootDir;
|
|
594
|
-
const env = resolveEnv(envRootDir, {
|
|
595
|
-
debug: debugHint,
|
|
596
|
-
baseEnv: processEnv
|
|
597
|
-
});
|
|
598
|
-
if (env.OPENSTEER_AI_MODEL) {
|
|
599
|
-
throw new Error(
|
|
600
|
-
"OPENSTEER_AI_MODEL is no longer supported. Use OPENSTEER_MODEL instead."
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
if (env.OPENSTEER_RUNTIME != null) {
|
|
604
|
-
throw new Error(
|
|
605
|
-
"OPENSTEER_RUNTIME is no longer supported. Use OPENSTEER_MODE instead."
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
if (env.OPENSTEER_CONNECT_URL != null) {
|
|
609
|
-
throw new Error(
|
|
610
|
-
"OPENSTEER_CONNECT_URL is no longer supported. Use OPENSTEER_CDP_URL instead."
|
|
611
|
-
);
|
|
612
|
-
}
|
|
613
|
-
if (env.OPENSTEER_CHANNEL != null) {
|
|
614
|
-
throw new Error(
|
|
615
|
-
"OPENSTEER_CHANNEL is no longer supported. Use OPENSTEER_BROWSER plus OPENSTEER_BROWSER_PATH when needed."
|
|
616
|
-
);
|
|
617
|
-
}
|
|
618
|
-
if (env.OPENSTEER_PROFILE_DIR != null) {
|
|
619
|
-
throw new Error(
|
|
620
|
-
"OPENSTEER_PROFILE_DIR is no longer supported. Use OPENSTEER_USER_DATA_DIR and OPENSTEER_PROFILE_DIRECTORY instead."
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
const envConfig = {
|
|
624
|
-
browser: {
|
|
625
|
-
headless: parseBool(env.OPENSTEER_HEADLESS),
|
|
626
|
-
executablePath: env.OPENSTEER_BROWSER_PATH || void 0,
|
|
627
|
-
slowMo: parseNumber(env.OPENSTEER_SLOW_MO),
|
|
628
|
-
mode: env.OPENSTEER_BROWSER === "real" || env.OPENSTEER_BROWSER === "chromium" ? env.OPENSTEER_BROWSER : void 0,
|
|
629
|
-
cdpUrl: env.OPENSTEER_CDP_URL || void 0,
|
|
630
|
-
userDataDir: env.OPENSTEER_USER_DATA_DIR || void 0,
|
|
631
|
-
profileDirectory: env.OPENSTEER_PROFILE_DIRECTORY || void 0
|
|
632
|
-
},
|
|
633
|
-
cursor: {
|
|
634
|
-
enabled: parseBool(env.OPENSTEER_CURSOR)
|
|
635
|
-
},
|
|
636
|
-
model: env.OPENSTEER_MODEL || void 0,
|
|
637
|
-
debug: parseBool(env.OPENSTEER_DEBUG)
|
|
638
|
-
};
|
|
639
|
-
const mergedWithFile = mergeDeep(runtimeDefaults, fileConfig);
|
|
640
|
-
const mergedWithEnv = mergeDeep(mergedWithFile, envConfig);
|
|
641
|
-
const resolved = mergeDeep(mergedWithEnv, input);
|
|
642
|
-
const browserHeadlessExplicit = input.browser?.headless !== void 0 || fileConfig.browser?.headless !== void 0 || envConfig.browser?.headless !== void 0;
|
|
643
|
-
if (!browserHeadlessExplicit && resolved.browser?.mode === "real") {
|
|
644
|
-
resolved.browser = {
|
|
645
|
-
...resolved.browser,
|
|
646
|
-
headless: true
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
function assertNoRemovedBrowserConfig(value, source) {
|
|
650
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
const record = value;
|
|
654
|
-
if (record.connectUrl !== void 0) {
|
|
655
|
-
throw new Error(
|
|
656
|
-
`${source}.browser.connectUrl is no longer supported. Use browser.cdpUrl instead.`
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
if (record.channel !== void 0) {
|
|
660
|
-
throw new Error(
|
|
661
|
-
`${source}.browser.channel is no longer supported. Use browser.mode plus browser.executablePath instead.`
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
if (record.profileDir !== void 0) {
|
|
665
|
-
throw new Error(
|
|
666
|
-
`${source}.browser.profileDir is no longer supported. Use browser.userDataDir and browser.profileDirectory instead.`
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
const envApiKey = resolveOpensteerApiKey(env);
|
|
671
|
-
const envAccessTokenRaw = resolveOpensteerAccessToken(env);
|
|
672
|
-
const envBaseUrl = resolveOpensteerBaseUrl(env);
|
|
673
|
-
const envAuthScheme = resolveOpensteerAuthScheme(env);
|
|
674
|
-
const envCloudProfileId = resolveOpensteerCloudProfileId(env);
|
|
675
|
-
const envCloudProfileReuseIfActive = resolveOpensteerCloudProfileReuseIfActive(env);
|
|
676
|
-
const envCloudAnnounce = parseCloudAnnounce(
|
|
677
|
-
env.OPENSTEER_REMOTE_ANNOUNCE,
|
|
678
|
-
"OPENSTEER_REMOTE_ANNOUNCE"
|
|
679
|
-
);
|
|
680
|
-
const inputCloudOptions = normalizeCloudOptions(input.cloud);
|
|
681
|
-
const inputCloudBrowserProfile = normalizeCloudBrowserProfileOptions(
|
|
682
|
-
inputCloudOptions?.browserProfile,
|
|
683
|
-
"cloud.browserProfile"
|
|
684
|
-
);
|
|
685
|
-
const inputAuthScheme = parseAuthScheme(
|
|
686
|
-
inputCloudOptions?.authScheme,
|
|
687
|
-
"cloud.authScheme"
|
|
688
|
-
);
|
|
689
|
-
const inputCloudAnnounce = parseCloudAnnounce(
|
|
690
|
-
inputCloudOptions?.announce,
|
|
691
|
-
"cloud.announce"
|
|
692
|
-
);
|
|
693
|
-
const inputHasCloudApiKey = Boolean(
|
|
694
|
-
inputCloudOptions && Object.prototype.hasOwnProperty.call(inputCloudOptions, "apiKey")
|
|
695
|
-
);
|
|
696
|
-
const inputHasCloudAccessToken = Boolean(
|
|
697
|
-
inputCloudOptions && Object.prototype.hasOwnProperty.call(inputCloudOptions, "accessToken")
|
|
698
|
-
);
|
|
699
|
-
const inputHasCloudBaseUrl = Boolean(
|
|
700
|
-
inputCloudOptions && Object.prototype.hasOwnProperty.call(inputCloudOptions, "baseUrl")
|
|
701
|
-
);
|
|
702
|
-
const cloudSelection = resolveCloudSelection({
|
|
703
|
-
cloud: resolved.cloud
|
|
704
|
-
}, env);
|
|
705
|
-
if (cloudSelection.cloud) {
|
|
706
|
-
const resolvedCloud = normalizeCloudOptions(resolved.cloud) ?? {};
|
|
707
|
-
const {
|
|
708
|
-
apiKey: resolvedCloudApiKeyRaw,
|
|
709
|
-
accessToken: resolvedCloudAccessTokenRaw,
|
|
710
|
-
...resolvedCloudRest
|
|
711
|
-
} = resolvedCloud;
|
|
712
|
-
const resolvedCloudBrowserProfile = normalizeCloudBrowserProfileOptions(
|
|
713
|
-
resolvedCloud.browserProfile,
|
|
714
|
-
"resolved.cloud.browserProfile"
|
|
715
|
-
);
|
|
716
|
-
const envCloudBrowserProfile = resolveEnvCloudBrowserProfile(
|
|
717
|
-
envCloudProfileId,
|
|
718
|
-
envCloudProfileReuseIfActive
|
|
719
|
-
);
|
|
720
|
-
const browserProfile = inputCloudBrowserProfile ?? envCloudBrowserProfile ?? resolvedCloudBrowserProfile;
|
|
721
|
-
let authScheme = inputAuthScheme ?? envAuthScheme ?? parseAuthScheme(resolvedCloud.authScheme, "cloud.authScheme") ?? "api-key";
|
|
722
|
-
const announce = inputCloudAnnounce ?? envCloudAnnounce ?? parseCloudAnnounce(resolvedCloud.announce, "cloud.announce") ?? "always";
|
|
723
|
-
const selectedCredentialLayer = selectCloudCredentialByPrecedence(
|
|
724
|
-
[
|
|
725
|
-
{
|
|
726
|
-
source: "input",
|
|
727
|
-
apiKey: inputCloudOptions?.apiKey,
|
|
728
|
-
accessToken: inputCloudOptions?.accessToken,
|
|
729
|
-
hasApiKey: inputHasCloudApiKey,
|
|
730
|
-
hasAccessToken: inputHasCloudAccessToken
|
|
731
|
-
},
|
|
732
|
-
{
|
|
733
|
-
source: "env",
|
|
734
|
-
apiKey: envApiKey,
|
|
735
|
-
accessToken: envAccessTokenRaw,
|
|
736
|
-
hasApiKey: envApiKey !== void 0,
|
|
737
|
-
hasAccessToken: envAccessTokenRaw !== void 0
|
|
738
|
-
},
|
|
739
|
-
{
|
|
740
|
-
source: "file",
|
|
741
|
-
apiKey: fileCloudOptions?.apiKey,
|
|
742
|
-
accessToken: fileCloudOptions?.accessToken,
|
|
743
|
-
hasApiKey: fileHasCloudApiKey,
|
|
744
|
-
hasAccessToken: fileHasCloudAccessToken
|
|
745
|
-
}
|
|
746
|
-
],
|
|
747
|
-
authScheme
|
|
748
|
-
);
|
|
749
|
-
const { apiKey, accessToken } = resolveCloudCredentialFields(selectedCredentialLayer);
|
|
750
|
-
if (accessToken) {
|
|
751
|
-
authScheme = "bearer";
|
|
752
|
-
}
|
|
753
|
-
resolved.cloud = {
|
|
754
|
-
...resolvedCloudRest,
|
|
755
|
-
...apiKey ? { apiKey } : selectedCredentialLayer?.hasApiKey && !accessToken ? { apiKey: selectedCredentialLayer.apiKey } : {},
|
|
756
|
-
...accessToken ? { accessToken } : selectedCredentialLayer?.hasAccessToken && !apiKey ? { accessToken: selectedCredentialLayer.accessToken } : {},
|
|
757
|
-
authScheme,
|
|
758
|
-
announce,
|
|
759
|
-
...browserProfile ? { browserProfile } : {}
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
if (envBaseUrl && cloudSelection.cloud && !inputHasCloudBaseUrl) {
|
|
763
|
-
resolved.cloud = {
|
|
764
|
-
...normalizeCloudOptions(resolved.cloud) ?? {},
|
|
765
|
-
baseUrl: envBaseUrl
|
|
766
|
-
};
|
|
767
|
-
}
|
|
768
|
-
return {
|
|
769
|
-
config: resolved,
|
|
770
|
-
env
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// src/auth/credential-resolver.ts
|
|
775
|
-
function resolveCloudCredential(options) {
|
|
776
|
-
const flagCredential = selectCloudCredential({
|
|
777
|
-
apiKey: options.apiKeyFlag,
|
|
778
|
-
accessToken: options.accessTokenFlag
|
|
779
|
-
});
|
|
780
|
-
if (flagCredential) {
|
|
781
|
-
return toResolvedCloudCredential("flag", flagCredential);
|
|
782
|
-
}
|
|
783
|
-
const envAuthScheme = parseEnvAuthScheme(options.env.OPENSTEER_AUTH_SCHEME);
|
|
784
|
-
const envCredential = selectCloudCredential({
|
|
785
|
-
apiKey: options.env.OPENSTEER_API_KEY,
|
|
786
|
-
accessToken: options.env.OPENSTEER_ACCESS_TOKEN,
|
|
787
|
-
authScheme: envAuthScheme
|
|
788
|
-
});
|
|
789
|
-
if (envCredential) {
|
|
790
|
-
return toResolvedCloudCredential("env", envCredential);
|
|
791
|
-
}
|
|
792
|
-
return null;
|
|
793
|
-
}
|
|
794
|
-
function applyCloudCredentialToEnv(env, credential) {
|
|
795
|
-
if (credential.kind === "access-token") {
|
|
796
|
-
env.OPENSTEER_ACCESS_TOKEN = credential.token;
|
|
797
|
-
delete env.OPENSTEER_API_KEY;
|
|
798
|
-
env.OPENSTEER_AUTH_SCHEME = "bearer";
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
env.OPENSTEER_API_KEY = credential.token;
|
|
802
|
-
delete env.OPENSTEER_ACCESS_TOKEN;
|
|
803
|
-
env.OPENSTEER_AUTH_SCHEME = credential.authScheme;
|
|
804
|
-
}
|
|
805
|
-
function parseEnvAuthScheme(value) {
|
|
806
|
-
const normalized = normalizeToken(value);
|
|
807
|
-
if (!normalized) return void 0;
|
|
808
|
-
if (normalized === "api-key" || normalized === "bearer") {
|
|
809
|
-
return normalized;
|
|
810
|
-
}
|
|
811
|
-
throw new Error(
|
|
812
|
-
`Invalid OPENSTEER_AUTH_SCHEME value "${value}". Use "api-key" or "bearer".`
|
|
813
|
-
);
|
|
814
|
-
}
|
|
815
|
-
function normalizeToken(value) {
|
|
816
|
-
if (typeof value !== "string") return void 0;
|
|
817
|
-
const normalized = value.trim();
|
|
818
|
-
return normalized.length ? normalized : void 0;
|
|
819
|
-
}
|
|
820
|
-
function toResolvedCloudCredential(source, credential) {
|
|
821
|
-
if (credential.compatibilityBearerApiKey) {
|
|
822
|
-
return {
|
|
823
|
-
kind: credential.kind,
|
|
824
|
-
source,
|
|
825
|
-
token: credential.token,
|
|
826
|
-
authScheme: credential.authScheme,
|
|
827
|
-
compatibilityBearerApiKey: true
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
|
-
return {
|
|
831
|
-
kind: credential.kind,
|
|
832
|
-
source,
|
|
833
|
-
token: credential.token,
|
|
834
|
-
authScheme: credential.authScheme
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// src/auth/machine-credential-store.ts
|
|
839
|
-
var import_node_crypto = require("crypto");
|
|
840
|
-
var import_node_fs = __toESM(require("fs"), 1);
|
|
841
|
-
var import_node_os = __toESM(require("os"), 1);
|
|
842
|
-
var import_node_path = __toESM(require("path"), 1);
|
|
843
|
-
|
|
844
|
-
// src/auth/keychain-store.ts
|
|
845
|
-
var import_node_child_process = require("child_process");
|
|
846
|
-
function commandExists(command) {
|
|
847
|
-
const result = (0, import_node_child_process.spawnSync)(command, ["--help"], {
|
|
848
|
-
encoding: "utf8",
|
|
849
|
-
stdio: "ignore"
|
|
850
|
-
});
|
|
851
|
-
return result.error == null;
|
|
852
|
-
}
|
|
853
|
-
function commandFailed(result) {
|
|
854
|
-
return typeof result.status === "number" && result.status !== 0;
|
|
855
|
-
}
|
|
856
|
-
function sanitizeCommandArgs(command, args) {
|
|
857
|
-
if (command !== "security") {
|
|
858
|
-
return args;
|
|
859
|
-
}
|
|
860
|
-
const sanitized = [];
|
|
861
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
862
|
-
const value = args[index];
|
|
863
|
-
sanitized.push(value);
|
|
864
|
-
if (value === "-w" && index + 1 < args.length) {
|
|
865
|
-
sanitized.push("[REDACTED]");
|
|
866
|
-
index += 1;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
return sanitized;
|
|
870
|
-
}
|
|
871
|
-
function buildCommandError(command, args, result) {
|
|
872
|
-
const stderr = typeof result.stderr === "string" && result.stderr.trim() ? result.stderr.trim() : `Command "${command}" failed with status ${String(result.status)}.`;
|
|
873
|
-
const sanitizedArgs = sanitizeCommandArgs(command, args);
|
|
874
|
-
return new Error(
|
|
875
|
-
[
|
|
876
|
-
`Unable to persist credential via ${command}.`,
|
|
877
|
-
`${command} ${sanitizedArgs.join(" ")}`,
|
|
878
|
-
stderr
|
|
879
|
-
].join(" ")
|
|
880
|
-
);
|
|
881
|
-
}
|
|
882
|
-
function createMacosSecurityStore() {
|
|
883
|
-
return {
|
|
884
|
-
backend: "macos-security",
|
|
885
|
-
get(service, account) {
|
|
886
|
-
const result = (0, import_node_child_process.spawnSync)(
|
|
887
|
-
"security",
|
|
888
|
-
["find-generic-password", "-s", service, "-a", account, "-w"],
|
|
889
|
-
{ encoding: "utf8" }
|
|
890
|
-
);
|
|
891
|
-
if (commandFailed(result)) {
|
|
892
|
-
return null;
|
|
893
|
-
}
|
|
894
|
-
const secret = result.stdout.trim();
|
|
895
|
-
return secret.length ? secret : null;
|
|
896
|
-
},
|
|
897
|
-
set(service, account, secret) {
|
|
898
|
-
const args = [
|
|
899
|
-
"add-generic-password",
|
|
900
|
-
"-U",
|
|
901
|
-
"-s",
|
|
902
|
-
service,
|
|
903
|
-
"-a",
|
|
904
|
-
account,
|
|
905
|
-
"-w",
|
|
906
|
-
secret
|
|
907
|
-
];
|
|
908
|
-
const result = (0, import_node_child_process.spawnSync)("security", args, { encoding: "utf8" });
|
|
909
|
-
if (commandFailed(result)) {
|
|
910
|
-
throw buildCommandError("security", args, result);
|
|
911
|
-
}
|
|
912
|
-
},
|
|
913
|
-
delete(service, account) {
|
|
914
|
-
const args = ["delete-generic-password", "-s", service, "-a", account];
|
|
915
|
-
const result = (0, import_node_child_process.spawnSync)("security", args, { encoding: "utf8" });
|
|
916
|
-
if (commandFailed(result)) {
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
function createLinuxSecretToolStore() {
|
|
923
|
-
return {
|
|
924
|
-
backend: "linux-secret-tool",
|
|
925
|
-
get(service, account) {
|
|
926
|
-
const result = (0, import_node_child_process.spawnSync)(
|
|
927
|
-
"secret-tool",
|
|
928
|
-
["lookup", "service", service, "account", account],
|
|
929
|
-
{
|
|
930
|
-
encoding: "utf8"
|
|
931
|
-
}
|
|
932
|
-
);
|
|
933
|
-
if (commandFailed(result)) {
|
|
934
|
-
return null;
|
|
935
|
-
}
|
|
936
|
-
const secret = result.stdout.trim();
|
|
937
|
-
return secret.length ? secret : null;
|
|
938
|
-
},
|
|
939
|
-
set(service, account, secret) {
|
|
940
|
-
const args = [
|
|
941
|
-
"store",
|
|
942
|
-
"--label",
|
|
943
|
-
"Opensteer CLI",
|
|
944
|
-
"service",
|
|
945
|
-
service,
|
|
946
|
-
"account",
|
|
947
|
-
account
|
|
948
|
-
];
|
|
949
|
-
const result = (0, import_node_child_process.spawnSync)("secret-tool", args, {
|
|
950
|
-
encoding: "utf8",
|
|
951
|
-
input: secret
|
|
952
|
-
});
|
|
953
|
-
if (commandFailed(result)) {
|
|
954
|
-
throw buildCommandError("secret-tool", args, result);
|
|
955
|
-
}
|
|
956
|
-
},
|
|
957
|
-
delete(service, account) {
|
|
958
|
-
const args = ["clear", "service", service, "account", account];
|
|
959
|
-
(0, import_node_child_process.spawnSync)("secret-tool", args, {
|
|
960
|
-
encoding: "utf8"
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
function createKeychainStore() {
|
|
966
|
-
if (process.platform === "darwin") {
|
|
967
|
-
if (!commandExists("security")) {
|
|
968
|
-
return null;
|
|
969
|
-
}
|
|
970
|
-
return createMacosSecurityStore();
|
|
971
|
-
}
|
|
972
|
-
if (process.platform === "linux") {
|
|
973
|
-
if (!commandExists("secret-tool")) {
|
|
974
|
-
return null;
|
|
975
|
-
}
|
|
976
|
-
return createLinuxSecretToolStore();
|
|
977
|
-
}
|
|
978
|
-
return null;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
// src/auth/machine-credential-store.ts
|
|
982
|
-
var METADATA_VERSION = 2;
|
|
983
|
-
var ACTIVE_TARGET_VERSION = 2;
|
|
984
|
-
var KEYCHAIN_SERVICE = "com.opensteer.cli.cloud";
|
|
985
|
-
var KEYCHAIN_ACCOUNT_PREFIX = "machine:";
|
|
986
|
-
var ACTIVE_TARGET_FILE_NAME = "cli-target.json";
|
|
987
|
-
var MachineCredentialStore = class {
|
|
988
|
-
authDir;
|
|
989
|
-
warn;
|
|
990
|
-
keychain = createKeychainStore();
|
|
991
|
-
warnedFallback = false;
|
|
992
|
-
constructor(options = {}) {
|
|
993
|
-
const appName = options.appName || "opensteer";
|
|
994
|
-
const env = options.env ?? process.env;
|
|
995
|
-
const configDir = resolveConfigDir(appName, env);
|
|
996
|
-
this.authDir = import_node_path.default.join(configDir, "auth");
|
|
997
|
-
this.warn = options.warn ?? (() => void 0);
|
|
998
|
-
}
|
|
999
|
-
readCloudCredential(target) {
|
|
1000
|
-
const normalizedTarget = normalizeCloudCredentialTarget(target);
|
|
1001
|
-
return this.readCredentialSlot(
|
|
1002
|
-
resolveCredentialSlot(this.authDir, normalizedTarget),
|
|
1003
|
-
normalizedTarget
|
|
1004
|
-
);
|
|
1005
|
-
}
|
|
1006
|
-
writeCloudCredential(args) {
|
|
1007
|
-
const accessToken = args.accessToken.trim();
|
|
1008
|
-
const refreshToken2 = args.refreshToken.trim();
|
|
1009
|
-
if (!accessToken || !refreshToken2) {
|
|
1010
|
-
throw new Error("Cannot persist empty machine credential secrets.");
|
|
1011
|
-
}
|
|
1012
|
-
const baseUrl = normalizeCredentialUrl(args.baseUrl);
|
|
1013
|
-
const slot = resolveCredentialSlot(this.authDir, { baseUrl });
|
|
1014
|
-
ensureDirectory(this.authDir);
|
|
1015
|
-
const secretPayload = {
|
|
1016
|
-
accessToken,
|
|
1017
|
-
refreshToken: refreshToken2
|
|
1018
|
-
};
|
|
1019
|
-
let secretBackend = "file";
|
|
1020
|
-
if (this.keychain) {
|
|
1021
|
-
try {
|
|
1022
|
-
this.keychain.set(
|
|
1023
|
-
KEYCHAIN_SERVICE,
|
|
1024
|
-
slot.keychainAccount,
|
|
1025
|
-
JSON.stringify(secretPayload)
|
|
1026
|
-
);
|
|
1027
|
-
secretBackend = "keychain";
|
|
1028
|
-
removeFileIfExists(slot.fallbackSecretPath);
|
|
1029
|
-
} catch {
|
|
1030
|
-
this.writeFallbackSecret(slot, secretPayload);
|
|
1031
|
-
secretBackend = "file";
|
|
1032
|
-
}
|
|
1033
|
-
} else {
|
|
1034
|
-
this.writeFallbackSecret(slot, secretPayload);
|
|
1035
|
-
}
|
|
1036
|
-
const metadata = {
|
|
1037
|
-
version: METADATA_VERSION,
|
|
1038
|
-
secretBackend,
|
|
1039
|
-
baseUrl,
|
|
1040
|
-
scope: args.scope,
|
|
1041
|
-
obtainedAt: args.obtainedAt,
|
|
1042
|
-
expiresAt: args.expiresAt,
|
|
1043
|
-
updatedAt: Date.now()
|
|
1044
|
-
};
|
|
1045
|
-
writeJsonFile(slot.metadataPath, metadata);
|
|
1046
|
-
}
|
|
1047
|
-
readActiveCloudTarget() {
|
|
1048
|
-
return readActiveCloudTargetMetadata(resolveActiveTargetPath(this.authDir));
|
|
1049
|
-
}
|
|
1050
|
-
writeActiveCloudTarget(target) {
|
|
1051
|
-
const baseUrl = normalizeCredentialUrl(target.baseUrl);
|
|
1052
|
-
ensureDirectory(this.authDir);
|
|
1053
|
-
writeJsonFile(resolveActiveTargetPath(this.authDir), {
|
|
1054
|
-
version: ACTIVE_TARGET_VERSION,
|
|
1055
|
-
baseUrl,
|
|
1056
|
-
updatedAt: Date.now()
|
|
1057
|
-
});
|
|
1058
|
-
}
|
|
1059
|
-
clearCloudCredential(target) {
|
|
1060
|
-
this.clearCredentialSlot(
|
|
1061
|
-
resolveCredentialSlot(this.authDir, normalizeCloudCredentialTarget(target))
|
|
1062
|
-
);
|
|
1063
|
-
}
|
|
1064
|
-
readCredentialSlot(slot, target) {
|
|
1065
|
-
const metadata = readMetadata(slot.metadataPath);
|
|
1066
|
-
if (!metadata) {
|
|
1067
|
-
return null;
|
|
1068
|
-
}
|
|
1069
|
-
if (target && !matchesCredentialTarget(metadata, target)) {
|
|
1070
|
-
return null;
|
|
1071
|
-
}
|
|
1072
|
-
const secret = this.readSecret(slot, metadata.secretBackend);
|
|
1073
|
-
if (!secret) {
|
|
1074
|
-
return null;
|
|
1075
|
-
}
|
|
1076
|
-
return {
|
|
1077
|
-
baseUrl: metadata.baseUrl,
|
|
1078
|
-
scope: metadata.scope,
|
|
1079
|
-
accessToken: secret.accessToken,
|
|
1080
|
-
refreshToken: secret.refreshToken,
|
|
1081
|
-
obtainedAt: metadata.obtainedAt,
|
|
1082
|
-
expiresAt: metadata.expiresAt
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
readSecret(slot, backend) {
|
|
1086
|
-
if (backend === "keychain" && this.keychain) {
|
|
1087
|
-
try {
|
|
1088
|
-
const secret = this.keychain.get(
|
|
1089
|
-
KEYCHAIN_SERVICE,
|
|
1090
|
-
slot.keychainAccount
|
|
1091
|
-
);
|
|
1092
|
-
if (!secret) return null;
|
|
1093
|
-
return parseSecretPayload(secret);
|
|
1094
|
-
} catch {
|
|
1095
|
-
return null;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
return readSecretFile(slot.fallbackSecretPath);
|
|
1099
|
-
}
|
|
1100
|
-
writeFallbackSecret(slot, secretPayload) {
|
|
1101
|
-
writeJsonFile(slot.fallbackSecretPath, secretPayload, {
|
|
1102
|
-
mode: 384
|
|
1103
|
-
});
|
|
1104
|
-
if (!this.warnedFallback) {
|
|
1105
|
-
this.warn({
|
|
1106
|
-
code: "fallback_file_store",
|
|
1107
|
-
path: slot.fallbackSecretPath,
|
|
1108
|
-
message: "Secure keychain is unavailable. Falling back to file-based credential storage with mode 0600."
|
|
1109
|
-
});
|
|
1110
|
-
this.warnedFallback = true;
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
clearCredentialSlot(slot) {
|
|
1114
|
-
removeFileIfExists(slot.metadataPath);
|
|
1115
|
-
removeFileIfExists(slot.fallbackSecretPath);
|
|
1116
|
-
if (this.keychain) {
|
|
1117
|
-
this.keychain.delete(KEYCHAIN_SERVICE, slot.keychainAccount);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
};
|
|
1121
|
-
function createMachineCredentialStore(options = {}) {
|
|
1122
|
-
return new MachineCredentialStore(options);
|
|
1123
|
-
}
|
|
1124
|
-
function resolveCredentialSlot(authDir, target) {
|
|
1125
|
-
const normalizedBaseUrl = normalizeCredentialUrl(target.baseUrl);
|
|
1126
|
-
const storageKey = (0, import_node_crypto.createHash)("sha256").update(normalizedBaseUrl).digest("hex").slice(0, 24);
|
|
1127
|
-
return {
|
|
1128
|
-
keychainAccount: `${KEYCHAIN_ACCOUNT_PREFIX}${storageKey}`,
|
|
1129
|
-
metadataPath: import_node_path.default.join(authDir, `cli-login.${storageKey}.json`),
|
|
1130
|
-
fallbackSecretPath: import_node_path.default.join(
|
|
1131
|
-
authDir,
|
|
1132
|
-
`cli-login.${storageKey}.secret.json`
|
|
1133
|
-
)
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
function resolveActiveTargetPath(authDir) {
|
|
1137
|
-
return import_node_path.default.join(authDir, ACTIVE_TARGET_FILE_NAME);
|
|
1138
|
-
}
|
|
1139
|
-
function matchesCredentialTarget(value, target) {
|
|
1140
|
-
return normalizeCredentialUrl(value.baseUrl) === normalizeCredentialUrl(target.baseUrl);
|
|
1141
|
-
}
|
|
1142
|
-
function normalizeCredentialUrl(value) {
|
|
1143
|
-
const normalized = stripTrailingSlashes(value.trim());
|
|
1144
|
-
if (!normalized) {
|
|
1145
|
-
throw new Error("Cannot persist machine credential without baseUrl.");
|
|
1146
|
-
}
|
|
1147
|
-
return normalized;
|
|
1148
|
-
}
|
|
1149
|
-
function normalizeCloudCredentialTarget(target) {
|
|
1150
|
-
return {
|
|
1151
|
-
baseUrl: normalizeCredentialUrl(target.baseUrl)
|
|
1152
|
-
};
|
|
1153
|
-
}
|
|
1154
|
-
function resolveConfigDir(appName, env) {
|
|
1155
|
-
if (process.platform === "win32") {
|
|
1156
|
-
const appData = env.APPDATA?.trim() || import_node_path.default.join(import_node_os.default.homedir(), "AppData", "Roaming");
|
|
1157
|
-
return import_node_path.default.join(appData, appName);
|
|
1158
|
-
}
|
|
1159
|
-
if (process.platform === "darwin") {
|
|
1160
|
-
return import_node_path.default.join(
|
|
1161
|
-
import_node_os.default.homedir(),
|
|
1162
|
-
"Library",
|
|
1163
|
-
"Application Support",
|
|
1164
|
-
appName
|
|
1165
|
-
);
|
|
1166
|
-
}
|
|
1167
|
-
const xdgConfigHome = env.XDG_CONFIG_HOME?.trim() || import_node_path.default.join(import_node_os.default.homedir(), ".config");
|
|
1168
|
-
return import_node_path.default.join(xdgConfigHome, appName);
|
|
1169
|
-
}
|
|
1170
|
-
function ensureDirectory(directoryPath) {
|
|
1171
|
-
import_node_fs.default.mkdirSync(directoryPath, { recursive: true, mode: 448 });
|
|
1172
|
-
}
|
|
1173
|
-
function removeFileIfExists(filePath) {
|
|
1174
|
-
try {
|
|
1175
|
-
import_node_fs.default.rmSync(filePath, { force: true });
|
|
1176
|
-
} catch {
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
function readMetadata(filePath) {
|
|
1181
|
-
if (!import_node_fs.default.existsSync(filePath)) {
|
|
1182
|
-
return null;
|
|
1183
|
-
}
|
|
1184
|
-
try {
|
|
1185
|
-
const raw = import_node_fs.default.readFileSync(filePath, "utf8");
|
|
1186
|
-
const parsed = JSON.parse(raw);
|
|
1187
|
-
if (parsed.version !== METADATA_VERSION) return null;
|
|
1188
|
-
if (parsed.secretBackend !== "keychain" && parsed.secretBackend !== "file") {
|
|
1189
|
-
return null;
|
|
1190
|
-
}
|
|
1191
|
-
if (typeof parsed.baseUrl !== "string" || !parsed.baseUrl.trim()) return null;
|
|
1192
|
-
if (!Array.isArray(parsed.scope)) return null;
|
|
1193
|
-
if (typeof parsed.obtainedAt !== "number") return null;
|
|
1194
|
-
if (typeof parsed.expiresAt !== "number") return null;
|
|
1195
|
-
if (typeof parsed.updatedAt !== "number") return null;
|
|
1196
|
-
return {
|
|
1197
|
-
version: parsed.version,
|
|
1198
|
-
secretBackend: parsed.secretBackend,
|
|
1199
|
-
baseUrl: parsed.baseUrl,
|
|
1200
|
-
scope: parsed.scope.filter(
|
|
1201
|
-
(value) => typeof value === "string"
|
|
1202
|
-
),
|
|
1203
|
-
obtainedAt: parsed.obtainedAt,
|
|
1204
|
-
expiresAt: parsed.expiresAt,
|
|
1205
|
-
updatedAt: parsed.updatedAt
|
|
1206
|
-
};
|
|
1207
|
-
} catch {
|
|
1208
|
-
return null;
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
function readActiveCloudTargetMetadata(filePath) {
|
|
1212
|
-
if (!import_node_fs.default.existsSync(filePath)) {
|
|
1213
|
-
return null;
|
|
1214
|
-
}
|
|
1215
|
-
try {
|
|
1216
|
-
const raw = import_node_fs.default.readFileSync(filePath, "utf8");
|
|
1217
|
-
const parsed = JSON.parse(raw);
|
|
1218
|
-
if (parsed.version !== ACTIVE_TARGET_VERSION) {
|
|
1219
|
-
return null;
|
|
1220
|
-
}
|
|
1221
|
-
if (typeof parsed.baseUrl !== "string" || !parsed.baseUrl.trim()) {
|
|
1222
|
-
return null;
|
|
1223
|
-
}
|
|
1224
|
-
return {
|
|
1225
|
-
baseUrl: parsed.baseUrl
|
|
1226
|
-
};
|
|
1227
|
-
} catch {
|
|
1228
|
-
return null;
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
function parseSecretPayload(raw) {
|
|
1232
|
-
try {
|
|
1233
|
-
const parsed = JSON.parse(raw);
|
|
1234
|
-
if (typeof parsed.accessToken !== "string" || !parsed.accessToken.trim() || typeof parsed.refreshToken !== "string" || !parsed.refreshToken.trim()) {
|
|
1235
|
-
return null;
|
|
1236
|
-
}
|
|
1237
|
-
return {
|
|
1238
|
-
accessToken: parsed.accessToken,
|
|
1239
|
-
refreshToken: parsed.refreshToken
|
|
1240
|
-
};
|
|
1241
|
-
} catch {
|
|
1242
|
-
return null;
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
function readSecretFile(filePath) {
|
|
1246
|
-
if (!import_node_fs.default.existsSync(filePath)) {
|
|
1247
|
-
return null;
|
|
1248
|
-
}
|
|
1249
|
-
try {
|
|
1250
|
-
return parseSecretPayload(import_node_fs.default.readFileSync(filePath, "utf8"));
|
|
1251
|
-
} catch {
|
|
1252
|
-
return null;
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
function writeJsonFile(filePath, value, options = {}) {
|
|
1256
|
-
import_node_fs.default.writeFileSync(filePath, JSON.stringify(value, null, 2), {
|
|
1257
|
-
encoding: "utf8",
|
|
1258
|
-
mode: options.mode ?? 384
|
|
1259
|
-
});
|
|
1260
|
-
if (typeof options.mode === "number") {
|
|
1261
|
-
import_node_fs.default.chmodSync(filePath, options.mode);
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
// src/cli/auth.ts
|
|
1266
|
-
var CliAuthHttpError = class extends Error {
|
|
1267
|
-
status;
|
|
1268
|
-
body;
|
|
1269
|
-
constructor(message, status, body) {
|
|
1270
|
-
super(message);
|
|
1271
|
-
this.name = "CliAuthHttpError";
|
|
1272
|
-
this.status = status;
|
|
1273
|
-
this.body = body;
|
|
1274
|
-
}
|
|
1275
|
-
};
|
|
1276
|
-
var HELP_TEXT = `Usage: opensteer auth <command> [options]
|
|
1277
|
-
|
|
1278
|
-
Authenticate Opensteer CLI with Opensteer Cloud.
|
|
1279
|
-
|
|
1280
|
-
Commands:
|
|
1281
|
-
login Start device login flow in browser
|
|
1282
|
-
status Show saved machine login state for the selected cloud host
|
|
1283
|
-
logout Revoke and remove saved machine login for the selected cloud host
|
|
1284
|
-
|
|
1285
|
-
Options:
|
|
1286
|
-
--base-url <url> Cloud API base URL (defaults to env or the last selected host)
|
|
1287
|
-
--json JSON output (login prompts go to stderr)
|
|
1288
|
-
--no-browser Do not auto-open your default browser during login
|
|
1289
|
-
-h, --help Show this help
|
|
1290
|
-
`;
|
|
1291
|
-
var DEFAULT_AUTH_SITE_URL = "https://opensteer.com";
|
|
1292
|
-
var INTERNAL_AUTH_SITE_URL_ENV = "OPENSTEER_INTERNAL_AUTH_SITE_URL";
|
|
1293
|
-
function createDefaultDeps() {
|
|
1294
|
-
const env = process.env;
|
|
1295
|
-
return {
|
|
1296
|
-
env,
|
|
1297
|
-
store: createMachineCredentialStore({
|
|
1298
|
-
env,
|
|
1299
|
-
warn: (warning) => {
|
|
1300
|
-
process.stderr.write(`${warning.message} (${warning.path})
|
|
1301
|
-
`);
|
|
1302
|
-
}
|
|
1303
|
-
}),
|
|
1304
|
-
fetchFn: fetch,
|
|
1305
|
-
writeStdout: (message) => process.stdout.write(message),
|
|
1306
|
-
writeStderr: (message) => process.stderr.write(message),
|
|
1307
|
-
isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
1308
|
-
sleep: async (ms) => {
|
|
1309
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1310
|
-
},
|
|
1311
|
-
now: () => Date.now(),
|
|
1312
|
-
openExternalUrl: openDefaultBrowser
|
|
1313
|
-
};
|
|
1314
|
-
}
|
|
1315
|
-
function readFlagValue(args, index, flag) {
|
|
1316
|
-
const value = args[index + 1];
|
|
1317
|
-
if (value === void 0 || value.startsWith("-")) {
|
|
1318
|
-
return {
|
|
1319
|
-
ok: false,
|
|
1320
|
-
error: `${flag} requires a value.`
|
|
1321
|
-
};
|
|
1322
|
-
}
|
|
1323
|
-
return {
|
|
1324
|
-
ok: true,
|
|
1325
|
-
value,
|
|
1326
|
-
nextIndex: index + 1
|
|
1327
|
-
};
|
|
1328
|
-
}
|
|
1329
|
-
function parseAuthCommonArgs(rawArgs) {
|
|
1330
|
-
const args = {};
|
|
1331
|
-
for (let i = 0; i < rawArgs.length; i++) {
|
|
1332
|
-
const arg = rawArgs[i];
|
|
1333
|
-
if (arg === "--json") {
|
|
1334
|
-
args.json = true;
|
|
1335
|
-
continue;
|
|
1336
|
-
}
|
|
1337
|
-
if (arg === "--base-url") {
|
|
1338
|
-
const value = readFlagValue(rawArgs, i, "--base-url");
|
|
1339
|
-
if (!value.ok) return { args, error: value.error };
|
|
1340
|
-
args.baseUrl = value.value;
|
|
1341
|
-
i = value.nextIndex;
|
|
1342
|
-
continue;
|
|
1343
|
-
}
|
|
1344
|
-
return {
|
|
1345
|
-
args,
|
|
1346
|
-
error: `Unsupported option "${arg}".`
|
|
1347
|
-
};
|
|
1348
|
-
}
|
|
1349
|
-
return { args };
|
|
1350
|
-
}
|
|
1351
|
-
function parseOpensteerAuthArgs(rawArgs) {
|
|
1352
|
-
if (!rawArgs.length) {
|
|
1353
|
-
return { mode: "help" };
|
|
1354
|
-
}
|
|
1355
|
-
const [subcommand, ...rest] = rawArgs;
|
|
1356
|
-
if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
1357
|
-
return { mode: "help" };
|
|
1358
|
-
}
|
|
1359
|
-
if (subcommand === "login") {
|
|
1360
|
-
let openBrowser = true;
|
|
1361
|
-
const filtered = [];
|
|
1362
|
-
for (const arg of rest) {
|
|
1363
|
-
if (arg === "--no-browser") {
|
|
1364
|
-
openBrowser = false;
|
|
1365
|
-
continue;
|
|
1366
|
-
}
|
|
1367
|
-
filtered.push(arg);
|
|
1368
|
-
}
|
|
1369
|
-
const parsed = parseAuthCommonArgs(filtered);
|
|
1370
|
-
if (parsed.error) return { mode: "error", error: parsed.error };
|
|
1371
|
-
return {
|
|
1372
|
-
mode: "login",
|
|
1373
|
-
args: {
|
|
1374
|
-
...parsed.args,
|
|
1375
|
-
openBrowser
|
|
1376
|
-
}
|
|
1377
|
-
};
|
|
1378
|
-
}
|
|
1379
|
-
if (subcommand === "status") {
|
|
1380
|
-
const parsed = parseAuthCommonArgs(rest);
|
|
1381
|
-
if (parsed.error) return { mode: "error", error: parsed.error };
|
|
1382
|
-
return { mode: "status", args: parsed.args };
|
|
1383
|
-
}
|
|
1384
|
-
if (subcommand === "logout") {
|
|
1385
|
-
const parsed = parseAuthCommonArgs(rest);
|
|
1386
|
-
if (parsed.error) return { mode: "error", error: parsed.error };
|
|
1387
|
-
return { mode: "logout", args: parsed.args };
|
|
1388
|
-
}
|
|
1389
|
-
return {
|
|
1390
|
-
mode: "error",
|
|
1391
|
-
error: `Unsupported auth subcommand "${subcommand}".`
|
|
1392
|
-
};
|
|
1393
|
-
}
|
|
1394
|
-
function printHelp(deps) {
|
|
1395
|
-
deps.writeStdout(`${HELP_TEXT}
|
|
1396
|
-
`);
|
|
1397
|
-
}
|
|
1398
|
-
function writeHumanLine(deps, message) {
|
|
1399
|
-
deps.writeStdout(`${message}
|
|
1400
|
-
`);
|
|
1401
|
-
}
|
|
1402
|
-
function writeJsonLine(deps, payload) {
|
|
1403
|
-
deps.writeStdout(`${JSON.stringify(payload)}
|
|
1404
|
-
`);
|
|
1405
|
-
}
|
|
1406
|
-
function resolveBaseUrl(provided, env) {
|
|
1407
|
-
const baseUrl = normalizeCloudBaseUrl(
|
|
1408
|
-
(provided || env.OPENSTEER_BASE_URL || DEFAULT_CLOUD_BASE_URL).trim()
|
|
1409
|
-
);
|
|
1410
|
-
assertSecureUrl(baseUrl, "--base-url");
|
|
1411
|
-
return baseUrl;
|
|
1412
|
-
}
|
|
1413
|
-
function resolveAuthSiteUrl(env) {
|
|
1414
|
-
const authSiteUrl = normalizeCloudBaseUrl(
|
|
1415
|
-
(env[INTERNAL_AUTH_SITE_URL_ENV] || DEFAULT_AUTH_SITE_URL).trim()
|
|
1416
|
-
);
|
|
1417
|
-
assertSecureUrl(
|
|
1418
|
-
authSiteUrl,
|
|
1419
|
-
`environment variable ${INTERNAL_AUTH_SITE_URL_ENV}`
|
|
1420
|
-
);
|
|
1421
|
-
return authSiteUrl;
|
|
1422
|
-
}
|
|
1423
|
-
function hasExplicitCloudTargetSelection(providedBaseUrl, env) {
|
|
1424
|
-
return Boolean(
|
|
1425
|
-
providedBaseUrl?.trim() || env.OPENSTEER_BASE_URL?.trim()
|
|
1426
|
-
);
|
|
1427
|
-
}
|
|
1428
|
-
function readRememberedCloudTarget(store) {
|
|
1429
|
-
const activeTarget = store.readActiveCloudTarget();
|
|
1430
|
-
if (!activeTarget) {
|
|
1431
|
-
return null;
|
|
1432
|
-
}
|
|
1433
|
-
try {
|
|
1434
|
-
const baseUrl = normalizeCloudBaseUrl(activeTarget.baseUrl);
|
|
1435
|
-
assertSecureUrl(baseUrl, "--base-url");
|
|
1436
|
-
return { baseUrl };
|
|
1437
|
-
} catch {
|
|
1438
|
-
return null;
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
function resolveCloudTarget(args, env, store, options = {}) {
|
|
1442
|
-
if (options.allowRememberedTarget !== false && !hasExplicitCloudTargetSelection(args.baseUrl, env)) {
|
|
1443
|
-
const rememberedTarget = readRememberedCloudTarget(store);
|
|
1444
|
-
if (rememberedTarget) {
|
|
1445
|
-
return rememberedTarget;
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
const baseUrl = resolveBaseUrl(args.baseUrl, env);
|
|
1449
|
-
return { baseUrl };
|
|
1450
|
-
}
|
|
1451
|
-
function assertSecureUrl(value, flag) {
|
|
1452
|
-
let parsed;
|
|
1453
|
-
try {
|
|
1454
|
-
parsed = new URL(value);
|
|
1455
|
-
} catch {
|
|
1456
|
-
throw new Error(`Invalid ${flag} "${value}".`);
|
|
1457
|
-
}
|
|
1458
|
-
if (parsed.protocol === "https:") {
|
|
1459
|
-
return;
|
|
1460
|
-
}
|
|
1461
|
-
if (parsed.protocol === "http:") {
|
|
1462
|
-
const host = parsed.hostname.toLowerCase();
|
|
1463
|
-
if (host === "localhost" || host === "127.0.0.1" || host === "::1") {
|
|
1464
|
-
return;
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
throw new Error(
|
|
1468
|
-
`Insecure URL "${value}". Use HTTPS, or HTTP only for localhost.`
|
|
1469
|
-
);
|
|
1470
|
-
}
|
|
1471
|
-
async function postJson(fetchFn, url, body) {
|
|
1472
|
-
const response = await fetchFn(url, {
|
|
1473
|
-
method: "POST",
|
|
1474
|
-
headers: {
|
|
1475
|
-
"content-type": "application/json"
|
|
1476
|
-
},
|
|
1477
|
-
body: JSON.stringify(body)
|
|
1478
|
-
});
|
|
1479
|
-
let payload = null;
|
|
1480
|
-
try {
|
|
1481
|
-
payload = await response.json();
|
|
1482
|
-
} catch {
|
|
1483
|
-
payload = null;
|
|
1484
|
-
}
|
|
1485
|
-
if (!response.ok) {
|
|
1486
|
-
throw new CliAuthHttpError(
|
|
1487
|
-
`Auth request failed with status ${response.status}.`,
|
|
1488
|
-
response.status,
|
|
1489
|
-
payload
|
|
1490
|
-
);
|
|
1491
|
-
}
|
|
1492
|
-
return payload;
|
|
1493
|
-
}
|
|
1494
|
-
function parseScope(rawScope) {
|
|
1495
|
-
if (!rawScope) return ["cloud:browser"];
|
|
1496
|
-
const values = rawScope.split(" ").map((value) => value.trim()).filter(Boolean);
|
|
1497
|
-
return values.length ? values : ["cloud:browser"];
|
|
1498
|
-
}
|
|
1499
|
-
function parseCliTokenResponse(payload) {
|
|
1500
|
-
const accessToken = typeof payload.access_token === "string" ? payload.access_token.trim() : "";
|
|
1501
|
-
const refreshToken2 = typeof payload.refresh_token === "string" ? payload.refresh_token.trim() : "";
|
|
1502
|
-
const expiresInSec = typeof payload.expires_in === "number" && Number.isFinite(payload.expires_in) && payload.expires_in > 0 ? Math.trunc(payload.expires_in) : 0;
|
|
1503
|
-
if (!accessToken || !refreshToken2 || !expiresInSec) {
|
|
1504
|
-
throw new Error("Invalid token response from cloud auth endpoint.");
|
|
1505
|
-
}
|
|
1506
|
-
return {
|
|
1507
|
-
accessToken,
|
|
1508
|
-
refreshToken: refreshToken2,
|
|
1509
|
-
expiresInSec,
|
|
1510
|
-
scope: parseScope(payload.scope)
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
function parseCliOauthError(error) {
|
|
1514
|
-
if (!error || typeof error !== "object" || Array.isArray(error)) {
|
|
1515
|
-
return null;
|
|
1516
|
-
}
|
|
1517
|
-
const root = error;
|
|
1518
|
-
return {
|
|
1519
|
-
error: typeof root.error === "string" ? root.error : void 0,
|
|
1520
|
-
error_description: typeof root.error_description === "string" ? root.error_description : void 0,
|
|
1521
|
-
interval: typeof root.interval === "number" ? root.interval : void 0
|
|
1522
|
-
};
|
|
1523
|
-
}
|
|
1524
|
-
async function startDeviceAuthorization(authSiteUrl, fetchFn) {
|
|
1525
|
-
const response = await postJson(
|
|
1526
|
-
fetchFn,
|
|
1527
|
-
`${authSiteUrl}/api/cli-auth/device/start`,
|
|
1528
|
-
{
|
|
1529
|
-
scope: ["cloud:browser"]
|
|
1530
|
-
}
|
|
1531
|
-
);
|
|
1532
|
-
if (!response || typeof response.device_code !== "string" || !response.device_code.trim() || typeof response.user_code !== "string" || !response.user_code.trim() || typeof response.verification_uri_complete !== "string" || !response.verification_uri_complete.trim() || typeof response.expires_in !== "number" || response.expires_in <= 0 || typeof response.interval !== "number" || response.interval <= 0) {
|
|
1533
|
-
throw new Error("Invalid device authorization response from cloud.");
|
|
1534
|
-
}
|
|
1535
|
-
return response;
|
|
1536
|
-
}
|
|
1537
|
-
async function pollDeviceToken(authSiteUrl, deviceCode, fetchFn) {
|
|
1538
|
-
return await postJson(
|
|
1539
|
-
fetchFn,
|
|
1540
|
-
`${authSiteUrl}/api/cli-auth/device/token`,
|
|
1541
|
-
{
|
|
1542
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
1543
|
-
device_code: deviceCode
|
|
1544
|
-
}
|
|
1545
|
-
);
|
|
1546
|
-
}
|
|
1547
|
-
async function refreshToken(authSiteUrl, refreshTokenValue, fetchFn) {
|
|
1548
|
-
return await postJson(fetchFn, `${authSiteUrl}/api/cli-auth/token`, {
|
|
1549
|
-
grant_type: "refresh_token",
|
|
1550
|
-
refresh_token: refreshTokenValue
|
|
1551
|
-
});
|
|
1552
|
-
}
|
|
1553
|
-
async function revokeToken(authSiteUrl, refreshTokenValue, fetchFn) {
|
|
1554
|
-
await postJson(fetchFn, `${authSiteUrl}/api/cli-auth/revoke`, {
|
|
1555
|
-
token: refreshTokenValue
|
|
1556
|
-
});
|
|
1557
|
-
}
|
|
1558
|
-
async function openDefaultBrowser(url) {
|
|
1559
|
-
try {
|
|
1560
|
-
const child = await (0, import_open.default)(url, {
|
|
1561
|
-
wait: false
|
|
1562
|
-
});
|
|
1563
|
-
child.on("error", () => void 0);
|
|
1564
|
-
child.unref();
|
|
1565
|
-
return true;
|
|
1566
|
-
} catch {
|
|
1567
|
-
return false;
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
async function runDeviceLoginFlow(args) {
|
|
1571
|
-
const start = await startDeviceAuthorization(args.authSiteUrl, args.fetchFn);
|
|
1572
|
-
if (args.openBrowser) {
|
|
1573
|
-
args.writeProgress(
|
|
1574
|
-
"Opening your default browser for Opensteer CLI authentication.\n"
|
|
1575
|
-
);
|
|
1576
|
-
args.writeProgress(
|
|
1577
|
-
`If nothing opens, use this URL:
|
|
1578
|
-
${start.verification_uri_complete}
|
|
1579
|
-
`
|
|
1580
|
-
);
|
|
1581
|
-
} else {
|
|
1582
|
-
if (args.openBrowserDisabledReason) {
|
|
1583
|
-
args.writeProgress(
|
|
1584
|
-
`Automatic browser open is disabled (${args.openBrowserDisabledReason}).
|
|
1585
|
-
`
|
|
1586
|
-
);
|
|
1587
|
-
}
|
|
1588
|
-
args.writeProgress(
|
|
1589
|
-
`Open this URL to authenticate Opensteer CLI:
|
|
1590
|
-
${start.verification_uri_complete}
|
|
1591
|
-
`
|
|
1592
|
-
);
|
|
1593
|
-
}
|
|
1594
|
-
args.writeProgress(`Verification code: ${start.user_code}
|
|
1595
|
-
`);
|
|
1596
|
-
if (args.openBrowser) {
|
|
1597
|
-
const browserOpened = await args.openExternalUrl(
|
|
1598
|
-
start.verification_uri_complete
|
|
1599
|
-
);
|
|
1600
|
-
if (browserOpened) {
|
|
1601
|
-
args.writeProgress(
|
|
1602
|
-
"Opened your default browser. Finish authentication there; this terminal will continue automatically.\n"
|
|
1603
|
-
);
|
|
1604
|
-
} else {
|
|
1605
|
-
args.writeProgress(
|
|
1606
|
-
"Could not open your default browser automatically. Paste the URL above into a browser to continue.\n"
|
|
1607
|
-
);
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
const deadline = args.now() + start.expires_in * 1e3;
|
|
1611
|
-
let pollIntervalMs = Math.max(1, Math.trunc(start.interval)) * 1e3;
|
|
1612
|
-
while (args.now() <= deadline) {
|
|
1613
|
-
await args.sleep(pollIntervalMs);
|
|
1614
|
-
try {
|
|
1615
|
-
const tokenPayload = await pollDeviceToken(
|
|
1616
|
-
args.authSiteUrl,
|
|
1617
|
-
start.device_code,
|
|
1618
|
-
args.fetchFn
|
|
1619
|
-
);
|
|
1620
|
-
const parsed = parseCliTokenResponse(tokenPayload);
|
|
1621
|
-
return {
|
|
1622
|
-
accessToken: parsed.accessToken,
|
|
1623
|
-
refreshToken: parsed.refreshToken,
|
|
1624
|
-
expiresAt: args.now() + parsed.expiresInSec * 1e3,
|
|
1625
|
-
scope: parsed.scope
|
|
1626
|
-
};
|
|
1627
|
-
} catch (error) {
|
|
1628
|
-
if (error instanceof CliAuthHttpError) {
|
|
1629
|
-
const oauthError = parseCliOauthError(error.body);
|
|
1630
|
-
if (!oauthError?.error) {
|
|
1631
|
-
throw error;
|
|
1632
|
-
}
|
|
1633
|
-
if (oauthError.error === "authorization_pending") {
|
|
1634
|
-
continue;
|
|
1635
|
-
}
|
|
1636
|
-
if (oauthError.error === "slow_down") {
|
|
1637
|
-
const hintedInterval = typeof oauthError.interval === "number" && oauthError.interval > 0 ? Math.trunc(oauthError.interval) * 1e3 : pollIntervalMs + 5e3;
|
|
1638
|
-
pollIntervalMs = Math.max(hintedInterval, pollIntervalMs + 1e3);
|
|
1639
|
-
continue;
|
|
1640
|
-
}
|
|
1641
|
-
if (oauthError.error === "expired_token") {
|
|
1642
|
-
throw new Error(
|
|
1643
|
-
'Device authorization expired before approval. Run "opensteer auth login" again.'
|
|
1644
|
-
);
|
|
1645
|
-
}
|
|
1646
|
-
if (oauthError.error === "access_denied") {
|
|
1647
|
-
throw new Error(
|
|
1648
|
-
'Cloud login was denied. Run "opensteer auth login" to retry.'
|
|
1649
|
-
);
|
|
1650
|
-
}
|
|
1651
|
-
throw new Error(
|
|
1652
|
-
oauthError.error_description || `Cloud login failed: ${oauthError.error}.`
|
|
1653
|
-
);
|
|
1654
|
-
}
|
|
1655
|
-
throw error;
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
throw new Error(
|
|
1659
|
-
'Timed out waiting for cloud login approval. Run "opensteer auth login" again.'
|
|
1660
|
-
);
|
|
1661
|
-
}
|
|
1662
|
-
async function refreshSavedCredential(saved, deps) {
|
|
1663
|
-
const tokenPayload = await refreshToken(
|
|
1664
|
-
resolveAuthSiteUrl(deps.env),
|
|
1665
|
-
saved.refreshToken,
|
|
1666
|
-
deps.fetchFn
|
|
1667
|
-
);
|
|
1668
|
-
const parsed = parseCliTokenResponse(tokenPayload);
|
|
1669
|
-
const updated = {
|
|
1670
|
-
accessToken: parsed.accessToken,
|
|
1671
|
-
refreshToken: parsed.refreshToken,
|
|
1672
|
-
expiresAt: deps.now() + parsed.expiresInSec * 1e3,
|
|
1673
|
-
scope: parsed.scope
|
|
1674
|
-
};
|
|
1675
|
-
deps.store.writeCloudCredential({
|
|
1676
|
-
baseUrl: saved.baseUrl,
|
|
1677
|
-
scope: updated.scope,
|
|
1678
|
-
accessToken: updated.accessToken,
|
|
1679
|
-
refreshToken: updated.refreshToken,
|
|
1680
|
-
obtainedAt: deps.now(),
|
|
1681
|
-
expiresAt: updated.expiresAt
|
|
1682
|
-
});
|
|
1683
|
-
return updated;
|
|
1684
|
-
}
|
|
1685
|
-
async function ensureSavedCredentialIsFresh(saved, deps) {
|
|
1686
|
-
const refreshSkewMs = 6e4;
|
|
1687
|
-
if (saved.expiresAt > deps.now() + refreshSkewMs) {
|
|
1688
|
-
return saved;
|
|
1689
|
-
}
|
|
1690
|
-
try {
|
|
1691
|
-
const refreshed = await refreshSavedCredential(saved, deps);
|
|
1692
|
-
return {
|
|
1693
|
-
...saved,
|
|
1694
|
-
accessToken: refreshed.accessToken,
|
|
1695
|
-
refreshToken: refreshed.refreshToken,
|
|
1696
|
-
expiresAt: refreshed.expiresAt,
|
|
1697
|
-
scope: refreshed.scope,
|
|
1698
|
-
obtainedAt: deps.now()
|
|
1699
|
-
};
|
|
1700
|
-
} catch (error) {
|
|
1701
|
-
if (error instanceof CliAuthHttpError) {
|
|
1702
|
-
const oauth = parseCliOauthError(error.body);
|
|
1703
|
-
if (oauth?.error === "invalid_grant" || oauth?.error === "expired_token") {
|
|
1704
|
-
deps.store.clearCloudCredential({
|
|
1705
|
-
baseUrl: saved.baseUrl
|
|
1706
|
-
});
|
|
1707
|
-
return null;
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
deps.writeStderr(
|
|
1711
|
-
`Unable to refresh saved cloud login: ${error instanceof Error ? error.message : "unknown error"}
|
|
1712
|
-
`
|
|
1713
|
-
);
|
|
1714
|
-
return null;
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
function toAuthMissingMessage(commandName) {
|
|
1718
|
-
return [
|
|
1719
|
-
`${commandName} requires cloud authentication.`,
|
|
1720
|
-
'Use --api-key, --access-token, OPENSTEER_API_KEY, OPENSTEER_ACCESS_TOKEN, or run "opensteer auth login".'
|
|
1721
|
-
].join(" ");
|
|
1722
|
-
}
|
|
1723
|
-
function describeBrowserOpenMode(args, deps) {
|
|
1724
|
-
if (!args.openBrowser) {
|
|
1725
|
-
return {
|
|
1726
|
-
enabled: false,
|
|
1727
|
-
disabledReason: "--no-browser"
|
|
1728
|
-
};
|
|
1729
|
-
}
|
|
1730
|
-
if (!deps.isInteractive()) {
|
|
1731
|
-
return {
|
|
1732
|
-
enabled: false,
|
|
1733
|
-
disabledReason: "this shell is not interactive"
|
|
1734
|
-
};
|
|
1735
|
-
}
|
|
1736
|
-
if (isCiEnvironment(deps.env)) {
|
|
1737
|
-
return {
|
|
1738
|
-
enabled: false,
|
|
1739
|
-
disabledReason: "CI"
|
|
1740
|
-
};
|
|
1741
|
-
}
|
|
1742
|
-
return {
|
|
1743
|
-
enabled: true
|
|
1744
|
-
};
|
|
1745
|
-
}
|
|
1746
|
-
function isCiEnvironment(env) {
|
|
1747
|
-
const value = env.CI?.trim().toLowerCase();
|
|
1748
|
-
return Boolean(value && value !== "0" && value !== "false");
|
|
1749
|
-
}
|
|
1750
|
-
function resolveCloudSessionEnvForRootDir(rootDir, env) {
|
|
1751
|
-
const resolved = resolveConfigWithEnv(
|
|
1752
|
-
{
|
|
1753
|
-
storage: { rootDir }
|
|
1754
|
-
},
|
|
1755
|
-
{
|
|
1756
|
-
env
|
|
1757
|
-
}
|
|
1758
|
-
);
|
|
1759
|
-
return {
|
|
1760
|
-
cloud: resolveCloudSelection(
|
|
1761
|
-
{
|
|
1762
|
-
cloud: resolved.config.cloud
|
|
1763
|
-
},
|
|
1764
|
-
resolved.env
|
|
1765
|
-
).cloud,
|
|
1766
|
-
env: resolved.env
|
|
1767
|
-
};
|
|
1768
|
-
}
|
|
1769
|
-
function isCloudModeEnabledForRootDir(rootDir, env) {
|
|
1770
|
-
return resolveCloudSessionEnvForRootDir(rootDir, env).cloud;
|
|
1771
|
-
}
|
|
1772
|
-
async function ensureCloudCredentialsForOpenCommand(options) {
|
|
1773
|
-
const processEnv = options.env ?? process.env;
|
|
1774
|
-
const runtime = resolveCloudSessionEnvForRootDir(options.scopeDir, processEnv);
|
|
1775
|
-
if (!runtime.cloud) {
|
|
1776
|
-
return null;
|
|
1777
|
-
}
|
|
1778
|
-
const writeStderr = options.writeStderr ?? ((message) => process.stderr.write(message));
|
|
1779
|
-
const auth = await ensureCloudCredentialsForCommand({
|
|
1780
|
-
commandName: "opensteer open",
|
|
1781
|
-
env: runtime.env,
|
|
1782
|
-
store: options.store,
|
|
1783
|
-
apiKeyFlag: options.apiKeyFlag,
|
|
1784
|
-
accessTokenFlag: options.accessTokenFlag,
|
|
1785
|
-
interactive: options.interactive,
|
|
1786
|
-
autoLoginIfNeeded: true,
|
|
1787
|
-
writeProgress: options.writeProgress ?? writeStderr,
|
|
1788
|
-
writeStderr,
|
|
1789
|
-
fetchFn: options.fetchFn,
|
|
1790
|
-
sleep: options.sleep,
|
|
1791
|
-
now: options.now,
|
|
1792
|
-
openExternalUrl: options.openExternalUrl
|
|
1793
|
-
});
|
|
1794
|
-
applyCloudCredentialToEnv(processEnv, {
|
|
1795
|
-
kind: auth.kind,
|
|
1796
|
-
source: auth.source,
|
|
1797
|
-
token: auth.token,
|
|
1798
|
-
authScheme: auth.authScheme
|
|
1799
|
-
});
|
|
1800
|
-
processEnv.OPENSTEER_BASE_URL = auth.baseUrl;
|
|
1801
|
-
return auth;
|
|
1802
|
-
}
|
|
1803
|
-
async function ensureCloudCredentialsForCommand(options) {
|
|
1804
|
-
const env = options.env ?? process.env;
|
|
1805
|
-
const writeProgress = options.writeProgress ?? options.writeStdout ?? ((message) => process.stdout.write(message));
|
|
1806
|
-
const writeStderr = options.writeStderr ?? ((message) => process.stderr.write(message));
|
|
1807
|
-
const fetchFn = options.fetchFn ?? fetch;
|
|
1808
|
-
const sleep = options.sleep ?? (async (ms) => {
|
|
1809
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1810
|
-
});
|
|
1811
|
-
const now = options.now ?? Date.now;
|
|
1812
|
-
const openExternalUrl = options.openExternalUrl ?? openDefaultBrowser;
|
|
1813
|
-
const store = options.store ?? createMachineCredentialStore({
|
|
1814
|
-
env,
|
|
1815
|
-
warn: (warning) => {
|
|
1816
|
-
writeStderr(`${warning.message} (${warning.path})
|
|
1817
|
-
`);
|
|
1818
|
-
}
|
|
1819
|
-
});
|
|
1820
|
-
const { baseUrl } = resolveCloudTarget(options, env, store);
|
|
1821
|
-
const initialCredential = resolveCloudCredential({
|
|
1822
|
-
env,
|
|
1823
|
-
apiKeyFlag: options.apiKeyFlag,
|
|
1824
|
-
accessTokenFlag: options.accessTokenFlag
|
|
1825
|
-
});
|
|
1826
|
-
let credential = initialCredential;
|
|
1827
|
-
if (!credential) {
|
|
1828
|
-
const saved = store.readCloudCredential({ baseUrl });
|
|
1829
|
-
const freshSaved = saved ? await ensureSavedCredentialIsFresh(saved, {
|
|
1830
|
-
env,
|
|
1831
|
-
fetchFn,
|
|
1832
|
-
store,
|
|
1833
|
-
now,
|
|
1834
|
-
writeStderr
|
|
1835
|
-
}) : null;
|
|
1836
|
-
if (freshSaved) {
|
|
1837
|
-
credential = {
|
|
1838
|
-
kind: "access-token",
|
|
1839
|
-
source: "saved",
|
|
1840
|
-
token: freshSaved.accessToken,
|
|
1841
|
-
authScheme: "bearer"
|
|
1842
|
-
};
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
if (!credential) {
|
|
1846
|
-
if (options.autoLoginIfNeeded && (options.interactive ?? false)) {
|
|
1847
|
-
const loggedIn = await runDeviceLoginFlow({
|
|
1848
|
-
authSiteUrl: resolveAuthSiteUrl(env),
|
|
1849
|
-
fetchFn,
|
|
1850
|
-
writeProgress,
|
|
1851
|
-
openExternalUrl,
|
|
1852
|
-
sleep,
|
|
1853
|
-
now,
|
|
1854
|
-
openBrowser: true
|
|
1855
|
-
});
|
|
1856
|
-
store.writeCloudCredential({
|
|
1857
|
-
baseUrl,
|
|
1858
|
-
scope: loggedIn.scope,
|
|
1859
|
-
accessToken: loggedIn.accessToken,
|
|
1860
|
-
refreshToken: loggedIn.refreshToken,
|
|
1861
|
-
obtainedAt: now(),
|
|
1862
|
-
expiresAt: loggedIn.expiresAt
|
|
1863
|
-
});
|
|
1864
|
-
credential = {
|
|
1865
|
-
kind: "access-token",
|
|
1866
|
-
source: "saved",
|
|
1867
|
-
token: loggedIn.accessToken,
|
|
1868
|
-
authScheme: "bearer"
|
|
1869
|
-
};
|
|
1870
|
-
writeProgress("Cloud login complete.\n");
|
|
1871
|
-
} else {
|
|
1872
|
-
throw new Error(toAuthMissingMessage(options.commandName));
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
store.writeActiveCloudTarget({ baseUrl });
|
|
1876
|
-
applyCloudCredentialToEnv(env, credential);
|
|
1877
|
-
env.OPENSTEER_BASE_URL = baseUrl;
|
|
1878
|
-
return {
|
|
1879
|
-
token: credential.token,
|
|
1880
|
-
authScheme: credential.authScheme,
|
|
1881
|
-
source: credential.source,
|
|
1882
|
-
kind: credential.kind,
|
|
1883
|
-
baseUrl
|
|
1884
|
-
};
|
|
1885
|
-
}
|
|
1886
|
-
async function runLogin(args, deps) {
|
|
1887
|
-
const { baseUrl } = resolveCloudTarget(args, deps.env, deps.store, {
|
|
1888
|
-
allowRememberedTarget: false
|
|
1889
|
-
});
|
|
1890
|
-
const writeProgress = args.json ? deps.writeStderr : deps.writeStdout;
|
|
1891
|
-
const browserOpenMode = describeBrowserOpenMode(args, deps);
|
|
1892
|
-
const login = await runDeviceLoginFlow({
|
|
1893
|
-
authSiteUrl: resolveAuthSiteUrl(deps.env),
|
|
1894
|
-
fetchFn: deps.fetchFn,
|
|
1895
|
-
writeProgress,
|
|
1896
|
-
openExternalUrl: deps.openExternalUrl,
|
|
1897
|
-
sleep: deps.sleep,
|
|
1898
|
-
now: deps.now,
|
|
1899
|
-
openBrowser: browserOpenMode.enabled,
|
|
1900
|
-
openBrowserDisabledReason: browserOpenMode.disabledReason
|
|
1901
|
-
});
|
|
1902
|
-
deps.store.writeCloudCredential({
|
|
1903
|
-
baseUrl,
|
|
1904
|
-
scope: login.scope,
|
|
1905
|
-
accessToken: login.accessToken,
|
|
1906
|
-
refreshToken: login.refreshToken,
|
|
1907
|
-
obtainedAt: deps.now(),
|
|
1908
|
-
expiresAt: login.expiresAt
|
|
1909
|
-
});
|
|
1910
|
-
deps.store.writeActiveCloudTarget({ baseUrl });
|
|
1911
|
-
if (args.json) {
|
|
1912
|
-
writeJsonLine(deps, {
|
|
1913
|
-
loggedIn: true,
|
|
1914
|
-
baseUrl,
|
|
1915
|
-
expiresAt: login.expiresAt,
|
|
1916
|
-
scope: login.scope,
|
|
1917
|
-
authSource: "device"
|
|
1918
|
-
});
|
|
1919
|
-
return 0;
|
|
1920
|
-
}
|
|
1921
|
-
writeHumanLine(deps, "Opensteer CLI login successful.");
|
|
1922
|
-
return 0;
|
|
1923
|
-
}
|
|
1924
|
-
async function runStatus(args, deps) {
|
|
1925
|
-
const { baseUrl } = resolveCloudTarget(args, deps.env, deps.store);
|
|
1926
|
-
deps.store.writeActiveCloudTarget({ baseUrl });
|
|
1927
|
-
const saved = deps.store.readCloudCredential({ baseUrl });
|
|
1928
|
-
if (!saved) {
|
|
1929
|
-
if (args.json) {
|
|
1930
|
-
writeJsonLine(deps, {
|
|
1931
|
-
loggedIn: false,
|
|
1932
|
-
baseUrl
|
|
1933
|
-
});
|
|
1934
|
-
} else {
|
|
1935
|
-
writeHumanLine(deps, `Opensteer CLI is not logged in for ${baseUrl}.`);
|
|
1936
|
-
}
|
|
1937
|
-
return 0;
|
|
1938
|
-
}
|
|
1939
|
-
const now = deps.now();
|
|
1940
|
-
const expired = saved.expiresAt <= now;
|
|
1941
|
-
if (args.json) {
|
|
1942
|
-
writeJsonLine(deps, {
|
|
1943
|
-
loggedIn: true,
|
|
1944
|
-
expired,
|
|
1945
|
-
baseUrl: saved.baseUrl,
|
|
1946
|
-
expiresAt: saved.expiresAt,
|
|
1947
|
-
scope: saved.scope
|
|
1948
|
-
});
|
|
1949
|
-
return 0;
|
|
1950
|
-
}
|
|
1951
|
-
writeHumanLine(
|
|
1952
|
-
deps,
|
|
1953
|
-
expired ? "Opensteer CLI has a saved login, but the access token is expired." : "Opensteer CLI is logged in."
|
|
1954
|
-
);
|
|
1955
|
-
writeHumanLine(deps, ` API Base URL: ${saved.baseUrl}`);
|
|
1956
|
-
writeHumanLine(deps, ` Expires At: ${new Date(saved.expiresAt).toISOString()}`);
|
|
1957
|
-
return 0;
|
|
1958
|
-
}
|
|
1959
|
-
async function runLogout(args, deps) {
|
|
1960
|
-
const { baseUrl } = resolveCloudTarget(args, deps.env, deps.store);
|
|
1961
|
-
deps.store.writeActiveCloudTarget({ baseUrl });
|
|
1962
|
-
const saved = deps.store.readCloudCredential({ baseUrl });
|
|
1963
|
-
if (saved) {
|
|
1964
|
-
try {
|
|
1965
|
-
await revokeToken(
|
|
1966
|
-
resolveAuthSiteUrl(deps.env),
|
|
1967
|
-
saved.refreshToken,
|
|
1968
|
-
deps.fetchFn
|
|
1969
|
-
);
|
|
1970
|
-
} catch {
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
deps.store.clearCloudCredential({ baseUrl });
|
|
1974
|
-
if (args.json) {
|
|
1975
|
-
writeJsonLine(deps, {
|
|
1976
|
-
loggedOut: true,
|
|
1977
|
-
baseUrl
|
|
1978
|
-
});
|
|
1979
|
-
return 0;
|
|
1980
|
-
}
|
|
1981
|
-
writeHumanLine(deps, `Opensteer CLI login removed for ${baseUrl}.`);
|
|
1982
|
-
return 0;
|
|
1983
|
-
}
|
|
1984
|
-
async function runOpensteerAuthCli(rawArgs, overrideDeps = {}) {
|
|
1985
|
-
const deps = {
|
|
1986
|
-
...createDefaultDeps(),
|
|
1987
|
-
...overrideDeps
|
|
1988
|
-
};
|
|
1989
|
-
const parsed = parseOpensteerAuthArgs(rawArgs);
|
|
1990
|
-
if (parsed.mode === "help") {
|
|
1991
|
-
printHelp(deps);
|
|
1992
|
-
return 0;
|
|
1993
|
-
}
|
|
1994
|
-
if (parsed.mode === "error") {
|
|
1995
|
-
deps.writeStderr(`${parsed.error}
|
|
1996
|
-
`);
|
|
1997
|
-
deps.writeStderr('Run "opensteer auth --help" for usage.\n');
|
|
1998
|
-
return 1;
|
|
1999
|
-
}
|
|
2000
|
-
try {
|
|
2001
|
-
if (parsed.mode === "login") {
|
|
2002
|
-
return await runLogin(parsed.args, deps);
|
|
2003
|
-
}
|
|
2004
|
-
if (parsed.mode === "status") {
|
|
2005
|
-
return await runStatus(parsed.args, deps);
|
|
2006
|
-
}
|
|
2007
|
-
return await runLogout(parsed.args, deps);
|
|
2008
|
-
} catch (error) {
|
|
2009
|
-
const message = error instanceof Error ? error.message : "Auth command failed.";
|
|
2010
|
-
deps.writeStderr(`${message}
|
|
2011
|
-
`);
|
|
2012
|
-
return 1;
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
2016
|
-
0 && (module.exports = {
|
|
2017
|
-
ensureCloudCredentialsForCommand,
|
|
2018
|
-
ensureCloudCredentialsForOpenCommand,
|
|
2019
|
-
isCloudModeEnabledForRootDir,
|
|
2020
|
-
parseOpensteerAuthArgs,
|
|
2021
|
-
runOpensteerAuthCli
|
|
2022
|
-
});
|