Skip to content

feat(resources): linkAuth content-fetch primitive and content cache (#113 slice 3)#136

Merged
ejdutton merged 5 commits into
mainfrom
feat/113-linkauth-content-fetch-slice3
Jun 26, 2026
Merged

feat(resources): linkAuth content-fetch primitive and content cache (#113 slice 3)#136
ejdutton merged 5 commits into
mainfrom
feat/113-linkauth-content-fetch-slice3

Conversation

@ejdutton

Copy link
Copy Markdown
Collaborator

Summary

Slice 3 of issue #113 — the authenticated content-fetch primitive and per-entry disk cache that sit on top of the slice-1 engine and slice-2 validator wiring.

  • fetchAuthenticated(url, config, options) — public primitive that wires the engine, transport, and cache. Returns { bytes, metadata, cached } on success; { outcome: 'unverified' } or { outcome: 'unsupported' } on short-circuit (neither ever touches the cache, per §6.3).
  • ContentCache — per-entry two-file layout (<sha256(url)>.json + .bin). Writes .bin before .json (commit-marker discipline so a crash mid-write can't serve stale metadata against new bytes). pickMetadata() whitelist prevents any auth header from persisting to disk (§8). Fail-soft IO: ENOENT/EACCES/corrupted JSON all degrade to a cache miss. Schema versioning for forward-compat.
  • §6.2 dual-mode headers — engine now expands both auth.headers (health-check Accept) and fetch.headers (content-retrieval Accept) against the same token/capture context. Primitive merges them: { ...auth.headers, ...fetch.headers } so Accept is overridden while Authorization survives.
  • wrapLinkAuthDepsWithMemo — extracted from ExternalLinkValidator to a shared module so both the validator and the primitive can share a single per-argv Map memo, amortizing gh auth token subprocess cost across all calls in a run.
  • Cross-slice integration test — asserts that one adopter config bridged once feeds both ExternalLinkValidator (slice 2) and fetchAuthenticated (slice 3) coherently: same rewritten URL, correct per-slice Accept headers, token resolved exactly once across 3 cross-slice calls.

Depends on: PR #125 (slice 2 — the validator wiring). This branch builds on top of that branch's commits.

Test plan

  • All existing tests pass: bun run validate (includes unit, integration, system)
  • content-cache.test.ts — round-trip, TTL boundary (at-exactly-TTL valid, TTL+1ms miss), version-gate (missing/wrong version → miss), fail-soft IO (EACCES → miss, corrupted JSON → miss), pickMetadata() whitelist (smuggled Authorization not written to disk)
  • link-auth-content-fetch.test.ts — short-circuit outcomes (unsupported, unverified), fetch headers override (§6.2), cache integration (write-through, cache hit, forceRefresh, no-cache-unsupported, no-cache-unverified per §6.3), token never persisted (recursive readdir asserting token string absent), AbortSignal pass-through
  • linkauth-cross-slice.integration.test.ts — one config, one memo, both slices; 2 HTTP requests total (validator + fetch, cache hit = 0 requests); wire-level Accept headers match per-slice headers; runCommand called exactly once

🤖 Generated with Claude Code

ejdutton and others added 4 commits June 16, 2026 13:53
…#113 slice 2)

Wires the slice-1 pure engine into ExternalLinkValidator and adds the
five LINK_AUTH_* outcome codes. End-to-end: when an adopter sets
resources.linkAuth in vibe-agent-toolkit.config.yaml, vat resources
validate bypasses markdown-link-check for any URL whose host is claimed
by a provider, issues an authenticated fetch against the rewritten URL,
and classifies the response per design §7 into one of:

  LINK_AUTH_DEAD                    (404/410 from honest-404 hosts)
  LINK_AUTH_DEAD_OR_UNAUTHORIZED    (404 from ambiguous hosts like GitHub)
  LINK_AUTH_FORBIDDEN               (403)
  LINK_AUTH_UNAUTHORIZED            (401)
  LINK_AUTH_UNVERIFIED              (no token resolved)

New surface: fetchAuthenticated() with cross-origin Authorization
stripping (§8, sticky across chains) and 429/Retry-After honoring
(§5.2, 60s DoS cap + 250ms good-neighbor floor); pure classifier per
§7; project-config → engine bridge with post-expansion validation
against InlineProviderSchema (catches typo'd macro overrides); auth
cache keyed by rewritten URL and scoped to a per-OS-user subdirectory
(§6.3) with an explicit version field forward-compat for slice 3.

196 new tests across 6 files. Doc-anchor coverage iterator
(packages/agent-schema/test/docs/validation-codes.test.ts) iterates
CODE_REGISTRY and asserts each code has a matching docs section.

Adopter-visible breaking changes: the LinkAuthConfig type exported
from @vibe-agent-toolkit/resources renames to LinkAuthProjectConfig
(resolves auto-import ambiguity with the engine type of the same
name); the external-link cache layout adds an auth-${osUser}/
subdirectory and a version field that invalidates pre-existing
entries on first read.

Fixed: ExternalLinkValidator.clearCache()/getCacheStats() now operate
on both anonymous and authenticated caches (previously the auth cache
survived a manual clear, surfacing stale 401/403 entries after token
rotation).

Deferred to later slices: content-fetch primitive and content cache
(slice 3); cross-platform .cmd-shim system test, VAT_LINKAUTH_ALLOW_COMMAND=0
opt-out, and contributor docs (slice 4).

See CHANGELOG.md [Unreleased] for full details.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nCommand, fail-soft cache IO

Addresses jdutton's three review findings on PR #125:

1. Postel's Law: the adopter-facing linkAuth schemas (InlineProviderSchema,
   LinkAuthConfigSchema, and the five nested object schemas) were `.strict()`
   but parse external input. Switched all seven to `.passthrough()` to match
   the repo CLAUDE.md rule for adopter configs — forward-compatible / typo'd
   fields now degrade rather than crash `vat resources validate`. Adjusted
   the file header comment that misread Postel's Law, and updated four
   schema tests to assert the new passthrough semantics. The compile-time
   _KeysAgree drift check moved from `keyof z.infer<...>` to
   `keyof Schema.shape` so passthrough's index signature doesn't defeat it.
   Post-expansion validation still catches missing required fields and
   wrong types on declared fields (e.g. invalid notFoundMeaning enum value);
   typo-catching DX belongs in a separate lint pass.

2. Token memoization: ExternalLinkValidator now wraps the linkAuth deps'
   runCommand with a Map<JSON-argv, RunResult>, so validating N URLs from
   the same host runs `gh auth token` (or any command-source token) at
   most once per validator instance. Treats *all* token sources as
   potentially expensive per the review. Engine's DEFAULT_RUN_COMMAND
   exported as `defaultRunCommand` so the wrapper calls through to a
   single source of truth (avoids a duplicate-implementation jscpd clone).
   Two new tests pin the memoization (single provider → 1 call across N
   URLs; distinct providers → separate calls).

3. Fail-soft cache IO: ExternalLinkCache.loadCache() previously rethrew
   anything other than ENOENT/SyntaxError; saveCache() had no try/catch.
   An EACCES/EROFS on the cache file aborted the whole validate run.
   Both paths now swallow IO errors — read returns empty cache, write
   no-ops while the in-memory cache stays authoritative for the rest of
   the run. Two POSIX-skipped tests using chmod simulate EACCES on read
   and write.

CHANGELOG updated to reflect the final post-review behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…113 slice 3)

Implements §6.2 dual-mode headers, §6.3 content cache with 30-min TTL,
and the public fetchAuthenticated primitive that wires the engine,
transport, and cache together for authenticated binary content retrieval.

Key changes:
- rename link-auth-fetch.ts → link-auth-transport.ts; rename fetchAuthenticated
  → authTransport to free the name for the public primitive
- engine (resolve.ts): add Provider.fetch?, dual-expand fetch.headers alongside
  auth.headers against the same token/capture context (§6.2)
- engine: add LinkAuthConfig.cache? so cache.ttlMinutes rides the same config
  object without a second source of truth
- content-cache.ts: per-entry two-file layout (<sha256>.json + .bin), write .bin
  before .json (commit-marker discipline), pickMetadata() whitelist against
  token persistence (§8), fail-soft IO, schema versioning
- link-auth-deps-memo.ts: extract wrapLinkAuthDepsWithMemo from validator so
  both slice 2 and slice 3 share the same per-argv Map memoization
- link-auth-content-fetch.ts: public fetchAuthenticated(); Object.hasOwn
  discrimination, fetch.headers merged over auth.headers, unverified/unsupported
  never touch cache (§6.3), cache write-through after full body read
- cross-slice integration test: one adopter config + one shared memo feeds both
  ExternalLinkValidator and fetchAuthenticated; asserts wire-level Accept headers,
  token memo runs exactly once across 3 calls

Bumps version to v0.1.39-rc.7.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ejdutton ejdutton self-assigned this Jun 26, 2026
@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.21830% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.31%. Comparing base (1a37ffe) to head (ad095e0).

Files with missing lines Patch % Lines
packages/resources/src/external-link-validator.ts 89.65% 12 Missing ⚠️
packages/resources/src/resource-registry.ts 12.50% 7 Missing ⚠️
packages/resources/src/content-cache.ts 97.22% 2 Missing ⚠️
packages/resources/src/link-auth-transport.ts 97.77% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #136      +/-   ##
==========================================
+ Coverage   81.94%   82.31%   +0.36%     
==========================================
  Files         227      233       +6     
  Lines       17632    18097     +465     
  Branches     3468     3600     +132     
==========================================
+ Hits        14449    14897     +448     
- Misses       3183     3200      +17     
Files with missing lines Coverage Δ
packages/agent-schema/src/validation-codes.ts 100.00% <100.00%> (ø)
packages/resources/src/external-link-cache.ts 94.59% <100.00%> (+2.28%) ⬆️
packages/resources/src/link-auth-classify.ts 100.00% <100.00%> (ø)
packages/resources/src/link-auth-config-build.ts 100.00% <100.00%> (ø)
packages/resources/src/link-auth-content-fetch.ts 100.00% <100.00%> (ø)
packages/resources/src/link-auth-deps-memo.ts 100.00% <100.00%> (ø)
packages/utils/src/link-auth/resolve-token.ts 82.92% <100.00%> (ø)
packages/utils/src/link-auth/resolve.ts 100.00% <100.00%> (ø)
packages/resources/src/content-cache.ts 97.22% <97.22%> (ø)
packages/resources/src/link-auth-transport.ts 97.77% <97.77%> (ø)
... and 2 more
Files with missing lines Coverage Δ
packages/agent-schema/src/validation-codes.ts 100.00% <100.00%> (ø)
packages/resources/src/external-link-cache.ts 94.59% <100.00%> (+2.28%) ⬆️
packages/resources/src/link-auth-classify.ts 100.00% <100.00%> (ø)
packages/resources/src/link-auth-config-build.ts 100.00% <100.00%> (ø)
packages/resources/src/link-auth-content-fetch.ts 100.00% <100.00%> (ø)
packages/resources/src/link-auth-deps-memo.ts 100.00% <100.00%> (ø)
packages/utils/src/link-auth/resolve-token.ts 82.92% <100.00%> (ø)
packages/utils/src/link-auth/resolve.ts 100.00% <100.00%> (ø)
packages/resources/src/content-cache.ts 97.22% <97.22%> (ø)
packages/resources/src/link-auth-transport.ts 97.77% <97.77%> (ø)
... and 2 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ats, signal, defaultSleep

Covers Codecov-flagged patch lines (ported from slice 2):

- external-link-validator: catch block in validateAuthenticatedLink (Error,
  null/falsy, plain object, empty {}) — exercises all safeSerializeError branches
- external-link-validator: clearCache() and getCacheStats() happy paths
- external-link-validator: resolveOsUser() default path (no osUser option)
- link-auth-transport: signal pass-through to fetchImpl RequestInit
- link-auth-transport: defaultSleep body via fake timers (no sleep injection)
- link-auth-config-build: TypeError when use value is not a string

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ejdutton ejdutton merged commit 042657f into main Jun 26, 2026
7 checks passed
@ejdutton ejdutton deleted the feat/113-linkauth-content-fetch-slice3 branch June 26, 2026 20:22
@sonarqubecloud

Copy link
Copy Markdown

jdutton added a commit that referenced this pull request Jun 26, 2026
Integrate #136 (linkAuth content-fetch primitive + content cache) into the
skill-test branch ahead of PR #135.

Conflict resolution:
- package.json (×24): adopted main's version 0.1.39-rc.7 across all packages
  (the branch's rc.8–rc.13 were throwaway adopter-test RCs cut from this branch;
  no new version bump for this PR).
- CHANGELOG.md: kept both sets of [Unreleased] entries — main's canonical set
  (incl. linkAuth slices) plus the branch's skill-test-unique entries
  (vat skill test run/configure, files: glob+integrity, NON_PORTABLE_ASSET_REFERENCE,
  NON_PORTABLE_COMMAND, skill-authoring guidance, resolveSkillSource resolver).
- bun.lock: regenerated via bun install — branch's glob dep added, main's yaml dep
  retained.

bun run validate passes (3 phases, 14 steps, all green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gx1K1J2YJMfcisAqyCzaf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant