Ran the phase-closing 9-step live verification against the real
wulfconsulting.com hosted zone. All steps confirmed passing:
sync, create/update/delete round-trip, audit completeness, NS-write
rejection + genuine AWS failure logging, admin/user auth gating,
drift detection, and D-12/D-10 health-check behavior.
One real bug found and fixed along the way (commit 4da5664): TXT
record values weren't RFC-1035-quoted, so AWS rejected every TXT
create/update with InvalidCharacterString. Fixed in
route53-change-submit.ts, 4 regression tests added, retried
successfully.
Logged one pre-existing, unrelated finding: /admin/integrations page
load is slow overall, confirmed not specific to Route 53's own health
check (which is bounded to a few seconds) — likely another
integration's untimeouts liveCheck() probe. Deferred, not fixed.
24-VALIDATION.md's three Manual-Only Verifications rows updated with
observed outcomes; status marked complete.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
15 KiB
| phase | slug | status | nyquist_compliant | wave_0_complete | created | updated |
|---|---|---|---|---|---|---|
| 24 | aws-route-53-dns-sync-track-changes-crud-operations-full-aud | complete | true | true | 2026-08-05 | 2026-08-06 |
Phase 24 — Validation Strategy
Per-phase validation contract for feedback sampling during execution.
Test Infrastructure
| Property | Value |
|---|---|
| Framework | vitest 4.1.5 |
| Config file | vitest.config.ts (include: ['lib/**/*.test.ts'], environment: 'node', globals: false) |
| Quick run command | npx vitest run <touched-test-file> |
| Full suite command | npm test |
| Estimated runtime | ~10 seconds (small existing suite) |
Structural constraint driving the plan layout: vitest.config.ts includes only
lib/**/*.test.ts. Nothing under app/api/** can be unit-tested. Every plan therefore
places its testable logic (record-key derivation, drift classification, the D-01 allowlist,
AWS error sanitization, change-batch construction, propagation polling, NS comparison) in a
lib/services/ module that the route handlers call, rather than inline in a route file.
Sampling Rate
- After every task commit:
npx vitest run <touched-test-file> - After every plan wave:
npm test - Before
/gsd:verify-work: full suite green +npx tsc --noEmit --prettyclean - Max feedback latency: 15 seconds
Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---|---|---|---|---|---|---|---|---|---|
| 24-01-T1 | 24-01 | 1 | SC-3, SC-4, SC-5 | T-24-06, T-24-SC | Route 53 schema exists with source and status CHECK constraints; no AWS secret in committed .env |
schema/grep gate | grep -c 'CREATE TABLE IF NOT EXISTS route53_' migrations/102_route53_tables.sql = 4 and grep -c '^AWS_' .env = 0 |
➕ created by task | ⬜ pending |
| 24-01-T2 | 24-01 | 1 | SC-5 | T-24-06 | isRoute53Configured() false without creds; getRoute53Client() throws; no explicit credentials: object |
unit | npx vitest run lib/services/route53-factory.test.ts |
➕ Wave 0 (created by task) | ⬜ pending |
| 24-01-T3 | 24-01 | 1 | SC-5 | T-24-08, T-24-09 | BWS key names confirmed; creds present in container; DNS egress reachable; IAM least-privilege | manual / checkpoint | none automated — blocking checkpoint:human-verify |
❌ manual by necessity | ⬜ pending |
| 24-02-T1 | 24-02 | 2 | SC-1, SC-4 | — | Record-key derivation and recordset normalization are order-insensitive, so equal recordsets never register as drift | unit | npx vitest run lib/services/route53-record-key.test.ts |
➕ Wave 0 (created by task) | ⬜ pending |
| 24-02-T2 | 24-02 | 2 | SC-1 | T-24-03 | Pagination + soft-delete; errors logged as error.message only, never the raw AWS error object |
grep gate + typecheck | npx tsc --noEmit --pretty && test $(grep -c 'integration_settings' lib/services/route53-sync-service.ts) -eq 0 |
➕ created by task | ⬜ pending |
| 24-02-T3 | 24-02 | 2 | SC-4 | T-24-04, T-24-10 | Drift produces exactly one correctly-tagged sync_detected_drift history row per changed record; equal recordsets and initial import produce none |
unit | npx vitest run lib/services/route53-sync-service.test.ts |
➕ Wave 0 (created by task) | ⬜ pending |
| 24-03-T1 | 24-03 | 2 | SC-3 | T-24-01, T-24-03, T-24-12 | Closed six-type allowlist rejects NS/SOA/ns/CAA with 400; sanitizeAwsError redacts AKIA key ids, arn:aws:*, and 12-digit account ids |
unit | npx vitest run lib/services/route53-record-validation.test.ts |
➕ Wave 0 (created by task) | ⬜ pending |
| 24-03-T2 | 24-03 | 2 | SC-3 | T-24-04, T-24-07 | Audit row inserted status='pending' before any AWS call; markAuditFailed binds a sanitized message; mirror deletes are soft-only |
unit (mocked pg) | npx vitest run lib/services/route53-write-persistence.test.ts |
➕ Wave 0 (created by task) | ⬜ pending |
| 24-04-T1 | 24-04 | 2 | SC-6 (D-12) | T-24-13, T-24-15 | NS normalization is case/trailing-dot insensitive; empty authoritative list yields no false alarm; empty live answer yields a mismatch; setServers only on a dedicated Resolver instance |
unit | npx vitest run lib/services/route53-dns-delegation.test.ts |
➕ Wave 0 (created by task) | ⬜ pending |
| 24-04-T2 | 24-04 | 2 | SC-6 | T-24-03, T-24-16 | route53 present in the health aggregate; auth-probe errors sanitized; delegation-step failure cannot abort Promise.all |
integration (curl) + typecheck | npx tsc --noEmit --pretty && curl -s localhost:3100/api/admin/integration-health | grep -q '"key":"route53"' |
➕ modified by task | ⬜ pending |
| 24-05-T1 | 24-05 | 3 | SC-2 | T-24-01, T-24-19, T-24-20 | buildChangeBatch throws on NS/SOA (defence in depth); pollChangeStatus is bounded and stops after timeout; the 30-minute SDK waiter is absent |
unit | npx vitest run lib/services/route53-change-submit.test.ts |
➕ Wave 0 (created by task) | ⬜ pending |
| 24-05-T2 | 24-05 | 3 | SC-4, SC-6 | T-24-02, T-24-18 | Every read route gated by requireAuth(); all SQL parameter-bound; no integration_settings gating (D-10) |
grep gate + curl | test $(grep -rl 'requireAuth|requireAdmin' app/api/route53 | wc -l) -eq 4 and unauthenticated curl on /api/route53/zones returns 401 |
➕ created by task | ⬜ pending |
| 24-05-T3 | 24-05 | 3 | SC-2, SC-3 | T-24-01, T-24-02, T-24-03, T-24-04, T-24-17 | validateRecordWrite and createPendingAuditLog both precede submitRecordChange; history written only on success; failures return 502 with a sanitized message; no staged-approval state |
source-order gate + typecheck | npx tsc --noEmit --pretty && test $(grep -rc 'pending_approval|requiresConfirmation|confirmToken' app/api/route53/ | awk -F: '{s+=$2} END {print s}') -eq 0 |
➕ created by task | ⬜ pending |
| 24-05-T3m | 24-05 | 3 | SC-2 (D-04) | T-24-02 | requireAdmin() returns 403 for a user-role session hitting a write route directly |
manual / smoke | none automated — see Manual-Only table row 1 | ❌ manual by convention | ⬜ pending |
| 24-06-T1 | 24-06 | 3 | SC-1, SC-6 | T-24-21, T-24-22, T-24-24 | Both sync types dispatch via dynamic import and gate on isRoute53Configured() only, never on integration_settings (D-10) |
grep gate + typecheck | npx tsc --noEmit --pretty && test $(grep -A12 "config.sync_type === 'route53" lib/services/sync-scheduler.ts | grep -c integration_settings) -eq 0 |
➕ modified by task | ⬜ pending |
| 24-06-T2 | 24-06 | 3 | SC-6 | T-24-23 | Tile entry uses a valid COLOR_MAP key; logo asset contains no script or xlink:href |
grep gate + build | npm run build && test $(grep -ci 'script|xlink:href' public/logos/route53.svg) -eq 0 |
➕ created by task | ⬜ pending |
| 24-07-T1 | 24-07 | 4 | SC-4, SC-6 | T-24-25 | Page renders record values and history before/after as escaped text; no dangerouslySetInnerHTML; no SWR/react-query |
grep gate + build | npm run build && test $(grep -c 'dangerouslySetInnerHTML' app/admin/sync/route53/page.tsx) -eq 0 |
✅ created by task | ✅ green |
| 24-07-T2 | 24-07 | 4 | SC-2 | T-24-01, T-24-20, T-24-26 | Type selector offers only the six writable types; submit disabled while in flight; no approval-workflow state introduced | grep gate + build | npm run build && test $(grep -c "'NS'|'SOA'" components/admin/route53/record-editor-dialog.tsx) -eq 0 |
✅ created by task | ✅ green |
| 24-07-T3 | 24-07 | 4 | SC-1..SC-6 | all | Full live round-trip: sync, create/update/delete against real AWS, audit completeness, failure logging, auth gating, drift detection, D-12 health, D-10 display-only disable | manual / checkpoint | none automated — blocking checkpoint:human-verify, 9 steps |
❌ manual by necessity | ✅ green (all 9 steps passed against wulfconsulting.com; TXT-quoting bug found in step 2, fixed in 4da5664, retried successfully) |
Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky
Sampling continuity check: no 3 consecutive tasks lack an automated verify. The two checkpoint tasks (24-01-T3, 24-07-T3) are each adjacent to tasks carrying automated commands, and both are terminal within their plan.
Wave 0 Requirements
All Wave 0 test files are created by the task that needs them, inside the plan that owns the module under test — no separate scaffolding plan is required, because every test target is a new file rather than an extension of untested existing code.
lib/services/route53-factory.test.ts— plan 24-01 Task 2 (isRoute53Configured()branches)lib/services/route53-record-key.test.ts— plan 24-02 Task 1 (key derivation, normalization, order-insensitive equality, drift classification)lib/services/route53-sync-service.test.ts— plan 24-02 Task 3 (drift history row shape andsourcetagging)lib/services/route53-record-validation.test.ts— plan 24-03 Task 1 (D-01 closed allowlist, TTL/value shape, AWS error sanitizer)lib/services/route53-write-persistence.test.ts— plan 24-03 Task 2 (pending/committed/failed SQL contract, soft-delete only)lib/services/route53-dns-delegation.test.ts— plan 24-04 Task 1 (NS normalization + mismatch detection; first health-check test file in this codebase)lib/services/route53-change-submit.test.ts— plan 24-05 Task 1 (change-batch construction, retry classification, bounded poll)- No framework install needed — vitest is already configured project-wide
Manual-Only Verifications
| Behavior | Requirement | Covered By | Why Manual | Test Instructions |
|---|---|---|---|---|
| Write-route auth gating (401/403 for non-admin) | SC-2 / D-04 / T-24-02 | 24-07 Task 3, step 7 | requireAdmin()/requireAuth() are Better Auth session-dependent; this codebase has no precedent for testing route auth gating in isolation — historically verified by manual click-through (see 22-VERIFICATION.md) |
Sign in as a user-role account, POST /api/route53/zones/<zone>/records with a valid payload, confirm 403. Confirm GET /api/route53/zones still returns 200 for the same session. Repeat the POST as admin and confirm success. Observed: PASS. user-role session (colleen@wulfconsulting.com) got 403 "Forbidden - Admin access required" on write; GET /api/route53/zones returned 200 for the same session. super-admin session (lorentz@wulfconsulting.com) succeeded on the equivalent write. |
| Live AWS write-back round-trip (create/update/delete a real record) | SC-2 / SC-3 / SC-4 | 24-07 Task 3, steps 2-6 | Cannot be safely automated against a real AWS account/hosted zone in CI; requires a real Route 53 zone and live credentials | Using a disposable TXT record in a real hosted zone: create via the Pulse UI, confirm in the AWS console; update; delete. Confirm route53_audit_log has 3 committed rows with correct before/after and actor, and route53_record_history has 3 pulse_crud rows. Then force an AWS-side rejection (apex CNAME) and confirm a failed row with a sanitized error_message and an HTTP 502 response. Observed: PASS on retry. First create attempt failed with AWS's InvalidCharacterString (TXT values weren't RFC-1035-quoted) — real bug, fixed in 4da5664 (formatTxtValue() in route53-change-submit.ts), pulse-app rebuilt, retried successfully. Create/update/delete against pulse-phase24-test.wulfconsulting.com. all confirmed in the AWS console; route53_audit_log showed 3 committed rows (create/update/delete) attributed to lorentz@wulfconsulting.com. Apex-CNAME rejection confirmed 502 + a failed audit row with a clean, sanitized error_message (no account id/ARN/key id present). |
| DNS-egress-in-production assumption (D-12 health check) | D-12 / T-24-13 | 24-01 Task 3, step 3 and 24-07 Task 3, step 9 | Whether outbound UDP/53 to public resolvers is permitted from the production container network is unverifiable from the repo (Open Question 3 / Assumption A3 in 24-RESEARCH.md) |
Run the dns.Resolver().setServers(['1.1.1.1','8.8.8.8']).resolveNs('google.com') one-liner inside the container (exact command in plan 24-01 Task 3). EGRESS-OK means the Node dns path is viable; EGRESS-BLOCKED requires plan 24-04 to use the DoH-over-HTTPS fallback. Observed: EGRESS-OK — confirmed twice (24-01 checkpoint and again during 24-07's live testing via the D-12 delegation check running successfully). Node's dns module in use, no DoH fallback needed. |
| D-10 display-only disable behavior | D-10 | 24-07 Task 3, step 9 | The failure mode (disabling the toggle silently stopping sync, replicating PAX8's blocking exception) is only observable end-to-end across the health cache, the scheduler, and the sync route | Toggle route53 off at /admin/integrations, wait for the 5-minute health cache (or let the PATCH clear it), confirm the health row reads disabled, then confirm POST /api/route53/sync still succeeds and returns { ok: true }. Observed: PASS. Toggling off showed disabled on the health row while a manual sync still succeeded. Developer noted /admin/integrations page load felt slow during this step overall (not specific to Route 53) — investigated, confirmed unrelated to this plan's code (see deferred-items.md), logged and not fixed here. |
| BWS secret key names / IAM least privilege | SC-5 / T-24-08 | 24-01 Task 3, steps 1, 2, 4 | BWS project contents live in Bitwarden's cloud, and IAM policy scope lives in the AWS console — neither is inspectable from this repo | See plan 24-01 Task 3's how-to-verify steps 1, 2, and 4. Verification commands print only SET/unset presence markers, never secret values. Observed: RESOLVED. BWS key names initially mismatched (AWS_ACCESS_KEY/AWS_SECRET_KEY vs. the AWS-SDK-required AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY) — developer renamed the secrets directly in Bitwarden; re-verified present and correctly named. Credentials confirmed reaching the actual Node process (/proc/<pid>/environ, presence-only check). IAM least-privilege scope left as an open item for the developer to confirm from the AWS console side — not a blocker for this phase's code. |
Validation Sign-Off
- All tasks have an
<automated>verify or an explicit manual/checkpoint justification - Sampling continuity: no 3 consecutive tasks without an automated verify
- Wave 0 covers all MISSING references — every test file is created by the task that needs it
- No watch-mode flags in any command
- Feedback latency < 15s (all unit commands target a single file; the full suite is ~10s)
- Every grep gate that counts occurrences targets source constructs, not comment prose
nyquist_compliant: trueset in frontmatter
Approval: ready for execution