@feralfile/cli 1.1.1

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/config.json.example +96 -0
  4. package/dist/index.js +54 -0
  5. package/dist/src/ai-orchestrator/index.js +1019 -0
  6. package/dist/src/ai-orchestrator/registry.js +96 -0
  7. package/dist/src/commands/build.js +69 -0
  8. package/dist/src/commands/chat.js +189 -0
  9. package/dist/src/commands/config.js +68 -0
  10. package/dist/src/commands/device.js +278 -0
  11. package/dist/src/commands/helpers/config-files.js +62 -0
  12. package/dist/src/commands/helpers/device-discovery.js +111 -0
  13. package/dist/src/commands/helpers/playlist-display.js +161 -0
  14. package/dist/src/commands/helpers/prompt.js +65 -0
  15. package/dist/src/commands/helpers/ssh-helpers.js +44 -0
  16. package/dist/src/commands/play.js +110 -0
  17. package/dist/src/commands/publish.js +115 -0
  18. package/dist/src/commands/setup.js +225 -0
  19. package/dist/src/commands/sign.js +41 -0
  20. package/dist/src/commands/ssh.js +108 -0
  21. package/dist/src/commands/status.js +126 -0
  22. package/dist/src/commands/validate.js +18 -0
  23. package/dist/src/config.js +441 -0
  24. package/dist/src/intent-parser/index.js +1382 -0
  25. package/dist/src/intent-parser/utils.js +108 -0
  26. package/dist/src/logger.js +82 -0
  27. package/dist/src/main.js +459 -0
  28. package/dist/src/types.js +5 -0
  29. package/dist/src/utilities/address-validator.js +242 -0
  30. package/dist/src/utilities/device-default.js +36 -0
  31. package/dist/src/utilities/device-lookup.js +107 -0
  32. package/dist/src/utilities/device-normalize.js +62 -0
  33. package/dist/src/utilities/device-upsert.js +91 -0
  34. package/dist/src/utilities/domain-resolver.js +291 -0
  35. package/dist/src/utilities/ed25519-key-derive.js +155 -0
  36. package/dist/src/utilities/feed-fetcher.js +471 -0
  37. package/dist/src/utilities/ff1-compatibility.js +269 -0
  38. package/dist/src/utilities/ff1-device.js +250 -0
  39. package/dist/src/utilities/ff1-discovery.js +330 -0
  40. package/dist/src/utilities/functions.js +308 -0
  41. package/dist/src/utilities/index.js +469 -0
  42. package/dist/src/utilities/nft-indexer.js +1024 -0
  43. package/dist/src/utilities/playlist-builder.js +523 -0
  44. package/dist/src/utilities/playlist-publisher.js +131 -0
  45. package/dist/src/utilities/playlist-send.js +260 -0
  46. package/dist/src/utilities/playlist-signer.js +204 -0
  47. package/dist/src/utilities/playlist-signing-role.js +41 -0
  48. package/dist/src/utilities/playlist-source.js +128 -0
  49. package/dist/src/utilities/playlist-verifier.js +274 -0
  50. package/dist/src/utilities/ssh-access.js +145 -0
  51. package/dist/src/utils.js +48 -0
  52. package/docs/CONFIGURATION.md +206 -0
  53. package/docs/EXAMPLES.md +390 -0
  54. package/docs/FUNCTION_CALLING.md +96 -0
  55. package/docs/PROJECT_SPEC.md +228 -0
  56. package/docs/README.md +348 -0
  57. package/docs/RELEASING.md +73 -0
  58. package/package.json +76 -0
