@instafy/cli 0.1.8-staging.350 → 0.1.8-staging.353

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,7 @@ Run Instafy projects locally and connect them back to Instafy Studio — from an
7
7
  ## Quickstart
8
8
 
9
9
  0. Log in once: `instafy login`
10
+ - Opens a Studio URL; the CLI continues automatically after you sign in.
10
11
  - Also enables Git auth (credential helper) for Instafy Git Service. Disable with `instafy login --no-git-setup`.
11
12
  - Optional: set defaults with `instafy config set controller-url <url>` / `instafy config set studio-url <url>`
12
13
  1. Link a folder to a project:
package/dist/auth.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import kleur from "kleur";
2
+ import { randomBytes } from "node:crypto";
3
+ import * as http from "node:http";
2
4
  import { createInterface } from "node:readline/promises";
3
5
  import { stdin as input, stdout as output } from "node:process";
4
- import { clearInstafyCliConfig, getInstafyConfigPath, resolveConfiguredStudioUrl, resolveControllerUrl, resolveUserAccessToken, writeInstafyCliConfig, } from "./config.js";
6
+ import { clearInstafyCliConfig, getInstafyConfigPath, resolveConfiguredControllerUrl, resolveConfiguredStudioUrl, resolveUserAccessToken, writeInstafyCliConfig, } from "./config.js";
5
7
  import { installGitCredentialHelper, uninstallGitCredentialHelper } from "./git-setup.js";
6
8
  function normalizeUrl(raw) {
7
9
  const trimmed = typeof raw === "string" ? raw.trim() : "";
@@ -37,8 +39,177 @@ function deriveDefaultStudioUrl(controllerUrl) {
37
39
  }
38
40
  return "https://staging.instafy.dev";
39
41
  }
