create-qpq-app 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/steps/006_applyDomain.js +3 -8
- package/lib/commonjs/steps/006_applyDomain.js.map +1 -1
- package/lib/commonjs/steps/013_printNextSteps.js +1 -1
- package/lib/commonjs/steps/013_printNextSteps.js.map +1 -1
- package/lib/esm/steps/006_applyDomain.js +3 -8
- package/lib/esm/steps/006_applyDomain.js.map +1 -1
- package/lib/esm/steps/013_printNextSteps.js +1 -1
- package/lib/esm/steps/013_printNextSteps.js.map +1 -1
- package/package.json +2 -2
- package/template/README.md +6 -3
- package/template/apps/qpqjs/account.qpq.ts +5 -13
- package/template/apps/qpqjs/bootstrap.qpq.ts +29 -14
- package/template/apps/qpqjs/deploy.config.json +0 -1
- package/template/apps/qpqjs/packages/constants/src/domain.ts +3 -1
- package/template/apps/qpqjs/packages/service-utils/src/defineQpqjsService.ts +15 -9
- package/template/apps/qpqjs/services/design/views/src/components/LandingPage/components/Infrastructure.tsx +1 -2
- package/template/apps/qpqjs/services/shell/service/src/infrastructure.ts +1 -4
- package/template/apps/qpqjs/services/test/models/package.json +2 -1
- package/template/apps/qpqjs/services/test/models/src/EchoRequest.ts +8 -0
- package/template/apps/qpqjs/services/test/models/src/EchoResponse.ts +9 -0
- package/template/apps/qpqjs/services/test/models/src/SmokeRun.ts +11 -9
- package/template/apps/qpqjs/services/test/models/src/SmokeRunStarted.ts +8 -0
- package/template/apps/qpqjs/services/test/models/src/SmokeRunSummary.ts +10 -6
- package/template/apps/qpqjs/services/test/models/src/SmokeRunWithSummary.ts +9 -5
- package/template/apps/qpqjs/services/test/models/src/SmokeTestResult.ts +11 -9
- package/template/apps/qpqjs/services/test/models/src/index.ts +3 -0
- package/template/apps/qpqjs/services/test/service/src/echo/config/defineEcho.ts +9 -0
- package/template/apps/qpqjs/services/test/service/src/echo/controller/echo.ts +32 -0
- package/template/apps/qpqjs/services/test/service/src/echo/controller/index.ts +1 -0
- package/template/apps/qpqjs/services/test/service/src/infrastructure.ts +3 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/config/defineSmoke.ts +83 -18
- package/template/apps/qpqjs/services/test/service/src/smoke/constants/index.ts +1 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/constants/smokeEventDoc.ts +27 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/constants/smokeRunQueue.ts +4 -4
- package/template/apps/qpqjs/services/test/service/src/smoke/controller/askRunSmokeTests.ts +5 -1
- package/template/apps/qpqjs/services/test/service/src/smoke/data/askRecordSmokeTestResult.ts +32 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/data/askSetSmokeRunOutcome.ts +24 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/data/index.ts +2 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/eventDoc/index.ts +1 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/eventDoc/smokeProbeDocDefinition.ts +74 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/index.ts +1 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askExecuteSmokeTest.ts +97 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askStartSmokeRun.ts +18 -10
- package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/index.ts +1 -1
- package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeEventDocAppendQueueEvent.ts +15 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeEventDocMark.ts +8 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeTestRequestedQueueEvent.ts +12 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/models/index.ts +3 -1
- package/template/apps/qpqjs/services/test/service/src/smoke/queue/index.ts +2 -1
- package/template/apps/qpqjs/services/test/service/src/smoke/queue/onSmokeEventDocAppend.ts +64 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/queue/onSmokeTestRequested.ts +17 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askAssertContiguousEventIds.ts +18 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askAssertSmokeProbeDocState.ts +32 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askAwaitSmokeEventDocLog.ts +35 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askRunEventDocConcurrentAppendTest.ts +82 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askRunEventDocInterleavedBatchAppendTest.ts +110 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/index.ts +5 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/index.ts +2 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/openApi/askRunEchoRoundTripTest.ts +45 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/openApi/askRunOpenApiDocumentTest.ts +42 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/openApi/index.ts +2 -0
- package/template/apps/qpqjs/services/test/service/src/smoke/tests/smokeTestRegistry.ts +18 -2
- package/template/apps/todo/deploy.config.json +0 -1
- package/template/apps/todo/packages/constants/src/domain.ts +2 -1
- package/template/apps/todo/packages/service-utils/src/defineTodoService.ts +5 -6
- package/template/apps/todo/services/shell/service/src/infrastructure.ts +1 -3
- package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-upsert-with-retry.md +2 -2
- package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-upsert.md +2 -1
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-append-server-event.md +1 -1
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-create.md +1 -1
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-append.md +18 -12
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-list.md +4 -4
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-write.md +6 -6
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-by-id.md +2 -2
- package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-draft.md +2 -2
- package/template/docusaurus/docs/actions/webserver/dns/ask-dns-list.md +9 -8
- package/template/docusaurus/docs/actions/webserver/dns/ask-dns-resolve-hosts.md +42 -0
- package/template/docusaurus/docs/actions/webserver/open-api/_category_.json +1 -0
- package/template/docusaurus/docs/actions/webserver/open-api/ask-open-api-get-document.md +41 -0
- package/template/docusaurus/docs/config/config-aws/account-github-oidc-provider.md +31 -0
- package/template/docusaurus/docs/config/config-aws/domain-certificate.md +16 -32
- package/template/docusaurus/docs/config/config-aws/email-sender-allow-list.md +7 -14
- package/template/docusaurus/docs/config/config-aws/github-deploy-role.md +68 -0
- package/template/docusaurus/docs/config/core/user-directory.md +2 -3
- package/template/docusaurus/docs/config/features/admin-settings.md +2 -6
- package/template/docusaurus/docs/config/features/event-doc-summary.md +2 -2
- package/template/docusaurus/docs/config/features/open-api-routes.md +82 -0
- package/template/docusaurus/docs/config/features/tenanted-web-socket-queue.md +2 -3
- package/template/docusaurus/docs/config/features/web-socket-queue.md +1 -5
- package/template/docusaurus/docs/config/webserver/api.md +6 -9
- package/template/docusaurus/docs/config/webserver/cache.md +1 -1
- package/template/docusaurus/docs/config/webserver/dns.md +25 -17
- package/template/docusaurus/docs/config/webserver/domain-proxy.md +1 -5
- package/template/docusaurus/docs/config/webserver/email-sender.md +9 -15
- package/template/docusaurus/docs/config/webserver/route.md +1 -0
- package/template/docusaurus/docs/config/webserver/seo.md +1 -1
- package/template/docusaurus/docs/config/webserver/web-entry.md +0 -5
- package/template/docusaurus/docs/config/webserver/websocket.md +3 -7
- package/template/docusaurus/docs/domains.md +76 -0
- package/template/docusaurus/docs/use-cases.md +0 -6
- package/template/docusaurus/docusaurus.config.ts +38 -14
- package/template/docusaurus/sidebars.ts +5 -5
- package/template/docusaurus/src/components/HomepageFeatures/index.tsx +2 -2
- package/template/scripts/deployedSmoke.mjs +82 -23
- package/template/scripts/smoke/runEdgeChecks.mjs +152 -0
- package/template/scripts/smoke/runSmokeRun.mjs +1 -1
- package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askExecuteSmokeRun.ts +0 -83
- package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeRunRequestedQueueEvent.ts +0 -9
- package/template/apps/qpqjs/services/test/service/src/smoke/queue/onSmokeRunRequested.ts +0 -15
- package/template/docusaurus/docs/actions/webserver/open-api-spec/_category_.json +0 -1
- package/template/docusaurus/docs/actions/webserver/open-api-spec/ask-get-open-api-spec.md +0 -43
- package/template/docusaurus/docs/config/webserver/open-api.md +0 -36
|
@@ -95,11 +95,10 @@ Each handler is a `QpqFunctionRuntime` — a reference to a story entry point, u
|
|
|
95
95
|
```typescript
|
|
96
96
|
export type AuthDirectoryDnsRecord = {
|
|
97
97
|
subdomain: string;
|
|
98
|
-
rootDomain: string;
|
|
99
98
|
};
|
|
100
99
|
```
|
|
101
100
|
|
|
102
|
-
Attaches a Cognito custom domain
|
|
101
|
+
Attaches a Cognito custom domain to the pool and creates the matching Route 53 alias record. A pool has one custom domain, so it lives on the **primary** root from [defineDns](../webserver/dns.md) (the `{ subdomain }` target under the app's domain resolver). Because Cognito custom domains run on CloudFront, the ACM certificate is looked up in `us-east-1`.
|
|
103
102
|
|
|
104
103
|
## Custom auth runtime
|
|
105
104
|
|
|
@@ -167,7 +166,7 @@ export default [
|
|
|
167
166
|
// Required MFA served from a custom domain
|
|
168
167
|
defineUserDirectory('staff', {
|
|
169
168
|
mfa: { mode: UserDirectoryMfaMode.required },
|
|
170
|
-
dnsRecord: { subdomain: 'auth'
|
|
169
|
+
dnsRecord: { subdomain: 'auth' },
|
|
171
170
|
}),
|
|
172
171
|
|
|
173
172
|
// Authenticate against a directory owned by another service
|
|
@@ -20,7 +20,7 @@ export default [
|
|
|
20
20
|
...defineAdminUserDirectory({ owner: { module: 'log' } }),
|
|
21
21
|
|
|
22
22
|
// The admin dashboard backend, owned by the 'log' service
|
|
23
|
-
...defineAdminSettings('log',
|
|
23
|
+
...defineAdminSettings('log', {
|
|
24
24
|
logRetentionDays: 90,
|
|
25
25
|
services: ['api', 'workers', 'web'],
|
|
26
26
|
}),
|
|
@@ -32,7 +32,6 @@ export default [
|
|
|
32
32
|
```typescript
|
|
33
33
|
function defineAdminSettings(
|
|
34
34
|
logServiceName: string,
|
|
35
|
-
rootDomain: string,
|
|
36
35
|
advancedSettings?: QPQConfigAdvancedLogSettings,
|
|
37
36
|
): QPQConfig;
|
|
38
37
|
```
|
|
@@ -43,9 +42,6 @@ function defineAdminSettings(
|
|
|
43
42
|
|
|
44
43
|
The name of the service that **owns** the admin resources. Owner-stamped settings (the log storage drives, the admin WebSocket event bus/queues, the key-value stores, the auth and log routes, the alarm pipeline, and the [session event doc](./admin-session-event-doc.md)) only flatten into deployable resources when the deploying service matches this name; elsewhere they resolve as foreign references. Owner resolution runs at IAM-scoping time.
|
|
45
44
|
|
|
46
|
-
### `rootDomain` — `string` (required)
|
|
47
|
-
|
|
48
|
-
The root domain the admin service is hosted on. Used to stand up the admin WebSocket endpoint (`defineWebSocketQueue`) under the `qpqadmin` subdomain of this domain.
|
|
49
45
|
|
|
50
46
|
### `advancedSettings` — `QPQConfigAdvancedLogSettings` (optional)
|
|
51
47
|
|
|
@@ -90,7 +86,7 @@ export default [
|
|
|
90
86
|
...defineAdminUserDirectory({ owner: { module: 'log' } }),
|
|
91
87
|
|
|
92
88
|
// Retain logs 30 days, move to deep cold storage after 7.
|
|
93
|
-
...defineAdminSettings('log',
|
|
89
|
+
...defineAdminSettings('log', {
|
|
94
90
|
logRetentionDays: 30,
|
|
95
91
|
coldStorageAfterDays: 7,
|
|
96
92
|
services: ['api', 'workers'],
|
|
@@ -8,8 +8,8 @@ description: Declare the stores for an event-sourced document collection — its
|
|
|
8
8
|
Declares the **stores** that back an event-document collection, without any routes. It returns a `QPQConfig` (an array of config settings) that expands to five underlying core stores:
|
|
9
9
|
|
|
10
10
|
1. A **summary key-value store** (partition key `type`, sort key `id`) — the queryable record for each document, derived by folding the identity/lifecycle events of its log. Rows are the [`EventDocSummary`](../../actions/features/event-doc/ask-event-doc-get-by-id.md) shape and carry the version history. A secondary index on `(type, updatedAt)` supports the recently-updated ordering [askEventDocList](../../actions/features/event-doc/ask-event-doc-list.md) returns. It is a pure projection: nothing on the append path writes it directly. The events store below declares an [`onStream`](../core/key-value-store.md#change-data-capture-onstream) handler that rebuilds a document's summary row from its log whenever an event is appended, so the summary is eventually (not immediately) consistent with the log.
|
|
11
|
-
2. An **append-only events store** (`<storeName>EventLog`, partition key `pk`,
|
|
12
|
-
3. A **snapshots store** (`<storeName>SS`, partition key `pk`, sort key `sk`) — per-view folded states at points along the log (`pk = docId#view`, `sk` the same
|
|
11
|
+
2. An **append-only events store** (`<storeName>EventLog`, partition key `pk`, numeric sort key `sk`) — the live ordered log every document is folded from, keyed on the event's contiguous position (`INIT_STATE` is `0`, every append is head + 1, claimed by a conditional write with retry under contention). Log order IS commit order. It has **no** secondary index on purpose: the local dev-server query processor can't target one, so all event reads go through the main table.
|
|
12
|
+
3. A **snapshots store** (`<storeName>SS`, partition key `pk`, sort key `sk`) — per-view folded states at points along the log (`pk = docId#view`, `sk` the same contiguous event id the log is ordered by). Populated only for the document types you enable via `options.snapshotFunctions`; a type with no entry there simply is not snapshotted. For a type with a registered functions object, the `onStream` handler runs ONE incremental fold per delivery — resuming from the newest usable snapshot and folding only the gap since it — and persists both the snapshot set and the summary row from that same fold (the fold's `summary` view IS the queryable record). The whole-log summary re-derivation remains only as the fallback: types with no registered functions, Remove stream records (a transfer rewrote the log, so snapshots can't seed), and folds that decline.
|
|
13
13
|
4. A **legacy events store** (`<storeName>Events`, partition key `pk`, numeric sort key `sk`) — the pre-sortable-id log, kept declared but unread/unwritten at runtime so its data stays reachable until it is migrated into the events store above.
|
|
14
14
|
5. A **storage drive** (`<storeName>edocs`, lower-cased) — the collection's blob bucket, holding each document's immutable uploaded assets (and later its derived runtime artifacts) under per-document prefixes.
|
|
15
15
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: defineOpenApiRoutes
|
|
3
|
+
description: Serve a generated OpenAPI document and an interactive reference page for the service's routes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# defineOpenApiRoutes
|
|
7
|
+
|
|
8
|
+
Mounts two routes: an interactive API reference page and the OpenAPI 3.1 document it reads. Nothing is written by hand. The document is generated from the service's route config, so every `defineRoute` shows up in it, and routes that declare a schema show their request and response shapes too.
|
|
9
|
+
|
|
10
|
+
- **On AWS:** two ordinary routes on the API Lambda. The document is built on request from the config already bundled with the service, so there is no extra infrastructure and nothing to keep in sync.
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { defineOpenApiRoutes } from 'quidproquo-features';
|
|
14
|
+
|
|
15
|
+
export default [
|
|
16
|
+
// GET /v1/docs (reference page) and GET /v1/docs/openapi.json (document)
|
|
17
|
+
defineOpenApiRoutes({ info: { title: 'Widgets API', version: '1.2.0' } }),
|
|
18
|
+
];
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The reference page loads [Scalar](https://scalar.com) from a CDN and points it at the document next to it. Browsers need to reach `cdn.jsdelivr.net` for the page to render; the document itself has no such dependency.
|
|
22
|
+
|
|
23
|
+
## Signature
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
function defineOpenApiRoutes(options?: OpenApiRoutesOptions): QPQConfig;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Parameters
|
|
30
|
+
|
|
31
|
+
### `options` — `OpenApiRoutesOptions` (optional)
|
|
32
|
+
|
|
33
|
+
| Property | Type | Default | Description |
|
|
34
|
+
| --- | --- | --- | --- |
|
|
35
|
+
| `basePath` | `string` | `'/docs'` | Where the reference page is served. The document is always at `${basePath}/openapi.json`. |
|
|
36
|
+
| `version` | `number` | `1` | Version prefix, as with [defineVersionedRoute](./versioned-route.md). |
|
|
37
|
+
| `info` | `OpenApiDocumentOptions` | – | `title`, `version` and `description` for the document's info block. The title defaults to the application and module name. |
|
|
38
|
+
| `routeAuthSettings` | `RouteAuthSettings` | – | Gate the docs behind a user directory or api key. Public when omitted. |
|
|
39
|
+
|
|
40
|
+
## Describing a route
|
|
41
|
+
|
|
42
|
+
Any route can carry a `schema` in its options: `bodyJsonSchema`, `queryJsonSchema` and `responseJsonSchema` as plain JSON Schema (from zod, `z.toJSONSchema(MySchema)`), plus `summary`, `description` and `tags`. Set `hidden: true` to keep a route out of the document.
|
|
43
|
+
|
|
44
|
+
Routes written with `dynamicRoute` (or a `createRouteDefinition` family) take zod schemas instead and get validation for free. The body and query are parsed before the handler runs, a mismatch becomes a 422, and the parsed values arrive typed as the handler's third argument.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { z } from 'zod/v4';
|
|
48
|
+
import { dynamicRoute } from 'quidproquo-features';
|
|
49
|
+
|
|
50
|
+
export const create = dynamicRoute(
|
|
51
|
+
['POST', '/widgets'],
|
|
52
|
+
function* (event, params, { body }) {
|
|
53
|
+
// body is { name: string }
|
|
54
|
+
return qpqWebServerUtils.toJsonEventResponse({ id: 'w1', name: body.name });
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
schema: {
|
|
58
|
+
summary: 'Create a widget',
|
|
59
|
+
tags: ['widgets'],
|
|
60
|
+
body: z.object({ name: z.string().min(1) }),
|
|
61
|
+
response: z.object({ id: z.string(), name: z.string() }),
|
|
62
|
+
},
|
|
63
|
+
knownErrors: { [WidgetErrorTypeEnum.Duplicate]: 409 },
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## What the document contains
|
|
69
|
+
|
|
70
|
+
- One operation per route, keyed by path and method, with a stable `operationId` derived from both.
|
|
71
|
+
- Path parameters read from the `{param}` segments of the path.
|
|
72
|
+
- One query parameter per property of the query schema.
|
|
73
|
+
- A JSON request body when a body schema is declared, and a typed 200 response when a response schema is.
|
|
74
|
+
- A 422 response on routes that validate input, and a 401 on routes with auth settings.
|
|
75
|
+
- Security requirements from the route's merged auth settings: bearer JWT for a user directory, `x-api-key` for api keys. Only the schemes in use are published.
|
|
76
|
+
- A server entry per non-deprecated [defineApi](../webserver/api.md) when the service has a domain.
|
|
77
|
+
|
|
78
|
+
## Related
|
|
79
|
+
|
|
80
|
+
- [askOpenApiGetDocument](../../actions/webserver/open-api/ask-open-api-get-document.md) — the action the document route yields; use it to serve the document somewhere else.
|
|
81
|
+
- [defineRoute](../webserver/route.md) — the `schema` option on plain routes.
|
|
82
|
+
- [defineVersionedRoute](./versioned-route.md) — the version prefix these routes sit under.
|
|
@@ -19,7 +19,7 @@ export default [
|
|
|
19
19
|
routeAuthSettings: { userDirectoryName: 'users' },
|
|
20
20
|
}),
|
|
21
21
|
|
|
22
|
-
defineTenantedWebSocketQueue('my-event-bus', 'api',
|
|
22
|
+
defineTenantedWebSocketQueue('my-event-bus', 'api', {
|
|
23
23
|
userDirectoryName: 'users',
|
|
24
24
|
}),
|
|
25
25
|
];
|
|
@@ -31,7 +31,6 @@ export default [
|
|
|
31
31
|
function defineTenantedWebSocketQueue(
|
|
32
32
|
eventBusName: string,
|
|
33
33
|
apiName: string,
|
|
34
|
-
rootDomain: string,
|
|
35
34
|
advancedSettings?: QPQConfigAdvancedTenantedWebsocketQueueSettings,
|
|
36
35
|
): QPQConfig;
|
|
37
36
|
```
|
|
@@ -40,7 +39,7 @@ function defineTenantedWebSocketQueue(
|
|
|
40
39
|
|
|
41
40
|
## Parameters
|
|
42
41
|
|
|
43
|
-
Same as [defineWebSocketQueue](./web-socket-queue.md#parameters): `eventBusName`, `apiName`,
|
|
42
|
+
Same as [defineWebSocketQueue](./web-socket-queue.md#parameters): `eventBusName`, `apiName`, and `advancedSettings` (without `connectionScopeResolver`, which this always sets to `TENANT_CONNECTION_SCOPE_RESOLVER_FN`).
|
|
44
43
|
|
|
45
44
|
## Returns
|
|
46
45
|
|
|
@@ -16,7 +16,7 @@ Defines a **WebSocket queue**: a batteries-included messaging layer on top of a
|
|
|
16
16
|
import { defineWebSocketQueue } from 'quidproquo-features';
|
|
17
17
|
|
|
18
18
|
export default [
|
|
19
|
-
defineWebSocketQueue('my-event-bus', 'api',
|
|
19
|
+
defineWebSocketQueue('my-event-bus', 'api', {
|
|
20
20
|
userDirectoryName: 'users',
|
|
21
21
|
}),
|
|
22
22
|
];
|
|
@@ -40,7 +40,6 @@ Because the underlying WebSocket API is created with `apiName` equal to the `api
|
|
|
40
40
|
function defineWebSocketQueue(
|
|
41
41
|
eventBusName: string,
|
|
42
42
|
apiName: string,
|
|
43
|
-
rootDomain: string,
|
|
44
43
|
advancedSettings?: QPQConfigAdvancedWebsocketQueueSettings,
|
|
45
44
|
): QPQConfig;
|
|
46
45
|
```
|
|
@@ -57,9 +56,6 @@ Name of the [event bus](../core/event-bus.md) the queue is associated with (stor
|
|
|
57
56
|
|
|
58
57
|
Logical name of the WebSocket API. Drives the resource names of the deployed API, the connection key-value store (`qpq-wsq-<apiName>`), and the globals — and is the value you pass as `websocketApiName` to send actions. Also used as the API subdomain.
|
|
59
58
|
|
|
60
|
-
### `rootDomain` — `string` (required)
|
|
61
|
-
|
|
62
|
-
The registered domain the WebSocket API is served under (passed straight through to the underlying [defineWebsocket](../webserver/websocket.md)).
|
|
63
59
|
|
|
64
60
|
### `advancedSettings` — `QPQConfigAdvancedWebsocketQueueSettings` (optional)
|
|
65
61
|
|
|
@@ -7,13 +7,13 @@ description: Define an HTTP API — the public web endpoint (a custom subdomain
|
|
|
7
7
|
|
|
8
8
|
Defines an **API**: the public HTTP endpoint for a service. An API declares the subdomain and root domain the service is reachable on; every [route](./route.md) you declare in the same service is served under it. A service typically declares exactly one API.
|
|
9
9
|
|
|
10
|
-
- **On AWS:** deploys a **regional API Gateway REST API** (`LambdaRestApi`, proxy mode, binary media types `*/*`) backed by a single Lambda that handles every route (`ApiQpqWebserverApiConstruct` in `quidproquo-deploy-awscdk`). The API is attached to a custom domain (`
|
|
10
|
+
- **On AWS:** deploys a **regional API Gateway REST API** (`LambdaRestApi`, proxy mode, binary media types `*/*`) backed by a single Lambda that handles every route (`ApiQpqWebserverApiConstruct` in `quidproquo-deploy-awscdk`). The API is attached to a custom domain on every root declared with [defineDns](./dns.md) (the `{ subdomain: apiName }` target under the app's [domain resolver](../../domains.md)) via a base-path mapping keyed on the module name, so multiple modules can share one domain. Access logs (JSON, one-year retention) and CloudWatch metrics are enabled; a 5XX alarm is wired by default, plus 401/403 rate alarms when error notifications are configured. When WAF protection is enabled for the deploy, the shared regional Web ACL is associated with the stage.
|
|
11
11
|
|
|
12
12
|
```typescript
|
|
13
13
|
import { defineApi } from 'quidproquo-webserver';
|
|
14
14
|
|
|
15
15
|
export default [
|
|
16
|
-
defineApi('api'
|
|
16
|
+
defineApi('api'),
|
|
17
17
|
];
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -24,7 +24,6 @@ This serves the service's routes at `https://api.<module-base>.example.com/<modu
|
|
|
24
24
|
```typescript
|
|
25
25
|
function defineApi(
|
|
26
26
|
apiName: string,
|
|
27
|
-
rootDomain: string,
|
|
28
27
|
options?: QPQConfigAdvancedApiSettings,
|
|
29
28
|
): ApiQPQWebServerConfigSetting;
|
|
30
29
|
```
|
|
@@ -33,11 +32,8 @@ function defineApi(
|
|
|
33
32
|
|
|
34
33
|
### `apiName` — `string` (required)
|
|
35
34
|
|
|
36
|
-
The API's name and its `uniqueKey` within the config. Unless overridden by `options.subDomain`, it is also the **subdomain** the API is served on
|
|
35
|
+
The API's name and its `uniqueKey` within the config. Unless overridden by `options.subDomain`, it is also the **subdomain** the API is served on: the host is the `{ subdomain: apiName }` target resolved on each root (`api.development.example.com` with the default shape).
|
|
37
36
|
|
|
38
|
-
### `rootDomain` — `string` (required)
|
|
39
|
-
|
|
40
|
-
The root domain the API is hosted under, e.g. `'example.com'`. The deploy derives the full hostname from this plus the environment/feature and the API subdomain, so the same config deploys to multiple environments without collisions.
|
|
41
37
|
|
|
42
38
|
### `options` — `QPQConfigAdvancedApiSettings` (optional)
|
|
43
39
|
|
|
@@ -56,7 +52,7 @@ import { defineApi, defineRoute } from 'quidproquo-webserver';
|
|
|
56
52
|
|
|
57
53
|
export default [
|
|
58
54
|
// Serve on a custom subdomain, capped at 50 concurrent requests
|
|
59
|
-
defineApi('public',
|
|
55
|
+
defineApi('public', {
|
|
60
56
|
subDomain: 'api',
|
|
61
57
|
maxConcurrentExecutions: 50,
|
|
62
58
|
}),
|
|
@@ -71,5 +67,6 @@ export default [
|
|
|
71
67
|
- [defineRoute](./route.md) — declares the individual method+path endpoints served under the API.
|
|
72
68
|
- [defineDefaultRouteOptions](./default-route-options.md) — service-wide defaults (CORS, auth) merged into every route.
|
|
73
69
|
- [defineServiceFunction](./service-function.md) — RPC-style callable functions, deployed as their own Lambdas rather than under the API.
|
|
74
|
-
- [
|
|
70
|
+
- [defineDns](./dns.md) — the roots the API's domain is created on.
|
|
71
|
+
- [defineDomainCertificate](../config-aws/domain-certificate.md) — the ACM certificate covering the API's host on every root.
|
|
75
72
|
- [defineWafProtection](../config-aws/waf-protection.md) — opt the API Gateway stage into the app's WAF web ACL.
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: defineDns
|
|
3
|
-
description: Declare the
|
|
3
|
+
description: Declare the root domains a service is served on; every hostname the service deploys is derived from them.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# defineDns
|
|
7
7
|
|
|
8
|
-
Declares the **
|
|
8
|
+
Declares the **root domains** for a service, primary first. Everything else that needs a hostname (API custom domains, web entries, websockets, [domain proxies](./domain-proxy.md), [subdomain redirects](./subdomain-redirect.md), email identities, CORS and CSP origins) is derived from this list through the app's [domain resolver](../../domains.md). A service has one `defineDns`; a second one is a synth error.
|
|
9
9
|
|
|
10
|
-
- **On AWS:** `defineDns` does **not** create a Route53 hosted zone
|
|
10
|
+
- **On AWS:** `defineDns` does **not** create a Route53 hosted zone. Deploy constructs look zones up with `HostedZone.fromLookup` and add records into them. A host's zone is its root's site root when it sits under it (`api.development.example.com` in `development.example.com`), otherwise the root itself (`development-api.example.com` in `example.com`); see [Domains](../../domains.md).
|
|
11
11
|
|
|
12
12
|
```typescript
|
|
13
13
|
import { defineDns } from 'quidproquo-webserver';
|
|
14
14
|
|
|
15
15
|
export default [
|
|
16
|
-
defineDns('example.com'
|
|
16
|
+
defineDns(['example.com', 'example.org'], {
|
|
17
|
+
resolver: { basePath: __dirname, relativePath: 'domainResolver', functionName: 'domainResolver' },
|
|
18
|
+
}),
|
|
17
19
|
];
|
|
18
20
|
```
|
|
19
21
|
|
|
@@ -21,22 +23,22 @@ export default [
|
|
|
21
23
|
|
|
22
24
|
```typescript
|
|
23
25
|
function defineDns(
|
|
24
|
-
|
|
26
|
+
rootDomains: string | string[],
|
|
27
|
+
options?: { resolver?: QpqPureFunction },
|
|
25
28
|
): DnsQPQWebServerConfigSetting;
|
|
26
29
|
```
|
|
27
30
|
|
|
28
31
|
## Parameters
|
|
29
32
|
|
|
30
|
-
### `
|
|
33
|
+
### `rootDomains` — `string | string[]` (required)
|
|
34
|
+
|
|
35
|
+
The root domains the service is served on. The first is the **primary**: anything that must bake exactly one absolute URL (email links, module-federation remotes, the Cognito custom domain) uses it. Every other resource is created on every root, so the app is fully live on all of them at once.
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
Hostnames are not built here. They come from the app's [domain resolver](../../domains.md), which by default produces `[subdomain.][service.][feature.][environment.]root` with no environment label in production.
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
- In another environment it becomes `<environment>.example.com` (e.g. `development.example.com`).
|
|
36
|
-
- With a feature branch it becomes `<feature>.<environment>.example.com`.
|
|
37
|
-
- A service's own resources then live under `<service>.<base>` (e.g. an API at `api.<service>.development.example.com`).
|
|
39
|
+
### `options.resolver` — `QpqPureFunction` (optional)
|
|
38
40
|
|
|
39
|
-
The
|
|
41
|
+
A pointer (`{ basePath, relativePath, functionName }`) to the app's `DomainResolver` export. The deploy tooling `require`s it at synth and build time, and the runtime loads it through the dynamic module loader (the pointer is a bundled src entry like a route), so every site resolves hosts the same way: `resolveHosts(qpqConfig, target, resolver)`. Omit it for the default shape.
|
|
40
42
|
|
|
41
43
|
## Examples
|
|
42
44
|
|
|
@@ -44,14 +46,20 @@ The list of declared bases is what [askDnsList](../../actions/webserver/dns/ask-
|
|
|
44
46
|
import { defineDns } from 'quidproquo-webserver';
|
|
45
47
|
|
|
46
48
|
export default [
|
|
47
|
-
//
|
|
49
|
+
// One root
|
|
48
50
|
defineDns('example.com'),
|
|
51
|
+
|
|
52
|
+
// Two roots, example.com primary: every api, web entry and websocket is served on both,
|
|
53
|
+
// shaped by the app's own resolver
|
|
54
|
+
defineDns(['example.com', 'example.org'], {
|
|
55
|
+
resolver: { basePath: __dirname, relativePath: 'domainResolver', functionName: 'domainResolver' },
|
|
56
|
+
}),
|
|
49
57
|
];
|
|
50
58
|
```
|
|
51
59
|
|
|
52
60
|
## Related
|
|
53
61
|
|
|
54
|
-
- [
|
|
55
|
-
- [
|
|
56
|
-
- [
|
|
57
|
-
- [
|
|
62
|
+
- [Domains](../../domains.md) — how roots, the resolver, zones and certificates fit together.
|
|
63
|
+
- [askDnsList](../../actions/webserver/dns/ask-dns-list.md) — returns the root list at runtime.
|
|
64
|
+
- [askDnsResolveHosts](../../actions/webserver/dns/ask-dns-resolve-hosts.md) — a target's host on every root at runtime.
|
|
65
|
+
- [defineDomainCertificate](../config-aws/domain-certificate.md) — the per-region certificate covering every root.
|
|
@@ -16,7 +16,6 @@ export default [
|
|
|
16
16
|
defineDomainProxy('marketing-proxy', {
|
|
17
17
|
httpProxyDomain: 'origin.internal.example.com',
|
|
18
18
|
domain: {
|
|
19
|
-
rootDomain: 'example.com',
|
|
20
19
|
onRootDomain: true,
|
|
21
20
|
},
|
|
22
21
|
domainProxyViewerProtocolPolicy: DomainProxyViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
@@ -53,7 +52,6 @@ Unique name for this proxy (the config's `uniqueKey`). Used to derive the distri
|
|
|
53
52
|
|
|
54
53
|
```typescript
|
|
55
54
|
export interface DomainProxyDomainOptions {
|
|
56
|
-
rootDomain: string;
|
|
57
55
|
subDomainNames?: string[];
|
|
58
56
|
onRootDomain: boolean;
|
|
59
57
|
}
|
|
@@ -61,7 +59,6 @@ export interface DomainProxyDomainOptions {
|
|
|
61
59
|
|
|
62
60
|
| Property | Type | Description |
|
|
63
61
|
| --- | --- | --- |
|
|
64
|
-
| `rootDomain` | `string` | The root domain (matches the service's [`dnsBase`](./dns.md)); the apex is resolved from it with the usual environment/feature prefixing, and the hosted zone is looked up in Route53. |
|
|
65
62
|
| `subDomainNames` | `string[]` | Subdomains to serve, each expanded to `<subDomain>.<apex>`. An `A` alias record is created for each. |
|
|
66
63
|
| `onRootDomain` | `boolean` | When `true` and no subdomains are given, the distribution also answers on the apex domain itself. |
|
|
67
64
|
|
|
@@ -91,7 +88,6 @@ export default [
|
|
|
91
88
|
defineDomainProxy('api-proxy', {
|
|
92
89
|
httpProxyDomain: 'backend.example.net',
|
|
93
90
|
domain: {
|
|
94
|
-
rootDomain: 'example.com',
|
|
95
91
|
subDomainNames: ['api', 'api-v2'],
|
|
96
92
|
onRootDomain: false,
|
|
97
93
|
},
|
|
@@ -104,6 +100,6 @@ export default [
|
|
|
104
100
|
|
|
105
101
|
## Related
|
|
106
102
|
|
|
107
|
-
- [defineDns](./dns.md) — declares the
|
|
103
|
+
- [defineDns](./dns.md) — declares the roots this proxy is served on.
|
|
108
104
|
- [defineCertificate](./certificate.md) — related certificate configuration (the proxy uses the central certificate).
|
|
109
105
|
- [defineSubdomainRedirect](./subdomain-redirect.md) — redirect a subdomain instead of proxying it.
|
|
@@ -1,43 +1,37 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: defineEmailSender
|
|
3
|
-
description: Declare
|
|
3
|
+
description: Declare that a service sends email from the app's root domains.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# defineEmailSender
|
|
7
7
|
|
|
8
|
-
Declares
|
|
8
|
+
Declares that the service sends email with [askEmailSendEmail](../../actions/webserver/email/ask-email-send-email.md) from the app's root domains. The sending identity on each root is that root's site root under the app's [domain resolver](../../domains.md) (`development.example.com` with the default shape).
|
|
9
9
|
|
|
10
|
-
- **On AWS:** creates an SES `EmailIdentity`
|
|
10
|
+
- **On AWS:** creates an SES `EmailIdentity` per root in its Route53 hosted zone (DKIM records land there automatically), and scopes the service role's `ses:SendEmail` / `ses:SendRawEmail` grant to those identities' exact ARNs. `ses:SendRawEmail` is needed for emails with attachments, which are sent as raw MIME. No grant or identity is created for services that declare no `defineEmailSender`.
|
|
11
11
|
|
|
12
12
|
```typescript
|
|
13
13
|
import { defineDns, defineEmailSender } from 'quidproquo-webserver';
|
|
14
14
|
|
|
15
15
|
export default [
|
|
16
16
|
defineDns('example.com'),
|
|
17
|
-
defineEmailSender(
|
|
17
|
+
defineEmailSender(),
|
|
18
18
|
];
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Signature
|
|
22
22
|
|
|
23
23
|
```typescript
|
|
24
|
-
function defineEmailSender(
|
|
25
|
-
rootDomain: string,
|
|
26
|
-
): EmailSenderQPQWebServerConfigSetting;
|
|
24
|
+
function defineEmailSender(): EmailSenderQPQWebServerConfigSetting;
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
### `rootDomain` — `string` (required)
|
|
32
|
-
|
|
33
|
-
The domain to send from, resolved the same way as [defineDns](./dns.md)'s `dnsBase` (environment/feature prefixed). This value is also the config's `uniqueKey`, so a service declares one sender per root domain.
|
|
27
|
+
Takes no arguments; a service declares it once.
|
|
34
28
|
|
|
35
29
|
## Returns
|
|
36
30
|
|
|
37
|
-
An `EmailSenderQPQWebServerConfigSetting` config entry. Deploy reads
|
|
31
|
+
An `EmailSenderQPQWebServerConfigSetting` config entry. Deploy reads it with `qpqWebServerUtils.getEmailSenderSettings` to create the SES identities and IAM grants.
|
|
38
32
|
|
|
39
33
|
## Related
|
|
40
34
|
|
|
41
|
-
- [askEmailSendEmail](../../actions/webserver/email/ask-email-send-email.md) — send email from
|
|
42
|
-
- [defineDns](./dns.md) — the
|
|
35
|
+
- [askEmailSendEmail](../../actions/webserver/email/ask-email-send-email.md) — send email from the identities declared here.
|
|
36
|
+
- [defineDns](./dns.md) — the roots the identities are created on.
|
|
43
37
|
- [defineEmailSenderAllowList](../config-aws/email-sender-allow-list.md) — recipient addresses allowed while the SES account is in sandbox mode.
|
|
@@ -48,6 +48,7 @@ A reference to the story that handles the request. Usually written as a relative
|
|
|
48
48
|
| --- | --- | --- | --- |
|
|
49
49
|
| `allowedOrigins` | `(string \| ServiceAllowedOrigin)[]` | – | Browser origins allowed to call this route (CORS). Either literal origin strings or `ServiceAllowedOrigin` descriptors resolved to another qpq service's domain. Merged with any [default route options](./default-route-options.md). |
|
|
50
50
|
| `routeAuthSettings` | `RouteAuthSettings` | – | How the route is authenticated/authorized. See below. |
|
|
51
|
+
| `schema` | `RouteSchema` | – | `bodyJsonSchema`, `queryJsonSchema` and `responseJsonSchema` as JSON Schema, plus `summary`, `description`, `tags` and `hidden`. Feeds the generated OpenAPI document; see [defineOpenApiRoutes](../features/open-api-routes.md). Routes written with `dynamicRoute` fill this in from zod schemas for you. |
|
|
51
52
|
|
|
52
53
|
#### `RouteAuthSettings`
|
|
53
54
|
|
|
@@ -53,7 +53,7 @@ import { defineSeo, defineWebEntry } from 'quidproquo-webserver';
|
|
|
53
53
|
export default [
|
|
54
54
|
defineWebEntry('website', {
|
|
55
55
|
buildPath: './web/dist',
|
|
56
|
-
domain: { onRootDomain: true
|
|
56
|
+
domain: { onRootDomain: true },
|
|
57
57
|
}),
|
|
58
58
|
|
|
59
59
|
// Render product pages server-side for crawlers & link previews
|
|
@@ -17,7 +17,6 @@ export default [
|
|
|
17
17
|
buildPath: './web/dist',
|
|
18
18
|
domain: {
|
|
19
19
|
onRootDomain: true,
|
|
20
|
-
rootDomain: 'example.com',
|
|
21
20
|
},
|
|
22
21
|
}),
|
|
23
22
|
];
|
|
@@ -60,13 +59,11 @@ The web entry's name and `uniqueKey`. This is the name you pass to [askWebEntryI
|
|
|
60
59
|
export interface WebDomainOptions {
|
|
61
60
|
subDomainName?: string;
|
|
62
61
|
onRootDomain: boolean;
|
|
63
|
-
rootDomain: string;
|
|
64
62
|
}
|
|
65
63
|
```
|
|
66
64
|
|
|
67
65
|
| Property | Type | Description |
|
|
68
66
|
| --- | --- | --- |
|
|
69
|
-
| `rootDomain` | `string` | The apex/root domain to host under, e.g. `'example.com'`. The ACM certificate and Route 53 hosted zone are resolved from this. |
|
|
70
67
|
| `onRootDomain` | `boolean` | Serve on the apex domain itself (`example.com`). |
|
|
71
68
|
| `subDomainName` | `string` (optional) | Serve on a subdomain instead of / in addition to the apex, e.g. `'app'` → `app.example.com`. |
|
|
72
69
|
|
|
@@ -112,7 +109,6 @@ export default [
|
|
|
112
109
|
ignoreCache: ['index.html', 'remoteEntry.js', 'mf-manifest.json'],
|
|
113
110
|
domain: {
|
|
114
111
|
onRootDomain: true,
|
|
115
|
-
rootDomain: 'example.com',
|
|
116
112
|
},
|
|
117
113
|
}),
|
|
118
114
|
];
|
|
@@ -126,7 +122,6 @@ const options: QPQConfigAdvancedWebEntrySettings = {
|
|
|
126
122
|
domain: {
|
|
127
123
|
subDomainName: 'views',
|
|
128
124
|
onRootDomain: false,
|
|
129
|
-
rootDomain: 'example.com',
|
|
130
125
|
},
|
|
131
126
|
storageDrive: {
|
|
132
127
|
sourceStorageDrive: 'views',
|
|
@@ -7,13 +7,13 @@ description: Define a WebSocket API with connect, disconnect, and message route
|
|
|
7
7
|
|
|
8
8
|
Defines a **WebSocket API**: a persistent, bidirectional connection endpoint served on its own subdomain. You supply story entry points that run when a client connects, disconnects, or sends a message, and the runtime wires them to the platform's WebSocket transport. Stories push messages back to connected clients with [askWebsocketSendMessage](../../actions/webserver/websocket/ask-websocket-send-message.md).
|
|
9
9
|
|
|
10
|
-
- **On AWS:** deploys an **API Gateway v2 WebSocket API** (`QpqApiWebserverWebsocketConstruct` in `quidproquo-deploy-awscdk`) fronted by a single Lambda proxy integration. Three routes — `$connect`, `$disconnect`, and `$default` — all target that Lambda, which dispatches to the matching handler in `eventProcessors`. The API is mapped to a custom
|
|
10
|
+
- **On AWS:** deploys an **API Gateway v2 WebSocket API** (`QpqApiWebserverWebsocketConstruct` in `quidproquo-deploy-awscdk`) fronted by a single Lambda proxy integration. Three routes — `$connect`, `$disconnect`, and `$default` — all target that Lambda, which dispatches to the matching handler in `eventProcessors`. The API is mapped to a custom domain on every root from [defineDns](./dns.md) (the `{ subdomain: apiSubdomain, service }` target, or `{ subdomain: apiSubdomain }` when `onRootDomain` is set) and served from the `prod` stage. Routes use `authorizationType: NONE`; do your own auth inside the `onConnect`/`onMessage` handlers. When `deprecated` is set, nothing is deployed.
|
|
11
11
|
|
|
12
12
|
```typescript
|
|
13
13
|
import { defineWebsocket } from 'quidproquo-webserver';
|
|
14
14
|
|
|
15
15
|
export default [
|
|
16
|
-
defineWebsocket('ws',
|
|
16
|
+
defineWebsocket('ws', {
|
|
17
17
|
onConnect: '/entry/ws/onConnect::onConnect',
|
|
18
18
|
onDisconnect: '/entry/ws/onDisconnect::onDisconnect',
|
|
19
19
|
onMessage: '/entry/ws/onMessage::onMessage',
|
|
@@ -54,7 +54,6 @@ export type WebsocketEvent<T extends string | Blob | ArrayBuffer = string> = {
|
|
|
54
54
|
```typescript
|
|
55
55
|
function defineWebsocket(
|
|
56
56
|
apiSubdomain: string,
|
|
57
|
-
rootDomain: string,
|
|
58
57
|
eventProcessors: QpqWebSocketEventProcessors,
|
|
59
58
|
options?: QPQConfigAdvancedWebSocketSettings,
|
|
60
59
|
): WebSocketQPQWebServerConfigSetting;
|
|
@@ -64,11 +63,8 @@ function defineWebsocket(
|
|
|
64
63
|
|
|
65
64
|
### `apiSubdomain` — `string` (required)
|
|
66
65
|
|
|
67
|
-
The subdomain the WebSocket API is served on
|
|
66
|
+
The subdomain the WebSocket API is served on; with the default shape the host is `apiSubdomain.<service>.<base>` (or `apiSubdomain.<base>` when `onRootDomain` is set). It alone is the setting's `uniqueKey`.
|
|
68
67
|
|
|
69
|
-
### `rootDomain` — `string` (required)
|
|
70
|
-
|
|
71
|
-
The registered domain the API lives under. On AWS a custom-domain mapping is created for `apiSubdomain.rootDomain` (or, when `onRootDomain` is set, directly under the base/service domain).
|
|
72
68
|
|
|
73
69
|
### `eventProcessors` — `QpqWebSocketEventProcessors` (required)
|
|
74
70
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Domains
|
|
3
|
+
description: How root domains, the app's hostname shape, hosted zones and certificates fit together.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Domains
|
|
7
|
+
|
|
8
|
+
An app is served on one or more **root domains** and every hostname it needs is derived from them. Three things decide the final names:
|
|
9
|
+
|
|
10
|
+
1. **The root list**: [defineDns](./config/webserver/dns.md), primary first, declared by the bootstrap fragment and by every service (normally from one shared constant).
|
|
11
|
+
2. **The domain resolver**: the app's hostname shape, a pure `DomainResolver` export that `defineDns` points at with a `QpqPureFunction` (`{ basePath, relativePath, functionName }`). Omit the pointer for the default shape.
|
|
12
|
+
3. **Hosted zones** (AWS only): derived from the resolver's output, nothing to declare. A host's records go in its root's site root when it sits under it (`api.development.example.com` in the delegated `development.example.com` zone), otherwise in the root itself (`development-api.example.com` in `example.com`). Whichever applies must already exist in Route53.
|
|
13
|
+
|
|
14
|
+
## Targets
|
|
15
|
+
|
|
16
|
+
Every place that needs a host asks for a **target**, `{ subdomain?, service? }`, and gets one host per root:
|
|
17
|
+
|
|
18
|
+
| Setting | Target |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `defineApi('api')` | `{ subdomain: 'api' }` |
|
|
21
|
+
| `defineWebEntry('site', { domain: { onRootDomain: true } })` | `{}` (the site root) |
|
|
22
|
+
| `defineWebEntry('views', { domain: { subDomainName: 'views', onRootDomain: true } })` | `{ subdomain: 'views' }` |
|
|
23
|
+
| `defineWebsocket('ws', ...)` on service `chat` | `{ subdomain: 'ws', service: 'chat' }` |
|
|
24
|
+
| CORS or CSP entry `{ api: 'admin', service: 'admin' }` | `{ subdomain: 'admin', service: 'admin' }` |
|
|
25
|
+
|
|
26
|
+
## The resolver
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// packages/domain/src/domainResolver.ts
|
|
30
|
+
import { DomainResolver } from 'quidproquo';
|
|
31
|
+
|
|
32
|
+
// dev-api.example.com, dev-ws-chat.example.com, example.com in production
|
|
33
|
+
export const domainResolver: DomainResolver = {
|
|
34
|
+
resolveHost: ({ rootDomain, environment, feature, service, subdomain }) => {
|
|
35
|
+
const env = environment === 'production' ? undefined : environment;
|
|
36
|
+
const label = [feature, env, subdomain, service].filter(Boolean).join('-');
|
|
37
|
+
return label ? `${label}.${rootDomain}` : rootDomain;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// packages/domain/src/index.ts: the pointer every config passes to defineDns
|
|
42
|
+
export const DOMAIN_RESOLVER = { basePath: __dirname, relativePath: 'domainResolver', functionName: 'domainResolver' };
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
defineDns(APP_DOMAINS, { resolver: DOMAIN_RESOLVER });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The resolver receives the root, environment, feature and target and returns a host that must be the root or end with `.<root>`. It can branch per root. It must be pure: no actions, no I/O, deterministic. The CDK app and the rspack builds `require` the pointer under ts-node; the lambda runtime bundles it as a src entry and loads it through the dynamic module loader once per processor (CORS, OpenAPI servers, the CloudFront origin-request domain, [askDnsResolveHosts](./actions/webserver/dns/ask-dns-resolve-hosts.md)). Nothing is precomputed and config is never rewritten: every site calls `resolveHosts(qpqConfig, target, resolver)`.
|
|
50
|
+
|
|
51
|
+
The default shape, with no pointer, is `[subdomain.][service.][feature.][environment.]root` with no environment label in production.
|
|
52
|
+
|
|
53
|
+
## The browser
|
|
54
|
+
|
|
55
|
+
Nothing here reaches the browser. How a page finds its api is the app's own concern, as before.
|
|
56
|
+
|
|
57
|
+
## Certificates
|
|
58
|
+
|
|
59
|
+
[defineDomainCertificate](./config/config-aws/domain-certificate.md) issues one certificate per region covering its targets on every root. CloudFront serves all roots from one distribution with the `us-east-1` cert; API Gateway custom domains use the deploy-region cert.
|
|
60
|
+
|
|
61
|
+
## Orphaned certificates
|
|
62
|
+
|
|
63
|
+
A certificate's names are immutable, so any change to the name set (a root added or removed, a target added, a resolver change) issues a new certificate and retains the old one until every distribution and api domain has redeployed. Retained certificates never expire away on their own. They are tagged `application` / `environment` (and `feature`), so list them per app and delete the ones nothing uses:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
aws acm list-certificates --region us-east-1 \
|
|
67
|
+
--query 'CertificateSummaryList[?InUseBy==`[]`].[CertificateArn,DomainName,Status]' --output table
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Adding a root
|
|
71
|
+
|
|
72
|
+
1. Create the Route53 zone for the new root in the deploy account.
|
|
73
|
+
2. Append it to the app's domain constant, which every `defineDns` reads.
|
|
74
|
+
3. Deploy the domain phase (new certificates, api domains), then each service.
|
|
75
|
+
|
|
76
|
+
The first root keeps the existing CloudFormation logical ids; extra roots get suffixed ones, so adding is purely additive and removing is purely deletes. Reordering the list rebuilds domain resources (aliases, records, api domain names), so do it in a quiet window.
|