@feralfile/cli 2.10.1 → 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 +31 -23
- package/config.json.example +6 -1
- package/dist/index.js +10 -2
- package/dist/package.json +5 -2
- package/dist/src/commands/build.js +40 -0
- package/dist/src/commands/config.js +1 -0
- package/dist/src/commands/device.js +85 -40
- package/dist/src/commands/enrich.js +18 -23
- package/dist/src/commands/find.js +144 -106
- 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/helpers/prompt.js +85 -10
- package/dist/src/commands/play.js +162 -84
- package/dist/src/commands/report.js +251 -0
- package/dist/src/commands/setup.js +82 -7
- package/dist/src/commands/status.js +4 -1
- package/dist/src/config.js +256 -6
- package/dist/src/main.js +14 -5
- 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-lookup.js +36 -11
- package/dist/src/utilities/device-network.js +129 -0
- package/dist/src/utilities/device-upsert.js +1 -1
- package/dist/src/utilities/feral-file-artwork.js +2 -2
- 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/index.js +270 -259
- package/dist/src/utilities/media-url.js +117 -0
- package/dist/src/utilities/neort-marketplace.js +3 -3
- package/dist/src/utilities/nft-indexer.js +12 -376
- package/dist/src/utilities/playlist-builder.js +11 -2
- package/dist/src/utilities/playlist-cast.js +6 -6
- package/dist/src/utilities/playlist-source.js +223 -34
- package/dist/src/utilities/raster-client.js +101 -14
- 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/dist/src/utilities/token-item.js +428 -0
- package/dist/src/utilities/token-source-contract.js +55 -0
- package/dist/src/utilities/token-source-indexer.js +99 -0
- package/dist/src/utilities/token-source-raster.js +378 -0
- package/dist/src/utilities/token-source.js +48 -0
- package/docs/CONFIGURATION.md +331 -28
- package/docs/EXAMPLES.md +33 -33
- package/docs/PROJECT_SPEC.md +55 -36
- package/docs/README.md +145 -43
- package/package.json +5 -2
package/docs/CONFIGURATION.md
CHANGED
|
@@ -27,6 +27,12 @@ npm run dev -- config show
|
|
|
27
27
|
- **generativeDuration** (number, seconds)
|
|
28
28
|
- Display duration stamped on generative/interactive (HTML) works, which have no intrinsic runtime and no end-of-stream event. Defaults to **60s** (override via `DEFAULT_GENERATIVE_DURATION`).
|
|
29
29
|
- Set to **0** to omit the duration entirely, so a conformant player parks on the work open-ended instead of rotating. Note: some players (including FF1) require every item to carry a `duration` and will reject a playlist whose items omit it — keep this non-zero unless you know your player tolerates an absent duration.
|
|
30
|
+
- **tokenSource** (`"raster"` | `"indexer"`)
|
|
31
|
+
- Which service `find`, `build`, and `enrich` read token data from. Defaults to `indexer`. See [tokenSource](#tokensource).
|
|
32
|
+
- **ipfsGateways** (object)
|
|
33
|
+
- Gateways used to turn bare `ipfs://` sources into HTTPS URLs. See [ipfsGateways](#ipfsgateways).
|
|
34
|
+
- **supportLogs** (object)
|
|
35
|
+
- Where `ff-cli report` sends a problem report. See [supportLogs](#supportlogs).
|
|
30
36
|
|
|
31
37
|
## browser
|
|
32
38
|
|
|
@@ -39,10 +45,9 @@ Optional settings used where headless/browser‑like behavior is needed.
|
|
|
39
45
|
|
|
40
46
|
Used for signing DP‑1 playlists.
|
|
41
47
|
|
|
42
|
-
- `playlist.privateKey` (string, Ed25519 private key in hex or base64): Used by the `sign` command to create DP-1 v1.1.0 multi-signatures, and as the default signing key for the owner-bound feed mutations (`publish --replace`, `unpublish`), which sign an authorization intent at request time. Those commands and `sign` all take `-k, --key` or `--key-file <path>` to override it for a single run — see [Where the signing key can come from](#where-the-signing-key-can-come-from). The `verify` command may derive the matching public key from this value (or `PLAYLIST_PRIVATE_KEY`) when you omit `--public-key`; **dp1-js applies that derived key only when verifying legacy flat `signature` strings**, not when checking `signatures[]` envelopes. If that derivation fails, `verify` prints a warning on stderr and continues without derived key material. The derived public key is emitted as PEM so Node can decode it without ambiguity. Hex may include or omit the `0x` prefix. You can also set this via `PLAYLIST_PRIVATE_KEY` in `.env`. `
|
|
48
|
+
- `playlist.privateKey` (string, Ed25519 private key in hex or base64): Used by the `sign` command to create DP-1 v1.1.0 multi-signatures, and as the default signing key for the owner-bound feed mutations (`publish --replace`, `unpublish`), which sign an authorization intent at request time. Those commands and `sign` all take `-k, --key` or `--key-file <path>` to override it for a single run — see [Where the signing key can come from](#where-the-signing-key-can-come-from). The `verify` command may derive the matching public key from this value (or `PLAYLIST_PRIVATE_KEY`) when you omit `--public-key`; **dp1-js applies that derived key only when verifying legacy flat `signature` strings**, not when checking `signatures[]` envelopes. If that derivation fails, `verify` prints a warning on stderr and continues without derived key material. The derived public key is emitted as PEM so Node can decode it without ambiguity. Hex may include or omit the `0x` prefix. You can also set this via `PLAYLIST_PRIVATE_KEY` in `.env`. `display` verifies playlists before delivery and only auto-signs the synthesized media URL fallback when signing is configured. `display` and `publish` verify before delivery or upload and reject unsigned or broken playlists.
|
|
43
49
|
|
|
44
50
|
**Signing and key encoding:** Signing paths (`sign`, deterministic `build` when configured, and `-k/--key` overrides) accept the private key in any of these encodings:
|
|
45
|
-
|
|
46
51
|
- **base64 PKCS#8 DER** — recommended; what `ff-cli setup` generates.
|
|
47
52
|
- **32-byte raw Ed25519 seed** as **hex** (optional `0x`) or **base64**.
|
|
48
53
|
- **PEM** (`-----BEGIN PRIVATE KEY-----`).
|
|
@@ -86,7 +91,7 @@ delivery mechanism rather than a fourth key format.
|
|
|
86
91
|
|
|
87
92
|
**Prefer `--key-file` to `--key`.** `--key` writes your private key into your shell history file and
|
|
88
93
|
exposes it in the process list, where any local user can read it for as long as the command runs.
|
|
89
|
-
Neither disclosure can be undone: an Ed25519 signing key
|
|
94
|
+
Neither disclosure can be undone: an Ed25519 signing key _is_ the identity it asserts, and a playlist's
|
|
90
95
|
`curators[]` owner set is immutable, so a leaked key cannot be rotated out of a document that already
|
|
91
96
|
names it. `--key-file` keeps the material in a file you control:
|
|
92
97
|
|
|
@@ -112,7 +117,7 @@ The rules on both flags:
|
|
|
112
117
|
something a publish or a delete can be taken back from.
|
|
113
118
|
- **A `--key-file` argument is always opened, whatever it looks like.** A directory really can be
|
|
114
119
|
called `BEGIN`, and a key file really can be named after a content hash. What is gated is not the
|
|
115
|
-
open but the
|
|
120
|
+
open but the _message_: a failure names the path only when it is short and no part of it is a
|
|
116
121
|
complete key encoding, and otherwise says it could not be read without repeating it. Typing the key
|
|
117
122
|
where the path goes — one keystroke from `--key` — is reported as that mistake, by name, with the
|
|
118
123
|
value withheld.
|
|
@@ -145,8 +150,8 @@ If you need a different role, set `playlist.role` to one of the DP-1 signing rol
|
|
|
145
150
|
### Signing role and ownership
|
|
146
151
|
|
|
147
152
|
A feed decides who owns a playlist from the document itself: the keys in `curators[]` are the owner set.
|
|
148
|
-
Under **role-aware ownership**, a key counts as an owner only when that same key
|
|
149
|
-
`curator`
|
|
153
|
+
Under **role-aware ownership**, a key counts as an owner only when that same key _also signed in the
|
|
154
|
+
`curator` role_ — being named is a claim, signing as `curator` is the proof.
|
|
150
155
|
|
|
151
156
|
`ff-cli publish` requires that role. **This is ff-cli's check, not every feed's answer today**: a feed that
|
|
152
157
|
does not yet enforce roles accepts an `agent`-signed document, so a manual upload of the same file can
|
|
@@ -181,7 +186,8 @@ DP‑1 Feed API configuration.
|
|
|
181
186
|
|
|
182
187
|
- `feed.baseURLs` (string[]): Array of DP‑1 Feed Operator API v1 base URLs. The CLI queries all feeds in parallel.
|
|
183
188
|
- Legacy support: `feed.baseURL` (string) is still accepted and normalized to an array.
|
|
184
|
-
- Default: `https://
|
|
189
|
+
- Default: `https://feed.feralfile.com/api/v1` if not set.
|
|
190
|
+
- A configured URL on `dp1-feed-operator-api-prod.autonomy-system.workers.dev` (the deleted server ff-cli 3.0.0's `setup` wrote into `config.json`) is read as `https://feed.feralfile.com` with the same path, with a one-line note on stderr until you update it.
|
|
185
191
|
- Compatibility: API v1 of the DP‑1 Feed Operator server. See the repository for endpoints and behavior: [dp1-feed](https://github.com/display-protocol/dp1-feed).
|
|
186
192
|
|
|
187
193
|
Endpoints used by the CLI:
|
|
@@ -212,12 +218,80 @@ defaulting to index 0 would silently write to whichever feed happens to be first
|
|
|
212
218
|
Environment variable alternative:
|
|
213
219
|
|
|
214
220
|
```env
|
|
215
|
-
FEED_BASE_URLS=https://
|
|
221
|
+
FEED_BASE_URLS=https://feed.feralfile.com/api/v1,https://feed.example.com/api/v1
|
|
216
222
|
```
|
|
217
223
|
|
|
218
|
-
##
|
|
224
|
+
## tokenSource
|
|
225
|
+
|
|
226
|
+
Token data for `find`, `build`, and `enrich` (title, artist, description, the source to display, on-chain provenance, and a still image where the service supplies one) comes from one service per run:
|
|
227
|
+
|
|
228
|
+
- `indexer` (default): the Feral File indexer (ff-indexer-v2). A token it has not seen yet triggers an indexing job, so a first build of unseen tokens can take minutes.
|
|
229
|
+
- `raster` (opt-in, under evaluation): Raster's public GraphQL API. Tokens are looked up 40 per request, so a build does not wait for indexing jobs. Raster's data still has known bugs, tracked on [feral-file#3546](https://github.com/feral-file/feral-file/issues/3546): some Feral File works point at a retired image host and display black, and evolving works display an older snapshot. No still image is supplied: Raster's preview renditions refuse non-browser clients, so items and `enrich` get no thumbnail from Raster.
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{ "tokenSource": "raster" }
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Omit the key, or set `"indexer"`, to use the default.
|
|
236
|
+
|
|
237
|
+
Environment variable alternative:
|
|
238
|
+
|
|
239
|
+
```env
|
|
240
|
+
FF_TOKEN_SOURCE=raster
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
There is no fallback between the two. A token Raster does not cover, or one that was burned, is reported as unresolved rather than looked up elsewhere.
|
|
244
|
+
|
|
245
|
+
Raster has no way to list a contract's tokens. With `tokenSource: "raster"`, a `build_playlist` requirement without `tokenIds` fails with a message saying so, and a `query_address` requirement for an address that holds nothing is not retried as a contract. Give token ids, an owner address, or run `find` on the series URL instead.
|
|
246
|
+
|
|
247
|
+
Only Ethereum mainnet and Tezos mainnet tokens are built. Tokens on other chains in a wallet or series are skipped and counted.
|
|
248
|
+
|
|
249
|
+
## ipfsGateways
|
|
250
|
+
|
|
251
|
+
With `tokenSource: "raster"`, many sources come back as bare `ipfs://` CIDs, which the FF1 cannot load. ff-cli rewrites them through a gateway:
|
|
219
252
|
|
|
220
|
-
|
|
253
|
+
- `ipfsGateways.feralFile`: used for Feral File works. Default `https://ipfs.feralfile.com/ipfs/`.
|
|
254
|
+
- `ipfsGateways.default`: used for everything else. Default `https://ipfs.filebase.io/ipfs/`.
|
|
255
|
+
|
|
256
|
+
A bare origin such as `https://gateway.example` is accepted and normalized to `https://gateway.example/ipfs/`. Sources on the retired `cdn.feralfileassets.com` host are rewritten to `cdn.artworks.feralfile.io`. Sources on the legacy `ipfs.bitmark.com` gateway go through `ipfsGateways.feralFile` instead.
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"ipfsGateways": {
|
|
261
|
+
"feralFile": "https://ipfs.feralfile.com/ipfs/",
|
|
262
|
+
"default": "https://ipfs.filebase.io/ipfs/"
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Environment variable alternative:
|
|
268
|
+
|
|
269
|
+
```env
|
|
270
|
+
IPFS_GATEWAY_FERAL_FILE=https://ipfs.feralfile.com/ipfs/
|
|
271
|
+
IPFS_GATEWAY_DEFAULT=https://ipfs.filebase.io/ipfs/
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## supportLogs
|
|
275
|
+
|
|
276
|
+
`ff-cli report` posts to Feral File's support-logs service, `https://support-logs.feralfile.com` by default. No configuration or key is needed; set `supportLogs.baseUrl` only to point at a staging or test service. It does not move an Art Computer's log upload, which goes to the support-logs its FF OS names.
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
{
|
|
280
|
+
"supportLogs": {
|
|
281
|
+
"baseUrl": "https://support-logs.feralfile.com"
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Environment variable alternative:
|
|
287
|
+
|
|
288
|
+
```env
|
|
289
|
+
FF_SUPPORT_LOGS_URL=https://support-logs.feralfile.com
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Raster API
|
|
293
|
+
|
|
294
|
+
`find` resolves tokens, series, and artist addresses through Raster's public GraphQL API ([docs.api.raster.art](https://docs.api.raster.art/)), and with the default `tokenSource` so do `build` and `enrich`. No configuration is required; two environment variables adjust the integration:
|
|
221
295
|
|
|
222
296
|
```env
|
|
223
297
|
# API key sent as the x-api-key header. The API currently answers without
|
|
@@ -230,13 +304,13 @@ RASTER_API_URL=https://api.raster.art/graphql
|
|
|
230
304
|
|
|
231
305
|
## ff1Devices
|
|
232
306
|
|
|
233
|
-
Configure devices you want to
|
|
307
|
+
Configure devices you want to display art on.
|
|
234
308
|
|
|
235
309
|
- `ff1Devices.devices` (array of objects):
|
|
236
310
|
- `name` (string): Friendly device label. Free‑form; pick anything memorable.
|
|
237
311
|
- `id` (string): Stable physical ID discovered from FF1 (for example
|
|
238
|
-
`FF1-
|
|
239
|
-
credential vault.
|
|
312
|
+
`FF1-ABCD1234`). Relayer pairing stores the topic under this ID in the OS
|
|
313
|
+
credential vault, and controller pairing names the controller key file after it.
|
|
240
314
|
- `host` (string): Device base URL. For LAN devices, use `http://<ip>:1111`. The device typically listens on port `1111`.
|
|
241
315
|
|
|
242
316
|
During `ff-cli setup`, the CLI will attempt local discovery via mDNS (`_ff1._tcp`). If devices are found, you can pick one and the host will be filled in automatically. If discovery returns nothing, setup falls back to manual entry.
|
|
@@ -256,12 +330,49 @@ You can also manage devices independently with:
|
|
|
256
330
|
- `ff-cli device rename <name> <new-name>` – Rename a device without touching its host, API key, or default status. Works offline; also accepts the host URL as the identifier, so unnamed legacy entries can be given a name.
|
|
257
331
|
- `ff-cli device remove <name>` – Remove a device by name.
|
|
258
332
|
- `ff-cli device default <name>` – Promote a device to the top of the list so it is used when `-d` is omitted.
|
|
259
|
-
- `ff-cli device pair [name]` –
|
|
260
|
-
|
|
261
|
-
|
|
333
|
+
- `ff-cli device pair [name]` – Pair ff-cli with an FF1. ff-cli reads the
|
|
334
|
+
FF1's status on the LAN to pick the path:
|
|
335
|
+
- FF OS with secure control and an owner: show a QR code (and the same text
|
|
336
|
+
to paste) that an owner scans with **Add controller** in the Feral File
|
|
337
|
+
app; ff-cli pins the FF1's control profile from the owner's sealed answer
|
|
338
|
+
and enrolls as a **controller**. Needs an internet connection; see
|
|
339
|
+
[Secure control pairing](#secure-control-pairing).
|
|
340
|
+
- FF OS without secure control (or with it switched on but no owner yet):
|
|
341
|
+
display a one-time code and key-bound security check; in Feral File
|
|
342
|
+
mobile choose **Pair with ff-cli**, enter the code, and confirm both values
|
|
343
|
+
match before sharing. The app sends the relayer topic, which ff-cli stores
|
|
344
|
+
in the operating system credential vault.
|
|
345
|
+
- Status unreadable (FF1 off the LAN): the QR path, with a hint. Pass
|
|
346
|
+
`--topic-handoff` to force the one-time-code path.
|
|
347
|
+
|
|
348
|
+
The status is not authenticated, so ff-cli says which path it chose from
|
|
349
|
+
it, and never chooses the one-time code when it already holds controller
|
|
350
|
+
keys for the FF1 (a controller file, a pending replacement or the keys of
|
|
351
|
+
an unfinished pairing); only `--topic-handoff` does then.
|
|
352
|
+
|
|
353
|
+
- `ff-cli device controllers [name]` – List the controllers paired with an FF1
|
|
354
|
+
and any pending requests (owner only). On FF OS with display grants the list
|
|
355
|
+
also shows Play sites that an owner approved through mint pairing, with role
|
|
356
|
+
`display`, platform `web` and the site's origin in the LABEL column
|
|
357
|
+
(`(unverified)` when the relay did not attest it). A display row may only
|
|
358
|
+
cast to the FF1; `ff-cli device revoke` removes it like any controller.
|
|
359
|
+
- `ff-cli device revoke <controller-id> [name]` – Revoke a controller, or deny
|
|
360
|
+
its pending request (owner only).
|
|
361
|
+
- `ff-cli device unpair [name]` – Revoke ff-cli's own access on the FF1 (best
|
|
362
|
+
effort) and delete its controller keys. It always asks first (owner keys get
|
|
363
|
+
a stronger warning); `-y, --yes` skips the question.
|
|
364
|
+
Owner keys need confirmation (or `--yes`): if no other owner can reach the
|
|
365
|
+
FF1, deleting them leaves it without a reachable owner until a factory reset.
|
|
262
366
|
|
|
263
367
|
Setup and `device add` both preserve existing devices. Adding a device with the same host as an existing one updates it in place.
|
|
264
368
|
|
|
369
|
+
For an explicitly enrolled owner Tailscale connection, use the FF1's Tailscale IPv4
|
|
370
|
+
address with the **same physical `--id` and existing `--name`**. Save its old
|
|
371
|
+
host first. This updates that device in place, preserving its list position,
|
|
372
|
+
API key and ID; relayer credentials remain in the vault under the same ID.
|
|
373
|
+
The configured host is replaced, with no automatic LAN/Tailscale failover.
|
|
374
|
+
See the [owner-access procedure](https://github.com/feral-file/ffos/blob/36e25cec84505271a7232d6936bb46a19fab5cfd/docs/OWNER_TAILSCALE.md).
|
|
375
|
+
|
|
265
376
|
Selection rules when sending:
|
|
266
377
|
|
|
267
378
|
- If you omit `-d`, the first configured device is used.
|
|
@@ -269,7 +380,7 @@ Selection rules when sending:
|
|
|
269
380
|
|
|
270
381
|
Device cast contract:
|
|
271
382
|
|
|
272
|
-
The device exposes a single HTTP endpoint, `POST http://<device>:1111/api/cast`. `ff-cli
|
|
383
|
+
The device exposes a single HTTP endpoint, `POST http://<device>:1111/api/cast`. `ff-cli display` casts a DP-1 playlist with:
|
|
273
384
|
|
|
274
385
|
```jsonc
|
|
275
386
|
POST http://<device>:1111/api/cast
|
|
@@ -287,18 +398,23 @@ Content-Type: application/json
|
|
|
287
398
|
|
|
288
399
|
The `intent` field is **required** — without it the device returns `{"message":{"ok":false}}` with no further explanation. `dp1_call` may be the playlist inline (as above) or a hosted playlist URL string; ff-cli sends the resolved playlist object.
|
|
289
400
|
|
|
290
|
-
When LAN delivery is unreachable, `
|
|
401
|
+
When LAN delivery is unreachable, `display` prints an explicit notice and retries
|
|
291
402
|
the same command through `ff1Relayer.baseUrl`. The per-device topic is read from
|
|
292
403
|
the operating system credential vault, never `config.json`. Pair it from the
|
|
293
404
|
mobile app first:
|
|
294
405
|
|
|
295
406
|
```bash
|
|
296
|
-
ff-cli device pair office
|
|
407
|
+
ff-cli device pair office # FF OS without secure control: one-time code
|
|
408
|
+
ff-cli device pair office --topic-handoff # force the one-time code (FF1 not on this LAN)
|
|
297
409
|
```
|
|
298
410
|
|
|
299
|
-
|
|
300
|
-
Comparing it authenticates the CLI public key
|
|
301
|
-
cannot silently substitute its own encryption
|
|
411
|
+
On FF OS without secure control, the CLI and mobile confirmation both show the
|
|
412
|
+
same 12-hex-digit security check. Comparing it authenticates the CLI public key
|
|
413
|
+
out of band, so an active broker cannot silently substitute its own encryption
|
|
414
|
+
key. On FF OS with secure control, the topic arrives inside the owner's sealed
|
|
415
|
+
answer when the FF1 still uses the topic relayer; ff-cli stores it only after
|
|
416
|
+
the FF1's enroll reply verified, and the enroll itself uses the relayer only
|
|
417
|
+
with a topic stored earlier. The topic is never printed.
|
|
302
418
|
|
|
303
419
|
The default relayer is
|
|
304
420
|
`https://tv-cast-coordination.autonomy-system.workers.dev`. Override it with
|
|
@@ -307,18 +423,205 @@ need no additional key. Deployments that retain the cloud-edge compatibility
|
|
|
307
423
|
gate may receive `ff1Relayer.apiKey` or `FF1_RELAYER_API_KEY`; ff-cli never
|
|
308
424
|
bundles that shared key.
|
|
309
425
|
|
|
426
|
+
#### Relay v2
|
|
427
|
+
|
|
428
|
+
Relay v2 routes by FF1 device ID instead of a topic, on a public channel that
|
|
429
|
+
carries only sealed envelopes: a reply counts only when it verifies under the
|
|
430
|
+
FF1 profile pinned at pairing. `device pair` uses it for its pairing channel.
|
|
431
|
+
|
|
432
|
+
With the default relayer, the v2 endpoint is the production relay v2 Worker,
|
|
433
|
+
`wss://relayer.feralfile.io`, which keeps working after the legacy relayer is
|
|
434
|
+
retired. A custom `ff1Relayer.baseUrl` that is a bare HTTPS origin is used as
|
|
435
|
+
its own v2 endpoint. Either way pairing needs no setting. Override it with
|
|
436
|
+
`ff1Relayer.v2Endpoint` or `FF1_RELAY_V2_ENDPOINT`: an HTTPS or WSS origin
|
|
437
|
+
(plain HTTP only on loopback), with no credentials, path or query. An empty
|
|
438
|
+
value turns v2 off; `device pair` then reports that pairing is unavailable.
|
|
439
|
+
|
|
440
|
+
When ff-cli is paired as a controller, a command that cannot reach the FF1 on
|
|
441
|
+
the LAN goes over relay v2 first when the v2 endpoint is set explicitly, or
|
|
442
|
+
when the FF1's last verified status listed `relayV2` in its control
|
|
443
|
+
transports (ff-cli remembers this in the controller file). Otherwise it uses
|
|
444
|
+
the topic relayer.
|
|
445
|
+
|
|
446
|
+
```json
|
|
447
|
+
{
|
|
448
|
+
"ff1Relayer": {
|
|
449
|
+
"v2Endpoint": "https://relay.example"
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
- The stored topic is used only when relay v2 proves the command was not
|
|
455
|
+
delivered: it could not connect, or the relay refused it for every peer and
|
|
456
|
+
no part of it was acknowledged. The topic route then carries a newly sealed
|
|
457
|
+
request.
|
|
458
|
+
- When the command may have reached the FF1 (it was acknowledged, or the
|
|
459
|
+
connection dropped after sending, or no verified reply came in time), ff-cli
|
|
460
|
+
reports "Outcome unknown" and does not send it again. Check the FF1 before
|
|
461
|
+
running the command again.
|
|
462
|
+
- Relay refusals while connecting, and relay backpressure, are retried with
|
|
463
|
+
backoff.
|
|
464
|
+
- `device pair` finishes its enroll over a relayer topic this machine held
|
|
465
|
+
from before the pairing, when it has one: that topic reaches the real FF1,
|
|
466
|
+
which cannot answer an enroll sealed to a forged profile. Only when the
|
|
467
|
+
relayer answers that the topic has no device (1003) or is retired (410),
|
|
468
|
+
or the legacy host answers 404 (it was shut down), or no topic is held,
|
|
469
|
+
does it go to the LAN and then relay v2; any other relayer failure
|
|
470
|
+
(offline, a timeout, a 5xx, a DNS failure) stops there. When the
|
|
471
|
+
credential vault cannot be read, whether a topic is held is unknown, so the
|
|
472
|
+
enroll uses the LAN only, never relay v2. Relay v2 lets a
|
|
473
|
+
controller be added remotely: the FF1 profile comes from the owner's sealed
|
|
474
|
+
pairing result, so a reply on the public device channel has to verify under
|
|
475
|
+
it. A first-owner enroll with a screen invitation never uses relay v2.
|
|
476
|
+
- A command that reaches the relayer topic only to hear that the topic leads
|
|
477
|
+
nowhere (the same 1003, 410 or 404) also tries relay v2, even before a
|
|
478
|
+
verified status has said the FF1 is on v2, and a verified reply there
|
|
479
|
+
records that it is. So does a command when ff-cli holds no topic for the
|
|
480
|
+
FF1, or cannot read its credential vault: with no topic route there is
|
|
481
|
+
nothing a relay v2 attempt could cost. If the legacy host's name stops resolving instead,
|
|
482
|
+
ff-cli cannot tell that from an offline network; update ff-cli to a
|
|
483
|
+
release without the legacy relayer.
|
|
484
|
+
- The owner's sealed pairing result must fit one relay frame (32 KiB; results
|
|
485
|
+
are about 1 KiB). If the relay connection drops while `device pair` waits,
|
|
486
|
+
pairing stops; run it again to show a new code.
|
|
487
|
+
- On relay v2 a `replay` answer is reported as an unknown outcome, never
|
|
488
|
+
retried: the FF1 may have received two copies of the same request.
|
|
489
|
+
|
|
310
490
|
On the first cast after boot the device may return an empty body. ff-cli retries this automatically and, if it persists, reports a clear "accepted the request but returned no usable response" error rather than crashing on a JSON parse.
|
|
311
491
|
|
|
312
492
|
Compatibility checks:
|
|
313
493
|
|
|
314
|
-
- `
|
|
494
|
+
- `display` and `ssh` perform a compatibility preflight before sending commands to FF1. The CLI gets the device version by calling `POST /api/cast` with `{ "command": "getDeviceStatus", "request": {} }` and reads `message.installedVersion` from the response.
|
|
315
495
|
|
|
316
496
|
- Minimum supported FF1 OS versions:
|
|
317
|
-
- `
|
|
497
|
+
- `display` (`displayPlaylist`): `1.0.0` or newer
|
|
318
498
|
- `ssh` (`sshAccess`): `1.0.9` or newer
|
|
319
499
|
|
|
320
500
|
- If the CLI cannot get a version from the device (e.g. network or malformed response), it continues and sends the command.
|
|
321
501
|
- If the detected version is below the minimum, the command fails early with an error that includes the detected version.
|
|
502
|
+
- `ssh` sends the relayer topic stored by `ff-cli device pair` (OS keyring) in the `X-FF1-Topic` header. A claimed FF1 on newer FF OS rejects plaintext `sshAccess` without it (HTTP 401 `topic_required`). With no stored topic the request is sent without the header, which unclaimed devices and older FF OS accept.
|
|
503
|
+
|
|
504
|
+
- When ff-cli is paired as a controller (a controller file exists for the
|
|
505
|
+
device), the preflight, `display` and `ssh` are all sent as secure envelopes
|
|
506
|
+
instead; see below.
|
|
507
|
+
|
|
508
|
+
### Secure control pairing
|
|
509
|
+
|
|
510
|
+
Current FF OS authorizes every command per controller (FF1 control protocol
|
|
511
|
+
v1). The FF1 itself decides who may do what; the LAN and the relayer only carry
|
|
512
|
+
encrypted, signed requests. ff-cli takes part as a controller:
|
|
513
|
+
|
|
514
|
+
```bash
|
|
515
|
+
ff-cli device pair office # show the QR, wait for an owner, enroll
|
|
516
|
+
ff-cli device pair office --label studio # name the owner sees (default: hostname); 1-64 characters,
|
|
517
|
+
# no control or invisible formatting (bidi, zero-width) characters
|
|
518
|
+
ff-cli device pair office --timeout 600 # wait longer for an owner (seconds, 10-600; default 300)
|
|
519
|
+
ff-cli device pair office --resume # finish a pairing an owner already approved
|
|
520
|
+
ff-cli device pair office --yes # move a corrupt controller file aside without asking
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
Pairing is owner-initiated, like adding an SSH key: nothing is requested from
|
|
524
|
+
the FF1 and no code is compared.
|
|
525
|
+
|
|
526
|
+
1. ff-cli shows a QR code and the same text to paste (`ff1-ctl:1|…`). It
|
|
527
|
+
carries this computer's public controller keys, a one-time pairing secret
|
|
528
|
+
and a relay channel ID. The QR uses this computer's existing keys for the
|
|
529
|
+
FF1 when it has them, so a re-pair (after a revoke, a role change or a
|
|
530
|
+
factory reset) keeps one controller ID on the FF1. Otherwise the keys are
|
|
531
|
+
saved to `controllers/pairing/<device ID>.keys.json` once the relay
|
|
532
|
+
channel is open, before the QR is shown, so a retry after a timeout or Ctrl-C shows the same keys (with a
|
|
533
|
+
new secret and channel) and completes a registration an owner already
|
|
534
|
+
made.
|
|
535
|
+
2. On an owner's phone, open the FF1 in the Feral File app, choose **Add
|
|
536
|
+
controller**, scan the code (or paste the text), and pick the role.
|
|
537
|
+
3. The app registers ff-cli on the FF1, then sends ff-cli a sealed answer over
|
|
538
|
+
the relay channel. ff-cli accepts only an answer it can open with this
|
|
539
|
+
run's keys that carries its pairing secret, its own controller ID, and the
|
|
540
|
+
configured FF1's device ID; anything else on the channel is ignored. It pins
|
|
541
|
+
the FF1's control profile from that answer: every reply must then be signed
|
|
542
|
+
by its key.
|
|
543
|
+
4. ff-cli enrolls with the FF1 (LAN first, relayer if the LAN is unreachable)
|
|
544
|
+
and prints the role it was granted.
|
|
545
|
+
|
|
546
|
+
Pairing needs an internet connection: the relay carries the owner's answer and
|
|
547
|
+
keeps nothing. It also needs an FF1 that already has an owner (choose **Secure
|
|
548
|
+
this FF1** in the app first); ff-cli cannot become an FF1's first owner. If no
|
|
549
|
+
owner answers within the timeout, the pairing is "not completed", not
|
|
550
|
+
rejected: running `device pair` again shows a new code for the same keys.
|
|
551
|
+
If an owner declines, the command fails. If the owner's
|
|
552
|
+
answer arrived but enrolling did not finish (the FF1 was unreachable, Ctrl-C),
|
|
553
|
+
`ff-cli device pair <name> --resume` finishes it without a new code.
|
|
554
|
+
|
|
555
|
+
If ff-cli is already paired with the FF1 under a different identity (the FF1
|
|
556
|
+
was factory reset), it says that the new pairing will replace it, and keeps
|
|
557
|
+
the current keys in place until the new pairing is enrolled: a failed or
|
|
558
|
+
abandoned pairing never loses an existing grant. The pending replacement is
|
|
559
|
+
kept in `controllers/pairing/` until then.
|
|
560
|
+
|
|
561
|
+
`ff-cli device unpair <name>` first asks the FF1 to revoke ff-cli's own access
|
|
562
|
+
(a sealed self-revoke, best effort with a short timeout), then deletes the keys
|
|
563
|
+
whatever the answer. Older FF OS refuses a controller's self-revoke, and an FF1
|
|
564
|
+
never lets its last owner revoke itself; in those cases the FF1 keeps listing
|
|
565
|
+
the controller until an owner revokes it. Unpair also deletes the keys of an
|
|
566
|
+
unfinished pairing; if an owner already added them, it prints their
|
|
567
|
+
controller ID for an owner to revoke.
|
|
568
|
+
|
|
569
|
+
Roles:
|
|
570
|
+
|
|
571
|
+
- **owner** – every command, including `ssh` and controller management.
|
|
572
|
+
- **controller** – displaying art (`display`) and device status. `ssh`,
|
|
573
|
+
`device controllers` and `device revoke` need an owner grant.
|
|
574
|
+
|
|
575
|
+
Controller keys live in one file per FF1:
|
|
576
|
+
|
|
577
|
+
```
|
|
578
|
+
$XDG_CONFIG_HOME/ff-cli/controllers/<device ID>.json # default ~/.config/ff-cli/controllers/
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
The file holds this computer's private controller keys, the pinned FF1 profile
|
|
582
|
+
(generation and key pins), facts from the owner's answer (channel, FF1 name,
|
|
583
|
+
role, expiry), the granted role, expiry and notification key, and the request
|
|
584
|
+
counter. ff-cli creates it with mode `0600` inside a `0700` directory
|
|
585
|
+
and replaces it atomically on every write. It is never printed or logged.
|
|
586
|
+
**Windows:** mode bits do not apply; the file relies on the ACL of your user
|
|
587
|
+
profile directory, so keep `%USERPROFILE%\.config` private to your account.
|
|
588
|
+
|
|
589
|
+
Treat the file like an SSH private key: anyone who copies it can act as this
|
|
590
|
+
controller until an owner revokes it. Do not restore it from an old backup —
|
|
591
|
+
the counter inside must only grow, and an FF1 rejects reused counters
|
|
592
|
+
(`replay`). When an FF1 answers `replay` in a reply signed by its key, ff-cli
|
|
593
|
+
moves the counter ahead and sends the command once more as a new request. If
|
|
594
|
+
it keeps happening, check that no other computer uses a copy of the file, then
|
|
595
|
+
run `ff-cli device pair <name>` again: the current keys stay in place until the
|
|
596
|
+
new pairing is finished. A controller file that
|
|
597
|
+
cannot be read (permissions, a program holding it) is never overwritten; a
|
|
598
|
+
corrupt one is moved aside to `<device ID>.json.corrupt-<time>` (after a
|
|
599
|
+
question, or `--yes`) before a new pairing.
|
|
600
|
+
`device remove` keeps the file, so re-adding the same FF1 keeps its pairing.
|
|
601
|
+
|
|
602
|
+
Requests go to `POST /api/cast` as
|
|
603
|
+
`{"command":"ff1Envelope","request":{"v":1,"jwe":"…"}}` on the LAN. When the
|
|
604
|
+
LAN is unreachable, `display`, enrolling (`device pair`), `device controllers`,
|
|
605
|
+
`device revoke` and unpair's self-revoke go through the relayer instead (they are encrypted and signed
|
|
606
|
+
either way); the version preflight and `ssh` stay LAN-only. Each attempt,
|
|
607
|
+
including every retry, is a new request with a new counter. FF1s on older FF OS
|
|
608
|
+
(no controller file for them) keep the plaintext path described above.
|
|
609
|
+
|
|
610
|
+
Errors and what to do:
|
|
611
|
+
|
|
612
|
+
| Message | Cause | Fix |
|
|
613
|
+
| ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
614
|
+
| "This FF1 is secured and only accepts requests from paired controllers." | The FF1 refuses plaintext and ff-cli is not paired | `ff-cli device pair <name>`; an owner scans the code with **Add controller** |
|
|
615
|
+
| "…does not know this ff-cli controller", "…was revoked or has expired", "…has not been approved", "…still waiting for approval" | No active grant for these keys | `ff-cli device pair <name>` (or `--resume`); an owner scans the code with **Add controller**. If the owner gave the FF1 up (it shows a setup code, or no one has set it up since), `ff-cli device unpair <name>` to use it without pairing |
|
|
616
|
+
| "Pairing was not completed: no owner answered…" | No verified answer reached ff-cli before `--timeout` | Run `ff-cli device pair <name>` again (needs internet) |
|
|
617
|
+
| "Pairing is not available: … relay v2 is turned off" | Relay v2 is off: `ff1Relayer.v2Endpoint`/`FF1_RELAY_V2_ENDPOINT` is empty, the relayer settings are invalid, or `ff1Relayer.baseUrl` is not a bare HTTPS origin and no v2 endpoint is set | Remove the empty override, or set an HTTPS/WSS origin (see [Relay v2](#relay-v2)) |
|
|
618
|
+
| "This command needs owner access…" | `ssh` or controller management with a controller grant | An owner grants the owner role in the app |
|
|
619
|
+
| "The FF1 could not open the request." / "…rejected the request signature." | The FF1's control identity changed (for example a factory reset), or the device address now reaches another FF1. These answers are not authenticated: anyone on the path can send one | Check the device address first. If the FF1 was factory reset: `ff-cli device pair <name>` (the current keys stay until the new pairing is finished). Do not unpair |
|
|
620
|
+
| "The reply did not verify against the paired FF1 identity…" | A reply not signed by the pinned FF1 (wrong device, or a forged relayer reply) | Check the device host; if the FF1 was reset, `ff-cli device pair <name>`. Do not unpair |
|
|
621
|
+
| "The FF1 is busy; try again." | The FF1 was checking too many requests at once and did not run this one (`display` retries it automatically with a new request) | Run the command again |
|
|
622
|
+
| "The FF1 has no grant for ff-cli, although an owner's Feral File app reported adding it." | An owner removed ff-cli again before it enrolled | `ff-cli device pair <name>` |
|
|
623
|
+
| "The FF1 rejected the request counter as already used." | The counter is still behind after ff-cli moved it ahead once (another computer uses a copy of the file) | Stop the other copy; run the command again. Do not unpair |
|
|
624
|
+
| "The FF1 rejected the request as malformed." | Usually a clock more than 5 minutes ahead of the FF1's | Fix the clock on this computer (or let the FF1 finish setting its time), then retry |
|
|
322
625
|
|
|
323
626
|
Troubleshooting note:
|
|
324
627
|
|
|
@@ -328,10 +631,10 @@ Examples:
|
|
|
328
631
|
|
|
329
632
|
```bash
|
|
330
633
|
# Send to first device
|
|
331
|
-
npm run dev --
|
|
634
|
+
npm run dev -- display playlist.json
|
|
332
635
|
|
|
333
|
-
#
|
|
334
|
-
npm run dev --
|
|
636
|
+
# Display on a specific device by exact name
|
|
637
|
+
npm run dev -- display playlist.json -d "Living Room Display"
|
|
335
638
|
```
|
|
336
639
|
|
|
337
640
|
Minimal `config.json` example (selected fields):
|
|
@@ -344,7 +647,7 @@ Minimal `config.json` example (selected fields):
|
|
|
344
647
|
"role": "curator"
|
|
345
648
|
},
|
|
346
649
|
"feed": {
|
|
347
|
-
"baseURLs": ["https://
|
|
650
|
+
"baseURLs": ["https://feed.feralfile.com/api/v1"]
|
|
348
651
|
},
|
|
349
652
|
"ff1Devices": {
|
|
350
653
|
"devices": [
|