7.6 KiB
| phase | plan | subsystem | tags | dependency-graph | tech-stack | key-files | decisions | metrics | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud | 03 | route53-crud-write-persistence |
|
|
|
|
|
|
Phase 24 Plan 03: Record Validation and Write-Persistence Summary
Server-side D-01 record-type allowlist validator + AWS error sanitizer, and the pending -> committed/failed audit lifecycle with pulse_crud history persistence that the plan 24-05 CRUD routes will depend on.
What Was Built
lib/services/route53-record-validation.ts
WRITABLE_RECORD_TYPES: frozen array of exactly['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'SRV']— a closed allowlist.NS/SOAappear nowhere in this array; they only appear inside the rejection branch'sZONE_DELEGATION_TYPESset and its explicit reason message.validateRecordWrite(input): validatesname(trim/lowercase/trailing-dot normalization),type(case-insensitive uppercase match against the allowlist, explicit delegation-aware rejection reason for NS/SOA, generic rejection for any other unlisted type such as CAA/DS),ttl(integer 0..2147483647, default 300), andresourceRecords(non-empty array, each entry a non-empty string value, capped at 100 entries — T-24-12 DoS guard). No AWS SDK or Postgres import — confirmed bygrep -c "@aws-sdk\|postgres-client"returning 0.sanitizeAwsError(err): redactsAKIA-prefixed access key ids,arn:aws:*substrings, and 12-digit AWS account ids, then truncates to 500 characters. Never throws, even on non-Error input.- 23 test assertions cover every
<behavior>bullet including case-insensitivity (NS/ns), the closed-allowlist rejection of an unlisted type (CAA,DS), TTL boundary values, and the sanitizer stripping both an example AWS key id and ARN from a single error message.
lib/services/route53-write-persistence.ts
createPendingAuditLog/markAuditCommitted/markAuditFailed: the three-function pending -> committed/failed shape mirrored fromlib/services/analyzer/asset-audit/persistence.ts.createPendingAuditLogmust be awaited before any AWSChangeResourceRecordSetsCommandis constructed by the (future) CRUD routes.markAuditFailedalways routes the error throughsanitizeAwsError— never a raw error object orJSON.stringify(err).insertPulseCrudHistory: writes asource='pulse_crud'row toroute53_record_history. Documented directly above the function that it must only be called aftermarkAuditCommitted— a failed AWS attempt gets an audit row but no history row.upsertMirrorRecord/softDeleteMirrorRecord: best-effort refresh/soft-delete of theroute53_recordsmirror after a committed write. Wrapped in try/catch, logged with a[ROUTE53-WRITE]prefix viasanitizeAwsError, never thrown — the AWS write already succeeded and the next incremental sync reconciles regardless. Confirmedgrep -c 'DELETE FROM route53_records'returns 0 — soft-delete only (D-08).loadMirrorRecord: manual snake_case -> camelCase transform (no ORM, per CLAUDE.md) of the current mirror row, supplyingbefore_valueand the exact TTL/value set a Route 53 DELETE needs to match.- 9 test assertions against a mocked
postgresClient(samevi.mockstyle aspax8-sync-service.test.ts) verify: the pending INSERT contains'pending', the committed/ failed UPDATEs contain their respective status literals,markAuditFailed's bound parameters never contain a raw AWS key id,insertPulseCrudHistorybinds the literal'pulse_crud',softDeleteMirrorRecordnever issuesDELETE FROM, both mirror helpers resolve (never throw) even when the underlying query rejects, andloadMirrorRecordcorrectly transforms a found row / returnsnullwhen absent.
Verification
npx vitest run lib/services/route53-record-validation.test.ts lib/services/route53-write-persistence.test.ts— 32/32 passednpx tsc --noEmit --pretty— exits 0npm test(full suite) — 499/501 passed; 2 pre-existing failures inlib/services/analyzer/itglue-search.test.ts, unrelated to this plan (see Deferred Issues)grep -c "'NS'\|'SOA'" lib/services/route53-record-validation.ts— 1 (only inside the rejection Set/reason, never insideWRITABLE_RECORD_TYPES)grep -c "@aws-sdk\|postgres-client" lib/services/route53-record-validation.ts— 0grep -c 'DELETE FROM route53_records' lib/services/route53-write-persistence.ts— 0grep -q 'sanitizeAwsError' lib/services/route53-write-persistence.ts— foundgrep -c "'pulse_crud'" lib/services/route53-write-persistence.ts— 2
Deviations from Plan
None — plan executed exactly as written, aside from one non-substantive typing adjustment:
1. [Rule 3 - blocking issue] ValidateRecordWriteInput fields made optional
- Found during: Task 1,
npx tsc --noEmit --pretty - Issue: The plan's signature
validateRecordWrite(input: { name: unknown; type: unknown; ... })requires thename/typeproperties to be present (even though typedunknown). A behavior test intentionally omitsnameto prove the runtime "missing name" rejection fires — TypeScript's structural typing blocked constructing that test payload. - Fix: Made
name?: unknownandtype?: unknownoptional in the internalValidateRecordWriteInputinterface. Runtime behavior is unchanged (the function still checkstypeof input.name !== 'string', which already coversundefined). - Files modified:
lib/services/route53-record-validation.ts - Commit:
4be4a19
Deferred Issues
None specific to this plan's own code. Two pre-existing, unrelated npm test failures in
lib/services/analyzer/itglue-search.test.ts were re-observed during full-suite verification
and logged (not fixed, out of scope) in deferred-items.md under both the original Plan 24-01
entry and a new Plan 24-03 entry.