@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.
Files changed (45) hide show
  1. package/README.md +20 -17
  2. package/config.json.example +1 -1
  3. package/dist/index.js +10 -2
  4. package/dist/package.json +5 -2
  5. package/dist/src/commands/build.js +1 -1
  6. package/dist/src/commands/device.js +84 -39
  7. package/dist/src/commands/enrich.js +2 -2
  8. package/dist/src/commands/find.js +71 -37
  9. package/dist/src/commands/helpers/device-control.js +937 -0
  10. package/dist/src/commands/helpers/hidden-alias-help.js +37 -0
  11. package/dist/src/commands/helpers/playlist-display.js +2 -15
  12. package/dist/src/commands/play.js +39 -26
  13. package/dist/src/commands/report.js +251 -0
  14. package/dist/src/commands/setup.js +1 -1
  15. package/dist/src/config.js +162 -6
  16. package/dist/src/utilities/control/codec.js +578 -0
  17. package/dist/src/utilities/control/control-errors.js +157 -0
  18. package/dist/src/utilities/control/controller-store.js +964 -0
  19. package/dist/src/utilities/control/jwk.js +141 -0
  20. package/dist/src/utilities/control/pairing-channel.js +52 -0
  21. package/dist/src/utilities/control/pairing.js +416 -0
  22. package/dist/src/utilities/control/registration.js +391 -0
  23. package/dist/src/utilities/control/relay-pairing-channel.js +191 -0
  24. package/dist/src/utilities/control/secure-transport.js +850 -0
  25. package/dist/src/utilities/control/strict-json.js +376 -0
  26. package/dist/src/utilities/device-network.js +129 -0
  27. package/dist/src/utilities/device-upsert.js +1 -1
  28. package/dist/src/utilities/ff1-compatibility.js +87 -11
  29. package/dist/src/utilities/ff1-device.js +161 -78
  30. package/dist/src/utilities/ff1-relayer.js +34 -14
  31. package/dist/src/utilities/handoff-client.js +23 -9
  32. package/dist/src/utilities/neort-marketplace.js +3 -3
  33. package/dist/src/utilities/playlist-builder.js +3 -3
  34. package/dist/src/utilities/playlist-cast.js +6 -6
  35. package/dist/src/utilities/playlist-source.js +7 -7
  36. package/dist/src/utilities/raster-client.js +1 -1
  37. package/dist/src/utilities/relay-client.js +774 -0
  38. package/dist/src/utilities/ssh-access.js +192 -58
  39. package/dist/src/utilities/support-report.js +511 -0
  40. package/dist/src/utilities/temporary-channel.js +139 -0
  41. package/docs/CONFIGURATION.md +270 -28
  42. package/docs/EXAMPLES.md +31 -31
  43. package/docs/PROJECT_SPEC.md +32 -19
  44. package/docs/README.md +113 -36
  45. package/package.json +5 -2
@@ -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
- : ['https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1'],
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 baseURLs = config.feedServers.map((server) => server.baseUrl);
232
+ const servers = config.feedServers.map((server) => ({
233
+ ...server,
234
+ baseUrl: replaceRetiredFeedUrl(server.baseUrl),
235
+ }));
193
236
  return {
194
- baseURLs,
195
- servers: config.feedServers,
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 = ['https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1'];
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,