@instafy/cli 0.1.8 → 0.1.10
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 +16 -9
- package/dist/api.js +50 -16
- package/dist/auth.js +510 -26
- package/dist/config-command.js +2 -0
- package/dist/config.js +187 -6
- package/dist/controller-fetch.js +33 -0
- package/dist/errors.js +63 -0
- package/dist/git-credential.js +205 -0
- package/dist/git-setup.js +56 -0
- package/dist/git-wrapper.js +502 -0
- package/dist/git.js +11 -5
- package/dist/index.js +293 -108
- package/dist/org.js +19 -9
- package/dist/project-manifest.js +24 -0
- package/dist/project.js +285 -45
- package/dist/rathole.js +14 -10
- package/dist/runtime.js +86 -45
- package/dist/supabase-session.js +89 -0
- package/dist/tunnel.js +293 -21
- package/package.json +3 -1
package/dist/auth.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import kleur from "kleur";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import * as http from "node:http";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
2
5
|
import { createInterface } from "node:readline/promises";
|
|
3
6
|
import { stdin as input, stdout as output } from "node:process";
|
|
4
|
-
import { clearInstafyCliConfig, getInstafyConfigPath,
|
|
7
|
+
import { clearInstafyCliConfig, clearInstafyProfileConfig, getInstafyConfigPath, getInstafyProfileConfigPath, resolveConfiguredControllerUrl, resolveConfiguredStudioUrl, resolveUserAccessToken, writeInstafyCliConfig, writeInstafyProfileConfig, } from "./config.js";
|
|
8
|
+
import { installGitCredentialHelper, uninstallGitCredentialHelper } from "./git-setup.js";
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const cliVersion = (() => {
|
|
11
|
+
try {
|
|
12
|
+
const pkg = require("../package.json");
|
|
13
|
+
return typeof pkg.version === "string" ? pkg.version : "";
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
})();
|
|
19
|
+
const isStagingCli = cliVersion.includes("-staging.");
|
|
5
20
|
function normalizeUrl(raw) {
|
|
6
21
|
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
|
7
22
|
if (!trimmed) {
|
|
@@ -20,6 +35,32 @@ function normalizeToken(raw) {
|
|
|
20
35
|
}
|
|
21
36
|
return trimmed;
|
|
22
37
|
}
|
|
38
|
+
async function loginWithPassword(params) {
|
|
39
|
+
const response = await fetch(`${params.supabaseUrl}/auth/v1/token?grant_type=password`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
apikey: params.supabaseAnonKey,
|
|
43
|
+
"content-type": "application/json",
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({ email: params.email, password: params.password }),
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const text = await response.text().catch(() => "");
|
|
49
|
+
throw new Error(`Supabase login failed (${response.status}): ${text}`);
|
|
50
|
+
}
|
|
51
|
+
const body = (await response.json());
|
|
52
|
+
const accessToken = typeof body["access_token"] === "string" ? body["access_token"] : null;
|
|
53
|
+
if (!accessToken) {
|
|
54
|
+
throw new Error("Supabase login response missing access_token");
|
|
55
|
+
}
|
|
56
|
+
const refreshToken = typeof body["refresh_token"] === "string" ? body["refresh_token"] : null;
|
|
57
|
+
return {
|
|
58
|
+
accessToken,
|
|
59
|
+
refreshToken: normalizeToken(refreshToken),
|
|
60
|
+
supabaseUrl: normalizeUrl(params.supabaseUrl),
|
|
61
|
+
supabaseAnonKey: normalizeToken(params.supabaseAnonKey),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
23
64
|
function looksLikeLocalControllerUrl(controllerUrl) {
|
|
24
65
|
try {
|
|
25
66
|
const parsed = new URL(controllerUrl);
|
|
@@ -30,50 +71,477 @@ function looksLikeLocalControllerUrl(controllerUrl) {
|
|
|
30
71
|
return controllerUrl.includes("127.0.0.1") || controllerUrl.includes("localhost");
|
|
31
72
|
}
|
|
32
73
|
}
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
74
|
+
async function isStudioHealthy(studioUrl, timeoutMs) {
|
|
75
|
+
const target = new URL("/cli/login", studioUrl).toString();
|
|
76
|
+
const abort = new AbortController();
|
|
77
|
+
const timeout = setTimeout(() => abort.abort(), timeoutMs);
|
|
78
|
+
timeout.unref?.();
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch(target, { signal: abort.signal });
|
|
81
|
+
return response.ok;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
clearTimeout(timeout);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function resolveDefaultStudioUrl(controllerUrl) {
|
|
91
|
+
const hosted = "https://staging.instafy.dev";
|
|
92
|
+
if (isStagingCli) {
|
|
93
|
+
return hosted;
|
|
94
|
+
}
|
|
95
|
+
if (!looksLikeLocalControllerUrl(controllerUrl)) {
|
|
96
|
+
return hosted;
|
|
97
|
+
}
|
|
98
|
+
const local = "http://localhost:5173";
|
|
99
|
+
const healthy = await isStudioHealthy(local, 250);
|
|
100
|
+
return healthy ? local : hosted;
|
|
101
|
+
}
|
|
102
|
+
async function isControllerHealthy(controllerUrl, timeoutMs) {
|
|
103
|
+
const target = `${controllerUrl.replace(/\/$/, "")}/healthz`;
|
|
104
|
+
const abort = new AbortController();
|
|
105
|
+
const timeout = setTimeout(() => abort.abort(), timeoutMs);
|
|
106
|
+
timeout.unref?.();
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetch(target, { signal: abort.signal });
|
|
109
|
+
return response.ok;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
clearTimeout(timeout);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function readRequestBody(request, maxBytes = 1000000) {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
let buffer = "";
|
|
121
|
+
request.setEncoding("utf8");
|
|
122
|
+
request.on("data", (chunk) => {
|
|
123
|
+
buffer += chunk;
|
|
124
|
+
if (buffer.length > maxBytes) {
|
|
125
|
+
reject(new Error("Request body too large."));
|
|
126
|
+
request.destroy();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
request.on("end", () => resolve(buffer));
|
|
130
|
+
request.on("error", (error) => reject(error));
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function applyCorsHeaders(request, response) {
|
|
134
|
+
const origin = typeof request.headers.origin === "string" ? request.headers.origin : "";
|
|
135
|
+
if (origin) {
|
|
136
|
+
response.setHeader("access-control-allow-origin", origin);
|
|
137
|
+
response.setHeader("vary", "origin");
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
response.setHeader("access-control-allow-origin", "*");
|
|
141
|
+
}
|
|
142
|
+
response.setHeader("access-control-allow-methods", "POST, OPTIONS");
|
|
143
|
+
response.setHeader("access-control-allow-headers", "content-type");
|
|
144
|
+
// Private Network Access preflight (Chrome): allow https -> http://127.0.0.1 callbacks.
|
|
145
|
+
if (request.headers["access-control-request-private-network"] === "true") {
|
|
146
|
+
response.setHeader("access-control-allow-private-network", "true");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function startCliLoginCallbackServer() {
|
|
150
|
+
const state = randomBytes(16).toString("hex");
|
|
151
|
+
let resolved = false;
|
|
152
|
+
let resolveToken = null;
|
|
153
|
+
let rejectToken = null;
|
|
154
|
+
const tokenPromise = new Promise((resolve, reject) => {
|
|
155
|
+
resolveToken = resolve;
|
|
156
|
+
rejectToken = reject;
|
|
157
|
+
});
|
|
158
|
+
const server = http.createServer(async (request, response) => {
|
|
159
|
+
applyCorsHeaders(request, response);
|
|
160
|
+
if (request.method === "OPTIONS") {
|
|
161
|
+
response.statusCode = 204;
|
|
162
|
+
response.end();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
166
|
+
if (request.method !== "POST" || url.pathname !== "/callback") {
|
|
167
|
+
response.statusCode = 404;
|
|
168
|
+
response.setHeader("content-type", "application/json");
|
|
169
|
+
response.end(JSON.stringify({ ok: false, error: "Not found" }));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (resolved) {
|
|
173
|
+
response.statusCode = 409;
|
|
174
|
+
response.setHeader("content-type", "application/json");
|
|
175
|
+
response.end(JSON.stringify({ ok: false, error: "Already completed" }));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const body = await readRequestBody(request);
|
|
180
|
+
const contentType = typeof request.headers["content-type"] === "string" ? request.headers["content-type"] : "";
|
|
181
|
+
let parsedToken = null;
|
|
182
|
+
let parsedRefreshToken = null;
|
|
183
|
+
let parsedSupabaseUrl = null;
|
|
184
|
+
let parsedSupabaseAnonKey = null;
|
|
185
|
+
let parsedState = null;
|
|
186
|
+
if (contentType.includes("application/json")) {
|
|
187
|
+
const json = JSON.parse(body);
|
|
188
|
+
parsedToken =
|
|
189
|
+
typeof json.token === "string"
|
|
190
|
+
? json.token
|
|
191
|
+
: typeof json.accessToken === "string"
|
|
192
|
+
? json.accessToken
|
|
193
|
+
: typeof json.access_token === "string"
|
|
194
|
+
? json.access_token
|
|
195
|
+
: null;
|
|
196
|
+
parsedRefreshToken =
|
|
197
|
+
typeof json.refreshToken === "string"
|
|
198
|
+
? json.refreshToken
|
|
199
|
+
: typeof json.refresh_token === "string"
|
|
200
|
+
? json.refresh_token
|
|
201
|
+
: null;
|
|
202
|
+
parsedSupabaseUrl = typeof json.supabaseUrl === "string" ? json.supabaseUrl : null;
|
|
203
|
+
parsedSupabaseAnonKey =
|
|
204
|
+
typeof json.supabaseAnonKey === "string"
|
|
205
|
+
? json.supabaseAnonKey
|
|
206
|
+
: typeof json.supabase_anon_key === "string"
|
|
207
|
+
? json.supabase_anon_key
|
|
208
|
+
: null;
|
|
209
|
+
parsedState = typeof json.state === "string" ? json.state : null;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const params = new URLSearchParams(body);
|
|
213
|
+
parsedToken = params.get("token");
|
|
214
|
+
parsedRefreshToken = params.get("refreshToken") ?? params.get("refresh_token");
|
|
215
|
+
parsedSupabaseUrl = params.get("supabaseUrl") ?? params.get("supabase_url");
|
|
216
|
+
parsedSupabaseAnonKey = params.get("supabaseAnonKey") ?? params.get("supabase_anon_key");
|
|
217
|
+
parsedState = params.get("state");
|
|
218
|
+
}
|
|
219
|
+
const token = normalizeToken(parsedToken);
|
|
220
|
+
const refreshToken = normalizeToken(parsedRefreshToken);
|
|
221
|
+
const supabaseUrl = normalizeUrl(parsedSupabaseUrl);
|
|
222
|
+
const supabaseAnonKey = normalizeToken(parsedSupabaseAnonKey);
|
|
223
|
+
const receivedState = normalizeToken(parsedState);
|
|
224
|
+
if (!token) {
|
|
225
|
+
response.statusCode = 400;
|
|
226
|
+
response.setHeader("content-type", "application/json");
|
|
227
|
+
response.end(JSON.stringify({ ok: false, error: "Missing token" }));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (!receivedState || receivedState !== state) {
|
|
231
|
+
response.statusCode = 403;
|
|
232
|
+
response.setHeader("content-type", "application/json");
|
|
233
|
+
response.end(JSON.stringify({ ok: false, error: "Invalid state" }));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
resolved = true;
|
|
237
|
+
response.statusCode = 200;
|
|
238
|
+
response.setHeader("content-type", "application/json");
|
|
239
|
+
response.end(JSON.stringify({ ok: true }));
|
|
240
|
+
resolveToken?.({
|
|
241
|
+
accessToken: token,
|
|
242
|
+
refreshToken,
|
|
243
|
+
supabaseUrl,
|
|
244
|
+
supabaseAnonKey,
|
|
245
|
+
});
|
|
246
|
+
resolveToken = null;
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
response.statusCode = 500;
|
|
250
|
+
response.setHeader("content-type", "application/json");
|
|
251
|
+
response.end(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
await new Promise((resolve, reject) => {
|
|
255
|
+
server.once("error", reject);
|
|
256
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
257
|
+
});
|
|
258
|
+
const address = server.address();
|
|
259
|
+
if (!address || typeof address !== "object" || typeof address.port !== "number") {
|
|
260
|
+
server.close();
|
|
261
|
+
throw new Error("Failed to start login callback server.");
|
|
36
262
|
}
|
|
37
|
-
return
|
|
263
|
+
return {
|
|
264
|
+
callbackUrl: `http://127.0.0.1:${address.port}/callback`,
|
|
265
|
+
state,
|
|
266
|
+
waitForToken: async (timeoutMs) => {
|
|
267
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
268
|
+
return tokenPromise;
|
|
269
|
+
}
|
|
270
|
+
const timeout = setTimeout(() => {
|
|
271
|
+
if (resolved)
|
|
272
|
+
return;
|
|
273
|
+
resolved = true;
|
|
274
|
+
rejectToken?.(new Error("Timed out waiting for browser login. Copy the token and paste it into the CLI instead."));
|
|
275
|
+
rejectToken = null;
|
|
276
|
+
}, timeoutMs);
|
|
277
|
+
timeout.unref?.();
|
|
278
|
+
try {
|
|
279
|
+
return await tokenPromise;
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
clearTimeout(timeout);
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
cancel: () => {
|
|
286
|
+
if (resolved)
|
|
287
|
+
return;
|
|
288
|
+
resolved = true;
|
|
289
|
+
rejectToken?.(new Error("Login cancelled."));
|
|
290
|
+
rejectToken = null;
|
|
291
|
+
resolveToken = null;
|
|
292
|
+
},
|
|
293
|
+
close: () => server.close(),
|
|
294
|
+
};
|
|
38
295
|
}
|
|
39
296
|
export async function login(options) {
|
|
40
|
-
const
|
|
297
|
+
const profile = typeof options.profile === "string" && options.profile.trim() ? options.profile.trim() : null;
|
|
298
|
+
const explicitControllerUrl = normalizeUrl(options.controllerUrl ?? null) ??
|
|
299
|
+
normalizeUrl(process.env["INSTAFY_SERVER_URL"] ?? null) ??
|
|
300
|
+
normalizeUrl(process.env["CONTROLLER_BASE_URL"] ?? null) ??
|
|
301
|
+
(isStagingCli ? null : resolveConfiguredControllerUrl({ profile }));
|
|
302
|
+
const defaultLocalControllerUrl = "http://127.0.0.1:8788";
|
|
303
|
+
const defaultHostedControllerUrl = "https://controller.instafy.dev";
|
|
304
|
+
const controllerUrl = explicitControllerUrl ??
|
|
305
|
+
(isStagingCli
|
|
306
|
+
? defaultHostedControllerUrl
|
|
307
|
+
: (await isControllerHealthy(defaultLocalControllerUrl, 250))
|
|
308
|
+
? defaultLocalControllerUrl
|
|
309
|
+
: defaultHostedControllerUrl);
|
|
41
310
|
const studioUrl = normalizeUrl(options.studioUrl ?? null) ??
|
|
42
311
|
normalizeUrl(process.env["INSTAFY_STUDIO_URL"] ?? null) ??
|
|
43
|
-
resolveConfiguredStudioUrl() ??
|
|
44
|
-
|
|
312
|
+
(isStagingCli ? null : resolveConfiguredStudioUrl({ profile })) ??
|
|
313
|
+
(await resolveDefaultStudioUrl(controllerUrl));
|
|
45
314
|
const url = new URL("/cli/login", studioUrl);
|
|
46
315
|
url.searchParams.set("serverUrl", controllerUrl);
|
|
47
316
|
if (options.json) {
|
|
48
|
-
console.log(JSON.stringify({
|
|
317
|
+
console.log(JSON.stringify({
|
|
318
|
+
url: url.toString(),
|
|
319
|
+
profile,
|
|
320
|
+
configPath: profile ? getInstafyProfileConfigPath(profile) : getInstafyConfigPath(),
|
|
321
|
+
}));
|
|
49
322
|
return;
|
|
50
323
|
}
|
|
324
|
+
const existing = resolveUserAccessToken({ profile });
|
|
325
|
+
const provided = normalizeToken(options.token ?? null);
|
|
326
|
+
let authPayload = provided
|
|
327
|
+
? { accessToken: provided, refreshToken: null, supabaseUrl: null, supabaseAnonKey: null }
|
|
328
|
+
: null;
|
|
329
|
+
let usedPasswordGrant = false;
|
|
330
|
+
if (!authPayload) {
|
|
331
|
+
const email = normalizeToken(options.email ?? null) ?? normalizeToken(process.env["INSTAFY_LOGIN_EMAIL"] ?? null);
|
|
332
|
+
const password = normalizeToken(options.password ?? null) ??
|
|
333
|
+
normalizeToken(process.env["INSTAFY_LOGIN_PASSWORD"] ?? null);
|
|
334
|
+
if (email && password) {
|
|
335
|
+
const supabaseUrl = normalizeUrl(process.env["SUPABASE_URL"] ?? null) ??
|
|
336
|
+
normalizeUrl(process.env["VITE_SUPABASE_URL"] ?? null) ??
|
|
337
|
+
normalizeUrl(process.env["SUPABASE_PROJECT_URL"] ?? null);
|
|
338
|
+
const supabaseAnonKey = normalizeToken(process.env["SUPABASE_ANON_KEY"] ?? null) ??
|
|
339
|
+
normalizeToken(process.env["VITE_SUPABASE_ANON_KEY"] ?? null);
|
|
340
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
341
|
+
throw new Error("Email/password login requires Supabase env.\n\nSet:\n- SUPABASE_URL + SUPABASE_ANON_KEY (or VITE_SUPABASE_URL + VITE_SUPABASE_ANON_KEY)\n\nThen retry: instafy login --email <email> --password <password>");
|
|
342
|
+
}
|
|
343
|
+
authPayload = await loginWithPassword({ supabaseUrl, supabaseAnonKey, email, password });
|
|
344
|
+
usedPasswordGrant = true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
let callbackServer = null;
|
|
348
|
+
if (!authPayload) {
|
|
349
|
+
try {
|
|
350
|
+
callbackServer = await startCliLoginCallbackServer();
|
|
351
|
+
url.searchParams.set("cliCallbackUrl", callbackServer.callbackUrl);
|
|
352
|
+
url.searchParams.set("cliState", callbackServer.state);
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
callbackServer = null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
51
358
|
console.log(kleur.green("Instafy CLI login"));
|
|
52
359
|
console.log("");
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
360
|
+
if (!authPayload) {
|
|
361
|
+
console.log("1) Open this URL in your browser:");
|
|
362
|
+
console.log(kleur.cyan(url.toString()));
|
|
363
|
+
console.log("");
|
|
364
|
+
if (callbackServer) {
|
|
365
|
+
console.log("2) Sign in — this terminal should continue automatically.");
|
|
366
|
+
console.log(kleur.gray("If it doesn't, copy the token shown on that page and paste it here."));
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
console.log("2) After you sign in, copy the token shown on that page.");
|
|
370
|
+
}
|
|
371
|
+
console.log("");
|
|
372
|
+
}
|
|
373
|
+
else if (usedPasswordGrant) {
|
|
374
|
+
console.log(kleur.gray("Authenticated via email/password."));
|
|
375
|
+
console.log("");
|
|
376
|
+
}
|
|
377
|
+
if (!authPayload && callbackServer) {
|
|
378
|
+
if (input.isTTY) {
|
|
379
|
+
console.log(kleur.gray("Waiting for browser login…"));
|
|
380
|
+
console.log(kleur.gray("If it doesn't continue, paste the token here and press Enter."));
|
|
381
|
+
console.log("");
|
|
382
|
+
const rl = createInterface({ input, output });
|
|
383
|
+
const abort = new AbortController();
|
|
384
|
+
const manualTokenPromise = (async () => {
|
|
385
|
+
while (true) {
|
|
386
|
+
const answer = await rl.question("Paste token (or wait): ", { signal: abort.signal });
|
|
387
|
+
const candidate = normalizeToken(answer);
|
|
388
|
+
if (!candidate)
|
|
389
|
+
continue;
|
|
390
|
+
if (candidate.startsWith("{")) {
|
|
391
|
+
try {
|
|
392
|
+
const json = JSON.parse(candidate);
|
|
393
|
+
const accessToken = typeof json.accessToken === "string"
|
|
394
|
+
? json.accessToken
|
|
395
|
+
: typeof json.access_token === "string"
|
|
396
|
+
? json.access_token
|
|
397
|
+
: typeof json.token === "string"
|
|
398
|
+
? json.token
|
|
399
|
+
: "";
|
|
400
|
+
if (accessToken.trim()) {
|
|
401
|
+
return {
|
|
402
|
+
accessToken: accessToken.trim(),
|
|
403
|
+
refreshToken: typeof json.refreshToken === "string"
|
|
404
|
+
? json.refreshToken.trim()
|
|
405
|
+
: typeof json.refresh_token === "string"
|
|
406
|
+
? json.refresh_token.trim()
|
|
407
|
+
: null,
|
|
408
|
+
supabaseUrl: typeof json.supabaseUrl === "string" ? json.supabaseUrl.trim() : null,
|
|
409
|
+
supabaseAnonKey: typeof json.supabaseAnonKey === "string"
|
|
410
|
+
? json.supabaseAnonKey.trim()
|
|
411
|
+
: typeof json.supabase_anon_key === "string"
|
|
412
|
+
? json.supabase_anon_key.trim()
|
|
413
|
+
: null,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// ignore and fall back to treating this as an access token
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return { accessToken: candidate, refreshToken: null, supabaseUrl: null, supabaseAnonKey: null };
|
|
422
|
+
}
|
|
423
|
+
})();
|
|
424
|
+
try {
|
|
425
|
+
const result = await Promise.race([
|
|
426
|
+
callbackServer
|
|
427
|
+
.waitForToken(10 * 60000)
|
|
428
|
+
.then((tokenValue) => ({ source: "browser", token: tokenValue })),
|
|
429
|
+
manualTokenPromise.then((tokenValue) => ({ source: "manual", token: tokenValue })),
|
|
430
|
+
]);
|
|
431
|
+
authPayload = result.token;
|
|
432
|
+
if (result.source === "browser") {
|
|
433
|
+
abort.abort();
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
callbackServer.cancel();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
catch (_error) {
|
|
440
|
+
// If browser login fails, keep waiting for a pasted token.
|
|
441
|
+
authPayload = await manualTokenPromise;
|
|
442
|
+
callbackServer.cancel();
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
try {
|
|
446
|
+
rl.close();
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
// ignore
|
|
450
|
+
}
|
|
451
|
+
callbackServer.close();
|
|
452
|
+
callbackServer = null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
try {
|
|
457
|
+
authPayload = await callbackServer.waitForToken(10 * 60000);
|
|
458
|
+
}
|
|
459
|
+
catch (_error) {
|
|
460
|
+
// Ignore and fall back to manual copy/paste if possible.
|
|
461
|
+
}
|
|
462
|
+
finally {
|
|
463
|
+
callbackServer.close();
|
|
464
|
+
callbackServer = null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (!authPayload?.accessToken) {
|
|
469
|
+
if (!input.isTTY) {
|
|
470
|
+
throw new Error("No token provided.");
|
|
471
|
+
}
|
|
62
472
|
const rl = createInterface({ input, output });
|
|
63
473
|
try {
|
|
64
|
-
|
|
474
|
+
const raw = normalizeToken(await rl.question("Paste token: "));
|
|
475
|
+
if (raw) {
|
|
476
|
+
if (raw.startsWith("{")) {
|
|
477
|
+
try {
|
|
478
|
+
const json = JSON.parse(raw);
|
|
479
|
+
const accessToken = typeof json.accessToken === "string"
|
|
480
|
+
? json.accessToken
|
|
481
|
+
: typeof json.access_token === "string"
|
|
482
|
+
? json.access_token
|
|
483
|
+
: typeof json.token === "string"
|
|
484
|
+
? json.token
|
|
485
|
+
: "";
|
|
486
|
+
if (accessToken.trim()) {
|
|
487
|
+
authPayload = {
|
|
488
|
+
accessToken: accessToken.trim(),
|
|
489
|
+
refreshToken: typeof json.refreshToken === "string"
|
|
490
|
+
? json.refreshToken.trim()
|
|
491
|
+
: typeof json.refresh_token === "string"
|
|
492
|
+
? json.refresh_token.trim()
|
|
493
|
+
: null,
|
|
494
|
+
supabaseUrl: typeof json.supabaseUrl === "string" ? json.supabaseUrl.trim() : null,
|
|
495
|
+
supabaseAnonKey: typeof json.supabaseAnonKey === "string"
|
|
496
|
+
? json.supabaseAnonKey.trim()
|
|
497
|
+
: typeof json.supabase_anon_key === "string"
|
|
498
|
+
? json.supabase_anon_key.trim()
|
|
499
|
+
: null,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// ignore and fall through to raw token
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
authPayload ?? (authPayload = { accessToken: raw, refreshToken: null, supabaseUrl: null, supabaseAnonKey: null });
|
|
508
|
+
}
|
|
65
509
|
}
|
|
66
510
|
finally {
|
|
67
511
|
rl.close();
|
|
68
512
|
}
|
|
69
513
|
}
|
|
70
|
-
if (!
|
|
514
|
+
if (!authPayload?.accessToken) {
|
|
71
515
|
throw new Error("No token provided.");
|
|
72
516
|
}
|
|
73
517
|
if (!options.noStore) {
|
|
74
|
-
|
|
518
|
+
const update = {
|
|
519
|
+
controllerUrl,
|
|
520
|
+
studioUrl,
|
|
521
|
+
accessToken: authPayload.accessToken,
|
|
522
|
+
refreshToken: authPayload.refreshToken ?? undefined,
|
|
523
|
+
supabaseUrl: authPayload.supabaseUrl ?? undefined,
|
|
524
|
+
supabaseAnonKey: authPayload.supabaseAnonKey ?? undefined,
|
|
525
|
+
};
|
|
526
|
+
if (profile) {
|
|
527
|
+
writeInstafyProfileConfig(profile, update);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
writeInstafyCliConfig(update);
|
|
531
|
+
}
|
|
75
532
|
console.log("");
|
|
76
|
-
console.log(kleur.green(`Saved token to ${getInstafyConfigPath()}`));
|
|
533
|
+
console.log(kleur.green(`Saved token to ${profile ? getInstafyProfileConfigPath(profile) : getInstafyConfigPath()}`));
|
|
534
|
+
if (options.gitSetup !== false) {
|
|
535
|
+
try {
|
|
536
|
+
const result = installGitCredentialHelper();
|
|
537
|
+
if (result.changed) {
|
|
538
|
+
console.log(kleur.green("Enabled git auth (credential helper installed)."));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
console.log(kleur.yellow(`Warning: failed to configure git credential helper: ${error instanceof Error ? error.message : String(error)}`));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
77
545
|
}
|
|
78
546
|
else if (existing) {
|
|
79
547
|
console.log("");
|
|
@@ -81,14 +549,30 @@ export async function login(options) {
|
|
|
81
549
|
}
|
|
82
550
|
console.log("");
|
|
83
551
|
console.log("Next:");
|
|
84
|
-
console.log(`- ${kleur.cyan("instafy project
|
|
85
|
-
console.log(`- ${kleur.cyan("instafy runtime
|
|
552
|
+
console.log(`- ${kleur.cyan("instafy project init")}`);
|
|
553
|
+
console.log(`- ${kleur.cyan("instafy runtime start")}`);
|
|
86
554
|
}
|
|
87
555
|
export async function logout(options) {
|
|
88
|
-
|
|
556
|
+
if (options?.profile) {
|
|
557
|
+
clearInstafyProfileConfig(options.profile, ["accessToken", "refreshToken", "supabaseUrl", "supabaseAnonKey"]);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
clearInstafyCliConfig(["accessToken", "refreshToken", "supabaseUrl", "supabaseAnonKey"]);
|
|
561
|
+
try {
|
|
562
|
+
uninstallGitCredentialHelper();
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
// ignore git helper cleanup failures
|
|
566
|
+
}
|
|
567
|
+
}
|
|
89
568
|
if (options?.json) {
|
|
90
569
|
console.log(JSON.stringify({ ok: true }));
|
|
91
570
|
return;
|
|
92
571
|
}
|
|
93
|
-
|
|
572
|
+
if (options?.profile) {
|
|
573
|
+
console.log(kleur.green(`Logged out (cleared saved access token for profile "${options.profile}").`));
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
console.log(kleur.green("Logged out (cleared saved access token)."));
|
|
577
|
+
}
|
|
94
578
|
}
|
package/dist/config-command.js
CHANGED
|
@@ -51,6 +51,7 @@ export function configList(options) {
|
|
|
51
51
|
controllerUrl: config.controllerUrl ?? null,
|
|
52
52
|
studioUrl: config.studioUrl ?? null,
|
|
53
53
|
accessTokenSet: Boolean(config.accessToken),
|
|
54
|
+
refreshTokenSet: Boolean(config.refreshToken),
|
|
54
55
|
updatedAt: config.updatedAt ?? null,
|
|
55
56
|
};
|
|
56
57
|
if (options?.json) {
|
|
@@ -62,6 +63,7 @@ export function configList(options) {
|
|
|
62
63
|
console.log(`controller-url: ${payload.controllerUrl ?? kleur.yellow("(not set)")}`);
|
|
63
64
|
console.log(`studio-url: ${payload.studioUrl ?? kleur.yellow("(not set)")}`);
|
|
64
65
|
console.log(`access-token: ${payload.accessTokenSet ? kleur.green("(set)") : kleur.yellow("(not set)")}`);
|
|
66
|
+
console.log(`refresh-token: ${payload.refreshTokenSet ? kleur.green("(set)") : kleur.yellow("(not set)")}`);
|
|
65
67
|
if (payload.updatedAt) {
|
|
66
68
|
console.log(`updated-at: ${payload.updatedAt}`);
|
|
67
69
|
}
|