diff --git a/README.md b/README.md index 007d2af..5cd8621 100644 --- a/README.md +++ b/README.md @@ -52,15 +52,15 @@ responsibility: there is no mock server, mock scenario, or fixture data in the r ## Runtime Configuration -Public browser configuration is source-controlled in `src/config/deployment.config.ts`. Both the -Gamma and production-site artifacts currently select its `gamma` entry, which contains the unified -REST URL, AppSync GraphQL URL, Cognito public identifiers, chatbot URL, feature defaults, timeout, -and log level. Vite bundles these values into browser JavaScript, so this file must never contain a -secret. - -No GitHub `VITE_*` variables or local `.env` are required. The Gamma and production CodeBuild -projects set only the non-secret selector `VITE_BACKEND_STAGE=gamma`; an unknown selector fails the -build. Developers may copy `.env.example` to ignored `.env` and override individual values for local +Public browser configuration is source-controlled in `src/config/deployment.config.ts`. The +production-site artifact and local development both select its `gamma` entry, which contains the +unified REST URL, AppSync GraphQL URL, Cognito public identifiers, chatbot URL, feature defaults, +timeout, and log level. Vite bundles these values into browser JavaScript, so this file must never +contain a secret. + +No GitHub `VITE_*` variables or local `.env` are required. The production CodeBuild project sets +only the non-secret selector `VITE_BACKEND_STAGE=gamma`; an unknown selector fails the build. +Developers may copy `.env.example` to ignored `.env` and override individual values for local work. Explicit boolean, timeout, and log-level overrides are validated, while blank values use source defaults. The unused `VITE_WS_URL` setting has been removed. @@ -109,14 +109,12 @@ gated inside the AWS pipeline because pull-request and `main` push events are se CodePipeline `Verify` stage runs `npm run lint` and `npm test` against the exact CodeConnections source revision before any deployment-capable CodeBuild project runs. -After Gamma deploys and passes its public smoke test, `BuildProduction` repeats type checking and -tests, builds the production bundle, verifies its promoted endpoints, and synthesizes a complete -CDK cloud assembly. Only that immutable pipeline artifact reaches AWS manual approval. The -post-approval project deploys the stored assembly with `--app cdk.out.prod`; it never runs Vite or -CDK synthesis again. +On every push to `main`, `BuildProduction` repeats type checking and tests, builds the production +bundle, verifies its bundled endpoints, and synthesizes a complete CDK cloud assembly. Only that +immutable pipeline artifact reaches AWS manual approval. The post-approval project deploys the +stored assembly with `--app cdk.out.prod`; it never runs Vite or CDK synthesis again. -A failed check, build, synth, deployment, or Gamma smoke test therefore cannot reach production -approval. +A failed check, build, synth, or deployment therefore cannot reach production approval. > **Repository setting:** keep `lint-and-test` as a required GitHub status check for `main`, so a > failing pull request cannot merge. CodePipeline then independently verifies the merged commit @@ -180,23 +178,24 @@ Real blockers found while scoping; they apply regardless of which option: ## Deployment A push to protected `main` is delivered directly to AWS through the CDK-managed GitHub -CodeConnection. A queued CodePipeline V2 execution then deploys both stages into frontend account -`851725215059`; GitHub Actions and GitHub OIDC are not in the deployment path. Both frontends live -in Mumbai (`ap-south-1`). Backend account `627367419734` owns **only** the API zones +CodeConnection. A queued CodePipeline V2 execution then deploys the production site into frontend +account `851725215059`; GitHub Actions and GitHub OIDC are not in the deployment path. The frontend +lives in Mumbai (`ap-south-1`). Backend account `627367419734` owns **only** the API zones (`api.gamma.onehook.club`, `api.onehook.club`); the pipeline never deploys backend resources. +There is a single deployed site — production `onehook.club`. Both it and local development point +their browser traffic at the Gamma backend for now (see "Runtime Configuration"). There is no +separate `gamma.onehook.club` frontend website. + | Stage | AWS account | Site | Runtime backend used by current artifact | Hosted zone | CloudFront certificate | |---|---|---|---|---|---| -| **gamma** | `851725215059` | `gamma.onehook.club` | REST `https://api.gamma.onehook.club`; GraphQL `https://graphql.api.gamma.onehook.club/graphql` | **new CDK-managed** `gamma.onehook.club` zone in the frontend account | reuses the frontend-account wildcard `*.onehook.club` cert (`us-east-1`) — no new cert, no separate stack | -| **production** | `851725215059` (owns the `onehook.club` zone) | `onehook.club` (+ `www`) | **temporarily the same Gamma REST/GraphQL backend**; switch to `api.onehook.club` / `graphql.api.onehook.club` only after backend production promotion | existing `onehook.club` zone | dedicated `OneHook-Certificate-prod` stack in `us-east-1` (ownership preserved) | +| **production** | `851725215059` (owns the `onehook.club` zone) | `onehook.club` (+ `www`) | **temporarily the Gamma REST/GraphQL backend** (`api.gamma.onehook.club` / `graphql.api.gamma.onehook.club`); switch to `api.onehook.club` / `graphql.api.onehook.club` only after backend production promotion | existing `onehook.club` zone | dedicated `OneHook-Certificate-prod` stack in `us-east-1` (ownership preserved) | The pipeline is strictly sequential: ```text Source (exact main commit) → Verify -→ Build + deploy Gamma -→ Smoke-test Gamma → Build/test/check + synthesize production cloud assembly → AWS manual approval → Deploy the exact approved assembly without rebuilding or re-synthesizing @@ -212,36 +211,12 @@ deploy and file-publishing bootstrap roles in `ap-south-1` and `us-east-1`. No p lookup or image-publishing roles, wildcard `cdk-*` roles, or roles in backend account `627367419734`. -For now, both artifacts deliberately select the checked-in Gamma backend entry. Production backend -promotion is a separate backend-repository operation; once it has been tested and deployed, add a -`prod` entry to `src/config/deployment.config.ts` and change only the production build's -`VITE_BACKEND_STAGE` selector. Both frontend stacks still create **A and AAAA** aliases in account +For now, the production artifact deliberately selects the checked-in Gamma backend entry. Production +backend promotion is a separate backend-repository operation; once it has been tested and deployed, +add a `prod` entry to `src/config/deployment.config.ts` and change only the production build's +`VITE_BACKEND_STAGE` selector. The frontend stack creates **A and AAAA** aliases in account `851725215059`, and the app rejects a deploy attempted with credentials from any other account. -### Gamma DNS: frontend-owned zone, backend-owned API (no cross-account writes) - -Gamma's `gamma.onehook.club` zone now lives in the frontend account and is fully CloudFormation- -owned (created, delegated and deletable — no stale resources). Because the backend account still -owns `api.gamma.onehook.club`, the gamma stack wires the delegation **without ever writing into the -backend account**: - -1. A deterministic-name Lambda execution role in `851725215059` - (`OneHook-Gamma-ApiDnsReader`) assumes a **read-only** backend role - (`arn:aws:iam::627367419734:role/OneHook-ApiDnsReader-gamma-Role`, gated by an `ExternalId`) and - discovers the exact `api.gamma.onehook.club` zone by name before calling `route53:GetHostedZone` - to read its delegation nameservers. -2. It creates the `api.gamma.onehook.club` **NS** record in the local gamma zone from those - nameservers. -3. Only **after** that, an idempotent `AwsCustomResource` **UPSERT**s the parent `onehook.club` - zone's `gamma.onehook.club` NS record to point at the new local gamma zone (with a clean - **DELETE** on teardown), completing the migration off the previously backend-owned gamma zone. - -The backend role ARN and `ExternalId` have matching deterministic defaults in -`infra/stacks/constants.ts` and may be intentionally re-pointed with -`--context backendDnsReaderRoleArn=…` or `--context dnsDelegationExternalId=…`. The API zone is -found by its exact code-owned DNS name, so no generated hosted-zone ID or manual workflow context is -required. - ### AWS pipeline infrastructure The delivery plane is defined in `infra/pipeline/frontend-pipeline-stack.ts` and its source-controlled @@ -251,8 +226,8 @@ buildspecs. It provisions: the source action fixes `pushpsood/OneHookClient` branch `main`, and the GitHub App installation is granted access only to that repository. - A queued CodePipeline V2 pipeline named `OneHook-Frontend`. -- Separate CodeBuild projects and service roles for verification, Gamma deployment, smoke tests, - production build/synth and production deployment. +- Separate CodeBuild projects and service roles for verification, smoke tests, production + build/synth and production deployment. - A private, encrypted, versioned S3 artifact bucket with lifecycle cleanup. - CloudWatch build logs retained for one month. - An SNS production-approval topic. @@ -312,54 +287,42 @@ There is no GitHub deployment workflow, deployment environment, `AWS_ROLE_ARN` s Do not delete the existing live GitHub OIDC roles until this migration succeeds end to end. Cut over in this order: -1. Deploy the backend Gamma prepare state (`retainLegacyGammaZone: true`). -2. Deploy the pipeline stack manually and authorize its PENDING connection. -3. Attach the generated production-approval policy to the designated AWS approver. -4. Push the frontend cutover commit and observe CodePipeline deploy/smoke-test Gamma. -5. Approve and verify the exact production assembly and production smoke test. -6. Only then remove GitHub environment secrets and the old +1. Deploy the pipeline stack manually and authorize its PENDING connection. +2. Attach the generated production-approval policy to the designated AWS approver. +3. Push the frontend cutover commit and observe CodePipeline build the production assembly. +4. Approve and verify the exact production assembly and production smoke test. +5. Only then remove GitHub environment secrets and the old `GitHubActionsDeployRole-gamma`, `GitHubActionsDeployRole-production`, and broad `GitHubActionsDeployRole` in a separately reviewed cleanup. -7. After DNS propagation is confirmed, deploy the backend Gamma final state with - `retainLegacyGammaZone: false`. `setup-aws-oidc.sh` is retained temporarily as a guarded rollback utility and refuses to run unless `ALLOW_LEGACY_GITHUB_OIDC=1` is explicitly set. ### Domain and DNS Setup -The frontend account `851725215059` hosts the Route53 zones for **both** site domains: the existing -`onehook.club` zone and a **new CDK-managed** `gamma.onehook.club` zone (created by the gamma -stack). The backend account keeps only the `api.gamma.onehook.club` / `api.onehook.club` API zones. -If you are migrating a domain, point the registrar's nameservers at the corresponding Route53 hosted -zone. During deployment CDK provisions the CloudFront certificate in `us-east-1` for production -(dedicated `OneHook-Certificate-prod` stack, DNS-validated in the `onehook.club` zone), while gamma -reuses the frontend-account wildcard `*.onehook.club` certificate (no new cert). Gamma also creates -its `gamma.onehook.club` NS delegation in the parent `onehook.club` zone and an -`api.gamma.onehook.club` NS delegation pointing back at the backend API zone — all read-only toward -the backend account (see "Gamma DNS" above). +The frontend account `851725215059` hosts the Route53 zone for the site domain: the existing +`onehook.club` zone. The backend account keeps only the `api.gamma.onehook.club` / +`api.onehook.club` API zones. If you are migrating a domain, point the registrar's nameservers at +the corresponding Route53 hosted zone. During deployment CDK provisions the CloudFront certificate +in `us-east-1` for production (dedicated `OneHook-Certificate-prod` stack, DNS-validated in the +`onehook.club` zone). ## Infrastructure -Each stage is deployed as a static site via: +The production site is deployed as a static site via: - **S3 (ap-south-1)** — Hosts the built assets - **CloudFront (Global)** — CDN with HTTPS, caching, and security headers -- **Route53** — A + AAAA alias records for CloudFront, in the frontend account's zone for the stage - (`gamma.onehook.club` for gamma, `onehook.club` for production); gamma also owns the - `gamma.onehook.club` and `api.gamma.onehook.club` NS delegations +- **Route53** — A + AAAA alias records for CloudFront, in the frontend account's `onehook.club` + zone (apex + `www`) - **AWS Certificate Manager (us-east-1)** — CloudFront TLS cert; a dedicated `OneHook-Certificate-prod` - stack for production (ownership preserved) and the reused frontend-account wildcard - `*.onehook.club` certificate for gamma -- **Lambda + custom resources** — gamma only: a deterministic-name reader Lambda reads the - backend API zone's nameservers (read-only cross-account assume-role with `ExternalId`), and an - `AwsCustomResource` UPSERTs the parent zone's gamma delegation + stack (ownership preserved) Stage infrastructure is defined in `infra/stacks/frontend-app.ts` and `infra/stacks/frontend-stack.ts`; delivery infrastructure is defined separately in -`infra/pipeline/pipeline-app.ts` and `infra/pipeline/frontend-pipeline-stack.ts`. Synthesize stages -with `npm run synth:gamma` / `npm run synth:prod` and the pipeline with -`AWS_PROFILE=pushp-sde-aws npm run synth:pipeline`. Direct stage deploy scripts remain available for -break-glass recovery, not the normal release path. +`infra/pipeline/pipeline-app.ts` and `infra/pipeline/frontend-pipeline-stack.ts`. Synthesize the +production stage with `npm run synth:prod` and the pipeline with +`AWS_PROFILE=pushp-sde-aws npm run synth:pipeline`. The direct `npm run deploy:prod` script remains +available for break-glass recovery, not the normal release path. ## Related Repositories diff --git a/docs/FRONTEND_PRODUCTION.md b/docs/FRONTEND_PRODUCTION.md index da372bd..f642461 100644 --- a/docs/FRONTEND_PRODUCTION.md +++ b/docs/FRONTEND_PRODUCTION.md @@ -303,11 +303,11 @@ than maintaining independent URL values in deployment settings. ### **Stage builds and deployment** AWS CodeConnections sends the exact protected-`main` revision to a queued CodePipeline V2 -execution. AWS then deploys Gamma first and proves the immutable production cloud assembly before -requesting release approval: +execution. AWS builds and proves the immutable production cloud assembly before requesting release +approval: ```text -source → verify → build/deploy Gamma → smoke-test Gamma → production lint/test/build/synth +source → verify → production lint/test/build/synth → AWS manual approval → deploy exact cloud assembly → smoke-test production ``` diff --git a/infra/lambda/api-dns-reader/index.mjs b/infra/lambda/api-dns-reader/index.mjs deleted file mode 100644 index fcbfcf5..0000000 --- a/infra/lambda/api-dns-reader/index.mjs +++ /dev/null @@ -1,91 +0,0 @@ -// @ts-nocheck -/** - * Custom-resource handler (invoked through the CDK `Provider` framework) that READS the delegation - * nameservers of the backend-owned `api.gamma.onehook.club` hosted zone WITHOUT ever writing into - * the backend account. - * - * It assumes a backend read-only role (scoped by ExternalId), finds the exact zone with - * route53:ListHostedZonesByName, and calls route53:GetHostedZone for its delegation nameservers. - * It returns those nameservers as a comma-joined string in `Data.NameServers`; the frontend stack - * turns that into the local `api.gamma.onehook.club` NS record. - * - * The AWS SDK v3 clients used here are provided by the Lambda Node.js 22 runtime — no bundling. - */ -import { STSClient, AssumeRoleCommand } from '@aws-sdk/client-sts'; -import { - Route53Client, - GetHostedZoneCommand, - ListHostedZonesByNameCommand, -} from '@aws-sdk/client-route-53'; - -export const handler = async (event) => { - const requestType = event.RequestType; - - // Read-only resource: nothing to undo on delete/update-remove. The NS record itself is an - // ordinary CloudFormation-owned resource that CFN deletes on its own. - if (requestType === 'Delete') { - return { PhysicalResourceId: event.PhysicalResourceId }; - } - - const props = event.ResourceProperties ?? {}; - const backendReaderRoleArn = props.BackendReaderRoleArn; - const externalId = props.ExternalId; - const backendApiZoneName = props.BackendApiZoneName; - - if (!backendReaderRoleArn || !externalId || !backendApiZoneName) { - throw new Error( - 'Missing required properties: BackendReaderRoleArn, ExternalId, BackendApiZoneName.' - ); - } - - // 1. Assume the backend read-only role (ExternalId enforced by the backend trust policy). - const sts = new STSClient({}); - const assumed = await sts.send( - new AssumeRoleCommand({ - RoleArn: backendReaderRoleArn, - RoleSessionName: 'onehook-gamma-api-dns-read', - ExternalId: externalId, - DurationSeconds: 900, - }) - ); - - const creds = assumed.Credentials; - if (!creds) { - throw new Error(`AssumeRole returned no credentials for ${backendReaderRoleArn}.`); - } - - // Construct a Route53 client with the short-lived, read-only backend credentials. - const route53 = new Route53Client({ - credentials: { - accessKeyId: creds.AccessKeyId, - secretAccessKey: creds.SecretAccessKey, - sessionToken: creds.SessionToken, - }, - }); - - // 2. Discover the generated hosted-zone id from its deterministic DNS name. Route53 returns - // sorted names; still require an exact match so a similarly-prefixed zone can never be selected. - const normalizedName = `${backendApiZoneName.replace(/\.$/, '')}.`; - const listed = await route53.send( - new ListHostedZonesByNameCommand({ DNSName: normalizedName, MaxItems: 1 }) - ); - const matchedZone = listed.HostedZones?.find((hostedZone) => hostedZone.Name === normalizedName); - if (!matchedZone?.Id) { - throw new Error(`Backend API zone ${normalizedName} was not found.`); - } - const backendApiZoneId = matchedZone.Id.replace(/^\/hostedzone\//, ''); - - // 3. Read the matched API zone's delegation nameservers (read-only). - const zone = await route53.send(new GetHostedZoneCommand({ Id: backendApiZoneId })); - const nameServers = zone.DelegationSet?.NameServers ?? []; - - if (nameServers.length === 0) { - throw new Error(`Backend API zone ${normalizedName} returned no delegation nameservers.`); - } - - // Comma-join so the CDK side can Fn.split() into the NS record's values. - return { - PhysicalResourceId: `api-gamma-ns-${backendApiZoneId}`, - Data: { NameServers: nameServers.join(',') }, - }; -}; diff --git a/infra/pipeline/buildspecs/gamma-deploy.yml b/infra/pipeline/buildspecs/gamma-deploy.yml deleted file mode 100644 index b886542..0000000 --- a/infra/pipeline/buildspecs/gamma-deploy.yml +++ /dev/null @@ -1,32 +0,0 @@ -version: 0.2 - -env: - shell: bash - -phases: - install: - runtime-versions: - nodejs: 22 - commands: - - npm ci - build: - commands: - - | - set -euo pipefail - echo "Building and deploying Gamma for commit ${SOURCE_COMMIT}" - npm run build:prod - test -s dist/index.html - grep -R -F -q 'https://api.gamma.onehook.club' dist - grep -R -F -q 'https://graphql.api.gamma.onehook.club/graphql' dist - if grep -R -F -q 'https://api.onehook.club' dist; then - echo 'Production backend URL was unexpectedly bundled in Gamma.' >&2 - exit 1 - fi - npx cdk deploy OneHook-Frontend-gamma \ - --context env=gamma \ - --require-approval never \ - --outputs-file gamma-outputs.json - -cache: - paths: - - '/root/.npm/**/*' diff --git a/infra/pipeline/frontend-pipeline-stack.ts b/infra/pipeline/frontend-pipeline-stack.ts index 500f9d6..f0a0498 100644 --- a/infra/pipeline/frontend-pipeline-stack.ts +++ b/infra/pipeline/frontend-pipeline-stack.ts @@ -52,7 +52,6 @@ const GITHUB_BRANCH = 'main'; const BUILDSPEC = { verify: 'infra/pipeline/buildspecs/verify.yml', - gammaDeploy: 'infra/pipeline/buildspecs/gamma-deploy.yml', smoke: 'infra/pipeline/buildspecs/smoke.yml', productionBuild: 'infra/pipeline/buildspecs/production-build.yml', productionDeploy: 'infra/pipeline/buildspecs/production-deploy.yml', @@ -217,57 +216,6 @@ export class FrontendPipelineStack extends Stack { ], }); - const gammaDeployRole = codeBuildRole( - 'GammaDeploy', - 'Builds Gamma and assumes only ap-south-1 frontend CDK bootstrap roles' - ); - grantBootstrapAssume( - gammaDeployRole, - bootstrapRoleArns([PIPELINE_REGION], ['deploy', 'file-publishing']) - ); - const gammaDeployProject = createProject( - 'GammaDeploy', - BUILDSPEC.gammaDeploy, - gammaDeployRole, - Duration.minutes(45) - ); - pipeline.addStage({ - stageName: 'DeployGamma', - actions: [ - new CodeBuildAction({ - actionName: 'BuildAndDeployGamma', - project: gammaDeployProject, - input: sourceArtifact, - environmentVariables: { - CDK_DEFAULT_ACCOUNT: { value: ACCOUNTS.frontend }, - CDK_DEFAULT_REGION: { value: PIPELINE_REGION }, - VITE_APP_ENV: { value: 'production' }, - VITE_BACKEND_STAGE: { value: 'gamma' }, - SOURCE_COMMIT: { value: sourceAction.variables.commitId }, - }, - }), - ], - }); - - const gammaSmokeRole = codeBuildRole( - 'GammaSmoke', - 'Performs the public Gamma HTTP smoke test; has no deployment permissions' - ); - const gammaSmokeProject = createProject('GammaSmoke', BUILDSPEC.smoke, gammaSmokeRole); - pipeline.addStage({ - stageName: 'SmokeGamma', - actions: [ - new CodeBuildAction({ - actionName: 'SmokeTestGamma', - project: gammaSmokeProject, - input: sourceArtifact, - environmentVariables: { - SITE_URL: { value: 'https://gamma.onehook.club' }, - }, - }), - ], - }); - const productionBuildRole = codeBuildRole( 'ProductionBuild', 'Builds and synthesizes the immutable production assembly without AWS deployment access' diff --git a/infra/stacks/constants.ts b/infra/stacks/constants.ts index 46a72f5..a3a57e2 100644 --- a/infra/stacks/constants.ts +++ b/infra/stacks/constants.ts @@ -1,96 +1,36 @@ /** * Single source of truth for the OneHook frontend ownership model. * - * FINAL OWNERSHIP MODEL - * --------------------- - * - The FRONTEND account (851725215059) owns BOTH stages' S3/CloudFront stacks AND every - * frontend hosted zone: the parent `onehook.club` zone AND a CDK-managed `gamma.onehook.club` - * zone (created by this repo, not looked up). - * - The BACKEND account (627367419734) owns ONLY the API zones `api.gamma.onehook.club` and - * `api.onehook.club`. The frontend never writes into the backend account; it only READS the - * backend API zone's delegation nameservers via a cross-account read-only role. + * OWNERSHIP MODEL + * --------------- + * - The FRONTEND account (851725215059) owns the production S3/CloudFront stack AND the + * `onehook.club` hosted zone. + * - The BACKEND account (627367419734) owns the API zones (`api.gamma.onehook.club`, + * `api.onehook.club`). The frontend never writes into the backend account. * - * Gamma delegation flow (all frontend-owned, all CloudFormation-owned & deletable): - * 1. Create the `gamma.onehook.club` zone locally in 851 and its A/AAAA aliases. - * 2. Read `api.gamma.onehook.club` nameservers from the backend zone (assume a backend - * read-only role with an ExternalId; discover by name, then call route53:GetHostedZone). - * 3. Create the `api.gamma.onehook.club` NS delegation record in the local gamma zone. - * 4. Switch the parent `onehook.club` zone's `gamma.onehook.club` NS record from the (old) - * backend nameservers to the new local gamma zone nameservers — idempotent UPSERT, clean - * DELETE on teardown. Ordered strictly AFTER step 3. + * There is only one deployed site: production `onehook.club`. Local development and the production + * build both point their browser traffic at the Gamma backend (`api.gamma.onehook.club`) until the + * production backend is promoted; that selection lives in `src/config/deployment.config.ts`, not + * here. The former `gamma.onehook.club` frontend website and its cross-account DNS delegation have + * been removed. */ /** AWS accounts by role. */ export const ACCOUNTS = { - /** Owns both frontend stacks and all frontend hosted zones. */ + /** Owns the production frontend stack and the onehook.club hosted zone. */ frontend: '851725215059', /** Owns only the API zones (api.gamma.onehook.club, api.onehook.club). */ backend: '627367419734', } as const; -/** Public site domains. */ +/** Public site domain. */ export const DOMAINS = { prod: 'onehook.club', - gamma: 'gamma.onehook.club', - apiGamma: 'api.gamma.onehook.club', } as const; /** - * Stable public identifier for the existing frontend-owned `onehook.club` hosted zone. Route53 - * hosted-zone IDs are public routing identifiers, not credentials. Importing this exact zone by ID - * makes every frontend synth deterministic and removes all CDK lookup-role access from CodeBuild. + * Stable public identifier for the frontend-owned `onehook.club` hosted zone. Route53 hosted-zone + * IDs are public routing identifiers, not credentials. Importing this exact zone by ID makes every + * frontend synth deterministic and removes all CDK lookup-role access from CodeBuild. */ export const PRODUCTION_HOSTED_ZONE_ID = 'Z0711151B3O279W1TQZ0'; - -/** - * Pre-existing wildcard ACM certificate in the FRONTEND account (us-east-1) covering - * `*.onehook.club` — reused as-is for the gamma CloudFront distribution so gamma provisions no new - * certificate. Production keeps its own dedicated `OneHook-Certificate-prod` stack (ownership - * preserved), so this constant is used by gamma only. - */ -export const GAMMA_CERTIFICATE_ARN = - 'arn:aws:acm:us-east-1:851725215059:certificate/2e05f27b-fef3-4400-9352-0b62a70c024c'; - -/** - * Cross-account contract for READING the backend-owned `api.gamma.onehook.club` delegation. - * - * The role ARN and ExternalId may be overridden via CDK context if the trust contract is - * intentionally re-pointed. The API zone itself is identified by its deterministic DNS name, so - * deployment never depends on a generated hosted-zone ID or a manually supplied context value. - * - * The frontend Lambda's execution role name is deterministic (see DNS_DELEGATION) so the backend - * can scope the reader role's trust policy to that exact principal + ExternalId. - */ -export const BACKEND_DNS = { - /** Matches apiDnsReaderRoleName('gamma') in the backend repository. */ - readerRoleArn: `arn:aws:iam::${ACCOUNTS.backend}:role/OneHook-ApiDnsReader-gamma-Role`, - /** Deterministic API hosted-zone name discovered read-only by the custom-resource Lambda. */ - apiGammaZoneName: DOMAINS.apiGamma, - /** Shared ExternalId for the cross-account assume-role. */ - externalId: 'onehook-gamma-api-dns-delegation', -} as const; - -/** Frontend-side delegation identifiers that must stay deterministic. */ -export const DNS_DELEGATION = { - /** - * Fixed execution-role name for the reader Lambda in the frontend account. The backend reader - * role trusts exactly `arn:aws:iam::851725215059:role/` + ExternalId. - */ - readerRoleName: 'OneHook-Gamma-ApiDnsReader', -} as const; - -/** - * Resolve the backend DNS contract, letting CDK context override the deterministic defaults. - * `node` is the construct/app node exposing `tryGetContext`. - */ -export function resolveBackendDns(node: { - tryGetContext(key: string): unknown; -}): { readerRoleArn: string; apiGammaZoneName: string; externalId: string } { - return { - readerRoleArn: - (node.tryGetContext('backendDnsReaderRoleArn') as string) ?? BACKEND_DNS.readerRoleArn, - apiGammaZoneName: BACKEND_DNS.apiGammaZoneName, - externalId: - (node.tryGetContext('dnsDelegationExternalId') as string) ?? BACKEND_DNS.externalId, - }; -} diff --git a/infra/stacks/frontend-app.ts b/infra/stacks/frontend-app.ts index 9fd9ed4..ddec624 100644 --- a/infra/stacks/frontend-app.ts +++ b/infra/stacks/frontend-app.ts @@ -3,75 +3,39 @@ import { App, Stack, StackProps, Tags } from 'aws-cdk-lib'; import { HostedZone } from 'aws-cdk-lib/aws-route53'; import { Certificate, CertificateValidation } from 'aws-cdk-lib/aws-certificatemanager'; import { Construct } from 'constructs'; -import { FrontendStack, ApiDelegationConfig } from './frontend-stack.ts'; -import { - ACCOUNTS, - DOMAINS, - GAMMA_CERTIFICATE_ARN, - PRODUCTION_HOSTED_ZONE_ID, - resolveBackendDns, -} from './constants.ts'; +import { FrontendStack } from './frontend-stack.ts'; +import { ACCOUNTS, DOMAINS, PRODUCTION_HOSTED_ZONE_ID } from './constants.ts'; /** - * FINAL OWNERSHIP MODEL — every frontend stack and every frontend hosted zone lives in the - * FRONTEND account (851725215059); the backend account owns only the API zones. See constants.ts. + * OWNERSHIP MODEL — the production frontend stack and the `onehook.club` hosted zone both live in + * the FRONTEND account (851725215059); the backend account owns only the API zones. See + * constants.ts. * - * gamma -> account 851725215059, NEW CDK-managed zone gamma.onehook.club, site gamma.onehook.club - * prod -> account 851725215059, existing zone onehook.club, site onehook.club (+ www) + * prod -> account 851725215059, existing zone onehook.club, site onehook.club (+ www) * - * The frontend (S3 + CloudFront) lives in ap-south-1 for both. CloudFront certificates live in - * us-east-1: - * - prod keeps its dedicated us-east-1 stack `OneHook-Certificate-prod` so the existing - * CloudFormation logical/resource ownership is preserved (no resource churn on redeploy). - * - gamma reuses the pre-existing frontend-account wildcard `*.onehook.club` certificate (no new - * cert, no separate stack). + * The frontend (S3 + CloudFront) lives in ap-south-1. The CloudFront certificate lives in us-east-1 + * in a dedicated stack `OneHook-Certificate-prod`, preserving the existing CloudFormation + * logical/resource ownership (no resource churn on redeploy). * - * gamma additionally delegates api.gamma.onehook.club to the backend-owned API zone WITHOUT writing - * into the backend account (read-only cross-account nameserver read + local NS records). + * The browser talks to the Gamma backend (`api.gamma.onehook.club`) for now; that selection is made + * at build time via `VITE_BACKEND_STAGE` and lives in `src/config/deployment.config.ts`. There is + * no separate `gamma.onehook.club` frontend website. */ -type Stage = 'gamma' | 'prod'; +type Stage = 'prod'; interface StageConfig { account: string; region: string; domainName: string; hostedZoneName: string; - /** Stable ID for an existing hosted zone; omitted only when this app creates the zone. */ - hostedZoneId?: string; + /** Stable ID for the existing hosted zone. */ + hostedZoneId: string; includeWww: boolean; - /** prod owns a dedicated us-east-1 CertificateStack; gamma reuses the wildcard cert ARN. */ - separateCertStack: boolean; - /** Pre-provisioned us-east-1 cert ARN. Set for gamma; prod derives it from its cert stack. */ - certificateArn?: string; - /** gamma creates a brand-new zone; prod looks up its existing zone. */ - createHostedZone: boolean; - /** gamma-only cross-account API delegation. */ - apiDelegation?: ApiDelegationConfig; } const app = new App(); -const backendDns = resolveBackendDns(app.node); - const STAGES: Record = { - gamma: { - account: ACCOUNTS.frontend, - region: 'ap-south-1', - domainName: DOMAINS.gamma, - hostedZoneName: DOMAINS.gamma, - includeWww: false, - separateCertStack: false, - certificateArn: GAMMA_CERTIFICATE_ARN, - createHostedZone: true, - apiDelegation: { - parentZoneName: DOMAINS.prod, - parentZoneId: PRODUCTION_HOSTED_ZONE_ID, - apiSubdomain: DOMAINS.apiGamma, - backendReaderRoleArn: backendDns.readerRoleArn, - backendApiZoneName: backendDns.apiGammaZoneName, - externalId: backendDns.externalId, - }, - }, prod: { account: ACCOUNTS.frontend, region: 'ap-south-1', @@ -79,13 +43,11 @@ const STAGES: Record = { hostedZoneName: DOMAINS.prod, hostedZoneId: PRODUCTION_HOSTED_ZONE_ID, includeWww: true, - separateCertStack: true, - createHostedZone: false, }, }; /** - * Dedicated us-east-1 certificate stack. Used only by prod to preserve the pre-existing + * Dedicated us-east-1 certificate stack for prod. Preserves the pre-existing * `OneHook-Certificate-prod` logical/resource ownership — do not change the `HostedZone`/ * `Certificate` construct ids or the stack name, or prod would replace live resources. */ @@ -110,11 +72,9 @@ class CertificateStack extends Stack { } } -const stageName = (app.node.tryGetContext('env') ?? 'gamma') as string; -if (stageName !== 'gamma' && stageName !== 'prod') { - throw new Error( - `Unknown stage "${stageName}". Use --context env=gamma or --context env=prod.` - ); +const stageName = (app.node.tryGetContext('env') ?? 'prod') as string; +if (stageName !== 'prod') { + throw new Error(`Unknown stage "${stageName}". Use --context env=prod.`); } const stage = STAGES[stageName as Stage]; @@ -135,23 +95,12 @@ Tags.of(app).add('Project', 'OneHook'); Tags.of(app).add('Component', 'Frontend'); Tags.of(app).add('ManagedBy', 'CDK'); -let certificateArn: string | undefined = stage.certificateArn; -if (stage.separateCertStack) { - if (!stage.hostedZoneId) { - throw new Error(`No hosted-zone ID configured for stage "${stageName}".`); - } - const certStack = new CertificateStack(app, `OneHook-Certificate-${stageName}`, { - env: { account: stage.account, region: 'us-east-1' }, // CloudFront certs MUST be in us-east-1 - crossRegionReferences: true, - domainName: stage.domainName, - hostedZoneId: stage.hostedZoneId, - }); - certificateArn = certStack.certificateArn; -} - -if (!certificateArn) { - throw new Error(`No certificate ARN resolved for stage "${stageName}".`); -} +const certStack = new CertificateStack(app, `OneHook-Certificate-${stageName}`, { + env: { account: stage.account, region: 'us-east-1' }, // CloudFront certs MUST be in us-east-1 + crossRegionReferences: true, + domainName: stage.domainName, + hostedZoneId: stage.hostedZoneId, +}); new FrontendStack(app, `OneHook-Frontend-${stageName}`, { env, @@ -163,9 +112,7 @@ new FrontendStack(app, `OneHook-Frontend-${stageName}`, { hostedZoneName: stage.hostedZoneName, hostedZoneId: stage.hostedZoneId, includeWww: stage.includeWww, - createHostedZone: stage.createHostedZone, - certificateArn, - apiDelegation: stage.apiDelegation, + certificateArn: certStack.certificateArn, }); app.synth(); diff --git a/infra/stacks/frontend-stack.ts b/infra/stacks/frontend-stack.ts index 39c4561..a5f345b 100644 --- a/infra/stacks/frontend-stack.ts +++ b/infra/stacks/frontend-stack.ts @@ -1,4 +1,4 @@ -import { Stack, StackProps, RemovalPolicy, Duration, CfnOutput, Fn } from 'aws-cdk-lib'; +import { Stack, StackProps, RemovalPolicy, Duration, CfnOutput } from 'aws-cdk-lib'; import { Bucket, BucketEncryption, BlockPublicAccess, ObjectOwnership } from 'aws-cdk-lib/aws-s3'; import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'; import { @@ -23,73 +23,31 @@ import { IHostedZone, ARecord, AaaaRecord, - NsRecord, RecordTarget, } from 'aws-cdk-lib/aws-route53'; import { CloudFrontTarget } from 'aws-cdk-lib/aws-route53-targets'; -import { Role, ServicePrincipal, ManagedPolicy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; -import { Runtime, Code, Function as LambdaFunction } from 'aws-cdk-lib/aws-lambda'; -import { - Provider, - AwsCustomResource, - AwsCustomResourcePolicy, - PhysicalResourceId, -} from 'aws-cdk-lib/custom-resources'; -import { CustomResource } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { join } from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; -import { DNS_DELEGATION } from './constants.ts'; // Get the directory of the current file const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -/** - * Cross-account delegation of `api.` to the backend-owned API zone. The frontend only - * READS the backend nameservers (never writes into the backend account) and then owns the NS - * record locally. Set for gamma only. - */ -export interface ApiDelegationConfig { - /** Parent zone that holds the coarse `gamma.onehook.club` delegation (e.g. onehook.club). */ - parentZoneName: string; - /** Stable ID of the existing frontend-owned parent zone; avoids deployment-time AWS lookups. */ - parentZoneId: string; - /** Fully-qualified API subdomain to delegate, e.g. api.gamma.onehook.club. */ - apiSubdomain: string; - /** Read-only backend role for zone-name discovery plus scoped route53:GetHostedZone. */ - backendReaderRoleArn: string; - /** Deterministic DNS name used to discover the API hosted zone read-only in the backend account. */ - backendApiZoneName: string; - /** ExternalId enforced by the backend reader role's trust policy. */ - externalId: string; -} - export interface FrontendStackProps extends StackProps { - /** Deployment stage. Drives retention/versioning/price-class and cert strategy. */ - stage: 'gamma' | 'prod'; - /** Public site domain, e.g. gamma.onehook.club or onehook.club. */ + /** Deployment stage. Drives retention/versioning/price-class. */ + stage: 'prod'; + /** Public site domain, e.g. onehook.club. */ domainName: string; /** In-account Route53 zone that owns {@link domainName}. */ hostedZoneName: string; - /** Stable ID required when importing an existing zone; omitted when createHostedZone is true. */ - hostedZoneId?: string; + /** Stable ID of the existing zone to import. */ + hostedZoneId: string; /** Also serve/alias www. (prod apex only). */ includeWww: boolean; - /** - * Create the hosted zone in this stack (gamma: a brand-new CDK-managed `gamma.onehook.club` - * zone in the frontend account) instead of importing an existing one by ID (prod: the pre-existing - * `onehook.club` zone). - */ - createHostedZone: boolean; - /** - * ARN of a pre-provisioned us-east-1 certificate. prod imports its dedicated CertificateStack - * ARN; gamma reuses the frontend-account wildcard cert. Always set in the final model. - */ + /** ARN of the pre-provisioned us-east-1 certificate from the dedicated CertificateStack. */ certificateArn: string; - /** Gamma-only cross-account API delegation. Omitted for prod. */ - apiDelegation?: ApiDelegationConfig; } export class FrontendStack extends Stack { @@ -102,19 +60,12 @@ export class FrontendStack extends Stack { const stage = props.stage; const isProd = stage === 'prod'; - // Hosted zone that owns the site domain, in the FRONTEND account either way. - // - gamma: a brand-new CDK-managed `gamma.onehook.club` zone (createHostedZone=true). It is - // CloudFormation-owned and deletable, replacing the previously backend-owned zone. - // - prod: the pre-existing `onehook.club` zone, imported by exact source-controlled ID. - if (!props.createHostedZone && !props.hostedZoneId) { - throw new Error(`Existing hosted zone ${props.hostedZoneName} requires hostedZoneId.`); - } - const hostedZone: IHostedZone = props.createHostedZone - ? new HostedZone(this, 'HostedZone', { zoneName: props.hostedZoneName }) - : HostedZone.fromHostedZoneAttributes(this, 'HostedZone', { - hostedZoneId: props.hostedZoneId as string, - zoneName: props.hostedZoneName, - }); + // The pre-existing `onehook.club` hosted zone, imported by exact source-controlled ID, in the + // FRONTEND account. + const hostedZone: IHostedZone = HostedZone.fromHostedZoneAttributes(this, 'HostedZone', { + hostedZoneId: props.hostedZoneId, + zoneName: props.hostedZoneName, + }); // S3 bucket for static website this.bucket = new Bucket(this, 'WebsiteBucket', { @@ -184,9 +135,8 @@ export class FrontendStack extends Stack { }, }); - // TLS certificate for CloudFront (must be us-east-1), always imported by ARN: - // - prod imports its dedicated us-east-1 CertificateStack ARN (ownership preserved). - // - gamma reuses the pre-existing frontend-account wildcard `*.onehook.club` cert. + // TLS certificate for CloudFront (must be us-east-1), imported by ARN from the dedicated + // us-east-1 CertificateStack (ownership preserved). const certificate: ICertificate = Certificate.fromCertificateArn( this, 'Certificate', @@ -253,7 +203,7 @@ export class FrontendStack extends Stack { memoryLimit: 512, }); - // Route53 A + AAAA aliases in the frontend-account zone for this stage. + // Route53 A + AAAA aliases in the frontend-account zone. const aliasTarget = RecordTarget.fromAlias(new CloudFrontTarget(this.distribution)); new ARecord(this, 'AliasRecord', { @@ -282,117 +232,6 @@ export class FrontendStack extends Stack { }); } - // ------------------------------------------------------------------------------------------ - // Gamma-only: cross-account API delegation + parent NS switch. - // - // `api.gamma.onehook.club` stays owned by the BACKEND account. We delegate to it from our new - // local gamma zone WITHOUT writing into the backend account: - // 1. A deterministic-role Lambda assumes a backend read-only role (ExternalId) and reads the - // backend API zone's delegation nameservers (route53:GetHostedZone). - // 2. We create the `api.gamma.onehook.club` NS record locally from those nameservers. - // 3. Only AFTER that, we UPSERT the parent `onehook.club` zone's `gamma.onehook.club` NS - // record to point at THIS new gamma zone's nameservers (idempotent; clean DELETE on - // teardown). All resources are CloudFormation-owned and deletable. - // ------------------------------------------------------------------------------------------ - if (props.apiDelegation) { - const del = props.apiDelegation; - - // (1) Deterministic execution role in the frontend account. Its fixed name lets the backend - // scope its read-only role's trust policy to exactly this principal + ExternalId. - const readerRole = new Role(this, 'ApiDnsReaderRole', { - roleName: DNS_DELEGATION.readerRoleName, - assumedBy: new ServicePrincipal('lambda.amazonaws.com'), - managedPolicies: [ - ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'), - ], - }); - readerRole.addToPolicy( - new PolicyStatement({ - actions: ['sts:AssumeRole'], - resources: [del.backendReaderRoleArn], - }) - ); - - const readerFn = new LambdaFunction(this, 'ApiDnsReaderFn', { - runtime: Runtime.NODEJS_22_X, - handler: 'index.handler', - code: Code.fromAsset(join(__dirname, '../lambda/api-dns-reader')), - role: readerRole, - timeout: Duration.minutes(2), - description: 'Reads api.gamma.onehook.club delegation nameservers from the backend account', - }); - - const readerProvider = new Provider(this, 'ApiDnsReaderProvider', { - onEventHandler: readerFn, - }); - - const apiNameServers = new CustomResource(this, 'ApiGammaNameServers', { - serviceToken: readerProvider.serviceToken, - properties: { - BackendReaderRoleArn: del.backendReaderRoleArn, - ExternalId: del.externalId, - BackendApiZoneName: del.backendApiZoneName, - }, - }); - - // (2) Local NS record delegating api.gamma.onehook.club to the backend API zone. - const apiDelegationRecord = new NsRecord(this, 'ApiGammaDelegation', { - zone: hostedZone, - recordName: del.apiSubdomain, - values: Fn.split(',', apiNameServers.getAttString('NameServers')), - ttl: Duration.minutes(5), - }); - - // (3) Switch the parent zone's coarse `gamma.onehook.club` delegation from the old backend - // nameservers to THIS local gamma zone. Public hosted zones always expose exactly four - // delegation nameservers, so we reference indices 0..3 (a token list cannot be `.map`ped at - // synth time). - const parentZone = HostedZone.fromHostedZoneAttributes(this, 'ParentZone', { - hostedZoneId: del.parentZoneId, - zoneName: del.parentZoneName, - }); - const gammaNameServers = hostedZone.hostedZoneNameServers as string[]; - const resourceRecords = [0, 1, 2, 3].map((i) => ({ - Value: Fn.select(i, gammaNameServers), - })); - const parentGammaRecordSet = { - Name: `${props.domainName}.`, - Type: 'NS', - TTL: 300, - ResourceRecords: resourceRecords, - }; - const changeBatch = (action: 'UPSERT' | 'DELETE') => ({ - service: 'Route53', - action: 'changeResourceRecordSets', - parameters: { - HostedZoneId: parentZone.hostedZoneId, - ChangeBatch: { - Comment: `${action} ${props.domainName} delegation -> frontend gamma zone`, - Changes: [{ Action: action, ResourceRecordSet: parentGammaRecordSet }], - }, - }, - physicalResourceId: PhysicalResourceId.of(`parent-gamma-ns-${parentZone.hostedZoneId}`), - }); - - const parentNsSwitch = new AwsCustomResource(this, 'ParentGammaNsSwitch', { - resourceType: 'Custom::ParentGammaNsSwitch', - onCreate: changeBatch('UPSERT'), - onUpdate: changeBatch('UPSERT'), - onDelete: changeBatch('DELETE'), - policy: AwsCustomResourcePolicy.fromStatements([ - new PolicyStatement({ - actions: ['route53:ChangeResourceRecordSets'], - resources: [parentZone.hostedZoneArn], - }), - ]), - installLatestAwsSdk: false, - }); - - // Ordering: the nested api.gamma.onehook.club delegation must exist BEFORE we point the world - // at this gamma zone via the parent switch, so api resolution never breaks mid-migration. - parentNsSwitch.node.addDependency(apiDelegationRecord); - } - // Outputs new CfnOutput(this, 'WebsiteURL', { value: `https://${this.distribution.distributionDomainName}`, diff --git a/package.json b/package.json index fc69695..709acc8 100644 --- a/package.json +++ b/package.json @@ -22,13 +22,11 @@ "generate": "graphql-codegen", "validate": "npm run lint && npm test", "cdk:synth": "npx tsx infra/stacks/frontend-app.ts", - "synth:gamma": "npx cdk synth --context env=gamma", "synth:prod": "npx cdk synth --context env=prod", "synth:pipeline": "npx cdk synth --app 'npx tsx infra/pipeline/pipeline-app.ts'", "diff:pipeline": "npx cdk diff OneHook-Frontend-Pipeline --app 'npx tsx infra/pipeline/pipeline-app.ts'", "deploy:pipeline": "npx cdk deploy OneHook-Frontend-Pipeline --app 'npx tsx infra/pipeline/pipeline-app.ts' --require-approval broadening", "cdk:deploy": "npm run build && npx cdk deploy --all --app 'npx tsx infra/stacks/frontend-app.ts' --require-approval never", - "deploy:gamma": "npm run cdk:deploy -- --context env=gamma", "deploy:prod": "npm run cdk:deploy -- --context env=prod --require-approval broadening" }, "dependencies": { diff --git a/src/config/deployment.config.ts b/src/config/deployment.config.ts index c108632..85c5309 100644 --- a/src/config/deployment.config.ts +++ b/src/config/deployment.config.ts @@ -5,9 +5,9 @@ * app-client and identity-pool IDs are public routing identifiers; authorization remains enforced * by Cognito, JWT validation and IAM policies. * - * Both gamma.onehook.club and onehook.club intentionally use `gamma` until the backend production - * rollout is complete. Add a `prod` entry and change the production workflow selector only after - * that backend has been deployed and validated. + * Local development and the production site (onehook.club) both intentionally use the `gamma` + * backend entry until the backend production rollout is complete. Add a `prod` entry and change the + * production build selector only after that backend has been deployed and validated. */ export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; diff --git a/src/tests/deployment-pipeline.test.ts b/src/tests/deployment-pipeline.test.ts index 4f3fb64..e9df43a 100644 --- a/src/tests/deployment-pipeline.test.ts +++ b/src/tests/deployment-pipeline.test.ts @@ -5,10 +5,6 @@ import { describe, expect, it } from 'vitest'; import { FrontendPipelineStack } from '../../infra/pipeline/frontend-pipeline-stack'; const root = new URL('../../', import.meta.url); -const gammaDeploy = readFileSync( - new URL('infra/pipeline/buildspecs/gamma-deploy.yml', root), - 'utf8' -); const smoke = readFileSync( new URL('infra/pipeline/buildspecs/smoke.yml', root), 'utf8' @@ -40,7 +36,7 @@ function resourcesOf( } describe('AWS-native frontend deployment pipeline', () => { - it('uses a queued V2 pipeline with Gamma-first deployment and AWS production approval', () => { + it('uses a queued V2 pipeline with direct production build and AWS production approval', () => { const template = synthPipeline(); const [, pipeline] = resourcesOf(template, 'AWS::CodePipeline::Pipeline')[0]; const properties = pipeline.Properties as Record; @@ -51,15 +47,13 @@ describe('AWS-native frontend deployment pipeline', () => { expect(stages.map((stage) => stage.Name)).toEqual([ 'Source', 'Verify', - 'DeployGamma', - 'SmokeGamma', 'BuildProduction', 'ApproveProduction', 'DeployProduction', 'SmokeProduction', ]); - const approvalAction = (stages[5].Actions as Array>)[0]; + const approvalAction = (stages[3].Actions as Array>)[0]; const actionType = approvalAction.ActionTypeId as Record; expect(actionType.Category).toBe('Approval'); expect(actionType.Provider).toBe('Manual'); @@ -110,12 +104,6 @@ describe('AWS-native frontend deployment pipeline', () => { return JSON.stringify(policy); }; - const gamma = policyJson('GammaDeploy'); - expect(gamma).toContain('deploy-role-851725215059-ap-south-1'); - expect(gamma).toContain('file-publishing-role-851725215059-ap-south-1'); - expect(gamma).not.toContain('lookup-role'); - expect(gamma).not.toContain('us-east-1'); - const productionBuildPolicy = policyJson('ProductionBuild'); expect(productionBuildPolicy).not.toContain('sts:AssumeRole'); expect(productionBuildPolicy).not.toContain('cdk-hnb659fds'); @@ -133,7 +121,7 @@ describe('AWS-native frontend deployment pipeline', () => { }, 15_000); it('builds and synthesizes before approval, then deploys the exact assembly without rebuilding', () => { - for (const strictBuildspec of [gammaDeploy, smoke, productionBuild, productionDeploy]) { + for (const strictBuildspec of [smoke, productionBuild, productionDeploy]) { expect(strictBuildspec).toContain('shell: bash'); expect(strictBuildspec).toContain('set -euo pipefail'); } diff --git a/src/tests/infra-ownership.test.ts b/src/tests/infra-ownership.test.ts index f02deac..63d68e6 100644 --- a/src/tests/infra-ownership.test.ts +++ b/src/tests/infra-ownership.test.ts @@ -1,76 +1,28 @@ import { describe, expect, it } from 'vitest'; -import { - ACCOUNTS, - DOMAINS, - GAMMA_CERTIFICATE_ARN, - PRODUCTION_HOSTED_ZONE_ID, - BACKEND_DNS, - DNS_DELEGATION, - resolveBackendDns, -} from '../../infra/stacks/constants'; +import { ACCOUNTS, DOMAINS, PRODUCTION_HOSTED_ZONE_ID } from '../../infra/stacks/constants'; /** - * Guards the FINAL ownership model so a future edit can't silently drift it back: - * - both frontend stages + all frontend zones live in the frontend account (851725215059); + * Guards the ownership model so a future edit can't silently drift it: + * - the production frontend stack and the onehook.club zone live in the frontend account + * (851725215059); * - the backend account (627367419734) owns only the API zones; - * - gamma reuses the frontend-account wildcard cert (us-east-1) and never mints a new one; - * - the cross-account DNS-read contract is configurable via CDK context. + * - there is a single deployed site (production onehook.club) — the former gamma.onehook.club + * frontend website has been removed. * - * These are pure constants/helpers with no CDK/AWS imports, so the test stays hermetic and fast. + * These are pure constants with no CDK/AWS imports, so the test stays hermetic and fast. */ describe('infra ownership model', () => { - it('places every frontend resource in the frontend account and API zones in the backend', () => { + it('places the frontend resources in the frontend account and API zones in the backend', () => { expect(ACCOUNTS.frontend).toBe('851725215059'); expect(ACCOUNTS.backend).toBe('627367419734'); }); - it('reuses the pre-existing frontend-account wildcard certificate for gamma', () => { - expect(GAMMA_CERTIFICATE_ARN).toBe( - 'arn:aws:acm:us-east-1:851725215059:certificate/2e05f27b-fef3-4400-9352-0b62a70c024c' - ); - // Certificate must be in the frontend account, us-east-1 (CloudFront requirement). - expect(GAMMA_CERTIFICATE_ARN).toContain(`:acm:us-east-1:${ACCOUNTS.frontend}:`); - }); - - it('models the domains and exact frontend-owned production zone', () => { + it('models the production domain and exact frontend-owned production zone', () => { expect(DOMAINS.prod).toBe('onehook.club'); - expect(DOMAINS.gamma).toBe('gamma.onehook.club'); - expect(DOMAINS.apiGamma).toBe('api.gamma.onehook.club'); expect(PRODUCTION_HOSTED_ZONE_ID).toBe('Z0711151B3O279W1TQZ0'); }); - it('scopes the backend DNS reader role to the backend account with the deployed role name', () => { - expect(BACKEND_DNS.readerRoleArn).toBe( - `arn:aws:iam::${ACCOUNTS.backend}:role/OneHook-ApiDnsReader-gamma-Role` - ); - // Deterministic frontend execution-role name the backend trust policy can pin to. - expect(DNS_DELEGATION.readerRoleName).toBe('OneHook-Gamma-ApiDnsReader'); - }); - - it('uses the exact API DNS name instead of a generated hosted-zone ID', () => { - expect(BACKEND_DNS.apiGammaZoneName).toBe(DOMAINS.apiGamma); - expect(BACKEND_DNS).not.toHaveProperty('apiGammaZoneId'); - }); - - it('resolves deterministic defaults when no context override is supplied', () => { - const node = { tryGetContext: () => undefined }; - const resolved = resolveBackendDns(node); - - expect(resolved.readerRoleArn).toBe(BACKEND_DNS.readerRoleArn); - expect(resolved.apiGammaZoneName).toBe(DOMAINS.apiGamma); - expect(resolved.externalId).toBe(BACKEND_DNS.externalId); - }); - - it('lets CDK context intentionally override the backend trust contract', () => { - const overrides: Record = { - backendDnsReaderRoleArn: 'arn:aws:iam::627367419734:role/Custom-Reader', - dnsDelegationExternalId: 'custom-external-id', - }; - const node = { tryGetContext: (k: string) => overrides[k] }; - const resolved = resolveBackendDns(node); - - expect(resolved.readerRoleArn).toBe('arn:aws:iam::627367419734:role/Custom-Reader'); - expect(resolved.apiGammaZoneName).toBe(DOMAINS.apiGamma); - expect(resolved.externalId).toBe('custom-external-id'); + it('no longer exposes a gamma frontend site domain', () => { + expect(DOMAINS).not.toHaveProperty('gamma'); }); });