@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/docs/CONFIGURATION.md
CHANGED
|
@@ -31,6 +31,8 @@ npm run dev -- config show
|
|
|
31
31
|
- Which service `find`, `build`, and `enrich` read token data from. Defaults to `indexer`. See [tokenSource](#tokensource).
|
|
32
32
|
- **ipfsGateways** (object)
|
|
33
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).
|
|
34
36
|
|
|
35
37
|
## browser
|
|
36
38
|
|
|
@@ -43,10 +45,9 @@ Optional settings used where headless/browser‑like behavior is needed.
|
|
|
43
45
|
|
|
44
46
|
Used for signing DP‑1 playlists.
|
|
45
47
|
|
|
46
|
-
- `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.
|
|
47
49
|
|
|
48
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:
|
|
49
|
-
|
|
50
51
|
- **base64 PKCS#8 DER** — recommended; what `ff-cli setup` generates.
|
|
51
52
|
- **32-byte raw Ed25519 seed** as **hex** (optional `0x`) or **base64**.
|
|
52
53
|
- **PEM** (`-----BEGIN PRIVATE KEY-----`).
|
|
@@ -90,7 +91,7 @@ delivery mechanism rather than a fourth key format.
|
|
|
90
91
|
|
|
91
92
|
**Prefer `--key-file` to `--key`.** `--key` writes your private key into your shell history file and
|
|
92
93
|
exposes it in the process list, where any local user can read it for as long as the command runs.
|
|
93
|
-
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
|
|
94
95
|
`curators[]` owner set is immutable, so a leaked key cannot be rotated out of a document that already
|
|
95
96
|
names it. `--key-file` keeps the material in a file you control:
|
|
96
97
|
|
|
@@ -116,7 +117,7 @@ The rules on both flags:
|
|
|
116
117
|
something a publish or a delete can be taken back from.
|
|
117
118
|
- **A `--key-file` argument is always opened, whatever it looks like.** A directory really can be
|
|
118
119
|
called `BEGIN`, and a key file really can be named after a content hash. What is gated is not the
|
|
119
|
-
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
|
|
120
121
|
complete key encoding, and otherwise says it could not be read without repeating it. Typing the key
|
|
121
122
|
where the path goes — one keystroke from `--key` — is reported as that mistake, by name, with the
|
|
122
123
|
value withheld.
|
|
@@ -149,8 +150,8 @@ If you need a different role, set `playlist.role` to one of the DP-1 signing rol
|
|
|
149
150
|
### Signing role and ownership
|
|
150
151
|
|
|
151
152
|
A feed decides who owns a playlist from the document itself: the keys in `curators[]` are the owner set.
|
|
152
|
-
Under **role-aware ownership**, a key counts as an owner only when that same key
|
|
153
|
-
`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.
|
|
154
155
|
|
|
155
156
|
`ff-cli publish` requires that role. **This is ff-cli's check, not every feed's answer today**: a feed that
|
|
156
157
|
does not yet enforce roles accepts an `agent`-signed document, so a manual upload of the same file can
|
|
@@ -185,7 +186,8 @@ DP‑1 Feed API configuration.
|
|
|
185
186
|
|
|
186
187
|
- `feed.baseURLs` (string[]): Array of DP‑1 Feed Operator API v1 base URLs. The CLI queries all feeds in parallel.
|
|
187
188
|
- Legacy support: `feed.baseURL` (string) is still accepted and normalized to an array.
|
|
188
|
-
- 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.
|
|
189
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).
|
|
190
192
|
|
|
191
193
|
Endpoints used by the CLI:
|
|
@@ -216,15 +218,15 @@ defaulting to index 0 would silently write to whichever feed happens to be first
|
|
|
216
218
|
Environment variable alternative:
|
|
217
219
|
|
|
218
220
|
```env
|
|
219
|
-
FEED_BASE_URLS=https://
|
|
221
|
+
FEED_BASE_URLS=https://feed.feralfile.com/api/v1,https://feed.example.com/api/v1
|
|
220
222
|
```
|
|
221
223
|
|
|
222
224
|
## tokenSource
|
|
223
225
|
|
|
224
|
-
Token data for `find`, `build`, and `enrich` (title, artist, description,
|
|
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:
|
|
225
227
|
|
|
226
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.
|
|
227
|
-
- `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
|
|
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.
|
|
228
230
|
|
|
229
231
|
```json
|
|
230
232
|
{ "tokenSource": "raster" }
|
|
@@ -269,6 +271,24 @@ IPFS_GATEWAY_FERAL_FILE=https://ipfs.feralfile.com/ipfs/
|
|
|
269
271
|
IPFS_GATEWAY_DEFAULT=https://ipfs.filebase.io/ipfs/
|
|
270
272
|
```
|
|
271
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
|
+
|
|
272
292
|
## Raster API
|
|
273
293
|
|
|
274
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:
|
|
@@ -284,13 +304,13 @@ RASTER_API_URL=https://api.raster.art/graphql
|
|
|
284
304
|
|
|
285
305
|
## ff1Devices
|
|
286
306
|
|
|
287
|
-
Configure devices you want to
|
|
307
|
+
Configure devices you want to display art on.
|
|
288
308
|
|
|
289
309
|
- `ff1Devices.devices` (array of objects):
|
|
290
310
|
- `name` (string): Friendly device label. Free‑form; pick anything memorable.
|
|
291
311
|
- `id` (string): Stable physical ID discovered from FF1 (for example
|
|
292
|
-
`FF1-
|
|
293
|
-
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.
|
|
294
314
|
- `host` (string): Device base URL. For LAN devices, use `http://<ip>:1111`. The device typically listens on port `1111`.
|
|
295
315
|
|
|
296
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.
|
|
@@ -310,9 +330,39 @@ You can also manage devices independently with:
|
|
|
310
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.
|
|
311
331
|
- `ff-cli device remove <name>` – Remove a device by name.
|
|
312
332
|
- `ff-cli device default <name>` – Promote a device to the top of the list so it is used when `-d` is omitted.
|
|
313
|
-
- `ff-cli device pair [name]` –
|
|
314
|
-
|
|
315
|
-
|
|
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.
|
|
316
366
|
|
|
317
367
|
Setup and `device add` both preserve existing devices. Adding a device with the same host as an existing one updates it in place.
|
|
318
368
|
|
|
@@ -330,7 +380,7 @@ Selection rules when sending:
|
|
|
330
380
|
|
|
331
381
|
Device cast contract:
|
|
332
382
|
|
|
333
|
-
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:
|
|
334
384
|
|
|
335
385
|
```jsonc
|
|
336
386
|
POST http://<device>:1111/api/cast
|
|
@@ -348,18 +398,23 @@ Content-Type: application/json
|
|
|
348
398
|
|
|
349
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.
|
|
350
400
|
|
|
351
|
-
When LAN delivery is unreachable, `
|
|
401
|
+
When LAN delivery is unreachable, `display` prints an explicit notice and retries
|
|
352
402
|
the same command through `ff1Relayer.baseUrl`. The per-device topic is read from
|
|
353
403
|
the operating system credential vault, never `config.json`. Pair it from the
|
|
354
404
|
mobile app first:
|
|
355
405
|
|
|
356
406
|
```bash
|
|
357
|
-
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)
|
|
358
409
|
```
|
|
359
410
|
|
|
360
|
-
|
|
361
|
-
Comparing it authenticates the CLI public key
|
|
362
|
-
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.
|
|
363
418
|
|
|
364
419
|
The default relayer is
|
|
365
420
|
`https://tv-cast-coordination.autonomy-system.workers.dev`. Override it with
|
|
@@ -368,18 +423,205 @@ need no additional key. Deployments that retain the cloud-edge compatibility
|
|
|
368
423
|
gate may receive `ff1Relayer.apiKey` or `FF1_RELAYER_API_KEY`; ff-cli never
|
|
369
424
|
bundles that shared key.
|
|
370
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
|
+
|
|
371
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.
|
|
372
491
|
|
|
373
492
|
Compatibility checks:
|
|
374
493
|
|
|
375
|
-
- `
|
|
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.
|
|
376
495
|
|
|
377
496
|
- Minimum supported FF1 OS versions:
|
|
378
|
-
- `
|
|
497
|
+
- `display` (`displayPlaylist`): `1.0.0` or newer
|
|
379
498
|
- `ssh` (`sshAccess`): `1.0.9` or newer
|
|
380
499
|
|
|
381
500
|
- If the CLI cannot get a version from the device (e.g. network or malformed response), it continues and sends the command.
|
|
382
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 |
|
|
383
625
|
|
|
384
626
|
Troubleshooting note:
|
|
385
627
|
|
|
@@ -389,10 +631,10 @@ Examples:
|
|
|
389
631
|
|
|
390
632
|
```bash
|
|
391
633
|
# Send to first device
|
|
392
|
-
npm run dev --
|
|
634
|
+
npm run dev -- display playlist.json
|
|
393
635
|
|
|
394
|
-
#
|
|
395
|
-
npm run dev --
|
|
636
|
+
# Display on a specific device by exact name
|
|
637
|
+
npm run dev -- display playlist.json -d "Living Room Display"
|
|
396
638
|
```
|
|
397
639
|
|
|
398
640
|
Minimal `config.json` example (selected fields):
|
|
@@ -405,7 +647,7 @@ Minimal `config.json` example (selected fields):
|
|
|
405
647
|
"role": "curator"
|
|
406
648
|
},
|
|
407
649
|
"feed": {
|
|
408
|
-
"baseURLs": ["https://
|
|
650
|
+
"baseURLs": ["https://feed.feralfile.com/api/v1"]
|
|
409
651
|
},
|
|
410
652
|
"ff1Devices": {
|
|
411
653
|
"devices": [
|
package/docs/EXAMPLES.md
CHANGED
|
@@ -18,7 +18,7 @@ npm run dev -- config validate
|
|
|
18
18
|
|
|
19
19
|
## Use with a coding agent
|
|
20
20
|
|
|
21
|
-
ff-cli ships a [Claude Code](https://docs.claude.com/en/docs/claude-code) / Codex skill at `skills/ff-control/SKILL.md`. Once installed, ask your agent in plain language ("Get 3 works from reas.eth and
|
|
21
|
+
ff-cli ships a [Claude Code](https://docs.claude.com/en/docs/claude-code) / Codex skill at `skills/ff-control/SKILL.md`. Once installed, ask your agent in plain language ("Get 3 works from reas.eth and display on Living Room", "Build a playlist from this Objkt URL and publish to my feed") and it translates the request into `ff-cli find` or `ff-cli build` and runs the validate → display/publish flow for you.
|
|
22
22
|
|
|
23
23
|
Recommended local install path:
|
|
24
24
|
|
|
@@ -32,19 +32,19 @@ The rest of this doc shows the deterministic commands the skill (or you) drives
|
|
|
32
32
|
|
|
33
33
|
## Find an artwork
|
|
34
34
|
|
|
35
|
-
`ff-cli find` resolves a marketplace URL, raw `chain:contract:tokenId`, or wallet address into a
|
|
35
|
+
`ff-cli find` resolves a marketplace URL, raw `chain:contract:tokenId`, or wallet address into a DP-1 playlist ready to display. Sources: Art Blocks, Objkt, fxhash, OpenSea, SuperRare, Feral File, Neort, Verse, raw on-chain coordinates, and wallet addresses.
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
# Paste a URL and
|
|
39
|
-
npm run dev -- find https://www.artblocks.io/collection/ringers-by-dmitri-cherniak --
|
|
38
|
+
# Paste a URL and display it on your FF1
|
|
39
|
+
npm run dev -- find https://www.artblocks.io/collection/ringers-by-dmitri-cherniak --display
|
|
40
40
|
|
|
41
41
|
# Tezos / hic et nunc via Objkt (the alias resolves to a KT1 contract)
|
|
42
|
-
npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 --
|
|
42
|
+
npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 --display
|
|
43
43
|
|
|
44
44
|
# Feral File artwork (public id may be hex or numeric)
|
|
45
|
-
npm run dev -- find https://feralfile.com/exhibitions/artwork/f0240e04d64717e319584957f6a83954b029254ad1260b6320472ea8c0c5b1cf --
|
|
45
|
+
npm run dev -- find https://feralfile.com/exhibitions/artwork/f0240e04d64717e319584957f6a83954b029254ad1260b6320472ea8c0c5b1cf --display
|
|
46
46
|
|
|
47
|
-
# Save without
|
|
47
|
+
# Save without displaying
|
|
48
48
|
npm run dev -- find ethereum:0xababababab20053426ad1c782de9ea8444358070:5001410 -o send-receive.json
|
|
49
49
|
|
|
50
50
|
# Single on-chain token by coordinates
|
|
@@ -62,11 +62,11 @@ npm run dev -- find https://www.artblocks.io/collection/ringers-by-dmitri-cherni
|
|
|
62
62
|
# Build and publish to a configured feed server
|
|
63
63
|
npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 --publish
|
|
64
64
|
|
|
65
|
-
# Build,
|
|
66
|
-
npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 --
|
|
65
|
+
# Build, display on a named device, and skip interactive prompts
|
|
66
|
+
npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 --display -d "Living Room Display" -y
|
|
67
67
|
|
|
68
|
-
# -y alone also
|
|
69
|
-
npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 -y --no-
|
|
68
|
+
# -y alone also displays (the scripted default); add --no-display to build and save without casting
|
|
69
|
+
npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 -y --no-display
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
OpenSea and Verse accept both item and collection/series URLs:
|
|
@@ -278,14 +278,14 @@ has to be re-signed:
|
|
|
278
278
|
```bash
|
|
279
279
|
ff-cli enrich playlist.json --assume-ethereum
|
|
280
280
|
ff-cli sign playlist.json
|
|
281
|
-
ff-cli
|
|
281
|
+
ff-cli display playlist.json -d "living room"
|
|
282
282
|
```
|
|
283
283
|
|
|
284
284
|
Casting does not care about the signing role. Add `-r curator` to that `sign` if the playlist is also
|
|
285
285
|
going to a feed: `ff-cli publish` requires the owner role, for the reasons in
|
|
286
286
|
[Publish to Feed Server](#publish-to-feed-server).
|
|
287
287
|
|
|
288
|
-
## Validate / Sign /
|
|
288
|
+
## Validate / Sign / Display
|
|
289
289
|
|
|
290
290
|
```bash
|
|
291
291
|
# Validate playlist structure
|
|
@@ -298,27 +298,27 @@ npm run dev -- verify playlist.json
|
|
|
298
298
|
# Sign playlist (uses key/role from config, or override via --key / --role)
|
|
299
299
|
npm run dev -- sign playlist.json -o signed.json
|
|
300
300
|
|
|
301
|
-
#
|
|
302
|
-
npm run dev --
|
|
301
|
+
# Display on the configured default device
|
|
302
|
+
npm run dev -- display playlist.json
|
|
303
303
|
|
|
304
|
-
#
|
|
305
|
-
npm run dev --
|
|
304
|
+
# Display on a specific named device
|
|
305
|
+
npm run dev -- display signed.json -d "Living Room Display"
|
|
306
306
|
|
|
307
|
-
#
|
|
308
|
-
npm run dev --
|
|
307
|
+
# Display a hosted DP-1 playlist
|
|
308
|
+
npm run dev -- display "https://cdn.example.com/playlist.json"
|
|
309
309
|
|
|
310
|
-
#
|
|
310
|
+
# Display a playlist straight from a DP-1 feed server (the URL the feed hands out)
|
|
311
311
|
# Wallet-signed (eip191) documents need --skip-verify until #103 lands
|
|
312
|
-
npm run dev --
|
|
312
|
+
npm run dev -- display "https://feed.feralfile.com/api/v1/playlists/<slug>" --skip-verify
|
|
313
313
|
|
|
314
|
-
#
|
|
315
|
-
npm run dev --
|
|
314
|
+
# Display a media URL directly
|
|
315
|
+
npm run dev -- display "https://example.com/video.mp4"
|
|
316
316
|
|
|
317
317
|
# Skip verification only if you must send a non-conformant payload (not recommended)
|
|
318
|
-
npm run dev --
|
|
318
|
+
npm run dev -- display playlist.json --skip-verify
|
|
319
319
|
```
|
|
320
320
|
|
|
321
|
-
> **No feed server is required to cast.** A device can
|
|
321
|
+
> **No feed server is required to cast.** A device can display any static, signed DP-1 playlist hosted at a public URL — sign it (`sign`), upload the JSON anywhere that serves it over HTTPS (S3, Supabase Storage, a static host, etc.), then `ff-cli display "<url>"`. The DP-1 Feed server ([Publish to Feed Server](#publish-to-feed-server) below) adds discovery and curation, but is optional for simply displaying your own playlists.
|
|
322
322
|
|
|
323
323
|
## Publish to Feed Server
|
|
324
324
|
|
|
@@ -921,7 +921,7 @@ says so rather than sending you after a key that does not exist. That is the sta
|
|
|
921
921
|
published without an owner-role signature is frozen in, and it is what the `publish` owner-role gate
|
|
922
922
|
above exists to prevent.
|
|
923
923
|
|
|
924
|
-
## Complete Flow (build → validate → sign →
|
|
924
|
+
## Complete Flow (build → validate → sign → display → publish)
|
|
925
925
|
|
|
926
926
|
```bash
|
|
927
927
|
# 1. Build a playlist (via find or build)
|
|
@@ -937,8 +937,8 @@ npm run dev -- validate playlist.json
|
|
|
937
937
|
# what feeds enforce today.
|
|
938
938
|
npm run dev -- sign playlist.json -r curator -o signed.json
|
|
939
939
|
|
|
940
|
-
# 4.
|
|
941
|
-
npm run dev --
|
|
940
|
+
# 4. Display it on a device
|
|
941
|
+
npm run dev -- display signed.json -d "Living Room Display"
|
|
942
942
|
|
|
943
943
|
# 5. Publish to a feed server
|
|
944
944
|
npm run dev -- publish signed.json -s 0
|
|
@@ -949,17 +949,17 @@ key declared in `curators[]`. Skip step 3 and use `playlist.json` in steps 4 and
|
|
|
949
949
|
`signed.json` on that path:
|
|
950
950
|
|
|
951
951
|
```bash
|
|
952
|
-
npm run dev --
|
|
952
|
+
npm run dev -- display playlist.json -d "Living Room Display"
|
|
953
953
|
npm run dev -- publish playlist.json -s 0
|
|
954
954
|
```
|
|
955
955
|
|
|
956
956
|
Set `playlist.curatorName` in the config to control the name recorded in `curators[]`; it defaults
|
|
957
957
|
to `ff-cli`.
|
|
958
958
|
|
|
959
|
-
`ff-cli find` can collapse build +
|
|
959
|
+
`ff-cli find` can collapse build + display + publish into one command:
|
|
960
960
|
|
|
961
961
|
```bash
|
|
962
|
-
npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 --
|
|
962
|
+
npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 --display -d "Living Room Display" --publish
|
|
963
963
|
```
|
|
964
964
|
|
|
965
965
|
## FF1 device management
|