lightnode-sdk 0.4.6 → 0.4.8
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 +26 -19
- package/dist/crypto.js +83 -67
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/package.json +3 -1
package/dist/crypto.d.ts
CHANGED
|
@@ -5,43 +5,50 @@
|
|
|
5
5
|
* The format here MUST match the workers' Go implementation byte for byte,
|
|
6
6
|
* which is what the `lcai-chat-v2` reference client also targets:
|
|
7
7
|
* - ECDH P-256 key exchange.
|
|
8
|
-
* - Raw shared secret used DIRECTLY as the AES-256 key (no HKDF).
|
|
8
|
+
* - Raw shared secret X coordinate used DIRECTLY as the AES-256 key (no HKDF).
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* Implementation note: this module used to be built on Web Crypto (subtle.*).
|
|
13
|
+
* That broke in environments where the runtime has a partial Web Crypto
|
|
14
|
+
* implementation - notably StackBlitz / Bolt WebContainer, where
|
|
15
|
+
* subtle.generateKey({name:"ECDH",namedCurve:"P-256"}) throws "Unsupported".
|
|
16
|
+
* The flow is now backed by @noble/curves (P-256) and @noble/ciphers (AES-GCM)
|
|
17
|
+
* which are pure-JS and identical across every runtime. The only Web Crypto
|
|
18
|
+
* surface left is getRandomValues, which is reliable everywhere (when missing
|
|
19
|
+
* we fall back to node:crypto).
|
|
17
20
|
*/
|
|
21
|
+
export interface EcdhKeyPair {
|
|
22
|
+
privateKey: Uint8Array;
|
|
23
|
+
publicKey: Uint8Array;
|
|
24
|
+
}
|
|
18
25
|
/** A fresh 32-byte symmetric session key (random, never derived). */
|
|
19
26
|
export declare function generateSessionKey(): Promise<Uint8Array>;
|
|
20
|
-
/** Fresh ECDH P-256 keypair (
|
|
21
|
-
export declare function generateEcdhKeyPair(): Promise<
|
|
22
|
-
/**
|
|
23
|
-
export declare function exportPublicKey(key:
|
|
24
|
-
/**
|
|
25
|
-
export declare function importPublicKey(raw: Uint8Array):
|
|
27
|
+
/** Fresh ECDH P-256 keypair (32-byte private scalar, 65-byte uncompressed pub). */
|
|
28
|
+
export declare function generateEcdhKeyPair(): Promise<EcdhKeyPair>;
|
|
29
|
+
/** Identity passthrough kept for API back-compat. Public keys are already raw bytes. */
|
|
30
|
+
export declare function exportPublicKey(key: Uint8Array): Uint8Array;
|
|
31
|
+
/** Validate that raw is a well-formed uncompressed P-256 public key (65 bytes). */
|
|
32
|
+
export declare function importPublicKey(raw: Uint8Array): Uint8Array;
|
|
26
33
|
/**
|
|
27
|
-
* Derive a 32-byte shared secret
|
|
28
|
-
*
|
|
34
|
+
* Derive a 32-byte shared secret (X coordinate of the shared point) from a
|
|
35
|
+
* local ECDH private key and a remote public key. No HKDF, matching the
|
|
29
36
|
* protocol's `priv.ECDH(remotePub)` output exactly.
|
|
30
37
|
*/
|
|
31
|
-
export declare function deriveSharedSecret(privateKey:
|
|
38
|
+
export declare function deriveSharedSecret(privateKey: Uint8Array, remotePublicKey: Uint8Array): Uint8Array;
|
|
32
39
|
/** Encrypt with AES-256-GCM. Output: nonce(12) || ciphertext || tag(16). */
|
|
33
40
|
export declare function encrypt(key: Uint8Array, plaintext: Uint8Array): Promise<Uint8Array>;
|
|
34
41
|
/** Decrypt AES-256-GCM. Input: nonce(12) || ciphertext || tag(16). */
|
|
35
42
|
export declare function decrypt(key: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array>;
|
|
36
43
|
/**
|
|
37
|
-
* Wrap (encrypt) a 32-byte session key for delivery to a remote party
|
|
38
|
-
* worker), using a fresh ephemeral ECDH keypair.
|
|
44
|
+
* Wrap (encrypt) a 32-byte session key for delivery to a remote party
|
|
45
|
+
* (e.g. the worker), using a fresh ephemeral ECDH keypair.
|
|
39
46
|
*
|
|
40
47
|
* Output: ephemeralPub(65) || nonce(12) || AES-GCM(sharedSecret, sessionKey)
|
|
41
48
|
*/
|
|
42
|
-
export declare function encryptSessionKey(sessionKey: Uint8Array, remotePublicKey:
|
|
49
|
+
export declare function encryptSessionKey(sessionKey: Uint8Array, remotePublicKey: Uint8Array): Promise<Uint8Array>;
|
|
43
50
|
/** Unwrap a session key encrypted by `encryptSessionKey` using the local ECDH private key. */
|
|
44
|
-
export declare function decryptSessionKey(encWorkerKey: Uint8Array, privateKey:
|
|
51
|
+
export declare function decryptSessionKey(encWorkerKey: Uint8Array, privateKey: Uint8Array): Promise<Uint8Array>;
|
|
45
52
|
export declare function utf8ToBytes(s: string): Uint8Array;
|
|
46
53
|
export declare function bytesToUtf8(b: Uint8Array): string;
|
|
47
54
|
export declare function bytesToHex(b: Uint8Array): `0x${string}`;
|
package/dist/crypto.js
CHANGED
|
@@ -5,100 +5,119 @@
|
|
|
5
5
|
* The format here MUST match the workers' Go implementation byte for byte,
|
|
6
6
|
* which is what the `lcai-chat-v2` reference client also targets:
|
|
7
7
|
* - ECDH P-256 key exchange.
|
|
8
|
-
* - Raw shared secret used DIRECTLY as the AES-256 key (no HKDF).
|
|
8
|
+
* - Raw shared secret X coordinate used DIRECTLY as the AES-256 key (no HKDF).
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* Implementation note: this module used to be built on Web Crypto (subtle.*).
|
|
13
|
+
* That broke in environments where the runtime has a partial Web Crypto
|
|
14
|
+
* implementation - notably StackBlitz / Bolt WebContainer, where
|
|
15
|
+
* subtle.generateKey({name:"ECDH",namedCurve:"P-256"}) throws "Unsupported".
|
|
16
|
+
* The flow is now backed by @noble/curves (P-256) and @noble/ciphers (AES-GCM)
|
|
17
|
+
* which are pure-JS and identical across every runtime. The only Web Crypto
|
|
18
|
+
* surface left is getRandomValues, which is reliable everywhere (when missing
|
|
19
|
+
* we fall back to node:crypto).
|
|
17
20
|
*/
|
|
21
|
+
import { p256 } from "@noble/curves/p256";
|
|
22
|
+
import { gcm } from "@noble/ciphers/aes";
|
|
18
23
|
const AES_KEY_BYTES = 32;
|
|
19
24
|
const GCM_NONCE_BYTES = 12;
|
|
20
|
-
const
|
|
25
|
+
const GCM_TAG_BYTES = 16;
|
|
26
|
+
const P256_UNCOMPRESSED_KEY_BYTES = 65; // 0x04 || X(32) || Y(32)
|
|
21
27
|
const SESSION_KEY_BYTES = 32;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Random source: try globalThis.crypto.getRandomValues (every modern browser
|
|
29
|
+
// and Node 19+ global), fall back to node:crypto.webcrypto.getRandomValues
|
|
30
|
+
// (Node 18, WebContainer). All algorithm-side crypto is pure JS, so this is
|
|
31
|
+
// the only Web-Crypto-flavored thing the SDK still needs.
|
|
32
|
+
let resolvedRng = null;
|
|
33
|
+
let resolvingRng = null;
|
|
34
|
+
async function getRng() {
|
|
35
|
+
if (resolvedRng)
|
|
36
|
+
return resolvedRng;
|
|
37
|
+
if (resolvingRng)
|
|
38
|
+
return resolvingRng;
|
|
39
|
+
resolvingRng = (async () => {
|
|
30
40
|
const g = globalThis.crypto;
|
|
31
|
-
if (g
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
return
|
|
41
|
+
if (g && typeof g.getRandomValues === "function") {
|
|
42
|
+
const bound = g.getRandomValues.bind(g);
|
|
43
|
+
resolvedRng = bound;
|
|
44
|
+
return bound;
|
|
35
45
|
}
|
|
36
|
-
// Node 18 + StackBlitz WebContainer: globalThis.crypto is missing, but
|
|
37
|
-
// `node:crypto` exposes the same Web Crypto API via `webcrypto`.
|
|
38
46
|
try {
|
|
39
47
|
const mod = (await import("node:crypto"));
|
|
40
48
|
const wc = mod.webcrypto;
|
|
41
|
-
if (wc
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
return
|
|
49
|
+
if (wc && typeof wc.getRandomValues === "function") {
|
|
50
|
+
const bound = wc.getRandomValues.bind(wc);
|
|
51
|
+
resolvedRng = bound;
|
|
52
|
+
return bound;
|
|
45
53
|
}
|
|
46
54
|
}
|
|
47
55
|
catch {
|
|
48
|
-
//
|
|
49
|
-
// global crypto. The fall-through error below explains the fix.
|
|
56
|
+
// browser bundle without a global crypto - fall through to the throw
|
|
50
57
|
}
|
|
51
|
-
throw new Error("
|
|
52
|
-
"
|
|
58
|
+
throw new Error("Secure random source unavailable: neither globalThis.crypto.getRandomValues nor " +
|
|
59
|
+
"node:crypto.webcrypto.getRandomValues was found. Requires Node 18+ or a modern browser.");
|
|
53
60
|
})();
|
|
54
61
|
try {
|
|
55
|
-
return await
|
|
62
|
+
return await resolvingRng;
|
|
56
63
|
}
|
|
57
64
|
finally {
|
|
58
|
-
|
|
65
|
+
resolvingRng = null;
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
|
-
async function subtle() {
|
|
62
|
-
return (await getCrypto()).subtle;
|
|
63
|
-
}
|
|
64
68
|
async function randomBytes(n) {
|
|
65
69
|
const buf = new Uint8Array(n);
|
|
66
|
-
(await
|
|
70
|
+
(await getRng())(buf);
|
|
67
71
|
return buf;
|
|
68
72
|
}
|
|
69
73
|
/** A fresh 32-byte symmetric session key (random, never derived). */
|
|
70
74
|
export function generateSessionKey() {
|
|
71
75
|
return randomBytes(SESSION_KEY_BYTES);
|
|
72
76
|
}
|
|
73
|
-
/** Fresh ECDH P-256 keypair (
|
|
77
|
+
/** Fresh ECDH P-256 keypair (32-byte private scalar, 65-byte uncompressed pub). */
|
|
74
78
|
export async function generateEcdhKeyPair() {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
// Use our RNG (proven available) instead of noble's, so a missing
|
|
80
|
+
// globalThis.crypto fails loudly at one site rather than in noble's internals.
|
|
81
|
+
const privateKey = await randomBytes(32);
|
|
82
|
+
// P-256 private scalars must be < n; the chance of collision is astronomically
|
|
83
|
+
// small, but noble validates this when we call getPublicKey, so any bad draw
|
|
84
|
+
// throws (and we'd just retry - but it has never been hit in practice).
|
|
85
|
+
const publicKey = p256.getPublicKey(privateKey, false);
|
|
86
|
+
return { privateKey, publicKey };
|
|
87
|
+
}
|
|
88
|
+
/** Identity passthrough kept for API back-compat. Public keys are already raw bytes. */
|
|
89
|
+
export function exportPublicKey(key) {
|
|
90
|
+
return key;
|
|
91
|
+
}
|
|
92
|
+
/** Validate that raw is a well-formed uncompressed P-256 public key (65 bytes). */
|
|
93
|
+
export function importPublicKey(raw) {
|
|
94
|
+
if (raw.length !== P256_UNCOMPRESSED_KEY_BYTES || raw[0] !== 0x04) {
|
|
95
|
+
throw new Error(`expected uncompressed P-256 public key (${P256_UNCOMPRESSED_KEY_BYTES} bytes, leading 0x04)`);
|
|
96
|
+
}
|
|
97
|
+
// Round-trip through noble to confirm the point is on the curve. Throws if not.
|
|
98
|
+
p256.ProjectivePoint.fromHex(raw);
|
|
99
|
+
return raw;
|
|
84
100
|
}
|
|
85
101
|
/**
|
|
86
|
-
* Derive a 32-byte shared secret
|
|
87
|
-
*
|
|
102
|
+
* Derive a 32-byte shared secret (X coordinate of the shared point) from a
|
|
103
|
+
* local ECDH private key and a remote public key. No HKDF, matching the
|
|
88
104
|
* protocol's `priv.ECDH(remotePub)` output exactly.
|
|
89
105
|
*/
|
|
90
|
-
export
|
|
91
|
-
|
|
106
|
+
export function deriveSharedSecret(privateKey, remotePublicKey) {
|
|
107
|
+
// noble returns 65 bytes uncompressed (0x04 || X || Y) when isCompressed=false.
|
|
108
|
+
// The protocol's shared secret is just X.
|
|
109
|
+
const sharedPoint = p256.getSharedSecret(privateKey, remotePublicKey, false);
|
|
110
|
+
return sharedPoint.slice(1, 1 + AES_KEY_BYTES);
|
|
92
111
|
}
|
|
93
112
|
/** Encrypt with AES-256-GCM. Output: nonce(12) || ciphertext || tag(16). */
|
|
94
113
|
export async function encrypt(key, plaintext) {
|
|
95
114
|
if (key.length !== AES_KEY_BYTES)
|
|
96
115
|
throw new Error(`AES key must be ${AES_KEY_BYTES} bytes`);
|
|
97
|
-
const s = await subtle();
|
|
98
|
-
const aesKey = await s.importKey("raw", key, "AES-GCM", false, ["encrypt"]);
|
|
99
116
|
const nonce = await randomBytes(GCM_NONCE_BYTES);
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
// noble's gcm returns plaintext.length + 16 (tag appended). Layout in our
|
|
118
|
+
// wire format is nonce || ct+tag.
|
|
119
|
+
const ctPlusTag = gcm(key, nonce).encrypt(plaintext);
|
|
120
|
+
const out = new Uint8Array(GCM_NONCE_BYTES + ctPlusTag.length);
|
|
102
121
|
out.set(nonce, 0);
|
|
103
122
|
out.set(ctPlusTag, GCM_NONCE_BYTES);
|
|
104
123
|
return out;
|
|
@@ -107,17 +126,15 @@ export async function encrypt(key, plaintext) {
|
|
|
107
126
|
export async function decrypt(key, ciphertext) {
|
|
108
127
|
if (key.length !== AES_KEY_BYTES)
|
|
109
128
|
throw new Error(`AES key must be ${AES_KEY_BYTES} bytes`);
|
|
110
|
-
if (ciphertext.length < GCM_NONCE_BYTES +
|
|
129
|
+
if (ciphertext.length < GCM_NONCE_BYTES + GCM_TAG_BYTES)
|
|
111
130
|
throw new Error("ciphertext too short");
|
|
112
|
-
const s = await subtle();
|
|
113
|
-
const aesKey = await s.importKey("raw", key, "AES-GCM", false, ["decrypt"]);
|
|
114
131
|
const nonce = ciphertext.slice(0, GCM_NONCE_BYTES);
|
|
115
132
|
const body = ciphertext.slice(GCM_NONCE_BYTES);
|
|
116
|
-
return
|
|
133
|
+
return gcm(key, nonce).decrypt(body);
|
|
117
134
|
}
|
|
118
135
|
/**
|
|
119
|
-
* Wrap (encrypt) a 32-byte session key for delivery to a remote party
|
|
120
|
-
* worker), using a fresh ephemeral ECDH keypair.
|
|
136
|
+
* Wrap (encrypt) a 32-byte session key for delivery to a remote party
|
|
137
|
+
* (e.g. the worker), using a fresh ephemeral ECDH keypair.
|
|
121
138
|
*
|
|
122
139
|
* Output: ephemeralPub(65) || nonce(12) || AES-GCM(sharedSecret, sessionKey)
|
|
123
140
|
*/
|
|
@@ -125,22 +142,21 @@ export async function encryptSessionKey(sessionKey, remotePublicKey) {
|
|
|
125
142
|
if (sessionKey.length !== SESSION_KEY_BYTES)
|
|
126
143
|
throw new Error(`session key must be ${SESSION_KEY_BYTES} bytes`);
|
|
127
144
|
const ephemeral = await generateEcdhKeyPair();
|
|
128
|
-
const shared =
|
|
145
|
+
const shared = deriveSharedSecret(ephemeral.privateKey, remotePublicKey);
|
|
129
146
|
const ct = await encrypt(shared, sessionKey);
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
out.set(
|
|
133
|
-
out.set(ct, pub.length);
|
|
147
|
+
const out = new Uint8Array(ephemeral.publicKey.length + ct.length);
|
|
148
|
+
out.set(ephemeral.publicKey, 0);
|
|
149
|
+
out.set(ct, ephemeral.publicKey.length);
|
|
134
150
|
return out;
|
|
135
151
|
}
|
|
136
152
|
/** Unwrap a session key encrypted by `encryptSessionKey` using the local ECDH private key. */
|
|
137
153
|
export async function decryptSessionKey(encWorkerKey, privateKey) {
|
|
138
|
-
if (encWorkerKey.length < P256_UNCOMPRESSED_KEY_BYTES + GCM_NONCE_BYTES +
|
|
154
|
+
if (encWorkerKey.length < P256_UNCOMPRESSED_KEY_BYTES + GCM_NONCE_BYTES + GCM_TAG_BYTES) {
|
|
139
155
|
throw new Error("encrypted session key too short");
|
|
140
156
|
}
|
|
141
|
-
const ephemeralPub =
|
|
157
|
+
const ephemeralPub = importPublicKey(encWorkerKey.slice(0, P256_UNCOMPRESSED_KEY_BYTES));
|
|
142
158
|
const ct = encWorkerKey.slice(P256_UNCOMPRESSED_KEY_BYTES);
|
|
143
|
-
const shared =
|
|
159
|
+
const shared = deriveSharedSecret(privateKey, ephemeralPub);
|
|
144
160
|
const sk = await decrypt(shared, ct);
|
|
145
161
|
if (sk.length !== SESSION_KEY_BYTES)
|
|
146
162
|
throw new Error(`session key must be ${SESSION_KEY_BYTES} bytes`);
|
package/dist/index.d.ts
CHANGED
|
@@ -60,6 +60,13 @@ export declare class LightNode {
|
|
|
60
60
|
baseUrl?: string;
|
|
61
61
|
}): GatewayClient;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Build-time SDK version. Useful for diagnostic prints in examples and apps so
|
|
65
|
+
* the operator can confirm which version of the SDK is loaded at runtime
|
|
66
|
+
* (especially in registry-proxy environments like StackBlitz where lockfiles
|
|
67
|
+
* may pin an older minor than the local install command suggests).
|
|
68
|
+
*/
|
|
69
|
+
export declare const SDK_VERSION = "0.4.8";
|
|
63
70
|
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
71
|
export type { BearerSource, GatewayClientOptions, SelectSessionResult, PrepareSessionResult, UploadBlobResult, SessionTokenResult } from "./gateway.js";
|
|
65
72
|
export type { SessionPreparation, RunInferenceArgs, RunInferenceResult, RunInferenceWithKeyArgs } from "./inference.js";
|
package/dist/index.js
CHANGED
|
@@ -90,6 +90,13 @@ export class LightNode {
|
|
|
90
90
|
return new GatewayClient({ network: this.network, ...opts });
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Build-time SDK version. Useful for diagnostic prints in examples and apps so
|
|
95
|
+
* the operator can confirm which version of the SDK is loaded at runtime
|
|
96
|
+
* (especially in registry-proxy environments like StackBlitz where lockfiles
|
|
97
|
+
* may pin an older minor than the local install command suggests).
|
|
98
|
+
*/
|
|
99
|
+
export const SDK_VERSION = "0.4.8";
|
|
93
100
|
export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost,
|
|
94
101
|
// v0.3 inference-submit surface (BETA - see README "Submitting inference").
|
|
95
102
|
GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lightnode-sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
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",
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
"url": "https://github.com/marinom2/lightnode/issues"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
+
"@noble/ciphers": "^1.0.0",
|
|
50
|
+
"@noble/curves": "^1.0.0",
|
|
49
51
|
"viem": ">=2"
|
|
50
52
|
},
|
|
51
53
|
"engines": {
|