@feralfile/cli 1.1.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/index.js +4 -2
- package/dist/package.json +84 -0
- package/dist/src/ai-orchestrator/index.js +14 -2
- package/dist/src/commands/build.js +3 -3
- package/dist/src/commands/chat.js +3 -3
- package/dist/src/commands/config.js +2 -2
- package/dist/src/commands/device.js +6 -6
- package/dist/src/commands/find.js +502 -0
- package/dist/src/commands/helpers/device-discovery.js +1 -1
- package/dist/src/commands/play.js +52 -11
- package/dist/src/commands/setup.js +1 -1
- package/dist/src/commands/status.js +2 -2
- package/dist/src/intent-parser/index.js +16 -5
- package/dist/src/intent-parser/utils.js +7 -2
- package/dist/src/main.js +61 -17
- package/dist/src/utilities/ab-marketplace.js +95 -0
- package/dist/src/utilities/feral-file-artwork.js +89 -0
- package/dist/src/utilities/ff-marketplace.js +115 -0
- package/dist/src/utilities/ff1-device.js +4 -4
- package/dist/src/utilities/functions.js +1 -1
- package/dist/src/utilities/fxhash-marketplace.js +146 -0
- package/dist/src/utilities/index.js +80 -1
- package/dist/src/utilities/marketplace-url.js +373 -0
- package/dist/src/utilities/neort-marketplace.js +102 -0
- package/dist/src/utilities/nft-indexer.js +22 -14
- package/dist/src/utilities/objkt-marketplace.js +77 -0
- package/dist/src/utilities/playlist-publisher.js +9 -42
- package/dist/src/utilities/raster-client.js +252 -0
- package/dist/src/utilities/user-agent.js +20 -0
- package/docs/CONFIGURATION.md +3 -1
- package/docs/EXAMPLES.md +23 -0
- package/docs/FUNCTION_CALLING.md +3 -1
- package/docs/README.md +11 -10
- package/docs/RELEASING.md +92 -22
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Neort (neort.io) resolver.
|
|
4
|
+
*
|
|
5
|
+
* Neort is an off-chain platform for code-based / generative art. Pieces are
|
|
6
|
+
* referenced by an opaque id (e.g. `ce3lvgkn70rlpj69ccc0`) and do NOT have
|
|
7
|
+
* on-chain coordinates, so the find flow bypasses Raster + FF indexer for
|
|
8
|
+
* Neort items and builds the DP-1 entry directly from Neort's API response
|
|
9
|
+
* using the off-chain `provenance.offChainURI` shape.
|
|
10
|
+
*
|
|
11
|
+
* Asset / thumbnail URLs are constructed by stitching CloudFront paths with
|
|
12
|
+
* fields from the API response (per Ryota / Neort, 2026-05-21):
|
|
13
|
+
* - artwork: https://d32h66pp7fue57.cloudfront.net/artPreview/{resourceFileName}
|
|
14
|
+
* - thumbnail: https://d32h66pp7fue57.cloudfront.net/artThumb/{thumbFileName-or-selectedThumbFileName}
|
|
15
|
+
*
|
|
16
|
+
* Field nuances observed across real responses:
|
|
17
|
+
* - `thumbFileName` and `selectedThumbFileName` are mutually exclusive in
|
|
18
|
+
* practice; one is populated, the other empty. Prefer
|
|
19
|
+
* `selectedThumbFileName` (newer field) and fall back to `thumbFileName`.
|
|
20
|
+
* - `resourceFileName` is empty for static-image pieces (`resourceType: 3`),
|
|
21
|
+
* where the thumbnail IS the artwork — fall back to the thumb URL when
|
|
22
|
+
* resourceFileName is missing so static-image pieces still build.
|
|
23
|
+
* - Unknown ids return HTTP 200 with `id: ""` (an empty stub), not a 404 —
|
|
24
|
+
* reject when `id` is empty.
|
|
25
|
+
*/
|
|
26
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
29
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
30
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
31
|
+
}
|
|
32
|
+
Object.defineProperty(o, k2, desc);
|
|
33
|
+
}) : (function(o, m, k, k2) {
|
|
34
|
+
if (k2 === undefined) k2 = k;
|
|
35
|
+
o[k2] = m[k];
|
|
36
|
+
}));
|
|
37
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
38
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
39
|
+
}) : function(o, v) {
|
|
40
|
+
o["default"] = v;
|
|
41
|
+
});
|
|
42
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
43
|
+
var ownKeys = function(o) {
|
|
44
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
45
|
+
var ar = [];
|
|
46
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
47
|
+
return ar;
|
|
48
|
+
};
|
|
49
|
+
return ownKeys(o);
|
|
50
|
+
};
|
|
51
|
+
return function (mod) {
|
|
52
|
+
if (mod && mod.__esModule) return mod;
|
|
53
|
+
var result = {};
|
|
54
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
55
|
+
__setModuleDefault(result, mod);
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
})();
|
|
59
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
exports.resolveNeortArt = resolveNeortArt;
|
|
61
|
+
const logger = __importStar(require("../logger"));
|
|
62
|
+
const user_agent_1 = require("./user-agent");
|
|
63
|
+
const NEORT_API_BASE = 'https://api.neort.io/v1';
|
|
64
|
+
const NEORT_CDN_BASE = 'https://d32h66pp7fue57.cloudfront.net';
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a Neort art id to a playable asset URL plus title/artist metadata.
|
|
67
|
+
*
|
|
68
|
+
* @throws When the id is unknown, the art is private, or no playable asset
|
|
69
|
+
* can be constructed from the response.
|
|
70
|
+
*/
|
|
71
|
+
async function resolveNeortArt(id) {
|
|
72
|
+
logger.debug(`[neort] Resolving art id "${id}"`);
|
|
73
|
+
const response = await fetch(`${NEORT_API_BASE}/art/${encodeURIComponent(id)}`, {
|
|
74
|
+
headers: { 'User-Agent': user_agent_1.USER_AGENT, Accept: 'application/json' },
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new Error(`Neort API ${response.status} ${response.statusText} for art id "${id}".`);
|
|
78
|
+
}
|
|
79
|
+
const data = (await response.json());
|
|
80
|
+
if (!data.id) {
|
|
81
|
+
throw new Error(`Neort: no art found for id "${id}". Check the URL — ids come from neort.io/art/{id}.`);
|
|
82
|
+
}
|
|
83
|
+
if (!data.isPublic) {
|
|
84
|
+
throw new Error(`Neort: art "${id}" (${data.title || 'untitled'}) is not public.`);
|
|
85
|
+
}
|
|
86
|
+
const thumbFile = data.selectedThumbFileName || data.thumbFileName;
|
|
87
|
+
const thumbnailUrl = thumbFile ? `${NEORT_CDN_BASE}/artThumb/${thumbFile}` : null;
|
|
88
|
+
const assetUrl = data.resourceFileName
|
|
89
|
+
? `${NEORT_CDN_BASE}/artPreview/${data.resourceFileName}`
|
|
90
|
+
: thumbnailUrl;
|
|
91
|
+
if (!assetUrl) {
|
|
92
|
+
throw new Error(`Neort: art "${id}" has no playable asset (resourceFileName and thumbnail are both empty).`);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
id: data.id,
|
|
96
|
+
title: data.title || `Neort ${data.id}`,
|
|
97
|
+
description: data.description || '',
|
|
98
|
+
artistName: data.user?.name ?? '',
|
|
99
|
+
assetUrl,
|
|
100
|
+
thumbnailUrl,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -180,7 +180,10 @@ async function queryTokens(params = {}) {
|
|
|
180
180
|
});
|
|
181
181
|
if (!response.ok) {
|
|
182
182
|
const errorBody = await response.text();
|
|
183
|
-
|
|
183
|
+
// Inside queryTokens — caller (queryTokenDataFromIndexer) falls back
|
|
184
|
+
// to the trigger-indexing path when this returns/throws. Demoting to
|
|
185
|
+
// debug so transient 502s don't flood stderr during recoverable runs.
|
|
186
|
+
logger.debug('[NFT Indexer] HTTP error response:', {
|
|
184
187
|
status: response.status,
|
|
185
188
|
body: errorBody.substring(0, 500),
|
|
186
189
|
});
|
|
@@ -188,7 +191,7 @@ async function queryTokens(params = {}) {
|
|
|
188
191
|
}
|
|
189
192
|
const result = await response.json();
|
|
190
193
|
if (result.errors) {
|
|
191
|
-
logger.
|
|
194
|
+
logger.debug('[NFT Indexer] GraphQL errors:', result.errors);
|
|
192
195
|
throw new Error(`GraphQL errors: ${result.errors.map((e) => e.message).join(', ')}`);
|
|
193
196
|
}
|
|
194
197
|
// v2 API wraps tokens in { items: [...], total: N }
|
|
@@ -197,7 +200,7 @@ async function queryTokens(params = {}) {
|
|
|
197
200
|
return tokens;
|
|
198
201
|
}
|
|
199
202
|
catch (error) {
|
|
200
|
-
logger.
|
|
203
|
+
logger.debug('[NFT Indexer] Failed to query tokens:', error.message);
|
|
201
204
|
throw error;
|
|
202
205
|
}
|
|
203
206
|
}
|
|
@@ -215,7 +218,7 @@ async function queryTokenDataFromIndexer(tokenCID) {
|
|
|
215
218
|
return tokens[0] || null;
|
|
216
219
|
}
|
|
217
220
|
catch (error) {
|
|
218
|
-
logger.
|
|
221
|
+
logger.debug('[NFT Indexer] Failed to query token data:', error.message);
|
|
219
222
|
return null;
|
|
220
223
|
}
|
|
221
224
|
}
|
|
@@ -520,7 +523,7 @@ async function getNFTTokenInfoSingle(params, duration = 10, options = {}) {
|
|
|
520
523
|
// Trigger background indexing workflow
|
|
521
524
|
const indexResult = await triggerIndexingAsync(chain, contractAddress, tokenId);
|
|
522
525
|
if (!indexResult.success) {
|
|
523
|
-
logger.
|
|
526
|
+
logger.debug(`[NFT Indexer] Failed to trigger indexing:`, indexResult.error);
|
|
524
527
|
return {
|
|
525
528
|
success: false,
|
|
526
529
|
error: `Token not found and indexing failed: ${indexResult.error}`,
|
|
@@ -532,7 +535,7 @@ async function getNFTTokenInfoSingle(params, duration = 10, options = {}) {
|
|
|
532
535
|
// Poll for job completion (queue-backed jobs use job_id / jobStatus)
|
|
533
536
|
const pollResult = await pollForJobCompletion(indexResult.job_id, options.jobPoll ?? {});
|
|
534
537
|
if (!pollResult.success) {
|
|
535
|
-
logger.
|
|
538
|
+
logger.debug('[NFT Indexer] Job polling failed:', pollResult.error);
|
|
536
539
|
return {
|
|
537
540
|
success: false,
|
|
538
541
|
error: `Indexing job failed: ${pollResult.error}`,
|
|
@@ -575,7 +578,7 @@ async function getNFTTokenInfoSingle(params, duration = 10, options = {}) {
|
|
|
575
578
|
return convertToDP1Item(tokenData, duration);
|
|
576
579
|
}
|
|
577
580
|
catch (error) {
|
|
578
|
-
logger.
|
|
581
|
+
logger.debug(`[NFT Indexer] Error fetching token:`, error.message);
|
|
579
582
|
return {
|
|
580
583
|
success: false,
|
|
581
584
|
error: error.message,
|
|
@@ -601,7 +604,7 @@ async function getNFTTokenInfoBatch(tokens, duration = 10) {
|
|
|
601
604
|
const batch = tokens.slice(i, i + concurrency);
|
|
602
605
|
logger.info(`[NFT Indexer] Processing batch ${Math.floor(i / concurrency) + 1}/${Math.ceil(tokens.length / concurrency)} (${batch.length} tokens)`);
|
|
603
606
|
const batchResults = await Promise.all(batch.map((token, idx) => getNFTTokenInfoSingle(token, duration).catch((error) => {
|
|
604
|
-
logger.
|
|
607
|
+
logger.debug(`[NFT Indexer] Token ${idx + 1} in batch failed:`, error.message);
|
|
605
608
|
return {
|
|
606
609
|
success: false,
|
|
607
610
|
error: error.message,
|
|
@@ -713,7 +716,7 @@ async function triggerIndexingAsync(chain, contractAddress, tokenId) {
|
|
|
713
716
|
};
|
|
714
717
|
}
|
|
715
718
|
catch (error) {
|
|
716
|
-
logger.
|
|
719
|
+
logger.debug('[NFT Indexer] Async indexing error:', error.message);
|
|
717
720
|
return {
|
|
718
721
|
success: false,
|
|
719
722
|
error: error.message,
|
|
@@ -761,7 +764,7 @@ async function queryJobStatus(jobId) {
|
|
|
761
764
|
};
|
|
762
765
|
}
|
|
763
766
|
catch (error) {
|
|
764
|
-
logger.
|
|
767
|
+
logger.debug('[NFT Indexer] Failed to query job status:', error.message);
|
|
765
768
|
return {
|
|
766
769
|
success: false,
|
|
767
770
|
error: error.message,
|
|
@@ -799,9 +802,14 @@ async function pollForJobCompletion(jobId, options = {}) {
|
|
|
799
802
|
pollCount += 1;
|
|
800
803
|
logger.debug(`[NFT Indexer] Poll #${pollCount}: status = ${status}`);
|
|
801
804
|
const normalized = typeof status === 'string' ? status.toLowerCase() : '';
|
|
802
|
-
|
|
805
|
+
// Indexer terminal-success states: both `completed` (older) and
|
|
806
|
+
// `succeeded` (current pgmq convention) appear in the wild. Accepting
|
|
807
|
+
// both prevents the CLI from polling-to-timeout on freshly-triggered
|
|
808
|
+
// tokens, which was masking as "No tokens could be indexed" on the
|
|
809
|
+
// first encounter while the underlying job had already finished.
|
|
810
|
+
if (normalized === 'completed' || normalized === 'succeeded') {
|
|
803
811
|
const elapsedMs = Date.now() - startTime;
|
|
804
|
-
logger.info(`[NFT Indexer] ✓ Job
|
|
812
|
+
logger.info(`[NFT Indexer] ✓ Job ${normalized} after ${pollCount} polls (${elapsedMs}ms)`);
|
|
805
813
|
return {
|
|
806
814
|
success: true,
|
|
807
815
|
completed: true,
|
|
@@ -834,7 +842,7 @@ async function pollForJobCompletion(jobId, options = {}) {
|
|
|
834
842
|
}
|
|
835
843
|
}
|
|
836
844
|
catch (error) {
|
|
837
|
-
logger.
|
|
845
|
+
logger.debug('[NFT Indexer] Error during job polling:', error.message);
|
|
838
846
|
return {
|
|
839
847
|
success: false,
|
|
840
848
|
error: error.message,
|
|
@@ -882,7 +890,7 @@ async function pollForMediaAssets(tokenCID, options = {}) {
|
|
|
882
890
|
}
|
|
883
891
|
}
|
|
884
892
|
catch (error) {
|
|
885
|
-
logger.
|
|
893
|
+
logger.debug('[NFT Indexer] Error during media assets polling:', error.message);
|
|
886
894
|
return null;
|
|
887
895
|
}
|
|
888
896
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Objkt marketplace resolver.
|
|
4
|
+
*
|
|
5
|
+
* Objkt URLs accept either a raw KT1 contract address OR a short collection
|
|
6
|
+
* alias (e.g. `objkt.com/tokens/hicetnunc/111068` where `hicetnunc` maps to
|
|
7
|
+
* `KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton`). The alias-to-contract mapping
|
|
8
|
+
* lives in their public GraphQL `fa.path` field.
|
|
9
|
+
*
|
|
10
|
+
* The URL parser stays sync; this module owns the one async call needed
|
|
11
|
+
* when an alias segment is detected.
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.resolveObjktAlias = resolveObjktAlias;
|
|
48
|
+
const logger = __importStar(require("../logger"));
|
|
49
|
+
const user_agent_1 = require("./user-agent");
|
|
50
|
+
const OBJKT_GRAPHQL = 'https://data.objkt.com/v3/graphql';
|
|
51
|
+
/**
|
|
52
|
+
* Resolve an Objkt URL alias to its KT1 contract address.
|
|
53
|
+
*
|
|
54
|
+
* @throws When the alias is not registered on Objkt, or the GraphQL call fails.
|
|
55
|
+
*/
|
|
56
|
+
async function resolveObjktAlias(alias) {
|
|
57
|
+
logger.debug(`[Objkt] Resolving alias "${alias}"`);
|
|
58
|
+
const query = 'query ($path: String!) { fa(where: { path: { _eq: $path } }) { contract } }';
|
|
59
|
+
const response = await fetch(OBJKT_GRAPHQL, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/json', 'User-Agent': user_agent_1.USER_AGENT },
|
|
62
|
+
body: JSON.stringify({ query, variables: { path: alias } }),
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new Error(`Objkt GraphQL ${response.status} ${response.statusText}`);
|
|
66
|
+
}
|
|
67
|
+
const data = (await response.json());
|
|
68
|
+
if (data.errors && data.errors.length > 0) {
|
|
69
|
+
throw new Error(`Objkt GraphQL errors: ${data.errors.map((e) => e.message).join('; ')}`);
|
|
70
|
+
}
|
|
71
|
+
const contract = data.data?.fa?.[0]?.contract;
|
|
72
|
+
if (!contract) {
|
|
73
|
+
throw new Error(`Objkt: alias "${alias}" did not resolve to a contract. ` +
|
|
74
|
+
'If this is a real collection, paste the URL using its KT1 contract address instead.');
|
|
75
|
+
}
|
|
76
|
+
return contract;
|
|
77
|
+
}
|
|
@@ -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
|
};
|
|
@@ -39,13 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
6
|
exports.publishPlaylist = publishPlaylist;
|
|
40
7
|
const axios_1 = __importDefault(require("axios"));
|
|
41
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const playlist_verifier_1 = require("./playlist-verifier");
|
|
42
10
|
/**
|
|
43
|
-
* Publish a
|
|
11
|
+
* Publish a verified playlist to a DP-1 feed server
|
|
44
12
|
*
|
|
45
13
|
* Flow:
|
|
46
14
|
* 1. Read and parse playlist file
|
|
47
|
-
* 2.
|
|
48
|
-
* 3. If valid, send the
|
|
15
|
+
* 2. Verify the playlist signature before upload
|
|
16
|
+
* 3. If valid, send the verified playlist to feed server
|
|
49
17
|
* 4. Return result with playlist ID or error
|
|
50
18
|
*
|
|
51
19
|
* @param {string} filePath - Path to playlist JSON file
|
|
@@ -80,14 +48,13 @@ async function publishPlaylist(filePath, feedServerUrl, apiKey) {
|
|
|
80
48
|
error: `Invalid JSON in playlist file: ${filePath}`,
|
|
81
49
|
};
|
|
82
50
|
}
|
|
83
|
-
// Step 2:
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
if (!validationResult.valid) {
|
|
51
|
+
// Step 2: Verify signature integrity before publishing.
|
|
52
|
+
const deliveryResult = await (0, playlist_verifier_1.verifyPlaylist)(playlist);
|
|
53
|
+
if (!deliveryResult.valid) {
|
|
87
54
|
return {
|
|
88
55
|
success: false,
|
|
89
|
-
error: `Playlist
|
|
90
|
-
message:
|
|
56
|
+
error: `Playlist verification failed: ${deliveryResult.error}`,
|
|
57
|
+
message: deliveryResult.details?.map((d) => ` • ${d.path}: ${d.message}`).join('\n'),
|
|
91
58
|
};
|
|
92
59
|
}
|
|
93
60
|
// Step 3: Send validated playlist to feed server
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Raster API client (kit.raster.art)
|
|
4
|
+
*
|
|
5
|
+
* Resolves on-chain coordinates and artist addresses into Raster `artwork_id`
|
|
6
|
+
* (series) and `artist_id` (catalog) — the inputs the `ff-cli find` flow needs
|
|
7
|
+
* before handing tokens to the FF GraphQL indexer for DP-1 conversion.
|
|
8
|
+
*
|
|
9
|
+
* This module is pure data: it does not construct DP-1 items, send anything
|
|
10
|
+
* to devices, or build playlists. The fulfiller layer (FF indexer) consumes
|
|
11
|
+
* the `{ chain, contractAddress, tokenId }` shapes this module emits.
|
|
12
|
+
*
|
|
13
|
+
* Chain filter: the FF indexer only supports Ethereum mainnet and Tezos
|
|
14
|
+
* mainnet. Token lists are filtered here at the CAIP level and a
|
|
15
|
+
* `skippedUnsupported` count is surfaced so callers can warn-skip without
|
|
16
|
+
* walking every row themselves.
|
|
17
|
+
*/
|
|
18
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
21
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
22
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(o, k2, desc);
|
|
25
|
+
}) : (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
o[k2] = m[k];
|
|
28
|
+
}));
|
|
29
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
30
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
31
|
+
}) : function(o, v) {
|
|
32
|
+
o["default"] = v;
|
|
33
|
+
});
|
|
34
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
35
|
+
var ownKeys = function(o) {
|
|
36
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
37
|
+
var ar = [];
|
|
38
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
39
|
+
return ar;
|
|
40
|
+
};
|
|
41
|
+
return ownKeys(o);
|
|
42
|
+
};
|
|
43
|
+
return function (mod) {
|
|
44
|
+
if (mod && mod.__esModule) return mod;
|
|
45
|
+
var result = {};
|
|
46
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
47
|
+
__setModuleDefault(result, mod);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.RasterNotFoundError = void 0;
|
|
53
|
+
exports.resolveTokenToArtwork = resolveTokenToArtwork;
|
|
54
|
+
exports.getArtworkSummary = getArtworkSummary;
|
|
55
|
+
exports.resolveAddressToArtist = resolveAddressToArtist;
|
|
56
|
+
exports.listArtworkTokens = listArtworkTokens;
|
|
57
|
+
exports.listArtistArtworks = listArtistArtworks;
|
|
58
|
+
exports.formatSummaryLine = formatSummaryLine;
|
|
59
|
+
const logger = __importStar(require("../logger"));
|
|
60
|
+
const user_agent_1 = require("./user-agent");
|
|
61
|
+
const RASTER_BASE_URL = 'https://kit.raster.art';
|
|
62
|
+
// CAIP-2 chain IDs for the chains the FF indexer supports.
|
|
63
|
+
// Tezos mainnet's CAIP form uses the genesis block hash (NetXdQprcVkpaWU),
|
|
64
|
+
// not the human-readable "tezos:mainnet" — Raster returns the hash form on
|
|
65
|
+
// token rows and rejects "tezos:mainnet" with HTTP 400.
|
|
66
|
+
const CAIP_TO_INDEXER_CHAIN = {
|
|
67
|
+
'eip155:1': 'ethereum',
|
|
68
|
+
'tezos:NetXdQprcVkpaWU': 'tezos',
|
|
69
|
+
};
|
|
70
|
+
function stripContractChainPrefix(raw) {
|
|
71
|
+
const slash = raw.indexOf('/');
|
|
72
|
+
return slash >= 0 ? raw.slice(slash + 1) : raw;
|
|
73
|
+
}
|
|
74
|
+
function caipToIndexerChain(caip) {
|
|
75
|
+
return CAIP_TO_INDEXER_CHAIN[caip] ?? null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Thrown when Raster returns 404 for a requested path. Used as a soft signal
|
|
79
|
+
* by callers that have a fallback path (e.g. `find` falls back to a
|
|
80
|
+
* single-token playlist when Raster doesn't index the artwork).
|
|
81
|
+
*/
|
|
82
|
+
class RasterNotFoundError extends Error {
|
|
83
|
+
notFound = true;
|
|
84
|
+
constructor(path) {
|
|
85
|
+
super(`Raster: not found (${path})`);
|
|
86
|
+
this.name = 'RasterNotFoundError';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.RasterNotFoundError = RasterNotFoundError;
|
|
90
|
+
async function rasterFetch(path) {
|
|
91
|
+
const url = `${RASTER_BASE_URL}${path}`;
|
|
92
|
+
logger.debug(`[Raster] GET ${url}`);
|
|
93
|
+
const response = await fetch(url, {
|
|
94
|
+
headers: { 'User-Agent': user_agent_1.USER_AGENT, Accept: 'application/json' },
|
|
95
|
+
});
|
|
96
|
+
if (response.status === 404) {
|
|
97
|
+
throw new RasterNotFoundError(path);
|
|
98
|
+
}
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const body = await response.text();
|
|
101
|
+
throw new Error(`Raster API ${response.status} ${response.statusText} for ${path}: ${body.slice(0, 200)}`);
|
|
102
|
+
}
|
|
103
|
+
return (await response.json());
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolve a token's on-chain coordinates to its Raster artwork (series) id.
|
|
107
|
+
*
|
|
108
|
+
* Accepts either a CAIP chain id (`eip155:1`) or human slug (`ethereum`) —
|
|
109
|
+
* Raster handles both. Returns `null` when Raster doesn't index this token
|
|
110
|
+
* (HTTP 404); callers can fall back to a single-token playlist in that case.
|
|
111
|
+
* Other Raster errors throw normally.
|
|
112
|
+
*/
|
|
113
|
+
async function resolveTokenToArtwork(chain, contract, tokenId) {
|
|
114
|
+
const path = `/token/${encodeURIComponent(chain)}` +
|
|
115
|
+
`/${encodeURIComponent(contract)}` +
|
|
116
|
+
`/${encodeURIComponent(tokenId)}`;
|
|
117
|
+
try {
|
|
118
|
+
const data = await rasterFetch(path);
|
|
119
|
+
return {
|
|
120
|
+
artworkId: data.artwork.id,
|
|
121
|
+
artworkTitle: data.artwork.title,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error instanceof RasterNotFoundError) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get the full Raster artwork summary suitable for the one-line resolve
|
|
133
|
+
* message ("Snowfro — send/receive — 8977 tokens") and downstream UX.
|
|
134
|
+
*/
|
|
135
|
+
async function getArtworkSummary(artworkId) {
|
|
136
|
+
const data = await rasterFetch(`/artwork/${artworkId}`);
|
|
137
|
+
return {
|
|
138
|
+
artworkId: data.id,
|
|
139
|
+
title: data.title,
|
|
140
|
+
artists: data.artists ?? [],
|
|
141
|
+
tokenCount: data.token_count,
|
|
142
|
+
editionSize: data.edition_size,
|
|
143
|
+
isEdition: data.is_edition,
|
|
144
|
+
hasOneOfOneTokens: data.has_one_of_one_tokens,
|
|
145
|
+
platforms: data.platforms ?? [],
|
|
146
|
+
contractAddresses: data.contract_addresses ?? [],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Resolve an artist's wallet address to a Raster artist id via `/search`.
|
|
151
|
+
*
|
|
152
|
+
* `/artist/{id}` does not accept raw addresses (404), so we bridge through
|
|
153
|
+
* the multi-search endpoint and pick the artist hit whose `addresses` list
|
|
154
|
+
* contains the input. Returns `null` when no artist matches.
|
|
155
|
+
*/
|
|
156
|
+
async function resolveAddressToArtist(address) {
|
|
157
|
+
const normalized = address.trim().toLowerCase();
|
|
158
|
+
const data = await rasterFetch(`/search?query=${encodeURIComponent(address)}`);
|
|
159
|
+
for (const result of data.results ?? []) {
|
|
160
|
+
for (const hit of result.hits ?? []) {
|
|
161
|
+
const doc = hit.document;
|
|
162
|
+
if (doc.data_type !== 'artist') {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const addrs = (doc.addresses ?? []).map((a) => a.toLowerCase());
|
|
166
|
+
if (!addrs.includes(normalized)) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const parsed = Number.parseInt(doc.data_pk, 10);
|
|
170
|
+
if (Number.isFinite(parsed)) {
|
|
171
|
+
return parsed;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* List tokens in a Raster artwork (series), one page at a time.
|
|
179
|
+
*
|
|
180
|
+
* Filters out tokens whose chain is not supported by the FF indexer
|
|
181
|
+
* (anything other than `eip155:1` or `tezos:NetXdQprcVkpaWU`) and reports
|
|
182
|
+
* the count via `skippedUnsupported` so the caller can print a single
|
|
183
|
+
* warn-skip line instead of one per token.
|
|
184
|
+
*
|
|
185
|
+
* Pagination: pass the returned `nextCursor` back as `options.cursor`.
|
|
186
|
+
* `nextCursor` is `null` on the final page. Raster signals end-of-stream by
|
|
187
|
+
* returning `cursor === total_count`; that is normalized to `null` here.
|
|
188
|
+
*/
|
|
189
|
+
async function listArtworkTokens(artworkId, options = {}) {
|
|
190
|
+
const params = new URLSearchParams();
|
|
191
|
+
if (options.cursor !== undefined) {
|
|
192
|
+
params.set('cursor', String(options.cursor));
|
|
193
|
+
}
|
|
194
|
+
if (options.pageSize !== undefined) {
|
|
195
|
+
params.set('page_size', String(options.pageSize));
|
|
196
|
+
}
|
|
197
|
+
const qs = params.toString();
|
|
198
|
+
const path = `/artwork/${artworkId}/tokens${qs ? `?${qs}` : ''}`;
|
|
199
|
+
const data = await rasterFetch(path);
|
|
200
|
+
const tokens = [];
|
|
201
|
+
let skippedUnsupported = 0;
|
|
202
|
+
for (const raw of data.tokens ?? []) {
|
|
203
|
+
const chain = caipToIndexerChain(raw.chain_id);
|
|
204
|
+
if (!chain) {
|
|
205
|
+
skippedUnsupported += 1;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
tokens.push({
|
|
209
|
+
caipChain: raw.chain_id,
|
|
210
|
+
chain,
|
|
211
|
+
contractAddress: stripContractChainPrefix(raw.contract_address),
|
|
212
|
+
tokenId: raw.token_id,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const rawCursor = data.cursor;
|
|
216
|
+
const nextCursor = rawCursor === null || rawCursor >= data.total_count ? null : rawCursor;
|
|
217
|
+
return {
|
|
218
|
+
tokens,
|
|
219
|
+
totalCount: data.total_count,
|
|
220
|
+
nextCursor,
|
|
221
|
+
skippedUnsupported,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* List an artist's full catalog as light artwork rows (id, title, token count,
|
|
226
|
+
* edition flag, mint date). Use {@link getArtworkSummary} when full details
|
|
227
|
+
* are needed for a specific row.
|
|
228
|
+
*
|
|
229
|
+
* The endpoint returns the full catalog in a single response; no cursor is
|
|
230
|
+
* exposed by Raster for this path.
|
|
231
|
+
*/
|
|
232
|
+
async function listArtistArtworks(artistId) {
|
|
233
|
+
const data = await rasterFetch(`/artist/${artistId}/artworks`);
|
|
234
|
+
return (data.artworks ?? []).map((row) => ({
|
|
235
|
+
artworkId: row.id,
|
|
236
|
+
title: row.title,
|
|
237
|
+
tokenCount: row.num_tokens ?? row.num_pieces ?? 0,
|
|
238
|
+
isEdition: row.is_edition,
|
|
239
|
+
mintDate: row.mint_date ?? null,
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Format a one-line summary for the resolved artwork.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* formatSummaryLine(summary) // "Snowfro — send/receive — 8977 tokens"
|
|
247
|
+
*/
|
|
248
|
+
function formatSummaryLine(summary) {
|
|
249
|
+
const artistNames = summary.artists.map((a) => a.name).join(', ') || 'Unknown artist';
|
|
250
|
+
const tokenWord = summary.tokenCount === 1 ? 'token' : 'tokens';
|
|
251
|
+
return `${artistNames} — ${summary.title} — ${summary.tokenCount} ${tokenWord}`;
|
|
252
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Outbound HTTP User-Agent for ff-cli.
|
|
4
|
+
*
|
|
5
|
+
* Set on every fetch to third-party APIs (Raster, Objkt, fxhash, AB, Feral
|
|
6
|
+
* File, Neort) so partners can distinguish CLI traffic from their own
|
|
7
|
+
* frontend/backend traffic in logs. Without it, all Node-side fetches blend
|
|
8
|
+
* into a generic default UA and partners can't attribute load or behavior
|
|
9
|
+
* back to the CLI when something looks unusual.
|
|
10
|
+
*
|
|
11
|
+
* Format: `ff-cli/<version>` — version is read from package.json at compile
|
|
12
|
+
* time via the bundler's JSON import support (`resolveJsonModule`).
|
|
13
|
+
*/
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.USER_AGENT = void 0;
|
|
19
|
+
const package_json_1 = __importDefault(require("../../package.json"));
|
|
20
|
+
exports.USER_AGENT = `ff-cli/${package_json_1.default.version}`;
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -60,10 +60,12 @@ Optional settings used where headless/browser‑like behavior is needed.
|
|
|
60
60
|
|
|
61
61
|
Used for signing DP‑1 playlists.
|
|
62
62
|
|
|
63
|
-
- `playlist.privateKey` (string, Ed25519 private key in hex or base64): Used by the `sign` command to create DP-1 v1.1.0 multi-signatures. The `verify` command may derive the matching public key from this value (or `PLAYLIST_PRIVATE_KEY`) when you omit `--public-key`; **dp1-js applies that derived key only when verifying legacy flat `signature` strings**, not when checking `signatures[]` envelopes. If that derivation fails, `verify` prints a warning on stderr and continues without derived key material. The derived public key is emitted as PEM so Node can decode it without ambiguity. Hex may include or omit the `0x` prefix. You can also set this via `PLAYLIST_PRIVATE_KEY` in `.env`.
|
|
63
|
+
- `playlist.privateKey` (string, Ed25519 private key in hex or base64): Used by the `sign` command to create DP-1 v1.1.0 multi-signatures. The `verify` command may derive the matching public key from this value (or `PLAYLIST_PRIVATE_KEY`) when you omit `--public-key`; **dp1-js applies that derived key only when verifying legacy flat `signature` strings**, not when checking `signatures[]` envelopes. If that derivation fails, `verify` prints a warning on stderr and continues without derived key material. The derived public key is emitted as PEM so Node can decode it without ambiguity. Hex may include or omit the `0x` prefix. You can also set this via `PLAYLIST_PRIVATE_KEY` in `.env`. `play` verifies playlists before delivery and only auto-signs the synthesized media URL fallback when signing is configured. `send` and `publish` verify before delivery or upload and reject unsigned or broken playlists.
|
|
64
64
|
|
|
65
65
|
**Signing and key encoding:** Signing paths (`sign`, deterministic `build` when configured, and `-k/--key` overrides) pass the private key string through to **`dp1-js`** (`SignMultiEd25519`) without an extra decoding step in ff-cli. `dp1-js` recognizes **hex** (optional `0x`) or **base64** encodings of the PKCS#8 DER blob produced by the OpenSSL examples below, then loads the key for Ed25519. Use those formats; ff-cli does not add a separate normalizer ahead of the library.
|
|
66
|
+
|
|
66
67
|
- `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`).
|
|
68
|
+
|
|
67
69
|
### Generate an Ed25519 private key
|
|
68
70
|
|
|
69
71
|
You can generate a key locally. The CLI accepts either base64 (preferred) or hex
|