42
+ async function isControllerHealthy(controllerUrl, timeoutMs) {
43
+ const target = `${controllerUrl.replace(/\/$/, "")}/healthz`;
44
+ const abort = new AbortController();
45
+ const timeout = setTimeout(() => abort.abort(), timeoutMs);
46
+ timeout.unref?.();
47
+ try {
48
+ const response = await fetch(target, { signal: abort.signal });
49
+ return response.ok;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ finally {
55
+ clearTimeout(timeout);
56
+ }
57
+ }
58
+ function readRequestBody(request, maxBytes = 1000000) {
59
+ return new Promise((resolve, reject) => {
60
+ let buffer = "";
61
+ request.setEncoding("utf8");
62
+ request.on("data", (chunk) => {
63
+ buffer += chunk;
64
+ if (buffer.length > maxBytes) {
65
+ reject(new Error("Request body too large."));
66
+ request.destroy();
67
+ }
68
+ });
69
+ request.on("end", () => resolve(buffer));
70
+ request.on("error", (error) => reject(error));
71
+ });
72
+ }
73
+ function applyCorsHeaders(request, response) {
74
+ const origin = typeof request.headers.origin === "string" ? request.headers.origin : "";
75
+ if (origin) {
76
+ response.setHeader("access-control-allow-origin", origin);
77
+ response.setHeader("vary", "origin");
78
+ }
79
+ else {
80
+ response.setHeader("access-control-allow-origin", "*");
81
+ }
82
+ response.setHeader("access-control-allow-methods", "POST, OPTIONS");
83
+ response.setHeader("access-control-allow-headers", "content-type");
84
+ // Private Network Access preflight (Chrome): allow https -> http://127.0.0.1 callbacks.
85
+ if (request.headers["access-control-request-private-network"] === "true") {
86
+ response.setHeader("access-control-allow-private-network", "true");
87
+ }
88
+ }
89
+ async function startCliLoginCallbackServer() {
90
+ const state = randomBytes(16).toString("hex");
91
+ let resolved = false;
92
+ let resolveToken = null;
93
+ let rejectToken = null;
94
+ const tokenPromise = new Promise((resolve, reject) => {
95
+ resolveToken = resolve;
96
+ rejectToken = reject;
97
+ });
98
+ const server = http.createServer(async (request, response) => {
99
+ applyCorsHeaders(request, response);
100
+ if (request.method === "OPTIONS") {
101
+ response.statusCode = 204;
102
+ response.end();
103
+ return;
104
+ }
105
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
106
+ if (request.method !== "POST" || url.pathname !== "/callback") {
107
+ response.statusCode = 404;
108
+ response.setHeader("content-type", "application/json");
109
+ response.end(JSON.stringify({ ok: false, error: "Not found" }));
110
+ return;
111
+ }
112
+ if (resolved) {
113
+ response.statusCode = 409;
114
+ response.setHeader("content-type", "application/json");
115
+ response.end(JSON.stringify({ ok: false, error: "Already completed" }));
116
+ return;
117
+ }
118
+ try {
119
+ const body = await readRequestBody(request);
120
+ const contentType = typeof request.headers["content-type"] === "string" ? request.headers["content-type"] : "";
121
+ let parsedToken = null;
122
+ let parsedState = null;
123
+ if (contentType.includes("application/json")) {
124
+ const json = JSON.parse(body);
125
+ parsedToken = typeof json.token === "string" ? json.token : null;
126
+ parsedState = typeof json.state === "string" ? json.state : null;
127
+ }
128
+ else {
129
+ const params = new URLSearchParams(body);
130
+ parsedToken = params.get("token");
131
+ parsedState = params.get("state");
132
+ }
133
+ const token = normalizeToken(parsedToken);
134
+ const receivedState = normalizeToken(parsedState);
135
+ if (!token) {
136
+ response.statusCode = 400;
137
+ response.setHeader("content-type", "application/json");
138
+ response.end(JSON.stringify({ ok: false, error: "Missing token" }));
139
+ return;
140
+ }
141
+ if (!receivedState || receivedState !== state) {
142
+ response.statusCode = 403;
143
+ response.setHeader("content-type", "application/json");
144
+ response.end(JSON.stringify({ ok: false, error: "Invalid state" }));
145
+ return;
146
+ }
147
+ resolved = true;
148
+ response.statusCode = 200;
149
+ response.setHeader("content-type", "application/json");
150
+ response.end(JSON.stringify({ ok: true }));
151
+ resolveToken?.(token);
152
+ resolveToken = null;
153
+ }
154
+ catch (error) {
155
+ response.statusCode = 500;
156
+ response.setHeader("content-type", "application/json");
157
+ response.end(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }));
158
+ }
159
+ });
160
+ await new Promise((resolve, reject) => {
161
+ server.once("error", reject);
162
+ server.listen(0, "127.0.0.1", () => resolve());
163
+ });
164
+ const address = server.address();
165
+ if (!address || typeof address !== "object" || typeof address.port !== "number") {
166
+ server.close();
167
+ throw new Error("Failed to start login callback server.");
168
+ }
169
+ return {
170
+ callbackUrl: `http://127.0.0.1:${address.port}/callback`,
171
+ state,
172
+ waitForToken: async (timeoutMs) => {
173
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
174
+ return tokenPromise;
175
+ }
176
+ const timeout = setTimeout(() => {
177
+ if (resolved)
178
+ return;
179
+ resolved = true;
180
+ rejectToken?.(new Error("Timed out waiting for browser login. Copy the token and paste it into the CLI instead."));
181
+ rejectToken = null;
182
+ }, timeoutMs);
183
+ timeout.unref?.();
184
+ try {
185
+ return await tokenPromise;
186
+ }
187
+ finally {
188
+ clearTimeout(timeout);
189
+ }
190
+ },
191
+ cancel: () => {
192
+ if (resolved)
193
+ return;
194
+ resolved = true;
195
+ rejectToken?.(new Error("Login cancelled."));
196
+ rejectToken = null;
197
+ resolveToken = null;
198
+ },
199
+ close: () => server.close(),
200
+ };
201
+ }
40
202
  export async function login(options) {
41
- const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
203
+ const explicitControllerUrl = normalizeUrl(options.controllerUrl ?? null) ??
204
+ normalizeUrl(process.env["INSTAFY_SERVER_URL"] ?? null) ??
205
+ normalizeUrl(process.env["CONTROLLER_BASE_URL"] ?? null) ??
206
+ resolveConfiguredControllerUrl();
207
+ const defaultLocalControllerUrl = "http://127.0.0.1:8788";
208
+ const defaultHostedControllerUrl = "https://controller.instafy.dev";
209
+ const controllerUrl = explicitControllerUrl ??
210
+ ((await isControllerHealthy(defaultLocalControllerUrl, 250))
211
+ ? defaultLocalControllerUrl
212
+ : defaultHostedControllerUrl);
42
213
  const studioUrl = normalizeUrl(options.studioUrl ?? null) ??
43
214
  normalizeUrl(process.env["INSTAFY_STUDIO_URL"] ?? null) ??
44
215
  resolveConfiguredStudioUrl() ??
@@ -49,17 +220,97 @@ export async function login(options) {
49
220
  console.log(JSON.stringify({ url: url.toString(), configPath: getInstafyConfigPath() }));
50
221
  return;
51
222
  }
223
+ let callbackServer = null;
224
+ const provided = normalizeToken(options.token ?? null);
225
+ if (!provided) {
226
+ try {
227
+ callbackServer = await startCliLoginCallbackServer();
228
+ url.searchParams.set("cliCallbackUrl", callbackServer.callbackUrl);
229
+ url.searchParams.set("cliState", callbackServer.state);
230
+ }
231
+ catch {
232
+ callbackServer = null;
233
+ }
234
+ }
52
235
  console.log(kleur.green("Instafy CLI login"));
53
236
  console.log("");
54
237
  console.log("1) Open this URL in your browser:");
55
238
  console.log(kleur.cyan(url.toString()));
56
239
  console.log("");
57
- console.log("2) After you sign in, copy the token shown on that page.");
240
+ if (callbackServer) {
241
+ console.log("2) Sign in — this terminal should continue automatically.");
242
+ console.log(kleur.gray("If it doesn't, copy the token shown on that page and paste it here."));
243
+ }
244
+ else {
245
+ console.log("2) After you sign in, copy the token shown on that page.");
246
+ }
58
247
  console.log("");
59
- const provided = normalizeToken(options.token ?? null);
60
248
  const existing = resolveUserAccessToken();
61
249
  let token = provided;
250
+ if (!token && callbackServer) {
251
+ if (input.isTTY) {
252
+ console.log(kleur.gray("Waiting for browser login…"));
253
+ console.log(kleur.gray("If it doesn't continue, paste the token here and press Enter."));
254
+ console.log("");
255
+ const rl = createInterface({ input, output });
256
+ const abort = new AbortController();
257
+ const manualTokenPromise = (async () => {
258
+ while (true) {
259
+ const answer = await rl.question("Paste token (or wait): ", { signal: abort.signal });
260
+ const candidate = normalizeToken(answer);
261
+ if (candidate) {
262
+ return candidate;
263
+ }
264
+ }
265
+ })();
266
+ try {
267
+ const result = await Promise.race([
268
+ callbackServer
269
+ .waitForToken(10 * 60000)
270
+ .then((tokenValue) => ({ source: "browser", token: tokenValue })),
271
+ manualTokenPromise.then((tokenValue) => ({ source: "manual", token: tokenValue })),
272
+ ]);
273
+ token = result.token;
274
+ if (result.source === "browser") {
275
+ abort.abort();
276
+ }
277
+ else {
278
+ callbackServer.cancel();
279
+ }
280
+ }
281
+ catch (_error) {
282
+ // If browser login fails, keep waiting for a pasted token.
283
+ token = await manualTokenPromise;
284
+ callbackServer.cancel();
285
+ }
286
+ finally {
287
+ try {
288
+ rl.close();
289
+ }
290
+ catch {
291
+ // ignore
292
+ }
293
+ callbackServer.close();
294
+ callbackServer = null;
295
+ }
296
+ }
297
+ else {
298
+ try {
299
+ token = await callbackServer.waitForToken(10 * 60000);
300
+ }
301
+ catch (_error) {
302
+ // Ignore and fall back to manual copy/paste if possible.
303
+ }
304
+ finally {
305
+ callbackServer.close();
306
+ callbackServer = null;
307
+ }
308
+ }
309
+ }
62
310
  if (!token) {
311
+ if (!input.isTTY) {
312
+ throw new Error("No token provided.");
313
+ }
63
314
  const rl = createInterface({ input, output });
64
315
  try {
65
316
  token = normalizeToken(await rl.question("Paste token: "));
package/dist/config.js CHANGED
@@ -106,6 +106,7 @@ export function resolveUserAccessToken(params) {
106
106
  return (normalizeToken(params?.accessToken ?? null) ??
107
107
  normalizeToken(process.env["INSTAFY_ACCESS_TOKEN"] ?? null) ??
108
108
  normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"] ?? null) ??
109
+ normalizeToken(process.env["RUNTIME_ACCESS_TOKEN"] ?? null) ??
109
110
  normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"] ?? null) ??
110
111
  normalizeToken(config.accessToken ?? null));
111
112
  }
package/dist/git.js CHANGED
@@ -25,6 +25,7 @@ function resolveControllerAccessToken(options) {
25
25
  return (normalizeToken(options.controllerAccessToken) ??
26
26
  normalizeToken(process.env["INSTAFY_ACCESS_TOKEN"]) ??
27
27
  normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"]) ??
28
+ normalizeToken(process.env["RUNTIME_ACCESS_TOKEN"]) ??
28
29
  normalizeToken(options.supabaseAccessToken) ??
29
30
  readTokenFromFile(options.supabaseAccessTokenFile) ??
30
31
  normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]) ??
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instafy/cli",
3
- "version": "0.1.8-staging.350",
3
+ "version": "0.1.8-staging.353",
4
4
  "description": "Run Instafy projects locally, link folders to Studio, and share previews/webhooks via tunnels.",
5
5
  "private": false,
6
6
  "publishConfig": {