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.1.1
to
0.1.2
+39
docs/functions/getSiteConfig.md
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / getSiteConfig
# Function: getSiteConfig()
> **getSiteConfig**(`urlString`, `proxyConfig`): [`SiteCacheConfig`](../interfaces/SiteCacheConfig.md)
Defined in: [utils/getSiteConfig.ts:17](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/utils/getSiteConfig.ts#L17)
根据 URL 获取对应的站点缓存配置
匹配逻辑:
1. 遍历 sites 中的所有 key。
2. 如果 key 是正则或 Glob 格式字符串,则对完整 URL 进行匹配。
3. 如果 key 是普通字符串,则作为 URL 前缀进行匹配。
4. 返回第一个匹配到的配置;若均未匹配,则返回 defaultConfig。
## Parameters
### urlString
`string`
请求的完整 URL
### proxyConfig
[`ProxyConfig`](../interfaces/ProxyConfig.md)
全局代理配置
## Returns
[`SiteCacheConfig`](../interfaces/SiteCacheConfig.md)
匹配到的站点配置
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / isGlob
# Function: isGlob()
> **isGlob**(`str`): `boolean`
Defined in: [utils/matcher.ts:7](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/utils/matcher.ts#L7)
判断一个模式是否为 Glob 模式
## Parameters
### str
`string`
## Returns
`boolean`
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / isMatch
# Function: isMatch()
> **isMatch**(`pattern`, `value`, `usePrefix`): `boolean`
Defined in: [utils/matcher.ts:25](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/utils/matcher.ts#L25)
通用匹配函数
逻辑优先级:
1. 如果 pattern 是数组,遵循:(匹配任一正向模式) 且 (不匹配任一负向模式)。
2. 如果 pattern 是 RegExp 对象,直接使用 regex.test(value)。
3. 如果 pattern 是 "/regex/flags" 格式的字符串,转为 RegExp 后使用 test。
4. 如果 pattern 是 Glob 字符串,使用 picomatch 进行匹配。
5. 否则,根据 usePrefix 参数进行前缀匹配或精确匹配。
## Parameters
### pattern
匹配模式 (RegExp 或 字符串 或 数组)
`string` | `RegExp` | (`string` \| `RegExp`)[]
### value
`string`
要匹配的值
### usePrefix
`boolean` = `false`
是否在普通字符串匹配时启用前缀匹配 (默认为 false,即精确匹配)
## Returns
`boolean`
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / BodyFilterConfig
# Interface: BodyFilterConfig
Defined in: [types.ts:13](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L13)
缓存键过滤配置
用于定义在生成缓存指纹时,哪些字段应该被包含或排除。
## Extends
- [`KeyFilterConfig`](KeyFilterConfig.md)
## Properties
### exclude?
> `optional` **exclude**: (`string` \| `RegExp`)[]
Defined in: [types.ts:10](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L10)
排除(黑名单):用于排除像 `timestamp`、`nonce` 等干扰缓存命中的动态字段。支持字符串、Glob 模式或正则表达式。
#### Inherited from
[`KeyFilterConfig`](KeyFilterConfig.md).[`exclude`](KeyFilterConfig.md#exclude)
***
### extract?
> `optional` **extract**: `string` \| `RegExp`
Defined in: [types.ts:18](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L18)
用于非 JSON (文本) Body 的提取正则表达式。
如果包含捕获组,则提取捕获组内容作为指纹;否则提取整个匹配部分。
***
### include?
> `optional` **include**: (`string` \| `RegExp`)[]
Defined in: [types.ts:8](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L8)
仅包含(白名单):如果设置,只有这些字段会参与 Key 的计算。支持字符串、Glob 模式或正则表达式。
#### Inherited from
[`KeyFilterConfig`](KeyFilterConfig.md).[`include`](KeyFilterConfig.md#include)
***
### maxLength?
> `optional` **maxLength**: `number`
Defined in: [types.ts:25](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L25)
用于正则匹配/提取 Body 时的最大长度限制,默认 1024 (1KB)
***
### sort?
> `optional` **sort**: `boolean`
Defined in: [types.ts:23](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L23)
是否对提取出的捕获组进行排序。
开启后可解决 Body 中参数顺序不一致导致的指纹失效问题。
[**@isdk/proxy**](../README.md)
***
[@isdk/proxy](../globals.md) / CacheRule
# Interface: CacheRule
Defined in: [types.ts:35](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L35)
精细化缓存匹配规则
用于在 `methods` 过滤的基础上,进一步限定哪些具体的请求路径或参数需要被缓存。
多个规则之间是 **OR (逻辑或)** 关系,即请求只需匹配其中一条规则即可。
在单个规则对象内部,各字段之间是 **AND (逻辑与)** 关系。
## Properties
### body?
> `optional` **body**: `string` \| `RegExp` \| (`string` \| `RegExp`)[]
Defined in: [types.ts:75](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L75)
Body 内容匹配。
仅当 Body 为文本或 JSON 时有效。
- 字符串: 支持 Glob 模式匹配。
- 正则表达式: 检查 Body 内容是否匹配。
- 数组: 支持传入多个模式。
***
### bodyType?
> `optional` **bodyType**: `"json"` \| `"text"` \| `"binary"`
Defined in: [types.ts:67](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L67)
强制指定 Body 类型。
如果不指定,则根据 `Content-Type` 自动判断。
***
### method?
> `optional` **method**: `string`
Defined in: [types.ts:40](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L40)
匹配的方法 (如 "POST")。
如果指定,则必须方法完全一致;如果不指定,则匹配所有 `methods` 中允许的方法。
***
### path?
> `optional` **path**: `string` \| `RegExp` \| (`string` \| `RegExp`)[]
Defined in: [types.ts:52](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L52)
路径匹配。
- 字符串: 默认进行 Glob 模式匹配(支持 `!` 否定),若非 Glob 且非正则字符串则退化为前缀匹配。
- 正则表达式: 检查 `url.pathname` 是否匹配。
- 数组: 支持传入多个模式(含否定模式),只要其中一个匹配即可。
***
### query?
> `optional` **query**: `Record`\<`string`, `string` \| `boolean` \| `RegExp`\>
Defined in: [types.ts:62](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L62)
Query 参数匹配规则。
- 键名: 支持字符串、Glob 或正则。
- 值:
- 字符串: 支持 Glob 模式匹配。
- 正则表达式: 检查参数值是否匹配。
- `true`: 要求该参数必须存在于 URL 中。
- `false`: 要求该参数必须 **不** 存在于 URL 中。
+272
-30

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

import { KeyvCacheableMemoryOptions } from '@cacheable/memory';
/**

@@ -9,20 +7,100 @@ * 缓存键过滤配置

interface KeyFilterConfig {
/** 仅包含(白名单):如果设置,只有这些字段会参与 Key 的计算 */
include?: string[];
/** 排除(黑名单):用于排除像 `timestamp`、`nonce` 等干扰缓存命中的动态字段 */
exclude?: string[];
/** 仅包含(白名单):如果设置,只有这些字段会参与 Key 的计算。支持字符串、Glob 模式或正则表达式。 */
include?: (string | RegExp)[];
/** 排除(黑名单):用于排除像 `timestamp`、`nonce` 等干扰缓存命中的动态字段。支持字符串、Glob 模式或正则表达式。 */
exclude?: (string | RegExp)[];
}
interface BodyFilterConfig extends KeyFilterConfig {
/**
* 用于非 JSON (文本) Body 的提取正则表达式。
* 如果包含捕获组,则提取捕获组内容作为指纹;否则提取整个匹配部分。
*/
extract?: string | RegExp;
/**
* 是否对提取出的捕获组进行排序。
* 开启后可解决 Body 中参数顺序不一致导致的指纹失效问题。
*/
sort?: boolean;
/** 用于正则匹配/提取 Body 时的最大长度限制,默认 1024 (1KB) */
maxLength?: number;
}
/**
* 精细化缓存匹配规则
*
* 用于在 `methods` 过滤的基础上,进一步限定哪些具体的请求路径或参数需要被缓存。
* 多个规则之间是 **OR (逻辑或)** 关系,即请求只需匹配其中一条规则即可。
* 在单个规则对象内部,各字段之间是 **AND (逻辑与)** 关系。
*/
interface CacheRule {
/**
* 匹配的方法 (如 "POST")。
* 如果指定,则必须方法完全一致;如果不指定,则匹配所有 `methods` 中允许的方法。
*/
method?: string;
/**
* 路径匹配。
* - 字符串: 默认进行 Glob 模式匹配(支持 `!` 否定),若非 Glob 且非正则字符串则退化为前缀匹配。
* - 正则表达式: 检查 `url.pathname` 是否匹配。
*/
/**
* 路径匹配。
* - 字符串: 默认进行 Glob 模式匹配(支持 `!` 否定),若非 Glob 且非正则字符串则退化为前缀匹配。
* - 正则表达式: 检查 `url.pathname` 是否匹配。
* - 数组: 支持传入多个模式(含否定模式),只要其中一个匹配即可。
*/
path?: string | RegExp | (string | RegExp)[];
/**
* Query 参数匹配规则。
* - 键名: 支持字符串、Glob 或正则。
* - 值:
* - 字符串: 支持 Glob 模式匹配。
* - 正则表达式: 检查参数值是否匹配。
* - `true`: 要求该参数必须存在于 URL 中。
* - `false`: 要求该参数必须 **不** 存在于 URL 中。
*/
query?: Record<string, string | boolean | RegExp>;
/**
* 强制指定 Body 类型。
* 如果不指定,则根据 `Content-Type` 自动判断。
*/
bodyType?: 'json' | 'text' | 'binary';
/**
* Body 内容匹配。
* 仅当 Body 为文本或 JSON 时有效。
* - 字符串: 支持 Glob 模式匹配。
* - 正则表达式: 检查 Body 内容是否匹配。
* - 数组: 支持传入多个模式。
*/
body?: string | RegExp | (string | RegExp)[];
}
/**
* 站点级缓存配置
*/
interface SiteCacheConfig {
/** Query 参数过滤配置 */
/**
* 允许缓存的 HTTP 方法列表。
* 默认值: ['GET', 'HEAD']。
* 若要缓存 POST/PUT,必须在此显式添加,并确保后端响应满足缓存条件(或开启 `forceCache`)。
*/
methods?: string[];
/**
* 精细化缓存规则列表。
* 如果配置了此项,请求必须匹配其中至少一条规则才会被允许进入缓存流程。
* 适用于只希望缓存特定 API 接口的场景。
*/
cacheRules?: CacheRule[];
/** Query 参数过滤配置:决定哪些查询参数参与缓存指纹 (Cache Key) 的计算 */
query?: KeyFilterConfig;
/** 请求头过滤配置 */
/** 请求头过滤配置:决定哪些 Header 参与缓存指纹计算 */
headers?: KeyFilterConfig;
/** Cookie 过滤配置 */
/** Cookie 过滤配置:决定哪些 Cookie 字段参与缓存指纹计算 */
cookies?: KeyFilterConfig;
/** 当后端请求失败且存在旧缓存时,是否强制返回旧缓存(容错机制) */
/**
* 请求体过滤配置 (仅限 JSON 类型)。
* 当方法为 POST/PUT/PATCH 且为 JSON 格式时,用于从 Body 中提取特定字段参与指纹计算。
*/
body?: BodyFilterConfig;
/** 容错机制:当后端请求失败(网络错误或 5xx)且存在旧缓存时,是否强制返回旧缓存 */
staleIfError?: boolean;
/** 是否强制缓存一切响应(无视 no-store 等不缓存指令),用于极端的离线可用容错场景 */
/** 强制缓存:是否忽略 `Cache-Control: no-store` 等指令强制入库。 */
forceCache?: boolean;

@@ -79,7 +157,24 @@ }

maxMemorySize?: number;
/** 透传给 L1 (Memory) 的高级配置 */
memoryOptions?: Partial<KeyvCacheableMemoryOptions>;
/** 内存缓存总大小阈值(字节)。默认 100MB。超过此值将清空内存缓存。 */
maxTotalMemorySize?: number;
/** 透传给 L1 (Memory) 的高级配置 (secondary-cache LRUCache options) */
memoryOptions?: {
capacity?: number;
expires?: number;
cleanInterval?: number;
[key: string]: any;
};
}
/**
* 智能混合缓存类 (Hybrid Cache)
* 智能混合缓存类 (Hybrid Multi-tier Cache)
*
* 该类实现了 L1 (内存) 和 L2 (磁盘) 的双层混合存储架构,旨在提供高性能且大容量的缓存能力。
*
* ### 核心特性:
* - **双层架构**: L1 使用 LRU 内存缓存(基于 `secondary-cache` 的 LRUCache),L2 使用持久化磁盘缓存(基于 `cacache`)。
* - **大小感知存储**: 自动识别响应体大小。小于阈值的文件同时存于内存和磁盘;超过阈值的文件仅存于磁盘,但其元数据仍保留在内存中。
* - **元数据驻留 (Meta-Residency)**: 无论 Body 多大,Headers、Status、Policy 等信息始终优先从内存读取,确保缓存判定性能。
* - **流式支持**: 支持通过 `setStream` 和 `getStream` 直接操作大数据流,防止 OOM。
* - **一致性保障**: 在并发写入时自动清理内存,确保后续读取不会拿到被污染的旧数据。
* - **内存限制**: 通过 `maxTotalMemorySize` 控制 L1 缓存的总内存占用。
*/

@@ -93,7 +188,21 @@ declare class SmartCache {

* 获取缓存条目
* 如果是小文件,返回带 Buffer 的 Entry;如果是大文件,返回带 ReadStream 的 Entry。
*
* 逻辑:
* 1. 首先尝试从 L1 内存获取。
* 2. 如果内存中有 Body,直接返回(Buffer 类型)。
* 3. 如果内存中只有 Meta(大文件),则从 L2 磁盘创建并返回 ReadStream。
* 4. 如果内存完全未命中,从磁盘 L2 检索,并根据大小决定是否回填 L1。
*
* @param key - 缓存指纹键
* @returns 完整的缓存条目(带 Buffer 或 Stream 的 Body),未命中返回 null
*/
get(key: string): Promise<CacheEntry | null>;
/**
* 写入缓存
* 写入缓存条目 (原子写入)
*
* 适用于已知长度的小型数据块。该操作会同时写入磁盘并回填内存(如果大小未超标)。
*
* @param key - 缓存指纹键
* @param body - 响应体数据 Buffer
* @param metadata - 响应元数据(不含 size,由本方法自动计算)
*/

@@ -105,3 +214,23 @@ set(key: string, body: Buffer, metadata: Omit<CacheMetadata, 'size'>): Promise<void>;

private saveToMemory;
/**
* 获取磁盘读取流
*
* 允许直接从 L2 磁盘层以流的形式读取数据,适用于大文件代理。
*
* @param key - 缓存指纹键
* @returns Node.js 可读流
*/
getStream(key: string): NodeJS.ReadableStream;
/**
* 获取磁盘写入流 (流式缓存)
*
* 该方法用于支持真正的流式代理。它会执行以下一致性操作:
* 1. 立即清除 L1 内存中的对应键,防止读到旧数据。
* 2. 返回一个可写流,数据将直接流入磁盘。
* 3. **一致性修复**: 在流写入完成(finish)时再次清理内存,防止写入期间的并发读取将旧数据再次回填进内存。
*
* @param key - 缓存指纹键
* @param metadata - 响应元数据
* @returns Node.js 可写流
*/
setStream(key: string, metadata: Omit<CacheMetadata, 'size'>): NodeJS.WritableStream;

@@ -113,5 +242,32 @@ delete(key: string): Promise<void>;

/**
* 根据 Request 和配置生成唯一的缓存键
* 根据 Request 对象和站点配置生成唯一的缓存指纹 (异步)
*
* 该函数是缓存系统的核心组件,用于将复杂的 HTTP 请求对象转换为唯一的 SHA-256 字符串。
* 它实现了高度可定制的提取逻辑,允许通过配置排除掉请求中不稳定的因素(如时间戳、Nonce 等)。
*
* ### 生成指纹包含的要素:
* 1. **Method**: 请求方法(统一转为大写)。
* 2. **Host & Path**: 请求的域名和路径。
* 3. **Query Params**: URL 查询参数,受 `config.query` 过滤影响。
* 4. **Headers**: 请求头信息,受 `config.headers` 过滤影响。默认排除 `cookie` 头。
* 5. **Cookies**: 特别提取的 Cookie 字段,受 `config.cookies` 过滤影响。
* 6. **Request Body**:
* - 对于 `POST`, `PUT`, `PATCH` 请求,会自动尝试读取 Body。
* - **JSON 类型**: 如果 `Content-Type` 包含 `application/json`,则解析为对象并应用 `config.body` 过滤。
* - **非 JSON/流类型**: 回退到对原始 Body 字节流进行 SHA-256 哈希计算。
* - **安全性**: 使用 `req.clone()` 读取 Body,确保不影响后续真实的 Fetch 请求流消费。
*
* @param req - 原始 Web 标准 Request 对象。
* @param config - 站点级缓存配置,决定了哪些字段参与指纹计算。
* @returns 返回一个 64 位十六进制的 SHA-256 哈希字符串作为缓存键。
*
* @example
* ```typescript
* const cacheKey = await generateCacheKey(request, {
* query: { exclude: ['timestamp'] },
* body: { include: ['id', 'action'] }
* });
* ```
*/
declare const generateCacheKey: (req: Request, config: SiteCacheConfig) => string;
declare function generateCacheKey(req: Request, config: SiteCacheConfig): Promise<string>;

@@ -150,2 +306,4 @@ /**

* 实现了基于流的混合缓存代理核心逻辑,主要机制包括:
* - **多方法支持与过滤**:支持通过 `allowedMethods` 配置可缓存的方法(如 POST, PUT),并通过 `cacheRules` 进行精细化的路径与参数匹配拦截。
* - **异步 Request Body 处理**:当缓存 POST/PUT 请求时,会自动读取 Body 并计算唯一指纹(支持 JSON 字段过滤)。
* - **大文件流式处理**:底层完全通过 Streams 实现,代理大文件时自动写入磁盘且防 OOM。

@@ -157,2 +315,7 @@ * - **SWR (Stale-While-Revalidate)**:后台静默更新机制。

* 并且会在响应头中自动注入 `x-proxy-cache` 标明缓存命中状态 (`HIT`, `STALE`, `MISS`, `STALE_IF_ERROR`)。
*
* @param request - 原始 Web 标准 Request 对象
* @param fetcher - 实际执行网络请求的函数
* @param options - 缓存配置选项
* @returns 带有缓存标识头和流式 Body 的 Response 对象
*/

@@ -195,3 +358,3 @@ declare function fetchWithCache(request: Request, fetcher: (req: Request) => Promise<Response>, options: FetchWithCacheOptions): Promise<Response>;

* 此函数主要用于生成缓存指纹。它会:
* 1. 根据 `config` (include/exclude) 过滤键。
* 1. 根据 `config` (include/exclude) 过滤键,调用 `isAllowed` 判断每个键是否允许。
* 2. 对键进行排序以保证指纹的一致性。

@@ -201,7 +364,30 @@ * 3. 将所有键转换为小写。

*
* **关于 `defaultAllowed` 参数**:
* - 只有当没有配置 `include` 和 `exclude` 时,`defaultAllowed` 才会生效。
* - 如果配置了 `include`(即使为空数组),`defaultAllowed` 也不会生效。
* - 详见 `isAllowed` 函数的优先级逻辑。
*
* @param source 原始数据对象 (如 QueryParams, Headers, Cookies)
* @param config 过滤配置 (白名单或黑名单)
* @returns 标准化后的数据 Map,键为小写,值为字符串数组
* @param config 过滤配置,支持 `include`(白名单)和 `exclude`(黑名单)
* @param defaultAllowed 当没有配置时的默认值(默认 `false`,即不提取任何键)
* @returns 标准化后的数据 Map,键为小写,值为排序后的字符串数组
*
* @example
* ```typescript
* const headers = { 'Content-Type': 'application/json', 'X-Request-Id': '123' };
*
* // 默认不提取任何键
* extractData(headers); // {}
*
* // 提取所有键
* extractData(headers, undefined, true); // { 'content-type': ['application/json'], 'x-request-id': ['123'] }
*
* // 白名单
* extractData(headers, { include: ['content-type'] }); // { 'content-type': ['application/json'] }
*
* // 黑名单(需要 include 或 defaultAllowed)
* extractData(headers, { include: ['*'], exclude: ['x-request-id'] }, true); // { 'content-type': ['application/json'] }
* ```
*/
declare const extractData: (source: Record<string, any>, config?: KeyFilterConfig) => Record<string, string[]>;
declare const extractData: (source: Record<string, any>, config?: KeyFilterConfig, defaultAllowed?: boolean) => Record<string, string[]>;

@@ -211,13 +397,69 @@ /**

*
* 优先级逻辑:
* 1. 如果配置了 `include` (白名单),则只有存在于 `include` 中的键才会被允许。
* 2. 否则,如果配置了 `exclude` (黑名单),则存在于 `exclude` 中的键将被拒绝。
* 3. 如果都没有配置,默认允许所有键。
* **优先级逻辑**:
* 1. `exclude` 命中 → 返回 `false`(优先级最高,会覆盖前面的结果)
* 2. `include` 存在且命中 → 返回 `true`
* 3. `include` 存在但不命中 → 返回 `false`
* 4. 都没有配置 → 使用 `defaultAllowed`(未传则返回 `undefined`)
*
* **注意**:`include` 和 `exclude` 可以同时配置,此时 `exclude` 优先级更高。
*
* @param key 要检查的键名
* @param config 过滤配置
* @returns 是否允许
* @param config 过滤配置,支持 `include`(白名单)和 `exclude`(黑名单)
* @param defaultAllowed 当没有配置或配置未命中时的默认值(可选)
* @returns 是否允许。返回 `boolean` 或 `undefined`(当没有配置且未传 defaultAllowed 时)
*
* @example
* ```typescript
* // 无配置
* isAllowed('key'); // undefined
*
* // 白名单
* isAllowed('id', { include: ['id', 'name'] }); // true
* isAllowed('email', { include: ['id', 'name'] }); // false
*
* // 黑名单
* isAllowed('password', { exclude: ['password'] }); // false
* isAllowed('name', { exclude: ['password'] }); // undefined
*
* // 设置默认值
* isAllowed('name', { exclude: ['password'] }, true); // true
* ```
*/
declare function isAllowed(key: string, config?: KeyFilterConfig): boolean;
declare function isAllowed(key: string, config?: KeyFilterConfig, defaultAllowed?: boolean): boolean;
export { type CacheEntry, type CacheMetadata, type FetchWithCacheContext, type FetchWithCacheOptions, type KeyFilterConfig, type ProxyConfig, type SiteCacheConfig, SmartCache, type SmartCacheOptions, createCachedFetch, createFetchWithCache, extractData, fetchWithCache, generateCacheKey, isAllowed };
/**
* 判断一个模式是否为 Glob 模式
*/
declare function isGlob(str: string): boolean;
/**
* 通用匹配函数
*
* 逻辑优先级:
* 1. 如果 pattern 是数组,遵循:(匹配任一正向模式) 且 (不匹配任一负向模式)。
* 2. 如果 pattern 是 RegExp 对象,直接使用 regex.test(value)。
* 3. 如果 pattern 是 "/regex/flags" 格式的字符串,转为 RegExp 后使用 test。
* 4. 如果 pattern 是 Glob 字符串,使用 picomatch 进行匹配。
* 5. 否则,根据 usePrefix 参数进行前缀匹配或精确匹配。
*
* @param pattern 匹配模式 (RegExp 或 字符串 或 数组)
* @param value 要匹配的值
* @param usePrefix 是否在普通字符串匹配时启用前缀匹配 (默认为 false,即精确匹配)
*/
declare function isMatch(pattern: string | RegExp | (string | RegExp)[], value: string, usePrefix?: boolean): boolean;
/**
* 根据 URL 获取对应的站点缓存配置
*
* 匹配逻辑:
* 1. 遍历 sites 中的所有 key。
* 2. 如果 key 是正则或 Glob 格式字符串,则对完整 URL 进行匹配。
* 3. 如果 key 是普通字符串,则作为 URL 前缀进行匹配。
* 4. 返回第一个匹配到的配置;若均未匹配,则返回 defaultConfig。
*
* @param urlString 请求的完整 URL
* @param proxyConfig 全局代理配置
* @returns 匹配到的站点配置
*/
declare function getSiteConfig(urlString: string, proxyConfig: ProxyConfig): SiteCacheConfig;
export { type BodyFilterConfig, type CacheEntry, type CacheMetadata, type CacheRule, type FetchWithCacheContext, type FetchWithCacheOptions, type KeyFilterConfig, type ProxyConfig, type SiteCacheConfig, SmartCache, type SmartCacheOptions, createCachedFetch, createFetchWithCache, extractData, fetchWithCache, generateCacheKey, getSiteConfig, isAllowed, isGlob, isMatch };

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

import { KeyvCacheableMemoryOptions } from '@cacheable/memory';
/**

@@ -9,20 +7,100 @@ * 缓存键过滤配置

interface KeyFilterConfig {
/** 仅包含(白名单):如果设置,只有这些字段会参与 Key 的计算 */
include?: string[];
/** 排除(黑名单):用于排除像 `timestamp`、`nonce` 等干扰缓存命中的动态字段 */
exclude?: string[];
/** 仅包含(白名单):如果设置,只有这些字段会参与 Key 的计算。支持字符串、Glob 模式或正则表达式。 */
include?: (string | RegExp)[];
/** 排除(黑名单):用于排除像 `timestamp`、`nonce` 等干扰缓存命中的动态字段。支持字符串、Glob 模式或正则表达式。 */
exclude?: (string | RegExp)[];
}
interface BodyFilterConfig extends KeyFilterConfig {
/**
* 用于非 JSON (文本) Body 的提取正则表达式。
* 如果包含捕获组,则提取捕获组内容作为指纹;否则提取整个匹配部分。
*/
extract?: string | RegExp;
/**
* 是否对提取出的捕获组进行排序。
* 开启后可解决 Body 中参数顺序不一致导致的指纹失效问题。
*/
sort?: boolean;
/** 用于正则匹配/提取 Body 时的最大长度限制,默认 1024 (1KB) */
maxLength?: number;
}
/**
* 精细化缓存匹配规则
*
* 用于在 `methods` 过滤的基础上,进一步限定哪些具体的请求路径或参数需要被缓存。
* 多个规则之间是 **OR (逻辑或)** 关系,即请求只需匹配其中一条规则即可。
* 在单个规则对象内部,各字段之间是 **AND (逻辑与)** 关系。
*/
interface CacheRule {
/**
* 匹配的方法 (如 "POST")。
* 如果指定,则必须方法完全一致;如果不指定,则匹配所有 `methods` 中允许的方法。
*/
method?: string;
/**
* 路径匹配。
* - 字符串: 默认进行 Glob 模式匹配(支持 `!` 否定),若非 Glob 且非正则字符串则退化为前缀匹配。
* - 正则表达式: 检查 `url.pathname` 是否匹配。
*/
/**
* 路径匹配。
* - 字符串: 默认进行 Glob 模式匹配(支持 `!` 否定),若非 Glob 且非正则字符串则退化为前缀匹配。
* - 正则表达式: 检查 `url.pathname` 是否匹配。
* - 数组: 支持传入多个模式(含否定模式),只要其中一个匹配即可。
*/
path?: string | RegExp | (string | RegExp)[];
/**
* Query 参数匹配规则。
* - 键名: 支持字符串、Glob 或正则。
* - 值:
* - 字符串: 支持 Glob 模式匹配。
* - 正则表达式: 检查参数值是否匹配。
* - `true`: 要求该参数必须存在于 URL 中。
* - `false`: 要求该参数必须 **不** 存在于 URL 中。
*/
query?: Record<string, string | boolean | RegExp>;
/**
* 强制指定 Body 类型。
* 如果不指定,则根据 `Content-Type` 自动判断。
*/
bodyType?: 'json' | 'text' | 'binary';
/**
* Body 内容匹配。
* 仅当 Body 为文本或 JSON 时有效。
* - 字符串: 支持 Glob 模式匹配。
* - 正则表达式: 检查 Body 内容是否匹配。
* - 数组: 支持传入多个模式。
*/
body?: string | RegExp | (string | RegExp)[];
}
/**
* 站点级缓存配置
*/
interface SiteCacheConfig {
/** Query 参数过滤配置 */
/**
* 允许缓存的 HTTP 方法列表。
* 默认值: ['GET', 'HEAD']。
* 若要缓存 POST/PUT,必须在此显式添加,并确保后端响应满足缓存条件(或开启 `forceCache`)。
*/
methods?: string[];
/**
* 精细化缓存规则列表。
* 如果配置了此项,请求必须匹配其中至少一条规则才会被允许进入缓存流程。
* 适用于只希望缓存特定 API 接口的场景。
*/
cacheRules?: CacheRule[];
/** Query 参数过滤配置:决定哪些查询参数参与缓存指纹 (Cache Key) 的计算 */
query?: KeyFilterConfig;
/** 请求头过滤配置 */
/** 请求头过滤配置:决定哪些 Header 参与缓存指纹计算 */
headers?: KeyFilterConfig;
/** Cookie 过滤配置 */
/** Cookie 过滤配置:决定哪些 Cookie 字段参与缓存指纹计算 */
cookies?: KeyFilterConfig;
/** 当后端请求失败且存在旧缓存时,是否强制返回旧缓存(容错机制) */
/**
* 请求体过滤配置 (仅限 JSON 类型)。
* 当方法为 POST/PUT/PATCH 且为 JSON 格式时,用于从 Body 中提取特定字段参与指纹计算。
*/
body?: BodyFilterConfig;
/** 容错机制:当后端请求失败(网络错误或 5xx)且存在旧缓存时,是否强制返回旧缓存 */
staleIfError?: boolean;
/** 是否强制缓存一切响应(无视 no-store 等不缓存指令),用于极端的离线可用容错场景 */
/** 强制缓存:是否忽略 `Cache-Control: no-store` 等指令强制入库。 */
forceCache?: boolean;

@@ -79,7 +157,24 @@ }

maxMemorySize?: number;
/** 透传给 L1 (Memory) 的高级配置 */
memoryOptions?: Partial<KeyvCacheableMemoryOptions>;
/** 内存缓存总大小阈值(字节)。默认 100MB。超过此值将清空内存缓存。 */
maxTotalMemorySize?: number;
/** 透传给 L1 (Memory) 的高级配置 (secondary-cache LRUCache options) */
memoryOptions?: {
capacity?: number;
expires?: number;
cleanInterval?: number;
[key: string]: any;
};
}
/**
* 智能混合缓存类 (Hybrid Cache)
* 智能混合缓存类 (Hybrid Multi-tier Cache)
*
* 该类实现了 L1 (内存) 和 L2 (磁盘) 的双层混合存储架构,旨在提供高性能且大容量的缓存能力。
*
* ### 核心特性:
* - **双层架构**: L1 使用 LRU 内存缓存(基于 `secondary-cache` 的 LRUCache),L2 使用持久化磁盘缓存(基于 `cacache`)。
* - **大小感知存储**: 自动识别响应体大小。小于阈值的文件同时存于内存和磁盘;超过阈值的文件仅存于磁盘,但其元数据仍保留在内存中。
* - **元数据驻留 (Meta-Residency)**: 无论 Body 多大,Headers、Status、Policy 等信息始终优先从内存读取,确保缓存判定性能。
* - **流式支持**: 支持通过 `setStream` 和 `getStream` 直接操作大数据流,防止 OOM。
* - **一致性保障**: 在并发写入时自动清理内存,确保后续读取不会拿到被污染的旧数据。
* - **内存限制**: 通过 `maxTotalMemorySize` 控制 L1 缓存的总内存占用。
*/

@@ -93,7 +188,21 @@ declare class SmartCache {

* 获取缓存条目
* 如果是小文件,返回带 Buffer 的 Entry;如果是大文件,返回带 ReadStream 的 Entry。
*
* 逻辑:
* 1. 首先尝试从 L1 内存获取。
* 2. 如果内存中有 Body,直接返回(Buffer 类型)。
* 3. 如果内存中只有 Meta(大文件),则从 L2 磁盘创建并返回 ReadStream。
* 4. 如果内存完全未命中,从磁盘 L2 检索,并根据大小决定是否回填 L1。
*
* @param key - 缓存指纹键
* @returns 完整的缓存条目(带 Buffer 或 Stream 的 Body),未命中返回 null
*/
get(key: string): Promise<CacheEntry | null>;
/**
* 写入缓存
* 写入缓存条目 (原子写入)
*
* 适用于已知长度的小型数据块。该操作会同时写入磁盘并回填内存(如果大小未超标)。
*
* @param key - 缓存指纹键
* @param body - 响应体数据 Buffer
* @param metadata - 响应元数据(不含 size,由本方法自动计算)
*/

@@ -105,3 +214,23 @@ set(key: string, body: Buffer, metadata: Omit<CacheMetadata, 'size'>): Promise<void>;

private saveToMemory;
/**
* 获取磁盘读取流
*
* 允许直接从 L2 磁盘层以流的形式读取数据,适用于大文件代理。
*
* @param key - 缓存指纹键
* @returns Node.js 可读流
*/
getStream(key: string): NodeJS.ReadableStream;
/**
* 获取磁盘写入流 (流式缓存)
*
* 该方法用于支持真正的流式代理。它会执行以下一致性操作:
* 1. 立即清除 L1 内存中的对应键,防止读到旧数据。
* 2. 返回一个可写流,数据将直接流入磁盘。
* 3. **一致性修复**: 在流写入完成(finish)时再次清理内存,防止写入期间的并发读取将旧数据再次回填进内存。
*
* @param key - 缓存指纹键
* @param metadata - 响应元数据
* @returns Node.js 可写流
*/
setStream(key: string, metadata: Omit<CacheMetadata, 'size'>): NodeJS.WritableStream;

@@ -113,5 +242,32 @@ delete(key: string): Promise<void>;

/**
* 根据 Request 和配置生成唯一的缓存键
* 根据 Request 对象和站点配置生成唯一的缓存指纹 (异步)
*
* 该函数是缓存系统的核心组件,用于将复杂的 HTTP 请求对象转换为唯一的 SHA-256 字符串。
* 它实现了高度可定制的提取逻辑,允许通过配置排除掉请求中不稳定的因素(如时间戳、Nonce 等)。
*
* ### 生成指纹包含的要素:
* 1. **Method**: 请求方法(统一转为大写)。
* 2. **Host & Path**: 请求的域名和路径。
* 3. **Query Params**: URL 查询参数,受 `config.query` 过滤影响。
* 4. **Headers**: 请求头信息,受 `config.headers` 过滤影响。默认排除 `cookie` 头。
* 5. **Cookies**: 特别提取的 Cookie 字段,受 `config.cookies` 过滤影响。
* 6. **Request Body**:
* - 对于 `POST`, `PUT`, `PATCH` 请求,会自动尝试读取 Body。
* - **JSON 类型**: 如果 `Content-Type` 包含 `application/json`,则解析为对象并应用 `config.body` 过滤。
* - **非 JSON/流类型**: 回退到对原始 Body 字节流进行 SHA-256 哈希计算。
* - **安全性**: 使用 `req.clone()` 读取 Body,确保不影响后续真实的 Fetch 请求流消费。
*
* @param req - 原始 Web 标准 Request 对象。
* @param config - 站点级缓存配置,决定了哪些字段参与指纹计算。
* @returns 返回一个 64 位十六进制的 SHA-256 哈希字符串作为缓存键。
*
* @example
* ```typescript
* const cacheKey = await generateCacheKey(request, {
* query: { exclude: ['timestamp'] },
* body: { include: ['id', 'action'] }
* });
* ```
*/
declare const generateCacheKey: (req: Request, config: SiteCacheConfig) => string;
declare function generateCacheKey(req: Request, config: SiteCacheConfig): Promise<string>;

@@ -150,2 +306,4 @@ /**

* 实现了基于流的混合缓存代理核心逻辑,主要机制包括:
* - **多方法支持与过滤**:支持通过 `allowedMethods` 配置可缓存的方法(如 POST, PUT),并通过 `cacheRules` 进行精细化的路径与参数匹配拦截。
* - **异步 Request Body 处理**:当缓存 POST/PUT 请求时,会自动读取 Body 并计算唯一指纹(支持 JSON 字段过滤)。
* - **大文件流式处理**:底层完全通过 Streams 实现,代理大文件时自动写入磁盘且防 OOM。

@@ -157,2 +315,7 @@ * - **SWR (Stale-While-Revalidate)**:后台静默更新机制。

* 并且会在响应头中自动注入 `x-proxy-cache` 标明缓存命中状态 (`HIT`, `STALE`, `MISS`, `STALE_IF_ERROR`)。
*
* @param request - 原始 Web 标准 Request 对象
* @param fetcher - 实际执行网络请求的函数
* @param options - 缓存配置选项
* @returns 带有缓存标识头和流式 Body 的 Response 对象
*/

@@ -195,3 +358,3 @@ declare function fetchWithCache(request: Request, fetcher: (req: Request) => Promise<Response>, options: FetchWithCacheOptions): Promise<Response>;

* 此函数主要用于生成缓存指纹。它会:
* 1. 根据 `config` (include/exclude) 过滤键。
* 1. 根据 `config` (include/exclude) 过滤键,调用 `isAllowed` 判断每个键是否允许。
* 2. 对键进行排序以保证指纹的一致性。

@@ -201,7 +364,30 @@ * 3. 将所有键转换为小写。

*
* **关于 `defaultAllowed` 参数**:
* - 只有当没有配置 `include` 和 `exclude` 时,`defaultAllowed` 才会生效。
* - 如果配置了 `include`(即使为空数组),`defaultAllowed` 也不会生效。
* - 详见 `isAllowed` 函数的优先级逻辑。
*
* @param source 原始数据对象 (如 QueryParams, Headers, Cookies)
* @param config 过滤配置 (白名单或黑名单)
* @returns 标准化后的数据 Map,键为小写,值为字符串数组
* @param config 过滤配置,支持 `include`(白名单)和 `exclude`(黑名单)
* @param defaultAllowed 当没有配置时的默认值(默认 `false`,即不提取任何键)
* @returns 标准化后的数据 Map,键为小写,值为排序后的字符串数组
*
* @example
* ```typescript
* const headers = { 'Content-Type': 'application/json', 'X-Request-Id': '123' };
*
* // 默认不提取任何键
* extractData(headers); // {}
*
* // 提取所有键
* extractData(headers, undefined, true); // { 'content-type': ['application/json'], 'x-request-id': ['123'] }
*
* // 白名单
* extractData(headers, { include: ['content-type'] }); // { 'content-type': ['application/json'] }
*
* // 黑名单(需要 include 或 defaultAllowed)
* extractData(headers, { include: ['*'], exclude: ['x-request-id'] }, true); // { 'content-type': ['application/json'] }
* ```
*/
declare const extractData: (source: Record<string, any>, config?: KeyFilterConfig) => Record<string, string[]>;
declare const extractData: (source: Record<string, any>, config?: KeyFilterConfig, defaultAllowed?: boolean) => Record<string, string[]>;

@@ -211,13 +397,69 @@ /**

*
* 优先级逻辑:
* 1. 如果配置了 `include` (白名单),则只有存在于 `include` 中的键才会被允许。
* 2. 否则,如果配置了 `exclude` (黑名单),则存在于 `exclude` 中的键将被拒绝。
* 3. 如果都没有配置,默认允许所有键。
* **优先级逻辑**:
* 1. `exclude` 命中 → 返回 `false`(优先级最高,会覆盖前面的结果)
* 2. `include` 存在且命中 → 返回 `true`
* 3. `include` 存在但不命中 → 返回 `false`
* 4. 都没有配置 → 使用 `defaultAllowed`(未传则返回 `undefined`)
*
* **注意**:`include` 和 `exclude` 可以同时配置,此时 `exclude` 优先级更高。
*
* @param key 要检查的键名
* @param config 过滤配置
* @returns 是否允许
* @param config 过滤配置,支持 `include`(白名单)和 `exclude`(黑名单)
* @param defaultAllowed 当没有配置或配置未命中时的默认值(可选)
* @returns 是否允许。返回 `boolean` 或 `undefined`(当没有配置且未传 defaultAllowed 时)
*
* @example
* ```typescript
* // 无配置
* isAllowed('key'); // undefined
*
* // 白名单
* isAllowed('id', { include: ['id', 'name'] }); // true
* isAllowed('email', { include: ['id', 'name'] }); // false
*
* // 黑名单
* isAllowed('password', { exclude: ['password'] }); // false
* isAllowed('name', { exclude: ['password'] }); // undefined
*
* // 设置默认值
* isAllowed('name', { exclude: ['password'] }, true); // true
* ```
*/
declare function isAllowed(key: string, config?: KeyFilterConfig): boolean;
declare function isAllowed(key: string, config?: KeyFilterConfig, defaultAllowed?: boolean): boolean;
export { type CacheEntry, type CacheMetadata, type FetchWithCacheContext, type FetchWithCacheOptions, type KeyFilterConfig, type ProxyConfig, type SiteCacheConfig, SmartCache, type SmartCacheOptions, createCachedFetch, createFetchWithCache, extractData, fetchWithCache, generateCacheKey, isAllowed };
/**
* 判断一个模式是否为 Glob 模式
*/
declare function isGlob(str: string): boolean;
/**
* 通用匹配函数
*
* 逻辑优先级:
* 1. 如果 pattern 是数组,遵循:(匹配任一正向模式) 且 (不匹配任一负向模式)。
* 2. 如果 pattern 是 RegExp 对象,直接使用 regex.test(value)。
* 3. 如果 pattern 是 "/regex/flags" 格式的字符串,转为 RegExp 后使用 test。
* 4. 如果 pattern 是 Glob 字符串,使用 picomatch 进行匹配。
* 5. 否则,根据 usePrefix 参数进行前缀匹配或精确匹配。
*
* @param pattern 匹配模式 (RegExp 或 字符串 或 数组)
* @param value 要匹配的值
* @param usePrefix 是否在普通字符串匹配时启用前缀匹配 (默认为 false,即精确匹配)
*/
declare function isMatch(pattern: string | RegExp | (string | RegExp)[], value: string, usePrefix?: boolean): boolean;
/**
* 根据 URL 获取对应的站点缓存配置
*
* 匹配逻辑:
* 1. 遍历 sites 中的所有 key。
* 2. 如果 key 是正则或 Glob 格式字符串,则对完整 URL 进行匹配。
* 3. 如果 key 是普通字符串,则作为 URL 前缀进行匹配。
* 4. 返回第一个匹配到的配置;若均未匹配,则返回 defaultConfig。
*
* @param urlString 请求的完整 URL
* @param proxyConfig 全局代理配置
* @returns 匹配到的站点配置
*/
declare function getSiteConfig(urlString: string, proxyConfig: ProxyConfig): SiteCacheConfig;
export { type BodyFilterConfig, type CacheEntry, type CacheMetadata, type CacheRule, type FetchWithCacheContext, type FetchWithCacheOptions, type KeyFilterConfig, type ProxyConfig, type SiteCacheConfig, SmartCache, type SmartCacheOptions, createCachedFetch, createFetchWithCache, extractData, fetchWithCache, generateCacheKey, getSiteConfig, isAllowed, isGlob, isMatch };
+1
-1

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

"use strict";var t,e=Object.create,r=Object.defineProperty,a=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,s=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,i=(t,e,s,i)=>{if(e&&"object"==typeof e||"function"==typeof e)for(let u of n(e))c.call(t,u)||u===s||r(t,u,{get:()=>e[u],enumerable:!(i=a(e,u))||i.enumerable});return t},u=(t,a,n)=>(n=null!=t?e(s(t)):{},i(!a&&t&&t.__esModule?n:r(n,"default",{value:t,enumerable:!0}),t)),o={};((t,e)=>{for(var a in e)r(t,a,{get:e[a],enumerable:!0})})(o,{SmartCache:()=>d,createCachedFetch:()=>v,createFetchWithCache:()=>q,extractData:()=>b,fetchWithCache:()=>R,generateCacheKey:()=>p,isAllowed:()=>m}),module.exports=(t=o,i(r({},"__esModule",{value:!0}),t));var h=require("@cacheable/memory"),f=u(require("cacache")),l=u(require("os")),w=u(require("path")),d=class{memory;storagePath;maxMemorySize;constructor(t={}){this.storagePath=t.storagePath||w.default.join(l.default.tmpdir(),"isdk-proxy-cache"),this.maxMemorySize=t.maxMemorySize??1048576,this.memory=new h.KeyvCacheableMemory({lruSize:500,ttl:3e5,...t.memoryOptions})}async get(t){const e=await this.memory.get(t);if(e){if(e.body)return e;if(0===e.size)return{...e,body:Buffer.alloc(0)};const r=f.default.get.stream(this.storagePath,t);return{...e,body:r}}try{const e=await f.default.get.info(this.storagePath,t);if(!e)return null;if(e.size<=this.maxMemorySize){const{data:e,metadata:r}=await f.default.get(this.storagePath,t),a=r,n={...a,body:e};return await this.saveToMemory(t,e,a),n}{const e=(await f.default.get.info(this.storagePath,t)).metadata;return await this.saveToMemory(t,null,e),{...e,body:f.default.get.stream(this.storagePath,t)}}}catch(t){return null}}async set(t,e,r){const a={...r,size:e.length};await f.default.put(this.storagePath,t,e,{metadata:a}),await this.saveToMemory(t,e,a)}async saveToMemory(t,e,r){if(e&&e.length>0&&e.length<=this.maxMemorySize)await this.memory.set(t,{...r,body:e});else{const{...e}=r;await this.memory.set(t,e)}}getStream(t){return f.default.get.stream(this.storagePath,t)}setStream(t,e){return this.memory.delete(t).catch(()=>{}),f.default.put.stream(this.storagePath,t,{metadata:e})}async delete(t){await this.memory.delete(t),await f.default.rm.entry(this.storagePath,t)}async clear(){await this.memory.clear(),await f.default.rm.all(this.storagePath)}},y=require("crypto");function m(t,e){return e?.include?e.include.includes(t):!e?.exclude||!e.exclude.includes(t)}var b=(t,e)=>{const r={};return Object.keys(t).filter(t=>m(t,e)).sort().forEach(e=>{const a=t[e];null!=a&&(r[e.toLowerCase()]=Array.isArray(a)?[...a].sort():[a])}),r},p=(t,e)=>{const r=new URL(t.url),a=t.headers.get("cookie")||"",n=Object.fromEntries(a.split(";").map(t=>t.trim()).filter(Boolean).map(t=>{const e=t.split("=");return[e[0],e.slice(1).join("=")]})),s={m:t.method.toUpperCase(),h:r.host,p:r.pathname,q:b(Object.fromEntries(r.searchParams),e.query),hd:b(Object.fromEntries(t.headers),{...e.headers,exclude:[...e.headers?.exclude||[],"cookie"]}),ck:b(n,e.cookies)};return(0,y.createHash)("sha256").update(JSON.stringify(s)).digest("hex")},O=require("stream"),j=require("stream/promises"),S=u(require("http-cache-semantics"));function T(t,e){const r=204===t.status||304===t.status||t.status<200?null:function(t){return t instanceof Buffer?new Uint8Array(t):t&&"function"==typeof t.pipe?O.Readable.toWeb(t):t}(t.body);return new Response(r,{status:t.status,headers:{...t.headers,"x-proxy-cache":e}})}async function x(t,e){let r,a;const n=new Promise((e,n)=>{r=()=>{t.activeCacheWrites.delete(t.cacheKey),e()},a=e=>{t.activeCacheWrites.delete(t.cacheKey),n(e)}});n.catch(()=>{}),t.activeCacheWrites.set(t.cacheKey,n);try{const e=await t.fetcher(t.request.clone()),n=new S.default({url:t.request.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)},{status:e.status,headers:Object.fromEntries(e.headers)}),s=new Headers(e.headers);if(s.set("x-proxy-cache","MISS"),!n.storable()&&!t.config.forceCache)return r(),new Response(e.body,{status:e.status,statusText:e.statusText,headers:s});const c={status:e.status,headers:Object.fromEntries(e.headers),policy:n.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(),new Response(null,{status:e.status,statusText:e.statusText,headers:s});const[i,u]=e.body.tee();return(0,j.pipeline)(O.Readable.fromWeb(u),t.cache.setStream(t.cacheKey,c)).then(r).catch(a),new Response(i,{status:e.status,statusText:e.statusText,headers:s})}catch(r){if(a(r),e&&t.config.staleIfError)return T(e,"STALE_IF_ERROR");throw r}}async function R(t,e,r){const a=function(t,e,r){const a=(r.generateKey||p)(t,r.config);return{...r,request:t,fetcher:e,cacheKey:a,activeCacheWrites:r.activeCacheWrites||new Map}}(t,e,r),n=await a.cache.get(a.cacheKey);if(n){const t=function(t,e){const r=S.default.fromObject(e.policy),a={url:t.request.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)};return r.satisfiesWithoutRevalidation(a)?"HIT":"STALE"}(a,n);if("HIT"===t)return T(n,"HIT");if("STALE"===t&&!1!==a.backgroundUpdate)return function(t,e){const r=x(t,e).catch(r=>(console.error(`[SWR Error] Background update failed for ${t.cacheKey}:`,r),T(e,"STALE_IF_ERROR")));t.onBackgroundUpdate?.(r)}(a,n),T(n,"STALE")}if(a.activeCacheWrites.has(a.cacheKey)){const t=await async function(t){const e=t.activeCacheWrites.get(t.cacheKey);if(!e)return null;try{await e;const r=await t.cache.get(t.cacheKey);if(r)return T(r,"HIT")}catch(e){console.warn(`[Cache Warning] Awaited active cache write failed for ${t.cacheKey}`)}return null}(a);if(t)return t}return x(a,n)}function q(t){return t||(t=new Map),async function(e,r,a){return R(e,r,{...a,activeCacheWrites:t})}}function v(t){const e=q(t.activeCacheWrites);return async function(r,a,n){return e(r,a,{...t,...n})}}
"use strict";var t,e=Object.create,n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getPrototypeOf,s=Object.prototype.hasOwnProperty,c=(t,e,a,c)=>{if(e&&"object"==typeof e||"function"==typeof e)for(let o of i(e))s.call(t,o)||o===a||n(t,o,{get:()=>e[o],enumerable:!(c=r(e,o))||c.enumerable});return t},o=(t,r,i)=>(i=null!=t?e(a(t)):{},c(!r&&t&&t.__esModule?i:n(i,"default",{value:t,enumerable:!0}),t)),u={};((t,e)=>{for(var r in e)n(t,r,{get:e[r],enumerable:!0})})(u,{SmartCache:()=>w,createCachedFetch:()=>I,createFetchWithCache:()=>H,extractData:()=>T,fetchWithCache:()=>C,generateCacheKey:()=>R,getSiteConfig:()=>g,isAllowed:()=>j,isGlob:()=>x,isMatch:()=>O}),module.exports=(t=u,c(n({},"__esModule",{value:!0}),t));var f=require("secondary-cache"),h=o(require("cacache")),l=o(require("os")),y=o(require("path")),w=class{memory;storagePath;maxMemorySize;constructor(t={}){this.storagePath=t.storagePath||y.default.join(l.default.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 f.LRUCache(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 n=h.default.get.stream(this.storagePath,t);return{...e,body:n}}try{const e=await h.default.get.info(this.storagePath,t);if(!e)return null;if(e.size<=this.maxMemorySize){const{data:e,metadata:n}=await h.default.get(this.storagePath,t),r=n,i={...r,body:e};return this.saveToMemory(t,e,r),i}{const e=(await h.default.get.info(this.storagePath,t)).metadata;return this.saveToMemory(t,null,e),{...e,body:h.default.get.stream(this.storagePath,t)}}}catch(t){return null}}async set(t,e,n){const r={...n,size:e.length};await h.default.put(this.storagePath,t,e,{metadata:r}),this.saveToMemory(t,e,r)}saveToMemory(t,e,n){if(e&&e.length>0&&e.length<=this.maxMemorySize)this.memory.set(t,{...n,body:e});else{const{...e}=n;this.memory.set(t,e)}}getStream(t){return h.default.get.stream(this.storagePath,t)}setStream(t,e){this.memory.del(t);const n=h.default.put.stream(this.storagePath,t,{metadata:e});return n.on("finish",()=>{this.memory.del(t)}),n}async delete(t){this.memory.del(t),await h.default.rm.entry(this.storagePath,t)}async clear(){this.memory.clear(),await h.default.rm.all(this.storagePath)}},d=require("crypto"),p=require("util-ex"),m=o(require("picomatch")),b=require("util-ex");function x(t){return/[!*?{}[\]()]/.test(t)}function O(t,e,n=!1){if(Array.isArray(t)&&t.length){const r=[],i=[];return t.forEach(t=>{"string"==typeof t&&t.startsWith("!")?i.push(t.slice(1)):r.push(t)}),!(i.length>0&&i.some(t=>O(t,e,n)))&&(0===r.length||r.some(t=>O(t,e,n)))}return t instanceof RegExp?t.test(e):"string"==typeof t&&((0,b.isRegExpStr)(t)?(0,b.toRegExp)(t).test(e):x(t)?(0,m.default)(t,{dot:!0})(e):n?e.startsWith(t):e===t)}function j(t,e,n){let r;return e?.include&&(r=O(e.include,t)),e?.exclude&&O(e.exclude,t)&&(r=!1),void 0===r&&(r=n),r}var T=(t,e,n)=>{const r={};return Object.keys(t).filter(t=>j(t,e,n)).sort().forEach(e=>{const n=t[e];null!=n&&(r[e.toLowerCase()]=Array.isArray(n)?[...n].sort():[n])}),r};function g(t,e){const{sites:n,default:r}=e;if(!n)return r;for(const[e,r]of Object.entries(n))if(O(e,t,!0))return r;return r}async function R(t,e){const n=new URL(t.url),r=t.headers.get("cookie")||"",i=Object.fromEntries(r.split(";").map(t=>t.trim()).filter(Boolean).map(t=>{const e=t.split("=");return[e[0],e.slice(1).join("=")]}));let a=null;const s=t.method.toUpperCase();if(["POST","PUT","PATCH"].includes(s))try{const n=t.headers.get("content-type")||"";if(n.includes("application/json")){const n=await t.clone().json();a=T(n,e.body,!0)}else if(e.body?.extract&&(n.includes("text/")||n.includes("application/xml")||n.includes("x-www-form-urlencoded"))){const n=e.body?.maxLength||1024,r=(await t.clone().text()).slice(0,n),i=e.body.extract,s="string"==typeof i&&(0,p.isRegExpStr)(i)?(0,p.toRegExp)(i):i instanceof RegExp?i:null;if(s){const t=r.match(s);if(t)if(t.length>1){const n=t.slice(1);e.body?.sort&&n.sort(),a=n.join(":")}else a=t[0]}else a=(0,d.createHash)("sha256").update(r).digest("hex")}else{const e=await t.clone().arrayBuffer();e.byteLength>0&&(a=(0,d.createHash)("sha256").update(new Uint8Array(e)).digest("hex"))}}catch(t){}const c={m:s,h:n.host,p:n.pathname,q:T(Object.fromEntries(n.searchParams),e.query,!0),hd:T(Object.fromEntries(t.headers),{...e.headers,exclude:[...e.headers?.exclude||[],"cookie"]}),ck:T(i,e.cookies)};return null!==a&&(c.b=a),(0,d.createHash)("sha256").update(JSON.stringify(c)).digest("hex")}var S=require("stream"),q=require("stream/promises"),A=o(require("http-cache-semantics"));function E(t,e){const n=204===t.status||304===t.status||t.status<200?null:function(t){return t instanceof Buffer?new Uint8Array(t):t&&"function"==typeof t.pipe?S.Readable.toWeb(t):t}(t.body);return new Response(n,{status:t.status,headers:{...t.headers,"x-proxy-cache":e}})}async function v(t,e){let n,r;const i=new Promise((e,i)=>{n=()=>{t.activeCacheWrites.delete(t.cacheKey),e()},r=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 A.default({url:t.request.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)},{status:e.status,headers:Object.fromEntries(e.headers)}),a=new Headers(e.headers);if(a.set("x-proxy-cache","MISS"),!i.storable()&&!t.config.forceCache)return n(),new Response(e.body,{status:e.status,statusText:e.statusText,headers:a});const s={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),s),n(),new Response(null,{status:e.status,statusText:e.statusText,headers:a});const[c,o]=e.body.tee();return(0,q.pipeline)(S.Readable.fromWeb(o),t.cache.setStream(t.cacheKey,s)).then(n).catch(r),new Response(c,{status:e.status,statusText:e.statusText,headers:a})}catch(n){if(r(n),e&&t.config.staleIfError)return E(e,"STALE_IF_ERROR");throw n}}async function C(t,e,n){if(!await async function(t,e){const n=t.method.toUpperCase();if(!(e.methods||["GET","HEAD"]).includes(n))return!1;if(e.cacheRules&&e.cacheRules.length>0){const r=new URL(t.url),i=r.searchParams;let a=null,s=!1;for(const o of e.cacheRules)if(await c(o,n,r,i,t))return!0;return!1;async function c(t,n,r,i,c){if(t.method&&t.method.toUpperCase()!==n)return!1;if(t.path&&!O(t.path,r.pathname,!0))return!1;if(t.query)for(const[e,n]of Object.entries(t.query)){const t=i.has(e),r=i.get(e)||"";if("boolean"==typeof n){if(n&&!t)return!1;if(!n&&t)return!1}else if(!O(n,r))return!1}if(t.bodyType||t.body){const n=c.headers.get("content-type")||"",r=n.includes("application/json")?"json":n.includes("text/")||n.includes("application/xml")||n.includes("x-www-form-urlencoded")?"text":"binary";if(t.bodyType&&t.bodyType!==r)return!1;if(t.body){if("binary"===r)return!1;if(!s){try{const t=e.body?.maxLength||1024,n=await c.clone().text();a=n.slice(0,t)}catch(t){a=""}s=!0}if(!a||!O(t.body,a))return!1}}return!0}}return!0}(t,n.config))return e(t);const r=await async function(t,e,n){const r=n.generateKey||R,i=await r(t,n.config);return{...n,request:t,fetcher:e,cacheKey:i,activeCacheWrites:n.activeCacheWrites||new Map}}(t,e,n),i=await r.cache.get(r.cacheKey);if(i){const t=function(t,e){const n=A.default.fromObject(e.policy),r={url:e.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)};return n.satisfiesWithoutRevalidation(r)?"HIT":"STALE"}(r,i);if("HIT"===t)return E(i,"HIT");if("STALE"===t&&!1!==r.backgroundUpdate)return function(t,e){if(t.activeCacheWrites.has(t.cacheKey))return;const n=v(t,e).catch(n=>(console.error(`[SWR Error] Background update failed for ${t.cacheKey}:`,n),E(e,"STALE_IF_ERROR")));t.onBackgroundUpdate?.(n)}(r,i),E(i,"STALE")}if(r.activeCacheWrites.has(r.cacheKey)){const t=await async function(t){const e=t.activeCacheWrites.get(t.cacheKey);if(!e)return null;await e;const n=await t.cache.get(t.cacheKey);return n?E(n,"HIT"):null}(r);if(t)return t}return v(r,i)}function H(t){return t||(t=new Map),async function(e,n,r){return C(e,n,{...r,activeCacheWrites:t})}}function I(t){const e=H(t.activeCacheWrites);return async function(n,r,i){return e(n,r,{...t,...i})}}

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

import{KeyvCacheableMemory as t}from"@cacheable/memory";import e from"cacache";import r from"os";import n from"path";var a=class{memory;storagePath;maxMemorySize;constructor(e={}){this.storagePath=e.storagePath||n.join(r.tmpdir(),"isdk-proxy-cache"),this.maxMemorySize=e.maxMemorySize??1048576,this.memory=new t({lruSize:500,ttl:3e5,...e.memoryOptions})}async get(t){const r=await this.memory.get(t);if(r){if(r.body)return r;if(0===r.size)return{...r,body:Buffer.alloc(0)};const n=e.get.stream(this.storagePath,t);return{...r,body:n}}try{const r=await e.get.info(this.storagePath,t);if(!r)return null;if(r.size<=this.maxMemorySize){const{data:r,metadata:n}=await e.get(this.storagePath,t),a=n,s={...a,body:r};return await this.saveToMemory(t,r,a),s}{const r=(await e.get.info(this.storagePath,t)).metadata;return await this.saveToMemory(t,null,r),{...r,body:e.get.stream(this.storagePath,t)}}}catch(t){return null}}async set(t,r,n){const a={...n,size:r.length};await e.put(this.storagePath,t,r,{metadata:a}),await this.saveToMemory(t,r,a)}async saveToMemory(t,e,r){if(e&&e.length>0&&e.length<=this.maxMemorySize)await this.memory.set(t,{...r,body:e});else{const{...e}=r;await this.memory.set(t,e)}}getStream(t){return e.get.stream(this.storagePath,t)}setStream(t,r){return this.memory.delete(t).catch(()=>{}),e.put.stream(this.storagePath,t,{metadata:r})}async delete(t){await this.memory.delete(t),await e.rm.entry(this.storagePath,t)}async clear(){await this.memory.clear(),await e.rm.all(this.storagePath)}};import{createHash as s}from"crypto";function i(t,e){return e?.include?e.include.includes(t):!e?.exclude||!e.exclude.includes(t)}var c=(t,e)=>{const r={};return Object.keys(t).filter(t=>i(t,e)).sort().forEach(e=>{const n=t[e];null!=n&&(r[e.toLowerCase()]=Array.isArray(n)?[...n].sort():[n])}),r},o=(t,e)=>{const r=new URL(t.url),n=t.headers.get("cookie")||"",a=Object.fromEntries(n.split(";").map(t=>t.trim()).filter(Boolean).map(t=>{const e=t.split("=");return[e[0],e.slice(1).join("=")]})),i={m:t.method.toUpperCase(),h:r.host,p:r.pathname,q:c(Object.fromEntries(r.searchParams),e.query),hd:c(Object.fromEntries(t.headers),{...e.headers,exclude:[...e.headers?.exclude||[],"cookie"]}),ck:c(a,e.cookies)};return s("sha256").update(JSON.stringify(i)).digest("hex")};import{Readable as u}from"stream";import{pipeline as h}from"stream/promises";import f from"http-cache-semantics";function m(t,e){const r=204===t.status||304===t.status||t.status<200?null:function(t){return t instanceof Buffer?new Uint8Array(t):t&&"function"==typeof t.pipe?u.toWeb(t):t}(t.body);return new Response(r,{status:t.status,headers:{...t.headers,"x-proxy-cache":e}})}async function w(t,e){let r,n;const a=new Promise((e,a)=>{r=()=>{t.activeCacheWrites.delete(t.cacheKey),e()},n=e=>{t.activeCacheWrites.delete(t.cacheKey),a(e)}});a.catch(()=>{}),t.activeCacheWrites.set(t.cacheKey,a);try{const e=await t.fetcher(t.request.clone()),a=new f({url:t.request.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)},{status:e.status,headers:Object.fromEntries(e.headers)}),s=new Headers(e.headers);if(s.set("x-proxy-cache","MISS"),!a.storable()&&!t.config.forceCache)return r(),new Response(e.body,{status:e.status,statusText:e.statusText,headers:s});const i={status:e.status,headers:Object.fromEntries(e.headers),policy:a.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),i),r(),new Response(null,{status:e.status,statusText:e.statusText,headers:s});const[c,o]=e.body.tee();return h(u.fromWeb(o),t.cache.setStream(t.cacheKey,i)).then(r).catch(n),new Response(c,{status:e.status,statusText:e.statusText,headers:s})}catch(r){if(n(r),e&&t.config.staleIfError)return m(e,"STALE_IF_ERROR");throw r}}async function l(t,e,r){const n=function(t,e,r){const n=(r.generateKey||o)(t,r.config);return{...r,request:t,fetcher:e,cacheKey:n,activeCacheWrites:r.activeCacheWrites||new Map}}(t,e,r),a=await n.cache.get(n.cacheKey);if(a){const t=function(t,e){const r=f.fromObject(e.policy),n={url:t.request.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)};return r.satisfiesWithoutRevalidation(n)?"HIT":"STALE"}(n,a);if("HIT"===t)return m(a,"HIT");if("STALE"===t&&!1!==n.backgroundUpdate)return function(t,e){const r=w(t,e).catch(r=>(console.error(`[SWR Error] Background update failed for ${t.cacheKey}:`,r),m(e,"STALE_IF_ERROR")));t.onBackgroundUpdate?.(r)}(n,a),m(a,"STALE")}if(n.activeCacheWrites.has(n.cacheKey)){const t=await async function(t){const e=t.activeCacheWrites.get(t.cacheKey);if(!e)return null;try{await e;const r=await t.cache.get(t.cacheKey);if(r)return m(r,"HIT")}catch(e){console.warn(`[Cache Warning] Awaited active cache write failed for ${t.cacheKey}`)}return null}(n);if(t)return t}return w(n,a)}function y(t){return t||(t=new Map),async function(e,r,n){return l(e,r,{...n,activeCacheWrites:t})}}function d(t){const e=y(t.activeCacheWrites);return async function(r,n,a){return e(r,n,{...t,...a})}}export{a as SmartCache,d as createCachedFetch,y as createFetchWithCache,c as extractData,l as fetchWithCache,o as generateCacheKey,i as isAllowed};
import{LRUCache as t}from"secondary-cache";import e from"cacache";import n from"os";import r from"path";var s=class{memory;storagePath;maxMemorySize;constructor(e={}){this.storagePath=e.storagePath||r.join(n.tmpdir(),"isdk-proxy-cache"),this.maxMemorySize=e.maxMemorySize??1048576;const s={capacity:0,expires:3e5,maxWeight:e.maxTotalMemorySize||104857600,weightOf:t=>{let e=0;return t.body&&Buffer.isBuffer(t.body)&&(e+=t.body.length),e+=512,e},...e.memoryOptions};this.memory=new t(s)}async get(t){const n=this.memory.get(t);if(n){if(n.body)return n;if(0===n.size)return{...n,body:Buffer.alloc(0)};const r=e.get.stream(this.storagePath,t);return{...n,body:r}}try{const n=await e.get.info(this.storagePath,t);if(!n)return null;if(n.size<=this.maxMemorySize){const{data:n,metadata:r}=await e.get(this.storagePath,t),s=r,i={...s,body:n};return this.saveToMemory(t,n,s),i}{const n=(await e.get.info(this.storagePath,t)).metadata;return this.saveToMemory(t,null,n),{...n,body:e.get.stream(this.storagePath,t)}}}catch(t){return null}}async set(t,n,r){const s={...r,size:n.length};await e.put(this.storagePath,t,n,{metadata:s}),this.saveToMemory(t,n,s)}saveToMemory(t,e,n){if(e&&e.length>0&&e.length<=this.maxMemorySize)this.memory.set(t,{...n,body:e});else{const{...e}=n;this.memory.set(t,e)}}getStream(t){return e.get.stream(this.storagePath,t)}setStream(t,n){this.memory.del(t);const r=e.put.stream(this.storagePath,t,{metadata:n});return r.on("finish",()=>{this.memory.del(t)}),r}async delete(t){this.memory.del(t),await e.rm.entry(this.storagePath,t)}async clear(){this.memory.clear(),await e.rm.all(this.storagePath)}};import{createHash as i}from"crypto";import{isRegExpStr as o,toRegExp as a}from"util-ex";import c from"picomatch";import{isRegExpStr as u,toRegExp as f}from"util-ex";function h(t){return/[!*?{}[\]()]/.test(t)}function l(t,e,n=!1){if(Array.isArray(t)&&t.length){const r=[],s=[];return t.forEach(t=>{"string"==typeof t&&t.startsWith("!")?s.push(t.slice(1)):r.push(t)}),!(s.length>0&&s.some(t=>l(t,e,n)))&&(0===r.length||r.some(t=>l(t,e,n)))}return t instanceof RegExp?t.test(e):"string"==typeof t&&(u(t)?f(t).test(e):h(t)?c(t,{dot:!0})(e):n?e.startsWith(t):e===t)}function p(t,e,n){let r;return e?.include&&(r=l(e.include,t)),e?.exclude&&l(e.exclude,t)&&(r=!1),void 0===r&&(r=n),r}var m=(t,e,n)=>{const r={};return Object.keys(t).filter(t=>p(t,e,n)).sort().forEach(e=>{const n=t[e];null!=n&&(r[e.toLowerCase()]=Array.isArray(n)?[...n].sort():[n])}),r};function y(t,e){const{sites:n,default:r}=e;if(!n)return r;for(const[e,r]of Object.entries(n))if(l(e,t,!0))return r;return r}async function w(t,e){const n=new URL(t.url),r=t.headers.get("cookie")||"",s=Object.fromEntries(r.split(";").map(t=>t.trim()).filter(Boolean).map(t=>{const e=t.split("=");return[e[0],e.slice(1).join("=")]}));let c=null;const u=t.method.toUpperCase();if(["POST","PUT","PATCH"].includes(u))try{const n=t.headers.get("content-type")||"";if(n.includes("application/json")){const n=await t.clone().json();c=m(n,e.body,!0)}else if(e.body?.extract&&(n.includes("text/")||n.includes("application/xml")||n.includes("x-www-form-urlencoded"))){const n=e.body?.maxLength||1024,r=(await t.clone().text()).slice(0,n),s=e.body.extract,u="string"==typeof s&&o(s)?a(s):s instanceof RegExp?s:null;if(u){const t=r.match(u);if(t)if(t.length>1){const n=t.slice(1);e.body?.sort&&n.sort(),c=n.join(":")}else c=t[0]}else c=i("sha256").update(r).digest("hex")}else{const e=await t.clone().arrayBuffer();e.byteLength>0&&(c=i("sha256").update(new Uint8Array(e)).digest("hex"))}}catch(t){}const f={m:u,h:n.host,p:n.pathname,q:m(Object.fromEntries(n.searchParams),e.query,!0),hd:m(Object.fromEntries(t.headers),{...e.headers,exclude:[...e.headers?.exclude||[],"cookie"]}),ck:m(s,e.cookies)};return null!==c&&(f.b=c),i("sha256").update(JSON.stringify(f)).digest("hex")}import{Readable as d}from"stream";import{pipeline as x}from"stream/promises";import b from"http-cache-semantics";function R(t,e){const n=204===t.status||304===t.status||t.status<200?null:function(t){return t instanceof Buffer?new Uint8Array(t):t&&"function"==typeof t.pipe?d.toWeb(t):t}(t.body);return new Response(n,{status:t.status,headers:{...t.headers,"x-proxy-cache":e}})}async function T(t,e){let n,r;const s=new Promise((e,s)=>{n=()=>{t.activeCacheWrites.delete(t.cacheKey),e()},r=e=>{t.activeCacheWrites.delete(t.cacheKey),s(e)}});s.catch(()=>{}),t.activeCacheWrites.set(t.cacheKey,s);try{const e=await t.fetcher(t.request.clone()),s=new b({url:t.request.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)},{status:e.status,headers:Object.fromEntries(e.headers)}),i=new Headers(e.headers);if(i.set("x-proxy-cache","MISS"),!s.storable()&&!t.config.forceCache)return n(),new Response(e.body,{status:e.status,statusText:e.statusText,headers:i});const o={status:e.status,headers:Object.fromEntries(e.headers),policy:s.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),o),n(),new Response(null,{status:e.status,statusText:e.statusText,headers:i});const[a,c]=e.body.tee();return x(d.fromWeb(c),t.cache.setStream(t.cacheKey,o)).then(n).catch(r),new Response(a,{status:e.status,statusText:e.statusText,headers:i})}catch(n){if(r(n),e&&t.config.staleIfError)return R(e,"STALE_IF_ERROR");throw n}}async function O(t,e,n){if(!await async function(t,e){const n=t.method.toUpperCase();if(!(e.methods||["GET","HEAD"]).includes(n))return!1;if(e.cacheRules&&e.cacheRules.length>0){const r=new URL(t.url),s=r.searchParams;let i=null,o=!1;for(const c of e.cacheRules)if(await a(c,n,r,s,t))return!0;return!1;async function a(t,n,r,s,a){if(t.method&&t.method.toUpperCase()!==n)return!1;if(t.path&&!l(t.path,r.pathname,!0))return!1;if(t.query)for(const[e,n]of Object.entries(t.query)){const t=s.has(e),r=s.get(e)||"";if("boolean"==typeof n){if(n&&!t)return!1;if(!n&&t)return!1}else if(!l(n,r))return!1}if(t.bodyType||t.body){const n=a.headers.get("content-type")||"",r=n.includes("application/json")?"json":n.includes("text/")||n.includes("application/xml")||n.includes("x-www-form-urlencoded")?"text":"binary";if(t.bodyType&&t.bodyType!==r)return!1;if(t.body){if("binary"===r)return!1;if(!o){try{const t=e.body?.maxLength||1024,n=await a.clone().text();i=n.slice(0,t)}catch(t){i=""}o=!0}if(!i||!l(t.body,i))return!1}}return!0}}return!0}(t,n.config))return e(t);const r=await async function(t,e,n){const r=n.generateKey||w,s=await r(t,n.config);return{...n,request:t,fetcher:e,cacheKey:s,activeCacheWrites:n.activeCacheWrites||new Map}}(t,e,n),s=await r.cache.get(r.cacheKey);if(s){const t=function(t,e){const n=b.fromObject(e.policy),r={url:e.url,method:t.request.method,headers:Object.fromEntries(t.request.headers)};return n.satisfiesWithoutRevalidation(r)?"HIT":"STALE"}(r,s);if("HIT"===t)return R(s,"HIT");if("STALE"===t&&!1!==r.backgroundUpdate)return function(t,e){if(t.activeCacheWrites.has(t.cacheKey))return;const n=T(t,e).catch(n=>(console.error(`[SWR Error] Background update failed for ${t.cacheKey}:`,n),R(e,"STALE_IF_ERROR")));t.onBackgroundUpdate?.(n)}(r,s),R(s,"STALE")}if(r.activeCacheWrites.has(r.cacheKey)){const t=await async function(t){const e=t.activeCacheWrites.get(t.cacheKey);if(!e)return null;await e;const n=await t.cache.get(t.cacheKey);return n?R(n,"HIT"):null}(r);if(t)return t}return T(r,s)}function E(t){return t||(t=new Map),async function(e,n,r){return O(e,n,{...r,activeCacheWrites:t})}}function S(t){const e=E(t.activeCacheWrites);return async function(n,r,s){return e(n,r,{...t,...s})}}export{s as SmartCache,S as createCachedFetch,E as createFetchWithCache,m as extractData,O as fetchWithCache,w as generateCacheKey,y as getSiteConfig,p as isAllowed,h as isGlob,l as isMatch};

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

