@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 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 resolveBearerToken(options) {
17
- return (normalizeToken(options.accessToken) ??
18
- normalizeToken(options.serviceToken) ??
19
- normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"]) ??
20
- normalizeToken(process.env["INSTAFY_ACCESS_TOKEN"]) ??
21
- normalizeToken(process.env["INSTAFY_SERVICE_TOKEN"]) ??
22
- normalizeToken(process.env["CONTROLLER_TOKEN"]) ??
23
- normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]) ??
24
- resolveConfiguredAccessToken());
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 bearer = resolveBearerToken(options);
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 response = await fetch(url, {
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
- return accessToken;
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 = typeof json.token === "string" ? json.token : null;
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?.(token);
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 token = provided;
326
+ let authPayload = provided
327
+ ? { accessToken: provided, refreshToken: null, supabaseUrl: null, supabaseAnonKey: null }
328
+ : null;
287
329
  let usedPasswordGrant = false;
288
- if (!token) {
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
- token = await loginWithPassword({ supabaseUrl, supabaseAnonKey, email, password });
343
+ authPayload = await loginWithPassword({ supabaseUrl, supabaseAnonKey, email, password });
302
344
  usedPasswordGrant = true;
303
345
  }
304
346
  }
305
347
  let callbackServer = null;
306
- if (!token) {
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 (!token) {
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 (!token && callbackServer) {
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
- return candidate;
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
- token = result.token;
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
- token = await manualTokenPromise;
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
- token = await callbackServer.waitForToken(10 * 60000);
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 (!token) {
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
- token = normalizeToken(await rl.question("Paste token: "));
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 (!token) {
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, { controllerUrl, studioUrl, accessToken: token });
527
+ writeInstafyProfileConfig(profile, update);
413
528
  }
414
529
  else {
415
- writeInstafyCliConfig({ controllerUrl, studioUrl, accessToken: token });
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
  }
@@ -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, resolveUserAccessToken } from "./config.js";
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 token = resolveUserAccessToken({ accessToken: params.accessToken ?? null });
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 fetch(`${controllerUrl}/orgs`, {
11
- headers: { authorization: `Bearer ${token}` },
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, resolveUserAccessToken, } from "./config.js";
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 fetchOrganizations(controllerUrl, token, retryCommand) {
14
- const response = await fetch(`${controllerUrl}/orgs`, {
15
- headers: {
16
- authorization: `Bearer ${token}`,
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, token, orgId, retryCommand) {
34
- const response = await fetch(`${controllerUrl}/orgs/${encodeURIComponent(orgId)}/projects`, {
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, token, payload, retryCommand) {
54
- const response = await fetch(`${controllerUrl}/orgs`, {
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, token, options, retryCommand) {
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, token, payload, retryCommand);
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, token, retryCommand);
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, token, retryCommand);
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, token, payload, retryCommand);
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, token, payload, retryCommand);
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 token = resolveUserAccessToken({ accessToken: options.accessToken ?? null });
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 orgs = await fetchOrganizations(controllerUrl, token, retryCommand);
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, token, org.id, retryCommand);
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 token = resolveUserAccessToken({
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 org = await resolveOrg(controllerUrl, token, options, retryCommand);
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 fetch(`${controllerUrl}/orgs/${encodeURIComponent(org.orgId)}/projects`, {
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 resolveControllerAccessTokenForCli(options, env, supabaseAccessToken) {
192
- return (normalizeToken(options.controllerAccessToken) ??
193
- normalizeToken(env["INSTAFY_ACCESS_TOKEN"]) ??
194
- normalizeToken(env["CONTROLLER_ACCESS_TOKEN"]) ??
195
- supabaseAccessToken ??
196
- normalizeToken(env["SUPABASE_ACCESS_TOKEN"]) ??
197
- resolveConfiguredAccessToken());
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 fetch(target, {
249
- method: "POST",
250
- headers: {
251
- authorization: `Bearer ${params.controllerAccessToken}`,
252
- "content-type": "application/json",
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
- body: JSON.stringify({
255
- runtimeId: params.runtimeId,
256
- scopes: params.scopes,
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(process.cwd());
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
- let controllerAccessToken = resolveControllerAccessTokenForCli(options, env, supabaseAccessToken);
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
- throw new Error(`docker run failed: ${stderr || "unknown error"}`);
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
- resolveConfiguredAccessToken();
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, resolveUserAccessToken, } from "./config.js";
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 accessToken = resolveUserAccessToken();
62
- if (accessToken) {
63
- return accessToken;
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, token, projectId, metadata) {
160
+ async function requestTunnel(controllerUrl, auth, projectId, metadata) {
160
161
  const target = `${cleanUrl(controllerUrl)}/projects/${encodeURIComponent(projectId)}/tunnels/request`;
161
- const response = await fetch(target, {
162
- method: "POST",
163
- headers: {
164
- authorization: `Bearer ${token}`,
165
- "content-type": "application/json",
166
- accept: "application/json",
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
- body: JSON.stringify({ metadata }),
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instafy/cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Run Instafy projects locally, link folders to Studio, and share previews/webhooks via tunnels.",
5
5
  "private": false,
6
6
  "publishConfig": {