@feralfile/cli 2.5.0 → 2.7.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.
@@ -1,37 +1,4 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
4
  };
@@ -46,7 +13,7 @@ const playlist_source_1 = require("./playlist-source");
46
13
  */
47
14
  async function getAvailableDevices() {
48
15
  try {
49
- const configModule = (await Promise.resolve().then(() => __importStar(require('../config'))));
16
+ const configModule = (await import('../config.js'));
50
17
  const getFF1DeviceConfig = configModule.getFF1DeviceConfig ||
51
18
  (configModule.default && configModule.default.getFF1DeviceConfig) ||
52
19
  configModule.default;
@@ -146,7 +113,7 @@ async function confirmPlaylistForSending(filePath, deviceName) {
146
113
  // Parse / structure validation only (same as `validate`; use `verify` CLI for signatures)
147
114
  console.log(chalk_1.default.cyan('Validation'));
148
115
  // Dynamic import to avoid circular dependency
149
- const { validatePlaylist } = await Promise.resolve().then(() => __importStar(require('./playlist-verifier')));
116
+ const { validatePlaylist } = await import('./playlist-verifier.js');
150
117
  const validateResult = await validatePlaylist(playlist);
151
118
  if (!validateResult.valid) {
152
119
  console.log(chalk_1.default.red('Playlist validation failed'));
@@ -1,40 +1,8 @@
1
+ "use strict";
1
2
  /**
2
3
  * Playlist Signing Utility.
3
4
  * Uses the DP-1 v1.1.0 signing contract via the `dp1-js` package.
4
5
  */
5
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- var desc = Object.getOwnPropertyDescriptor(m, k);
8
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
- desc = { enumerable: true, get: function() { return m[k]; } };
10
- }
11
- Object.defineProperty(o, k2, desc);
12
- }) : (function(o, m, k, k2) {
13
- if (k2 === undefined) k2 = k;
14
- o[k2] = m[k];
15
- }));
16
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
- Object.defineProperty(o, "default", { enumerable: true, value: v });
18
- }) : function(o, v) {
19
- o["default"] = v;
20
- });
21
- var __importStar = (this && this.__importStar) || (function () {
22
- var ownKeys = function(o) {
23
- ownKeys = Object.getOwnPropertyNames || function (o) {
24
- var ar = [];
25
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
- return ar;
27
- };
28
- return ownKeys(o);
29
- };
30
- return function (mod) {
31
- if (mod && mod.__esModule) return mod;
32
- var result = {};
33
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
- __setModuleDefault(result, mod);
35
- return result;
36
- };
37
- })();
38
6
  const { getPlaylistConfig } = require('../config');
39
7
  const { isDp1PlaylistSigningRole } = require('./playlist-signing-role');
40
8
  const { parsePlaylistPrivateKeyToKeyObject } = require('./ed25519-key-derive');
@@ -261,7 +229,7 @@ async function verifySignedPlaylistEnvelope(signedPlaylist, dp1) {
261
229
  * instead of leaving a runtime require that can't resolve.
262
230
  */
263
231
  async function loadDp1() {
264
- return Promise.resolve().then(() => __importStar(require('dp1-js')));
232
+ return import('dp1-js');
265
233
  }
266
234
  function currentTimestamp() {
267
235
  return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  /**
2
3
  * Supported DP-1 playlist signing roles.
3
4
  *
@@ -4,39 +4,6 @@
4
4
  * Delegates signature-shape handling to dp1-js and keeps the CLI as a thin
5
5
  * orchestration layer.
6
6
  */
7
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
- if (k2 === undefined) k2 = k;
9
- var desc = Object.getOwnPropertyDescriptor(m, k);
10
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
- desc = { enumerable: true, get: function() { return m[k]; } };
12
- }
13
- Object.defineProperty(o, k2, desc);
14
- }) : (function(o, m, k, k2) {
15
- if (k2 === undefined) k2 = k;
16
- o[k2] = m[k];
17
- }));
18
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
- Object.defineProperty(o, "default", { enumerable: true, value: v });
20
- }) : function(o, v) {
21
- o["default"] = v;
22
- });
23
- var __importStar = (this && this.__importStar) || (function () {
24
- var ownKeys = function(o) {
25
- ownKeys = Object.getOwnPropertyNames || function (o) {
26
- var ar = [];
27
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
- return ar;
29
- };
30
- return ownKeys(o);
31
- };
32
- return function (mod) {
33
- if (mod && mod.__esModule) return mod;
34
- var result = {};
35
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
- __setModuleDefault(result, mod);
37
- return result;
38
- };
39
- })();
40
7
  var __importDefault = (this && this.__importDefault) || function (mod) {
41
8
  return (mod && mod.__esModule) ? mod : { "default": mod };
42
9
  };
