@blinkdotnew/cli 0.4.3 → 0.5.0

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