@feralfile/cli 2.2.1 → 2.2.2
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/dist/index.js +8 -0
- package/dist/package.json +1 -1
- package/dist/src/commands/find.js +45 -5
- package/dist/src/utilities/ab-marketplace.js +2 -1
- package/dist/src/utilities/feed-fetcher.js +4 -3
- package/dist/src/utilities/feral-file-artwork.js +2 -1
- package/dist/src/utilities/ff-marketplace.js +2 -1
- package/dist/src/utilities/ff1-compatibility.js +4 -1
- package/dist/src/utilities/ff1-device.js +4 -1
- package/dist/src/utilities/ff1-relayer.js +3 -1
- package/dist/src/utilities/fxhash-marketplace.js +3 -2
- package/dist/src/utilities/handoff-client.js +3 -1
- package/dist/src/utilities/http.js +40 -0
- package/dist/src/utilities/neort-marketplace.js +2 -1
- package/dist/src/utilities/nft-indexer.js +18 -4
- package/dist/src/utilities/objkt-marketplace.js +2 -1
- package/dist/src/utilities/opensea-marketplace.js +2 -1
- package/dist/src/utilities/playlist-source.js +2 -1
- package/dist/src/utilities/raster-client.js +59 -7
- package/dist/src/utilities/ssh-access.js +2 -1
- package/dist/src/utilities/verse-marketplace.js +2 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,7 +9,15 @@ process.on('warning', (warning) => {
|
|
|
9
9
|
console.warn(warning.name + ': ' + warning.message);
|
|
10
10
|
});
|
|
11
11
|
require("dotenv/config");
|
|
12
|
+
const net_1 = require("net");
|
|
12
13
|
const commander_1 = require("commander");
|
|
14
|
+
// Node's happy-eyeballs gives each address-family connect attempt 250 ms by
|
|
15
|
+
// default. That is shorter than a single round trip to a far-away API host
|
|
16
|
+
// (e.g. api.raster.art from Asia is ~250 ms RTT), so on networks with broken
|
|
17
|
+
// IPv6 the IPv4 attempt gets cut off mid-handshake, the IPv6 fallback has no
|
|
18
|
+
// route, and fetch fails or stalls intermittently (#97). 2.5 s per attempt is
|
|
19
|
+
// still fast-failing but no longer races the speed of light.
|
|
20
|
+
(0, net_1.setDefaultAutoSelectFamilyAttemptTimeout)(2500);
|
|
13
21
|
const fs_1 = require("fs");
|
|
14
22
|
const path_1 = require("path");
|
|
15
23
|
const setup_1 = require("./src/commands/setup");
|
package/dist/package.json
CHANGED
|
@@ -168,6 +168,13 @@ async function runResolvedTarget(target, options) {
|
|
|
168
168
|
return;
|
|
169
169
|
}
|
|
170
170
|
console.log(chalk_1.default.dim(`Indexing ${tokens.length} token${tokens.length === 1 ? '' : 's'} via FF indexer...`));
|
|
171
|
+
// Previously-unseen tokens make the indexer warm renditions by polling —
|
|
172
|
+
// minutes of wall time on a large series. Say so upfront (the --limit
|
|
173
|
+
// warming hint used to appear only after a failure) and render per-batch
|
|
174
|
+
// progress so a long index never reads as a hang.
|
|
175
|
+
if (tokens.length > 10) {
|
|
176
|
+
console.log(chalk_1.default.dim(' First-time tokens can take the indexer a while to warm; use `--limit 5` for a faster first pass.'));
|
|
177
|
+
}
|
|
171
178
|
// Second positional arg on getNFTTokenInfoBatch is `duration` (DP-1 item
|
|
172
179
|
// display seconds), not concurrency — concurrency is hardcoded inside.
|
|
173
180
|
// Omit it for auto timing: video/audio items carry no duration and play
|
|
@@ -176,7 +183,14 @@ async function runResolvedTarget(target, options) {
|
|
|
176
183
|
chain: t.chain,
|
|
177
184
|
contractAddress: t.contract,
|
|
178
185
|
tokenId: t.tokenId,
|
|
179
|
-
}))
|
|
186
|
+
})), undefined,
|
|
187
|
+
// Unconditional: suppressing the done===total call meant a one-batch
|
|
188
|
+
// index (including the recommended --limit 5 warm-up) printed nothing
|
|
189
|
+
// and multi-batch runs never showed completion. The final N/N line IS
|
|
190
|
+
// the "indexing finished" signal.
|
|
191
|
+
(done, total) => {
|
|
192
|
+
console.log(chalk_1.default.dim(` ${done}/${total} tokens indexed...`));
|
|
193
|
+
});
|
|
180
194
|
// FF-indexer bypass for Raster-minted tokens. The indexer doesn't carry
|
|
181
195
|
// Raster's tokens, so it returns nothing — but Raster handed us each
|
|
182
196
|
// token's playable `media.contentUrl`. Build DP-1 items straight from
|
|
@@ -223,10 +237,20 @@ async function resolveTarget(input, parsed, skipPrompt, limitOption) {
|
|
|
223
237
|
return resolveCoords(parsed.coords);
|
|
224
238
|
}
|
|
225
239
|
if (parsed.kind === 'ff-url') {
|
|
226
|
-
|
|
227
|
-
|
|
240
|
+
// Shows and series both resolve through the source-resolver's Feral File
|
|
241
|
+
// site module, which enumerates exactly the tokens the page describes
|
|
242
|
+
// (series slug → /api/series → /api/artworks?seriesID). The old series
|
|
243
|
+
// path resolved ONE token via ff-marketplace and then asked Raster, which
|
|
244
|
+
// expands to the token's parent artwork — so a single-edition series page
|
|
245
|
+
// (e.g. a Display Edition) surprisingly built the sibling collection's
|
|
246
|
+
// full token list. A series URL means the user pointed at a specific
|
|
247
|
+
// series; build exactly that. Artwork URLs keep the coords→Raster path:
|
|
248
|
+
// a single-token page carries no series intent to preserve.
|
|
249
|
+
if (parsed.urlKind === 'show' || parsed.urlKind === 'series') {
|
|
250
|
+
const label = parsed.urlKind === 'show' ? 'show' : 'series';
|
|
251
|
+
const target = await resolveTokenListInput(input, resolverLimitFromOption(limitOption), `Feral File ${label} ${parsed.identifier}`);
|
|
228
252
|
if (target === null) {
|
|
229
|
-
throw new Error(`Feral File: no supported tokens found for
|
|
253
|
+
throw new Error(`Feral File: no supported tokens found for ${label} "${parsed.identifier}".`);
|
|
230
254
|
}
|
|
231
255
|
return target;
|
|
232
256
|
}
|
|
@@ -326,9 +350,25 @@ async function resolveTokenListInput(input, limit, fallbackTitle = 'Resolved sou
|
|
|
326
350
|
/**
|
|
327
351
|
* Resolve on-chain coords to a target. If Raster doesn't index this token,
|
|
328
352
|
* fall back to single-token mode so we still build something playable.
|
|
353
|
+
*
|
|
354
|
+
* Raster being unreachable gets the same fallback: it only enriches the find
|
|
355
|
+
* (series enumeration, artist labels) — the coords in hand are enough to
|
|
356
|
+
* build a playable single-token playlist, and a network blip on one optional
|
|
357
|
+
* dependency must not kill the whole command (#97).
|
|
329
358
|
*/
|
|
330
359
|
async function resolveCoords(coords) {
|
|
331
|
-
|
|
360
|
+
let summary;
|
|
361
|
+
try {
|
|
362
|
+
summary = await (0, raster_client_1.resolveTokenToArtwork)(coords.chain, coords.contract, coords.tokenId);
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
if (!(error instanceof raster_client_1.RasterUnreachableError)) {
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
console.log(chalk_1.default.yellow(` ${error.message}`));
|
|
369
|
+
console.log(chalk_1.default.dim(' Continuing without Raster — building a one-item playlist.'));
|
|
370
|
+
return { kind: 'single', coords };
|
|
371
|
+
}
|
|
332
372
|
if (summary === null) {
|
|
333
373
|
return { kind: 'single', coords };
|
|
334
374
|
}
|
|
@@ -46,6 +46,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
46
46
|
})();
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
48
|
exports.resolveArtBlocksCollection = resolveArtBlocksCollection;
|
|
49
|
+
const http_1 = require("./http");
|
|
49
50
|
const logger = __importStar(require("../logger"));
|
|
50
51
|
const user_agent_1 = require("./user-agent");
|
|
51
52
|
const AB_GRAPHQL = 'https://artblocks-mainnet.hasura.app/v1/graphql';
|
|
@@ -64,7 +65,7 @@ async function resolveArtBlocksCollection(slug) {
|
|
|
64
65
|
' tokens(limit: 1, order_by: { invocation: asc }) { token_id }' +
|
|
65
66
|
' }' +
|
|
66
67
|
'}';
|
|
67
|
-
const response = await
|
|
68
|
+
const response = await (0, http_1.fetchWithTimeout)(AB_GRAPHQL, {
|
|
68
69
|
method: 'POST',
|
|
69
70
|
headers: { 'Content-Type': 'application/json', 'User-Agent': user_agent_1.USER_AGENT },
|
|
70
71
|
body: JSON.stringify({ query, variables: { slug } }),
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Utilities to fetch playlists from DP1 Feed API
|
|
4
4
|
* API: https://github.com/display-protocol/dp1-feed
|
|
5
5
|
*/
|
|
6
|
+
const { fetchWithTimeout } = require('./http');
|
|
6
7
|
const chalk = require('chalk');
|
|
7
8
|
const fuzzysort = require('fuzzysort');
|
|
8
9
|
const { getFeedConfig } = require('../config');
|
|
@@ -42,7 +43,7 @@ async function fetchPlaylistsFromFeed(feedUrl, limit = 100) {
|
|
|
42
43
|
try {
|
|
43
44
|
// API has a maximum limit of 100
|
|
44
45
|
const validLimit = Math.min(limit, 100);
|
|
45
|
-
const response = await
|
|
46
|
+
const response = await fetchWithTimeout(`${feedUrl}/playlists?limit=${validLimit}&sort=-created`);
|
|
46
47
|
if (!response.ok) {
|
|
47
48
|
console.log(chalk.yellow(` Feed ${feedUrl} returned ${response.status}`));
|
|
48
49
|
return {
|
|
@@ -97,7 +98,7 @@ async function fetchPlaylistsWithPagination(feedUrl, searchTerm, pageSize = 50,
|
|
|
97
98
|
const remainingItems = maxItems - totalFetched;
|
|
98
99
|
const currentLimit = Math.min(pageSize, remainingItems, 100);
|
|
99
100
|
try {
|
|
100
|
-
const response = await
|
|
101
|
+
const response = await fetchWithTimeout(`${feedUrl}/playlists?limit=${currentLimit}&offset=${offset}&sort=-created`);
|
|
101
102
|
reachable = true;
|
|
102
103
|
if (!response.ok) {
|
|
103
104
|
if (response.status === 404 || response.status === 400) {
|
|
@@ -297,7 +298,7 @@ async function getPlaylistById(idOrSlug, feedUrl = null) {
|
|
|
297
298
|
// Try each feed URL until we find the playlist
|
|
298
299
|
for (const url of feedUrls) {
|
|
299
300
|
try {
|
|
300
|
-
const response = await
|
|
301
|
+
const response = await fetchWithTimeout(`${url}/playlists/${idOrSlug}`);
|
|
301
302
|
if (response.ok) {
|
|
302
303
|
const playlist = await response.json();
|
|
303
304
|
return playlist;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.resolveFeralFileArtwork = resolveFeralFileArtwork;
|
|
12
12
|
exports.parseFeralFileArtworkId = parseFeralFileArtworkId;
|
|
13
|
+
const http_1 = require("./http");
|
|
13
14
|
const user_agent_1 = require("./user-agent");
|
|
14
15
|
/**
|
|
15
16
|
* resolveFeralFileArtwork resolves a public Feral File artwork id into indexer
|
|
@@ -19,7 +20,7 @@ const user_agent_1 = require("./user-agent");
|
|
|
19
20
|
* @param fetchImpl - Fetch implementation, injected by tests.
|
|
20
21
|
* @returns Token coordinates suitable for ff-indexer-v2 lookup.
|
|
21
22
|
*/
|
|
22
|
-
async function resolveFeralFileArtwork(artworkId, fetchImpl =
|
|
23
|
+
async function resolveFeralFileArtwork(artworkId, fetchImpl = http_1.defaultDeadlineFetch) {
|
|
23
24
|
const normalizedArtworkId = artworkId.trim();
|
|
24
25
|
if (!normalizedArtworkId) {
|
|
25
26
|
throw new Error('Feral File artwork id is required');
|
|
@@ -50,6 +50,7 @@ exports.resolveFeralFileToken = resolveFeralFileToken;
|
|
|
50
50
|
* Show URLs are rejected: a single exhibition spans multiple series, which
|
|
51
51
|
* is wider than v1 supports.
|
|
52
52
|
*/
|
|
53
|
+
const http_1 = require("./http");
|
|
53
54
|
const logger = __importStar(require("../logger"));
|
|
54
55
|
const feral_file_artwork_1 = require("./feral-file-artwork");
|
|
55
56
|
const user_agent_1 = require("./user-agent");
|
|
@@ -57,7 +58,7 @@ const FF_API_BASE = 'https://feralfile.com/api';
|
|
|
57
58
|
async function ffFetch(path) {
|
|
58
59
|
const url = `${FF_API_BASE}${path}`;
|
|
59
60
|
logger.debug(`[FF API] GET ${url}`);
|
|
60
|
-
const response = await
|
|
61
|
+
const response = await (0, http_1.fetchWithTimeout)(url, {
|
|
61
62
|
headers: { 'User-Agent': user_agent_1.USER_AGENT, Accept: 'application/json' },
|
|
62
63
|
});
|
|
63
64
|
if (!response.ok) {
|
|
@@ -38,6 +38,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.resolveConfiguredDevice = resolveConfiguredDevice;
|
|
40
40
|
exports.assertFF1CommandCompatibility = assertFF1CommandCompatibility;
|
|
41
|
+
const http_1 = require("./http");
|
|
41
42
|
const config_1 = require("../config");
|
|
42
43
|
const device_lookup_1 = require("./device-lookup");
|
|
43
44
|
const logger = __importStar(require("../logger"));
|
|
@@ -113,7 +114,9 @@ function resolveConfiguredDevice(deviceName, options = {}) {
|
|
|
113
114
|
* const result = await assertFF1CommandCompatibility(device, 'displayPlaylist');
|
|
114
115
|
*/
|
|
115
116
|
async function assertFF1CommandCompatibility(device, command, options = {}) {
|
|
116
|
-
|
|
117
|
+
// Default probe fetch rides the shared deadline: a device that accepts
|
|
118
|
+
// the TCP connection but never answers must not hang `ff-cli ssh`.
|
|
119
|
+
const fetchFn = options.fetchFn || ((input, init) => (0, http_1.fetchWithTimeout)(input, init ?? {}));
|
|
117
120
|
const policy = getCommandPolicy(command);
|
|
118
121
|
const versionResult = await detectFF1VersionSafely(device.host, buildVersionHeaders(device), fetchFn);
|
|
119
122
|
return resolveCompatibility(device, command, policy, versionResult);
|
|
@@ -40,6 +40,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
40
40
|
exports.isTransientDeviceNetworkError = isTransientDeviceNetworkError;
|
|
41
41
|
exports.parseRetryAfterMs = parseRetryAfterMs;
|
|
42
42
|
exports.sendPlaylistToDevice = sendPlaylistToDevice;
|
|
43
|
+
const http_1 = require("./http");
|
|
43
44
|
const logger = __importStar(require("../logger"));
|
|
44
45
|
const config_1 = require("../config");
|
|
45
46
|
const ff1_compatibility_1 = require("./ff1-compatibility");
|
|
@@ -178,7 +179,9 @@ function parseRetryAfterMs(headerValue) {
|
|
|
178
179
|
*/
|
|
179
180
|
async function sendPlaylistToDevice({ playlist, deviceName }, overrides = {}) {
|
|
180
181
|
const dependencies = {
|
|
181
|
-
|
|
182
|
+
// Production default rides the shared deadline; injected test fetches
|
|
183
|
+
// are preserved untouched (#101 review).
|
|
184
|
+
fetchFn: overrides.fetchFn ?? http_1.defaultDeadlineFetch,
|
|
182
185
|
getTopicIdFn: overrides.getTopicIdFn ?? topic_store_1.getTopicId,
|
|
183
186
|
getRelayerConfigFn: overrides.getRelayerConfigFn ?? config_1.getFF1RelayerConfig,
|
|
184
187
|
waitFn: overrides.waitFn ?? waitForRetry,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.sendPlaylistViaRelayer = sendPlaylistViaRelayer;
|
|
4
|
+
const http_1 = require("./http");
|
|
4
5
|
function nestedOk(value) {
|
|
5
6
|
if (!value || typeof value !== 'object') {
|
|
6
7
|
return undefined;
|
|
@@ -26,7 +27,8 @@ function nestedOk(value) {
|
|
|
26
27
|
* and JSON parsing errors.
|
|
27
28
|
*/
|
|
28
29
|
async function sendPlaylistViaRelayer(input) {
|
|
29
|
-
|
|
30
|
+
// Production default rides the shared deadline (#101 review).
|
|
31
|
+
const fetchFn = input.fetchFn ?? http_1.defaultDeadlineFetch;
|
|
30
32
|
let url;
|
|
31
33
|
try {
|
|
32
34
|
url = new URL('/api/cast', input.baseUrl);
|
|
@@ -51,6 +51,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
51
51
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
52
|
exports.resolveFxhashIteration = resolveFxhashIteration;
|
|
53
53
|
exports.resolveFxhashProject = resolveFxhashProject;
|
|
54
|
+
const http_1 = require("./http");
|
|
54
55
|
const logger = __importStar(require("../logger"));
|
|
55
56
|
const user_agent_1 = require("./user-agent");
|
|
56
57
|
const FXHASH_GRAPHQL = 'https://api.fxhash.xyz/graphql';
|
|
@@ -63,7 +64,7 @@ const FXHASH_GRAPHQL = 'https://api.fxhash.xyz/graphql';
|
|
|
63
64
|
async function resolveFxhashIteration(slug) {
|
|
64
65
|
logger.debug(`[fxhash] Resolving iteration slug "${slug}"`);
|
|
65
66
|
const query = 'query ($slug: String!) { objkt(slug: $slug) { gentkContractAddress onChainId } }';
|
|
66
|
-
const response = await
|
|
67
|
+
const response = await (0, http_1.fetchWithTimeout)(FXHASH_GRAPHQL, {
|
|
67
68
|
method: 'POST',
|
|
68
69
|
headers: { 'Content-Type': 'application/json', 'User-Agent': user_agent_1.USER_AGENT },
|
|
69
70
|
body: JSON.stringify({ query, variables: { slug } }),
|
|
@@ -115,7 +116,7 @@ async function resolveFxhashProject(slug) {
|
|
|
115
116
|
' }' +
|
|
116
117
|
' }' +
|
|
117
118
|
'}';
|
|
118
|
-
const response = await
|
|
119
|
+
const response = await (0, http_1.fetchWithTimeout)(FXHASH_GRAPHQL, {
|
|
119
120
|
method: 'POST',
|
|
120
121
|
headers: { 'Content-Type': 'application/json', 'User-Agent': user_agent_1.USER_AGENT },
|
|
121
122
|
body: JSON.stringify({ query, variables: { slug } }),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TopicHandoffReceiver = void 0;
|
|
4
|
+
const http_1 = require("./http");
|
|
4
5
|
const handoff_crypto_1 = require("./handoff-crypto");
|
|
5
6
|
const DEFAULT_HANDOFF_URL = 'https://handoff.feralfile.com';
|
|
6
7
|
function brokerBaseUrl(value) {
|
|
@@ -69,7 +70,8 @@ class TopicHandoffReceiver {
|
|
|
69
70
|
}
|
|
70
71
|
/** Ask the handoff broker for a one-time six-digit pairing channel. */
|
|
71
72
|
static async create(options = {}) {
|
|
72
|
-
|
|
73
|
+
// Production default rides the shared deadline (#101 review).
|
|
74
|
+
const fetchFn = options.fetchFn ?? http_1.defaultDeadlineFetch;
|
|
73
75
|
const baseUrl = brokerBaseUrl(options.baseUrl);
|
|
74
76
|
const keyPair = await (0, handoff_crypto_1.generateHandoffKeyPair)();
|
|
75
77
|
const publicKey = await (0, handoff_crypto_1.exportHandoffPublicJwk)(keyPair.publicKey);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared fetch-with-timeout for every outbound HTTP call in the CLI.
|
|
4
|
+
*
|
|
5
|
+
* Bare `fetch` has no deadline: a stalled connect or an unanswered request
|
|
6
|
+
* hangs the command silently — the failure mode agents can least diagnose,
|
|
7
|
+
* and the one behind #97. Every resolver and indexer call goes through here
|
|
8
|
+
* so "silent hang" is structurally impossible, not per-call-site discipline.
|
|
9
|
+
*
|
|
10
|
+
* Callers that pass their own `signal` keep it (composed with the deadline
|
|
11
|
+
* via AbortSignal.any), so cancellation semantics are never silently
|
|
12
|
+
* replaced by a timeout.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.defaultDeadlineFetch = exports.DEFAULT_HTTP_TIMEOUT_MS = void 0;
|
|
16
|
+
exports.fetchWithTimeout = fetchWithTimeout;
|
|
17
|
+
exports.DEFAULT_HTTP_TIMEOUT_MS = 20_000;
|
|
18
|
+
function fetchWithTimeout(input, init = {}, timeoutMs = exports.DEFAULT_HTTP_TIMEOUT_MS) {
|
|
19
|
+
const deadline = AbortSignal.timeout(timeoutMs);
|
|
20
|
+
const signals = [deadline];
|
|
21
|
+
if (init.signal) {
|
|
22
|
+
signals.push(init.signal);
|
|
23
|
+
}
|
|
24
|
+
else if (input instanceof Request && input.signal) {
|
|
25
|
+
// A Request input carries its own abort signal; passing only the
|
|
26
|
+
// deadline in init would silently REPLACE it (init wins over the
|
|
27
|
+
// request's signal in fetch), stripping the caller's cancellation.
|
|
28
|
+
signals.push(input.signal);
|
|
29
|
+
}
|
|
30
|
+
const signal = signals.length > 1 ? AbortSignal.any(signals) : deadline;
|
|
31
|
+
return fetch(input, { ...init, signal });
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Drop-in `fetch` for production defaults of injectable-fetch modules:
|
|
35
|
+
* identical signature, deadline attached. Modules that accept a `fetchFn`
|
|
36
|
+
* override default to this instead of bare `globalThis.fetch`, so tests
|
|
37
|
+
* keep full injection control while production can never hang.
|
|
38
|
+
*/
|
|
39
|
+
const defaultDeadlineFetch = (input, init) => fetchWithTimeout(input, init ?? {});
|
|
40
|
+
exports.defaultDeadlineFetch = defaultDeadlineFetch;
|
|
@@ -58,6 +58,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
58
58
|
})();
|
|
59
59
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
60
|
exports.resolveNeortArt = resolveNeortArt;
|
|
61
|
+
const http_1 = require("./http");
|
|
61
62
|
const logger = __importStar(require("../logger"));
|
|
62
63
|
const user_agent_1 = require("./user-agent");
|
|
63
64
|
const NEORT_API_BASE = 'https://api.neort.io/v1';
|
|
@@ -70,7 +71,7 @@ const NEORT_CDN_BASE = 'https://d32h66pp7fue57.cloudfront.net';
|
|
|
70
71
|
*/
|
|
71
72
|
async function resolveNeortArt(id) {
|
|
72
73
|
logger.debug(`[neort] Resolving art id "${id}"`);
|
|
73
|
-
const response = await
|
|
74
|
+
const response = await (0, http_1.fetchWithTimeout)(`${NEORT_API_BASE}/art/${encodeURIComponent(id)}`, {
|
|
74
75
|
headers: { 'User-Agent': user_agent_1.USER_AGENT, Accept: 'application/json' },
|
|
75
76
|
});
|
|
76
77
|
if (!response.ok) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* to retrieve comprehensive token information.
|
|
5
5
|
*/
|
|
6
6
|
const GRAPHQL_ENDPOINT = 'https://indexer.feralfile.com/graphql';
|
|
7
|
+
const { fetchWithTimeout } = require('./http');
|
|
7
8
|
const logger = require('../logger');
|
|
8
9
|
const { applyItemTiming } = require('./playlist-builder');
|
|
9
10
|
// Polling configuration (in milliseconds)
|
|
@@ -174,7 +175,7 @@ async function queryTokens(params = {}) {
|
|
|
174
175
|
const headers = { 'Content-Type': 'application/json' };
|
|
175
176
|
logger.debug('[NFT Indexer] Querying tokens:', { token_cids, owners, limit, offset });
|
|
176
177
|
logger.debug('[NFT Indexer] GraphQL query:', query);
|
|
177
|
-
const response = await
|
|
178
|
+
const response = await fetchWithTimeout(GRAPHQL_ENDPOINT, {
|
|
178
179
|
method: 'POST',
|
|
179
180
|
headers,
|
|
180
181
|
body: JSON.stringify({ query }),
|
|
@@ -598,7 +599,7 @@ async function getNFTTokenInfoSingle(params, duration, options = {}) {
|
|
|
598
599
|
* @param {number} duration - Display duration in seconds
|
|
599
600
|
* @returns {Promise<Array>} Array of DP1 items
|
|
600
601
|
*/
|
|
601
|
-
async function getNFTTokenInfoBatch(tokens, duration) {
|
|
602
|
+
async function getNFTTokenInfoBatch(tokens, duration, onProgress) {
|
|
602
603
|
logger.info(`[NFT Indexer] 📦 Starting batch processing for ${tokens.length} token(s)...`);
|
|
603
604
|
logger.debug('[NFT Indexer] Batch tokens:', tokens);
|
|
604
605
|
const results = [];
|
|
@@ -620,6 +621,19 @@ async function getNFTTokenInfoBatch(tokens, duration) {
|
|
|
620
621
|
const failed = batchResults.filter((r) => !r.success).length;
|
|
621
622
|
logger.info(`[NFT Indexer] Batch complete: ${successful} success, ${failed} failed`);
|
|
622
623
|
results.push(...batchResults);
|
|
624
|
+
// Indexing previously-unseen tokens can take minutes (the indexer warms
|
|
625
|
+
// renditions by polling); without user-visible progress the CLI reads as
|
|
626
|
+
// hung. logger.info above is verbose-gated, so callers that face a human
|
|
627
|
+
// pass onProgress to render it. Errors in the callback must never break
|
|
628
|
+
// the batch — reporting is strictly best-effort.
|
|
629
|
+
if (typeof onProgress === 'function') {
|
|
630
|
+
try {
|
|
631
|
+
onProgress(results.length, tokens.length);
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
/* progress display is best-effort */
|
|
635
|
+
}
|
|
636
|
+
}
|
|
623
637
|
}
|
|
624
638
|
logger.info(`[NFT Indexer] ✓ Batch processing complete: ${results.length} total results`);
|
|
625
639
|
const successCount = results.filter((r) => r.success && r.item).length;
|
|
@@ -687,7 +701,7 @@ async function triggerIndexingAsync(chain, contractAddress, tokenId) {
|
|
|
687
701
|
token_cids: [tokenCID],
|
|
688
702
|
};
|
|
689
703
|
const headers = { 'Content-Type': 'application/json' };
|
|
690
|
-
const response = await
|
|
704
|
+
const response = await fetchWithTimeout(GRAPHQL_ENDPOINT, {
|
|
691
705
|
method: 'POST',
|
|
692
706
|
headers,
|
|
693
707
|
body: JSON.stringify({ query: mutation, variables }),
|
|
@@ -742,7 +756,7 @@ async function queryJobStatus(jobId) {
|
|
|
742
756
|
const query = JOB_STATUS_QUERY;
|
|
743
757
|
const variables = { job_id: id };
|
|
744
758
|
const headers = { 'Content-Type': 'application/json' };
|
|
745
|
-
const response = await
|
|
759
|
+
const response = await fetchWithTimeout(GRAPHQL_ENDPOINT, {
|
|
746
760
|
method: 'POST',
|
|
747
761
|
headers,
|
|
748
762
|
body: JSON.stringify({ query, variables }),
|
|
@@ -45,6 +45,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
45
45
|
})();
|
|
46
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
47
|
exports.resolveObjktAlias = resolveObjktAlias;
|
|
48
|
+
const http_1 = require("./http");
|
|
48
49
|
const logger = __importStar(require("../logger"));
|
|
49
50
|
const user_agent_1 = require("./user-agent");
|
|
50
51
|
const OBJKT_GRAPHQL = 'https://data.objkt.com/v3/graphql';
|
|
@@ -56,7 +57,7 @@ const OBJKT_GRAPHQL = 'https://data.objkt.com/v3/graphql';
|
|
|
56
57
|
async function resolveObjktAlias(alias) {
|
|
57
58
|
logger.debug(`[Objkt] Resolving alias "${alias}"`);
|
|
58
59
|
const query = 'query ($path: String!) { fa(where: { path: { _eq: $path } }) { contract } }';
|
|
59
|
-
const response = await
|
|
60
|
+
const response = await (0, http_1.fetchWithTimeout)(OBJKT_GRAPHQL, {
|
|
60
61
|
method: 'POST',
|
|
61
62
|
headers: { 'Content-Type': 'application/json', 'User-Agent': user_agent_1.USER_AGENT },
|
|
62
63
|
body: JSON.stringify({ query, variables: { path: alias } }),
|
|
@@ -59,6 +59,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
59
59
|
})();
|
|
60
60
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
61
61
|
exports.resolveOpenSeaCollection = resolveOpenSeaCollection;
|
|
62
|
+
const http_1 = require("./http");
|
|
62
63
|
const logger = __importStar(require("../logger"));
|
|
63
64
|
const user_agent_1 = require("./user-agent");
|
|
64
65
|
/**
|
|
@@ -80,7 +81,7 @@ const FALLBACK_HINT = 'Paste a specific token URL (opensea.io/item/ethereum/{con
|
|
|
80
81
|
*/
|
|
81
82
|
async function resolveOpenSeaCollection(slug) {
|
|
82
83
|
logger.debug(`[OpenSea] Resolving collection slug "${slug}"`);
|
|
83
|
-
const response = await
|
|
84
|
+
const response = await (0, http_1.fetchWithTimeout)(`https://opensea.io/collection/${encodeURIComponent(slug)}`, {
|
|
84
85
|
headers: { 'User-Agent': user_agent_1.USER_AGENT, Accept: 'text/html' },
|
|
85
86
|
});
|
|
86
87
|
if (!response.ok) {
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isPlaylistSourceUrl = isPlaylistSourceUrl;
|
|
4
4
|
exports.loadPlaylistSource = loadPlaylistSource;
|
|
5
5
|
exports.resolvePlaySource = resolvePlaySource;
|
|
6
|
+
const http_1 = require("./http");
|
|
6
7
|
const fs_1 = require("fs");
|
|
7
8
|
// playlist-builder is still CommonJS; require keeps the interop simple.
|
|
8
9
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -39,7 +40,7 @@ async function loadPlaylistSource(source) {
|
|
|
39
40
|
throw new Error('Playlist source is required');
|
|
40
41
|
}
|
|
41
42
|
if (isPlaylistSourceUrl(trimmedSource)) {
|
|
42
|
-
const response = await
|
|
43
|
+
const response = await (0, http_1.fetchWithTimeout)(trimmedSource);
|
|
43
44
|
if (!response.ok) {
|
|
44
45
|
throw new Error(`Failed to fetch playlist URL: ${response.status} ${response.statusText}`);
|
|
45
46
|
}
|
|
@@ -57,6 +57,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
57
57
|
};
|
|
58
58
|
})();
|
|
59
59
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
exports.RasterUnreachableError = void 0;
|
|
60
61
|
exports.resolveTokenToArtwork = resolveTokenToArtwork;
|
|
61
62
|
exports.resolveSlugToArtwork = resolveSlugToArtwork;
|
|
62
63
|
exports.resolveAddressToArtist = resolveAddressToArtist;
|
|
@@ -66,6 +67,14 @@ exports.formatSummaryLine = formatSummaryLine;
|
|
|
66
67
|
const logger = __importStar(require("../logger"));
|
|
67
68
|
const user_agent_1 = require("./user-agent");
|
|
68
69
|
const DEFAULT_RASTER_GRAPHQL_URL = 'https://api.raster.art/graphql';
|
|
70
|
+
/**
|
|
71
|
+
* Hard ceiling on a single Raster round trip. Without it, a stalled TCP
|
|
72
|
+
* connect or unanswered request leaves `find` hanging with no output (#97) —
|
|
73
|
+
* the failure mode agents can least diagnose. 20 s is generous for a
|
|
74
|
+
* transcontinental GraphQL query and still turns "silent hang" into a fast,
|
|
75
|
+
* actionable error.
|
|
76
|
+
*/
|
|
77
|
+
const RASTER_REQUEST_TIMEOUT_MS = 20_000;
|
|
69
78
|
// CAIP-2 chain IDs for the chains the FF indexer supports.
|
|
70
79
|
// Tezos mainnet's CAIP form uses the genesis block hash (NetXdQprcVkpaWU),
|
|
71
80
|
// not the human-readable "tezos:mainnet" — Raster returns the hash form on
|
|
@@ -78,6 +87,19 @@ const INDEXER_CHAIN_TO_CAIP = {
|
|
|
78
87
|
ethereum: 'eip155:1',
|
|
79
88
|
tezos: 'tezos:NetXdQprcVkpaWU',
|
|
80
89
|
};
|
|
90
|
+
/**
|
|
91
|
+
* Raster could not be reached at the network level (connect failure, broken
|
|
92
|
+
* IPv6 path, timeout). Distinct from API-level errors so callers that hold
|
|
93
|
+
* usable token coordinates can degrade gracefully instead of dying — Raster
|
|
94
|
+
* only enriches a find, it is not required to build a playable playlist.
|
|
95
|
+
*/
|
|
96
|
+
class RasterUnreachableError extends Error {
|
|
97
|
+
constructor(message) {
|
|
98
|
+
super(message);
|
|
99
|
+
this.name = 'RasterUnreachableError';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.RasterUnreachableError = RasterUnreachableError;
|
|
81
103
|
function caipToIndexerChain(caip) {
|
|
82
104
|
return CAIP_TO_INDEXER_CHAIN[caip] ?? null;
|
|
83
105
|
}
|
|
@@ -99,16 +121,46 @@ async function rasterQuery(query, variables) {
|
|
|
99
121
|
if (apiKey) {
|
|
100
122
|
headers['x-api-key'] = apiKey;
|
|
101
123
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
let response;
|
|
125
|
+
try {
|
|
126
|
+
response = await fetch(endpoint, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers,
|
|
129
|
+
body: JSON.stringify({ query, variables }),
|
|
130
|
+
signal: AbortSignal.timeout(RASTER_REQUEST_TIMEOUT_MS),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
// Undici's "fetch failed" carries no endpoint and no cause in its
|
|
135
|
+
// message — name the host and the real failure so the error is
|
|
136
|
+
// actionable, and let callers detect network failure by type.
|
|
137
|
+
const cause = error.cause;
|
|
138
|
+
const detail = cause?.message ?? error.message;
|
|
139
|
+
throw new RasterUnreachableError(`Raster API unreachable (${endpoint}): ${detail}`);
|
|
140
|
+
}
|
|
107
141
|
if (!response.ok) {
|
|
108
|
-
|
|
142
|
+
// Body text is best-effort context; the HTTP status alone is the error.
|
|
143
|
+
// A body read that rejects here must not mask the status — and an HTTP
|
|
144
|
+
// error is an API-level failure, deliberately NOT RasterUnreachableError.
|
|
145
|
+
const body = await response.text().catch(() => '');
|
|
109
146
|
throw new Error(`Raster API ${response.status} ${response.statusText}: ${body.slice(0, 200)}`);
|
|
110
147
|
}
|
|
111
|
-
|
|
148
|
+
// The network-error boundary extends through body consumption: a response
|
|
149
|
+
// whose stream stalls past the headers or terminates mid-body rejects
|
|
150
|
+
// HERE, not in fetch() above. Those must still surface as
|
|
151
|
+
// RasterUnreachableError or resolveCoords cannot perform its promised
|
|
152
|
+
// single-token fallback. (A 200 whose body cannot be read or parsed is
|
|
153
|
+
// unusable either way — falling back is right even for server-side
|
|
154
|
+
// garbage.)
|
|
155
|
+
let payload;
|
|
156
|
+
try {
|
|
157
|
+
payload = (await response.json());
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
const cause = error.cause;
|
|
161
|
+
const detail = cause?.message ?? error.message;
|
|
162
|
+
throw new RasterUnreachableError(`Raster API unreachable (${endpoint}): response body failed: ${detail}`);
|
|
163
|
+
}
|
|
112
164
|
if (payload.errors && payload.errors.length > 0) {
|
|
113
165
|
throw new Error(`Raster API error: ${payload.errors.map((e) => e.message).join('; ')}`);
|
|
114
166
|
}
|
|
@@ -37,6 +37,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.sendSshAccessCommand = sendSshAccessCommand;
|
|
40
|
+
const http_1 = require("./http");
|
|
40
41
|
const logger = __importStar(require("../logger"));
|
|
41
42
|
const ff1_compatibility_1 = require("./ff1-compatibility");
|
|
42
43
|
/**
|
|
@@ -106,7 +107,7 @@ async function sendSshAccessCommand({ enabled, deviceName, publicKey, ttlSeconds
|
|
|
106
107
|
if (device.apiKey) {
|
|
107
108
|
headers['API-KEY'] = device.apiKey;
|
|
108
109
|
}
|
|
109
|
-
const response = await
|
|
110
|
+
const response = await (0, http_1.fetchWithTimeout)(apiUrl, {
|
|
110
111
|
method: 'POST',
|
|
111
112
|
headers,
|
|
112
113
|
body: JSON.stringify(requestBody),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resolveVerseSeries = resolveVerseSeries;
|
|
4
|
+
const http_1 = require("./http");
|
|
4
5
|
/**
|
|
5
6
|
* Resolve a Verse series slug to representative Ethereum token coordinates.
|
|
6
7
|
*
|
|
@@ -15,7 +16,7 @@ exports.resolveVerseSeries = resolveVerseSeries;
|
|
|
15
16
|
*/
|
|
16
17
|
async function resolveVerseSeries(slug) {
|
|
17
18
|
const url = `https://verse.works/series/${encodeURIComponent(slug)}`;
|
|
18
|
-
const response = await
|
|
19
|
+
const response = await (0, http_1.fetchWithTimeout)(url);
|
|
19
20
|
if (!response.ok) {
|
|
20
21
|
throw new Error(`Verse series lookup failed for ${slug}: ${response.status}`);
|
|
21
22
|
}
|