@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.
- package/config.json.example +1 -1
- package/dist/package.json +3 -3
- package/dist/src/commands/enrich.js +23 -4
- package/dist/src/commands/find.js +2 -35
- package/dist/src/commands/helpers/playlist-display.js +3 -36
- package/dist/src/commands/publish.js +2 -35
- package/dist/src/commands/ssh.js +1 -34
- package/dist/src/utilities/enrich-playlist.js +88 -79
- package/dist/src/utilities/feed-fetcher.js +1 -0
- package/dist/src/utilities/functions.js +4 -36
- package/dist/src/utilities/index.js +2 -34
- package/dist/src/utilities/nft-indexer.js +219 -40
- package/dist/src/utilities/playlist-builder.js +19 -1
- package/dist/src/utilities/playlist-cast.js +21 -35
- package/dist/src/utilities/playlist-publisher.js +74 -5
- package/dist/src/utilities/playlist-send.js +2 -35
- package/dist/src/utilities/playlist-signer.js +2 -34
- package/dist/src/utilities/playlist-signing-role.js +1 -0
- package/dist/src/utilities/playlist-verifier.js +4 -37
- package/docs/CONFIGURATION.md +29 -2
- package/docs/EXAMPLES.md +94 -9
- package/docs/PROJECT_SPEC.md +38 -15
- package/docs/README.md +27 -6
- package/package.json +3 -3
|
@@ -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
|
|
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
|
|
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
|
|
232
|
+
return import('dp1-js');
|
|
265
233
|
}
|
|
266
234
|
function currentTimestamp() {
|
|
267
235
|
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
@@ -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
|
|
54
|
+
const { getPlaylistConfig } = await import('../config.js');
|
|
88
55
|
const privateKeyMaterial = getPlaylistConfig().privateKey;
|
|
89
56
|
if (privateKeyMaterial) {
|
|
90
|
-
const { deriveEd25519PublicKeyForVerify } = await
|
|
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
|
|
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
|
|
132
|
+
return (await import('dp1-js'));
|
|
166
133
|
}
|
|
167
134
|
/**
|
|
168
135
|
* Verify playlist file
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -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
|
|
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": "
|
|
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
|
|
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
|
-
|
|
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
|
|
406
|
-
|
|
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
|
-
|
|
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"
|
package/docs/PROJECT_SPEC.md
CHANGED
|
@@ -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
|
|
243
|
-
|
|
244
|
-
|
|
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.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
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
|
|
289
|
-
|
|
290
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
71
|
+
"viem": "^2.56.3"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@types/node": "^24.7.1",
|