@zbase-protocol/core 0.1.1 → 0.1.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.
@@ -1,15 +1,23 @@
1
1
  /**
2
2
  * @zbase-protocol/core/experimental — UNFINISHED UTXO surface. NOT PRODUCTION.
3
3
  *
4
- * ⚠️ These variable-amount UTXO primitives are a PRE-DEPLOYMENT scaffold. The UTXO
5
- * pool is not deployed (routes return 501) and the trusted-setup ceremony has not
6
- * run. The 2026-07-09 SDK audit found integrator fund-lock footguns here:
7
- * - the note wire format is inconsistent (packCiphertext is length-prefixed but
8
- * the scanner decodes a fixed bytes32[4]) → a recipient may be unable to
9
- * decrypt their own note;
4
+ * ⚠️⚠️ DO NOT USE WITH REAL FUNDS. These variable-amount UTXO primitives are a
5
+ * PRE-DEPLOYMENT scaffold. The UTXO pool is not deployed (routes return 501) and
6
+ * the trusted-setup ceremony has not run. The 2026-07-09 SDK + black-hat audits
7
+ * confirmed integrator FUND-LOSS footguns here, still present by design until the
8
+ * post-ceremony rewrite:
9
+ * - WIRE-FORMAT BREAK (HIGH): encryptNoteForTransfer produces a ~330-byte
10
+ * length-prefixed envelope (packCiphertext), but the scanner's
11
+ * decodeTransferredLog reassembles a FIXED 128-byte bytes32[4] blob →
12
+ * decryption returns null → a recipient CANNOT see or spend their own inbound
13
+ * note. Building a wallet on createScanner today will LOSE received funds.
10
14
  * - createNote defaults NPK material to 0 → a note addressable to nobody;
11
- * - deriveViewingKeyFromSeed diverges from the mnemonic path for non-64-byte
12
- * seeds recovery can silently fail.
15
+ * - deserializeNote now range-validates (fixed), but malformed notes throw;
16
+ * - deriveViewingKeyFromSeed now requires the exact 64-byte BIP39 seed (fixed).
17
+ *
18
+ * The wire-format break is NOT fixed here (it needs the frozen post-ceremony
19
+ * schema); it is quarantined behind this subpath so no main-entry integrator hits
20
+ * it. If you import this, you accept these are unfinished and fund-unsafe.
13
21
  *
14
22
  * This surface lives on a SEPARATE subpath (not the main entry) so no integrator
15
23
  * reaches it by accident: you must explicitly `import ... from
@@ -1,15 +1,23 @@
1
1
  /**
2
2
  * @zbase-protocol/core/experimental — UNFINISHED UTXO surface. NOT PRODUCTION.
3
3
  *
4
- * ⚠️ These variable-amount UTXO primitives are a PRE-DEPLOYMENT scaffold. The UTXO
5
- * pool is not deployed (routes return 501) and the trusted-setup ceremony has not
6
- * run. The 2026-07-09 SDK audit found integrator fund-lock footguns here:
7
- * - the note wire format is inconsistent (packCiphertext is length-prefixed but
8
- * the scanner decodes a fixed bytes32[4]) → a recipient may be unable to
9
- * decrypt their own note;
4
+ * ⚠️⚠️ DO NOT USE WITH REAL FUNDS. These variable-amount UTXO primitives are a
5
+ * PRE-DEPLOYMENT scaffold. The UTXO pool is not deployed (routes return 501) and
6
+ * the trusted-setup ceremony has not run. The 2026-07-09 SDK + black-hat audits
7
+ * confirmed integrator FUND-LOSS footguns here, still present by design until the
8
+ * post-ceremony rewrite:
9
+ * - WIRE-FORMAT BREAK (HIGH): encryptNoteForTransfer produces a ~330-byte
10
+ * length-prefixed envelope (packCiphertext), but the scanner's
11
+ * decodeTransferredLog reassembles a FIXED 128-byte bytes32[4] blob →
12
+ * decryption returns null → a recipient CANNOT see or spend their own inbound
13
+ * note. Building a wallet on createScanner today will LOSE received funds.
10
14
  * - createNote defaults NPK material to 0 → a note addressable to nobody;
11
- * - deriveViewingKeyFromSeed diverges from the mnemonic path for non-64-byte
12
- * seeds recovery can silently fail.
15
+ * - deserializeNote now range-validates (fixed), but malformed notes throw;
16
+ * - deriveViewingKeyFromSeed now requires the exact 64-byte BIP39 seed (fixed).
17
+ *
18
+ * The wire-format break is NOT fixed here (it needs the frozen post-ceremony
19
+ * schema); it is quarantined behind this subpath so no main-entry integrator hits
20
+ * it. If you import this, you accept these are unfinished and fund-unsafe.
13
21
  *
14
22
  * This surface lives on a SEPARATE subpath (not the main entry) so no integrator
15
23
  * reaches it by accident: you must explicitly `import ... from
@@ -19,13 +19,38 @@
19
19
  */
