@vibe-agent-toolkit/resources 0.1.39-rc.9 → 0.1.40-rc.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.
Files changed (75) hide show
  1. package/dist/content-cache.d.ts +67 -0
  2. package/dist/content-cache.d.ts.map +1 -0
  3. package/dist/content-cache.js +160 -0
  4. package/dist/content-cache.js.map +1 -0
  5. package/dist/external-link-cache.d.ts +16 -2
  6. package/dist/external-link-cache.d.ts.map +1 -1
  7. package/dist/external-link-cache.js +43 -13
  8. package/dist/external-link-cache.js.map +1 -1
  9. package/dist/external-link-validator.d.ts +75 -2
  10. package/dist/external-link-validator.d.ts.map +1 -1
  11. package/dist/external-link-validator.js +184 -4
  12. package/dist/external-link-validator.js.map +1 -1
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +12 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/link-auth-classify.d.ts +34 -0
  18. package/dist/link-auth-classify.d.ts.map +1 -0
  19. package/dist/link-auth-classify.js +42 -0
  20. package/dist/link-auth-classify.js.map +1 -0
  21. package/dist/link-auth-config-build.d.ts +47 -0
  22. package/dist/link-auth-config-build.d.ts.map +1 -0
  23. package/dist/link-auth-config-build.js +86 -0
  24. package/dist/link-auth-config-build.js.map +1 -0
  25. package/dist/link-auth-content-fetch.d.ts +75 -0
  26. package/dist/link-auth-content-fetch.d.ts.map +1 -0
  27. package/dist/link-auth-content-fetch.js +101 -0
  28. package/dist/link-auth-content-fetch.js.map +1 -0
  29. package/dist/link-auth-deps-memo.d.ts +40 -0
  30. package/dist/link-auth-deps-memo.d.ts.map +1 -0
  31. package/dist/link-auth-deps-memo.js +47 -0
  32. package/dist/link-auth-deps-memo.js.map +1 -0
  33. package/dist/link-auth-transport.d.ts +44 -0
  34. package/dist/link-auth-transport.d.ts.map +1 -0
  35. package/dist/link-auth-transport.js +151 -0
  36. package/dist/link-auth-transport.js.map +1 -0
  37. package/dist/link-parser.d.ts.map +1 -1
  38. package/dist/link-parser.js +8 -1
  39. package/dist/link-parser.js.map +1 -1
  40. package/dist/link-validator.d.ts +8 -8
  41. package/dist/link-validator.d.ts.map +1 -1
  42. package/dist/link-validator.js +51 -18
  43. package/dist/link-validator.js.map +1 -1
  44. package/dist/resource-registry.d.ts +2 -0
  45. package/dist/resource-registry.d.ts.map +1 -1
  46. package/dist/resource-registry.js +19 -4
  47. package/dist/resource-registry.js.map +1 -1
  48. package/dist/schemas/link-auth.d.ts +1049 -303
  49. package/dist/schemas/link-auth.d.ts.map +1 -1
  50. package/dist/schemas/link-auth.js +39 -20
  51. package/dist/schemas/link-auth.js.map +1 -1
  52. package/dist/schemas/project-config.d.ts +2860 -759
  53. package/dist/schemas/project-config.d.ts.map +1 -1
  54. package/dist/schemas/project-config.js +18 -1
  55. package/dist/schemas/project-config.js.map +1 -1
  56. package/dist/schemas/resource-metadata.d.ts +10 -9
  57. package/dist/schemas/resource-metadata.d.ts.map +1 -1
  58. package/dist/schemas/resource-metadata.js +2 -0
  59. package/dist/schemas/resource-metadata.js.map +1 -1
  60. package/package.json +4 -4
  61. package/src/content-cache.ts +190 -0
  62. package/src/external-link-cache.ts +50 -13
  63. package/src/external-link-validator.ts +268 -5
  64. package/src/index.ts +23 -0
  65. package/src/link-auth-classify.ts +61 -0
  66. package/src/link-auth-config-build.ts +130 -0
  67. package/src/link-auth-content-fetch.ts +146 -0
  68. package/src/link-auth-deps-memo.ts +56 -0
  69. package/src/link-auth-transport.ts +179 -0
  70. package/src/link-parser.ts +8 -1
  71. package/src/link-validator.ts +65 -17
  72. package/src/resource-registry.ts +25 -5
  73. package/src/schemas/link-auth.ts +47 -21
  74. package/src/schemas/project-config.ts +22 -1
  75. 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":"AAKA;;GAEG;AACH,UAAU,UAAU;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CAClB;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;;OAEG;YACW,SAAS;IAsBvB;;OAEG;YACW,SAAS;IAWvB;;;;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;IAkBlD;;;;;;OAMG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAahF;;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
+ {"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 (error) {
59
- // Handle missing file or corrupted JSON - start with empty cache
60
- if (error.code === 'ENOENT' || error instanceof SyntaxError) {
61
- this.cache = {};
62
- return this.cache;
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
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- cacheDir is constructor parameter, controlled by caller
75
- await fs.mkdir(this.cacheDir, { recursive: true });
76
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- cacheFile is derived from cacheDir
77
- await fs.writeFile(this.cacheFile, JSON.stringify(this.cache, null, 2), 'utf-8');
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":"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"}
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 the validation cache
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":"AAyCA;;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;CACnB;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;CAChB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,qBAAqB;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyC;IAEjE;;;;;OAKG;gBACS,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,4BAAiC;IAaxE;;;;;OAKG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAsC9D;;;;;OAKG;IACG,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAIpE;;OAEG;YACW,SAAS;IAkEvB;;OAEG;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;CAGlE"}
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"}