# Serverless Maven Plugin - AI Quick Reference

## Essential Information

**Plugin:** `uk.co.taylor-made-software:plugin-maven:1.0.1-SNAPSHOT`  
**Config File:** `src/main/serverless/config.json`  
**Schema:** https://www.taylor-made-software.co.uk/schema/serverless-config.schema.json

## Root Structure

```json
{
  "name": "string",
  "application": "com.example.app",
  "provider": "aws|azure|gcp",
  "variables": { "key": "value" },
  "regions": [
    {
      "region": "region-name|global",
      "items": [ /* components */ ]
    }
  ]
}
```

## Component Types

| Type | Region | Description |
|------|--------|-------------|
| `website` | specific | Static website with CDN |
| `storage` | specific | Object storage bucket |
| `api-gateway` | specific | HTTP REST API with routes, or WebSocket API (`protocol: "websocket"`) |
| `functions` (alias `function`) | specific | Serverless function group |
| `database` | specific | NoSQL database |
| `authentication` | global | User identity provider |
| `email` | global | Transactional email |
| `sms` | global | SMS messaging |
| `event-bus` | specific | Pub/sub event bus |
| `waf` | specific | Web application firewall (protects website / api-gateway) |

## Component Base Structure

```json
{
  "type": "component-type",
  "name": "unique-name",
  "providerSettings": { /* type-specific config */ },
  "links": [
    {
      "sourceComponent": "this-component",
      "targetComponent": "target-component",
      "linkType": "LINK_TYPE"
    }
  ]
}
```

**Authoritative config shape (do not deviate):**
- `type` is matched case-insensitively. `function` and `functions` are equivalent aliases for the function group.
- `links` are declared **per-component** (inside each component object). There is **no top-level `links` array** — a root-level `links` block is silently ignored by the parser.
- Each link uses `sourceComponent`, `targetComponent`, `linkType` (NOT `source`/`target`/`type`).
- `linkType` is **case-sensitive** and must exactly match an enum value (e.g. `FUNCTION_TO_DATABASE`).

## Authentication Component

```json
{
  "type": "authentication",
  "name": "user-auth",
  "providerSettings": {
    "loginProviders": ["email", "phone", "google", "facebook", "apple"],
    "registration": "open|admin-only",
    "mfa": "off|optional|required",
    "passwordPolicy": {
      "minLength": 8,
      "requireUppercase": true,
      "requireNumbers": true,
      "requireSymbols": false
    },
    "groups": [
      { "name": "admin", "description": "Administrators" }
    ],
    "users": [
      { "email": "user@example.com", "groups": ["admin"] }
    ]
  },
  "links": [
    {
      "sourceComponent": "user-auth",
      "targetComponent": "email-component",
      "linkType": "AUTHENTICATION_TO_EMAIL"
    }
  ]
}
```

**Key Points:**
- Deploy in `global` region
- Groups provisioned before users
- User groups must reference defined group names
- Both name and description required for groups
- Email required for users
- **AUTHENTICATION_TO_EMAIL link**: When present, Cognito uses SES (DEVELOPER mode) to send invite/reset emails instead of the default email service (limited to 50/day)
- The Email component MUST be deployed before Auth — the link ensures this ordering
- The `sourceArn` for SES is automatically resolved from the Email component's deployment result
- **Idempotent deployment**: User Pools and Clients are found by name before creating (no duplicates); existing users (`UsernameExistsException`) and groups (`GroupExistsException`) are silently skipped

### Cognito Lambda Triggers (AWS)

Attach user-pool Lambda triggers via `providerSettings.triggers` — a map of trigger point → function name. The function is an **implicit reference** (not added to `links`).

```json
{
  "type": "authentication",
  "name": "staff-auth",
  "providerSettings": {
    "mfa": "required",
    "triggers": { "preTokenGeneration": "mfa-enforcer" }
  }
}
```

