lightnode-sdk 0.7.8 → 0.7.10
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/auth.d.ts +106 -0
- package/dist/auth.js +144 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -1
- package/dist/inference.js +26 -17
- package/dist/networks.js +2 -0
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SIWE sign-in against LightChain's consumer-api. The gateway accepts the
|
|
3
|
+
* JWT minted here as its `Authorization: Bearer <token>` header, so this
|
|
4
|
+
* unlocks every protected gateway endpoint (selectSession, prepareSession,
|
|
5
|
+
* uploadBlob, getSessionToken).
|
|
6
|
+
*
|
|
7
|
+
* Reverse-engineered from lightchain-protocol/lcai-chat-v2 (the official
|
|
8
|
+
* chat reference app). The same two endpoints work on mainnet and testnet
|
|
9
|
+
* - only the host differs (see {@link NetworkConfig.consumerApi}).
|
|
10
|
+
*
|
|
11
|
+
* 1. `GET /api/auth/challenge?address=0x...`
|
|
12
|
+
* -> { nonce: string, message: string }
|
|
13
|
+
* The server pre-builds the SIWE message; clients sign it verbatim.
|
|
14
|
+
*
|
|
15
|
+
* 2. `POST /api/auth/verify` body { message, signature }
|
|
16
|
+
* -> { success, address, token, user }
|
|
17
|
+
* `token` is the JWT bearer for the gateway.
|
|
18
|
+
*
|
|
19
|
+
* Returns a {@link SiweSession} that the SDK's GatewayClient consumes as
|
|
20
|
+
* its `bearer` source. The session also exposes `expiresAt` so consumers
|
|
21
|
+
* can refresh proactively.
|
|
22
|
+
*/
|
|
23
|
+
import type { NetworkId, NetworkConfig } from "./types.js";
|
|
24
|
+
/**
|
|
25
|
+
* Minimal subset of viem's `WalletClient` we need to sign the SIWE
|
|
26
|
+
* message. Accepts viem's `WalletClient`, wagmi's `useWalletClient().data`,
|
|
27
|
+
* or any structurally compatible object with `account.address` and
|
|
28
|
+
* `signMessage`.
|
|
29
|
+
*/
|
|
30
|
+
export interface SiweWalletClient {
|
|
31
|
+
account?: {
|
|
32
|
+
address?: `0x${string}`;
|
|
33
|
+
};
|
|
34
|
+
signMessage(args: {
|
|
35
|
+
account: `0x${string}` | {
|
|
36
|
+
address: `0x${string}`;
|
|
37
|
+
};
|
|
38
|
+
message: string;
|
|
39
|
+
}): Promise<`0x${string}`>;
|
|
40
|
+
}
|
|
41
|
+
export interface SiweChallenge {
|
|
42
|
+
nonce: string;
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
45
|
+
export interface SiweVerifyResult {
|
|
46
|
+
success: boolean;
|
|
47
|
+
address: `0x${string}`;
|
|
48
|
+
token: string;
|
|
49
|
+
user?: {
|
|
50
|
+
id: string;
|
|
51
|
+
username?: string | null;
|
|
52
|
+
walletAddress: `0x${string}`;
|
|
53
|
+
type: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export interface SiweSession {
|
|
57
|
+
/** ES256K JWT bearer accepted by the worker-gateway. */
|
|
58
|
+
token: string;
|
|
59
|
+
/** Wallet that signed the message. */
|
|
60
|
+
address: `0x${string}`;
|
|
61
|
+
/** Network the session is bound to. */
|
|
62
|
+
network: NetworkId;
|
|
63
|
+
/** SIWE message expiry as a unix-ms timestamp (null when not parseable). */
|
|
64
|
+
expiresAt: number | null;
|
|
65
|
+
/**
|
|
66
|
+
* Drop-in `BearerSource` for `new GatewayClient({ bearer })`. Returns
|
|
67
|
+
* the same JWT for every call; refresh by calling {@link siweSignIn}
|
|
68
|
+
* again before {@link expiresAt} elapses.
|
|
69
|
+
*/
|
|
70
|
+
bearer: () => string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Fetch a SIWE challenge for `address` from the network's consumer-api.
|
|
74
|
+
* The returned `message` is the canonical SIWE string the wallet must
|
|
75
|
+
* sign verbatim - do NOT reformat or strip whitespace.
|
|
76
|
+
*/
|
|
77
|
+
export declare function siweChallenge(network: NetworkId | NetworkConfig, address: `0x${string}`, opts?: {
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
}): Promise<SiweChallenge>;
|
|
80
|
+
/**
|
|
81
|
+
* Verify a signed SIWE message and mint a JWT bearer.
|
|
82
|
+
*/
|
|
83
|
+
export declare function siweVerify(network: NetworkId | NetworkConfig, args: {
|
|
84
|
+
message: string;
|
|
85
|
+
signature: `0x${string}`;
|
|
86
|
+
}, opts?: {
|
|
87
|
+
signal?: AbortSignal;
|
|
88
|
+
}): Promise<SiweVerifyResult>;
|
|
89
|
+
/**
|
|
90
|
+
* End-to-end SIWE sign-in: challenge -> sign -> verify -> JWT.
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* import { siweSignIn, GatewayClient } from "lightnode-sdk";
|
|
94
|
+
* // browser: walletClient from wagmi's useWalletClient().data
|
|
95
|
+
* const session = await siweSignIn(walletClient, "testnet");
|
|
96
|
+
* const gateway = new GatewayClient({ network: "testnet", bearer: session.bearer });
|
|
97
|
+
* // ... use the gateway normally (runInference, selectSession, etc.)
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* Pass `address` explicitly when the wallet client cannot expose its own
|
|
101
|
+
* account (some viem clients are intentionally accountless).
|
|
102
|
+
*/
|
|
103
|
+
export declare function siweSignIn(walletClient: SiweWalletClient, network: NetworkId | NetworkConfig, opts?: {
|
|
104
|
+
address?: `0x${string}`;
|
|
105
|
+
signal?: AbortSignal;
|
|
106
|
+
}): Promise<SiweSession>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SIWE sign-in against LightChain's consumer-api. The gateway accepts the
|
|
3
|
+
* JWT minted here as its `Authorization: Bearer <token>` header, so this
|
|
4
|
+
* unlocks every protected gateway endpoint (selectSession, prepareSession,
|
|
5
|
+
* uploadBlob, getSessionToken).
|
|
6
|
+
*
|
|
7
|
+
* Reverse-engineered from lightchain-protocol/lcai-chat-v2 (the official
|
|
8
|
+
* chat reference app). The same two endpoints work on mainnet and testnet
|
|
9
|
+
* - only the host differs (see {@link NetworkConfig.consumerApi}).
|
|
10
|
+
*
|
|
11
|
+
* 1. `GET /api/auth/challenge?address=0x...`
|
|
12
|
+
* -> { nonce: string, message: string }
|
|
13
|
+
* The server pre-builds the SIWE message; clients sign it verbatim.
|
|
14
|
+
*
|
|
15
|
+
* 2. `POST /api/auth/verify` body { message, signature }
|
|
16
|
+
* -> { success, address, token, user }
|
|
17
|
+
* `token` is the JWT bearer for the gateway.
|
|
18
|
+
*
|
|
19
|
+
* Returns a {@link SiweSession} that the SDK's GatewayClient consumes as
|
|
20
|
+
* its `bearer` source. The session also exposes `expiresAt` so consumers
|
|
21
|
+
* can refresh proactively.
|
|
22
|
+
*/
|
|
23
|
+
import { NETWORKS } from "./networks.js";
|
|
24
|
+
const REQUEST_TIMEOUT_MS = 15000;
|
|
25
|
+
function resolveNetwork(network) {
|
|
26
|
+
if (typeof network === "string") {
|
|
27
|
+
const cfg = NETWORKS[network];
|
|
28
|
+
if (!cfg)
|
|
29
|
+
throw new Error(`siweSignIn: unknown network "${network}"`);
|
|
30
|
+
return cfg;
|
|
31
|
+
}
|
|
32
|
+
return network;
|
|
33
|
+
}
|
|
34
|
+
async function httpJson(url, init) {
|
|
35
|
+
const ctrl = new AbortController();
|
|
36
|
+
const onAbort = () => ctrl.abort();
|
|
37
|
+
init.signal?.addEventListener("abort", onAbort);
|
|
38
|
+
const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(url, {
|
|
41
|
+
method: init.method,
|
|
42
|
+
headers: { Accept: "application/json", ...(init.body ? { "content-type": "application/json" } : {}) },
|
|
43
|
+
body: init.body ? JSON.stringify(init.body) : undefined,
|
|
44
|
+
signal: ctrl.signal,
|
|
45
|
+
});
|
|
46
|
+
const text = await res.text();
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
// Surface the server-side validation message when present.
|
|
49
|
+
const detail = text.length < 400 ? text : `${text.slice(0, 380)}...`;
|
|
50
|
+
throw new Error(`siwe ${url}: ${res.status} ${detail}`);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(text);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new Error(`siwe ${url}: non-JSON response (${text.slice(0, 120)})`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
init.signal?.removeEventListener("abort", onAbort);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Fetch a SIWE challenge for `address` from the network's consumer-api.
|
|
66
|
+
* The returned `message` is the canonical SIWE string the wallet must
|
|
67
|
+
* sign verbatim - do NOT reformat or strip whitespace.
|
|
68
|
+
*/
|
|
69
|
+
export async function siweChallenge(network, address, opts = {}) {
|
|
70
|
+
const cfg = resolveNetwork(network);
|
|
71
|
+
if (!cfg.consumerApi) {
|
|
72
|
+
throw new Error(`siweChallenge: network "${cfg.id}" has no consumerApi configured`);
|
|
73
|
+
}
|
|
74
|
+
const url = `${cfg.consumerApi.replace(/\/+$/, "")}/api/auth/challenge?address=${address}`;
|
|
75
|
+
return httpJson(url, { method: "GET", signal: opts.signal });
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Verify a signed SIWE message and mint a JWT bearer.
|
|
79
|
+
*/
|
|
80
|
+
export async function siweVerify(network, args, opts = {}) {
|
|
81
|
+
const cfg = resolveNetwork(network);
|
|
82
|
+
if (!cfg.consumerApi) {
|
|
83
|
+
throw new Error(`siweVerify: network "${cfg.id}" has no consumerApi configured`);
|
|
84
|
+
}
|
|
85
|
+
const url = `${cfg.consumerApi.replace(/\/+$/, "")}/api/auth/verify`;
|
|
86
|
+
const out = await httpJson(url, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
body: { message: args.message, signature: args.signature },
|
|
89
|
+
signal: opts.signal,
|
|
90
|
+
});
|
|
91
|
+
if (!out.success || !out.token) {
|
|
92
|
+
throw new Error("siweVerify: gateway returned success=false or missing token");
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Extract a SIWE message's `Expiration Time` (ISO-8601) and convert to a
|
|
98
|
+
* unix-ms timestamp. Best-effort: returns null if the field is absent or
|
|
99
|
+
* unparseable.
|
|
100
|
+
*/
|
|
101
|
+
function parseExpiry(message) {
|
|
102
|
+
const m = message.match(/Expiration Time:\s*(\S+)/);
|
|
103
|
+
if (!m)
|
|
104
|
+
return null;
|
|
105
|
+
const t = Date.parse(m[1]);
|
|
106
|
+
return Number.isFinite(t) ? t : null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* End-to-end SIWE sign-in: challenge -> sign -> verify -> JWT.
|
|
110
|
+
*
|
|
111
|
+
* ```ts
|
|
112
|
+
* import { siweSignIn, GatewayClient } from "lightnode-sdk";
|
|
113
|
+
* // browser: walletClient from wagmi's useWalletClient().data
|
|
114
|
+
* const session = await siweSignIn(walletClient, "testnet");
|
|
115
|
+
* const gateway = new GatewayClient({ network: "testnet", bearer: session.bearer });
|
|
116
|
+
* // ... use the gateway normally (runInference, selectSession, etc.)
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* Pass `address` explicitly when the wallet client cannot expose its own
|
|
120
|
+
* account (some viem clients are intentionally accountless).
|
|
121
|
+
*/
|
|
122
|
+
export async function siweSignIn(walletClient, network, opts = {}) {
|
|
123
|
+
const cfg = resolveNetwork(network);
|
|
124
|
+
const address = opts.address ?? walletClient.account?.address;
|
|
125
|
+
if (!address) {
|
|
126
|
+
throw new Error("siweSignIn: walletClient has no account; pass `address` explicitly");
|
|
127
|
+
}
|
|
128
|
+
const { message } = await siweChallenge(cfg, address, { signal: opts.signal });
|
|
129
|
+
// viem's signMessage requires `account` even when one is set on the
|
|
130
|
+
// client; passing it explicitly works with both wagmi and bare viem.
|
|
131
|
+
const signature = await walletClient.signMessage({
|
|
132
|
+
account: walletClient.account?.address ?? address,
|
|
133
|
+
message,
|
|
134
|
+
});
|
|
135
|
+
const verified = await siweVerify(cfg, { message, signature }, { signal: opts.signal });
|
|
136
|
+
const token = verified.token;
|
|
137
|
+
return {
|
|
138
|
+
token,
|
|
139
|
+
address: verified.address ?? address,
|
|
140
|
+
network: cfg.id,
|
|
141
|
+
expiresAt: parseExpiry(message),
|
|
142
|
+
bearer: () => token,
|
|
143
|
+
};
|
|
144
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { DAO, DAO_ADDRESSES, ProposalState, PROPOSAL_STATE_LABEL, VoteSupport, G
|
|
|
12
12
|
import { OnchainModelRegistry, AIVM_MODEL_REGISTRY_ABI, BENCHMARK_REGISTRY_ABI, ModelStatus, MODEL_STATUS_LABEL } from "./onchain-models.js";
|
|
13
13
|
import { StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker } from "./errors.js";
|
|
14
14
|
import { GatewayClient, GatewayHttpError } from "./gateway.js";
|
|
15
|
+
import { siweSignIn, siweChallenge, siweVerify } from "./auth.js";
|
|
15
16
|
import * as crypto from "./crypto.js";
|
|
16
17
|
import type { NetworkId, NetworkConfig, Worker, Job, JobTransactions, ModelInfo, WorkerModel, ServedModel, NetworkStats, ModelStat, WorkerStat, NetworkAnalytics } from "./types.js";
|
|
17
18
|
/**
|
|
@@ -133,8 +134,8 @@ export declare class LightNode {
|
|
|
133
134
|
* (especially in registry-proxy environments like StackBlitz where lockfiles
|
|
134
135
|
* may pin an older minor than the local install command suggests).
|
|
135
136
|
*/
|
|
136
|
-
export declare const SDK_VERSION = "0.7.
|
|
137
|
-
export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, resolveJobTransactions, fetchWorkerModels, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, runInferenceStream, Conversation, chat, runInferenceBatch, Agent, parseAgentOutput, workerPreflight, workerWatch, Bridge, BRIDGE_ROUTE, HYPERLANE_ROUTER_ABI, ERC20_ABI, addressToBytes32, quoteBridgeFee, bridgeableBalance, bridgeAllowance, approveBridge, bridgeTransfer, DAO, DAO_ADDRESSES, ProposalState, PROPOSAL_STATE_LABEL, VoteSupport, GOVERNOR_ABI, VOTES_ABI, OnchainModelRegistry, AIVM_MODEL_REGISTRY_ABI, BENCHMARK_REGISTRY_ABI, ModelStatus, MODEL_STATUS_LABEL, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, WorkerOperator, WORKER_REGISTRY_ABI, JOB_REGISTRY_OPERATOR_ABI, AI_CONFIG_ABI, JOB_STATE, decodeWorkerError, WorkerOpError, isWorkerOpError, };
|
|
137
|
+
export declare const SDK_VERSION = "0.7.10";
|
|
138
|
+
export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, resolveJobTransactions, siweSignIn, siweChallenge, siweVerify, fetchWorkerModels, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, runInferenceStream, Conversation, chat, runInferenceBatch, Agent, parseAgentOutput, workerPreflight, workerWatch, Bridge, BRIDGE_ROUTE, HYPERLANE_ROUTER_ABI, ERC20_ABI, addressToBytes32, quoteBridgeFee, bridgeableBalance, bridgeAllowance, approveBridge, bridgeTransfer, DAO, DAO_ADDRESSES, ProposalState, PROPOSAL_STATE_LABEL, VoteSupport, GOVERNOR_ABI, VOTES_ABI, OnchainModelRegistry, AIVM_MODEL_REGISTRY_ABI, BENCHMARK_REGISTRY_ABI, ModelStatus, MODEL_STATUS_LABEL, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, WorkerOperator, WORKER_REGISTRY_ABI, JOB_REGISTRY_OPERATOR_ABI, AI_CONFIG_ABI, JOB_STATE, decodeWorkerError, WorkerOpError, isWorkerOpError, };
|
|
138
139
|
export type { BearerSource, GatewayClientOptions, SelectSessionResult, PrepareSessionResult, UploadBlobResult, SessionTokenResult } from "./gateway.js";
|
|
139
140
|
export type { SessionPreparation, RunInferenceArgs, RunInferenceResult, RunInferenceWithKeyArgs, RunInferenceStreamResult } from "./inference.js";
|
|
140
141
|
export type { ChatRole, ChatMessage, ConversationOptions, ConversationSendResult } from "./chat.js";
|
|
@@ -146,3 +147,4 @@ export type { DaoChain, DaoAddresses, ProposalSummary, ProposalRow, DaoConfig }
|
|
|
146
147
|
export type { BaseModel, ModelVariant, AccessTier, AccessPolicy, Benchmark, OnchainModelRegistryOptions } from "./onchain-models.js";
|
|
147
148
|
export type { MinimalWalletClient, MinimalPublicClient, WorkerOperatorOpts, WorkerProtocolConfig, WorkerStatus, DeregisterReadiness, StuckJob, EarningsBreakdown, OnchainJob, JobState, DecodedWorkerError, } from "./worker-operator.js";
|
|
148
149
|
export type { NetworkId, NetworkConfig, Worker, Job, JobTransactions, ModelInfo, WorkerModel, ServedModel, NetworkStats, ModelStat, WorkerStat, NetworkAnalytics };
|
|
150
|
+
export type { SiweWalletClient, SiweChallenge, SiweVerifyResult, SiweSession } from "./auth.js";
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { DAO, DAO_ADDRESSES, ProposalState, PROPOSAL_STATE_LABEL, VoteSupport, G
|
|
|
13
13
|
import { OnchainModelRegistry, AIVM_MODEL_REGISTRY_ABI, BENCHMARK_REGISTRY_ABI, ModelStatus, MODEL_STATUS_LABEL, } from "./onchain-models.js";
|
|
14
14
|
import { StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, } from "./errors.js";
|
|
15
15
|
import { GatewayClient, GatewayHttpError } from "./gateway.js";
|
|
16
|
+
import { siweSignIn, siweChallenge, siweVerify } from "./auth.js";
|
|
16
17
|
import * as crypto from "./crypto.js";
|
|
17
18
|
/**
|
|
18
19
|
* Read-only client for a LightChain AI network. Pure reads from the public indexer
|
|
@@ -212,11 +213,15 @@ export class LightNode {
|
|
|
212
213
|
* (especially in registry-proxy environments like StackBlitz where lockfiles
|
|
213
214
|
* may pin an older minor than the local install command suggests).
|
|
214
215
|
*/
|
|
215
|
-
export const SDK_VERSION = "0.7.
|
|
216
|
+
export const SDK_VERSION = "0.7.10";
|
|
216
217
|
export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei,
|
|
217
218
|
// v0.7.3 per-job transaction-hash resolver (lifts the upstream
|
|
218
219
|
// subgraph's "block-only" Job entity to a deep-linkable Job + tx pair).
|
|
219
220
|
resolveJobTransactions,
|
|
221
|
+
// v0.7.10 SIWE sign-in against the consumer-api: returns a JWT bearer
|
|
222
|
+
// the worker-gateway accepts. End-to-end wallet-signed inference with
|
|
223
|
+
// no shared demo-wallet state.
|
|
224
|
+
siweSignIn, siweChallenge, siweVerify,
|
|
220
225
|
// v0.7.4 per-worker model-registration list (the authoritative "what is
|
|
221
226
|
// this worker offering to serve" signal, not derived from past jobs).
|
|
222
227
|
fetchWorkerModels, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost,
|
package/dist/inference.js
CHANGED
|
@@ -78,25 +78,35 @@ export async function prepareSession(gateway, modelTag) {
|
|
|
78
78
|
// The gateway returns 409 selection_mismatch when a NEWER selectSession()
|
|
79
79
|
// for the same wallet supersedes ours between the select and the prepare.
|
|
80
80
|
// The error message is literally "re-run POST /api/sessions/select", so we
|
|
81
|
-
// do exactly that: rebuild from a fresh selection.
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
const
|
|
81
|
+
// do exactly that: rebuild from a fresh selection. Backoff spans several
|
|
82
|
+
// seconds because the gateway's selection TTL can be in that range - quick
|
|
83
|
+
// retries inside that window just hit the same stuck state. Random jitter
|
|
84
|
+
// keeps two concurrent callers from synchronising on identical schedules.
|
|
85
|
+
const MAX_ATTEMPTS = 6;
|
|
86
|
+
const BACKOFFS_MS = [0, 500, 1500, 4000, 9000, 18000];
|
|
87
|
+
const jitter = (ms) => ms + Math.floor(Math.random() * 250);
|
|
86
88
|
let lastErr = null;
|
|
89
|
+
const isSelectionMismatch = (e) => {
|
|
90
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
91
|
+
return /selection_mismatch|selection was superseded|409/.test(msg);
|
|
92
|
+
};
|
|
87
93
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
88
94
|
if (attempt > 0)
|
|
89
|
-
await new Promise((r) => setTimeout(r, BACKOFFS_MS[attempt]));
|
|
90
|
-
const selected = await gateway.selectSession(id);
|
|
91
|
-
const sessionKey = await generateSessionKey();
|
|
92
|
-
// Workers' pubkeys arrive as base64; disputer's as hex - decodePublicKey
|
|
93
|
-
// accepts either.
|
|
94
|
-
const workerPub = await importPublicKey(decodePublicKey(selected.workerEncryptionKey));
|
|
95
|
-
const encWorker = await encryptSessionKey(sessionKey, workerPub);
|
|
96
|
-
const encDisputer = selected.disputerEncryptionKey
|
|
97
|
-
? await encryptSessionKey(sessionKey, await importPublicKey(decodePublicKey(selected.disputerEncryptionKey)))
|
|
98
|
-
: new Uint8Array(0);
|
|
95
|
+
await new Promise((r) => setTimeout(r, jitter(BACKOFFS_MS[attempt])));
|
|
99
96
|
try {
|
|
97
|
+
// Wrap BOTH gateway calls. selectSession itself can 409 if a newer
|
|
98
|
+
// call has already superseded ours by the time the gateway processes
|
|
99
|
+
// it. prepareSession 409s when the same happens between select and
|
|
100
|
+
// prepare. The whole select -> prepare flow is one atomic unit.
|
|
101
|
+
const selected = await gateway.selectSession(id);
|
|
102
|
+
const sessionKey = await generateSessionKey();
|
|
103
|
+
// Workers' pubkeys arrive as base64; disputer's as hex - decodePublicKey
|
|
104
|
+
// accepts either.
|
|
105
|
+
const workerPub = await importPublicKey(decodePublicKey(selected.workerEncryptionKey));
|
|
106
|
+
const encWorker = await encryptSessionKey(sessionKey, workerPub);
|
|
107
|
+
const encDisputer = selected.disputerEncryptionKey
|
|
108
|
+
? await encryptSessionKey(sessionKey, await importPublicKey(decodePublicKey(selected.disputerEncryptionKey)))
|
|
109
|
+
: new Uint8Array(0);
|
|
100
110
|
const prepared = await gateway.prepareSession({
|
|
101
111
|
modelId: id,
|
|
102
112
|
encWorkerKey: bytesToBase64(encWorker),
|
|
@@ -117,8 +127,7 @@ export async function prepareSession(gateway, modelTag) {
|
|
|
117
127
|
}
|
|
118
128
|
catch (e) {
|
|
119
129
|
lastErr = e;
|
|
120
|
-
|
|
121
|
-
if (!/selection_mismatch|selection was superseded|409/.test(msg))
|
|
130
|
+
if (!isSelectionMismatch(e))
|
|
122
131
|
throw e;
|
|
123
132
|
// else loop: a newer select stole this session, try again from select.
|
|
124
133
|
}
|
package/dist/networks.js
CHANGED
|
@@ -12,6 +12,7 @@ export const NETWORKS = {
|
|
|
12
12
|
rpc: "https://rpc.mainnet.lightchain.ai",
|
|
13
13
|
explorer: "https://mainnet.lightscan.app",
|
|
14
14
|
workerGateway: "https://worker-gateway.mainnet.lightchain.ai",
|
|
15
|
+
consumerApi: "https://chat-api.mainnet.lightchain.ai",
|
|
15
16
|
subgraph: "https://workers-api.mainnet.lightchain.ai/graphql",
|
|
16
17
|
workerRegistry: "0x0000000000000000000000000000000000001002",
|
|
17
18
|
aiConfig: "0x24D11533C354092ed6E18b964257819cE78Ce77D",
|
|
@@ -31,6 +32,7 @@ export const NETWORKS = {
|
|
|
31
32
|
rpc: "https://rpc.testnet.lightchain.ai",
|
|
32
33
|
explorer: "https://testnet.lightscan.app",
|
|
33
34
|
workerGateway: "https://worker-gateway.testnet.lightchain.ai",
|
|
35
|
+
consumerApi: "https://chat-api.testnet.lightchain.ai",
|
|
34
36
|
subgraph: "https://workers-api.testnet.lightchain.ai/graphql",
|
|
35
37
|
workerRegistry: "0x0000000000000000000000000000000000001002",
|
|
36
38
|
aiConfig: "0xeCF4Ca5Ba6D97ae586993e170764a1E92231b67e",
|
package/dist/types.d.ts
CHANGED
|
@@ -6,6 +6,16 @@ export interface NetworkConfig {
|
|
|
6
6
|
rpc: string;
|
|
7
7
|
explorer: string;
|
|
8
8
|
workerGateway: string;
|
|
9
|
+
/**
|
|
10
|
+
* LightChain consumer-api host (SIWE sign-in + JWT issuance). The worker
|
|
11
|
+
* gateway accepts the JWT minted here as its Authorization Bearer.
|
|
12
|
+
*
|
|
13
|
+
* - `GET {consumerApi}/api/auth/challenge?address=0x...` -> { nonce, message }
|
|
14
|
+
* - `POST {consumerApi}/api/auth/verify` body { message, signature } -> { token, ... }
|
|
15
|
+
*
|
|
16
|
+
* Wrapped end-to-end by {@link siweSignIn}.
|
|
17
|
+
*/
|
|
18
|
+
consumerApi: string;
|
|
9
19
|
subgraph: string;
|
|
10
20
|
/** Genesis predeploy, same address on both networks. */
|
|
11
21
|
workerRegistry: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lightnode-sdk",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.10",
|
|
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",
|