@hiyve/cli 1.0.13 → 1.0.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hiyve/cli",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Hiyve SDK CLI - Configure npm for private @hiyve packages",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,8 +6,9 @@
6
6
 
7
7
  import chalk from 'chalk';
8
8
  import ora from 'ora';
9
- import { getApiUrl, isDevMode } from '../config.js';
9
+ import { getApiUrl } from '../config.js';
10
10
  import { getCurrentConfig } from '../utils/npmrc.js';
11
+ import { printDevHint, printDevModeBanner, fetchWithTimeout } from '../utils/output.js';
11
12
 
12
13
  /**
13
14
  * List available packages
@@ -16,9 +17,7 @@ export async function list() {
16
17
  console.log('');
17
18
  console.log(chalk.cyan('Available @hiyve Packages'));
18
19
  console.log(chalk.gray('─'.repeat(40)));
19
- if (isDevMode()) {
20
- console.log(chalk.yellow(' Mode: development (--dev)'));
21
- }
20
+ printDevModeBanner();
22
21
  console.log('');
23
22
 
24
23
  // Get current config for auth token
@@ -33,7 +32,7 @@ export async function list() {
33
32
  const spinner = ora('Fetching packages...').start();
34
33
 
35
34
  try {
36
- const response = await fetch(`${getApiUrl()}packages`, {
35
+ const response = await fetchWithTimeout(`${getApiUrl()}packages`, {
37
36
  headers: {
38
37
  Authorization: `Bearer ${config.apiKey}`,
39
38
  },
@@ -44,14 +43,7 @@ export async function list() {
44
43
  const error = await response.json().catch(() => ({ error: 'Unknown error' }));
45
44
  console.log('');
46
45
  console.log(chalk.red(` ${error.error || 'Failed to fetch packages'}`));
47
- if (isDevMode()) {
48
- console.log(chalk.gray(' You are using --dev mode (development registry).'));
49
- console.log(chalk.gray(' If you meant to use production, run without --dev:'));
50
- console.log(chalk.cyan(' npx @hiyve/cli list'));
51
- } else {
52
- console.log(chalk.gray(' If you meant to use the development registry, use --dev:'));
53
- console.log(chalk.cyan(' npx @hiyve/cli --dev list'));
54
- }
46
+ printDevHint('list');
55
47
  process.exit(1);
56
48
  }
57
49
 
@@ -82,18 +74,17 @@ export async function list() {
82
74
  console.log(chalk.gray(' npm install <package-name>'));
83
75
  console.log('');
84
76
  } catch (err) {
85
- spinner.fail('Connection failed');
86
- console.log('');
87
- console.log(chalk.red(` ${err.message}`));
88
- console.log(chalk.gray(' Please check your internet connection.'));
89
- if (isDevMode()) {
90
- console.log(chalk.gray(' You are using --dev mode (development registry).'));
91
- console.log(chalk.gray(' If you meant to use production, run without --dev:'));
92
- console.log(chalk.cyan(' npx @hiyve/cli list'));
77
+ if (err.name === 'AbortError') {
78
+ spinner.fail('Request timed out');
79
+ console.log('');
80
+ console.log(chalk.gray(' The server did not respond within 15 seconds.'));
93
81
  } else {
94
- console.log(chalk.gray(' If you meant to use the development registry, use --dev:'));
95
- console.log(chalk.cyan(' npx @hiyve/cli --dev list'));
82
+ spinner.fail('Connection failed');
83
+ console.log('');
84
+ console.log(chalk.red(` ${err.message}`));
96
85
  }
86
+ console.log(chalk.gray(' Please check your internet connection.'));
87
+ printDevHint('list');
97
88
  process.exit(1);
98
89
  }
99
90
  }
@@ -8,7 +8,8 @@ import prompts from 'prompts';
8
8
  import chalk from 'chalk';
9
9
  import ora from 'ora';
10
10
  import { configureNpmrc } from '../utils/npmrc.js';
11
- import { getApiUrl, getRegistryUrl, isDevMode } from '../config.js';
11
+ import { getApiUrl, getRegistryUrl } from '../config.js';
12
+ import { printDevHint, printDevModeBanner, fetchWithTimeout } from '../utils/output.js';
12
13
 
13
14
  /**
14
15
  * Login to Hiyve and configure npm
@@ -21,9 +22,7 @@ export async function login(options) {
21
22
  console.log('');
22
23
  console.log(chalk.cyan('Hiyve SDK Authentication'));
23
24
  console.log(chalk.gray('─'.repeat(40)));
24
- if (isDevMode()) {
25
- console.log(chalk.yellow(' Mode: development (--dev)'));
26
- }
25
+ printDevModeBanner();
27
26
  console.log('');
28
27
 
29
28
  // Prompt for API key if not provided
@@ -78,7 +77,7 @@ export async function login(options) {
78
77
  const spinner = ora('Verifying API key...').start();
79
78
 
80
79
  try {
81
- const response = await fetch(`${getApiUrl()}verify`, {
80
+ const response = await fetchWithTimeout(`${getApiUrl()}verify`, {
82
81
  headers: {
83
82
  Authorization: `Bearer ${apiKey}`,
84
83
  },
@@ -89,36 +88,32 @@ export async function login(options) {
89
88
  spinner.fail('API key verification failed');
90
89
  console.log('');
91
90
  console.log(chalk.red(` ${error.error || 'Invalid API key'}`));
92
- if (isDevMode()) {
93
- console.log(chalk.gray(' You are using --dev mode (development registry).'));
94
- console.log(chalk.gray(' If you meant to use production, run without --dev:'));
95
- console.log(chalk.cyan(' npx @hiyve/cli login'));
96
- } else {
97
- console.log(chalk.gray(' If you are using a test key (pk_test_*), use --dev mode:'));
98
- console.log(chalk.cyan(' npx @hiyve/cli --dev login'));
99
- }
91
+ printDevHint('login');
100
92
  process.exit(1);
101
93
  }
102
94
 
103
95
  const data = await response.json();
104
96
  spinner.succeed('API key verified');
105
97
 
98
+ // Server returns a masked key — always mask client-side as a safety net
106
99
  if (data.apiKey) {
107
- console.log(chalk.gray(` Account: ${data.apiKey}`));
100
+ const display = data.apiKey.length > 12
101
+ ? `${data.apiKey.slice(0, 8)}...${data.apiKey.slice(-4)}`
102
+ : data.apiKey;
103
+ console.log(chalk.gray(` Account: ${display}`));
108
104
  }
109
105
  } catch (err) {
110
- spinner.fail('Connection failed');
111
- console.log('');
112
- console.log(chalk.red(` ${err.message}`));
113
- console.log(chalk.gray(' Please check your internet connection.'));
114
- if (isDevMode()) {
115
- console.log(chalk.gray(' You are using --dev mode (development registry).'));
116
- console.log(chalk.gray(' If you meant to use production, run without --dev:'));
117
- console.log(chalk.cyan(' npx @hiyve/cli login'));
106
+ if (err.name === 'AbortError') {
107
+ spinner.fail('Request timed out');
108
+ console.log('');
109
+ console.log(chalk.gray(' The server did not respond within 15 seconds.'));
118
110
  } else {
119
- console.log(chalk.gray(' If you meant to use the development registry, use --dev:'));
120
- console.log(chalk.cyan(' npx @hiyve/cli --dev login'));
111
+ spinner.fail('Connection failed');
112
+ console.log('');
113
+ console.log(chalk.red(` ${err.message}`));
121
114
  }
115
+ console.log(chalk.gray(' Please check your internet connection.'));
116
+ printDevHint('login');
122
117
  process.exit(1);
123
118
  }
124
119
 
@@ -142,7 +137,7 @@ export async function login(options) {
142
137
 
143
138
  // Fetch available packages from registry
144
139
  try {
145
- const packagesResponse = await fetch(`${getApiUrl()}packages`, {
140
+ const packagesResponse = await fetchWithTimeout(`${getApiUrl()}packages`, {
146
141
  headers: { Authorization: `Bearer ${apiKey}` },
147
142
  });
148
143
 
package/src/config.js CHANGED
@@ -28,10 +28,10 @@ export function getApiUrl() {
28
28
 
29
29
  /**
30
30
  * Get the registry URL for ~/.npmrc (where npm fetches tarballs).
31
- * Must match the API URL since tarballs are served through the API.
31
+ * Matches the API URL tarballs are served through the same API.
32
32
  */