Defined in: [core/SmartCache.ts:22](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L22)
Defined in: [core/SmartCache.ts:39](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L39)
智能混合缓存类 (Hybrid Cache)
智能混合缓存类 (Hybrid Multi-tier Cache)
该类实现了 L1 (内存) 和 L2 (磁盘) 的双层混合存储架构,旨在提供高性能且大容量的缓存能力。
### 核心特性:
- **双层架构**: L1 使用 LRU 内存缓存(基于 `secondary-cache` 的 LRUCache),L2 使用持久化磁盘缓存(基于 `cacache`)。
- **大小感知存储**: 自动识别响应体大小。小于阈值的文件同时存于内存和磁盘;超过阈值的文件仅存于磁盘,但其元数据仍保留在内存中。
- **元数据驻留 (Meta-Residency)**: 无论 Body 多大,Headers、Status、Policy 等信息始终优先从内存读取,确保缓存判定性能。
- **流式支持**: 支持通过 `setStream` 和 `getStream` 直接操作大数据流,防止 OOM。
- **一致性保障**: 在并发写入时自动清理内存,确保后续读取不会拿到被污染的旧数据。
- **内存限制**: 通过 `maxTotalMemorySize` 控制 L1 缓存的总内存占用。
## Constructors

@@ -20,3 +30,3 @@

