mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-01 12:22:24 +03:00
fix(deepseek-web): replace unlicensed PoW artifacts (#11732)
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.
This commit is contained in:
committed by
GitHub
parent
700a735949
commit
657d3a484a
@@ -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)).
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -141,13 +141,14 @@ function generateFakeCookie(): string {
|
||||
|
||||
// ── PoW Solver (DeepSeekHashV1) ─────────────────────────────────────────
|
||||
|
||||
async function solvePow(challenge: PowChallenge): Promise<string> {
|
||||
async function solvePow(challenge: PowChallenge, signal?: AbortSignal | null): Promise<string> {
|
||||
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<string, string> = {
|
||||
...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,
|
||||
|
||||
450
open-sse/lib/deepseek-pow-hash.js
Normal file
450
open-sse/lib/deepseek-pow-hash.js
Normal file
@@ -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;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
20
open-sse/lib/deepseek-pow-worker.mjs
Normal file
20
open-sse/lib/deepseek-pow-worker.mjs
Normal file
@@ -0,0 +1,20 @@
|
||||
import { parentPort, workerData } from "node:worker_threads";
|
||||
|
||||
import { findDeepSeekPowNonce, MAX_DEEPSEEK_POW_DIFFICULTY } from "./deepseek-pow-hash.js";
|
||||
|
||||
if (!parentPort) {
|
||||
throw new Error("DeepSeek PoW worker requires a parent port");
|
||||
}
|
||||
|
||||
const { challenge, prefix, difficulty } = workerData;
|
||||
if (
|
||||
typeof challenge !== "string" ||
|
||||
typeof prefix !== "string" ||
|
||||
!Number.isSafeInteger(difficulty) ||
|
||||
difficulty < 1 ||
|
||||
difficulty > MAX_DEEPSEEK_POW_DIFFICULTY
|
||||
) {
|
||||
throw new Error("DeepSeek PoW worker received invalid input");
|
||||
}
|
||||
|
||||
parentPort.postMessage(findDeepSeekPowNonce(prefix, challenge, difficulty));
|
||||
@@ -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<void> {
|
||||
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<DeepSeekHashWasm | null> {
|
||||
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<number> {
|
||||
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<number>((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<number> {
|
||||
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);
|
||||
}
|
||||
|
||||
Binary file not shown.
238
tests/unit/deepseek-pow-js-only.test.ts
Normal file
238
tests/unit/deepseek-pow-js-only.test.ts
Normal file
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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 ──────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user