33
33
  export function getRegistryUrl() {
34
- return _devMode ? DEV_API_URL : PROD_API_URL;
34
+ return getApiUrl();
35
35
  }
36
36
 
37
37
  /**
@@ -10,6 +10,28 @@ import path from 'path';
10
10
 
11
11
  const NPMRC_PATH = path.join(os.homedir(), '.npmrc');
12
12
 
13
+ // Known Hiyve registry hosts (current and historical)
14
+ const HIYVE_HOSTS = ['//console.hiyve.dev/', '//api.hiyve.dev/', '//api.muziemedia.com/'];
15
+
16
+ /**
17
+ * Check if a line in .npmrc is a Hiyve-related config line.
18
+ * Only matches lines that reference a known Hiyve host or @hiyve scope.
19
+ * Will NOT match third-party tokens that happen to start with sk_/pk_ (e.g. Stripe).
20
+ */
21
+ function isHiyveConfigLine(line) {
22
+ const trimmed = line.trim();
23
+ if (trimmed.startsWith('@hiyve:registry')) return true;
24
+ if (HIYVE_HOSTS.some((host) => trimmed.includes(host))) return true;
25
+ return false;
26
+ }
27
+
28
+ /**
29
+ * Filter out all Hiyve-related lines from .npmrc content.
30
+ */
31
+ function filterOutHiyveLines(content) {
32
+ return content.split('\n').filter((line) => !isHiyveConfigLine(line));
33
+ }
34
+
13
35
  /**
14
36
  * Configure ~/.npmrc for Hiyve registry
15
37
  * @param {string} registryUrl - The registry URL (e.g., https://api.hiyve.dev/registry/)
@@ -23,19 +45,8 @@ export async function configureNpmrc(registryUrl, apiKey) {
23
45
  content = fs.readFileSync(NPMRC_PATH, 'utf8');
24
46
  }
25
47
 
26
- // Remove any existing @hiyve config lines (both old and new registry URLs)
27
- const lines = content.split('\n').filter((line) => {
28
- const trimmed = line.trim();
29
- return (
30
- !trimmed.startsWith('@hiyve:registry') &&
31
- !trimmed.includes('//console.hiyve.dev/') &&
32
- !trimmed.includes('//api.hiyve.dev/') &&
33
- !trimmed.includes('//api.muziemedia.com/') &&
34
- !trimmed.includes(':_authToken=mk_') &&
35
- !trimmed.includes(':_authToken=sk_') &&
36
- !trimmed.includes(':_authToken=pk_')
37
- );
38
- });
48
+ // Remove any existing Hiyve config lines (scoped to known Hiyve hosts)
49
+ const lines = filterOutHiyveLines(content);
39
50
 
40
51
  // Parse the registry URL to derive the auth token path
41
52
  const url = new URL(registryUrl);
@@ -47,7 +58,7 @@ export async function configureNpmrc(registryUrl, apiKey) {
47
58
 
48
59
  // Write back, removing empty lines at start/end
49
60
  const finalContent = lines.filter((line) => line.trim()).join('\n') + '\n';
50
- fs.writeFileSync(NPMRC_PATH, finalContent, 'utf8');
61
+ fs.writeFileSync(NPMRC_PATH, finalContent, { encoding: 'utf8', mode: 0o600 });
51
62
  }
52
63
 
53
64
  /**
@@ -60,23 +71,12 @@ export async function removeNpmrc() {
60
71
 
61
72
  const content = fs.readFileSync(NPMRC_PATH, 'utf8');
62
73
 
63
- // Remove @hiyve config lines (both old and new registry URLs)
64
- const lines = content.split('\n').filter((line) => {
65
- const trimmed = line.trim();
66
- return (
67
- !trimmed.startsWith('@hiyve:registry') &&
68
- !trimmed.includes('//console.hiyve.dev/') &&
69
- !trimmed.includes('//api.hiyve.dev/') &&
70
- !trimmed.includes('//api.muziemedia.com/') &&
71
- !trimmed.includes(':_authToken=mk_') &&
72
- !trimmed.includes(':_authToken=sk_') &&
73
- !trimmed.includes(':_authToken=pk_')
74
- );
75
- });
74
+ // Remove Hiyve config lines (scoped to known Hiyve hosts)
75
+ const lines = filterOutHiyveLines(content);
76
76
 
77
77
  // Write back
78
78
  const finalContent = lines.filter((line) => line.trim()).join('\n');
79
- fs.writeFileSync(NPMRC_PATH, finalContent ? finalContent + '\n' : '', 'utf8');
79
+ fs.writeFileSync(NPMRC_PATH, finalContent ? finalContent + '\n' : '', { encoding: 'utf8', mode: 0o600 });
80
80
  }
81
81
 
82
82
  /**
@@ -94,10 +94,11 @@ export function getCurrentConfig() {
94
94
  // Find registry line
95
95
  const registryLine = lines.find((line) => line.trim().startsWith('@hiyve:registry'));
96
96
 
97
- // Find token line (pk_*, sk_*, or legacy mk_*)
97
+ // Find token line on a known Hiyve host
98
98
  const tokenLine = lines.find((line) => {
99
99
  const trimmed = line.trim();
100
- return trimmed.includes(':_authToken=pk_') || trimmed.includes(':_authToken=sk_') || trimmed.includes(':_authToken=mk_');
100
+ return HIYVE_HOSTS.some((host) => trimmed.includes(host)) &&
101
+ (trimmed.includes(':_authToken=pk_') || trimmed.includes(':_authToken=sk_') || trimmed.includes(':_authToken=mk_'));
101
102
  });
102
103
 
103
104
  if (!registryLine || !tokenLine) {
@@ -114,7 +115,7 @@ export function getCurrentConfig() {
114
115
  return {
115
116
  apiKey,
116
117
  maskedApiKey,
117
- registryUrl: registryLine.split('=')[1]?.trim(),
118
+ registryUrl: registryLine.substring(registryLine.indexOf('=') + 1).trim(),
118
119
  };
119
120
  }
120
121
 
@@ -139,6 +139,39 @@ describe('configureNpmrc', () => {
139
139
  expect(content).toContain('api.hiyve.dev');
140
140
  });
141
141
 
142
+ it('does not delete third-party sk_ tokens (e.g. Stripe)', async () => {
143
+ const existingContent = [
144
+ '@hiyve:registry=https://api.hiyve.dev/registry/',
145
+ '//api.hiyve.dev/registry/:_authToken=pk_live_hiyvekey123',
146
+ '//npm.stripe.com/:_authToken=sk_live_stripekey456',
147
+ 'other-config=value',
148
+ ].join('\n');
149
+
150
+ fs.existsSync.mockReturnValue(true);
151
+ fs.readFileSync.mockReturnValue(existingContent);
152
+ fs.writeFileSync.mockImplementation(() => {});
153
+
154
+ await configureNpmrc('https://api.hiyve.dev/registry/', 'pk_live_newkey789');
155
+
156
+ const [, content] = fs.writeFileSync.mock.calls[0];
157
+ // Hiyve lines replaced
158
+ expect(content).not.toContain('pk_live_hiyvekey123');
159
+ expect(content).toContain(':_authToken=pk_live_newkey789');
160
+ // Stripe token preserved
161
+ expect(content).toContain('//npm.stripe.com/:_authToken=sk_live_stripekey456');
162
+ });
163
+
164
+ it('writes correct auth path for dev registry URL', async () => {
165
+ fs.existsSync.mockReturnValue(false);
166
+ fs.writeFileSync.mockImplementation(() => {});
167
+
168
+ await configureNpmrc('https://api.muziemedia.com/registry/', 'pk_test_abc123def456');
169
+
170
+ const [, content] = fs.writeFileSync.mock.calls[0];
171
+ expect(content).toContain('@hiyve:registry=https://api.muziemedia.com/registry/');
172
+ expect(content).toContain('//api.muziemedia.com/registry/:_authToken=pk_test_abc123def456');
173
+ });
174
+
142
175
  it('preserves non-hiyve lines', async () => {
143
176
  const existingContent = [
144
177
  'registry=https://registry.npmjs.org/',
@@ -163,8 +196,8 @@ describe('configureNpmrc', () => {
163
196
 
164
197
  await configureNpmrc('https://api.hiyve.dev/registry/', 'sk_live_1a2b3c4d5e6f');
165
198
 
166
- const [, content, encoding] = fs.writeFileSync.mock.calls[0];
167
- expect(encoding).toBe('utf8');
199
+ const [, content, options] = fs.writeFileSync.mock.calls[0];
200
+ expect(options).toEqual({ encoding: 'utf8', mode: 0o600 });
168
201
 
169
202
  const lines = content.split('\n').filter((l) => l.trim());
170
203
  const registryLine = lines.find((l) => l.startsWith('@hiyve:registry='));
@@ -281,6 +314,27 @@ describe('removeNpmrc', () => {
281
314
  expect(content).not.toContain('mk_abc123');
282
315
  });
283
316
 
317
+ it('does not delete third-party sk_ tokens (e.g. Stripe)', async () => {
318
+ const existingContent = [
319
+ 'registry=https://registry.npmjs.org/',
320
+ '@hiyve:registry=https://api.hiyve.dev/registry/',
321
+ '//api.hiyve.dev/registry/:_authToken=pk_live_hiyvekey123',
322
+ '//npm.stripe.com/:_authToken=sk_live_stripekey456',
323
+ ].join('\n');
324
+
325
+ fs.existsSync.mockReturnValue(true);
326
+ fs.readFileSync.mockReturnValue(existingContent);
327
+ fs.writeFileSync.mockImplementation(() => {});
328
+
329
+ await removeNpmrc();
330
+
331
+ const [, content] = fs.writeFileSync.mock.calls[0];
332
+ expect(content).not.toContain('@hiyve:registry');
333
+ expect(content).not.toContain('pk_live_hiyvekey123');
334
+ // Third-party token preserved
335
+ expect(content).toContain('//npm.stripe.com/:_authToken=sk_live_stripekey456');
336
+ });
337
+
284
338
  it('preserves non-hiyve lines', async () => {
285
339
  const existingContent = [
286
340
  'registry=https://registry.npmjs.org/',
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Shared CLI output helpers
3
+ */
4
+
5
+ import chalk from 'chalk';
6
+ import { isDevMode } from '../config.js';
7
+
8
+ const FETCH_TIMEOUT_MS = 15_000;
9
+
10
+ /**
11
+ * Print a --dev mode hint when a CLI command fails.
12
+ * Suggests the opposite mode to help the user try the right registry.
13
+ * @param {string} command - The CLI command name (e.g., 'login', 'list')
14
+ */
15
+ export function printDevHint(command) {
16
+ if (isDevMode()) {
17
+ console.log(chalk.gray(' You are using --dev mode (development registry).'));
18
+ console.log(chalk.gray(' If you meant to use production, run without --dev:'));
19
+ console.log(chalk.cyan(` npx @hiyve/cli ${command}`));
20
+ } else {
21
+ console.log(chalk.gray(' If you meant to use the development registry, use --dev:'));
22
+ console.log(chalk.cyan(` npx @hiyve/cli --dev ${command}`));
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Print the dev mode banner if --dev is active.
28
+ */
29
+ export function printDevModeBanner() {
30
+ if (isDevMode()) {
31
+ console.log(chalk.yellow(' Mode: development (--dev)'));
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Create a fetch call with a timeout via AbortController.
37
+ * @param {string} url
38
+ * @param {RequestInit} [options]
39
+ * @returns {Promise<Response>}
40
+ */
41
+ export async function fetchWithTimeout(url, options = {}) {
42
+ const controller = new AbortController();
43
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
44
+
45
+ try {
46
+ return await fetch(url, { ...options, signal: controller.signal });
47
+ } finally {
48
+ clearTimeout(timeout);
49
+ }
50
+ }