Defined in: [core/SmartCache.ts:27](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L27)
Defined in: [core/SmartCache.ts:44](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L44)

@@ -39,3 +49,3 @@ #### Parameters

Defined in: [core/SmartCache.ts:122](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L122)
Defined in: [core/SmartCache.ts:193](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L193)

@@ -52,3 +62,3 @@ #### Returns

Defined in: [core/SmartCache.ts:117](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L117)
Defined in: [core/SmartCache.ts:188](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L188)

@@ -71,7 +81,12 @@ #### Parameters

Defined in: [core/SmartCache.ts:41](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L41)
Defined in: [core/SmartCache.ts:79](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L79)
获取缓存条目
如果是小文件,返回带 Buffer 的 Entry;如果是大文件,返回带 ReadStream 的 Entry。
逻辑:
1. 首先尝试从 L1 内存获取。
2. 如果内存中有 Body,直接返回(Buffer 类型)。
3. 如果内存中只有 Meta(大文件),则从 L2 磁盘创建并返回 ReadStream。
4. 如果内存完全未命中,从磁盘 L2 检索,并根据大小决定是否回填 L1。
#### Parameters

@@ -83,2 +98,4 @@

缓存指纹键
#### Returns

@@ -88,2 +105,4 @@

完整的缓存条目(带 Buffer 或 Stream 的 Body),未命中返回 null
***

