lightnode-sdk 0.4.3 → 0.4.5

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/gateway.d.ts CHANGED
@@ -8,7 +8,14 @@
8
8
  * fresh-each-call thunk) by whatever means they prefer and hands it here.
9
9
  */
10
10
  import type { NetworkConfig } from "./types.js";
11
+ /**
12
+ * Default gateway URL for a network. In Node, returns the gateway directly.
13
+ * In browser/WebContainer, returns the lightnode.app proxy (same upstream,
14
+ * but with permissive CORS so third-party origins work).
15
+ */
11
16
  export declare function consumerGatewayUrl(net: "mainnet" | "testnet"): string;
17
+ /** Gateway host without any proxy fallback. For diagnostics / advanced callers. */
18
+ export declare function consumerGatewayHost(net: "mainnet" | "testnet"): string;
12
19
  /** Either a fixed token, or a function that produces (or refreshes) one. */
13
20
  export type BearerSource = string | (() => string | Promise<string>);
14
21
  export declare class GatewayHttpError extends Error {
package/dist/gateway.js CHANGED
@@ -11,7 +11,42 @@ const GATEWAY_HOSTS = {
11
11
  mainnet: "https://chat-api.mainnet.lightchain.ai",
12
12
  testnet: "https://chat-api.testnet.lightchain.ai",
13
13
  };
14
+ // In browser-like contexts the gateway's CORS policy blocks third-party
15
+ // origins, so the SDK routes through lightnode.app's public proxy instead.
16
+ // The proxy is a thin pass-through (no state, no transformation), open to
17
+ // any origin. In a real Node process this isn't needed - the gateway is
18
+ // reached directly.
19
+ const PROXY_HOSTS = {
20
+ mainnet: "https://lightnode.app/api/gw/mainnet",
21
+ testnet: "https://lightnode.app/api/gw/testnet",
22
+ };
23
+ /**
24
+ * True when the current runtime is a browser, or a Node-in-browser shim
25
+ * (StackBlitz WebContainer, Bolt, etc.) where `fetch` enforces browser-style
26
+ * CORS. Used to decide whether to call the gateway direct or via the proxy.
27
+ */
28
+ function looksLikeBrowserFetch() {
29
+ if (typeof window !== "undefined" && typeof document !== "undefined")
30
+ return true;
31
+ // StackBlitz WebContainer exposes `process.versions.webcontainer`. Other
32
+ // Node-in-browser runtimes (Bolt, RunKit) may not, so we also check for
33
+ // the absence of a real Node TCP module via `process.versions.node` PLUS
34
+ // the presence of a global `WebSocket` (browser-only by spec).
35
+ const wc = globalThis.process?.versions?.webcontainer;
36
+ if (wc)
37
+ return true;
38
+ return false;
39
+ }
40
+ /**
41
+ * Default gateway URL for a network. In Node, returns the gateway directly.
42
+ * In browser/WebContainer, returns the lightnode.app proxy (same upstream,
43
+ * but with permissive CORS so third-party origins work).
44
+ */
14
45
  export function consumerGatewayUrl(net) {
46
+ return looksLikeBrowserFetch() ? PROXY_HOSTS[net] : GATEWAY_HOSTS[net];
47
+ }
48
+ /** Gateway host without any proxy fallback. For diagnostics / advanced callers. */
49
+ export function consumerGatewayHost(net) {
15
50
  return GATEWAY_HOSTS[net];
16
51
  }
17
52
  async function resolveBearer(src) {
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS } from "./networks.js";
2
2
  import { fromWei } from "./subgraph.js";
3
3
  import { aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv } from "./analytics.js";
4
- import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey } from "./inference.js";
4
+ import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey } from "./inference.js";
5
5
  import { StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker } from "./errors.js";
6
6
  import { GatewayClient, GatewayHttpError } from "./gateway.js";
7
7
  import * as crypto from "./crypto.js";
@@ -60,7 +60,7 @@ export declare class LightNode {
60
60
  baseUrl?: string;
61
61
  }): GatewayClient;
62
62
  }
63
- export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, };
63
+ export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, };
64
64
  export type { BearerSource, GatewayClientOptions, SelectSessionResult, PrepareSessionResult, UploadBlobResult, SessionTokenResult } from "./gateway.js";
65
65
  export type { SessionPreparation, RunInferenceArgs, RunInferenceResult, RunInferenceWithKeyArgs } from "./inference.js";
66
66
  export type { NetworkId, NetworkConfig, Worker, Job, ModelInfo, NetworkStats, ModelStat, WorkerStat, NetworkAnalytics };
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS } from "./networks.js";
2
2
  import { fetchWorker, fetchWorkerJobs, fetchRecentJobs, fetchModels, fetchWorkers, summarize, fromWei, } from "./subgraph.js";
