@instafy/cli 0.1.8-staging.348 → 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 +1 -0
- package/dist/auth.js +228 -2
- package/dist/config.js +1 -0
- package/dist/git.js +1 -0
- package/dist/index.js +32 -32
- package/package.json +1 -1
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
|
-
|
|
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/dist/index.js
CHANGED
|
@@ -73,6 +73,38 @@ program
|
|
|
73
73
|
process.exit(1);
|
|
74
74
|
}
|
|
75
75
|
});
|
|
76
|
+
const projectInitCommand = program
|
|
77
|
+
.command("project:init")
|
|
78
|
+
.description("Create an Instafy project and link this folder (.instafy/project.json)")
|
|
79
|
+
.option("--path <dir>", "Directory where the manifest should be written (default: cwd)");
|
|
80
|
+
addServerUrlOptions(projectInitCommand);
|
|
81
|
+
projectInitCommand
|
|
82
|
+
.option("--access-token <token>", "Instafy or Supabase access token")
|
|
83
|
+
.option("--project-type <type>", "Project type (customer|sandbox)")
|
|
84
|
+
.option("--org-id <uuid>", "Optional organization id")
|
|
85
|
+
.option("--org-name <name>", "Optional organization name")
|
|
86
|
+
.option("--org-slug <slug>", "Optional organization slug")
|
|
87
|
+
.option("--owner-user-id <uuid>", "Explicit owner user id (defaults to caller)")
|
|
88
|
+
.option("--json", "Output JSON")
|
|
89
|
+
.action(async (opts) => {
|
|
90
|
+
try {
|
|
91
|
+
await projectInit({
|
|
92
|
+
path: opts.path,
|
|
93
|
+
controllerUrl: opts.serverUrl ?? opts.controllerUrl,
|
|
94
|
+
accessToken: opts.accessToken,
|
|
95
|
+
projectType: opts.projectType,
|
|
96
|
+
orgId: opts.orgId,
|
|
97
|
+
orgName: opts.orgName,
|
|
98
|
+
orgSlug: opts.orgSlug,
|
|
99
|
+
ownerUserId: opts.ownerUserId,
|
|
100
|
+
json: opts.json,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error(kleur.red(String(error)));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
76
108
|
const configCommand = program.command("config").description("Get/set saved CLI configuration");
|
|
77
109
|
configCommand
|
|
78
110
|
.command("path")
|
|
@@ -295,38 +327,6 @@ program
|
|
|
295
327
|
process.exit(1);
|
|
296
328
|
}
|
|
297
329
|
});
|
|
298
|
-
const projectInitCommand = program
|
|
299
|
-
.command("project:init")
|
|
300
|
-
.description("Create an Instafy project and link this folder (.instafy/project.json)")
|
|
301
|
-
.option("--path <dir>", "Directory where the manifest should be written (default: cwd)");
|
|
302
|
-
addServerUrlOptions(projectInitCommand);
|
|
303
|
-
projectInitCommand
|
|
304
|
-
.option("--access-token <token>", "Instafy or Supabase access token")
|
|
305
|
-
.option("--project-type <type>", "Project type (customer|sandbox)")
|
|
306
|
-
.option("--org-id <uuid>", "Optional organization id")
|
|
307
|
-
.option("--org-name <name>", "Optional organization name")
|
|
308
|
-
.option("--org-slug <slug>", "Optional organization slug")
|
|
309
|
-
.option("--owner-user-id <uuid>", "Explicit owner user id (defaults to caller)")
|
|
310
|
-
.option("--json", "Output JSON")
|
|
311
|
-
.action(async (opts) => {
|
|
312
|
-
try {
|
|
313
|
-
await projectInit({
|
|
314
|
-
path: opts.path,
|
|
315
|
-
controllerUrl: opts.serverUrl ?? opts.controllerUrl,
|
|
316
|
-
accessToken: opts.accessToken,
|
|
317
|
-
projectType: opts.projectType,
|
|
318
|
-
orgId: opts.orgId,
|
|
319
|
-
orgName: opts.orgName,
|
|
320
|
-
orgSlug: opts.orgSlug,
|
|
321
|
-
ownerUserId: opts.ownerUserId,
|
|
322
|
-
json: opts.json,
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
catch (error) {
|
|
326
|
-
console.error(kleur.red(String(error)));
|
|
327
|
-
process.exit(1);
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
330
|
const tunnelCommand = program
|
|
331
331
|
.command("tunnel")
|
|
332
332
|
.description("Start a shareable tunnel URL for a local port (defaults to detached)")
|
package/package.json
CHANGED