@@ -95,4 +114,8 @@

Defined in: [core/SmartCache.ts:107](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L107)
Defined in: [core/SmartCache.ts:159](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L159)
获取磁盘读取流
允许直接从 L2 磁盘层以流的形式读取数据,适用于大文件代理。
#### Parameters

@@ -104,2 +127,4 @@

缓存指纹键
#### Returns

@@ -109,2 +134,4 @@

Node.js 可读流
***

@@ -116,6 +143,8 @@

Defined in: [core/SmartCache.ts:85](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L85)
Defined in: [core/SmartCache.ts:129](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L129)
写入缓存
写入缓存条目 (原子写入)
适用于已知长度的小型数据块。该操作会同时写入磁盘并回填内存(如果大小未超标)。
#### Parameters

@@ -127,2 +156,4 @@

缓存指纹键
##### body

@@ -132,2 +163,4 @@

响应体数据 Buffer
##### metadata

@@ -137,2 +170,4 @@

响应元数据(不含 size,由本方法自动计算)
#### Returns

@@ -148,4 +183,11 @@

Defined in: [core/SmartCache.ts:111](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L111)
Defined in: [core/SmartCache.ts:175](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L175)
获取磁盘写入流 (流式缓存)
该方法用于支持真正的流式代理。它会执行以下一致性操作:
1. 立即清除 L1 内存中的对应键,防止读到旧数据。
2. 返回一个可写流,数据将直接流入磁盘。
3. **一致性修复**: 在流写入完成(finish)时再次清理内存,防止写入期间的并发读取将旧数据再次回填进内存。
#### Parameters