@@ -84,10 +51,10 @@ async function verifyPlaylist(playlist, publicKey) {
84
51
  let key = publicKey?.trim() || undefined;
85
52
  if (!key) {
86
53
  try {
87
- const { getPlaylistConfig } = await Promise.resolve().then(() => __importStar(require('../config')));
54
+ const { getPlaylistConfig } = await import('../config.js');
88
55
  const privateKeyMaterial = getPlaylistConfig().privateKey;
89
56
  if (privateKeyMaterial) {
90
- const { deriveEd25519PublicKeyForVerify } = await Promise.resolve().then(() => __importStar(require('./ed25519-key-derive')));
57
+ const { deriveEd25519PublicKeyForVerify } = await import('./ed25519-key-derive.js');
91
58
  key = deriveEd25519PublicKeyForVerify(privateKeyMaterial);
92
59
  }
93
60
  }
@@ -98,7 +65,7 @@ async function verifyPlaylist(playlist, publicKey) {
98
65
  }
99
66
  if (key) {
100
67
  try {
101
- const { normalizeVerifyPublicKeyToPem } = await Promise.resolve().then(() => __importStar(require('./ed25519-key-derive')));
68
+ const { normalizeVerifyPublicKeyToPem } = await import('./ed25519-key-derive.js');
102
69
  key = normalizeVerifyPublicKeyToPem(key);
103
70
  }
104
71
  catch (err) {
@@ -162,7 +129,7 @@ async function parseDp1Playlist(playlist) {
162
129
  * stays lazy, so dp1-js's transitive deps don't load on unrelated commands.
163
130
  */
164
131
  async function loadDp1() {
165
- return (await Promise.resolve().then(() => __importStar(require('dp1-js'))));
132
+ return (await import('dp1-js'));
166
133
  }
167
134
  /**
168
135
  * Verify playlist file
@@ -63,7 +63,7 @@ Used for signing DP‑1 playlists.
63
63
  `PLAYLIST_PRIVATE_KEY` even when no config file exists yet, and `ff-cli status --key <privateKey>`
64
64
  reports it for an explicit key — the one `sign --key` would use.
65
65
 
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`).
66
+ - `playlist.role` (string): DP-1 signing role used by `ff-cli sign`. The shipped `config.json.example` sets `curator`, because a document signed as `agent` is ownerless to a role-aware feed and `ff-cli publish` refuses it. The code default when the key is omitted entirely is still `agent`. 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`). It does **not** apply to `find` and `build`, which always sign as `curator` — see [Signing role and ownership](#signing-role-and-ownership).
67
67
 
68
68
  ### Generate an Ed25519 private key
69
69
 
@@ -88,6 +88,33 @@ Paste the value into `playlist.privateKey`. Any of these are accepted:
88
88
 
89
89
  If you need a different role, set `playlist.role` to one of the DP-1 signing roles such as `agent`, `feed`, `curator`, `institution`, or `licensor`. The CLI rejects any other string before it reaches `dp1-js`.
90
90
 
91
+ ### Signing role and ownership
92
+
93
+ A feed decides who owns a playlist from the document itself: the keys in `curators[]` are the owner set.
94
+ Under **role-aware ownership**, a key counts as an owner only when that same key *also signed in the
95
+ `curator` role* — being named is a claim, signing as `curator` is the proof.
96
+
97
+ `ff-cli publish` requires that role. **This is ff-cli's check, not every feed's answer today**: a feed that
98
+ does not yet enforce roles accepts an `agent`-signed document, so a manual upload of the same file can
99
+ still succeed. It is refused here because once accepted, such a document can never be replaced or deleted —
100
+ both need an owner signature it does not carry, and the only way to add one is to replace it. Publishing is
101
+ what makes that permanent.
102
+
103
+ The `kid`-to-`curators[]` match is different: feeds enforce that today, and a playlist failing it is
104
+ rejected by the server regardless of ff-cli.
105
+
106
+ Two consequences:
107
+
108
+ - `find` and `build` declare your key in `curators[]` and therefore always sign as `curator`, whatever
109
+ `playlist.role` says. To build under a different role, build without a configured signing key and sign the
110
+ result yourself with `ff-cli sign --role`.
111
+ - `ff-cli publish` refuses a playlist whose declared key signed under another role, and names the fix
112
+ (`ff-cli sign <file> -r curator`, from any key declared in `curators[]`). This runs before any request,
113
+ so it costs nothing — and deliberately does not defer to a feed that would accept the document today,
114
+ because acceptance is what makes it unfixable.
115
+
116
+ Channels use `publisher` as the owner role instead of `curator`; ff-cli does not publish channels.
117
+
91
118
  If you already have a base64 key and want hex, convert it:
92
119
 
93
120
  ```bash
@@ -240,7 +267,7 @@ Minimal `config.json` example (selected fields):
240
267
  "defaultDuration": 10,
241
268
  "playlist": {
242
269
  "privateKey": "your_ed25519_private_key_hex_or_base64_here",
243
- "role": "agent"
270
+ "role": "curator"
244
271
  },
245
272
  "feed": {
246
273
  "baseURLs": ["https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1"]
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
@@ -390,7 +402,17 @@ Publish failed
390
402
  Playlist verification failed: Playlist signature verification failed
391
403
  ```
392
404
 
393
- Run `ff-cli sign <file>` before publishing. (Posting the same document straight to the feed answers
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
394
416
  `{"error":"unauthorized","message":"missing authentication: request body must carry signatures"}`.)
395
417
 
396
418
  **Signed, but the signer is not declared as a curator:**
@@ -402,8 +424,12 @@ Publish failed
402
424
  The feed accepts a publish when a signature's kid appears in the playlist's own curators[].
403
425
  Add this to the playlist before signing:
404
426
  "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.
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.
407
433
  ```
408
434
 
409
435
  This is the most common publish failure, and it is not about credentials. The feed accepts a create when
@@ -425,8 +451,8 @@ ff-cli status # → Signing identity (did:key) did:key:z6Mk...
425
451
  # "curators": [{ "name": "Your Name", "key": "did:key:z6Mk..." }]
426
452
  # DP-1 requires `name` alongside `key`.
427
453
 
428
- # 3. Sign once, then publish.
429
- ff-cli sign playlist.json
454
+ # 3. Sign once as curator, then publish. The role must match the claim in curators[].
455
+ ff-cli sign playlist.json -r curator
430
456
  ff-cli publish playlist.json
431
457
  ```
432
458
 
@@ -436,6 +462,63 @@ rather than re-signing: remove `signatures`, declare the curator, then sign.
436
462
  `ff-cli publish` checks this before uploading, so a missing declaration fails immediately with the key to
437
463
  add rather than as a server error. No API key is involved: the feed does not accept one.
438
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.
489
+ ```
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
+
439
522
  ## Complete Flow (build → validate → sign → play → publish)
440
523
 
441
524
  ```bash
@@ -448,7 +531,9 @@ npm run dev -- validate playlist.json
448
531
  # 3. Sign it. Declare curators[] BEFORE this step: the signature covers it, and the feed only
449
532
  # accepts a publish when a signature's kid matches a declared curator key.
450
533
  # Run `ff-cli status` for the kid (add -k <key> if you sign with `sign --key`).
451
- npm run dev -- sign playlist.json -o signed.json
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
452
537
 
453
538
  # 4. Play it on a device
454
539
  npm run dev -- play signed.json -d "Living Room Display"
@@ -159,6 +159,16 @@ Today the CLI groups into these workflow areas:
159
159
  - `find`
160
160
  - `build`
161
161
 
162
+ An item's `provenance.contract` is read from the indexer row, not inferred. Each
163
+ row carries the token's own `standard` (`erc721`, `erc1155`, `fa2`) and its
164
+ CAIP-2 `chain` (`eip155:1`, `tezos:mainnet`), and both are written through. The
165
+ client's own detection sees a chain name and an address and nothing else, so it
166
+ answers `erc721` for every EVM contract; it applies only to rows that carry no
167
+ standard, and to the CID a lookup by coordinate must build before the indexer
168
+ has been asked anything. A row naming a chain DP-1 has no word for is recorded
169
+ as `other` — the item is still built, and the client's guess is never
170
+ substituted for a chain the row already named.
171
+
162
172
  ### DP-1 output integrity
163
173
 
164
174
  - `verify`
@@ -237,28 +247,41 @@ and nothing else. `enrich` is the repair path for those.
237
247
  changing the title would produce two documents claiming to be the same cached
238
248
  manifest. The replacement is derived from the original id and the title, so
239
249
  it is deterministic across runs.
250
+ - The token standard the playlist asserts (`provenance.contract.standard`)
251
+ rides the lookup. The indexer keys ERC-721 and ERC-1155 tokens under
252
+ different CIDs (`eip155:1:erc721:…`, `eip155:1:erc1155:…`) and the client's
253
+ detection, having only an address, answers erc721 for every EVM contract —
254
+ so without the assertion an ERC-1155 work is queried as ERC-721 and never
255
+ found. A value outside the standards the CID can carry falls back to
256
+ detection.
240
257
  - The indexer resolving a token and having something worth attaching are
241
258
  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.
259
+ only a title, since on the items `find` and `build` create a title-only
260
+ manifest repeats what the item already says. A curator's item may carry no
261
+ title at all, and the tombstone then shows nothing, so enrichment does
262
+ synthesize a title-only manifest in that one case — the indexer's title is
263
+ substantiated, and `item.title` itself is still never written. An item that
264
+ has a title and resolves to nothing more is reported as resolved-without-
265
+ metadata, separately from tokens the indexer could not resolve at all.
245
266
  - A still the manifest builder suppressed is recovered against the item's own
246
267
  source. `resolveStillUri` blanks a thumbnail equal to the source it was
247
268
  handed, which is the source the *indexer* chose; for a static work that is
248
269
  often the still itself. When the curator kept a live HTML source instead, the
249
270
  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.
271
+ command exists to remove. The lookup therefore carries the still the indexer
272
+ names for the token (`display.image_url`, returned beside the DP-1 item by
273
+ `resolveTokenBatch`), and enrichment applies the same rule against the
274
+ curator's source: when that still differs from the item's source it becomes
275
+ the thumbnail. It is the same field `find` and `build` write into
276
+ `thumbnails.default`, taken on the indexer's word as they take it. Nothing is
277
+ inferred from the indexer's *source*: `getBestMediaUrl` prefers
278
+ `display.animation_url` and media assets over `display.image_url`, so that
279
+ source is frequently a live HTML rendition, and a file-suffix check would
280
+ reject the extensionless CDN URLs the indexer commonly returns as stills
281
+ while `display.mime_type` describes the primary rendition rather than the
282
+ still (a live work reports `text/html` beside a PNG `image_url`). A
283
+ thumbnail-only manifest is emitted where none was: it is the difference
284
+ between a tile and an empty square.
262
285
  - `evm` names a family, and enrichment refuses to guess which member. DP-1 §6
263
286
  carries no network identity, and the indexer maps Ethereum, Polygon,
264
287
  Arbitrum, Optimism, Base, and Zora all back to `evm`, so neither the request
package/docs/README.md CHANGED
@@ -193,8 +193,8 @@ image, video, or SVG source, but not from a live HTML work.
193
193
 
194
194
  ```bash
195
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
196
+ ff-cli enrich playlist.json --assume-ethereum -o labelled.json
197
+ ff-cli enrich playlist.json --assume-ethereum --force # replace existing manifests
198
198
  ```
199
199
 
200
200
  `--assume-ethereum` is needed for most playlists. DP-1 records an EVM
@@ -212,9 +212,12 @@ changes the document, so the signature is removed and the playlist must be
212
212
  re-signed before playing:
213
213
 
214
214
  ```bash
215
- ff-cli enrich playlist.json && ff-cli sign playlist.json
215
+ ff-cli enrich playlist.json --assume-ethereum && ff-cli sign playlist.json
216
216
  ```
217
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
+
218
221
  ### Validate, sign, and play
219
222
 
220
223
  ```bash
@@ -282,12 +285,30 @@ Configure feed servers in `config.json`:
282
285
  Publishing needs no credentials. The feed authorizes a create from the signatures inside the playlist:
283
286
  it requires a signature whose `kid` matches a key the document declares in `curators[]`.
284
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
+
285
295
  Declare it before signing, not after: a signature covers `curators[]`, so adding the entry to an
286
296
  already-signed playlist invalidates it, and signing again appends rather than replaces. Run
287
297
  `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.
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.
291
312
 
292
313
  ```json
293
314
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feralfile/cli",
3
- "version": "2.5.0",
3
+ "version": "2.7.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",