@hiyve/cli 1.0.14 → 1.0.18
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 +23 -9
- package/bin/hiyve.js +1 -1
- package/package.json +4 -5
- package/src/commands/init.js +3 -3
- package/src/commands/list.js +7 -13
- package/src/commands/login.js +9 -25
- package/src/commands/whoami.js +3 -12
- package/src/config.js +21 -12
- package/src/config.test.js +23 -9
- package/src/utils/npmrc.js +11 -2
- package/src/utils/output.js +5 -5
- package/src/utils/registryApi.js +145 -0
- package/src/utils/registryApi.test.js +169 -0
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Authenticate with Hiyve and configure npm for @hiyve packages.
|
|
|
27
27
|
npx @hiyve/cli login
|
|
28
28
|
|
|
29
29
|
# Non-interactive (CI/CD)
|
|
30
|
-
npx @hiyve/cli login --key
|
|
30
|
+
npx @hiyve/cli login --key pk_live_your_api_key_here
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
### `logout`
|
|
@@ -67,9 +67,11 @@ Templates: `basic`, `ai`, `full`
|
|
|
67
67
|
|
|
68
68
|
## Getting Your API Key
|
|
69
69
|
|
|
70
|
-
1. Log in to the [Hiyve Developer Console](https://
|
|
70
|
+
1. Log in to the [Hiyve Developer Console](https://console.hiyve.dev)
|
|
71
71
|
2. Navigate to **API Keys** in the sidebar
|
|
72
|
-
3. Copy your
|
|
72
|
+
3. Copy your **API key** (starts with `pk_test_` or `pk_live_`). The client
|
|
73
|
+
secret (`sk_*`) is for minting room tokens server-side and is **not** used
|
|
74
|
+
for registry login — the CLI rejects it.
|
|
73
75
|
|
|
74
76
|
## What Does Login Do?
|
|
75
77
|
|
|
@@ -78,8 +80,8 @@ The `login` command:
|
|
|
78
80
|
1. Validates your API key with the Hiyve registry
|
|
79
81
|
2. Adds two lines to your `~/.npmrc` file:
|
|
80
82
|
```
|
|
81
|
-
@hiyve:registry=https://
|
|
82
|
-
//
|
|
83
|
+
@hiyve:registry=https://registry.muziemedia.com/
|
|
84
|
+
//registry.muziemedia.com/:_authToken=pk_live_...
|
|
83
85
|
```
|
|
84
86
|
|
|
85
87
|
This tells npm to fetch `@hiyve/*` packages from the private Hiyve registry instead of the public npm registry.
|
|
@@ -89,8 +91,8 @@ This tells npm to fetch `@hiyve/*` packages from the private Hiyve registry inst
|
|
|
89
91
|
For automated deployments, add registry credentials as environment variables in your CI/CD pipeline. Your project `.npmrc` should use variable expansion:
|
|
90
92
|
|
|
91
93
|
```ini
|
|
92
|
-
@hiyve:registry=https://
|
|
93
|
-
//
|
|
94
|
+
@hiyve:registry=https://registry.muziemedia.com/
|
|
95
|
+
//registry.muziemedia.com/:_authToken=${HIYVE_API_KEY}
|
|
94
96
|
```
|
|
95
97
|
|
|
96
98
|
Set `HIYVE_API_KEY` in your CI environment (GitHub Actions secrets, AWS SSM, etc.). npm natively expands `${ENV_VAR}` in `.npmrc` files — no custom tooling needed.
|
|
@@ -99,7 +101,7 @@ Set `HIYVE_API_KEY` in your CI environment (GitHub Actions secrets, AWS SSM, etc
|
|
|
99
101
|
|
|
100
102
|
### "Invalid API key" error
|
|
101
103
|
|
|
102
|
-
- Make sure your key starts with `
|
|
104
|
+
- Make sure your key starts with `pk_test_` or `pk_live_` (not `sk_` — that is the client secret)
|
|
103
105
|
- Verify your account is active in the developer console
|
|
104
106
|
|
|
105
107
|
### "Connection failed" error
|
|
@@ -107,6 +109,18 @@ Set `HIYVE_API_KEY` in your CI environment (GitHub Actions secrets, AWS SSM, etc
|
|
|
107
109
|
- Check your internet connection
|
|
108
110
|
- The registry may be temporarily unavailable
|
|
109
111
|
|
|
112
|
+
### "401 Unauthorized" when running `npx @hiyve/cli login`
|
|
113
|
+
|
|
114
|
+
If you've previously logged in and your token has expired, `npx` may fail because npm routes all `@hiyve/*` packages (including the CLI) through the private registry. To fix this, remove the stale registry line from your `~/.npmrc` and re-run login:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Remove the stale @hiyve registry config
|
|
118
|
+
npm config delete @hiyve:registry
|
|
119
|
+
|
|
120
|
+
# Now login will work again
|
|
121
|
+
npx @hiyve/cli login
|
|
122
|
+
```
|
|
123
|
+
|
|
110
124
|
### Packages not installing
|
|
111
125
|
|
|
112
126
|
After login, verify your configuration:
|
|
@@ -129,6 +143,6 @@ cat ~/.npmrc | grep hiyve
|
|
|
129
143
|
|
|
130
144
|
## Support
|
|
131
145
|
|
|
132
|
-
- Documentation: https://
|
|
146
|
+
- Documentation: https://sdk.hiyve.dev
|
|
133
147
|
- Issues: https://github.com/hiyve/hiyve-sdk/issues
|
|
134
148
|
- Email: support@hiyve.io
|
package/bin/hiyve.js
CHANGED
|
@@ -18,7 +18,7 @@ program
|
|
|
18
18
|
.name('hiyve')
|
|
19
19
|
.description('Hiyve SDK CLI - Configure npm for private @hiyve packages')
|
|
20
20
|
.version('1.0.0')
|
|
21
|
-
.option('--dev', '
|
|
21
|
+
.option('--dev', 'Retained for compatibility — there is no separate dev registry; same host as production')
|
|
22
22
|
.hook('preAction', () => {
|
|
23
23
|
if (program.opts().dev) {
|
|
24
24
|
setDevMode(true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hiyve/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
4
4
|
"description": "Hiyve SDK CLI - Configure npm for private @hiyve packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
"src"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"test": "vitest run",
|
|
16
|
-
"test:watch": "vitest"
|
|
17
|
-
"deploy": "./deploy.sh"
|
|
15
|
+
"test": "vitest run --pool=forks --poolOptions.forks.singleFork",
|
|
16
|
+
"test:watch": "vitest"
|
|
18
17
|
},
|
|
19
18
|
"keywords": [
|
|
20
19
|
"hiyve",
|
|
@@ -44,6 +43,6 @@
|
|
|
44
43
|
"prompts": "^2.4.2"
|
|
45
44
|
},
|
|
46
45
|
"devDependencies": {
|
|
47
|
-
"vitest": "^2.
|
|
46
|
+
"vitest": "^3.2.6"
|
|
48
47
|
}
|
|
49
48
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -217,7 +217,7 @@ export default defineConfig({
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
function generateEnvExample(template) {
|
|
220
|
-
let env = `# Hiyve API Key (from
|
|
220
|
+
let env = `# Hiyve API Key (from cloud.hiyve.io)
|
|
221
221
|
HIYVE_API_KEY=pk_live_your_api_key_here
|
|
222
222
|
|
|
223
223
|
# Room configuration
|
|
@@ -227,7 +227,7 @@ HIYVE_SIGNALING_URL=https://signal.hiyve.dev
|
|
|
227
227
|
if (template.features.intelligence) {
|
|
228
228
|
env += `
|
|
229
229
|
# Cloud API (for AI features)
|
|
230
|
-
HIYVE_CLOUD_URL=https://
|
|
230
|
+
HIYVE_CLOUD_URL=https://cloud.hiyve.io
|
|
231
231
|
`;
|
|
232
232
|
}
|
|
233
233
|
|
|
@@ -383,7 +383,7 @@ app.use(express.json());
|
|
|
383
383
|
|
|
384
384
|
const API_KEY = process.env.HIYVE_API_KEY;
|
|
385
385
|
const SIGNALING_URL = process.env.HIYVE_SIGNALING_URL || 'https://signal.hiyve.dev';
|
|
386
|
-
${hasCloud ? `const CLOUD_URL = process.env.HIYVE_CLOUD_URL || 'https://
|
|
386
|
+
${hasCloud ? `const CLOUD_URL = process.env.HIYVE_CLOUD_URL || 'https://cloud.hiyve.io';\n` : ''}
|
|
387
387
|
/**
|
|
388
388
|
* Generate a room token for the client.
|
|
389
389
|
* In production, add your own authentication before issuing tokens.
|
package/src/commands/list.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import ora from 'ora';
|
|
9
|
-
import {
|
|
9
|
+
import { fetchPackageCatalogue } from '../utils/registryApi.js';
|
|
10
10
|
import { getCurrentConfig } from '../utils/npmrc.js';
|
|
11
|
-
import { printDevHint, printDevModeBanner
|
|
11
|
+
import { printDevHint, printDevModeBanner } from '../utils/output.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* List available packages
|
|
@@ -32,22 +32,16 @@ export async function list() {
|
|
|
32
32
|
const spinner = ora('Fetching packages...').start();
|
|
33
33
|
|
|
34
34
|
try {
|
|
35
|
-
const
|
|
36
|
-
headers: {
|
|
37
|
-
Authorization: `Bearer ${config.apiKey}`,
|
|
38
|
-
},
|
|
39
|
-
});
|
|
35
|
+
const data = await fetchPackageCatalogue(config.apiKey);
|
|
40
36
|
|
|
41
|
-
if (!
|
|
37
|
+
if (!data.ok) {
|
|
42
38
|
spinner.fail('Failed to fetch packages');
|
|
43
|
-
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
44
39
|
console.log('');
|
|
45
|
-
console.log(chalk.red(` ${
|
|
40
|
+
console.log(chalk.red(` ${data.error}`));
|
|
46
41
|
printDevHint('list');
|
|
47
42
|
process.exit(1);
|
|
48
43
|
}
|
|
49
44
|
|
|
50
|
-
const data = await response.json();
|
|
51
45
|
spinner.succeed(`Found ${data.total} packages`);
|
|
52
46
|
console.log('');
|
|
53
47
|
|
|
@@ -55,7 +49,7 @@ export async function list() {
|
|
|
55
49
|
if (data.sdk?.length > 0) {
|
|
56
50
|
console.log(chalk.white.bold('SDK Packages:'));
|
|
57
51
|
for (const pkg of data.sdk) {
|
|
58
|
-
console.log(chalk.cyan(` ${pkg.name}`));
|
|
52
|
+
console.log(chalk.cyan(` ${pkg.name}`) + (pkg.version ? chalk.gray(` ${pkg.version}`) : ''));
|
|
59
53
|
}
|
|
60
54
|
console.log('');
|
|
61
55
|
}
|
|
@@ -64,7 +58,7 @@ export async function list() {
|
|
|
64
58
|
if (data.components?.length > 0) {
|
|
65
59
|
console.log(chalk.white.bold('Component Packages:'));
|
|
66
60
|
for (const pkg of data.components) {
|
|
67
|
-
console.log(chalk.cyan(` ${pkg.name}`));
|
|
61
|
+
console.log(chalk.cyan(` ${pkg.name}`) + (pkg.version ? chalk.gray(` ${pkg.version}`) : ''));
|
|
68
62
|
}
|
|
69
63
|
console.log('');
|
|
70
64
|
}
|
package/src/commands/login.js
CHANGED
|
@@ -8,8 +8,9 @@ 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 {
|
|
12
|
-
import {
|
|
11
|
+
import { getRegistryUrl } from '../config.js';
|
|
12
|
+
import { verifyApiKey, fetchPackageCatalogue } from '../utils/registryApi.js';
|
|
13
|
+
import { printDevHint, printDevModeBanner } from '../utils/output.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Login to Hiyve and configure npm
|
|
@@ -77,31 +78,18 @@ export async function login(options) {
|
|
|
77
78
|
const spinner = ora('Verifying API key...').start();
|
|
78
79
|
|
|
79
80
|
try {
|
|
80
|
-
const
|
|
81
|
-
headers: {
|
|
82
|
-
Authorization: `Bearer ${apiKey}`,
|
|
83
|
-
},
|
|
84
|
-
});
|
|
81
|
+
const result = await verifyApiKey(apiKey);
|
|
85
82
|
|
|
86
|
-
if (!
|
|
87
|
-
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
83
|
+
if (!result.ok) {
|
|
88
84
|
spinner.fail('API key verification failed');
|
|
89
85
|
console.log('');
|
|
90
|
-
console.log(chalk.red(` ${
|
|
86
|
+
console.log(chalk.red(` ${result.error}`));
|
|
91
87
|
printDevHint('login');
|
|
92
88
|
process.exit(1);
|
|
93
89
|
}
|
|
94
90
|
|
|
95
|
-
const data = await response.json();
|
|
96
91
|
spinner.succeed('API key verified');
|
|
97
|
-
|
|
98
|
-
// Server returns a masked key — always mask client-side as a safety net
|
|
99
|
-
if (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}`));
|
|
104
|
-
}
|
|
92
|
+
console.log(chalk.gray(` Account: ${result.apiKey}`));
|
|
105
93
|
} catch (err) {
|
|
106
94
|
if (err.name === 'AbortError') {
|
|
107
95
|
spinner.fail('Request timed out');
|
|
@@ -137,13 +125,9 @@ export async function login(options) {
|
|
|
137
125
|
|
|
138
126
|
// Fetch available packages from registry
|
|
139
127
|
try {
|
|
140
|
-
const
|
|
141
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
if (packagesResponse.ok) {
|
|
145
|
-
const packages = await packagesResponse.json();
|
|
128
|
+
const packages = await fetchPackageCatalogue(apiKey);
|
|
146
129
|
|
|
130
|
+
if (packages.ok) {
|
|
147
131
|
console.log(`You can now install ${packages.total} Hiyve packages:`);
|
|
148
132
|
console.log('');
|
|
149
133
|
|
package/src/commands/whoami.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import ora from 'ora';
|
|
9
9
|
import { getCurrentConfig } from '../utils/npmrc.js';
|
|
10
|
-
import {
|
|
10
|
+
import { verifyApiKey } from '../utils/registryApi.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Show current authentication status
|
|
@@ -39,19 +39,10 @@ export async function whoami() {
|
|
|
39
39
|
const spinner = ora('Verifying with server...').start();
|
|
40
40
|
|
|
41
41
|
try {
|
|
42
|
-
const
|
|
43
|
-
headers: {
|
|
44
|
-
Authorization: `Bearer ${config.apiKey}`,
|
|
45
|
-
},
|
|
46
|
-
});
|
|
42
|
+
const result = await verifyApiKey(config.apiKey);
|
|
47
43
|
|
|
48
|
-
if (
|
|
49
|
-
const data = await response.json();
|
|
44
|
+
if (result.ok) {
|
|
50
45
|
spinner.succeed('API key is valid');
|
|
51
|
-
|
|
52
|
-
if (data.email) {
|
|
53
|
-
console.log(` Email: ${chalk.cyan(data.email)}`);
|
|
54
|
-
}
|
|
55
46
|
} else {
|
|
56
47
|
spinner.warn('API key may be invalid or expired');
|
|
57
48
|
console.log('');
|
package/src/config.js
CHANGED
|
@@ -1,37 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hiyve CLI Configuration
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* One host: registry.muziemedia.com, the Verdaccio registry `changeset
|
|
5
|
+
* publish` writes to. It is both where npm fetches tarballs (what `login`
|
|
6
|
+
* puts in ~/.npmrc) and where the CLI verifies keys and lists packages,
|
|
7
|
+
* through standard npm endpoints — see utils/registryApi.js.
|
|
8
|
+
*
|
|
9
|
+
* The CLI used to talk to a separate API at cloud.hiyve.io/registry/
|
|
10
|
+
* ({verify,packages}). That is the retired S3-backed registry: a frozen
|
|
11
|
+
* catalogue (it still lists packages that no longer exist and reports
|
|
12
|
+
* versions five majors behind) whose tarball URLs once named
|
|
13
|
+
* api.muziemedia.com — a host with no DNS record. Nothing here should point
|
|
14
|
+
* at it again. `--dev` remains a valid flag but there is only one registry,
|
|
15
|
+
* so it changes nothing (there has been no separate dev host since 2026-08-24).
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
|
-
const
|
|
10
|
-
const DEV_API_URL = 'https://api.muziemedia.com/registry/';
|
|
18
|
+
const REGISTRY_URL_VALUE = 'https://registry.muziemedia.com/';
|
|
11
19
|
|
|
12
20
|
let _devMode = false;
|
|
13
21
|
|
|
14
22
|
/**
|
|
15
|
-
* Enable dev mode (
|
|
23
|
+
* Enable dev mode (there is no separate dev cloud — same host as prod)
|
|
16
24
|
*/
|
|
17
25
|
export function setDevMode(enabled) {
|
|
18
26
|
_devMode = enabled;
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
30
|
+
* Base URL for the CLI's own calls (verify, list). This IS the registry —
|
|
31
|
+
* kept as a separate function because index.js exports it and because the
|
|
32
|
+
* two roles were once different hosts. Ignores --dev: one registry.
|
|
24
33
|
*/
|
|
25
34
|
export function getApiUrl() {
|
|
26
|
-
return
|
|
35
|
+
return REGISTRY_URL_VALUE;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
/**
|
|
30
39
|
* Get the registry URL for ~/.npmrc (where npm fetches tarballs).
|
|
31
|
-
*
|
|
40
|
+
* Ignores --dev: there is one registry.
|
|
32
41
|
*/
|
|
33
42
|
export function getRegistryUrl() {
|
|
34
|
-
return
|
|
43
|
+
return REGISTRY_URL_VALUE;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
/**
|
|
@@ -42,7 +51,7 @@ export function isDevMode() {
|
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
// Backwards-compatible named export
|
|
45
|
-
export const REGISTRY_URL =
|
|
54
|
+
export const REGISTRY_URL = REGISTRY_URL_VALUE;
|
|
46
55
|
|
|
47
56
|
export default {
|
|
48
57
|
REGISTRY_URL,
|
package/src/config.test.js
CHANGED
|
@@ -3,30 +3,44 @@ import config, { REGISTRY_URL, getApiUrl, getRegistryUrl, setDevMode } from './c
|
|
|
3
3
|
|
|
4
4
|
describe('config', () => {
|
|
5
5
|
it('exports REGISTRY_URL as a named export', () => {
|
|
6
|
-
expect(REGISTRY_URL).toBe('https://
|
|
6
|
+
expect(REGISTRY_URL).toBe('https://registry.muziemedia.com/');
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
it('default export contains REGISTRY_URL', () => {
|
|
10
10
|
expect(config).toHaveProperty('REGISTRY_URL');
|
|
11
|
-
expect(config.REGISTRY_URL).toBe('https://
|
|
11
|
+
expect(config.REGISTRY_URL).toBe('https://registry.muziemedia.com/');
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
it('getApiUrl
|
|
14
|
+
it('getApiUrl is the registry — there is no separate CLI API host', () => {
|
|
15
15
|
setDevMode(false);
|
|
16
|
-
expect(getApiUrl()).toBe('https://
|
|
16
|
+
expect(getApiUrl()).toBe('https://registry.muziemedia.com/');
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
it('getApiUrl
|
|
19
|
+
it('getApiUrl ignores dev mode', () => {
|
|
20
20
|
setDevMode(true);
|
|
21
|
-
expect(getApiUrl()).toBe('https://
|
|
21
|
+
expect(getApiUrl()).toBe('https://registry.muziemedia.com/');
|
|
22
22
|
setDevMode(false);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
it('
|
|
25
|
+
it('never points at the retired S3-backed registry', () => {
|
|
26
|
+
// cloud.hiyve.io/registry/ still answers but serves a frozen catalogue
|
|
27
|
+
// (@hiyve/core 2.0.22 there vs 7.0.0 live) and once advertised tarballs
|
|
28
|
+
// on a host with no DNS record. The CLI must verify keys and list
|
|
29
|
+
// packages against the registry npm actually installs from.
|
|
30
|
+
expect(getApiUrl()).not.toContain('cloud.hiyve.io');
|
|
31
|
+
expect(getRegistryUrl()).not.toContain('cloud.hiyve.io');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('getRegistryUrl and getApiUrl are the same host', () => {
|
|
35
|
+
expect(getRegistryUrl()).toBe('https://registry.muziemedia.com/');
|
|
36
|
+
expect(getRegistryUrl()).toBe(getApiUrl());
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('getRegistryUrl does NOT switch with dev mode — there is one registry', () => {
|
|
26
40
|
setDevMode(false);
|
|
27
|
-
|
|
41
|
+
const prod = getRegistryUrl();
|
|
28
42
|
setDevMode(true);
|
|
29
|
-
expect(getRegistryUrl()).toBe(
|
|
43
|
+
expect(getRegistryUrl()).toBe(prod);
|
|
30
44
|
setDevMode(false);
|
|
31
45
|
});
|
|
32
46
|
});
|
package/src/utils/npmrc.js
CHANGED
|
@@ -11,7 +11,16 @@ import path from 'path';
|
|
|
11
11
|
const NPMRC_PATH = path.join(os.homedir(), '.npmrc');
|
|
12
12
|
|
|
13
13
|
// Known Hiyve registry hosts (current and historical)
|
|
14
|
-
const HIYVE_HOSTS = [
|
|
14
|
+
const HIYVE_HOSTS = [
|
|
15
|
+
'//registry.muziemedia.com/',
|
|
16
|
+
'//cloud.hiyve.io/',
|
|
17
|
+
// Historical hosts stay listed FOREVER: this array's job is to strip
|
|
18
|
+
// stale entries from a developer's .npmrc, so dropping a retired host
|
|
19
|
+
// leaves its dead token line behind.
|
|
20
|
+
'//console.hiyve.dev/',
|
|
21
|
+
'//api.hiyve.dev/',
|
|
22
|
+
'//api.muziemedia.com/',
|
|
23
|
+
];
|
|
15
24
|
|
|
16
25
|
/**
|
|
17
26
|
* Check if a line in .npmrc is a Hiyve-related config line.
|
|
@@ -34,7 +43,7 @@ function filterOutHiyveLines(content) {
|
|
|
34
43
|
|
|
35
44
|
/**
|
|
36
45
|
* Configure ~/.npmrc for Hiyve registry
|
|
37
|
-
* @param {string} registryUrl - The registry URL (e.g., https://
|
|
46
|
+
* @param {string} registryUrl - The registry URL (e.g., https://registry.muziemedia.com/)
|
|
38
47
|
* @param {string} apiKey - The API key for authentication
|
|
39
48
|
*/
|
|
40
49
|
export async function configureNpmrc(registryUrl, apiKey) {
|
package/src/utils/output.js
CHANGED
|
@@ -14,12 +14,12 @@ const FETCH_TIMEOUT_MS = 15_000;
|
|
|
14
14
|
*/
|
|
15
15
|
export function printDevHint(command) {
|
|
16
16
|
if (isDevMode()) {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// No longer suggests the opposite mode: both use the same host since
|
|
18
|
+
// 2026-08-24, so "try without --dev" sent people hunting a difference
|
|
19
|
+
// that cannot exist.
|
|
20
|
+
console.log(chalk.gray(' --dev no longer selects a different registry — same host as the'));
|
|
21
|
+
console.log(chalk.gray(' default, so re-running without it will not change this result.'));
|
|
19
22
|
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
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's view of the npm registry.
|
|
3
|
+
*
|
|
4
|
+
* Every command used to call a bespoke API at cloud.hiyve.io/registry/
|
|
5
|
+
* ({verify,packages}). That is the retired S3-backed registry: its catalogue
|
|
6
|
+
* is frozen (it still lists @hiyve/auth, which no longer exists, and reports
|
|
7
|
+
* @hiyve/core at 2.0.22 against 7.0.0 live) and it is not where npm installs
|
|
8
|
+
* from. registry.muziemedia.com — the Verdaccio registry `changeset publish`
|
|
9
|
+
* writes to and the host `login` puts in ~/.npmrc — is the only source of
|
|
10
|
+
* truth, and it exposes everything the CLI needs through standard npm
|
|
11
|
+
* endpoints. Verified 2026-09-01:
|
|
12
|
+
*
|
|
13
|
+
* GET /@hiyve/<pkg> 200 with a valid key, 401 without or with
|
|
14
|
+
* a bad one. Packuments and tarballs are
|
|
15
|
+
* both gated.
|
|
16
|
+
* GET /-/whoami 200 `{username}` for a valid key — but
|
|
17
|
+
* ALSO 200 `{}` for a bad key or none, so
|
|
18
|
+
* it cannot be the validity check.
|
|
19
|
+
* GET /-/v1/search?text=@hiyve 200, the live catalogue; unauthenticated.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { getApiUrl } from '../config.js';
|
|
23
|
+
import { fetchWithTimeout } from './output.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A packument that always exists and is small. Fetching it with the key is
|
|
27
|
+
* the validity check: the response code is the registry's own verdict on
|
|
28
|
+
* whether this key can install @hiyve packages.
|
|
29
|
+
*/
|
|
30
|
+
const PROBE_PACKAGE = '@hiyve/cli';
|
|
31
|
+
|
|
32
|
+
/** Verdaccio's search cap is per request; the catalogue is ~70 packages. */
|
|
33
|
+
const SEARCH_PAGE_SIZE = 250;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The packages the getting-started guide installs first. Listed as "SDK";
|
|
37
|
+
* everything else on the registry is listed as a component package. This
|
|
38
|
+
* is presentation only — it does not gate anything.
|
|
39
|
+
*/
|
|
40
|
+
const FOUNDATION_PACKAGES = new Set([
|
|
41
|
+
'@hiyve/core',
|
|
42
|
+
'@hiyve/react',
|
|
43
|
+
'@hiyve/react-ui',
|
|
44
|
+
'@hiyve/react-room',
|
|
45
|
+
'@hiyve/rtc-client',
|
|
46
|
+
'@hiyve/rtc-client-rn',
|
|
47
|
+
'@hiyve/utilities',
|
|
48
|
+
'@hiyve/admin',
|
|
49
|
+
'@hiyve/cloud',
|
|
50
|
+
'@hiyve/identity-client',
|
|
51
|
+
'@hiyve/cli',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Mask an API key for display: `pk_live_...abcd`.
|
|
56
|
+
* @param {string} apiKey
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
export function maskApiKey(apiKey) {
|
|
60
|
+
if (typeof apiKey !== 'string') return '';
|
|
61
|
+
return apiKey.length > 12 ? `${apiKey.slice(0, 8)}...${apiKey.slice(-4)}` : apiKey;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Verify an API key against the registry.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} apiKey
|
|
68
|
+
* @returns {Promise<{ ok: true, apiKey: string } | { ok: false, status: number, error: string }>}
|
|
69
|
+
* `apiKey` in the success shape is already masked.
|
|
70
|
+
* @throws on network failure / timeout (AbortError) — callers own that message.
|
|
71
|
+
*/
|
|
72
|
+
export async function verifyApiKey(apiKey) {
|
|
73
|
+
const response = await fetchWithTimeout(`${getApiUrl()}${PROBE_PACKAGE}`, {
|
|
74
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (response.ok) {
|
|
78
|
+
return { ok: true, apiKey: maskApiKey(apiKey) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const body = await response.json().catch(() => ({}));
|
|
82
|
+
const error =
|
|
83
|
+
body.error ||
|
|
84
|
+
(response.status === 401 || response.status === 403
|
|
85
|
+
? 'Invalid API key'
|
|
86
|
+
: `Registry returned HTTP ${response.status}`);
|
|
87
|
+
return { ok: false, status: response.status, error };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Turn Verdaccio's search response into the catalogue the commands print.
|
|
92
|
+
* Exported for tests; `fetchPackageCatalogue` is the one commands call.
|
|
93
|
+
*
|
|
94
|
+
* @param {{ objects?: Array<{ package?: { name?: string, version?: string, 'dist-tags'?: { latest?: string } } }> }} searchResult
|
|
95
|
+
* @returns {{ total: number, sdk: Array<{name: string, version: string}>, components: Array<{name: string, version: string}> }}
|
|
96
|
+
*/
|
|
97
|
+
export function catalogueFromSearch(searchResult) {
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
const packages = [];
|
|
100
|
+
for (const entry of searchResult?.objects ?? []) {
|
|
101
|
+
const name = entry?.package?.name;
|
|
102
|
+
if (typeof name !== 'string' || !name.startsWith('@hiyve/') || seen.has(name)) continue;
|
|
103
|
+
seen.add(name);
|
|
104
|
+
// Verdaccio's search objects carry the current version as
|
|
105
|
+
// `dist-tags.latest`, not `version` (that is the npmjs.com shape).
|
|
106
|
+
const pkg = entry.package;
|
|
107
|
+
const version = pkg.version ?? pkg['dist-tags']?.latest ?? '';
|
|
108
|
+
packages.push({ name, version });
|
|
109
|
+
}
|
|
110
|
+
packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
total: packages.length,
|
|
114
|
+
sdk: packages.filter((p) => FOUNDATION_PACKAGES.has(p.name)),
|
|
115
|
+
components: packages.filter((p) => !FOUNDATION_PACKAGES.has(p.name)),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* List the @hiyve packages the registry currently serves.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} apiKey
|
|
123
|
+
* @returns {Promise<{ ok: true, total: number, sdk: Array<{name: string, version: string}>, components: Array<{name: string, version: string}> } | { ok: false, status: number, error: string }>}
|
|
124
|
+
* @throws on network failure / timeout (AbortError)
|
|
125
|
+
*/
|
|
126
|
+
export async function fetchPackageCatalogue(apiKey) {
|
|
127
|
+
const url = `${getApiUrl()}-/v1/search?text=${encodeURIComponent('@hiyve')}&size=${SEARCH_PAGE_SIZE}`;
|
|
128
|
+
const response = await fetchWithTimeout(url, {
|
|
129
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
const body = await response.json().catch(() => ({}));
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
status: response.status,
|
|
137
|
+
error: body.error || `Registry returned HTTP ${response.status}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const searchResult = await response.json().catch(() => ({}));
|
|
142
|
+
return { ok: true, ...catalogueFromSearch(searchResult) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default { maskApiKey, verifyApiKey, catalogueFromSearch, fetchPackageCatalogue };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI verifies keys and lists packages against the ONE registry npm
|
|
3
|
+
* installs from, through standard npm endpoints. These tests pin the
|
|
4
|
+
* behaviours that the live registry forced (verified 2026-09-01):
|
|
5
|
+
*
|
|
6
|
+
* - /-/whoami answers 200 `{}` for a bad key, so validity is decided by a
|
|
7
|
+
* gated packument fetch (200 vs 401), never by whoami.
|
|
8
|
+
* - the catalogue comes from /-/v1/search, restricted to the @hiyve scope,
|
|
9
|
+
* de-duplicated, sorted, and split into SDK vs component packages.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
12
|
+
import {
|
|
13
|
+
maskApiKey,
|
|
14
|
+
verifyApiKey,
|
|
15
|
+
catalogueFromSearch,
|
|
16
|
+
fetchPackageCatalogue,
|
|
17
|
+
} from './registryApi.js';
|
|
18
|
+
|
|
19
|
+
const REGISTRY = 'https://registry.muziemedia.com/';
|
|
20
|
+
const KEY = 'pk_live_0123456789abcdef0123456789abcdef01234567';
|
|
21
|
+
|
|
22
|
+
function jsonResponse(status, body) {
|
|
23
|
+
return {
|
|
24
|
+
ok: status >= 200 && status < 300,
|
|
25
|
+
status,
|
|
26
|
+
json: () => Promise.resolve(body),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let fetchMock;
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
fetchMock = vi.fn();
|
|
33
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
34
|
+
});
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.unstubAllGlobals();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('maskApiKey', () => {
|
|
40
|
+
it('keeps the prefix and last four characters only', () => {
|
|
41
|
+
expect(maskApiKey(KEY)).toBe('pk_live_...4567');
|
|
42
|
+
});
|
|
43
|
+
it('leaves a short value alone and tolerates non-strings', () => {
|
|
44
|
+
expect(maskApiKey('pk_short')).toBe('pk_short');
|
|
45
|
+
expect(maskApiKey(undefined)).toBe('');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('verifyApiKey', () => {
|
|
50
|
+
it('probes a gated packument on the registry with the key as a bearer token', async () => {
|
|
51
|
+
fetchMock.mockResolvedValue(jsonResponse(200, { name: '@hiyve/cli' }));
|
|
52
|
+
await verifyApiKey(KEY);
|
|
53
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
54
|
+
expect(url).toBe(`${REGISTRY}@hiyve/cli`);
|
|
55
|
+
expect(init.headers.Authorization).toBe(`Bearer ${KEY}`);
|
|
56
|
+
expect(url).not.toContain('cloud.hiyve.io');
|
|
57
|
+
expect(url).not.toContain('/verify');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('reports a masked key on success, never the raw one', async () => {
|
|
61
|
+
fetchMock.mockResolvedValue(jsonResponse(200, {}));
|
|
62
|
+
const result = await verifyApiKey(KEY);
|
|
63
|
+
expect(result).toEqual({ ok: true, apiKey: 'pk_live_...4567' });
|
|
64
|
+
expect(JSON.stringify(result)).not.toContain(KEY);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('treats 401 as an invalid key', async () => {
|
|
68
|
+
fetchMock.mockResolvedValue(jsonResponse(401, {}));
|
|
69
|
+
const result = await verifyApiKey(KEY);
|
|
70
|
+
expect(result.ok).toBe(false);
|
|
71
|
+
expect(result.status).toBe(401);
|
|
72
|
+
expect(result.error).toBe('Invalid API key');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('surfaces the registry error message when it sends one', async () => {
|
|
76
|
+
fetchMock.mockResolvedValue(jsonResponse(403, { error: 'key revoked' }));
|
|
77
|
+
const result = await verifyApiKey(KEY);
|
|
78
|
+
expect(result).toEqual({ ok: false, status: 403, error: 'key revoked' });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('does not misreport an outage as a bad key', async () => {
|
|
82
|
+
fetchMock.mockResolvedValue({ ok: false, status: 502, json: () => Promise.reject(new Error('html')) });
|
|
83
|
+
const result = await verifyApiKey(KEY);
|
|
84
|
+
expect(result.ok).toBe(false);
|
|
85
|
+
expect(result.error).toBe('Registry returned HTTP 502');
|
|
86
|
+
expect(result.error).not.toMatch(/invalid/i);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('lets network failures propagate for the caller to phrase', async () => {
|
|
90
|
+
const abort = Object.assign(new Error('aborted'), { name: 'AbortError' });
|
|
91
|
+
fetchMock.mockRejectedValue(abort);
|
|
92
|
+
await expect(verifyApiKey(KEY)).rejects.toBe(abort);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('catalogueFromSearch', () => {
|
|
97
|
+
const search = {
|
|
98
|
+
objects: [
|
|
99
|
+
{ package: { name: '@hiyve/react-ui', version: '22.0.0' } },
|
|
100
|
+
{ package: { name: '@hiyve/core', version: '7.0.0' } },
|
|
101
|
+
{ package: { name: 'react', version: '19.0.0' } }, // not ours
|
|
102
|
+
{ package: { name: '@hiyve/core', version: '7.0.0' } }, // duplicate
|
|
103
|
+
{ package: { name: '@hiyve/whiteboard', version: '3.1.0' } },
|
|
104
|
+
{ package: {} }, // malformed
|
|
105
|
+
{},
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
it('keeps only @hiyve packages, de-duplicated and sorted', () => {
|
|
110
|
+
const cat = catalogueFromSearch(search);
|
|
111
|
+
const names = [...cat.sdk, ...cat.components].map((p) => p.name).sort();
|
|
112
|
+
expect(names).toEqual(['@hiyve/core', '@hiyve/react-ui', '@hiyve/whiteboard']);
|
|
113
|
+
expect(cat.total).toBe(3);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('splits foundation packages from component packages', () => {
|
|
117
|
+
const cat = catalogueFromSearch(search);
|
|
118
|
+
expect(cat.sdk.map((p) => p.name)).toEqual(['@hiyve/core', '@hiyve/react-ui']);
|
|
119
|
+
expect(cat.components.map((p) => p.name)).toEqual(['@hiyve/whiteboard']);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('carries the live version through', () => {
|
|
123
|
+
const cat = catalogueFromSearch(search);
|
|
124
|
+
expect(cat.sdk.find((p) => p.name === '@hiyve/core').version).toBe('7.0.0');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("reads the version from Verdaccio's dist-tags.latest (its search has no `version`)", () => {
|
|
128
|
+
const cat = catalogueFromSearch({
|
|
129
|
+
objects: [{ package: { name: '@hiyve/admin', 'dist-tags': { latest: '2.3.0' } } }],
|
|
130
|
+
});
|
|
131
|
+
expect(cat.sdk).toEqual([{ name: '@hiyve/admin', version: '2.3.0' }]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('is empty, not broken, for a missing or empty result', () => {
|
|
135
|
+
expect(catalogueFromSearch(undefined)).toEqual({ total: 0, sdk: [], components: [] });
|
|
136
|
+
expect(catalogueFromSearch({})).toEqual({ total: 0, sdk: [], components: [] });
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('fetchPackageCatalogue', () => {
|
|
141
|
+
it('searches the @hiyve scope on the registry, not the retired catalogue', async () => {
|
|
142
|
+
fetchMock.mockResolvedValue(jsonResponse(200, { objects: [] }));
|
|
143
|
+
await fetchPackageCatalogue(KEY);
|
|
144
|
+
const [url] = fetchMock.mock.calls[0];
|
|
145
|
+
expect(url.startsWith(`${REGISTRY}-/v1/search?`)).toBe(true);
|
|
146
|
+
expect(url).toContain('text=%40hiyve');
|
|
147
|
+
expect(url).not.toContain('cloud.hiyve.io');
|
|
148
|
+
expect(url).not.toContain('/packages');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('returns the catalogue shape the commands print', async () => {
|
|
152
|
+
fetchMock.mockResolvedValue(
|
|
153
|
+
jsonResponse(200, { objects: [{ package: { name: '@hiyve/rtc-client', version: '2.5.0' } }] }),
|
|
154
|
+
);
|
|
155
|
+
const result = await fetchPackageCatalogue(KEY);
|
|
156
|
+
expect(result).toEqual({
|
|
157
|
+
ok: true,
|
|
158
|
+
total: 1,
|
|
159
|
+
sdk: [{ name: '@hiyve/rtc-client', version: '2.5.0' }],
|
|
160
|
+
components: [],
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('reports a failed search without throwing', async () => {
|
|
165
|
+
fetchMock.mockResolvedValue(jsonResponse(500, {}));
|
|
166
|
+
const result = await fetchPackageCatalogue(KEY);
|
|
167
|
+
expect(result).toEqual({ ok: false, status: 500, error: 'Registry returned HTTP 500' });
|
|
168
|
+
});
|
|
169
|
+
});
|