@hiyve/cli 1.0.17 → 1.0.19
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 +10 -8
- package/package.json +2 -3
- package/src/commands/init.js +200 -176
- package/src/commands/init.test.js +151 -0
- 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 +17 -32
- package/src/config.test.js +15 -9
- package/src/utils/registryApi.js +145 -0
- package/src/utils/registryApi.test.js +169 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scaffolder must produce a project that installs and runs against the
|
|
3
|
+
* PUBLISHED packages and matches the getting-started guide. It drifted
|
|
4
|
+
* badly once (2026-09-01): MUI ^5 against a ^9 peer (ERESOLVE on install), a
|
|
5
|
+
* hand-rolled token route posting to a host with no DNS record, no region
|
|
6
|
+
* pin, and env var names the SDK does not read. These tests check the
|
|
7
|
+
* generated output against the real peer ranges in this workspace so the
|
|
8
|
+
* next drift fails here, not on a developer's first `npm install`.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import {
|
|
15
|
+
PINS,
|
|
16
|
+
DEFAULT_REGION,
|
|
17
|
+
generatePackageJson,
|
|
18
|
+
generateEnvExample,
|
|
19
|
+
generateMain,
|
|
20
|
+
generateApp,
|
|
21
|
+
generateVideoRoom,
|
|
22
|
+
generateServer,
|
|
23
|
+
generateViteConfig,
|
|
24
|
+
generateTsConfig,
|
|
25
|
+
} from './init.js';
|
|
26
|
+
|
|
27
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const workspacePkg = (name) =>
|
|
29
|
+
JSON.parse(readFileSync(join(here, '../../../..', name, 'package.json'), 'utf8'));
|
|
30
|
+
|
|
31
|
+
const TEMPLATES = {
|
|
32
|
+
basic: { packages: ['@hiyve/react', '@hiyve/react-ui', '@hiyve/admin'], features: { intelligence: false, collaboration: false } },
|
|
33
|
+
ai: { packages: ['@hiyve/react', '@hiyve/react-ui', '@hiyve/react-intelligence', '@hiyve/admin'], features: { intelligence: true, collaboration: false } },
|
|
34
|
+
full: { packages: ['@hiyve/react-room', '@hiyve/react-intelligence', '@hiyve/react-semantic-relay', '@hiyve/admin'], features: { intelligence: true, collaboration: true } },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Major of a ^x.y.z / ~x.y.z pin. */
|
|
38
|
+
const majorOf = (range) => Number.parseInt(range.replace(/^[\^~]/, ''), 10);
|
|
39
|
+
/** Majors a caret-only peer range admits, e.g. "^18.0.0 || ^19.0.0" -> [18, 19]. */
|
|
40
|
+
const peerMajors = (peer) => peer.split('||').map((r) => majorOf(r.trim()));
|
|
41
|
+
/** Does a caret pin fall inside a caret-only peer range? */
|
|
42
|
+
const rangeFitsPeer = (pin, peer) => peerMajors(peer).includes(majorOf(pin));
|
|
43
|
+
|
|
44
|
+
describe('generated package.json', () => {
|
|
45
|
+
const pkg = JSON.parse(generatePackageJson('demo', TEMPLATES.basic));
|
|
46
|
+
|
|
47
|
+
it('pins MUI inside the range @hiyve/react-ui and @hiyve/react-room actually require', () => {
|
|
48
|
+
for (const p of ['react-ui', 'react-room']) {
|
|
49
|
+
const peer = workspacePkg(`packages/${p}`).peerDependencies['@mui/material'];
|
|
50
|
+
expect(rangeFitsPeer(pkg.dependencies['@mui/material'], peer), `${p} wants ${peer}`).toBe(true);
|
|
51
|
+
}
|
|
52
|
+
expect(pkg.dependencies['@mui/icons-material']).toBe(pkg.dependencies['@mui/material']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('pins React inside the range @hiyve/react requires', () => {
|
|
56
|
+
const peer = workspacePkg('packages/react').peerDependencies.react;
|
|
57
|
+
expect(rangeFitsPeer(pkg.dependencies.react, peer)).toBe(true);
|
|
58
|
+
expect(pkg.dependencies['react-dom']).toBe(pkg.dependencies.react);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('never pins MUI 5 or React 18 — the drift that broke install', () => {
|
|
62
|
+
expect(majorOf(pkg.dependencies['@mui/material'])).toBeGreaterThanOrEqual(9);
|
|
63
|
+
expect(majorOf(pkg.dependencies.react)).toBeGreaterThanOrEqual(19);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('includes @hiyve/admin — the token server is the SDK, not hand-rolled', () => {
|
|
67
|
+
expect(pkg.dependencies['@hiyve/admin']).toBeDefined();
|
|
68
|
+
expect(pkg.dependencies.express).toBeDefined();
|
|
69
|
+
expect(pkg.dependencies.cors).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('every dev dependency the tsconfig relies on is present', () => {
|
|
73
|
+
for (const k of ['@types/node', 'typescript', 'vite', '@vitejs/plugin-react', 'tsx', 'concurrently']) {
|
|
74
|
+
expect(pkg.devDependencies[k], k).toBe(PINS[k]);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('generated server', () => {
|
|
80
|
+
const server = generateServer();
|
|
81
|
+
|
|
82
|
+
it('mounts the SDK routes instead of posting to a signaling host by hand', () => {
|
|
83
|
+
expect(server).toContain("from '@hiyve/admin'");
|
|
84
|
+
expect(server).toContain('mountHiyveRoutes(apiRouter, loadHiyveConfig())');
|
|
85
|
+
expect(server).toContain("app.use('/api', apiRouter)");
|
|
86
|
+
expect(server).not.toContain('signal.hiyve.dev');
|
|
87
|
+
expect(server).not.toContain('/api/rooms/token');
|
|
88
|
+
expect(server).not.toContain('HIYVE_API_KEY');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('listens where vite proxies /api to', () => {
|
|
92
|
+
const port = server.match(/\|\| (\d+);/)[1];
|
|
93
|
+
expect(generateViteConfig()).toContain(`http://localhost:${port}`);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('generated .env.example', () => {
|
|
98
|
+
it('uses the variable names loadHiyveConfig reads, and the region the client pins', () => {
|
|
99
|
+
const env = generateEnvExample(TEMPLATES.basic);
|
|
100
|
+
expect(env).toMatch(/^APIKEY=pk_/m);
|
|
101
|
+
expect(env).toMatch(/^CLIENT_SECRET=sk_/m);
|
|
102
|
+
expect(env).toContain(`SERVER_REGION=${DEFAULT_REGION}`);
|
|
103
|
+
expect(env).not.toContain('HIYVE_API_KEY');
|
|
104
|
+
expect(env).not.toContain('signal.hiyve.dev');
|
|
105
|
+
expect(env).toContain('console.hiyve.dev');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('region pin', () => {
|
|
110
|
+
it('basic/ai: HiyveProvider in main.tsx carries the region', () => {
|
|
111
|
+
expect(generateMain(TEMPLATES.basic)).toContain(`<HiyveProvider region="${DEFAULT_REGION}">`);
|
|
112
|
+
expect(generateMain(TEMPLATES.ai)).toContain(`<HiyveProvider region="${DEFAULT_REGION}">`);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('full: HiyveRoom carries the region and main.tsx does not double-wrap', () => {
|
|
116
|
+
expect(generateApp(TEMPLATES.full)).toContain(`region={REGION}`);
|
|
117
|
+
expect(generateApp(TEMPLATES.full)).toContain(`const REGION = '${DEFAULT_REGION}'`);
|
|
118
|
+
expect(generateMain(TEMPLATES.full)).not.toContain('HiyveProvider');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('client and server agree on the region', () => {
|
|
122
|
+
const env = generateEnvExample(TEMPLATES.full);
|
|
123
|
+
const serverRegion = env.match(/^SERVER_REGION=(\S+)/m)[1];
|
|
124
|
+
expect(serverRegion).toBe(DEFAULT_REGION);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('generated client', () => {
|
|
129
|
+
it('basic/ai is the guide verbatim: useRoomFlow + JoinForm, SDK fetches the token', () => {
|
|
130
|
+
const app = generateApp(TEMPLATES.basic);
|
|
131
|
+
expect(app).toContain("import { useRoomFlow } from '@hiyve/react'");
|
|
132
|
+
expect(app).toContain('<JoinForm autoConnect devicePreviewMode="inline" />');
|
|
133
|
+
expect(app).not.toContain('fetch(');
|
|
134
|
+
expect(generateVideoRoom()).toContain('<VideoGrid localVideoElementId="local-video"');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('full fetches tokens from the SDK routes and reads the fields they return', () => {
|
|
138
|
+
const app = generateApp(TEMPLATES.full);
|
|
139
|
+
expect(app).toContain("fetch('/api/generate-room-token'");
|
|
140
|
+
expect(app).toContain('return data.roomToken');
|
|
141
|
+
expect(app).toContain("fetch('/api/generate-cloud-token'");
|
|
142
|
+
expect(app).toContain('return data.cloudToken');
|
|
143
|
+
expect(app).not.toContain('/api/token');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('tsconfig covers both the browser and the server sources', () => {
|
|
147
|
+
const ts = JSON.parse(generateTsConfig());
|
|
148
|
+
expect(ts.include).toEqual(expect.arrayContaining(['src', 'server']));
|
|
149
|
+
expect(ts.compilerOptions.types).toEqual(expect.arrayContaining(['vite/client', 'node']));
|
|
150
|
+
});
|
|
151
|
+
});
|
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,35 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hiyve CLI Configuration
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// The CLI's own API — key verification and the package catalogue, served by
|
|
10
|
-
// hiyve-cloud at /registry/{verify,packages}.
|
|
11
|
-
const PROD_API_URL = 'https://cloud.hiyve.io/registry/';
|
|
12
|
-
// No separate development cloud exists — kept so --dev stays a valid flag,
|
|
13
|
-
// but it reaches the SAME live host (2026-08-24).
|
|
14
|
-
const DEV_API_URL = 'https://cloud.hiyve.io/registry/';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Where npm actually fetches tarballs. DELIBERATELY NOT the API URL.
|
|
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.
|
|
18
8
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* registry.muziemedia.com is the Verdaccio registry `changeset publish`
|
|
28
|
-
* actually publishes to. Verified 2026-08-24 with a customer pk_ key: it
|
|
29
|
-
* authenticates API keys natively (the proxy is not needed for auth),
|
|
30
|
-
* serves current versions, its tarball URLs point at itself, and the
|
|
31
|
-
* download returns a valid package.
|
|
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).
|
|
32
16
|
*/
|
|
17
|
+
|
|
33
18
|
const REGISTRY_URL_VALUE = 'https://registry.muziemedia.com/';
|
|
34
19
|
|
|
35
20
|
let _devMode = false;
|
|
@@ -42,16 +27,17 @@ export function setDevMode(enabled) {
|
|
|
42
27
|
}
|
|
43
28
|
|
|
44
29
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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.
|
|
47
33
|
*/
|
|
48
34
|
export function getApiUrl() {
|
|
49
|
-
return
|
|
35
|
+
return REGISTRY_URL_VALUE;
|
|
50
36
|
}
|
|
51
37
|
|
|
52
38
|
/**
|
|
53
39
|
* Get the registry URL for ~/.npmrc (where npm fetches tarballs).
|
|
54
|
-
*
|
|
40
|
+
* Ignores --dev: there is one registry.
|
|
55
41
|
*/
|
|
56
42
|
export function getRegistryUrl() {
|
|
57
43
|
return REGISTRY_URL_VALUE;
|
|
@@ -65,7 +51,6 @@ export function isDevMode() {
|
|
|
65
51
|
}
|
|
66
52
|
|
|
67
53
|
// Backwards-compatible named export
|
|
68
|
-
// The npm registry, NOT the CLI API (they are different hosts — see above).
|
|
69
54
|
export const REGISTRY_URL = REGISTRY_URL_VALUE;
|
|
70
55
|
|
|
71
56
|
export default {
|
package/src/config.test.js
CHANGED
|
@@ -11,23 +11,29 @@ describe('config', () => {
|
|
|
11
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('
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
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', () => {
|
|
29
35
|
expect(getRegistryUrl()).toBe('https://registry.muziemedia.com/');
|
|
30
|
-
expect(getRegistryUrl()).
|
|
36
|
+
expect(getRegistryUrl()).toBe(getApiUrl());
|
|
31
37
|
});
|
|
32
38
|
|
|
33
39
|
it('getRegistryUrl does NOT switch with dev mode — there is one registry', () => {
|
|
@@ -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 };
|