@@ -157,2 +199,4 @@

缓存指纹键
##### metadata

@@ -162,4 +206,8 @@

响应元数据
#### Returns
`WritableStream`
Node.js 可写流

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

Defined in: [core/createCachedFetch.ts:17](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/createCachedFetch.ts#L17)
Defined in: [core/createCachedFetch.ts:17](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/createCachedFetch.ts#L17)

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

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

Defined in: [core/createFetchWithCache.ts:16](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/createFetchWithCache.ts#L16)
Defined in: [core/createFetchWithCache.ts:16](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/createFetchWithCache.ts#L16)

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

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

> **extractData**(`source`, `config?`): `Record`\<`string`, `string`[]\>
> **extractData**(`source`, `config?`, `defaultAllowed?`): `Record`\<`string`, `string`[]\>
Defined in: [utils/extractData.ts:17](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/utils/extractData.ts#L17)
Defined in: [utils/extractData.ts:40](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/utils/extractData.ts#L40)

@@ -17,3 +17,3 @@ 从源对象中根据过滤配置提取数据并标准化。

此函数主要用于生成缓存指纹。它会:
1. 根据 `config` (include/exclude) 过滤键。
1. 根据 `config` (include/exclude) 过滤键,调用 `isAllowed` 判断每个键是否允许。
2. 对键进行排序以保证指纹的一致性。

@@ -23,2 +23,7 @@ 3. 将所有键转换为小写。

**关于 `defaultAllowed` 参数**:
- 只有当没有配置 `include` 和 `exclude` 时,`defaultAllowed` 才会生效。
- 如果配置了 `include`(即使为空数组),`defaultAllowed` 也不会生效。
- 详见 `isAllowed` 函数的优先级逻辑。
## Parameters

@@ -36,4 +41,10 @@

过滤配置 (白名单或黑名单)
过滤配置,支持 `include`(白名单)和 `exclude`(黑名单)
### defaultAllowed?
`boolean`
当没有配置时的默认值(默认 `false`,即不提取任何键)
## Returns

@@ -43,2 +54,20 @@

标准化后的数据 Map,键为小写,值为字符串数组
标准化后的数据 Map,键为小写,值为排序后的字符串数组
## Example
```typescript
const headers = { 'Content-Type': 'application/json', 'X-Request-Id': '123' };
// 默认不提取任何键
extractData(headers); // {}
// 提取所有键
extractData(headers, undefined, true); // { 'content-type': ['application/json'], 'x-request-id': ['123'] }
// 白名单
extractData(headers, { include: ['content-type'] }); // { 'content-type': ['application/json'] }
// 黑名单(需要 include 或 defaultAllowed)
extractData(headers, { include: ['*'], exclude: ['x-request-id'] }, true); // { 'content-type': ['application/json'] }
```

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

Defined in: [core/fetchWithCache.ts:244](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L244)
Defined in: [core/fetchWithCache.ts:370](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L370)

@@ -17,2 +17,4 @@ 核心协调函数 (Fetcher Orchestrator)

实现了基于流的混合缓存代理核心逻辑,主要机制包括:
- **多方法支持与过滤**:支持通过 `allowedMethods` 配置可缓存的方法(如 POST, PUT),并通过 `cacheRules` 进行精细化的路径与参数匹配拦截。
- **异步 Request Body 处理**:当缓存 POST/PUT 请求时,会自动读取 Body 并计算唯一指纹(支持 JSON 字段过滤)。
- **大文件流式处理**:底层完全通过 Streams 实现,代理大文件时自动写入磁盘且防 OOM。

@@ -31,2 +33,4 @@ - **SWR (Stale-While-Revalidate)**:后台静默更新机制。

原始 Web 标准 Request 对象
### fetcher

@@ -36,2 +40,4 @@

实际执行网络请求的函数
### options

@@ -41,4 +47,8 @@

缓存配置选项
## Returns
`Promise`\<`Response`\>
带有缓存标识头和流式 Body 的 Response 对象

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

> **generateCacheKey**(`req`, `config`): `string`
> **generateCacheKey**(`req`, `config`): `Promise`\<`string`\>
Defined in: [core/generateCacheKey.ts:8](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/generateCacheKey.ts#L8)
Defined in: [core/generateCacheKey.ts:36](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/generateCacheKey.ts#L36)
根据 Request 和配置生成唯一的缓存键
根据 Request 对象和站点配置生成唯一的缓存指纹 (异步)
该函数是缓存系统的核心组件,用于将复杂的 HTTP 请求对象转换为唯一的 SHA-256 字符串。
它实现了高度可定制的提取逻辑,允许通过配置排除掉请求中不稳定的因素(如时间戳、Nonce 等)。
### 生成指纹包含的要素:
1. **Method**: 请求方法(统一转为大写)。
2. **Host & Path**: 请求的域名和路径。
3. **Query Params**: URL 查询参数,受 `config.query` 过滤影响。
4. **Headers**: 请求头信息,受 `config.headers` 过滤影响。默认排除 `cookie` 头。
5. **Cookies**: 特别提取的 Cookie 字段,受 `config.cookies` 过滤影响。
6. **Request Body**:
- 对于 `POST`, `PUT`, `PATCH` 请求,会自动尝试读取 Body。
- **JSON 类型**: 如果 `Content-Type` 包含 `application/json`,则解析为对象并应用 `config.body` 过滤。
- **非 JSON/流类型**: 回退到对原始 Body 字节流进行 SHA-256 哈希计算。
- **安全性**: 使用 `req.clone()` 读取 Body,确保不影响后续真实的 Fetch 请求流消费。
## Parameters

@@ -22,2 +37,4 @@

原始 Web 标准 Request 对象。
### config

@@ -27,4 +44,17 @@

站点级缓存配置,决定了哪些字段参与指纹计算。
## Returns
`string`
`Promise`\<`string`\>
返回一个 64 位十六进制的 SHA-256 哈希字符串作为缓存键。
## Example
```typescript
const cacheKey = await generateCacheKey(request, {
query: { exclude: ['timestamp'] },
body: { include: ['id', 'action'] }
});
```

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

> **isAllowed**(`key`, `config?`): `boolean`
> **isAllowed**(`key`, `config?`, `defaultAllowed?`): `boolean`
Defined in: [utils/isAllowed.ts:15](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/utils/isAllowed.ts#L15)
Defined in: [utils/isAllowed.ts:37](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/utils/isAllowed.ts#L37)
判断给定的键是否允许参与缓存指纹计算。
优先级逻辑:
1. 如果配置了 `include` (白名单),则只有存在于 `include` 中的键才会被允许。
2. 否则,如果配置了 `exclude` (黑名单),则存在于 `exclude` 中的键将被拒绝。
3. 如果都没有配置,默认允许所有键。
**优先级逻辑**:
1. `exclude` 命中 → 返回 `false`(优先级最高,会覆盖前面的结果)
2. `include` 存在且命中 → 返回 `true`
3. `include` 存在但不命中 → 返回 `false`
4. 都没有配置 → 使用 `defaultAllowed`(未传则返回 `undefined`)
**注意**:`include` 和 `exclude` 可以同时配置,此时 `exclude` 优先级更高。
## Parameters

@@ -33,4 +36,10 @@

过滤配置
过滤配置,支持 `include`(白名单)和 `exclude`(黑名单)
### defaultAllowed?
`boolean`
当没有配置或配置未命中时的默认值(可选)
## Returns

@@ -40,2 +49,20 @@

是否允许
是否允许。返回 `boolean` 或 `undefined`(当没有配置且未传 defaultAllowed 时)
## Example
```typescript
// 无配置
isAllowed('key'); // undefined
// 白名单
isAllowed('id', { include: ['id', 'name'] }); // true
isAllowed('email', { include: ['id', 'name'] }); // false
// 黑名单
isAllowed('password', { exclude: ['password'] }); // false
isAllowed('name', { exclude: ['password'] }); // undefined
// 设置默认值
isAllowed('name', { exclude: ['password'] }, true); // true
```

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

- [BodyFilterConfig](interfaces/BodyFilterConfig.md)
- [CacheEntry](interfaces/CacheEntry.md)
- [CacheMetadata](interfaces/CacheMetadata.md)
- [CacheRule](interfaces/CacheRule.md)
- [FetchWithCacheContext](interfaces/FetchWithCacheContext.md)

@@ -30,2 +32,5 @@ - [FetchWithCacheOptions](interfaces/FetchWithCacheOptions.md)

- [generateCacheKey](functions/generateCacheKey.md)
- [getSiteConfig](functions/getSiteConfig.md)
- [isAllowed](functions/isAllowed.md)
- [isGlob](functions/isGlob.md)
- [isMatch](functions/isMatch.md)

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

Defined in: [types.ts:55](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L55)
Defined in: [types.ts:137](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L137)

@@ -24,3 +24,3 @@ 完整的缓存条目

Defined in: [types.ts:57](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L57)
Defined in: [types.ts:139](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L139)

@@ -35,3 +35,3 @@ 响应体数据:小文件为 Buffer,大文件为可读流

Defined in: [types.ts:39](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L39)
Defined in: [types.ts:121](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L121)

@@ -50,3 +50,3 @@ 响应头对象

Defined in: [types.ts:45](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L45)
Defined in: [types.ts:127](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L127)

@@ -65,3 +65,3 @@ 原始请求方法

Defined in: [types.ts:41](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L41)
Defined in: [types.ts:123](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L123)

@@ -80,3 +80,3 @@ http-cache-semantics 策略对象,包含 TTL 和缓存指令

Defined in: [types.ts:49](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L49)
Defined in: [types.ts:131](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L131)

@@ -95,3 +95,3 @@ Body 的字节长度,用于精确区分“空响应”与“未入内存的大响应”

Defined in: [types.ts:37](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L37)
Defined in: [types.ts:119](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L119)

@@ -110,3 +110,3 @@ HTTP 状态码

Defined in: [types.ts:47](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L47)
Defined in: [types.ts:129](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L129)

@@ -125,3 +125,3 @@ 缓存写入时的时间戳

Defined in: [types.ts:43](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L43)
Defined in: [types.ts:125](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L125)

@@ -128,0 +128,0 @@ 原始请求 URL

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

Defined in: [types.ts:35](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L35)
Defined in: [types.ts:117](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L117)

@@ -27,3 +27,3 @@ 缓存元数据

Defined in: [types.ts:39](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L39)
Defined in: [types.ts:121](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L121)

@@ -38,3 +38,3 @@ 响应头对象

Defined in: [types.ts:45](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L45)
Defined in: [types.ts:127](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L127)

@@ -49,3 +49,3 @@ 原始请求方法

Defined in: [types.ts:41](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L41)
Defined in: [types.ts:123](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L123)

@@ -60,3 +60,3 @@ http-cache-semantics 策略对象,包含 TTL 和缓存指令

Defined in: [types.ts:49](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L49)
Defined in: [types.ts:131](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L131)

@@ -71,3 +71,3 @@ Body 的字节长度,用于精确区分“空响应”与“未入内存的大响应”

Defined in: [types.ts:37](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L37)
Defined in: [types.ts:119](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L119)

@@ -82,3 +82,3 @@ HTTP 状态码

Defined in: [types.ts:47](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L47)
Defined in: [types.ts:129](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L129)

@@ -93,4 +93,4 @@ 缓存写入时的时间戳

Defined in: [types.ts:43](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L43)
Defined in: [types.ts:125](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L125)
原始请求 URL

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

Defined in: [core/fetchWithCache.ts:31](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L31)
Defined in: [core/fetchWithCache.ts:32](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L32)

@@ -24,3 +24,3 @@ 内部流水线上下文,合并了入参和计算出的关键状态

Defined in: [core/fetchWithCache.ts:35](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L35)
Defined in: [core/fetchWithCache.ts:36](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L36)

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

Defined in: [core/fetchWithCache.ts:17](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L17)
Defined in: [core/fetchWithCache.ts:18](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L18)

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

Defined in: [core/fetchWithCache.ts:13](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L13)
Defined in: [core/fetchWithCache.ts:14](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L14)

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

Defined in: [core/fetchWithCache.ts:34](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L34)
Defined in: [core/fetchWithCache.ts:35](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L35)

@@ -80,3 +80,3 @@ ***

Defined in: [core/fetchWithCache.ts:15](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L15)
Defined in: [core/fetchWithCache.ts:16](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L16)

@@ -95,3 +95,3 @@ 站点级缓存配置

Defined in: [core/fetchWithCache.ts:33](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L33)
Defined in: [core/fetchWithCache.ts:34](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L34)

@@ -112,10 +112,25 @@ #### Parameters

> `optional` **generateKey**: (`req`, `config`) => `string`
> `optional` **generateKey**: (`req`, `config`) => `Promise`\<`string`\>
Defined in: [core/fetchWithCache.ts:21](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L21)
Defined in: [core/fetchWithCache.ts:22](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L22)
自定义缓存键生成函数
根据 Request 和配置生成唯一的缓存键
根据 Request 对象和站点配置生成唯一的缓存指纹 (异步)
该函数是缓存系统的核心组件,用于将复杂的 HTTP 请求对象转换为唯一的 SHA-256 字符串。
它实现了高度可定制的提取逻辑,允许通过配置排除掉请求中不稳定的因素(如时间戳、Nonce 等)。
### 生成指纹包含的要素:
1. **Method**: 请求方法(统一转为大写)。
2. **Host & Path**: 请求的域名和路径。
3. **Query Params**: URL 查询参数,受 `config.query` 过滤影响。
4. **Headers**: 请求头信息,受 `config.headers` 过滤影响。默认排除 `cookie` 头。
5. **Cookies**: 特别提取的 Cookie 字段,受 `config.cookies` 过滤影响。
6. **Request Body**:
- 对于 `POST`, `PUT`, `PATCH` 请求,会自动尝试读取 Body。
- **JSON 类型**: 如果 `Content-Type` 包含 `application/json`,则解析为对象并应用 `config.body` 过滤。
- **非 JSON/流类型**: 回退到对原始 Body 字节流进行 SHA-256 哈希计算。
- **安全性**: 使用 `req.clone()` 读取 Body,确保不影响后续真实的 Fetch 请求流消费。
#### Parameters

@@ -127,2 +142,4 @@

原始 Web 标准 Request 对象。
##### config

@@ -132,6 +149,19 @@

站点级缓存配置,决定了哪些字段参与指纹计算。
#### Returns
`string`
`Promise`\<`string`\>
返回一个 64 位十六进制的 SHA-256 哈希字符串作为缓存键。
#### Example
```typescript
const cacheKey = await generateCacheKey(request, {
query: { exclude: ['timestamp'] },
body: { include: ['id', 'action'] }
});
```
#### Inherited from

@@ -147,3 +177,3 @@

Defined in: [core/fetchWithCache.ts:19](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L19)
Defined in: [core/fetchWithCache.ts:20](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L20)

@@ -172,2 +202,2 @@ 后台更新 Promise 触发时的回调

Defined in: [core/fetchWithCache.ts:32](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L32)
Defined in: [core/fetchWithCache.ts:33](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L33)

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

Defined in: [core/fetchWithCache.ts:11](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L11)
Defined in: [core/fetchWithCache.ts:12](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L12)

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

Defined in: [core/fetchWithCache.ts:27](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L27)
Defined in: [core/fetchWithCache.ts:28](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L28)

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

Defined in: [core/fetchWithCache.ts:17](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L17)
Defined in: [core/fetchWithCache.ts:18](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L18)

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

Defined in: [core/fetchWithCache.ts:13](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L13)
Defined in: [core/fetchWithCache.ts:14](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L14)

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

Defined in: [core/fetchWithCache.ts:15](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L15)
Defined in: [core/fetchWithCache.ts:16](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L16)

@@ -68,10 +68,25 @@ 站点级缓存配置

> `optional` **generateKey**: (`req`, `config`) => `string`
> `optional` **generateKey**: (`req`, `config`) => `Promise`\<`string`\>
Defined in: [core/fetchWithCache.ts:21](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L21)
Defined in: [core/fetchWithCache.ts:22](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L22)
自定义缓存键生成函数
根据 Request 和配置生成唯一的缓存键
根据 Request 对象和站点配置生成唯一的缓存指纹 (异步)
该函数是缓存系统的核心组件,用于将复杂的 HTTP 请求对象转换为唯一的 SHA-256 字符串。
它实现了高度可定制的提取逻辑,允许通过配置排除掉请求中不稳定的因素(如时间戳、Nonce 等)。
### 生成指纹包含的要素:
1. **Method**: 请求方法(统一转为大写)。
2. **Host & Path**: 请求的域名和路径。
3. **Query Params**: URL 查询参数,受 `config.query` 过滤影响。
4. **Headers**: 请求头信息,受 `config.headers` 过滤影响。默认排除 `cookie` 头。
5. **Cookies**: 特别提取的 Cookie 字段,受 `config.cookies` 过滤影响。
6. **Request Body**:
- 对于 `POST`, `PUT`, `PATCH` 请求,会自动尝试读取 Body。
- **JSON 类型**: 如果 `Content-Type` 包含 `application/json`,则解析为对象并应用 `config.body` 过滤。
- **非 JSON/流类型**: 回退到对原始 Body 字节流进行 SHA-256 哈希计算。
- **安全性**: 使用 `req.clone()` 读取 Body,确保不影响后续真实的 Fetch 请求流消费。
#### Parameters

@@ -83,2 +98,4 @@

原始 Web 标准 Request 对象。
##### config

@@ -88,6 +105,19 @@

站点级缓存配置,决定了哪些字段参与指纹计算。
#### Returns
`string`
`Promise`\<`string`\>
返回一个 64 位十六进制的 SHA-256 哈希字符串作为缓存键。
#### Example
```typescript
const cacheKey = await generateCacheKey(request, {
query: { exclude: ['timestamp'] },
body: { include: ['id', 'action'] }
});
```
***

@@ -99,3 +129,3 @@

Defined in: [core/fetchWithCache.ts:19](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/fetchWithCache.ts#L19)
Defined in: [core/fetchWithCache.ts:20](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/fetchWithCache.ts#L20)

@@ -102,0 +132,0 @@ 后台更新 Promise 触发时的回调

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

Defined in: [types.ts:6](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L6)
Defined in: [types.ts:6](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L6)

@@ -16,2 +16,6 @@ 缓存键过滤配置

## Extended by
- [`BodyFilterConfig`](BodyFilterConfig.md)
## Properties

@@ -21,7 +25,7 @@

> `optional` **exclude**: `string`[]
> `optional` **exclude**: (`string` \| `RegExp`)[]
Defined in: [types.ts:10](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L10)
Defined in: [types.ts:10](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L10)
排除(黑名单):用于排除像 `timestamp`、`nonce` 等干扰缓存命中的动态字段
排除(黑名单):用于排除像 `timestamp`、`nonce` 等干扰缓存命中的动态字段。支持字符串、Glob 模式或正则表达式。

@@ -32,6 +36,6 @@ ***

> `optional` **include**: `string`[]
> `optional` **include**: (`string` \| `RegExp`)[]
Defined in: [types.ts:8](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L8)
Defined in: [types.ts:8](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L8)
仅包含(白名单):如果设置,只有这些字段会参与 Key 的计算
仅包含(白名单):如果设置,只有这些字段会参与 Key 的计算。支持字符串、Glob 模式或正则表达式。

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

Defined in: [types.ts:63](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L63)
Defined in: [types.ts:145](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L145)

@@ -20,3 +20,3 @@ 代理拦截器全局配置

Defined in: [types.ts:65](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L65)
Defined in: [types.ts:147](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L147)

@@ -31,3 +31,3 @@ 默认缓存配置,当请求的域名未在 sites 中匹配时使用

Defined in: [types.ts:67](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L67)
Defined in: [types.ts:149](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L149)

@@ -42,4 +42,4 @@ 针对特定域名的精细化缓存配置

Defined in: [types.ts:69](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L69)
Defined in: [types.ts:151](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L151)
磁盘缓存(cacache)的物理存储路径,可选,默认为系统临时目录

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

Defined in: [types.ts:16](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L16)
Defined in: [types.ts:81](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L81)

@@ -16,2 +16,25 @@ 站点级缓存配置

### body?
> `optional` **body**: [`BodyFilterConfig`](BodyFilterConfig.md)
Defined in: [types.ts:104](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L104)
请求体过滤配置 (仅限 JSON 类型)。
当方法为 POST/PUT/PATCH 且为 JSON 格式时,用于从 Body 中提取特定字段参与指纹计算。
***
### cacheRules?
> `optional` **cacheRules**: [`CacheRule`](CacheRule.md)[]
Defined in: [types.ts:93](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L93)
精细化缓存规则列表。
如果配置了此项,请求必须匹配其中至少一条规则才会被允许进入缓存流程。
适用于只希望缓存特定 API 接口的场景。
***
### cookies?

@@ -21,5 +44,5 @@

Defined in: [types.ts:22](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L22)
Defined in: [types.ts:99](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L99)
Cookie 过滤配置
Cookie 过滤配置:决定哪些 Cookie 字段参与缓存指纹计算

@@ -32,5 +55,5 @@ ***

Defined in: [types.ts:26](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L26)
Defined in: [types.ts:108](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L108)
是否强制缓存一切响应(无视 no-store 等不缓存指令),用于极端的离线可用容错场景
强制缓存:是否忽略 `Cache-Control: no-store` 等指令强制入库。

@@ -43,8 +66,20 @@ ***

Defined in: [types.ts:20](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L20)
Defined in: [types.ts:97](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L97)
请求头过滤配置
请求头过滤配置:决定哪些 Header 参与缓存指纹计算
***
### methods?
> `optional` **methods**: `string`[]
Defined in: [types.ts:87](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L87)
允许缓存的 HTTP 方法列表。
默认值: ['GET', 'HEAD']。
若要缓存 POST/PUT,必须在此显式添加,并确保后端响应满足缓存条件(或开启 `forceCache`)。
***
### query?

@@ -54,5 +89,5 @@

Defined in: [types.ts:18](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L18)
Defined in: [types.ts:95](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L95)
Query 参数过滤配置
Query 参数过滤配置:决定哪些查询参数参与缓存指纹 (Cache Key) 的计算

@@ -65,4 +100,4 @@ ***

Defined in: [types.ts:24](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/types.ts#L24)
Defined in: [types.ts:106](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/types.ts#L106)
当后端请求失败且存在旧缓存时,是否强制返回旧缓存(容错机制)
容错机制:当后端请求失败(网络错误或 5xx)且存在旧缓存时,是否强制返回旧缓存

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

Defined in: [core/SmartCache.ts:10](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L10)
Defined in: [core/SmartCache.ts:10](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L10)

@@ -20,3 +20,3 @@ SmartCache 选项

Defined in: [core/SmartCache.ts:14](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L14)
Defined in: [core/SmartCache.ts:14](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L14)

@@ -27,10 +27,36 @@ 内存缓存阈值(字节)。响应体大小超过此值时,Body 将只存入磁盘,而 Meta 仍保留在内存。默认 1MB。

### maxTotalMemorySize?
> `optional` **maxTotalMemorySize**: `number`
Defined in: [core/SmartCache.ts:16](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L16)
内存缓存总大小阈值(字节)。默认 100MB。超过此值将清空内存缓存。
***
### memoryOptions?
> `optional` **memoryOptions**: `Partial`\<`KeyvCacheableMemoryOptions`\>
> `optional` **memoryOptions**: `object`
Defined in: [core/SmartCache.ts:16](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L16)
Defined in: [core/SmartCache.ts:18](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L18)
透传给 L1 (Memory) 的高级配置
透传给 L1 (Memory) 的高级配置 (secondary-cache LRUCache options)
#### Index Signature
\[`key`: `string`\]: `any`
#### capacity?
> `optional` **capacity**: `number`
#### cleanInterval?
> `optional` **cleanInterval**: `number`
#### expires?
> `optional` **expires**: `number`
***

@@ -42,4 +68,4 @@

Defined in: [core/SmartCache.ts:12](https://github.com/isdk/proxy.js/blob/bed37fa43507dcbe5cdfa453876163571399d761/src/core/SmartCache.ts#L12)
Defined in: [core/SmartCache.ts:12](https://github.com/isdk/proxy.js/blob/76fee3a101f98e5bf29599fe7ea02ab06479cf70/src/core/SmartCache.ts#L12)
磁盘缓存的物理路径。如果不提供,将默认使用系统临时目录。

@@ -22,2 +22,4 @@ **@isdk/proxy**

- **🚀 Hybrid Multi-tier Cache**: Extreme speed with L1 (LRU Memory) and persistence with L2 (Content Addressable Disk via `cacache`).
- **📥 HTTP POST & Method Support**: Full support for caching POST, PUT, and other methods with intelligent request body fingerprinting.
- **🎯 Precision Filtering**: Fine-grained `cacheRules` to intercept specific paths or query parameters.
- **🌊 Streaming Native**: Fully stream-based internal pipeline natively prevents Out-Of-Memory (OOM) issues when proxying large files.

@@ -41,2 +43,4 @@ - **🧠 Intelligent Meta-Residency**: Metadata (Headers, Status, Policy) stays in memory regardless of body size, ensuring nanosecond cache policy evaluations.

### Basic Usage (GET)
```typescript

@@ -51,3 +55,3 @@ import { SmartCache, createCachedFetch } from '@isdk/proxy';

// 2. Create a pre-configured cached fetcher (automatically tracks concurrent requests)
// 2. Create a pre-configured cached fetcher
const myFetch = createCachedFetch({

@@ -57,3 +61,2 @@ cache,

staleIfError: true,
forceCache: false // Set to true to cache everything (ignore no-store) for offline-first apps
},

@@ -63,10 +66,79 @@ backgroundUpdate: true // Enable SWR

// 3. Use it anywhere in your app!
const request = new Request('https://api.example.com/data');
const response = await myFetch(request, (req) => fetch(req));
// 3. Use it!
const response = await myFetch(new Request('https://api.example.com/data'), (req) => fetch(req));
console.log(response.headers.get('x-proxy-cache'));
```
console.log(response.headers.get('x-proxy-cache')); // "MISS", "HIT", "STALE", or "STALE_IF_ERROR"
const data = await response.json();
### Advanced Usage: Caching POST Requests
You can cache POST/PUT requests by enabling methods and defining body filters to ignore dynamic fields (like timestamps) in the request body.
```typescript
const myPostFetch = createCachedFetch({
cache,
config: {
methods: ['GET', 'POST'], // Enable POST caching
body: {
exclude: ['timestamp', 'nonce'] // Ignore these fields when generating cache keys
},
cacheRules: [
{ method: 'POST', path: '/api/v1/query' } // Only cache specific POST endpoints
],
forceCache: true // Often needed for POST if backend doesn't send Cache-Control
}
});
```
## Configuration: `SiteCacheConfig`
| Field | Type | Description |
| :--- | :--- | :--- |
| `methods` | `string[]` | List of allowed HTTP methods. Default: `['GET', 'HEAD']`. |
| `cacheRules` | `CacheRule[]` | Fine-grained rules. If set, a request must match at least one rule to be cached. |
| `query` | `KeyFilterConfig` | Filters for URL search parameters (`include`/`exclude`). |
| `headers` | `KeyFilterConfig` | Filters for request headers. |
| `cookies` | `KeyFilterConfig` | Filters for cookies. |
| `body` | `KeyFilterConfig` | Filters for JSON request body fields. |
| `staleIfError`| `boolean` | Serve stale cache on network failure. |
| `forceCache` | `boolean` | Ignore `no-store` and force caching (useful for offline support). |
### `CacheRule` Object
- `method`: HTTP method to match.
- `path`: URL pathname matching (supports **RegExp**, **Glob**, **Array**, or **prefix match**).
- `query`: Key-value pairs. Values can be `string` (exact/Glob match), `true` (must exist), `false` (must not exist), or `RegExp`.
- `body`: Body content matching (supports **RegExp**, **Glob**, or **Array**).
### Pattern Matching
`@isdk/proxy` provides powerful pattern matching for all configurable fields:
| Pattern Type | Example | Description |
| :--- | :--- | :--- |
| **RegExp** | `/api/v[12]/.*/i` | JavaScript RegExp (in JSON, use string like `"/api/v[12]/.*/i"`) |
| **Glob** | `/**/*.json` | File path style wildcard matching |
| **Negation** | `['!/api/private/**', '/api/**']` | Exclude patterns (prefixed with `!`, checked first) |
| **Array** | `['/api/v1/*', '/api/v2/*']` | Multiple patterns (OR logic, negative takes precedence) |
| **Boolean** | `true` / `false` | For query params: must/must not exist |
**Example with advanced pattern matching:**
```typescript
const myFetch = createCachedFetch({
cache,
config: {
cacheRules: [
{
path: ['/api/v1/items/*', '!/api/v1/items/private/*'], // v1 items, exclude private
query: {
format: '/^(json|xml)$/', // Regex for format param
'page*': true // Glob: any param starting with 'page' must exist
},
body: /\"action\"\s*:\s*\"query\"/ // Regex body match
}
]
}
});
```
## Adapters

@@ -76,2 +148,4 @@

- **HTTP Caching Proxy Server (Node.js)**: See [@isdk/proxy-server](https://www.npmjs.com/package/@isdk/proxy-server) (separate package) for running a standalone HTTP forward proxy.
- **Crawlee Adapter**: See [@isdk/proxy-crawlee](https://www.npmjs.com/package/@isdk/proxy-crawlee) (separate package) for integrating with Crawlee web scraping lifecycle.
- **MSW Adapter**: See `@isdk/proxy-msw` (separate package) to use this caching engine as an MSW interceptor.

@@ -129,5 +203,115 @@ - **Axios Adapter**: Easily implemented by converting Axios config to Web `Request`.

### Utility Functions
Exported from `@isdk/proxy` for advanced usage:
#### `isMatch(pattern, value, usePrefix?)`
Universal pattern matching function. Supports RegExp, Glob, array patterns (with negation), and string prefix/exact matching.
- **`pattern`**: `string | RegExp | (string | RegExp)[]`
- **`value`**: The string to test against
- **`usePrefix`**: For plain strings, use prefix match instead of exact match (default: `false`)
- **Returns**: `boolean`
```typescript
import { isMatch } from '@isdk/proxy';
isMatch('/api/v[12]/.*', '/api/v1/users'); // RegExp
isMatch('/api/**/*.json', '/api/v1/data.json'); // Glob
isMatch(['!/private/**', '/api/**'], '/api/data'); // Negation
```
#### `isGlob(pattern)`
Check if a pattern is Glob syntax.
- **`pattern`**: `string`
- **Returns**: `boolean`
#### `getSiteConfig(urlString, proxyConfig)`
Get the site-specific cache configuration for a given URL.
- **`urlString`**: Full URL to match
- **`proxyConfig`**: `ProxyConfig` object with `sites` and `default` config
- **Returns**: `SiteCacheConfig`
```typescript
import { getSiteConfig } from '@isdk/proxy';
const config = getSiteConfig('https://api.example.com/data', {
default: { methods: ['GET'] },
sites: {
'api.example.com': { methods: ['GET', 'POST'], forceCache: true },
'/internal/': { staleIfError: true } // prefix match
}
});
```
#### `isAllowed(key, config, defaultAllowed?)`
Check if a key is allowed to participate in cache key fingerprinting.
- **`key`**: The key name to check
- **`config`**: `KeyFilterConfig` with `include` (whitelist) or `exclude` (blacklist)
- **`defaultAllowed`**: Optional. Default value when no config or no match
- **Returns**: `boolean | undefined`
**Priority Logic**:
1. `exclude` hit → returns `false` (highest priority)
2. `include` exists and hits → returns `true`
3. `include` exists but no hit → returns `false`
4. No config → uses `defaultAllowed` (returns `undefined` if not provided)
```typescript
import { isAllowed } from '@isdk/proxy';
// No config
isAllowed('key'); // undefined (falsy)
// Whitelist
isAllowed('id', { include: ['id', 'name'] }); // true
isAllowed('email', { include: ['id', 'name'] }); // false
// Blacklist
isAllowed('password', { exclude: ['password'] }); // false
isAllowed('name', { exclude: ['password'] }); // undefined (falsy)
// Need defaultAllowed to set default
isAllowed('name', { exclude: ['password'] }, true); // true
```
#### `extractData(source, config, defaultAllowed?)`
Extract and normalize data from a source object based on filter config. Used for generating cache fingerprints.
- **`source`**: Original data object (Query, Headers, Cookies, etc.)
- **`config`**: `KeyFilterConfig` object
- **`defaultAllowed`**: Optional. Whether to allow extraction when no config or no match (default `false`)
- **Returns**: `Record<string, string[]>` normalized data with lowercase keys and sorted array values
```typescript
import { extractData } from '@isdk/proxy';
const headers = { 'Content-Type': 'application/json', 'X-Request-Id': '123' };
// No extraction by default
extractData(headers); // {}
// Extract all keys
extractData(headers, undefined, true); // { 'content-type': ['application/json'], 'x-request-id': ['123'] }
// Whitelist
extractData(headers, { include: ['content-type'] }); // { 'content-type': ['application/json'] }
// Blacklist
extractData(headers, { include: ['*'], exclude: ['x-request-id'] }, true); // { 'content-type': ['application/json'] }
```
### Cache Status Headers
Every response processed by `@isdk/proxy` will include an `x-proxy-cache` header indicating its lifecycle:
- `HIT`: Served entirely from L1 or L2 cache.

@@ -134,0 +318,0 @@ - `MISS`: Bypassed cache and fetched from the origin server.

{
"name": "@isdk/proxy",
"version": "0.1.1",
"version": "0.1.2",
"description": "A framework-agnostic, high-performance hybrid caching middleware with SWR, request collapsing, and stale-if-error support.",

@@ -22,3 +22,2 @@ "license": "MIT",

"dependencies": {
"@cacheable/memory": "^2.0.8",
"@isdk/common-error": "^0.2.0",

@@ -30,2 +29,4 @@ "cacache": "^20.0.4",

"nanoid": "^5.1.11",
"picomatch": "^4.0.4",
"secondary-cache": "^2.1.0",
"util-ex": "^2.5.2"

@@ -45,2 +46,3 @@ },

"@types/node": "^20.0.0",
"@types/picomatch": "^4.0.3",
"@typescript-eslint/eslint-plugin": "~8.41.0",

@@ -47,0 +49,0 @@ "eslint": "^9.34.0",

@@ -16,2 +16,4 @@ # @isdk/proxy

- **🚀 混合多级缓存**: L1 (LRU 内存) 提供极速响应,L2 (内容寻址磁盘 `cacache`) 提供持久化存储。
- **📥 HTTP POST & 多方法支持**: 完整支持 POST、PUT 等非 GET 方法的缓存,内置智能请求体指纹计算机制。
- **🎯 精细化规则拦截**: 支持通过 `cacheRules` 对特定路径或 Query 参数进行外科手术式的精确缓存控制。
- **🌊 原生流式分发**: 内部完全基于 Stream 管道化构建,在代理大文件时天然防 OOM 内存溢出。

@@ -35,2 +37,4 @@ - **🧠 智能元数据驻留**: 无论文件多大,元数据 (Headers, Status, Policy) 始终驻留在内存中,确保纳秒级的缓存策略判定。

### 基础用法 (GET 请求)
```typescript

@@ -45,3 +49,3 @@ import { SmartCache, createCachedFetch } from '@isdk/proxy';

// 2. 创建一个预配置的缓存 Fetcher (内部会自动防缓存击穿)
// 2. 创建一个预配置的缓存 Fetcher
const myFetch = createCachedFetch({

@@ -51,3 +55,2 @@ cache,

staleIfError: true,
forceCache: false // 设置为 true 可无视 no-store 强制缓存一切,适用于离线应用
},

@@ -57,10 +60,79 @@ backgroundUpdate: true // 开启 SWR (过期后后台静默更新)

// 3. 在应用的任何地方愉快地使用它!
const request = new Request('https://api.example.com/data');
const response = await myFetch(request, (req) => fetch(req)); // 传入任何返回 Promise<Response> 的获取函数
// 3. 愉快地使用它!
const response = await myFetch(new Request('https://api.example.com/data'), (req) => fetch(req));
console.log(response.headers.get('x-proxy-cache'));
```
console.log(response.headers.get('x-proxy-cache')); // 输出: "MISS", "HIT", "STALE" 或 "STALE_IF_ERROR"
const data = await response.json();
### 进阶用法:缓存 POST 请求
你可以通过配置 `methods` 开启 POST/PUT 缓存,并使用 `body` 过滤器排除请求体中的动态字段(如时间戳、随机数),从而确保缓存键的稳定性。
```typescript
const myPostFetch = createCachedFetch({
cache,
config: {
methods: ['GET', 'POST'], // 允许缓存 POST
body: {
exclude: ['timestamp', 'nonce'] // 生成缓存键时忽略这些动态字段
},
cacheRules: [
{ method: 'POST', path: '/api/v1/query' } // 仅对特定的 POST 接口生效
],
forceCache: true // 对于 POST 请求,后端通常不发 Cache-Control,建议开启强制缓存
}
});
```
## 配置详解:`SiteCacheConfig`
| 配置项 | 类型 | 说明 |
| :--- | :--- | :--- |
| `methods` | `string[]` | 允许缓存的 HTTP 方法列表。默认仅为 `['GET', 'HEAD']`。 |
| `cacheRules` | `CacheRule[]` | 精细化拦截规则。如果配置,请求必须匹配其中至少一条规则才会被缓存。 |
| `query` | `KeyFilterConfig` | URL 查询参数过滤(`include` 白名单 / `exclude` 黑名单)。 |
| `headers` | `KeyFilterConfig` | 请求头过滤。 |
| `cookies` | `KeyFilterConfig` | Cookie 字段过滤。 |
| `body` | `KeyFilterConfig` | **仅限 JSON** 的请求体字段过滤。 |
| `staleIfError`| `boolean` | 网络请求失败时,是否强制返回本地过期的旧缓存。 |
| `forceCache` | `boolean` | 是否无视源站指令强制执行缓存,常用于离线应用。 |
### `CacheRule` 规则对象
- `method`: 匹配的 HTTP 方法。
- `path`: 路径匹配(支持**正则表达式**、**Glob 通配符**、**数组格式**或**前缀匹配**)。
- `query`: 键值对匹配。值可以是 `string`(全等/Glob匹配)、`true`(参数必须存在)、`false`(参数必须不存在)、或 `RegExp`(正则匹配)。
- `body`: Body 内容匹配(支持**正则表达式**、**Glob 通配符**或**数组格式**)。
### 模式匹配说明
`@isdk/proxy` 为所有可配置字段提供强大的模式匹配能力:
| 模式类型 | 示例 | 说明 |
| :--- | :--- | :--- |
| **正则表达式** | `/api/v[12]/.*/i` | JavaScript 正则(JSON 中用字符串表示,如 `"/api/v[12]/.*/i"`) |
| **Glob 通配符** | `/**/*.json` | 文件路径风格通配符匹配 |
| **否定模式** | `['!/api/private/**', '/api/**']` | 排除匹配(以 `!` 开头) |
| **数组格式** | `['/api/v1/*', '/api/v2/*']` | 多模式组合(OR 逻辑,负向优先) |
| **布尔值** | `true` / `false` | 用于 query 参数:必须存在/不存在 |
**高级模式匹配示例:**
```typescript
const myFetch = createCachedFetch({
cache,
config: {
cacheRules: [
{
path: ['/api/v1/items/*', '!/api/v1/items/private/*'], // v1 items,排除 private
query: {
format: '/^(json|xml)$/', // 正则匹配 format 参数
'page*': true // Glob:任何以 page 开头的参数必须存在
},
body: /\"action\"\s*:\s*\"query\"/ // 正则匹配 Body 内容
}
]
}
});
```
## 适配器 (Adapters)

@@ -70,5 +142,6 @@

- **MSW 适配器**: 参见 `@isdk/proxy-msw` (独立包),将此缓存引擎作为 MSW 拦截器使用。
- **HTTP 代理服务器 (Node.js)**: 参见 [@isdk/proxy-server](https://www.npmjs.com/package/@isdk/proxy-server)(独立包),用于启动独立的 HTTP 缓存代理服务器。
- **Crawlee 适配器**: 参见 [@isdk/proxy-crawlee](https://www.npmjs.com/package/@isdk/proxy-crawlee)(独立包),用于集成到 Crawlee 网页爬虫生命周期中。
- **MSW 适配器**: 参见 `@isdk/proxy-msw`(独立包),将此缓存引擎作为 MSW 拦截器使用。
- **Axios 适配器**: 可以通过将 Axios 配置转换为 Web 标准 `Request` 轻松实现。
- **Crawlee 适配器**: 能够集成到爬虫生命周期中,减少重复抓取。

@@ -133,2 +206,111 @@ ## 架构设计详解

### 工具函数
导出以下工具函数供高级用法:
#### `isMatch(pattern, value, usePrefix?)`
通用模式匹配函数。支持正则表达式、Glob、数组模式(含否定)和字符串前缀/精确匹配。
- **`pattern`**: `string | RegExp | (string | RegExp)[]`
- **`value`**: 要测试的字符串
- **`usePrefix`**: 对于普通字符串,是否使用前缀匹配而非精确匹配(默认:`false`)
- **返回值**: `boolean`
```typescript
import { isMatch } from '@isdk/proxy';
isMatch('/api/v[12]/.*', '/api/v1/users'); // 正则表达式
isMatch('/api/**/*.json', '/api/v1/data.json'); // Glob 通配符
isMatch(['!/private/**', '/api/**'], '/api/data'); // 否定模式
```
#### `isGlob(pattern)`
判断字符串是否为 Glob 语法。
- **`pattern`**: `string`
- **返回值**: `boolean`
#### `getSiteConfig(urlString, proxyConfig)`
根据 URL 获取对应的站点级缓存配置。
- **`urlString`**: 完整的请求 URL
- **`proxyConfig`**: 包含 `sites` 和 `default` 配置的 `ProxyConfig` 对象
- **返回值**: `SiteCacheConfig`
```typescript
import { getSiteConfig } from '@isdk/proxy';
const config = getSiteConfig('https://api.example.com/data', {
default: { methods: ['GET'] },
sites: {
'api.example.com': { methods: ['GET', 'POST'], forceCache: true },
'/internal/': { staleIfError: true } // 前缀匹配
}
});
```
#### `isAllowed(key, config, defaultAllowed?)`
判断指定的键是否允许参与缓存指纹计算。
- **`key`**: 要检查的键名
- **`config`**: `KeyFilterConfig` 对象,支持 `include`(白名单)或 `exclude`(黑名单)
- **`defaultAllowed`**: 可选参数。当没有配置或配置未命中时使用的默认值
- **返回值**: `boolean | undefined`
**优先级逻辑**:
1. `exclude` 命中 → 直接返回 `false`(优先级最高)
2. `include` 存在且命中 → 返回 `true`
3. `include` 存在但不命中 → 返回 `false`
4. 都没有配置 → 使用 `defaultAllowed`(未传则返回 `undefined`)
```typescript
import { isAllowed } from '@isdk/proxy';
// 无配置
isAllowed('key'); // undefined (falsy)
// 白名单
isAllowed('id', { include: ['id', 'name'] }); // true
isAllowed('email', { include: ['id', 'name'] }); // false
// 黑名单
isAllowed('password', { exclude: ['password'] }); // false
isAllowed('name', { exclude: ['password'] }); // undefined (falsy)
// 需要 defaultAllowed 来设置默认值
isAllowed('name', { exclude: ['password'] }, true); // true
```
#### `extractData(source, config, defaultAllowed?)`
从源对象中根据过滤配置提取数据并标准化。用于生成缓存指纹。
- **`source`**: 原始数据对象(Query、Headers、Cookies 等)
- **`config`**: `KeyFilterConfig` 对象
- **`defaultAllowed`**: 可选参数。当没有配置或配置未命中时,是否允许提取(默认 `false`)
- **返回值**: `Record<string, string[]>` 标准化后的数据,键为小写,值为排序后的数组
```typescript
import { extractData } from '@isdk/proxy';
const headers = { 'Content-Type': 'application/json', 'X-Request-Id': '123' };
// 默认不提取任何键
extractData(headers); // {}
// 提取所有键
extractData(headers, undefined, true); // { 'content-type': ['application/json'], 'x-request-id': ['123'] }
// 白名单
extractData(headers, { include: ['content-type'] }); // { 'content-type': ['application/json'] }
// 黑名单
extractData(headers, { include: ['*'], exclude: ['x-request-id'] }, true); // { 'content-type': ['application/json'] }
```
### 缓存状态标头 (Cache Status Headers)

@@ -135,0 +317,0 @@

+191
-7

@@ -18,2 +18,4 @@ # @isdk/proxy

- **🚀 Hybrid Multi-tier Cache**: Extreme speed with L1 (LRU Memory) and persistence with L2 (Content Addressable Disk via `cacache`).
- **📥 HTTP POST & Method Support**: Full support for caching POST, PUT, and other methods with intelligent request body fingerprinting.
- **🎯 Precision Filtering**: Fine-grained `cacheRules` to intercept specific paths or query parameters.
- **🌊 Streaming Native**: Fully stream-based internal pipeline natively prevents Out-Of-Memory (OOM) issues when proxying large files.

@@ -37,2 +39,4 @@ - **🧠 Intelligent Meta-Residency**: Metadata (Headers, Status, Policy) stays in memory regardless of body size, ensuring nanosecond cache policy evaluations.

### Basic Usage (GET)
```typescript

@@ -47,3 +51,3 @@ import { SmartCache, createCachedFetch } from '@isdk/proxy';

// 2. Create a pre-configured cached fetcher (automatically tracks concurrent requests)
// 2. Create a pre-configured cached fetcher
const myFetch = createCachedFetch({

@@ -53,3 +57,2 @@ cache,

staleIfError: true,
forceCache: false // Set to true to cache everything (ignore no-store) for offline-first apps
},

@@ -59,10 +62,79 @@ backgroundUpdate: true // Enable SWR

// 3. Use it anywhere in your app!
const request = new Request('https://api.example.com/data');
const response = await myFetch(request, (req) => fetch(req));
// 3. Use it!
const response = await myFetch(new Request('https://api.example.com/data'), (req) => fetch(req));
console.log(response.headers.get('x-proxy-cache'));
```
console.log(response.headers.get('x-proxy-cache')); // "MISS", "HIT", "STALE", or "STALE_IF_ERROR"
const data = await response.json();
### Advanced Usage: Caching POST Requests
You can cache POST/PUT requests by enabling methods and defining body filters to ignore dynamic fields (like timestamps) in the request body.
```typescript
const myPostFetch = createCachedFetch({
cache,
config: {
methods: ['GET', 'POST'], // Enable POST caching
body: {
exclude: ['timestamp', 'nonce'] // Ignore these fields when generating cache keys
},
cacheRules: [
{ method: 'POST', path: '/api/v1/query' } // Only cache specific POST endpoints
],
forceCache: true // Often needed for POST if backend doesn't send Cache-Control
}
});
```
## Configuration: `SiteCacheConfig`
| Field | Type | Description |
| :--- | :--- | :--- |
| `methods` | `string[]` | List of allowed HTTP methods. Default: `['GET', 'HEAD']`. |
| `cacheRules` | `CacheRule[]` | Fine-grained rules. If set, a request must match at least one rule to be cached. |
| `query` | `KeyFilterConfig` | Filters for URL search parameters (`include`/`exclude`). |
| `headers` | `KeyFilterConfig` | Filters for request headers. |
| `cookies` | `KeyFilterConfig` | Filters for cookies. |
| `body` | `KeyFilterConfig` | Filters for JSON request body fields. |
| `staleIfError`| `boolean` | Serve stale cache on network failure. |
| `forceCache` | `boolean` | Ignore `no-store` and force caching (useful for offline support). |
### `CacheRule` Object
- `method`: HTTP method to match.
- `path`: URL pathname matching (supports **RegExp**, **Glob**, **Array**, or **prefix match**).
- `query`: Key-value pairs. Values can be `string` (exact/Glob match), `true` (must exist), `false` (must not exist), or `RegExp`.
- `body`: Body content matching (supports **RegExp**, **Glob**, or **Array**).
### Pattern Matching
`@isdk/proxy` provides powerful pattern matching for all configurable fields:
| Pattern Type | Example | Description |
| :--- | :--- | :--- |
| **RegExp** | `/api/v[12]/.*/i` | JavaScript RegExp (in JSON, use string like `"/api/v[12]/.*/i"`) |
| **Glob** | `/**/*.json` | File path style wildcard matching |
| **Negation** | `['!/api/private/**', '/api/**']` | Exclude patterns (prefixed with `!`, checked first) |
| **Array** | `['/api/v1/*', '/api/v2/*']` | Multiple patterns (OR logic, negative takes precedence) |
| **Boolean** | `true` / `false` | For query params: must/must not exist |
**Example with advanced pattern matching:**
```typescript
const myFetch = createCachedFetch({
cache,
config: {
cacheRules: [
{
path: ['/api/v1/items/*', '!/api/v1/items/private/*'], // v1 items, exclude private
query: {
format: '/^(json|xml)$/', // Regex for format param
'page*': true // Glob: any param starting with 'page' must exist
},
body: /\"action\"\s*:\s*\"query\"/ // Regex body match
}
]
}
});
```
## Adapters

@@ -72,2 +144,4 @@

- **HTTP Caching Proxy Server (Node.js)**: See [@isdk/proxy-server](https://www.npmjs.com/package/@isdk/proxy-server) (separate package) for running a standalone HTTP forward proxy.
- **Crawlee Adapter**: See [@isdk/proxy-crawlee](https://www.npmjs.com/package/@isdk/proxy-crawlee) (separate package) for integrating with Crawlee web scraping lifecycle.
- **MSW Adapter**: See `@isdk/proxy-msw` (separate package) to use this caching engine as an MSW interceptor.

@@ -125,5 +199,115 @@ - **Axios Adapter**: Easily implemented by converting Axios config to Web `Request`.

### Utility Functions
Exported from `@isdk/proxy` for advanced usage:
#### `isMatch(pattern, value, usePrefix?)`
Universal pattern matching function. Supports RegExp, Glob, array patterns (with negation), and string prefix/exact matching.
- **`pattern`**: `string | RegExp | (string | RegExp)[]`
- **`value`**: The string to test against
- **`usePrefix`**: For plain strings, use prefix match instead of exact match (default: `false`)
- **Returns**: `boolean`
```typescript
import { isMatch } from '@isdk/proxy';
isMatch('/api/v[12]/.*', '/api/v1/users'); // RegExp
isMatch('/api/**/*.json', '/api/v1/data.json'); // Glob
isMatch(['!/private/**', '/api/**'], '/api/data'); // Negation
```
#### `isGlob(pattern)`
Check if a pattern is Glob syntax.
- **`pattern`**: `string`
- **Returns**: `boolean`
#### `getSiteConfig(urlString, proxyConfig)`
Get the site-specific cache configuration for a given URL.
- **`urlString`**: Full URL to match
- **`proxyConfig`**: `ProxyConfig` object with `sites` and `default` config
- **Returns**: `SiteCacheConfig`
```typescript
import { getSiteConfig } from '@isdk/proxy';
const config = getSiteConfig('https://api.example.com/data', {
default: { methods: ['GET'] },
sites: {
'api.example.com': { methods: ['GET', 'POST'], forceCache: true },
'/internal/': { staleIfError: true } // prefix match
}
});
```
#### `isAllowed(key, config, defaultAllowed?)`
Check if a key is allowed to participate in cache key fingerprinting.
- **`key`**: The key name to check
- **`config`**: `KeyFilterConfig` with `include` (whitelist) or `exclude` (blacklist)
- **`defaultAllowed`**: Optional. Default value when no config or no match
- **Returns**: `boolean | undefined`
**Priority Logic**:
1. `exclude` hit → returns `false` (highest priority)
2. `include` exists and hits → returns `true`
3. `include` exists but no hit → returns `false`
4. No config → uses `defaultAllowed` (returns `undefined` if not provided)
```typescript
import { isAllowed } from '@isdk/proxy';
// No config
isAllowed('key'); // undefined (falsy)
// Whitelist
isAllowed('id', { include: ['id', 'name'] }); // true
isAllowed('email', { include: ['id', 'name'] }); // false
// Blacklist
isAllowed('password', { exclude: ['password'] }); // false
isAllowed('name', { exclude: ['password'] }); // undefined (falsy)
// Need defaultAllowed to set default
isAllowed('name', { exclude: ['password'] }, true); // true
```
#### `extractData(source, config, defaultAllowed?)`
Extract and normalize data from a source object based on filter config. Used for generating cache fingerprints.
- **`source`**: Original data object (Query, Headers, Cookies, etc.)
- **`config`**: `KeyFilterConfig` object
- **`defaultAllowed`**: Optional. Whether to allow extraction when no config or no match (default `false`)
- **Returns**: `Record<string, string[]>` normalized data with lowercase keys and sorted array values
```typescript
import { extractData } from '@isdk/proxy';
const headers = { 'Content-Type': 'application/json', 'X-Request-Id': '123' };
// No extraction by default
extractData(headers); // {}
// Extract all keys
extractData(headers, undefined, true); // { 'content-type': ['application/json'], 'x-request-id': ['123'] }
// Whitelist
extractData(headers, { include: ['content-type'] }); // { 'content-type': ['application/json'] }
// Blacklist
extractData(headers, { include: ['*'], exclude: ['x-request-id'] }, true); // { 'content-type': ['application/json'] }
```
### Cache Status Headers
Every response processed by `@isdk/proxy` will include an `x-proxy-cache` header indicating its lifecycle:
- `HIT`: Served entirely from L1 or L2 cache.

@@ -130,0 +314,0 @@ - `MISS`: Bypassed cache and fetched from the origin server.