- Supported trigger points: `preSignUp`, `postConfirmation`, `preAuthentication`, `postAuthentication`, `preTokenGeneration`, `customMessage`, `defineAuthChallenge`, `createAuthChallenge`, `verifyAuthChallengeResponse`, `userMigration`.
- Each value names a `function` component; do NOT add it to `links` (the plugin synthesizes an implicit `AUTHENTICATION_TO_FUNCTION` link, so the function deploys before the pool).
- Deploy sets the pool `LambdaConfig` and grants `cognito-idp.amazonaws.com` `lambda:InvokeFunction` on each function (scoped to the pool ARN). Redeploy repoints/removes triggers declaratively; `"triggers": {}` clears them.
- **Deploy permissions:** `cognito-idp:*` plus `lambda:AddPermission`.

## Functions Component

```json
{
  "type": "functions",
  "name": "api-functions",
  "providerSettings": {
    "codeStorage": "storage-component-name",
    "codePath": "path/to/artifact.jar",
    "functions": [
      {
        "name": "handler-name",
        "handler": "com.example.Handler::method",
        "runtime": "java21",
        "memory": 512,
        "timeout": 30,
        "environment": { "KEY": "value" }
      }
    ]
  },
  "links": [
    {
      "sourceComponent": "api-functions",
      "targetComponent": "database-name",
      "linkType": "FUNCTION_TO_DATABASE"
    }
  ]
}
```

**Key Points:**
- `codeStorage` and `codePath` shared by all functions
- Individual functions referenced by `name` in routes/subscriptions
- Links at component level apply to all functions
- `runtime` (per function or group): common values `java21`, `nodejs22.x`, `python3.12`, `rust` (maps to `provided.al2023`; handler = `bootstrap`), `go` (maps to `provided.al2023`). Default `nodejs22.x`. **No allow-list is enforced**: the value is forwarded verbatim to the AWS Lambda SDK (after `rust`/`go`/`provided` normalise to `provided.al2023`), so any runtime identifier AWS accepts will work; an invalid string fails at deploy time, not validation time.
- `environment` (optional): group-level env vars for all functions; per-function `environment` overrides
- `logging: true` enables JSON CloudWatch logging (group or per-function)
- **`schedule` (per function, AWS)**: a `rate(...)`/`cron(...)` expression that invokes the function on a schedule via an EventBridge rule (`{app}-{name}-schedule`) with `events.amazonaws.com` invoke permission. Redeploy updates the expression; undeploy removes the rule. See below.
- **Links auto-inject env vars**: target name → `UPPER_SNAKE_CASE_ARN` (e.g., `Content_Store` → `CONTENT_STORE_ARN`); for `FUNCTION_TO_FUNCTION` links: `FUNCTION_{NAME}` (e.g., `provision-site` → `FUNCTION_PROVISION_SITE`)
- Per-function `links` array injects env vars only into that function
- **`permissions` array**: grants access to resource types without requiring a component link (see below)

### Explicit Permissions

Declare a `permissions` array on a function (or at component level as a fallback) to grant IAM access to resource types that don't exist in the configuration:

```json
{
  "name": "domain-manager",
  "handler": "handler.manage",
  "runtime": "nodejs22.x",
  "permissions": ["storage:create", "storage:delete", "dns:create", "dns:update", "dns:delete"]
}
```

Component-level permissions apply to all functions unless the function defines its own:

```json
{
  "type": "functions",
  "name": "admin-tools",
  "providerSettings": {
    "permissions": ["database:read", "database:write"],
    "functions": [...]
  }
}
```

**Available permissions:**

| Resource Type | Actions |
|---------------|---------|
| `storage` | `create`, `read`, `write`, `delete`, `list` |
| `database` | `create`, `read`, `write`, `delete` |
| `dns` | `create`, `update`, `delete`, `list` |
| `email` | `send` |
| `sms` | `send` |
| `event-bus` | `publish`, `subscribe`, `manage` |
| `authentication` | `create`, `read`, `update`, `delete` |
| `certificate` | `create`, `read`, `delete` |
| `website` | `create`, `read`, `delete`, `invalidate` |
| `queue` | `create`, `send`, `receive`, `delete` |

Format: `"resource:action"` (e.g. `"storage:create"`, `"dns:update"`, `"authentication:delete"`)

These map to provider-specific IAM actions:
- **AWS**: S3, DynamoDB, Route53, SES, SNS, EventBridge, Cognito actions with broad resource ARN patterns
- **Azure**: Azure RBAC actions scoped to resource types
- **GCP**: Cloud IAM permissions scoped to resource types

### Scheduled Functions (AWS)