@@ -0,0 +1,260 @@
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 playlist_source_1 = require("./playlist-source");
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.dim(`[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, runs parse/structure validation (same as `validate`),
78
+ * and returns confirmation result for user review. Does not verify signatures.
79
+ *
80
+ * @param {string} filePath - Playlist file path or URL
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.dim(`[DEBUG] confirmPlaylistForSending called with: filePath="${filePath}", deviceName="${deviceName}" -> "${actualDeviceName}"`));
91
+ }
92
+ try {
93
+ // Load playlist from file or URL
94
+ console.log(chalk_1.default.cyan(`Playlist source: ${resolvedPath}`));
95
+ let playlist;
96
+ let fileExists = false;
97
+ let loadedFrom = 'file';
98
+ try {
99
+ const loaded = await (0, playlist_source_1.loadPlaylistSource)(resolvedPath);
100
+ playlist = loaded.playlist;
101
+ fileExists = loaded.sourceType === 'file';
102
+ loadedFrom = loaded.sourceType;
103
+ console.log(chalk_1.default.green(`Loaded from ${loadedFrom}: ${resolvedPath}`));
104
+ }
105
+ catch (error) {
106
+ const message = error.message;
107
+ const isUrl = (0, playlist_source_1.isPlaylistSourceUrl)(resolvedPath);
108
+ if (isUrl) {
109
+ return {
110
+ success: false,
111
+ filePath: resolvedPath,
112
+ fileExists: false,
113
+ playlistValid: false,
114
+ error: `Could not load playlist URL: ${resolvedPath}`,
115
+ message: `${message}\n\nHint:\n • Check the URL is reachable\n • Confirm it returns JSON\n • Use "send ./path/to/playlist.json" for local files`,
116
+ };
117
+ }
118
+ if (message.includes('Invalid JSON in')) {
119
+ return {
120
+ success: false,
121
+ filePath: resolvedPath,
122
+ fileExists: true,
123
+ playlistValid: false,
124
+ error: message,
125
+ message: `${message}\n\nHint:\n • Check the JSON payload is valid DP-1 format\n • Check local path points to a playlist file`,
126
+ };
127
+ }
128
+ return {
129
+ success: false,
130
+ filePath: resolvedPath,
131
+ fileExists: false,
132
+ playlistValid: false,
133
+ error: `Playlist file not found at ${resolvedPath}`,
134
+ 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"`,
135
+ };
136
+ }
137
+ if (!playlist) {
138
+ return {
139
+ success: false,
140
+ filePath: resolvedPath,
141
+ fileExists,
142
+ playlistValid: false,
143
+ error: `Playlist source is empty: ${loadedFrom}`,
144
+ };
145
+ }
146
+ // Parse / structure validation only (same as `validate`; use `verify` CLI for signatures)
147
+ console.log(chalk_1.default.cyan('Validation'));
148
+ // Dynamic import to avoid circular dependency
149
+ const { validatePlaylist } = await Promise.resolve().then(() => __importStar(require('./playlist-verifier')));
150
+ const validateResult = await validatePlaylist(playlist);
151
+ if (!validateResult.valid) {
152
+ console.log(chalk_1.default.red('Playlist validation failed'));
153
+ const detailLines = validateResult.details?.map((d) => ` • ${d.path}: ${d.message}`).join('\n') ||
154
+ validateResult.error;
155
+ const detailPaths = validateResult.details?.map((d) => d.path) || [];
156
+ const hints = [];
157
+ if (detailPaths.some((path) => path.includes('signature'))) {
158
+ hints.push('Add `playlist.privateKey` (or `PLAYLIST_PRIVATE_KEY`) and rebuild the playlist to include signatures.');
159
+ }
160
+ if (detailPaths.some((path) => path.includes('defaults.display.margin'))) {
161
+ hints.push('Rebuild the playlist with the latest CLI defaults (margin must be numeric).');
162
+ }
163
+ const hintText = hints.length > 0 ? `\n\nHint:\n${hints.map((h) => ` • ${h}`).join('\n')}` : '';
164
+ return {
165
+ success: false,
166
+ filePath: resolvedPath,
167
+ fileExists: true,
168
+ playlistValid: false,
169
+ playlist,
170
+ deviceName: actualDeviceName,
171
+ error: `Playlist is invalid: ${validateResult.error}`,
172
+ message: `This playlist doesn't match DP-1 specification.\n\nErrors:\n${detailLines}${hintText}`,
173
+ };
174
+ }
175
+ console.log(chalk_1.default.green('Valid DP-1 playlist'));
176
+ // Display confirmation details
177
+ const itemCount = playlist.items?.length || 0;
178
+ const title = playlist.title || 'Untitled';
179
+ // Handle device selection
180
+ let selectedDevice = actualDeviceName;
181
+ let needsDeviceSelection = false;
182
+ let availableDevices = [];
183
+ if (!selectedDevice) {
184
+ // Get available devices
185
+ availableDevices = await getAvailableDevices();
186
+ if (process.env.DEBUG) {
187
+ console.error(chalk_1.default.dim(`[DEBUG] selectedDevice is null/undefined`));
188
+ console.error(chalk_1.default.dim(`[DEBUG] Available devices found: ${availableDevices.length}`));
189
+ availableDevices.forEach((d) => {
190
+ console.error(chalk_1.default.dim(`[DEBUG] Device: ${d.name} (${d.host})`));
191
+ });
192
+ }
193
+ if (availableDevices.length === 0) {
194
+ return {
195
+ success: false,
196
+ filePath: resolvedPath,
197
+ fileExists: true,
198
+ playlistValid: true,
199
+ playlist,
200
+ error: 'No FF1 devices configured',
201
+ 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}`,
202
+ };
203
+ }
204
+ else if (availableDevices.length === 1) {
205
+ // Auto-select single device
206
+ selectedDevice = availableDevices[0].name || availableDevices[0].host;
207
+ console.log(chalk_1.default.cyan(`Device: ${selectedDevice} (auto)`));
208
+ }
209
+ else {
210
+ // Multiple devices - need user to choose
211
+ needsDeviceSelection = true;
212
+ }
213
+ }
214
+ console.log();
215
+ console.log(chalk_1.default.bold('Send Summary'));
216
+ console.log(chalk_1.default.dim(` Title: ${title}`));
217
+ console.log(chalk_1.default.dim(` Items: ${itemCount}`));
218
+ if (selectedDevice) {
219
+ console.log(chalk_1.default.dim(` Device: ${selectedDevice}`));
220
+ }
221
+ else if (availableDevices.length > 1) {
222
+ console.log(chalk_1.default.dim(' Device: select one'));
223
+ }
224
+ console.log();
225
+ // If multiple devices, return needsDeviceSelection flag
226
+ if (needsDeviceSelection) {
227
+ return {
228
+ success: false,
229
+ filePath: resolvedPath,
230
+ fileExists: true,
231
+ playlistValid: true,
232
+ playlist,
233
+ needsDeviceSelection: true,
234
+ availableDevices,
235
+ error: 'Multiple devices available - please choose one',
236
+ 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"`,
237
+ };
238
+ }
239
+ return {
240
+ success: true,
241
+ filePath: resolvedPath,
242
+ fileExists: true,
243
+ playlistValid: true,
244
+ playlist,
245
+ deviceName: selectedDevice,
246
+ message: `Ready to send "${title}" (${itemCount} items) to ${selectedDevice}!`,
247
+ };
248
+ }
249
+ catch (error) {
250
+ const errorMsg = error.message;
251
+ console.log(chalk_1.default.red(`Error: ${errorMsg}`));
252
+ return {
253
+ success: false,
254
+ filePath: resolvedPath,
255
+ fileExists: false,
256
+ playlistValid: false,
257
+ error: errorMsg,
258
+ };
259
+ }
260
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Playlist Signing Utility.
3
+ * Uses the DP-1 v1.1.0 signing contract via the `dp1-js` package.
4
+ */
5
+ const { getPlaylistConfig } = require('../config');
6
+ const { isDp1PlaylistSigningRole } = require('./playlist-signing-role');
7
+ /**
8
+ * Sign a playlist using the DP-1 signing API.
9
+ * The signed payload excludes any pre-existing signature fields so the output
10
+ * is stable across re-signing and matches the library's canonical digest.
11
+ *
12
+ * @param {Object} playlist - Playlist object without signature
13
+ * @param {string} [privateKeyBase64] - Ed25519 private key in hex or base64 format (optional, uses config if not provided)
14
+ * @param {string} [roleOverride] - DP-1 signing role override (optional, uses config if not provided)
15
+ * @returns {Promise<Object>} DP-1 signature envelope
16
+ * @throws {Error} If private key is invalid or signing fails
17
+ */
18
+ async function signPlaylist(playlist, privateKeyBase64, roleOverride) {
19
+ // Get private key from config if not provided
20
+ let privateKey = privateKeyBase64;
21
+ if (!privateKey) {
22
+ const config = getPlaylistConfig();
23
+ privateKey = config.privateKey;
24
+ }
25
+ if (!privateKey) {
26
+ throw new Error('Private key is required for signing');
27
+ }
28
+ try {
29
+ const playlistToSign = { ...playlist };
30
+ delete playlistToSign.signature;
31
+ delete playlistToSign.signatures;
32
+ const dp1 = await loadDp1();
33
+ const raw = Buffer.from(JSON.stringify(playlistToSign));
34
+ const config = getPlaylistConfig();
35
+ const role = resolvePlaylistSigningRole(roleOverride || config.role);
36
+ if (typeof dp1.SignMultiEd25519 === 'function') {
37
+ return dp1.SignMultiEd25519(raw, privateKey, role, currentTimestamp());
38
+ }
39
+ throw new Error('dp1-js does not expose SignMultiEd25519');
40
+ }
41
+ catch (error) {
42
+ throw new Error(`Failed to sign playlist: ${error.message}`);
43
+ }
44
+ }
45
+ /**
46
+ * Verify a playlist signature with the DP-1 verification API.
47
+ *
48
+ * @param {Object} playlist - Playlist object with signature field
49
+ * @param {string} publicKeyHex - Ed25519 public key in hex format (with or without 0x prefix)
50
+ * @returns {Promise<boolean>} True if signature is valid, false otherwise
51
+ * @throws {Error} If verification process fails
52
+ */
53
+ async function verifyPlaylist(playlist, publicKeyHex) {
54
+ if (!publicKeyHex) {
55
+ throw new Error('Public key is required for verification');
56
+ }
57
+ try {
58
+ const dp1 = await loadDp1();
59
+ const verifyFn = dp1.verifyPlaylist;
60
+ if (typeof verifyFn !== 'function') {
61
+ throw new Error('dp1-js does not expose verifyPlaylist');
62
+ }
63
+ const isValid = await verifyFn(playlist, publicKeyHex);
64
+ return isValid;
65
+ }
66
+ catch (error) {
67
+ throw new Error(`Failed to verify playlist signature: ${error.message}`);
68
+ }
69
+ }
70
+ /**
71
+ * Sign a playlist file
72
+ * Reads playlist from file, signs it, and writes back
73
+ *
74
+ * @param {string} playlistPath - Path to playlist JSON file
75
+ * @param {string} [privateKeyBase64] - Ed25519 private key in hex or base64 format (optional, uses config if not provided)
76
+ * @param {string} [outputPath] - Output path (optional, overwrites input if not provided)
77
+ * @returns {Promise<Object>} Result with signed playlist
78
+ * @returns {boolean} returns.success - Whether signing succeeded
79
+ * @returns {Object} [returns.playlist] - Signed playlist object
80
+ * @returns {string} [returns.error] - Error message if failed
81
+ */
82
+ async function signPlaylistFile(playlistPath, privateKeyBase64, outputPath, roleOverride) {
83
+ const fs = require('fs');
84
+ const path = require('path');
85
+ try {
86
+ // Read playlist file
87
+ if (!fs.existsSync(playlistPath)) {
88
+ throw new Error(`Playlist file not found: ${playlistPath}`);
89
+ }
90
+ const playlistContent = fs.readFileSync(playlistPath, 'utf-8');
91
+ const playlist = JSON.parse(playlistContent);
92
+ const config = getPlaylistConfig();
93
+ const privateKey = privateKeyBase64 || config.privateKey;
94
+ const role = resolvePlaylistSigningRole(roleOverride || config.role);
95
+ const validation = await validatePlaylistForSigning(playlist);
96
+ if (!validation.valid) {
97
+ throw new Error(`Playlist validation failed: ${validation.error}`);
98
+ }
99
+ const dp1 = await loadDp1();
100
+ if (!privateKey) {
101
+ throw new Error('Private key is required for signing');
102
+ }
103
+ const signedPlaylist = await buildSignedPlaylistEnvelope(playlist, privateKey, dp1, role);
104
+ const verification = await verifySignedPlaylistEnvelope(signedPlaylist, dp1);
105
+ if (!verification.valid) {
106
+ throw new Error(`Signed playlist verification failed: ${verification.error}`);
107
+ }
108
+ // Write to output file
109
+ const output = outputPath || playlistPath;
110
+ fs.writeFileSync(output, JSON.stringify(signedPlaylist, null, 2), 'utf-8');
111
+ console.log(`✓ Playlist signed and saved to: ${path.resolve(output)}`);
112
+ return {
113
+ success: true,
114
+ playlist: signedPlaylist,
115
+ };
116
+ }
117
+ catch (error) {
118
+ return {
119
+ success: false,
120
+ error: error.message,
121
+ };
122
+ }
123
+ }
124
+ module.exports = {
125
+ signPlaylist,
126
+ verifyPlaylist,
127
+ signPlaylistFile,
128
+ };
129
+ function resolvePlaylistSigningRole(role) {
130
+ const candidate = typeof role === 'string' ? role.trim() : '';
131
+ const effectiveRole = candidate || 'agent';
132
+ if (!isDp1PlaylistSigningRole(effectiveRole)) {
133
+ throw new Error(`Unsupported DP-1 playlist signing role "${effectiveRole}". Expected one of: agent, feed, curator, institution, licensor`);
134
+ }
135
+ return effectiveRole;
136
+ }
137
+ async function validatePlaylistForSigning(playlist) {
138
+ const dp1 = await loadDp1();
139
+ const parseFn = dp1.parseDP1Playlist;
140
+ if (typeof parseFn !== 'function') {
141
+ throw new Error('dp1-js does not expose parseDP1Playlist');
142
+ }
143
+ const result = parseFn(playlist);
144
+ if (result && result.error) {
145
+ return { valid: false, error: result.error.message };
146
+ }
147
+ return { valid: true };
148
+ }
149
+ /**
150
+ * Produce a DP-1 v1.1.0 playlist object with a new multi-signature appended.
151
+ * The digest uses JSON with top-level `signature` and `signatures` removed (same
152
+ * as dp1-js/dp1-go §7.1); prior `signatures[]` entries are kept on the returned
153
+ * object so repeated `sign` runs accumulate endorsements instead of replacing them.
154
+ *
155
+ * @param {Object} playlist - Parsed playlist (may already include `signatures[]`)
156
+ * @param {string} privateKey - Private key material forwarded to dp1-js
157
+ * @param {Object} dp1 - Loaded dp1-js module
158
+ * @param {string} role - DP-1 signing role
159
+ * @returns {Promise<Object>} Playlist with legacy `signature` cleared and merged `signatures[]`
160
+ */
161
+ async function buildSignedPlaylistEnvelope(playlist, privateKey, dp1, role) {
162
+ const playlistToSign = { ...playlist };
163
+ delete playlistToSign.signature;
164
+ delete playlistToSign.signatures;
165
+ const existingSignatures = Array.isArray(playlist.signatures)
166
+ ? playlist.signatures.filter((entry) => Boolean(entry))
167
+ : [];
168
+ if (typeof dp1.SignMultiEd25519 === 'function') {
169
+ const signature = await dp1.SignMultiEd25519(Buffer.from(JSON.stringify(playlistToSign)), privateKey, role, currentTimestamp());
170
+ return {
171
+ ...playlist,
172
+ signature: undefined,
173
+ signatures: [...existingSignatures, signature],
174
+ };
175
+ }
176
+ throw new Error('dp1-js does not expose SignMultiEd25519');
177
+ }
178
+ /**
179
+ * Verify a signed playlist envelope with dp1-js before it is persisted.
180
+ * The sign command must only write outputs that the same verifier path accepts;
181
+ * otherwise it can succeed while immediately generating a broken artifact.
182
+ *
183
+ * @param {Object} signedPlaylist - Playlist envelope with signatures attached
184
+ * @param {Object} dp1 - Loaded dp1-js module
185
+ * @returns {Promise<{ valid: boolean; error?: string }>} Verification result
186
+ */
187
+ async function verifySignedPlaylistEnvelope(signedPlaylist, dp1) {
188
+ const verifyFn = dp1.verifyPlaylist;
189
+ if (typeof verifyFn !== 'function') {
190
+ throw new Error('dp1-js does not expose verifyPlaylist');
191
+ }
192
+ const isValid = await verifyFn(signedPlaylist);
193
+ if (!isValid) {
194
+ return { valid: false, error: 'signed playlist is not verifiable' };
195
+ }
196
+ return { valid: true };
197
+ }
198
+ /** Loads `dp1-js`; env overrides are not supported (see playlist-verifier). */
199
+ async function loadDp1() {
200
+ return require('dp1-js');
201
+ }
202
+ function currentTimestamp() {
203
+ return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
204
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Supported DP-1 playlist signing roles.
3
+ *
4
+ * Keep this list aligned with the published dp1-js role constants so guided
5
+ * setup, config validation, and signing all agree on the same contract.
6
+ */
7
+ const DP1_PLAYLIST_SIGNING_ROLES = Object.freeze([
8
+ 'agent',
9
+ 'feed',
10
+ 'curator',
11
+ 'institution',
12
+ 'licensor',
13
+ ]);
14
+ function isDp1PlaylistSigningRole(role) {
15
+ return typeof role === 'string' && DP1_PLAYLIST_SIGNING_ROLES.includes(role);
16
+ }
17
+ function formatUnsupportedPlaylistSigningRoleError(role) {
18
+ return `Unsupported DP-1 playlist signing role "${role}". Expected one of: ${DP1_PLAYLIST_SIGNING_ROLES.join(', ')}`;
19
+ }
20
+ function normalizeDp1PlaylistSigningRole(value, defaultRole = 'agent') {
21
+ const trimmedValue = typeof value === 'string' ? value.trim() : '';
22
+ if (trimmedValue) {
23
+ return trimmedValue;
24
+ }
25
+ const trimmedDefault = typeof defaultRole === 'string' ? defaultRole.trim() : '';
26
+ return trimmedDefault || 'agent';
27
+ }
28
+ function resolveDp1PlaylistSigningRole(role, fallbackRole = 'agent') {
29
+ const candidate = normalizeDp1PlaylistSigningRole(role, fallbackRole);
30
+ if (!isDp1PlaylistSigningRole(candidate)) {
31
+ throw new Error(formatUnsupportedPlaylistSigningRoleError(candidate));
32
+ }
33
+ return candidate;
34
+ }
35
+ module.exports = {
36
+ DP1_PLAYLIST_SIGNING_ROLES,
37
+ isDp1PlaylistSigningRole,
38
+ formatUnsupportedPlaylistSigningRoleError,
39
+ normalizeDp1PlaylistSigningRole,
40
+ resolveDp1PlaylistSigningRole,
41
+ };
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isPlaylistSourceUrl = isPlaylistSourceUrl;
4
+ exports.loadPlaylistSource = loadPlaylistSource;
5
+ exports.resolvePlaySource = resolvePlaySource;
6
+ const fs_1 = require("fs");
7
+ // playlist-builder is still CommonJS; require keeps the interop simple.
8
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9
+ const { buildUrlItem, buildDP1Playlist } = require('./playlist-builder');
10
+ /**
11
+ * Determine whether a playlist source is an HTTP(S) URL.
12
+ *
13
+ * @param {string} source - Playlist source value
14
+ * @returns {boolean} Whether the value parses as http:// or https:// URL
15
+ */
16
+ function isPlaylistSourceUrl(source) {
17
+ const trimmed = source.trim();
18
+ if (!trimmed) {
19
+ return false;
20
+ }
21
+ try {
22
+ const parsed = new URL(trimmed);
23
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ /**
30
+ * Load a DP-1 playlist from a local file or hosted URL.
31
+ *
32
+ * @param {string} source - Playlist file path or URL
33
+ * @returns {Promise<LoadedPlaylist>} Loaded playlist payload with source metadata
34
+ * @throws {Error} When source is empty, cannot be loaded, or JSON is invalid
35
+ */
36
+ async function loadPlaylistSource(source) {
37
+ const trimmedSource = source.trim();
38
+ if (!trimmedSource) {
39
+ throw new Error('Playlist source is required');
40
+ }
41
+ if (isPlaylistSourceUrl(trimmedSource)) {
42
+ const response = await fetch(trimmedSource);
43
+ if (!response.ok) {
44
+ throw new Error(`Failed to fetch playlist URL: ${response.status} ${response.statusText}`);
45
+ }
46
+ let playlistText;
47
+ try {
48
+ playlistText = await response.text();
49
+ }
50
+ catch (error) {
51
+ throw new Error(`Failed to read playlist response from ${trimmedSource}: ${error.message}`);
52
+ }
53
+ try {
54
+ return {
55
+ playlist: JSON.parse(playlistText),
56
+ source: trimmedSource,
57
+ sourceType: 'url',
58
+ };
59
+ }
60
+ catch (error) {
61
+ throw new Error(`Invalid JSON from playlist URL ${trimmedSource}: ${error.message}`);
62
+ }
63
+ }
64
+ let fileText;
65
+ try {
66
+ fileText = await fs_1.promises.readFile(trimmedSource, 'utf-8');
67
+ }
68
+ catch (_error) {
69
+ throw new Error(`Playlist file not found at ${trimmedSource}`);
70
+ }
71
+ try {
72
+ return {
73
+ playlist: JSON.parse(fileText),
74
+ source: trimmedSource,
75
+ sourceType: 'file',
76
+ };
77
+ }
78
+ catch (error) {
79
+ throw new Error(`Invalid JSON in ${trimmedSource}: ${error.message}`);
80
+ }
81
+ }
82
+ /**
83
+ * Resolve a `play` command argument to a playable source.
84
+ *
85
+ * Files are always treated as playlists. URLs are tried as playlists first;
86
+ * if loading fails (network, non-JSON, etc.), the URL is wrapped in a
87
+ * synthesized single-item playlist so direct media URLs still work.
88
+ *
89
+ * The "URL → playlist or media" path uses throw-and-fallback because there
90
+ * is no cheap way to distinguish a 200-OK media file from a malformed
91
+ * playlist response without trying to parse it. Keeping the fallback
92
+ * scoped to one helper means the play action no longer carries the
93
+ * `loadedAsPlaylist` boolean dance.
94
+ *
95
+ * @param source - User-supplied path or URL
96
+ * @param defaultDuration - Duration (seconds) for the synthesized media item
97
+ * @returns Resolved playlist + metadata describing how it was loaded
98
+ * @throws Error When `source` is empty, or is a non-URL file path that cannot be loaded
99
+ */
100
+ async function resolvePlaySource(source, defaultDuration) {
101
+ const trimmed = source.trim();
102
+ if (!trimmed) {
103
+ throw new Error('Playlist source is required');
104
+ }
105
+ if (!isPlaylistSourceUrl(trimmed)) {
106
+ const loaded = await loadPlaylistSource(trimmed);
107
+ return {
108
+ kind: 'playlist',
109
+ playlist: loaded.playlist,
110
+ sourceType: loaded.sourceType,
111
+ source: loaded.source,
112
+ };
113
+ }
114
+ try {
115
+ const loaded = await loadPlaylistSource(trimmed);
116
+ return {
117
+ kind: 'playlist',
118
+ playlist: loaded.playlist,
119
+ sourceType: loaded.sourceType,
120
+ source: loaded.source,
121
+ };
122
+ }
123
+ catch {
124
+ const item = buildUrlItem(trimmed, defaultDuration);
125
+ const playlist = (await buildDP1Playlist({ items: [item], title: item.title }));
126
+ return { kind: 'media', playlist, source: trimmed };
127
+ }
128
+ }