lightnode-sdk 0.4.4 → 0.4.6

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/crypto.d.ts CHANGED
@@ -9,12 +9,14 @@
9
9
  * - AES-GCM ciphertext layout: nonce(12) || ciphertext || tag(16).
10
10
  * - Encrypted session key layout: ephemeralPub(65) || nonce(12) || ciphertext || tag(16).
11
11
  *
12
- * Uses the Web Crypto API exposed via `globalThis.crypto.subtle`, available in
13
- * Node 18+ and all modern browsers. No Node-only dependencies; the SDK runs
14
- * unchanged in either environment.
12
+ * Uses the Web Crypto API. Tries `globalThis.crypto` first (real browsers +
13
+ * Node 19+ where it is global), then falls back to `node:crypto`'s
14
+ * `webcrypto` export (Node 18 and StackBlitz's WebContainer, which exposes
15
+ * the node: module but not the global). No hard dependency on Node, and no
16
+ * polyfill in the browser bundle.
15
17
  */
16
18
  /** A fresh 32-byte symmetric session key (random, never derived). */
17
- export declare function generateSessionKey(): Uint8Array;
19
+ export declare function generateSessionKey(): Promise<Uint8Array>;
18
20
  /** Fresh ECDH P-256 keypair (extractable; we need to export the public key on the wire). */
19
21
  export declare function generateEcdhKeyPair(): Promise<CryptoKeyPair>;
20
22
  /** Export an ECDH public key to its raw uncompressed P-256 encoding (65 bytes). */
package/dist/crypto.js CHANGED
@@ -9,23 +9,61 @@
9
9
  * - AES-GCM ciphertext layout: nonce(12) || ciphertext || tag(16).
10
10
  * - Encrypted session key layout: ephemeralPub(65) || nonce(12) || ciphertext || tag(16).
11
11
  *
12
- * Uses the Web Crypto API exposed via `globalThis.crypto.subtle`, available in
13
- * Node 18+ and all modern browsers. No Node-only dependencies; the SDK runs
14
- * unchanged in either environment.
12
+ * Uses the Web Crypto API. Tries `globalThis.crypto` first (real browsers +
13
+ * Node 19+ where it is global), then falls back to `node:crypto`'s
14
+ * `webcrypto` export (Node 18 and StackBlitz's WebContainer, which exposes
15
+ * the node: module but not the global). No hard dependency on Node, and no
16
+ * polyfill in the browser bundle.
15
17
  */
16
18
  const AES_KEY_BYTES = 32;
17
19
  const GCM_NONCE_BYTES = 12;
18
20
  const P256_UNCOMPRESSED_KEY_BYTES = 65;
19
21
  const SESSION_KEY_BYTES = 32;
