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
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: askEventDocEventAppend
|
|
3
|
-
description: Append a client-authored event to a document's log
|
|
3
|
+
description: Append a client-authored event to a document's log at the next contiguous position, validating it against the document's state and retrying under write contention.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# askEventDocEventAppend
|
|
7
7
|
|
|
8
|
-
Appends a single client-authored event to a document's ordered event stream — the write half of the event-sourcing core. The event's id is
|
|
8
|
+
Appends a single client-authored event to a document's ordered event stream — the write half of the event-sourcing core. The event's id is its **contiguous position** in the log (`INIT_STATE` is `0`, every append is head + 1), so log order IS commit order: a cursor "after N" is exact, and a snapshot "at N" holds exactly events `0..N`.
|
|
9
9
|
|
|
10
|
-
**
|
|
10
|
+
The append is **expected-version optimistic concurrency**: it resolves the log's current head with a consistent read (and, when `options.validate` is true and the collection has registered validation functions, the document state at that head), validates the event against that state, then writes it at `head + 1` with a **conditional** write. Two writers that resolved the same head race for that slot; exactly one wins, and the other gets back the namespaced Upsert `Conflict`. A losing lap does not start over — it folds only the handful of events that beat it onto the state it already holds, re-validates, and claims the new head + 1 — and retries up to a bounded number of times before giving up (see [Notes](#notes)).
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
**Validation against the resolved state happens here, at append time, when enabled.** Dedup (a repeated `clientMessageId`) and version monotonicity are still decided when the log is folded, against the accepted events before the one in question, and the fold's own acceptance rules remain in place as defence in depth. But the collection's registered `validateEvent` runs against the exact state the event will land on, before the write — the gate that stops a bad event from ever entering the log, not just from being read back. A collection with no registered validation functions (or a caller that passes `{ validate: false }`, e.g. trusted server-authored appends) skips the state resolve and the check, and behaves as write-and-go instead.
|
|
13
|
+
|
|
14
|
+
- **Built from:** `askDateNow`, `askEventDocAppendBaseResolve` / `askEventDocAppendBaseAdvance` (head + state resolution), `askEventDocValidateAppend`, `askEventDocEventWrite`, and `askRetry` (plus, when the collection configures `onPublish`/`onAppend`, `askEventDocGetByIdOrThrow`, `askEventDocHookStates`, and `askInlineFunctionExecute`). Not a single action.
|
|
15
|
+
- **Does not maintain the summary record itself.** The queryable summary is rebuilt from the log by the events store's stream projector ([`onStream`](../../../config/features/event-doc-summary.md)), so it is eventually (not immediately) consistent with a just-written event.
|
|
13
16
|
- **Requires the store context** — wrap the call in [askEventDocProvideStore](./ask-event-doc-provide-store.md) (custom routes) or [askEventDocProvideStoreFromGlobals](./ask-event-doc-provide-store.md#askeventdocprovidestorefromglobals) (built-in routes).
|
|
14
17
|
|
|
15
18
|
```typescript
|
|
@@ -41,6 +44,7 @@ function* askEventDocEventAppend(
|
|
|
41
44
|
modelId: string,
|
|
42
45
|
input: EventDocEventInput,
|
|
43
46
|
actor: EventDocEventActor,
|
|
47
|
+
options?: EventDocEventAppendOptions, // default { validate: true }
|
|
44
48
|
): AskResponse<EventDocEvent>;
|
|
45
49
|
```
|
|
46
50
|
|
|
@@ -48,9 +52,10 @@ function* askEventDocEventAppend(
|
|
|
48
52
|
|
|
49
53
|
| Parameter | Type | Description |
|
|
50
54
|
| --- | --- | --- |
|
|
51
|
-
| `modelId` | `string` | The document id whose log the event is appended to.
|
|
55
|
+
| `modelId` | `string` | The document id whose log the event is appended to. The base resolve throws `NotFound` when the log has no head to append after — every real log opens with `INIT_STATE`, so a missing document (not an empty log) is what this catches. |
|
|
52
56
|
| `input` | `EventDocEventInput` | The client-authored event envelope — see below. |
|
|
53
57
|
| `actor` | `EventDocEventActor` | Who authored the event; stamped onto the event as `createdBy`. Usually obtained from [askEventDocResolveActor](./ask-event-doc-resolve-actor.md). |
|
|
58
|
+
| `options.validate` | `boolean` | Default `true`. Whether to resolve the document state at the append's head and run the collection's registered `validateEvent` before the write. The append route (the client trust boundary) leaves this on; [askEventDocAppendServerEvent](./ask-event-doc-append-server-event.md) passes `false` for trusted server-authored writes, since the fold remains their gate. |
|
|
54
59
|
|
|
55
60
|
### `EventDocEventInput`
|
|
56
61
|
|
|
@@ -72,7 +77,7 @@ What the client POSTs to append an event. `modelId` and the server-stamped prove
|
|
|
72
77
|
|
|
73
78
|
## Returns
|
|
74
79
|
|
|
75
|
-
`AskResponse<EventDocEvent>` — the event now written to the log
|
|
80
|
+
`AskResponse<EventDocEvent>` — the event now durably written to the log at its claimed `eventId`, with server-stamped metadata (`eventId`, `createdAt`, `createdBy`) filled in. Passing pre-write validation does not exempt it from the fold's own rules — a fold may still skip it on a duplicate `clientMessageId` or a stale version.
|
|
76
81
|
|
|
77
82
|
### `EventDocEvent`
|
|
78
83
|
|
|
@@ -80,15 +85,16 @@ What the client POSTs to append an event. `modelId` and the server-stamped prove
|
|
|
80
85
|
| --- | --- | --- |
|
|
81
86
|
| `type` | `string` | The event type discriminant. |
|
|
82
87
|
| `payload.data` | `T` | The typed domain data. |
|
|
83
|
-
| `payload.metadata` | `EventDocEventMetadata` | Full provenance: `version`, `clientMessageId`, `createdBy`, `createdAt`, and `eventId` (
|
|
88
|
+
| `payload.metadata` | `EventDocEventMetadata` | Full provenance: `version`, `clientMessageId`, `createdBy`, `createdAt`, and `eventId` (the event's contiguous position in the log — mirrors the storage sort key). |
|
|
84
89
|
|
|
85
90
|
## Notes
|
|
86
91
|
|
|
87
|
-
- **
|
|
88
|
-
- **
|
|
89
|
-
- **Write
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
+
- **Dedup and version monotonicity are still decided at fold time**, against the accepted events before the one in question: a repeated `clientMessageId` is ignored, and an event whose version is older than the log's highest accepted version is ignored. The append does not check either.
|
|
93
|
+
- **Domain/lifecycle validation now runs at append time too, when enabled.** When `options.validate` is `true` and the collection has registered `validateEvent` functions, the event is checked against the document state at the head it will land on, before the write; a rejection throws `ErrorTypeEnum.Invalid` and nothing is written. A collection with no registered functions has nothing to validate with and falls back to write-and-go, same as `{ validate: false }`.
|
|
94
|
+
- **Write contention is expected and retried, not treated as a bug.** [askEventDocEventWrite](./ask-event-doc-event-write.md)'s conditional (`ifNotExists`) write is the slot two writers that resolved the same head race for; the loser gets `KeyValueStoreUpsertErrorTypeEnum.Conflict`, folds just the events that beat it onto the state it already holds, re-validates, and re-laps at the new head. Retries are bounded (`EVENT_DOC_APPEND_MAX_RETRIES`, with linear backoff and jitter); exhausting them throws `ErrorTypeEnum.Conflict` — sustained contention on one document means something is hammering it, not ordinary concurrent editing. Different documents are different partition keys and never contend with each other.
|
|
95
|
+
- **Log order is commit order.** Because the id is the log's next contiguous position rather than a value minted independently by each writer, `afterEventId` cursors and snapshot positions (`upToEventId`) are exact — no two events can claim the same position, and there is no "arbitrary but stable" ordering case to reason about.
|
|
96
|
+
- **Does not maintain the summary record.** The summary is rebuilt from the log by the events store's stream projector, so it lags a just-written event until the stream delivers.
|
|
97
|
+
- Hooks (`onPublish`/`onAppend`, when the collection configures them) run after the event is durably written and outside the retry loop; a hook failure propagates so the caller knows the side effect — not the append — failed.
|
|
92
98
|
|
|
93
99
|
## Related
|
|
94
100
|
|
|
@@ -5,7 +5,7 @@ description: Read a document's event log — a page of events, the whole log fla
|
|
|
5
5
|
|
|
6
6
|
# Reading the event log
|
|
7
7
|
|
|
8
|
-
Three read helpers over a document's event stream. All three resolve the collection's events store from the store context and query it by `pk = modelId`, ascending by event id (except `askEventDocEventLast`, which reads the tail). Event ids are
|
|
8
|
+
Three read helpers over a document's event stream. All three resolve the collection's events store from the store context and query it by `pk = modelId`, ascending by event id (except `askEventDocEventLast`, which reads the tail). Event ids are the event's contiguous position in the log (`INIT_STATE` is `0`, every append is head + 1, claimed by a conditional write), so log order IS commit order and ascending-by-id and ascending-by-creation-time always agree. They are the read side of the event-sourcing core, feeding the fold that reconstructs a document from its events.
|
|
9
9
|
|
|
10
10
|
- **Requires the store context** — provide it via [askEventDocProvideStore](./ask-event-doc-provide-store.md) / [askEventDocProvideStoreFromGlobals](./ask-event-doc-provide-store.md#askeventdocprovidestorefromglobals).
|
|
11
11
|
- **Built from:** [askKeyValueStoreQuery](../../core/key-value-store/ask-key-value-store-query.md) against the events store, plus [askEventDocResolveStore](./ask-event-doc-provide-store.md#askeventdocresolvestore). Not single actions.
|
|
@@ -17,7 +17,7 @@ Returns one page of events for a document, oldest first. Supports paging and, vi
|
|
|
17
17
|
```typescript
|
|
18
18
|
import { askEventDocEventList } from 'quidproquo-features';
|
|
19
19
|
|
|
20
|
-
export function* refreshSince(docId: string, lastSeenEventId:
|
|
20
|
+
export function* refreshSince(docId: string, lastSeenEventId: number) {
|
|
21
21
|
const page = yield* askEventDocEventList(docId, { afterEventId: lastSeenEventId });
|
|
22
22
|
return page.items; // events after lastSeenEventId
|
|
23
23
|
}
|
|
@@ -45,8 +45,8 @@ function* askEventDocEventList(
|
|
|
45
45
|
| --- | --- | --- | --- |
|
|
46
46
|
| `limit` | `number` | (store default) | Max number of events to return in the page. |
|
|
47
47
|
| `nextPageKey` | `string` | — | Continuation token from a previous page's `nextPageKey`. |
|
|
48
|
-
| `afterEventId` | `
|
|
49
|
-
| `upToEventId` | `
|
|
48
|
+
| `afterEventId` | `number` | — | Return only events whose event id is greater than this one (exclusive). A sort-key range condition on the events store's primary key — no GSI involved. |
|
|
49
|
+
| `upToEventId` | `number` | — | Return only events whose event id is less than or equal to this one (inclusive) — the log prefix up to a known event, for folding a document as of that event (a snapshot). Combinable with `afterEventId` for a bounded gap range. |
|
|
50
50
|
| `sortDescending` | `boolean` | `false` | Newest first, for a display read that walks backwards in time (e.g. a history panel's latest-page-then-load-older). Folding reads never set this — a fold consumes the log in order. |
|
|
51
51
|
| `consistentRead` | `boolean` | `false` | Strongly consistent read. Needed by a caller that just appended and is now folding to decide something — the default eventually-consistent read can otherwise miss that caller's own most recent event. Costs roughly double the read capacity, so leave it off for ordinary reads. |
|
|
52
52
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: askEventDocEventWrite
|
|
3
|
-
description: Low-level conditional write of a single event to a document's events store, keyed by its
|
|
3
|
+
description: Low-level conditional write of a single event to a document's events store, keyed by its contiguous event id.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# askEventDocEventWrite
|
|
7
7
|
|
|
8
|
-
The low-level write primitive behind the event log. It persists one already-built [EventDocEvent](./ask-event-doc-event-append.md#eventdocevent) into the collection's events store, keyed by `pk = modelId` / `sk = eventId` (a
|
|
8
|
+
The low-level write primitive behind the event log. It persists one already-built [EventDocEvent](./ask-event-doc-event-append.md#eventdocevent) into the collection's events store, keyed by `pk = modelId` / `sk = eventId` (a contiguous integer — `INIT_STATE` is `0`, every later event is the previous head plus one). The write is **conditional** (`ifNotExists`), and that condition is the slot two concurrent writers race for: whichever append resolved the losing writer's `headEventId` first claims `headEventId + 1`, and the loser gets `Conflict` back.
|
|
9
9
|
|
|
10
|
-
Id assignment, dedup, and validation all live one layer up in [askEventDocEventAppend](./ask-event-doc-event-append.md) (dedup and validation are
|
|
10
|
+
Id assignment (advancing past a lost race), dedup, and domain validation all live one layer up in [askEventDocEventAppend](./ask-event-doc-event-append.md) (dedup and domain validation are decided later still, at fold time) — you almost always want that instead. Call this directly only when you are implementing your own append semantics.
|
|
11
11
|
|
|
12
12
|
- **Built from:** [askKeyValueStoreUpsertWithRetry](../../core/key-value-store/ask-key-value-store-upsert-with-retry.md) with `{ ifNotExists: true }`, plus [askEventDocResolveStore](./ask-event-doc-provide-store.md#askeventdocresolvestore) to find the events store name. Not a single action.
|
|
13
13
|
- **Requires the store context** — provide it via [askEventDocProvideStore](./ask-event-doc-provide-store.md) / [askEventDocProvideStoreFromGlobals](./ask-event-doc-provide-store.md#askeventdocprovidestorefromglobals).
|
|
@@ -31,7 +31,7 @@ function* askEventDocEventWrite(modelId: string, event: EventDocEvent): AskRespo
|
|
|
31
31
|
| Parameter | Type | Description |
|
|
32
32
|
| --- | --- | --- |
|
|
33
33
|
| `modelId` | `string` | The document id — becomes the partition key (`pk`) of the stored event. |
|
|
34
|
-
| `event` | `EventDocEvent` | A fully-formed event, including `payload.metadata.eventId` — the
|
|
34
|
+
| `event` | `EventDocEvent` | A fully-formed event, including `payload.metadata.eventId` — the contiguous id becomes the sort key (`sk`) and the slot that is claimed conditionally. |
|
|
35
35
|
|
|
36
36
|
## Returns
|
|
37
37
|
|
|
@@ -40,11 +40,11 @@ function* askEventDocEventWrite(modelId: string, event: EventDocEvent): AskRespo
|
|
|
40
40
|
## Notes
|
|
41
41
|
|
|
42
42
|
- The stored shape is `{ pk: modelId, sk: eventId, data: event }`; the `EventDocStoredEvent` mapping is the only place that knows the key layout, keeping the domain event free of storage concerns.
|
|
43
|
-
- Because the write is conditional,
|
|
43
|
+
- Because the write is conditional, a slot already taken surfaces `KeyValueStoreUpsertErrorTypeEnum.Conflict`. [askEventDocEventAppend](./ask-event-doc-event-append.md) treats that as the expected outcome of losing a race for `headEventId + 1` — it catches the conflict and retries at the new head, up to a bounded number of laps, rather than treating it as a bug.
|
|
44
44
|
|
|
45
45
|
## Related
|
|
46
46
|
|
|
47
|
-
- [askEventDocEventAppend](./ask-event-doc-event-append.md) — the high-level append that
|
|
47
|
+
- [askEventDocEventAppend](./ask-event-doc-event-append.md) — the high-level append that resolves the next contiguous id and writes through this.
|
|
48
48
|
- [askEventDocEventList / EventListAll / EventLast](./ask-event-doc-event-list.md) — reading events back.
|
|
49
49
|
- [askKeyValueStoreUpsertWithRetry](../../core/key-value-store/ask-key-value-store-upsert-with-retry.md) — the underlying conditional upsert.
|
|
50
50
|
- [askEventDocProvideStore](./ask-event-doc-provide-store.md) — provides the required store context.
|
|
@@ -26,7 +26,7 @@ export function* loadArticle(id: string) {
|
|
|
26
26
|
An event document is never stored as a mutable blob. Its authoritative state is an **append-only log of events**; the document you read is *derived by folding that log*.
|
|
27
27
|
|
|
28
28
|
- **Summary record** ([`EventDocSummary`](#the-summary-record)) — the queryable projection folded from the log's identity/lifecycle events (`INIT_STATE`, `SET_CODE`, `SET_NAME`, `PUBLISH`, `DELETE`, `RESTORE`, …). It holds identity (`id`, `code`, `name`), audit fields, and a `versions` array. Every field on it is derived from the log — including `deletedAt`, set and cleared by `DELETE`/`RESTORE` events rather than written directly — so the whole record can be dropped and rebuilt from the log at any time. The document's editable **content** is folded separately (on the client) from the same log; the backend never reduces content.
|
|
29
|
-
- **Draft vs published** — the tail (highest) version with no `publishedAt` is the **draft**; a `PUBLISH` event freezes it and starts the next draft. Each version pointer records the `eventId` (
|
|
29
|
+
- **Draft vs published** — the tail (highest) version with no `publishedAt` is the **draft**; a `PUBLISH` event freezes it and starts the next draft. Each version pointer records the `eventId` (its contiguous position in the log) of its last event (its head), so folding events whose `eventId` is at or before that head reconstructs the version's content as it was. `publishedAt` is when a version was published; `effectiveFrom` is when that publish takes effect (used for as-of time-travel).
|
|
30
30
|
- **Code** — the caller-chosen, stable business key set at create (via `INIT_STATE`) and editable with `SET_CODE`. It stays constant across versions and is expected unique within the collection (and any owner scope), so you can address a document by `code` instead of its generated `id`.
|
|
31
31
|
|
|
32
32
|
The version-pointer reads ([askEventDocGetDraft, askEventDocGetLatestPublished, askEventDocGetPublishedAsOf, askEventDocPublishedVersionAsOf](./ask-event-doc-get-draft.md)) resolve entries in this model.
|
|
@@ -49,7 +49,7 @@ type EventDocSummary = {
|
|
|
49
49
|
|
|
50
50
|
type EventDocVersion = {
|
|
51
51
|
version: number;
|
|
52
|
-
eventId:
|
|
52
|
+
eventId: number; // log id of this version's head event
|
|
53
53
|
publishedAt?: string; // unset while it is the tail draft
|
|
54
54
|
effectiveFrom?: string; // when the publish takes effect (as-of selection)
|
|
55
55
|
};
|
|
@@ -41,13 +41,13 @@ function* askEventDocGetDraft(id: string): AskResponse<Nullable<EventDocVersion>
|
|
|
41
41
|
```typescript
|
|
42
42
|
type EventDocVersion = {
|
|
43
43
|
version: number;
|
|
44
|
-
eventId:
|
|
44
|
+
eventId: number; // log id of this version's head event
|
|
45
45
|
publishedAt?: string; // unset while it is the tail draft
|
|
46
46
|
effectiveFrom?: string; // when the publish takes effect (as-of selection)
|
|
47
47
|
};
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
To fold or render a version's content, fold the log's events whose `eventId`
|
|
50
|
+
To fold or render a version's content, fold the log's events whose `eventId` is at or before the version's `eventId`.
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: askDnsList
|
|
3
|
-
description: List the
|
|
3
|
+
description: List the root domains a service has declared.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# askDnsList
|
|
7
7
|
|
|
8
|
-
Returns the
|
|
8
|
+
Returns the root domains the service declared with [defineDns](../../../config/webserver/dns.md), primary first. Use it when a story needs to know which roots the service is served on; for a hostname, use [askDnsResolveHosts](./ask-dns-resolve-hosts.md).
|
|
9
9
|
|
|
10
10
|
- **Action type:** `DnsActionType.List`
|
|
11
11
|
|
|
12
12
|
```typescript
|
|
13
13
|
import { askDnsList } from 'quidproquo-webserver';
|
|
14
14
|
|
|
15
|
-
export function*
|
|
16
|
-
const
|
|
17
|
-
return
|
|
15
|
+
export function* askPrimaryRootDomain() {
|
|
16
|
+
const roots = yield* askDnsList();
|
|
17
|
+
return roots[0];
|
|
18
18
|
}
|
|
19
19
|
```
|
|
20
20
|
|
|
@@ -28,10 +28,11 @@ Takes no arguments.
|
|
|
28
28
|
|
|
29
29
|
## Returns
|
|
30
30
|
|
|
31
|
-
`string[]` — the `
|
|
31
|
+
`string[]` — the `rootDomains` of the service's [defineDns](../../../config/webserver/dns.md) config, empty when it declares none.
|
|
32
32
|
|
|
33
|
-
- **On AWS:** this does **not** query Route53. The processor reads the service's own config
|
|
33
|
+
- **On AWS:** this does **not** query Route53. The processor reads the service's own config, so the result is exactly what was declared.
|
|
34
34
|
|
|
35
35
|
## Related
|
|
36
36
|
|
|
37
|
-
- [defineDns](../../../config/webserver/dns.md) — declares the
|
|
37
|
+
- [defineDns](../../../config/webserver/dns.md) — declares the roots this action returns.
|
|
38
|
+
- [askDnsResolveHosts](./ask-dns-resolve-hosts.md) — a target's host on every root.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: askDnsResolveHosts
|
|
3
|
+
description: Resolve a host target to its hostname on every root domain the service declares.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# askDnsResolveHosts
|
|
7
|
+
|
|
8
|
+
Returns the hostname for a `{ subdomain?, service? }` target on every root the service declared with [defineDns](../../../config/webserver/dns.md), primary first. The processor runs the app's [domain resolver](../../../domains.md) (loaded once through the dynamic module loader), so any target resolves, not only ones the config declares.
|
|
9
|
+
|
|
10
|
+
- **Action type:** `DnsActionType.ResolveHosts`
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { askDnsResolveHosts } from 'quidproquo-webserver';
|
|
14
|
+
|
|
15
|
+
export function* askPrimarySiteUrl() {
|
|
16
|
+
const [siteHost] = yield* askDnsResolveHosts();
|
|
17
|
+
return `https://${siteHost}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function* askApiHosts() {
|
|
21
|
+
return yield* askDnsResolveHosts({ subdomain: 'api' });
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Signature
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
function* askDnsResolveHosts(target?: DomainTarget): AskResponse<string[]>;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### `target` — `DomainTarget` (optional)
|
|
32
|
+
|
|
33
|
+
`{ subdomain?: string; service?: string }`. Omitted means the site root.
|
|
34
|
+
|
|
35
|
+
## Returns
|
|
36
|
+
|
|
37
|
+
`string[]` — one host per root, primary first; empty when the service declares no domain.
|
|
38
|
+
|
|
39
|
+
## Related
|
|
40
|
+
|
|
41
|
+
- [defineDns](../../../config/webserver/dns.md) — the roots resolved against.
|
|
42
|
+
- [askDnsList](./ask-dns-list.md) — the root list itself.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "label": "OpenAPI", "link": { "type": "generated-index", "description": "Generate the service's OpenAPI document from its route config." } }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: askOpenApiGetDocument
|
|
3
|
+
description: Build the service's OpenAPI 3.1 document from its route config.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# askOpenApiGetDocument
|
|
7
|
+
|
|
8
|
+
Returns the **OpenAPI document** describing every route in the service. Stories never see the service config, so the walk happens in the processor, which has it. The result is plain data, ready to serialise.
|
|
9
|
+
|
|
10
|
+
- **Action type:** `OpenApiActionType.GetDocument`
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { askOpenApiGetDocument, qpqWebServerUtils } from 'quidproquo-webserver';
|
|
14
|
+
|
|
15
|
+
export function* getSpec() {
|
|
16
|
+
const document = yield* askOpenApiGetDocument({ title: 'Widgets API', version: '1.2.0' });
|
|
17
|
+
|
|
18
|
+
return qpqWebServerUtils.toJsonEventResponse(document);
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Signature
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
function* askOpenApiGetDocument(options?: OpenApiDocumentOptions): AskResponse<OpenApiDocument>;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Parameters
|
|
29
|
+
|
|
30
|
+
### `options` — `OpenApiDocumentOptions` (optional)
|
|
31
|
+
|
|
32
|
+
`title`, `version` and `description` for the document's info block. The title defaults to the application and module name, the version to `1.0.0`.
|
|
33
|
+
|
|
34
|
+
## Returns
|
|
35
|
+
|
|
36
|
+
`OpenApiDocument` — an OpenAPI 3.1 document with `info`, `servers`, `paths` and `components.securitySchemes`. The same structure `buildOpenApiDocument` in `qpqWebServerUtils` produces when given a config directly, which is the way to generate it at build time.
|
|
37
|
+
|
|
38
|
+
## Related
|
|
39
|
+
|
|
40
|
+
- [defineOpenApiRoutes](../../../config/features/open-api-routes.md) — serves this document and a reference page for it.
|
|
41
|
+
- [defineRoute](../../../config/webserver/route.md) — the `schema` option that fills in request and response shapes.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: defineAccountGithubOidcProvider
|
|
3
|
+
description: The account-level GitHub Actions OpenID Connect provider that every app's deploy role trusts.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# defineAccountGithubOidcProvider
|
|
7
|
+
|
|
8
|
+
Declares the account's **GitHub Actions OpenID Connect provider**, `token.actions.githubusercontent.com`. It is an account setting: declared once in `account.qpq.ts`, deployed by the `qpq-account` stack, and shared by every app's [`defineAwsGithubDeployRole`](./github-deploy-role.md) in that account.
|
|
9
|
+
|
|
10
|
+
- **On AWS:** creates an `aws_iam.OpenIdConnectProvider` for `https://token.actions.githubusercontent.com` with the `sts.amazonaws.com` audience. The provider's ARN is fixed by its URL, which is what the deploy roles trust. An account can hold only one provider for that URL, so a provider created by hand must be deleted before the account stack can own it.
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { defineAccountGithubOidcProvider } from 'quidproquo-config-aws';
|
|
14
|
+
|
|
15
|
+
export default [
|
|
16
|
+
defineAccountGithubOidcProvider(),
|
|
17
|
+
];
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Signature
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
function defineAccountGithubOidcProvider(): AccountGithubOidcProviderQPQConfigSetting;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Takes no arguments.
|
|
27
|
+
|
|
28
|
+
## Related
|
|
29
|
+
|
|
30
|
+
- [defineAwsGithubDeployRole](./github-deploy-role.md) — the per-app, per-environment role that trusts this provider.
|
|
31
|
+
- [defineAccountBudget](./account-budget.md) — another account-stack setting.
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: defineDomainCertificate
|
|
3
|
-
description: Issue
|
|
3
|
+
description: Issue one DNS-validated ACM certificate per region covering the app's hosts on every root domain.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# defineDomainCertificate
|
|
7
7
|
|
|
8
|
-
Issues a real **ACM (AWS Certificate Manager) certificate
|
|
8
|
+
Issues a real **ACM (AWS Certificate Manager) certificate** for a region, covering a set of host targets on **every** root declared with [defineDns](../webserver/dns.md). This is the certificate API Gateway and CloudFront use for HTTPS.
|
|
9
9
|
|
|
10
|
-
- **On AWS:** each
|
|
10
|
+
- **On AWS:** each region becomes one `DomainCertificateStack` (via `createDomainCertificateStacks` in `quidproquo-deploy-awscdk`). Every target is resolved on every root through the app's [domain resolver](../../domains.md), each name is DNS-validated in its own hosted zone (`CertificateValidation.fromDnsMultiZone`), and the ARN is published to SSM under an app-keyed parameter (`/qpq/domain/certificate-arn/<region>/<app>-<environment>[-<feature>]`), cross-region via a custom resource when the cert region differs from the deploy region. The certificate is **retained** on replacement: changing the name set issues a new cert while distributions still referencing the old ARN keep working until they redeploy. Entries for the same region are merged. ACM allows 10 names per certificate by default; synth fails with the name list when a config exceeds it.
|
|
11
11
|
|
|
12
12
|
```typescript
|
|
13
13
|
import { defineDomainCertificate } from 'quidproquo-config-aws';
|
|
14
14
|
|
|
15
|
-
export default [
|
|
16
|
-
//
|
|
17
|
-
defineDomainCertificate('
|
|
15
|
+
export default ({ region }) => [
|
|
16
|
+
// CloudFront: the site root plus www on every root
|
|
17
|
+
defineDomainCertificate('us-east-1', [{ subdomain: 'www' }], { includeApex: true }),
|
|
18
|
+
|
|
19
|
+
// Regional API Gateway: the api and a service-scoped websocket
|
|
20
|
+
defineDomainCertificate(region, [{ subdomain: 'api' }, { subdomain: 'ws', service: 'chat' }]),
|
|
18
21
|
];
|
|
19
22
|
```
|
|
20
23
|
|
|
@@ -22,48 +25,29 @@ export default [
|
|
|
22
25
|
|
|
23
26
|
```typescript
|
|
24
27
|
function defineDomainCertificate(
|
|
25
|
-
rootDomain: string,
|
|
26
28
|
region: string,
|
|
27
|
-
|
|
29
|
+
targets: DomainTarget[],
|
|
28
30
|
options?: { includeApex?: boolean },
|
|
29
31
|
): DomainCertificateQPQConfigSetting;
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
## Parameters
|
|
33
35
|
|
|
34
|
-
### `rootDomain` — `string` (required)
|
|
35
|
-
|
|
36
|
-
The base, un-prefixed apex domain — the same value you pass to `defineApi` / web-entry `rootDomain` fields. At synth time it is resolved against the config's environment and feature (a dev deploy of `"example.com"` becomes `development.example.com`, or `myfeature.development.example.com`). Together with `region` it forms the setting's `uniqueKey`.
|
|
37
|
-
|
|
38
36
|
### `region` — `string` (required)
|
|
39
37
|
|
|
40
|
-
The AWS region to issue the certificate in. CloudFront requires
|
|
38
|
+
The AWS region to issue the certificate in. CloudFront requires `us-east-1`; regional API Gateway custom domains need the deploy region. It is the setting's `uniqueKey`, so entries for one region merge.
|
|
41
39
|
|
|
42
|
-
### `
|
|
40
|
+
### `targets` — `DomainTarget[]` (required)
|
|
43
41
|
|
|
44
|
-
The
|
|
42
|
+
The hosts to cover, as `{ subdomain?, service? }` targets. Each is resolved on every root with the app's domain resolver, exactly as the api, web entry or websocket that uses it will be. `{ subdomain: 'api' }` covers the api host; `{ subdomain: 'ws', service: 'chat' }` covers a websocket that is not `onRootDomain`.
|
|
45
43
|
|
|
46
44
|
### `options` — `{ includeApex?: boolean }` (optional)
|
|
47
45
|
|
|
48
46
|
| Property | Type | Default | Description |
|
|
49
47
|
| --- | --- | --- | --- |
|
|
50
|
-
| `includeApex` | `boolean` | `false` | Also cover the
|
|
51
|
-
|
|
52
|
-
## Examples
|
|
53
|
-
|
|
54
|
-
```typescript
|
|
55
|
-
import { defineDomainCertificate } from 'quidproquo-config-aws';
|
|
56
|
-
|
|
57
|
-
export default [
|
|
58
|
-
// Regional API cert in the deploy region
|
|
59
|
-
defineDomainCertificate('example.com', 'us-east-1', ['api']),
|
|
60
|
-
|
|
61
|
-
// CloudFront cert (must be us-east-1) covering the apex and www
|
|
62
|
-
defineDomainCertificate('example.com', 'us-east-1', ['www'], { includeApex: true }),
|
|
63
|
-
];
|
|
64
|
-
```
|
|
48
|
+
| `includeApex` | `boolean` | `false` | Also cover each root's site root (the `{}` target), which a web entry with `onRootDomain: true` and no subdomain serves on. |
|
|
65
49
|
|
|
66
50
|
## Related
|
|
67
51
|
|
|
68
|
-
- [
|
|
69
|
-
- [
|
|
52
|
+
- [Domains](../../domains.md) — the resolver every target goes through.
|
|
53
|
+
- [defineDns](../webserver/dns.md) — the roots the certificate covers.
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: defineEmailSenderAllowList
|
|
3
|
-
description: Recipient addresses a service
|
|
3
|
+
description: Recipient addresses a service may email while its SES account is still in sandbox mode.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# defineEmailSenderAllowList
|
|
7
7
|
|
|
8
|
-
Declares recipient addresses
|
|
8
|
+
Declares recipient addresses the service is allowed to email while the SES account is in **sandbox** mode. In sandbox, SES authorises a send against the recipient's identity as well as the sender's, so the exact-ARN send grant needs each recipient identity listed too. The addresses must also be verified identities in the SES console.
|
|
9
9
|
|
|
10
|
-
This is an AWS-specific concession, not a portable email concept, which is why it lives in `quidproquo-config-aws`
|
|
10
|
+
This is an AWS-specific concession, not a portable email concept, which is why it lives in `quidproquo-config-aws` beside [defineEmailSender](../webserver/email-sender.md) rather than on that webserver setting. Once the account has SES production access this setting does nothing useful and can be deleted.
|
|
11
11
|
|
|
12
12
|
```typescript
|
|
13
13
|
import { defineEmailSender } from 'quidproquo-webserver';
|
|
14
14
|
import { defineEmailSenderAllowList } from 'quidproquo-config-aws';
|
|
15
15
|
|
|
16
16
|
export default [
|
|
17
|
-
defineEmailSender(
|
|
18
|
-
|
|
19
|
-
defineEmailSenderAllowList('example.com', ['joe@external.com', 'test@external.com']),
|
|
17
|
+
defineEmailSender(),
|
|
18
|
+
defineEmailSenderAllowList(['joe@external.com', 'test@external.com']),
|
|
20
19
|
];
|
|
21
20
|
```
|
|
22
21
|
|
|
@@ -24,22 +23,16 @@ export default [
|
|
|
24
23
|
|
|
25
24
|
```typescript
|
|
26
25
|
function defineEmailSenderAllowList(
|
|
27
|
-
rootDomain: string,
|
|
28
26
|
allowedEmailAddresses: string[],
|
|
29
27
|
): EmailSenderAllowListQPQConfigSetting;
|
|
30
28
|
```
|
|
31
29
|
|
|
32
30
|
## Parameters
|
|
33
31
|
|
|
34
|
-
### `rootDomain` — `string` (required)
|
|
35
|
-
|
|
36
|
-
The `rootDomain` of the matching [defineEmailSender](../webserver/email-sender.md) this allow-list applies to.
|
|
37
|
-
|
|
38
32
|
### `allowedEmailAddresses` — `string[]` (required)
|
|
39
33
|
|
|
40
|
-
Recipient addresses to grant sandbox send access to. Multiple calls
|
|
34
|
+
Recipient addresses to grant sandbox send access to. Multiple calls are additive.
|
|
41
35
|
|
|
42
36
|
## Related
|
|
43
37
|
|
|
44
|
-
- [defineEmailSender](../webserver/email-sender.md) — the sending
|
|
45
|
-
- [askEmailSendEmail](../../actions/webserver/email/ask-email-send-email.md) — the action these addresses need to be reachable from.
|
|
38
|
+
- [defineEmailSender](../webserver/email-sender.md) — the sending identities this allow-list extends.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: defineAwsGithubDeployRole
|
|
3
|
+
description: The IAM role GitHub Actions assumes through OIDC to deploy an app environment.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# defineAwsGithubDeployRole
|
|
7
|
+
|
|
8
|
+
Declares the **IAM role a GitHub Actions workflow assumes** (via OpenID Connect, no stored keys) to deploy this app's environment. It is a bootstrap setting: declared once in `bootstrap.qpq.ts`, deployed by the bootstrap stack.
|
|
9
|
+
|
|
10
|
+
- **On AWS:** creates `github-actions-deploy-<app>-<environment>` with a two-hour session and only what a deploy needs: `sts:AssumeRole` on the CDK bootstrap roles (deploy, file-publishing, image-publishing, lookup) in every region the app deploys to, read and write on this app's S3 buckets (`*-<app>-*-<environment>`, where the built views and federated remotes are synced), and read on the `/cdk-bootstrap/*` and `/qpq/*` SSM parameters. CloudFormation applies the stacks as the bootstrap exec role, so the deploy identity never holds the permissions of what it deploys. Its trust policy allows `sts:AssumeRoleWithWebIdentity` from the account's `token.actions.githubusercontent.com` provider when the token's audience is `sts.amazonaws.com` and its subject is the repository's immutable-id form `repo:<owner>@<ownerId>/<name>@<repositoryId>:environment:<githubEnvironment>`. The name form `repo:<owner>/<name>:...` is trusted only when no ids are given or `trustNameForm` is set, since a name can be re-earned by whoever next owns it and an id cannot. GitHub only issues the id form for repositories switched to immutable subjects (new repositories, or `gh api -X PUT repos/<owner>/<name>/actions/oidc/customization/sub -F use_default=false -F use_immutable_subject=true` for older ones); switch the repository before deploying an id-only role. The role ARN is a stack output. The provider itself is one per account and comes from [`defineAccountGithubOidcProvider`](./account-github-oidc-provider.md) in the account config.
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { defineAwsGithubDeployRole } from 'quidproquo-config-aws';
|
|
14
|
+
|
|
15
|
+
export default [
|
|
16
|
+
defineAwsGithubDeployRole('qpqjs/quidproquo', { ownerId: 314167689, repositoryId: 571382961 }),
|
|
17
|
+
];
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Signature
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
function defineAwsGithubDeployRole(
|
|
24
|
+
repository: string,
|
|
25
|
+
options?: { ownerId?: number; repositoryId?: number; githubEnvironment?: string },
|
|
26
|
+
): AwsGithubDeployRoleQPQConfigSetting;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Parameters
|
|
30
|
+
|
|
31
|
+
### `repository` — `string` (required)
|
|
32
|
+
|
|
33
|
+
`owner/name` of the GitHub repository whose workflows may assume the role.
|
|
34
|
+
|
|
35
|
+
### `options` (optional)
|
|
36
|
+
|
|
37
|
+
| Property | Type | Default | Description |
|
|
38
|
+
| --- | --- | --- | --- |
|
|
39
|
+
| `ownerId` | `number` | – | The owner's numeric GitHub id. `gh api repos/<owner>/<name> --jq .owner.id`. With `repositoryId`, also trusts the immutable-id subject form. |
|
|
40
|
+
| `repositoryId` | `number` | – | The repository's numeric GitHub id. `gh api repos/<owner>/<name> --jq .id`. |
|
|
41
|
+
| `githubEnvironment` | `string` | the deploy environment | The GitHub Environment the deploy job declares (`environment:` on the job). A job without it is refused by STS. |
|
|
42
|
+
| `trustNameForm` | `boolean` | `true` without ids, else `false` | Also trust the name-form subject. Only for a repository not yet switched to immutable subjects. |
|
|
43
|
+
|
|
44
|
+
## Locking it down further
|
|
45
|
+
|
|
46
|
+
Protect the GitHub Environment itself (Settings, Environments): required reviewers and a deployment-branch rule limited to `main` mean a token with the right subject only exists for approved runs of trusted branches.
|
|
47
|
+
|
|
48
|
+
## Wiring the workflow
|
|
49
|
+
|
|
50
|
+
After the bootstrap stack deploys, `qpq setup --check` prints the role ARN. Put it in the GitHub Environment's variables as `DEPLOY_ROLE_ARN` with `AWS_REGION`, and have the job declare that environment and `permissions: id-token: write`:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
jobs:
|
|
54
|
+
deploy:
|
|
55
|
+
environment: development
|
|
56
|
+
permissions:
|
|
57
|
+
id-token: write
|
|
58
|
+
contents: read
|
|
59
|
+
steps:
|
|
60
|
+
- uses: aws-actions/configure-aws-credentials@v6
|
|
61
|
+
with:
|
|
62
|
+
role-to-assume: ${{ vars.DEPLOY_ROLE_ARN }}
|
|
63
|
+
aws-region: ${{ vars.AWS_REGION }}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Related
|
|
67
|
+
|
|
68
|
+
- [defineBootstrapWaf](./bootstrap-waf.md) — another bootstrap-stack setting.
|