@vibe-agent-toolkit/resources 0.1.39-rc.9 → 0.1.39
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/content-cache.d.ts +67 -0
- package/dist/content-cache.d.ts.map +1 -0
- package/dist/content-cache.js +160 -0
- package/dist/content-cache.js.map +1 -0
- package/dist/external-link-cache.d.ts +16 -2
- package/dist/external-link-cache.d.ts.map +1 -1
- package/dist/external-link-cache.js +43 -13
- package/dist/external-link-cache.js.map +1 -1
- package/dist/external-link-validator.d.ts +75 -2
- package/dist/external-link-validator.d.ts.map +1 -1
- package/dist/external-link-validator.js +184 -4
- package/dist/external-link-validator.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/link-auth-classify.d.ts +34 -0
- package/dist/link-auth-classify.d.ts.map +1 -0
- package/dist/link-auth-classify.js +42 -0
- package/dist/link-auth-classify.js.map +1 -0
- package/dist/link-auth-config-build.d.ts +47 -0
- package/dist/link-auth-config-build.d.ts.map +1 -0
- package/dist/link-auth-config-build.js +86 -0
- package/dist/link-auth-config-build.js.map +1 -0
- package/dist/link-auth-content-fetch.d.ts +75 -0
- package/dist/link-auth-content-fetch.d.ts.map +1 -0
- package/dist/link-auth-content-fetch.js +101 -0
- package/dist/link-auth-content-fetch.js.map +1 -0
- package/dist/link-auth-deps-memo.d.ts +40 -0
- package/dist/link-auth-deps-memo.d.ts.map +1 -0
- package/dist/link-auth-deps-memo.js +47 -0
- package/dist/link-auth-deps-memo.js.map +1 -0
- package/dist/link-auth-transport.d.ts +44 -0
- package/dist/link-auth-transport.d.ts.map +1 -0
- package/dist/link-auth-transport.js +151 -0
- package/dist/link-auth-transport.js.map +1 -0
- package/dist/link-parser.d.ts.map +1 -1
- package/dist/link-parser.js +8 -1
- package/dist/link-parser.js.map +1 -1
- package/dist/link-validator.d.ts +8 -8
- package/dist/link-validator.d.ts.map +1 -1
- package/dist/link-validator.js +51 -18
- package/dist/link-validator.js.map +1 -1
- package/dist/resource-registry.d.ts +2 -0
- package/dist/resource-registry.d.ts.map +1 -1
- package/dist/resource-registry.js +19 -4
- package/dist/resource-registry.js.map +1 -1
- package/dist/schemas/link-auth.d.ts +1049 -303
- package/dist/schemas/link-auth.d.ts.map +1 -1
- package/dist/schemas/link-auth.js +39 -20
- package/dist/schemas/link-auth.js.map +1 -1
- package/dist/schemas/project-config.d.ts +2860 -759
- package/dist/schemas/project-config.d.ts.map +1 -1
- package/dist/schemas/project-config.js +18 -1
- package/dist/schemas/project-config.js.map +1 -1
- package/dist/schemas/resource-metadata.d.ts +10 -9
- package/dist/schemas/resource-metadata.d.ts.map +1 -1
- package/dist/schemas/resource-metadata.js +2 -0
- package/dist/schemas/resource-metadata.js.map +1 -1
- package/package.json +4 -4
- package/src/content-cache.ts +190 -0
- package/src/external-link-cache.ts +50 -13
- package/src/external-link-validator.ts +268 -5
- package/src/index.ts +23 -0
- package/src/link-auth-classify.ts +61 -0
- package/src/link-auth-config-build.ts +130 -0
- package/src/link-auth-content-fetch.ts +146 -0
- package/src/link-auth-deps-memo.ts +56 -0
- package/src/link-auth-transport.ts +179 -0
- package/src/link-parser.ts +8 -1
- package/src/link-validator.ts +65 -17
- package/src/resource-registry.ts +25 -5
- package/src/schemas/link-auth.ts +47 -21
- package/src/schemas/project-config.ts +22 -1
- package/src/schemas/resource-metadata.ts +2 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-entry content cache for the linkAuth content-fetch primitive (design
|
|
3
|
+
* issue #113 §6.2 + §6.3).
|
|
4
|
+
*
|
|
5
|
+
* Layout: under `cacheDir/` each entry is two files keyed by `sha256(url).hex`:
|
|
6
|
+
* - `<hash>.json` — metadata (status, content-type, etag/last-modified,
|
|
7
|
+
* fetchedAt, rewrittenUrl, schema version)
|
|
8
|
+
* - `<hash>.bin` — raw response bytes, binary-clean
|
|
9
|
+
*
|
|
10
|
+
* The split lets one entry read load only what we need: metadata for the
|
|
11
|
+
* TTL/version check, then the bytes only on a hit. A single-JSON layout would
|
|
12
|
+
* have meant base64-encoding bytes (~33% bloat) and reparsing every entry on
|
|
13
|
+
* every lookup. Two-file layout pays one extra `open()` per hit and scales
|
|
14
|
+
* better as content size grows.
|
|
15
|
+
*
|
|
16
|
+
* **Cross-user isolation is the caller's job** (§6.3) — pass a cacheDir
|
|
17
|
+
* already scoped to the OS user (e.g. `<cacheDir>/content/auth-<osUser>/`).
|
|
18
|
+
* The cache class itself only knows about the directory it was given.
|
|
19
|
+
*
|
|
20
|
+
* **TTL** defaults to 30 minutes per §6.3. The content-fetch primitive's
|
|
21
|
+
* `forceRefresh` option bypasses the cache; this class has no `clear()` —
|
|
22
|
+
* stale entries fall out naturally on TTL expiry, and `forceRefresh` covers
|
|
23
|
+
* "I authenticated as the wrong identity" per §6.3.
|
|
24
|
+
*
|
|
25
|
+
* **Fail-soft IO** per #125 review: ENOENT/EACCES/EROFS/corrupted-JSON all
|
|
26
|
+
* degrade to a miss (for reads) or a no-op (for writes). A non-persisted
|
|
27
|
+
* entry costs an extra fetch on the next run; an exception costs the whole
|
|
28
|
+
* current run.
|
|
29
|
+
*
|
|
30
|
+
* **Strict metadata shape**: `set()` writes only declared `ContentMetadata`
|
|
31
|
+
* fields, even if the caller passes structurally-typed extras. The fetch
|
|
32
|
+
* primitive is the authority on what may be persisted, and a cache that
|
|
33
|
+
* silently passes through unknown fields could leak a token value if the
|
|
34
|
+
* primitive (or future code) ever forgot.
|
|
35
|
+
*/
|
|
36
|
+
export interface ContentMetadata {
|
|
37
|
+
readonly status: number;
|
|
38
|
+
readonly contentType: string | null;
|
|
39
|
+
readonly etag: string | null;
|
|
40
|
+
readonly lastModified: string | null;
|
|
41
|
+
/** Epoch ms when the response was received (used for TTL evaluation). */
|
|
42
|
+
readonly fetchedAt: number;
|
|
43
|
+
/** The rewritten URL the bytes were fetched from (§6.3 cache-key discipline). */
|
|
44
|
+
readonly rewrittenUrl: string;
|
|
45
|
+
}
|
|
46
|
+
export interface ContentCacheEntry {
|
|
47
|
+
readonly bytes: Uint8Array;
|
|
48
|
+
readonly metadata: ContentMetadata;
|
|
49
|
+
}
|
|
50
|
+
export declare class ContentCache {
|
|
51
|
+
private readonly cacheDir;
|
|
52
|
+
private readonly ttlMs;
|
|
53
|
+
/**
|
|
54
|
+
* @param cacheDir - Already-OS-user-scoped directory for entries. Caller
|
|
55
|
+
* builds this; the class does not know about users or scoping.
|
|
56
|
+
* @param ttlMinutes - Time-to-live in minutes. Defaults to the §6.3 30-min
|
|
57
|
+
* "session" window.
|
|
58
|
+
*/
|
|
59
|
+
constructor(cacheDir: string, ttlMinutes?: number);
|
|
60
|
+
get(url: string): Promise<ContentCacheEntry | null>;
|
|
61
|
+
set(url: string, bytes: Uint8Array, metadata: ContentMetadata): Promise<void>;
|
|
62
|
+
private keyFor;
|
|
63
|
+
private isExpired;
|
|
64
|
+
private readMetadata;
|
|
65
|
+
private readBytes;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=content-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-cache.d.ts","sourceRoot":"","sources":["../src/content-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAiBH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;CACpC;AAMD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAE/B;;;;;OAKG;gBACS,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAE,MAA4B;IAKhE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAqBnD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAiCnF,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,SAAS;YAIH,YAAY;YAYZ,SAAS;CAWxB"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-entry content cache for the linkAuth content-fetch primitive (design
|
|
3
|
+
* issue #113 §6.2 + §6.3).
|
|
4
|
+
*
|
|
5
|
+
* Layout: under `cacheDir/` each entry is two files keyed by `sha256(url).hex`:
|
|
6
|
+
* - `<hash>.json` — metadata (status, content-type, etag/last-modified,
|
|
7
|
+
* fetchedAt, rewrittenUrl, schema version)
|
|
8
|
+
* - `<hash>.bin` — raw response bytes, binary-clean
|
|
9
|
+
*
|
|
10
|
+
* The split lets one entry read load only what we need: metadata for the
|
|
11
|
+
* TTL/version check, then the bytes only on a hit. A single-JSON layout would
|
|
12
|
+
* have meant base64-encoding bytes (~33% bloat) and reparsing every entry on
|
|
13
|
+
* every lookup. Two-file layout pays one extra `open()` per hit and scales
|
|
14
|
+
* better as content size grows.
|
|
15
|
+
*
|
|
16
|
+
* **Cross-user isolation is the caller's job** (§6.3) — pass a cacheDir
|
|
17
|
+
* already scoped to the OS user (e.g. `<cacheDir>/content/auth-<osUser>/`).
|
|
18
|
+
* The cache class itself only knows about the directory it was given.
|
|
19
|
+
*
|
|
20
|
+
* **TTL** defaults to 30 minutes per §6.3. The content-fetch primitive's
|
|
21
|
+
* `forceRefresh` option bypasses the cache; this class has no `clear()` —
|
|
22
|
+
* stale entries fall out naturally on TTL expiry, and `forceRefresh` covers
|
|
23
|
+
* "I authenticated as the wrong identity" per §6.3.
|
|
24
|
+
*
|
|
25
|
+
* **Fail-soft IO** per #125 review: ENOENT/EACCES/EROFS/corrupted-JSON all
|
|
26
|
+
* degrade to a miss (for reads) or a no-op (for writes). A non-persisted
|
|
27
|
+
* entry costs an extra fetch on the next run; an exception costs the whole
|
|
28
|
+
* current run.
|
|
29
|
+
*
|
|
30
|
+
* **Strict metadata shape**: `set()` writes only declared `ContentMetadata`
|
|
31
|
+
* fields, even if the caller passes structurally-typed extras. The fetch
|
|
32
|
+
* primitive is the authority on what may be persisted, and a cache that
|
|
33
|
+
* silently passes through unknown fields could leak a token value if the
|
|
34
|
+
* primitive (or future code) ever forgot.
|
|
35
|
+
*/
|
|
36
|
+
import { createHash } from 'node:crypto';
|
|
37
|
+
import { promises as fs } from 'node:fs';
|
|
38
|
+
import { safePath } from '@vibe-agent-toolkit/utils';
|
|
39
|
+
/**
|
|
40
|
+
* Cache schema version. Bump when `ContentMetadata` or the on-disk layout
|
|
41
|
+
* changes. Entries with a missing or non-matching version are read as a miss
|
|
42
|
+
* (rather than misparsed) — the next fetch repopulates them under the new
|
|
43
|
+
* schema.
|
|
44
|
+
*/
|
|
45
|
+
const CACHE_VERSION = 1;
|
|
46
|
+
const DEFAULT_TTL_MINUTES = 30;
|
|
47
|
+
export class ContentCache {
|
|
48
|
+
cacheDir;
|
|
49
|
+
ttlMs;
|
|
50
|
+
/**
|
|
51
|
+
* @param cacheDir - Already-OS-user-scoped directory for entries. Caller
|
|
52
|
+
* builds this; the class does not know about users or scoping.
|
|
53
|
+
* @param ttlMinutes - Time-to-live in minutes. Defaults to the §6.3 30-min
|
|
54
|
+
* "session" window.
|
|
55
|
+
*/
|
|
56
|
+
constructor(cacheDir, ttlMinutes = DEFAULT_TTL_MINUTES) {
|
|
57
|
+
this.cacheDir = cacheDir;
|
|
58
|
+
this.ttlMs = ttlMinutes * 60 * 1000;
|
|
59
|
+
}
|
|
60
|
+
async get(url) {
|
|
61
|
+
const key = this.keyFor(url);
|
|
62
|
+
const jsonPath = safePath.join(this.cacheDir, `${key}.json`);
|
|
63
|
+
const binPath = safePath.join(this.cacheDir, `${key}.bin`);
|
|
64
|
+
const stored = await this.readMetadata(jsonPath);
|
|
65
|
+
if (stored === null)
|
|
66
|
+
return null;
|
|
67
|
+
if (stored.version !== CACHE_VERSION)
|
|
68
|
+
return null;
|
|
69
|
+
if (this.isExpired(stored))
|
|
70
|
+
return null;
|
|
71
|
+
const bytes = await this.readBytes(binPath);
|
|
72
|
+
if (bytes === null)
|
|
73
|
+
return null;
|
|
74
|
+
// Strip the version field — that is an on-disk concern, not part of
|
|
75
|
+
// the caller-facing `ContentMetadata` contract.
|
|
76
|
+
return {
|
|
77
|
+
bytes,
|
|
78
|
+
metadata: pickMetadata(stored),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async set(url, bytes, metadata) {
|
|
82
|
+
const key = this.keyFor(url);
|
|
83
|
+
const jsonPath = safePath.join(this.cacheDir, `${key}.json`);
|
|
84
|
+
const binPath = safePath.join(this.cacheDir, `${key}.bin`);
|
|
85
|
+
// Whitelist exactly the declared ContentMetadata fields — defense in depth
|
|
86
|
+
// against a caller that smuggles extra structurally-typed fields, so a
|
|
87
|
+
// future regression in the primitive cannot accidentally persist a token
|
|
88
|
+
// through this cache.
|
|
89
|
+
const stored = { ...pickMetadata(metadata), version: CACHE_VERSION };
|
|
90
|
+
try {
|
|
91
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- cacheDir is constructor parameter, controlled by caller
|
|
92
|
+
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
93
|
+
// Write order matters: .bin first, then .json. The reader checks .json
|
|
94
|
+
// first (version + TTL); a partially-written entry where .bin lands but
|
|
95
|
+
// .json doesn't reads as a miss (no .json). The hazard we are avoiding
|
|
96
|
+
// is the reverse — a fresh .json with a stale .bin from a prior set()
|
|
97
|
+
// for the same URL would serve old bytes under new metadata. Writing
|
|
98
|
+
// .json second guarantees that whenever .json reflects the new entry,
|
|
99
|
+
// .bin already does too.
|
|
100
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- binPath derived from cacheDir
|
|
101
|
+
await fs.writeFile(binPath, Buffer.from(bytes));
|
|
102
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- jsonPath derived from cacheDir
|
|
103
|
+
await fs.writeFile(jsonPath, JSON.stringify(stored), 'utf-8');
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Fail-soft per #125 review: a write IO failure (EACCES on the dir,
|
|
107
|
+
// ENOSPC on the disk, EROFS on read-only mounts) becomes a no-op. The
|
|
108
|
+
// current run still has the fresh bytes; only the disk persistence is
|
|
109
|
+
// lost.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
keyFor(url) {
|
|
113
|
+
return createHash('sha256').update(url).digest('hex');
|
|
114
|
+
}
|
|
115
|
+
isExpired(stored) {
|
|
116
|
+
return Date.now() - stored.fetchedAt > this.ttlMs;
|
|
117
|
+
}
|
|
118
|
+
async readMetadata(jsonPath) {
|
|
119
|
+
try {
|
|
120
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- jsonPath derived from cacheDir
|
|
121
|
+
const raw = await fs.readFile(jsonPath, 'utf-8');
|
|
122
|
+
return JSON.parse(raw);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// ENOENT (never written), EACCES (perms revoked), SyntaxError
|
|
126
|
+
// (corrupted JSON) — all degrade to a miss. The next fetch repopulates.
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async readBytes(binPath) {
|
|
131
|
+
try {
|
|
132
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- binPath derived from cacheDir
|
|
133
|
+
const buf = await fs.readFile(binPath);
|
|
134
|
+
// Return a fresh Uint8Array view that does not alias the Node Buffer's
|
|
135
|
+
// underlying ArrayBuffer slab — callers may mutate or store the bytes.
|
|
136
|
+
return new Uint8Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Single source of truth for the on-disk metadata field list. Used in both
|
|
145
|
+
* `set()` (whitelist before write — strips smuggled fields, including the
|
|
146
|
+
* `version` field on round-trip) and `get()` (drop the `version` field before
|
|
147
|
+
* returning to the caller). Centralizing the list means future §8 audits
|
|
148
|
+
* only need to inspect one place.
|
|
149
|
+
*/
|
|
150
|
+
function pickMetadata(source) {
|
|
151
|
+
return {
|
|
152
|
+
status: source.status,
|
|
153
|
+
contentType: source.contentType,
|
|
154
|
+
etag: source.etag,
|
|
155
|
+
lastModified: source.lastModified,
|
|
156
|
+
fetchedAt: source.fetchedAt,
|
|
157
|
+
rewrittenUrl: source.rewrittenUrl,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=content-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-cache.js","sourceRoot":"","sources":["../src/content-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,12 @@ interface CacheEntry {
|
|
|
5
5
|
statusCode: number;
|
|
6
6
|
statusMessage: string;
|
|
7
7
|
timestamp: number;
|
|
8
|
+
/**
|
|
9
|
+
* Schema version. Optional for backwards-compat: legacy entries without
|
|
10
|
+
* `version` are treated as a cache miss, forcing a refetch. Reading on
|
|
11
|
+
* version mismatch produces a miss rather than a parse error.
|
|
12
|
+
*/
|
|
13
|
+
version?: number;
|
|
8
14
|
}
|
|
9
15
|
/**
|
|
10
16
|
* External link validation cache
|
|
@@ -42,11 +48,19 @@ export declare class ExternalLinkCache {
|
|
|
42
48
|
*/
|
|
43
49
|
constructor(cacheDir: string, ttlHours?: number);
|
|
44
50
|
/**
|
|
45
|
-
* Load cache from disk
|
|
51
|
+
* Load cache from disk. Fail-soft: any IO error (ENOENT, EACCES, EROFS,
|
|
52
|
+
* corrupted JSON, …) degrades to an empty cache instead of throwing.
|
|
53
|
+
*
|
|
54
|
+
* Why no error propagation: `vat resources validate` should keep running
|
|
55
|
+
* when the cache file isn't reachable (read-only filesystem, permission
|
|
56
|
+
* mismatch, full disk) — a missing cache costs an extra network round-trip,
|
|
57
|
+
* an exception costs the whole run. Per #125 review.
|
|
46
58
|
*/
|
|
47
59
|
private loadCache;
|
|
48
60
|
/**
|
|
49
|
-
* Save cache to disk
|
|
61
|
+
* Save cache to disk. Fail-soft: any IO error becomes a no-op instead of
|
|
62
|
+
* throwing. Same rationale as `loadCache` — a non-persisted entry costs an
|
|
63
|
+
* extra fetch on the next run, an exception costs the whole current run.
|
|
50
64
|
*/
|
|
51
65
|
private saveCache;
|
|
52
66
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"external-link-cache.d.ts","sourceRoot":"","sources":["../src/external-link-cache.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"external-link-cache.d.ts","sourceRoot":"","sources":["../src/external-link-cache.ts"],"names":[],"mappings":"AAaA;;GAEG;AACH,UAAU,UAAU;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AASD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,KAAK,CAA0B;IAEvC;;;;;OAKG;gBACS,QAAQ,EAAE,MAAM,EAAE,QAAQ,SAAK;IAM3C;;;;;;;;OAQG;YACW,SAAS;IAqBvB;;;;OAIG;YACW,SAAS;IAiBvB;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAiBpB;;;;OAIG;IACH,OAAO,CAAC,WAAW;IAKnB;;OAEG;IACH,OAAO,CAAC,SAAS;IAOjB;;;;;OAKG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IA2BlD;;;;;;OAMG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAchF;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAiB7D"}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import { safePath } from '@vibe-agent-toolkit/utils';
|
|
4
|
+
/**
|
|
5
|
+
* Cache schema version. Incremented when CacheEntry shape changes; entries
|
|
6
|
+
* written by an older code path that read as a different version are treated
|
|
7
|
+
* as cache misses (rather than misparsed) — forward-compat for slice 3's
|
|
8
|
+
* content-cache additions and any future entry-shape evolution.
|
|
9
|
+
*/
|
|
10
|
+
const CACHE_VERSION = 1;
|
|
4
11
|
/**
|
|
5
12
|
* External link validation cache
|
|
6
13
|
*
|
|
@@ -41,7 +48,13 @@ export class ExternalLinkCache {
|
|
|
41
48
|
this.cacheFile = safePath.join(cacheDir, 'external-links.json');
|
|
42
49
|
}
|
|
43
50
|
/**
|
|
44
|
-
* Load cache from disk
|
|
51
|
+
* Load cache from disk. Fail-soft: any IO error (ENOENT, EACCES, EROFS,
|
|
52
|
+
* corrupted JSON, …) degrades to an empty cache instead of throwing.
|
|
53
|
+
*
|
|
54
|
+
* Why no error propagation: `vat resources validate` should keep running
|
|
55
|
+
* when the cache file isn't reachable (read-only filesystem, permission
|
|
56
|
+
* mismatch, full disk) — a missing cache costs an extra network round-trip,
|
|
57
|
+
* an exception costs the whole run. Per #125 review.
|
|
45
58
|
*/
|
|
46
59
|
async loadCache() {
|
|
47
60
|
if (this.cache !== null) {
|
|
@@ -55,26 +68,34 @@ export class ExternalLinkCache {
|
|
|
55
68
|
this.cache = JSON.parse(data);
|
|
56
69
|
return this.cache;
|
|
57
70
|
}
|
|
58
|
-
catch
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
throw error;
|
|
71
|
+
catch {
|
|
72
|
+
// All IO and parse errors degrade to an empty cache. Subsequent
|
|
73
|
+
// reads see the same empty cache (this.cache is set), so we don't
|
|
74
|
+
// re-spam mkdir/read on every lookup within the same run.
|
|
75
|
+
this.cache = {};
|
|
76
|
+
return this.cache;
|
|
65
77
|
}
|
|
66
78
|
}
|
|
67
79
|
/**
|
|
68
|
-
* Save cache to disk
|
|
80
|
+
* Save cache to disk. Fail-soft: any IO error becomes a no-op instead of
|
|
81
|
+
* throwing. Same rationale as `loadCache` — a non-persisted entry costs an
|
|
82
|
+
* extra fetch on the next run, an exception costs the whole current run.
|
|
69
83
|
*/
|
|
70
84
|
async saveCache() {
|
|
71
85
|
if (this.cache === null) {
|
|
72
86
|
return;
|
|
73
87
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
try {
|
|
89
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- cacheDir is constructor parameter, controlled by caller
|
|
90
|
+
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
91
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- cacheFile is derived from cacheDir
|
|
92
|
+
await fs.writeFile(this.cacheFile, JSON.stringify(this.cache, null, 2), 'utf-8');
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// No-op on IO failure. The in-memory cache (`this.cache`) is still
|
|
96
|
+
// authoritative for the current run; only the disk persistence is
|
|
97
|
+
// lost.
|
|
98
|
+
}
|
|
78
99
|
}
|
|
79
100
|
/**
|
|
80
101
|
* Normalize URL for caching
|
|
@@ -129,6 +150,14 @@ export class ExternalLinkCache {
|
|
|
129
150
|
if (!entry) {
|
|
130
151
|
return null;
|
|
131
152
|
}
|
|
153
|
+
// Forward-compat: an entry written under a different (or missing)
|
|
154
|
+
// schema version is treated as a miss. Forces a refetch rather than
|
|
155
|
+
// silently misparsing a slice-3+ shape with slice-2 reader code.
|
|
156
|
+
if (entry.version !== CACHE_VERSION) {
|
|
157
|
+
delete cache[key];
|
|
158
|
+
await this.saveCache();
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
132
161
|
if (this.isExpired(entry)) {
|
|
133
162
|
delete cache[key];
|
|
134
163
|
await this.saveCache();
|
|
@@ -150,6 +179,7 @@ export class ExternalLinkCache {
|
|
|
150
179
|
statusCode,
|
|
151
180
|
statusMessage,
|
|
152
181
|
timestamp: Date.now(),
|
|
182
|
+
version: CACHE_VERSION,
|
|
153
183
|
};
|
|
154
184
|
await this.saveCache();
|
|
155
185
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"external-link-cache.js","sourceRoot":"","sources":["../src/external-link-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AAEzC,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"external-link-cache.js","sourceRoot":"","sources":["../src/external-link-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { IssueCode } from '@vibe-agent-toolkit/agent-schema';
|
|
2
|
+
import { type LinkAuthConfig } from '@vibe-agent-toolkit/utils';
|
|
3
|
+
import { type LinkAuthDeps } from './link-auth-deps-memo.js';
|
|
1
4
|
/**
|
|
2
5
|
* Configuration options for external link validation
|
|
3
6
|
*/
|
|
@@ -10,6 +13,38 @@ export interface ExternalLinkValidatorOptions {
|
|
|
10
13
|
retries?: number;
|
|
11
14
|
/** User agent string for requests (default: generic) */
|
|
12
15
|
userAgent?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Optional linkAuth config (per issue #113). When set, URLs whose host is
|
|
18
|
+
* claimed by a provider in this config bypass the anonymous markdown-link-check
|
|
19
|
+
* path and use an authenticated direct fetch instead. URLs no provider claims
|
|
20
|
+
* continue to use markdown-link-check.
|
|
21
|
+
*/
|
|
22
|
+
linkAuthConfig?: LinkAuthConfig;
|
|
23
|
+
/**
|
|
24
|
+
* Override the `fetch` implementation used by the authenticated branch.
|
|
25
|
+
* Defaults to `globalThis.fetch`. Tests inject a stub; advanced adopters
|
|
26
|
+
* may inject a wrapper for corporate proxies, custom TLS, or telemetry.
|
|
27
|
+
*/
|
|
28
|
+
fetchImpl?: typeof fetch;
|
|
29
|
+
/**
|
|
30
|
+
* Override the linkAuth engine's token-resolution dependencies (env map +
|
|
31
|
+
* runCommand). Defaults to reading `process.env` and running real commands
|
|
32
|
+
* via `safeExecSync`. Tests inject a deterministic env; advanced adopters
|
|
33
|
+
* usually leave unset.
|
|
34
|
+
*/
|
|
35
|
+
linkAuthDeps?: LinkAuthDeps;
|
|
36
|
+
/**
|
|
37
|
+
* Override the sleep used between 429 retries. Defaults to `setTimeout`.
|
|
38
|
+
* Test-only — production benefits from real wall-clock delay so Retry-After
|
|
39
|
+
* hints are honored.
|
|
40
|
+
*/
|
|
41
|
+
sleep?: (ms: number) => Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* OS user to scope the authenticated cache by (#113 §6.3). Defaults to
|
|
44
|
+
* `os.userInfo().username` with env-var fallbacks. Tests inject a fixed
|
|
45
|
+
* value; production callers omit.
|
|
46
|
+
*/
|
|
47
|
+
osUser?: string;
|
|
13
48
|
}
|
|
14
49
|
/**
|
|
15
50
|
* Result of validating a single external link
|
|
@@ -25,6 +60,21 @@ export interface LinkValidationResult {
|
|
|
25
60
|
error?: string;
|
|
26
61
|
/** Whether result came from cache */
|
|
27
62
|
cached: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Set when the authenticated branch classified the response (issue #113 §7).
|
|
65
|
+
* Consumers use this directly instead of mapping `statusCode` to a code, so
|
|
66
|
+
* `notFoundMeaning`-dependent routing (404 → `LINK_AUTH_DEAD` vs
|
|
67
|
+
* `LINK_AUTH_DEAD_OR_UNAUTHORIZED`) reflects the matched provider's config.
|
|
68
|
+
*
|
|
69
|
+
* Invariant: `code` is set iff the URL hit the authenticated branch AND
|
|
70
|
+
* the outcome maps to a `LINK_AUTH_*` code per §7 (`unverified`, or any
|
|
71
|
+
* classified status: `unauthorized`, `forbidden`, `dead`,
|
|
72
|
+
* `dead_or_unauthorized`). Unset on the anonymous markdown-link-check path,
|
|
73
|
+
* on `unsupported` (no provider claimed the host), and on classifier-`null`
|
|
74
|
+
* statuses (5xx, unclassified) where the consumer's status-code mapping
|
|
75
|
+
* (`EXTERNAL_URL_*`) is the correct fallback.
|
|
76
|
+
*/
|
|
77
|
+
code?: IssueCode;
|
|
28
78
|
}
|
|
29
79
|
/**
|
|
30
80
|
* Validates external URLs in markdown content
|
|
@@ -47,7 +97,18 @@ export interface LinkValidationResult {
|
|
|
47
97
|
*/
|
|
48
98
|
export declare class ExternalLinkValidator {
|
|
49
99
|
private readonly cache;
|
|
100
|
+
/**
|
|
101
|
+
* Auth-branch cache — scoped to a per-OS-user subdirectory of `cacheDir`
|
|
102
|
+
* (#113 §6.3) so two users on a shared machine never read each other's
|
|
103
|
+
* authenticated results. Distinct from `cache` (the anonymous cache, which
|
|
104
|
+
* stays shared across users for the markdown-link-check path).
|
|
105
|
+
*/
|
|
106
|
+
private readonly authCache;
|
|
50
107
|
private readonly options;
|
|
108
|
+
private readonly linkAuthConfig;
|
|
109
|
+
private readonly fetchImpl;
|
|
110
|
+
private readonly linkAuthDeps;
|
|
111
|
+
private readonly sleep;
|
|
51
112
|
/**
|
|
52
113
|
* Create a new external link validator
|
|
53
114
|
*
|
|
@@ -69,16 +130,28 @@ export declare class ExternalLinkValidator {
|
|
|
69
130
|
* @returns Array of validation results
|
|
70
131
|
*/
|
|
71
132
|
validateLinks(urls: string[]): Promise<LinkValidationResult[]>;
|
|
133
|
+
/**
|
|
134
|
+
* Issue an authenticated fetch for `originalUrl` using the engine's plan
|
|
135
|
+
* (rewritten URL + auth headers + provider's check config), classify the
|
|
136
|
+
* response per #113 §7, and write a status-cache entry. Cache is keyed by
|
|
137
|
+
* the *rewritten* URL (§6.3) — the original `blob/` URL 404s, so caching
|
|
138
|
+
* by original would poison results.
|
|
139
|
+
*/
|
|
140
|
+
private validateAuthenticatedLink;
|
|
72
141
|
/**
|
|
73
142
|
* Check a link using markdown-link-check
|
|
74
143
|
*/
|
|
75
144
|
private checkLink;
|
|
76
145
|
/**
|
|
77
|
-
* Clear
|
|
146
|
+
* Clear both validation caches (anonymous + authenticated).
|
|
147
|
+
*
|
|
148
|
+
* Adopters call this after rotating a token or invalidating link data —
|
|
149
|
+
* the operation must wipe both surfaces or stale auth results survive
|
|
150
|
+
* (a real bug: a 401 cached under the old token would haunt the new one).
|
|
78
151
|
*/
|
|
79
152
|
clearCache(): Promise<void>;
|
|
80
153
|
/**
|
|
81
|
-
* Get cache statistics
|
|
154
|
+
* Get combined cache statistics across both caches.
|
|
82
155
|
*/
|
|
83
156
|
getCacheStats(): Promise<{
|
|
84
157
|
total: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"external-link-validator.d.ts","sourceRoot":"","sources":["../src/external-link-validator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"external-link-validator.d.ts","sourceRoot":"","sources":["../src/external-link-validator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAGL,KAAK,cAAc,EAEpB,MAAM,2BAA2B,CAAC;AAKnC,OAAO,EAAE,KAAK,YAAY,EAA4B,MAAM,0BAA0B,CAAC;AAyHvF;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC5C,4DAA4D;IAC5D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B;;;;OAIG;IACH,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,iCAAiC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,0DAA0D;IAC1D,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC;IACvB,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,MAAM,EAAE,OAAO,CAAC;IAChB;;;;;;;;;;;;;OAaG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,qBAAqB;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;IAC9C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAKtB;IACF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8C;IAEpE;;;;;OAKG;gBACS,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,4BAAiC;IA4BxE;;;;;OAKG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IA6D9D;;;;;OAKG;IACG,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAIpE;;;;;;OAMG;YACW,yBAAyB;IAyDvC;;OAEG;YACW,SAAS;IAkEvB;;;;;;OAMG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAIlE"}
|