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

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,4 +1,6 @@
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
6
  import { clearInstafyCliConfig, getInstafyConfigPath, resolveConfiguredStudioUrl, resolveControllerUrl, resolveUserAccessToken, writeInstafyCliConfig, } from "./config.js";
@@ -37,6 +39,150 @@ function deriveDefaultStudioUrl(controllerUrl) {
37
39
  }
38
40
  return "https://staging.instafy.dev";
39
41
  }
42
+ function readRequestBody(request, maxBytes = 1000000) {
43
+ return new Promise((resolve, reject) => {
44
+ let buffer = "";
45
+ request.setEncoding("utf8");
46
+ request.on("data", (chunk) => {
47
+ buffer += chunk;
48
+ if (buffer.length > maxBytes) {
49
+ reject(new Error("Request body too large."));
50
+ request.destroy();
51
+ }
52
+ });
53
+ request.on("end", () => resolve(buffer));
54
+ request.on("error", (error) => reject(error));
55
+ });
56
+ }
57
+ function applyCorsHeaders(request, response) {
58
+ const origin = typeof request.headers.origin === "string" ? request.headers.origin : "";
59
+ if (origin) {
60
+ response.setHeader("access-control-allow-origin", origin);
61
+ response.setHeader("vary", "origin");
62
+ }
63
+ else {
64
+ response.setHeader("access-control-allow-origin", "*");
65
+ }
66
+ response.setHeader("access-control-allow-methods", "POST, OPTIONS");
67
+ response.setHeader("access-control-allow-headers", "content-type");
68
+ // Private Network Access preflight (Chrome): allow https -> http://127.0.0.1 callbacks.
69
+ if (request.headers["access-control-request-private-network"] === "true") {
70
+ response.setHeader("access-control-allow-private-network", "true");
71
+ }
72
+ }
73
+ async function startCliLoginCallbackServer() {
74
+ const state = randomBytes(16).toString("hex");
75
+ let resolved = false;
76
+ let resolveToken = null;
77
+ let rejectToken = null;
78
+ const tokenPromise = new Promise((resolve, reject) => {
79
+ resolveToken = resolve;
80
+ rejectToken = reject;
81
+ });
82
+ const server = http.createServer(async (request, response) => {
83
+ applyCorsHeaders(request, response);
84
+ if (request.method === "OPTIONS") {
85
+ response.statusCode = 204;
86
+ response.end();
87
+ return;
88
+ }
89
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
90
+ if (request.method !== "POST" || url.pathname !== "/callback") {
91
+ response.statusCode = 404;
92
+ response.setHeader("content-type", "application/json");
93
+ response.end(JSON.stringify({ ok: false, error: "Not found" }));
94
+ return;
95
+ }
96
+ if (resolved) {
97
+ response.statusCode = 409;
98
+ response.setHeader("content-type", "application/json");
99
+ response.end(JSON.stringify({ ok: false, error: "Already completed" }));
100
+ return;
101
+ }
102
+ try {
103
+ const body = await readRequestBody(request);
104
+ const contentType = typeof request.headers["content-type"] === "string" ? request.headers["content-type"] : "";
105
+ let parsedToken = null;
106
+ let parsedState = null;
107
+ if (contentType.includes("application/json")) {
108
+ const json = JSON.parse(body);
109
+ parsedToken = typeof json.token === "string" ? json.token : null;
110
+ parsedState = typeof json.state === "string" ? json.state : null;
111
+ }
112
+ else {
113
+ const params = new URLSearchParams(body);
114
+ parsedToken = params.get("token");
115
+ parsedState = params.get("state");
116
+ }
117
+ const token = normalizeToken(parsedToken);
118
+ const receivedState = normalizeToken(parsedState);
119
+ if (!token) {
120
+ response.statusCode = 400;
121
+ response.setHeader("content-type", "application/json");
122
+ response.end(JSON.stringify({ ok: false, error: "Missing token" }));
123
+ return;
124
+ }
125
+ if (!receivedState || receivedState !== state) {
126
+ response.statusCode = 403;
127
+ response.setHeader("content-type", "application/json");
128
+ response.end(JSON.stringify({ ok: false, error: "Invalid state" }));
129
+ return;
130
+ }
131
+ resolved = true;
132
+ response.statusCode = 200;
133
+ response.setHeader("content-type", "application/json");
134
+ response.end(JSON.stringify({ ok: true }));
135
+ resolveToken?.(token);
136
+ resolveToken = null;
137
+ }
138
+ catch (error) {
139
+ response.statusCode = 500;
140
+ response.setHeader("content-type", "application/json");
141
+ response.end(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }));
142
+ }
143
+ });
144
+ await new Promise((resolve, reject) => {
145
+ server.once("error", reject);
146
+ server.listen(0, "127.0.0.1", () => resolve());
147
+ });
148
+ const address = server.address();
149
+ if (!address || typeof address !== "object" || typeof address.port !== "number") {
150
+ server.close();
151
+ throw new Error("Failed to start login callback server.");
152
+ }
153
+ return {
154
+ callbackUrl: `http://127.0.0.1:${address.port}/callback`,
155
+ state,
156
+ waitForToken: async (timeoutMs) => {
157
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
158
+ return tokenPromise;
159
+ }
160
+ const timeout = setTimeout(() => {
161
+ if (resolved)
162
+ return;
163
+ resolved = true;
164
+ rejectToken?.(new Error("Timed out waiting for browser login. Copy the token and paste it into the CLI instead."));
165
+ rejectToken = null;
166
+ }, timeoutMs);
167
+ timeout.unref?.();
168
+ try {
169
+ return await tokenPromise;
170
+ }
171
+ finally {
172
+ clearTimeout(timeout);
173
+ }
174
+ },
175
+ cancel: () => {
176
+ if (resolved)
177
+ return;
178
+ resolved = true;
179
+ rejectToken?.(new Error("Login cancelled."));
180
+ rejectToken = null;
181
+ resolveToken = null;
182
+ },
183
+ close: () => server.close(),
184
+ };
185
+ }
40
186
  export async function login(options) {
41
187
  const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
42
188
  const studioUrl = normalizeUrl(options.studioUrl ?? null) ??
@@ -49,17 +195,97 @@ export async function login(options) {
49
195
  console.log(JSON.stringify({ url: url.toString(), configPath: getInstafyConfigPath() }));
50
196
  return;
51
197
  }
198
+ let callbackServer = null;
199
+ const provided = normalizeToken(options.token ?? null);
200
+ if (!provided) {
201
+ try {
202
+ callbackServer = await startCliLoginCallbackServer();
203
+ url.searchParams.set("cliCallbackUrl", callbackServer.callbackUrl);
204
+ url.searchParams.set("cliState", callbackServer.state);
205
+ }
206
+ catch {
207
+ callbackServer = null;
208
+ }
209
+ }
52
210
  console.log(kleur.green("Instafy CLI login"));
53
211
  console.log("");
54
212
  console.log("1) Open this URL in your browser:");
55
213
  console.log(kleur.cyan(url.toString()));
56
214
  console.log("");
57
- console.log("2) After you sign in, copy the token shown on that page.");
215
+ if (callbackServer) {
216
+ console.log("2) Sign in — this terminal should continue automatically.");
217
+ console.log(kleur.gray("If it doesn't, copy the token shown on that page and paste it here."));
218
+ }
219
+ else {
220
+ console.log("2) After you sign in, copy the token shown on that page.");
221
+ }
58
222
  console.log("");
59
- const provided = normalizeToken(options.token ?? null);
60
223
  const existing = resolveUserAccessToken();
61
224
  let token = provided;
225
+ if (!token && callbackServer) {
226
+ if (input.isTTY) {
227
+ console.log(kleur.gray("Waiting for browser login…"));
228
+ console.log(kleur.gray("If it doesn't continue, paste the token here and press Enter."));
229
+ console.log("");
230
+ const rl = createInterface({ input, output });
231
+ const abort = new AbortController();
232
+ const manualTokenPromise = (async () => {
233
+ while (true) {
234
+ const answer = await rl.question("Paste token (or wait): ", { signal: abort.signal });
235
+ const candidate = normalizeToken(answer);
236
+ if (candidate) {
237
+ return candidate;
238
+ }
239
+ }
240
+ })();
241
+ try {
242
+ const result = await Promise.race([
243
+ callbackServer
244
+ .waitForToken(10 * 60000)
245
+ .then((tokenValue) => ({ source: "browser", token: tokenValue })),
246
+ manualTokenPromise.then((tokenValue) => ({ source: "manual", token: tokenValue })),
247
+ ]);
248
+ token = result.token;
249
+ if (result.source === "browser") {
250
+ abort.abort();
251
+ }
252
+ else {
253
+ callbackServer.cancel();
254
+ }
255
+ }
256
+ catch (_error) {
257
+ // If browser login fails, keep waiting for a pasted token.
258
+ token = await manualTokenPromise;
259
+ callbackServer.cancel();
260
+ }
261
+ finally {
262
+ try {
263
+ rl.close();
264
+ }
265
+ catch {
266
+ // ignore
267
+ }
268
+ callbackServer.close();
269
+ callbackServer = null;
270
+ }
271
+ }
272
+ else {
273
+ try {
274
+ token = await callbackServer.waitForToken(10 * 60000);
275
+ }
276
+ catch (_error) {
277
+ // Ignore and fall back to manual copy/paste if possible.
278
+ }
279
+ finally {
280
+ callbackServer.close();
281
+ callbackServer = null;
282
+ }
283
+ }
284
+ }
62
285
  if (!token) {
286
+ if (!input.isTTY) {
287
+ throw new Error("No token provided.");
288
+ }
63
289
  const rl = createInterface({ input, output });
64
290
  try {
65
291
  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.351",
4
4
  "description": "Run Instafy projects locally, link folders to Studio, and share previews/webhooks via tunnels.",
5
5
  "private": false,
6
6
  "publishConfig": {