@blinkdotnew/cli 0.4.3 → 0.5.1
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/cli.js +113 -24
- package/package.json +1 -1
- package/src/commands/auth.ts +132 -26
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,12 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
7
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
8
|
+
}) : x)(function(x) {
|
|
9
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
10
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
11
|
+
});
|
|
6
12
|
var __esm = (fn, res) => function __init() {
|
|
7
13
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
14
|
};
|
|
@@ -2074,12 +2080,115 @@ After linking, most commands work without specifying a project_id:
|
|
|
2074
2080
|
|
|
2075
2081
|
// src/commands/auth.ts
|
|
2076
2082
|
import chalk10 from "chalk";
|
|
2083
|
+
import { createServer } from "http";
|
|
2084
|
+
var TIMEOUT_MS = 12e4;
|
|
2085
|
+
function getBaseUrl() {
|
|
2086
|
+
return process.env.BLINK_APP_URL || "https://blink.new";
|
|
2087
|
+
}
|
|
2088
|
+
function generateState() {
|
|
2089
|
+
const { randomBytes } = __require("crypto");
|
|
2090
|
+
return randomBytes(24).toString("base64url");
|
|
2091
|
+
}
|
|
2092
|
+
function findFreePort() {
|
|
2093
|
+
return new Promise((resolve, reject) => {
|
|
2094
|
+
const srv = createServer();
|
|
2095
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
2096
|
+
const port = srv.address().port;
|
|
2097
|
+
srv.close(() => resolve(port));
|
|
2098
|
+
});
|
|
2099
|
+
srv.on("error", reject);
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
function callbackHtml() {
|
|
2103
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Blink CLI</title>
|
|
2104
|
+
<style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#fafafa;color:#111}
|
|
2105
|
+
.card{text-align:center;padding:3rem;border-radius:12px;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.08)}
|
|
2106
|
+
h1{font-size:1.5rem;margin:0 0 .5rem}p{color:#666;margin:0}</style></head>
|
|
2107
|
+
<body><div class="card"><h1>\u2713 CLI authorized</h1><p>You can close this tab.</p></div></body></html>`;
|
|
2108
|
+
}
|
|
2109
|
+
function parseCallback(url) {
|
|
2110
|
+
const params = new URL(url, "http://localhost").searchParams;
|
|
2111
|
+
const key = params.get("key");
|
|
2112
|
+
const workspace_id = params.get("workspace_id");
|
|
2113
|
+
const workspace_name = params.get("workspace_name");
|
|
2114
|
+
const email = params.get("email");
|
|
2115
|
+
if (!key || !workspace_id || !workspace_name || !email) return null;
|
|
2116
|
+
return { key, workspace_id, workspace_name, email };
|
|
2117
|
+
}
|
|
2118
|
+
function startCallbackServer(port, expectedState) {
|
|
2119
|
+
let resolveCb;
|
|
2120
|
+
let rejectCb;
|
|
2121
|
+
const promise = new Promise((res, rej) => {
|
|
2122
|
+
resolveCb = res;
|
|
2123
|
+
rejectCb = rej;
|
|
2124
|
+
});
|
|
2125
|
+
const server = createServer((req, res) => {
|
|
2126
|
+
if (!req.url?.startsWith("/callback")) {
|
|
2127
|
+
res.writeHead(404).end();
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
const state = new URL(req.url, "http://localhost").searchParams.get("state");
|
|
2131
|
+
if (state !== expectedState) {
|
|
2132
|
+
res.writeHead(403).end("State mismatch");
|
|
2133
|
+
rejectCb(new Error("State mismatch \u2014 possible CSRF. Try again."));
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
const result = parseCallback(req.url);
|
|
2137
|
+
if (!result) {
|
|
2138
|
+
res.writeHead(400).end("Missing parameters");
|
|
2139
|
+
rejectCb(new Error("Incomplete callback \u2014 missing parameters."));
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
res.writeHead(200, { "Content-Type": "text/html" }).end(callbackHtml());
|
|
2143
|
+
resolveCb(result);
|
|
2144
|
+
});
|
|
2145
|
+
server.listen(port, "127.0.0.1");
|
|
2146
|
+
return { promise, close: () => server.close() };
|
|
2147
|
+
}
|
|
2148
|
+
async function openBrowserLogin(port, state) {
|
|
2149
|
+
const url = `${getBaseUrl()}/auth/cli?port=${port}&state=${state}`;
|
|
2150
|
+
const open = await import("open").then((m) => m.default).catch(() => null);
|
|
2151
|
+
if (open) await open(url).catch(() => {
|
|
2152
|
+
});
|
|
2153
|
+
else console.log(chalk10.yellow(" Browser did not open automatically."));
|
|
2154
|
+
console.log(chalk10.dim(` If needed, visit: ${url}
|
|
2155
|
+
`));
|
|
2156
|
+
}
|
|
2157
|
+
async function waitForCallback(promise) {
|
|
2158
|
+
return Promise.race([
|
|
2159
|
+
promise,
|
|
2160
|
+
new Promise(
|
|
2161
|
+
(_, reject) => setTimeout(() => reject(new Error("Timed out after 120s \u2014 no callback received.")), TIMEOUT_MS)
|
|
2162
|
+
)
|
|
2163
|
+
]);
|
|
2164
|
+
}
|
|
2165
|
+
async function browserLogin() {
|
|
2166
|
+
const state = generateState();
|
|
2167
|
+
const port = await findFreePort();
|
|
2168
|
+
const { promise, close } = startCallbackServer(port, state);
|
|
2169
|
+
console.log(chalk10.bold("\n Opening browser to authorize...\n"));
|
|
2170
|
+
await openBrowserLogin(port, state);
|
|
2171
|
+
const result = await waitForCallback(promise).finally(close);
|
|
2172
|
+
writeConfig({ api_key: result.key, workspace_id: result.workspace_id });
|
|
2173
|
+
console.log(chalk10.green("\u2713") + ` Logged in as ${chalk10.bold(result.email)} (${result.workspace_name})`);
|
|
2174
|
+
}
|
|
2175
|
+
async function interactiveLogin() {
|
|
2176
|
+
const { password } = await import("@clack/prompts");
|
|
2177
|
+
const apiKey = await password({ message: "Paste your API key (blnk_ak_...):" });
|
|
2178
|
+
if (!apiKey?.startsWith("blnk_ak_")) {
|
|
2179
|
+
console.error("Error: API key must start with blnk_ak_");
|
|
2180
|
+
process.exit(1);
|
|
2181
|
+
}
|
|
2182
|
+
writeConfig({ api_key: apiKey });
|
|
2183
|
+
console.log(chalk10.green("\u2713") + " Saved to ~/.config/blink/config.toml");
|
|
2184
|
+
}
|
|
2077
2185
|
function registerAuthCommands(program2) {
|
|
2078
2186
|
program2.command("login").description("Authenticate with your Blink API key").option("--interactive", "Prompt for API key (for headless/SSH/CI environments)").addHelpText("after", `
|
|
2079
2187
|
Get your API key at: blink.new \u2192 Settings \u2192 API Keys (starts with blnk_ak_)
|
|
2080
2188
|
|
|
2081
2189
|
Examples:
|
|
2082
|
-
$ blink login
|
|
2190
|
+
$ blink login Opens browser for one-click auth
|
|
2191
|
+
$ blink login --interactive Paste key manually (headless/SSH/CI)
|
|
2083
2192
|
$ export BLINK_API_KEY=blnk_ak_... Alternative: set env var directly (no login needed)
|
|
2084
2193
|
|
|
2085
2194
|
In Blink Claw agents: BLINK_API_KEY is already set \u2014 login is not needed.
|
|
@@ -2089,28 +2198,8 @@ For CI/GitHub Actions: set BLINK_API_KEY as a secret, skip login entirely.
|
|
|
2089
2198
|
console.log(chalk10.green("\u2713") + " Already authenticated via BLINK_API_KEY env var.");
|
|
2090
2199
|
return;
|
|
2091
2200
|
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
console.log(chalk10.bold("\n Open this page to get your API key:\n"));
|
|
2095
|
-
console.log(` ${chalk10.cyan(url)}
|
|
2096
|
-
`);
|
|
2097
|
-
const open = await import("open").then((m) => m.default).catch(() => null);
|
|
2098
|
-
if (open) {
|
|
2099
|
-
await open(url).catch(() => {
|
|
2100
|
-
});
|
|
2101
|
-
console.log(chalk10.dim(" (opened in browser)"));
|
|
2102
|
-
}
|
|
2103
|
-
console.log(chalk10.dim(" Then run: blink login --interactive\n"));
|
|
2104
|
-
return;
|
|
2105
|
-
}
|
|
2106
|
-
const { password } = await import("@clack/prompts");
|
|
2107
|
-
const apiKey = await password({ message: "Paste your API key (blnk_ak_...):" });
|
|
2108
|
-
if (!apiKey?.startsWith("blnk_ak_")) {
|
|
2109
|
-
console.error("Error: API key must start with blnk_ak_");
|
|
2110
|
-
process.exit(1);
|
|
2111
|
-
}
|
|
2112
|
-
writeConfig({ api_key: apiKey });
|
|
2113
|
-
console.log(chalk10.green("\u2713") + " Saved to ~/.config/blink/config.toml");
|
|
2201
|
+
if (opts.interactive) return interactiveLogin();
|
|
2202
|
+
return browserLogin();
|
|
2114
2203
|
});
|
|
2115
2204
|
program2.command("logout").description("Remove stored credentials").action(() => {
|
|
2116
2205
|
clearConfig();
|
|
@@ -2119,7 +2208,7 @@ For CI/GitHub Actions: set BLINK_API_KEY as a secret, skip login entirely.
|
|
|
2119
2208
|
program2.command("whoami").description("Show current authentication status and key info").action(async () => {
|
|
2120
2209
|
const token = resolveToken();
|
|
2121
2210
|
if (!token) {
|
|
2122
|
-
console.error("Error: Not authenticated. Run `blink login
|
|
2211
|
+
console.error("Error: Not authenticated. Run `blink login` or set BLINK_API_KEY.");
|
|
2123
2212
|
process.exit(1);
|
|
2124
2213
|
}
|
|
2125
2214
|
if (isJsonMode()) {
|
package/package.json
CHANGED
package/src/commands/auth.ts
CHANGED
|
@@ -1,9 +1,134 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
|
-
import {
|
|
3
|
-
import { requireToken, resolveToken } from '../lib/auth.js'
|
|
2
|
+
import { resolveToken } from '../lib/auth.js'
|
|
4
3
|
import { writeConfig, clearConfig } from '../lib/config.js'
|
|
5
4
|
import { printJson, isJsonMode } from '../lib/output.js'
|
|
6
5
|
import chalk from 'chalk'
|
|
6
|
+
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
|
|
7
|
+
import type { AddressInfo } from 'node:net'
|
|
8
|
+
|
|
9
|
+
const TIMEOUT_MS = 120_000
|
|
10
|
+
|
|
11
|
+
interface CallbackResult {
|
|
12
|
+
key: string
|
|
13
|
+
workspace_id: string
|
|
14
|
+
workspace_name: string
|
|
15
|
+
email: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getBaseUrl(): string {
|
|
19
|
+
return process.env.BLINK_APP_URL || 'https://blink.new'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function generateState(): string {
|
|
23
|
+
const { randomBytes } = require('node:crypto')
|
|
24
|
+
return randomBytes(24).toString('base64url')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findFreePort(): Promise<number> {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const srv = createServer()
|
|
30
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
31
|
+
const port = (srv.address() as AddressInfo).port
|
|
32
|
+
srv.close(() => resolve(port))
|
|
33
|
+
})
|
|
34
|
+
srv.on('error', reject)
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function callbackHtml(): string {
|
|
39
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Blink CLI</title>
|
|
40
|
+
<style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#fafafa;color:#111}
|
|
41
|
+
.card{text-align:center;padding:3rem;border-radius:12px;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.08)}
|
|
42
|
+
h1{font-size:1.5rem;margin:0 0 .5rem}p{color:#666;margin:0}</style></head>
|
|
43
|
+
<body><div class="card"><h1>✓ CLI authorized</h1><p>You can close this tab.</p></div></body></html>`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseCallback(url: string): CallbackResult | null {
|
|
47
|
+
const params = new URL(url, 'http://localhost').searchParams
|
|
48
|
+
const key = params.get('key')
|
|
49
|
+
const workspace_id = params.get('workspace_id')
|
|
50
|
+
const workspace_name = params.get('workspace_name')
|
|
51
|
+
const email = params.get('email')
|
|
52
|
+
if (!key || !workspace_id || !workspace_name || !email) return null
|
|
53
|
+
return { key, workspace_id, workspace_name, email }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function startCallbackServer(
|
|
57
|
+
port: number,
|
|
58
|
+
expectedState: string,
|
|
59
|
+
): { promise: Promise<CallbackResult>; close: () => void } {
|
|
60
|
+
let resolveCb: (v: CallbackResult) => void
|
|
61
|
+
let rejectCb: (e: Error) => void
|
|
62
|
+
const promise = new Promise<CallbackResult>((res, rej) => { resolveCb = res; rejectCb = rej })
|
|
63
|
+
|
|
64
|
+
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
65
|
+
if (!req.url?.startsWith('/callback')) {
|
|
66
|
+
res.writeHead(404).end()
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const state = new URL(req.url, 'http://localhost').searchParams.get('state')
|
|
70
|
+
if (state !== expectedState) {
|
|
71
|
+
res.writeHead(403).end('State mismatch')
|
|
72
|
+
rejectCb(new Error('State mismatch — possible CSRF. Try again.'))
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
const result = parseCallback(req.url)
|
|
76
|
+
if (!result) {
|
|
77
|
+
res.writeHead(400).end('Missing parameters')
|
|
78
|
+
rejectCb(new Error('Incomplete callback — missing parameters.'))
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
res.writeHead(200, { 'Content-Type': 'text/html' }).end(callbackHtml())
|
|
82
|
+
resolveCb(result)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
server.listen(port, '127.0.0.1')
|
|
86
|
+
return { promise, close: () => server.close() }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function openBrowserLogin(port: number, state: string) {
|
|
90
|
+
const url = `${getBaseUrl()}/auth/cli?port=${port}&state=${state}`
|
|
91
|
+
const open = await import('open').then(m => m.default).catch(() => null)
|
|
92
|
+
if (open) await open(url).catch(() => {})
|
|
93
|
+
else console.log(chalk.yellow(' Browser did not open automatically.'))
|
|
94
|
+
console.log(chalk.dim(` If needed, visit: ${url}\n`))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function waitForCallback(
|
|
98
|
+
promise: Promise<CallbackResult>,
|
|
99
|
+
): Promise<CallbackResult> {
|
|
100
|
+
return Promise.race([
|
|
101
|
+
promise,
|
|
102
|
+
new Promise<never>((_, reject) =>
|
|
103
|
+
setTimeout(() => reject(new Error('Timed out after 120s — no callback received.')), TIMEOUT_MS),
|
|
104
|
+
),
|
|
105
|
+
])
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function browserLogin() {
|
|
109
|
+
const state = generateState()
|
|
110
|
+
const port = await findFreePort()
|
|
111
|
+
const { promise, close } = startCallbackServer(port, state)
|
|
112
|
+
|
|
113
|
+
console.log(chalk.bold('\n Opening browser to authorize...\n'))
|
|
114
|
+
await openBrowserLogin(port, state)
|
|
115
|
+
|
|
116
|
+
const result = await waitForCallback(promise).finally(close)
|
|
117
|
+
|
|
118
|
+
writeConfig({ api_key: result.key, workspace_id: result.workspace_id })
|
|
119
|
+
console.log(chalk.green('✓') + ` Logged in as ${chalk.bold(result.email)} (${result.workspace_name})`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function interactiveLogin() {
|
|
123
|
+
const { password } = await import('@clack/prompts')
|
|
124
|
+
const apiKey = await password({ message: 'Paste your API key (blnk_ak_...):' }) as string
|
|
125
|
+
if (!apiKey?.startsWith('blnk_ak_')) {
|
|
126
|
+
console.error('Error: API key must start with blnk_ak_')
|
|
127
|
+
process.exit(1)
|
|
128
|
+
}
|
|
129
|
+
writeConfig({ api_key: apiKey })
|
|
130
|
+
console.log(chalk.green('✓') + ' Saved to ~/.config/blink/config.toml')
|
|
131
|
+
}
|
|
7
132
|
|
|
8
133
|
export function registerAuthCommands(program: Command) {
|
|
9
134
|
program.command('login')
|
|
@@ -13,7 +138,8 @@ export function registerAuthCommands(program: Command) {
|
|
|
13
138
|
Get your API key at: blink.new → Settings → API Keys (starts with blnk_ak_)
|
|
14
139
|
|
|
15
140
|
Examples:
|
|
16
|
-
$ blink login
|
|
141
|
+
$ blink login Opens browser for one-click auth
|
|
142
|
+
$ blink login --interactive Paste key manually (headless/SSH/CI)
|
|
17
143
|
$ export BLINK_API_KEY=blnk_ak_... Alternative: set env var directly (no login needed)
|
|
18
144
|
|
|
19
145
|
In Blink Claw agents: BLINK_API_KEY is already set — login is not needed.
|
|
@@ -24,28 +150,8 @@ For CI/GitHub Actions: set BLINK_API_KEY as a secret, skip login entirely.
|
|
|
24
150
|
console.log(chalk.green('✓') + ' Already authenticated via BLINK_API_KEY env var.')
|
|
25
151
|
return
|
|
26
152
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (!opts.interactive) {
|
|
30
|
-
console.log(chalk.bold('\n Open this page to get your API key:\n'))
|
|
31
|
-
console.log(` ${chalk.cyan(url)}\n`)
|
|
32
|
-
const open = await import('open').then(m => m.default).catch(() => null)
|
|
33
|
-
if (open) {
|
|
34
|
-
await open(url).catch(() => {})
|
|
35
|
-
console.log(chalk.dim(' (opened in browser)'))
|
|
36
|
-
}
|
|
37
|
-
console.log(chalk.dim(' Then run: blink login --interactive\n'))
|
|
38
|
-
return
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const { password } = await import('@clack/prompts')
|
|
42
|
-
const apiKey = await password({ message: 'Paste your API key (blnk_ak_...):' }) as string
|
|
43
|
-
if (!apiKey?.startsWith('blnk_ak_')) {
|
|
44
|
-
console.error('Error: API key must start with blnk_ak_')
|
|
45
|
-
process.exit(1)
|
|
46
|
-
}
|
|
47
|
-
writeConfig({ api_key: apiKey })
|
|
48
|
-
console.log(chalk.green('✓') + ' Saved to ~/.config/blink/config.toml')
|
|
153
|
+
if (opts.interactive) return interactiveLogin()
|
|
154
|
+
return browserLogin()
|
|
49
155
|
})
|
|
50
156
|
|
|
51
157
|
program.command('logout')
|
|
@@ -60,7 +166,7 @@ For CI/GitHub Actions: set BLINK_API_KEY as a secret, skip login entirely.
|
|
|
60
166
|
.action(async () => {
|
|
61
167
|
const token = resolveToken()
|
|
62
168
|
if (!token) {
|
|
63
|
-
console.error('Error: Not authenticated. Run `blink login
|
|
169
|
+
console.error('Error: Not authenticated. Run `blink login` or set BLINK_API_KEY.')
|
|
64
170
|
process.exit(1)
|
|
65
171
|
}
|
|
66
172
|
|