farseer-cli 1.0.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/LICENSE +15 -0
- package/README.md +741 -0
- package/dist/commands/app.d.ts +2 -0
- package/dist/commands/app.js +349 -0
- package/dist/commands/app.js.map +7 -0
- package/dist/commands/apps.d.ts +2 -0
- package/dist/commands/apps.js +111 -0
- package/dist/commands/apps.js.map +7 -0
- package/dist/commands/checkout.d.ts +2 -0
- package/dist/commands/checkout.js +166 -0
- package/dist/commands/checkout.js.map +7 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +139 -0
- package/dist/commands/config.js.map +7 -0
- package/dist/commands/diff.d.ts +2 -0
- package/dist/commands/diff.js +183 -0
- package/dist/commands/diff.js.map +7 -0
- package/dist/commands/files.js +99 -0
- package/dist/commands/files.js.map +7 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +79 -0
- package/dist/commands/install.js.map +7 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.js +92 -0
- package/dist/commands/list.js.map +7 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +134 -0
- package/dist/commands/login.js.map +7 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +59 -0
- package/dist/commands/logout.js.map +7 -0
- package/dist/commands/mcp-server.d.ts +8 -0
- package/dist/commands/mcp-server.js +41 -0
- package/dist/commands/mcp-server.js.map +7 -0
- package/dist/commands/model.d.ts +2 -0
- package/dist/commands/model.js +189 -0
- package/dist/commands/model.js.map +7 -0
- package/dist/commands/pull.d.ts +2 -0
- package/dist/commands/pull.js +287 -0
- package/dist/commands/pull.js.map +7 -0
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/push.js +251 -0
- package/dist/commands/push.js.map +7 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +246 -0
- package/dist/commands/run.js.map +7 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +137 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +145 -0
- package/dist/commands/status.js.map +7 -0
- package/dist/commands/unsetup.d.ts +2 -0
- package/dist/commands/unsetup.js +122 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +63 -0
- package/dist/commands/whoami.js.map +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +135 -0
- package/dist/index.js.map +7 -0
- package/dist/mcp/index.d.ts +7 -0
- package/dist/mcp/index.js +35 -0
- package/dist/mcp/index.js.map +7 -0
- package/dist/mcp/prompts/workflows.d.ts +7 -0
- package/dist/mcp/prompts/workflows.js +374 -0
- package/dist/mcp/prompts/workflows.js.map +7 -0
- package/dist/mcp/resources/documentation.d.ts +8 -0
- package/dist/mcp/resources/documentation.js +167 -0
- package/dist/mcp/resources/documentation.js.map +7 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +49 -0
- package/dist/mcp/server.js.map +7 -0
- package/dist/mcp/tools/appTools.d.ts +7 -0
- package/dist/mcp/tools/appTools.js +377 -0
- package/dist/mcp/tools/appTools.js.map +7 -0
- package/dist/mcp/tools/authTools.d.ts +7 -0
- package/dist/mcp/tools/authTools.js +158 -0
- package/dist/mcp/tools/authTools.js.map +7 -0
- package/dist/mcp/tools/modelTools.d.ts +7 -0
- package/dist/mcp/tools/modelTools.js +331 -0
- package/dist/mcp/tools/modelTools.js.map +7 -0
- package/dist/mcp/tools/runTools.d.ts +7 -0
- package/dist/mcp/tools/runTools.js +231 -0
- package/dist/mcp/tools/runTools.js.map +7 -0
- package/dist/mcp/tools/syncTools.d.ts +7 -0
- package/dist/mcp/tools/syncTools.js +382 -0
- package/dist/mcp/tools/syncTools.js.map +7 -0
- package/dist/mcp/utils/helpers.d.ts +69 -0
- package/dist/mcp/utils/helpers.js +113 -0
- package/dist/mcp/utils/helpers.js.map +7 -0
- package/dist/services/appSyncService.d.ts +75 -0
- package/dist/services/appSyncService.js +370 -0
- package/dist/services/appSyncService.js.map +7 -0
- package/dist/services/configService.d.ts +39 -0
- package/dist/services/configService.js +196 -0
- package/dist/services/configService.js.map +7 -0
- package/dist/services/farseerApi.d.ts +166 -0
- package/dist/services/farseerApi.js +378 -0
- package/dist/services/farseerApi.js.map +7 -0
- package/dist/services/farseerFactory.d.ts +88 -0
- package/dist/services/farseerFactory.js +179 -0
- package/dist/services/farseerFactory.js.map +7 -0
- package/dist/services/farseerService.d.ts +96 -0
- package/dist/services/farseerService.js +614 -0
- package/dist/services/farseerService.js.map +7 -0
- package/dist/services/gitService.d.ts +31 -0
- package/dist/services/gitService.js +134 -0
- package/dist/services/gitService.js.map +7 -0
- package/dist/services/syncService.d.ts +44 -0
- package/dist/services/syncService.js +320 -0
- package/dist/services/syncService.js.map +7 -0
- package/dist/utils/constants.d.ts +7 -0
- package/dist/utils/constants.js +46 -0
- package/dist/utils/constants.js.map +7 -0
- package/dist/utils/helpers.d.ts +69 -0
- package/dist/utils/helpers.js +413 -0
- package/dist/utils/helpers.js.map +7 -0
- package/dist/utils/logger.d.ts +14 -0
- package/dist/utils/logger.js +76 -0
- package/dist/utils/logger.js.map +7 -0
- package/package.json +62 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
var farseerService_exports = {};
|
|
29
|
+
__export(farseerService_exports, {
|
|
30
|
+
FarseerService: () => FarseerService,
|
|
31
|
+
loginWithBrowser: () => loginWithBrowser,
|
|
32
|
+
loginWithPassword: () => loginWithPassword,
|
|
33
|
+
refreshAccessToken: () => refreshAccessToken
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(farseerService_exports);
|
|
36
|
+
var farseer = __toESM(require("farseer-client"));
|
|
37
|
+
var import_configService = require("./configService");
|
|
38
|
+
class FarseerService {
|
|
39
|
+
constructor(credential) {
|
|
40
|
+
this.credential = credential;
|
|
41
|
+
this.client = new farseer.FarseerClient({
|
|
42
|
+
basePath: credential.basePath,
|
|
43
|
+
headers: {
|
|
44
|
+
"X-TENANT-ID": credential.tenantId,
|
|
45
|
+
"X-API-KEY": credential.apiKey
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
static fromUserAuth(tenant, basePath) {
|
|
50
|
+
const auth = (0, import_configService.getUserAuth)();
|
|
51
|
+
if (!auth) return null;
|
|
52
|
+
const service = new FarseerService({
|
|
53
|
+
type: "apiKey",
|
|
54
|
+
tenantId: tenant,
|
|
55
|
+
apiKey: "",
|
|
56
|
+
basePath
|
|
57
|
+
});
|
|
58
|
+
service.client = new farseer.FarseerClient({
|
|
59
|
+
basePath,
|
|
60
|
+
headers: {
|
|
61
|
+
"X-TENANT-ID": tenant
|
|
62
|
+
},
|
|
63
|
+
accessToken: auth.accessToken
|
|
64
|
+
});
|
|
65
|
+
return service;
|
|
66
|
+
}
|
|
67
|
+
async testConnection() {
|
|
68
|
+
try {
|
|
69
|
+
await this.client.getItemByPath(["Files"]);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async getFilesFolder() {
|
|
76
|
+
try {
|
|
77
|
+
const item = await this.client.getItemByPath(["Files"]);
|
|
78
|
+
return {
|
|
79
|
+
id: item.id,
|
|
80
|
+
name: item.name,
|
|
81
|
+
type: item.type
|
|
82
|
+
};
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async listFiles(folderPath = ["Files"]) {
|
|
88
|
+
const files = [];
|
|
89
|
+
try {
|
|
90
|
+
const folder = await this.client.getItemByPath(folderPath);
|
|
91
|
+
const folderContent = await this.client.folders.listItemsBatch([{ id: folder.id }]);
|
|
92
|
+
for (const item of folderContent[0]?.items || []) {
|
|
93
|
+
const itemPath = [...folderPath.slice(1), item.name].join("/");
|
|
94
|
+
if (item.type === farseer.FolderItemType.Folder) {
|
|
95
|
+
const subFiles = await this.listFiles([...folderPath, item.name]);
|
|
96
|
+
files.push(...subFiles);
|
|
97
|
+
} else if (item.type === farseer.FolderItemType.FarseerFile && "reference" in item) {
|
|
98
|
+
files.push({
|
|
99
|
+
name: item.name,
|
|
100
|
+
path: itemPath,
|
|
101
|
+
reference: item.reference
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
return files;
|
|
109
|
+
}
|
|
110
|
+
async getFileContent(reference) {
|
|
111
|
+
const blob = await this.client.farseerFiles.get(reference);
|
|
112
|
+
return await blob.text();
|
|
113
|
+
}
|
|
114
|
+
async createFile(content, fileName, folderPath = ["Files"]) {
|
|
115
|
+
await this.ensureFolderPath(folderPath);
|
|
116
|
+
const folder = await this.client.getItemByPath(folderPath);
|
|
117
|
+
const blob = content instanceof Buffer ? new Blob([content]) : new Blob([content]);
|
|
118
|
+
const file = new farseer.WebFile([blob], fileName);
|
|
119
|
+
await this.client.farseerFiles.create(file, "GENERAL", folder.id.toString());
|
|
120
|
+
}
|
|
121
|
+
async updateFile(reference, content, fileName) {
|
|
122
|
+
const blob = content instanceof Buffer ? new Blob([content]) : new Blob([content]);
|
|
123
|
+
const file = new farseer.WebFile([blob], fileName);
|
|
124
|
+
await this.client.farseerFiles.update(reference, file);
|
|
125
|
+
}
|
|
126
|
+
async deleteFile(reference) {
|
|
127
|
+
await this.client.farseerFiles.remove(reference);
|
|
128
|
+
}
|
|
129
|
+
async ensureFolderPath(folderPath) {
|
|
130
|
+
let currentPath = [];
|
|
131
|
+
for (const segment of folderPath) {
|
|
132
|
+
currentPath.push(segment);
|
|
133
|
+
try {
|
|
134
|
+
await this.client.getItemByPath(currentPath);
|
|
135
|
+
} catch {
|
|
136
|
+
if (currentPath.length > 1) {
|
|
137
|
+
const parentPath = currentPath.slice(0, -1);
|
|
138
|
+
const parent = await this.client.getItemByPath(parentPath);
|
|
139
|
+
await this.client.folders.create({
|
|
140
|
+
parentId: parent.id,
|
|
141
|
+
name: segment,
|
|
142
|
+
allowedTypes: []
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async getFileByPath(filePath) {
|
|
149
|
+
const pathParts = ["Files", ...filePath.split("/")];
|
|
150
|
+
const fileName = pathParts.pop();
|
|
151
|
+
const folderPath = pathParts;
|
|
152
|
+
try {
|
|
153
|
+
const folder = await this.client.getItemByPath(folderPath);
|
|
154
|
+
const folderContent = await this.client.folders.listItemsBatch([{ id: folder.id }]);
|
|
155
|
+
for (const item of folderContent[0]?.items || []) {
|
|
156
|
+
if (item.name === fileName && item.type === farseer.FolderItemType.FarseerFile && "reference" in item) {
|
|
157
|
+
return {
|
|
158
|
+
name: item.name,
|
|
159
|
+
path: filePath,
|
|
160
|
+
reference: item.reference
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
async getFileMetadata(reference) {
|
|
170
|
+
try {
|
|
171
|
+
const metadata = await this.client.farseerFiles.getMetadata(reference);
|
|
172
|
+
return {
|
|
173
|
+
uploadTime: metadata.uploadTime?.toISOString(),
|
|
174
|
+
uploader: metadata.uploader ? {
|
|
175
|
+
id: metadata.uploader.id,
|
|
176
|
+
email: metadata.uploader.email || "",
|
|
177
|
+
firstName: metadata.uploader.firstName || "",
|
|
178
|
+
lastName: metadata.uploader.lastName || ""
|
|
179
|
+
} : void 0,
|
|
180
|
+
size: metadata.size
|
|
181
|
+
};
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async getFileContentAsBuffer(reference) {
|
|
187
|
+
const blob = await this.client.farseerFiles.get(reference);
|
|
188
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
189
|
+
return Buffer.from(arrayBuffer);
|
|
190
|
+
}
|
|
191
|
+
isTextFile(filename) {
|
|
192
|
+
const ext = filename.toLowerCase().split(".").pop() || "";
|
|
193
|
+
return ["ts", "js", "mjs", "cjs", "json", "txt", "md", "csv", "xml", "html", "css", "yaml", "yml"].includes(ext);
|
|
194
|
+
}
|
|
195
|
+
isBinaryFile(filename) {
|
|
196
|
+
return !this.isTextFile(filename);
|
|
197
|
+
}
|
|
198
|
+
// ==================== Apps (Remote Jobs) API ====================
|
|
199
|
+
/**
|
|
200
|
+
* List all apps (Remote Jobs) on the tenant
|
|
201
|
+
*/
|
|
202
|
+
async listApps() {
|
|
203
|
+
const response = await this.apiRequest("/remoteJobs", "GET");
|
|
204
|
+
return response.map((app) => ({
|
|
205
|
+
id: app.id,
|
|
206
|
+
name: app.name,
|
|
207
|
+
type: "REMOTE_JOB",
|
|
208
|
+
reference: String(app.id)
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Get app details by reference ID
|
|
213
|
+
*/
|
|
214
|
+
async getApp(referenceId) {
|
|
215
|
+
try {
|
|
216
|
+
return await this.apiRequest(`/remoteJobs/${referenceId}`, "GET");
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get app by name
|
|
223
|
+
*/
|
|
224
|
+
async getAppByName(name) {
|
|
225
|
+
const apps = await this.listApps();
|
|
226
|
+
const app = apps.find((a) => a.name.toLowerCase() === name.toLowerCase());
|
|
227
|
+
if (!app) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
return this.getApp(app.reference);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Create a new app
|
|
234
|
+
*/
|
|
235
|
+
async createApp(name) {
|
|
236
|
+
const appsFolderId = await this.getAppsFolderId();
|
|
237
|
+
if (!appsFolderId) {
|
|
238
|
+
throw new Error("Apps folder not found");
|
|
239
|
+
}
|
|
240
|
+
return await this.apiRequest("/remoteJobs", "POST", {
|
|
241
|
+
folderId: appsFolderId,
|
|
242
|
+
name
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Update an app
|
|
247
|
+
*/
|
|
248
|
+
async updateApp(referenceId, update) {
|
|
249
|
+
await this.apiRequest(`/remoteJobs/${referenceId}`, "PUT", {
|
|
250
|
+
...update,
|
|
251
|
+
schedule: null,
|
|
252
|
+
runnerName: null,
|
|
253
|
+
predefinedActions: []
|
|
254
|
+
});
|
|
255
|
+
const app = await this.getApp(referenceId);
|
|
256
|
+
if (!app) {
|
|
257
|
+
throw new Error("Failed to fetch updated app");
|
|
258
|
+
}
|
|
259
|
+
return app;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Delete an app
|
|
263
|
+
*/
|
|
264
|
+
async deleteApp(referenceId) {
|
|
265
|
+
await this.apiRequest(`/remoteJobs/${referenceId}`, "DELETE");
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get script files available for apps
|
|
269
|
+
*/
|
|
270
|
+
async getScriptFiles() {
|
|
271
|
+
const allFiles = await this.listFiles(["Files"]);
|
|
272
|
+
return allFiles.filter((f) => f.name.endsWith(".ts") || f.name.endsWith(".js")).map((f) => ({
|
|
273
|
+
id: f.reference,
|
|
274
|
+
name: f.name
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Export the entire model structure
|
|
279
|
+
*/
|
|
280
|
+
async exportModel() {
|
|
281
|
+
return this.apiRequest("/model/export", "POST");
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Get the Apps folder ID
|
|
285
|
+
*/
|
|
286
|
+
async getAppsFolderId() {
|
|
287
|
+
try {
|
|
288
|
+
const item = await this.client.getItemByPath(["Apps"]);
|
|
289
|
+
return item.id;
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Make a direct API request (for endpoints not in farseer-client)
|
|
296
|
+
*/
|
|
297
|
+
async apiRequest(endpoint, method, body) {
|
|
298
|
+
const url = `${this.credential.basePath}${endpoint}`;
|
|
299
|
+
const headers = {
|
|
300
|
+
"Content-Type": "application/json",
|
|
301
|
+
"X-TENANT-ID": this.credential.tenantId,
|
|
302
|
+
"X-API-KEY": this.credential.apiKey,
|
|
303
|
+
"x-api-version": "3.3.0"
|
|
304
|
+
};
|
|
305
|
+
const response = await fetch(url, {
|
|
306
|
+
method,
|
|
307
|
+
headers,
|
|
308
|
+
body: body ? JSON.stringify(body) : void 0
|
|
309
|
+
});
|
|
310
|
+
if (!response.ok) {
|
|
311
|
+
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
|
312
|
+
}
|
|
313
|
+
if (response.status === 204) {
|
|
314
|
+
return void 0;
|
|
315
|
+
}
|
|
316
|
+
return response.json();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function loginWithPassword(email, password) {
|
|
320
|
+
const tokenUrl = "https://login.farseer.io/auth/realms/master/protocol/openid-connect/token";
|
|
321
|
+
try {
|
|
322
|
+
const response = await fetch(tokenUrl, {
|
|
323
|
+
method: "POST",
|
|
324
|
+
headers: {
|
|
325
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
326
|
+
},
|
|
327
|
+
body: new URLSearchParams({
|
|
328
|
+
grant_type: "password",
|
|
329
|
+
client_id: "security-admin-console",
|
|
330
|
+
username: email,
|
|
331
|
+
password
|
|
332
|
+
})
|
|
333
|
+
});
|
|
334
|
+
if (!response.ok) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
const data = await response.json();
|
|
338
|
+
return {
|
|
339
|
+
accessToken: data.access_token,
|
|
340
|
+
refreshToken: data.refresh_token,
|
|
341
|
+
expiresIn: data.expires_in
|
|
342
|
+
};
|
|
343
|
+
} catch {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function getErrorPage(title, message) {
|
|
348
|
+
return `
|
|
349
|
+
<html>
|
|
350
|
+
<head>
|
|
351
|
+
<title>Farseer CLI - Error</title>
|
|
352
|
+
<style>
|
|
353
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
354
|
+
body {
|
|
355
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
356
|
+
background: #fafafa;
|
|
357
|
+
min-height: 100vh;
|
|
358
|
+
display: flex;
|
|
359
|
+
align-items: center;
|
|
360
|
+
justify-content: center;
|
|
361
|
+
}
|
|
362
|
+
.container {
|
|
363
|
+
text-align: center;
|
|
364
|
+
padding: 48px;
|
|
365
|
+
}
|
|
366
|
+
.error-icon {
|
|
367
|
+
width: 80px;
|
|
368
|
+
height: 80px;
|
|
369
|
+
border-radius: 50%;
|
|
370
|
+
background: #ef4444;
|
|
371
|
+
display: flex;
|
|
372
|
+
align-items: center;
|
|
373
|
+
justify-content: center;
|
|
374
|
+
margin: 0 auto 24px;
|
|
375
|
+
}
|
|
376
|
+
.error-icon svg {
|
|
377
|
+
width: 40px;
|
|
378
|
+
height: 40px;
|
|
379
|
+
stroke: white;
|
|
380
|
+
stroke-width: 3;
|
|
381
|
+
fill: none;
|
|
382
|
+
}
|
|
383
|
+
h1 {
|
|
384
|
+
color: #18181b;
|
|
385
|
+
font-size: 24px;
|
|
386
|
+
font-weight: 600;
|
|
387
|
+
margin-bottom: 8px;
|
|
388
|
+
}
|
|
389
|
+
p {
|
|
390
|
+
color: #71717a;
|
|
391
|
+
font-size: 15px;
|
|
392
|
+
}
|
|
393
|
+
</style>
|
|
394
|
+
</head>
|
|
395
|
+
<body>
|
|
396
|
+
<div class="container">
|
|
397
|
+
<div class="error-icon">
|
|
398
|
+
<svg viewBox="0 0 24 24">
|
|
399
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
400
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
401
|
+
</svg>
|
|
402
|
+
</div>
|
|
403
|
+
<h1>${title}</h1>
|
|
404
|
+
<p>${message}</p>
|
|
405
|
+
<p style="margin-top: 16px;">You can close this window.</p>
|
|
406
|
+
</div>
|
|
407
|
+
</body>
|
|
408
|
+
</html>
|
|
409
|
+
`;
|
|
410
|
+
}
|
|
411
|
+
async function loginWithBrowser(realm = "master") {
|
|
412
|
+
const http = await import("http");
|
|
413
|
+
const crypto = await import("crypto");
|
|
414
|
+
const { exec } = await import("child_process");
|
|
415
|
+
const { promisify } = await import("util");
|
|
416
|
+
const execAsync = promisify(exec);
|
|
417
|
+
const KEYCLOAK_BASE = `https://login.farseer.io/auth/realms/${realm}/protocol/openid-connect`;
|
|
418
|
+
const CLIENT_ID = "auth";
|
|
419
|
+
const REDIRECT_PORT = 8787;
|
|
420
|
+
const REDIRECT_URI = `http://localhost:${REDIRECT_PORT}/callback`;
|
|
421
|
+
const codeVerifier = crypto.randomBytes(32).toString("base64url");
|
|
422
|
+
const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
423
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
424
|
+
return new Promise((resolve) => {
|
|
425
|
+
const server = http.createServer(async (req, res) => {
|
|
426
|
+
const url = new URL(req.url || "", `http://localhost:${REDIRECT_PORT}`);
|
|
427
|
+
if (url.pathname === "/callback") {
|
|
428
|
+
const code = url.searchParams.get("code");
|
|
429
|
+
const returnedState = url.searchParams.get("state");
|
|
430
|
+
const error = url.searchParams.get("error");
|
|
431
|
+
if (error) {
|
|
432
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
433
|
+
res.end(getErrorPage("Login Failed", `Error: ${error}`));
|
|
434
|
+
server.close();
|
|
435
|
+
resolve(null);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (!code || returnedState !== state) {
|
|
439
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
440
|
+
res.end(getErrorPage("Invalid Response", "Missing code or state mismatch."));
|
|
441
|
+
server.close();
|
|
442
|
+
resolve(null);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
const tokenResponse = await fetch(`${KEYCLOAK_BASE}/token`, {
|
|
447
|
+
method: "POST",
|
|
448
|
+
headers: {
|
|
449
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
450
|
+
},
|
|
451
|
+
body: new URLSearchParams({
|
|
452
|
+
grant_type: "authorization_code",
|
|
453
|
+
client_id: CLIENT_ID,
|
|
454
|
+
code,
|
|
455
|
+
redirect_uri: REDIRECT_URI,
|
|
456
|
+
code_verifier: codeVerifier
|
|
457
|
+
})
|
|
458
|
+
});
|
|
459
|
+
if (!tokenResponse.ok) {
|
|
460
|
+
throw new Error("Token exchange failed");
|
|
461
|
+
}
|
|
462
|
+
const data = await tokenResponse.json();
|
|
463
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
464
|
+
res.end(`
|
|
465
|
+
<html>
|
|
466
|
+
<head>
|
|
467
|
+
<title>Farseer CLI - Login</title>
|
|
468
|
+
<style>
|
|
469
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
470
|
+
body {
|
|
471
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
472
|
+
background: #fafafa;
|
|
473
|
+
min-height: 100vh;
|
|
474
|
+
display: flex;
|
|
475
|
+
align-items: center;
|
|
476
|
+
justify-content: center;
|
|
477
|
+
}
|
|
478
|
+
.container {
|
|
479
|
+
text-align: center;
|
|
480
|
+
padding: 48px;
|
|
481
|
+
}
|
|
482
|
+
.checkmark {
|
|
483
|
+
width: 80px;
|
|
484
|
+
height: 80px;
|
|
485
|
+
border-radius: 50%;
|
|
486
|
+
background: #22c55e;
|
|
487
|
+
display: flex;
|
|
488
|
+
align-items: center;
|
|
489
|
+
justify-content: center;
|
|
490
|
+
margin: 0 auto 24px;
|
|
491
|
+
animation: scale-in 0.3s ease-out;
|
|
492
|
+
}
|
|
493
|
+
.checkmark svg {
|
|
494
|
+
width: 40px;
|
|
495
|
+
height: 40px;
|
|
496
|
+
stroke: white;
|
|
497
|
+
stroke-width: 3;
|
|
498
|
+
fill: none;
|
|
499
|
+
animation: draw 0.4s ease-out 0.2s forwards;
|
|
500
|
+
stroke-dasharray: 50;
|
|
501
|
+
stroke-dashoffset: 50;
|
|
502
|
+
}
|
|
503
|
+
@keyframes scale-in {
|
|
504
|
+
from { transform: scale(0); }
|
|
505
|
+
to { transform: scale(1); }
|
|
506
|
+
}
|
|
507
|
+
@keyframes draw {
|
|
508
|
+
to { stroke-dashoffset: 0; }
|
|
509
|
+
}
|
|
510
|
+
h1 {
|
|
511
|
+
color: #18181b;
|
|
512
|
+
font-size: 24px;
|
|
513
|
+
font-weight: 600;
|
|
514
|
+
margin-bottom: 8px;
|
|
515
|
+
}
|
|
516
|
+
p {
|
|
517
|
+
color: #71717a;
|
|
518
|
+
font-size: 15px;
|
|
519
|
+
}
|
|
520
|
+
</style>
|
|
521
|
+
</head>
|
|
522
|
+
<body>
|
|
523
|
+
<div class="container">
|
|
524
|
+
<div class="checkmark">
|
|
525
|
+
<svg viewBox="0 0 24 24">
|
|
526
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
|
527
|
+
</svg>
|
|
528
|
+
</div>
|
|
529
|
+
<h1>Login Successful</h1>
|
|
530
|
+
<p>You can close this window and return to the terminal.</p>
|
|
531
|
+
</div>
|
|
532
|
+
</body>
|
|
533
|
+
</html>
|
|
534
|
+
`);
|
|
535
|
+
server.close();
|
|
536
|
+
resolve({
|
|
537
|
+
accessToken: data.access_token,
|
|
538
|
+
refreshToken: data.refresh_token,
|
|
539
|
+
expiresIn: data.expires_in
|
|
540
|
+
});
|
|
541
|
+
} catch {
|
|
542
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
543
|
+
res.end(getErrorPage("Token Exchange Failed", "Could not complete login."));
|
|
544
|
+
server.close();
|
|
545
|
+
resolve(null);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
server.listen(REDIRECT_PORT, async () => {
|
|
550
|
+
const authUrl = new URL(`${KEYCLOAK_BASE}/auth`);
|
|
551
|
+
authUrl.searchParams.set("client_id", CLIENT_ID);
|
|
552
|
+
authUrl.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
553
|
+
authUrl.searchParams.set("response_type", "code");
|
|
554
|
+
authUrl.searchParams.set("scope", "openid offline_access");
|
|
555
|
+
authUrl.searchParams.set("state", state);
|
|
556
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
557
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
558
|
+
const url = authUrl.toString();
|
|
559
|
+
try {
|
|
560
|
+
if (process.platform === "darwin") {
|
|
561
|
+
await execAsync(`open "${url}"`);
|
|
562
|
+
} else if (process.platform === "win32") {
|
|
563
|
+
await execAsync(`start "${url}"`);
|
|
564
|
+
} else {
|
|
565
|
+
await execAsync(`xdg-open "${url}"`);
|
|
566
|
+
}
|
|
567
|
+
} catch {
|
|
568
|
+
console.log(`
|
|
569
|
+
Open this URL in your browser:
|
|
570
|
+
${url}
|
|
571
|
+
`);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
setTimeout(() => {
|
|
575
|
+
server.close();
|
|
576
|
+
resolve(null);
|
|
577
|
+
}, 5 * 60 * 1e3);
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
async function refreshAccessToken(refreshToken, realm = "master") {
|
|
581
|
+
const tokenUrl = `https://login.farseer.io/auth/realms/${realm}/protocol/openid-connect/token`;
|
|
582
|
+
try {
|
|
583
|
+
const response = await fetch(tokenUrl, {
|
|
584
|
+
method: "POST",
|
|
585
|
+
headers: {
|
|
586
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
587
|
+
},
|
|
588
|
+
body: new URLSearchParams({
|
|
589
|
+
grant_type: "refresh_token",
|
|
590
|
+
client_id: "auth",
|
|
591
|
+
refresh_token: refreshToken
|
|
592
|
+
})
|
|
593
|
+
});
|
|
594
|
+
if (!response.ok) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
const data = await response.json();
|
|
598
|
+
return {
|
|
599
|
+
accessToken: data.access_token,
|
|
600
|
+
refreshToken: data.refresh_token,
|
|
601
|
+
expiresIn: data.expires_in
|
|
602
|
+
};
|
|
603
|
+
} catch {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
608
|
+
0 && (module.exports = {
|
|
609
|
+
FarseerService,
|
|
610
|
+
loginWithBrowser,
|
|
611
|
+
loginWithPassword,
|
|
612
|
+
refreshAccessToken
|
|
613
|
+
});
|
|
614
|
+
//# sourceMappingURL=farseerService.js.map
|