anyapi-mcp-server 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -27
- package/build/api-client.js +42 -21
- package/build/api-index.js +79 -27
- package/build/data-cache.js +96 -0
- package/build/graphql-schema.js +342 -40
- package/build/index.js +144 -128
- package/build/json-filter.js +62 -0
- package/build/pagination.js +123 -0
- package/build/query-suggestions.js +34 -9
- package/build/rate-limit-tracker.js +59 -0
- package/build/token-budget.js +135 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ See the [Google Workspace guide](docs/google-workspace.md) for a complete OAuth
|
|
|
94
94
|
|
|
95
95
|
## Tools
|
|
96
96
|
|
|
97
|
-
The server exposes
|
|
97
|
+
The server exposes four tools (plus `auth` when OAuth is configured):
|
|
98
98
|
|
|
99
99
|
### `list_api` — Browse endpoints
|
|
100
100
|
|
|
@@ -102,11 +102,11 @@ Discover what the API offers. Call with no arguments to see all categories, prov
|
|
|
102
102
|
|
|
103
103
|
### `call_api` — Inspect an endpoint
|
|
104
104
|
|
|
105
|
-
Makes a real HTTP request and returns the **inferred GraphQL schema** (SDL) — not the data itself. Use this to discover the response shape and get `suggestedQueries` you can copy into `query_api`.
|
|
105
|
+
Makes a real HTTP request and returns the **inferred GraphQL schema** (SDL) — not the data itself. Use this to discover the response shape and get `suggestedQueries` you can copy into `query_api`. Also returns per-field token costs (`fieldTokenCosts`) and a `dataKey` for cache reuse.
|
|
106
106
|
|
|
107
107
|
### `query_api` — Fetch data
|
|
108
108
|
|
|
109
|
-
Fetches data and returns **only the fields you select** via a GraphQL query. Supports both reads and writes (mutations for POST/PUT/DELETE/PATCH).
|
|
109
|
+
Fetches data and returns **only the fields you select** via a GraphQL query. Supports both reads and writes (mutations for POST/PUT/DELETE/PATCH). Pass a `dataKey` from `call_api` to reuse cached data with zero HTTP calls.
|
|
110
110
|
|
|
111
111
|
```graphql
|
|
112
112
|
# Read
|
|
@@ -116,14 +116,15 @@ Fetches data and returns **only the fields you select** via a GraphQL query. Sup
|
|
|
116
116
|
mutation { post_endpoint(input: { name: "example" }) { id } }
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
Key parameters:
|
|
120
|
+
- **`maxTokens`** — token budget for the response (default 4000). Arrays are truncated to fit.
|
|
121
|
+
- **`dataKey`** — reuse cached data from a previous `call_api` or `query_api` response.
|
|
122
|
+
- **`jsonFilter`** — dot-path to extract nested values after the GraphQL query (e.g. `"data[].attributes.name"`).
|
|
123
|
+
|
|
119
124
|
### `explain_api` — Read the docs
|
|
120
125
|
|
|
121
126
|
Returns spec documentation for an endpoint (parameters, request body schema, response codes) **without making an HTTP request**.
|
|
122
127
|
|
|
123
|
-
### `batch_query` — Parallel requests
|
|
124
|
-
|
|
125
|
-
Fetches data from up to 10 endpoints concurrently in a single tool call. Each request follows the `query_api` flow.
|
|
126
|
-
|
|
127
128
|
### `auth` — OAuth authentication
|
|
128
129
|
|
|
129
130
|
Only available when `--oauth-*` flags are configured. Manages the OAuth flow:
|
|
@@ -140,11 +141,11 @@ list_api → discover what's available
|
|
|
140
141
|
↓
|
|
141
142
|
explain_api → read the docs for an endpoint
|
|
142
143
|
↓
|
|
143
|
-
call_api → inspect the response schema
|
|
144
|
+
call_api → inspect the response schema (returns dataKey)
|
|
144
145
|
↓
|
|
145
|
-
query_api → fetch exactly the fields you need
|
|
146
|
+
query_api → fetch exactly the fields you need (pass dataKey for zero HTTP calls)
|
|
146
147
|
↓
|
|
147
|
-
|
|
148
|
+
query_api → re-query with different fields using the same dataKey
|
|
148
149
|
```
|
|
149
150
|
|
|
150
151
|
## How it works
|
|
@@ -153,21 +154,20 @@ batch_query → fetch from multiple endpoints at once
|
|
|
153
154
|
OpenAPI/Postman spec
|
|
154
155
|
│
|
|
155
156
|
▼
|
|
156
|
-
┌─────────┐ ┌─────────────┐ ┌──────────┐ ┌───────────┐
|
|
157
|
-
│list_api │ │ explain_api │ │ call_api │ │ query_api │
|
|
158
|
-
│(browse) │ │ (docs) │ │ (schema) │ │ (data) │
|
|
159
|
-
└─────────┘ └─────────────┘ └──────────┘ └───────────┘
|
|
160
|
-
│ │ no HTTP │ │
|
|
161
|
-
▼ ▼ request ▼ ▼
|
|
162
|
-
Spec index Spec index REST API call
|
|
163
|
-
(tags, (params, (with retry
|
|
164
|
-
paths) responses,
|
|
165
|
-
body schema) │
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
response data
|
|
157
|
+
┌─────────┐ ┌─────────────┐ ┌──────────┐ ┌───────────┐
|
|
158
|
+
│list_api │ │ explain_api │ │ call_api │ │ query_api │
|
|
159
|
+
│(browse) │ │ (docs) │ │ (schema) │ │ (data) │
|
|
160
|
+
└─────────┘ └─────────────┘ └──────────┘ └───────────┘
|
|
161
|
+
│ │ no HTTP │ │
|
|
162
|
+
▼ ▼ request ▼ ▼
|
|
163
|
+
Spec index Spec index REST API call dataKey cache
|
|
164
|
+
(tags, (params, (with retry) hit → no HTTP
|
|
165
|
+
paths) responses, │ miss → fetch
|
|
166
|
+
body schema) ▼ │
|
|
167
|
+
Infer schema + ▼
|
|
168
|
+
return dataKey Execute GraphQL
|
|
169
|
+
+ token budget
|
|
170
|
+
truncation
|
|
171
171
|
```
|
|
172
172
|
|
|
173
173
|
## Features
|
|
@@ -179,10 +179,14 @@ OpenAPI/Postman spec
|
|
|
179
179
|
- **Multi-sample merging** — samples up to 10 array elements for richer schemas
|
|
180
180
|
- **Mutation support** — write operations get typed GraphQL mutations from OpenAPI body schemas
|
|
181
181
|
- **Smart suggestions** — `call_api` returns ready-to-use queries based on the inferred schema
|
|
182
|
-
- **Response caching** —
|
|
182
|
+
- **Response caching** — filesystem-based cache with 5-min TTL; `dataKey` tokens let `query_api` reuse data with zero HTTP calls
|
|
183
|
+
- **Token budget** — `query_api` accepts `maxTokens` (default 4000) and truncates array results to fit via binary search
|
|
184
|
+
- **Per-field token costs** — `call_api` returns a `fieldTokenCosts` tree so the LLM can make informed field selections
|
|
185
|
+
- **Rate limit tracking** — parses `X-RateLimit-*` headers and warns when limits are nearly exhausted
|
|
186
|
+
- **Pagination detection** — auto-detects cursor, next-page-token, and link-based pagination patterns in responses
|
|
187
|
+
- **JSON filter** — `query_api` accepts a `jsonFilter` dot-path for post-query extraction (e.g. `"data[].name"`)
|
|
183
188
|
- **Retry with backoff** — automatic retries for 429/5xx with exponential backoff and `Retry-After` support
|
|
184
189
|
- **Multi-format** — parses JSON, XML, CSV, and plain text responses
|
|
185
|
-
- **Pagination** — API-level via `params`, client-side slicing via `limit`/`offset`
|
|
186
190
|
- **Rich errors** — structured error messages with status-specific suggestions and spec context for self-correction
|
|
187
191
|
- **OAuth 2.0** — Authorization Code (with PKCE) and Client Credentials flows with automatic token refresh
|
|
188
192
|
- **Env var interpolation** — `${ENV_VAR}` in base URLs, headers, and spec paths
|
package/build/api-client.js
CHANGED
|
@@ -1,9 +1,45 @@
|
|
|
1
1
|
import { withRetry, RetryableError, isRetryableStatus } from "./retry.js";
|
|
2
|
-
import { buildCacheKey, consumeCached, setCache } from "./response-cache.js";
|
|
3
2
|
import { logEntry, isLoggingEnabled } from "./logger.js";
|
|
4
3
|
import { parseResponse } from "./response-parser.js";
|
|
5
4
|
import { ApiError } from "./error-context.js";
|
|
6
5
|
import { getValidAccessToken, refreshTokens } from "./oauth.js";
|
|
6
|
+
import { waitIfNeeded, trackRateLimit } from "./rate-limit-tracker.js";
|
|
7
|
+
function tryParseInt(val) {
|
|
8
|
+
if (!val)
|
|
9
|
+
return null;
|
|
10
|
+
const n = parseInt(val, 10);
|
|
11
|
+
return isNaN(n) ? null : n;
|
|
12
|
+
}
|
|
13
|
+
export function parseRateLimits(headers) {
|
|
14
|
+
const lower = {};
|
|
15
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
16
|
+
lower[k.toLowerCase()] = v;
|
|
17
|
+
}
|
|
18
|
+
const remaining = tryParseInt(lower["x-ratelimit-remaining"]) ??
|
|
19
|
+
tryParseInt(lower["ratelimit-remaining"]) ??
|
|
20
|
+
tryParseInt(lower["x-rate-limit-remaining"]);
|
|
21
|
+
const limit = tryParseInt(lower["x-ratelimit-limit"]) ??
|
|
22
|
+
tryParseInt(lower["ratelimit-limit"]) ??
|
|
23
|
+
tryParseInt(lower["x-rate-limit-limit"]);
|
|
24
|
+
const resetRaw = lower["x-ratelimit-reset"] ??
|
|
25
|
+
lower["ratelimit-reset"] ??
|
|
26
|
+
lower["x-rate-limit-reset"];
|
|
27
|
+
if (remaining === null && limit === null && !resetRaw)
|
|
28
|
+
return null;
|
|
29
|
+
let resetAt = null;
|
|
30
|
+
if (resetRaw) {
|
|
31
|
+
const asNumber = parseInt(resetRaw, 10);
|
|
32
|
+
if (!isNaN(asNumber)) {
|
|
33
|
+
resetAt = asNumber > 1_000_000_000
|
|
34
|
+
? new Date(asNumber * 1000).toISOString()
|
|
35
|
+
: `${asNumber}s`;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
resetAt = resetRaw;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { remaining, limit, resetAt };
|
|
42
|
+
}
|
|
7
43
|
const TIMEOUT_MS = 30_000;
|
|
8
44
|
function interpolatePath(pathTemplate, params) {
|
|
9
45
|
const remaining = { ...params };
|
|
@@ -17,22 +53,7 @@ function interpolatePath(pathTemplate, params) {
|
|
|
17
53
|
});
|
|
18
54
|
return { url, remainingParams: remaining };
|
|
19
55
|
}
|
|
20
|
-
|
|
21
|
-
* @param cacheMode
|
|
22
|
-
* - "populate" — skip cache read, always fetch, store result (used by call_api)
|
|
23
|
-
* - "consume" — read-and-evict cache, fetch on miss, do NOT re-store (used by query_api)
|
|
24
|
-
* - "none" — no caching at all (default)
|
|
25
|
-
*/
|
|
26
|
-
export async function callApi(config, method, pathTemplate, params, body, extraHeaders, cacheMode = "none") {
|
|
27
|
-
// --- Cache check (consume mode only) ---
|
|
28
|
-
const cacheKey = cacheMode !== "none"
|
|
29
|
-
? buildCacheKey(method, pathTemplate, params, body, extraHeaders)
|
|
30
|
-
: "";
|
|
31
|
-
if (cacheMode === "consume") {
|
|
32
|
-
const cached = consumeCached(cacheKey);
|
|
33
|
-
if (cached !== undefined)
|
|
34
|
-
return cached;
|
|
35
|
-
}
|
|
56
|
+
export async function callApi(config, method, pathTemplate, params, body, extraHeaders) {
|
|
36
57
|
// --- URL construction ---
|
|
37
58
|
const { url: interpolatedPath, remainingParams } = interpolatePath(pathTemplate, params ?? {});
|
|
38
59
|
let fullUrl = `${config.baseUrl}${interpolatedPath}`;
|
|
@@ -97,6 +118,8 @@ export async function callApi(config, method, pathTemplate, params, body, extraH
|
|
|
97
118
|
durationMs,
|
|
98
119
|
});
|
|
99
120
|
}
|
|
121
|
+
// Track rate limit headers from every response
|
|
122
|
+
trackRateLimit(parseRateLimits(responseHeaders));
|
|
100
123
|
if (!response.ok) {
|
|
101
124
|
if (isRetryableStatus(response.status)) {
|
|
102
125
|
const msg = `API error ${response.status} ${response.statusText}: ${bodyText}`;
|
|
@@ -125,6 +148,8 @@ export async function callApi(config, method, pathTemplate, params, body, extraH
|
|
|
125
148
|
}
|
|
126
149
|
});
|
|
127
150
|
};
|
|
151
|
+
// --- Rate limit pre-check ---
|
|
152
|
+
await waitIfNeeded();
|
|
128
153
|
// --- Execute with 401 refresh-and-retry ---
|
|
129
154
|
let result;
|
|
130
155
|
try {
|
|
@@ -148,9 +173,5 @@ export async function callApi(config, method, pathTemplate, params, body, extraH
|
|
|
148
173
|
throw error;
|
|
149
174
|
}
|
|
150
175
|
}
|
|
151
|
-
// --- Cache store (populate mode only) ---
|
|
152
|
-
if (cacheMode === "populate") {
|
|
153
|
-
setCache(cacheKey, result);
|
|
154
|
-
}
|
|
155
176
|
return result;
|
|
156
177
|
}
|
package/build/api-index.js
CHANGED
|
@@ -1,6 +1,82 @@
|
|
|
1
1
|
import yaml from "js-yaml";
|
|
2
2
|
const PAGE_SIZE = 20;
|
|
3
3
|
const HTTP_METHODS = new Set(["get", "post", "put", "delete", "patch"]);
|
|
4
|
+
const MAX_BODY_SCHEMA_DEPTH = 6;
|
|
5
|
+
function extractProperties(schema, spec, depth, visited) {
|
|
6
|
+
const properties = schema.properties;
|
|
7
|
+
if (!properties)
|
|
8
|
+
return undefined;
|
|
9
|
+
const requiredFields = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
10
|
+
const result = {};
|
|
11
|
+
for (const [propName, propDef] of Object.entries(properties)) {
|
|
12
|
+
let def = propDef;
|
|
13
|
+
let refPath;
|
|
14
|
+
// Resolve property-level $ref
|
|
15
|
+
if (def["$ref"] && typeof def["$ref"] === "string") {
|
|
16
|
+
refPath = def["$ref"];
|
|
17
|
+
if (visited.has(refPath)) {
|
|
18
|
+
result[propName] = { type: "object", required: requiredFields.has(propName) };
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const resolved = resolveRef(refPath, spec);
|
|
22
|
+
if (resolved)
|
|
23
|
+
def = resolved;
|
|
24
|
+
}
|
|
25
|
+
const prop = {
|
|
26
|
+
type: def.type ?? "string",
|
|
27
|
+
required: requiredFields.has(propName),
|
|
28
|
+
};
|
|
29
|
+
if (def.description)
|
|
30
|
+
prop.description = def.description;
|
|
31
|
+
// Recurse into nested objects
|
|
32
|
+
if (prop.type === "object" && def.properties && depth < MAX_BODY_SCHEMA_DEPTH) {
|
|
33
|
+
const branch = new Set(visited);
|
|
34
|
+
if (refPath)
|
|
35
|
+
branch.add(refPath);
|
|
36
|
+
const nested = extractProperties(def, spec, depth + 1, branch);
|
|
37
|
+
if (nested) {
|
|
38
|
+
prop.properties = nested;
|
|
39
|
+
if (Array.isArray(def.required) && def.required.length > 0) {
|
|
40
|
+
prop.required_fields = def.required;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (def.items && typeof def.items === "object") {
|
|
45
|
+
let itemsDef = def.items;
|
|
46
|
+
let itemsRefPath;
|
|
47
|
+
if (itemsDef["$ref"] && typeof itemsDef["$ref"] === "string") {
|
|
48
|
+
itemsRefPath = itemsDef["$ref"];
|
|
49
|
+
if (!visited.has(itemsRefPath)) {
|
|
50
|
+
const resolved = resolveRef(itemsRefPath, spec);
|
|
51
|
+
if (resolved)
|
|
52
|
+
itemsDef = resolved;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const itemType = itemsDef.type ?? "string";
|
|
56
|
+
if (itemType === "object" && itemsDef.properties && depth < MAX_BODY_SCHEMA_DEPTH) {
|
|
57
|
+
const branch = new Set(visited);
|
|
58
|
+
if (itemsRefPath)
|
|
59
|
+
branch.add(itemsRefPath);
|
|
60
|
+
const nestedItems = extractProperties(itemsDef, spec, depth + 1, branch);
|
|
61
|
+
if (nestedItems) {
|
|
62
|
+
prop.items = {
|
|
63
|
+
type: itemType,
|
|
64
|
+
properties: nestedItems,
|
|
65
|
+
required: Array.isArray(itemsDef.required) ? itemsDef.required : undefined,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
prop.items = { type: itemType };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
prop.items = { type: itemType };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
result[propName] = prop;
|
|
77
|
+
}
|
|
78
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
79
|
+
}
|
|
4
80
|
function extractRequestBodySchema(requestBody, spec) {
|
|
5
81
|
if (!requestBody || typeof requestBody !== "object")
|
|
6
82
|
return undefined;
|
|
@@ -26,34 +102,10 @@ function extractRequestBodySchema(requestBody, spec) {
|
|
|
26
102
|
return undefined;
|
|
27
103
|
schema = resolved;
|
|
28
104
|
}
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
105
|
+
const result = extractProperties(schema, spec, 0, new Set());
|
|
106
|
+
if (!result)
|
|
31
107
|
return undefined;
|
|
32
|
-
|
|
33
|
-
const result = {};
|
|
34
|
-
for (const [propName, propDef] of Object.entries(properties)) {
|
|
35
|
-
let def = propDef;
|
|
36
|
-
// Resolve property-level $ref
|
|
37
|
-
if (def["$ref"] && typeof def["$ref"] === "string") {
|
|
38
|
-
const resolved = resolveRef(def["$ref"], spec);
|
|
39
|
-
if (resolved)
|
|
40
|
-
def = resolved;
|
|
41
|
-
}
|
|
42
|
-
const prop = {
|
|
43
|
-
type: def.type ?? "string",
|
|
44
|
-
required: requiredFields.has(propName),
|
|
45
|
-
};
|
|
46
|
-
if (def.description)
|
|
47
|
-
prop.description = def.description;
|
|
48
|
-
if (def.items && typeof def.items === "object") {
|
|
49
|
-
const items = def.items;
|
|
50
|
-
prop.items = { type: items.type ?? "string" };
|
|
51
|
-
}
|
|
52
|
-
result[propName] = prop;
|
|
53
|
-
}
|
|
54
|
-
return Object.keys(result).length > 0
|
|
55
|
-
? { contentType: "application/json", properties: result }
|
|
56
|
-
: undefined;
|
|
108
|
+
return { contentType: "application/json", properties: result };
|
|
57
109
|
}
|
|
58
110
|
function resolveRef(ref, spec) {
|
|
59
111
|
// Handle "#/components/schemas/Foo" style refs
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { platform, env } from "node:process";
|
|
5
|
+
const TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
6
|
+
function defaultResponseDir() {
|
|
7
|
+
if (platform === "win32") {
|
|
8
|
+
const base = env.LOCALAPPDATA ?? join(env.USERPROFILE ?? "", "AppData", "Local");
|
|
9
|
+
return join(base, "anyapi-mcp", "responses");
|
|
10
|
+
}
|
|
11
|
+
return join(env.HOME ?? "/tmp", ".cache", "anyapi-mcp", "responses");
|
|
12
|
+
}
|
|
13
|
+
let responseDir = defaultResponseDir();
|
|
14
|
+
export function _setResponseDirForTests(dir) {
|
|
15
|
+
responseDir = dir;
|
|
16
|
+
}
|
|
17
|
+
export function _clearAllForTests() {
|
|
18
|
+
try {
|
|
19
|
+
for (const file of readdirSync(responseDir)) {
|
|
20
|
+
if (file.endsWith(".json")) {
|
|
21
|
+
try {
|
|
22
|
+
unlinkSync(join(responseDir, file));
|
|
23
|
+
}
|
|
24
|
+
catch { /* ignore */ }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch { /* dir may not exist */ }
|
|
29
|
+
}
|
|
30
|
+
function ensureDir() {
|
|
31
|
+
mkdirSync(responseDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
export function cleanupExpired() {
|
|
34
|
+
try {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
for (const file of readdirSync(responseDir)) {
|
|
37
|
+
if (!file.endsWith(".json"))
|
|
38
|
+
continue;
|
|
39
|
+
try {
|
|
40
|
+
const content = readFileSync(join(responseDir, file), "utf-8");
|
|
41
|
+
const entry = JSON.parse(content);
|
|
42
|
+
if (entry.expiresAt < now) {
|
|
43
|
+
unlinkSync(join(responseDir, file));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* ignore individual file errors */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* dir may not exist yet */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function storeResponse(method, path, data, responseHeaders) {
|
|
56
|
+
const dataKey = randomBytes(4).toString("hex");
|
|
57
|
+
const entry = {
|
|
58
|
+
method,
|
|
59
|
+
path,
|
|
60
|
+
data,
|
|
61
|
+
responseHeaders,
|
|
62
|
+
expiresAt: Date.now() + TTL_MS,
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
ensureDir();
|
|
66
|
+
writeFileSync(join(responseDir, `${dataKey}.json`), JSON.stringify(entry));
|
|
67
|
+
cleanupExpired();
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
process.stderr.write(`data-cache: failed to store ${dataKey}: ${err}\n`);
|
|
71
|
+
}
|
|
72
|
+
return dataKey;
|
|
73
|
+
}
|
|
74
|
+
export function loadResponse(dataKey) {
|
|
75
|
+
try {
|
|
76
|
+
const filePath = join(responseDir, `${dataKey}.json`);
|
|
77
|
+
const content = readFileSync(filePath, "utf-8");
|
|
78
|
+
const entry = JSON.parse(content);
|
|
79
|
+
if (entry.expiresAt < Date.now()) {
|
|
80
|
+
try {
|
|
81
|
+
unlinkSync(filePath);
|
|
82
|
+
}
|
|
83
|
+
catch { /* ignore */ }
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
method: entry.method,
|
|
88
|
+
path: entry.path,
|
|
89
|
+
data: entry.data,
|
|
90
|
+
responseHeaders: entry.responseHeaders,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|