Skip to content

Latest commit

 

History

History
106 lines (85 loc) · 5.53 KB

README.md

File metadata and controls

106 lines (85 loc) · 5.53 KB

@cloudfront-redirect-rules/api

The control-plane console API (CF-11). A single Node 22 Lambda behind an HTTP API Gateway (v2) that manages redirect/rewrite rules in DynamoDB. It is a client of the rules table — it only ever writes rules the Lambda@Edge reads; the two never talk directly.

Spec-first: openapi.yaml is the contract, and every rule request/response body $refs the schemas in ../../shared — rule shapes are never redefined here.

Routes

Method Path Status
GET /health ✅ implemented
GET / POST /targets ✅ implemented (CF-12)
GET/PUT/DELETE /targets/{id} ✅ implemented (CF-12)
GET / POST /targets/{targetId}/hosts/{host}/rules ✅ implemented (CF-13)
GET/PUT/DELETE /targets/{targetId}/hosts/{host}/rules/{sk} ✅ implemented (CF-13)
PATCH /targets/{targetId}/hosts/{host}/rules/{sk} ✅ disabled toggle (CF-13)

Rule routes are scoped to a target (a DynamoDB table from the targets registry, CF-12) and a host (the partition key), and write to that target's table.

The server owns both keys. A request body carries the rule's fields plus a priority integer; pk is the host from the path and sk is TYPE#priority, zero-padded to five digits (REDIRECT#00100). Responses are the stored item — the exact shape the Lambda@Edge reads. Because the priority is part of the key, a PUT that changes it moves the rule: one transaction writes the new key and removes the old, so the rule is never live at two priorities.

PATCH is the exception to full-replace semantics: it takes { "disabled": … } and nothing else, so turning a rule off never depends on the client holding a complete copy of it. A disabled rule keeps its place in the table and its priority; the Lambda@Edge skips it. Like every rule change, it reaches the edge within the edge cache TTL (~1 min), not instantly.

Design

  • Router (src/router.ts) — a hand-rolled method+path matcher with :param extraction. No web framework; one plain Lambda.
  • One request path — src/handler.ts (Lambda) and src/local.ts (dev server) both build the same ApiRequest and run the same router.
  • Errors (src/lib/errors.ts) — every failure is an ApiError serialized to a standard envelope: { "error": { "code", "message", "details"? } }. code is a stable string the SPA switches on.
  • Validation (src/lib/validate.ts) — Ajv compiled against the shared schemas; failures become 400 VALIDATION_ERROR with per-field details.

Why registering a target only half-checks the table

src/lib/verify-table.ts rejects a new target when DescribeTable says ResourceNotFoundException, and lets every other failure through with a warning. That asymmetry is deliberate, and worth knowing before you tighten it.

A mistyped table name would otherwise be a valid registration. The duplicate check compares tableName exactly — and has to, since DynamoDB table names are case-sensitive — so Edgeroute-rules is a different table from edgeroute-rules, not a duplicate. Both entries sit in the registry under the same display name, and the broken one only surfaces on the first rules request, as a 502.

But "cannot tell" is not "does not exist". A target that is unreachable right now is expected: registering is a runtime action while IAM is granted at apply time, so target_table_arns is routinely still empty when the target goes in. Rejecting on a failed assume-role, a denied DescribeTable or a throttle would break that order; those keep surfacing later as 502 TARGET_UNREACHABLE, which explains itself.

Which makes the IAM grant decide how much of the check works, because IAM answers a denied DescribeTable before DynamoDB can say whether the table is there. The module grants it account-wide (table/*, the DescribeTargetTables statement) rather than over target_table_arns: a grant scoped to the tables that should exist denies precisely the mistyped names, which arrive as AccessDenied and are allowed through. For a target with its own roleArn the same applies to that role's policy, which this module does not write — if it names its table exactly, a typo reads as "cannot tell" and the check is inert for that target. The could not verify table warning is what makes that visible.

Develop

npm run dev -w console/api          # local HTTP server on :3000 (PORT to override)
curl localhost:3000/health          # {"status":"ok"}

Test / lint / build

npm test -w console/api             # vitest (router, handler, validation)
npm run typecheck -w console/api    # tsc --noEmit
npm run openapi:lint -w console/api # redocly lint openapi.yaml
npm run build -w console/api        # esbuild -> dist/index.mjs

All four run in CI.

Deploy

Terraform lives in infra/ — HTTP API Gateway + Lambda + IAM + logs. See its README, and cognito.tf beside it for auth (CF-23): a Cognito user pool, and a JWT authorizer that refuses a request at the gateway before it reaches the Lambda. Four routes stay public — /health, and the three /auth routes that issue the token in the first place.