@feralfile/cli 3.0.0 → 4.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/README.md +20 -17
- package/config.json.example +1 -1
- package/dist/index.js +10 -2
- package/dist/package.json +5 -2
- package/dist/src/commands/build.js +1 -1
- package/dist/src/commands/device.js +84 -39
- package/dist/src/commands/enrich.js +2 -2
- package/dist/src/commands/find.js +71 -37
- package/dist/src/commands/helpers/device-control.js +937 -0
- package/dist/src/commands/helpers/hidden-alias-help.js +37 -0
- package/dist/src/commands/helpers/playlist-display.js +2 -15
- package/dist/src/commands/play.js +39 -26
- package/dist/src/commands/report.js +251 -0
- package/dist/src/commands/setup.js +1 -1
- package/dist/src/config.js +162 -6
- package/dist/src/utilities/control/codec.js +578 -0
- package/dist/src/utilities/control/control-errors.js +157 -0
- package/dist/src/utilities/control/controller-store.js +964 -0
- package/dist/src/utilities/control/jwk.js +141 -0
- package/dist/src/utilities/control/pairing-channel.js +52 -0
- package/dist/src/utilities/control/pairing.js +416 -0
- package/dist/src/utilities/control/registration.js +391 -0
- package/dist/src/utilities/control/relay-pairing-channel.js +191 -0
- package/dist/src/utilities/control/secure-transport.js +850 -0
- package/dist/src/utilities/control/strict-json.js +376 -0
- package/dist/src/utilities/device-network.js +129 -0
- package/dist/src/utilities/device-upsert.js +1 -1
- package/dist/src/utilities/ff1-compatibility.js +87 -11
- package/dist/src/utilities/ff1-device.js +161 -78
- package/dist/src/utilities/ff1-relayer.js +34 -14
- package/dist/src/utilities/handoff-client.js +23 -9
- package/dist/src/utilities/neort-marketplace.js +3 -3
- package/dist/src/utilities/playlist-builder.js +3 -3
- package/dist/src/utilities/playlist-cast.js +6 -6
- package/dist/src/utilities/playlist-source.js +7 -7
- package/dist/src/utilities/raster-client.js +1 -1
- package/dist/src/utilities/relay-client.js +774 -0
- package/dist/src/utilities/ssh-access.js +192 -58
- package/dist/src/utilities/support-report.js +511 -0
- package/dist/src/utilities/temporary-channel.js +139 -0
- package/docs/CONFIGURATION.md +270 -28
- package/docs/EXAMPLES.md +31 -31
- package/docs/PROJECT_SPEC.md +32 -19
- package/docs/README.md +113 -36
- package/package.json +5 -2
package/dist/src/config.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.getPlaylistConfig = getPlaylistConfig;
|
|
|
11
11
|
exports.getFeedConfig = getFeedConfig;
|
|
12
12
|
exports.getFF1DeviceConfig = getFF1DeviceConfig;
|
|
13
13
|
exports.getFF1RelayerConfig = getFF1RelayerConfig;
|
|
14
|
+
exports.getSupportLogsConfig = getSupportLogsConfig;
|
|
14
15
|
exports.getTokenSourceConfig = getTokenSourceConfig;
|
|
15
16
|
exports.validateConfig = validateConfig;
|
|
16
17
|
exports.createSampleConfig = createSampleConfig;
|
|
@@ -19,6 +20,45 @@ const path_1 = __importDefault(require("path"));
|
|
|
19
20
|
const os_1 = __importDefault(require("os"));
|
|
20
21
|
const playlist_signing_role_1 = require("./utilities/playlist-signing-role");
|
|
21
22
|
const config_placeholders_1 = require("./utilities/config-placeholders");
|
|
23
|
+
/**
|
|
24
|
+
* The feed server used when neither config.json nor FEED_BASE_URLS names one. It replaced
|
|
25
|
+
* dp1-feed-operator-api-prod.autonomy-system.workers.dev, a deleted worker whose zone belongs to
|
|
26
|
+
* an account being retired: a default must never name a host someone else could register.
|
|
27
|
+
* DEFAULT_FF1_RELAYER_URL below is in the same zone but still live; the ff-cli release after
|
|
28
|
+
* the legacy relayer shutdown turns it off (ffos-user docs/ff1-control-v1.md §15.4).
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULT_FEED_BASE_URL = 'https://feed.feralfile.com/api/v1';
|
|
31
|
+
/**
|
|
32
|
+
* The deleted worker ff-cli 3.0.0 named. Its `setup` and `config init` copied it into every
|
|
33
|
+
* config.json they wrote, and an explicit feed wins over the default, so a configured URL on
|
|
34
|
+
* this host is read as feed.feralfile.com (path kept) instead: same operator, same API.
|
|
35
|
+
*/
|
|
36
|
+
const RETIRED_FEED_HOST = 'dp1-feed-operator-api-prod.autonomy-system.workers.dev';
|
|
37
|
+
const RETIRED_FEED_REPLACEMENT_ORIGIN = 'https://feed.feralfile.com';
|
|
38
|
+
let retiredFeedNoted = false;
|
|
39
|
+
/**
|
|
40
|
+
* replaceRetiredFeedUrl maps a feed URL on RETIRED_FEED_HOST to feed.feralfile.com, keeping its
|
|
41
|
+
* path, and notes it once per run on stderr (stdout may be a script's JSON). Any other URL,
|
|
42
|
+
* including an unparseable one, is returned unchanged.
|
|
43
|
+
*/
|
|
44
|
+
function replaceRetiredFeedUrl(url) {
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
parsed = new URL(url);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return url;
|
|
51
|
+
}
|
|
52
|
+
if (parsed.hostname.toLowerCase() !== RETIRED_FEED_HOST) {
|
|
53
|
+
return url;
|
|
54
|
+
}
|
|
55
|
+
const replaced = `${RETIRED_FEED_REPLACEMENT_ORIGIN}${parsed.pathname.replace(/\/$/, '')}${parsed.search}`;
|
|
56
|
+
if (!retiredFeedNoted) {
|
|
57
|
+
retiredFeedNoted = true;
|
|
58
|
+
console.error(`Note: ${url} is a retired feed server; using ${replaced}. Update the feed in config.json or FEED_BASE_URLS to stop this note.`);
|
|
59
|
+
}
|
|
60
|
+
return replaced;
|
|
61
|
+
}
|
|
22
62
|
function getConfigPaths() {
|
|
23
63
|
const localPath = path_1.default.join(process.cwd(), 'config.json');
|
|
24
64
|
const configBase = process.env.XDG_CONFIG_HOME || path_1.default.join(os_1.default.homedir(), '.config');
|
|
@@ -44,7 +84,7 @@ function loadConfig() {
|
|
|
44
84
|
feed: {
|
|
45
85
|
baseURLs: process.env.FEED_BASE_URLS
|
|
46
86
|
? process.env.FEED_BASE_URLS.split(',')
|
|
47
|
-
: [
|
|
87
|
+
: [DEFAULT_FEED_BASE_URL],
|
|
48
88
|
},
|
|
49
89
|
};
|
|
50
90
|
// Try to load config.json if it exists
|
|
@@ -189,10 +229,13 @@ function getFeedConfig() {
|
|
|
189
229
|
const config = getConfig();
|
|
190
230
|
// Check for new feedServers format first
|
|
191
231
|
if (config.feedServers && Array.isArray(config.feedServers) && config.feedServers.length > 0) {
|
|
192
|
-
const
|
|
232
|
+
const servers = config.feedServers.map((server) => ({
|
|
233
|
+
...server,
|
|
234
|
+
baseUrl: replaceRetiredFeedUrl(server.baseUrl),
|
|
235
|
+
}));
|
|
193
236
|
return {
|
|
194
|
-
baseURLs,
|
|
195
|
-
servers
|
|
237
|
+
baseURLs: servers.map((server) => server.baseUrl),
|
|
238
|
+
servers,
|
|
196
239
|
};
|
|
197
240
|
}
|
|
198
241
|
// Fall back to legacy feed format
|
|
@@ -207,10 +250,10 @@ function getFeedConfig() {
|
|
|
207
250
|
}
|
|
208
251
|
else {
|
|
209
252
|
// Default feed URL
|
|
210
|
-
urls = [
|
|
253
|
+
urls = [DEFAULT_FEED_BASE_URL];
|
|
211
254
|
}
|
|
212
255
|
return {
|
|
213
|
-
baseURLs: urls,
|
|
256
|
+
baseURLs: urls.map(replaceRetiredFeedUrl),
|
|
214
257
|
};
|
|
215
258
|
}
|
|
216
259
|
/**
|
|
@@ -231,6 +274,12 @@ function getFF1DeviceConfig() {
|
|
|
231
274
|
};
|
|
232
275
|
}
|
|
233
276
|
const DEFAULT_FF1_RELAYER_URL = 'https://tv-cast-coordination.autonomy-system.workers.dev';
|
|
277
|
+
/**
|
|
278
|
+
* The production relay v2 origin. Relay v2 runs on its own Worker, not on the
|
|
279
|
+
* legacy relayer's origin, and must keep working after the legacy host is
|
|
280
|
+
* retired (ffos-user docs/ff1-control-v1.md §15.3, §15.4).
|
|
281
|
+
*/
|
|
282
|
+
const DEFAULT_FF1_RELAY_V2_ENDPOINT = 'wss://relayer.feralfile.io';
|
|
234
283
|
/**
|
|
235
284
|
* Get the FF1 relayer endpoint using repository config priority.
|
|
236
285
|
*
|
|
@@ -271,12 +320,55 @@ function resolveFF1RelayerConfig(config) {
|
|
|
271
320
|
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
272
321
|
throw new Error('ff1Relayer.baseUrl must be a valid HTTP(S) URL');
|
|
273
322
|
}
|
|
323
|
+
const v2 = resolveV2Endpoint(fields.v2Endpoint, parsed);
|
|
274
324
|
const apiKey = (fields.apiKey || process.env.FF1_RELAYER_API_KEY)?.trim();
|
|
275
325
|
return {
|
|
276
326
|
baseUrl,
|
|
277
327
|
apiKey: apiKey || undefined,
|
|
328
|
+
...(v2.endpoint ? { v2Endpoint: v2.endpoint } : {}),
|
|
329
|
+
...(v2.endpoint && v2.explicit ? { v2Explicit: true } : {}),
|
|
278
330
|
};
|
|
279
331
|
}
|
|
332
|
+
const DEFAULT_SUPPORT_LOGS_URL = 'https://support-logs.feralfile.com';
|
|
333
|
+
/**
|
|
334
|
+
* getSupportLogsConfig returns the support-logs origin `ff-cli report` posts to.
|
|
335
|
+
*
|
|
336
|
+
* `config.json` (`supportLogs.baseUrl`) wins over FF_SUPPORT_LOGS_URL, which
|
|
337
|
+
* wins over the production default, the same order as `ff1Relayer.baseUrl`.
|
|
338
|
+
* The override is for tests and staging. It does not move the Art Computer's
|
|
339
|
+
* log upload: FF OS posts its logs to the support-logs its firmware names.
|
|
340
|
+
*/
|
|
341
|
+
function getSupportLogsConfig() {
|
|
342
|
+
return resolveSupportLogsConfig(getConfig());
|
|
343
|
+
}
|
|
344
|
+
/** Resolve and validate the effective support-logs configuration. */
|
|
345
|
+
function resolveSupportLogsConfig(config) {
|
|
346
|
+
const configured = config.supportLogs;
|
|
347
|
+
if (configured !== undefined &&
|
|
348
|
+
(configured === null || typeof configured !== 'object' || Array.isArray(configured))) {
|
|
349
|
+
throw new Error('supportLogs must be an object');
|
|
350
|
+
}
|
|
351
|
+
const baseUrl = configured?.baseUrl;
|
|
352
|
+
if (baseUrl !== undefined && typeof baseUrl !== 'string') {
|
|
353
|
+
throw new Error('supportLogs.baseUrl must be a string');
|
|
354
|
+
}
|
|
355
|
+
const value = (baseUrl ||
|
|
356
|
+
process.env.FF_SUPPORT_LOGS_URL ||
|
|
357
|
+
DEFAULT_SUPPORT_LOGS_URL)
|
|
358
|
+
.trim()
|
|
359
|
+
.replace(/\/+$/, '');
|
|
360
|
+
let parsed;
|
|
361
|
+
try {
|
|
362
|
+
parsed = new URL(value);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
throw new Error('supportLogs.baseUrl must be a valid HTTP(S) URL');
|
|
366
|
+
}
|
|
367
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
368
|
+
throw new Error('supportLogs.baseUrl must be a valid HTTP(S) URL');
|
|
369
|
+
}
|
|
370
|
+
return { baseUrl: value };
|
|
371
|
+
}
|
|
280
372
|
const TOKEN_SOURCES = ['raster', 'indexer'];
|
|
281
373
|
const DEFAULT_TOKEN_SOURCE = 'indexer';
|
|
282
374
|
const DEFAULT_IPFS_GATEWAY_FERAL_FILE = 'https://ipfs.feralfile.com/ipfs/';
|
|
@@ -364,6 +456,64 @@ function normalizeIpfsGateway(value, label) {
|
|
|
364
456
|
parsed.hash = '';
|
|
365
457
|
return parsed.toString();
|
|
366
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* resolveV2Endpoint picks the relay v2 origin (ffos-user docs/ff1-control-v1.md
|
|
461
|
+
* §10). An explicit `ff1Relayer.v2Endpoint` or FF1_RELAY_V2_ENDPOINT wins; an
|
|
462
|
+
* empty string turns v2 off. Otherwise the production legacy relayer maps to
|
|
463
|
+
* the production relay v2 Worker (DEFAULT_FF1_RELAY_V2_ENDPOINT), which lives
|
|
464
|
+
* on its own origin. A custom HTTPS legacy relayer (a development Worker that
|
|
465
|
+
* serves both `/api` and `/v2/channels/…`) still uses its own origin. A
|
|
466
|
+
* custom plain-HTTP or path-bearing legacy relayer gets no v2 unless one is
|
|
467
|
+
* configured.
|
|
468
|
+
*
|
|
469
|
+
* The derived default is only a pairing default for commands: during the
|
|
470
|
+
* rollout most FF1s are not on v2, and a stranger's ACK on the device channel
|
|
471
|
+
* would turn a command that the legacy topic could have delivered into an
|
|
472
|
+
* ambiguous one. secure-transport therefore sends commands on a derived
|
|
473
|
+
* endpoint only to an FF1 that advertised `relayV2` in a verified status.
|
|
474
|
+
*
|
|
475
|
+
* @param configured - The `ff1Relayer.v2Endpoint` value, if any
|
|
476
|
+
* @param baseUrl - The validated legacy relayer URL
|
|
477
|
+
* @returns The v2 origin (undefined when v2 is off) and whether it was configured
|
|
478
|
+
* @throws {Error} When a configured endpoint is not an acceptable origin
|
|
479
|
+
*/
|
|
480
|
+
function resolveV2Endpoint(configured, baseUrl) {
|
|
481
|
+
const value = configured ?? process.env.FF1_RELAY_V2_ENDPOINT;
|
|
482
|
+
if (value === undefined) {
|
|
483
|
+
if (baseUrl.origin === new URL(DEFAULT_FF1_RELAYER_URL).origin) {
|
|
484
|
+
return { endpoint: DEFAULT_FF1_RELAY_V2_ENDPOINT, explicit: false };
|
|
485
|
+
}
|
|
486
|
+
const bare = !baseUrl.username && !baseUrl.password && !baseUrl.search && !baseUrl.hash;
|
|
487
|
+
const derived = baseUrl.protocol === 'https:' && bare && ['', '/'].includes(baseUrl.pathname);
|
|
488
|
+
return { endpoint: derived ? baseUrl.origin : undefined, explicit: false };
|
|
489
|
+
}
|
|
490
|
+
if (typeof value !== 'string') {
|
|
491
|
+
throw new Error('ff1Relayer.v2Endpoint must be a string');
|
|
492
|
+
}
|
|
493
|
+
if (value.trim() === '') {
|
|
494
|
+
return { explicit: true };
|
|
495
|
+
}
|
|
496
|
+
let endpoint;
|
|
497
|
+
try {
|
|
498
|
+
endpoint = new URL(value);
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
throw new Error('Invalid ff1Relayer.v2Endpoint');
|
|
502
|
+
}
|
|
503
|
+
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(endpoint.hostname);
|
|
504
|
+
if (!['https:', 'wss:'].includes(endpoint.protocol) &&
|
|
505
|
+
!(['http:', 'ws:'].includes(endpoint.protocol) && loopback)) {
|
|
506
|
+
throw new Error('ff1Relayer.v2Endpoint requires WSS/HTTPS outside loopback');
|
|
507
|
+
}
|
|
508
|
+
if (endpoint.username ||
|
|
509
|
+
endpoint.password ||
|
|
510
|
+
endpoint.search ||
|
|
511
|
+
endpoint.hash ||
|
|
512
|
+
!['', '/'].includes(endpoint.pathname)) {
|
|
513
|
+
throw new Error('Invalid ff1Relayer.v2Endpoint');
|
|
514
|
+
}
|
|
515
|
+
return { endpoint: value, explicit: true };
|
|
516
|
+
}
|
|
367
517
|
/**
|
|
368
518
|
* Validate configuration
|
|
369
519
|
*
|
|
@@ -435,6 +585,12 @@ function validateConfig() {
|
|
|
435
585
|
catch (error) {
|
|
436
586
|
errors.push(error.message);
|
|
437
587
|
}
|
|
588
|
+
try {
|
|
589
|
+
resolveSupportLogsConfig(config);
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
errors.push(error.message);
|
|
593
|
+
}
|
|
438
594
|
return {
|
|
439
595
|
valid: errors.length === 0,
|
|
440
596
|
errors,
|