@instafy/cli 0.1.9 → 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/dist/api.js +41 -16
- package/dist/auth.js +136 -21
- package/dist/config-command.js +2 -0
- package/dist/config.js +37 -0
- package/dist/controller-fetch.js +33 -0
- package/dist/org.js +10 -5
- package/dist/project.js +45 -30
- package/dist/runtime.js +75 -21
- package/dist/supabase-session.js +89 -0
- package/dist/tunnel.js +23 -16
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import { resolveConfiguredAccessToken } from "./config.js";
|
|
2
|
+
import { resolveActiveProfileName, resolveConfiguredAccessToken } from "./config.js";
|
|
3
3
|
import { formatAuthRejectedError } from "./errors.js";
|
|
4
|
+
import { fetchWithControllerAuth } from "./controller-fetch.js";
|
|
4
5
|
function normalizeUrl(raw) {
|
|
5
6
|
const value = (raw ?? "").trim();
|
|
6
7
|
if (!value)
|
|
@@ -13,15 +14,31 @@ function normalizeToken(raw) {
|
|
|
13
14
|
const trimmed = raw.trim();
|
|
14
15
|
return trimmed.length ? trimmed : null;
|
|
15
16
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
function resolveBearerTokenWithSource(options) {
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
const profile = resolveActiveProfileName({ cwd });
|
|
20
|
+
const stored = resolveConfiguredAccessToken({ profile, cwd });
|
|
21
|
+
const explicit = normalizeToken(options.accessToken) ?? normalizeToken(options.serviceToken);
|
|
22
|
+
if (explicit) {
|
|
23
|
+
return { token: explicit, source: "explicit", profile };
|
|
24
|
+
}
|
|
25
|
+
const envKeys = [
|
|
26
|
+
"CONTROLLER_ACCESS_TOKEN",
|
|
27
|
+
"INSTAFY_ACCESS_TOKEN",
|
|
28
|
+
"INSTAFY_SERVICE_TOKEN",
|
|
29
|
+
"CONTROLLER_TOKEN",
|
|
30
|
+
"SUPABASE_ACCESS_TOKEN",
|
|
31
|
+
];
|
|
32
|
+
for (const key of envKeys) {
|
|
33
|
+
const value = normalizeToken(process.env[key]);
|
|
34
|
+
if (value) {
|
|
35
|
+
return { token: value, source: "env", profile };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (stored) {
|
|
39
|
+
return { token: stored, source: "config", profile };
|
|
40
|
+
}
|
|
41
|
+
return { token: null, source: "none", profile };
|
|
25
42
|
}
|
|
26
43
|
function parseKeyValue(raw) {
|
|
27
44
|
const trimmed = raw.trim();
|
|
@@ -99,12 +116,10 @@ function maybePrettyPrintJson(text, pretty) {
|
|
|
99
116
|
}
|
|
100
117
|
export async function requestControllerApi(options) {
|
|
101
118
|
const url = buildRequestUrl(options);
|
|
102
|
-
const
|
|
119
|
+
const resolved = resolveBearerTokenWithSource(options);
|
|
120
|
+
const bearer = resolved.token;
|
|
103
121
|
const headers = new Headers();
|
|
104
122
|
headers.set("accept", "application/json");
|
|
105
|
-
if (bearer) {
|
|
106
|
-
headers.set("authorization", `Bearer ${bearer}`);
|
|
107
|
-
}
|
|
108
123
|
for (const header of options.headers ?? []) {
|
|
109
124
|
const { key, value } = parseHeader(header);
|
|
110
125
|
headers.set(key, value);
|
|
@@ -113,11 +128,21 @@ export async function requestControllerApi(options) {
|
|
|
113
128
|
if (body !== undefined && !headers.has("content-type")) {
|
|
114
129
|
headers.set("content-type", "application/json");
|
|
115
130
|
}
|
|
116
|
-
const
|
|
131
|
+
const init = {
|
|
117
132
|
method: options.method,
|
|
118
133
|
headers,
|
|
119
134
|
body,
|
|
120
|
-
}
|
|
135
|
+
};
|
|
136
|
+
const response = bearer
|
|
137
|
+
? (await fetchWithControllerAuth({
|
|
138
|
+
url: url.toString(),
|
|
139
|
+
init,
|
|
140
|
+
accessToken: bearer,
|
|
141
|
+
tokenSource: resolved.source,
|
|
142
|
+
profile: resolved.profile,
|
|
143
|
+
cwd: process.cwd(),
|
|
144
|
+
})).response
|
|
145
|
+
: await fetch(url, init);
|
|
121
146
|
const responseText = await response.text().catch(() => "");
|
|
122
147
|
const contentType = response.headers.get("content-type") ?? "";
|
|
123
148
|
const isJson = contentType.includes("application/json") || contentType.includes("+json");
|
package/dist/auth.js
CHANGED
|
@@ -53,7 +53,13 @@ async function loginWithPassword(params) {
|
|
|
53
53
|
if (!accessToken) {
|
|
54
54
|
throw new Error("Supabase login response missing access_token");
|
|
55
55
|
}
|
|
56
|
-
|
|
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
|
+
};
|
|
57
63
|
}
|
|
58
64
|
function looksLikeLocalControllerUrl(controllerUrl) {
|
|
59
65
|
try {
|
|
@@ -173,18 +179,47 @@ async function startCliLoginCallbackServer() {
|
|
|
173
179
|
const body = await readRequestBody(request);
|
|
174
180
|
const contentType = typeof request.headers["content-type"] === "string" ? request.headers["content-type"] : "";
|
|
175
181
|
let parsedToken = null;
|
|
182
|
+
let parsedRefreshToken = null;
|
|
183
|
+
let parsedSupabaseUrl = null;
|
|
184
|
+
let parsedSupabaseAnonKey = null;
|
|
176
185
|
let parsedState = null;
|
|
177
186
|
if (contentType.includes("application/json")) {
|
|
178
187
|
const json = JSON.parse(body);
|
|
179
|
-
parsedToken =
|
|
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;
|
|
180
209
|
parsedState = typeof json.state === "string" ? json.state : null;
|
|
181
210
|
}
|
|
182
211
|
else {
|
|
183
212
|
const params = new URLSearchParams(body);
|
|
184
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");
|
|
185
217
|
parsedState = params.get("state");
|
|
186
218
|
}
|
|
187
219
|
const token = normalizeToken(parsedToken);
|
|
220
|
+
const refreshToken = normalizeToken(parsedRefreshToken);
|
|
221
|
+
const supabaseUrl = normalizeUrl(parsedSupabaseUrl);
|
|
222
|
+
const supabaseAnonKey = normalizeToken(parsedSupabaseAnonKey);
|
|
188
223
|
const receivedState = normalizeToken(parsedState);
|
|
189
224
|
if (!token) {
|
|
190
225
|
response.statusCode = 400;
|
|
@@ -202,7 +237,12 @@ async function startCliLoginCallbackServer() {
|
|
|
202
237
|
response.statusCode = 200;
|
|
203
238
|
response.setHeader("content-type", "application/json");
|
|
204
239
|
response.end(JSON.stringify({ ok: true }));
|
|
205
|
-
resolveToken?.(
|
|
240
|
+
resolveToken?.({
|
|
241
|
+
accessToken: token,
|
|
242
|
+
refreshToken,
|
|
243
|
+
supabaseUrl,
|
|
244
|
+
supabaseAnonKey,
|
|
245
|
+
});
|
|
206
246
|
resolveToken = null;
|
|
207
247
|
}
|
|
208
248
|
catch (error) {
|
|
@@ -283,9 +323,11 @@ export async function login(options) {
|
|
|
283
323
|
}
|
|
284
324
|
const existing = resolveUserAccessToken({ profile });
|
|
285
325
|
const provided = normalizeToken(options.token ?? null);
|
|
286
|
-
let
|
|
326
|
+
let authPayload = provided
|
|
327
|
+
? { accessToken: provided, refreshToken: null, supabaseUrl: null, supabaseAnonKey: null }
|
|
328
|
+
: null;
|
|
287
329
|
let usedPasswordGrant = false;
|
|
288
|
-
if (!
|
|
330
|
+
if (!authPayload) {
|
|
289
331
|
const email = normalizeToken(options.email ?? null) ?? normalizeToken(process.env["INSTAFY_LOGIN_EMAIL"] ?? null);
|
|
290
332
|
const password = normalizeToken(options.password ?? null) ??
|
|
291
333
|
normalizeToken(process.env["INSTAFY_LOGIN_PASSWORD"] ?? null);
|
|
@@ -298,12 +340,12 @@ export async function login(options) {
|
|
|
298
340
|
if (!supabaseUrl || !supabaseAnonKey) {
|
|
299
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>");
|
|
300
342
|
}
|
|
301
|
-
|
|
343
|
+
authPayload = await loginWithPassword({ supabaseUrl, supabaseAnonKey, email, password });
|
|
302
344
|
usedPasswordGrant = true;
|
|
303
345
|
}
|
|
304
346
|
}
|
|
305
347
|
let callbackServer = null;
|
|
306
|
-
if (!
|
|
348
|
+
if (!authPayload) {
|
|
307
349
|
try {
|
|
308
350
|
callbackServer = await startCliLoginCallbackServer();
|
|
309
351
|
url.searchParams.set("cliCallbackUrl", callbackServer.callbackUrl);
|
|
@@ -315,7 +357,7 @@ export async function login(options) {
|
|
|
315
357
|
}
|
|
316
358
|
console.log(kleur.green("Instafy CLI login"));
|
|
317
359
|
console.log("");
|
|
318
|
-
if (!
|
|
360
|
+
if (!authPayload) {
|
|
319
361
|
console.log("1) Open this URL in your browser:");
|
|
320
362
|
console.log(kleur.cyan(url.toString()));
|
|
321
363
|
console.log("");
|
|
@@ -332,7 +374,7 @@ export async function login(options) {
|
|
|
332
374
|
console.log(kleur.gray("Authenticated via email/password."));
|
|
333
375
|
console.log("");
|
|
334
376
|
}
|
|
335
|
-
if (!
|
|
377
|
+
if (!authPayload && callbackServer) {
|
|
336
378
|
if (input.isTTY) {
|
|
337
379
|
console.log(kleur.gray("Waiting for browser login…"));
|
|
338
380
|
console.log(kleur.gray("If it doesn't continue, paste the token here and press Enter."));
|
|
@@ -343,9 +385,40 @@ export async function login(options) {
|
|
|
343
385
|
while (true) {
|
|
344
386
|
const answer = await rl.question("Paste token (or wait): ", { signal: abort.signal });
|
|
345
387
|
const candidate = normalizeToken(answer);
|
|
346
|
-
if (candidate)
|
|
347
|
-
|
|
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
|
+
}
|
|
348
420
|
}
|
|
421
|
+
return { accessToken: candidate, refreshToken: null, supabaseUrl: null, supabaseAnonKey: null };
|
|
349
422
|
}
|
|
350
423
|
})();
|
|
351
424
|
try {
|
|
@@ -355,7 +428,7 @@ export async function login(options) {
|
|
|
355
428
|
.then((tokenValue) => ({ source: "browser", token: tokenValue })),
|
|
356
429
|
manualTokenPromise.then((tokenValue) => ({ source: "manual", token: tokenValue })),
|
|
357
430
|
]);
|
|
358
|
-
|
|
431
|
+
authPayload = result.token;
|
|
359
432
|
if (result.source === "browser") {
|
|
360
433
|
abort.abort();
|
|
361
434
|
}
|
|
@@ -365,7 +438,7 @@ export async function login(options) {
|
|
|
365
438
|
}
|
|
366
439
|
catch (_error) {
|
|
367
440
|
// If browser login fails, keep waiting for a pasted token.
|
|
368
|
-
|
|
441
|
+
authPayload = await manualTokenPromise;
|
|
369
442
|
callbackServer.cancel();
|
|
370
443
|
}
|
|
371
444
|
finally {
|
|
@@ -381,7 +454,7 @@ export async function login(options) {
|
|
|
381
454
|
}
|
|
382
455
|
else {
|
|
383
456
|
try {
|
|
384
|
-
|
|
457
|
+
authPayload = await callbackServer.waitForToken(10 * 60000);
|
|
385
458
|
}
|
|
386
459
|
catch (_error) {
|
|
387
460
|
// Ignore and fall back to manual copy/paste if possible.
|
|
@@ -392,27 +465,69 @@ export async function login(options) {
|
|
|
392
465
|
}
|
|
393
466
|
}
|
|
394
467
|
}
|
|
395
|
-
if (!
|
|
468
|
+
if (!authPayload?.accessToken) {
|
|
396
469
|
if (!input.isTTY) {
|
|
397
470
|
throw new Error("No token provided.");
|
|
398
471
|
}
|
|
399
472
|
const rl = createInterface({ input, output });
|
|
400
473
|
try {
|
|
401
|
-
|
|
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
|
+
}
|
|
402
509
|
}
|
|
403
510
|
finally {
|
|
404
511
|
rl.close();
|
|
405
512
|
}
|
|
406
513
|
}
|
|
407
|
-
if (!
|
|
514
|
+
if (!authPayload?.accessToken) {
|
|
408
515
|
throw new Error("No token provided.");
|
|
409
516
|
}
|
|
410
517
|
if (!options.noStore) {
|
|
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
|
+
};
|
|
411
526
|
if (profile) {
|
|
412
|
-
writeInstafyProfileConfig(profile,
|
|
527
|
+
writeInstafyProfileConfig(profile, update);
|
|
413
528
|
}
|
|
414
529
|
else {
|
|
415
|
-
writeInstafyCliConfig(
|
|
530
|
+
writeInstafyCliConfig(update);
|
|
416
531
|
}
|
|
417
532
|
console.log("");
|
|
418
533
|
console.log(kleur.green(`Saved token to ${profile ? getInstafyProfileConfigPath(profile) : getInstafyConfigPath()}`));
|
|
@@ -439,10 +554,10 @@ export async function login(options) {
|
|
|
439
554
|
}
|
|
440
555
|
export async function logout(options) {
|
|
441
556
|
if (options?.profile) {
|
|
442
|
-
clearInstafyProfileConfig(options.profile, ["accessToken"]);
|
|
557
|
+
clearInstafyProfileConfig(options.profile, ["accessToken", "refreshToken", "supabaseUrl", "supabaseAnonKey"]);
|
|
443
558
|
}
|
|
444
559
|
else {
|
|
445
|
-
clearInstafyCliConfig(["accessToken"]);
|
|
560
|
+
clearInstafyCliConfig(["accessToken", "refreshToken", "supabaseUrl", "supabaseAnonKey"]);
|
|
446
561
|
try {
|
|
447
562
|
uninstallGitCredentialHelper();
|
|
448
563
|
}
|
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
|
}
|
package/dist/config.js
CHANGED
|
@@ -92,6 +92,9 @@ export function readInstafyCliConfig() {
|
|
|
92
92
|
controllerUrl: normalizeUrl(typeof record.controllerUrl === "string" ? record.controllerUrl : null),
|
|
93
93
|
studioUrl: normalizeUrl(typeof record.studioUrl === "string" ? record.studioUrl : null),
|
|
94
94
|
accessToken: normalizeToken(typeof record.accessToken === "string" ? record.accessToken : null),
|
|
95
|
+
refreshToken: normalizeToken(typeof record.refreshToken === "string" ? record.refreshToken : null),
|
|
96
|
+
supabaseUrl: normalizeUrl(typeof record.supabaseUrl === "string" ? record.supabaseUrl : null),
|
|
97
|
+
supabaseAnonKey: normalizeToken(typeof record.supabaseAnonKey === "string" ? record.supabaseAnonKey : null),
|
|
95
98
|
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : null,
|
|
96
99
|
};
|
|
97
100
|
}
|
|
@@ -112,6 +115,9 @@ export function readInstafyProfileConfig(profile) {
|
|
|
112
115
|
controllerUrl: normalizeUrl(typeof record.controllerUrl === "string" ? record.controllerUrl : null),
|
|
113
116
|
studioUrl: normalizeUrl(typeof record.studioUrl === "string" ? record.studioUrl : null),
|
|
114
117
|
accessToken: normalizeToken(typeof record.accessToken === "string" ? record.accessToken : null),
|
|
118
|
+
refreshToken: normalizeToken(typeof record.refreshToken === "string" ? record.refreshToken : null),
|
|
119
|
+
supabaseUrl: normalizeUrl(typeof record.supabaseUrl === "string" ? record.supabaseUrl : null),
|
|
120
|
+
supabaseAnonKey: normalizeToken(typeof record.supabaseAnonKey === "string" ? record.supabaseAnonKey : null),
|
|
115
121
|
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : null,
|
|
116
122
|
};
|
|
117
123
|
}
|
|
@@ -125,6 +131,9 @@ export function writeInstafyCliConfig(update) {
|
|
|
125
131
|
controllerUrl: normalizeUrl(update.controllerUrl ?? existing.controllerUrl ?? null),
|
|
126
132
|
studioUrl: normalizeUrl(update.studioUrl ?? existing.studioUrl ?? null),
|
|
127
133
|
accessToken: normalizeToken(update.accessToken ?? existing.accessToken ?? null),
|
|
134
|
+
refreshToken: normalizeToken(update.refreshToken ?? existing.refreshToken ?? null),
|
|
135
|
+
supabaseUrl: normalizeUrl(update.supabaseUrl ?? existing.supabaseUrl ?? null),
|
|
136
|
+
supabaseAnonKey: normalizeToken(update.supabaseAnonKey ?? existing.supabaseAnonKey ?? null),
|
|
128
137
|
updatedAt: new Date().toISOString(),
|
|
129
138
|
};
|
|
130
139
|
fs.mkdirSync(INSTAFY_DIR, { recursive: true });
|
|
@@ -144,6 +153,9 @@ export function writeInstafyProfileConfig(profile, update) {
|
|
|
144
153
|
controllerUrl: normalizeUrl(update.controllerUrl ?? existing.controllerUrl ?? null),
|
|
145
154
|
studioUrl: normalizeUrl(update.studioUrl ?? existing.studioUrl ?? null),
|
|
146
155
|
accessToken: normalizeToken(update.accessToken ?? existing.accessToken ?? null),
|
|
156
|
+
refreshToken: normalizeToken(update.refreshToken ?? existing.refreshToken ?? null),
|
|
157
|
+
supabaseUrl: normalizeUrl(update.supabaseUrl ?? existing.supabaseUrl ?? null),
|
|
158
|
+
supabaseAnonKey: normalizeToken(update.supabaseAnonKey ?? existing.supabaseAnonKey ?? null),
|
|
147
159
|
updatedAt: new Date().toISOString(),
|
|
148
160
|
};
|
|
149
161
|
fs.mkdirSync(PROFILES_DIR, { recursive: true });
|
|
@@ -253,3 +265,28 @@ export function resolveUserAccessToken(params) {
|
|
|
253
265
|
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"] ?? null) ??
|
|
254
266
|
normalizeToken(config.accessToken ?? null));
|
|
255
267
|
}
|
|
268
|
+
export function resolveUserAccessTokenWithSource(params) {
|
|
269
|
+
const profile = resolveActiveProfileName({ profile: params?.profile ?? null, cwd: params?.cwd ?? null });
|
|
270
|
+
const config = profile ? readInstafyProfileConfig(profile) : readInstafyCliConfig();
|
|
271
|
+
const explicit = normalizeToken(params?.accessToken ?? null);
|
|
272
|
+
if (explicit) {
|
|
273
|
+
return { token: explicit, source: "explicit", profile };
|
|
274
|
+
}
|
|
275
|
+
const envKeys = [
|
|
276
|
+
"INSTAFY_ACCESS_TOKEN",
|
|
277
|
+
"CONTROLLER_ACCESS_TOKEN",
|
|
278
|
+
"RUNTIME_ACCESS_TOKEN",
|
|
279
|
+
"SUPABASE_ACCESS_TOKEN",
|
|
280
|
+
];
|
|
281
|
+
for (const key of envKeys) {
|
|
282
|
+
const value = normalizeToken(process.env[key] ?? null);
|
|
283
|
+
if (value) {
|
|
284
|
+
return { token: value, source: "env", profile };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const stored = normalizeToken(config.accessToken ?? null);
|
|
288
|
+
if (stored) {
|
|
289
|
+
return { token: stored, source: "config", profile };
|
|
290
|
+
}
|
|
291
|
+
return { token: null, source: "none", profile };
|
|
292
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { extractControllerErrorMessage } from "./errors.js";
|
|
2
|
+
import { refreshStoredSupabaseSession } from "./supabase-session.js";
|
|
3
|
+
function shouldAttemptRefresh(status, body) {
|
|
4
|
+
if (status !== 401) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
const message = extractControllerErrorMessage(body) ?? "";
|
|
8
|
+
return message.toLowerCase().includes("expired");
|
|
9
|
+
}
|
|
10
|
+
function withBearer(init, token) {
|
|
11
|
+
const headers = new Headers(init?.headers);
|
|
12
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
13
|
+
return { ...init, headers };
|
|
14
|
+
}
|
|
15
|
+
export async function fetchWithControllerAuth(params) {
|
|
16
|
+
const response = await fetch(params.url, withBearer(params.init, params.accessToken));
|
|
17
|
+
if (response.ok) {
|
|
18
|
+
return { response, accessToken: params.accessToken };
|
|
19
|
+
}
|
|
20
|
+
if (params.tokenSource !== "config") {
|
|
21
|
+
return { response, accessToken: params.accessToken };
|
|
22
|
+
}
|
|
23
|
+
const responseText = await response.clone().text().catch(() => "");
|
|
24
|
+
if (!shouldAttemptRefresh(response.status, responseText)) {
|
|
25
|
+
return { response, accessToken: params.accessToken };
|
|
26
|
+
}
|
|
27
|
+
const refreshed = await refreshStoredSupabaseSession({ profile: params.profile ?? null, cwd: params.cwd ?? null });
|
|
28
|
+
if (!refreshed?.accessToken) {
|
|
29
|
+
return { response, accessToken: params.accessToken };
|
|
30
|
+
}
|
|
31
|
+
const retry = await fetch(params.url, withBearer(params.init, refreshed.accessToken));
|
|
32
|
+
return { response: retry, accessToken: refreshed.accessToken };
|
|
33
|
+
}
|
package/dist/org.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import kleur from "kleur";
|
|
2
|
-
import { resolveControllerUrl,
|
|
2
|
+
import { resolveControllerUrl, resolveUserAccessTokenWithSource } from "./config.js";
|
|
3
3
|
import { formatAuthRejectedError, formatAuthRequiredError } from "./errors.js";
|
|
4
|
+
import { fetchWithControllerAuth } from "./controller-fetch.js";
|
|
4
5
|
export async function listOrganizations(params) {
|
|
5
6
|
const controllerUrl = resolveControllerUrl({ controllerUrl: params.controllerUrl ?? null });
|
|
6
|
-
const
|
|
7
|
-
if (!token) {
|
|
7
|
+
const resolved = resolveUserAccessTokenWithSource({ accessToken: params.accessToken ?? null });
|
|
8
|
+
if (!resolved.token) {
|
|
8
9
|
throw formatAuthRequiredError({ retryCommand: "instafy org list" });
|
|
9
10
|
}
|
|
10
|
-
const response = await
|
|
11
|
-
|
|
11
|
+
const { response } = await fetchWithControllerAuth({
|
|
12
|
+
url: `${controllerUrl}/orgs`,
|
|
13
|
+
accessToken: resolved.token,
|
|
14
|
+
tokenSource: resolved.source,
|
|
15
|
+
profile: resolved.profile,
|
|
16
|
+
cwd: process.cwd(),
|
|
12
17
|
});
|
|
13
18
|
if (!response.ok) {
|
|
14
19
|
const text = await response.text().catch(() => "");
|
package/dist/project.js
CHANGED
|
@@ -2,20 +2,29 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import kleur from "kleur";
|
|
4
4
|
import { stdin as input } from "node:process";
|
|
5
|
-
import { getInstafyProfileConfigPath, resolveConfiguredStudioUrl, resolveControllerUrl,
|
|
5
|
+
import { getInstafyProfileConfigPath, resolveConfiguredStudioUrl, resolveControllerUrl, resolveUserAccessTokenWithSource, } from "./config.js";
|
|
6
6
|
import { formatAuthRejectedError, formatAuthRequiredError } from "./errors.js";
|
|
7
7
|
import { findProjectManifest } from "./project-manifest.js";
|
|
8
|
+
import { fetchWithControllerAuth } from "./controller-fetch.js";
|
|
8
9
|
let promptsModule = null;
|
|
9
10
|
async function loadPrompts() {
|
|
10
11
|
promptsModule ?? (promptsModule = import("@clack/prompts"));
|
|
11
12
|
return promptsModule;
|
|
12
13
|
}
|
|
13
|
-
async function
|
|
14
|
-
const response = await
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
async function controllerFetch(controllerUrl, auth, pathname, init) {
|
|
15
|
+
const { response, accessToken } = await fetchWithControllerAuth({
|
|
16
|
+
url: `${controllerUrl}${pathname}`,
|
|
17
|
+
init,
|
|
18
|
+
accessToken: auth.accessToken,
|
|
19
|
+
tokenSource: auth.tokenSource,
|
|
20
|
+
profile: auth.profile,
|
|
21
|
+
cwd: auth.cwd,
|
|
18
22
|
});
|
|
23
|
+
auth.accessToken = accessToken;
|
|
24
|
+
return response;
|
|
25
|
+
}
|
|
26
|
+
async function fetchOrganizations(controllerUrl, auth, retryCommand) {
|
|
27
|
+
const response = await controllerFetch(controllerUrl, auth, "/orgs");
|
|
19
28
|
if (!response.ok) {
|
|
20
29
|
const text = await response.text().catch(() => "");
|
|
21
30
|
if (response.status === 401 || response.status === 403) {
|
|
@@ -30,12 +39,8 @@ async function fetchOrganizations(controllerUrl, token, retryCommand) {
|
|
|
30
39
|
const body = (await response.json());
|
|
31
40
|
return Array.isArray(body.orgs) ? body.orgs : [];
|
|
32
41
|
}
|
|
33
|
-
async function fetchOrgProjects(controllerUrl,
|
|
34
|
-
const response = await
|
|
35
|
-
headers: {
|
|
36
|
-
authorization: `Bearer ${token}`,
|
|
37
|
-
},
|
|
38
|
-
});
|
|
42
|
+
async function fetchOrgProjects(controllerUrl, auth, orgId, retryCommand) {
|
|
43
|
+
const response = await controllerFetch(controllerUrl, auth, `/orgs/${encodeURIComponent(orgId)}/projects`);
|
|
39
44
|
if (!response.ok) {
|
|
40
45
|
const text = await response.text().catch(() => "");
|
|
41
46
|
if (response.status === 401 || response.status === 403) {
|
|
@@ -50,11 +55,10 @@ async function fetchOrgProjects(controllerUrl, token, orgId, retryCommand) {
|
|
|
50
55
|
const body = (await response.json());
|
|
51
56
|
return Array.isArray(body.projects) ? body.projects : [];
|
|
52
57
|
}
|
|
53
|
-
async function createOrganization(controllerUrl,
|
|
54
|
-
const response = await
|
|
58
|
+
async function createOrganization(controllerUrl, auth, payload, retryCommand) {
|
|
59
|
+
const response = await controllerFetch(controllerUrl, auth, "/orgs", {
|
|
55
60
|
method: "POST",
|
|
56
61
|
headers: {
|
|
57
|
-
authorization: `Bearer ${token}`,
|
|
58
62
|
"content-type": "application/json",
|
|
59
63
|
},
|
|
60
64
|
body: JSON.stringify(payload),
|
|
@@ -77,7 +81,7 @@ async function createOrganization(controllerUrl, token, payload, retryCommand) {
|
|
|
77
81
|
}
|
|
78
82
|
return { orgId, orgName: json.org_name ?? json.orgName ?? null };
|
|
79
83
|
}
|
|
80
|
-
async function resolveOrg(controllerUrl,
|
|
84
|
+
async function resolveOrg(controllerUrl, auth, options, retryCommand) {
|
|
81
85
|
if (options.orgId) {
|
|
82
86
|
return { orgId: options.orgId, orgName: options.orgName ?? null };
|
|
83
87
|
}
|
|
@@ -112,18 +116,18 @@ async function resolveOrg(controllerUrl, token, options, retryCommand) {
|
|
|
112
116
|
if (chosenSlug) {
|
|
113
117
|
payload.orgSlug = chosenSlug;
|
|
114
118
|
}
|
|
115
|
-
const created = await createOrganization(controllerUrl,
|
|
119
|
+
const created = await createOrganization(controllerUrl, auth, payload, retryCommand);
|
|
116
120
|
return { orgId: created.orgId, orgName: created.orgName ?? chosenName };
|
|
117
121
|
}
|
|
118
122
|
if (orgSlug) {
|
|
119
|
-
const orgs = await fetchOrganizations(controllerUrl,
|
|
123
|
+
const orgs = await fetchOrganizations(controllerUrl, auth, retryCommand);
|
|
120
124
|
const matches = orgs.filter((org) => org.slug === orgSlug);
|
|
121
125
|
if (matches.length === 1) {
|
|
122
126
|
return { orgId: matches[0].id, orgName: matches[0].name ?? null };
|
|
123
127
|
}
|
|
124
128
|
}
|
|
125
129
|
if (!orgSlug && !orgName) {
|
|
126
|
-
const orgs = await fetchOrganizations(controllerUrl,
|
|
130
|
+
const orgs = await fetchOrganizations(controllerUrl, auth, retryCommand);
|
|
127
131
|
if (orgs.length === 0) {
|
|
128
132
|
if (allowInteractive) {
|
|
129
133
|
const { confirm, isCancel } = await loadPrompts();
|
|
@@ -191,7 +195,7 @@ async function resolveOrg(controllerUrl, token, options, retryCommand) {
|
|
|
191
195
|
if (options.ownerUserId) {
|
|
192
196
|
payload.ownerUserId = options.ownerUserId;
|
|
193
197
|
}
|
|
194
|
-
const created = await createOrganization(controllerUrl,
|
|
198
|
+
const created = await createOrganization(controllerUrl, auth, payload, retryCommand);
|
|
195
199
|
return { orgId: created.orgId, orgName: created.orgName ?? chosenName };
|
|
196
200
|
}
|
|
197
201
|
throw new Error(`Organization slug "${orgSlug}" did not match an existing org, and org name is required to create one.\n\nNext:\n- Create an org in Studio: ${studioOrgUrl}\n- Or rerun: instafy project init --org-name \"My Org\" --org-slug "${orgSlug}"`);
|
|
@@ -209,17 +213,23 @@ async function resolveOrg(controllerUrl, token, options, retryCommand) {
|
|
|
209
213
|
if (!payload.orgName) {
|
|
210
214
|
throw new Error(`Organization name is required.\n\nNext:\n- Create an org in Studio: ${studioOrgUrl}\n- Or rerun: instafy project init --org-name \"My Org\"`);
|
|
211
215
|
}
|
|
212
|
-
const created = await createOrganization(controllerUrl,
|
|
216
|
+
const created = await createOrganization(controllerUrl, auth, payload, retryCommand);
|
|
213
217
|
return { orgId: created.orgId, orgName: created.orgName ?? orgName ?? null };
|
|
214
218
|
}
|
|
215
219
|
export async function listProjects(options) {
|
|
216
220
|
const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
|
|
217
|
-
const
|
|
218
|
-
if (!token) {
|
|
221
|
+
const resolved = resolveUserAccessTokenWithSource({ accessToken: options.accessToken ?? null });
|
|
222
|
+
if (!resolved.token) {
|
|
219
223
|
throw formatAuthRequiredError({ retryCommand: "instafy project list" });
|
|
220
224
|
}
|
|
221
225
|
const retryCommand = "instafy project list";
|
|
222
|
-
const
|
|
226
|
+
const auth = {
|
|
227
|
+
accessToken: resolved.token,
|
|
228
|
+
tokenSource: resolved.source,
|
|
229
|
+
profile: resolved.profile,
|
|
230
|
+
cwd: process.cwd(),
|
|
231
|
+
};
|
|
232
|
+
const orgs = await fetchOrganizations(controllerUrl, auth, retryCommand);
|
|
223
233
|
let targetOrgs = orgs;
|
|
224
234
|
if (options.orgId) {
|
|
225
235
|
targetOrgs = orgs.filter((org) => org.id === options.orgId);
|
|
@@ -235,7 +245,7 @@ export async function listProjects(options) {
|
|
|
235
245
|
}
|
|
236
246
|
const summaries = [];
|
|
237
247
|
for (const org of targetOrgs) {
|
|
238
|
-
const projects = await fetchOrgProjects(controllerUrl,
|
|
248
|
+
const projects = await fetchOrgProjects(controllerUrl, auth, org.id, retryCommand);
|
|
239
249
|
summaries.push({ org, projects });
|
|
240
250
|
}
|
|
241
251
|
if (options.json) {
|
|
@@ -267,27 +277,32 @@ export async function projectInit(options) {
|
|
|
267
277
|
profile: options.profile ?? null,
|
|
268
278
|
cwd: rootDir,
|
|
269
279
|
});
|
|
270
|
-
const
|
|
280
|
+
const resolved = resolveUserAccessTokenWithSource({
|
|
271
281
|
accessToken: options.accessToken ?? null,
|
|
272
282
|
profile: options.profile ?? null,
|
|
273
283
|
cwd: rootDir,
|
|
274
284
|
});
|
|
275
|
-
if (!token) {
|
|
285
|
+
if (!resolved.token) {
|
|
276
286
|
throw formatAuthRequiredError({
|
|
277
287
|
retryCommand: "instafy project init",
|
|
278
288
|
advancedHint: "pass --access-token or set INSTAFY_ACCESS_TOKEN / SUPABASE_ACCESS_TOKEN",
|
|
279
289
|
});
|
|
280
290
|
}
|
|
281
291
|
const retryCommand = "instafy project init";
|
|
282
|
-
const
|
|
292
|
+
const auth = {
|
|
293
|
+
accessToken: resolved.token,
|
|
294
|
+
tokenSource: resolved.source,
|
|
295
|
+
profile: resolved.profile,
|
|
296
|
+
cwd: rootDir,
|
|
297
|
+
};
|
|
298
|
+
const org = await resolveOrg(controllerUrl, auth, options, retryCommand);
|
|
283
299
|
const body = {
|
|
284
300
|
projectType: options.projectType,
|
|
285
301
|
ownerUserId: options.ownerUserId,
|
|
286
302
|
};
|
|
287
|
-
const response = await
|
|
303
|
+
const response = await controllerFetch(controllerUrl, auth, `/orgs/${encodeURIComponent(org.orgId)}/projects`, {
|
|
288
304
|
method: "POST",
|
|
289
305
|
headers: {
|
|
290
|
-
authorization: `Bearer ${token}`,
|
|
291
306
|
"content-type": "application/json",
|
|
292
307
|
},
|
|
293
308
|
body: JSON.stringify(body),
|
package/dist/runtime.js
CHANGED
|
@@ -6,9 +6,10 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
import { ensureRatholeBinary } from "./rathole.js";
|
|
9
|
-
import { resolveConfiguredAccessToken } from "./config.js";
|
|
9
|
+
import { resolveActiveProfileName, resolveConfiguredAccessToken } from "./config.js";
|
|
10
10
|
import { formatAuthRejectedError } from "./errors.js";
|
|
11
11
|
import { findProjectManifest } from "./project-manifest.js";
|
|
12
|
+
import { fetchWithControllerAuth } from "./controller-fetch.js";
|
|
12
13
|
const INSTAFY_DIR = path.join(os.homedir(), ".instafy");
|
|
13
14
|
const STATE_FILE = path.join(INSTAFY_DIR, "cli-runtime-state.json");
|
|
14
15
|
const LOG_DIR = path.join(INSTAFY_DIR, "cli-runtime-logs");
|
|
@@ -188,13 +189,28 @@ function findRatholeOnPath() {
|
|
|
188
189
|
}
|
|
189
190
|
return null;
|
|
190
191
|
}
|
|
191
|
-
function
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
192
|
+
function resolveControllerAccessTokenForCliWithSource(options, env, supabaseAccessToken, profile, cwd) {
|
|
193
|
+
const explicit = normalizeToken(options.controllerAccessToken);
|
|
194
|
+
if (explicit) {
|
|
195
|
+
return { token: explicit, source: "explicit" };
|
|
196
|
+
}
|
|
197
|
+
const envToken = normalizeToken(env["INSTAFY_ACCESS_TOKEN"]) ??
|
|
198
|
+
normalizeToken(env["CONTROLLER_ACCESS_TOKEN"]);
|
|
199
|
+
if (envToken) {
|
|
200
|
+
return { token: envToken, source: "env" };
|
|
201
|
+
}
|
|
202
|
+
if (supabaseAccessToken) {
|
|
203
|
+
return { token: supabaseAccessToken, source: "explicit" };
|
|
204
|
+
}
|
|
205
|
+
const supabaseEnvToken = normalizeToken(env["SUPABASE_ACCESS_TOKEN"]);
|
|
206
|
+
if (supabaseEnvToken) {
|
|
207
|
+
return { token: supabaseEnvToken, source: "env" };
|
|
208
|
+
}
|
|
209
|
+
const stored = resolveConfiguredAccessToken({ profile, cwd });
|
|
210
|
+
if (stored) {
|
|
211
|
+
return { token: stored, source: "config" };
|
|
212
|
+
}
|
|
213
|
+
return { token: null, source: "none" };
|
|
198
214
|
}
|
|
199
215
|
export async function resolveRatholeBinaryForCli(options) {
|
|
200
216
|
const warn = options.warn ??
|
|
@@ -245,16 +261,22 @@ export async function resolveRatholeBinaryForCli(options) {
|
|
|
245
261
|
export async function mintRuntimeAccessToken(params) {
|
|
246
262
|
const url = params.controllerUrl.replace(/\/$/, "");
|
|
247
263
|
const target = `${url}/projects/${encodeURIComponent(params.projectId)}/runtime/token`;
|
|
248
|
-
const response = await
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
264
|
+
const { response } = await fetchWithControllerAuth({
|
|
265
|
+
url: target,
|
|
266
|
+
init: {
|
|
267
|
+
method: "POST",
|
|
268
|
+
headers: {
|
|
269
|
+
"content-type": "application/json",
|
|
270
|
+
},
|
|
271
|
+
body: JSON.stringify({
|
|
272
|
+
runtimeId: params.runtimeId,
|
|
273
|
+
scopes: params.scopes,
|
|
274
|
+
}),
|
|
253
275
|
},
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
276
|
+
accessToken: params.controllerAccessToken,
|
|
277
|
+
tokenSource: params.tokenSource ?? "env",
|
|
278
|
+
profile: params.profile ?? null,
|
|
279
|
+
cwd: params.cwd ?? null,
|
|
258
280
|
});
|
|
259
281
|
if (!response.ok) {
|
|
260
282
|
const text = await response.text().catch(() => "");
|
|
@@ -284,6 +306,7 @@ function isProcessAlive(pid) {
|
|
|
284
306
|
}
|
|
285
307
|
export async function runtimeStart(options) {
|
|
286
308
|
const env = { ...process.env };
|
|
309
|
+
const cwd = process.cwd();
|
|
287
310
|
const existing = readState();
|
|
288
311
|
if (existing) {
|
|
289
312
|
if (existing.runner === "docker" && existing.containerId && dockerContainerRunning(existing.containerId)) {
|
|
@@ -293,7 +316,7 @@ export async function runtimeStart(options) {
|
|
|
293
316
|
throw new Error(`Runtime already running (pid ${existing.pid}) for project ${existing.projectId}. Stop it first.`);
|
|
294
317
|
}
|
|
295
318
|
}
|
|
296
|
-
const manifestInfo = findProjectManifest(
|
|
319
|
+
const manifestInfo = findProjectManifest(cwd);
|
|
297
320
|
const projectId = options.project ?? env["PROJECT_ID"] ?? manifestInfo.manifest?.projectId ?? null;
|
|
298
321
|
if (!projectId) {
|
|
299
322
|
throw new Error("No project configured. Run `instafy project init` in this folder (recommended) or pass --project.");
|
|
@@ -303,7 +326,10 @@ export async function runtimeStart(options) {
|
|
|
303
326
|
if (supabaseAccessToken) {
|
|
304
327
|
env["SUPABASE_ACCESS_TOKEN"] = supabaseAccessToken;
|
|
305
328
|
}
|
|
306
|
-
|
|
329
|
+
const profile = resolveActiveProfileName({ cwd });
|
|
330
|
+
const controllerAccessTokenResult = resolveControllerAccessTokenForCliWithSource(options, env, supabaseAccessToken, profile, cwd);
|
|
331
|
+
let controllerAccessToken = controllerAccessTokenResult.token;
|
|
332
|
+
const controllerAccessTokenSource = controllerAccessTokenResult.source;
|
|
307
333
|
let runtimeAccessToken = normalizeToken(options.runtimeToken) ?? normalizeToken(env["RUNTIME_ACCESS_TOKEN"]);
|
|
308
334
|
const agentKey = options.controllerToken ??
|
|
309
335
|
env["INSTAFY_SERVICE_TOKEN"] ??
|
|
@@ -329,6 +355,9 @@ export async function runtimeStart(options) {
|
|
|
329
355
|
controllerUrl: env["CONTROLLER_BASE_URL"],
|
|
330
356
|
controllerAccessToken,
|
|
331
357
|
projectId,
|
|
358
|
+
tokenSource: controllerAccessTokenSource,
|
|
359
|
+
profile,
|
|
360
|
+
cwd,
|
|
332
361
|
});
|
|
333
362
|
}
|
|
334
363
|
if (runtimeAccessToken) {
|
|
@@ -362,6 +391,9 @@ export async function runtimeStart(options) {
|
|
|
362
391
|
controllerAccessToken,
|
|
363
392
|
projectId,
|
|
364
393
|
runtimeId: env["RUNTIME_ID"],
|
|
394
|
+
tokenSource: controllerAccessTokenSource,
|
|
395
|
+
profile,
|
|
396
|
+
cwd,
|
|
365
397
|
});
|
|
366
398
|
}
|
|
367
399
|
if (!originToken) {
|
|
@@ -500,7 +532,14 @@ export async function runtimeStart(options) {
|
|
|
500
532
|
const started = spawnSync("docker", runArgs, { encoding: "utf8" });
|
|
501
533
|
if (started.status !== 0) {
|
|
502
534
|
const stderr = String(started.stderr ?? "").trim();
|
|
503
|
-
|
|
535
|
+
const normalized = (stderr || "").toLowerCase();
|
|
536
|
+
const hints = [];
|
|
537
|
+
if (image.startsWith("ghcr.io/") && (normalized.includes("not found") || normalized.includes("denied") || normalized.includes("unauthorized") || normalized.includes("manifest unknown"))) {
|
|
538
|
+
hints.push("If this is a GHCR image, ensure it's published and you are authenticated (`docker login ghcr.io`).");
|
|
539
|
+
}
|
|
540
|
+
hints.push("Override the image with `INSTAFY_RUNTIME_AGENT_IMAGE=... instafy runtime start`.");
|
|
541
|
+
const suffix = hints.length > 0 ? `\n\nNext:\n- ${hints.join("\n- ")}` : "";
|
|
542
|
+
throw new Error(`docker run failed: ${stderr || "unknown error"}${suffix}`);
|
|
504
543
|
}
|
|
505
544
|
const containerId = String(started.stdout ?? "").trim();
|
|
506
545
|
if (!containerId) {
|
|
@@ -740,15 +779,27 @@ export async function runtimeStop(options) {
|
|
|
740
779
|
}
|
|
741
780
|
}
|
|
742
781
|
export async function runtimeToken(options) {
|
|
782
|
+
const cwd = process.cwd();
|
|
743
783
|
const controllerUrl = options.controllerUrl ??
|
|
744
784
|
process.env["INSTAFY_SERVER_URL"] ??
|
|
745
785
|
process.env["CONTROLLER_BASE_URL"] ??
|
|
746
786
|
"http://127.0.0.1:8788";
|
|
787
|
+
const profile = resolveActiveProfileName({ cwd });
|
|
788
|
+
const stored = resolveConfiguredAccessToken({ profile, cwd });
|
|
789
|
+
const tokenSource = options.controllerAccessToken
|
|
790
|
+
? "explicit"
|
|
791
|
+
: process.env["INSTAFY_ACCESS_TOKEN"] ||
|
|
792
|
+
process.env["CONTROLLER_ACCESS_TOKEN"] ||
|
|
793
|
+
process.env["SUPABASE_ACCESS_TOKEN"]
|
|
794
|
+
? "env"
|
|
795
|
+
: stored
|
|
796
|
+
? "config"
|
|
797
|
+
: "none";
|
|
747
798
|
const token = options.controllerAccessToken ??
|
|
748
799
|
process.env["INSTAFY_ACCESS_TOKEN"] ??
|
|
749
800
|
process.env["CONTROLLER_ACCESS_TOKEN"] ??
|
|
750
801
|
process.env["SUPABASE_ACCESS_TOKEN"] ??
|
|
751
|
-
|
|
802
|
+
stored;
|
|
752
803
|
if (!token) {
|
|
753
804
|
throw new Error("Login required. Run `instafy login` or pass --access-token / set SUPABASE_ACCESS_TOKEN.");
|
|
754
805
|
}
|
|
@@ -758,6 +809,9 @@ export async function runtimeToken(options) {
|
|
|
758
809
|
projectId: options.project,
|
|
759
810
|
runtimeId: options.runtimeId,
|
|
760
811
|
scopes: options.scopes,
|
|
812
|
+
tokenSource: tokenSource === "none" ? "env" : tokenSource,
|
|
813
|
+
profile,
|
|
814
|
+
cwd,
|
|
761
815
|
});
|
|
762
816
|
if (options.json) {
|
|
763
817
|
console.log(JSON.stringify({ token: minted }));
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readInstafyCliConfig, readInstafyProfileConfig, resolveActiveProfileName, writeInstafyCliConfig, writeInstafyProfileConfig, } from "./config.js";
|
|
2
|
+
function normalizeToken(value) {
|
|
3
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
4
|
+
if (!trimmed)
|
|
5
|
+
return null;
|
|
6
|
+
const lowered = trimmed.toLowerCase();
|
|
7
|
+
if (lowered === "null" || lowered === "undefined")
|
|
8
|
+
return null;
|
|
9
|
+
return trimmed;
|
|
10
|
+
}
|
|
11
|
+
function normalizeUrl(value) {
|
|
12
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
13
|
+
if (!trimmed)
|
|
14
|
+
return null;
|
|
15
|
+
return trimmed.replace(/\/$/, "");
|
|
16
|
+
}
|
|
17
|
+
function resolveSupabaseUrl(configured) {
|
|
18
|
+
return (normalizeUrl(configured) ??
|
|
19
|
+
normalizeUrl(process.env["SUPABASE_URL"]) ??
|
|
20
|
+
normalizeUrl(process.env["VITE_SUPABASE_URL"]) ??
|
|
21
|
+
normalizeUrl(process.env["SUPABASE_PROJECT_URL"]) ??
|
|
22
|
+
null);
|
|
23
|
+
}
|
|
24
|
+
function resolveSupabaseAnonKey(configured) {
|
|
25
|
+
return (normalizeToken(configured) ??
|
|
26
|
+
normalizeToken(process.env["SUPABASE_ANON_KEY"]) ??
|
|
27
|
+
normalizeToken(process.env["VITE_SUPABASE_ANON_KEY"]) ??
|
|
28
|
+
null);
|
|
29
|
+
}
|
|
30
|
+
async function refreshSupabaseSession(params) {
|
|
31
|
+
const response = await fetch(`${params.supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
apikey: params.supabaseAnonKey,
|
|
35
|
+
authorization: `Bearer ${params.supabaseAnonKey}`,
|
|
36
|
+
"content-type": "application/json",
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify({ refresh_token: params.refreshToken }),
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const body = (await response.json());
|
|
44
|
+
const accessToken = typeof body["access_token"] === "string" ? body["access_token"].trim() : "";
|
|
45
|
+
if (!accessToken) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const refreshToken = typeof body["refresh_token"] === "string" ? body["refresh_token"].trim() : null;
|
|
49
|
+
return {
|
|
50
|
+
accessToken,
|
|
51
|
+
refreshToken,
|
|
52
|
+
supabaseUrl: params.supabaseUrl,
|
|
53
|
+
supabaseAnonKey: params.supabaseAnonKey,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export async function refreshStoredSupabaseSession(params) {
|
|
57
|
+
const profile = resolveActiveProfileName(params);
|
|
58
|
+
const config = profile ? readInstafyProfileConfig(profile) : readInstafyCliConfig();
|
|
59
|
+
const refreshToken = normalizeToken(config.refreshToken ?? null);
|
|
60
|
+
if (!refreshToken) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const supabaseUrl = resolveSupabaseUrl(config.supabaseUrl ?? null);
|
|
64
|
+
const supabaseAnonKey = resolveSupabaseAnonKey(config.supabaseAnonKey ?? null);
|
|
65
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const refreshed = await refreshSupabaseSession({
|
|
69
|
+
supabaseUrl,
|
|
70
|
+
supabaseAnonKey,
|
|
71
|
+
refreshToken,
|
|
72
|
+
});
|
|
73
|
+
if (!refreshed) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const next = {
|
|
77
|
+
accessToken: refreshed.accessToken,
|
|
78
|
+
refreshToken: refreshed.refreshToken ?? refreshToken,
|
|
79
|
+
supabaseUrl: refreshed.supabaseUrl,
|
|
80
|
+
supabaseAnonKey: refreshed.supabaseAnonKey,
|
|
81
|
+
};
|
|
82
|
+
if (profile) {
|
|
83
|
+
writeInstafyProfileConfig(profile, next);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
writeInstafyCliConfig(next);
|
|
87
|
+
}
|
|
88
|
+
return refreshed;
|
|
89
|
+
}
|
package/dist/tunnel.js
CHANGED
|
@@ -5,8 +5,9 @@ import { spawn } from "node:child_process";
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import kleur from "kleur";
|
|
7
7
|
import { findProjectManifest, resolveRatholeBinaryForCli } from "./runtime.js";
|
|
8
|
-
import { resolveConfiguredControllerUrl, resolveControllerUrl as resolveDefaultControllerUrl,
|
|
8
|
+
import { resolveConfiguredControllerUrl, resolveControllerUrl as resolveDefaultControllerUrl, resolveUserAccessTokenWithSource, } from "./config.js";
|
|
9
9
|
import { formatAuthRejectedError, formatAuthRequiredError } from "./errors.js";
|
|
10
|
+
import { fetchWithControllerAuth } from "./controller-fetch.js";
|
|
10
11
|
const INSTAFY_DIR = path.join(os.homedir(), ".instafy");
|
|
11
12
|
const TUNNEL_STATE_FILE = path.join(INSTAFY_DIR, "cli-tunnel-state.json");
|
|
12
13
|
const TUNNEL_LOG_DIR = path.join(INSTAFY_DIR, "cli-tunnel-logs");
|
|
@@ -56,11 +57,11 @@ function resolveTunnelControllerUrl(opts) {
|
|
|
56
57
|
function resolveControllerToken(opts, retryCommand = "instafy tunnel start") {
|
|
57
58
|
const explicit = opts.controllerToken?.trim();
|
|
58
59
|
if (explicit) {
|
|
59
|
-
return explicit;
|
|
60
|
+
return { token: explicit, source: "explicit", profile: null };
|
|
60
61
|
}
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
return
|
|
62
|
+
const resolved = resolveUserAccessTokenWithSource();
|
|
63
|
+
if (resolved.token) {
|
|
64
|
+
return { token: resolved.token, source: resolved.source, profile: resolved.profile };
|
|
64
65
|
}
|
|
65
66
|
const serviceToken = readEnv("INSTAFY_SERVICE_TOKEN") ||
|
|
66
67
|
readEnv("CONTROLLER_BEARER") ||
|
|
@@ -71,7 +72,7 @@ function resolveControllerToken(opts, retryCommand = "instafy tunnel start") {
|
|
|
71
72
|
readEnv("CONTROLLER_INTERNAL_TOKEN") ||
|
|
72
73
|
null;
|
|
73
74
|
if (serviceToken) {
|
|
74
|
-
return serviceToken;
|
|
75
|
+
return { token: serviceToken, source: "env", profile: null };
|
|
75
76
|
}
|
|
76
77
|
throw formatAuthRequiredError({
|
|
77
78
|
retryCommand,
|
|
@@ -156,16 +157,22 @@ function removeTunnelState(tunnelId) {
|
|
|
156
157
|
writeStateFile({ version: 1, tunnels: next });
|
|
157
158
|
return existing;
|
|
158
159
|
}
|
|
159
|
-
async function requestTunnel(controllerUrl,
|
|
160
|
+
async function requestTunnel(controllerUrl, auth, projectId, metadata) {
|
|
160
161
|
const target = `${cleanUrl(controllerUrl)}/projects/${encodeURIComponent(projectId)}/tunnels/request`;
|
|
161
|
-
const response = await
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
const { response } = await fetchWithControllerAuth({
|
|
163
|
+
url: target,
|
|
164
|
+
init: {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: {
|
|
167
|
+
"content-type": "application/json",
|
|
168
|
+
accept: "application/json",
|
|
169
|
+
},
|
|
170
|
+
body: JSON.stringify({ metadata }),
|
|
167
171
|
},
|
|
168
|
-
|
|
172
|
+
accessToken: auth.token,
|
|
173
|
+
tokenSource: auth.source,
|
|
174
|
+
profile: auth.profile,
|
|
175
|
+
cwd: process.cwd(),
|
|
169
176
|
});
|
|
170
177
|
if (!response.ok) {
|
|
171
178
|
const text = await response.text().catch(() => "");
|
|
@@ -262,7 +269,7 @@ export async function startTunnelSession(opts) {
|
|
|
262
269
|
return;
|
|
263
270
|
cleanedUp = true;
|
|
264
271
|
child.kill("SIGTERM");
|
|
265
|
-
await revokeTunnel(controllerUrl, controllerToken, projectId, grant.tunnelId);
|
|
272
|
+
await revokeTunnel(controllerUrl, controllerToken.token, projectId, grant.tunnelId);
|
|
266
273
|
fs.rmSync(workdir, { recursive: true, force: true });
|
|
267
274
|
};
|
|
268
275
|
child.on("exit", () => {
|
|
@@ -413,7 +420,7 @@ export async function stopTunnelSession(opts) {
|
|
|
413
420
|
}
|
|
414
421
|
}
|
|
415
422
|
const controllerToken = resolveControllerToken(opts, `instafy tunnel stop ${tunnelId}`);
|
|
416
|
-
await revokeTunnel(entry.controllerUrl, controllerToken, entry.projectId, entry.tunnelId);
|
|
423
|
+
await revokeTunnel(entry.controllerUrl, controllerToken.token, entry.projectId, entry.tunnelId);
|
|
417
424
|
try {
|
|
418
425
|
fs.rmSync(entry.workdir, { recursive: true, force: true });
|
|
419
426
|
}
|