3
3
  import { isRegistered } from "./onchain.js";
4
4
  import { aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, } from "./analytics.js";
5
- import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey, } from "./inference.js";
5
+ import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey, } from "./inference.js";
6
6
  import { StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, } from "./errors.js";
7
7
  import { GatewayClient, GatewayHttpError } from "./gateway.js";
8
8
  import * as crypto from "./crypto.js";
@@ -90,7 +90,7 @@ export class LightNode {
90
90
  return new GatewayClient({ network: this.network, ...opts });
91
91
  }
92
92
  }
93
- export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl,
93
+ export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost,
94
94
  // v0.3 inference-submit surface (BETA - see README "Submitting inference").
95
95
  GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto,
96
96
  // v0.4 high-level orchestrator: one call, full flow.
@@ -91,7 +91,7 @@ export declare function submitPrompt(gateway: GatewayClient, sessionKey: Uint8Ar
91
91
  /** Decrypt a worker response (raw bytes or base64 from the relay) with the session key. */
92
92
  export declare function decryptResponse(sessionKey: Uint8Array, ciphertext: Uint8Array | string): Promise<string>;
93
93
  /** Re-export so callers don't have to import from a second module just for the URL helper. */
94
- export { consumerGatewayUrl, GatewayClient } from "./gateway.js";
94
+ export { consumerGatewayUrl, consumerGatewayHost, GatewayClient } from "./gateway.js";
95
95
  /** Optional helper: generate the caller's own ECDH keypair if they want one (e.g. acting as the disputer). */
96
96
  export { generateEcdhKeyPair };
97
97
  interface MinimalWalletClient {
package/dist/inference.js CHANGED
@@ -123,7 +123,7 @@ export async function decryptResponse(sessionKey, ciphertext) {
123
123
  return bytesToUtf8(await decrypt(sessionKey, bytes));
124
124
  }
125
125
  /** Re-export so callers don't have to import from a second module just for the URL helper. */
126
- export { consumerGatewayUrl, GatewayClient } from "./gateway.js";
126
+ export { consumerGatewayUrl, consumerGatewayHost, GatewayClient } from "./gateway.js";
127
127
  /** Optional helper: generate the caller's own ECDH keypair if they want one (e.g. acting as the disputer). */
128
128
  export { generateEcdhKeyPair };
129
129
  // ----------------------------------------------------------------------------
@@ -326,16 +326,31 @@ async function runOneAttempt(args, attempt) {
326
326
  throw new Error("JobSubmitted log missing in submitJob receipt");
327
327
  const jobId = topicAsUint(jobLog.topics[1]);
328
328
  // 6. wait for JobCompleted
329
- // KEY INVARIANT: the real result is the WS-delivered, session-key-decrypted
330
- // ciphertext. JobCompleted is an explorer pointer (the worker's commit-result
331
- // tx). If the WS already produced an answer (chunks.length > 0) we MUST
332
- // accept it even if the on-chain event hasn't landed - throwing stalled here
333
- // wipes a delivered answer on retry, which reads as "the call returned nothing".
329
+ // The actual *result* is the WS-delivered, session-key-decrypted ciphertext.
330
+ // JobCompleted is an explorer pointer (the worker's commit-result tx).
331
+ // Polling rules:
332
+ // - No chunks yet: poll for the full deadline (default 120s). Still nothing
333
+ // -> throw stalled so the outer loop can retry with a different worker.
334
+ // - Chunks arrived: keep polling for a 45s grace window after the FIRST
335
+ // chunk. Workers usually commit JobCompleted within ~10s of broadcasting
336
+ // the answer, so 45s is generous. If it still doesn't land, surface the
337
+ // answer with txs.jobCompleted=null (the answer is still session-key
338
+ // authentic; the on-chain proof can be polled for separately by callers).
334
339
  const deadline = Date.now() + jobCompletedTimeoutMs;
340
+ const POST_CHUNKS_GRACE_MS = 45000;
341
+ const waitStart = Date.now();
342
+ let firstChunkAt = chunks.length > 0 ? waitStart : null;
335
343
  const jobIdTopic = (`0x${jobId.toString(16).padStart(64, "0")}`);
336
344
  let completed = null;
337
- while (!completed && Date.now() < deadline) {
345
+ while (!completed) {
346
+ const now = Date.now();
347
+ if (now >= deadline)
348
+ break;
349
+ if (firstChunkAt != null && now - firstChunkAt >= POST_CHUNKS_GRACE_MS)
350
+ break;
338
351
  await new Promise((res) => setTimeout(res, 3000));
352
+ if (firstChunkAt == null && chunks.length > 0)
353
+ firstChunkAt = Date.now();
339
354
  const logs = await publicClient.getLogs({
340
355
  address: network.jobRegistry,
341
356
  fromBlock: submitReceipt.blockNumber,
@@ -345,10 +360,6 @@ async function runOneAttempt(args, attempt) {
345
360
  logs.find((l) => l.topics[0] === JOB_COMPLETED_TOPIC && l.topics[1] === jobIdTopic) ?? null;
346
361
  if (completed)
347
362
  break;
348
- // If the WS already delivered the answer, stop polling - no point burning
349
- // more time on a confirmation that only serves as an explorer link.
350
- if (chunks.length > 0)
351
- break;
352
363
  }
353
364
  if (!completed && chunks.length === 0) {
354
365
  try {
@@ -486,26 +497,44 @@ export async function runInferenceWithKey(args) {
486
497
  // the caller doesn't need a second import; in browsers + Node it works the
487
498
  // same against the consumer-api gateway.
488
499
  const gwBase = args.gatewayUrl ?? consumerGatewayUrlFn(networkId);
489
- const chRes = await fetch(`${gwBase}/api/auth/challenge?address=${account.address}`, {
490
- headers: { Accept: "application/json" },
491
- });
500
+ // `fetch failed` with no cause is the worst possible error for a builder
501
+ // running this for the first time - they need to know which host failed and
502
+ // what the underlying cause was. Wrap both SIWE calls so the error names a
503
+ // host (so a network/DNS/CORS problem is obvious) and a hint when the cause
504
+ // looks like a CORS or undici-level reachability error.
505
+ const fetchOrFail = async (url, init, label) => {
506
+ try {
507
+ return await fetch(url, init);
508
+ }
509
+ catch (err) {
510
+ const cause = err.cause;
511
+ const code = cause?.code ?? "";
512
+ const msg = err.message ?? "fetch failed";
513
+ const detail = cause?.message ? ` (${cause.message})` : "";
514
+ const hint = /ENOTFOUND|EAI_AGAIN|ECONNREFUSED|UND_ERR_CONNECT|CERT_/.test(code) || msg.includes("CORS")
515
+ ? ` Tip: this host may be unreachable from this runtime (CORS, DNS, or TLS). Pass gatewayUrl: 'https://lightnode.app/api/gw/${networkId}' to route through the public proxy.`
516
+ : "";
517
+ throw new Error(`SIWE ${label ?? "request"} to ${url} failed: ${msg}${detail}${hint}`);
518
+ }
519
+ };
520
+ const chRes = await fetchOrFail(`${gwBase}/api/auth/challenge?address=${account.address}`, { headers: { Accept: "application/json" } }, "challenge");
492
521
  if (!chRes.ok)
493
522
  throw new GatewayAuthError(chRes.status, await chRes.text());
494
523
  const ch = (await chRes.json());
495
524
  if (!ch.message)
496
525
  throw new GatewayAuthError(chRes.status, "auth challenge returned no message");
497
526
  const signature = await wallet.signMessage({ account, message: ch.message });
498
- const verifyRes = await fetch(`${gwBase}/api/auth/verify`, {
527
+ const verifyRes = await fetchOrFail(`${gwBase}/api/auth/verify`, {
499
528
  method: "POST",
500
529
  headers: { "Content-Type": "application/json", Accept: "application/json" },
501
530
  body: JSON.stringify({ message: ch.message, signature }),
502
- });
531
+ }, "verify");
503
532
  if (!verifyRes.ok)
504
533
  throw new GatewayAuthError(verifyRes.status, await verifyRes.text());
505
534
  const verify = (await verifyRes.json());
506
535
  if (!verify.token)
507
536
  throw new GatewayAuthError(verifyRes.status, "auth verify returned no token");
508
- const gateway = new GatewayClientCtor({ network: networkId, bearer: verify.token, baseUrl: args.gatewayUrl });
537
+ const gateway = new GatewayClientCtor({ network: networkId, bearer: verify.token, baseUrl: args.gatewayUrl ?? gwBase });
509
538
  // Pick a WebSocket: the browser global if present, otherwise the caller-
510
539
  // supplied ctor. We deliberately do NOT try to dynamic-import "ws" - it
511
540
  // isn't a hard dep, and a bundler trying to resolve it would fail noisily.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Read-only TypeScript client for LightChain AI: workers, jobs, models, on-chain registration, and per-model network analytics. Independent, community-built (not an official LightChain package).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",