pushnetgo-cli 1.0.3 → 1.0.5
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 +13 -1
- package/commands/sync-music.js +34 -20
- package/commands/token.js +35 -28
- package/lib/auth.js +56 -33
- package/lib/http-client.js +57 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# PushNetGo CLI
|
|
2
2
|
|
|
3
|
-
Command-line tool for PushNetGo App — sync music, validate API keys.
|
|
3
|
+
Command-line tool for PushNetGo App — sync music, validate API keys.
|
|
4
|
+
|
|
5
|
+
## Quickstart
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g pushnetgo-cli
|
|
9
|
+
export MYAPP_API_KEY="pushnetgo-xxx"
|
|
10
|
+
pushnetgo-cli token check
|
|
11
|
+
pushnetgo-cli api-url
|
|
12
|
+
pushnetgo-cli sync-music --url "https://..."
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Three commands. No Firebase config needed. Works on any machine.**
|
|
4
16
|
|
|
5
17
|
## Install
|
|
6
18
|
|
package/commands/sync-music.js
CHANGED
|
@@ -2,16 +2,21 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { resolveApiKey } = require('../lib/auth');
|
|
5
|
-
const {
|
|
5
|
+
const { hasServiceAccount, syncMusicHttp } = require('../lib/http-client');
|
|
6
6
|
|
|
7
7
|
async function syncMusic(options) {
|
|
8
8
|
const projectId = options.project || 'default';
|
|
9
9
|
const dir = options.dir;
|
|
10
10
|
const url = options.url;
|
|
11
|
-
|
|
12
|
-
await resolveApiKey(options.key);
|
|
11
|
+
const key = await resolveApiKey(options.key);
|
|
13
12
|
|
|
14
13
|
if (dir) {
|
|
14
|
+
if (!hasServiceAccount()) {
|
|
15
|
+
console.error('Local directory upload requires Firebase service account.');
|
|
16
|
+
console.error('Use --url instead: pushnetgo-cli sync-music --url "https://..."');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const { uploadFile, saveTrack } = require('../lib/uploader');
|
|
15
20
|
const files = fs.readdirSync(dir)
|
|
16
21
|
.filter(f => f.endsWith('.mp3'))
|
|
17
22
|
.map(f => path.join(dir, f));
|
|
@@ -40,23 +45,32 @@ async function syncMusic(options) {
|
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
if (url) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
48
|
+
if (hasServiceAccount()) {
|
|
49
|
+
// SDK mode: download → upload via Firebase Admin
|
|
50
|
+
const { downloadUrlToFile, uploadFile, saveTrack } = require('../lib/uploader');
|
|
51
|
+
console.log(`Download: ${url}`);
|
|
52
|
+
const tmpDir = os.tmpdir();
|
|
53
|
+
const tmpFile = path.join(tmpDir, `mureka_${Date.now()}.mp3`);
|
|
54
|
+
await downloadUrlToFile(url, tmpFile);
|
|
55
|
+
console.log(`Downloaded to: ${tmpFile}`);
|
|
56
|
+
|
|
57
|
+
const trackId = `mureka_${Date.now()}`;
|
|
58
|
+
const artist = 'Mureka AI';
|
|
59
|
+
const title = `Generated ${new Date().toISOString().slice(0, 10)}`;
|
|
60
|
+
|
|
61
|
+
process.stdout.write('Uploading...');
|
|
62
|
+
const downloadUrl = await uploadFile(tmpFile, `music_files/${trackId}.mp3`);
|
|
63
|
+
await saveTrack({ trackId, title, artist, downloadUrl, projectId });
|
|
64
|
+
console.log(' OK');
|
|
65
|
+
|
|
66
|
+
try { fs.unlinkSync(tmpFile); } catch (_) {}
|
|
67
|
+
console.log(`Mureka track published to project ${projectId}`);
|
|
68
|
+
} else {
|
|
69
|
+
// HTTP mode: call API
|
|
70
|
+
console.log(`Submitting: ${url} -> project ${projectId}`);
|
|
71
|
+
const result = await syncMusicHttp(key, url, projectId);
|
|
72
|
+
console.log(`OK: ${result.trackId} — ${result.downloadUrl}`);
|
|
73
|
+
}
|
|
60
74
|
}
|
|
61
75
|
}
|
|
62
76
|
|
package/commands/token.js
CHANGED
|
@@ -1,37 +1,44 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { hasServiceAccount, checkTokenHttp } = require('../lib/http-client');
|
|
2
|
+
const { validateKeyFormat, resolveKeyEnv } = require('../lib/auth');
|
|
2
3
|
|
|
3
4
|
async function checkToken(key) {
|
|
4
|
-
const token = key
|
|
5
|
+
const token = resolveKeyEnv(key);
|
|
5
6
|
|
|
6
|
-
if (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
console.error('Invalid prefix. PushNetGo keys must start with pushnetgo-');
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
7
|
+
if (hasServiceAccount()) {
|
|
8
|
+
const { getFirestore } = require('../lib/firestore');
|
|
9
|
+
const db = getFirestore();
|
|
10
|
+
const snap = await db.collection('tokens')
|
|
11
|
+
.where('tokenValue', '==', token)
|
|
12
|
+
.limit(1)
|
|
13
|
+
.get();
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.get();
|
|
15
|
+
if (snap.empty) {
|
|
16
|
+
console.log(`NOT FOUND: ${token.slice(0, 16)}...`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
console.log(`
|
|
25
|
-
|
|
20
|
+
const d = snap.docs[0].data();
|
|
21
|
+
console.log(`OK: ${token}`);
|
|
22
|
+
console.log(` active: ${d.isActive}`);
|
|
23
|
+
console.log(` usage: ${d.usageCount || 0}`);
|
|
24
|
+
console.log(` userId: ${d.userId}`);
|
|
25
|
+
console.log(` perms: ${JSON.stringify(d.permissions || d.scopes || [])}`);
|
|
26
|
+
console.log(` created: ${d.createdAt?.toDate?.() || d.createdAt}`);
|
|
27
|
+
} else {
|
|
28
|
+
// HTTP mode
|
|
29
|
+
try {
|
|
30
|
+
const result = await checkTokenHttp(token);
|
|
31
|
+
console.log(`OK: ${result.token}`);
|
|
32
|
+
console.log(` active: ${result.active}`);
|
|
33
|
+
console.log(` usage: ${result.usageCount || 0}`);
|
|
34
|
+
console.log(` userId: ${result.userId}`);
|
|
35
|
+
console.log(` perms: ${JSON.stringify(result.permissions || [])}`);
|
|
36
|
+
console.log(` created: ${result.createdAt}`);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error(`Error: ${e.error || e.message || e}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
26
41
|
}
|
|
27
|
-
|
|
28
|
-
const d = snap.docs[0].data();
|
|
29
|
-
console.log(`OK: ${token}`);
|
|
30
|
-
console.log(` active: ${d.isActive}`);
|
|
31
|
-
console.log(` usage: ${d.usageCount || 0}`);
|
|
32
|
-
console.log(` userId: ${d.userId}`);
|
|
33
|
-
console.log(` perms: ${JSON.stringify(d.permissions || d.scopes || [])}`);
|
|
34
|
-
console.log(` created: ${d.createdAt?.toDate?.() || d.createdAt}`);
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
module.exports = { checkToken };
|
package/lib/auth.js
CHANGED
|
@@ -1,49 +1,72 @@
|
|
|
1
|
-
const {
|
|
2
|
-
|
|
3
|
-
async function resolveApiKey(cliKey) {
|
|
4
|
-
const key = cliKey || process.env.MYAPP_API_KEY;
|
|
5
|
-
|
|
6
|
-
if (!key) {
|
|
7
|
-
console.error('No API Key found. Set env var or pass --key:');
|
|
8
|
-
console.error(' export MYAPP_API_KEY="pushnetgo-xxxx"');
|
|
9
|
-
console.error(' or: pushnetgo-cli sync-music --key pushnetgo-xxxx ...');
|
|
10
|
-
process.exit(1);
|
|
11
|
-
}
|
|
1
|
+
const { hasServiceAccount } = require('./http-client');
|
|
12
2
|
|
|
3
|
+
function validateKeyFormat(key) {
|
|
13
4
|
if (!key.startsWith('pushnetgo-') || key.length < 22) {
|
|
14
5
|
console.error('Invalid key format. Expected pushnetgo- prefix + 32 hex chars');
|
|
15
6
|
process.exit(1);
|
|
16
7
|
}
|
|
8
|
+
}
|
|
17
9
|
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
.
|
|
22
|
-
.
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
if (snap.empty) {
|
|
26
|
-
console.error('Key not found or inactive');
|
|
10
|
+
function resolveKeyEnv(cliKey) {
|
|
11
|
+
const key = cliKey || process.env.MYAPP_API_KEY;
|
|
12
|
+
if (!key) {
|
|
13
|
+
console.error('No API Key found. Set env var or pass --key:');
|
|
14
|
+
console.error(' export MYAPP_API_KEY="pushnetgo-xxxx"');
|
|
15
|
+
console.error(' or: pushnetgo-cli sync-music --key pushnetgo-xxxx ...');
|
|
27
16
|
process.exit(1);
|
|
28
17
|
}
|
|
18
|
+
validateKeyFormat(key);
|
|
19
|
+
return key;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function resolveApiKey(cliKey) {
|
|
23
|
+
const key = resolveKeyEnv(cliKey);
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
if (hasServiceAccount()) {
|
|
26
|
+
const { getFirestore, admin } = require('./firestore');
|
|
27
|
+
const db = getFirestore();
|
|
28
|
+
const snap = await db.collection('tokens')
|
|
29
|
+
.where('tokenValue', '==', key)
|
|
30
|
+
.where('isActive', '==', true)
|
|
31
|
+
.limit(1)
|
|
32
|
+
.get();
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
if (snap.empty) {
|
|
35
|
+
console.error('Key not found or inactive');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
const data = snap.docs[0].data();
|
|
40
|
+
const perms = data.permissions || data.scopes || [];
|
|
41
|
+
const hasUpload = perms.some(p => p === 'musicUpload' || p === 'music_upload');
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
if (!hasUpload) {
|
|
44
|
+
console.error('This key lacks musicUpload permission');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`Auth OK — uid: ${data.userId}`);
|
|
49
|
+
await db.collection('tokens').doc(snap.docs[0].id).update({
|
|
50
|
+
lastUsedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
51
|
+
usageCount: admin.firestore.FieldValue.increment(1),
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
// HTTP mode: validate via API
|
|
55
|
+
const { checkTokenHttp } = require('./http-client');
|
|
56
|
+
try {
|
|
57
|
+
const result = await checkTokenHttp(key);
|
|
58
|
+
if (!result.active) {
|
|
59
|
+
console.error('Token is inactive');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
console.log(`Auth OK — uid: ${result.userId}`);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error('Token validation failed:', e.error || e.message || e);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
45
68
|
|
|
46
69
|
return key;
|
|
47
70
|
}
|
|
48
71
|
|
|
49
|
-
module.exports = { resolveApiKey };
|
|
72
|
+
module.exports = { resolveApiKey, resolveKeyEnv, validateKeyFormat };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
|
|
4
|
+
const API_URL = 'https://us-central1-flutter-ai-playground-f8e3b.cloudfunctions.net/unifiedGatewayApi';
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
let _hasSA = null;
|
|
10
|
+
function hasServiceAccount() {
|
|
11
|
+
if (_hasSA !== null) return _hasSA;
|
|
12
|
+
const saPath = path.resolve(__dirname, '../../configs/service-account.json');
|
|
13
|
+
_hasSA = fs.existsSync(saPath);
|
|
14
|
+
return _hasSA;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function _request(body, apiKey) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const data = JSON.stringify(body);
|
|
20
|
+
const url = new URL(API_URL);
|
|
21
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
22
|
+
const req = client.request({
|
|
23
|
+
hostname: url.hostname,
|
|
24
|
+
path: url.pathname,
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
'Content-Length': Buffer.byteLength(data),
|
|
29
|
+
'x-api-key': apiKey,
|
|
30
|
+
},
|
|
31
|
+
}, (res) => {
|
|
32
|
+
let body = '';
|
|
33
|
+
res.on('data', chunk => body += chunk);
|
|
34
|
+
res.on('end', () => {
|
|
35
|
+
try {
|
|
36
|
+
const json = JSON.parse(body);
|
|
37
|
+
res.statusCode >= 400 ? reject(json) : resolve(json);
|
|
38
|
+
} catch (_) {
|
|
39
|
+
reject({ error: `HTTP ${res.statusCode}: ${body.slice(0, 200)}` });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
req.on('error', reject);
|
|
44
|
+
req.write(data);
|
|
45
|
+
req.end();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function checkTokenHttp(key) {
|
|
50
|
+
return _request({ action: 'token-check' }, key);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function syncMusicHttp(key, url, projectId) {
|
|
54
|
+
return _request({ action: 'sync-music', url, projectId }, key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { hasServiceAccount, checkTokenHttp, syncMusicHttp, API_URL };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pushnetgo-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "PushNetGo App CLI — HTTP API: https://us-central1-flutter-ai-playground-f8e3b.cloudfunctions.net/unifiedGatewayApi",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pushnetgo-cli": "./bin/pushnetgo-cli.js"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"scripts": {
|
|
9
9
|
"start": "node bin/pushnetgo-cli.js"
|
|
10
10
|
},
|
|
11
|
-
"keywords": ["pushnetgo", "music", "sync", "cli", "ai-agent"],
|
|
11
|
+
"keywords": ["pushnetgo", "pushnetgo-cli", "pushnetgo api", "pushnetgo upload", "music", "sync", "cli", "ai-agent"],
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|