Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@isdk/proxy

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@isdk/proxy - npm Package Compare versions

Comparing version
0.3.0
to
0.4.0
+17
docs/functions/clearWAFPresets.md
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / clearWAFPresets
# Function: clearWAFPresets()
> **clearWAFPresets**(): `void`
Defined in: [packages/proxy/src/core/wafPresets.ts:87](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/wafPresets.ts#L87)
Clear all registered WAF presets.
## Returns
`void`
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / getWAFPresets
# Function: getWAFPresets()
> **getWAFPresets**(): [`ProxyCacheRule`](../interfaces/ProxyCacheRule.md)[]
Defined in: [packages/proxy/src/core/wafPresets.ts:64](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/wafPresets.ts#L64)
Get all current WAF presets.
## Returns
[`ProxyCacheRule`](../interfaces/ProxyCacheRule.md)[]
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / isResponseCacheable
# Function: isResponseCacheable()
> **isResponseCacheable**(`response`, `rule`, `options`): `Promise`\<[`ResponseCacheCheckResult`](../interfaces/ResponseCacheCheckResult.md)\>
Defined in: [packages/proxy/src/core/isResponseCacheable.ts:22](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/isResponseCacheable.ts#L22)
判断响应是否满足缓存条件 (响应侧校验)
## Parameters
### response
`Response`
### rule
[`ProxyCacheRule`](../interfaces/ProxyCacheRule.md)
### options
#### bodyText?
`string`
#### useWafPresets?
`boolean`
## Returns
`Promise`\<[`ResponseCacheCheckResult`](../interfaces/ResponseCacheCheckResult.md)\>
## Description
此函数执行以下检查:
1. 状态码匹配 (statuses)
2. 响应头匹配 (headers)
3. 最小长度校验 (minLength)
4. 响应体内容校验 (body) - 支持正向包含与负向 (!) 排除
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / isWAFChallenge
# Function: isWAFChallenge()
> **isWAFChallenge**(`response`, `presets`): `Promise`\<`boolean`\>
Defined in: [packages/proxy/src/core/wafPresets.ts:98](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/wafPresets.ts#L98)
高度可复用的简单好使的 WAF 挑战判定函数
## Parameters
### response
`Response`
Web 标准 Response 对象
### presets
[`ProxyCacheRule`](../interfaces/ProxyCacheRule.md)[] = `...`
自定义规则,默认使用内置所有已注册的 WAF 预设
## Returns
`Promise`\<`boolean`\>
是否为人机挑战页面
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / matchField
# Function: matchField()
> **matchField**(`source`, `config`, `__namedParameters`): `boolean`
Defined in: [packages/proxy/src/utils/matcher.ts:91](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/utils/matcher.ts#L91)
## Parameters
### source
`Record`\<`string`, `any`\> | `URLSearchParams` | `Headers`
### config
[`ProxyMatchPatterns`](../type-aliases/ProxyMatchPatterns.md) | [`ProxyFieldConfig`](../type-aliases/ProxyFieldConfig.md)
### \_\_namedParameters
#### defaultAllowed?
`boolean` = `true`
#### ignoreNegative?
`boolean`
## Returns
`boolean`
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / registerWAFPreset
# Function: registerWAFPreset()
> **registerWAFPreset**(`preset`): `void`
Defined in: [packages/proxy/src/core/wafPresets.ts:72](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/wafPresets.ts#L72)
Register a new WAF preset.
## Parameters
### preset
[`ProxyCacheRule`](../interfaces/ProxyCacheRule.md)
The WAF signature to detect.
## Returns
`void`
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / unregisterWAFPreset
# Function: unregisterWAFPreset()
> **unregisterWAFPreset**(`preset`): `void`
Defined in: [packages/proxy/src/core/wafPresets.ts:80](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/wafPresets.ts#L80)
Unregister a WAF preset.
## Parameters
### preset
[`ProxyCacheRule`](../interfaces/ProxyCacheRule.md)
The WAF signature to remove.
## Returns
`void`
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / CacheAnalysis
# Interface: CacheAnalysis
Defined in: [packages/proxy/src/core/isCacheable.ts:137](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/isCacheable.ts#L137)
Analysis of request cacheability (returned when cacheable).
请求可缓存性分析结果(通过门控时返回)。
## Properties
### bodyState
> **bodyState**: `object`
Defined in: [packages/proxy/src/core/isCacheable.ts:147](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/isCacheable.ts#L147)
Current body reading state (reusable for fingerprinting).
请求体读取状态(可供后续生成 Key 等环节复用,避免重复读取 Stream)。
#### checked
> **checked**: `boolean`
#### limit
> **limit**: `number`
#### text
> **text**: `string` \| `null`
***
### matchedRule
> **matchedRule**: [`ProxyCacheRule`](ProxyCacheRule.md) \| `null`
Defined in: [packages/proxy/src/core/isCacheable.ts:142](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/isCacheable.ts#L142)
The specific rule that matched the request.
匹配到的细化规则。
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / ResponseCacheCheckResult
# Interface: ResponseCacheCheckResult
Defined in: [packages/proxy/src/core/isResponseCacheable.ts:5](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/isResponseCacheable.ts#L5)
## Properties
### cacheable
> **cacheable**: `boolean`
Defined in: [packages/proxy/src/core/isResponseCacheable.ts:6](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/isResponseCacheable.ts#L6)
***
### keepOldCache?
> `optional` **keepOldCache**: `boolean`
Defined in: [packages/proxy/src/core/isResponseCacheable.ts:9](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/isResponseCacheable.ts#L9)
Whether we should keep the old cache if this response is deemed "dirty"
***
### reason?
> `optional` **reason**: `string`
Defined in: [packages/proxy/src/core/isResponseCacheable.ts:7](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/isResponseCacheable.ts#L7)
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / AWS\_WAF\_PRESET
# Variable: AWS\_WAF\_PRESET
> `const` **AWS\_WAF\_PRESET**: [`ProxyCacheRule`](../interfaces/ProxyCacheRule.md)
Defined in: [packages/proxy/src/core/wafPresets.ts:25](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/wafPresets.ts#L25)
AWS WAF specific Challenge/CAPTCHA detection signatures.
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / CLOUDFLARE\_WAF\_PRESET
# Variable: CLOUDFLARE\_WAF\_PRESET
> `const` **CLOUDFLARE\_WAF\_PRESET**: [`ProxyCacheRule`](../interfaces/ProxyCacheRule.md)
Defined in: [packages/proxy/src/core/wafPresets.ts:7](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/wafPresets.ts#L7)
Cloudflare specific WAF Challenge detection signatures.
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / GENERAL\_WAF\_PRESET
# Variable: GENERAL\_WAF\_PRESET
> `const` **GENERAL\_WAF\_PRESET**: [`ProxyCacheRule`](../interfaces/ProxyCacheRule.md)
Defined in: [packages/proxy/src/core/wafPresets.ts:37](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/wafPresets.ts#L37)
General/Common WAF and Bot detection signatures.
+1
-1

@@ -1,1 +0,1 @@

"use strict";var e,t=Object.create,r=Object.defineProperty,n=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,c=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t,c,o)=>{if(t&&"object"==typeof t||"function"==typeof t)for(let s of i(t))a.call(e,s)||s===c||r(e,s,{get:()=>t[s],enumerable:!(o=n(t,s))||o.enumerable});return e},s=(e,n,i)=>(i=null!=e?t(c(e)):{},o(!n&&e&&e.__esModule?i:r(i,"default",{value:e,enumerable:!0}),e)),u={};((e,t)=>{for(var n in t)r(e,n,{get:t[n],enumerable:!0})})(u,{OfflineCacheMissError:()=>y,OfflineCacheMissErrorCode:()=>l,OfflineCacheMissErrorMsg:()=>h,SmartCache:()=>m,createCachedFetch:()=>G,createFetchWithCache:()=>J,createResponse:()=>k,decorateResponseWithUrl:()=>M,extractData:()=>O,fetchWithCache:()=>D,generateCacheKey:()=>U,getEffectiveConfig:()=>q,getEffectiveConfigFromRequest:()=>H,getMatchedRule:()=>P,getSiteConfig:()=>S,isAllowed:()=>v,isCacheable:()=>F,isGlob:()=>E,isMatch:()=>R,normalizeBodyConfig:()=>T,prefetch:()=>K}),module.exports=(e=u,o(r({},"__esModule",{value:!0}),e));var f=require("@isdk/common-error"),l=f.ErrorCode.OfflineCacheMiss,h="Offline mode: No cached response",y=class extends f.CommonError{static code=l;constructor(e,t){super(`${h} for ${e}`,t,l),this.data={url:e}}};f.CommonError[l]=y;var d=require("secondary-cache"),p=s(require("cacache"),1),w=s(require("os"),1),b=s(require("path"),1),m=class{memory;storagePath;maxMemorySize;constructor(e={}){this.storagePath=e.storagePath||b.default.join(w.default.tmpdir(),"isdk-proxy-cache"),this.maxMemorySize=e.maxMemorySize??1048576;const t={capacity:0,expires:3e5,maxWeight:e.maxTotalMemorySize||104857600,weightOf:e=>{let t=0;return e.body&&Buffer.isBuffer(e.body)&&(t+=e.body.length),t+=512,t},...e.memoryOptions};this.memory=new d.LRUCache(t)}async get(e){const t=this.memory.get(e);if(t){if(t.body)return t;if(0===t.size)return{...t,body:Buffer.alloc(0)};const r=p.default.get.stream(this.storagePath,e);return{...t,body:r}}try{const t=await p.default.get.info(this.storagePath,e);if(!t)return null;if(t.size<=this.maxMemorySize){const{data:t,metadata:r}=await p.default.get(this.storagePath,e),n=r,i={...n,body:t};return this.saveToMemory(e,t,n),i}{const t=(await p.default.get.info(this.storagePath,e)).metadata;return this.saveToMemory(e,null,t),{...t,body:p.default.get.stream(this.storagePath,e)}}}catch(e){return null}}async set(e,t,r){const n={...r,size:t.length};await p.default.put(this.storagePath,e,t,{metadata:n}),this.saveToMemory(e,t,n)}saveToMemory(e,t,r){if(t&&t.length>0&&t.length<=this.maxMemorySize)this.memory.set(e,{...r,body:t});else{const{...t}=r;this.memory.set(e,t)}}getStream(e){return p.default.get.stream(this.storagePath,e)}setStream(e,t){this.memory.del(e);const r=p.default.put.stream(this.storagePath,e,{metadata:t});return r.on("finish",()=>{this.memory.del(e)}),r}async delete(e,t=!0){this.memory.del(e),t&&await p.default.rm.entry(this.storagePath,e)}async clear(e=!0){this.memory.clear(),e&&await p.default.rm.all(this.storagePath)}},g=require("crypto"),x=require("util-ex");function O(e,t,r=!0){const n={},i=e=>{if(null==e)return[];return(Array.isArray(e)?e.map(String):[String(e)]).filter(e=>null!=e&&"null"!==e&&"undefined"!==e).sort()};if(!t){if(r)for(const[t,r]of Object.entries(e)){const e=i(r);e.length>0&&(n[t.toLowerCase()]=e)}return n}if(Array.isArray(t)||"string"==typeof t||t instanceof RegExp)for(const c of Object.keys(e)){const a=i(e[c]);a.length>0&&v(c.toLowerCase(),t,r)&&(n[c.toLowerCase()]=a)}else for(const[r,c]of Object.entries(t)){const t=e[Object.keys(e).find(e=>e.toLowerCase()===r.toLowerCase())||r];if(void 0!==t)if(!0===c)n[r.toLowerCase()]=i(t);else if(!1===c);else{const e=i(t).filter(e=>R(c,e));e.length>0&&(n[r.toLowerCase()]=e.sort())}}return n}var j=s(require("picomatch"),1),C=require("util-ex");function E(e){return/[!*?{}[\]()]/.test(e)}function R(e,t,r=!1,n=!0,i=!0){if(Array.isArray(e)&&e.length){const c=[],a=[];return e.forEach(e=>{"string"==typeof e&&e.startsWith("!")?a.push(e.slice(1)):c.push(e)}),!(a.length>0&&a.some(e=>R(e,t,r,!0,i)))&&(0===c.length?n:c.some(e=>R(e,t,r,n,i)))}if(e instanceof RegExp)return e.test(t);if("string"==typeof e){const n=i?t.toLowerCase():t,c=i?e.toLowerCase():e;return(0,C.isRegExpStr)(e)?(0,C.toRegExp)(e).test(t):E(e)?(0,j.default)(c,{dot:!0})(n):r?n.startsWith(c):n===c}return!1}function v(e,t,r=!1){return null!=t?R(t,e,!1,r):r}function S(e,t){const{sites:r}=t;if(!r)return t;let n="";try{n=new URL(e).hostname}catch{}for(const[t,i]of Object.entries(r)){if(n&&t===n)return i;if(R(t,e,!0))return i;if(n&&n.endsWith(t)&&(t.startsWith(".")||"."===n.charAt(n.length-t.length-1)))return i}return t}var A=require("lodash-es");function T(e){return e?"object"!=typeof e||e instanceof RegExp||Array.isArray(e)?{match:e}:e:{}}function q(e,t){const r=(0,A.defaultsDeep)({},e,t);return(e.body||t.body)&&(r.body=(0,A.defaultsDeep)({},T(e.body),T(t.body))),r}function M(e,t){if(t&&e.url!==t){Object.defineProperty(e,"url",{value:t,writable:!1,enumerable:!0,configurable:!0});const r=e.clone;e.clone=function(){return M(r.call(this),t)}}return e}function k(e,t){const{url:r,...n}=t,i=new Response(e,n);return r?M(i,r):i}function L(e,t,r=!0){if(!t||"object"!=typeof t||Array.isArray(t)||t instanceof RegExp){const n=e instanceof URLSearchParams||e instanceof Headers?Array.from(e.keys()):Object.keys(e);return 0===n.length?r:n.some(e=>R(t,e))}for(const[r,n]of Object.entries(t)){let t=null,i=!1;if(e instanceof URLSearchParams||e instanceof Headers?(t=e.get(r),i=e.has(r)):(t=e[r]??null,i=void 0!==e[r]&&null!==e[r]),"boolean"==typeof n){if(n&&!i)return!1;if(!n&&i)return!1}else if(null===t||!R(n,t))return!1}return!0}async function P(e,t,r){const n=e.method.toUpperCase(),i=new URL(e.url),c=T(t.body).maxLength||1024,a=r||{text:null,checked:!1,limit:c};if(t.rules&&t.rules.length>0)for(const r of t.rules)if(await I(r,n,i,e,a))return r;return null}async function H(e,t){return q(await P(e,t)||{},t)}async function I(e,t,r,n,i){if(e.methods&&!R(e.methods,t))return!1;if(e.path&&!R(e.path,r.pathname,!0))return!1;if(e.query&&!L(r.searchParams,e.query,!0))return!1;if(e.headers&&!L(n.headers,e.headers,!1))return!1;if(e.cookies){const t=n.headers.get("cookie")||"";if(!L(Object.fromEntries(t.split(";").map(e=>e.trim()).filter(Boolean).map(e=>{const t=e.split("=");return[t[0],t.slice(1).join("=")]})),e.cookies,!1))return!1}if(e.body){const t=n.headers.get("content-type")||"",r=t.includes("application/json")?"json":t.includes("text/")||t.includes("application/xml")||t.includes("x-www-form-urlencoded")?"text":"binary";if("object"!=typeof e.body||Array.isArray(e.body)||e.body instanceof RegExp){if("binary"===r)return!1;if(!i.checked){try{i.text=(await n.clone().text()).slice(0,i.limit)}catch{i.text=""}i.checked=!0}if(!i.text||!R(e.body,i.text))return!1}else{const t=T(e.body),c=t.maxLength||i.limit;if(t.type&&t.type!==r)return!1;if(t.match&&"json"===r){if(!i.checked){try{i.text=(await n.clone().text()).slice(0,c),i.json=JSON.parse(i.text)}catch{i.json={}}i.checked=!0}if(!L(i.json,t.match,!0))return!1}if(t.extract&&"text"===r){if(!i.checked){try{i.text=(await n.clone().text()).slice(0,c)}catch{i.text=""}i.checked=!0}if(!i.text||!R(t.extract,i.text))return!1}}}return!0}async function F(e,t){const r=e.method.toUpperCase();if(!R(t.methods||["GET","HEAD"],r))return!1;if(t.rules&&t.rules.length>0){return null!==await P(e,t)}return!0}async function U(e,t){const r=new URL(e.url),n=e.method.toUpperCase(),i=await H(e,t),c=e.headers.get("cookie")||"",a=Object.fromEntries(c.split(";").map(e=>e.trim()).filter(Boolean).map(e=>{const t=e.split("=");return[t[0],t.slice(1).join("=")]}));let o=null;if(["POST","PUT","PATCH"].includes(n))try{const t=e.headers.get("content-type")||"",r=T(i.body);if(t.includes("application/json")){const t=await e.clone().json();o=O(t,r.match||i.body,!0)}else if(r.extract&&(t.includes("text/")||t.includes("application/xml")||t.includes("x-www-form-urlencoded"))){const t=r.maxLength||1024,n=(await e.clone().text()).slice(0,t),i=r.extract,c="string"==typeof i&&(0,x.isRegExpStr)(i)?(0,x.toRegExp)(i):i instanceof RegExp?i:null;if(c){const e=n.match(c);if(e)if(e.length>1){const t=e.slice(1);r.sort&&t.sort(),o=t.join(":")}else o=e[0]}else o=(0,g.createHash)("sha256").update(n).digest("hex")}else{const t=await e.clone().arrayBuffer();t.byteLength>0&&(o=(0,g.createHash)("sha256").update(new Uint8Array(t)).digest("hex"))}}catch(e){}const s=i.headers;let u=s;Array.isArray(s)?u=[...s,"!cookie"]:"string"==typeof s?u=[s,"!cookie"]:s&&"object"==typeof s?(u={...s},delete u.cookie,delete u.Cookie):void 0===s&&(u=["!*"]);const f={m:n,h:r.host,p:r.pathname,q:O(Object.fromEntries(r.searchParams),i.query,!0),hd:O(Object.fromEntries(e.headers),u,!1),ck:O(a,i.cookies,!1)};return null!==o&&(f.b=o),(0,g.createHash)("sha256").update(JSON.stringify(f)).digest("hex")}var W=require("stream"),_=require("stream/promises"),B=s(require("http-cache-semantics"),1),N=(0,require("debug").debug)("@isdk/proxy:fetchWithCache");function $(e,t){return k(204===e.status||304===e.status||e.status<200?null:function(e){if(e instanceof Buffer)return new Uint8Array(e);if(e&&"function"==typeof e.pipe)try{const t="function"==typeof e._read&&"object"==typeof e._readableState?e:W.Readable.from(e);return W.Readable.toWeb(t)}catch(t){return e}return e}(e.body),{status:e.status,headers:{...e.headers,"x-proxy-cache":t},url:e.url})}async function z(e,t){let r,n;const i=new Promise((t,i)=>{r=()=>{e.activeCacheWrites.delete(e.cacheKey),t()},n=t=>{e.activeCacheWrites.delete(e.cacheKey),i(t)}});i.catch(()=>{}),e.activeCacheWrites.set(e.cacheKey,i);try{const t=await e.fetcher(e.request.clone()),i=new B.default({url:e.request.url,method:e.request.method,headers:Object.fromEntries(e.request.headers)},{status:t.status,headers:Object.fromEntries(t.headers)}),c=new Headers(t.headers);if(c.set("x-proxy-cache","MISS"),N("executeFetch And Cache",e.request.url),!i.storable()&&!e.effectiveConfig.forceCache)return r(),k(t.body,{status:t.status,statusText:t.statusText,headers:c,url:t.url});const a={status:t.status,headers:Object.fromEntries(t.headers),policy:i.toObject(),url:e.request.url,method:e.request.method,timestamp:Date.now()};if(!t.body)return await e.cache.set(e.cacheKey,Buffer.alloc(0),a),r(),k(null,{status:t.status,statusText:t.statusText,headers:c,url:t.url});const[o,s]=t.body.tee();return(0,_.pipeline)(W.Readable.fromWeb(s),e.cache.setStream(e.cacheKey,a)).then(r).catch(n),k(o,{status:t.status,statusText:t.statusText,headers:c,url:t.url})}catch(r){if(n(r),t&&e.effectiveConfig.staleIfError)return $(t,"STALE_IF_ERROR");throw r}}async function D(e,t,r){const n=await async function(e,t,r){const n=r.generateKey||U,i=await n(e,r.config),c=await H(e,r.config);return{...r,request:e,fetcher:t,cacheKey:i,effectiveConfig:c,activeCacheWrites:r.activeCacheWrites||new Map}}(e,t,r),{effectiveConfig:i}=n,c=await n.cache.get(n.cacheKey);if(i.offline)return c?$(c,"OFFLINE_HIT"):k(h,{status:l,headers:{"x-proxy-cache":"OFFLINE_HIT"},url:e.url});if(!await F(e,r.config))return t(e);if(c){const t=function(e,t){const r=B.default.fromObject(t.policy),n={url:t.url,method:e.request.method,headers:Object.fromEntries(e.request.headers)};return r.satisfiesWithoutRevalidation(n)?"HIT":"STALE"}(n,c);if(N("evaluateCachePolicy:",e.url,t),c.policy?.resh&&Object.keys(c.policy.resh).length&&N("evaluateCachePolicy:","resh =",JSON.stringify(c.policy.resh)),"HIT"===t)return $(c,"HIT");if("STALE"===t&&!1!==r.backgroundUpdate)return function(e,t){if(e.activeCacheWrites.has(e.cacheKey))return;const r=z(e,t).catch(r=>(console.error(`[SWR Error] ${e.cacheKey}:`,r),$(t,"STALE_IF_ERROR")));try{e.onBackgroundUpdate?.(r)}catch(t){console.error(`[SWR Callback Error] ${e.cacheKey}:`,t)}}(n,c),$(c,"STALE")}if(n.activeCacheWrites.has(n.cacheKey)){const t=await async function(e){const t=e.activeCacheWrites.get(e.cacheKey);if(!t)return null;await t;const r=await e.cache.get(e.cacheKey);return r?$(r,"HIT"):null}(n);if(t)return N("activeCacheWrites has this, waiting response",e.url),t}return z(n,c)}function J(e){return e||(e=new Map),async function(t,r,n){return D(t,r,{...n,activeCacheWrites:e})}}function G(e){const t=J(e.activeCacheWrites);return async function(r,n,i){return t(r,n,{...e,...i})}}async function K(e){const{urls:t,config:r,cache:n,fetcher:i=e=>globalThis.fetch(e),concurrency:c=3,onProgress:a,signal:o}=e,s={succeeded:0,failed:0,errors:[]};if(0===t.length)return s;if(o?.aborted)return s;const u=new Map,f=J(u),l=[...t];let h=0;const y=Array.from({length:Math.min(c,t.length)},()=>(async()=>{for(;l.length>0&&!o?.aborted;){const e=l.shift();if(!e)break;try{const t=S(e.url,r),c=new Request(e.url,{...e.request,signal:o});if(!await F(c,t))continue;const a=await f(c,i,{cache:n,config:{...t,offline:!1},backgroundUpdate:!1});a.headers.has("x-proxy-cache")&&(await a.arrayBuffer(),s.succeeded++)}catch(t){if("AbortError"===t.name||o?.aborted)break;s.failed++,s.errors.push({url:e.url,error:t})}finally{h++,a?.(h,t.length,e.url)}}})());return await Promise.all(y),u.size>0&&await Promise.allSettled(u.values()),s}
"use strict";var e,t=Object.create,r=Object.defineProperty,n=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,s=(e,t,a,s)=>{if(t&&"object"==typeof t||"function"==typeof t)for(let o of i(t))c.call(e,o)||o===a||r(e,o,{get:()=>t[o],enumerable:!(s=n(t,o))||s.enumerable});return e},o=(e,n,i)=>(i=null!=e?t(a(e)):{},s(!n&&e&&e.__esModule?i:r(i,"default",{value:e,enumerable:!0}),e)),u={};((e,t)=>{for(var n in t)r(e,n,{get:t[n],enumerable:!0})})(u,{AWS_WAF_PRESET:()=>z,CLOUDFLARE_WAF_PRESET:()=>$,GENERAL_WAF_PRESET:()=>J,OfflineCacheMissError:()=>d,OfflineCacheMissErrorCode:()=>l,OfflineCacheMissErrorMsg:()=>h,SmartCache:()=>g,clearWAFPresets:()=>Z,createCachedFetch:()=>ce,createFetchWithCache:()=>ae,createResponse:()=>W,decorateResponseWithUrl:()=>N,extractData:()=>E,fetchWithCache:()=>ie,generateCacheKey:()=>k,getEffectiveConfig:()=>P,getEffectiveConfigFromRequest:()=>I,getMatchedRule:()=>F,getSiteConfig:()=>_,getWAFPresets:()=>G,isAllowed:()=>C,isCacheable:()=>U,isGlob:()=>S,isMatch:()=>v,isResponseCacheable:()=>Y,isWAFChallenge:()=>V,matchField:()=>R,normalizeBodyConfig:()=>T,prefetch:()=>se,registerWAFPreset:()=>X,unregisterWAFPreset:()=>Q}),module.exports=(e=u,s(r({},"__esModule",{value:!0}),e));var f=require("@isdk/common-error"),l=f.ErrorCode.OfflineCacheMiss,h="Offline mode: No cached response",d=class extends f.CommonError{static code=l;constructor(e,t){super(`${h} for ${e}`,t,l),this.data={url:e}}};f.CommonError[l]=d;var y=require("secondary-cache"),p=o(require("cacache"),1),w=o(require("os"),1),b=o(require("path"),1),g=class{memory;storagePath;maxMemorySize;initialized;constructor(e={}){this.init(e)}init(e){if(e)this.initialized&&this.free();else{if(this.initialized)return;e=this}this!==e&&(this.storagePath=e.storagePath||b.default.join(w.default.tmpdir(),"isdk-proxy-cache"),this.maxMemorySize=e.maxMemorySize??1048576);const t={capacity:0,expires:3e5,maxWeight:e.maxTotalMemorySize||104857600,weightOf:e=>{let t=0;return e.body&&Buffer.isBuffer(e.body)&&(t+=e.body.length),t+=512,t},...e.memoryOptions};this.memory=new y.LRUCache(t),this.initialized=!0}free(){this.memory?.clear(),p.default.clearMemoized(),this.initialized=!1}async get(e){const t=this.memory.get(e);if(t){if(t.body)return t;if(0===t.size)return{...t,body:Buffer.alloc(0)};const r=p.default.get.stream(this.storagePath,e);return{...t,body:r}}try{const t=await p.default.get.info(this.storagePath,e);if(!t)return null;if(t.size<=this.maxMemorySize){const{data:t,metadata:r}=await p.default.get(this.storagePath,e),n=r,i={...n,body:t};return this.saveToMemory(e,t,n),i}{const t=(await p.default.get.info(this.storagePath,e)).metadata;return this.saveToMemory(e,null,t),{...t,body:p.default.get.stream(this.storagePath,e)}}}catch(e){return null}}async set(e,t,r){const n={...r,size:t.length};await p.default.put(this.storagePath,e,t,{metadata:n}),this.saveToMemory(e,t,n)}saveToMemory(e,t,r){if(t&&t.length>0&&t.length<=this.maxMemorySize)this.memory.set(e,{...r,body:t});else{const{...t}=r;this.memory.set(e,t)}}getStream(e){return p.default.get.stream(this.storagePath,e)}setStream(e,t){this.memory.del(e);const r=p.default.put.stream(this.storagePath,e,{metadata:t});return r.on("finish",()=>{this.memory.del(e)}),r}async delete(e,t=!0){this.memory.del(e),t&&await p.default.rm.entry(this.storagePath,e)}async clear(e=!0){this.memory.clear(),e&&await p.default.rm.all(this.storagePath)}},x=require("crypto"),m=require("util-ex");function E(e,t,r=!0){const n={},i=e=>{if(null==e)return[];return(Array.isArray(e)?e.map(String):[String(e)]).filter(e=>null!=e&&"null"!==e&&"undefined"!==e).sort()};if(!t){if(r)for(const[t,r]of Object.entries(e)){const e=i(r);e.length>0&&(n[t.toLowerCase()]=e)}return n}if(Array.isArray(t)||"string"==typeof t||t instanceof RegExp)for(const a of Object.keys(e)){const c=i(e[a]);c.length>0&&C(a.toLowerCase(),t,r)&&(n[a.toLowerCase()]=c)}else for(const[r,a]of Object.entries(t)){const t=e[Object.keys(e).find(e=>e.toLowerCase()===r.toLowerCase())||r];if(void 0!==t)if(!0===a)n[r.toLowerCase()]=i(t);else if(!1===a);else{const e=i(t).filter(e=>v(a,e));e.length>0&&(n[r.toLowerCase()]=e.sort())}}return n}var A=o(require("picomatch"),1),O=require("util-ex");function S(e){return/[!*?{}[\]()]/.test(e)}function v(e,t,{usePrefix:r=!1,defaultIfNoPositives:n=!0,ignoreCase:i=!0,ignoreNegative:a}={}){if(Array.isArray(e)&&e.length){const c=[],s=[];return e.forEach(e=>{"string"==typeof e&&e.startsWith("!")?s.push(e.slice(1)):c.push(e)}),!(!a&&s.length>0&&s.some(e=>v(e,t,{usePrefix:r,defaultIfNoPositives:!0,ignoreCase:i})))&&(0===c.length?n:c.some(e=>v(e,t,{usePrefix:r,defaultIfNoPositives:n,ignoreCase:i})))}if(e instanceof RegExp)return e.test(t);const c=null==e?"":String(e),s=i?t.toLowerCase():t,o=i?c.toLowerCase():c;return(0,O.isRegExpStr)(c)?(0,O.toRegExp)(c).test(t):S(o)?(0,A.default)(o,{dot:!0,bash:!0})(s):r?s.startsWith(o):s===o}function R(e,t,{defaultAllowed:r=!0,ignoreNegative:n}={}){if(!t||"object"!=typeof t||Array.isArray(t)||t instanceof RegExp){const n=e instanceof URLSearchParams||e instanceof Headers?Array.from(e.keys()):Object.keys(e);if(0===n.length)return r;return Array.isArray(t)?n.some(e=>v(t,e,{ignoreNegative:!0})):n.every(e=>v(t,e))}for(const[r,i]of Object.entries(t)){let t=null,a=!1;if(e instanceof URLSearchParams||e instanceof Headers?(t=e.get(r),a=e.has(r)):(t=e[r]??null,a=void 0!==e[r]&&null!==e[r]),"boolean"==typeof i){if(i&&!a)return!1;if(!i&&a)return!1}else if(null===t||!v(i,t,{ignoreNegative:n}))return!1}return!0}function C(e,t,r=!1){return null!=t?v(t,e,{usePrefix:!1,defaultIfNoPositives:r}):r}function _(e,t){const{sites:r}=t;if(!r)return t;let n="";try{n=new URL(e).hostname}catch{}for(const[t,i]of Object.entries(r)){if(n&&t===n)return i;if(v(t,e,{usePrefix:!0}))return i;if(n&&n.endsWith(t)&&(t.startsWith(".")||"."===n.charAt(n.length-t.length-1)))return i}return t}var j=require("lodash-es");function T(e){return e?"object"!=typeof e||e instanceof RegExp||Array.isArray(e)?{match:e}:e:{}}function P(e,t){const r=(0,j.defaultsDeep)({},e,t);return(e.body||t.body)&&(r.body=(0,j.defaultsDeep)({},T(e.body),T(t.body))),r}function N(e,t){if(t&&e.url!==t){Object.defineProperty(e,"url",{value:t,writable:!1,enumerable:!0,configurable:!0});const r=e.clone;e.clone=function(){return N(r.call(this),t)}}return e}function W(e,t){const{url:r,...n}=t,i=new Response(e,n);return r?N(i,r):i}async function F(e,t,r){const n=e.method.toUpperCase(),i=new URL(e.url),a=T(t.body).maxLength||1024,c=r||{text:null,checked:!1,limit:a};if(t.rules&&t.rules.length>0)for(const r of t.rules)if(await L(r,n,i,e,c))return r;return null}async function I(e,t){return P(await F(e,t)||{},t)}async function L(e,t,r,n,i){if(e.methods&&!v(e.methods,t))return!1;if(e.path&&!v(e.path,r.pathname,{usePrefix:!0}))return!1;if(e.query&&!R(r.searchParams,e.query,{defaultAllowed:!0}))return!1;if(e.headers&&!R(n.headers,e.headers,{defaultAllowed:!1}))return!1;if(e.cookies){const t=n.headers.get("cookie")||"";if(!R(Object.fromEntries(t.split(";").map(e=>e.trim()).filter(Boolean).map(e=>{const t=e.split("=");return[t[0],t.slice(1).join("=")]})),e.cookies,{defaultAllowed:!1}))return!1}if(e.body){const t=n.headers.get("content-type")||"",r=t.includes("application/json")?"json":t.includes("text/")||t.includes("application/xml")||t.includes("x-www-form-urlencoded")?"text":"binary";if("object"!=typeof e.body||Array.isArray(e.body)||e.body instanceof RegExp){if("binary"===r)return!1;if(!i.checked){try{i.text=(await n.clone().text()).slice(0,i.limit)}catch{i.text=""}i.checked=!0}if(!i.text||!v(e.body,i.text))return!1}else{const t=T(e.body),a=t.maxLength||i.limit;if(t.type&&t.type!==r)return!1;if(t.match&&"json"===r){if(!i.checked){try{i.text=(await n.clone().text()).slice(0,a),i.json=JSON.parse(i.text)}catch{i.json={}}i.checked=!0}if(!R(i.json,t.match,{defaultAllowed:!0}))return!1}if(t.match&&"text"===r){const e=t.match;if(!("string"==typeof e||Array.isArray(e)||e instanceof RegExp))return!1;if(!i.checked){try{i.text=(await n.clone().text()).slice(0,a)}catch{i.text=""}i.checked=!0}if(!i.text||!v(e,i.text))return!1}}}return!0}async function U(e,t){const r=e.method.toUpperCase(),n=new URL(e.url),i={text:null,checked:!1,limit:T(t.body).maxLength||1024},a={...t,methods:t.methods||["GET","HEAD"]};if(!await L(a,r,n,e,i))return;let c=null;return t.rules&&t.rules.length>0&&(c=await F(e,t,i),!c)?void 0:{matchedRule:c,bodyState:i}}async function k(e,t,r,n){const i=new URL(e.url),a=e.method.toUpperCase(),c=n||await I(e,t),s=e.headers.get("cookie")||"",o=Object.fromEntries(s.split(";").map(e=>e.trim()).filter(Boolean).map(e=>{const t=e.split("=");return[t[0],t.slice(1).join("=")]}));let u=null;if(["POST","PUT","PATCH"].includes(a))try{const t=e.headers.get("content-type")||"",n=T(c.body);if(t.includes("application/json")){let t=r?.json;t||(t=r?.text?JSON.parse(r.text):await e.clone().json());u=E(t,n.extract||n.match||c.body,!0)}else if(n.extract&&(t.includes("text/")||t.includes("application/xml")||t.includes("x-www-form-urlencoded"))){const t=n.maxLength||1024,i=r?.text||(await e.clone().text()).slice(0,t),a=n.extract,c="string"==typeof a&&(0,m.isRegExpStr)(a)?(0,m.toRegExp)(a):a instanceof RegExp?a:null;if(c){const e=i.match(c);if(e)if(e.length>1){const t=e.slice(1);n.sort&&t.sort(),u=t.join(":")}else u=e[0]}else u=(0,x.createHash)("sha256").update(i).digest("hex")}else{const t=await e.clone().arrayBuffer();t.byteLength>0&&(u=(0,x.createHash)("sha256").update(new Uint8Array(t)).digest("hex"))}}catch(e){}const f=c.headers;let l=f;Array.isArray(f)?l=[...f,"!cookie"]:"string"==typeof f?l=[f,"!cookie"]:f&&"object"==typeof f?(l={...f},delete l.cookie,delete l.Cookie):void 0===f&&(l=["!*"]);const h={m:a,h:i.host,p:i.pathname,q:E(Object.fromEntries(i.searchParams),c.query,!0),hd:E(Object.fromEntries(e.headers),l,!1),ck:E(o,c.cookies,!1)};return null!==u&&(h.b=u),(0,x.createHash)("sha256").update(JSON.stringify(h)).digest("hex")}var q=require("stream"),M=require("stream/promises"),H=o(require("http-cache-semantics"),1),D=require("debug"),B=require("lodash-es"),$={response:{statuses:["403","429","503"],body:["*<title>Just a moment...</title>*","*__cf_chl_opt*","*cf-browser-verification*","*cf-ray*"],headers:{"cf-mitigated":!0}}},z={response:{statuses:["202","405"],headers:{"x-amzn-waf-action":!0}}},J={response:{statuses:["403","429"],body:["*captcha-delivery.com*","*g-recaptcha*","*h-captcha*","*verify you are human*","*security check to access*","*bot detection*","*interstitial*"]}},K=new Set([$,z,J]);function G(){return Array.from(K)}function X(e){K.add(e)}function Q(e){K.delete(e)}function Z(){K.clear()}async function V(e,t=G()){const r=e.status.toString(),n=e.headers;let i;for(const a of t){const t=a.response;if(t){if(t.statuses&&v(t.statuses,r))return!0;if(t.headers&&R(n,t.headers))return!0;if(t.body){if(void 0===i)try{i=await e.clone().text()}catch(e){}if(void 0!==i&&v(t.body,i))return!0}}}return!1}async function Y(e,t,r={}){const{useWafPresets:n=!0}=r,i=e.status,a=e.headers;if(n&&await V(e))return{cacheable:!1,reason:"waf_challenge",keepOldCache:!0};const c=t.response,s=c?.statuses||[200,203,204,206,300,301,404,405,410,414].map(e=>e.toString());if(s&&!v(s,i.toString())){return{cacheable:!1,reason:`status_mismatch:${i}`,keepOldCache:202===i||403===i||405===i||428===i||429===i||i>=500&&i<600}}if(c){if(c.headers&&!R(a,c.headers,{defaultAllowed:!0}))return{cacheable:!1,reason:"headers_mismatch"};if(void 0!==c.minLength){const e=a.get("content-length"),t=e?parseInt(e,10):0;if(e&&t<c.minLength)return{cacheable:!1,reason:"too_short",keepOldCache:!0}}if(c.body){const t=a.get("content-type")||"";if((t.includes("text/")||t.includes("application/json")||t.includes("application/xml"))&&e.body){let t=r.bodyText;if(void 0===t)try{t=await e.clone().text()}catch(e){return{cacheable:!1,reason:"body_read_error"}}if(void 0!==t){const e=Buffer.byteLength(t);if(void 0!==c.minLength&&e<c.minLength)return{cacheable:!1,reason:"too_short",keepOldCache:!0};if(c.body&&!v(c.body,t))return{cacheable:!1,reason:"body_match_failed",keepOldCache:!0}}}}}return{cacheable:!0}}var ee=(0,D.debug)("@isdk/proxy:fetchWithCache");function te(e){if(e instanceof Buffer)return new Uint8Array(e);if(e&&"function"==typeof e.pipe)try{const t="function"==typeof e._read&&"object"==typeof e._readableState?e:q.Readable.from(e);return q.Readable.toWeb(t)}catch(t){return e}return e}function re(e,t){return W(204===e.status||304===e.status||e.status<200?null:te(e.body),{status:e.status,headers:{...e.headers,"x-proxy-cache":t},url:e.url})}async function ne(e,t){let r,n;const i=new Promise((t,i)=>{r=()=>{e.activeCacheWrites.delete(e.cacheKey),t()},n=t=>{e.activeCacheWrites.delete(e.cacheKey),i(t)}});i.catch(()=>{}),e.activeCacheWrites.set(e.cacheKey,i);try{const i=await e.fetcher(e.request.clone()),a=new Headers(i.headers),c=await Y(i,e.effectiveConfig);if(!c.cacheable){if(ee("Response not cacheable:",c.reason),r(),c.keepOldCache&&t){const e=c.reason?.toUpperCase().replace(/[^A-Z0-9]/g,"_")||"UNKNOWN";return ee(`Triggering DR protection (${e}), returning old cache`),re(t,`STALE_RESCUE_${e}`)}const e=c.reason?.toUpperCase().replace(/[^A-Z0-9]/g,"_")||"UNKNOWN";return a.set("x-proxy-cache",`MISS_EXCLUDED_${e}`),W(i.body,{status:i.status,statusText:i.statusText,headers:a,url:i.url})}const s=new H.default({url:e.request.url,method:e.request.method,headers:Object.fromEntries(e.request.headers)},{status:i.status,headers:Object.fromEntries(i.headers)});ee("executeFetch And Cache",e.request.url);if(!(s.storable()||e.effectiveConfig.forceCache))return r(),a.set("x-proxy-cache","MISS_UNSTORABLE"),W(i.body,{status:i.status,statusText:i.statusText,headers:a,url:i.url});a.set("x-proxy-cache","MISS");const o={status:i.status,headers:Object.fromEntries(i.headers),policy:s.toObject(),url:e.request.url,method:e.request.method,timestamp:Date.now()};if(!i.body)return await e.cache.set(e.cacheKey,Buffer.alloc(0),o),r(),W(null,{status:i.status,statusText:i.statusText,headers:a,url:i.url});const[u,f]=i.body.tee();return(0,M.pipeline)(q.Readable.fromWeb(f),e.cache.setStream(e.cacheKey,o)).then(r).catch(n),W(u,{status:i.status,statusText:i.statusText,headers:a,url:i.url})}catch(r){if(n(r),t&&e.effectiveConfig.staleIfError)return re(t,"STALE_IF_ERROR");throw r}}async function ie(e,t,r){const n=e.isdkProxy||{};r=(0,B.defaultsDeep)({},n,r);const{config:i,cache:a}=r,c=await U(e,i);let s=P(c?.matchedRule||{},i);if(n.config&&(s=(0,B.defaultsDeep)({},n.config,s)),!c){if(s.offline)return W(h,{status:l,headers:{"x-proxy-cache":"OFFLINE_MISS_EXCLUDED_REQUEST"},url:e.url});const r=await t(e)||{},n=new Headers(r.headers);return n.set("x-proxy-cache","MISS_EXCLUDED_REQUEST"),W(te(r.body),{status:r.status,statusText:r.statusText,headers:n,url:r.url})}const{bodyState:o}=c,u=r.generateKey||k,f=await u(e,i,o,s),d={...r,request:e,fetcher:t,cacheKey:f,effectiveConfig:s,activeCacheWrites:r.activeCacheWrites||new Map},y=await a.get(f);if(s.offline)return y?re(y,"OFFLINE_HIT"):W(h,{status:l,headers:{"x-proxy-cache":"OFFLINE_HIT"},url:e.url});if(y&&!r.refresh){const t=function(e,t){const r=H.default.fromObject(t.policy),n={url:t.url,method:e.request.method,headers:Object.fromEntries(e.request.headers)};return r.satisfiesWithoutRevalidation(n)?"HIT":"STALE"}(d,y);if(ee("evaluateCachePolicy:",e.url,t),"HIT"===t)return re(y,"HIT");if("STALE"===t&&!1!==r.backgroundUpdate)return function(e,t){if(e.activeCacheWrites.has(e.cacheKey))return;const r=ne(e,t).catch(r=>(console.error(`[SWR Error] ${e.cacheKey}:`,r),re(t,"STALE_IF_ERROR")));try{e.onBackgroundUpdate?.(r)}catch(t){console.error(`[SWR Callback Error] ${e.cacheKey}:`,t)}}(d,y),re(y,"STALE")}if(d.activeCacheWrites.has(f)){const t=await async function(e){const t=e.activeCacheWrites.get(e.cacheKey);if(!t)return null;await t;const r=await e.cache.get(e.cacheKey);return r?re(r,"HIT"):null}(d);if(t){if(ee("activeCacheWrites has this, waiting response",e.url),r.refresh){const e=new Headers(t.headers);return e.set("x-proxy-cache","MISS"),W(t.body,{status:t.status,statusText:t.statusText,headers:e,url:t.url})}return t}}return ne(d,y)}function ae(e){return e||(e=new Map),async function(t,r,n){return ie(t,r,{...n,activeCacheWrites:e})}}function ce(e){const t=ae(e.activeCacheWrites);return async function(r,n,i){return t(r,n,{...e,...i,activeCacheWrites:i?.activeCacheWrites||e.activeCacheWrites,refresh:i?.refresh})}}async function se(e){const{urls:t,config:r,cache:n,fetcher:i=e=>globalThis.fetch(e),concurrency:a=3,onProgress:c,signal:s}=e,o={succeeded:0,failed:0,errors:[]};if(0===t.length)return o;if(s?.aborted)return o;const u=new Map,f=ae(u),l=[...t];let h=0;const d=Array.from({length:Math.min(a,t.length)},()=>(async()=>{for(;l.length>0&&!s?.aborted;){const e=l.shift();if(!e)break;try{const t=_(e.url,r),a=new Request(e.url,{...e.request,signal:s});if(!await U(a,t))continue;const c=await f(a,i,{cache:n,config:{...t,offline:!1},backgroundUpdate:!1});c.headers.has("x-proxy-cache")&&(await c.arrayBuffer(),o.succeeded++)}catch(t){if("AbortError"===t.name||s?.aborted)break;o.failed++,o.errors.push({url:e.url,error:t})}finally{h++,c?.(h,t.length,e.url)}}})());return await Promise.all(d),u.size>0&&await Promise.allSettled(u.values()),o}

@@ -62,11 +62,13 @@ import { CommonError, ErrorCode } from '@isdk/common-error';

/**
* Field-level matching and extraction for JSON bodies.
* 针对 JSON Body 的字段级匹配与提取。
* Body matching rules (used for Gatekeeping).
* Supports JSON field-level matching or string/regex matching for text bodies.
* Body 匹配规则(用于门控)。支持针对 JSON 的字段级匹配,或针对文本 Body 的字符串/正则匹配。
*/
match?: ProxyFieldConfig | ProxyMatchPatterns;
/**
* Regex for extracting data from non-JSON (text) bodies.
* 用于非 JSON (文本) Body 的提取正则表达式。
* Data extraction rules (used for Fingerprinting).
* Supports JSON field filtering or Regex for text bodies.
* 数据提取规则(用于指纹提取)。支持 JSON 字段过滤或针对文本 Body 的正则提取。
*/
extract?: string | RegExp;
extract?: ProxyFieldConfig | ProxyMatchPatterns;
/**

@@ -124,2 +126,29 @@ * Whether to sort extracted JSON keys or regex capture groups to ensure fingerprint consistency.

/**
* Response-side cacheability criteria.
* 响应侧可缓存性判定准则。
*/
response?: {
/**
* Allowed HTTP statuses.
* 允许缓存的状态码模式。
*/
statuses?: ProxyMatchPatterns;
/**
* Required or forbidden response headers.
* 响应头匹配要求。
*/
headers?: ProxyFieldConfig;
/**
* Response body matching patterns (for text/json).
* Supports Glob negation (e.g., "!*captcha*") to exclude dirty data.
* 响应体匹配模式(仅限文本/JSON)。支持 Glob 否定(如 "!*captcha*")来排除脏数据。
*/
body?: ProxyMatchPatterns;
/**
* Minimum body length (in bytes) to be considered valid.
* 最小有效响应体长度(字节),防止缓存截断或错误页面。
*/
minLength?: number;
};
/**
* Fault tolerance: If backend fails (network error or 5xx), return stale cache if available.

@@ -198,15 +227,48 @@ * 容错机制:当后端请求失败且存在旧缓存时,强制返回旧缓存。

* SmartCache 选项
* @example
* ```ts
* const cache = new SmartCache({
* storagePath: '/tmp/my-cache',
* maxMemorySize: 2 * 1024 * 1024, // 2MB
* maxTotalMemorySize: 200 * 1024 * 1024, // 200MB
* memoryOptions: {
* capacity: 1000,
* expires: 10 * 60 * 1000 // 10分钟
* }
* });
* ```
*/
interface SmartCacheOptions {
/** 磁盘缓存的物理路径。如果不提供,将默认使用系统临时目录。 */
/**
* 磁盘缓存的物理路径。
* @description 如果不提供,将默认使用系统临时目录 (`os.tmpdir()`) 下的 `isdk-proxy-cache` 目录。
* @default os.tmpdir() + '/isdk-proxy-cache'
*/
storagePath?: string;
/** 内存缓存阈值(字节)。响应体大小超过此值时,Body 将只存入磁盘,而 Meta 仍保留在内存。默认 1MB。 */
/**
* 内存缓存阈值(字节)。
* @description 响应体大小超过此值时,Body 将只存入磁盘,而 Meta 元数据仍保留在内存中。
* 此优化可减少大文件对内存的占用。
* @default 1024 * 1024 (1MB)
*/
maxMemorySize?: number;
/** 内存缓存总大小阈值(字节)。默认 100MB。超过此值将清空内存缓存。 */
/**
* 内存缓存总大小阈值(字节)。
* @description 超过此值时,LRU 缓存会自动清除最久未使用的条目以释放内存。
* @default 100 * 1024 * 1024 (100MB)
*/
maxTotalMemorySize?: number;
/** 透传给 L1 (Memory) 的高级配置 (secondary-cache LRUCache options) */
/**
* 透传给 L1 内存缓存的高级配置。
* @description 基于 secondary-cache 的 LRUCache 选项,可自定义容量、过期时间等参数。
* @see https://www.npmjs.com/package/secondary-cache
*/
memoryOptions?: {
/** LRU 缓存的最大条目数,为 0 时仅按 maxWeight 限制 */
capacity?: number;
/** 缓存条目过期时间(毫秒),默认 5 分钟 */
expires?: number;
/** 清理检查间隔(毫秒) */
cleanInterval?: number;
/** 允许添加其他 LRUCache 支持的选项 */
[key: string]: any;

@@ -217,18 +279,149 @@ };

* 智能混合缓存类 (Hybrid Multi-tier Cache)
*
* @description
* 实现 L1 内存缓存 + L2 磁盘缓存的两级缓存架构:
* - **L1 (Memory)**: 基于 LRUCache 的内存缓存,存储最近使用的热点数据
* - **L2 (Disk)**: 基于 cacache 的持久化磁盘缓存,支持大文件存储
*
* ### 缓存策略
* 1. **读取时**: 先查内存,未命中则查磁盘;磁盘命中且小于 `maxMemorySize` 时回填内存
* 2. **写入时**: 同时写入磁盘和内存(大文件 body 不进内存)
* 3. **大文件优化**: 超过 `maxMemorySize` 的响应只存磁盘,元数据存内存
*
* ### 适用场景
* - HTTP 响应缓存,减少重复请求
* - 大文件流式缓存,内存友好
* - 需要持久化 + LRU 淘汰的缓存场景
*
* @example
* ```ts
* import { SmartCache } from '@isdk/proxy';
*
* const cache = new SmartCache({ maxMemorySize: 2 * 1024 * 1024 });
*
* // 写入缓存
* await cache.set('key1', Buffer.from('hello'), {
* url: 'https://api.example.com/data',
* createdAt: Date.now()
* });
*
* // 读取缓存
* const entry = await cache.get('key1');
* if (entry) {
* console.log(entry.body.toString());
* }
*
* // 流式写入(适用于大文件)
* const writeStream = cache.setStream('large-file', { url: '...' });
* fs.createReadStream('big-file.zip').pipe(writeStream);
*
* // 流式读取
* const readStream = cache.getStream('large-file');
* readStream.pipe(fs.createWriteStream('output.zip'));
*
* // 清理
* await cache.clear();
* ```
*/
declare class SmartCache {
/** L1 内存缓存实例 */
private memory;
/** L2 磁盘缓存路径 */
private storagePath;
/** 单条目内存阈值(字节) */
private maxMemorySize;
/** 初始化状态标志 */
private initialized;
/**
* 构造函数
* @param options 缓存配置选项
* @example
* ```ts
* const cache = new SmartCache(); // 使用默认配置
* const cache = new SmartCache({ storagePath: '/tmp/cache' });
* ```
*/
constructor(options?: SmartCacheOptions);
/**
* 初始化或重新初始化缓存
* @param options 缓存配置选项,如果为 undefined 且已初始化则跳过
* @description
* - 首次调用时使用传入的 options 初始化
* - 已初始化时调用会先调用 `free()` 释放旧资源
* - 传入 undefined 且已初始化时跳过(用于外部传入 this 的场景)
*/
init(options?: SmartCacheOptions): void;
/**
* 释放缓存资源
* @description
* 清空 L1 内存缓存并清除 cacache 的内部 memoization 状态。
* 调用后 `initialized` 标志会被设为 false,但不会删除磁盘上的缓存文件。
* 重新调用 `init()` 可重新初始化。
*/
free(): void;
/**
* 获取缓存条目
* @param key - 缓存键
* @returns 缓存条目,包含 body 和 metadata;若不存在或读取失败返回 null
*
* @description
* **查找顺序**:
* 1. 先查 L1 内存缓存
* 2. 内存命中则直接返回(body 在内存则返回 Buffer,否则返回磁盘流)
* 3. 内存未命中则查 L2 磁盘
* 4. 磁盘命中时:
* - 小文件(≤ maxMemorySize):读取到内存并回填 L1
* - 大文件:只将 metadata 回填 L1,body 返回磁盘流
*
* @example
* ```ts
* const entry = await cache.get('user-123');
* if (entry) {
* // entry.body 可能是 Buffer(内存命中)或 ReadableStream(磁盘读取)
* const data = Buffer.isBuffer(entry.body) ? entry.body : await streamToBuffer(entry.body);
* console.log(entry.metadata);
* }
* ```
*
* @throws 磁盘 IO 错误时静默返回 null,不抛出异常
*/
get(key: string): Promise<ProxyCacheEntry | null>;
/**
* 写入缓存条目 (原子写入)
* 写入缓存条目
* @param key - 缓存键
* @param body - 缓存体(Buffer)
* @param metadata - 元数据(不含 size,会自动填充 body.length)
* @returns Promise<void>
*
* @description
* **写入策略**:
* 1. 先计算 body 长度,自动添加到 metadata 中
* 2. 同步写入 L2 磁盘缓存(cacache)
* 3. 根据 body 大小决定是否写入 L1 内存:
* - ≤ maxMemorySize:body 和 metadata 都存入 L1
* - > maxMemorySize:只存入 metadata,body 保持在磁盘
*
* @example
* ```ts
* const response = await fetch('https://api.example.com/data');
* const body = Buffer.from(await response.arrayBuffer());
* await cache.set('api-data', body, {
* url: response.url,
* status: response.status,
* headers: Object.fromEntries(response.headers.entries()),
* createdAt: Date.now()
* });
* ```
*/
set(key: string, body: Buffer, metadata: Omit<ProxyCacheMetadata, 'size'>): Promise<void>;
/**
* 内部方法:处理内存回填
* 内部方法:处理 L1 内存回填
* @param key - 缓存键
* @param body - 缓存体
* @param metadata - 完整元数据(含 size)
*
* @description
* 根据 body 大小决定存储策略:
* - body 存在且非空且 ≤ maxMemorySize:完整存入 L1
* - 否则:仅存储 metadata(不含 body),节省内存
*/

@@ -238,9 +431,87 @@ private saveToMemory;

* 获取磁盘读取流
* @param key - 缓存键
* @returns ReadableStream,从磁盘读取缓存内容
*
* @description
* 返回 cacache 的流式读取接口,用于大文件场景的流式消费。
* 不经过 L1 内存缓存,直接从 L2 磁盘读取。
*
* @example
* ```ts
* const readStream = cache.getStream('large-file');
* readStream.on('data', (chunk) => { /* 处理数据 *\/ });
* readStream.on('end', () => console.log('完成'));
* ```
*
* @see {@link setStream} 配对使用
*/
getStream(key: string): NodeJS.ReadableStream;
/**
* 获取磁盘写入流 (流式缓存)
* 获取磁盘写入流
* @param key - 缓存键
* @param metadata - 元数据(不含 size)
* @returns WritableStream,接收数据并写入磁盘缓存
*
* @description
* 返回 cacache 的流式写入接口,适用于大文件场景。
* - 写入前会先清除 L1 内存缓存中该 key 的条目(如果存在)
* - 写入完成后(finish 事件)会再次清除 L1 条目,确保内存和磁盘一致
*
* **注意**:流式写入无法自动计算 size,metadata 中不会包含 size 字段。
* 如需 size,需在写入完成后手动调用其他方法补充。
*
* @example
* ```ts
* const writeStream = cache.setStream('large-file', { url: '...' });
* const readStream = fs.createReadStream('big-file.zip');
* readStream.pipe(writeStream);
*
* writeStream.on('finish', () => {
* console.log('写入完成');
* });
* ```
*
* @see {@link getStream} 配对使用
*/
setStream(key: string, metadata: Omit<ProxyCacheMetadata, 'size'>): NodeJS.WritableStream;
/**
* 删除缓存条目
* @param key - 缓存键
* @param clearPersistent - 是否同时删除磁盘缓存,默认 true
* @returns Promise<void>
*
* @description
* - 始终清除 L1 内存缓存中的条目
* - `clearPersistent` 为 true 时,同时删除 L2 磁盘缓存条目
*
* @example
* ```ts
* // 仅从内存删除,保留磁盘缓存
* await cache.delete('key1', false);
*
* // 完全删除(内存 + 磁盘)
* await cache.delete('key1');
* ```
*/
delete(key: string, clearPersistent?: boolean): Promise<void>;
/**
* 清空所有缓存
* @param clearPersistent - 是否同时清空磁盘缓存,默认 true
* @returns Promise<void>
*
* @description
* - 始终清空 L1 内存缓存(所有条目)
* - `clearPersistent` 为 true 时,同时清空 L2 磁盘缓存目录下的所有条目
*
* @example
* ```ts
* // 清空所有缓存
* await cache.clear();
*
* // 仅清空内存,保留磁盘缓存
* await cache.clear(false);
* ```
*
* @see {@link free} 释放资源但不清理磁盘缓存
*/
clear(clearPersistent?: boolean): Promise<void>;

@@ -251,4 +522,14 @@ }

* 根据 Request 对象和配置生成唯一的缓存指纹 (异步)
*
* @param req 请求对象
* @param siteConfig 站点级配置
* @param bodyState 可选的 Body 读取状态(用于性能优化,避免重复读取)
* @param effectiveConfig 可选的最终生效配置(用于性能优化,避免重复合并)
*/
declare function generateCacheKey(req: Request, config: ProxySiteConfig): Promise<string>;
declare function generateCacheKey(req: Request, siteConfig: ProxySiteConfig, bodyState?: {
text: string | null;
checked: boolean;
limit: number;
json?: any;
}, effectiveConfig?: ProxyCacheRule): Promise<string>;

@@ -265,2 +546,4 @@ /**

backgroundUpdate?: boolean;
/** 是否强制刷新缓存(跳过读取,但请求成功后会更新缓存) */
refresh?: boolean;
/** 后台更新 Promise 触发时的回调 */

@@ -302,3 +585,3 @@ onBackgroundUpdate?: (promise: Promise<Response>) => void;

*/
declare function createFetchWithCache(activeCacheWrites?: Map<string, Promise<void>>): (request: Request, fetcher: (req: Request) => Promise<Response>, options: Omit<FetchWithCacheOptions, "activeCacheWrites">) => Promise<Response>;
declare function createFetchWithCache(activeCacheWrites?: Map<string, Promise<void>>): (request: Request, fetcher: (req: Request) => Promise<Response>, options: FetchWithCacheOptions) => Promise<Response>;

@@ -318,3 +601,3 @@ /**

*/
declare function createCachedFetch(defaultOptions: FetchWithCacheOptions): (request: Request, fetcher: (req: Request) => Promise<Response>, overrideOptions?: Partial<FetchWithCacheOptions>) => Promise<Response>;
declare function createCachedFetch(defaultOptions: FetchWithCacheOptions): (request: Request, fetcher: (req: Request) => Promise<Response>, overrideOptions?: FetchWithCacheOptions) => Promise<Response>;

@@ -384,7 +667,100 @@ /**

/**
* 判断当前请求是否满足可缓存的基础条件
* Analysis of request cacheability (returned when cacheable).
* 请求可缓存性分析结果(通过门控时返回)。
*/
declare function isCacheable(request: Request, config: ProxySiteConfig): Promise<boolean>;
interface CacheAnalysis {
/**
* The specific rule that matched the request.
* 匹配到的细化规则。
*/
matchedRule: ProxyCacheRule | null;
/**
* Current body reading state (reusable for fingerprinting).
* 请求体读取状态(可供后续生成 Key 等环节复用,避免重复读取 Stream)。
*/
bodyState: {
text: string | null;
checked: boolean;
limit: number;
};
}
/**
* Validates if the request meets the base cacheability criteria and returns analysis metadata.
* 判断当前请求是否满足可缓存的基础条件(门控校验)并返回分析上下文。
*
* @param request Request object. 请求对象。
* @param config Site-level configuration. 站点级配置。
* @returns
* - `CacheAnalysis`: If cacheable. Returns metadata for downstream steps (fingerprinting/fetching).
* 如果可缓存,返回包含规则和 Body 状态的对象,供后续步骤复用。
* - `undefined`: If NOT cacheable. Blocked by site-level or rule-level gatekeeping.
* 如果不可缓存(被门控拦截),返回 undefined。
*
* @important DO NOT simplify to boolean. The returned `bodyState` is CRITICAL for preventing
* multiple stream reads in subsequent `generateCacheKey` and `fetch` calls.
* 请勿简化为 boolean。返回的 `bodyState` 对于防止后续流程中重复读取请求流至关重要。
*/
declare function isCacheable(request: Request, config: ProxySiteConfig): Promise<CacheAnalysis | undefined>;
interface ResponseCacheCheckResult {
cacheable: boolean;
reason?: string;
/** Whether we should keep the old cache if this response is deemed "dirty" */
keepOldCache?: boolean;
}
/**
* 判断响应是否满足缓存条件 (响应侧校验)
*
* @description
* 此函数执行以下检查:
* 1. 状态码匹配 (statuses)
* 2. 响应头匹配 (headers)
* 3. 最小长度校验 (minLength)
* 4. 响应体内容校验 (body) - 支持正向包含与负向 (!) 排除
*/
declare function isResponseCacheable(response: Response, rule: ProxyCacheRule, options?: {
useWafPresets?: boolean;
bodyText?: string;
}): Promise<ResponseCacheCheckResult>;
/**
* Cloudflare specific WAF Challenge detection signatures.
*/
declare const CLOUDFLARE_WAF_PRESET: ProxyCacheRule;
/**
* AWS WAF specific Challenge/CAPTCHA detection signatures.
*/
declare const AWS_WAF_PRESET: ProxyCacheRule;
/**
* General/Common WAF and Bot detection signatures.
*/
declare const GENERAL_WAF_PRESET: ProxyCacheRule;
/**
* Get all current WAF presets.
*/
declare function getWAFPresets(): ProxyCacheRule[];
/**
* Register a new WAF preset.
* @param preset The WAF signature to detect.
*/
declare function registerWAFPreset(preset: ProxyCacheRule): void;
/**
* Unregister a WAF preset.
* @param preset The WAF signature to remove.
*/
declare function unregisterWAFPreset(preset: ProxyCacheRule): void;
/**
* Clear all registered WAF presets.
*/
declare function clearWAFPresets(): void;
/**
* 高度可复用的简单好使的 WAF 挑战判定函数
*
* @param response Web 标准 Response 对象
* @param presets 自定义规则,默认使用内置所有已注册的 WAF 预设
* @returns 是否为人机挑战页面
*/
declare function isWAFChallenge(response: Response, presets?: ProxyCacheRule[]): Promise<boolean>;
/**
* Universal Data Extraction and Filtering Utility (for Objects)

@@ -455,3 +831,12 @@ * 通用数据提取与过滤函数 (针对对象)

*/
declare function isMatch(pattern: string | RegExp | (string | RegExp)[], value: string, usePrefix?: boolean, defaultIfNoPositives?: boolean, ignoreCase?: boolean): boolean;
declare function isMatch(pattern: number | string | RegExp | (number | string | RegExp)[], value: string, { usePrefix, defaultIfNoPositives, ignoreCase, ignoreNegative, }?: {
usePrefix?: boolean;
defaultIfNoPositives?: boolean;
ignoreCase?: boolean;
ignoreNegative?: boolean;
}): boolean;
declare function matchField(source: URLSearchParams | Headers | Record<string, any>, config: ProxyFieldConfig | ProxyMatchPatterns, { defaultAllowed, ignoreNegative, }?: {
defaultAllowed?: boolean;
ignoreNegative?: boolean;
}): boolean;

@@ -496,2 +881,2 @@ /**

export { type FetchWithCacheContext, type FetchWithCacheOptions, OfflineCacheMissError, OfflineCacheMissErrorCode, OfflineCacheMissErrorMsg, type PrefetchOptions, type PrefetchRequest, type PrefetchResult, type ProxyBodyConfig, type ProxyCacheEntry, type ProxyCacheMetadata, type ProxyCacheRule, type ProxyConfig, type ProxyFieldConfig, type ProxyMatchPattern, type ProxyMatchPatterns, type ProxySiteConfig, SmartCache, type SmartCacheOptions, createCachedFetch, createFetchWithCache, createResponse, decorateResponseWithUrl, extractData, fetchWithCache, generateCacheKey, getEffectiveConfig, getEffectiveConfigFromRequest, getMatchedRule, getSiteConfig, isAllowed, isCacheable, isGlob, isMatch, normalizeBodyConfig, prefetch };
export { AWS_WAF_PRESET, CLOUDFLARE_WAF_PRESET, type CacheAnalysis, type FetchWithCacheContext, type FetchWithCacheOptions, GENERAL_WAF_PRESET, OfflineCacheMissError, OfflineCacheMissErrorCode, OfflineCacheMissErrorMsg, type PrefetchOptions, type PrefetchRequest, type PrefetchResult, type ProxyBodyConfig, type ProxyCacheEntry, type ProxyCacheMetadata, type ProxyCacheRule, type ProxyConfig, type ProxyFieldConfig, type ProxyMatchPattern, type ProxyMatchPatterns, type ProxySiteConfig, type ResponseCacheCheckResult, SmartCache, type SmartCacheOptions, clearWAFPresets, createCachedFetch, createFetchWithCache, createResponse, decorateResponseWithUrl, extractData, fetchWithCache, generateCacheKey, getEffectiveConfig, getEffectiveConfigFromRequest, getMatchedRule, getSiteConfig, getWAFPresets, isAllowed, isCacheable, isGlob, isMatch, isResponseCacheable, isWAFChallenge, matchField, normalizeBodyConfig, prefetch, registerWAFPreset, unregisterWAFPreset };

@@ -62,11 +62,13 @@ import { CommonError, ErrorCode } from '@isdk/common-error';

/**
* Field-level matching and extraction for JSON bodies.
* 针对 JSON Body 的字段级匹配与提取。
* Body matching rules (used for Gatekeeping).
* Supports JSON field-level matching or string/regex matching for text bodies.
* Body 匹配规则(用于门控)。支持针对 JSON 的字段级匹配,或针对文本 Body 的字符串/正则匹配。
*/
match?: ProxyFieldConfig | ProxyMatchPatterns;
/**
* Regex for extracting data from non-JSON (text) bodies.
* 用于非 JSON (文本) Body 的提取正则表达式。
* Data extraction rules (used for Fingerprinting).
* Supports JSON field filtering or Regex for text bodies.
* 数据提取规则(用于指纹提取)。支持 JSON 字段过滤或针对文本 Body 的正则提取。
*/
extract?: string | RegExp;
extract?: ProxyFieldConfig | ProxyMatchPatterns;
/**

@@ -124,2 +126,29 @@ * Whether to sort extracted JSON keys or regex capture groups to ensure fingerprint consistency.

/**
* Response-side cacheability criteria.
* 响应侧可缓存性判定准则。
*/
response?: {
/**
* Allowed HTTP statuses.
* 允许缓存的状态码模式。
*/
statuses?: ProxyMatchPatterns;
/**
* Required or forbidden response headers.
* 响应头匹配要求。
*/
headers?: ProxyFieldConfig;
/**
* Response body matching patterns (for text/json).
* Supports Glob negation (e.g., "!*captcha*") to exclude dirty data.
* 响应体匹配模式(仅限文本/JSON)。支持 Glob 否定(如 "!*captcha*")来排除脏数据。
*/
body?: ProxyMatchPatterns;
/**
* Minimum body length (in bytes) to be considered valid.
* 最小有效响应体长度(字节),防止缓存截断或错误页面。
*/
minLength?: number;
};
/**
* Fault tolerance: If backend fails (network error or 5xx), return stale cache if available.

@@ -198,15 +227,48 @@ * 容错机制:当后端请求失败且存在旧缓存时,强制返回旧缓存。

* SmartCache 选项
* @example
* ```ts
* const cache = new SmartCache({
* storagePath: '/tmp/my-cache',
* maxMemorySize: 2 * 1024 * 1024, // 2MB
* maxTotalMemorySize: 200 * 1024 * 1024, // 200MB
* memoryOptions: {
* capacity: 1000,
* expires: 10 * 60 * 1000 // 10分钟
* }
* });
* ```
*/
interface SmartCacheOptions {
/** 磁盘缓存的物理路径。如果不提供,将默认使用系统临时目录。 */
/**
* 磁盘缓存的物理路径。
* @description 如果不提供,将默认使用系统临时目录 (`os.tmpdir()`) 下的 `isdk-proxy-cache` 目录。
* @default os.tmpdir() + '/isdk-proxy-cache'
*/
storagePath?: string;
/** 内存缓存阈值(字节)。响应体大小超过此值时,Body 将只存入磁盘,而 Meta 仍保留在内存。默认 1MB。 */
/**
* 内存缓存阈值(字节)。
* @description 响应体大小超过此值时,Body 将只存入磁盘,而 Meta 元数据仍保留在内存中。
* 此优化可减少大文件对内存的占用。
* @default 1024 * 1024 (1MB)
*/
maxMemorySize?: number;
/** 内存缓存总大小阈值(字节)。默认 100MB。超过此值将清空内存缓存。 */
/**
* 内存缓存总大小阈值(字节)。
* @description 超过此值时,LRU 缓存会自动清除最久未使用的条目以释放内存。
* @default 100 * 1024 * 1024 (100MB)
*/
maxTotalMemorySize?: number;
/** 透传给 L1 (Memory) 的高级配置 (secondary-cache LRUCache options) */
/**
* 透传给 L1 内存缓存的高级配置。
* @description 基于 secondary-cache 的 LRUCache 选项,可自定义容量、过期时间等参数。
* @see https://www.npmjs.com/package/secondary-cache
*/
memoryOptions?: {
/** LRU 缓存的最大条目数,为 0 时仅按 maxWeight 限制 */
capacity?: number;
/** 缓存条目过期时间(毫秒),默认 5 分钟 */
expires?: number;
/** 清理检查间隔(毫秒) */
cleanInterval?: number;
/** 允许添加其他 LRUCache 支持的选项 */
[key: string]: any;

@@ -217,18 +279,149 @@ };

* 智能混合缓存类 (Hybrid Multi-tier Cache)
*
* @description
* 实现 L1 内存缓存 + L2 磁盘缓存的两级缓存架构:
* - **L1 (Memory)**: 基于 LRUCache 的内存缓存,存储最近使用的热点数据
* - **L2 (Disk)**: 基于 cacache 的持久化磁盘缓存,支持大文件存储
*
* ### 缓存策略
* 1. **读取时**: 先查内存,未命中则查磁盘;磁盘命中且小于 `maxMemorySize` 时回填内存
* 2. **写入时**: 同时写入磁盘和内存(大文件 body 不进内存)
* 3. **大文件优化**: 超过 `maxMemorySize` 的响应只存磁盘,元数据存内存
*
* ### 适用场景
* - HTTP 响应缓存,减少重复请求
* - 大文件流式缓存,内存友好
* - 需要持久化 + LRU 淘汰的缓存场景
*
* @example
* ```ts
* import { SmartCache } from '@isdk/proxy';
*
* const cache = new SmartCache({ maxMemorySize: 2 * 1024 * 1024 });
*
* // 写入缓存
* await cache.set('key1', Buffer.from('hello'), {
* url: 'https://api.example.com/data',
* createdAt: Date.now()
* });
*
* // 读取缓存
* const entry = await cache.get('key1');
* if (entry) {
* console.log(entry.body.toString());
* }
*
* // 流式写入(适用于大文件)
* const writeStream = cache.setStream('large-file', { url: '...' });
* fs.createReadStream('big-file.zip').pipe(writeStream);
*
* // 流式读取
* const readStream = cache.getStream('large-file');
* readStream.pipe(fs.createWriteStream('output.zip'));
*
* // 清理
* await cache.clear();
* ```
*/
declare class SmartCache {
/** L1 内存缓存实例 */
private memory;
/** L2 磁盘缓存路径 */
private storagePath;
/** 单条目内存阈值(字节) */
private maxMemorySize;
/** 初始化状态标志 */
private initialized;
/**
* 构造函数
* @param options 缓存配置选项
* @example
* ```ts
* const cache = new SmartCache(); // 使用默认配置
* const cache = new SmartCache({ storagePath: '/tmp/cache' });
* ```
*/
constructor(options?: SmartCacheOptions);
/**
* 初始化或重新初始化缓存
* @param options 缓存配置选项,如果为 undefined 且已初始化则跳过
* @description
* - 首次调用时使用传入的 options 初始化
* - 已初始化时调用会先调用 `free()` 释放旧资源
* - 传入 undefined 且已初始化时跳过(用于外部传入 this 的场景)
*/
init(options?: SmartCacheOptions): void;
/**
* 释放缓存资源
* @description
* 清空 L1 内存缓存并清除 cacache 的内部 memoization 状态。
* 调用后 `initialized` 标志会被设为 false,但不会删除磁盘上的缓存文件。
* 重新调用 `init()` 可重新初始化。
*/
free(): void;
/**
* 获取缓存条目
* @param key - 缓存键
* @returns 缓存条目,包含 body 和 metadata;若不存在或读取失败返回 null
*
* @description
* **查找顺序**:
* 1. 先查 L1 内存缓存
* 2. 内存命中则直接返回(body 在内存则返回 Buffer,否则返回磁盘流)
* 3. 内存未命中则查 L2 磁盘
* 4. 磁盘命中时:
* - 小文件(≤ maxMemorySize):读取到内存并回填 L1
* - 大文件:只将 metadata 回填 L1,body 返回磁盘流
*
* @example
* ```ts
* const entry = await cache.get('user-123');
* if (entry) {
* // entry.body 可能是 Buffer(内存命中)或 ReadableStream(磁盘读取)
* const data = Buffer.isBuffer(entry.body) ? entry.body : await streamToBuffer(entry.body);
* console.log(entry.metadata);
* }
* ```
*
* @throws 磁盘 IO 错误时静默返回 null,不抛出异常
*/
get(key: string): Promise<ProxyCacheEntry | null>;
/**
* 写入缓存条目 (原子写入)
* 写入缓存条目
* @param key - 缓存键
* @param body - 缓存体(Buffer)
* @param metadata - 元数据(不含 size,会自动填充 body.length)
* @returns Promise<void>
*
* @description
* **写入策略**:
* 1. 先计算 body 长度,自动添加到 metadata 中
* 2. 同步写入 L2 磁盘缓存(cacache)
* 3. 根据 body 大小决定是否写入 L1 内存:
* - ≤ maxMemorySize:body 和 metadata 都存入 L1
* - > maxMemorySize:只存入 metadata,body 保持在磁盘
*
* @example
* ```ts
* const response = await fetch('https://api.example.com/data');
* const body = Buffer.from(await response.arrayBuffer());
* await cache.set('api-data', body, {
* url: response.url,
* status: response.status,
* headers: Object.fromEntries(response.headers.entries()),
* createdAt: Date.now()
* });
* ```
*/
set(key: string, body: Buffer, metadata: Omit<ProxyCacheMetadata, 'size'>): Promise<void>;
/**
* 内部方法:处理内存回填
* 内部方法:处理 L1 内存回填
* @param key - 缓存键
* @param body - 缓存体
* @param metadata - 完整元数据(含 size)
*
* @description
* 根据 body 大小决定存储策略:
* - body 存在且非空且 ≤ maxMemorySize:完整存入 L1
* - 否则:仅存储 metadata(不含 body),节省内存
*/

@@ -238,9 +431,87 @@ private saveToMemory;

* 获取磁盘读取流
* @param key - 缓存键
* @returns ReadableStream,从磁盘读取缓存内容
*
* @description
* 返回 cacache 的流式读取接口,用于大文件场景的流式消费。
* 不经过 L1 内存缓存,直接从 L2 磁盘读取。
*
* @example
* ```ts
* const readStream = cache.getStream('large-file');
* readStream.on('data', (chunk) => { /* 处理数据 *\/ });
* readStream.on('end', () => console.log('完成'));
* ```
*
* @see {@link setStream} 配对使用
*/
getStream(key: string): NodeJS.ReadableStream;
/**
* 获取磁盘写入流 (流式缓存)
* 获取磁盘写入流
* @param key - 缓存键
* @param metadata - 元数据(不含 size)
* @returns WritableStream,接收数据并写入磁盘缓存
*
* @description
* 返回 cacache 的流式写入接口,适用于大文件场景。
* - 写入前会先清除 L1 内存缓存中该 key 的条目(如果存在)
* - 写入完成后(finish 事件)会再次清除 L1 条目,确保内存和磁盘一致
*
* **注意**:流式写入无法自动计算 size,metadata 中不会包含 size 字段。
* 如需 size,需在写入完成后手动调用其他方法补充。
*
* @example
* ```ts
* const writeStream = cache.setStream('large-file', { url: '...' });
* const readStream = fs.createReadStream('big-file.zip');
* readStream.pipe(writeStream);
*
* writeStream.on('finish', () => {
* console.log('写入完成');
* });
* ```
*
* @see {@link getStream} 配对使用
*/
setStream(key: string, metadata: Omit<ProxyCacheMetadata, 'size'>): NodeJS.WritableStream;
/**
* 删除缓存条目
* @param key - 缓存键
* @param clearPersistent - 是否同时删除磁盘缓存,默认 true
* @returns Promise<void>
*
* @description
* - 始终清除 L1 内存缓存中的条目
* - `clearPersistent` 为 true 时,同时删除 L2 磁盘缓存条目
*
* @example
* ```ts
* // 仅从内存删除,保留磁盘缓存
* await cache.delete('key1', false);
*
* // 完全删除(内存 + 磁盘)
* await cache.delete('key1');
* ```
*/
delete(key: string, clearPersistent?: boolean): Promise<void>;
/**
* 清空所有缓存
* @param clearPersistent - 是否同时清空磁盘缓存,默认 true
* @returns Promise<void>
*
* @description
* - 始终清空 L1 内存缓存(所有条目)
* - `clearPersistent` 为 true 时,同时清空 L2 磁盘缓存目录下的所有条目
*
* @example
* ```ts
* // 清空所有缓存
* await cache.clear();
*
* // 仅清空内存,保留磁盘缓存
* await cache.clear(false);
* ```
*
* @see {@link free} 释放资源但不清理磁盘缓存
*/
clear(clearPersistent?: boolean): Promise<void>;

@@ -251,4 +522,14 @@ }

* 根据 Request 对象和配置生成唯一的缓存指纹 (异步)
*
* @param req 请求对象
* @param siteConfig 站点级配置
* @param bodyState 可选的 Body 读取状态(用于性能优化,避免重复读取)
* @param effectiveConfig 可选的最终生效配置(用于性能优化,避免重复合并)
*/
declare function generateCacheKey(req: Request, config: ProxySiteConfig): Promise<string>;
declare function generateCacheKey(req: Request, siteConfig: ProxySiteConfig, bodyState?: {
text: string | null;
checked: boolean;
limit: number;
json?: any;
}, effectiveConfig?: ProxyCacheRule): Promise<string>;

@@ -265,2 +546,4 @@ /**

backgroundUpdate?: boolean;
/** 是否强制刷新缓存(跳过读取,但请求成功后会更新缓存) */
refresh?: boolean;
/** 后台更新 Promise 触发时的回调 */

@@ -302,3 +585,3 @@ onBackgroundUpdate?: (promise: Promise<Response>) => void;

*/
declare function createFetchWithCache(activeCacheWrites?: Map<string, Promise<void>>): (request: Request, fetcher: (req: Request) => Promise<Response>, options: Omit<FetchWithCacheOptions, "activeCacheWrites">) => Promise<Response>;
declare function createFetchWithCache(activeCacheWrites?: Map<string, Promise<void>>): (request: Request, fetcher: (req: Request) => Promise<Response>, options: FetchWithCacheOptions) => Promise<Response>;

@@ -318,3 +601,3 @@ /**

*/
declare function createCachedFetch(defaultOptions: FetchWithCacheOptions): (request: Request, fetcher: (req: Request) => Promise<Response>, overrideOptions?: Partial<FetchWithCacheOptions>) => Promise<Response>;
declare function createCachedFetch(defaultOptions: FetchWithCacheOptions): (request: Request, fetcher: (req: Request) => Promise<Response>, overrideOptions?: FetchWithCacheOptions) => Promise<Response>;

@@ -384,7 +667,100 @@ /**

/**
* 判断当前请求是否满足可缓存的基础条件
* Analysis of request cacheability (returned when cacheable).
* 请求可缓存性分析结果(通过门控时返回)。
*/
declare function isCacheable(request: Request, config: ProxySiteConfig): Promise<boolean>;
interface CacheAnalysis {
/**
* The specific rule that matched the request.
* 匹配到的细化规则。
*/
matchedRule: ProxyCacheRule | null;
/**
* Current body reading state (reusable for fingerprinting).
* 请求体读取状态(可供后续生成 Key 等环节复用,避免重复读取 Stream)。
*/
bodyState: {
text: string | null;
checked: boolean;
limit: number;
};
}
/**
* Validates if the request meets the base cacheability criteria and returns analysis metadata.
* 判断当前请求是否满足可缓存的基础条件(门控校验)并返回分析上下文。
*
* @param request Request object. 请求对象。
* @param config Site-level configuration. 站点级配置。
* @returns
* - `CacheAnalysis`: If cacheable. Returns metadata for downstream steps (fingerprinting/fetching).
* 如果可缓存,返回包含规则和 Body 状态的对象,供后续步骤复用。
* - `undefined`: If NOT cacheable. Blocked by site-level or rule-level gatekeeping.
* 如果不可缓存(被门控拦截),返回 undefined。
*
* @important DO NOT simplify to boolean. The returned `bodyState` is CRITICAL for preventing
* multiple stream reads in subsequent `generateCacheKey` and `fetch` calls.
* 请勿简化为 boolean。返回的 `bodyState` 对于防止后续流程中重复读取请求流至关重要。
*/
declare function isCacheable(request: Request, config: ProxySiteConfig): Promise<CacheAnalysis | undefined>;
interface ResponseCacheCheckResult {
cacheable: boolean;
reason?: string;
/** Whether we should keep the old cache if this response is deemed "dirty" */
keepOldCache?: boolean;
}
/**
* 判断响应是否满足缓存条件 (响应侧校验)
*
* @description
* 此函数执行以下检查:
* 1. 状态码匹配 (statuses)
* 2. 响应头匹配 (headers)
* 3. 最小长度校验 (minLength)
* 4. 响应体内容校验 (body) - 支持正向包含与负向 (!) 排除
*/
declare function isResponseCacheable(response: Response, rule: ProxyCacheRule, options?: {
useWafPresets?: boolean;
bodyText?: string;
}): Promise<ResponseCacheCheckResult>;
/**
* Cloudflare specific WAF Challenge detection signatures.
*/
declare const CLOUDFLARE_WAF_PRESET: ProxyCacheRule;
/**
* AWS WAF specific Challenge/CAPTCHA detection signatures.
*/
declare const AWS_WAF_PRESET: ProxyCacheRule;
/**
* General/Common WAF and Bot detection signatures.
*/
declare const GENERAL_WAF_PRESET: ProxyCacheRule;
/**
* Get all current WAF presets.
*/
declare function getWAFPresets(): ProxyCacheRule[];
/**
* Register a new WAF preset.
* @param preset The WAF signature to detect.
*/
declare function registerWAFPreset(preset: ProxyCacheRule): void;
/**
* Unregister a WAF preset.
* @param preset The WAF signature to remove.
*/
declare function unregisterWAFPreset(preset: ProxyCacheRule): void;
/**
* Clear all registered WAF presets.
*/
declare function clearWAFPresets(): void;
/**
* 高度可复用的简单好使的 WAF 挑战判定函数
*
* @param response Web 标准 Response 对象
* @param presets 自定义规则,默认使用内置所有已注册的 WAF 预设
* @returns 是否为人机挑战页面
*/
declare function isWAFChallenge(response: Response, presets?: ProxyCacheRule[]): Promise<boolean>;
/**
* Universal Data Extraction and Filtering Utility (for Objects)

@@ -455,3 +831,12 @@ * 通用数据提取与过滤函数 (针对对象)

*/
declare function isMatch(pattern: string | RegExp | (string | RegExp)[], value: string, usePrefix?: boolean, defaultIfNoPositives?: boolean, ignoreCase?: boolean): boolean;
declare function isMatch(pattern: number | string | RegExp | (number | string | RegExp)[], value: string, { usePrefix, defaultIfNoPositives, ignoreCase, ignoreNegative, }?: {
usePrefix?: boolean;
defaultIfNoPositives?: boolean;
ignoreCase?: boolean;
ignoreNegative?: boolean;
}): boolean;
declare function matchField(source: URLSearchParams | Headers | Record<string, any>, config: ProxyFieldConfig | ProxyMatchPatterns, { defaultAllowed, ignoreNegative, }?: {
defaultAllowed?: boolean;
ignoreNegative?: boolean;
}): boolean;

@@ -496,2 +881,2 @@ /**

export { type FetchWithCacheContext, type FetchWithCacheOptions, OfflineCacheMissError, OfflineCacheMissErrorCode, OfflineCacheMissErrorMsg, type PrefetchOptions, type PrefetchRequest, type PrefetchResult, type ProxyBodyConfig, type ProxyCacheEntry, type ProxyCacheMetadata, type ProxyCacheRule, type ProxyConfig, type ProxyFieldConfig, type ProxyMatchPattern, type ProxyMatchPatterns, type ProxySiteConfig, SmartCache, type SmartCacheOptions, createCachedFetch, createFetchWithCache, createResponse, decorateResponseWithUrl, extractData, fetchWithCache, generateCacheKey, getEffectiveConfig, getEffectiveConfigFromRequest, getMatchedRule, getSiteConfig, isAllowed, isCacheable, isGlob, isMatch, normalizeBodyConfig, prefetch };
export { AWS_WAF_PRESET, CLOUDFLARE_WAF_PRESET, type CacheAnalysis, type FetchWithCacheContext, type FetchWithCacheOptions, GENERAL_WAF_PRESET, OfflineCacheMissError, OfflineCacheMissErrorCode, OfflineCacheMissErrorMsg, type PrefetchOptions, type PrefetchRequest, type PrefetchResult, type ProxyBodyConfig, type ProxyCacheEntry, type ProxyCacheMetadata, type ProxyCacheRule, type ProxyConfig, type ProxyFieldConfig, type ProxyMatchPattern, type ProxyMatchPatterns, type ProxySiteConfig, type ResponseCacheCheckResult, SmartCache, type SmartCacheOptions, clearWAFPresets, createCachedFetch, createFetchWithCache, createResponse, decorateResponseWithUrl, extractData, fetchWithCache, generateCacheKey, getEffectiveConfig, getEffectiveConfigFromRequest, getMatchedRule, getSiteConfig, getWAFPresets, isAllowed, isCacheable, isGlob, isMatch, isResponseCacheable, isWAFChallenge, matchField, normalizeBodyConfig, prefetch, registerWAFPreset, unregisterWAFPreset };

@@ -1,1 +0,1 @@

import{CommonError as t,ErrorCode as e}from"@isdk/common-error";var r=e.OfflineCacheMiss,n="Offline mode: No cached response",i=class extends t{static code=r;constructor(t,e){super(`${n} for ${t}`,e,r),this.data={url:t}}};t[r]=i;import{LRUCache as o}from"secondary-cache";import c from"cacache";import a from"os";import s from"path";var f=class{memory;storagePath;maxMemorySize;constructor(t={}){this.storagePath=t.storagePath||s.join(a.tmpdir(),"isdk-proxy-cache"),this.maxMemorySize=t.maxMemorySize??1048576;const e={capacity:0,expires:3e5,maxWeight:t.maxTotalMemorySize||104857600,weightOf:t=>{let e=0;return t.body&&Buffer.isBuffer(t.body)&&(e+=t.body.length),e+=512,e},...t.memoryOptions};this.memory=new o(e)}async get(t){const e=this.memory.get(t);if(e){if(e.body)return e;if(0===e.size)return{...e,body:Buffer.alloc(0)};const r=c.get.stream(this.storagePath,t);return{...e,body:r}}try{const e=await c.get.info(this.storagePath,t);if(!e)return null;if(e.size<=this.maxMemorySize){const{data:e,metadata:r}=await c.get(this.storagePath,t),n=r,i={...n,body:e};return this.saveToMemory(t,e,n),i}{const e=(await c.get.info(this.storagePath,t)).metadata;return this.saveToMemory(t,null,e),{...e,body:c.get.stream(this.storagePath,t)}}}catch(t){return null}}async set(t,e,r){const n={...r,size:e.length};await c.put(this.storagePath,t,e,{metadata:n}),this.saveToMemory(t,e,n)}saveToMemory(t,e,r){if(e&&e.length>0&&e.length<=this.maxMemorySize)this.memory.set(t,{...r,body:e});else{const{...e}=r;this.memory.set(t,e)}}getStream(t){return c.get.stream(this.storagePath,t)}setStream(t,e){this.memory.del(t);const r=c.put.stream(this.storagePath,t,{metadata:e});return r.on("finish",()=>{this.memory.del(t)}),r}async delete(t,e=!0){this.memory.del(t),e&&await c.rm.entry(this.storagePath,t)}async clear(t=!0){this.memory.clear(),t&&await c.rm.all(this.storagePath)}};import{createHash as u}from"crypto";import{isRegExpStr as l,toRegExp as h}from"util-ex";function y(t,e,r=!0){const n={},i=t=>{if(null==t)return[];return(Array.isArray(t)?t.map(String):[String(t)]).filter(t=>null!=t&&"null"!==t&&"undefined"!==t).sort()};if(!e){if(r)for(const[e,r]of Object.entries(t)){const t=i(r);t.length>0&&(n[e.toLowerCase()]=t)}return n}if(Array.isArray(e)||"string"==typeof e||e instanceof RegExp)for(const o of Object.keys(t)){const c=i(t[o]);c.length>0&&x(o.toLowerCase(),e,r)&&(n[o.toLowerCase()]=c)}else for(const[r,o]of Object.entries(e)){const e=t[Object.keys(t).find(t=>t.toLowerCase()===r.toLowerCase())||r];if(void 0!==e)if(!0===o)n[r.toLowerCase()]=i(e);else if(!1===o);else{const t=i(e).filter(t=>b(o,t));t.length>0&&(n[r.toLowerCase()]=t.sort())}}return n}import p from"picomatch";import{isRegExpStr as m,toRegExp as d}from"util-ex";function w(t){return/[!*?{}[\]()]/.test(t)}function b(t,e,r=!1,n=!0,i=!0){if(Array.isArray(t)&&t.length){const o=[],c=[];return t.forEach(t=>{"string"==typeof t&&t.startsWith("!")?c.push(t.slice(1)):o.push(t)}),!(c.length>0&&c.some(t=>b(t,e,r,!0,i)))&&(0===o.length?n:o.some(t=>b(t,e,r,n,i)))}if(t instanceof RegExp)return t.test(e);if("string"==typeof t){const n=i?e.toLowerCase():e,o=i?t.toLowerCase():t;return m(t)?d(t).test(e):w(t)?p(o,{dot:!0})(n):r?n.startsWith(o):n===o}return!1}function x(t,e,r=!1){return null!=e?b(e,t,!1,r):r}function g(t,e){const{sites:r}=e;if(!r)return e;let n="";try{n=new URL(t).hostname}catch{}for(const[e,i]of Object.entries(r)){if(n&&e===n)return i;if(b(e,t,!0))return i;if(n&&n.endsWith(e)&&(e.startsWith(".")||"."===n.charAt(n.length-e.length-1)))return i}return e}import{defaultsDeep as O}from"lodash-es";function j(t){return t?"object"!=typeof t||t instanceof RegExp||Array.isArray(t)?{match:t}:t:{}}function R(t,e){const r=O({},t,e);return(t.body||e.body)&&(r.body=O({},j(t.body),j(e.body))),r}function E(t,e){if(e&&t.url!==e){Object.defineProperty(t,"url",{value:e,writable:!1,enumerable:!0,configurable:!0});const r=t.clone;t.clone=function(){return E(r.call(this),e)}}return t}function S(t,e){const{url:r,...n}=e,i=new Response(t,n);return r?E(i,r):i}function A(t,e,r=!0){if(!e||"object"!=typeof e||Array.isArray(e)||e instanceof RegExp){const n=t instanceof URLSearchParams||t instanceof Headers?Array.from(t.keys()):Object.keys(t);return 0===n.length?r:n.some(t=>b(e,t))}for(const[r,n]of Object.entries(e)){let e=null,i=!1;if(t instanceof URLSearchParams||t instanceof Headers?(e=t.get(r),i=t.has(r)):(e=t[r]??null,i=void 0!==t[r]&&null!==t[r]),"boolean"==typeof n){if(n&&!i)return!1;if(!n&&i)return!1}else if(null===e||!b(n,e))return!1}return!0}async function T(t,e,r){const n=t.method.toUpperCase(),i=new URL(t.url),o=j(e.body).maxLength||1024,c=r||{text:null,checked:!1,limit:o};if(e.rules&&e.rules.length>0)for(const r of e.rules)if(await k(r,n,i,t,c))return r;return null}async function v(t,e){return R(await T(t,e)||{},e)}async function k(t,e,r,n,i){if(t.methods&&!b(t.methods,e))return!1;if(t.path&&!b(t.path,r.pathname,!0))return!1;if(t.query&&!A(r.searchParams,t.query,!0))return!1;if(t.headers&&!A(n.headers,t.headers,!1))return!1;if(t.cookies){const e=n.headers.get("cookie")||"";if(!A(Object.fromEntries(e.split(";").map(t=>t.trim()).filter(Boolean).map(t=>{const e=t.split("=");return[e[0],e.slice(1).join("=")]})),t.cookies,!1))return!1}if(t.body){const e=n.headers.get("content-type")||"",r=e.includes("application/json")?"json":e.includes("text/")||e.includes("application/xml")||e.includes("x-www-form-urlencoded")?"text":"binary";if("object"!=typeof t.body||Array.isArray(t.body)||t.body instanceof RegExp){if("binary"===r)return!1;if(!i.checked){try{i.text=(await n.clone().text()).slice(0,i.limit)}catch{i.text=""}i.checked=!0}if(!i.text||!b(t.body,i.text))return!1}else{const e=j(t.body),o=e.maxLength||i.limit;if(e.type&&e.type!==r)return!1;if(e.match&&"json"===r){if(!i.checked){try{i.text=(await n.clone().text()).slice(0,o),i.json=JSON.parse(i.text)}catch{i.json={}}i.checked=!0}if(!A(i.json,e.match,!0))return!1}if(e.extract&&"text"===r){if(!i.checked){try{i.text=(await n.clone().text()).slice(0,o)}catch{i.text=""}i.checked=!0}if(!i.text||!b(e.extract,i.text))return!1}}}return!0}async function L(t,e){const r=t.method.toUpperCase();if(!b(e.methods||["GET","HEAD"],r))return!1;if(e.rules&&e.rules.length>0){return null!==await T(t,e)}return!0}async function P(t,e){const r=new URL(t.url),n=t.method.toUpperCase(),i=await v(t,e),o=t.headers.get("cookie")||"",c=Object.fromEntries(o.split(";").map(t=>t.trim()).filter(Boolean).map(t=>{const e=t.split("=");return[e[0],e.slice(1).join("=")]}));let a=null;if(["POST","PUT","PATCH"].includes(n))try{const e=t.headers.get("content-type")||"",r=j(i.body);if(e.includes("application/json")){const e=await t.clone().json();a=y(e,r.match||i.body,!0)}else if(r.extract&&(e.includes("text/")||e.includes("application/xml")||e.includes("x-www-form-urlencoded"))){const e=r.maxLength||1024,n=(await t.clone().text()).slice(0,e),i=r.extract,o="string"==typeof i&&l(i)?h(i):i instanceof RegExp?i:null;if(o){const t=n.match(o);if(t)if(t.length>1){const e=t.slice(1);r.sort&&e.sort(),a=e.join(":")}else a=t[0]}else a=u("sha256").update(n).digest("hex")}else{const e=await t.clone().arrayBuffer();e.byteLength>0&&(a=u("sha256").update(new Uint8Array(e)).digest("hex"))}}catch(t){}const s=i.headers;let f=s;Array.isArray(s)?f=[...s,"!cookie"]:"string"==typeof s?f=[s,"!cookie"]:s&&"object"==typeof s?(f={...s},delete f.cookie,delete f.Cookie):void 0===s&&(f=["!*"]);const p={m:n,h:r.host,p:r.pathname,q:y(Object.fromEntries(r.searchParams),i.query,!0),hd:y(Object.fromEntries(t.headers),f,!1),ck:y(c,i.cookies,!1)};return null!==a&&(p.b=a),u("sha256").update(JSON.stringify(p)).digest("hex")}import{Readable as C}from"stream";import{pipeline as H}from"stream/promises";import I from"http-cache-semantics";import{debug as U}from"debug";var F=U("@isdk/proxy:fetchWithCache");function M(t,e){return S(204===t.status||304===t.status||t.status<200?null:function(t){if(t instanceof Buffer)return new Uint8Array(t);if(t&&"function"==typeof t.pipe)try{const e="function"==typeof t._read&&"object"==typeof t._readableState?t:C.from(t);return C.toWeb(e)}catch(e){return t}return t}(t.body),{status:t.status,headers:{...t.headers,"x-proxy-cache":e},url:t.url})}async function W(t,e){let r,n;const i=new Promise((e,i)=>{r=()=>{t.activeCacheWrites.delete(t.cacheKey),e()},n=e=>{t.activeCacheWrites.delete(t.cacheKey),i(e)}});i.catch(()=>{}),t.activeCacheWrites.set(t.cacheKey,i);try{const e=await t.fetcher(t.request.clone()),i=new I({url:t.request.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)},{status:e.status,headers:Object.fromEntries(e.headers)}),o=new Headers(e.headers);if(o.set("x-proxy-cache","MISS"),F("executeFetch And Cache",t.request.url),!i.storable()&&!t.effectiveConfig.forceCache)return r(),S(e.body,{status:e.status,statusText:e.statusText,headers:o,url:e.url});const c={status:e.status,headers:Object.fromEntries(e.headers),policy:i.toObject(),url:t.request.url,method:t.request.method,timestamp:Date.now()};if(!e.body)return await t.cache.set(t.cacheKey,Buffer.alloc(0),c),r(),S(null,{status:e.status,statusText:e.statusText,headers:o,url:e.url});const[a,s]=e.body.tee();return H(C.fromWeb(s),t.cache.setStream(t.cacheKey,c)).then(r).catch(n),S(a,{status:e.status,statusText:e.statusText,headers:o,url:e.url})}catch(r){if(n(r),e&&t.effectiveConfig.staleIfError)return M(e,"STALE_IF_ERROR");throw r}}async function B(t,e,i){const o=await async function(t,e,r){const n=r.generateKey||P,i=await n(t,r.config),o=await v(t,r.config);return{...r,request:t,fetcher:e,cacheKey:i,effectiveConfig:o,activeCacheWrites:r.activeCacheWrites||new Map}}(t,e,i),{effectiveConfig:c}=o,a=await o.cache.get(o.cacheKey);if(c.offline)return a?M(a,"OFFLINE_HIT"):S(n,{status:r,headers:{"x-proxy-cache":"OFFLINE_HIT"},url:t.url});if(!await L(t,i.config))return e(t);if(a){const e=function(t,e){const r=I.fromObject(e.policy),n={url:e.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)};return r.satisfiesWithoutRevalidation(n)?"HIT":"STALE"}(o,a);if(F("evaluateCachePolicy:",t.url,e),a.policy?.resh&&Object.keys(a.policy.resh).length&&F("evaluateCachePolicy:","resh =",JSON.stringify(a.policy.resh)),"HIT"===e)return M(a,"HIT");if("STALE"===e&&!1!==i.backgroundUpdate)return function(t,e){if(t.activeCacheWrites.has(t.cacheKey))return;const r=W(t,e).catch(r=>(console.error(`[SWR Error] ${t.cacheKey}:`,r),M(e,"STALE_IF_ERROR")));try{t.onBackgroundUpdate?.(r)}catch(e){console.error(`[SWR Callback Error] ${t.cacheKey}:`,e)}}(o,a),M(a,"STALE")}if(o.activeCacheWrites.has(o.cacheKey)){const e=await async function(t){const e=t.activeCacheWrites.get(t.cacheKey);if(!e)return null;await e;const r=await t.cache.get(t.cacheKey);return r?M(r,"HIT"):null}(o);if(e)return F("activeCacheWrites has this, waiting response",t.url),e}return W(o,a)}function N(t){return t||(t=new Map),async function(e,r,n){return B(e,r,{...n,activeCacheWrites:t})}}function _(t){const e=N(t.activeCacheWrites);return async function(r,n,i){return e(r,n,{...t,...i})}}async function $(t){const{urls:e,config:r,cache:n,fetcher:i=t=>globalThis.fetch(t),concurrency:o=3,onProgress:c,signal:a}=t,s={succeeded:0,failed:0,errors:[]};if(0===e.length)return s;if(a?.aborted)return s;const f=new Map,u=N(f),l=[...e];let h=0;const y=Array.from({length:Math.min(o,e.length)},()=>(async()=>{for(;l.length>0&&!a?.aborted;){const t=l.shift();if(!t)break;try{const e=g(t.url,r),o=new Request(t.url,{...t.request,signal:a});if(!await L(o,e))continue;const c=await u(o,i,{cache:n,config:{...e,offline:!1},backgroundUpdate:!1});c.headers.has("x-proxy-cache")&&(await c.arrayBuffer(),s.succeeded++)}catch(e){if("AbortError"===e.name||a?.aborted)break;s.failed++,s.errors.push({url:t.url,error:e})}finally{h++,c?.(h,e.length,t.url)}}})());return await Promise.all(y),f.size>0&&await Promise.allSettled(f.values()),s}export{i as OfflineCacheMissError,r as OfflineCacheMissErrorCode,n as OfflineCacheMissErrorMsg,f as SmartCache,_ as createCachedFetch,N as createFetchWithCache,S as createResponse,E as decorateResponseWithUrl,y as extractData,B as fetchWithCache,P as generateCacheKey,R as getEffectiveConfig,v as getEffectiveConfigFromRequest,T as getMatchedRule,g as getSiteConfig,x as isAllowed,L as isCacheable,w as isGlob,b as isMatch,j as normalizeBodyConfig,$ as prefetch};
import{CommonError as t,ErrorCode as e}from"@isdk/common-error";var r=e.OfflineCacheMiss,n="Offline mode: No cached response",i=class extends t{static code=r;constructor(t,e){super(`${n} for ${t}`,e,r),this.data={url:t}}};t[r]=i;import{LRUCache as a}from"secondary-cache";import o from"cacache";import s from"os";import c from"path";var f=class{memory;storagePath;maxMemorySize;initialized;constructor(t={}){this.init(t)}init(t){if(t)this.initialized&&this.free();else{if(this.initialized)return;t=this}this!==t&&(this.storagePath=t.storagePath||c.join(s.tmpdir(),"isdk-proxy-cache"),this.maxMemorySize=t.maxMemorySize??1048576);const e={capacity:0,expires:3e5,maxWeight:t.maxTotalMemorySize||104857600,weightOf:t=>{let e=0;return t.body&&Buffer.isBuffer(t.body)&&(e+=t.body.length),e+=512,e},...t.memoryOptions};this.memory=new a(e),this.initialized=!0}free(){this.memory?.clear(),o.clearMemoized(),this.initialized=!1}async get(t){const e=this.memory.get(t);if(e){if(e.body)return e;if(0===e.size)return{...e,body:Buffer.alloc(0)};const r=o.get.stream(this.storagePath,t);return{...e,body:r}}try{const e=await o.get.info(this.storagePath,t);if(!e)return null;if(e.size<=this.maxMemorySize){const{data:e,metadata:r}=await o.get(this.storagePath,t),n=r,i={...n,body:e};return this.saveToMemory(t,e,n),i}{const e=(await o.get.info(this.storagePath,t)).metadata;return this.saveToMemory(t,null,e),{...e,body:o.get.stream(this.storagePath,t)}}}catch(t){return null}}async set(t,e,r){const n={...r,size:e.length};await o.put(this.storagePath,t,e,{metadata:n}),this.saveToMemory(t,e,n)}saveToMemory(t,e,r){if(e&&e.length>0&&e.length<=this.maxMemorySize)this.memory.set(t,{...r,body:e});else{const{...e}=r;this.memory.set(t,e)}}getStream(t){return o.get.stream(this.storagePath,t)}setStream(t,e){this.memory.del(t);const r=o.put.stream(this.storagePath,t,{metadata:e});return r.on("finish",()=>{this.memory.del(t)}),r}async delete(t,e=!0){this.memory.del(t),e&&await o.rm.entry(this.storagePath,t)}async clear(t=!0){this.memory.clear(),t&&await o.rm.all(this.storagePath)}};import{createHash as u}from"crypto";import{isRegExpStr as l,toRegExp as h}from"util-ex";function d(t,e,r=!0){const n={},i=t=>{if(null==t)return[];return(Array.isArray(t)?t.map(String):[String(t)]).filter(t=>null!=t&&"null"!==t&&"undefined"!==t).sort()};if(!e){if(r)for(const[e,r]of Object.entries(t)){const t=i(r);t.length>0&&(n[e.toLowerCase()]=t)}return n}if(Array.isArray(e)||"string"==typeof e||e instanceof RegExp)for(const a of Object.keys(t)){const o=i(t[a]);o.length>0&&g(a.toLowerCase(),e,r)&&(n[a.toLowerCase()]=o)}else for(const[r,a]of Object.entries(e)){const e=t[Object.keys(t).find(t=>t.toLowerCase()===r.toLowerCase())||r];if(void 0!==e)if(!0===a)n[r.toLowerCase()]=i(e);else if(!1===a);else{const t=i(e).filter(t=>x(a,t));t.length>0&&(n[r.toLowerCase()]=t.sort())}}return n}import y from"picomatch";import{isRegExpStr as p,toRegExp as m}from"util-ex";function w(t){return/[!*?{}[\]()]/.test(t)}function x(t,e,{usePrefix:r=!1,defaultIfNoPositives:n=!0,ignoreCase:i=!0,ignoreNegative:a}={}){if(Array.isArray(t)&&t.length){const o=[],s=[];return t.forEach(t=>{"string"==typeof t&&t.startsWith("!")?s.push(t.slice(1)):o.push(t)}),!(!a&&s.length>0&&s.some(t=>x(t,e,{usePrefix:r,defaultIfNoPositives:!0,ignoreCase:i})))&&(0===o.length?n:o.some(t=>x(t,e,{usePrefix:r,defaultIfNoPositives:n,ignoreCase:i})))}if(t instanceof RegExp)return t.test(e);const o=null==t?"":String(t),s=i?e.toLowerCase():e,c=i?o.toLowerCase():o;return p(o)?m(o).test(e):w(c)?y(c,{dot:!0,bash:!0})(s):r?s.startsWith(c):s===c}function b(t,e,{defaultAllowed:r=!0,ignoreNegative:n}={}){if(!e||"object"!=typeof e||Array.isArray(e)||e instanceof RegExp){const n=t instanceof URLSearchParams||t instanceof Headers?Array.from(t.keys()):Object.keys(t);if(0===n.length)return r;return Array.isArray(e)?n.some(t=>x(e,t,{ignoreNegative:!0})):n.every(t=>x(e,t))}for(const[r,i]of Object.entries(e)){let e=null,a=!1;if(t instanceof URLSearchParams||t instanceof Headers?(e=t.get(r),a=t.has(r)):(e=t[r]??null,a=void 0!==t[r]&&null!==t[r]),"boolean"==typeof i){if(i&&!a)return!1;if(!i&&a)return!1}else if(null===e||!x(i,e,{ignoreNegative:n}))return!1}return!0}function g(t,e,r=!1){return null!=e?x(e,t,{usePrefix:!1,defaultIfNoPositives:r}):r}function S(t,e){const{sites:r}=e;if(!r)return e;let n="";try{n=new URL(t).hostname}catch{}for(const[e,i]of Object.entries(r)){if(n&&e===n)return i;if(x(e,t,{usePrefix:!0}))return i;if(n&&n.endsWith(e)&&(e.startsWith(".")||"."===n.charAt(n.length-e.length-1)))return i}return e}import{defaultsDeep as E}from"lodash-es";function O(t){return t?"object"!=typeof t||t instanceof RegExp||Array.isArray(t)?{match:t}:t:{}}function v(t,e){const r=E({},t,e);return(t.body||e.body)&&(r.body=E({},O(t.body),O(e.body))),r}function A(t,e){if(e&&t.url!==e){Object.defineProperty(t,"url",{value:e,writable:!1,enumerable:!0,configurable:!0});const r=t.clone;t.clone=function(){return A(r.call(this),e)}}return t}function R(t,e){const{url:r,...n}=e,i=new Response(t,n);return r?A(i,r):i}async function _(t,e,r){const n=t.method.toUpperCase(),i=new URL(t.url),a=O(e.body).maxLength||1024,o=r||{text:null,checked:!1,limit:a};if(e.rules&&e.rules.length>0)for(const r of e.rules)if(await j(r,n,i,t,o))return r;return null}async function T(t,e){return v(await _(t,e)||{},e)}async function j(t,e,r,n,i){if(t.methods&&!x(t.methods,e))return!1;if(t.path&&!x(t.path,r.pathname,{usePrefix:!0}))return!1;if(t.query&&!b(r.searchParams,t.query,{defaultAllowed:!0}))return!1;if(t.headers&&!b(n.headers,t.headers,{defaultAllowed:!1}))return!1;if(t.cookies){const e=n.headers.get("cookie")||"";if(!b(Object.fromEntries(e.split(";").map(t=>t.trim()).filter(Boolean).map(t=>{const e=t.split("=");return[e[0],e.slice(1).join("=")]})),t.cookies,{defaultAllowed:!1}))return!1}if(t.body){const e=n.headers.get("content-type")||"",r=e.includes("application/json")?"json":e.includes("text/")||e.includes("application/xml")||e.includes("x-www-form-urlencoded")?"text":"binary";if("object"!=typeof t.body||Array.isArray(t.body)||t.body instanceof RegExp){if("binary"===r)return!1;if(!i.checked){try{i.text=(await n.clone().text()).slice(0,i.limit)}catch{i.text=""}i.checked=!0}if(!i.text||!x(t.body,i.text))return!1}else{const e=O(t.body),a=e.maxLength||i.limit;if(e.type&&e.type!==r)return!1;if(e.match&&"json"===r){if(!i.checked){try{i.text=(await n.clone().text()).slice(0,a),i.json=JSON.parse(i.text)}catch{i.json={}}i.checked=!0}if(!b(i.json,e.match,{defaultAllowed:!0}))return!1}if(e.match&&"text"===r){const t=e.match;if(!("string"==typeof t||Array.isArray(t)||t instanceof RegExp))return!1;if(!i.checked){try{i.text=(await n.clone().text()).slice(0,a)}catch{i.text=""}i.checked=!0}if(!i.text||!x(t,i.text))return!1}}}return!0}async function C(t,e){const r=t.method.toUpperCase(),n=new URL(t.url),i={text:null,checked:!1,limit:O(e.body).maxLength||1024},a={...e,methods:e.methods||["GET","HEAD"]};if(!await j(a,r,n,t,i))return;let o=null;return e.rules&&e.rules.length>0&&(o=await _(t,e,i),!o)?void 0:{matchedRule:o,bodyState:i}}async function I(t,e,r,n){const i=new URL(t.url),a=t.method.toUpperCase(),o=n||await T(t,e),s=t.headers.get("cookie")||"",c=Object.fromEntries(s.split(";").map(t=>t.trim()).filter(Boolean).map(t=>{const e=t.split("=");return[e[0],e.slice(1).join("=")]}));let f=null;if(["POST","PUT","PATCH"].includes(a))try{const e=t.headers.get("content-type")||"",n=O(o.body);if(e.includes("application/json")){let e=r?.json;e||(e=r?.text?JSON.parse(r.text):await t.clone().json());f=d(e,n.extract||n.match||o.body,!0)}else if(n.extract&&(e.includes("text/")||e.includes("application/xml")||e.includes("x-www-form-urlencoded"))){const e=n.maxLength||1024,i=r?.text||(await t.clone().text()).slice(0,e),a=n.extract,o="string"==typeof a&&l(a)?h(a):a instanceof RegExp?a:null;if(o){const t=i.match(o);if(t)if(t.length>1){const e=t.slice(1);n.sort&&e.sort(),f=e.join(":")}else f=t[0]}else f=u("sha256").update(i).digest("hex")}else{const e=await t.clone().arrayBuffer();e.byteLength>0&&(f=u("sha256").update(new Uint8Array(e)).digest("hex"))}}catch(t){}const y=o.headers;let p=y;Array.isArray(y)?p=[...y,"!cookie"]:"string"==typeof y?p=[y,"!cookie"]:y&&"object"==typeof y?(p={...y},delete p.cookie,delete p.Cookie):void 0===y&&(p=["!*"]);const m={m:a,h:i.host,p:i.pathname,q:d(Object.fromEntries(i.searchParams),o.query,!0),hd:d(Object.fromEntries(t.headers),p,!1),ck:d(c,o.cookies,!1)};return null!==f&&(m.b=f),u("sha256").update(JSON.stringify(m)).digest("hex")}import{Readable as N}from"stream";import{pipeline as P}from"stream/promises";import k from"http-cache-semantics";import{debug as L}from"debug";import{defaultsDeep as U}from"lodash-es";var H={response:{statuses:["403","429","503"],body:["*<title>Just a moment...</title>*","*__cf_chl_opt*","*cf-browser-verification*","*cf-ray*"],headers:{"cf-mitigated":!0}}},M={response:{statuses:["202","405"],headers:{"x-amzn-waf-action":!0}}},W={response:{statuses:["403","429"],body:["*captcha-delivery.com*","*g-recaptcha*","*h-captcha*","*verify you are human*","*security check to access*","*bot detection*","*interstitial*"]}},D=new Set([H,M,W]);function F(){return Array.from(D)}function B(t){D.add(t)}function $(t){D.delete(t)}function z(){D.clear()}async function J(t,e=F()){const r=t.status.toString(),n=t.headers;let i;for(const a of e){const e=a.response;if(e){if(e.statuses&&x(e.statuses,r))return!0;if(e.headers&&b(n,e.headers))return!0;if(e.body){if(void 0===i)try{i=await t.clone().text()}catch(t){}if(void 0!==i&&x(e.body,i))return!0}}}return!1}async function q(t,e,r={}){const{useWafPresets:n=!0}=r,i=t.status,a=t.headers;if(n&&await J(t))return{cacheable:!1,reason:"waf_challenge",keepOldCache:!0};const o=e.response,s=o?.statuses||[200,203,204,206,300,301,404,405,410,414].map(t=>t.toString());if(s&&!x(s,i.toString())){return{cacheable:!1,reason:`status_mismatch:${i}`,keepOldCache:202===i||403===i||405===i||428===i||429===i||i>=500&&i<600}}if(o){if(o.headers&&!b(a,o.headers,{defaultAllowed:!0}))return{cacheable:!1,reason:"headers_mismatch"};if(void 0!==o.minLength){const t=a.get("content-length"),e=t?parseInt(t,10):0;if(t&&e<o.minLength)return{cacheable:!1,reason:"too_short",keepOldCache:!0}}if(o.body){const e=a.get("content-type")||"";if((e.includes("text/")||e.includes("application/json")||e.includes("application/xml"))&&t.body){let e=r.bodyText;if(void 0===e)try{e=await t.clone().text()}catch(t){return{cacheable:!1,reason:"body_read_error"}}if(void 0!==e){const t=Buffer.byteLength(e);if(void 0!==o.minLength&&t<o.minLength)return{cacheable:!1,reason:"too_short",keepOldCache:!0};if(o.body&&!x(o.body,e))return{cacheable:!1,reason:"body_match_failed",keepOldCache:!0}}}}}return{cacheable:!0}}var K=L("@isdk/proxy:fetchWithCache");function X(t){if(t instanceof Buffer)return new Uint8Array(t);if(t&&"function"==typeof t.pipe)try{const e="function"==typeof t._read&&"object"==typeof t._readableState?t:N.from(t);return N.toWeb(e)}catch(e){return t}return t}function Q(t,e){return R(204===t.status||304===t.status||t.status<200?null:X(t.body),{status:t.status,headers:{...t.headers,"x-proxy-cache":e},url:t.url})}async function Z(t,e){let r,n;const i=new Promise((e,i)=>{r=()=>{t.activeCacheWrites.delete(t.cacheKey),e()},n=e=>{t.activeCacheWrites.delete(t.cacheKey),i(e)}});i.catch(()=>{}),t.activeCacheWrites.set(t.cacheKey,i);try{const i=await t.fetcher(t.request.clone()),a=new Headers(i.headers),o=await q(i,t.effectiveConfig);if(!o.cacheable){if(K("Response not cacheable:",o.reason),r(),o.keepOldCache&&e){const t=o.reason?.toUpperCase().replace(/[^A-Z0-9]/g,"_")||"UNKNOWN";return K(`Triggering DR protection (${t}), returning old cache`),Q(e,`STALE_RESCUE_${t}`)}const t=o.reason?.toUpperCase().replace(/[^A-Z0-9]/g,"_")||"UNKNOWN";return a.set("x-proxy-cache",`MISS_EXCLUDED_${t}`),R(i.body,{status:i.status,statusText:i.statusText,headers:a,url:i.url})}const s=new k({url:t.request.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)},{status:i.status,headers:Object.fromEntries(i.headers)});K("executeFetch And Cache",t.request.url);if(!(s.storable()||t.effectiveConfig.forceCache))return r(),a.set("x-proxy-cache","MISS_UNSTORABLE"),R(i.body,{status:i.status,statusText:i.statusText,headers:a,url:i.url});a.set("x-proxy-cache","MISS");const c={status:i.status,headers:Object.fromEntries(i.headers),policy:s.toObject(),url:t.request.url,method:t.request.method,timestamp:Date.now()};if(!i.body)return await t.cache.set(t.cacheKey,Buffer.alloc(0),c),r(),R(null,{status:i.status,statusText:i.statusText,headers:a,url:i.url});const[f,u]=i.body.tee();return P(N.fromWeb(u),t.cache.setStream(t.cacheKey,c)).then(r).catch(n),R(f,{status:i.status,statusText:i.statusText,headers:a,url:i.url})}catch(r){if(n(r),e&&t.effectiveConfig.staleIfError)return Q(e,"STALE_IF_ERROR");throw r}}async function G(t,e,i){const a=t.isdkProxy||{};i=U({},a,i);const{config:o,cache:s}=i,c=await C(t,o);let f=v(c?.matchedRule||{},o);if(a.config&&(f=U({},a.config,f)),!c){if(f.offline)return R(n,{status:r,headers:{"x-proxy-cache":"OFFLINE_MISS_EXCLUDED_REQUEST"},url:t.url});const i=await e(t)||{},a=new Headers(i.headers);return a.set("x-proxy-cache","MISS_EXCLUDED_REQUEST"),R(X(i.body),{status:i.status,statusText:i.statusText,headers:a,url:i.url})}const{bodyState:u}=c,l=i.generateKey||I,h=await l(t,o,u,f),d={...i,request:t,fetcher:e,cacheKey:h,effectiveConfig:f,activeCacheWrites:i.activeCacheWrites||new Map},y=await s.get(h);if(f.offline)return y?Q(y,"OFFLINE_HIT"):R(n,{status:r,headers:{"x-proxy-cache":"OFFLINE_HIT"},url:t.url});if(y&&!i.refresh){const e=function(t,e){const r=k.fromObject(e.policy),n={url:e.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)};return r.satisfiesWithoutRevalidation(n)?"HIT":"STALE"}(d,y);if(K("evaluateCachePolicy:",t.url,e),"HIT"===e)return Q(y,"HIT");if("STALE"===e&&!1!==i.backgroundUpdate)return function(t,e){if(t.activeCacheWrites.has(t.cacheKey))return;const r=Z(t,e).catch(r=>(console.error(`[SWR Error] ${t.cacheKey}:`,r),Q(e,"STALE_IF_ERROR")));try{t.onBackgroundUpdate?.(r)}catch(e){console.error(`[SWR Callback Error] ${t.cacheKey}:`,e)}}(d,y),Q(y,"STALE")}if(d.activeCacheWrites.has(h)){const e=await async function(t){const e=t.activeCacheWrites.get(t.cacheKey);if(!e)return null;await e;const r=await t.cache.get(t.cacheKey);return r?Q(r,"HIT"):null}(d);if(e){if(K("activeCacheWrites has this, waiting response",t.url),i.refresh){const t=new Headers(e.headers);return t.set("x-proxy-cache","MISS"),R(e.body,{status:e.status,statusText:e.statusText,headers:t,url:e.url})}return e}}return Z(d,y)}function V(t){return t||(t=new Map),async function(e,r,n){return G(e,r,{...n,activeCacheWrites:t})}}function Y(t){const e=V(t.activeCacheWrites);return async function(r,n,i){return e(r,n,{...t,...i,activeCacheWrites:i?.activeCacheWrites||t.activeCacheWrites,refresh:i?.refresh})}}async function tt(t){const{urls:e,config:r,cache:n,fetcher:i=t=>globalThis.fetch(t),concurrency:a=3,onProgress:o,signal:s}=t,c={succeeded:0,failed:0,errors:[]};if(0===e.length)return c;if(s?.aborted)return c;const f=new Map,u=V(f),l=[...e];let h=0;const d=Array.from({length:Math.min(a,e.length)},()=>(async()=>{for(;l.length>0&&!s?.aborted;){const t=l.shift();if(!t)break;try{const e=S(t.url,r),a=new Request(t.url,{...t.request,signal:s});if(!await C(a,e))continue;const o=await u(a,i,{cache:n,config:{...e,offline:!1},backgroundUpdate:!1});o.headers.has("x-proxy-cache")&&(await o.arrayBuffer(),c.succeeded++)}catch(e){if("AbortError"===e.name||s?.aborted)break;c.failed++,c.errors.push({url:t.url,error:e})}finally{h++,o?.(h,e.length,t.url)}}})());return await Promise.all(d),f.size>0&&await Promise.allSettled(f.values()),c}export{M as AWS_WAF_PRESET,H as CLOUDFLARE_WAF_PRESET,W as GENERAL_WAF_PRESET,i as OfflineCacheMissError,r as OfflineCacheMissErrorCode,n as OfflineCacheMissErrorMsg,f as SmartCache,z as clearWAFPresets,Y as createCachedFetch,V as createFetchWithCache,R as createResponse,A as decorateResponseWithUrl,d as extractData,G as fetchWithCache,I as generateCacheKey,v as getEffectiveConfig,T as getEffectiveConfigFromRequest,_ as getMatchedRule,S as getSiteConfig,F as getWAFPresets,g as isAllowed,C as isCacheable,w as isGlob,x as isMatch,q as isResponseCacheable,J as isWAFChallenge,b as matchField,O as normalizeBodyConfig,tt as prefetch,B as registerWAFPreset,$ as unregisterWAFPreset};

@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/errors.ts:25](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/errors.ts#L25)
Defined in: [packages/proxy/src/errors.ts:25](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/errors.ts#L25)

@@ -30,3 +30,3 @@ Offline 缓存未命中错误

Defined in: [packages/proxy/src/errors.ts:27](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/errors.ts#L27)
Defined in: [packages/proxy/src/errors.ts:27](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/errors.ts#L27)

@@ -143,3 +143,3 @@ #### Parameters

Defined in: [packages/proxy/src/errors.ts:26](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/errors.ts#L26)
Defined in: [packages/proxy/src/errors.ts:26](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/errors.ts#L26)

@@ -146,0 +146,0 @@ The error code associated with the error.

@@ -9,6 +9,53 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/core/SmartCache.ts:29](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/SmartCache.ts#L29)
Defined in: [packages/proxy/src/core/SmartCache.ts:107](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/SmartCache.ts#L107)
智能混合缓存类 (Hybrid Multi-tier Cache)
## Description
实现 L1 内存缓存 + L2 磁盘缓存的两级缓存架构:
- **L1 (Memory)**: 基于 LRUCache 的内存缓存,存储最近使用的热点数据
- **L2 (Disk)**: 基于 cacache 的持久化磁盘缓存,支持大文件存储
### 缓存策略
1. **读取时**: 先查内存,未命中则查磁盘;磁盘命中且小于 `maxMemorySize` 时回填内存
2. **写入时**: 同时写入磁盘和内存(大文件 body 不进内存)
3. **大文件优化**: 超过 `maxMemorySize` 的响应只存磁盘,元数据存内存
### 适用场景
- HTTP 响应缓存,减少重复请求
- 大文件流式缓存,内存友好
- 需要持久化 + LRU 淘汰的缓存场景
## Example
```ts
import { SmartCache } from '@isdk/proxy';
const cache = new SmartCache({ maxMemorySize: 2 * 1024 * 1024 });
// 写入缓存
await cache.set('key1', Buffer.from('hello'), {
url: 'https://api.example.com/data',
createdAt: Date.now()
});
// 读取缓存
const entry = await cache.get('key1');
if (entry) {
console.log(entry.body.toString());
}
// 流式写入(适用于大文件)
const writeStream = cache.setStream('large-file', { url: '...' });
fs.createReadStream('big-file.zip').pipe(writeStream);
// 流式读取
const readStream = cache.getStream('large-file');
readStream.pipe(fs.createWriteStream('output.zip'));
// 清理
await cache.clear();
```
## Constructors

@@ -20,4 +67,6 @@

Defined in: [packages/proxy/src/core/SmartCache.ts:34](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/SmartCache.ts#L34)
Defined in: [packages/proxy/src/core/SmartCache.ts:126](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/SmartCache.ts#L126)
构造函数
#### Parameters

@@ -29,2 +78,4 @@

缓存配置选项
#### Returns

@@ -34,2 +85,9 @@

#### Example
```ts
const cache = new SmartCache(); // 使用默认配置
const cache = new SmartCache({ storagePath: '/tmp/cache' });
```
## Methods

@@ -41,4 +99,6 @@

Defined in: [packages/proxy/src/core/SmartCache.ts:147](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/SmartCache.ts#L147)
Defined in: [packages/proxy/src/core/SmartCache.ts:406](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/SmartCache.ts#L406)
清空所有缓存
#### Parameters

@@ -50,2 +110,4 @@

是否同时清空磁盘缓存,默认 true
#### Returns

@@ -55,2 +117,23 @@

Promise<void>
#### Description
- 始终清空 L1 内存缓存(所有条目)
- `clearPersistent` 为 true 时,同时清空 L2 磁盘缓存目录下的所有条目
#### Example
```ts
// 清空所有缓存
await cache.clear();
// 仅清空内存,保留磁盘缓存
await cache.clear(false);
```
#### See
[free](#free) 释放资源但不清理磁盘缓存
***

@@ -62,4 +145,6 @@

Defined in: [packages/proxy/src/core/SmartCache.ts:142](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/SmartCache.ts#L142)
Defined in: [packages/proxy/src/core/SmartCache.ts:381](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/SmartCache.ts#L381)
删除缓存条目
#### Parameters

@@ -71,2 +156,4 @@

缓存键
##### clearPersistent

@@ -76,2 +163,4 @@

是否同时删除磁盘缓存,默认 true
#### Returns

@@ -81,4 +170,41 @@

Promise<void>
#### Description
- 始终清除 L1 内存缓存中的条目
- `clearPersistent` 为 true 时,同时删除 L2 磁盘缓存条目
#### Example
```ts
// 仅从内存删除,保留磁盘缓存
await cache.delete('key1', false);
// 完全删除(内存 + 磁盘)
await cache.delete('key1');
```
***
### free()
> **free**(): `void`
Defined in: [packages/proxy/src/core/SmartCache.ts:176](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/SmartCache.ts#L176)
释放缓存资源
#### Returns
`void`
#### Description
清空 L1 内存缓存并清除 cacache 的内部 memoization 状态。
调用后 `initialized` 标志会被设为 false,但不会删除磁盘上的缓存文件。
重新调用 `init()` 可重新初始化。
***
### get()

@@ -88,3 +214,3 @@

Defined in: [packages/proxy/src/core/SmartCache.ts:59](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/SmartCache.ts#L59)
Defined in: [packages/proxy/src/core/SmartCache.ts:208](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/SmartCache.ts#L208)

@@ -99,2 +225,4 @@ 获取缓存条目

缓存键
#### Returns

@@ -104,2 +232,29 @@

缓存条目,包含 body 和 metadata;若不存在或读取失败返回 null
#### Description
**查找顺序**:
1. 先查 L1 内存缓存
2. 内存命中则直接返回(body 在内存则返回 Buffer,否则返回磁盘流)
3. 内存未命中则查 L2 磁盘
4. 磁盘命中时:
- 小文件(≤ maxMemorySize):读取到内存并回填 L1
- 大文件:只将 metadata 回填 L1,body 返回磁盘流
#### Example
```ts
const entry = await cache.get('user-123');
if (entry) {
// entry.body 可能是 Buffer(内存命中)或 ReadableStream(磁盘读取)
const data = Buffer.isBuffer(entry.body) ? entry.body : await streamToBuffer(entry.body);
console.log(entry.metadata);
}
```
#### Throws
磁盘 IO 错误时静默返回 null,不抛出异常
***

@@ -111,3 +266,3 @@

Defined in: [packages/proxy/src/core/SmartCache.ts:124](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/SmartCache.ts#L124)
Defined in: [packages/proxy/src/core/SmartCache.ts:320](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/SmartCache.ts#L320)

@@ -122,2 +277,4 @@ 获取磁盘读取流

缓存键
#### Returns

@@ -127,4 +284,51 @@

ReadableStream,从磁盘读取缓存内容
#### Description
返回 cacache 的流式读取接口,用于大文件场景的流式消费。
不经过 L1 内存缓存,直接从 L2 磁盘读取。
#### Example
```ts
const readStream = cache.getStream('large-file');
readStream.on('data', (chunk) => { /* 处理数据 */ });
readStream.on('end', () => console.log('完成'));
```
#### See
[setStream](#setstream) 配对使用
***
### init()
> **init**(`options?`): `void`
Defined in: [packages/proxy/src/core/SmartCache.ts:138](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/SmartCache.ts#L138)
初始化或重新初始化缓存
#### Parameters
##### options?
[`SmartCacheOptions`](../interfaces/SmartCacheOptions.md)
缓存配置选项,如果为 undefined 且已初始化则跳过
#### Returns
`void`
#### Description
- 首次调用时使用传入的 options 初始化
- 已初始化时调用会先调用 `free()` 释放旧资源
- 传入 undefined 且已初始化时跳过(用于外部传入 this 的场景)
***
### set()

@@ -134,5 +338,5 @@

Defined in: [packages/proxy/src/core/SmartCache.ts:99](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/SmartCache.ts#L99)
Defined in: [packages/proxy/src/core/SmartCache.ts:272](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/SmartCache.ts#L272)
写入缓存条目 (原子写入)
写入缓存条目

@@ -145,2 +349,4 @@ #### Parameters

缓存键
##### body

@@ -150,2 +356,4 @@

缓存体(Buffer)
##### metadata

@@ -155,2 +363,4 @@

元数据(不含 size,会自动填充 body.length)
#### Returns

@@ -160,2 +370,26 @@

Promise<void>
#### Description
**写入策略**:
1. 先计算 body 长度,自动添加到 metadata 中
2. 同步写入 L2 磁盘缓存(cacache)
3. 根据 body 大小决定是否写入 L1 内存:
- ≤ maxMemorySize:body 和 metadata 都存入 L1
- > maxMemorySize:只存入 metadata,body 保持在磁盘
#### Example
```ts
const response = await fetch('https://api.example.com/data');
const body = Buffer.from(await response.arrayBuffer());
await cache.set('api-data', body, {
url: response.url,
status: response.status,
headers: Object.fromEntries(response.headers.entries()),
createdAt: Date.now()
});
```
***

@@ -167,5 +401,5 @@

Defined in: [packages/proxy/src/core/SmartCache.ts:131](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/SmartCache.ts#L131)
Defined in: [packages/proxy/src/core/SmartCache.ts:351](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/SmartCache.ts#L351)
获取磁盘写入流 (流式缓存)
获取磁盘写入流

@@ -178,2 +412,4 @@ #### Parameters

缓存键
##### metadata

@@ -183,4 +419,33 @@

元数据(不含 size)
#### Returns
`WritableStream`
WritableStream,接收数据并写入磁盘缓存
#### Description
返回 cacache 的流式写入接口,适用于大文件场景。
- 写入前会先清除 L1 内存缓存中该 key 的条目(如果存在)
- 写入完成后(finish 事件)会再次清除 L1 条目,确保内存和磁盘一致
**注意**:流式写入无法自动计算 size,metadata 中不会包含 size 字段。
如需 size,需在写入完成后手动调用其他方法补充。
#### Example
```ts
const writeStream = cache.setStream('large-file', { url: '...' });
const readStream = fs.createReadStream('big-file.zip');
readStream.pipe(writeStream);
writeStream.on('finish', () => {
console.log('写入完成');
});
```
#### See
[getStream](#getstream) 配对使用

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/core/createCachedFetch.ts:17](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/createCachedFetch.ts#L17)
Defined in: [packages/proxy/src/core/createCachedFetch.ts:17](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/createCachedFetch.ts#L17)

@@ -49,3 +49,3 @@ 缓存请求工厂函数 (针对终端用户的顶层高阶 API)

`Partial`\<[`FetchWithCacheOptions`](../interfaces/FetchWithCacheOptions.md)\>
[`FetchWithCacheOptions`](../interfaces/FetchWithCacheOptions.md)

@@ -52,0 +52,0 @@ ### Returns

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/core/createFetchWithCache.ts:16](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/createFetchWithCache.ts#L16)
Defined in: [packages/proxy/src/core/createFetchWithCache.ts:16](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/createFetchWithCache.ts#L16)

@@ -49,3 +49,3 @@ 单一职责高阶函数:专门用于封装和隔离 activeCacheWrites 并发追踪器。

`Omit`\<[`FetchWithCacheOptions`](../interfaces/FetchWithCacheOptions.md), `"activeCacheWrites"`\>
[`FetchWithCacheOptions`](../interfaces/FetchWithCacheOptions.md)

@@ -52,0 +52,0 @@ ### Returns

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/utils/createResponse.ts:26](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/utils/createResponse.ts#L26)
Defined in: [packages/proxy/src/utils/createResponse.ts:26](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/utils/createResponse.ts#L26)

@@ -14,0 +14,0 @@ 创建带 url 的 Web Response 实例,并确保其 clone() 方法能正常保留该 url

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/utils/createResponse.ts:4](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/utils/createResponse.ts#L4)
Defined in: [packages/proxy/src/utils/createResponse.ts:4](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/utils/createResponse.ts#L4)

@@ -14,0 +14,0 @@ 核心辅助:为 Response 实例添加 url 属性并确保 clone 正常工作

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/utils/extractData.ts:23](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/utils/extractData.ts#L23)
Defined in: [packages/proxy/src/utils/extractData.ts:23](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/utils/extractData.ts#L23)

@@ -14,0 +14,0 @@ Universal Data Extraction and Filtering Utility (for Objects)

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/core/fetchWithCache.ts:233](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L233)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:245](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L245)

@@ -14,0 +14,0 @@ 核心协调函数:协调请求、缓存命中、并发控制和 SWR

@@ -9,5 +9,5 @@ [**@isdk/proxy**](../README.md)

> **generateCacheKey**(`req`, `config`): `Promise`\<`string`\>
> **generateCacheKey**(`req`, `siteConfig`, `bodyState?`, `effectiveConfig?`): `Promise`\<`string`\>
Defined in: [packages/proxy/src/core/generateCacheKey.ts:10](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/generateCacheKey.ts#L10)
Defined in: [packages/proxy/src/core/generateCacheKey.ts:15](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/generateCacheKey.ts#L15)

@@ -22,8 +22,38 @@ 根据 Request 对象和配置生成唯一的缓存指纹 (异步)

### config
请求对象
### siteConfig
[`ProxySiteConfig`](../interfaces/ProxySiteConfig.md)
站点级配置
### bodyState?
可选的 Body 读取状态(用于性能优化,避免重复读取)
#### checked
`boolean`
#### json?
`any`
#### limit
`number`
#### text
`string` \| `null`
### effectiveConfig?
[`ProxyCacheRule`](../interfaces/ProxyCacheRule.md)
可选的最终生效配置(用于性能优化,避免重复合并)
## Returns
`Promise`\<`string`\>

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/utils/getEffectiveConfig.ts:19](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/utils/getEffectiveConfig.ts#L19)
Defined in: [packages/proxy/src/utils/getEffectiveConfig.ts:19](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/utils/getEffectiveConfig.ts#L19)

@@ -14,0 +14,0 @@ 获取合并后的有效配置 (Rule -> Site -> Global)

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/core/isCacheable.ts:75](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/isCacheable.ts#L75)
Defined in: [packages/proxy/src/core/isCacheable.ts:33](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/isCacheable.ts#L33)

@@ -14,0 +14,0 @@ 获取叠加后的最终生效配置 (Rule -> Site -> Global)

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/core/isCacheable.ts:49](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/isCacheable.ts#L49)
Defined in: [packages/proxy/src/core/isCacheable.ts:7](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/isCacheable.ts#L7)

@@ -14,0 +14,0 @@ 获取请求匹配到的规则

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/utils/getSiteConfig.ts:17](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/utils/getSiteConfig.ts#L17)
Defined in: [packages/proxy/src/utils/getSiteConfig.ts:17](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/utils/getSiteConfig.ts#L17)

@@ -14,0 +14,0 @@ 根据 URL 获取对应的站点缓存配置

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/utils/isAllowed.ts:25](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/utils/isAllowed.ts#L25)
Defined in: [packages/proxy/src/utils/isAllowed.ts:25](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/utils/isAllowed.ts#L25)

@@ -14,0 +14,0 @@ 判断给定的键是否允许参与缓存指纹计算。

@@ -9,7 +9,8 @@ [**@isdk/proxy**](../README.md)

> **isCacheable**(`request`, `config`): `Promise`\<`boolean`\>
> **isCacheable**(`request`, `config`): `Promise`\<[`CacheAnalysis`](../interfaces/CacheAnalysis.md) \| `undefined`\>
Defined in: [packages/proxy/src/core/isCacheable.ts:171](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/isCacheable.ts#L171)
Defined in: [packages/proxy/src/core/isCacheable.ts:166](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/isCacheable.ts#L166)
判断当前请求是否满足可缓存的基础条件
Validates if the request meets the base cacheability criteria and returns analysis metadata.
判断当前请求是否满足可缓存的基础条件(门控校验)并返回分析上下文。

@@ -22,2 +23,4 @@ ## Parameters

Request object. 请求对象。
### config

@@ -27,4 +30,17 @@

Site-level configuration. 站点级配置。
## Returns
`Promise`\<`boolean`\>
`Promise`\<[`CacheAnalysis`](../interfaces/CacheAnalysis.md) \| `undefined`\>
- `CacheAnalysis`: If cacheable. Returns metadata for downstream steps (fingerprinting/fetching).
如果可缓存,返回包含规则和 Body 状态的对象,供后续步骤复用。
- `undefined`: If NOT cacheable. Blocked by site-level or rule-level gatekeeping.
如果不可缓存(被门控拦截),返回 undefined。
## Important
DO NOT simplify to boolean. The returned `bodyState` is CRITICAL for preventing
multiple stream reads in subsequent `generateCacheKey` and `fetch` calls.
请勿简化为 boolean。返回的 `bodyState` 对于防止后续流程中重复读取请求流至关重要。

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/utils/matcher.ts:8](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/utils/matcher.ts#L8)
Defined in: [packages/proxy/src/utils/matcher.ts:9](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/utils/matcher.ts#L9)

@@ -14,0 +14,0 @@ Checks if a string is a Glob pattern.

@@ -9,5 +9,5 @@ [**@isdk/proxy**](../README.md)

> **isMatch**(`pattern`, `value`, `usePrefix`, `defaultIfNoPositives`, `ignoreCase`): `boolean`
> **isMatch**(`pattern`, `value`, `usePrefix`): `boolean`
Defined in: [packages/proxy/src/utils/matcher.ts:29](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/utils/matcher.ts#L29)
Defined in: [packages/proxy/src/utils/matcher.ts:30](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/utils/matcher.ts#L30)

@@ -30,3 +30,3 @@ Universal matching function with advanced logic.

`string` | `RegExp` | (`string` \| `RegExp`)[]
`string` | `number` | `RegExp` | (`string` \| `number` \| `RegExp`)[]

@@ -41,20 +41,22 @@ ### value

`boolean` = `false`
Whether to use prefix matching for simple strings (default: false)
### defaultIfNoPositives
#### defaultIfNoPositives?
`boolean` = `true`
Return value when only negatives are provided and none match (default: true)
#### ignoreCase?
### ignoreCase
`boolean` = `true`
Whether to perform case-insensitive matching (default: true)
#### ignoreNegative?
`boolean`
#### usePrefix?
`boolean` = `false`
## Returns
`boolean`

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/utils/getEffectiveConfig.ts:7](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/utils/getEffectiveConfig.ts#L7)
Defined in: [packages/proxy/src/utils/getEffectiveConfig.ts:7](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/utils/getEffectiveConfig.ts#L7)

@@ -14,0 +14,0 @@ 标准化 Body 配置,确保其为对象形式以支持深度合并

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/core/prefetch.ts:55](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L55)
Defined in: [packages/proxy/src/core/prefetch.ts:55](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L55)

@@ -14,0 +14,0 @@ 预缓存函数

@@ -14,2 +14,3 @@ [**@isdk/proxy**](README.md)

- [CacheAnalysis](interfaces/CacheAnalysis.md)
- [FetchWithCacheContext](interfaces/FetchWithCacheContext.md)

@@ -26,2 +27,3 @@ - [FetchWithCacheOptions](interfaces/FetchWithCacheOptions.md)

- [ProxySiteConfig](interfaces/ProxySiteConfig.md)
- [ResponseCacheCheckResult](interfaces/ResponseCacheCheckResult.md)
- [SmartCacheOptions](interfaces/SmartCacheOptions.md)

@@ -37,2 +39,5 @@

- [AWS\_WAF\_PRESET](variables/AWS_WAF_PRESET.md)
- [CLOUDFLARE\_WAF\_PRESET](variables/CLOUDFLARE_WAF_PRESET.md)
- [GENERAL\_WAF\_PRESET](variables/GENERAL_WAF_PRESET.md)
- [OfflineCacheMissErrorCode](variables/OfflineCacheMissErrorCode.md)

@@ -43,2 +48,3 @@ - [OfflineCacheMissErrorMsg](variables/OfflineCacheMissErrorMsg.md)

- [clearWAFPresets](functions/clearWAFPresets.md)
- [createCachedFetch](functions/createCachedFetch.md)

@@ -55,2 +61,3 @@ - [createFetchWithCache](functions/createFetchWithCache.md)

- [getSiteConfig](functions/getSiteConfig.md)
- [getWAFPresets](functions/getWAFPresets.md)
- [isAllowed](functions/isAllowed.md)

@@ -60,3 +67,8 @@ - [isCacheable](functions/isCacheable.md)

- [isMatch](functions/isMatch.md)
- [isResponseCacheable](functions/isResponseCacheable.md)
- [isWAFChallenge](functions/isWAFChallenge.md)
- [matchField](functions/matchField.md)
- [normalizeBodyConfig](functions/normalizeBodyConfig.md)
- [prefetch](functions/prefetch.md)
- [registerWAFPreset](functions/registerWAFPreset.md)
- [unregisterWAFPreset](functions/unregisterWAFPreset.md)

@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/core/fetchWithCache.ts:34](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L34)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:38](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L38)

@@ -24,3 +24,3 @@ 内部流水线上下文

Defined in: [packages/proxy/src/core/fetchWithCache.ts:38](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L38)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:42](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L42)

@@ -39,3 +39,3 @@ 并发写入任务追踪器

Defined in: [packages/proxy/src/core/fetchWithCache.ts:22](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L22)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:24](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L24)

@@ -54,3 +54,3 @@ 是否启用后台异步更新 (SWR)

Defined in: [packages/proxy/src/core/fetchWithCache.ts:18](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L18)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:20](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L20)

@@ -69,3 +69,3 @@ 混合缓存实例

Defined in: [packages/proxy/src/core/fetchWithCache.ts:37](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L37)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:41](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L41)

@@ -78,3 +78,3 @@ ***

Defined in: [packages/proxy/src/core/fetchWithCache.ts:20](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L20)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:22](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L22)

@@ -93,3 +93,3 @@ 站点级基础配置

Defined in: [packages/proxy/src/core/fetchWithCache.ts:40](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L40)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:44](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L44)

@@ -104,3 +104,3 @@ 最终生效的合并配置

Defined in: [packages/proxy/src/core/fetchWithCache.ts:36](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L36)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:40](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L40)

@@ -121,5 +121,5 @@ #### Parameters

> `optional` **generateKey**: (`req`, `config`) => `Promise`\<`string`\>
> `optional` **generateKey**: (`req`, `siteConfig`, `bodyState?`, `effectiveConfig?`) => `Promise`\<`string`\>
Defined in: [packages/proxy/src/core/fetchWithCache.ts:26](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L26)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:30](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L30)

@@ -136,6 +136,36 @@ 自定义缓存键生成函数

##### config
请求对象
##### siteConfig
[`ProxySiteConfig`](ProxySiteConfig.md)
站点级配置
##### bodyState?
可选的 Body 读取状态(用于性能优化,避免重复读取)
###### checked
`boolean`
###### json?
`any`
###### limit
`number`
###### text
`string` \| `null`
##### effectiveConfig?
[`ProxyCacheRule`](ProxyCacheRule.md)
可选的最终生效配置(用于性能优化,避免重复合并)
#### Returns

@@ -155,3 +185,3 @@

Defined in: [packages/proxy/src/core/fetchWithCache.ts:24](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L24)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:28](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L28)

@@ -176,2 +206,16 @@ 后台更新 Promise 触发时的回调

### refresh?
> `optional` **refresh**: `boolean`
Defined in: [packages/proxy/src/core/fetchWithCache.ts:26](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L26)
是否强制刷新缓存(跳过读取,但请求成功后会更新缓存)
#### Inherited from
[`FetchWithCacheOptions`](FetchWithCacheOptions.md).[`refresh`](FetchWithCacheOptions.md#refresh)
***
### request

@@ -181,2 +225,2 @@

Defined in: [packages/proxy/src/core/fetchWithCache.ts:35](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L35)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:39](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L39)

@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/core/fetchWithCache.ts:16](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L16)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:18](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L18)

@@ -24,3 +24,3 @@ fetchWithCache 选项

Defined in: [packages/proxy/src/core/fetchWithCache.ts:30](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L30)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:34](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L34)

@@ -35,3 +35,3 @@ 并发写入任务追踪器

Defined in: [packages/proxy/src/core/fetchWithCache.ts:22](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L22)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:24](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L24)

@@ -46,3 +46,3 @@ 是否启用后台异步更新 (SWR)

Defined in: [packages/proxy/src/core/fetchWithCache.ts:18](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L18)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:20](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L20)

@@ -57,3 +57,3 @@ 混合缓存实例

Defined in: [packages/proxy/src/core/fetchWithCache.ts:20](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L20)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:22](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L22)

@@ -66,5 +66,5 @@ 站点级基础配置

> `optional` **generateKey**: (`req`, `config`) => `Promise`\<`string`\>
> `optional` **generateKey**: (`req`, `siteConfig`, `bodyState?`, `effectiveConfig?`) => `Promise`\<`string`\>
Defined in: [packages/proxy/src/core/fetchWithCache.ts:26](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L26)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:30](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L30)

@@ -81,6 +81,36 @@ 自定义缓存键生成函数

##### config
请求对象
##### siteConfig
[`ProxySiteConfig`](ProxySiteConfig.md)
站点级配置
##### bodyState?
可选的 Body 读取状态(用于性能优化,避免重复读取)
###### checked
`boolean`
###### json?
`any`
###### limit
`number`
###### text
`string` \| `null`
##### effectiveConfig?
[`ProxyCacheRule`](ProxyCacheRule.md)
可选的最终生效配置(用于性能优化,避免重复合并)
#### Returns

@@ -96,3 +126,3 @@

Defined in: [packages/proxy/src/core/fetchWithCache.ts:24](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/fetchWithCache.ts#L24)
Defined in: [packages/proxy/src/core/fetchWithCache.ts:28](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L28)

@@ -110,1 +140,11 @@ 后台更新 Promise 触发时的回调

`void`
***
### refresh?
> `optional` **refresh**: `boolean`
Defined in: [packages/proxy/src/core/fetchWithCache.ts:26](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/fetchWithCache.ts#L26)
是否强制刷新缓存(跳过读取,但请求成功后会更新缓存)

@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/core/prefetch.ts:17](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L17)
Defined in: [packages/proxy/src/core/prefetch.ts:17](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L17)

@@ -18,3 +18,3 @@ ## Properties

Defined in: [packages/proxy/src/core/prefetch.ts:23](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L23)
Defined in: [packages/proxy/src/core/prefetch.ts:23](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L23)

@@ -29,3 +29,3 @@ SmartCache 实例

Defined in: [packages/proxy/src/core/prefetch.ts:27](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L27)
Defined in: [packages/proxy/src/core/prefetch.ts:27](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L27)

@@ -40,3 +40,3 @@ 并发数,默认 3

Defined in: [packages/proxy/src/core/prefetch.ts:21](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L21)
Defined in: [packages/proxy/src/core/prefetch.ts:21](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L21)

@@ -51,3 +51,3 @@ 完整的代理配置

Defined in: [packages/proxy/src/core/prefetch.ts:25](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L25)
Defined in: [packages/proxy/src/core/prefetch.ts:25](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L25)

@@ -72,3 +72,3 @@ 自定义 fetcher,默认使用 globalThis.fetch

Defined in: [packages/proxy/src/core/prefetch.ts:29](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L29)
Defined in: [packages/proxy/src/core/prefetch.ts:29](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L29)

@@ -101,3 +101,3 @@ 进度回调 (completed, total, url)

Defined in: [packages/proxy/src/core/prefetch.ts:31](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L31)
Defined in: [packages/proxy/src/core/prefetch.ts:31](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L31)

@@ -112,4 +112,4 @@ 取消信号

Defined in: [packages/proxy/src/core/prefetch.ts:19](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L19)
Defined in: [packages/proxy/src/core/prefetch.ts:19](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L19)
要预缓存的 URL 列表及其请求选项

@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/core/prefetch.ts:10](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L10)
Defined in: [packages/proxy/src/core/prefetch.ts:10](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L10)

@@ -20,3 +20,3 @@ 预缓存请求选项

Defined in: [packages/proxy/src/core/prefetch.ts:14](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L14)
Defined in: [packages/proxy/src/core/prefetch.ts:14](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L14)

@@ -31,4 +31,4 @@ 可选的请求配置(method, headers, body 等)

Defined in: [packages/proxy/src/core/prefetch.ts:12](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L12)
Defined in: [packages/proxy/src/core/prefetch.ts:12](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L12)
请求 URL

@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/core/prefetch.ts:34](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L34)
Defined in: [packages/proxy/src/core/prefetch.ts:34](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L34)

@@ -18,3 +18,3 @@ ## Properties

Defined in: [packages/proxy/src/core/prefetch.ts:40](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L40)
Defined in: [packages/proxy/src/core/prefetch.ts:40](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L40)

@@ -37,3 +37,3 @@ 失败详情

Defined in: [packages/proxy/src/core/prefetch.ts:38](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L38)
Defined in: [packages/proxy/src/core/prefetch.ts:38](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L38)

@@ -48,4 +48,4 @@ 失败数量

Defined in: [packages/proxy/src/core/prefetch.ts:36](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/prefetch.ts#L36)
Defined in: [packages/proxy/src/core/prefetch.ts:36](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/prefetch.ts#L36)
成功数量

@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/types.ts:29](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L29)
Defined in: [packages/proxy/src/types.ts:29](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L29)

@@ -19,8 +19,9 @@ Special configuration for Request/Response Body.

> `optional` **extract**: `string` \| `RegExp`
> `optional` **extract**: [`ProxyMatchPatterns`](../type-aliases/ProxyMatchPatterns.md) \| [`ProxyFieldConfig`](../type-aliases/ProxyFieldConfig.md)
Defined in: [packages/proxy/src/types.ts:44](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L44)
Defined in: [packages/proxy/src/types.ts:46](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L46)
Regex for extracting data from non-JSON (text) bodies.
用于非 JSON (文本) Body 的提取正则表达式。
Data extraction rules (used for Fingerprinting).
Supports JSON field filtering or Regex for text bodies.
数据提取规则(用于指纹提取)。支持 JSON 字段过滤或针对文本 Body 的正则提取。

@@ -33,6 +34,7 @@ ***

Defined in: [packages/proxy/src/types.ts:39](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L39)
Defined in: [packages/proxy/src/types.ts:40](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L40)
Field-level matching and extraction for JSON bodies.
针对 JSON Body 的字段级匹配与提取。
Body matching rules (used for Gatekeeping).
Supports JSON field-level matching or string/regex matching for text bodies.
Body 匹配规则(用于门控)。支持针对 JSON 的字段级匹配,或针对文本 Body 的字符串/正则匹配。

@@ -45,3 +47,3 @@ ***

Defined in: [packages/proxy/src/types.ts:54](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L54)
Defined in: [packages/proxy/src/types.ts:56](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L56)

@@ -57,3 +59,3 @@ Maximum length limit when matching/extracting Body, default is 1024 (1KB).

Defined in: [packages/proxy/src/types.ts:49](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L49)
Defined in: [packages/proxy/src/types.ts:51](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L51)

@@ -69,5 +71,5 @@ Whether to sort extracted JSON keys or regex capture groups to ensure fingerprint consistency.

Defined in: [packages/proxy/src/types.ts:34](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L34)
Defined in: [packages/proxy/src/types.ts:34](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L34)
Body type. If not specified, automatically determined by Content-Type.
Body 类型。不指定时根据 Content-Type 自动判断。

@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/types.ts:169](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L169)
Defined in: [packages/proxy/src/types.ts:199](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L199)

@@ -25,3 +25,3 @@ Complete Cache Entry.

Defined in: [packages/proxy/src/types.ts:171](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L171)
Defined in: [packages/proxy/src/types.ts:201](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L201)

@@ -36,3 +36,3 @@ Response body data: Buffer for small files, Readable Stream for large ones. 响应体数据。

Defined in: [packages/proxy/src/types.ts:152](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L152)
Defined in: [packages/proxy/src/types.ts:182](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L182)

@@ -51,3 +51,3 @@ Response headers object. 响应头对象。

Defined in: [packages/proxy/src/types.ts:158](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L158)
Defined in: [packages/proxy/src/types.ts:188](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L188)

@@ -66,3 +66,3 @@ Original request method. 原始请求方法。

Defined in: [packages/proxy/src/types.ts:154](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L154)
Defined in: [packages/proxy/src/types.ts:184](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L184)

@@ -81,3 +81,3 @@ http-cache-semantics policy object. 策略对象,包含 TTL。

Defined in: [packages/proxy/src/types.ts:162](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L162)
Defined in: [packages/proxy/src/types.ts:192](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L192)

@@ -96,3 +96,3 @@ Byte length of the body. Body 的字节长度。

Defined in: [packages/proxy/src/types.ts:150](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L150)
Defined in: [packages/proxy/src/types.ts:180](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L180)

@@ -111,3 +111,3 @@ HTTP Status Code. HTTP 状态码。

Defined in: [packages/proxy/src/types.ts:160](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L160)
Defined in: [packages/proxy/src/types.ts:190](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L190)

@@ -126,3 +126,3 @@ Timestamp when cache was written. 写入时间戳。

Defined in: [packages/proxy/src/types.ts:156](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L156)
Defined in: [packages/proxy/src/types.ts:186](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L186)

@@ -129,0 +129,0 @@ Original request URL. 原始请求 URL。

@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/types.ts:148](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L148)
Defined in: [packages/proxy/src/types.ts:178](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L178)

@@ -25,3 +25,3 @@ Cache Metadata.

Defined in: [packages/proxy/src/types.ts:152](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L152)
Defined in: [packages/proxy/src/types.ts:182](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L182)

@@ -36,3 +36,3 @@ Response headers object. 响应头对象。

Defined in: [packages/proxy/src/types.ts:158](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L158)
Defined in: [packages/proxy/src/types.ts:188](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L188)

@@ -47,3 +47,3 @@ Original request method. 原始请求方法。

Defined in: [packages/proxy/src/types.ts:154](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L154)
Defined in: [packages/proxy/src/types.ts:184](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L184)

@@ -58,3 +58,3 @@ http-cache-semantics policy object. 策略对象,包含 TTL。

Defined in: [packages/proxy/src/types.ts:162](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L162)
Defined in: [packages/proxy/src/types.ts:192](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L192)

@@ -69,3 +69,3 @@ Byte length of the body. Body 的字节长度。

Defined in: [packages/proxy/src/types.ts:150](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L150)
Defined in: [packages/proxy/src/types.ts:180](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L180)

@@ -80,3 +80,3 @@ HTTP Status Code. HTTP 状态码。

Defined in: [packages/proxy/src/types.ts:160](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L160)
Defined in: [packages/proxy/src/types.ts:190](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L190)

@@ -91,4 +91,4 @@ Timestamp when cache was written. 写入时间戳。

Defined in: [packages/proxy/src/types.ts:156](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L156)
Defined in: [packages/proxy/src/types.ts:186](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L186)
Original request URL. 原始请求 URL。

@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/types.ts:64](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L64)
Defined in: [packages/proxy/src/types.ts:66](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L66)

@@ -29,3 +29,3 @@ Core Cache Rule Definition (V8).

Defined in: [packages/proxy/src/types.ts:96](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L96)
Defined in: [packages/proxy/src/types.ts:98](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L98)

@@ -41,3 +41,3 @@ Body matching and extraction configuration.

Defined in: [packages/proxy/src/types.ts:91](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L91)
Defined in: [packages/proxy/src/types.ts:93](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L93)

@@ -53,3 +53,3 @@ Cookie matching and fingerprinting configuration.

Defined in: [packages/proxy/src/types.ts:107](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L107)
Defined in: [packages/proxy/src/types.ts:137](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L137)

@@ -65,3 +65,3 @@ Force cache: Ignore `Cache-Control: no-store` etc. and force store in cache.

Defined in: [packages/proxy/src/types.ts:86](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L86)
Defined in: [packages/proxy/src/types.ts:88](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L88)

@@ -77,3 +77,3 @@ Headers matching and fingerprinting configuration.

Defined in: [packages/proxy/src/types.ts:76](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L76)
Defined in: [packages/proxy/src/types.ts:78](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L78)

@@ -89,3 +89,3 @@ Allowed HTTP methods (e.g., "GET", ["GET", "POST"]).

Defined in: [packages/proxy/src/types.ts:112](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L112)
Defined in: [packages/proxy/src/types.ts:142](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L142)

@@ -101,3 +101,3 @@ Strict offline mode: No network requests, read only from cache. Fails if cache miss.

Defined in: [packages/proxy/src/types.ts:71](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L71)
Defined in: [packages/proxy/src/types.ts:73](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L73)

@@ -115,3 +115,3 @@ Path gatekeeping.

Defined in: [packages/proxy/src/types.ts:81](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L81)
Defined in: [packages/proxy/src/types.ts:83](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L83)

@@ -123,2 +123,42 @@ Query parameter matching and fingerprinting configuration.

### response?
> `optional` **response**: `object`
Defined in: [packages/proxy/src/types.ts:104](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L104)
Response-side cacheability criteria.
响应侧可缓存性判定准则。
#### body?
> `optional` **body**: [`ProxyMatchPatterns`](../type-aliases/ProxyMatchPatterns.md)
Response body matching patterns (for text/json).
Supports Glob negation (e.g., "!*captcha*") to exclude dirty data.
响应体匹配模式(仅限文本/JSON)。支持 Glob 否定(如 "!*captcha*")来排除脏数据。
#### headers?
> `optional` **headers**: [`ProxyFieldConfig`](../type-aliases/ProxyFieldConfig.md)
Required or forbidden response headers.
响应头匹配要求。
#### minLength?
> `optional` **minLength**: `number`
Minimum body length (in bytes) to be considered valid.
最小有效响应体长度(字节),防止缓存截断或错误页面。
#### statuses?
> `optional` **statuses**: [`ProxyMatchPatterns`](../type-aliases/ProxyMatchPatterns.md)
Allowed HTTP statuses.
允许缓存的状态码模式。
***
### staleIfError?

@@ -128,5 +168,5 @@

Defined in: [packages/proxy/src/types.ts:102](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L102)
Defined in: [packages/proxy/src/types.ts:132](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L132)
Fault tolerance: If backend fails (network error or 5xx), return stale cache if available.
容错机制:当后端请求失败且存在旧缓存时,强制返回旧缓存。

@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/types.ts:133](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L133)
Defined in: [packages/proxy/src/types.ts:163](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L163)

@@ -25,3 +25,3 @@ Global Interceptor Configuration.

Defined in: [packages/proxy/src/types.ts:96](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L96)
Defined in: [packages/proxy/src/types.ts:98](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L98)

@@ -41,3 +41,3 @@ Body matching and extraction configuration.

Defined in: [packages/proxy/src/types.ts:91](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L91)
Defined in: [packages/proxy/src/types.ts:93](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L93)

@@ -57,3 +57,3 @@ Cookie matching and fingerprinting configuration.

Defined in: [packages/proxy/src/types.ts:107](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L107)
Defined in: [packages/proxy/src/types.ts:137](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L137)

@@ -73,3 +73,3 @@ Force cache: Ignore `Cache-Control: no-store` etc. and force store in cache.

Defined in: [packages/proxy/src/types.ts:86](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L86)
Defined in: [packages/proxy/src/types.ts:88](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L88)

@@ -89,3 +89,3 @@ Headers matching and fingerprinting configuration.

Defined in: [packages/proxy/src/types.ts:76](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L76)
Defined in: [packages/proxy/src/types.ts:78](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L78)

@@ -105,3 +105,3 @@ Allowed HTTP methods (e.g., "GET", ["GET", "POST"]).

Defined in: [packages/proxy/src/types.ts:112](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L112)
Defined in: [packages/proxy/src/types.ts:142](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L142)

@@ -121,3 +121,3 @@ Strict offline mode: No network requests, read only from cache. Fails if cache miss.

Defined in: [packages/proxy/src/types.ts:71](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L71)
Defined in: [packages/proxy/src/types.ts:73](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L73)

@@ -139,3 +139,3 @@ Path gatekeeping.

Defined in: [packages/proxy/src/types.ts:81](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L81)
Defined in: [packages/proxy/src/types.ts:83](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L83)

@@ -151,2 +151,46 @@ Query parameter matching and fingerprinting configuration.

### response?
> `optional` **response**: `object`
Defined in: [packages/proxy/src/types.ts:104](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L104)
Response-side cacheability criteria.
响应侧可缓存性判定准则。
#### body?
> `optional` **body**: [`ProxyMatchPatterns`](../type-aliases/ProxyMatchPatterns.md)
Response body matching patterns (for text/json).
Supports Glob negation (e.g., "!*captcha*") to exclude dirty data.
响应体匹配模式(仅限文本/JSON)。支持 Glob 否定(如 "!*captcha*")来排除脏数据。
#### headers?
> `optional` **headers**: [`ProxyFieldConfig`](../type-aliases/ProxyFieldConfig.md)
Required or forbidden response headers.
响应头匹配要求。
#### minLength?
> `optional` **minLength**: `number`
Minimum body length (in bytes) to be considered valid.
最小有效响应体长度(字节),防止缓存截断或错误页面。
#### statuses?
> `optional` **statuses**: [`ProxyMatchPatterns`](../type-aliases/ProxyMatchPatterns.md)
Allowed HTTP statuses.
允许缓存的状态码模式。
#### Inherited from
[`ProxyCacheRule`](ProxyCacheRule.md).[`response`](ProxyCacheRule.md#response)
***
### sites?

@@ -156,3 +200,3 @@

Defined in: [packages/proxy/src/types.ts:139](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L139)
Defined in: [packages/proxy/src/types.ts:169](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L169)

@@ -169,3 +213,3 @@ Granular cache configuration for specific domains.

Defined in: [packages/proxy/src/types.ts:102](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L102)
Defined in: [packages/proxy/src/types.ts:132](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L132)

@@ -185,4 +229,4 @@ Fault tolerance: If backend fails (network error or 5xx), return stale cache if available.

Defined in: [packages/proxy/src/types.ts:141](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L141)
Defined in: [packages/proxy/src/types.ts:171](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L171)
Physical storage path for disk cache (cacache). 磁盘缓存物理存储路径。

@@ -9,3 +9,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/types.ts:119](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L119)
Defined in: [packages/proxy/src/types.ts:149](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L149)

@@ -25,3 +25,3 @@ Site-level Cache Configuration.

Defined in: [packages/proxy/src/types.ts:96](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L96)
Defined in: [packages/proxy/src/types.ts:98](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L98)

@@ -41,3 +41,3 @@ Body matching and extraction configuration.

Defined in: [packages/proxy/src/types.ts:91](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L91)
Defined in: [packages/proxy/src/types.ts:93](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L93)

@@ -57,3 +57,3 @@ Cookie matching and fingerprinting configuration.

Defined in: [packages/proxy/src/types.ts:107](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L107)
Defined in: [packages/proxy/src/types.ts:137](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L137)

@@ -73,3 +73,3 @@ Force cache: Ignore `Cache-Control: no-store` etc. and force store in cache.

Defined in: [packages/proxy/src/types.ts:86](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L86)
Defined in: [packages/proxy/src/types.ts:88](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L88)

@@ -89,3 +89,3 @@ Headers matching and fingerprinting configuration.

Defined in: [packages/proxy/src/types.ts:76](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L76)
Defined in: [packages/proxy/src/types.ts:78](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L78)

@@ -105,3 +105,3 @@ Allowed HTTP methods (e.g., "GET", ["GET", "POST"]).

Defined in: [packages/proxy/src/types.ts:112](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L112)
Defined in: [packages/proxy/src/types.ts:142](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L142)

@@ -121,3 +121,3 @@ Strict offline mode: No network requests, read only from cache. Fails if cache miss.

Defined in: [packages/proxy/src/types.ts:71](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L71)
Defined in: [packages/proxy/src/types.ts:73](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L73)

@@ -139,3 +139,3 @@ Path gatekeeping.

Defined in: [packages/proxy/src/types.ts:81](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L81)
Defined in: [packages/proxy/src/types.ts:83](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L83)

@@ -151,2 +151,46 @@ Query parameter matching and fingerprinting configuration.

### response?
> `optional` **response**: `object`
Defined in: [packages/proxy/src/types.ts:104](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L104)
Response-side cacheability criteria.
响应侧可缓存性判定准则。
#### body?
> `optional` **body**: [`ProxyMatchPatterns`](../type-aliases/ProxyMatchPatterns.md)
Response body matching patterns (for text/json).
Supports Glob negation (e.g., "!*captcha*") to exclude dirty data.
响应体匹配模式(仅限文本/JSON)。支持 Glob 否定(如 "!*captcha*")来排除脏数据。
#### headers?
> `optional` **headers**: [`ProxyFieldConfig`](../type-aliases/ProxyFieldConfig.md)
Required or forbidden response headers.
响应头匹配要求。
#### minLength?
> `optional` **minLength**: `number`
Minimum body length (in bytes) to be considered valid.
最小有效响应体长度(字节),防止缓存截断或错误页面。
#### statuses?
> `optional` **statuses**: [`ProxyMatchPatterns`](../type-aliases/ProxyMatchPatterns.md)
Allowed HTTP statuses.
允许缓存的状态码模式。
#### Inherited from
[`ProxyCacheRule`](ProxyCacheRule.md).[`response`](ProxyCacheRule.md#response)
***
### rules?

@@ -156,3 +200,3 @@

Defined in: [packages/proxy/src/types.ts:126](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L126)
Defined in: [packages/proxy/src/types.ts:156](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L156)

@@ -170,3 +214,3 @@ List of granular path-based rules.

Defined in: [packages/proxy/src/types.ts:102](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L102)
Defined in: [packages/proxy/src/types.ts:132](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L132)

@@ -173,0 +217,0 @@ Fault tolerance: If backend fails (network error or 5xx), return stale cache if available.

@@ -9,6 +9,20 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/core/SmartCache.ts:10](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/SmartCache.ts#L10)
Defined in: [packages/proxy/src/core/SmartCache.ts:22](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/SmartCache.ts#L22)
SmartCache 选项
## Example
```ts
const cache = new SmartCache({
storagePath: '/tmp/my-cache',
maxMemorySize: 2 * 1024 * 1024, // 2MB
maxTotalMemorySize: 200 * 1024 * 1024, // 200MB
memoryOptions: {
capacity: 1000,
expires: 10 * 60 * 1000 // 10分钟
}
});
```
## Properties

@@ -20,6 +34,17 @@

Defined in: [packages/proxy/src/core/SmartCache.ts:14](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/SmartCache.ts#L14)
Defined in: [packages/proxy/src/core/SmartCache.ts:35](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/SmartCache.ts#L35)
内存缓存阈值(字节)。响应体大小超过此值时,Body 将只存入磁盘,而 Meta 仍保留在内存。默认 1MB。
内存缓存阈值(字节)。
#### Description
响应体大小超过此值时,Body 将只存入磁盘,而 Meta 元数据仍保留在内存中。
此优化可减少大文件对内存的占用。
#### Default
```ts
1024 * 1024 (1MB)
```
***

@@ -31,6 +56,16 @@

Defined in: [packages/proxy/src/core/SmartCache.ts:16](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/SmartCache.ts#L16)
Defined in: [packages/proxy/src/core/SmartCache.ts:41](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/SmartCache.ts#L41)
内存缓存总大小阈值(字节)。默认 100MB。超过此值将清空内存缓存。
内存缓存总大小阈值(字节)。
#### Description
超过此值时,LRU 缓存会自动清除最久未使用的条目以释放内存。
#### Default
```ts
100 * 1024 * 1024 (100MB)
```
***

@@ -42,5 +77,5 @@

Defined in: [packages/proxy/src/core/SmartCache.ts:18](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/SmartCache.ts#L18)
Defined in: [packages/proxy/src/core/SmartCache.ts:47](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/SmartCache.ts#L47)
透传给 L1 (Memory) 的高级配置 (secondary-cache LRUCache options)
透传给 L1 内存缓存的高级配置。

@@ -51,2 +86,4 @@ #### Index Signature

允许添加其他 LRUCache 支持的选项
#### capacity?

@@ -56,2 +93,4 @@

LRU 缓存的最大条目数,为 0 时仅按 maxWeight 限制
#### cleanInterval?

@@ -61,2 +100,4 @@

清理检查间隔(毫秒)
#### expires?

@@ -66,2 +107,12 @@

缓存条目过期时间(毫秒),默认 5 分钟
#### Description
基于 secondary-cache 的 LRUCache 选项,可自定义容量、过期时间等参数。
#### See
https://www.npmjs.com/package/secondary-cache
***

@@ -73,4 +124,14 @@

Defined in: [packages/proxy/src/core/SmartCache.ts:12](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/core/SmartCache.ts#L12)
Defined in: [packages/proxy/src/core/SmartCache.ts:28](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/core/SmartCache.ts#L28)
磁盘缓存的物理路径。如果不提供,将默认使用系统临时目录。
磁盘缓存的物理路径。
#### Description
如果不提供,将默认使用系统临时目录 (`os.tmpdir()`) 下的 `isdk-proxy-cache` 目录。
#### Default
```ts
os.tmpdir() + '/isdk-proxy-cache'
```

@@ -27,4 +27,5 @@ **@isdk/proxy**

- **🛡️ Request Coalescing**: Merges concurrent requests for the same resource to protect upstream servers.
- **🚑 High Resiliency**: Automatically returns stale cache on backend failure (`staleIfError`) or forces caching regardless of origin directives (`forceCache`).
- **🕵️ Transparent Status**: Injects `x-proxy-cache` header (`HIT`, `STALE`, `MISS`, `STALE_IF_ERROR`) for easy debugging.
- **🚑 High Resiliency & STALE_RESCUE**: Automatically returns stale cache on backend failure (`staleIfError`). When WAF challenges, dirty data (via `minLength` or `body` patterns), or 403/429 blocks are detected, it protects the valid old cache and returns it as `STALE_RESCUE`.
- **🛡️ Built-in WAF Presets**: Pre-integrated presets for Cloudflare, AWS WAF, and others, ready to use out of the box.
- **🕵️ Transparent Status**: Injects `x-proxy-cache` header (`HIT`, `STALE`, `MISS`, `STALE_RESCUE`, `STALE_IF_ERROR`) for easy debugging.

@@ -102,7 +103,92 @@ ## Installation

| `cookies` | `FieldConfig` | Cookie filtering. Defaults to none. |
| `body` | `BodyConfig` | Body matching & extraction. |
| `body` | `BodyConfig` | Body matching & extraction. Supports gatekeeping via `match` and fingerprinting via `extract`. |
| `staleIfError`| `boolean` | Return stale cache on backend errors. |
| `forceCache` | `boolean` | Force caching regardless of origin directives. |
| `offline` | `boolean` | Strict offline mode: Read-only cache, returns `512` on cache miss. |
| `response` | `ResponseConfig` | Response-side cacheability validation. Supports status, headers, and body matching. |
### `ResponseConfig`
Define "what is valid and cacheable content" to automatically filter out WAF challenge pages.
| Option | Type | Description |
| :--- | :--- | :--- |
| `statuses` | `MatchPatterns`| Allowed HTTP status codes. Defaults to common cacheable statuses (200, 404, etc.). |
| `headers` | `FieldConfig` | Required or forbidden response headers. |
| `body` | `MatchPatterns`| Response body matching. Supports Glob negation (e.g., `!*Challenge*`) to exclude dirty data. |
| `minLength` | `number` | Minimum content length. Shorter responses will be intercepted (triggers `STALE_RESCUE`). |
### `BodyConfig` Deep Dive
For complex bodies, `@isdk/proxy` supports a clean separation of concerns:
| Option | Type | Description |
| :--- | :--- | :--- |
| `type` | `'json' \| 'text' \| 'binary'` | Body type. Automatically determined by Content-Type if omitted. |
| `match` | `FieldConfig \| MatchPatterns` | **Gatekeeping**. Field-level validation for JSON or Pattern matching for Text. |
| `extract`| `FieldConfig \| MatchPatterns` | **Fingerprinting**. Priority over `match`. Supports field filtering for JSON fingerprints. |
| `maxLength`| `number` | Maximum read limit during validation/extraction. |
| `sort` | `boolean` | Sort JSON keys to ensure fingerprint stability. Defaults to `true`. |
### Cache Status Meanings (`x-proxy-cache`)
| Status | Description |
| :--- | :--- |
| `HIT` | Cache hit, fresh content within TTL. |
| `OFFLINE_HIT` | Cache hit successfully in `offline: true` mode. |
| `STALE` | Cache hit but expired, SWR background update triggered. |
| `MISS` | Cache miss, request sent to origin and result cached. |
| `STALE_IF_ERROR` | Origin request failed (network error or 5xx), returned expired stale cache. |
| `STALE_RESCUE_{REASON}` | Disaster recovery protection. Served valid old cache when origin returned invalid data (e.g., `WAF_CHALLENGE` or `TOO_SHORT`). |
| `MISS_EXCLUDED_REQUEST` | Request excluded from caching by configuration rules (method, path, etc.). |
| `OFFLINE_MISS_EXCLUDED_REQUEST` | Offline mode, request excluded by rules and no local cache available. |
| `MISS_UNSTORABLE` | Response not storable (e.g., `no-store` directive and `forceCache` off). |
| `MISS_EXCLUDED_{REASON}` | Response validation failed (e.g., body too short or WAF challenge detected). |
| `MISS_EXCLUDED_WAF_CHALLENGE`| Explicitly detected WAF challenge page and no old cache available. |
### Built-in WAF Protection
`@isdk/proxy` includes built-in detection rules for major WAF providers (e.g., Cloudflare, AWS WAF), enabled by default. These rules are defined as **Positive Signatures**, meaning if a response matches *any* of the defined features (status code, header, or body keyword), it's identified as a WAF challenge.
You can dynamically manage WAF presets via the following APIs:
```typescript
import {
registerWAFPreset,
unregisterWAFPreset,
isWAFChallenge,
CLOUDFLARE_WAF_PRESET
} from '@isdk/proxy';
// 1. Register a custom WAF signature
registerWAFPreset({
response: {
statuses: ['418'],
body: ['*I am a teapot*']
}
});
// 2. Programmatic Detection (Manual check in code)
// This function automatically handles clone(), so it won't consume the original stream
if (await isWAFChallenge(response)) {
console.log('WAF Challenge detected, intervention required');
}
// 3. Unregister a specific preset
unregisterWAFPreset(CLOUDFLARE_WAF_PRESET);
```
#### WAF Management API Reference
| Function | Description |
| :--- | :--- |
| `isWAFChallenge(res, presets?)` | Determines if a response is a WAF challenge. Supports optional custom presets. |
| `getWAFPresets()` | Retrieves all currently registered WAF preset rules. |
| `registerWAFPreset(rule)` | Registers a new WAF signature rule. |
| `unregisterWAFPreset(rule)` | Unregisters an existing rule. |
| `clearWAFPresets()` | Clears all registered WAF presets. |
> [!NOTE]
> `fetchWithCache` automatically calls `isWAFChallenge` when processing responses. If a WAF challenge is detected and a valid old cache exists, it triggers `STALE_RESCUE_WAF_CHALLENGE` to prevent your clean data from being overwritten by "dirty" data.
### MatchPatterns Syntax

@@ -128,21 +214,27 @@

* **Types**: `string | RegExp | Array`
* **Semantic**: **Existence Filter**. "At least one field in the request must match this pattern."
* **Logic**: Based on `some` logic.
* **Types**: `string | RegExp | Array`
* **Semantic**: Distinction between **Single Value (String/Regex)** and **List Mode (Array)**.
| Config Example | Semantic | Result for Empty Request |
| Form | Matching Rule (Blocking) | Description | Typical Use Case |
| :--- | :--- | :--- | :--- |
| **Single Value (String/Regex)** | **Strict (All keys)** | **Every** key in the request must satisfy this rule. | **Strict Exclusion/Access**. e.g., `!id` strictly forbids 'id' entirely; `id` only allows requests with 'id' alone. |
| **List Mode (Array)** | **Lenient (Any key)** | Matches if **any** key satisfies the rule; **negations are ignored** during matching. | **Parameter Filtering**. e.g., `['*', '!sid']` ignores 'sid' for the cache key without blocking the request. |
##### Behavior Comparison:
| Config Example | Is Request Blocked? | What's in the Cache Key? |
| :--- | :--- | :--- |
| `['*']` | Pass if any field exists. | ❌ Blocked (No keys to match) |
| `['id', 'name']` | Must contain 'id' or 'name'. | ❌ Blocked |
| `['*', '!sid']` | Must contain fields other than 'sid'.| ❌ Blocked |
| `[]` (Empty Array) | Block all (No key can match an empty array).| ❌ Blocked |
| `query: '!id'` | ❌ Blocked if `id` is present | All params except `id` |
| `query: ['*', '!id']` | ✅ Not blocked even if only `id` exists | All params except `id` |
| `query: 'id'` | ✅ Only allowed if `id` is the **only** key | Only `id` field |
| `query: ['id']` | ✅ Allowed if `id` is present | Only `id` field |
> [!TIP]
> **MatchPatterns is best for Whitelists or coarse Blacklists.** e.g., `query: ['id']` means "the request must have an id parameter and will be cached based ONLY on that id."
> **Simple Rule**: Use a single value for "Strict format constraints"; use an array for "Excluding parameters from the cache key".
#### 2. Record Mode
* **Types**: `Record<string, ProxyMatchPatterns | boolean>`
* **Semantic**: **Field Validation**. Logic declarations for specific keys.
* **Logic**: Based on `AND` logic.
* **Types**: `Record<string, ProxyMatchPatterns | boolean>`
* **Semantic**: **Field Validation**. Logic declarations for specific keys.
* **Logic**: Based on `AND` logic.

@@ -165,2 +257,33 @@ | Config Example | Semantic | Result for Empty Request |

#### 🚀 Runtime Dynamic Configuration (`isdkProxy`)
`@isdk/proxy` allows you to attach an `isdkProxy` property directly to the `Request` object. This is the **highest priority** configuration method, enabling you to adjust cache behavior dynamically based on business logic at the moment of the request.
```typescript
const req = new Request('https://api.example.com/data');
// Attach runtime instructions
(req as any).isdkProxy = {
refresh: true, // Bypass cache and force a "healing" update
forceCache: true, // Force caching even if origin says no-store
onBackgroundUpdate: (res) => { ... }, // Override global SWR callback
generateKey: async (req) => 'custom_key', // Override hashing logic
config: { // Temporary rule overrides
offline: true, // Dynamic offline mode
body: {
match: ['*'], // Gatekeeping: allow all
extract: ['id', '!ts'] // Extraction: exclude 'ts' field from fingerprint
}
}
};
const res = await fetchWithCache(req, fetcher, { cache, config: siteConfig });
```
**Priority Order**:
1. **`Request.isdkProxy` (Runtime)** - Top-level override.
2. **`Matched Rule` (Rule Level)** - Specific rule matching the URL/Body.
3. **`Site Config` (Site Level)** - Domain-based configuration.
4. **`Global Config` (Global Level)** - System defaults.
---

@@ -191,2 +314,3 @@

- **`options.onBackgroundUpdate`**: Callback that receives the background update Promise when triggered.
- **`options.refresh`**: **Force Refresh**. Bypasses cache reading to force an origin request. If a valid response is received, it automatically "heals" and updates the cache. Often used to "pierce" through WAF challenges.
- **`options.activeCacheWrites`**: Optional. Shared concurrency tracker Map.

@@ -280,14 +404,2 @@ - **Returns**: A wrapped fetch function `(request, fetcher) => Promise<Response>`.

```typescript
import { getSiteConfig } from '@isdk/proxy';
const config = getSiteConfig('https://api.example.com/data', {
methods: ['GET'],
sites: {
'api.example.com': { forceCache: true }, // Hostname match
'/internal/': { offline: true } // Path prefix match
}
});
```
#### `isAllowed(key, config, defaultAllowed?)`

@@ -376,11 +488,54 @@

Responses managed by `@isdk/proxy` include the `x-proxy-cache` header:
All `Response` objects returned by `@isdk/proxy` include an `x-proxy-cache` header for observability. This header provides granular status information:
- `HIT`: Cache hit.
- `MISS`: Cache miss, fetched from origin.
- `STALE`: Stale hit (triggered background SWR update).
- `STALE_IF_ERROR`: Origin failed, returned stale cache as fallback.
- **Core Hits**:
- `HIT`: Cache hit. Data served from L1 (memory) or L2 (disk).
- `OFFLINE_HIT`: Served from cache in offline mode.
- **Fetching & Updates**:
- `MISS`: Cache miss. Fetched from origin and successfully cached.
- `STALE`: Stale hit. Served from cache while a background SWR update is triggered.
- **Failovers**:
- `STALE_IF_ERROR`: Backend failed; serving stale cache as a fallback.
- `STALE_RESCUE_{REASON}`: Disaster recovery protection. Served valid old cache when origin returned invalid data.
- **Exclusion Reasons**:
- `MISS_EXCLUDED_REQUEST`: Request excluded by configuration rules.
- `OFFLINE_MISS_EXCLUDED_REQUEST`: Offline mode, request excluded and no cache found.
- `MISS_UNSTORABLE`: Response not cacheable (e.g., `Cache-Control: no-store`).
- `MISS_EXCLUDED_{REASON}`: Response validation failed; data fetched but not cached.
**Common `{REASON}` Suffixes:**
| Suffix | Meaning |
| :--- | :--- |
| `WAF_CHALLENGE` | Explicitly detected WAF challenge page (via built-in or custom rules). |
| `TOO_SHORT` | Content length is less than the configured `minLength`. |
| `BODY_MATCH_FAILED` | Content failed body keyword matching (negation hit or positive miss). |
| `STATUS_MISMATCH_{CODE}` | Status code not in the allowed cache list (e.g., `STATUS_MISMATCH_503`). |
| `HEADERS_MISMATCH` | Response headers do not meet configuration requirements. |
| `BODY_READ_ERROR` | Error occurred while reading response body for analysis. |
| `UNKNOWN` | Other unspecified validation failure. |
### Response Object Properties
To ensure consistency for downstream consumers, responses returned by `fetchWithCache` feature:
1. **URL Preservation**: The `response.url` property correctly reflects the original request URL, even when served from cache.
2. **Clone Compatibility**: Custom properties and headers are preserved when calling `response.clone()`.
### Debugging
This library uses the [debug](https://github.com/debug-js/debug) package. Enable internal tracing by setting the `DEBUG` environment variable:
```bash
# Trace all cache logic for fetch operations
DEBUG=@isdk/proxy:fetchWithCache node app.js
# Trace everything
DEBUG=@isdk/proxy:* node app.js
```
Logs cover configuration merging, fingerprinting, policy evaluation, SWR tasks, and response validation.
## License
MIT

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/types.ts:23](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L23)
Defined in: [packages/proxy/src/types.ts:23](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L23)

@@ -18,5 +18,5 @@ Field-level configuration: Uses a Record structure to give each Key explicit gatekeeping and fingerprinting semantics.

- key: Field name (e.g., "id", "Authorization").
- value:
- value:
- true: Field MUST exist (gatekeeping) and be included in the fingerprint (extraction). 必须存在并包含在指纹中。
- false: Field MUST NOT exist (gatekeeping) and be excluded from the fingerprint. 必须不存在且不包含在指纹中。
- MatchPatterns: Field value MUST match the pattern (gatekeeping) and be included in the fingerprint. 值必须匹配且包含在指纹中。

@@ -11,5 +11,5 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/types.ts:5](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L5)
Defined in: [packages/proxy/src/types.ts:5](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L5)
Atomic matching pattern: supports strings (including Glob/Negation patterns like '!id') or RegExp objects.
原子匹配模式:支持字符串(含 Glob/否定模式如 '!id')或正则表达式。

@@ -11,5 +11,5 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/types.ts:11](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/types.ts#L11)
Defined in: [packages/proxy/src/types.ts:11](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/types.ts#L11)
Collection of matching patterns: supports a single pattern or an array of patterns.
匹配模式集合:支持单模式或模式数组。

@@ -11,3 +11,3 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/errors.ts:14](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/errors.ts#L14)
Defined in: [packages/proxy/src/errors.ts:14](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/errors.ts#L14)

@@ -14,0 +14,0 @@ Offline 缓存未命中错误代码

@@ -11,2 +11,2 @@ [**@isdk/proxy**](../README.md)

Defined in: [packages/proxy/src/errors.ts:15](https://github.com/isdk/proxy.js/blob/ca0753e2e2dcac65190c537ce1634a27f5ee2158/src/errors.ts#L15)
Defined in: [packages/proxy/src/errors.ts:15](https://github.com/isdk/proxy.js/blob/f5a749970f69b68943b2d54ecd2dc1b566c7b859/src/errors.ts#L15)
{
"name": "@isdk/proxy",
"version": "0.3.0",
"version": "0.4.0",
"type": "module",

@@ -5,0 +5,0 @@ "description": "A framework-agnostic, high-performance hybrid caching middleware with SWR, request collapsing, and stale-if-error support.",

@@ -23,4 +23,5 @@ # @isdk/proxy

- **🛡️ 请求合并防击穿 (Request Coalescing)**: 当大量并发请求同一资源时,通过全局 Map 合并排队,确保只有一个源站网络请求被发出,彻底防止缓存击穿。
- **🚑 强离线容灾**: 当后端服务宕机时,自动强制返回旧缓存 (`staleIfError`);甚至可以无视 `no-store` 指令强制缓存一切内容 (`forceCache`)。
- **🕵️ 透明的缓存状态**: 自动在返回结果中注入 `x-proxy-cache` 响应头 (`HIT`, `STALE`, `MISS`, `STALE_IF_ERROR`),极大方便调试与监控。
- **🚑 强离线容灾与 STALE_RESCUE**: 当后端服务宕机时,自动强制返回旧缓存 (`staleIfError`);检测到 WAF 人机挑战、脏数据(通过 `minLength` 或 `body` 匹配)或 403/429 拦截时,自动保护并返回旧缓存 (`STALE_RESCUE`),确保“旧的正确数据”不被“新的错误数据”覆盖。
- **🛡️ 内置 WAF 挑战识别**: 预集成 Cloudflare、AWS WAF 等人机挑战识别预设,开箱即用。
- **🕵️ 透明的缓存状态**: 自动在返回结果中注入 `x-proxy-cache` 响应头 (`HIT`, `STALE`, `MISS`, `STALE_RESCUE`, `STALE_IF_ERROR`),极大方便调试与监控。
- **🌐 环境中立**: 完美适配所有支持 Web 标准 `Request`/`Response` API 的环境。

@@ -98,7 +99,93 @@

| `cookies` | `FieldConfig` | Cookie 字段过滤。默认全量**不**提取。 |
| `body` | `BodyConfig` | 请求体匹配与提取。支持 JSON 字段过滤、Text 正则提取和 Binary 全量哈希。 |
| `body` | `BodyConfig` | 请求体匹配与提取。支持通过 `match` 进行门控准入,通过 `extract` 进行字段指纹提取。 |
| `staleIfError`| `boolean` | 网络请求失败时,是否强制返回本地过期的旧缓存。 |
| `forceCache` | `boolean` | 是否无视源站指令强制执行缓存。 |
| `offline` | `boolean` | 离线模式。开启后只读缓存,若无缓存则返回状态码 `512` 的 Response (`OfflineCacheMissErrorCode`)。 |
| `response` | `ResponseConfig` | 响应侧可缓存性校验。支持状态码、Header 及 Body 内容匹配。 |
### `ResponseConfig` 响应校验
通过 `response` 字段,你可以精准定义“什么是有效且值得缓存的内容”,从而自动屏蔽 WAF 挑战页面。
| 配置项 | 类型 | 说明 |
| :--- | :--- | :--- |
| `statuses` | `MatchPatterns`| 允许缓存的状态码模式。默认支持 200, 404, 301 等常见状态。 |
| `headers` | `FieldConfig` | 响应头匹配要求。 |
| `body` | `MatchPatterns`| 响应体内容匹配。支持 Glob 否定 (如 `!*Challenge*`) 排除脏数据。 |
| `minLength` | `number` | 最小内容长度。小于此长度的响应将被拦截(触发 `STALE_RESCUE`)。 |
### `BodyConfig` 请求体匹配与提取
针对复杂的 Body,支持细化的职责分离:
| 配置项 | 类型 | 说明 |
| :--- | :--- | :--- |
| `type` | `'json' \| 'text' \| 'binary'` | Body 类型。默认根据 Content-Type 自动判断。 |
| `match` | `FieldConfig \| MatchPatterns` | **门控准入**。JSON 模式下支持字段校验,文本模式下支持通配符/正则。 |
| `extract`| `FieldConfig \| MatchPatterns` | **指纹提取**。优先级高于 `match`。支持 JSON 字段级过滤(提取特定字段作为 Key)。 |
| `maxLength`| `number` | 校验/提取时的最大 Body 长度限制。 |
| `sort` | `boolean` | 是否对提取出的 JSON 键进行排序,确保指纹一致性。默认 `true`。 |
### 缓存状态说明 (`x-proxy-cache`)
| 状态码 | 说明 |
| :--- | :--- |
| `HIT` | 缓存命中,内容新鲜且在有效期内。 |
| `OFFLINE_HIT` | 离线模式下成功命中缓存。 |
| `STALE` | 缓存命中但已过期,已触发 SWR 后台异步更新。 |
| `MISS` | 缓存未命中,已发起源站请求并存入缓存。 |
| `STALE_IF_ERROR` | 源站请求失败(网络错误或 5xx),强制返回过期的旧缓存作为兜底。 |
| `STALE_RESCUE_{REASON}` | 容灾保护中。响应校验失败(如 `WAF_CHALLENGE` 或 `TOO_SHORT`),为保护数据一致性返回旧缓存。 |
| `MISS_EXCLUDED_REQUEST` | 请求因配置规则(方法、路径等)被排除在缓存之外。 |
| `OFFLINE_MISS_EXCLUDED_REQUEST` | 离线模式下,请求不符合缓存规则且无本地缓存。 |
| `MISS_UNSTORABLE` | 响应不可存储(如 `no-store` 指令且未开启 `forceCache`)。 |
| `MISS_EXCLUDED_{REASON}` | 响应侧校验未通过(如 Body 过短或识别到 `WAF_CHALLENGE`)。 |
| `MISS_EXCLUDED_WAF_CHALLENGE`| 明确识别到人机挑战页面且无可用旧缓存。 |
### 内置 WAF 防护
`@isdk/proxy` 内置了主流 WAF 厂商(如 Cloudflare, AWS WAF)的识别规则,默认开启。这些规则被定义为 **正向特征签名**(Positive Signatures),即只要响应命中其中任何一个特征(状态码、Header 或 Body 关键字),就会被判定为人机挑战页面。
你可以通过以下 API 动态管理 WAF 预设:
```typescript
import {
registerWAFPreset,
unregisterWAFPreset,
isWAFChallenge,
CLOUDFLARE_WAF_PRESET
} from '@isdk/proxy';
// 1. 注册自定义 WAF 签名
registerWAFPreset({
response: {
statuses: ['418'],
body: ['*I am a teapot*']
}
});
// 2. 编程式判定(在业务逻辑中主动识别)
// 该函数会自动处理 clone(),不会消耗原始响应流
if (await isWAFChallenge(response)) {
console.log('检测到人机挑战,需人工介入');
}
// 3. 注销特定预设
unregisterWAFPreset(CLOUDFLARE_WAF_PRESET);
```
#### WAF 管理 API 列表
| 函数 | 说明 |
| :--- | :--- |
| `isWAFChallenge(res, presets?)` | 判定响应是否为 WAF 挑战。支持传入自定义预设列表。 |
| `getWAFPresets()` | 获取当前所有已注册的 WAF 预设规则。 |
| `registerWAFPreset(rule)` | 注册一个新的 WAF 签名规则。 |
| `unregisterWAFPreset(rule)` | 注销一个已存在的规则。 |
| `clearWAFPresets()` | 清空所有 WAF 预设。 |
> [!NOTE]
> `fetchWithCache` 在处理响应时会自动调用 `isWAFChallenge`。如果判定为 WAF 挑战且本地存在旧缓存,将自动触发 `STALE_RESCUE_WAF_CHALLENGE` 容灾保护,确保不被“脏数据”覆盖。
### `ProxyCacheRule` 规则对象

@@ -118,4 +205,36 @@

| `onBackgroundUpdate`| `function` | 当触发后台更新时,接收该更新 Promise 的回调。可用作任务追踪。 |
| `refresh` | `boolean` | **强制刷新**:忽略现有缓存(即使命中且新鲜也会回源),若回源拿到合法数据则自动更新并“愈合”缓存。常用于配合真人验证进行“穿透”。 |
| `generateKey` | `function` | 自定义缓存键生成函数。 |
#### 🚀 运行时动态配置 (`isdkProxy`)
`@isdk/proxy` 支持在 `Request` 对象上直接添加 `isdkProxy` 属性。这是 **优先级最高** 的配置方式,允许你在发起请求的那一刻,根据业务逻辑动态调整缓存行为。
```typescript
const req = new Request('https://api.example.com/data');
// 在 JS 环境下,你可以直接给 Request 添加属性
(req as any).isdkProxy = {
refresh: true, // 强制穿透:忽略现有缓存并更新(愈合模式)
forceCache: true, // 强制缓存:即使源站禁止缓存也入库
onBackgroundUpdate: (res) => { ... }, // 运行时回调:覆盖全局的 SWR 回调
generateKey: async (req) => 'custom_key', // 自定义 Key:覆盖默认哈希逻辑
config: { // 临时覆盖缓存规则 (Site/Rule Config)
offline: true, // 动态进入离线模式
body: {
match: ['*'], // 门控:允许所有 Body
extract: ['id', '!ts'] // 提取:指纹中排除 ts 字段
}
}
};
const res = await fetchWithCache(req, fetcher, { cache, config: siteConfig });
```
**优先级顺序**:
1. **`Request.isdkProxy` (运行时)** - 最顶级覆盖。
2. **`Matched Rule` (规则级)** - 针对特定 URL/Body 匹配出的规则。
3. **`Site Config` (站点级)** - 针对域名的基础配置。
4. **`Global Config` (全局级)** - 系统默认兜底。
### 模式匹配说明 (MatchPatterns)

@@ -136,23 +255,29 @@

#### 1. 模式匹配模式 (MatchPatterns)
#### 1. 匹配模式 (MatchPatterns)
* **适用类型**:`string | RegExp | Array`
* **核心语义**:**存在性过滤 (Existence Filter)**。即“请求中必须存在至少一个匹配该模式的字段”。
* **逻辑判定**:基于 `some` 逻辑。
* **适用类型**:`string | RegExp | Array`
* **核心逻辑**:区分 **单值模式(字符串/正则)** 与 **列表模式(数组)**。
| 配置示例 | 语义说明 | 无字段请求结果 |
| 配置形式 | 匹配规则 (拦截) | 语义说明 | 典型场景 |
| :--- | :--- | :--- | :--- |
| **单值 (字符串/正则)** | **严格匹配 (所有)** | 请求中**每一个**参数都必须符合要求。 | **严格排他或精确准入**。如 `!id` 彻底禁掉 id,`id` 只认 id。 |
| **列表 (数组)** | **宽松匹配 (任一)** | 只要请求里**有一个**参数符合要求即可,且**自动忽略排除项**。 | **参数过滤**。如 `['*', '!sid']` 忽略 sid 生成缓存键,但不影响请求正常通过。 |
##### 行为对照表:
| 配置示例 | 请求是否会被拦截? | 缓存键(指纹)里包含什么? |
| :--- | :--- | :--- |
| `['*']` | 只要存在任何字段即可。 | ❌ 拦截 (无字段可匹配) |
| `['id', 'name']` | 必须包含 id 或 name。 | ❌ 拦截 |
| `['*', '!sid']` | 必须包含除 sid 以外的字段。 | ❌ 拦截 |
| `[]` (空数组) | 不允许任何字段(因为没有任何 key 能匹配空数组)。 | ❌ 拦截 |
| `query: '!id'` | ❌ 如果请求里带了 `id` 就拦截 | 除了 `id` 以外的所有参数 |
| `query: ['*', '!id']` | ✅ 即使只有 `id` 也不拦截 | 除了 `id` 以外的所有参数 |
| `query: 'id'` | ✅ 只有当请求**只带了** `id` 时才通过 | 只有 `id` 字段 |
| `query: ['id']` | ✅ 只要请求中**包含了** `id` 就通过 | 只有 `id` 字段 |
> [!TIP]
> **数组模式适合“白名单”或“粗粒度黑名单”场景**。例如 `query: ['id']` 表示“必须带 id 且只按 id 缓存”。
> **简单记法**:如果你想实现“严格的格式限制”,请使用单值;如果你想实现“忽略某些参数生成缓存”,请使用数组。
#### 2. 对象配置模式 (Record)
* **适用类型**:`Record<string, ProxyMatchPatterns | boolean>`
* **核心语义**:**字段验证 (Validation)**。针对特定字段名及其值进行逻辑声明。
* **逻辑判定**:基于 `AND` 逻辑。
* **适用类型**:`Record<string, ProxyMatchPatterns | boolean>`
* **核心语义**:**字段验证 (Validation)**。针对特定字段名及其值进行逻辑声明。
* **逻辑判定**:基于 `AND` 逻辑。

@@ -378,11 +503,54 @@ | 配置示例 | 语义说明 | 无字段请求结果 |

由 `@isdk/proxy` 处理并返回的所有 `Response`,其 Headers 中都会注入 `x-proxy-cache` 字段以便观测生命周期,可能的值有:
由 `@isdk/proxy` 处理并返回的所有 `Response`,其 Headers 中都会注入 `x-proxy-cache` 字段以便观测生命周期。为了实现精准的观测与调试,该标头进行了细粒度的划分:
- `HIT`: 完美命中,数据完全来自于 L1 内存或 L2 磁盘缓存。
- `MISS`: 缓存未命中(或主动绕过缓存),数据真实来自于源站请求。
- `STALE`: 命中过期缓存(已通过 SWR 机制在后台发起了静默网络更新)。
- `STALE_IF_ERROR`: 源站请求失败(网络断开或报错),系统作为兜底强制返回了过期的旧缓存。
- **核心命中状态**:
- `HIT`: 完美命中。数据完全来自于 L1 内存或 L2 磁盘缓存,且处于有效期内。
- `OFFLINE_HIT`: 离线模式命中。在 `offline: true` 模式下成功从缓存读取数据。
- **回源与更新状态**:
- `MISS`: 缓存未命中。数据真实来自于源站请求,且响应符合缓存规则,已存入缓存。
- `STALE`: 命中过期缓存。数据来自缓存,但已触发 SWR (Stale-While-Revalidate) 机制在后台静默发起网络更新。
- **异常与兜底状态**:
- `STALE_IF_ERROR`: 源站请求失败(网络断开或 5xx 错误),系统作为兜底强制返回了过期的旧缓存。
- `STALE_RESCUE_{REASON}`: 容灾保护命中。当源站返回非预期数据时,拒绝更新坏缓存并强制返回旧的有效缓存。
- **排除状态 (未缓存原因)**:
- `MISS_EXCLUDED_REQUEST`: 请求本身不符合缓存规则(如方法不支持、路径被排除等)。
- `OFFLINE_MISS_EXCLUDED_REQUEST`: 离线模式下,请求不符合规则且无可用缓存。
- `MISS_UNSTORABLE`: 响应本身不符合缓存规范(如 `Cache-Control: no-store` 或状态码不在缓存范围内)。
- `MISS_EXCLUDED_{REASON}`: 响应侧校验未通过,数据虽然来自源站但不会被存入缓存。
**常见的 `{REASON}` 后缀含义:**
| 后缀 | 含义 |
| :--- | :--- |
| `WAF_CHALLENGE` | 明确识别到人机挑战页面(由内置或自定义 WAF 规则判定)。 |
| `TOO_SHORT` | 响应体长度未达到配置的 `minLength` 阈值。 |
| `BODY_MATCH_FAILED` | 响应内容未通过 Body 关键字校验(命中排除项或未命中必含项)。 |
| `STATUS_MISMATCH_{CODE}` | 状态码不在允许缓存的范围内(如 `STATUS_MISMATCH_503`)。 |
| `HEADERS_MISMATCH` | 响应头不满足配置的匹配要求。 |
| `BODY_READ_ERROR` | 尝试读取响应体进行内容分析时发生错误。 |
| `UNKNOWN` | 其他未知原因导致的校验失败。 |
### 响应对象特性
为了确保下游处理的一致性,`fetchWithCache` 返回的 `Response` 对象具有以下特性:
1. **URL 持久化**: 即使是手动从缓存构建的响应,其 `response.url` 也会正确保留原始请求的 URL。
2. **克隆友好**: 调用 `response.clone()` 产生的副本将完美继承所有自定义属性(包括 `url` 和注入的标头)。
### 调试 (Debugging)
本库集成了 [debug](https://github.com/debug-js/debug) 模块,可以通过设置环境变量开启详细的内部追踪日志:
```bash
# 开启所有 fetch 相关的缓存逻辑追踪
DEBUG=@isdk/proxy:fetchWithCache node app.js
# 开启所有日志
DEBUG=@isdk/proxy:* node app.js
```
日志涵盖了配置合并、指纹生成、缓存策略评估、后台 SWR 任务触发以及响应侧校验等关键环节。
## 许可证
MIT
+188
-31

@@ -23,4 +23,5 @@ # @isdk/proxy

- **🛡️ Request Coalescing**: Merges concurrent requests for the same resource to protect upstream servers.
- **🚑 High Resiliency**: Automatically returns stale cache on backend failure (`staleIfError`) or forces caching regardless of origin directives (`forceCache`).
- **🕵️ Transparent Status**: Injects `x-proxy-cache` header (`HIT`, `STALE`, `MISS`, `STALE_IF_ERROR`) for easy debugging.
- **🚑 High Resiliency & STALE_RESCUE**: Automatically returns stale cache on backend failure (`staleIfError`). When WAF challenges, dirty data (via `minLength` or `body` patterns), or 403/429 blocks are detected, it protects the valid old cache and returns it as `STALE_RESCUE`.
- **🛡️ Built-in WAF Presets**: Pre-integrated presets for Cloudflare, AWS WAF, and others, ready to use out of the box.
- **🕵️ Transparent Status**: Injects `x-proxy-cache` header (`HIT`, `STALE`, `MISS`, `STALE_RESCUE`, `STALE_IF_ERROR`) for easy debugging.

@@ -98,7 +99,93 @@ ## Installation

| `cookies` | `FieldConfig` | Cookie filtering. Defaults to none. |
| `body` | `BodyConfig` | Body matching & extraction. |
| `body` | `BodyConfig` | Body matching & extraction. Supports gatekeeping via `match` and fingerprinting via `extract`. |
| `staleIfError`| `boolean` | Return stale cache on backend errors. |
| `forceCache` | `boolean` | Force caching regardless of origin directives. |
| `offline` | `boolean` | Strict offline mode: Read-only cache, returns `512` on cache miss. |
| `response` | `ResponseConfig` | Response-side cacheability validation. Supports status, headers, and body matching. |
### `ResponseConfig`
Define "what is valid and cacheable content" to automatically filter out WAF challenge pages.
| Option | Type | Description |
| :--- | :--- | :--- |
| `statuses` | `MatchPatterns`| Allowed HTTP status codes. Defaults to common cacheable statuses (200, 404, etc.). |
| `headers` | `FieldConfig` | Required or forbidden response headers. |
| `body` | `MatchPatterns`| Response body matching. Supports Glob negation (e.g., `!*Challenge*`) to exclude dirty data. |
| `minLength` | `number` | Minimum content length. Shorter responses will be intercepted (triggers `STALE_RESCUE`). |
### `BodyConfig` Deep Dive
For complex bodies, `@isdk/proxy` supports a clean separation of concerns:
| Option | Type | Description |
| :--- | :--- | :--- |
| `type` | `'json' \| 'text' \| 'binary'` | Body type. Automatically determined by Content-Type if omitted. |
| `match` | `FieldConfig \| MatchPatterns` | **Gatekeeping**. Field-level validation for JSON or Pattern matching for Text. |
| `extract`| `FieldConfig \| MatchPatterns` | **Fingerprinting**. Priority over `match`. Supports field filtering for JSON fingerprints. |
| `maxLength`| `number` | Maximum read limit during validation/extraction. |
| `sort` | `boolean` | Sort JSON keys to ensure fingerprint stability. Defaults to `true`. |
### Cache Status Meanings (`x-proxy-cache`)
| Status | Description |
| :--- | :--- |
| `HIT` | Cache hit, fresh content within TTL. |
| `OFFLINE_HIT` | Cache hit successfully in `offline: true` mode. |
| `STALE` | Cache hit but expired, SWR background update triggered. |
| `MISS` | Cache miss, request sent to origin and result cached. |
| `STALE_IF_ERROR` | Origin request failed (network error or 5xx), returned expired stale cache. |
| `STALE_RESCUE_{REASON}` | Disaster recovery protection. Served valid old cache when origin returned invalid data (e.g., `WAF_CHALLENGE` or `TOO_SHORT`). |
| `MISS_EXCLUDED_REQUEST` | Request excluded from caching by configuration rules (method, path, etc.). |
| `OFFLINE_MISS_EXCLUDED_REQUEST` | Offline mode, request excluded by rules and no local cache available. |
| `MISS_UNSTORABLE` | Response not storable (e.g., `no-store` directive and `forceCache` off). |
| `MISS_EXCLUDED_{REASON}` | Response validation failed (e.g., body too short or WAF challenge detected). |
| `MISS_EXCLUDED_WAF_CHALLENGE`| Explicitly detected WAF challenge page and no old cache available. |
### Built-in WAF Protection
`@isdk/proxy` includes built-in detection rules for major WAF providers (e.g., Cloudflare, AWS WAF), enabled by default. These rules are defined as **Positive Signatures**, meaning if a response matches *any* of the defined features (status code, header, or body keyword), it's identified as a WAF challenge.
You can dynamically manage WAF presets via the following APIs:
```typescript
import {
registerWAFPreset,
unregisterWAFPreset,
isWAFChallenge,
CLOUDFLARE_WAF_PRESET
} from '@isdk/proxy';
// 1. Register a custom WAF signature
registerWAFPreset({
response: {
statuses: ['418'],
body: ['*I am a teapot*']
}
});
// 2. Programmatic Detection (Manual check in code)
// This function automatically handles clone(), so it won't consume the original stream
if (await isWAFChallenge(response)) {
console.log('WAF Challenge detected, intervention required');
}
// 3. Unregister a specific preset
unregisterWAFPreset(CLOUDFLARE_WAF_PRESET);
```
#### WAF Management API Reference
| Function | Description |
| :--- | :--- |
| `isWAFChallenge(res, presets?)` | Determines if a response is a WAF challenge. Supports optional custom presets. |
| `getWAFPresets()` | Retrieves all currently registered WAF preset rules. |
| `registerWAFPreset(rule)` | Registers a new WAF signature rule. |
| `unregisterWAFPreset(rule)` | Unregisters an existing rule. |
| `clearWAFPresets()` | Clears all registered WAF presets. |
> [!NOTE]
> `fetchWithCache` automatically calls `isWAFChallenge` when processing responses. If a WAF challenge is detected and a valid old cache exists, it triggers `STALE_RESCUE_WAF_CHALLENGE` to prevent your clean data from being overwritten by "dirty" data.
### MatchPatterns Syntax

@@ -124,21 +211,27 @@

* **Types**: `string | RegExp | Array`
* **Semantic**: **Existence Filter**. "At least one field in the request must match this pattern."
* **Logic**: Based on `some` logic.
* **Types**: `string | RegExp | Array`
* **Semantic**: Distinction between **Single Value (String/Regex)** and **List Mode (Array)**.
| Config Example | Semantic | Result for Empty Request |
| Form | Matching Rule (Blocking) | Description | Typical Use Case |
| :--- | :--- | :--- | :--- |
| **Single Value (String/Regex)** | **Strict (All keys)** | **Every** key in the request must satisfy this rule. | **Strict Exclusion/Access**. e.g., `!id` strictly forbids 'id' entirely; `id` only allows requests with 'id' alone. |
| **List Mode (Array)** | **Lenient (Any key)** | Matches if **any** key satisfies the rule; **negations are ignored** during matching. | **Parameter Filtering**. e.g., `['*', '!sid']` ignores 'sid' for the cache key without blocking the request. |
##### Behavior Comparison:
| Config Example | Is Request Blocked? | What's in the Cache Key? |
| :--- | :--- | :--- |
| `['*']` | Pass if any field exists. | ❌ Blocked (No keys to match) |
| `['id', 'name']` | Must contain 'id' or 'name'. | ❌ Blocked |
| `['*', '!sid']` | Must contain fields other than 'sid'.| ❌ Blocked |
| `[]` (Empty Array) | Block all (No key can match an empty array).| ❌ Blocked |
| `query: '!id'` | ❌ Blocked if `id` is present | All params except `id` |
| `query: ['*', '!id']` | ✅ Not blocked even if only `id` exists | All params except `id` |
| `query: 'id'` | ✅ Only allowed if `id` is the **only** key | Only `id` field |
| `query: ['id']` | ✅ Allowed if `id` is present | Only `id` field |
> [!TIP]
> **MatchPatterns is best for Whitelists or coarse Blacklists.** e.g., `query: ['id']` means "the request must have an id parameter and will be cached based ONLY on that id."
> **Simple Rule**: Use a single value for "Strict format constraints"; use an array for "Excluding parameters from the cache key".
#### 2. Record Mode
* **Types**: `Record<string, ProxyMatchPatterns | boolean>`
* **Semantic**: **Field Validation**. Logic declarations for specific keys.
* **Logic**: Based on `AND` logic.
* **Types**: `Record<string, ProxyMatchPatterns | boolean>`
* **Semantic**: **Field Validation**. Logic declarations for specific keys.
* **Logic**: Based on `AND` logic.

@@ -161,2 +254,33 @@ | Config Example | Semantic | Result for Empty Request |

#### 🚀 Runtime Dynamic Configuration (`isdkProxy`)
`@isdk/proxy` allows you to attach an `isdkProxy` property directly to the `Request` object. This is the **highest priority** configuration method, enabling you to adjust cache behavior dynamically based on business logic at the moment of the request.
```typescript
const req = new Request('https://api.example.com/data');
// Attach runtime instructions
(req as any).isdkProxy = {
refresh: true, // Bypass cache and force a "healing" update
forceCache: true, // Force caching even if origin says no-store
onBackgroundUpdate: (res) => { ... }, // Override global SWR callback
generateKey: async (req) => 'custom_key', // Override hashing logic
config: { // Temporary rule overrides
offline: true, // Dynamic offline mode
body: {
match: ['*'], // Gatekeeping: allow all
extract: ['id', '!ts'] // Extraction: exclude 'ts' field from fingerprint
}
}
};
const res = await fetchWithCache(req, fetcher, { cache, config: siteConfig });
```
**Priority Order**:
1. **`Request.isdkProxy` (Runtime)** - Top-level override.
2. **`Matched Rule` (Rule Level)** - Specific rule matching the URL/Body.
3. **`Site Config` (Site Level)** - Domain-based configuration.
4. **`Global Config` (Global Level)** - System defaults.
---

@@ -187,2 +311,3 @@

- **`options.onBackgroundUpdate`**: Callback that receives the background update Promise when triggered.
- **`options.refresh`**: **Force Refresh**. Bypasses cache reading to force an origin request. If a valid response is received, it automatically "heals" and updates the cache. Often used to "pierce" through WAF challenges.
- **`options.activeCacheWrites`**: Optional. Shared concurrency tracker Map.

@@ -276,14 +401,3 @@ - **Returns**: A wrapped fetch function `(request, fetcher) => Promise<Response>`.

```typescript
import { getSiteConfig } from '@isdk/proxy';
const config = getSiteConfig('https://api.example.com/data', {
methods: ['GET'],
sites: {
'api.example.com': { forceCache: true }, // Hostname match
'/internal/': { offline: true } // Path prefix match
}
});
```
#### `isAllowed(key, config, defaultAllowed?)`

@@ -372,11 +486,54 @@

Responses managed by `@isdk/proxy` include the `x-proxy-cache` header:
All `Response` objects returned by `@isdk/proxy` include an `x-proxy-cache` header for observability. This header provides granular status information:
- `HIT`: Cache hit.
- `MISS`: Cache miss, fetched from origin.
- `STALE`: Stale hit (triggered background SWR update).
- `STALE_IF_ERROR`: Origin failed, returned stale cache as fallback.
- **Core Hits**:
- `HIT`: Cache hit. Data served from L1 (memory) or L2 (disk).
- `OFFLINE_HIT`: Served from cache in offline mode.
- **Fetching & Updates**:
- `MISS`: Cache miss. Fetched from origin and successfully cached.
- `STALE`: Stale hit. Served from cache while a background SWR update is triggered.
- **Failovers**:
- `STALE_IF_ERROR`: Backend failed; serving stale cache as a fallback.
- `STALE_RESCUE_{REASON}`: Disaster recovery protection. Served valid old cache when origin returned invalid data.
- **Exclusion Reasons**:
- `MISS_EXCLUDED_REQUEST`: Request excluded by configuration rules.
- `OFFLINE_MISS_EXCLUDED_REQUEST`: Offline mode, request excluded and no cache found.
- `MISS_UNSTORABLE`: Response not cacheable (e.g., `Cache-Control: no-store`).
- `MISS_EXCLUDED_{REASON}`: Response validation failed; data fetched but not cached.
**Common `{REASON}` Suffixes:**
| Suffix | Meaning |
| :--- | :--- |
| `WAF_CHALLENGE` | Explicitly detected WAF challenge page (via built-in or custom rules). |
| `TOO_SHORT` | Content length is less than the configured `minLength`. |
| `BODY_MATCH_FAILED` | Content failed body keyword matching (negation hit or positive miss). |
| `STATUS_MISMATCH_{CODE}` | Status code not in the allowed cache list (e.g., `STATUS_MISMATCH_503`). |
| `HEADERS_MISMATCH` | Response headers do not meet configuration requirements. |
| `BODY_READ_ERROR` | Error occurred while reading response body for analysis. |
| `UNKNOWN` | Other unspecified validation failure. |
### Response Object Properties
To ensure consistency for downstream consumers, responses returned by `fetchWithCache` feature:
1. **URL Preservation**: The `response.url` property correctly reflects the original request URL, even when served from cache.
2. **Clone Compatibility**: Custom properties and headers are preserved when calling `response.clone()`.
### Debugging
This library uses the [debug](https://github.com/debug-js/debug) package. Enable internal tracing by setting the `DEBUG` environment variable:
```bash
# Trace all cache logic for fetch operations
DEBUG=@isdk/proxy:fetchWithCache node app.js
# Trace everything
DEBUG=@isdk/proxy:* node app.js
```
Logs cover configuration merging, fingerprinting, policy evaluation, SWR tasks, and response validation.
## License
MIT