Add a `schedule` to a function entry to invoke it on a time cadence via an EventBridge rule.

```json
{
  "type": "function",
  "name": "groups-functions",
  "providerSettings": {
    "functions": [
      {
        "name": "group-lifecycle-scheduler",
        "handler": "dist/group-lifecycle-scheduler.handler",
        "runtime": "nodejs22.x",
        "schedule": "rate(1 day)"
      }
    ]
  }
}
```

- `schedule` is a `rate(value unit)` or `cron(...)` expression. Rate units: `minute(s)`, `hour(s)`, `day(s)`; singular for value 1, plural for >1 (e.g. `rate(1 day)`, `rate(5 minutes)`). Cron has 6 fields, e.g. `cron(0 12 * * ? *)`.
- Deploy provisions an EventBridge rule `{app}-{name}-schedule` targeting the function and grants `events.amazonaws.com` `lambda:InvokeFunction`.
- Redeploy updates the expression in place; undeploy removes targets then the rule.
- AWS-only; ignored on Azure/GCP.
- **Deploy permissions:** `events:PutRule`, `events:PutTargets`, `events:DeleteRule`, `events:RemoveTargets`, `events:ListTargetsByRule`, and `lambda:AddPermission`.

## API Gateway Component

```json
{
  "type": "api-gateway",
  "name": "main-api",
  "providerSettings": {
    "domain": "api.example.com",
    "corsOrigins": ["https://www.example.com", "https://admin.example.com"],
    "preProcessFunction": "validator",
    "postProcessFunction": "logger",
    "routes": [
      {
        "path": "/users",
        "method": "GET|POST|PUT|DELETE|PATCH",
        "function": "function-name",
        "auth": "auth-component-name"
      }
    ]
  },
  "links": []
}
```