20
20
  export type FacilitatorNetwork = "eip155:84532" | "eip155:8453";
21
21
  export interface FacilitatorClientConfig {
22
- /** Base URL of the zBase deployment, e.g. "https://zbase.app" or "http://localhost:3009". */
22
+ /**
23
+ * Base URL of the zBase deployment, e.g. "https://zbase.app".
24
+ *
25
+ * ⚠️ SECURITY (CRITICAL, 2026-07-09): `baseUrl` is FULLY TRUSTED WITH YOUR SPEND
26
+ * SECRETS. In the current server-side-proving model, `settlePrivately` sends the
27
+ * deposit's `{nullifier, secret}` — the COMPLETE spend authority for the pool
28
+ * note — to this URL. A malicious or man-in-the-middle'd `baseUrl` can withdraw
29
+ * your funds. Therefore:
30
+ * - `https:` is REQUIRED (an on-path attacker cannot read an HTTPS body).
31
+ * - `http://localhost` / `http://127.0.0.1` is allowed for local dev only.
32
+ * - any other `http:` URL is REJECTED unless you explicitly set
33
+ * `allowInsecureHttp: true` (do this ONLY on a trusted private network).
34
+ * - Always PIN this URL to a deployment you control/trust; never take it from
35
+ * an untrusted discovery response or user input.
36
+ */
23
37
  baseUrl: string;
24
38
  /** CAIP-2 network. Defaults to Base Sepolia. */
25
39
  network?: FacilitatorNetwork;
26
40
  /** Optional fetch override (for tests / non-browser runtimes). */
27
41
  fetchImpl?: typeof fetch;
42
+ /**
43
+ * Explicitly allow a non-localhost `http://` baseUrl. Only for trusted private
44
+ * networks — this transmits spend secrets in cleartext. Default false.
45
+ */
46
+ allowInsecureHttp?: boolean;
28
47
  }
48
+ /**
49
+ * Validate the facilitator baseUrl before we ever send spend secrets to it.
50
+ * Throws on a URL that would leak secrets to an on-path attacker. Exported for
51
+ * testing the guard.
52
+ */
53
+ export declare function assertSafeFacilitatorBaseUrl(baseUrl: string, allowInsecureHttp?: boolean): URL;
29
54
  /** Deposit secrets the payer keeps locally; the input to every later settle. */
30
55
  export interface DepositSecrets {
31
56
  nullifier: string;
@@ -18,11 +18,39 @@
18
18
  * mainnet). Zero new dependencies — fetch only.
19
19
  */
20
20
  import { generateDepositSecrets, computePrecommitment } from "./account.js";
