From 657d3a484a3a6d320362d646f7bd2bb5d743f656 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Fri, 28 Aug 2026 04:44:34 -0300 Subject: [PATCH] fix(deepseek-web): replace unlicensed PoW artifacts (#11732) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged via /merge-batch (v3.8.51 provenance sweep). Boarded and validated together with the batch's other provenance/dependency PRs in a combined worktree — full gate suite green. Thank you. --- .../11732-remove-unlicensed-deepseek-wasm.md | 1 + config/quality/eslint-suppressions.json | 5 - next.config.mjs | 4 +- open-sse/executors/deepseek-web.ts | 15 +- open-sse/lib/deepseek-pow-hash.js | 450 ++++++++++++++++++ open-sse/lib/deepseek-pow-solver.cjs | 3 - open-sse/lib/deepseek-pow-worker.mjs | 20 + open-sse/lib/deepseek-pow.ts | 291 ++++++----- open-sse/lib/sha3_wasm_bg.wasm | Bin 26612 -> 0 bytes tests/unit/deepseek-pow-js-only.test.ts | 238 +++++++++ tests/unit/deepseek-pow-solver-strict.test.ts | 21 - tests/unit/deepseek-web.test.ts | 81 +++- 12 files changed, 916 insertions(+), 213 deletions(-) create mode 100644 changelog.d/maintenance/11732-remove-unlicensed-deepseek-wasm.md create mode 100644 open-sse/lib/deepseek-pow-hash.js delete mode 100644 open-sse/lib/deepseek-pow-solver.cjs create mode 100644 open-sse/lib/deepseek-pow-worker.mjs delete mode 100644 open-sse/lib/sha3_wasm_bg.wasm create mode 100644 tests/unit/deepseek-pow-js-only.test.ts delete mode 100644 tests/unit/deepseek-pow-solver-strict.test.ts diff --git a/changelog.d/maintenance/11732-remove-unlicensed-deepseek-wasm.md b/changelog.d/maintenance/11732-remove-unlicensed-deepseek-wasm.md new file mode 100644 index 0000000000..95af43647c --- /dev/null +++ b/changelog.d/maintenance/11732-remove-unlicensed-deepseek-wasm.md @@ -0,0 +1 @@ +- **chore(deepseek-web):** remove the provenance-unresolved DeepSeek PoW WASM binary and its runtime loader/tracing while retaining the existing JavaScript solver (slower at high difficulty) ([#11732](https://github.com/diegosouzapw/OmniRoute/pull/11732)). diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index 86fc92a447..9e7aca2f54 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -317,11 +317,6 @@ "count": 1 } }, - "open-sse/lib/deepseek-pow.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 5 - } - }, "open-sse/mcp-server/__tests__/a2aLifecycle.test.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 diff --git a/next.config.mjs b/next.config.mjs index ef67001f34..296a514bb8 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -244,8 +244,8 @@ const nextConfig = { "./src/mitm/server.cjs", "./open-sse/services/compression/engines/rtk/filters/**/*.json", "./open-sse/services/compression/rules/**/*.json", - "./open-sse/lib/sha3_wasm_bg.wasm", - "./open-sse/lib/deepseek-pow-solver.cjs", + "./open-sse/lib/deepseek-pow-hash.js", + "./open-sse/lib/deepseek-pow-worker.mjs", // sql.js WASM is loaded at runtime by the sqljsAdapter fallback tier // (better-sqlite3 → node:sqlite → sql.js). Next traces sql-wasm.js but can // omit the runtime sql-wasm.wasm asset from the standalone bundle. diff --git a/open-sse/executors/deepseek-web.ts b/open-sse/executors/deepseek-web.ts index 2b0fccb489..914024be82 100644 --- a/open-sse/executors/deepseek-web.ts +++ b/open-sse/executors/deepseek-web.ts @@ -141,13 +141,14 @@ function generateFakeCookie(): string { // ── PoW Solver (DeepSeekHashV1) ───────────────────────────────────────── -async function solvePow(challenge: PowChallenge): Promise { +async function solvePow(challenge: PowChallenge, signal?: AbortSignal | null): Promise { const answer = await solveDeepSeekPowAsync( challenge.algorithm, challenge.challenge, challenge.salt, challenge.difficulty, - challenge.expire_at + challenge.expire_at, + { signal } ); if (answer < 0) throw new Error("PoW solver failed"); return Buffer.from( @@ -566,11 +567,7 @@ export function messagesToPrompt( } const effectiveWindow = - historyWindow > 0 - ? historyWindow - : conversation.length > 1 - ? DEFAULT_AUTO_HISTORY_WINDOW - : 0; + historyWindow > 0 ? historyWindow : conversation.length > 1 ? DEFAULT_AUTO_HISTORY_WINDOW : 0; if (effectiveWindow > 0 && conversation.length > 1) { // Rolling-window transcript of the most recent turns (#2942, auto-applied per @@ -932,7 +929,7 @@ export class DeepSeekWebExecutor extends BaseExecutor { // One completion attempt against a given session id (fresh PoW per attempt). const performCompletion = async (sid: string) => { const powChallenge = await getPowChallenge(accessToken, signal); - const powAnswer = await solvePow(powChallenge); + const powAnswer = await solvePow(powChallenge, signal); const reqHeaders: Record = { ...FAKE_HEADERS, "Content-Type": "application/json", @@ -1145,7 +1142,7 @@ export class DeepSeekWebExecutor extends BaseExecutor { const msg = err instanceof Error ? err.message : String(err); log?.error?.("DEEPSEEK-WEB", `Execute failed: ${msg}`); - if (err instanceof DOMException && err.name === "AbortError") { + if (err instanceof Error && err.name === "AbortError") { return { response: errorResponse(499, "Request cancelled"), url: COMPLETION_URL, diff --git a/open-sse/lib/deepseek-pow-hash.js b/open-sse/lib/deepseek-pow-hash.js new file mode 100644 index 0000000000..10941df9b4 --- /dev/null +++ b/open-sse/lib/deepseek-pow-hash.js @@ -0,0 +1,450 @@ +// Clean-room reference derived from NIST FIPS 202, Algorithms 1-9. FIPS SHA3-256 +// uses KECCAK-p[1600,24]. Differential testing against the supplied black-box +// vectors identifies DeepSeekHashV1 as the same sponge construction with +// KECCAK-p[1600,23] (the last 23 rounds, with round indices 1 through 23). + +const LANE_MASK = (1n << 64n) - 1n; +const SHA3_256_RATE_BYTES = 136; +const SHA3_DOMAIN_SUFFIX = 0x06; +const SHA3_256_OUTPUT_BYTES = 32; + +// Indexed as x + 5*y, matching the FIPS 202 state coordinates. +const ROTATION_OFFSETS = [ + 0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14, +]; + +const ROUND_CONSTANTS = [ + 0x0000000000000001n, + 0x0000000000008082n, + 0x800000000000808an, + 0x8000000080008000n, + 0x000000000000808bn, + 0x0000000080000001n, + 0x8000000080008081n, + 0x8000000000008009n, + 0x000000000000008an, + 0x0000000000000088n, + 0x0000000080008009n, + 0x000000008000000an, + 0x000000008000808bn, + 0x800000000000008bn, + 0x8000000000008089n, + 0x8000000000008003n, + 0x8000000000008002n, + 0x8000000000000080n, + 0x000000000000800an, + 0x800000008000000an, + 0x8000000080008081n, + 0x8000000000008080n, + 0x0000000080000001n, + 0x8000000080008008n, +]; + +/** + * @param {bigint} value + * @param {number} amount + * @returns {bigint} + */ +function rotateLeft64(value, amount) { + if (amount === 0) return value; + const shift = BigInt(amount); + return ((value << shift) | (value >> (64n - shift))) & LANE_MASK; +} + +/** + * Apply the last `roundCount` rounds of KECCAK-p[1600, roundCount]. + * + * @param {bigint[]} state + * @param {number} roundCount + */ +function keccakP1600Reference(state, roundCount) { + /** @type {bigint[]} */ + const columnParity = new Array(5).fill(0n); + /** @type {bigint[]} */ + const thetaMix = new Array(5).fill(0n); + /** @type {bigint[]} */ + const rhoPiState = new Array(25).fill(0n); + const firstRound = ROUND_CONSTANTS.length - roundCount; + + for (let round = firstRound; round < ROUND_CONSTANTS.length; round++) { + for (let x = 0; x < 5; x++) { + columnParity[x] = state[x] ^ state[x + 5] ^ state[x + 10] ^ state[x + 15] ^ state[x + 20]; + } + for (let x = 0; x < 5; x++) { + thetaMix[x] = columnParity[(x + 4) % 5] ^ rotateLeft64(columnParity[(x + 1) % 5], 1); + } + for (let y = 0; y < 5; y++) { + for (let x = 0; x < 5; x++) state[x + 5 * y] ^= thetaMix[x]; + } + + for (let y = 0; y < 5; y++) { + for (let x = 0; x < 5; x++) { + const destinationX = y; + const destinationY = (2 * x + 3 * y) % 5; + const lane = x + 5 * y; + rhoPiState[destinationX + 5 * destinationY] = rotateLeft64( + state[lane], + ROTATION_OFFSETS[lane] + ); + } + } + + for (let y = 0; y < 5; y++) { + const row = 5 * y; + for (let x = 0; x < 5; x++) { + state[x + row] = + rhoPiState[x + row] ^ + (~rhoPiState[((x + 1) % 5) + row] & LANE_MASK & rhoPiState[((x + 2) % 5) + row]); + } + } + state[0] ^= ROUND_CONSTANTS[round]; + } +} + +/** + * @param {bigint[]} state + * @param {Uint8Array} block + * @param {number} roundCount + */ +function absorbReferenceBlock(state, block, roundCount) { + for (let index = 0; index < SHA3_256_RATE_BYTES; index++) { + const lane = Math.floor(index / 8); + const shift = BigInt((index % 8) * 8); + state[lane] ^= BigInt(block[index]) << shift; + } + keccakP1600Reference(state, roundCount); +} + +/** + * SHA3-256's sponge parameters with a selectable KECCAK-p round count. + * + * @param {string} input + * @param {number} roundCount + * @returns {string} + */ +function sha3_256ReferenceWithRoundCount(input, roundCount) { + const bytes = new TextEncoder().encode(input); + /** @type {bigint[]} */ + const state = new Array(25).fill(0n); + let offset = 0; + + while (offset + SHA3_256_RATE_BYTES <= bytes.length) { + absorbReferenceBlock(state, bytes.subarray(offset, offset + SHA3_256_RATE_BYTES), roundCount); + offset += SHA3_256_RATE_BYTES; + } + + const finalBlock = new Uint8Array(SHA3_256_RATE_BYTES); + finalBlock.set(bytes.subarray(offset)); + finalBlock[bytes.length - offset] ^= SHA3_DOMAIN_SUFFIX; + finalBlock[SHA3_256_RATE_BYTES - 1] ^= 0x80; + absorbReferenceBlock(state, finalBlock, roundCount); + + const output = new Uint8Array(SHA3_256_OUTPUT_BYTES); + for (let index = 0; index < output.length; index++) { + const lane = Math.floor(index / 8); + const shift = BigInt((index % 8) * 8); + output[index] = Number((state[lane] >> shift) & 0xffn); + } + return Array.from(output, (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +/** + * Readable FIPS 202 control implementation used to validate the permutation, + * byte order, padding, and multi-block absorption against `node:crypto`. + * + * @param {string} input + * @returns {string} + */ +export function sha3_256Fips202Reference(input) { + return sha3_256ReferenceWithRoundCount(input, 24); +} + +/** + * Readable DeepSeekHashV1 reference model. Runtime searches use an equivalent + * 32-bit implementation below so a bounded synchronous compatibility call does + * not turn a 144k challenge into minutes of BigInt work. + * + * @param {string} input + * @returns {string} + */ +export function deepSeekHashV1Reference(input) { + return sha3_256ReferenceWithRoundCount(input, 23); +} + +const DEEPSEEK_HASH_ROUNDS = 23; +const DIGEST_HEX_PATTERN = /^[a-f0-9]{64}$/i; +const ROUND_CONSTANTS_LOW = Uint32Array.from(ROUND_CONSTANTS, (value) => + Number(value & 0xffffffffn) +); +const ROUND_CONSTANTS_HIGH = Uint32Array.from(ROUND_CONSTANTS, (value) => + Number((value >> 32n) & 0xffffffffn) +); +const RHO_PI_DESTINATION_WORDS = Uint8Array.from({ length: 25 }, (_, lane) => { + const x = lane % 5; + const y = Math.floor(lane / 5); + return 2 * (y + 5 * ((2 * x + 3 * y) % 5)); +}); +const CHI_NEXT_WORDS = Uint8Array.from({ length: 25 }, (_, lane) => { + const x = lane % 5; + const row = lane - x; + return 2 * (row + ((x + 1) % 5)); +}); +const CHI_NEXT_NEXT_WORDS = Uint8Array.from({ length: 25 }, (_, lane) => { + const x = lane % 5; + const row = lane - x; + return 2 * (row + ((x + 2) % 5)); +}); +const HEX_DIGITS = "0123456789abcdef"; + +export const MAX_DEEPSEEK_POW_DIFFICULTY = 250_000; + +/** + * Equivalent 32-bit form of the reference permutation. Each 64-bit lane is + * stored as adjacent little-endian low/high uint32 words. + * + * @param {Uint32Array} state + * @param {Uint32Array} rhoPiState + * @param {Uint32Array} columnParity + * @param {Uint32Array} thetaMix + * @param {number} roundCount + */ +function keccakP1600Uint32(state, rhoPiState, columnParity, thetaMix, roundCount) { + const firstRound = ROUND_CONSTANTS.length - roundCount; + + for (let round = firstRound; round < ROUND_CONSTANTS.length; round++) { + for (let x = 0; x < 5; x++) { + const word = 2 * x; + columnParity[word] = + state[word] ^ state[word + 10] ^ state[word + 20] ^ state[word + 30] ^ state[word + 40]; + columnParity[word + 1] = + state[word + 1] ^ state[word + 11] ^ state[word + 21] ^ state[word + 31] ^ state[word + 41]; + } + + for (let x = 0; x < 5; x++) { + const previous = 2 * ((x + 4) % 5); + const next = 2 * ((x + 1) % 5); + const rotatedLow = (columnParity[next] << 1) | (columnParity[next + 1] >>> 31); + const rotatedHigh = (columnParity[next + 1] << 1) | (columnParity[next] >>> 31); + thetaMix[2 * x] = columnParity[previous] ^ rotatedLow; + thetaMix[2 * x + 1] = columnParity[previous + 1] ^ rotatedHigh; + } + + for (let x = 0; x < 5; x++) { + const word = 2 * x; + const low = thetaMix[word]; + const high = thetaMix[word + 1]; + state[word] ^= low; + state[word + 1] ^= high; + state[word + 10] ^= low; + state[word + 11] ^= high; + state[word + 20] ^= low; + state[word + 21] ^= high; + state[word + 30] ^= low; + state[word + 31] ^= high; + state[word + 40] ^= low; + state[word + 41] ^= high; + } + + rhoPiState[0] = state[0]; + rhoPiState[1] = state[1]; + for (let lane = 1; lane < 25; lane++) { + const source = 2 * lane; + const destination = RHO_PI_DESTINATION_WORDS[lane]; + const amount = ROTATION_OFFSETS[lane]; + const low = state[source]; + const high = state[source + 1]; + + if (amount < 32) { + rhoPiState[destination] = (low << amount) | (high >>> (32 - amount)); + rhoPiState[destination + 1] = (high << amount) | (low >>> (32 - amount)); + } else { + const reduced = amount - 32; + rhoPiState[destination] = (high << reduced) | (low >>> (32 - reduced)); + rhoPiState[destination + 1] = (low << reduced) | (high >>> (32 - reduced)); + } + } + + for (let lane = 0; lane < 25; lane++) { + const word = 2 * lane; + const nextWord = CHI_NEXT_WORDS[lane]; + const nextNextWord = CHI_NEXT_NEXT_WORDS[lane]; + state[word] = rhoPiState[word] ^ (~rhoPiState[nextWord] & rhoPiState[nextNextWord]); + state[word + 1] = + rhoPiState[word + 1] ^ (~rhoPiState[nextWord + 1] & rhoPiState[nextNextWord + 1]); + } + + state[0] ^= ROUND_CONSTANTS_LOW[round]; + state[1] ^= ROUND_CONSTANTS_HIGH[round]; + } +} + +/** + * @param {Uint32Array} state + * @param {Uint8Array} bytes + * @param {number} offset + * @param {Uint32Array} rhoPiState + * @param {Uint32Array} columnParity + * @param {Uint32Array} thetaMix + * @param {number} roundCount + */ +function absorbFullUint32Block( + state, + bytes, + offset, + rhoPiState, + columnParity, + thetaMix, + roundCount +) { + for (let index = 0; index < SHA3_256_RATE_BYTES; index++) { + const word = index >>> 2; + state[word] ^= bytes[offset + index] << ((index & 3) * 8); + } + keccakP1600Uint32(state, rhoPiState, columnParity, thetaMix, roundCount); +} + +/** + * @param {Uint32Array} state + * @returns {string} + */ +function digestStateToHex(state) { + let digest = ""; + for (let index = 0; index < SHA3_256_OUTPUT_BYTES; index++) { + const byte = (state[index >>> 2] >>> ((index & 3) * 8)) & 0xff; + digest += HEX_DIGITS[byte >>> 4] + HEX_DIGITS[byte & 0x0f]; + } + return digest; +} + +/** + * @param {string} input + * @param {number} roundCount + * @returns {string} + */ +function sha3_256Uint32WithRoundCount(input, roundCount) { + const bytes = new TextEncoder().encode(input); + const state = new Uint32Array(50); + const rhoPiState = new Uint32Array(50); + const columnParity = new Uint32Array(10); + const thetaMix = new Uint32Array(10); + let offset = 0; + + while (offset + SHA3_256_RATE_BYTES <= bytes.length) { + absorbFullUint32Block(state, bytes, offset, rhoPiState, columnParity, thetaMix, roundCount); + offset += SHA3_256_RATE_BYTES; + } + + const remaining = bytes.length - offset; + for (let index = 0; index < remaining; index++) { + state[index >>> 2] ^= bytes[offset + index] << ((index & 3) * 8); + } + state[remaining >>> 2] ^= SHA3_DOMAIN_SUFFIX << ((remaining & 3) * 8); + state[(SHA3_256_RATE_BYTES - 1) >>> 2] ^= 0x80 << 24; + keccakP1600Uint32(state, rhoPiState, columnParity, thetaMix, roundCount); + return digestStateToHex(state); +} + +/** + * Optimized, allocation-bounded DeepSeekHashV1 digest. + * + * @param {string} input + * @returns {string} + */ +export function deepSeekHashV1(input) { + return sha3_256Uint32WithRoundCount(input, DEEPSEEK_HASH_ROUNDS); +} + +/** + * @param {string} digestHex + * @returns {Uint32Array} + */ +function parseDigestWords(digestHex) { + const words = new Uint32Array(SHA3_256_OUTPUT_BYTES / 4); + for (let index = 0; index < SHA3_256_OUTPUT_BYTES; index++) { + const byte = Number.parseInt(digestHex.slice(index * 2, index * 2 + 2), 16); + words[index >>> 2] |= byte << ((index & 3) * 8); + } + return words; +} + +/** + * Search `prefix + nonce` without allocating a digest or re-encoding the prefix + * for every candidate. Inputs are validated here as well as at the public solver + * boundary so the worker cannot be coerced into an unbounded loop. + * + * @param {string} prefix + * @param {string} challenge + * @param {number} difficulty + * @returns {number} + */ +export function findDeepSeekPowNonce(prefix, challenge, difficulty) { + if (typeof prefix !== "string") throw new TypeError("DeepSeek PoW prefix must be a string"); + if (!DIGEST_HEX_PATTERN.test(challenge)) { + throw new TypeError("DeepSeek PoW challenge must be a 64-character hex digest"); + } + if ( + !Number.isSafeInteger(difficulty) || + difficulty < 1 || + difficulty > MAX_DEEPSEEK_POW_DIFFICULTY + ) { + throw new RangeError( + `DeepSeek PoW difficulty must be an integer from 1 to ${MAX_DEEPSEEK_POW_DIFFICULTY}` + ); + } + + const prefixBytes = new TextEncoder().encode(prefix); + const baseState = new Uint32Array(50); + const rhoPiState = new Uint32Array(50); + const columnParity = new Uint32Array(10); + const thetaMix = new Uint32Array(10); + let prefixOffset = 0; + + while (prefixOffset + SHA3_256_RATE_BYTES <= prefixBytes.length) { + absorbFullUint32Block( + baseState, + prefixBytes, + prefixOffset, + rhoPiState, + columnParity, + thetaMix, + DEEPSEEK_HASH_ROUNDS + ); + prefixOffset += SHA3_256_RATE_BYTES; + } + + const tailLength = prefixBytes.length - prefixOffset; + const tailWords = new Uint32Array(Math.ceil(tailLength / 4)); + for (let index = 0; index < tailLength; index++) { + tailWords[index >>> 2] ^= prefixBytes[prefixOffset + index] << ((index & 3) * 8); + } + + const targetWords = parseDigestWords(challenge.toLowerCase()); + const state = new Uint32Array(50); + + nonceLoop: for (let nonce = 0; nonce < difficulty; nonce++) { + state.set(baseState); + for (let word = 0; word < tailWords.length; word++) state[word] ^= tailWords[word]; + + let position = tailLength; + const nonceText = String(nonce); + for (let index = 0; index < nonceText.length; index++) { + state[position >>> 2] ^= nonceText.charCodeAt(index) << ((position & 3) * 8); + position += 1; + if (position === SHA3_256_RATE_BYTES) { + keccakP1600Uint32(state, rhoPiState, columnParity, thetaMix, DEEPSEEK_HASH_ROUNDS); + position = 0; + } + } + + state[position >>> 2] ^= SHA3_DOMAIN_SUFFIX << ((position & 3) * 8); + state[(SHA3_256_RATE_BYTES - 1) >>> 2] ^= 0x80 << 24; + keccakP1600Uint32(state, rhoPiState, columnParity, thetaMix, DEEPSEEK_HASH_ROUNDS); + + for (let word = 0; word < targetWords.length; word++) { + if (state[word] !== targetWords[word]) continue nonceLoop; + } + return nonce; + } + + return -1; +} diff --git a/open-sse/lib/deepseek-pow-solver.cjs b/open-sse/lib/deepseek-pow-solver.cjs deleted file mode 100644 index 4b4ab62722..0000000000 --- a/open-sse/lib/deepseek-pow-solver.cjs +++ /dev/null @@ -1,3 +0,0 @@ -const r=(id)=>{if(id===46743)return{Buffer};return{}};const t={},e={}; -"use strict";let n,i;r(42551),r(40966),r(70968),r(76966),r(35399),r(36279),r(87801),r(16389),r(36073),r(27448),r(10681),r(32014),r(46596),r(39008),r(71),r(85540);var o=r(46743),f=Object.create,u=Object.defineProperty,s=Object.getOwnPropertyDescriptor,a=Object.getOwnPropertyNames,c=Object.getPrototypeOf,h=Object.prototype.hasOwnProperty,l=(t,e,r)=>(r=null!=t?f(c(t)):{},((t,e,r,n)=>{if(e&&"object"==typeof e||"function"==typeof e)for(let i of a(e))h.call(t,i)||i===r||u(t,i,{get:()=>e[i],enumerable:!(n=s(e,i))||n.enumerable});return t})(!e&&t&&t.__esModule?r:u(r,"default",{value:t,enumerable:!0}),t)),p=(n=(t,e)=>{e.exports=(t,e)=>(r,n)=>{let i=2*n,o=2*e;r[i]=t[o],r[i+1]=t[o+1]}},()=>(i||n((i={exports:{}}).exports,i),i.exports)),g=l(p()),y=t=>{let{A:e,C:r}=t;for(let t=0;t<25;t+=5){for(let n=0;n<5;n++)(0,g.default)(e,t+n)(r,n);for(let n=0;n<5;n++){let i=(t+n)*2,o=(n+1)%5*2,f=(n+2)%5*2;e[i]^=~r[o]&r[f],e[i+1]^=~r[o+1]&r[f+1]}}},d=new Uint32Array([0,1,0,32898,0x80000000,32906,0x80000000,0x80008000,0,32907,0,0x80000001,0x80000000,0x80008081,0x80000000,32777,0,138,0,136,0,0x80008009,0,0x8000000a,0,0x8000808b,0x80000000,139,0x80000000,32905,0x80000000,32771,0x80000000,32770,0x80000000,128,0,32778,0x80000000,0x8000000a,0x80000000,0x80008081,0x80000000,32896,0,0x80000001,0x80000000,0x80008008]),b=t=>{let{A:e,I:r}=t,n=2*r;e[0]^=d[n],e[1]^=d[n+1]},v=[10,7,11,17,18,3,5,16,8,21,24,4,15,23,19,13,12,2,20,14,22,9,6,1],w=[1,3,6,10,15,21,28,36,45,55,2,14,27,41,56,8,25,43,62,18,39,61,20,44],x=l(p()),E=t=>{let{A:e,C:r,W:n}=t,i=0;(0,x.default)(e,i+1)(n,i);let o=0,f=0,u=0,s=32;for(;i<24;i++){let t=v[i],a=w[i];(0,x.default)(e,t)(r,0),o=n[0],f=n[1],s=32-a,n[u=a<32?0:1]=o<>>s,n[(u+1)%2]=f<>>s,(0,x.default)(n,0)(e,t),(0,x.default)(r,0)(n,0)}},m=l(p()),B=t=>{let{A:e,C:r,D:n,W:i}=t,o=0,f=0;for(let t=0;t<5;t++){let n=2*t,i=(t+5)*2,o=(t+10)*2,f=(t+15)*2,u=(t+20)*2;r[n]=e[n]^e[i]^e[o]^e[f]^e[u],r[n+1]=e[n+1]^e[i+1]^e[o+1]^e[f+1]^e[u+1]}for(let t=0;t<5;t++){(0,m.default)(r,(t+1)%5)(i,0),o=i[0],f=i[1],i[0]=o<<1|f>>>31,i[1]=f<<1|o>>>31,n[2*t]=r[(t+4)%5*2]^i[0],n[2*t+1]=r[(t+4)%5*2+1]^i[1];for(let r=0;r<25;r+=5)e[(r+t)*2]^=n[2*t],e[(r+t)*2+1]^=n[2*t+1]}},I=(t,e)=>{for(let r=0;r{for(let r=0;r>>8,e[r+2]=t[n+1]>>>16,e[r+3]=t[n+1]>>>24,e[r+4]=t[n],e[r+5]=t[n]>>>8,e[r+6]=t[n]>>>16,e[r+7]=t[n]>>>24}return e},U=function(t){let e,r,n,{capacity:i,padding:f}=t,u=i/8,s=200-i/4,a={keccak:(e=new Uint32Array(10),r=new Uint32Array(10),n=new Uint32Array(2),t=>{for(let i=1;i<24;i++)B({A:t,C:e,D:r,W:n}),E({A:t,C:e,W:n}),y({A:t,C:e}),b({A:t,I:i});e.fill(0),r.fill(0),n.fill(0)}),state:new Uint32Array(50),queue:o.Buffer.allocUnsafe(s),queueOffset:0};return this.getState=()=>a,this.setState=t=>{a.keccak=t.keccak,a.state.set(t.state.slice()),t.queue.copy(a.queue),a.queueOffset=t.queueOffset},this.absorb=t=>{for(let e=0;e=s&&(I(a.queue,a.state),a.keccak(a.state),a.queueOffset=0);return this},this.squeeze=t=>{let e={buffer:o.Buffer.allocUnsafe(u),padding:t,queue:o.Buffer.allocUnsafe(a.queue.length),state:new Uint32Array(a.state.length)};a.queue.copy(e.queue);for(let t=0;t(a.queue.fill(0),a.state.fill(0),a.queueOffset=0,this),this.copy=()=>{let t=new U({capacity:i,padding:f});return t.setState(this.getState()),t},this};/* #2724: guard Web Worker handler — Node.js require() loads this CJS in strict mode where bare `onmessage = ...` throws ReferenceError */typeof self!=="undefined"&&typeof postMessage!=="undefined"&&(globalThis.onmessage=t=>{if("pow-challenge"!==t.data.type)return;let{algorithm:e,challenge:r,salt:n,difficulty:i,signature:f,expireAt:u}=t.data.challenge;try{let t=((t,e,r,n,i)=>{if("DeepSeekHashV1"!==t)throw Error("Unsupported algorithm: "+t);let f="".concat(r,"_").concat(i,"_"),u=function(t,e,r){if(t.length%2!=0)throw RangeError("c.length");if(r<=0||!Number.isSafeInteger(r))throw RangeError("d");for(var n=(function t(){var e=this;return this&&this.constructor===t?(this._sponge=new U({capacity:256}),this.update=t=>{if("string"==typeof t)return this._sponge.absorb(o.Buffer.from(t,"utf8")),this;throw TypeError("input not a string")},this.digest=function(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"hex";return e._sponge.squeeze(6).toString(t)},this.copy=()=>{let e=new t;return e._sponge=this._sponge.copy(),e},this):new t})(256).update(e),i=0;i MAX_DEEPSEEK_POW_DIFFICULTY +) { + throw new Error("DeepSeek PoW worker received invalid input"); +} + +parentPort.postMessage(findDeepSeekPowNonce(prefix, challenge, difficulty)); diff --git a/open-sse/lib/deepseek-pow.ts b/open-sse/lib/deepseek-pow.ts index 29382c6fb3..ce28d05b35 100644 --- a/open-sse/lib/deepseek-pow.ts +++ b/open-sse/lib/deepseek-pow.ts @@ -1,181 +1,156 @@ -import { createRequire } from "node:module"; -import fs from "node:fs"; +import { existsSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { Worker } from "node:worker_threads"; -// ── WASM solver (fast — ~50-100ms at difficulty 144000) ────────────────── +import { findDeepSeekPowNonce, MAX_DEEPSEEK_POW_DIFFICULTY } from "./deepseek-pow-hash.js"; -class DeepSeekHashWasm { - private wasmInstance: any; - private offset = 0; - private cachedUint8Memory: Uint8Array | null = null; - private cachedTextEncoder = new TextEncoder(); +const DEEPSEEK_POW_ALGORITHM = "DeepSeekHashV1"; +const SHA3_256_HEX_PATTERN = /^[a-f0-9]{64}$/i; +const MAX_DIFFICULTY = MAX_DEEPSEEK_POW_DIFFICULTY; +const MAX_SALT_LENGTH = 1_024; +const MAX_CONCURRENT_WORKERS = 2; +const DEFAULT_TIMEOUT_MS = 10_000; +const MAX_TIMEOUT_MS = 30_000; - private getCachedUint8Memory(): Uint8Array { - if (!this.cachedUint8Memory?.byteLength) { - this.cachedUint8Memory = new Uint8Array(this.wasmInstance.memory.buffer); - } - return this.cachedUint8Memory; - } +let activeWorkerCount = 0; - private encodeString( - text: string, - allocate: (size: number, align: number) => number, - reallocate: (ptr: number, oldSize: number, newSize: number, align: number) => number - ): number { - const strLength = text.length; - let ptr = allocate(strLength, 1) >>> 0; - const memory = this.getCachedUint8Memory(); - let asciiLength = 0; - - for (; asciiLength < strLength; asciiLength++) { - if (text.charCodeAt(asciiLength) > 127) break; - memory[ptr + asciiLength] = text.charCodeAt(asciiLength); - } - - if (asciiLength !== strLength) { - if (asciiLength > 0) text = text.slice(asciiLength); - ptr = reallocate(ptr, strLength, asciiLength + text.length * 3, 1) >>> 0; - const result = this.cachedTextEncoder.encodeInto( - text, - this.getCachedUint8Memory().subarray(ptr + asciiLength, ptr + asciiLength + text.length * 3) - ); - asciiLength += result.written!; - ptr = reallocate(ptr, asciiLength + text.length * 3, asciiLength, 1) >>> 0; - } - - this.offset = asciiLength; - return ptr; - } - - calculateHash(challenge: string, prefix: string, difficulty: number): number | undefined { - try { - const retptr = this.wasmInstance.__wbindgen_add_to_stack_pointer(-16); - - const ptr0 = this.encodeString( - challenge, - this.wasmInstance.__wbindgen_export_0, - this.wasmInstance.__wbindgen_export_1 - ); - const len0 = this.offset; - - const ptr1 = this.encodeString( - prefix, - this.wasmInstance.__wbindgen_export_0, - this.wasmInstance.__wbindgen_export_1 - ); - const len1 = this.offset; - - this.wasmInstance.wasm_solve(retptr, ptr0, len0, ptr1, len1, difficulty); - - const dv = new DataView(this.wasmInstance.memory.buffer); - const status = dv.getInt32(retptr + 0, true); - const value = dv.getFloat64(retptr + 8, true); - - return status === 0 ? undefined : value; - } finally { - this.wasmInstance.__wbindgen_add_to_stack_pointer(16); - } - } - - async init(wasmPath: string): Promise { - const wasmBuffer = await fs.promises.readFile(wasmPath); - const { instance } = await WebAssembly.instantiate(wasmBuffer, { wbg: {} }); - this.wasmInstance = instance.exports; - } +export interface SolveDeepSeekPowOptions { + signal?: AbortSignal | null; + timeoutMs?: number; } -let _wasmSolver: DeepSeekHashWasm | null = null; -let _wasmInitFailed = false; - -async function getWasmSolver(): Promise { - if (_wasmInitFailed) return null; - if (_wasmSolver) return _wasmSolver; - - try { - const solver = new DeepSeekHashWasm(); - const wasmPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "sha3_wasm_bg.wasm"); - await solver.init(wasmPath); - _wasmSolver = solver; - return solver; - } catch { - _wasmInitFailed = true; - return null; - } +interface ValidatedChallenge { + challenge: string; + prefix: string; + difficulty: number; } -// ── JS fallback solver (slow — ~5-6s at difficulty 144000) ─────────────── - -const require = createRequire(import.meta.url); -let _U: any | undefined; -function loadU(): any { - if (_U === undefined) { - _U = require("./deepseek-pow-solver.cjs").U; - } - return _U; +function createAbortError(): Error { + const error = new Error("DeepSeek PoW computation aborted"); + error.name = "AbortError"; + return error; } -function solveWithJS(challenge: string, prefix: string, difficulty: number): number { - const U = loadU(); - const createHash = () => { - const self: any = {}; - self._sponge = new U({ capacity: 256, padding: 6 }); - self.update = (s: string) => { - self._sponge.absorb(Buffer.from(s, "utf8")); - return self; - }; - self.digest = (fmt?: string) => { - return self._sponge.squeeze(6).toString(fmt || "hex"); - }; - self.copy = () => { - const c: any = {}; - c._sponge = self._sponge.copy(); - c.update = (s: string) => { - c._sponge.absorb(Buffer.from(s, "utf8")); - return c; - }; - c.digest = (fmt?: string) => { - return c._sponge.squeeze(6).toString(fmt || "hex"); - }; - return c; - }; - return self; +function validateChallenge( + algorithm: string, + challenge: string, + salt: string, + difficulty: number, + expireAt: number +): ValidatedChallenge { + if (algorithm !== DEEPSEEK_POW_ALGORITHM) { + throw new Error(`Unsupported DeepSeek PoW algorithm: ${algorithm}`); + } + if (!SHA3_256_HEX_PATTERN.test(challenge)) { + throw new Error("DeepSeek PoW challenge must be a 64-character SHA3-256 hex digest"); + } + if (typeof salt !== "string" || salt.length === 0 || salt.length > MAX_SALT_LENGTH) { + throw new Error(`DeepSeek PoW salt must contain 1-${MAX_SALT_LENGTH} characters`); + } + if (!Number.isSafeInteger(difficulty) || difficulty < 1 || difficulty > MAX_DIFFICULTY) { + throw new Error(`DeepSeek PoW difficulty must be an integer from 1 to ${MAX_DIFFICULTY}`); + } + if (!Number.isSafeInteger(expireAt) || expireAt < 0) { + throw new Error("DeepSeek PoW expiry must be a non-negative safe integer"); + } + + return { + challenge: challenge.toLowerCase(), + prefix: `${salt}_${expireAt}_`, + difficulty, }; - - const h = createHash(); - h.update(prefix); - - for (let nonce = 0; nonce < difficulty; nonce++) { - if (h.copy().update(String(nonce)).digest("hex") === challenge) { - return nonce; - } - } - return -1; } -// ── Public API ─────────────────────────────────────────────────────────── +function resolveTimeoutMs(timeoutMs: number | undefined): number { + if (timeoutMs === undefined) return DEFAULT_TIMEOUT_MS; + if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) { + throw new Error(`DeepSeek PoW timeout must be an integer from 1 to ${MAX_TIMEOUT_MS}ms`); + } + return timeoutMs; +} + +function solveSynchronously({ challenge, prefix, difficulty }: ValidatedChallenge): number { + return findDeepSeekPowNonce(prefix, challenge, difficulty); +} + +function resolveWorkerPath(): string { + const tracedPath = path.join(process.cwd(), "open-sse/lib/deepseek-pow-worker.mjs"); + if (existsSync(tracedPath)) return tracedPath; + return fileURLToPath(new URL("./deepseek-pow-worker.mjs", import.meta.url)); +} + +function solveInWorker( + validated: ValidatedChallenge, + options: SolveDeepSeekPowOptions +): Promise { + const { signal } = options; + if (signal?.aborted) return Promise.reject(createAbortError()); + if (activeWorkerCount >= MAX_CONCURRENT_WORKERS) { + return Promise.reject( + new Error(`DeepSeek PoW worker capacity reached (${MAX_CONCURRENT_WORKERS})`) + ); + } + + const timeoutMs = resolveTimeoutMs(options.timeoutMs); + activeWorkerCount += 1; + + return new Promise((resolve, reject) => { + const worker = new Worker(resolveWorkerPath(), { workerData: validated }); + let settled = false; + + const cleanup = () => { + clearTimeout(timeout); + signal?.removeEventListener("abort", onAbort); + activeWorkerCount = Math.max(0, activeWorkerCount - 1); + }; + const finish = (callback: () => void, terminate: boolean) => { + if (settled) return; + settled = true; + cleanup(); + if (terminate) void worker.terminate(); + callback(); + }; + const onAbort = () => { + finish(() => reject(createAbortError()), true); + }; + const timeout = setTimeout(() => { + finish(() => reject(new Error(`DeepSeek PoW computation exceeded ${timeoutMs}ms`)), true); + }, timeoutMs); + + signal?.addEventListener("abort", onAbort, { once: true }); + worker.once("message", (answer: unknown) => { + if (!Number.isSafeInteger(answer) || (answer as number) < -1) { + finish(() => reject(new Error("DeepSeek PoW worker returned an invalid answer")), true); + return; + } + finish(() => resolve(answer as number), false); + }); + worker.once("error", (error) => { + finish(() => reject(error), true); + }); + worker.once("exit", (code) => { + if (code !== 0) { + finish(() => reject(new Error(`DeepSeek PoW worker exited with code ${code}`)), false); + } else { + finish(() => reject(new Error("DeepSeek PoW worker exited without an answer")), false); + } + }); + }); +} export async function solveDeepSeekPowAsync( algorithm: string, challenge: string, salt: string, difficulty: number, - expireAt: number + expireAt: number, + options: SolveDeepSeekPowOptions = {} ): Promise { - if (algorithm !== "DeepSeekHashV1") throw new Error(`Unsupported: ${algorithm}`); - const prefix = `${salt}_${expireAt}_`; - - const wasm = await getWasmSolver(); - if (wasm) { - const answer = wasm.calculateHash(challenge, prefix, difficulty); - if (answer === undefined) return -1; - return answer; - } - - return solveWithJS(challenge, prefix, difficulty); + const validated = validateChallenge(algorithm, challenge, salt, difficulty, expireAt); + return solveInWorker(validated, options); } -// Sync wrapper kept for backward compat (uses JS fallback only) export function solveDeepSeekPow( algorithm: string, challenge: string, @@ -183,7 +158,9 @@ export function solveDeepSeekPow( difficulty: number, expireAt: number ): number { - if (algorithm !== "DeepSeekHashV1") throw new Error(`Unsupported: ${algorithm}`); - const prefix = `${salt}_${expireAt}_`; - return solveWithJS(challenge, prefix, difficulty); + // Compatibility-only synchronous API. The validated 250k ceiling bounds CPU + // use; request handling must use solveDeepSeekPowAsync() so hashing stays off + // the event loop and remains abortable. + const validated = validateChallenge(algorithm, challenge, salt, difficulty, expireAt); + return solveSynchronously(validated); } diff --git a/open-sse/lib/sha3_wasm_bg.wasm b/open-sse/lib/sha3_wasm_bg.wasm deleted file mode 100644 index ac92b1d87e8cdf3a5c2af4d189743c1f9d5feba7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26612 zcmeHwdwf+_o$p#}?|sfbujGUV2qeHdr?d@)kOV?b09&)8P$GT#XsxyiDWruH8ps1m zZKoV4QLxqGj5E`ZuR(9eQmq!}I$E__oM~%y)Z)ErJ5C+xOdWgg^`m%4$6l@Fe!su9 z&&i`$KhEDn$XWZhp1<{5zx8{qwNKQ**iBj~r7zEHHv9JJeVe^~k^oThtTt;rX%_h? zVj3{5WvI;wfp1frlk&hKucJLkDdqcK!X%YRV+^A_&o_SPd!A=JrM;v|7^Onv2Pnj+ z-lcQ7z|*QBk-~UUrE)@Ll7FU?iJJy*+BtfQQL}Fbx|_BR4(=Ko930xTZD4HMrahf1 zk(T_iojdjnD!pjarkk(dKD_nD!Qo8O?504LysteDVQe$xM zuAQUfn>y6Ov*dQFbI+2yLN%vmX%*>A_I$If=q+!Xv#4(AdFlW4Dc$~WMHBARx0~MS zKUPJ*(pUJsNWXRF)+=%W9x~;kj`ofeg6KmZJE2;QDu(NFezd5NjFb(A3LfhG^30j! zWoQ;e39F-U&y7Vjv?tOdDAMCY(Z%ILGGmjG8ILM|bwZWdiC*XTMyjB)Cc^|x=b?t) zGlX7~(V|P5#|yp(^sH_*KE^L5Y|x)m7ReBvy({pn3&tjjUQVG050J|G8P;z^1=iHo zj~48S{E>2$cY|Ki%z|c&=L55XZ*@*(vN{vp6VJ_$@-pY>1&`Qg`kRYE3xnPuZe6&*Q>SrrW$+`}iCU#kg=-X$Q4XHB=sT4TL#6IRE<__y2O=8T0NUIm~w3G~yaZGq*gZpoNgg z%oi2NhKELZ0I8Y^N!KOmmMyCFKv+kOpn}j*HOpj7PRDD-?2t3%l+cx%5}TCqkuO`- z%yqMVAz=+DgKQ<)9Mhl-AbU5}jxqg2gx-)Q!B?OKMrf(crU+yeshEuNQKL;_4py&k zMz3gMG53ILF8Bb zIbJm2Ml&>uVl_Upt(cg)+{l|;ZlGCPZcWga-pHBnfuM|+#>Hcg~xzG#4wtUgwf9W=%J&a4A}NkL<^K-kNR z@)Rq;C!RT;uzPHE(jLI+llIr;xP8wuYcplmiHMarYkzXqzD>-WHFp+y71ND3qs-a} zDM%bF7cu(%`w%jMWVgXI+GMg2Z0LH_NizekNm3wy2;iD1)?sr2e5bt;6cBDuJw=MPLeiz0D)42UkCLy`dq>PX0`yDt;-|u>Izw2tp z)Au_!Ax4<8-@#rp_q&<8-~Fs70#n#JXJ+nqcINiGhfVB$rqDxg1Q!O;y>0^)6_`?V zM-V+&@wxo@J-yIP(Z2OX1F;H;hW!Kq8y!art}3+sZ6lBs*sjb1vS>w?_$({%16h=h zT-yF-ln{sLfocK9t3X8*6c?XW!I==&#RSCX`kXLBC`(Db_;B1xMz;F0W^x%bE!IJ0 zq7!(G&Me~a2hpk9KgDFNoNXuU|K7AfWXnXglVnXX9*mOOPJQGJxt(uT*92;AC9Fv{vmo0#C;!Ol#GCO5jvn!n9WHmjs@VOPJQGy-476 zT*9<=hJ+BMj{cyWD&R#*71Qff3M30PE(R9?12dx^)ub`(WSz-~OmZ{)BPWGU z6OkRFZVo{QU`|Z{OWBVg@Eqn#j4Gs{n)Bu85HJH+GB7-Bs${~m3Mc*lwow@LFS z5!IJt`ayTo^6-ZWG>2kEu^v`~SGDuj!z}iwBn&)|NYg}z-lR{Y@*F5AqbazuTx6|+ z$w;vy2keC!C17IFdO?mO2FgH;G#!jdVyG#^FdzoKpYftMNeoyXGp#F*E;eN_!=}V= zVK$W!eM>}#iX@6mvN}^NCT0&Bf{@U+)y-736BRh3;AI16eL-Osl<6hKV8TcBmq8!W zDsw7=UXnavD44>vZCTVbTB{Bf_6jR<*&qw~Bp3jH@+YQTYbRA$>iPnytA;RaFD~ee ziMTa|^?+R=y>nkxZ4E9Dn9=H?GI|j@Oq;M@K8y5#4Ipxe$Vo@F-e{{jjAKr(l87q+ z>Cch*0g8!TJFPCz@SO(*uZZ?fwc8>pUj}zt1OlG_bL$0P!JG6KV3EO&rQG6yFXVlfG=L%l^nj1!`OcEF(dP3)C(}iB6 zo-zh8g<4-X6mvD`^lP>tc433hK!WwDOo1&<>T)3u9Q^2A8XL*xF*84(n-9OjXJ-_4 z9D&);c3X+%sRifPZt`|4|psYeI4xBrqq1a z*@MnhF@sUX`9B_@<4m`CfN9umjs>V>h6-8J>-C~>njK1~2qKk>VV!4`h62zwSze}8 z^vJ$Tb18%fZifj?wGh(GXCZ34^4H}I1d&bJ?0Wc>?4}$K4mjyn_Q!? z_%?-cim7e`>V?H5+zskPX^e3y__vp8B(a1|8@V}L8Nw_E%Pw6O!^j_^8q? z=++x4rkX)LYRpumY#P{IP~O;_(>BaA-~t7Q{}`?DzCvhv`}Wz{eahyVYaIIX*KBPr z&b<;m+So@-cHL2E!emY-%7uEQNBwf4foqHBrxEU`GBTL2!REnU`CK7~Tu6$8p-o5^ z`I31ti9BzOdID?`*>3X?Q`C8s>7mLAJf4WN$Wbf;Arp9dF$sD7a1b>Xl6Fq9vG2&Y z)mt&S#=>mD;Sl&Y30i^}mH*%(7qj57*}sb4J_;sMxEVv=BrP&U(F%usAB;d`~Fp3LnQ7iq`23>lwdT@ zBrKPaJ9XUDD~!OtQo?03x&VJnL)HzVrwo#cI!3`u^py9>_7Z-J5bSeka0emiHk411 zufZ{u5LO-17n22Hr-NWTESF8Rsr!OrcBohfE7NA-$fn?@L+JHFIxdEMj7kzfh`uYU zgy=@}s5~{=cqpm{6+vybAo=DUT+1e_`b^0zMis^oKjqlqV9D!|oG{L@$mx)r4lJ@o z-KKK%!u}H~zZe{vwYf5OP0|TA?Emr_pvv74m()p#uwgFJ5OGOc#|2B-x^kwFMK#WF z7`vHLZ&tZ*j%09eIOfgTy8J9q0}W?^w(Q>KENY2B6p9wJu5NVF$Y^cq*GUW^OI^&7 z-)Ex<#0g82z&J|C7N=DIHR_SRcc-F?#%kS-1;D2E$k|K0L*Yb89tn`?mv~2*B@z_? zx5yIj9%k{80Jq2z? z6U^cxvrbFaNoMhpSuaS|)6C){vnuj=idlSQ)*<_ck))DzU%`84L>mK=hfmwWH z)=~Maq>#l&W*w8yL(IZsHt!3=HwjZ!{5MdYwy9$fr4ZehIOu+XbPRn$AQMBM6v&Gq zC@!?~W9U-?1u^t#ff6zFpg_qOIxbKcL!S{S6+?d`P&$VGyFi&3`m8|N82Vd*axwHd zf$C!D^8(e!&_e>{W9SP4HN?;t1)3E@UlQn?7=8a7ieJ&eNCW6(CQgWdxA2Y3N1;6 zmJ@}Rq(VcWG*j-+h1k5%R5s%jnrBn_AYD!8P?A$lNtM;H}r6X80Ehze8VIIvF zsCFpR5XM3as=(T2hJr0LWjKrt{aGC@jo}cW#c(NTd(oe;oiH3Nz>M{QtQWTqiL7fK z64|gB26{iFXeh1yDzYAtHE5{gh8`RGGAyjyk)qd((*rj2(2(AY3=3P94OPuFv^Z;P zUhP2)13x`R=O6+5w7gisZg)meNTVqFe;+=fqUF)Ck3gDQzz^X8@bfYJ&9u($6XO>I zqOVc#xT_2hgqI9ca2;%8Ms$n8I(m>OK$sc^WwKnL+CovbC^nOagBPqI60esBM>Ydf z&Y*ADI99=i19{ZtIarShwi{M?bH4-N38bQ3G2oXojJ*!QJu4(&wg3fKi{KPS#@Qv@ zKJ-NsH%X0W6I??8{ag&Rsv3*XfMPJ&B--GasAU*;$23uqA$PdV-G&3pp&}f;G?t|R+zV_c@tZKZjee^t1ehvw>Ho$9GpMesSP-b$uLQhW33>|{ z2?=Csj%sdhu!UcTh>7%rQvyeU*y2dJ*`|Trh!-DgHf1;2k9sKA7d~)D)LSNAgD+zNBVHTP30&RafauSV#ksqB-j+ALc6cn0E zz#H*$o~S1G0O`b@(P;mHgW(8P9hCjOB{+lpJ#DgQEddU+A;4G^uV9D3W*KowLn?q( z;Ir+C&g_3NiP*kN%hCRdzXRt1GE<8)Zx|D4%;`nMjm$XcN48cm5^550&>lpWSU!DR z|7>-*Kq2W1tXK*0yq9-`O60*L_6^TGC{_yG5-=#iuH>kgF=O7bUUl{O-eobSF9IcoZ*$zMIm>t%X;76nZOTS^sLm*j zjsnuA5OCWf{Iv(vMpEVz#ULJ$gN>U9S0HHu10cZ}n}-dFUU+JU5^yI>-F$-r2-&CW zc~bz{5MM0FHfV))KsNDUQyai%izq{@V@&TtI=4YTg)GLCjyS&SEAv!Fr}x2ThSzAS zaRRA`YX%nxqQx|WDIqX&J~jvfj&LM6`=?0dqONN-I{xPcTKiRCYMLRC1efdR8MQtK zO6llKurZh59R$;-N;-|r@ysc;R!f9)GV6KGEUKnjt>+|*NAtC;XCw=pJel>7WMRQ3 zv%W3eYpiwD5Mh;~2ao#@DGIa;M~faZGKkGW$l@}|`~Tcswm=qqa7tdERc4l1wb7ND zS1g69c!jpFP&xw@hWXZm8T^e1ouJRC!05^(Qbk0oRWuu18T~vcBt#FPk_cOdkx+t@ z-J05A??PMn)9B|F(ml*mbjN@Rfn!w_? z{jq0C1=t|S^7l+{wn%cZ!KZb$P;#>~au-N$enxH!dex&hLIV{w25-U)NZ#K988U<( zihd=J2(;6c#)f}JU=#fgX#q`mUfr&NHRfQ=64O-jgq6Lh82$47uyN*L_9;*>ItYpr z0{ze#FOTTR(znH)P_QFC(u6DnF3Ro;!{q>fJBiS*yRJP&**-gNX5 zmoNIr>GXO4rvy0lt@L`_FNse4EIkz6vJp?mKb#qgMmCNz_w-YE-o@O?X*~UK$42H> zD%qjv!{f|8`EfkH1C>W9v7*X>o}uW$$KHfs6rj_0^Z;cy^ylsdGOf`&k0AM&iH`j# z()$f@gG@tZIlX{~xkiQ5HYQs3`(F3*ST?u&8Y^^MTN`-L7HG!ui$+fFZp{7huXR z@pcvROg0o6nAk!ylZSKxQ4sjLy0C!hw!$nXy|6Qp;0gzm;~J3&q=yhaOFUJ` zMqyz&)LYs%!EZp^WCH&{fOBl4oehx^3T*oH;fW%`mJ^6$wAclffs~vSLAy$!o+z|| zfOQjn=5TSL1U%*=dfZ@}rCJV^teGg9iN4a|;v)21XW^4UKBc6*4uL?9;ZO8ke7I=) z_D$R|0b=HCon$9$(?s8r!^Hv?EI-kA!Qo<&RqUb(kRY&gC;HNdi|1mcTPFH?4i^`r z|Hg^F`oqQZTu(fJXrAb6K3qKCo(t)kndn=5xOjmpMDWt4C;Da`E~aJ1eTl=xR$vxP zVC>=&SBYmE^C$Yk!$q?8tckvPhl@)Eht>(u-M8mV^j&ng$g5Wk6Mc<`p>7aT3;)!AcEDY0LK#g(0yaj9RLaALO51(%rOX+!7Rj~3Bn-=iG2{80RTM(c) z0X|OK3k6s@0WQA4o@dV`I02?D+C@@gC%~jjZL0uFCcvZ1aW+w09-Ux(2CQn+i1lcs z5bAr8%qw|UBFU7zRwQ+Z`mk^gf=f*2mb`0_q+ICGgD?Pe4&r7kY(%(;>FkoX8A<3O zj(*9*UbPyLG!_~JpqS1odE1cWO4uUU>?IGI$krpa#=-h;hO;0}Y5>o zCaG`cnS+zuCKatZ9!57=Eqr)L>Dos#c7$m z1^~CBcD0mrJIkF2cMU+oHgsV#U`MedxRWq6#S2z|3FaNv3v;0ecNDe8tM2Yn(LQvO$vl?SA>G822rzlDFTC0* zu;)&Jk;AjygyE)jXS-0rR*oJ(uIkw?+c8^CH+&p?a3}MmcLy$`6azWiWjkCI2;?XO zXY1J8DL0@y$?)3L*Mfo{DY`7t6NqF5?lgmUSx%s(94OMr4Y{ZY^?5C^QN06Pg$vFE|H5)b?;=b6uZR*Jc3^p8#JjvUz=$5q;v=(;%V#BlEIu;pA^AMSEIu;pgnS-h79W}QgnZt^ zEIu;pqi;v8DUOpdU79W{)T0T!Oi;v8DK|Y^g79LZ1 zgm7LUjgI0b8#o)A9IX@U{e9>Htb=oGWW8vx&NwbZMe*C{ zsKT6gqe=^3#DZ%9!-h@38wtd3%7~g_oKVI|H@o)SM{#@L2t?82#g|yTfglboyX0Qr z&$QUNyj6l=8bbEem_^^k&Uv96dK~*gV&@QPjGZTHBbWwRCjlug+c(eL3I`u=TX=Yj2jZ3~RtD`5_(mJ}i>4p;Ae{Bl{!u;* zA?g`kF$!M?mnFm~!G(`M z)r)MFz8M6~^uDCA7A5H#XCY>*+OUcjtSpCqXW_)&i*c2RqT6ar5pM0~#v6OSpqF$d?< z%PZcl!rZZrw?%t}GB6dr5pWaMMUX_H*Wa9nTbU2U76X4lp2WHHT^lj(FwMzRCS2y{ z(H2+@(+_bk1ne&-Q{cQmaANd_%Fj+uUg`wTFb2SDclSagi|Tg981LFy8nG&Ke?mjr zps;E?Lnr%1EXGs=S~NG%7oAN5+8JdaAO+9M4QMeErY;@_h?hVt9*NUKi`6XXKhknW zLw3n%VYOdQ!C5CY>uc4uu3VaLN5NaIlaWK_x*sKq+uZV?WM5Xu7P z$U(5rIuw!)1soMS6rjNnIvh$jn(BbldlSmwB0ei* zeqzL!;akHnH_&I4zYK>qLwIQkkqbcBGR_1+$~OhdQML>XC9kU)1V6+m!NS0$u?Ta3 zLv%ro-A&CbO3;FVDlJ3ps3Cdx0Q(yNWbhEjlYliW3%-+#dxE_1%DI6;kYBNarlAHg z3j=r3o`4vcX*Yl!XH;@V2Ns0kmx5+D&g^4V>^!))GLooRIh3LCLx(C~78+26O+y3b z2ioH32G(@-vN}eM6;0LD@$)!+gC&Mr@Q^H-Ul>tvSgbfW6pMMs2cGFA8rD#exfg9Q zd%k#Erqp$>^%*&8rLT(NDUw8o$!mR+@9*(dU2wk-*3+=ns&D&2IpWebjeF#ARjVB% zRDg*<+VOs?Z%Bl5FcE^*ZM?vgA3dNNep5m^X=wpoCdG)vNQKuA9e=- z9GF`pFT+%)Kv4@x$k$%0feXI=3qdXIz@=h^K~eDtRf9ehsT;AdR7>&*-d8gJja>$BZ!e|)!BuCMT;YBbQ zwhEyXa0xn#?!i%1e!k)rOdKaGgb5&Ju{=niuCWVv)iR6Z=sQo4bndDpCk;O&zF66T zibpVLFb?*?_jG9pb?f~4AX~U;4X3i9?5*p=p^s+ z(5sLUL=jUss3Gy~sG4}=3YNRog#>5|8378?g`{ki_yzPOy2a_9+z*khiOV{0VK5g>CCgXa5Lkk*uK9*45hR{)E(yOa|*nfm-by z%)}jx^%lW4L?;ylgH4V@GGvi7NYMl`2PmKbS8b&RnAr(GwsgIS_{=w=i6zBkf3UF! zn;aa&S>)m6M=J9r1L5EZ=piFbsf@jVE98z3eJm%LEke8l!W~ODV$<$yb2kiuA9%qW5fHhEKBERTBp3i25Eslhf@u^N7ijp=zbalmKn|J8peG*k z02KylYIoWpXVEz{@>x0;t>*5QPiqcMJq=CQKQ?K#e~2A00VoB=!)}P@AEmJ`@{Ayo ze3>s&nf;f8@}F@~GW%(9f$=IJy1!V(_xB+wS|E?|VnLb6jzC6%$@BHl3lOm)6tHoN zFo9?XBZy}hP%{!xGl=K8n3~ft5DWdN9D|S)h&U(cjuw@@CFB8N%%^@S339_!q))4#=)BjZw9>C!P^LL1H8?_y9nu5hOnV>7c(O8tFU?YXGH~FDq{Szvb@3F#=6>E?~ z@&R`)Fa^HDhy=Z{jpbqkV?V}N)0!b}xB@CZN)2N}WTD=u<^bvw8lmM?ilOK1eMst7~DLZ|5q@SV=|)qKkc*FT(<3)0fg`ji;I z;18?$XweJTm4+`_19z)nn}RR5O&JUp09i)3){W`8vApOZRHqr?xv{)>EHJ4Xh}H~< zFT+}N;>BahT+P`B5}U9(UUi`F#*=#aK($H9SZ-2h8|Zp0o$!gURLmXny1C;H2?H6P zNsv3hl(J3!DDJoxJA;h)N}bE0#?;QSjEqwMp$PIdM1P+|VC97aIS=;0T4JMst3X?< zWj+aOG&A{9s?Mv7>|`t-L?7mbx$)}^_`DAMn%SU$vfdu)OJEA)4&dAB z+pV(YB}hel(;QTxYl+2Um)3(nBVY)=B8T$>MrUlUVwY9TRh}$WElziG3p8MV0y&6O z_An{EJ<0_h@;J`x^Gjs3nk*Lii_#|UI^+vC^>8vq0&~20%FZd&8YEIAVKkCV!ar;S zp$18ZJ8qd*VdzLZGnW(4`3AH{X=C)n0c>6AC%)0ljyVa`DbV!Kv~g5#3JJX-91QWx zlYjxl9&k(Yt1Unzt1|yv;}(K!Wapl6P1fpayn+ZtNm(RlL2Qt-h>6N&wCH?zqP=V!=f46zQ+vA}&j%g%78L&(#|X!Ui=}l?6W| zs?G(4Far;@sZ@S0XHpFnLGFMe5xm)#RhkFnNJD;^U?d)2^;USJ5D~>fORfTh+!uIj z2va|e_mE-zQFw5^(9>WLZr<7tLcFrqbhKZN1E8FjsfxTEkTdXrmZ>v*{c7B%qyy99ZN0~1^{V6soMK4@`jB}%aWIihW->-oc95|2nrG_#2o?64#nvk ztf9QDgIECqfCv_2v&7M9G=z(f_XR`(SkEy$-uH!c#ydJwYoV#s5($8zSXD-wR?CZP zA<*I~Aclu`DEJ;aG&Kg}%L!u3I+PL-z9*5_<8coH8-ch=gwWF90;aV|L%-qZ4kWom zF7zOdhndAnn+BR@6h(<4L(iV>FG#JTMI+!$6>XC1+)-j2U;75158R7oWQ3~QEOtwY zMh1;waVHRKVbfitD?$%Z78&9vdeJQ0n5vvsBOJr4RUgI34CLc0SK_l1bc!A-v@hmk z-PmE^D5NvA*EcjiEs68 zlbq_Czs%VzIjwP>6AD?^NY>&w>m;+Tlq@?XjJ1JZ$wDg@Q6C6(AQNH35QPU0RZ;NH zhJx#TDhjUiDhdpWZD3Rm8x#x~$X5yA?A~>LS8aZfLoc77o6XeD-Ei3{n7iSs?>f8T zpd7&L(bGVkF}~=t5E>FEqm+OEc1U1yPbkp42M--(R|y8o#mdsqa_U1tc7UI_MHSTt zW!Q%x#zhBGk>rs9Yv&>4(FjqQIu-o@K(rKU4Kg}|_U9@X=;x};&wQPsP=eWo_g5f~ z@v8nZB-WiKqSIDD?mhj*z!Mo38A$MDflLr@GQIqvEX=zX1M!q)SXMisNjCQoh9!d` z>d)`P!24Jdze7z`P+l)ed3L#X0JTjK28 z@wizErAJ?h+Za8^p<&2oywX-18_}og&>;f7FT((4{uB&Ec~miwHN^X8ltS|5q(USN z-NKhMgm3_wI1$nf`;(7y)hY$ul#B^QRFMx_Kz$kD%7`S+-HKV{f@nzm0&XqJE2nVb zXefYpxYF3AuFGmyXYB{)LhJ`g+z&2>NI!h>17AMTXklx>Nn&Fx3{Q4vkd=Y~#}~@- zhyX8?#Bp0umHrjzTh@rQ?HIgiaCm%ReEZH}J32VN zdvthks~s4&gB;fG9>zEL4sIDA+`8kI_N&GQN5|Sn-?4nxz;OHW(cNR?yLYvZ@7%d# z%eH~-!(;9E9^mVD47LpnjBeT1-PJa*3!S%Z9T>fN`!MlpXA7pplh4c?9T>f(ePG9q zom(V(Y(a%a?a_u2|W%s=L&)df@skTL*90f@YZN<|}q_?rYZU9=>^W zU{~vs%@&jxu$wR6IXt-8?itvzdk|lDJh<2H+&yl0-e9laxqEo)*cv;&ZP4x*9JbrX zti2vZgk44Cs0oa3G45X@orQD_0MeiT@YOfUAlR8p$-BIf3=~&&_(b?I#qH|?uSLdqE?#@zYPv`0t9VRQ#+ z-Bs%9=~}(2V^!y>6{}XR>RPpGRrjjWs-9J=yF0o&yH|9t?C$Db)!p4)>h9@YUFs-x zmR6Kjmbyx-O5LSWsi(BMr=zE{XGPD-p01u%J>5N}o}Ql7tAXNb48Iy(uST=g$Qm6O zzH!ikM@GkEI#ns^N^StVjqTXJWspP$VQ(DYW|jJearAozX%X~rbq9yH#(m^*VJMIM zYPsvdt%Hm;W(UUI0-=!rssnB2pv`)uIi%MvU%vcp*AL#feb~P0BFny8`LM*^ybbHi z9>J4#^XBcSPhPwkDR<$mNICYMNV&Gx-!fih0dmFg&T;S&_Qt5=8F2bqyB(7s1MXwn zw+>p{$|hqN2~*yz9zZ|b(Vs_pKGGi|<*xrR(gf1#IP75>GrH!r_Km)qU$uUX&#=4N zZ`!$)eD+$@A)oyMzp&0Gd2#pf_ANWN4z};YMwI=`+DdIh{RK$pn-zPr(;FnjOn)R;3@BN6Mt819Gq_bpu7S=9Xlb&NR;LUcKhn{ZId6s&wRi$CBx_7vHe`?munVx#^Lg z{rs)h|MJB%Z+z1SKDfO7g4U}(eDtIL?cV#2ed>!3KZ@@Tn!RAntJl5$Vq_|-r8>CewR_PB4)J@5S1l|5@VyzbH~-+0y4*Sz(*w{PAuc*D@x-apy*?t4G+ z;PDeDKmFkF&L95spS`_!YvB29-VL5^UtT$|!0W7U@fW8S21|q2`gIppK9N}LFZNr* zuFU0LS9X6XH76Z5tzEa;+Y+Wa<^%<=Incd5{>Gr)PbX8!UVDL`O?7)~g84~5o4m5W zv@*9cxjalK&i59CrgHKPd9SoAyl~F^^IIC{rY^@&ug%R%rV|^&3sSo?eHUMtSR14h zug9g%yccBN5}F0qFWeBOD<6M*aa|^z$ept$kxn$WEc549zVNE8Z_I8;rPr@(-VnYq z*Pl#RK3A+uC)TGIc$f5-ygH1!I+@)7v%luT*)Fmn(xZ`QR!OM9!`W#{Meb=i79@6DQZ zPHK*x>o@6n-uz^)? zo{x3B`IZJYk(;CtTtzCXSHGhh7DV~>CR8~^;=kIyJy zHb803+Kc*M_qK!YMd4?^_@&3c@$Dy{`?0ERkG0$%Z{Iq2@O>Zr@YlcnWUgV!nzift z-+b+LZ^s`1c+dOpN0Z0C{-fu9d^*>#u7B%b<>22u{FO((_tfcM+%a+Iy&wO|BaeOU z$shdFhI_vJt;fIpWdG$?y!kC}-*ouBcYo${pMT`5kALl{hBi}RPa z-}m6>ANksoPyOfvs`oGN>9~8*x1anU-~INP%ddFrwaKtPe?j}tem=ajbkVE()*X4@ z8*kkG#Me)L=jng^$xCOH-BdjABmcl_!)8Cxu>XO&$|r+G;rC+lCI zcDBpq5bLe@t-^1`GxetOl)3|+%=?1P>TTy7RkP;WMOnLf(a)FJr5AP_UApt*o0pkm zr;1FDX)KySbo8K83=>ep+N%!b!%lgcFfO*8t_ zx>TnzVvx~`Q~Cy9C(xCdXM8X3*fvPTmp(nfbI>hwF(zIT2q3rEN#dQgBy;7Mcd@o>i~$-zAHc(2lb4~O(U zdb3rD?MC?;$KO{P$TZ+R&jk7}&HPz8eSSDEqxCYc12Y4P(CV)N*Itqc?fR2U+xowHMA7+PZb9OM1a;G)P2t@O(_J&Ri98l5eo%Vfw1mpV}*CZ3Nb_wGx#}cR{$fVye2dJTscO=j~ zRssCSfwaQ}#c8?Y|z}gt;#_b5-Ca!q6la`tQe^EdGj6*XcQd zu1C)e(rd6)A4QvseC5A{@oq}0&4_Eszp4*$pyJ?nyR@SCqg3Ti2h$#vr~Pq$%H>zy zjY|A?$R!H-6+Xg_lN9Y;%1q=PuDH~TQxm5yQI&N35Vx;X$E(Eks(6I1-->hmw&G9B zv~S&UlXz}z9m`iN?-oa>nguyDFa~c^ywV#6w!=|dV|R^hf5)J`_#!LmQhU?(;Z5`g zw+#+#RVVB6YKcWa)Re#eWWmdCOOq{y&&D>M{TT diff --git a/tests/unit/deepseek-pow-js-only.test.ts b/tests/unit/deepseek-pow-js-only.test.ts new file mode 100644 index 0000000000..53660f94ed --- /dev/null +++ b/tests/unit/deepseek-pow-js-only.test.ts @@ -0,0 +1,238 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { existsSync, readFileSync } from "node:fs"; +import test from "node:test"; + +import { solveDeepSeekPow, solveDeepSeekPowAsync } from "../../open-sse/lib/deepseek-pow.ts"; +import { + deepSeekHashV1, + deepSeekHashV1Reference, + sha3_256Fips202Reference, +} from "../../open-sse/lib/deepseek-pow-hash.js"; + +const repoRoot = new URL("../../", import.meta.url); +const noMatchChallenge = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; +const redistributedArtifacts = [ + new URL("../../open-sse/lib/sha3_wasm_bg.wasm", import.meta.url), + new URL("../../open-sse/lib/deepseek-pow-solver.cjs", import.meta.url), +]; + +const deepSeekHashV1Vectors = [ + [ + "1122334455667788_1778891543095_0", + "311b26ae1e0fe7375e242958ce46db5552a6c67fea3f96880dcd846c63a74286", + ], + [ + "1122334455667788_1778891543095_1", + "526f9103dfe22bcda9481b7f304a157b8edb18c5bb96a2061eefec1fbd0db706", + ], + ["vector_42_0", "1813da791ddac0da3a225e5878ff3d3ea07577e048f8e9575b82a274b71e3810"], + ["vector_42_1", "b582fdb4f83662baec734e23373efc860515c03c40decbef076697eb5832f83c"], + ["vector_42_2", "48b348ad54c372f78ccb6a26cbf156668f4c6a5b7d5a1cf19ccd78888586019b"], + ["vector_42_3", "2ffed26ea9e1d6f4bbe49a266d98fe04ad9a5ad4c6d765862de8d18c513c3815"], + ["bb_1778891543095_0", "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64"], +] as const; + +test("DeepSeekHashV1 is SHA3-256 with the last 23 KECCAK-p[1600] rounds", () => { + for (const [input, expected] of deepSeekHashV1Vectors) { + assert.equal(deepSeekHashV1Reference(input), expected, input); + } +}); + +test("the 24-round FIPS 202 reference agrees with node:crypto on a separate corpus", () => { + const corpus = [ + "", + "abc", + "café", + "🧪", + "a".repeat(135), + "b".repeat(136), + "c".repeat(137), + "multi-block-".repeat(40), + ]; + + for (const input of corpus) { + const expected = createHash("sha3-256").update(input).digest("hex"); + assert.equal(sha3_256Fips202Reference(input), expected, JSON.stringify(input)); + } +}); + +test("the optimized DeepSeekHashV1 implementation agrees with the readable reference", () => { + const corpus = [ + ...deepSeekHashV1Vectors.map(([input]) => input), + "", + "café-🧪", + "a".repeat(135), + "b".repeat(136), + "c".repeat(137), + "multi-block-".repeat(40), + ]; + + for (const input of corpus) { + assert.equal(deepSeekHashV1(input), deepSeekHashV1Reference(input), JSON.stringify(input)); + } +}); + +test("DeepSeek PoW remains functional without redistributing extracted solver artifacts", async () => { + const answer = await solveDeepSeekPowAsync( + "DeepSeekHashV1", + "311b26ae1e0fe7375e242958ce46db5552a6c67fea3f96880dcd846c63a74286", + "1122334455667788", + 1, + 1778891543095 + ); + + assert.equal(answer, 0, "the retained JavaScript solver must satisfy the known PoW vector"); + for (const artifact of redistributedArtifacts) { + assert.equal(existsSync(artifact), false, `${artifact.pathname} must not be redistributed`); + } + assert.equal( + existsSync(new URL("../../open-sse/lib/deepseek-pow-hash.js", import.meta.url)), + true, + "the clean-room JavaScript hash core must be packaged" + ); + assert.equal( + existsSync(new URL("../../open-sse/lib/deepseek-pow-worker.mjs", import.meta.url)), + true, + "the asynchronous worker entry must be packaged" + ); + + for (const relativePath of [ + "open-sse/lib/deepseek-pow-hash.js", + "open-sse/lib/deepseek-pow.ts", + "open-sse/lib/deepseek-pow-worker.mjs", + "next.config.mjs", + "package.json", + "open-sse/package.json", + ]) { + const contents = readFileSync(new URL(relativePath, repoRoot), "utf8"); + assert.doesNotMatch( + contents, + /sha3_wasm_bg\.wasm|deepseek-pow-solver\.cjs/, + `${relativePath} must not reference an extracted solver artifact` + ); + } + + for (const relativePath of [ + "open-sse/lib/deepseek-pow-hash.js", + "open-sse/lib/deepseek-pow.ts", + "open-sse/lib/deepseek-pow-worker.mjs", + ]) { + const contents = readFileSync(new URL(relativePath, repoRoot), "utf8"); + assert.doesNotMatch( + contents, + /createRequire|WebAssembly/, + `${relativePath} must not dynamically load a binary solver` + ); + } + + const nextConfig = readFileSync(new URL("../../next.config.mjs", import.meta.url), "utf8"); + assert.match(nextConfig, /\.\/open-sse\/lib\/deepseek-pow-hash\.js/); + assert.match(nextConfig, /\.\/open-sse\/lib\/deepseek-pow-worker\.mjs/); +}); + +test("DeepSeek PoW rejects malformed or unsafe challenges before hashing", async () => { + await assert.rejects( + solveDeepSeekPowAsync("DeepSeekHashV1", "not-a-sha3-digest", "salt", 1, 1), + /challenge/i + ); + await assert.rejects( + solveDeepSeekPowAsync( + "DeepSeekHashV1", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "salt", + -1, + 1 + ), + /difficulty/i + ); + await assert.rejects( + solveDeepSeekPowAsync("DeepSeekHashV1", noMatchChallenge, "salt", 250_001, 1), + /difficulty/i + ); + assert.throws( + () => solveDeepSeekPow("DeepSeekHashV1", noMatchChallenge, "salt", 250_001, 1), + /difficulty/i + ); + assert.throws( + () => + solveDeepSeekPow( + "UnknownHash", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "salt", + 1, + 1 + ), + /unsupported/i + ); +}); + +test("DeepSeek PoW sync and async APIs agree for non-zero and missing answers", async () => { + const nonceThreeChallenge = "2ffed26ea9e1d6f4bbe49a266d98fe04ad9a5ad4c6d765862de8d18c513c3815"; + + assert.equal(solveDeepSeekPow("DeepSeekHashV1", nonceThreeChallenge, "vector", 4, 42), 3); + assert.equal( + await solveDeepSeekPowAsync("DeepSeekHashV1", nonceThreeChallenge, "vector", 4, 42), + 3 + ); + assert.equal(solveDeepSeekPow("DeepSeekHashV1", nonceThreeChallenge, "vector", 3, 42), -1); + assert.equal( + await solveDeepSeekPowAsync("DeepSeekHashV1", nonceThreeChallenge, "vector", 3, 42), + -1 + ); +}); + +test("DeepSeek PoW async search yields to timers and honors cancellation", async () => { + const controller = new AbortController(); + let timerFired = false; + const timer = setTimeout(() => { + timerFired = true; + controller.abort(); + }, 0); + + try { + await assert.rejects( + solveDeepSeekPowAsync( + "DeepSeekHashV1", + noMatchChallenge, + "1122334455667788", + 256, + 1778891543095, + { signal: controller.signal } + ), + (error: unknown) => error instanceof Error && error.name === "AbortError" + ); + assert.equal(timerFired, true, "the event loop must progress while PoW is being searched"); + } finally { + clearTimeout(timer); + } +}); + +test("DeepSeek PoW async search enforces a deterministic timeout", async () => { + await assert.rejects( + solveDeepSeekPowAsync("DeepSeekHashV1", noMatchChallenge, "timeout", 250_000, 1, { + timeoutMs: 1, + }), + /exceeded 1ms/i + ); +}); + +test("DeepSeek PoW caps concurrent worker searches at two", async () => { + const controllers = [new AbortController(), new AbortController()]; + const searches = controllers.map((controller) => + solveDeepSeekPowAsync("DeepSeekHashV1", noMatchChallenge, "capacity", 250_000, 1, { + signal: controller.signal, + timeoutMs: 30_000, + }) + ); + + try { + await assert.rejects( + solveDeepSeekPowAsync("DeepSeekHashV1", noMatchChallenge, "capacity", 1, 1), + /capacity reached \(2\)/i + ); + } finally { + for (const controller of controllers) controller.abort(); + await Promise.allSettled(searches); + } +}); diff --git a/tests/unit/deepseek-pow-solver-strict.test.ts b/tests/unit/deepseek-pow-solver-strict.test.ts deleted file mode 100644 index 81a41b847e..0000000000 --- a/tests/unit/deepseek-pow-solver-strict.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { createRequire } from "node:module"; - -const requireCJS = createRequire(import.meta.url); - -test("deepseek-pow-solver.cjs loads under Node strict mode without ReferenceError (#2724)", () => { - let mod: { U?: unknown }; - assert.doesNotThrow(() => { - mod = requireCJS("../../open-sse/lib/deepseek-pow-solver.cjs"); - }); - assert.ok(mod!, "module loaded"); - assert.equal(typeof mod!.U, "function", "expected exported constructor U"); -}); - -test("deepseek-pow-solver.cjs does not pollute globalThis.onmessage in Node (#2724)", () => { - const before = (globalThis as { onmessage?: unknown }).onmessage; - requireCJS("../../open-sse/lib/deepseek-pow-solver.cjs"); - const after = (globalThis as { onmessage?: unknown }).onmessage; - assert.equal(after, before, "onmessage must remain unchanged outside Worker context"); -}); diff --git a/tests/unit/deepseek-web.test.ts b/tests/unit/deepseek-web.test.ts index 17266edf00..b210a31086 100644 --- a/tests/unit/deepseek-web.test.ts +++ b/tests/unit/deepseek-web.test.ts @@ -648,24 +648,73 @@ test("execute with a new DeepSeek userToken restarts auto-refresh", async () => // ─── Abort handling ────────────────────────────────────────────────────── -test("execute: handles abort signal gracefully", async () => { +test("execute: propagates the request signal into an in-flight PoW search", async () => { const dsMod4 = await import("../../open-sse/executors/deepseek-web.ts"); - if (dsMod4.tokenCache) dsMod4.tokenCache.clear(); - const executor = new DeepSeekWebExecutor(); + dsMod4.tokenCache.clear(); + dsMod4.sessionCache.clear(); + const originalFetch = globalThis.fetch; const controller = new AbortController(); - controller.abort(); - const result = await executor.execute({ - model: "default", - body: { messages: [{ role: "user", content: "hi" }] }, - stream: true, - credentials: { apiKey: "test-token-abort" }, - signal: controller.signal, - }); - assert.ok(result.response, "Should return response"); - assert.ok( - result.response.status >= 400 || result.response.status === 499, - "Should indicate error or abort" - ); + let completionCalls = 0; + + globalThis.fetch = async (input) => { + const url = input.toString(); + if (url.includes("/users/current")) { + return new Response( + JSON.stringify({ code: 0, data: { biz_data: { token: "abort-access-token" } } }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + if (url.includes("/chat_session/create")) { + return new Response( + JSON.stringify({ + code: 0, + data: { biz_data: { chat_session: { id: "abort-session" } } }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + if (url.includes("/chat/create_pow_challenge")) { + setTimeout(() => controller.abort(), 0); + return new Response( + JSON.stringify({ + code: 0, + data: { + biz_data: { + challenge: { + algorithm: "DeepSeekHashV1", + challenge: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + salt: "abort", + signature: "test-signature", + difficulty: 50_000, + expire_at: 1, + expire_after: 1, + target_path: "/api/v0/chat/completion", + }, + }, + }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + if (url.includes("/chat/completion")) completionCalls += 1; + throw new Error(`Unexpected request after PoW cancellation: ${url}`); + }; + + try { + const result = await new DeepSeekWebExecutor().execute({ + model: "default", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: false, + credentials: { apiKey: "test-token-abort" }, + signal: controller.signal, + }); + assert.equal(result.response.status, 499); + assert.equal(completionCalls, 0, "an aborted request must not reach completion"); + } finally { + globalThis.fetch = originalFetch; + dsMod4.tokenCache.clear(); + dsMod4.sessionCache.clear(); + } }); // ─── Search enabled ──────────────────────────────────────────────────────