@feralfile/cli 2.4.0 → 2.6.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/docs/EXAMPLES.md CHANGED
@@ -239,7 +239,7 @@ title-only on the FF1 tombstone, and shows empty tiles in the app for every
239
239
  live HTML work. `enrich` repairs it in place.
240
240
 
241
241
  ```bash
242
- ff-cli enrich playlist.json
242
+ ff-cli enrich playlist.json --assume-ethereum
243
243
  ```
244
244
 
245
245
  ```text
@@ -251,25 +251,37 @@ Enrich playlist
251
251
  Output: playlist.json
252
252
  ```
253
253
 
254
+ `--assume-ethereum` is the operator's assertion that the playlist's `evm`
255
+ coordinates are Ethereum. DP-1 records every EVM network as `chain: "evm"`,
256
+ and `find` writes that for Ethereum works too, so an Ethereum playlist needs
257
+ the flag; without it those items are skipped rather than guessed at, because
258
+ the wrong network returns a different artwork's metadata. A Tezos playlist
259
+ needs no flag.
260
+
254
261
  Items the indexer cannot resolve are listed rather than guessed at:
255
262
 
256
263
  ```text
257
264
  Nothing to enrich
258
265
 
259
- Skipped 2:
266
+ Skipped 3:
260
267
  Kim Asendorf — PXL NET — no provenance.contract chain/address/tokenId to look up
261
268
  Untitled — the indexer returned nothing for it
269
+ Pre-Process — chain "evm" names a family; pass --assume-ethereum to assert the network
262
270
  ```
263
271
 
264
272
  Enrichment changes the document, so a signed playlist loses its envelope and
265
273
  has to be re-signed:
266
274
 
267
275
  ```bash
268
- ff-cli enrich playlist.json
276
+ ff-cli enrich playlist.json --assume-ethereum
269
277
  ff-cli sign playlist.json
270
278
  ff-cli play playlist.json -d "living room"
271
279
  ```
272
280
 