21
+ /**
22
+ * Validate the facilitator baseUrl before we ever send spend secrets to it.
23
+ * Throws on a URL that would leak secrets to an on-path attacker. Exported for
24
+ * testing the guard.
25
+ */
26
+ export function assertSafeFacilitatorBaseUrl(baseUrl, allowInsecureHttp = false) {
27
+ let u;
28
+ try {
29
+ u = new URL(baseUrl);
30
+ }
31
+ catch {
32
+ throw new Error(`FacilitatorClient: baseUrl is not a valid URL: ${baseUrl}`);
33
+ }
34
+ if (u.protocol === "https:")
35
+ return u;
36
+ if (u.protocol === "http:") {
37
+ const host = u.hostname.toLowerCase();
38
+ const isLocal = host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
39
+ if (isLocal || allowInsecureHttp)
40
+ return u;
41
+ throw new Error(`FacilitatorClient: refusing an insecure http:// baseUrl (${baseUrl}). settlePrivately sends your spend secrets to this host — use https, or set allowInsecureHttp:true only on a trusted private network.`);
42
+ }
43
+ throw new Error(`FacilitatorClient: unsupported baseUrl protocol '${u.protocol}' — use https.`);
44
+ }
21
45
  export class FacilitatorClient {
22
46
  base;
23
47
  network;
24
48
  doFetch;
25
49
  constructor(cfg) {
50
+ // CRITICAL C3 fix (2026-07-09): validate baseUrl BEFORE any secret is sent to
51
+ // it. settlePrivately transmits the deposit spend secrets; an http:// or
52
+ // malformed baseUrl would leak them to an on-path attacker → fund theft.
53
+ assertSafeFacilitatorBaseUrl(cfg.baseUrl, cfg.allowInsecureHttp ?? false);
26
54
  this.base = cfg.baseUrl.replace(/\/$/, "");
27
55
  this.network = cfg.network ?? "eip155:84532";
28
56
  const f = cfg.fetchImpl ?? globalThis.fetch;
package/dist/index.d.ts CHANGED
@@ -18,7 +18,7 @@ export type { ProofInput, Groth16Proof, ProofResult } from "./proofs.js";
18
18
  export { generateWithdrawalProof, verifyProofLocally } from "./proofs.js";
19
19
  export { buildMerkleTree, generateMerkleProof, getTreeDepth } from "./merkle.js";
20
20
  export type { FacilitatorClientConfig, FacilitatorNetwork, DepositSecrets, PreparedDeposit, VerifyResult, SettleResult, } from "./facilitatorClient.js";
21
- export { FacilitatorClient, createFacilitatorClient } from "./facilitatorClient.js";
21
+ export { FacilitatorClient, createFacilitatorClient, assertSafeFacilitatorBaseUrl } from "./facilitatorClient.js";
22
22
  export type { BuildSwapCallPlanOpts } from "./swapPlan.js";
23
23
  export { buildSwapCallPlan, EXACT_INPUT_SINGLE_SELECTOR, } from "./swapPlan.js";
24
24
  export type { ForwardingNoteSecrets } from "./forwardingNotes.js";
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ export { generateDepositSecrets, computeCommitment, computePrecommitment, comput
17
17
  export { generateWithdrawalProof, verifyProofLocally } from "./proofs.js";
18
18
  // Merkle tree
19
19
  export { buildMerkleTree, generateMerkleProof, getTreeDepth } from "./merkle.js";
20
- export { FacilitatorClient, createFacilitatorClient } from "./facilitatorClient.js";
20
+ export { FacilitatorClient, createFacilitatorClient, assertSafeFacilitatorBaseUrl } from "./facilitatorClient.js";
21
21
  export { buildSwapCallPlan, EXACT_INPUT_SINGLE_SELECTOR, } from "./swapPlan.js";
22
22
  export { deriveForwardingNote, precommitmentForRelayer, recoverForwardingNotes, ZBASE_FORWARDING_PATH_PREFIX, } from "./forwardingNotes.js";
23
23
  export { STEALTH_SCHEME_ID, DEFAULT_CHAIN_TAG, generateMetaAddress, parseMetaAddress, isStealthMetaAddress, deriveStealthAddress, scanForPayments, computeStealthPrivateKey, } from "./stealth.js";
package/dist/notes.js CHANGED
@@ -29,7 +29,7 @@ import { xchacha20poly1305 } from "@noble/ciphers/chacha";
29
29
  import { keccak_256 } from "@noble/hashes/sha3";
30
30
  import { randomBytes } from "@noble/hashes/utils";
31
31
  // Shared, unbiased (rejection-sampling) field sampler — see account.ts (audit F7).
32
- import { randomFieldElement } from "./account.js";
32
+ import { randomFieldElement, SNARK_SCALAR_FIELD } from "./account.js";
33
33
  // -----------------------------------------------------------------------------
34
34
  // Note creation
35
35
  // -----------------------------------------------------------------------------
@@ -290,7 +290,7 @@ export function serializeNote(n) {
290
290
  }
291
291
  export function deserializeNote(s) {
292
292
  const o = JSON.parse(s);
293
- return {
293
+ const note = {
294
294
  amount: BigInt(o.amount),
295
295
  label: BigInt(o.label),
296
296
  // Tolerate pre-v1 serialized notes (no NPK fields) by defaulting to 0 —
@@ -300,6 +300,26 @@ export function deserializeNote(s) {
300
300
  nullifier: BigInt(o.nullifier),
301
301
  secret: BigInt(o.secret),
302
302
  };
303
+ // HIGH fix (SDK audit 2026-07-09): RANGE-VALIDATE on deserialize. A corrupted /
304
+ // malicious serialized note with an out-of-range amount (>= 2^64) or an
305
+ // out-of-field element silently produces a commitment the circuit can never
306
+ // accept → funds locked (or, unchecked, value-confusion). Reject early with a
307
+ // clear error instead of failing opaquely at proof time.
308
+ if (note.amount < 0n || note.amount >= MAX_NOTE_AMOUNT) {
309
+ throw new NoteValueError(`deserializeNote: amount out of range [0, 2^64): ${note.amount}`);
310
+ }
311
+ for (const [name, v] of [
312
+ ["label", note.label],
313
+ ["nullifier", note.nullifier],
314
+ ["secret", note.secret],
315
+ ["spendingPK", note.spendingPK],
316
+ ["viewingPKBlind", note.viewingPKBlind],
317
+ ]) {
318
+ if (v < 0n || v >= SNARK_SCALAR_FIELD) {
319
+ throw new NoteValueError(`deserializeNote: ${name} is not a valid field element (>= SNARK field): ${v}`);
320
+ }
321
+ }
322
+ return note;
303
323
  }
304
324
  // -----------------------------------------------------------------------------
305
325
  // Viewing-key cryptography
@@ -135,8 +135,17 @@ export function deriveViewingKeyFromMnemonic(mnemonic, index = 0) {
135
135
  * The mnemonic field on the returned ViewingKey will be `undefined`.
136
136
  */
137
137
  export function deriveViewingKeyFromSeed(seed, index = 0) {
138
- if (seed.length < 32) {
139
- throw new Error(`deriveViewingKeyFromSeed: seed must be 32 bytes, got ${seed.length}`);
138
+ // HIGH fix (SDK audit 2026-07-09): require EXACTLY the 64-byte BIP39 seed
139
+ // (`mnemonicToSeedSync(mnemonic)`). The prior `>= 32` check let a caller pass
140
+ // 32-byte entropy (or any 32+ bytes) that derives a DIFFERENT viewing key than
141
+ // the mnemonic path — so a wallet "recovering" from the wrong-length seed scans
142
+ // the chain, finds none of its own notes, and concludes funds are lost. Only
143
+ // the 64-byte BIP39 seed matches deriveViewingKeyFromMnemonic. Enforce it.
144
+ if (seed.length !== 64) {
145
+ throw new Error(`deriveViewingKeyFromSeed: seed must be the 64-byte BIP39 seed (output of ` +
146
+ `mnemonicToSeedSync), got ${seed.length} bytes. Passing raw entropy or a ` +
147
+ `different length derives a DIFFERENT key than the mnemonic path and ` +
148
+ `breaks note recovery. Use deriveViewingKeyFromMnemonic if you have the phrase.`);
140
149
  }
141
150
  const derivationPath = pathFor(index);
142
151
  const node = HDKey.fromMasterSeed(seed).derive(derivationPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zbase-protocol/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Chain-agnostic ZK privacy core for x402 AI agent payments. Poseidon hashing, Merkle trees, Groth16 proof generation, account/secrets management. Used by @zbase-protocol/svm and @zbase-protocol/mcp.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",