ff1-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/config.json.example +78 -0
- package/dist/index.js +627 -0
- package/dist/src/ai-orchestrator/index.js +870 -0
- package/dist/src/ai-orchestrator/registry.js +96 -0
- package/dist/src/config.js +352 -0
- package/dist/src/intent-parser/index.js +1342 -0
- package/dist/src/intent-parser/utils.js +108 -0
- package/dist/src/logger.js +72 -0
- package/dist/src/main.js +393 -0
- package/dist/src/types.js +5 -0
- package/dist/src/utilities/address-validator.js +242 -0
- package/dist/src/utilities/domain-resolver.js +291 -0
- package/dist/src/utilities/feed-fetcher.js +387 -0
- package/dist/src/utilities/ff1-device.js +176 -0
- package/dist/src/utilities/functions.js +325 -0
- package/dist/src/utilities/index.js +372 -0
- package/dist/src/utilities/nft-indexer.js +1013 -0
- package/dist/src/utilities/playlist-builder.js +522 -0
- package/dist/src/utilities/playlist-publisher.js +131 -0
- package/dist/src/utilities/playlist-send.js +241 -0
- package/dist/src/utilities/playlist-signer.js +171 -0
- package/dist/src/utilities/playlist-verifier.js +156 -0
- package/dist/src/utils.js +48 -0
- package/docs/CONFIGURATION.md +178 -0
- package/docs/EXAMPLES.md +331 -0
- package/docs/FUNCTION_CALLING.md +92 -0
- package/docs/README.md +267 -0
- package/docs/RELEASING.md +22 -0
- package/package.json +75 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.confirmPlaylistForSending = confirmPlaylistForSending;
|
|
40
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
41
|
+
const fs_1 = require("fs");
|
|
42
|
+
/**
|
|
43
|
+
* Get available FF1 devices from config
|
|
44
|
+
*
|
|
45
|
+
* @returns {Promise<Array>} Array of device objects
|
|
46
|
+
*/
|
|
47
|
+
async function getAvailableDevices() {
|
|
48
|
+
try {
|
|
49
|
+
const configModule = (await Promise.resolve().then(() => __importStar(require('../config'))));
|
|
50
|
+
const getFF1DeviceConfig = configModule.getFF1DeviceConfig ||
|
|
51
|
+
(configModule.default && configModule.default.getFF1DeviceConfig) ||
|
|
52
|
+
configModule.default;
|
|
53
|
+
if (typeof getFF1DeviceConfig !== 'function') {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const deviceConfig = getFF1DeviceConfig();
|
|
57
|
+
if (deviceConfig.devices && Array.isArray(deviceConfig.devices)) {
|
|
58
|
+
return deviceConfig.devices
|
|
59
|
+
.filter((d) => d && d.host)
|
|
60
|
+
.map((d) => ({
|
|
61
|
+
name: d.name || d.host,
|
|
62
|
+
host: d.host,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (process.env.DEBUG) {
|
|
68
|
+
console.log(chalk_1.default.gray(`[DEBUG] Error loading devices: ${error.message}`));
|
|
69
|
+
}
|
|
70
|
+
// Silently fail if config can't be loaded
|
|
71
|
+
}
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Confirm playlist file path and validate the playlist
|
|
76
|
+
*
|
|
77
|
+
* Reads the playlist file, validates it against DP-1 spec,
|
|
78
|
+
* and returns confirmation result for user review.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} filePath - Path to playlist file
|
|
81
|
+
* @param {string} [deviceName] - Device name (optional)
|
|
82
|
+
* @returns {Promise<PlaylistSendConfirmation>} Validation result
|
|
83
|
+
*/
|
|
84
|
+
async function confirmPlaylistForSending(filePath, deviceName) {
|
|
85
|
+
const defaultPath = './playlist.json';
|
|
86
|
+
const resolvedPath = filePath || defaultPath;
|
|
87
|
+
// Convert string "null" to undefined (in case model passes it literally)
|
|
88
|
+
const actualDeviceName = deviceName === 'null' || deviceName === '' ? undefined : deviceName;
|
|
89
|
+
if (process.env.DEBUG) {
|
|
90
|
+
console.error(chalk_1.default.gray(`[DEBUG] confirmPlaylistForSending called with: filePath="${filePath}", deviceName="${deviceName}" -> "${actualDeviceName}"`));
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
// Check if file exists
|
|
94
|
+
console.log(chalk_1.default.cyan(`Checking playlist file: ${resolvedPath}...`));
|
|
95
|
+
let _fileExists = false;
|
|
96
|
+
let playlist;
|
|
97
|
+
try {
|
|
98
|
+
const content = await fs_1.promises.readFile(resolvedPath, 'utf-8');
|
|
99
|
+
playlist = JSON.parse(content);
|
|
100
|
+
_fileExists = true;
|
|
101
|
+
console.log(chalk_1.default.green('✓ File found'));
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const errorMsg = error.message;
|
|
105
|
+
if (errorMsg.includes('ENOENT') || errorMsg.includes('no such file')) {
|
|
106
|
+
console.log(chalk_1.default.red(`✗ File not found: ${resolvedPath}`));
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
filePath: resolvedPath,
|
|
110
|
+
fileExists: false,
|
|
111
|
+
playlistValid: false,
|
|
112
|
+
error: `Playlist file not found at ${resolvedPath}`,
|
|
113
|
+
message: `Could not find playlist file. Try:\n • Run a playlist build first\n • Check the file path is correct\n • Use "send ./path/to/playlist.json"`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
if (!playlist) {
|
|
119
|
+
return {
|
|
120
|
+
success: false,
|
|
121
|
+
filePath: resolvedPath,
|
|
122
|
+
fileExists: true,
|
|
123
|
+
playlistValid: false,
|
|
124
|
+
error: 'Playlist file is empty',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Validate playlist structure
|
|
128
|
+
console.log(chalk_1.default.cyan('Validating playlist...'));
|
|
129
|
+
// Dynamic import to avoid circular dependency
|
|
130
|
+
const { verifyPlaylist } = await Promise.resolve().then(() => __importStar(require('./playlist-verifier')));
|
|
131
|
+
const verifyResult = verifyPlaylist(playlist);
|
|
132
|
+
if (!verifyResult.valid) {
|
|
133
|
+
console.log(chalk_1.default.red('✗ Playlist validation failed'));
|
|
134
|
+
const detailLines = verifyResult.details?.map((d) => ` • ${d.path}: ${d.message}`).join('\n') ||
|
|
135
|
+
verifyResult.error;
|
|
136
|
+
const detailPaths = verifyResult.details?.map((d) => d.path) || [];
|
|
137
|
+
const hints = [];
|
|
138
|
+
if (detailPaths.some((path) => path.includes('signature'))) {
|
|
139
|
+
hints.push('Add `playlist.privateKey` (or `PLAYLIST_PRIVATE_KEY`) and rebuild the playlist to include a signature.');
|
|
140
|
+
}
|
|
141
|
+
if (detailPaths.some((path) => path.includes('defaults.display.margin'))) {
|
|
142
|
+
hints.push('Rebuild the playlist with the latest CLI defaults (margin must be numeric).');
|
|
143
|
+
}
|
|
144
|
+
const hintText = hints.length > 0 ? `\n\nHint:\n${hints.map((h) => ` • ${h}`).join('\n')}` : '';
|
|
145
|
+
return {
|
|
146
|
+
success: false,
|
|
147
|
+
filePath: resolvedPath,
|
|
148
|
+
fileExists: true,
|
|
149
|
+
playlistValid: false,
|
|
150
|
+
playlist,
|
|
151
|
+
deviceName: actualDeviceName,
|
|
152
|
+
error: `Playlist is invalid: ${verifyResult.error}`,
|
|
153
|
+
message: `This playlist doesn't match DP-1 specification.\n\nErrors:\n${detailLines}${hintText}`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
console.log(chalk_1.default.green('✓ Playlist is valid'));
|
|
157
|
+
// Display confirmation details
|
|
158
|
+
const itemCount = playlist.items?.length || 0;
|
|
159
|
+
const title = playlist.title || 'Untitled';
|
|
160
|
+
// Handle device selection
|
|
161
|
+
let selectedDevice = actualDeviceName;
|
|
162
|
+
let needsDeviceSelection = false;
|
|
163
|
+
let availableDevices = [];
|
|
164
|
+
if (!selectedDevice) {
|
|
165
|
+
// Get available devices
|
|
166
|
+
availableDevices = await getAvailableDevices();
|
|
167
|
+
if (process.env.DEBUG) {
|
|
168
|
+
console.error(chalk_1.default.gray(`[DEBUG] selectedDevice is null/undefined`));
|
|
169
|
+
console.error(chalk_1.default.gray(`[DEBUG] Available devices found: ${availableDevices.length}`));
|
|
170
|
+
availableDevices.forEach((d) => {
|
|
171
|
+
console.error(chalk_1.default.gray(`[DEBUG] Device: ${d.name} (${d.host})`));
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (availableDevices.length === 0) {
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
filePath: resolvedPath,
|
|
178
|
+
fileExists: true,
|
|
179
|
+
playlistValid: true,
|
|
180
|
+
playlist,
|
|
181
|
+
error: 'No FF1 devices configured',
|
|
182
|
+
message: `No FF1 devices found in your configuration.\n\nPlease add devices to your config.json:\n{\n "devices": [{\n "name": "Living Room",\n "host": "192.168.1.100"\n }]\n}`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
else if (availableDevices.length === 1) {
|
|
186
|
+
// Auto-select single device
|
|
187
|
+
selectedDevice = availableDevices[0].name || availableDevices[0].host;
|
|
188
|
+
console.log(chalk_1.default.cyan(`Auto-selecting device: ${selectedDevice}`));
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// Multiple devices - need user to choose
|
|
192
|
+
needsDeviceSelection = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
console.log();
|
|
196
|
+
console.log(chalk_1.default.bold('Playlist Summary:'));
|
|
197
|
+
console.log(chalk_1.default.gray(` Title: ${title}`));
|
|
198
|
+
console.log(chalk_1.default.gray(` Items: ${itemCount}`));
|
|
199
|
+
if (selectedDevice) {
|
|
200
|
+
console.log(chalk_1.default.gray(` Device: ${selectedDevice}`));
|
|
201
|
+
}
|
|
202
|
+
else if (availableDevices.length > 1) {
|
|
203
|
+
console.log(chalk_1.default.gray(` Device: (to be selected)`));
|
|
204
|
+
}
|
|
205
|
+
console.log();
|
|
206
|
+
// If multiple devices, return needsDeviceSelection flag
|
|
207
|
+
if (needsDeviceSelection) {
|
|
208
|
+
return {
|
|
209
|
+
success: false,
|
|
210
|
+
filePath: resolvedPath,
|
|
211
|
+
fileExists: true,
|
|
212
|
+
playlistValid: true,
|
|
213
|
+
playlist,
|
|
214
|
+
needsDeviceSelection: true,
|
|
215
|
+
availableDevices,
|
|
216
|
+
error: 'Multiple devices available - please choose one',
|
|
217
|
+
message: `Which device would you like to display on?\n\nAvailable devices:\n${availableDevices.map((d, i) => ` ${i + 1}. ${d.name || d.host}`).join('\n')}\n\nSay: "send to [device name]" or "send to device 1"`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
success: true,
|
|
222
|
+
filePath: resolvedPath,
|
|
223
|
+
fileExists: true,
|
|
224
|
+
playlistValid: true,
|
|
225
|
+
playlist,
|
|
226
|
+
deviceName: selectedDevice,
|
|
227
|
+
message: `Ready to send "${title}" (${itemCount} items) to ${selectedDevice}!`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
const errorMsg = error.message;
|
|
232
|
+
console.log(chalk_1.default.red(`✗ Error: ${errorMsg}`));
|
|
233
|
+
return {
|
|
234
|
+
success: false,
|
|
235
|
+
filePath: resolvedPath,
|
|
236
|
+
fileExists: false,
|
|
237
|
+
playlistValid: false,
|
|
238
|
+
error: errorMsg,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playlist Signing Utility
|
|
3
|
+
* Uses dp1-js library for DP-1 specification-compliant playlist signing
|
|
4
|
+
*/
|
|
5
|
+
const { signDP1Playlist, verifyPlaylistSignature } = require('dp1-js');
|
|
6
|
+
const { getPlaylistConfig } = require('../config');
|
|
7
|
+
/**
|
|
8
|
+
* Convert base64-encoded key to Uint8Array (or hex string if needed)
|
|
9
|
+
*
|
|
10
|
+
* @param {string} base64Key - Ed25519 private key in base64 format
|
|
11
|
+
* @returns {Uint8Array} Private key as Uint8Array
|
|
12
|
+
*/
|
|
13
|
+
function base64ToUint8Array(base64Key) {
|
|
14
|
+
const buffer = Buffer.from(base64Key, 'base64');
|
|
15
|
+
return new Uint8Array(buffer);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Convert hex string to Uint8Array
|
|
19
|
+
*
|
|
20
|
+
* @param {string} hexKey - Ed25519 public key in hex format
|
|
21
|
+
* @returns {Uint8Array} Public key as Uint8Array
|
|
22
|
+
*/
|
|
23
|
+
function hexToUint8Array(hexKey) {
|
|
24
|
+
const cleanHex = hexKey.replace(/^0x/, '');
|
|
25
|
+
const buffer = Buffer.from(cleanHex, 'hex');
|
|
26
|
+
return new Uint8Array(buffer);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if a string is valid hex (with or without 0x prefix)
|
|
30
|
+
*
|
|
31
|
+
* @param {string} str - String to test
|
|
32
|
+
* @returns {boolean} True if string is valid hex
|
|
33
|
+
*/
|
|
34
|
+
function isHexString(str) {
|
|
35
|
+
const cleanHex = str.replace(/^0x/, '');
|
|
36
|
+
return /^[0-9a-fA-F]+$/.test(cleanHex) && cleanHex.length % 2 === 0;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Sign a playlist using ed25519 as per DP-1 specification
|
|
40
|
+
* Uses dp1-js library for standards-compliant signing
|
|
41
|
+
* Accepts private key in hex (with or without 0x prefix) or base64 format
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} playlist - Playlist object without signature
|
|
44
|
+
* @param {string} [privateKeyBase64] - Ed25519 private key in hex or base64 format (optional, uses config if not provided)
|
|
45
|
+
* @returns {Promise<string>} Signature in format "ed25519:0x{hex}"
|
|
46
|
+
* @throws {Error} If private key is invalid or signing fails
|
|
47
|
+
* @example
|
|
48
|
+
* const signature = await signPlaylist(playlist, privateKeyHexOrBase64);
|
|
49
|
+
* // Returns: "ed25519:0x1234abcd..."
|
|
50
|
+
*/
|
|
51
|
+
async function signPlaylist(playlist, privateKeyBase64) {
|
|
52
|
+
// Get private key from config if not provided
|
|
53
|
+
let privateKey = privateKeyBase64;
|
|
54
|
+
if (!privateKey) {
|
|
55
|
+
const config = getPlaylistConfig();
|
|
56
|
+
privateKey = config.privateKey;
|
|
57
|
+
}
|
|
58
|
+
if (!privateKey) {
|
|
59
|
+
throw new Error('Private key is required for signing');
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
// Remove signature field if it exists (for re-signing)
|
|
63
|
+
const playlistToSign = { ...playlist };
|
|
64
|
+
delete playlistToSign.signature;
|
|
65
|
+
// Try hex first (with or without 0x prefix), then fall back to base64
|
|
66
|
+
let keyInput;
|
|
67
|
+
if (isHexString(privateKey)) {
|
|
68
|
+
// It's hex - ensure it has 0x prefix for dp1-js
|
|
69
|
+
keyInput = privateKey.startsWith('0x') ? privateKey : '0x' + privateKey;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Fall back to base64
|
|
73
|
+
const keyBytes = base64ToUint8Array(privateKey);
|
|
74
|
+
keyInput = '0x' + Buffer.from(keyBytes).toString('hex');
|
|
75
|
+
}
|
|
76
|
+
// Sign using dp1-js library
|
|
77
|
+
const signature = await signDP1Playlist(playlistToSign, keyInput);
|
|
78
|
+
return signature;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw new Error(`Failed to sign playlist: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Verify a playlist's ed25519 signature
|
|
86
|
+
*
|
|
87
|
+
* @param {Object} playlist - Playlist object with signature field
|
|
88
|
+
* @param {string} publicKeyHex - Ed25519 public key in hex format (with or without 0x prefix)
|
|
89
|
+
* @returns {Promise<boolean>} True if signature is valid, false otherwise
|
|
90
|
+
* @throws {Error} If verification process fails
|
|
91
|
+
* @example
|
|
92
|
+
* const isValid = await verifyPlaylist(signedPlaylist, publicKeyHex);
|
|
93
|
+
* if (isValid) {
|
|
94
|
+
* console.log('Signature is valid');
|
|
95
|
+
* }
|
|
96
|
+
*/
|
|
97
|
+
async function verifyPlaylist(playlist, publicKeyHex) {
|
|
98
|
+
if (!playlist.signature) {
|
|
99
|
+
throw new Error('Playlist does not have a signature');
|
|
100
|
+
}
|
|
101
|
+
if (!publicKeyHex) {
|
|
102
|
+
throw new Error('Public key is required for verification');
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
// Convert hex public key to Uint8Array
|
|
106
|
+
const publicKeyBytes = hexToUint8Array(publicKeyHex);
|
|
107
|
+
// Verify using dp1-js library
|
|
108
|
+
const isValid = await verifyPlaylistSignature(playlist, publicKeyBytes);
|
|
109
|
+
return isValid;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
throw new Error(`Failed to verify playlist signature: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Sign a playlist file
|
|
117
|
+
* Reads playlist from file, signs it, and writes back
|
|
118
|
+
*
|
|
119
|
+
* @param {string} playlistPath - Path to playlist JSON file
|
|
120
|
+
* @param {string} [privateKeyBase64] - Ed25519 private key in hex or base64 format (optional, uses config if not provided)
|
|
121
|
+
* @param {string} [outputPath] - Output path (optional, overwrites input if not provided)
|
|
122
|
+
* @returns {Promise<Object>} Result with signed playlist
|
|
123
|
+
* @returns {boolean} returns.success - Whether signing succeeded
|
|
124
|
+
* @returns {Object} [returns.playlist] - Signed playlist object
|
|
125
|
+
* @returns {string} [returns.error] - Error message if failed
|
|
126
|
+
* @example
|
|
127
|
+
* const result = await signPlaylistFile('playlist.json');
|
|
128
|
+
* if (result.success) {
|
|
129
|
+
* console.log('Playlist signed:', result.playlist);
|
|
130
|
+
* }
|
|
131
|
+
*/
|
|
132
|
+
async function signPlaylistFile(playlistPath, privateKeyBase64, outputPath) {
|
|
133
|
+
const fs = require('fs');
|
|
134
|
+
const path = require('path');
|
|
135
|
+
try {
|
|
136
|
+
// Read playlist file
|
|
137
|
+
if (!fs.existsSync(playlistPath)) {
|
|
138
|
+
throw new Error(`Playlist file not found: ${playlistPath}`);
|
|
139
|
+
}
|
|
140
|
+
const playlistContent = fs.readFileSync(playlistPath, 'utf-8');
|
|
141
|
+
const playlist = JSON.parse(playlistContent);
|
|
142
|
+
// Sign playlist
|
|
143
|
+
const signature = await signPlaylist(playlist, privateKeyBase64);
|
|
144
|
+
// Add signature to playlist
|
|
145
|
+
const signedPlaylist = {
|
|
146
|
+
...playlist,
|
|
147
|
+
signature,
|
|
148
|
+
};
|
|
149
|
+
// Write to output file
|
|
150
|
+
const output = outputPath || playlistPath;
|
|
151
|
+
fs.writeFileSync(output, JSON.stringify(signedPlaylist, null, 2), 'utf-8');
|
|
152
|
+
console.log(`✓ Playlist signed and saved to: ${path.resolve(output)}`);
|
|
153
|
+
return {
|
|
154
|
+
success: true,
|
|
155
|
+
playlist: signedPlaylist,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
error: error.message,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
module.exports = {
|
|
166
|
+
signPlaylist,
|
|
167
|
+
verifyPlaylist,
|
|
168
|
+
signPlaylistFile,
|
|
169
|
+
base64ToUint8Array,
|
|
170
|
+
hexToUint8Array,
|
|
171
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Playlist Verification Utility
|
|
4
|
+
* Uses dp1-js library for DP-1 specification-compliant playlist validation
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.verifyPlaylist = verifyPlaylist;
|
|
11
|
+
exports.verifyPlaylistFile = verifyPlaylistFile;
|
|
12
|
+
exports.printVerificationResult = printVerificationResult;
|
|
13
|
+
const dp1_js_1 = require("dp1-js");
|
|
14
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
15
|
+
const fs_1 = require("fs");
|
|
16
|
+
/**
|
|
17
|
+
* Verify playlist structure and integrity
|
|
18
|
+
*
|
|
19
|
+
* Validates playlist against DP-1 specification using dp1-js parser.
|
|
20
|
+
* Returns detailed validation errors if playlist is invalid.
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} playlist - Playlist object to verify
|
|
23
|
+
* @returns {Object} Verification result
|
|
24
|
+
* @returns {boolean} returns.valid - Whether playlist is valid
|
|
25
|
+
* @returns {string} [returns.error] - Error message if invalid
|
|
26
|
+
* @returns {Array<Object>} [returns.details] - Detailed validation errors
|
|
27
|
+
* @example
|
|
28
|
+
* const result = verifyPlaylist(playlist);
|
|
29
|
+
* if (result.valid) {
|
|
30
|
+
* console.log('Playlist is valid');
|
|
31
|
+
* } else {
|
|
32
|
+
* console.error('Invalid:', result.error);
|
|
33
|
+
* }
|
|
34
|
+
*/
|
|
35
|
+
function verifyPlaylist(playlist) {
|
|
36
|
+
try {
|
|
37
|
+
// Use dp1-js parseDP1Playlist for validation
|
|
38
|
+
const result = (0, dp1_js_1.parseDP1Playlist)(playlist);
|
|
39
|
+
if (result.error) {
|
|
40
|
+
return {
|
|
41
|
+
valid: false,
|
|
42
|
+
error: result.error.message,
|
|
43
|
+
details: result.error.details || [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Validation successful
|
|
47
|
+
return {
|
|
48
|
+
valid: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
valid: false,
|
|
54
|
+
error: `Verification failed: ${error.message}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Verify playlist file
|
|
60
|
+
*
|
|
61
|
+
* Reads playlist from file and validates structure.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} playlistPath - Path to playlist JSON file
|
|
64
|
+
* @returns {Promise<Object>} Verification result
|
|
65
|
+
* @returns {boolean} returns.valid - Whether playlist is valid
|
|
66
|
+
* @returns {Object} [returns.playlist] - Validated playlist object
|
|
67
|
+
* @returns {string} [returns.error] - Error message if invalid
|
|
68
|
+
* @returns {Array<Object>} [returns.details] - Detailed validation errors
|
|
69
|
+
* @example
|
|
70
|
+
* const result = await verifyPlaylistFile('playlist.json');
|
|
71
|
+
* if (result.valid) {
|
|
72
|
+
* console.log('Playlist is valid');
|
|
73
|
+
* }
|
|
74
|
+
*/
|
|
75
|
+
async function verifyPlaylistFile(playlistPath) {
|
|
76
|
+
try {
|
|
77
|
+
// Check if file exists
|
|
78
|
+
try {
|
|
79
|
+
await fs_1.promises.access(playlistPath);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return {
|
|
83
|
+
valid: false,
|
|
84
|
+
error: `Playlist file not found: ${playlistPath}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Read and parse playlist file
|
|
88
|
+
const playlistContent = await fs_1.promises.readFile(playlistPath, 'utf-8');
|
|
89
|
+
let playlistData;
|
|
90
|
+
try {
|
|
91
|
+
playlistData = JSON.parse(playlistContent);
|
|
92
|
+
}
|
|
93
|
+
catch (parseError) {
|
|
94
|
+
return {
|
|
95
|
+
valid: false,
|
|
96
|
+
error: `Invalid JSON: ${parseError.message}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// Verify using dp1-js
|
|
100
|
+
const result = verifyPlaylist(playlistData);
|
|
101
|
+
if (result.valid) {
|
|
102
|
+
return {
|
|
103
|
+
valid: true,
|
|
104
|
+
playlist: playlistData,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
valid: false,
|
|
109
|
+
error: result.error,
|
|
110
|
+
details: result.details,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return {
|
|
115
|
+
valid: false,
|
|
116
|
+
error: `Failed to verify playlist file: ${error.message}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Print verification results to console
|
|
122
|
+
*
|
|
123
|
+
* @param {Object} result - Verification result
|
|
124
|
+
* @param {string} [filename] - Optional filename to include in output
|
|
125
|
+
*/
|
|
126
|
+
function printVerificationResult(result, filename) {
|
|
127
|
+
if (result.valid) {
|
|
128
|
+
console.log(chalk_1.default.green('\n✅ Playlist is valid!'));
|
|
129
|
+
if (filename) {
|
|
130
|
+
console.log(chalk_1.default.gray(` File: ${filename}`));
|
|
131
|
+
}
|
|
132
|
+
if (result.playlist) {
|
|
133
|
+
console.log(chalk_1.default.gray(` Title: ${result.playlist.title}`));
|
|
134
|
+
console.log(chalk_1.default.gray(` Items: ${result.playlist.items?.length || 0}`));
|
|
135
|
+
console.log(chalk_1.default.gray(` DP Version: ${result.playlist.dpVersion}`));
|
|
136
|
+
if (result.playlist.signature && typeof result.playlist.signature === 'string') {
|
|
137
|
+
console.log(chalk_1.default.gray(` Signature: ${result.playlist.signature.substring(0, 30)}...`));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
console.log();
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
console.log(chalk_1.default.red('\n❌ Playlist validation failed!'));
|
|
144
|
+
if (filename) {
|
|
145
|
+
console.log(chalk_1.default.gray(` File: ${filename}`));
|
|
146
|
+
}
|
|
147
|
+
console.log(chalk_1.default.red(` Error: ${result.error}`));
|
|
148
|
+
if (result.details && result.details.length > 0) {
|
|
149
|
+
console.log(chalk_1.default.yellow('\n Validation errors:'));
|
|
150
|
+
result.details.forEach((detail) => {
|
|
151
|
+
console.log(chalk_1.default.yellow(` • ${detail.path}: ${detail.message}`));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
console.log();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.savePlaylist = savePlaylist;
|
|
7
|
+
exports.loadPlaylist = loadPlaylist;
|
|
8
|
+
exports.formatFileSize = formatFileSize;
|
|
9
|
+
const fs_1 = require("fs");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
/**
|
|
12
|
+
* Save playlist to a JSON file
|
|
13
|
+
* @param {Object} playlist - The playlist object
|
|
14
|
+
* @param {string} filename - Output filename
|
|
15
|
+
* @returns {Promise<string>} Path to the saved file
|
|
16
|
+
*/
|
|
17
|
+
async function savePlaylist(playlist, filename) {
|
|
18
|
+
const outputPath = path_1.default.resolve(process.cwd(), filename);
|
|
19
|
+
// Ensure the playlist is properly formatted
|
|
20
|
+
const formattedPlaylist = JSON.stringify(playlist, null, 2);
|
|
21
|
+
await fs_1.promises.writeFile(outputPath, formattedPlaylist, 'utf-8');
|
|
22
|
+
return outputPath;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Load playlist from a JSON file
|
|
26
|
+
* @param {string} filename - Input filename
|
|
27
|
+
* @returns {Promise<Object>} The playlist object
|
|
28
|
+
*/
|
|
29
|
+
async function loadPlaylist(filename) {
|
|
30
|
+
const filePath = path_1.default.resolve(process.cwd(), filename);
|
|
31
|
+
const content = await fs_1.promises.readFile(filePath, 'utf-8');
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Format file size in human-readable format
|
|
36
|
+
* @param {number} bytes - Size in bytes
|
|
37
|
+
* @returns {string} Formatted size
|
|
38
|
+
*/
|
|
39
|
+
function formatFileSize(bytes) {
|
|
40
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
41
|
+
let size = bytes;
|
|
42
|
+
let unitIndex = 0;
|
|
43
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
44
|
+
size /= 1024;
|
|
45
|
+
unitIndex++;
|
|
46
|
+
}
|
|
47
|
+
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
|
48
|
+
}
|