@apitap/core 1.6.0 → 1.6.1
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/dist/cli.js +58 -21
- package/dist/cli.js.map +1 -1
- package/dist/replay/engine.d.ts +6 -0
- package/dist/replay/engine.js +26 -5
- package/dist/replay/engine.js.map +1 -1
- package/dist/skill/apis-guru.js +23 -15
- package/dist/skill/apis-guru.js.map +1 -1
- package/dist/skill/openapi-converter.js +7 -0
- package/dist/skill/openapi-converter.js.map +1 -1
- package/dist/skill/signing.d.ts +5 -0
- package/dist/skill/signing.js +9 -0
- package/dist/skill/signing.js.map +1 -1
- package/dist/skill/store.js +9 -4
- package/dist/skill/store.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/package.json +3 -1
- package/src/cli.ts +62 -21
- package/src/replay/engine.ts +29 -5
- package/src/skill/apis-guru.ts +25 -19
- package/src/skill/openapi-converter.ts +8 -0
- package/src/skill/signing.ts +14 -0
- package/src/skill/store.ts +8 -4
- package/src/types.ts +2 -2
package/src/replay/engine.ts
CHANGED
|
@@ -45,6 +45,15 @@ const BLOCKED_REPLAY_HEADERS = new Set([
|
|
|
45
45
|
'sec-fetch-user',
|
|
46
46
|
]);
|
|
47
47
|
|
|
48
|
+
export function safeParseJson(text: string): unknown {
|
|
49
|
+
if (text.length === 0) return text;
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(text);
|
|
52
|
+
} catch {
|
|
53
|
+
return text;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
export interface ReplayOptions {
|
|
49
58
|
/** User-provided parameters for path, query, and body substitution */
|
|
50
59
|
params?: Record<string, string>;
|
|
@@ -70,6 +79,8 @@ export interface ReplayResult {
|
|
|
70
79
|
truncated?: boolean;
|
|
71
80
|
/** Contract warnings from schema drift detection */
|
|
72
81
|
contractWarnings?: ContractWarning[];
|
|
82
|
+
/** Upgrade hint: set when a low-confidence endpoint gets a 2xx response */
|
|
83
|
+
upgrade?: { confidence: 1.0; endpointProvenance: 'captured' };
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
/**
|
|
@@ -548,8 +559,8 @@ export async function replayEndpoint(
|
|
|
548
559
|
let retryData: unknown;
|
|
549
560
|
const retryCt = retryResponse.headers.get('content-type') ?? '';
|
|
550
561
|
const retryText = await retryResponse.text();
|
|
551
|
-
if (retryCt.includes('json')
|
|
552
|
-
retryData =
|
|
562
|
+
if (retryCt.includes('json')) {
|
|
563
|
+
retryData = safeParseJson(retryText);
|
|
553
564
|
} else {
|
|
554
565
|
retryData = retryText;
|
|
555
566
|
}
|
|
@@ -558,6 +569,11 @@ export async function replayEndpoint(
|
|
|
558
569
|
? wrapAuthError(retryResponse.status, retryData, skill.domain)
|
|
559
570
|
: retryData;
|
|
560
571
|
|
|
572
|
+
const retryUpgrade = (endpoint.confidence !== undefined && endpoint.confidence < 1.0
|
|
573
|
+
&& retryResponse.status >= 200 && retryResponse.status < 300)
|
|
574
|
+
? { confidence: 1.0 as const, endpointProvenance: 'captured' as const }
|
|
575
|
+
: undefined;
|
|
576
|
+
|
|
561
577
|
if (options.maxBytes) {
|
|
562
578
|
const truncated = truncateResponse(retryFinalData, { maxBytes: options.maxBytes });
|
|
563
579
|
return {
|
|
@@ -566,6 +582,7 @@ export async function replayEndpoint(
|
|
|
566
582
|
data: truncated.data,
|
|
567
583
|
refreshed,
|
|
568
584
|
...(truncated.truncated ? { truncated: true } : {}),
|
|
585
|
+
...(retryUpgrade ? { upgrade: retryUpgrade } : {}),
|
|
569
586
|
};
|
|
570
587
|
}
|
|
571
588
|
|
|
@@ -574,6 +591,7 @@ export async function replayEndpoint(
|
|
|
574
591
|
headers: retryHeaders,
|
|
575
592
|
data: retryFinalData,
|
|
576
593
|
refreshed,
|
|
594
|
+
...(retryUpgrade ? { upgrade: retryUpgrade } : {}),
|
|
577
595
|
};
|
|
578
596
|
}
|
|
579
597
|
}
|
|
@@ -586,8 +604,8 @@ export async function replayEndpoint(
|
|
|
586
604
|
let data: unknown;
|
|
587
605
|
const ct = response.headers.get('content-type') ?? '';
|
|
588
606
|
const text = await response.text();
|
|
589
|
-
if (ct.includes('json')
|
|
590
|
-
data =
|
|
607
|
+
if (ct.includes('json')) {
|
|
608
|
+
data = safeParseJson(text);
|
|
591
609
|
} else {
|
|
592
610
|
data = text;
|
|
593
611
|
}
|
|
@@ -606,6 +624,11 @@ export async function replayEndpoint(
|
|
|
606
624
|
}
|
|
607
625
|
}
|
|
608
626
|
|
|
627
|
+
const upgrade = (endpoint.confidence !== undefined && endpoint.confidence < 1.0
|
|
628
|
+
&& response.status >= 200 && response.status < 300)
|
|
629
|
+
? { confidence: 1.0 as const, endpointProvenance: 'captured' as const }
|
|
630
|
+
: undefined;
|
|
631
|
+
|
|
609
632
|
// Apply truncation if maxBytes is set
|
|
610
633
|
if (options.maxBytes) {
|
|
611
634
|
const truncated = truncateResponse(finalData, { maxBytes: options.maxBytes });
|
|
@@ -616,10 +639,11 @@ export async function replayEndpoint(
|
|
|
616
639
|
...(refreshed ? { refreshed } : {}),
|
|
617
640
|
...(truncated.truncated ? { truncated: true } : {}),
|
|
618
641
|
...(contractWarnings ? { contractWarnings } : {}),
|
|
642
|
+
...(upgrade ? { upgrade } : {}),
|
|
619
643
|
};
|
|
620
644
|
}
|
|
621
645
|
|
|
622
|
-
return { status: response.status, headers: responseHeaders, data: finalData, ...(refreshed ? { refreshed } : {}), ...(contractWarnings ? { contractWarnings } : {}) };
|
|
646
|
+
return { status: response.status, headers: responseHeaders, data: finalData, ...(refreshed ? { refreshed } : {}), ...(contractWarnings ? { contractWarnings } : {}), ...(upgrade ? { upgrade } : {}) };
|
|
623
647
|
}
|
|
624
648
|
|
|
625
649
|
// --- Batch replay ---
|
package/src/skill/apis-guru.ts
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
// src/skill/apis-guru.ts
|
|
2
2
|
import { resolveAndValidateUrl } from '../skill/ssrf.js';
|
|
3
3
|
|
|
4
|
+
const MAX_SPEC_SIZE = 10 * 1024 * 1024; // 10 MB per spec
|
|
5
|
+
const MAX_LIST_SIZE = 100 * 1024 * 1024; // 100 MB for APIs.guru list
|
|
6
|
+
|
|
7
|
+
async function fetchWithSizeLimit(url: string, maxBytes: number, options?: RequestInit): Promise<string> {
|
|
8
|
+
const response = await fetch(url, {
|
|
9
|
+
signal: AbortSignal.timeout(30_000),
|
|
10
|
+
...options,
|
|
11
|
+
headers: { 'User-Agent': 'apitap-import/1.0', ...(options?.headers as Record<string, string> || {}) },
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
throw new Error(`HTTP ${response.status} ${response.statusText} for ${url}`);
|
|
15
|
+
}
|
|
16
|
+
const contentLength = response.headers.get('content-length');
|
|
17
|
+
if (contentLength && parseInt(contentLength, 10) > maxBytes) {
|
|
18
|
+
throw new Error(`Response too large: ${contentLength} bytes (limit: ${maxBytes})`);
|
|
19
|
+
}
|
|
20
|
+
const text = await response.text();
|
|
21
|
+
if (text.length > maxBytes) {
|
|
22
|
+
throw new Error(`Response body too large: ${text.length} bytes (limit: ${maxBytes})`);
|
|
23
|
+
}
|
|
24
|
+
return text;
|
|
25
|
+
}
|
|
26
|
+
|
|
4
27
|
export interface ApisGuruEntry {
|
|
5
28
|
apiId: string; // e.g., "twilio.com:api"
|
|
6
29
|
providerName: string; // e.g., "twilio.com"
|
|
@@ -121,15 +144,7 @@ export async function fetchApisGuruList(): Promise<ApisGuruEntry[]> {
|
|
|
121
144
|
if (!ssrf.safe) {
|
|
122
145
|
throw new Error(`SSRF check failed for APIs.guru list URL: ${ssrf.reason}`);
|
|
123
146
|
}
|
|
124
|
-
const
|
|
125
|
-
signal: AbortSignal.timeout(30_000),
|
|
126
|
-
});
|
|
127
|
-
if (!response.ok) {
|
|
128
|
-
throw new Error(
|
|
129
|
-
`Failed to fetch APIs.guru list: ${response.status} ${response.statusText}`,
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
const text = await response.text();
|
|
147
|
+
const text = await fetchWithSizeLimit(APIS_GURU_LIST_URL, MAX_LIST_SIZE);
|
|
133
148
|
try {
|
|
134
149
|
return parseApisGuruList(JSON.parse(text) as Record<string, any>);
|
|
135
150
|
} catch {
|
|
@@ -145,16 +160,7 @@ export async function fetchSpec(specUrl: string): Promise<Record<string, any>> {
|
|
|
145
160
|
if (!ssrf.safe) {
|
|
146
161
|
throw new Error(`SSRF check failed for spec URL ${specUrl}: ${ssrf.reason}`);
|
|
147
162
|
}
|
|
148
|
-
const
|
|
149
|
-
signal: AbortSignal.timeout(30_000),
|
|
150
|
-
redirect: 'error',
|
|
151
|
-
});
|
|
152
|
-
if (!response.ok) {
|
|
153
|
-
throw new Error(
|
|
154
|
-
`Failed to fetch spec at ${specUrl}: ${response.status} ${response.statusText}`,
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
const text = await response.text();
|
|
163
|
+
const text = await fetchWithSizeLimit(specUrl, MAX_SPEC_SIZE, { redirect: 'error' });
|
|
158
164
|
try {
|
|
159
165
|
return JSON.parse(text) as Record<string, any>;
|
|
160
166
|
} catch {
|
|
@@ -31,6 +31,14 @@ export function resolveRef(
|
|
|
31
31
|
return merged;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Handle oneOf/anyOf: use first option as representative
|
|
35
|
+
if (obj.oneOf && Array.isArray(obj.oneOf) && obj.oneOf.length > 0) {
|
|
36
|
+
return resolveRef(obj.oneOf[0], spec, new Set(visited), depth + 1);
|
|
37
|
+
}
|
|
38
|
+
if (obj.anyOf && Array.isArray(obj.anyOf) && obj.anyOf.length > 0) {
|
|
39
|
+
return resolveRef(obj.anyOf[0], spec, new Set(visited), depth + 1);
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
if (!obj.$ref) return obj;
|
|
35
43
|
|
|
36
44
|
const ref = obj.$ref as string;
|
package/src/skill/signing.ts
CHANGED
|
@@ -44,6 +44,20 @@ export function signSkillFile(skill: SkillFile, key: Buffer): SkillFile {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Sign a skill file with a specific provenance value.
|
|
49
|
+
* Use 'self' for captured files, 'imported-signed' for import-only files.
|
|
50
|
+
*/
|
|
51
|
+
export function signSkillFileAs(
|
|
52
|
+
skill: SkillFile,
|
|
53
|
+
key: Buffer,
|
|
54
|
+
provenance: 'self' | 'imported-signed',
|
|
55
|
+
): SkillFile {
|
|
56
|
+
const payload = canonicalize(skill);
|
|
57
|
+
const signature = hmacSign(payload, key);
|
|
58
|
+
return { ...skill, provenance, signature };
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
/**
|
|
48
62
|
* Verify a skill file's signature.
|
|
49
63
|
* Returns true if the signature is valid for the given key.
|
package/src/skill/store.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/skill/store.ts
|
|
2
|
-
import { readFile, writeFile, mkdir, readdir, access } from 'node:fs/promises';
|
|
2
|
+
import { readFile, writeFile, mkdir, readdir, access, rename } from 'node:fs/promises';
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import type { SkillFile, SkillSummary } from '../types.js';
|
|
@@ -40,7 +40,10 @@ export async function writeSkillFile(
|
|
|
40
40
|
await mkdir(skillsDir, { recursive: true, mode: 0o700 });
|
|
41
41
|
await ensureGitignore(skillsDir);
|
|
42
42
|
const filePath = skillPath(skill.domain, skillsDir);
|
|
43
|
-
|
|
43
|
+
const tmpPath = `${filePath}.${process.pid}.tmp`;
|
|
44
|
+
const content = JSON.stringify(skill, null, 2) + '\n';
|
|
45
|
+
await writeFile(tmpPath, content, { mode: 0o600 });
|
|
46
|
+
await rename(tmpPath, filePath);
|
|
44
47
|
return filePath;
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -114,8 +117,9 @@ export async function listSkillFiles(
|
|
|
114
117
|
let files: string[];
|
|
115
118
|
try {
|
|
116
119
|
files = await readdir(skillsDir);
|
|
117
|
-
} catch {
|
|
118
|
-
return [];
|
|
120
|
+
} catch (err: unknown) {
|
|
121
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
122
|
+
throw err;
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
const summaries: SkillSummary[] = [];
|
package/src/types.ts
CHANGED
|
@@ -162,7 +162,7 @@ export interface SkillFile {
|
|
|
162
162
|
endpointsEnriched: number;
|
|
163
163
|
}>;
|
|
164
164
|
};
|
|
165
|
-
provenance: 'self' | 'imported' | 'unsigned';
|
|
165
|
+
provenance: 'self' | 'imported' | 'imported-signed' | 'unsigned';
|
|
166
166
|
signature?: string;
|
|
167
167
|
auth?: SkillAuth; // v0.8: top-level auth config
|
|
168
168
|
}
|
|
@@ -215,7 +215,7 @@ export interface SkillSummary {
|
|
|
215
215
|
skillFile: string;
|
|
216
216
|
endpointCount: number;
|
|
217
217
|
capturedAt: string;
|
|
218
|
-
provenance: 'self' | 'imported' | 'unsigned';
|
|
218
|
+
provenance: 'self' | 'imported' | 'imported-signed' | 'unsigned';
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
// --- Discovery types (Milestone 2: Smart Discovery) ---
|