**Key Points:**
- `domain` required (full hostname)
- Function names in routes are implicit references (don't add to links)
- Auth component names in routes are implicit references
- `preProcessFunction` (optional): Wired as a REQUEST Lambda Authorizer on routes without `auth`
- `corsOrigins` (optional, array of strings): Specific CORS origins with `allowCredentials: true`. When omitted, uses `*` (works for JWT auth via Authorization header). Required for cookie-based auth.
- Routes and integrations are synced declaratively on redeploy — stale routes/integrations are removed
- All API Gateway SDK calls are retried with exponential backoff on throttling (429) and 5xx errors

### WebSocket API (real-time)

Set `protocol: "websocket"` to provision an AWS API Gateway **WebSocket** API instead of an HTTP API. Routes use a `routeKey` (not `method` + `path`):

```json
{
  "type": "api-gateway",
  "name": "chat-ws",
  "providerSettings": {
    "protocol": "websocket",
    "domain": "ws.example.com",
    "routes": [
      { "routeKey": "$connect",    "function": "chat-connect" },
      { "routeKey": "$disconnect", "function": "chat-disconnect" },
      { "routeKey": "sendMessage", "function": "chat-send" }
    ]
  },
  "links": []
}
```

**WebSocket Key Points:**
- `protocol: "websocket"` (or `protocolType: "WEBSOCKET"`) selects a WebSocket API. Default is HTTP.
- Each route uses `routeKey` instead of `method`/`path`. Reserved keys: `$connect`, `$disconnect`, `$default`; any other value is a custom route key.
- `routeKey` is required and must be unique per gateway; each route's `function` must exist.
- `routeSelectionExpression` (optional): defaults to `$request.body.action` (chooses the route key from the incoming message).
- Route-level authorization is `NONE`; authorize inside the `$connect` function. `auth` / JWT authorizers and `preProcessFunction` are ignored for WebSocket APIs.
- Integrations use Lambda proxy with payload format version `1.0`; the API is deployed to the `$default` stage with auto-deploy.
- CORS is not applied (WebSocket APIs do not support the HTTP CORS object).
- Backing functions are automatically granted `execute-api:ManageConnections` (via an implicit `FUNCTION_TO_API_GATEWAY` link) so they can post messages back to connected clients.

## Storage Component

```json
{
  "type": "storage",
  "name": "artifacts",
  "providerSettings": {
    "uploads": [
      {
        "artifact": "groupId:artifactId:version",
        "destination": "path/",
        "unpack": true,
        "classifier": "dist",
        "extension": "jar"
      }
    ]
  },
  "links": []
}
```

**Artifact Coordinate Format:**

The `artifact` field supports 3, 4, or 5 colon-separated parts:
- `groupId:artifactId:version` — standard 3-part coordinate
- `groupId:artifactId:version:classifier` — 4-part with inline classifier
- `groupId:artifactId:version:classifier:extension` — 5-part with inline classifier and extension

When classifier or extension is specified inline, it takes precedence over the separate `"classifier"` and `"extension"` fields.

**`${version}` Interpolation:**

Use `${version}` in the artifact coordinate to reference the Maven project version (`${project.version}`) at deploy time:
```json
{ "artifact": "com.example:my-lib:${version}", "destination": "releases/", "unpack": true }
```

Examples:
```json
{ "artifact": "com.example:my-lib:${version}:sources", "destination": "src/" }
{ "artifact": "com.example:assets:1.0.0:static:zip", "destination": "static/", "unpack": true }
```

**Key Points:**
- `destination` must end with `/`
- `unpack: true` extracts archive contents
- `classifier` and `extension` optional (can also be specified inline in the artifact coordinate)
- `${version}` is automatically replaced with the Maven project version at deploy time

## Event Bus Component

```json
{
  "type": "event-bus",
  "name": "events",
  "providerSettings": {
    "subscriptions": [
      {
        "eventPattern": "event.type",
        "function": "function-name"
      }
    ]
  },
  "links": []
}
```

**Key Points:**
- Function names in subscriptions are implicit references (don't add to links)

## Website Component

```json
{
  "type": "website",
  "name": "frontend",
  "providerSettings": {
    "domain": "www.example.com",
    "uploads": [
      {
        "artifact": "com.example:frontend-dist:${version}:dist:zip",
        "destination": "",
        "unpack": true
      }
    ]
  },
  "links": [
    {
      "sourceComponent": "frontend",
      "targetComponent": "api-name",
      "linkType": "WEBSITE_TO_API_GATEWAY"
    }
  ]
}
```

**Key Points:**
- Always provisions its own CDN-backed storage (origin bucket)
- Don't create separate storage component for website assets
- Supports `uploads` for deploying initial static content (HTML/CSS/JS) to the origin bucket
- Upload format is identical to the Storage component (same `artifact`, `destination`, `unpack` fields)
- Uploads run after the CloudFront distribution and bucket policy are configured
- `${version}` interpolation works the same as in Storage uploads
- CloudFront compression is always enabled
- Uses CachingOptimized managed cache policy for optimal performance
- `errorDocument` (defaults to "index.html") configures custom error responses for 403/404 with HTTP 200 — enables SPA client-side routing
- On redeploy, existing CloudFront distributions are updated with the latest configuration
- Use `WEBSITE_TO_STORAGE` links with a `path` property to serve content from other storage buckets at specific paths (e.g. `/assets` routes to a separate S3 bucket)

**Path Routing Example:**
```json
{
  "links": [
    {
      "sourceComponent": "frontend",
      "targetComponent": "asset-store",
      "linkType": "WEBSITE_TO_STORAGE",
      "path": "/assets"
    }
  ]
}
```

### Access Gate (edge HTTP Basic auth)

Put a username/password prompt in front of the whole site at the CloudFront edge. When enabled, every viewer request must present valid HTTP Basic credentials or CloudFront returns `401 Unauthorized`. Implemented as a CloudFront Function on the default cache behavior's `viewer-request` event.

```json
{
  "type": "website",
  "name": "marketing-site",
  "providerSettings": {
    "domain": "www.example.com",
    "accessGate": {
      "enabled": "${siteGateEnabled}",
      "credentialsSecret": "example/site-gate-credentials",
      "realm": "Example Staging"
    }
  }
}
```

**accessGate fields:**
- `enabled` (optional): boolean or the string `"true"`/`"false"` (so it can be a `${variable}` for per-environment toggling). Absent means disabled.
- `credentialsSecret` (required when enabled): Secrets Manager secret name or ARN holding the credentials. **Credentials are never inline in config.**
- `realm` (optional): HTTP Basic realm shown in the browser prompt. Defaults to `Restricted`.

**Key points:**
- Credentials are read from Secrets Manager **at deploy time** and the base64 `Basic` tokens (not raw passwords) are baked into the generated edge function. CloudFront Functions cannot make network calls, so rotate the secret and redeploy to change credentials.
- The secret payload may be a JSON array of `{username,password}` objects, a JSON object of `username`→`password`, or newline-separated `username:password` lines (`#` comments and blank lines ignored). Multiple credentials are supported.
- Toggle per environment: set `enabled` to `true` in staging variables and `false` in production to gate only pre-release environments.
- Disabling and redeploying removes the function association; `undeploy` deletes the CloudFront Function.
- Coexists with a WAF (`WAF_TO_WEBSITE`) association — the webACLId is preserved when the gate association is updated.
- **Deploy permissions:** requires `secretsmanager:GetSecretValue` plus `cloudfront:CreateFunction`, `UpdateFunction`, `PublishFunction`, `DescribeFunction`, `DeleteFunction` on the deploy identity.

### Edge Functions (viewer-request)

Associate a user-provided CloudFront Function with the distribution on the `viewer-request` event (e.g. an edge language router that rewrites `/` to `/{lang}/…`). The source is a project-relative JS file read at deploy time and published as `{resourceName}-edge-viewer-request`.

```json
{
  "type": "website",
  "name": "marketing-site",
  "providerSettings": {
    "domain": "www.example.com",
    "edgeFunctions": [
      {
        "event": "viewer-request",
        "runtime": "cloudfront-function",
        "source": "edge/language-router.js",
        "variables": { "supportedLanguages": "['en','fr','de']" }
      }
    ]
  }
}
```

**edgeFunctions fields (per entry):**
- `event` (required): only `"viewer-request"` is supported.
- `runtime` (optional): only `"cloudfront-function"` is supported (default).
- `source` (required): project-relative path to the JS source, resolved against `src/main/serverless`. Must stay within that directory — absolute paths and `..` traversal are rejected by validation.
- `variables` (optional): deploy-time `${key}` substitutions applied to the source; unknown `${...}` left untouched.

**Key points:**
- Source is read at deploy time; redeploy updates the function code in place; `undeploy` deletes the function.
- **Mutually exclusive with an enabled `accessGate`** — both use the single `viewer-request` slot, so `validate` fails if both are declared. Fold the access check into your own source if you need both.
- Only one `viewer-request` entry is allowed per website.
- **Deploy permissions:** the `cloudfront:*Function` actions (covered by `cloudfront:*`); no Secrets Manager access needed.

## Database Component

```json
{
  "type": "database",
  "name": "app-db",
  "providerSettings": {
    "tables": [
      {
        "name": "users",
        "partitionKey": { "name": "userId", "type": "string" }
      },
      {
        "name": "orders",
        "partitionKey": { "name": "orderId", "type": "string" },
        "sortKey": { "name": "createdAt", "type": "number" }
      }
    ],
    "billingMode": "PAY_PER_REQUEST"
  }
}
```

**Key Points:**
- `tables` array is required — define one or more tables
- Each table needs a `partitionKey` with `name` and `type`
- `sortKey` is optional (omit for partition-key-only tables)
- Key types: `string`, `number`, `binary` (provider-agnostic)
- Provider mapping: AWS → DynamoDB (S/N/B), Azure → Cosmos DB, GCP → Firestore
- Table names must be unique within the component
- **`ttlAttribute` (per table, AWS)**: enables DynamoDB Time To Live on the named item attribute (epoch-seconds expiry), e.g. `"ttlAttribute": "expireAt"`. Items **without** the attribute never expire (so you can exempt specific items just by not stamping it). Redeploy reconciles declaratively (enable/re-point/disable); TTL is delete-only and free; removed with the table on undeploy. Deploy permissions: `dynamodb:DescribeTimeToLive`, `dynamodb:UpdateTimeToLive`. Ignored on Azure/GCP.

```json
{
  "type": "database",
  "name": "ChatMessage",
  "providerSettings": {
    "tables": [
      {
        "name": "ChatMessage",
        "partitionKey": { "name": "chatId", "type": "string" },
        "sortKey": { "name": "ts", "type": "string" },
        "ttlAttribute": "expireAt"
      }
    ]
  }
}
```

## Email Component

```json
{
  "type": "email",
  "name": "transactional-email",
  "providerSettings": {
    "fromAddress": "noreply@example.com"
  },
  "links": []
}
```

## SMS Component

```json
{
  "type": "sms",
  "name": "notifications",
  "providerSettings": {},
  "links": []
}
```

## WAF Component

Web Application Firewall. Deploys as an AWS WAFv2 Web ACL and associates with the linked
`website` (CloudFront) and/or `api-gateway`. Geo rules detect the visitor country at the edge
and allow / block / count by country, and the resolved country can be forwarded downstream.

```json
{
  "type": "waf",
  "name": "edge-waf",
  "providerSettings": {
    "geoRestriction": {
      "type": "blocklist",
      "countries": ["CU", "IR"],
      "action": "BLOCK"
    },
    "countryHeader": "x-fp-country"
  },
  "links": [
    { "sourceComponent": "edge-waf", "targetComponent": "marketing-site", "linkType": "WAF_TO_WEBSITE" },
    { "sourceComponent": "edge-waf", "targetComponent": "core-api",       "linkType": "WAF_TO_API_GATEWAY" }
  ]
}
```

**Key Points:**
- `geoRestriction.type`: `blocklist` (block the listed `countries`, allow the rest) or `allowlist` (allow only the listed `countries`, block the rest).
- `geoRestriction.countries`: array of ISO 3166-1 alpha-2 codes (e.g. `US`, `GB`, `CU`).
- `geoRestriction.action` (optional): `ALLOW`, `BLOCK`, or `COUNT`. Overrides the default derived from `type`. Use `COUNT` to observe geo matches without enforcing.
- `countryHeader` (optional): a request header name (e.g. `x-fp-country`) inserted on matched requests so downstream functions/api-gateway receive one authoritative country signal.
- **Association / scope**: a `WAF_TO_WEBSITE` link creates a `CLOUDFRONT`-scope Web ACL in `us-east-1` and sets it on the CloudFront distribution; a `WAF_TO_API_GATEWAY` link creates a `REGIONAL`-scope Web ACL in the deploy region and associates it with the API's `$default` stage. A WAF linked to both target types produces two Web ACLs (one per scope), because AWS forbids sharing an ACL across scopes.
- Deploy order: the WAF is provisioned after its targets so it can resolve their ARNs.
- Also supported in `providerSettings` (validated): `rules`, `rateLimit`, `ipAllowList`, `ipBlockList`, `managedRuleGroups`, `logging`.

## Link Types

| Link Type | Source | Target | Properties |
|-----------|--------|--------|------------|
| `WEBSITE_TO_STORAGE` | website | storage | `path` (required) — URL path prefix routed to the storage bucket |
| `WEBSITE_TO_API_GATEWAY` | website | api-gateway | |
| `API_GATEWAY_TO_FUNCTION` | api-gateway | functions | |
| `API_GATEWAY_TO_AUTHENTICATION` | api-gateway | authentication | |
| `FUNCTION_TO_DATABASE` | functions | database | |
| `FUNCTION_TO_STORAGE` | functions | storage | |
| `FUNCTION_TO_EMAIL` | functions | email | |
| `FUNCTION_TO_SMS` | functions | sms | |
| `FUNCTION_TO_EVENT_BUS` | functions | event-bus | |
| `FUNCTION_TO_FUNCTION` | functions | functions | Grants invoke permission on the target function (use individual function name as target, not component group) |
| `FUNCTION_TO_API_GATEWAY` | functions | api-gateway | Grants `execute-api:ManageConnections` so a function can post messages to WebSocket clients. Synthesised automatically for WebSocket route functions |
| `FUNCTION_TO_AUTHENTICATION` | functions | authentication | Function manages users in the identity provider |
| `FUNCTION_TO_DOMAIN` | functions | (DNS) | Function manages DNS records |
| `EVENT_BUS_TO_FUNCTION` | event-bus | functions | |
| `AUTHENTICATION_TO_FUNCTION` | authentication | functions | Cognito Lambda trigger (implicit, from `triggers`) |
| `AUTHENTICATION_TO_EMAIL` | authentication | email | |
| `AUTHENTICATION_TO_SMS` | authentication | sms | |
| `WAF_TO_WEBSITE` | waf | website | WAF protects a website distribution |
| `WAF_TO_API_GATEWAY` | waf | api-gateway | WAF protects an API gateway |

### WEBSITE_TO_STORAGE Path Routing

The `WEBSITE_TO_STORAGE` link maps a URL path on the website's CloudFront distribution to a separate Storage component's S3 bucket. This allows serving assets from different buckets under the same domain.

**Requirements:**
- The `path` property is required and must start with `/`
- The target must be a defined `storage` component
- Multiple storage links are supported (each with a different path)

**How it works (AWS):**
- Adds the target storage bucket as an additional CloudFront origin
- Creates a cache behavior for `{path}/*` routing requests to that origin
- Shares the same Origin Access Control (OAC) as the primary origin
- Automatically sets the bucket policy on the target storage bucket to allow CloudFront access

**How it works (Azure):**
- Configures Azure Front Door with an additional origin group per storage path
- Creates a routing rule matching the path pattern to the target Blob Storage account

**How it works (GCP):**
- Configures Cloud CDN URL map with a path matcher per storage path
- Creates a backend bucket pointing to the target Cloud Storage bucket

**Example:**
```json
{
  "links": [
    {
      "sourceComponent": "frontend",
      "targetComponent": "asset-store",
      "linkType": "WEBSITE_TO_STORAGE",
      "path": "/assets"
    }
  ]
}
```

Requests to `https://example.com/assets/*` are served from the `asset-store` S3 bucket.

## Implicit vs Explicit References

**Implicit (don't add to links):**
- Function names in `api-gateway` routes
- Auth component names in `api-gateway` routes
- `preProcessFunction` and `postProcessFunction` in `api-gateway`
- For WebSocket gateways (`protocol: "websocket"`), each route function additionally gets an implicit `FUNCTION_TO_API_GATEWAY` link (ManageConnections)
- Function names in `event-bus` subscriptions

**Explicit (add to links):**
- All other component relationships

## Maven Goals & Phase Chaining

### Invoking Goals

Goals can be invoked using the full plugin coordinate or the short form (if declared in pom.xml):

```bash
# Full form (works without pom.xml declaration):
mvn uk.co.taylor-made-software:plugin-maven:1.0.1-SNAPSHOT:validate
mvn uk.co.taylor-made-software:plugin-maven:1.0.1-SNAPSHOT:plan
mvn uk.co.taylor-made-software:plugin-maven:1.0.1-SNAPSHOT:deploy
mvn uk.co.taylor-made-software:plugin-maven:1.0.1-SNAPSHOT:verify
mvn uk.co.taylor-made-software:plugin-maven:1.0.1-SNAPSHOT:undeploy

# Short form (requires plugin in pom.xml):
mvn serverless:validate
mvn serverless:plan
mvn serverless:deploy
mvn serverless:verify
mvn serverless:undeploy
```

### Phase Descriptions

| Goal | Phase | Description |
|------|-------|-------------|
| `validate` | VALIDATE | Parses config, resolves variables, validates all components and links |
| `plan` | PLAN | Computes a deployment plan showing what will be created/updated/deleted |
| `deploy` | DEPLOY | Provisions cloud resources according to the plan |
| `verify` | VERIFY | Runs health checks on deployed resources |
| `undeploy` | UNDEPLOY | Tears down all provisioned infrastructure |

### Phase Chaining (Automatic Prerequisites)

Each goal automatically runs all prerequisite phases in order. Already-completed phases within the same Maven invocation are skipped:

```
validate  →  VALIDATE only
plan      →  VALIDATE → PLAN
deploy    →  VALIDATE → PLAN → DEPLOY
verify    →  VALIDATE → PLAN → DEPLOY → VERIFY
undeploy  →  VALIDATE → UNDEPLOY (skips plan/deploy)
```

This means you never need to run `validate` manually before `deploy` — it's done automatically. Running `mvn serverless:deploy` will validate, plan, then deploy in one invocation.

## Validation Rules

1. All component names must be unique
2. All link targets must reference existing components
3. Authentication groups: name and description required, non-blank
4. Authentication users: email required, non-blank; groups must reference defined groups
5. Functions: codeStorage must reference existing storage component
6. API Gateway (HTTP): domain required; `method` + `path` required per route; no duplicate `(method, path)`; function/auth names must reference existing components
7. API Gateway (WebSocket, `protocol: "websocket"`): `routeKey` required and unique per route; each route `function` must reference an existing function
8. Storage uploads: destination must end with `/`
9. Event bus subscriptions: function names must reference existing functions
10. WAF: rule `name`/`priority`/`action` valid and unique priorities; `rateLimit`, IP lists (CIDR), `geoRestriction` country codes and action, `countryHeader` valid header name, logging destination required when enabled
11. Website `accessGate`: must be an object; `enabled` is a boolean or `"true"`/`"false"` string; `credentialsSecret` is a required non-blank string when enabled; `realm` (if present) must be a non-blank string
12. Website `edgeFunctions`: must be an array; each entry an object with `event` == `"viewer-request"`, optional `runtime` == `"cloudfront-function"`, and a required non-blank `source` that is project-relative (no absolute paths or `..`); at most one `viewer-request` entry; a `viewer-request` edge function cannot be combined with an enabled `accessGate`
13. Authentication `triggers`: must be an object; each key a known Cognito trigger point (`preSignUp`, `postConfirmation`, `preAuthentication`, `postAuthentication`, `preTokenGeneration`, `customMessage`, `defineAuthChallenge`, `createAuthChallenge`, `verifyAuthChallengeResponse`, `userMigration`); each value a non-blank function name that exists in the registry
14. Function `schedule` (per `functions[]` entry): must be a non-blank `rate(value unit)` (unit `minute(s)`/`hour(s)`/`day(s)`, singular for 1, plural for >1) or `cron(...)` (6 fields) expression
15. Database table `ttlAttribute` (per `tables[]` entry): when present, must be a non-blank string (the item attribute holding the epoch-seconds expiry timestamp)

## Common Patterns

### Multi-tier Web App
- Website (specific region)
- API Gateway (specific region)
- Functions (specific region)
- Database (specific region)
- Authentication (global)
- Email (global)

### Event-Driven
- Functions publish to event-bus
- Event-bus subscriptions trigger other functions
- Functions link to database, email, sms

### Multi-Region
- Authentication in global
- Separate database and functions per region
- Same component types, different names

## Best Practices

1. Use descriptive kebab-case names
2. Deploy auth, email, sms in global region
3. Use variables for environment-specific values
4. Group related functions in one component
5. Validate before deploy
6. Use preProcessFunction for request validation
7. Use postProcessFunction for audit logging
8. Define groups before users in authentication
9. Use PAY_PER_REQUEST billing for databases (default)
10. Run `mvn serverless:plan` before deploy

## Variable Substitution

Use `${variableName}` in any string field. Variable substitution works recursively in all string values including inside lists and nested maps in providerSettings (e.g., `corsOrigins: ["https://admin.${rootDomain}"]` or route objects with `${env}-handler` function names).

```json
{
  "variables": {
    "stage": "prod",
    "version": "2.0.0"
  },
  "regions": [
    {
      "region": "us-east-1",
      "items": [
        {
          "type": "storage",
          "name": "artifacts-${stage}",
          "providerSettings": {
            "uploads": [
              {
                "artifact": "com.example:app:${version}",
                "destination": "releases/${stage}/"
              }
            ]
          }
        }
      ]
    }
  ]
}
```

## Error Messages

| Error | Cause | Fix |
|-------|-------|-----|
| `groups[n].name` | Blank group name | Provide non-blank name |
| `groups[n].description` | Blank group description | Provide non-blank description |
| `users[n].email` | Blank user email | Provide non-blank email |
| `users[n].groups` | User references undefined group | Ensure group exists in groups array |
| Component not found | Link references non-existent component | Check component name spelling |
| Invalid link type | Wrong link type for components | Use correct link type from table |

## Example: Complete E-Commerce App

See `docs/example-complete-config.json` for a full production example with:
- Authentication with 3 groups and 2 pre-invited users
- 2 functions components (6 API functions, 3 event processors)
- API Gateway with 10 routes
- Event bus with 3 subscriptions
- Storage with Maven uploads
- Database, email, SMS
- Website with CDN
- Variable substitution
