@feralfile/cli 2.3.0 → 2.5.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/config.json.example +4 -5
- package/dist/index.js +2 -0
- package/dist/package.json +1 -1
- package/dist/src/commands/enrich.js +315 -0
- package/dist/src/commands/find.js +1 -2
- package/dist/src/commands/play.js +5 -2
- package/dist/src/commands/publish.js +1 -8
- package/dist/src/commands/status.js +51 -1
- package/dist/src/config.js +45 -5
- package/dist/src/utilities/enrich-playlist.js +459 -0
- package/dist/src/utilities/index.js +1 -1
- package/dist/src/utilities/nft-indexer.js +13 -4
- package/dist/src/utilities/playlist-builder.js +12 -0
- package/dist/src/utilities/playlist-publisher.js +61 -13
- package/dist/src/utilities/signing-identity.js +34 -0
- package/docs/CONFIGURATION.md +13 -0
- package/docs/EXAMPLES.md +106 -8
- package/docs/PROJECT_SPEC.md +122 -0
- package/docs/README.md +52 -5
- package/package.json +1 -1
package/docs/CONFIGURATION.md
CHANGED
|
@@ -50,6 +50,19 @@ Used for signing DP‑1 playlists.
|
|
|
50
50
|
|
|
51
51
|
ff-cli normalizes whichever form you supply to base64 PKCS#8 DER before handing it to **`dp1-js`** (`SignMultiEd25519`). A malformed key fails with an actionable message (e.g. _"Invalid Ed25519 signing key … run `ff-cli setup` to generate one"_) instead of dp1-js's cryptic OpenSSL ASN.1 error (`header too long` / `wrong tag`).
|
|
52
52
|
|
|
53
|
+
- `playlist.curatorName` (string): Display name recorded alongside your signing key in the `curators[]`
|
|
54
|
+
of playlists built by `find` and `build`. Defaults to `ff-cli`; you can also set it via
|
|
55
|
+
`PLAYLIST_CURATOR_NAME`. The feed authorizes a create by matching a signature's `kid` against a key
|
|
56
|
+
declared in the document's own `curators[]`, so those commands write that declaration **before**
|
|
57
|
+
signing — a name is required next to the key by DP-1, and the value is public and covered by the
|
|
58
|
+
signature. The sample `YOUR_CURATOR_NAME` in `config.json.example` is treated as unset, so an untouched
|
|
59
|
+
config falls through to `PLAYLIST_CURATOR_NAME` or the default rather than publishing under a
|
|
60
|
+
placeholder.
|
|
61
|
+
|
|
62
|
+
To read the `did:key` your signatures will carry, run `ff-cli status`. It reports the identity from
|
|
63
|
+
`PLAYLIST_PRIVATE_KEY` even when no config file exists yet, and `ff-cli status --key <privateKey>`
|
|
64
|
+
reports it for an explicit key — the one `sign --key` would use.
|
|
65
|
+
|
|
53
66
|
- `playlist.role` (string): DP-1 signing role that travels with the private key. Defaults to `agent` if omitted. You can also set this via `PLAYLIST_ROLE` in `.env`. Guided `ff-cli setup`, `config validate`, and `sign --role` only accept the usual DP-1 signing roles (`agent`, `feed`, `curator`, `institution`, `licensor`).
|
|
54
67
|
|
|
55
68
|
### Generate an Ed25519 private key
|
package/docs/EXAMPLES.md
CHANGED
|
@@ -232,6 +232,44 @@ JSON
|
|
|
232
232
|
npm run dev -- build /tmp/mixed.json -o playlist-mixed.json -v
|
|
233
233
|
```
|
|
234
234
|
|
|
235
|
+
## Enrich an existing playlist
|
|
236
|
+
|
|
237
|
+
A playlist whose items carry `provenance` but no `inlineManifest` labels
|
|
238
|
+
title-only on the FF1 tombstone, and shows empty tiles in the app for every
|
|
239
|
+
live HTML work. `enrich` repairs it in place.
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
ff-cli enrich playlist.json
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
```text
|
|
246
|
+
Enrich playlist
|
|
247
|
+
|
|
248
|
+
18/18 looked up...
|
|
249
|
+
|
|
250
|
+
18 of 18 item(s) enriched
|
|
251
|
+
Output: playlist.json
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Items the indexer cannot resolve are listed rather than guessed at:
|
|
255
|
+
|
|
256
|
+
```text
|
|
257
|
+
Nothing to enrich
|
|
258
|
+
|
|
259
|
+
Skipped 2:
|
|
260
|
+
Kim Asendorf — PXL NET — no provenance.contract chain/address/tokenId to look up
|
|
261
|
+
Untitled — the indexer returned nothing for it
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Enrichment changes the document, so a signed playlist loses its envelope and
|
|
265
|
+
has to be re-signed:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
ff-cli enrich playlist.json
|
|
269
|
+
ff-cli sign playlist.json
|
|
270
|
+
ff-cli play playlist.json -d "living room"
|
|
271
|
+
```
|
|
272
|
+
|
|
235
273
|
## Validate / Sign / Play
|
|
236
274
|
|
|
237
275
|
```bash
|
|
@@ -279,12 +317,10 @@ Add feed servers to `config.json`:
|
|
|
279
317
|
{
|
|
280
318
|
"feedServers": [
|
|
281
319
|
{
|
|
282
|
-
"baseUrl": "http://localhost:8787/api/v1"
|
|
283
|
-
"apiKey": "your-api-key"
|
|
320
|
+
"baseUrl": "http://localhost:8787/api/v1"
|
|
284
321
|
},
|
|
285
322
|
{
|
|
286
|
-
"baseUrl": "https://feed.example.com/api/v1"
|
|
287
|
-
"apiKey": "your-api-key"
|
|
323
|
+
"baseUrl": "https://feed.example.com/api/v1"
|
|
288
324
|
}
|
|
289
325
|
]
|
|
290
326
|
}
|
|
@@ -345,13 +381,61 @@ Select server (0-based index): 0
|
|
|
345
381
|
Playlist file not found: /path/to/playlist.json
|
|
346
382
|
```
|
|
347
383
|
|
|
348
|
-
|
|
384
|
+
Both of these are caught locally, before anything is uploaded.
|
|
385
|
+
|
|
386
|
+
**Not signed:**
|
|
349
387
|
|
|
350
388
|
```
|
|
351
|
-
|
|
352
|
-
|
|
389
|
+
Publish failed
|
|
390
|
+
Playlist verification failed: Playlist signature verification failed
|
|
353
391
|
```
|
|
354
392
|
|
|
393
|
+
Run `ff-cli sign <file>` before publishing. (Posting the same document straight to the feed answers
|
|
394
|
+
`{"error":"unauthorized","message":"missing authentication: request body must carry signatures"}`.)
|
|
395
|
+
|
|
396
|
+
**Signed, but the signer is not declared as a curator:**
|
|
397
|
+
|
|
398
|
+
```
|
|
399
|
+
Publish failed
|
|
400
|
+
Playlist is signed, but the signing key is not declared as a curator.
|
|
401
|
+
|
|
402
|
+
The feed accepts a publish when a signature's kid appears in the playlist's own curators[].
|
|
403
|
+
Add this to the playlist before signing:
|
|
404
|
+
"curators": [{ "name": "Your name", "key": "did:key:z6Mkv7qJ..." }]
|
|
405
|
+
then sign again from the unsigned file — signing appends, so re-signing an already-signed
|
|
406
|
+
playlist leaves the earlier signature covering a document that no longer exists.
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
This is the most common publish failure, and it is not about credentials. The feed accepts a create when
|
|
410
|
+
the document carries a signature whose `kid` matches a key declared in its own `curators[]`; posting such
|
|
411
|
+
a document directly returns
|
|
412
|
+
`{"error":"signature_verification_failed","message":"curator signature verification: no valid curator signature found"}`,
|
|
413
|
+
which reads as a signing fault, so `ff-cli` diagnoses it locally instead.
|
|
414
|
+
|
|
415
|
+
**Order matters.** A signature covers every field except `signature`/`signatures`, so `curators[]` has to
|
|
416
|
+
be declared *before* signing. Adding it afterwards invalidates the signature, and signing again does not
|
|
417
|
+
repair it — signing **appends**, leaving the earlier entry covering a document that no longer exists, so
|
|
418
|
+
verification fails before upload.
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
# 1. Read your signing identity — this is the kid your signatures will carry.
|
|
422
|
+
ff-cli status # → Signing identity (did:key) did:key:z6Mk...
|
|
423
|
+
|
|
424
|
+
# 2. Declare it in the UNSIGNED playlist.
|
|
425
|
+
# "curators": [{ "name": "Your Name", "key": "did:key:z6Mk..." }]
|
|
426
|
+
# DP-1 requires `name` alongside `key`.
|
|
427
|
+
|
|
428
|
+
# 3. Sign once, then publish.
|
|
429
|
+
ff-cli sign playlist.json
|
|
430
|
+
ff-cli publish playlist.json
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
If a playlist is already signed and you need to add `curators[]`, start again from the unsigned file
|
|
434
|
+
rather than re-signing: remove `signatures`, declare the curator, then sign.
|
|
435
|
+
|
|
436
|
+
`ff-cli publish` checks this before uploading, so a missing declaration fails immediately with the key to
|
|
437
|
+
add rather than as a server error. No API key is involved: the feed does not accept one.
|
|
438
|
+
|
|
355
439
|
## Complete Flow (build → validate → sign → play → publish)
|
|
356
440
|
|
|
357
441
|
```bash
|
|
@@ -361,7 +445,9 @@ npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 -o playlist.json
|
|
|
361
445
|
# 2. Validate it
|
|
362
446
|
npm run dev -- validate playlist.json
|
|
363
447
|
|
|
364
|
-
# 3. Sign it
|
|
448
|
+
# 3. Sign it. Declare curators[] BEFORE this step: the signature covers it, and the feed only
|
|
449
|
+
# accepts a publish when a signature's kid matches a declared curator key.
|
|
450
|
+
# Run `ff-cli status` for the kid (add -k <key> if you sign with `sign --key`).
|
|
365
451
|
npm run dev -- sign playlist.json -o signed.json
|
|
366
452
|
|
|
367
453
|
# 4. Play it on a device
|
|
@@ -371,6 +457,18 @@ npm run dev -- play signed.json -d "Living Room Display"
|
|
|
371
457
|
npm run dev -- publish signed.json -s 0
|
|
372
458
|
```
|
|
373
459
|
|
|
460
|
+
With a signing key configured, `find` and `build` already produce a signed `playlist.json` with your
|
|
461
|
+
key declared in `curators[]`. Skip step 3 and use `playlist.json` in steps 4 and 5 — there is no
|
|
462
|
+
`signed.json` on that path:
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
npm run dev -- play playlist.json -d "Living Room Display"
|
|
466
|
+
npm run dev -- publish playlist.json -s 0
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Set `playlist.curatorName` in the config to control the name recorded in `curators[]`; it defaults
|
|
470
|
+
to `ff-cli`.
|
|
471
|
+
|
|
374
472
|
`ff-cli find` can collapse build + play + publish into one command:
|
|
375
473
|
|
|
376
474
|
```bash
|
package/docs/PROJECT_SPEC.md
CHANGED
|
@@ -197,6 +197,128 @@ nowhere to host a manifest document. Behavior worth knowing downstream:
|
|
|
197
197
|
- An inline manifest has no `refHash`; its integrity comes from the playlist
|
|
198
198
|
signature.
|
|
199
199
|
|
|
200
|
+
### Enriching a playlist the CLI did not build
|
|
201
|
+
|
|
202
|
+
`find` and `build` attach a manifest as they create each item. A playlist that
|
|
203
|
+
arrives another way — assembled by hand, copied out of another document, or
|
|
204
|
+
written before §3.6 existed — has items with a `source` and a `provenance` block
|
|
205
|
+
and nothing else. `enrich` is the repair path for those.
|
|
206
|
+
|
|
207
|
+
- Enrichment is keyed on `provenance.contract` (chain, address, token id), not
|
|
208
|
+
on `source`. A source URL is one rendition of a work and several renditions
|
|
209
|
+
share one; the coordinate is what identifies the artwork.
|
|
210
|
+
- It writes `inlineManifest` and nothing else. `source`, `duration`, `title`,
|
|
211
|
+
`id`, and `display` are curator decisions and survive untouched even when the
|
|
212
|
+
indexer disagrees with them.
|
|
213
|
+
- An item the indexer cannot resolve is reported, never synthesized. The same
|
|
214
|
+
rule the manifest mapping follows applies here: the CLI does not invent fields
|
|
215
|
+
it cannot substantiate, and an invented artist line inside a signed document
|
|
216
|
+
is worse than an absent one.
|
|
217
|
+
- DP-1 names the EVM family `evm`; the FF indexer names it `ethereum`. Enrich
|
|
218
|
+
translates between the two. Unlisted chain names pass through so the indexer
|
|
219
|
+
decides what it supports.
|
|
220
|
+
- Enrichment changes the document, so both the `signatures[]` envelope and the
|
|
221
|
+
legacy flat `signature` are removed and the playlist must be re-signed.
|
|
222
|
+
Leaving a stale envelope in place would produce a file that fails
|
|
223
|
+
verification at the device with no explanation.
|
|
224
|
+
- An item carrying a `ref` is already labelled by a manifest the CLI cannot
|
|
225
|
+
see, and `ref` outranks `inlineManifest` in the resolution order, so
|
|
226
|
+
enrichment skips it. `--force` does not override that: making the inline
|
|
227
|
+
manifest win would mean deleting the curator's `ref` and `refHash`, which
|
|
228
|
+
discards the integrity hash the remote manifest is checked against. That
|
|
229
|
+
conversion, if ever wanted, belongs behind its own flag.
|
|
230
|
+
- Enrichment looks each distinct coordinate up once, however many items share
|
|
231
|
+
it. A token the indexer has not seen triggers an indexing job and a poll that
|
|
232
|
+
can take minutes, and a playlist that repeats a work would otherwise submit
|
|
233
|
+
that job once per appearance.
|
|
234
|
+
- Overriding an item's title changes the manifest's content, so it also changes
|
|
235
|
+
the manifest's `id`. The indexer derives that id from the token coordinate,
|
|
236
|
+
so two items of one artwork receive the same one; leaving it alone while
|
|
237
|
+
changing the title would produce two documents claiming to be the same cached
|
|
238
|
+
manifest. The replacement is derived from the original id and the title, so
|
|
239
|
+
it is deterministic across runs.
|
|
240
|
+
- The indexer resolving a token and having something worth attaching are
|
|
241
|
+
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.
|
|
245
|
+
- A still the manifest builder suppressed is recovered against the item's own
|
|
246
|
+
source. `resolveStillUri` blanks a thumbnail equal to the source it was
|
|
247
|
+
handed, which is the source the *indexer* chose; for a static work that is
|
|
248
|
+
often the still itself. When the curator kept a live HTML source instead, the
|
|
249
|
+
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.
|
|
262
|
+
- `evm` names a family, and enrichment refuses to guess which member. DP-1 §6
|
|
263
|
+
carries no network identity, and the indexer maps Ethereum, Polygon,
|
|
264
|
+
Arbitrum, Optimism, Base, and Zora all back to `evm`, so neither the request
|
|
265
|
+
nor the response can say which network is meant. Ethereum is chosen because
|
|
266
|
+
it is the only EVM network this client can reach — `buildTokenCID` maps
|
|
267
|
+
ethereum to `eip155:1` and tezos to `tezos:mainnet` and throws for anything
|
|
268
|
+
else. The residual risk is narrow: an L2 work whose address and token id also
|
|
269
|
+
exist on Ethereum would take the Ethereum work's metadata. The command
|
|
270
|
+
is the operator's to make: `--assume-ethereum` asserts it, and without that
|
|
271
|
+
flag `evm` items are skipped with a reason that names the flag. The warning
|
|
272
|
+
prints before anything is written, so an operator who did not mean to assert
|
|
273
|
+
it still has the file they started with. Reaching an actual L2 belongs in the
|
|
274
|
+
indexer client, which maps only `ethereum` and `tezos` today.
|
|
275
|
+
- In-place writes refuse to run over an input that changed while the lookup
|
|
276
|
+
was in flight. The comparison is a hash of the bytes actually parsed, against
|
|
277
|
+
the resolved write target, so `-o` naming the input by another spelling and a
|
|
278
|
+
symlink pointing at it are both recognized as the in-place case. It runs
|
|
279
|
+
immediately before the rename, so the exposed interval is a few syscalls
|
|
280
|
+
rather than the minutes a lookup takes. It narrows the race and does not
|
|
281
|
+
close it: compare-then-replace is not atomic against an editor participating
|
|
282
|
+
in no protocol, and no userspace sequence makes it so. Writing to a distinct
|
|
283
|
+
`--output` opts out of in-place replacement entirely, which is the right
|
|
284
|
+
choice when a file is being actively edited.
|
|
285
|
+
- An attached manifest's id is derived from its whole finished metadata block,
|
|
286
|
+
every time rather than only when this command changed something. The indexer
|
|
287
|
+
keys its id on the token coordinate, so every rendition of one artwork
|
|
288
|
+
carries the same id whatever its content, and under `--force` the indexer may
|
|
289
|
+
itself return a different artist or description under that unchanged id.
|
|
290
|
+
Deriving from the payload is the only rule that holds in every case:
|
|
291
|
+
identical content keeps one identity, differing content never shares one. The
|
|
292
|
+
original id is folded in so the result stays anchored to the token and stays
|
|
293
|
+
deterministic across runs.
|
|
294
|
+
- In-place writes refuse to run over an input that changed while the lookup
|
|
295
|
+
was in flight. The lookup can take minutes and the default destination is the
|
|
296
|
+
input, so a curator's edit or re-sign in that window would otherwise be
|
|
297
|
+
overwritten by a result computed from older bytes.
|
|
298
|
+
- In-place writes are atomic and crash-durable: the contents are synced before
|
|
299
|
+
the rename and the containing directory after it, so a power loss cannot
|
|
300
|
+
leave the destination name pointing at incomplete data. The directory sync is
|
|
301
|
+
best-effort, since it is not portable.
|
|
302
|
+
- In-place writes are atomic: the candidate goes to a temporary file beside the
|
|
303
|
+
destination and is renamed over it, so an interruption cannot leave a
|
|
304
|
+
truncated playlist. A symlinked destination is followed to its target rather
|
|
305
|
+
than replaced, and the existing file's permission mode is read before the
|
|
306
|
+
temporary file is created and applied to it before any bytes are written, so
|
|
307
|
+
a curator-restricted playlist is never briefly world-readable mid-write. The
|
|
308
|
+
destination's owner and group are carried across too, since a replacement is
|
|
309
|
+
a new inode and would otherwise take this process's ownership — silently
|
|
310
|
+
reassigning a shared playlist. Where that cannot be done, the in-place
|
|
311
|
+
replacement is refused and `--output` is offered instead. Extended ACLs are
|
|
312
|
+
not preserved, because Node exposes no portable way to read them; a playlist
|
|
313
|
+
carrying them should be enriched through `--output`.
|
|
314
|
+
- `--output` names a file the caller expects to exist afterwards, so it is
|
|
315
|
+
written even when nothing was enriched. A no-op without `--output` writes
|
|
316
|
+
nothing rather than rewriting the input for no gain.
|
|
317
|
+
- The playlist is validated before lookup and the enriched candidate is
|
|
318
|
+
validated before writing. On either failure the diagnostics are printed and
|
|
319
|
+
the file is left untouched, rather than replaced with a document that sign,
|
|
320
|
+
publish, and play would each reject later.
|
|
321
|
+
|
|
200
322
|
## Deterministic-first behavior
|
|
201
323
|
|
|
202
324
|
The CLI is deterministic end to end. There is no natural-language interface; any natural-language layer lives in the user's coding agent, which drives these commands:
|
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": {
|
|
@@ -104,6 +105,9 @@ Both paths run the same deterministic pipeline: fetch metadata, assemble a DP-1
|
|
|
104
105
|
- OpenSea forms: `/assets/ethereum/{contract}/{tokenId}`, `/item/ethereum/{contract}/{tokenId}`, and `/collection/{slug}` (Ethereum collections; the slug is resolved from the public collection page, no API key needed)
|
|
105
106
|
- Verse forms: `/items/ethereum/{contract}/{tokenId}` and `/series/{slug}`
|
|
106
107
|
- Options: `-o, --output <path>`, `-l, --limit <n>`, `-p, --play`, `-d, --device <name>`, `--publish`, `-s, --server <index>`, `-y, --yes`, `--skip-verify`
|
|
108
|
+
- `enrich <file>` – Add missing artist, title, and thumbnail metadata to a playlist that already exists. Resolves each item by its `provenance.contract` and writes an inline Ref Manifest; `source`, `duration`, `title`, and `id` are never touched. Drops the signature when it changes anything, so re-sign afterwards
|
|
109
|
+
- DP-1 `evm` names a chain family rather than a network, so those items are skipped unless `--assume-ethereum` asserts which one they are. Getting that wrong attaches another artwork's metadata
|
|
110
|
+
- Options: `-o, --output <file>`, `--force` (replace existing manifests), `--assume-ethereum`, `-v, --verbose`
|
|
107
111
|
- `publish <file>` – Publish a playlist to a feed server (runs `verify` before upload and rejects unsigned or broken playlists)
|
|
108
112
|
- Options: `-s, --server <index>` (server index if multiple configured)
|
|
109
113
|
- `ssh <enable|disable>` – Manage SSH access on an FF1 device
|
|
@@ -176,6 +180,41 @@ cat params.json | ff-cli build -o playlist.json
|
|
|
176
180
|
}
|
|
177
181
|
```
|
|
178
182
|
|
|
183
|
+
### Enrich a playlist that came from somewhere else
|
|
184
|
+
|
|
185
|
+
`find` and `build` label every item they create. A playlist assembled by hand,
|
|
186
|
+
copied from another document, or written before inline manifests existed has no
|
|
187
|
+
`inlineManifest`, and on a device that shows as a tombstone with no artist line
|
|
188
|
+
and an empty tile in the app's grid — the app can draw a thumbnail from an
|
|
189
|
+
image, video, or SVG source, but not from a live HTML work.
|
|
190
|
+
|
|
191
|
+
`ff-cli enrich` fills that in from the indexer, keyed on each item's
|
|
192
|
+
`provenance.contract`:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
ff-cli enrich playlist.json --assume-ethereum
|
|
196
|
+
ff-cli enrich playlist.json -o labelled.json
|
|
197
|
+
ff-cli enrich playlist.json --force # replace existing manifests
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`--assume-ethereum` is needed for most playlists. DP-1 records an EVM
|
|
201
|
+
coordinate as `chain: "evm"`, which names Ethereum, Polygon, Arbitrum,
|
|
202
|
+
Optimism, Base and Zora alike, and the indexer's answer collapses back to the
|
|
203
|
+
same word. Enrichment will not pick one for you, because the wrong member
|
|
204
|
+
returns a different artwork's artist and still — into a document you then sign.
|
|
205
|
+
|
|
206
|
+
It writes `inlineManifest` and nothing else. Items with no
|
|
207
|
+
`provenance.contract` chain, address, and token id cannot be looked up and are
|
|
208
|
+
reported rather than guessed at, and items carrying an external `ref` are left
|
|
209
|
+
alone because `ref` outranks an inline manifest at the device. The playlist is
|
|
210
|
+
validated before and after; on failure the file is left untouched. Enriching
|
|
211
|
+
changes the document, so the signature is removed and the playlist must be
|
|
212
|
+
re-signed before playing:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
ff-cli enrich playlist.json && ff-cli sign playlist.json
|
|
216
|
+
```
|
|
217
|
+
|
|
179
218
|
### Validate, sign, and play
|
|
180
219
|
|
|
181
220
|
```bash
|
|
@@ -240,16 +279,24 @@ The `publish` command:
|
|
|
240
279
|
|
|
241
280
|
Configure feed servers in `config.json`:
|
|
242
281
|
|
|
282
|
+
Publishing needs no credentials. The feed authorizes a create from the signatures inside the playlist:
|
|
283
|
+
it requires a signature whose `kid` matches a key the document declares in `curators[]`.
|
|
284
|
+
|
|
285
|
+
Declare it before signing, not after: a signature covers `curators[]`, so adding the entry to an
|
|
286
|
+
already-signed playlist invalidates it, and signing again appends rather than replaces. Run
|
|
287
|
+
`ff-cli status` to read the `did:key` your signatures will carry, put it in `curators[]`, then sign
|
|
288
|
+
once. If you sign with `sign --key <privateKey>`, read that key's identity with
|
|
289
|
+
`ff-cli status --key <privateKey>` — the configured identity would be the wrong one to declare. `find` and `build` do this for you when a signing key is configured. An `apiKey` left over in
|
|
290
|
+
an existing config is ignored.
|
|
291
|
+
|
|
243
292
|
```json
|
|
244
293
|
{
|
|
245
294
|
"feedServers": [
|
|
246
295
|
{
|
|
247
|
-
"baseUrl": "http://localhost:8787/api/v1"
|
|
248
|
-
"apiKey": "your-api-key-optional"
|
|
296
|
+
"baseUrl": "http://localhost:8787/api/v1"
|
|
249
297
|
},
|
|
250
298
|
{
|
|
251
|
-
"baseUrl": "https://feed.example.com/api/v1"
|
|
252
|
-
"apiKey": "your-api-key-optional"
|
|
299
|
+
"baseUrl": "https://feed.example.com/api/v1"
|
|
253
300
|
}
|
|
254
301
|
]
|
|
255
302
|
}
|