20
- function subtle() {
21
- const c = globalThis.crypto;
22
- if (!c?.subtle)
23
- throw new Error("Web Crypto unavailable - Node 18+ or a browser is required");
24
- return c.subtle;
22
+ let resolvedCrypto = null;
23
+ let resolvingCrypto = null;
24
+ async function getCrypto() {
25
+ if (resolvedCrypto)
26
+ return resolvedCrypto;
27
+ if (resolvingCrypto)
28
+ return resolvingCrypto;
29
+ resolvingCrypto = (async () => {
30
+ const g = globalThis.crypto;
31
+ if (g?.subtle && typeof g.getRandomValues === "function") {
32
+ const provider = { subtle: g.subtle, getRandomValues: g.getRandomValues.bind(g) };
33
+ resolvedCrypto = provider;
34
+ return provider;
35
+ }
36
+ // Node 18 + StackBlitz WebContainer: globalThis.crypto is missing, but
37
+ // `node:crypto` exposes the same Web Crypto API via `webcrypto`.
38
+ try {
39
+ const mod = (await import("node:crypto"));
40
+ const wc = mod.webcrypto;
41
+ if (wc?.subtle && typeof wc.getRandomValues === "function") {
42
+ const provider = { subtle: wc.subtle, getRandomValues: wc.getRandomValues.bind(wc) };
43
+ resolvedCrypto = provider;
44
+ return provider;
45
+ }
46
+ }
47
+ catch {
48
+ // node:crypto not importable - we're in a browser bundle without a
49
+ // global crypto. The fall-through error below explains the fix.
50
+ }
51
+ throw new Error("Web Crypto unavailable: globalThis.crypto is missing and node:crypto could not be loaded. " +
52
+ "The SDK requires Node 18+ or a modern browser.");
53
+ })();
54
+ try {
55
+ return await resolvingCrypto;
56
+ }
57
+ finally {
58
+ resolvingCrypto = null;
59
+ }
60
+ }
61
+ async function subtle() {
62
+ return (await getCrypto()).subtle;
25
63
  }
26
- function randomBytes(n) {
64
+ async function randomBytes(n) {
27
65
  const buf = new Uint8Array(n);
28
- globalThis.crypto.getRandomValues(buf);
66
+ (await getCrypto()).getRandomValues(buf);
29
67
  return buf;
30
68
  }
31
69
  /** A fresh 32-byte symmetric session key (random, never derived). */
@@ -33,16 +71,16 @@ export function generateSessionKey() {
33
71
  return randomBytes(SESSION_KEY_BYTES);
34
72
  }
35
73
  /** Fresh ECDH P-256 keypair (extractable; we need to export the public key on the wire). */
36
- export function generateEcdhKeyPair() {
37
- return subtle().generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
74
+ export async function generateEcdhKeyPair() {
75
+ return (await subtle()).generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
38
76
  }
39
77
  /** Export an ECDH public key to its raw uncompressed P-256 encoding (65 bytes). */
40
78
  export async function exportPublicKey(key) {
41
- return new Uint8Array(await subtle().exportKey("raw", key));
79
+ return new Uint8Array(await (await subtle()).exportKey("raw", key));
42
80
  }
43
81
  /** Import a raw uncompressed P-256 public key (65 bytes) into a CryptoKey. */
44
- export function importPublicKey(raw) {
45
- return subtle().importKey("raw", raw, { name: "ECDH", namedCurve: "P-256" }, true, []);
82
+ export async function importPublicKey(raw) {
83
+ return (await subtle()).importKey("raw", raw, { name: "ECDH", namedCurve: "P-256" }, true, []);
46
84
  }
47
85
  /**
48
86
  * Derive a 32-byte shared secret from a local ECDH private key and a remote
@@ -50,15 +88,16 @@ export function importPublicKey(raw) {
50
88
  * protocol's `priv.ECDH(remotePub)` output exactly.
51
89
  */
52
90
  export async function deriveSharedSecret(privateKey, remotePublicKey) {
53
- return new Uint8Array(await subtle().deriveBits({ name: "ECDH", public: remotePublicKey }, privateKey, 256));
91
+ return new Uint8Array(await (await subtle()).deriveBits({ name: "ECDH", public: remotePublicKey }, privateKey, 256));
54
92
  }
55
93
  /** Encrypt with AES-256-GCM. Output: nonce(12) || ciphertext || tag(16). */
56
94
  export async function encrypt(key, plaintext) {
57
95
  if (key.length !== AES_KEY_BYTES)
58
96
  throw new Error(`AES key must be ${AES_KEY_BYTES} bytes`);
59
- const aesKey = await subtle().importKey("raw", key, "AES-GCM", false, ["encrypt"]);
60
- const nonce = randomBytes(GCM_NONCE_BYTES);
61
- const ctPlusTag = new Uint8Array(await subtle().encrypt({ name: "AES-GCM", iv: nonce }, aesKey, plaintext));
97
+ const s = await subtle();
98
+ const aesKey = await s.importKey("raw", key, "AES-GCM", false, ["encrypt"]);
99
+ const nonce = await randomBytes(GCM_NONCE_BYTES);
100
+ const ctPlusTag = new Uint8Array(await s.encrypt({ name: "AES-GCM", iv: nonce }, aesKey, plaintext));
62
101
  const out = new Uint8Array(GCM_NONCE_BYTES + ctPlusTag.byteLength);
63
102
  out.set(nonce, 0);
64
103
  out.set(ctPlusTag, GCM_NONCE_BYTES);
@@ -70,10 +109,11 @@ export async function decrypt(key, ciphertext) {
70
109
  throw new Error(`AES key must be ${AES_KEY_BYTES} bytes`);
71
110
  if (ciphertext.length < GCM_NONCE_BYTES + 16)
72
111
  throw new Error("ciphertext too short");
73
- const aesKey = await subtle().importKey("raw", key, "AES-GCM", false, ["decrypt"]);
112
+ const s = await subtle();
113
+ const aesKey = await s.importKey("raw", key, "AES-GCM", false, ["decrypt"]);
74
114
  const nonce = ciphertext.slice(0, GCM_NONCE_BYTES);
75
115
  const body = ciphertext.slice(GCM_NONCE_BYTES);
76
- return new Uint8Array(await subtle().decrypt({ name: "AES-GCM", iv: nonce }, aesKey, body));
116
+ return new Uint8Array(await s.decrypt({ name: "AES-GCM", iv: nonce }, aesKey, body));
77
117
  }
78
118
  /**
79
119
  * Wrap (encrypt) a 32-byte session key for delivery to a remote party (e.g. the
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
@@ -76,7 +76,7 @@ export const JOB_REGISTRY_CONSUMER_ABI = [
76
76
  export async function prepareSession(gateway, modelTag) {
77
77
  const id = modelId(modelTag);
78
78
  const selected = await gateway.selectSession(id);
79
- const sessionKey = generateSessionKey();
79
+ const sessionKey = await generateSessionKey();
80
80
  // Workers' pubkeys arrive as base64; disputer's as hex - decodePublicKey
81
81
  // accepts either.
82
82
  const workerPub = await importPublicKey(decodePublicKey(selected.workerEncryptionKey));
@@ -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
  // ----------------------------------------------------------------------------
@@ -497,26 +497,44 @@ export async function runInferenceWithKey(args) {
497
497
  // the caller doesn't need a second import; in browsers + Node it works the
498
498
  // same against the consumer-api gateway.
499
499
  const gwBase = args.gatewayUrl ?? consumerGatewayUrlFn(networkId);
500
- const chRes = await fetch(`${gwBase}/api/auth/challenge?address=${account.address}`, {
501
- headers: { Accept: "application/json" },
502
- });
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");
503
521
  if (!chRes.ok)
504
522
  throw new GatewayAuthError(chRes.status, await chRes.text());
505
523
  const ch = (await chRes.json());
506
524
  if (!ch.message)
507
525
  throw new GatewayAuthError(chRes.status, "auth challenge returned no message");
508
526
  const signature = await wallet.signMessage({ account, message: ch.message });
509
- const verifyRes = await fetch(`${gwBase}/api/auth/verify`, {
527
+ const verifyRes = await fetchOrFail(`${gwBase}/api/auth/verify`, {
510
528
  method: "POST",
511
529
  headers: { "Content-Type": "application/json", Accept: "application/json" },
512
530
  body: JSON.stringify({ message: ch.message, signature }),
513
- });
531
+ }, "verify");
514
532
  if (!verifyRes.ok)
515
533
  throw new GatewayAuthError(verifyRes.status, await verifyRes.text());
516
534
  const verify = (await verifyRes.json());
517
535
  if (!verify.token)
518
536
  throw new GatewayAuthError(verifyRes.status, "auth verify returned no token");
519
- 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 });
520
538
  // Pick a WebSocket: the browser global if present, otherwise the caller-
521
539
  // supplied ctor. We deliberately do NOT try to dynamic-import "ws" - it
522
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.4",
3
+ "version": "0.4.6",
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",