@apitap/core 1.4.1 → 1.4.3
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 +73 -12
- package/dist/cli.js +136 -1
- package/dist/cli.js.map +1 -1
- package/dist/index/reader.d.ts +36 -0
- package/dist/index/reader.js +32 -0
- package/dist/index/reader.js.map +1 -0
- package/dist/mcp.js +13 -3
- package/dist/mcp.js.map +1 -1
- package/dist/native-host.d.ts +2 -1
- package/dist/native-host.js +24 -1
- package/dist/native-host.js.map +1 -1
- package/dist/orchestration/browse.js +2 -0
- package/dist/orchestration/browse.js.map +1 -1
- package/dist/read/peek.js +15 -0
- package/dist/read/peek.js.map +1 -1
- package/dist/replay/engine.js +1 -1
- package/dist/replay/engine.js.map +1 -1
- package/dist/skill/store.js +11 -1
- package/dist/skill/store.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +144 -1
- package/src/index/reader.ts +65 -0
- package/src/mcp.ts +9 -3
- package/src/native-host.ts +29 -2
- package/src/orchestration/browse.ts +2 -0
- package/src/read/peek.ts +16 -0
- package/src/replay/engine.ts +1 -1
- package/src/skill/store.ts +11 -1
package/src/cli.ts
CHANGED
|
@@ -17,11 +17,13 @@ import { buildInspectReport, formatInspectHuman } from './inspect/report.js';
|
|
|
17
17
|
import { generateStatsReport, formatStatsHuman } from './stats/report.js';
|
|
18
18
|
import { detectAntiBot, type AntiBotSignal } from './capture/anti-bot.js';
|
|
19
19
|
import { discover } from './discovery/index.js';
|
|
20
|
+
import { readIndexEntry } from './index/reader.js';
|
|
20
21
|
import { peek } from './read/peek.js';
|
|
21
22
|
import { read } from './read/index.js';
|
|
22
23
|
import { homedir } from 'node:os';
|
|
23
24
|
import { join, resolve, dirname } from 'node:path';
|
|
24
25
|
import { readFileSync } from 'node:fs';
|
|
26
|
+
import { stat, unlink } from 'node:fs/promises';
|
|
25
27
|
import { fileURLToPath } from 'node:url';
|
|
26
28
|
import { createMcpServer } from './mcp.js';
|
|
27
29
|
|
|
@@ -79,6 +81,8 @@ function printUsage(): void {
|
|
|
79
81
|
apitap browse <url> Browse a URL (discover + replay in one step)
|
|
80
82
|
apitap peek <url> Zero-cost triage (HEAD only)
|
|
81
83
|
apitap read <url> Extract content without a browser
|
|
84
|
+
apitap audit Audit stored skill files and credentials
|
|
85
|
+
apitap forget <domain> Remove skill file and credentials for a domain
|
|
82
86
|
apitap stats Show token savings report
|
|
83
87
|
apitap extension install Register native messaging host for Chrome
|
|
84
88
|
|
|
@@ -847,8 +851,17 @@ async function handleDiscover(positional: string[], flags: Record<string, string
|
|
|
847
851
|
|
|
848
852
|
const result = await discover(url);
|
|
849
853
|
|
|
854
|
+
// Look up passive index data for this domain
|
|
855
|
+
let domain: string;
|
|
856
|
+
try {
|
|
857
|
+
domain = new URL(fullDiscoverUrl).hostname;
|
|
858
|
+
} catch {
|
|
859
|
+
domain = url;
|
|
860
|
+
}
|
|
861
|
+
const indexEntry = await readIndexEntry(domain);
|
|
862
|
+
|
|
850
863
|
if (json) {
|
|
851
|
-
console.log(JSON.stringify(result, null, 2));
|
|
864
|
+
console.log(JSON.stringify({ ...result, indexEntry: indexEntry ?? undefined }, null, 2));
|
|
852
865
|
} else {
|
|
853
866
|
// Confidence summary
|
|
854
867
|
const confidenceLabels: Record<string, string> = {
|
|
@@ -895,6 +908,24 @@ async function handleDiscover(positional: string[], flags: Record<string, string
|
|
|
895
908
|
console.log(`\n Skill file: ${result.skillFile.endpoints.length} endpoints predicted`);
|
|
896
909
|
}
|
|
897
910
|
|
|
911
|
+
if (indexEntry) {
|
|
912
|
+
console.log(`\n Passive Index (from browser extension):`);
|
|
913
|
+
console.log(` Domain: ${indexEntry.domain}`);
|
|
914
|
+
console.log(` Total hits: ${indexEntry.totalHits}`);
|
|
915
|
+
console.log(` Endpoints: ${indexEntry.endpoints.length}`);
|
|
916
|
+
console.log(` Promoted: ${indexEntry.promoted ? 'yes' : 'no'}`);
|
|
917
|
+
if (indexEntry.endpoints.length > 0) {
|
|
918
|
+
console.log(` Observed endpoints:`);
|
|
919
|
+
for (const ep of indexEntry.endpoints) {
|
|
920
|
+
const methods = ep.methods.join(', ');
|
|
921
|
+
const auth = ep.authType ? ` [${ep.authType}]` : '';
|
|
922
|
+
const pagination = ep.pagination ? ` (${ep.pagination})` : '';
|
|
923
|
+
const gql = ep.type === 'graphql' ? ' [GraphQL]' : '';
|
|
924
|
+
console.log(` ${methods} ${ep.path} \u2014 ${ep.hits} hits${auth}${pagination}${gql}`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
898
929
|
if (result.confidence === 'none') {
|
|
899
930
|
console.log(`\n Recommendation: Run \`apitap capture ${url}\` for browser-based discovery`);
|
|
900
931
|
}
|
|
@@ -1050,6 +1081,112 @@ async function handleRead(positional: string[], flags: Record<string, string | b
|
|
|
1050
1081
|
console.log();
|
|
1051
1082
|
}
|
|
1052
1083
|
|
|
1084
|
+
async function handleAudit(flags: Record<string, string | boolean>): Promise<void> {
|
|
1085
|
+
const skillsDir = SKILLS_DIR || join(APITAP_DIR, 'skills');
|
|
1086
|
+
const summaries = await listSkillFiles(skillsDir);
|
|
1087
|
+
const machineId = await getEffectiveMachineId();
|
|
1088
|
+
const authManager = new AuthManager(APITAP_DIR, machineId);
|
|
1089
|
+
const authDomains = new Set(await authManager.listDomains());
|
|
1090
|
+
|
|
1091
|
+
// Get skill file last-modified dates
|
|
1092
|
+
const rows: { domain: string; endpoints: number; auth: string; modified: string }[] = [];
|
|
1093
|
+
for (const s of summaries) {
|
|
1094
|
+
let modified = 'unknown';
|
|
1095
|
+
try {
|
|
1096
|
+
const st = await stat(s.skillFile);
|
|
1097
|
+
modified = st.mtime.toISOString().slice(0, 10);
|
|
1098
|
+
} catch {}
|
|
1099
|
+
rows.push({
|
|
1100
|
+
domain: s.domain,
|
|
1101
|
+
endpoints: s.endpointCount,
|
|
1102
|
+
auth: authDomains.has(s.domain) ? 'yes' : 'no',
|
|
1103
|
+
modified,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Get auth.enc last-modified
|
|
1108
|
+
let authModified = 'none';
|
|
1109
|
+
try {
|
|
1110
|
+
const authStat = await stat(join(APITAP_DIR, 'auth.enc'));
|
|
1111
|
+
authModified = authStat.mtime.toISOString().slice(0, 10);
|
|
1112
|
+
} catch {}
|
|
1113
|
+
|
|
1114
|
+
if (flags.json === true) {
|
|
1115
|
+
console.log(JSON.stringify({ domains: rows, authFileModified: authModified }, null, 2));
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
if (rows.length === 0) {
|
|
1120
|
+
console.log('\n No skill files found.\n');
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Table header
|
|
1125
|
+
const domW = Math.max(6, ...rows.map(r => r.domain.length));
|
|
1126
|
+
console.log();
|
|
1127
|
+
console.log(` ${'DOMAIN'.padEnd(domW)} ${'ENDPOINTS'.padStart(9)} ${'AUTH'.padEnd(4)} MODIFIED`);
|
|
1128
|
+
console.log(` ${''.padEnd(domW, '-')} ${''.padEnd(9, '-')} ${''.padEnd(4, '-')} ${''.padEnd(10, '-')}`);
|
|
1129
|
+
for (const r of rows) {
|
|
1130
|
+
console.log(` ${r.domain.padEnd(domW)} ${String(r.endpoints).padStart(9)} ${r.auth.padEnd(4)} ${r.modified}`);
|
|
1131
|
+
}
|
|
1132
|
+
console.log();
|
|
1133
|
+
console.log(` auth.enc last modified: ${authModified}`);
|
|
1134
|
+
console.log();
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
async function handleForget(positional: string[]): Promise<void> {
|
|
1138
|
+
const domain = positional[0];
|
|
1139
|
+
if (!domain) {
|
|
1140
|
+
console.error('Error: Domain required. Usage: apitap forget <domain>');
|
|
1141
|
+
process.exit(1);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(domain) || domain.includes('..')) {
|
|
1145
|
+
console.error('Error: Invalid domain name');
|
|
1146
|
+
process.exit(1);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const skillsDir = SKILLS_DIR || join(APITAP_DIR, 'skills');
|
|
1150
|
+
let skillRemoved = false;
|
|
1151
|
+
let authRemoved = false;
|
|
1152
|
+
let notFound = true;
|
|
1153
|
+
|
|
1154
|
+
// Remove skill file
|
|
1155
|
+
const skillFilePath = join(skillsDir, `${domain}.json`);
|
|
1156
|
+
try {
|
|
1157
|
+
await stat(skillFilePath);
|
|
1158
|
+
notFound = false;
|
|
1159
|
+
await unlink(skillFilePath);
|
|
1160
|
+
skillRemoved = true;
|
|
1161
|
+
} catch {}
|
|
1162
|
+
|
|
1163
|
+
// Remove stored auth
|
|
1164
|
+
try {
|
|
1165
|
+
const machineId = await getEffectiveMachineId();
|
|
1166
|
+
const authManager = new AuthManager(APITAP_DIR, machineId);
|
|
1167
|
+
const hasAuth = await authManager.has(domain);
|
|
1168
|
+
if (hasAuth) {
|
|
1169
|
+
notFound = false;
|
|
1170
|
+
await authManager.clear(domain);
|
|
1171
|
+
authRemoved = true;
|
|
1172
|
+
}
|
|
1173
|
+
} catch (err: any) {
|
|
1174
|
+
if (skillRemoved) {
|
|
1175
|
+
console.error(`Warning: Could not remove auth credentials: ${err.message}`);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (notFound) {
|
|
1180
|
+
console.log(`${domain} not found`);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const parts: string[] = [];
|
|
1185
|
+
if (skillRemoved) parts.push('skill file');
|
|
1186
|
+
if (authRemoved) parts.push('credentials');
|
|
1187
|
+
console.log(`Forgot ${domain} — ${parts.join(' and ')} removed`);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1053
1190
|
async function handleExtension(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
|
|
1054
1191
|
const subcommand = positional[0];
|
|
1055
1192
|
|
|
@@ -1150,6 +1287,12 @@ async function main(): Promise<void> {
|
|
|
1150
1287
|
case 'read':
|
|
1151
1288
|
await handleRead(positional, flags);
|
|
1152
1289
|
break;
|
|
1290
|
+
case 'audit':
|
|
1291
|
+
await handleAudit(flags);
|
|
1292
|
+
break;
|
|
1293
|
+
case 'forget':
|
|
1294
|
+
await handleForget(positional);
|
|
1295
|
+
break;
|
|
1153
1296
|
case 'extension':
|
|
1154
1297
|
await handleExtension(positional, flags);
|
|
1155
1298
|
break;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
|
|
5
|
+
// Types re-declared here (extension types can't be imported due to different tsconfig).
|
|
6
|
+
// Keep in sync with extension/src/types.ts IndexFile/IndexEntry/IndexEndpoint.
|
|
7
|
+
export interface IndexFile {
|
|
8
|
+
v: 1;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
entries: IndexEntry[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface IndexEntry {
|
|
14
|
+
domain: string;
|
|
15
|
+
firstSeen: string;
|
|
16
|
+
lastSeen: string;
|
|
17
|
+
totalHits: number;
|
|
18
|
+
promoted: boolean;
|
|
19
|
+
lastPromoted?: string;
|
|
20
|
+
skillFileSource?: 'extension' | 'cli';
|
|
21
|
+
endpoints: IndexEndpoint[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface IndexEndpoint {
|
|
25
|
+
path: string;
|
|
26
|
+
methods: string[];
|
|
27
|
+
authType?: string;
|
|
28
|
+
hasBody: boolean;
|
|
29
|
+
hits: number;
|
|
30
|
+
lastSeen: string;
|
|
31
|
+
pagination?: string;
|
|
32
|
+
type?: 'graphql';
|
|
33
|
+
queryParamNames?: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const DEFAULT_APITAP_DIR = path.join(os.homedir(), '.apitap');
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Read the full passive index from disk.
|
|
40
|
+
* Returns null if index.json doesn't exist or is invalid.
|
|
41
|
+
*/
|
|
42
|
+
export async function readIndex(apitapDir: string = DEFAULT_APITAP_DIR): Promise<IndexFile | null> {
|
|
43
|
+
const indexPath = path.join(apitapDir, 'index.json');
|
|
44
|
+
try {
|
|
45
|
+
const raw = await fs.readFile(indexPath, 'utf-8');
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
if (parsed.v !== 1 || !Array.isArray(parsed.entries)) return null;
|
|
48
|
+
return parsed as IndexFile;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read a single domain's index entry.
|
|
56
|
+
* Returns null if the domain is not in the index.
|
|
57
|
+
*/
|
|
58
|
+
export async function readIndexEntry(
|
|
59
|
+
domain: string,
|
|
60
|
+
apitapDir: string = DEFAULT_APITAP_DIR,
|
|
61
|
+
): Promise<IndexEntry | null> {
|
|
62
|
+
const index = await readIndex(apitapDir);
|
|
63
|
+
if (!index) return null;
|
|
64
|
+
return index.entries.find(e => e.domain === domain) ?? null;
|
|
65
|
+
}
|
package/src/mcp.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { deriveSigningKey } from './auth/crypto.js';
|
|
|
11
11
|
import { requestAuth } from './auth/handoff.js';
|
|
12
12
|
import { CaptureSession } from './capture/session.js';
|
|
13
13
|
import { discover } from './discovery/index.js';
|
|
14
|
+
import { readIndexEntry } from './index/reader.js';
|
|
14
15
|
import { SessionCache } from './orchestration/cache.js';
|
|
15
16
|
import { peek } from './read/peek.js';
|
|
16
17
|
import { read } from './read/index.js';
|
|
@@ -139,6 +140,11 @@ export function createMcpServer(options: McpServerOptions = {}): McpServer {
|
|
|
139
140
|
}
|
|
140
141
|
const result = await discover(url);
|
|
141
142
|
|
|
143
|
+
// Look up passive index data for this domain
|
|
144
|
+
let domain: string;
|
|
145
|
+
try { domain = new URL(url).hostname; } catch { domain = url; }
|
|
146
|
+
const indexEntry = await readIndexEntry(domain);
|
|
147
|
+
|
|
142
148
|
// If we got a skill file, sign and save it automatically
|
|
143
149
|
if (result.skillFile && (result.confidence === 'high' || result.confidence === 'medium')) {
|
|
144
150
|
const { writeSkillFile } = await import('./skill/store.js');
|
|
@@ -147,10 +153,10 @@ export function createMcpServer(options: McpServerOptions = {}): McpServer {
|
|
|
147
153
|
const sigKey = deriveSigningKey(machineId);
|
|
148
154
|
result.skillFile = signSkillFile(result.skillFile, sigKey);
|
|
149
155
|
const path = await writeSkillFile(result.skillFile, skillsDir);
|
|
150
|
-
return wrapExternalContent({ ...result, savedTo: path }, 'apitap_discover');
|
|
156
|
+
return wrapExternalContent({ ...result, savedTo: path, indexEntry: indexEntry ?? undefined }, 'apitap_discover');
|
|
151
157
|
}
|
|
152
158
|
|
|
153
|
-
return wrapExternalContent(result, 'apitap_discover');
|
|
159
|
+
return wrapExternalContent({ ...result, indexEntry: indexEntry ?? undefined }, 'apitap_discover');
|
|
154
160
|
} catch (err: any) {
|
|
155
161
|
return {
|
|
156
162
|
content: [{ type: 'text' as const, text: `Discovery failed: ${err.message}` }],
|
|
@@ -186,7 +192,7 @@ export function createMcpServer(options: McpServerOptions = {}): McpServer {
|
|
|
186
192
|
}
|
|
187
193
|
const machineId = await getMachineId();
|
|
188
194
|
const signingKey = deriveSigningKey(machineId);
|
|
189
|
-
const skill = await readSkillFile(domain, skillsDir, { verifySignature: true, signingKey });
|
|
195
|
+
const skill = await readSkillFile(domain, skillsDir, { verifySignature: true, signingKey, trustUnsigned: true });
|
|
190
196
|
if (!skill) {
|
|
191
197
|
return {
|
|
192
198
|
content: [{ type: 'text' as const, text: `No skill file found for "${domain}". Use apitap_capture to capture it first.` }],
|
package/src/native-host.ts
CHANGED
|
@@ -27,10 +27,11 @@ async function signSkillJson(skillJson: string): Promise<string> {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export interface NativeRequest {
|
|
30
|
-
action: 'save_skill' | 'save_batch' | 'ping' | 'capture_request';
|
|
30
|
+
action: 'save_skill' | 'save_batch' | 'ping' | 'capture_request' | 'save_index';
|
|
31
31
|
domain?: string;
|
|
32
32
|
skillJson?: string;
|
|
33
33
|
skills?: Array<{ domain: string; skillJson: string }>;
|
|
34
|
+
indexJson?: string;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export interface NativeResponse {
|
|
@@ -115,6 +116,32 @@ export async function handleNativeMessage(
|
|
|
115
116
|
return { success: true, paths };
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
if (request.action === 'save_index') {
|
|
120
|
+
if (!request.indexJson) {
|
|
121
|
+
return { success: false, error: 'Missing indexJson' };
|
|
122
|
+
}
|
|
123
|
+
if (request.indexJson.length > 5 * 1024 * 1024) {
|
|
124
|
+
return { success: false, error: 'Index too large (max 5MB)' };
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
JSON.parse(request.indexJson);
|
|
128
|
+
} catch {
|
|
129
|
+
return { success: false, error: 'Invalid JSON in indexJson' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// index.json lives in ~/.apitap/ (parent of skills dir)
|
|
133
|
+
const apitapDir = path.dirname(skillsDir);
|
|
134
|
+
await fs.mkdir(apitapDir, { recursive: true });
|
|
135
|
+
const indexPath = path.join(apitapDir, 'index.json');
|
|
136
|
+
|
|
137
|
+
// Atomic write: temp file + rename
|
|
138
|
+
const tmpPath = indexPath + '.tmp.' + process.pid;
|
|
139
|
+
await fs.writeFile(tmpPath, request.indexJson, { mode: 0o600 });
|
|
140
|
+
await fs.rename(tmpPath, indexPath);
|
|
141
|
+
|
|
142
|
+
return { success: true, path: indexPath };
|
|
143
|
+
}
|
|
144
|
+
|
|
118
145
|
return { success: false, error: `Unknown action: ${request.action}` };
|
|
119
146
|
} catch (err) {
|
|
120
147
|
return { success: false, error: String(err) };
|
|
@@ -124,7 +151,7 @@ export async function handleNativeMessage(
|
|
|
124
151
|
// --- Relay handler ---
|
|
125
152
|
|
|
126
153
|
// Actions handled locally by the native host (filesystem operations)
|
|
127
|
-
const LOCAL_ACTIONS = new Set(['save_skill', 'save_batch', 'ping']);
|
|
154
|
+
const LOCAL_ACTIONS = new Set(['save_skill', 'save_batch', 'ping', 'save_index']);
|
|
128
155
|
|
|
129
156
|
// Actions relayed to the extension (browser operations)
|
|
130
157
|
const EXTENSION_ACTIONS = new Set(['capture_request']);
|
|
@@ -315,6 +315,8 @@ export async function browse(
|
|
|
315
315
|
// Check content-type: HTML responses are not usable API data
|
|
316
316
|
const contentType = result.headers['content-type'] ?? '';
|
|
317
317
|
if (contentType.includes('text/html')) {
|
|
318
|
+
// Invalidate stale cache so next call reads fresh skill file from disk
|
|
319
|
+
cache?.invalidate(domain);
|
|
318
320
|
return {
|
|
319
321
|
success: false,
|
|
320
322
|
reason: 'non_api_response',
|
package/src/read/peek.ts
CHANGED
|
@@ -48,6 +48,22 @@ export async function peek(url: string, options: PeekOptions = {}): Promise<Peek
|
|
|
48
48
|
const contentType = headers['content-type'] || null;
|
|
49
49
|
const server = headers['server'] || null;
|
|
50
50
|
|
|
51
|
+
// JSON API responses with HTTP 200 are always accessible, regardless of CDN headers
|
|
52
|
+
if (status === 200 && contentType && contentType.includes('application/json')) {
|
|
53
|
+
const framework = detectFramework(headers, signals);
|
|
54
|
+
return {
|
|
55
|
+
url,
|
|
56
|
+
status,
|
|
57
|
+
accessible: true,
|
|
58
|
+
contentType,
|
|
59
|
+
server,
|
|
60
|
+
framework,
|
|
61
|
+
botProtection: null,
|
|
62
|
+
signals,
|
|
63
|
+
recommendation: 'read',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
51
67
|
// Detect bot protection
|
|
52
68
|
const botProtection = detectBotProtection(headers, signals);
|
|
53
69
|
|
package/src/replay/engine.ts
CHANGED
|
@@ -628,7 +628,7 @@ export async function replayMultiple(
|
|
|
628
628
|
const skillCache = new Map<string, SkillFile | null>();
|
|
629
629
|
const uniqueDomains = [...new Set(requests.map(r => r.domain))];
|
|
630
630
|
await Promise.all(uniqueDomains.map(async (domain) => {
|
|
631
|
-
const skill = await readSkillFile(domain, options.skillsDir, { verifySignature: true, signingKey });
|
|
631
|
+
const skill = await readSkillFile(domain, options.skillsDir, { verifySignature: true, signingKey, trustUnsigned: true });
|
|
632
632
|
skillCache.set(domain, skill);
|
|
633
633
|
}));
|
|
634
634
|
|
package/src/skill/store.ts
CHANGED
|
@@ -85,7 +85,17 @@ export async function readSkillFile(
|
|
|
85
85
|
}
|
|
86
86
|
} else {
|
|
87
87
|
const { verifySignature } = await import('./signing.js');
|
|
88
|
-
|
|
88
|
+
let verified = verifySignature(skill, signingKey);
|
|
89
|
+
if (!verified) {
|
|
90
|
+
// Fallback: try pre-v1.4.0 legacy key (before HKDF signing key separation)
|
|
91
|
+
// Files signed with deriveKey() directly (not deriveSigningKey()) will match here
|
|
92
|
+
const { deriveKey } = await import('../auth/crypto.js');
|
|
93
|
+
const { getMachineId } = await import('../auth/manager.js');
|
|
94
|
+
const machineId = await getMachineId();
|
|
95
|
+
const legacyKey = deriveKey(machineId);
|
|
96
|
+
verified = verifySignature(skill, legacyKey);
|
|
97
|
+
}
|
|
98
|
+
if (!verified) {
|
|
89
99
|
throw new Error(`Skill file signature verification failed for ${domain} — file may be tampered`);
|
|
90
100
|
}
|
|
91
101
|
}
|