@feralfile/cli 1.1.4 → 1.3.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.
Files changed (36) hide show
  1. package/README.md +50 -0
  2. package/dist/index.js +4 -2
  3. package/dist/package.json +84 -0
  4. package/dist/src/ai-orchestrator/index.js +14 -2
  5. package/dist/src/commands/build.js +3 -3
  6. package/dist/src/commands/chat.js +3 -3
  7. package/dist/src/commands/config.js +2 -2
  8. package/dist/src/commands/device.js +6 -6
  9. package/dist/src/commands/find.js +502 -0
  10. package/dist/src/commands/helpers/device-discovery.js +1 -1
  11. package/dist/src/commands/play.js +52 -11
  12. package/dist/src/commands/setup.js +1 -1
  13. package/dist/src/commands/status.js +2 -2
  14. package/dist/src/intent-parser/index.js +16 -5
  15. package/dist/src/intent-parser/utils.js +7 -2
  16. package/dist/src/main.js +61 -17
  17. package/dist/src/utilities/ab-marketplace.js +95 -0
  18. package/dist/src/utilities/feral-file-artwork.js +89 -0
  19. package/dist/src/utilities/ff-marketplace.js +115 -0
  20. package/dist/src/utilities/ff1-device.js +4 -4
  21. package/dist/src/utilities/functions.js +1 -1
  22. package/dist/src/utilities/fxhash-marketplace.js +146 -0
  23. package/dist/src/utilities/index.js +80 -1
  24. package/dist/src/utilities/marketplace-url.js +373 -0
  25. package/dist/src/utilities/neort-marketplace.js +102 -0
  26. package/dist/src/utilities/nft-indexer.js +22 -14
  27. package/dist/src/utilities/objkt-marketplace.js +77 -0
  28. package/dist/src/utilities/playlist-publisher.js +9 -42
  29. package/dist/src/utilities/raster-client.js +252 -0
  30. package/dist/src/utilities/user-agent.js +20 -0
  31. package/docs/CONFIGURATION.md +3 -1
  32. package/docs/EXAMPLES.md +23 -0
  33. package/docs/FUNCTION_CALLING.md +3 -1
  34. package/docs/README.md +11 -10
  35. package/docs/RELEASING.md +92 -22
  36. 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
- logger.error('[NFT Indexer] HTTP error response:', {
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.error('[NFT Indexer] GraphQL errors:', result.errors);
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.error('[NFT Indexer] Failed to query tokens:', error.message);
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.error('[NFT Indexer] Failed to query token data:', error.message);
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.error(`[NFT Indexer] Failed to trigger indexing:`, indexResult.error);
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.error('[NFT Indexer] Job polling failed:', pollResult.error);
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.error(`[NFT Indexer] Error fetching token:`, error.message);
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.error(`[NFT Indexer] Token ${idx + 1} in batch failed:`, error.message);
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.error('[NFT Indexer] Async indexing error:', error.message);
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.error('[NFT Indexer] Failed to query job status:', error.message);
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
- if (normalized === 'completed') {
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 completed after ${pollCount} polls (${elapsedMs}ms)`);
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.error('[NFT Indexer] Error during job polling:', error.message);
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.error('[NFT Indexer] Error during media assets polling:', error.message);
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 validated playlist to a DP-1 feed server
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. Parse / structure validation via validatePlaylist (same as `validate`; not cryptographic verify)
48
- * 3. If valid, send the original playlist to feed server
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: Parse / structure validation (use `verify` CLI for signature checks)
84
- const { validatePlaylist } = await Promise.resolve().then(() => __importStar(require('./playlist-verifier')));
85
- const validationResult = await validatePlaylist(playlist);
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 validation failed: ${validationResult.error}`,
90
- message: validationResult.details?.map((d) => ` • ${d.path}: ${d.message}`).join('\n'),
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}`;
@@ -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