From b9df27b656fdaaacc1d36b5f11bf157ba2f86685 Mon Sep 17 00:00:00 2001 From: lorentz Date: Wed, 5 Aug 2026 19:23:43 -0400 Subject: [PATCH 1/6] feat(24-01): install AWS Route 53 SDK and create dedicated schema migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add @aws-sdk/client-route-53 dependency (official aws-sdk-js-v3 package) - Add migrations/102_route53_tables.sql: route53_zones, route53_records, route53_record_history (D-06 change ledger), route53_audit_log (D-03/D-07 attempt audit log with pending/committed/failed status) - Seed integration_settings row for key='route53' (D-10, display-only toggle) - Unbounded retention by design (D-08) — no purge job, no TTL, no DELETE --- migrations/102_route53_tables.sql | 149 +++++++++++ package-lock.json | 423 +++++++++++++++++++++++------- package.json | 1 + 3 files changed, 478 insertions(+), 95 deletions(-) create mode 100644 migrations/102_route53_tables.sql diff --git a/migrations/102_route53_tables.sql b/migrations/102_route53_tables.sql new file mode 100644 index 0000000..1585f37 --- /dev/null +++ b/migrations/102_route53_tables.sql @@ -0,0 +1,149 @@ +-- AWS Route 53 DNS sync — Phase 24 dedicated Postgres schema. +-- +-- Lays down the full Route 53 schema this phase's downstream plans (sync +-- service, CRUD routes, health check, admin UI) will populate and consume. +-- +-- • Hosted zones (mirror of AWS Route 53 zones) -> route53_zones +-- • Resource record sets (mirror of AWS records) -> route53_records +-- • Append-only change ledger (drift + CRUD) -> route53_record_history +-- • Append-only attempt audit log (D-03/D-07) -> route53_audit_log +-- +-- Retention is unbounded by design (D-08) — no purge job, no TTL, and no +-- DELETE statement against route53_record_history or route53_audit_log +-- anywhere in this phase, matching existing Pulse convention (itglue_writes, +-- itglue_asset_audits, phishing audit_events are all forever-retained too). + +-- --------------------------------------------------------------------------- +-- 1. route53_zones — mirror of AWS Route 53 hosted zones +-- --------------------------------------------------------------------------- +-- id is the AWS hosted zone id with the '/hostedzone/' prefix stripped — the +-- natural stable identifier Route 53 already provides, no synthetic UUID +-- needed. authoritative_name_servers backs the D-12 NS-delegation health +-- check planned in 24-04. + +CREATE TABLE IF NOT EXISTS route53_zones ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + comment TEXT, + private_zone BOOLEAN NOT NULL DEFAULT false, + record_count INTEGER NOT NULL DEFAULT 0, + authoritative_name_servers JSONB, + raw_payload JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_deleted BOOLEAN NOT NULL DEFAULT false, + deleted_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_route53_zones_is_deleted ON route53_zones(is_deleted); +CREATE INDEX IF NOT EXISTS idx_route53_zones_name ON route53_zones(name); + +-- --------------------------------------------------------------------------- +-- 2. route53_records — mirror of AWS Route 53 resource record sets +-- --------------------------------------------------------------------------- +-- record_key is the composite string `zoneId:name:type:setIdentifier` (empty +-- string when there is no set identifier) — Route 53 recordsets are uniquely +-- identified by zone+name+type+SetIdentifier; there is no AWS-side record id +-- to use as a natural primary key. + +CREATE TABLE IF NOT EXISTS route53_records ( + record_key TEXT PRIMARY KEY, + zone_id TEXT NOT NULL REFERENCES route53_zones(id) ON DELETE CASCADE, + name TEXT NOT NULL, + type TEXT NOT NULL, + set_identifier TEXT, + ttl INTEGER, + resource_records JSONB, + alias_target JSONB, + raw_payload JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_deleted BOOLEAN NOT NULL DEFAULT false, + deleted_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_route53_records_zone ON route53_records(zone_id); +CREATE INDEX IF NOT EXISTS idx_route53_records_is_deleted ON route53_records(is_deleted); +CREATE INDEX IF NOT EXISTS idx_route53_records_name_type ON route53_records(zone_id, name, type); + +-- --------------------------------------------------------------------------- +-- 3. route53_record_history — append-only change ledger (D-06) +-- --------------------------------------------------------------------------- +-- Written by BOTH the sync service (source='sync_detected_drift', when a +-- previously-synced record's live AWS value no longer matches Postgres) and +-- the CRUD routes (source='pulse_crud', on every successful write). Distinct +-- from route53_audit_log below: this table is a per-record timeline of +-- resolved changes; the audit log below is a per-attempt ledger including +-- failures. audit_log_id is a soft (non-FK) reference so a history row +-- survives independent of the audit log's own lifecycle. + +CREATE TABLE IF NOT EXISTS route53_record_history ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + zone_id TEXT NOT NULL REFERENCES route53_zones(id) ON DELETE CASCADE, + record_key TEXT NOT NULL, + record_name TEXT NOT NULL, + record_type TEXT NOT NULL, + change_action TEXT NOT NULL CHECK (change_action IN ('create','update','delete')), + before_value JSONB, + after_value JSONB, + source TEXT NOT NULL CHECK (source IN ('pulse_crud','sync_detected_drift')), + changed_by_user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL, + changed_by_email TEXT, + audit_log_id UUID, -- soft ref -> route53_audit_log(id), no hard FK + changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_route53_record_history_record ON route53_record_history(record_key, changed_at DESC); +CREATE INDEX IF NOT EXISTS idx_route53_record_history_source ON route53_record_history(source); +CREATE INDEX IF NOT EXISTS idx_route53_record_history_zone ON route53_record_history(zone_id, changed_at DESC); + +-- --------------------------------------------------------------------------- +-- 4. route53_audit_log — append-only attempt audit log (D-03/D-07) +-- --------------------------------------------------------------------------- +-- One row per attempted CRUD/sync operation, including failures. zone_id is +-- deliberately NOT an FK here (unlike route53_record_history above) — a +-- failed attempt against a zone that was never synced must still be +-- recordable. D-03 accepted tradeoff: destructive record operations execute +-- immediately with no staged approval gate; this table is the compensating +-- post-hoc traceability control (actor, timestamp, before/after for every +-- attempt), not a pre-write block. + +CREATE TABLE IF NOT EXISTS route53_audit_log ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + operation TEXT NOT NULL CHECK (operation IN ('create','update','delete','sync')), + zone_id TEXT, + record_key TEXT, + record_name TEXT, + record_type TEXT, + before_value JSONB, + after_value JSONB, + performed_by_user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL, + performed_by_email TEXT, + performed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + completed_at TIMESTAMPTZ, + status TEXT NOT NULL CHECK (status IN ('pending','committed','failed')), + aws_change_id TEXT, + aws_change_status TEXT, + aws_response JSONB, + error_message TEXT +); + +CREATE INDEX IF NOT EXISTS ix_route53_audit_log_record ON route53_audit_log(zone_id, record_key, performed_at DESC); +CREATE INDEX IF NOT EXISTS ix_route53_audit_log_status ON route53_audit_log(status); + +COMMENT ON TABLE route53_record_history IS + 'Append-only per-record change timeline. source distinguishes CRUD writes (pulse_crud) from sync-detected drift (sync_detected_drift). Unbounded retention by design (D-08) — no purge job.'; +COMMENT ON TABLE route53_audit_log IS + 'Append-only per-attempt audit log including failures. D-03 accepted tradeoff: destructive record ops execute immediately with no pre-write approval gate; this table is the post-hoc compensating control. Unbounded retention by design (D-08) — no purge job.'; + +-- --------------------------------------------------------------------------- +-- 5. integration_settings seed row (D-10) +-- --------------------------------------------------------------------------- +-- Display-only toggle on /admin/integrations — no sync/CRUD-blocking +-- behavior anywhere in this phase (unlike PAX8's disable-gates-fullSync +-- exception). Extends the existing seed list from migrations/081 rather +-- than editing that committed file. + +INSERT INTO integration_settings (key, disabled) VALUES ('route53', false) ON CONFLICT (key) DO NOTHING; diff --git a/package-lock.json b/package-lock.json index fba10db..91c83bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@anthropic-ai/sdk": "^0.91.1", + "@aws-sdk/client-route-53": "^3.1104.0", "@better-auth/cli": "^1.4.10", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-accordion": "^1.2.12", @@ -246,6 +247,279 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/client-route-53": { + "version": "3.1104.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-route-53/-/client-route-53-3.1104.0.tgz", + "integrity": "sha512-19Wzq64FbYtZKcN/AoXwq4Nht+HwxTIVNI5QbUzLZ8Dyld0da6xdNIOBxX/0bGu5bnijj4dxJjokvLRoTpOpOA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.6", + "@aws-sdk/credential-provider-node": "^3.972.78", + "@aws-sdk/middleware-sdk-route53": "^3.972.23", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/core": { + "version": "3.977.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.6.tgz", + "integrity": "sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.2", + "@aws-sdk/xml-builder": "^3.972.37", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.31.1", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.67", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.67.tgz", + "integrity": "sha512-rcIpk5kxUqDaaNa6Xk23pQ6ViY7jlqzmfFWCahQcBT97ddXaXYYwzCen9Tz1Jvo6aJft6wDl5bN44/Jw5B4oLA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.6", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.69", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.69.tgz", + "integrity": "sha512-nggwJtZ4eeNsUw5IeWBMXsi1ryct5idi0K+/SCRF3kybLubOMaNTb3XCihXpWMiVpyzyPeIrl0zTkzhBH9porA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.6", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.12.tgz", + "integrity": "sha512-pNEf/OeyN5X3VmLKlgSO6TqaWmW10CvI3TfwL1XhsuhYjSLT2VDaxFnCPHnOeQXSaFisMX4jNhpETriqN8DOmg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.6", + "@aws-sdk/credential-provider-env": "^3.972.67", + "@aws-sdk/credential-provider-http": "^3.972.69", + "@aws-sdk/credential-provider-login": "^3.972.74", + "@aws-sdk/credential-provider-process": "^3.972.67", + "@aws-sdk/credential-provider-sso": "^3.973.11", + "@aws-sdk/credential-provider-web-identity": "^3.972.73", + "@aws-sdk/nested-clients": "^3.997.41", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.74", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.74.tgz", + "integrity": "sha512-0AQfDcf99TNmqVKv0owHrw/TQs6i4ZE5t9qmz6NvO53bE/sA/tpXhXL9AAcEP1qHc6Zzjd1UMb69+/9zdhvY3g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.6", + "@aws-sdk/nested-clients": "^3.997.41", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.78", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.78.tgz", + "integrity": "sha512-OgPAnfvbGAMWac6yvxJ1ihslrvDpPVwR68D2csospdNCCyPvHk9JLzYKwz48SNiS1T2znDwHauywRKRFfpyYng==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.67", + "@aws-sdk/credential-provider-http": "^3.972.69", + "@aws-sdk/credential-provider-ini": "^3.973.12", + "@aws-sdk/credential-provider-process": "^3.972.67", + "@aws-sdk/credential-provider-sso": "^3.973.11", + "@aws-sdk/credential-provider-web-identity": "^3.972.73", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.67", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.67.tgz", + "integrity": "sha512-IlUEejorGTWKb4/Dm7K5Yw4QxUmXLThLhrvBmzVBqZFTbW72cv9LTcITmo1dsnYriALE4h68mOq4LB99x6sQ7Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.6", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.11.tgz", + "integrity": "sha512-gAQBkBZxUB84d71+pPcI9L+jh2ujhuAVxc/4FgGiWFDjkPBlMKxzd5XDtkSXTFX8Ro7ansnT88+XadasxMeCRw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.6", + "@aws-sdk/nested-clients": "^3.997.41", + "@aws-sdk/token-providers": "3.1103.0", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.73", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.73.tgz", + "integrity": "sha512-SnlEmQa6SjOgs6iOPLUQl1Eyq4AKiAdPQlkOhFhqNfDtDCwibMGvL6QlkSmf3o6vAUSImzdPCxowT5dfQUZP1A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.6", + "@aws-sdk/nested-clients": "^3.997.41", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/nested-clients": { + "version": "3.997.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.41.tgz", + "integrity": "sha512-RDHqPGQWlF6tatA/Tp3rg6oIwtgN9IVderxE+9av2Y93Dfyu+mO1hZ5Bu2jpfZg2rwdNbsssnwM+sLafIczMlQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.6", + "@aws-sdk/signature-v4-multi-region": "^3.996.43", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.43.tgz", + "integrity": "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.2", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/token-providers": { + "version": "3.1103.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1103.0.tgz", + "integrity": "sha512-N4wy26MNn31ItGVHYHPrEuCIFY4MBBjC+C5v1lJKqIUSA7OZBdhleCY53zCCrXn27hsk7YNOaTuhQu807S4AfQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.6", + "@aws-sdk/nested-clients": "^3.997.41", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/types": { + "version": "3.974.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.2.tgz", + "integrity": "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.37.tgz", + "integrity": "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/client-sesv2": { "version": "3.943.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.943.0.tgz", @@ -587,6 +861,33 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/middleware-sdk-route53": { + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-route53/-/middleware-sdk-route53-3.972.23.tgz", + "integrity": "sha512-p2mbkuh45ZLjaDPiQVHP679Rpj7hcK7zUAy4SBe91xur8jG43OZOyX0M9ScLP5BcqWksX87V+ucyvq8YJ/gIHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.2", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-route53/node_modules/@aws-sdk/types": { + "version": "3.974.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.2.tgz", + "integrity": "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/middleware-sdk-s3": { "version": "3.943.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.943.0.tgz", @@ -5370,20 +5671,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@smithy/abort-controller": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", - "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/config-resolver": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", @@ -5403,21 +5690,12 @@ } }, "node_modules/@smithy/core": { - "version": "3.18.7", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.7.tgz", - "integrity": "sha512-axG9MvKhMWOhFbvf5y2DuyTxQueO0dkedY9QC3mAfndLosRI/9LJv8WaL0mw7ubNhsO4IuXX9/9dYGPFvHrqlw==", - "dev": true, + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.31.1.tgz", + "integrity": "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.6", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-stream": "^4.5.6", - "@smithy/util-utf8": "^4.2.0", - "@smithy/uuid": "^1.1.0", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -5425,16 +5703,13 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", - "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", - "dev": true, + "version": "4.4.16", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.16.tgz", + "integrity": "sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -5442,16 +5717,13 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", - "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", - "dev": true, + "version": "5.6.13", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.13.tgz", + "integrity": "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", - "@smithy/util-base64": "^4.3.0", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -5603,16 +5875,13 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", - "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", - "dev": true, + "version": "4.9.13", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.13.tgz", + "integrity": "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -5647,21 +5916,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/querystring-builder": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", - "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "@smithy/util-uri-escape": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/querystring-parser": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", @@ -5704,19 +5958,13 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", - "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", - "dev": true, + "version": "5.6.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.12.tgz", + "integrity": "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-uri-escape": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -5743,10 +5991,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", - "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", - "dev": true, + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.1.tgz", + "integrity": "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -5950,19 +6197,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/util-uri-escape": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", - "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/util-utf8": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", @@ -8016,7 +8250,6 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", - "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { diff --git a/package.json b/package.json index e99a78e..1d9bd15 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.91.1", + "@aws-sdk/client-route-53": "^3.1104.0", "@better-auth/cli": "^1.4.10", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-accordion": "^1.2.12", From 4dd9d5dab85fb05c935907f584cb85f84e62354d Mon Sep 17 00:00:00 2001 From: lorentz Date: Wed, 5 Aug 2026 19:24:28 -0400 Subject: [PATCH 2/6] test(24-01): add failing test for Route 53 credential factory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib/types/route53.ts: camelCase interfaces for zones/records/history/audit-log/sync-result - lib/services/route53-factory.test.ts: isRoute53Configured() + getRoute53Client() behavior cases — fails RED, factory module does not exist yet --- lib/services/route53-factory.test.ts | 63 +++++++++++++++++ lib/types/route53.ts | 100 +++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 lib/services/route53-factory.test.ts create mode 100644 lib/types/route53.ts diff --git a/lib/services/route53-factory.test.ts b/lib/services/route53-factory.test.ts new file mode 100644 index 0000000..74a7d5f --- /dev/null +++ b/lib/services/route53-factory.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { isRoute53Configured, getRoute53Client, resetRoute53Client } from './route53-factory'; + +const ORIGINAL_ENV = { ...process.env }; + +beforeEach(() => { + delete process.env.AWS_ACCESS_KEY_ID; + delete process.env.AWS_SECRET_ACCESS_KEY; + delete process.env.AWS_REGION; + resetRoute53Client(); +}); + +afterEach(() => { + process.env = { ...ORIGINAL_ENV }; + resetRoute53Client(); +}); + +describe('isRoute53Configured', () => { + it('returns false when AWS_ACCESS_KEY_ID is unset', () => { + process.env.AWS_SECRET_ACCESS_KEY = 'secret1'; + expect(isRoute53Configured()).toBe(false); + }); + + it('returns false when AWS_SECRET_ACCESS_KEY is unset', () => { + process.env.AWS_ACCESS_KEY_ID = 'id1'; + expect(isRoute53Configured()).toBe(false); + }); + + it('returns false when both are set to empty strings', () => { + process.env.AWS_ACCESS_KEY_ID = ''; + process.env.AWS_SECRET_ACCESS_KEY = ''; + expect(isRoute53Configured()).toBe(false); + }); + + it('returns true when both are set to non-empty values', () => { + process.env.AWS_ACCESS_KEY_ID = 'id1'; + process.env.AWS_SECRET_ACCESS_KEY = 'secret1'; + expect(isRoute53Configured()).toBe(true); + }); +}); + +describe('getRoute53Client', () => { + it('throws an Error mentioning AWS_ACCESS_KEY_ID when credentials are absent', () => { + expect(() => getRoute53Client()).toThrow(/AWS_ACCESS_KEY_ID/); + }); + + it('returns the same cached instance on a second call', () => { + process.env.AWS_ACCESS_KEY_ID = 'id1'; + process.env.AWS_SECRET_ACCESS_KEY = 'secret1'; + const first = getRoute53Client(); + const second = getRoute53Client(); + expect(second).toBe(first); + }); + + it('returns a different instance after resetRoute53Client()', () => { + process.env.AWS_ACCESS_KEY_ID = 'id1'; + process.env.AWS_SECRET_ACCESS_KEY = 'secret1'; + const first = getRoute53Client(); + resetRoute53Client(); + const second = getRoute53Client(); + expect(second).not.toBe(first); + }); +}); diff --git a/lib/types/route53.ts b/lib/types/route53.ts new file mode 100644 index 0000000..54782fd --- /dev/null +++ b/lib/types/route53.ts @@ -0,0 +1,100 @@ +/** + * AWS Route 53 DNS sync — shared type definitions. + * API-response shapes (camelCase) — route handlers transform snake_case + * Postgres rows into these manually (no ORM, per CLAUDE.md). + */ + +// ============================================================================ +// Zone / Record mirror types +// ============================================================================ + +export interface Route53Zone { + id: string; + name: string; + comment: string | null; + privateZone: boolean; + recordCount: number; + authoritativeNameServers: string[] | null; + syncedAt: string; + isDeleted: boolean; +} + +export interface Route53RecordValue { + value: string; +} + +export interface Route53Record { + recordKey: string; + zoneId: string; + name: string; + type: string; + setIdentifier: string | null; + ttl: number | null; + resourceRecords: Route53RecordValue[] | null; + aliasTarget: Record | null; + syncedAt: string; + isDeleted: boolean; +} + +// ============================================================================ +// Change ledger / audit log types +// ============================================================================ + +export type Route53HistorySource = 'pulse_crud' | 'sync_detected_drift'; + +export interface Route53RecordHistory { + id: string; + zoneId: string; + recordKey: string; + recordName: string; + recordType: string; + changeAction: 'create' | 'update' | 'delete'; + beforeValue: Record | null; + afterValue: Record | null; + source: Route53HistorySource; + changedByUserId: string | null; + changedByEmail: string | null; + changedAt: string; +} + +export type Route53AuditStatus = 'pending' | 'committed' | 'failed'; + +export interface Route53AuditLog { + id: string; + operation: 'create' | 'update' | 'delete' | 'sync'; + zoneId: string | null; + recordKey: string | null; + recordName: string | null; + recordType: string | null; + beforeValue: Record | null; + afterValue: Record | null; + performedByUserId: string | null; + performedByEmail: string | null; + performedAt: string; + completedAt: string | null; + status: Route53AuditStatus; + awsChangeId: string | null; + awsChangeStatus: string | null; + errorMessage: string | null; +} + +// ============================================================================ +// Writable record types (D-01) +// ============================================================================ + +export type Route53WritableType = 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'SRV'; + +// ============================================================================ +// Sync result +// ============================================================================ + +export interface Route53SyncResult { + syncId: string; + syncType: string; + status: string; + startedAt: string; + completedAt: string | null; + duration: number | null; + entities: Record; + errors: string[]; +} From 210f84d3436fc337243f3bf23cbc39e975ca13c1 Mon Sep 17 00:00:00 2001 From: lorentz Date: Wed, 5 Aug 2026 19:25:36 -0400 Subject: [PATCH 3/6] feat(24-01): implement Route 53 credential factory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib/services/route53-factory.ts: isRoute53Configured() / getRoute53Client() / resetRoute53Client(), following the veeam-factory.ts singleton shape - No explicit credentials option passed to Route53Client — relies on the AWS SDK's default credential chain reading AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY from process.env, exactly how BWS injects them at the container entrypoint - CLAUDE.md: document the AWS_* env-prefix exception in the integration table - All 7 route53-factory.test.ts assertions pass; npx tsc --noEmit clean --- CLAUDE.md | 1 + lib/services/route53-factory.ts | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 lib/services/route53-factory.ts diff --git a/CLAUDE.md b/CLAUDE.md index 7700b81..ae09049 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,6 +80,7 @@ Examples: `getAutotaskClient()`, `getMsgraphClient()`, `getDattoRmmClient()`, | Veeam VSPC | `VEEAM_VSPC_*` | | Auvik / Addigy / IT Glue / Mimecast / S1 / Duo / Zoom / QBO / Zabbix / Salesbldr | `_*` | | PAX8 | `PAX8_*` (OAuth2 client-credentials, read-only partner/reseller API) | +| AWS Route 53 | `AWS_*` (literal `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_REGION` — intentional exception to the per-service prefix convention; the AWS SDK's default credential chain hardcodes these names. Injected by BWS at the container entrypoint, never in `.env`) | | Anthropic | `ANTHROPIC_API_KEY` (analyzer pipeline + `ai-triage-service.ts`) | | OpenRouter | `OPENROUTER_API_KEY` (alternate analyzer provider, opt-in per request) | | Backblaze B2 | `B2_*` (LogLift evidence storage) | diff --git a/lib/services/route53-factory.ts b/lib/services/route53-factory.ts new file mode 100644 index 0000000..4b3c1bd --- /dev/null +++ b/lib/services/route53-factory.ts @@ -0,0 +1,47 @@ +import { Route53Client } from '@aws-sdk/client-route-53'; + +let route53ClientInstance: Route53Client | null = null; + +/** + * Check if AWS Route 53 credentials are configured. + */ +export function isRoute53Configured(): boolean { + return !!(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY); +} + +/** + * Get or create the Route 53 client singleton instance. + * + * Intentionally does NOT pass an explicit `credentials` option — omitting it + * lets @aws-sdk/credential-provider-node's default credential chain read + * AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN from + * process.env automatically (fromEnv() is first in the chain). This is + * exactly how Bitwarden Secrets Manager (`bws run`) injects credentials at + * the docker-entrypoint.sh layer. Do NOT "fix" this by adding an explicit + * credentials object — see 24-RESEARCH.md Pitfall 1. + */ +export function getRoute53Client(): Route53Client { + if (!route53ClientInstance) { + if (!isRoute53Configured()) { + throw new Error( + 'AWS credentials missing. Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY (and AWS_REGION) environment variables.' + ); + } + + // Route 53 is a global service but the SDK still requires a signing + // region; us-east-1 is the conventional default AWS's own CLI/console use. + route53ClientInstance = new Route53Client({ + region: process.env.AWS_REGION || 'us-east-1', + }); + console.log('Route 53 client initialized'); + } + + return route53ClientInstance; +} + +/** + * Reset the singleton instance (useful for testing). + */ +export function resetRoute53Client(): void { + route53ClientInstance = null; +} From fadae68fe42101b25810ee5cc1f810616e76eb74 Mon Sep 17 00:00:00 2001 From: lorentz Date: Wed, 5 Aug 2026 19:25:39 -0400 Subject: [PATCH 4/6] docs(24-01): log pre-existing itglue-search test failures as out-of-scope Unrelated to Route 53 factory/schema work; not fixed per scope boundary rule. --- .../deferred-items.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/deferred-items.md diff --git a/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/deferred-items.md b/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/deferred-items.md new file mode 100644 index 0000000..364f6c7 --- /dev/null +++ b/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/deferred-items.md @@ -0,0 +1,16 @@ +# Deferred Items — Phase 24 (AWS Route 53 DNS Sync) + +Out-of-scope discoveries logged during execution. Not fixed per the scope +boundary rule (only auto-fix issues directly caused by the current task's +changes). + +## Plan 24-01 + +- **`lib/services/analyzer/itglue-search.test.ts`** — 2 pre-existing test + failures (`returns capped, redacted doc snippets when the org is found`, + `tolerates per-call failures (configurations errors, flex still returns)`) + surfaced by `npm test` (full suite) while verifying Task 2. Neither + `itglue-search.ts` nor its test file were touched by this plan (git status + confirms zero changes to either path from Task 1 or Task 2). Unrelated to + the Route 53 factory/schema work in this plan — not fixed, logged here for + visibility. From 97ec5722b19e4296c64d464acf341ce36bbbf78e Mon Sep 17 00:00:00 2001 From: lorentz Date: Wed, 5 Aug 2026 19:26:46 -0400 Subject: [PATCH 5/6] docs(24-01): create partial SUMMARY, checkpoint pending MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tasks 1-2 complete (AWS SDK + schema migration, types + credential factory). Task 3 is a blocking human-verify checkpoint requiring BWS/AWS credential confirmation and live docker/DNS-egress verification — not fabricated, not run unilaterally. Execution stops here pending developer response. --- .../24-01-SUMMARY.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 .planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-01-SUMMARY.md diff --git a/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-01-SUMMARY.md b/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-01-SUMMARY.md new file mode 100644 index 0000000..70a5875 --- /dev/null +++ b/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-01-SUMMARY.md @@ -0,0 +1,116 @@ +--- +phase: 24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud +plan: 01 +subsystem: aws-route53 +tags: [route53, aws-sdk, migration, factory, foundation] +dependency-graph: + requires: [] + provides: + - "@aws-sdk/client-route-53 dependency" + - "route53_zones / route53_records / route53_record_history / route53_audit_log schema" + - "lib/types/route53.ts contracts" + - "lib/services/route53-factory.ts (getRoute53Client / isRoute53Configured / resetRoute53Client)" + affects: + - "plans 24-02 through 24-07 (all import these four artifacts)" +tech-stack: + added: + - "@aws-sdk/client-route-53 ^3.1104.0" + patterns: + - "factory + isConfigured() singleton (mirrors veeam-factory.ts)" + - "dedicated-schema-per-integration migration (mirrors pax8/itglue conventions)" +key-files: + created: + - migrations/102_route53_tables.sql + - lib/types/route53.ts + - lib/services/route53-factory.ts + - lib/services/route53-factory.test.ts + - .planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/deferred-items.md + modified: + - package.json + - package-lock.json + - CLAUDE.md +decisions: + - "Fast-forwarded this worktree's branch onto master before starting (14 commits behind, zero unique commits — pure catch-up, not a merge) to pick up the phase 24 planning docs (24-01-PLAN.md etc.) that were committed to master after this worktree was created." +metrics: + duration: "partial — Tasks 1-2 complete, Task 3 checkpoint pending" + completed: "2026-08-05" +--- + +# Phase 24 Plan 1: Route 53 Foundation Summary + +Installed the official AWS Route 53 SDK client, created the dedicated four-table +Postgres schema (mirror tables + change-history ledger + audit ledger), defined +shared camelCase TypeScript types, and built the `route53-factory.ts` credential +factory following the exact `isConfigured()` + singleton shape every other +Pulse integration uses — TDD RED/GREEN cycle, 7/7 tests passing. + +## What Was Built + +**Task 1 — AWS SDK + migration:** +- `npm install @aws-sdk/client-route-53` (official `aws/aws-sdk-js-v3` package, confirmed `[OK]` in 24-RESEARCH.md's package legitimacy audit) +- `migrations/102_route53_tables.sql` — four tables: + - `route53_zones` (mirror, PK = AWS hosted zone id with `/hostedzone/` prefix stripped) + - `route53_records` (mirror, PK = composite `record_key` string since Route 53 recordsets have no native id) + - `route53_record_history` (D-06 append-only change ledger, `source` CHECK constrained to `pulse_crud`/`sync_detected_drift`) + - `route53_audit_log` (D-03/D-07 append-only attempt log including failures, `status` CHECK constrained to `pending`/`committed`/`failed`, `zone_id` deliberately not an FK) + - Seed row `INSERT INTO integration_settings (key, disabled) VALUES ('route53', false)` (D-10, display-only toggle — extends the existing seed list rather than editing the committed `081_integration_settings.sql`) +- Migration was **not yet applied to a live database** in this worktree (no `pulse-postgres` container reachable from here) — flagged as a deployment follow-up. The committed file is the source of truth for new installs. + +**Task 2 — Types + factory (TDD):** +- RED: `lib/services/route53-factory.test.ts` written first, confirmed failing (module didn't exist) +- GREEN: `lib/types/route53.ts` (camelCase interfaces/unions per the plan's `` contract) + `lib/services/route53-factory.ts` (singleton factory, no explicit `credentials:` option passed to `Route53Client` — relies on the AWS SDK default credential chain reading `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` from `process.env`, per 24-RESEARCH.md Pitfall 1) +- `CLAUDE.md` integration table updated with the `AWS Route 53` / `AWS_*` exception row +- 7/7 test assertions pass; `npx tsc --noEmit --pretty` exits clean + +## Deviations from Plan + +### Auto-fixed Issues + +None — plan executed exactly as written for Tasks 1-2. + +### Environment Note (not a deviation, a pre-condition) + +This worktree's branch (`worktree-agent-aa690b9b15c5b0f8e`) was created before the +phase 24 planning commits landed on `master` — it had zero unique commits and was +purely 14 commits behind. Fast-forwarded (`git merge --ff-only master`) to pick up +`24-01-PLAN.md` and related planning docs before execution could start. This was a +clean fast-forward (no merge, no conflicts, nothing discarded). + +### Out-of-Scope Discovery (logged, not fixed) + +`npm test` (full suite, run as part of Task 2 verification) surfaced 2 pre-existing +failures in `lib/services/analyzer/itglue-search.test.ts`, unrelated to this plan — +neither that file nor `itglue-search.ts` were touched by Tasks 1-2. Logged to this +phase's `deferred-items.md` per the scope boundary rule rather than fixed. + +## Checkpoint Status: PENDING (Task 3 not yet answered) + +Task 3 is a `checkpoint:human-verify` gate requiring the developer to confirm, from +outside this worktree/sandbox: + +1. **BWS secret key names** — whether Bitwarden Secrets Manager's project (referenced + by `BWS_PROJECT_ID`) stores AWS credentials under the literal keys + `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_REGION`. +2. **Credentials reach the container** — `docker exec pulse-app sh -lc 'echo "id=${AWS_ACCESS_KEY_ID:+SET} secret=${AWS_SECRET_ACCESS_KEY:+SET} region=${AWS_REGION:-unset}"'` +3. **Outbound DNS egress** to 1.1.1.1/8.8.8.8 on UDP/53 from inside the container — + drives plan 24-04's implementation choice (Node `dns` module vs. DoH-over-HTTPS fallback). +4. **IAM scope** — confirm the IAM principal is scoped to the five Route 53 actions only. + +This executor did not fabricate these answers or run live/destructive docker commands +unilaterally, per explicit orchestrator instruction. Execution stops here; a +continuation agent should resume at Task 3 once the developer responds. + +## Self-Check: PASSED + +All created files confirmed present: +- FOUND: migrations/102_route53_tables.sql +- FOUND: lib/types/route53.ts +- FOUND: lib/services/route53-factory.ts +- FOUND: lib/services/route53-factory.test.ts +- FOUND: .planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/deferred-items.md + +All commits confirmed present in `git log`: +- b9df27b feat(24-01): install AWS Route 53 SDK and create dedicated schema migration +- 4dd9d5d test(24-01): add failing test for Route 53 credential factory +- 210f84d feat(24-01): implement Route 53 credential factory +- fadae68 docs(24-01): log pre-existing itglue-search test failures as out-of-scope From b81ad3ecc237cb56a4759d1630e080ce4813a2e7 Mon Sep 17 00:00:00 2001 From: lorentz Date: Wed, 5 Aug 2026 20:16:45 -0400 Subject: [PATCH 6/6] =?UTF-8?q?docs(24-01):=20resolve=20checkpoint=20?= =?UTF-8?q?=E2=80=94=20BWS=20key=20rename=20confirmed,=20DNS=20egress=20OK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checkpoint task 3 resolved: BWS secret keys renamed in Bitwarden (AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY confirmed present), DNS egress to public resolvers confirmed OK, credentials confirmed reaching the Node process. IAM scope left as an open operational item for the developer to confirm via AWS console. Co-Authored-By: Claude Sonnet 5 --- .../24-01-SUMMARY.md | 71 +++++++++++++++---- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-01-SUMMARY.md b/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-01-SUMMARY.md index 70a5875..6bf830b 100644 --- a/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-01-SUMMARY.md +++ b/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-01-SUMMARY.md @@ -32,7 +32,7 @@ key-files: decisions: - "Fast-forwarded this worktree's branch onto master before starting (14 commits behind, zero unique commits — pure catch-up, not a merge) to pick up the phase 24 planning docs (24-01-PLAN.md etc.) that were committed to master after this worktree was created." metrics: - duration: "partial — Tasks 1-2 complete, Task 3 checkpoint pending" + duration: "Tasks 1-3 complete, checkpoint resolved" completed: "2026-08-05" --- @@ -54,7 +54,7 @@ Pulse integration uses — TDD RED/GREEN cycle, 7/7 tests passing. - `route53_record_history` (D-06 append-only change ledger, `source` CHECK constrained to `pulse_crud`/`sync_detected_drift`) - `route53_audit_log` (D-03/D-07 append-only attempt log including failures, `status` CHECK constrained to `pending`/`committed`/`failed`, `zone_id` deliberately not an FK) - Seed row `INSERT INTO integration_settings (key, disabled) VALUES ('route53', false)` (D-10, display-only toggle — extends the existing seed list rather than editing the committed `081_integration_settings.sql`) -- Migration was **not yet applied to a live database** in this worktree (no `pulse-postgres` container reachable from here) — flagged as a deployment follow-up. The committed file is the source of truth for new installs. +- Migration was applied to the live database by the orchestrator after this worktree's commits landed: `docker exec -i pulse-postgres psql -U pulse_user -d pulse_autotask < migrations/102_route53_tables.sql`. All four tables confirmed present via `\dt route53_*`. **Task 2 — Types + factory (TDD):** - RED: `lib/services/route53-factory.test.ts` written first, confirmed failing (module didn't exist) @@ -83,22 +83,63 @@ failures in `lib/services/analyzer/itglue-search.test.ts`, unrelated to this pla neither that file nor `itglue-search.ts` were touched by Tasks 1-2. Logged to this phase's `deferred-items.md` per the scope boundary rule rather than fixed. -## Checkpoint Status: PENDING (Task 3 not yet answered) +## Checkpoint Status: RESOLVED -Task 3 is a `checkpoint:human-verify` gate requiring the developer to confirm, from -outside this worktree/sandbox: +Task 3's four verification items, confirmed by the orchestrator against the live +`pulse-app` / `pulse-postgres` containers with the developer: -1. **BWS secret key names** — whether Bitwarden Secrets Manager's project (referenced - by `BWS_PROJECT_ID`) stores AWS credentials under the literal keys - `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_REGION`. -2. **Credentials reach the container** — `docker exec pulse-app sh -lc 'echo "id=${AWS_ACCESS_KEY_ID:+SET} secret=${AWS_SECRET_ACCESS_KEY:+SET} region=${AWS_REGION:-unset}"'` -3. **Outbound DNS egress** to 1.1.1.1/8.8.8.8 on UDP/53 from inside the container — - drives plan 24-04's implementation choice (Node `dns` module vs. DoH-over-HTTPS fallback). -4. **IAM scope** — confirm the IAM principal is scoped to the five Route 53 actions only. +1. **BWS secret key names** — initially a MISMATCH: the Bitwarden project exposed + `AWS_ACCESS_KEY` / `AWS_SECRET_KEY`, not the literal `AWS_ACCESS_KEY_ID` / + `AWS_SECRET_ACCESS_KEY` the AWS SDK's default credential chain requires. No + `AWS_REGION` key exists (factory defaults to `us-east-1`, as designed). **Resolved + by renaming the secrets directly in Bitwarden Secrets Manager** (developer's choice, + over adding an env-var alias shim in `docker-entrypoint.sh`) — re-verified via + `bws secret list` inside the container afterward, confirmed `AWS_ACCESS_KEY_ID` / + `AWS_SECRET_ACCESS_KEY` now present. **No change needed to + `lib/services/route53-factory.ts`** — it already reads the AWS SDK's hardcoded + env var names via the default credential chain, per Pitfall 1. +2. **Credentials reach the container** — confirmed present in the actual Node process + environment (`/proc//environ`, presence-only check, count=2). Note: + `docker exec pulse-app sh -lc 'echo $VAR'` as written in this task does NOT work + for this verification — `docker exec` attaches a fresh process to the container + namespace and does not inherit the runtime env of the `bws run`-wrapped PID 1 + process tree. Verified via the actual node process's `/proc//environ` instead. +3. **Outbound DNS egress** — `EGRESS-OK`: `1.1.1.1`/`8.8.8.8` UDP/53 reachable from + inside the container. Plan 24-04 can use Node's `dns` module directly; no + DoH-over-HTTPS fallback needed. +4. **IAM scope** — developer to confirm from the AWS console side; not verifiable + from inside the container/repo. Recorded here as an open operational item, not a + blocker for 24-02 through 24-07 (D-05/D-06/D-07/D-10 schema/factory contracts do + not depend on the IAM policy's exact scope). -This executor did not fabricate these answers or run live/destructive docker commands -unilaterally, per explicit orchestrator instruction. Execution stops here; a -continuation agent should resume at Task 3 once the developer responds. +### Incident: credential value briefly exposed during diagnosis (not part of this plan's code) + +While diagnosing item 1's mismatch, the orchestrator ran `bws secret list -o json` +inside the container to enumerate key *names* — this command prints full secret +*values* by design (unlike the task's own `${VAR:+SET}`-style presence checks) and +briefly exposed the AWS access key ID/secret value in the session transcript. The +developer was notified immediately and advised to rotate the exposed key in AWS IAM. +This is an operational incident, not a code defect — recorded here for traceability +since it happened during this plan's checkpoint verification. No repository file +contains the exposed values. + +### Additional finding fixed during checkpoint verification (outside this plan's `files_modified`, pre-existing uncommitted work) + +Two bugs in already-staged, uncommitted BWS infrastructure files (`Dockerfile`, +`docker-compose.yml`, `docker-entrypoint.sh` — not part of this plan's scope, but +blocking checkpoint verification) were found and fixed by the orchestrator: +- `docker-compose.yml`: the `app` service's `environment:` block re-declared + `BWS_ACCESS_TOKEN`/`BWS_PROJECT_ID` as `${VAR:-}` substitutions, which resolve + against the root `.env` (not `.env.local`) and silently overrode the real token + with an empty string. Fixed by removing the redundant re-declaration. +- `Dockerfile`: the generated `bws` CLI config only set `state_dir`, but bws 2.x + requires `server_base` (or `server_identity`) even for the default Bitwarden cloud + instance — this crash-looped the `pulse-app` container on every start. Fixed by + adding `server_base = "https://vault.bitwarden.com"` to the generated config. + +Both fixes were verified live (container rebuilt, restarted, confirmed healthy) but +remain uncommitted, matching the state of the rest of this BWS infra work — the +developer owns when to commit that separately from this phase's plans. ## Self-Check: PASSED