281
+ Casting does not care about the signing role. Add `-r curator` to that `sign` if the playlist is also
282
+ going to a feed: `ff-cli publish` requires the owner role, for the reasons in
283
+ [Publish to Feed Server](#publish-to-feed-server).
284
+
273
285
  ## Validate / Sign / Play
274
286
 
275
287
  ```bash
@@ -317,12 +329,10 @@ Add feed servers to `config.json`:
317
329
  {
318
330
  "feedServers": [
319
331
  {
320
- "baseUrl": "http://localhost:8787/api/v1",
321
- "apiKey": "your-api-key"
332
+ "baseUrl": "http://localhost:8787/api/v1"
322
333
  },
323
334
  {
324
- "baseUrl": "https://feed.example.com/api/v1",
325
- "apiKey": "your-api-key"
335
+ "baseUrl": "https://feed.example.com/api/v1"
326
336
  }
327
337
  ]
328
338
  }
@@ -383,13 +393,132 @@ Select server (0-based index): 0
383
393
  Playlist file not found: /path/to/playlist.json
384
394
  ```
385
395
 
386
- **API error:**
396
+ Both of these are caught locally, before anything is uploaded.
397
+
398
+ **Not signed:**
387
399
 
388
400
  ```
389
- ❌ Failed to publish playlist
390
- Failed to publish: {"error":"unauthorized","message":"Invalid API key"}
401
+ Publish failed
402
+ Playlist verification failed: Playlist signature verification failed
403
+ ```
404
+
405
+ Declare your key in `curators[]`, then sign it as the owner before publishing:
406
+
407
+ ```bash
408
+ ff-cli status # the did:key to declare
409
+ ff-cli sign <file> -r curator
410
+ ```
411
+
412
+ Plain `ff-cli sign` would use `playlist.role` (default `agent`), producing a document the next two checks
413
+ refuse — under role-aware ownership a declared key counts as an owner only when it signed as `curator`, and
414
+ `ff-cli publish` requires that whether or not the target feed enforces it yet. (Posting the unsigned
415
+ document straight to the feed answers
416
+ `{"error":"unauthorized","message":"missing authentication: request body must carry signatures"}`.)
417
+
418
+ **Signed, but the signer is not declared as a curator:**
419
+
420
+ ```
421
+ Publish failed
422
+ Playlist is signed, but the signing key is not declared as a curator.
423
+
424
+ The feed accepts a publish when a signature's kid appears in the playlist's own curators[].
425
+ Add this to the playlist before signing:
426
+ "curators": [{ "name": "Your name", "key": "did:key:z6Mkv7qJ..." }]
427
+ then sign again from the unsigned file, with the same key you just declared:
428
+ ff-cli sign <file> -r curator --key <private key for did:key:z6Mkv7qJ...>
429
+ Drop --key if that key is your configured one; "sign" uses the configured key otherwise, and a
430
+ signature from an undeclared key would not satisfy the feed. Start from the unsigned file
431
+ because declaring curators[] changes the signed payload: signing appends, so the earlier
432
+ signature would be left covering a document that no longer exists.
433
+ ```
434
+
435
+ This is the most common publish failure, and it is not about credentials. The feed accepts a create when
436
+ the document carries a signature whose `kid` matches a key declared in its own `curators[]`; posting such
437
+ a document directly returns
438
+ `{"error":"signature_verification_failed","message":"curator signature verification: no valid curator signature found"}`,
439
+ which reads as a signing fault, so `ff-cli` diagnoses it locally instead.
440
+
441
+ **Order matters.** A signature covers every field except `signature`/`signatures`, so `curators[]` has to
442
+ be declared *before* signing. Adding it afterwards invalidates the signature, and signing again does not
443
+ repair it — signing **appends**, leaving the earlier entry covering a document that no longer exists, so
444
+ verification fails before upload.
445
+
446
+ ```bash
447
+ # 1. Read your signing identity — this is the kid your signatures will carry.
448
+ ff-cli status # → Signing identity (did:key) did:key:z6Mk...
449
+
450
+ # 2. Declare it in the UNSIGNED playlist.
451
+ # "curators": [{ "name": "Your Name", "key": "did:key:z6Mk..." }]
452
+ # DP-1 requires `name` alongside `key`.
453
+
454
+ # 3. Sign once as curator, then publish. The role must match the claim in curators[].
455
+ ff-cli sign playlist.json -r curator
456
+ ff-cli publish playlist.json
457
+ ```
458
+
459
+ If a playlist is already signed and you need to add `curators[]`, start again from the unsigned file
460
+ rather than re-signing: remove `signatures`, declare the curator, then sign.
461
+
462
+ `ff-cli publish` checks this before uploading, so a missing declaration fails immediately with the key to
463
+ add rather than as a server error. No API key is involved: the feed does not accept one.
464
+
465
+ **Declared, but signed under a non-owner role:**
466
+
467
+ ```
468
+ Publish failed
469
+ Playlist is signed by a declared curator, but under a non-owner role ("agent").
470
+
471
+ This is ff-cli's check, not a feed's answer. Feeds are moving to role-aware ownership, where
472
+ a key in curators[] counts as an owner only if it also signed as "curator". A feed that
473
+ does not check the role yet will accept this document, and then be unable to authorize a
474
+ replace or a delete for it: both need an owner signature it does not carry. Publishing it is
475
+ what makes that permanent, so ff-cli refuses here instead.
476
+
477
+ curators[] is already correct — only the role is missing, so add that signature to this file:
478
+ ff-cli sign <file> -r curator --key <private key for a declared curator>
479
+ Any key this playlist declares will do — you do not need the one that signed under the wrong
480
+ role. Declared: did:key:z6Mkv7qJ...
481
+ It must be one of those: a curator signature from an undeclared key satisfies neither
482
+ this check nor role-aware ownership, since both read roles only from declared keys. "sign"
483
+ uses the configured key unless --key says otherwise; drop --key if a declared key is already
484
+ your configured one, and confirm which identity a key carries with
485
+ "ff-cli status --key <private key>".
486
+ Signing appends, and the payload hash excludes signatures, so the existing entry stays valid
487
+ and the document ends up carrying both. No unsigned copy is needed: you only have to start
488
+ from one when changing signed content such as curators[] itself.
391
489
  ```
392
490
 
491
+ Declaring a key in `curators[]` is a claim that it owns the playlist; signing as `curator` is the proof.
492
+ This failure means the document is right and the signature is not — the fix is the role, not `curators[]`.
493
+
494
+ **This one is repairable in place**, unlike the two above. Nothing signed needs to change, so appending is
495
+ enough — the payload hash covers the document with `signature`/`signatures` stripped, which leaves the
496
+ earlier entry valid over bytes that did not move:
497
+
498
+ ```bash
499
+ # --key only when the declared curator is not your configured key; `ff-cli status --key <k>`
500
+ # reports which identity a key carries.
501
+ ff-cli sign playlist.json -r curator --key <declared curator's key> # ["agent"] -> ["agent","curator"]
502
+ ff-cli publish playlist.json
503
+ ```
504
+
505
+ The signature has to come from a key the playlist already declares — **any** of them, not specifically the
506
+ one that signed under the wrong role. On a playlist with several curators, whoever holds one of the other
507
+ declared keys can repair it. `sign` uses the configured key unless `--key` says otherwise, so on a machine
508
+ whose configured key is not declared, the plain command appends a `curator` signature the feed ignores and
509
+ the publish fails identically.
510
+
511
+ Start again from an unsigned document only when the fix changes signed content — adding `curators[]`, for
512
+ instance. There the earlier signature would cover a document that no longer exists.
513
+
514
+ **This is a CLI preflight, not a report of what a feed said.** A feed that does not yet check the role
515
+ accepts such a document, so uploading it by hand can still succeed. It is refused here because once
516
+ accepted it can never be replaced **or** deleted: both need an owner signature it does not carry, and the
517
+ only way to add one is to replace the document. Publishing is what makes that permanent.
518
+
519
+ `find` and `build` sign as `curator` automatically, since they declare the key themselves. This applies to
520
+ documents you sign by hand, where `playlist.role` (default `agent`) decides the role.
521
+
393
522
  ## Complete Flow (build → validate → sign → play → publish)
394
523
 
395
524
  ```bash
@@ -399,8 +528,12 @@ npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 -o playlist.json
399
528
  # 2. Validate it
400
529
  npm run dev -- validate playlist.json
401
530
 
402
- # 3. Sign it
403
- npm run dev -- sign playlist.json -o signed.json
531
+ # 3. Sign it. Declare curators[] BEFORE this step: the signature covers it, and the feed only
532
+ # accepts a publish when a signature's kid matches a declared curator key.
533
+ # Run `ff-cli status` for the kid (add -k <key> if you sign with `sign --key`).
534
+ # -r curator: ff-cli publish requires the owner role (role-aware ownership); the kid match is
535
+ # what feeds enforce today.
536
+ npm run dev -- sign playlist.json -r curator -o signed.json
404
537
 
405
538
  # 4. Play it on a device
406
539
  npm run dev -- play signed.json -d "Living Room Display"
@@ -409,6 +542,18 @@ npm run dev -- play signed.json -d "Living Room Display"
409
542
  npm run dev -- publish signed.json -s 0
410
543
  ```
411
544
 
545
+ With a signing key configured, `find` and `build` already produce a signed `playlist.json` with your
546
+ key declared in `curators[]`. Skip step 3 and use `playlist.json` in steps 4 and 5 — there is no
547
+ `signed.json` on that path:
548
+
549
+ ```bash
550
+ npm run dev -- play playlist.json -d "Living Room Display"
551
+ npm run dev -- publish playlist.json -s 0
552
+ ```
553
+
554
+ Set `playlist.curatorName` in the config to control the name recorded in `curators[]`; it defaults
555
+ to `ff-cli`.
556
+
412
557
  `ff-cli find` can collapse build + play + publish into one command:
413
558
 
414
559
  ```bash
@@ -237,28 +237,41 @@ and nothing else. `enrich` is the repair path for those.
237
237
  changing the title would produce two documents claiming to be the same cached
238
238
  manifest. The replacement is derived from the original id and the title, so
239
239
  it is deterministic across runs.
240
+ - The token standard the playlist asserts (`provenance.contract.standard`)
241
+ rides the lookup. The indexer keys ERC-721 and ERC-1155 tokens under
242
+ different CIDs (`eip155:1:erc721:…`, `eip155:1:erc1155:…`) and the client's
243
+ detection, having only an address, answers erc721 for every EVM contract —
244
+ so without the assertion an ERC-1155 work is queried as ERC-721 and never
245
+ found. A value outside the standards the CID can carry falls back to
246
+ detection.
240
247
  - The indexer resolving a token and having something worth attaching are
241
248
  different outcomes: `buildInlineManifestForToken` returns nothing when it has
242
- only a title, since a title-only manifest adds payload without adding
243
- information. Enrichment reports those separately from tokens it could not
244
- resolve at all.
249
+ only a title, since on the items `find` and `build` create a title-only
250
+ manifest repeats what the item already says. A curator's item may carry no
251
+ title at all, and the tombstone then shows nothing, so enrichment does
252
+ synthesize a title-only manifest in that one case — the indexer's title is
253
+ substantiated, and `item.title` itself is still never written. An item that
254
+ has a title and resolves to nothing more is reported as resolved-without-
255
+ metadata, separately from tokens the indexer could not resolve at all.
245
256
  - A still the manifest builder suppressed is recovered against the item's own
246
257
  source. `resolveStillUri` blanks a thumbnail equal to the source it was
247
258
  handed, which is the source the *indexer* chose; for a static work that is
248
259
  often the still itself. When the curator kept a live HTML source instead, the
249
260
  item would otherwise end up with no thumbnail — the empty grid tile this
250
- command exists to remove. When the indexer's source differs from the item's
251
- and is http(s) **and is media the app can rasterize**, it is that suppressed
252
- still
253
- and becomes the thumbnail. The type check matters: `getBestMediaUrl` prefers
254
- `display.animation_url` and media assets over `display.image_url`, so the
255
- indexer's source is frequently a live HTML rendition, and putting one in the
256
- thumbnail slot is worse than leaving it empty — the grid still cannot
257
- rasterize it and the item now claims a still it does not have. The predicate
258
- matches the media types `playlist-builder.js` already recognizes, images plus
259
- SVG and video, and rejects HTML. A
260
- thumbnail-only manifest is emitted where none was, unlike a title-only one:
261
- it is the difference between a tile and an empty square.
261
+ command exists to remove. The lookup therefore carries the still the indexer
262
+ names for the token (`display.image_url`, returned beside the DP-1 item by
263
+ `resolveTokenBatch`), and enrichment applies the same rule against the
264
+ curator's source: when that still differs from the item's source it becomes
265
+ the thumbnail. It is the same field `find` and `build` write into
266
+ `thumbnails.default`, taken on the indexer's word as they take it. Nothing is
267
+ inferred from the indexer's *source*: `getBestMediaUrl` prefers
268
+ `display.animation_url` and media assets over `display.image_url`, so that
269
+ source is frequently a live HTML rendition, and a file-suffix check would
270
+ reject the extensionless CDN URLs the indexer commonly returns as stills
271
+ while `display.mime_type` describes the primary rendition rather than the
272
+ still (a live work reports `text/html` beside a PNG `image_url`). A
273
+ thumbnail-only manifest is emitted where none was: it is the difference
274
+ between a tile and an empty square.
262
275
  - `evm` names a family, and enrichment refuses to guess which member. DP-1 §6
263
276
  carries no network identity, and the indexer maps Ethereum, Polygon,
264
277
  Arbitrum, Optimism, Base, and Zora all back to `evm`, so neither the request
package/docs/README.md CHANGED
@@ -36,7 +36,8 @@ ff-cli config validate
36
36
  {
37
37
  "defaultDuration": 10,
38
38
  "playlist": {
39
- "privateKey": "your_ed25519_private_key_hex_or_base64_here"
39
+ "privateKey": "your_ed25519_private_key_hex_or_base64_here",
40
+ "curatorName": "YOUR_CURATOR_NAME"
40
41
  },
41
42
  "feed": { "baseURLs": ["https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1"] },
42
43
  "ff1Devices": {
@@ -192,8 +193,8 @@ image, video, or SVG source, but not from a live HTML work.
192
193
 
193
194
  ```bash
194
195
  ff-cli enrich playlist.json --assume-ethereum
195
- ff-cli enrich playlist.json -o labelled.json
196
- ff-cli enrich playlist.json --force # replace existing manifests
196
+ ff-cli enrich playlist.json --assume-ethereum -o labelled.json
197
+ ff-cli enrich playlist.json --assume-ethereum --force # replace existing manifests
197
198
  ```
198
199
 
199
200
  `--assume-ethereum` is needed for most playlists. DP-1 records an EVM
@@ -211,9 +212,12 @@ changes the document, so the signature is removed and the playlist must be
211
212
  re-signed before playing:
212
213
 
213
214
  ```bash
214
- ff-cli enrich playlist.json && ff-cli sign playlist.json
215
+ ff-cli enrich playlist.json --assume-ethereum && ff-cli sign playlist.json
215
216
  ```
216
217
 
218
+ That role is fine for casting. Use `ff-cli sign playlist.json -r curator` if the enriched playlist is
219
+ also going to a feed.
220
+
217
221
  ### Validate, sign, and play
218
222
 
219
223
  ```bash
@@ -278,16 +282,42 @@ The `publish` command:
278
282
 
279
283
  Configure feed servers in `config.json`:
280
284
 
285
+ Publishing needs no credentials. The feed authorizes a create from the signatures inside the playlist:
286
+ it requires a signature whose `kid` matches a key the document declares in `curators[]`.
287
+
288
+ `ff-cli publish` additionally requires that signature to carry the **`curator` role**. That is ff-cli's
289
+ check, not every feed's answer today: feeds are moving to role-aware ownership, where being named in
290
+ `curators[]` is a claim and signing as `curator` is the proof. A feed that does not check the role yet
291
+ accepts an `agent`-signed document — and can then authorize neither a replace nor a delete for it, since
292
+ both need an owner signature it does not carry. Publishing is what makes that permanent, so the CLI
293
+ refuses first.
294
+
295
+ Declare it before signing, not after: a signature covers `curators[]`, so adding the entry to an
296
+ already-signed playlist invalidates it, and signing again appends rather than replaces. Run
297
+ `ff-cli status` to read the `did:key` your signatures will carry, put it in `curators[]`, then sign
298
+ once **with the owner role**:
299
+
300
+ ```bash
301
+ ff-cli sign playlist.json -r curator
302
+ ```
303
+
304
+ The flag is needed because plain `ff-cli sign` uses `playlist.role`, which defaults to `agent` — a
305
+ document `ff-cli publish` then refuses. Set `"role": "curator"` under `playlist` in `config.json` to make
306
+ that the default for hand-signed playlists.
307
+
308
+ If you sign with `sign --key <privateKey>`, read that key's identity with
309
+ `ff-cli status --key <privateKey>` — the configured identity would be the wrong one to declare. `find`
310
+ and `build` need none of this: they declare the key and sign it as `curator` for you when a signing key
311
+ is configured. An `apiKey` left over in an existing config is ignored.
312
+
281
313
  ```json
282
314
  {
283
315
  "feedServers": [
284
316
  {
285
- "baseUrl": "http://localhost:8787/api/v1",
286
- "apiKey": "your-api-key-optional"
317
+ "baseUrl": "http://localhost:8787/api/v1"
287
318
  },
288
319
  {
289
- "baseUrl": "https://feed.example.com/api/v1",
290
- "apiKey": "your-api-key-optional"
320
+ "baseUrl": "https://feed.example.com/api/v1"
291
321
  }
292
322
  ]
293
323
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feralfile/cli",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "description": "CLI for building, signing, and playing DP-1 playlists of digital art on Feral File devices",
5
5
  "repository": {
6
6
  "type": "git",
@@ -60,7 +60,7 @@
60
60
  "dependencies": {
61
61
  "@feralfile/source-resolver": "^1.0.0",
62
62
  "@napi-rs/keyring": "1.3.0",
63
- "axios": "^1.6.2",
63
+ "axios": "^1.20.0",
64
64
  "bonjour-service": "^1.3.0",
65
65
  "chalk": "^4.1.2",
66
66
  "commander": "^11.1.0",
@@ -68,7 +68,7 @@
68
68
  "dp1-js": "2.3.1",
69
69
  "ff1": "^1.0.0",
70
70
  "fuzzysort": "^3.1.0",
71
- "viem": "^2.38.2"
71
+ "viem": "^2.56.3"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@types/node": "^24.7.1",