create-qpq-app 0.1.10 → 0.1.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-qpq-app",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Scaffold a new quidproquo app — npx create-qpq-app my-app",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "^22.13.13",
54
- "quidproquo-tsconfig": "0.1.10"
54
+ "quidproquo-tsconfig": "0.1.11"
55
55
  },
56
56
  "bin": {
57
57
  "create-qpq-app": "./lib/commonjs/bin/createQpqApp.js"
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: askEventDocReadIdentity
3
+ description: Declaratively read where an event-doc's own document lives (serviceName/basePath/id), for verbs that build links relative to their own doc.
4
+ ---
5
+
6
+ # askEventDocReadIdentity
7
+
8
+ The address sibling of [askEventDocReadState](./ask-event-doc-read-state.md): the story yields no payload at all, and a registered processor answers with the doc's own identity — `serviceName`, `basePath`, and `id`. Verbs written in terms of it (for example, building an `EventDocLink` relative to their own doc) stay workspace-blind, resolving whichever doc the processor is bound to.
9
+
10
+ - **Action type:** `EventDocActionType.ReadIdentity`
11
+ - **quidproquo-features ships no default processor** for this action. Calling it with no processor registered fails loudly, the same as an unbound [askEventDocReadState](./ask-event-doc-read-state.md).
12
+ - Returns `null` until the doc has an identity to report: before the workspace initializes the slot, and always for an unsaved doc (it has no server-backed identity to resolve).
13
+
14
+ ```typescript
15
+ import { askEventDocReadIdentity } from 'quidproquo-features';
16
+
17
+ export function* askTemplateBuildSelfLink() {
18
+ const identity = yield* askEventDocReadIdentity();
19
+ return identity ? { serviceName: identity.serviceName, basePath: identity.basePath, id: identity.id } : null;
20
+ }
21
+ ```
22
+
23
+ ## Signature
24
+
25
+ ```typescript
26
+ function* askEventDocReadIdentity(): AskResponse<Nullable<EventDocWorkspaceDocumentIdentity>>;
27
+ ```
28
+
29
+ ## Parameters
30
+
31
+ None — WHICH doc's identity to read is the answering processor's ambient context, the same as `askEventDocReadState`.
32
+
33
+ ## Returns
34
+
35
+ `AskResponse<Nullable<EventDocWorkspaceDocumentIdentity>>` — `{ serviceName, basePath, id }`, concretely typed since every doc's identity has the same shape. `null` until the doc is bound to a real document.
36
+
37
+ ## Notes
38
+
39
+ - This is the contract only — it carries no default behavior. A runtime that answers `askEventDocReadState` for a doc should also answer `askEventDocReadIdentity` for it.
40
+ - In the event-doc workspace, the binding registered for a slot answers with `state.slots[slotKey].documentIdentity`, so this reflects the same identity `askInit` seeded for the slot.
41
+
42
+ ## Related
43
+
44
+ - [askEventDocReadState](./ask-event-doc-read-state.md) — the content sibling: what the doc currently holds, rather than where it lives.
45
+ - [askApplyEventDocEvent](./ask-apply-event-doc-event.md) — the write action this doc's verbs typically pair with.
@@ -0,0 +1,46 @@
1
+ ---
2
+ title: askEventDocReadState
3
+ description: Declaratively read an event-doc's current folded state, leaving WHICH doc and HOW it is resolved to a registered processor.
4
+ ---
5
+
6
+ # askEventDocReadState
7
+
8
+ The read counterpart of [askApplyEventDocEvent](./ask-apply-event-doc-event.md): the story yields no payload at all, and a registered processor answers with the doc's current folded state. Because it has no target of its own, verbs written in terms of it (a domain's read-to-derive-a-write action creators) run unchanged wherever a processor for it is registered, resolving whichever doc the processor is bound to.
9
+
10
+ - **Action type:** `EventDocActionType.ReadState`
11
+ - **quidproquo-features ships no default processor** for this action. Calling it with no processor registered fails loudly, the same as an unbound [askApplyEventDocEvent](./ask-apply-event-doc-event.md).
12
+ - Returns `unknown` — the raw action can't know a slot's view type. Call it through a per-doc `createEventDocStateReader<TView>()` instead of directly, for a typed result.
13
+ - In the event-doc workspace, the binding registered for a slot answers with the doc's current memoized view (history + pending folded and migrated to latest), so a commit earlier in the same story is visible to a read that follows it.
14
+
15
+ ```typescript
16
+ import { askEventDocReadState } from 'quidproquo-features';
17
+
18
+ export function* askTemplateAppendLine(line: string) {
19
+ const template = (yield* askEventDocReadState()) as TemplateState;
20
+ yield* askApplyEventDocEvent<AppendLineEffect>(TemplateEffect.AppendLine, { line: template.body ? `${template.body}\n${line}` : line });
21
+ }
22
+ ```
23
+
24
+ ## Signature
25
+
26
+ ```typescript
27
+ function* askEventDocReadState(): AskResponse<unknown>;
28
+ ```
29
+
30
+ ## Parameters
31
+
32
+ None — WHICH doc to read is the answering processor's ambient context, the same as `askApplyEventDocEvent`'s target.
33
+
34
+ ## Returns
35
+
36
+ `AskResponse<unknown>` — the doc's current folded state, untyped at this layer. Use `createEventDocStateReader<TView>()` to mint a typed `askRead<Doc>()` for a specific doc instead of casting the result by hand at every call site.
37
+
38
+ ## Notes
39
+
40
+ - This is the contract only — it carries no default behavior. A runtime that answers `askApplyEventDocEvent` for a doc should also answer `askEventDocReadState` for it, so read-to-derive-a-write verbs work the same way commits do.
41
+ - `createEventDocDefinition` mints the doc's fold config and api together; pair it with a standalone `createEventDocStateReader<TView>()` for the doc's typed read verb.
42
+
43
+ ## Related
44
+
45
+ - [askApplyEventDocEvent](./ask-apply-event-doc-event.md) — the write counterpart this action mirrors.
46
+ - [askEventDocReadIdentity](./ask-event-doc-read-identity.md) — the address sibling: where the doc lives, rather than what it currently holds.
@@ -0,0 +1,50 @@
1
+ ---
2
+ title: askWebSocketQueueBroadcastMessage
3
+ description: Push a server-initiated message to every live connection on a WebSocket queue's API.
4
+ ---
5
+
6
+ # askWebSocketQueueBroadcastMessage
7
+
8
+ Sends a message to **every live connection** on a named [WebSocket queue](../../../config/features/web-socket-queue.md) API. Unlike a connection-scoped send, this does not need a websocket-triggered context — the caller names the `apiName` explicitly, so any story (a storage event, a queue handler, a cron job) can push to every connected frontend. Connections whose socket died without their `onDisconnect` firing are skipped and their stale record is cleaned up automatically; a single dead connection never aborts the broadcast.
9
+
10
+ ```typescript
11
+ import { askWebSocketQueueBroadcastMessage } from 'quidproquo-features';
12
+
13
+ interface Announcement {
14
+ type: 'Announcement';
15
+ payload: { text: string };
16
+ }
17
+
18
+ export function* askAnnounce(text: string) {
19
+ yield* askWebSocketQueueBroadcastMessage<Announcement>('api', {
20
+ type: 'Announcement',
21
+ payload: { text },
22
+ });
23
+ }
24
+ ```
25
+
26
+ ## Signature
27
+
28
+ ```typescript
29
+ function* askWebSocketQueueBroadcastMessage<E extends AnyWebSocketQueueEventMessage>(
30
+ websocketApiName: string,
31
+ message: E,
32
+ ): AskResponse<void>;
33
+ ```
34
+
35
+ ## Parameters
36
+
37
+ | Parameter | Type | Description |
38
+ | --- | --- | --- |
39
+ | `websocketApiName` | `string` | The `apiName` of the target [defineWebSocketQueue](../../../config/features/web-socket-queue.md) — every connection currently registered against this API receives the message. |
40
+ | `message` | `E extends AnyWebSocketQueueEventMessage` | The typed event message to send, e.g. `{ type, payload }`. |
41
+
42
+ ## Returns
43
+
44
+ `void`
45
+
46
+ ## Related
47
+
48
+ - [askWebSocketQueueBroadcastServiceUpdated](./ask-web-socket-queue-broadcast-service-updated.md) — a typed wrapper around this for the built-in `ServiceUpdated` message.
49
+ - [defineWebSocketQueue](../../../config/features/web-socket-queue.md) — declares the WebSocket queue API this broadcasts on.
50
+ - [askServiceRequest](./ask-service-request.md) — an RPC-style request/response over the same queue, addressed to one service rather than broadcast to every connection.
@@ -0,0 +1,41 @@
1
+ ---
2
+ title: askWebSocketQueueBroadcastServiceUpdated
3
+ description: Tell every connected frontend that a service's deployed artifacts changed, so it can offer a module reload.
4
+ ---
5
+
6
+ # askWebSocketQueueBroadcastServiceUpdated
7
+
8
+ Broadcasts a `ServiceUpdated` message to every live connection on a [WebSocket queue](../../../config/features/web-socket-queue.md) API, naming the service whose deployed artifacts changed (for example, a new views bundle going live). A typed wrapper around [askWebSocketQueueBroadcastMessage](./ask-web-socket-queue-broadcast-message.md) for this one built-in message type.
9
+
10
+ ```typescript
11
+ import { askWebSocketQueueBroadcastServiceUpdated } from 'quidproquo-features';
12
+
13
+ export function* askNotifyDeploy(serviceName: string) {
14
+ yield* askWebSocketQueueBroadcastServiceUpdated('api', serviceName);
15
+ }
16
+ ```
17
+
18
+ ## Signature
19
+
20
+ ```typescript
21
+ function* askWebSocketQueueBroadcastServiceUpdated(
22
+ websocketApiName: string,
23
+ serviceName: string,
24
+ ): AskResponse<void>;
25
+ ```
26
+
27
+ ## Parameters
28
+
29
+ | Parameter | Type | Description |
30
+ | --- | --- | --- |
31
+ | `websocketApiName` | `string` | The `apiName` of the target [defineWebSocketQueue](../../../config/features/web-socket-queue.md) — every connection currently registered against this API receives the message. |
32
+ | `serviceName` | `string` | The qpq service (module) whose deployed artifacts changed. Sent as `payload.serviceName` on a `WebSocketQueueServerMessageEventType.ServiceUpdated` message. |
33
+
34
+ ## Returns
35
+
36
+ `void`
37
+
38
+ ## Related
39
+
40
+ - [askWebSocketQueueBroadcastMessage](./ask-web-socket-queue-broadcast-message.md) — the untyped broadcast this wraps.
41
+ - [defineWebSocketQueue](../../../config/features/web-socket-queue.md) — declares the WebSocket queue API this broadcasts on.
@@ -0,0 +1 @@
1
+ { "label": "Email", "link": { "type": "generated-index", "description": "Actions for sending email from a service." } }
@@ -0,0 +1,85 @@
1
+ ---
2
+ title: askEmailSendEmail
3
+ description: Send an email from a domain declared with defineEmailSender.
4
+ ---
5
+
6
+ # askEmailSendEmail
7
+
8
+ Sends an email. The `from` address must be under a domain declared with [defineEmailSender](../../../config/webserver/email-sender.md) — the deploy grants the service's role permission to send only from its own verified identity domains.
9
+
10
+ - **Action type:** `EmailActionType.SendEmail`
11
+ - **On AWS:** sent through **SES v2** (`SendEmailCommand`). `bcc` addresses are only ever placed in the SES `Destination`, never written into a message header, so other recipients can't see them. When `attachments` are provided the message is built as a raw MIME email (multipart/mixed wrapping a multipart/alternative body); otherwise it's sent as SES "Simple" content. While the SES account is in sandbox mode, recipients also need to be listed with [defineEmailSenderAllowList](../../../config/config-aws/email-sender-allow-list.md) and verified in the SES console.
12
+
13
+ ```typescript
14
+ import { askEmailSendEmail } from 'quidproquo-webserver';
15
+
16
+ export function* askSendWelcomeEmail(to: string) {
17
+ const messageId = yield* askEmailSendEmail({
18
+ from: 'noreply@example.com',
19
+ to: [to],
20
+ subject: 'Welcome!',
21
+ bodyText: 'Thanks for signing up.',
22
+ bodyHtml: '<p>Thanks for signing up.</p>',
23
+ });
24
+
25
+ return messageId;
26
+ }
27
+ ```
28
+
29
+ ## Signature
30
+
31
+ ```typescript
32
+ function* askEmailSendEmail(payload: EmailSendEmailActionPayload): AskResponse<string>;
33
+ ```
34
+
35
+ ## Parameters
36
+
37
+ ### `payload` — `EmailSendEmailActionPayload` (required)
38
+
39
+ | Property | Type | Description |
40
+ | --- | --- | --- |
41
+ | `from` | `string` | Sender address. Must be under a domain declared with `defineEmailSender`. |
42
+ | `to` | `string[]` | Recipient addresses. |
43
+ | `cc` | `string[]` (optional) | Addresses copied on the message. |
44
+ | `bcc` | `string[]` (optional) | Addresses blind-copied on the message. Never written into a message header, on AWS or otherwise. |
45
+ | `replyTo` | `string[]` (optional) | Addresses to route replies to. |
46
+ | `subject` | `string` | Message subject. |
47
+ | `bodyText` | `string` (optional) | Plain-text body. At least one of `bodyText` / `bodyHtml` is required. |
48
+ | `bodyHtml` | `string` (optional) | HTML body. At least one of `bodyText` / `bodyHtml` is required. |
49
+ | `attachments` | `QPQBinaryData[]` (optional) | Files to attach, each `{ base64Data, filename, mimetype?, contentDisposition? }`. Presence of any attachment switches the message to the raw MIME send path. |
50
+
51
+ ## Returns
52
+
53
+ `string` — the provider's message id for the sent email (SES's `MessageId`), or `''` if the provider didn't return one.
54
+
55
+ ## Errors
56
+
57
+ | Error | Meaning |
58
+ | --- | --- |
59
+ | `EmailSendEmailErrorTypeEnum.MessageRejected` | SES rejected the message content. |
60
+ | `EmailSendEmailErrorTypeEnum.SenderNotVerified` | The `from` domain isn't a verified SES identity (AWS `MailFromDomainNotVerifiedException`). |
61
+ | `EmailSendEmailErrorTypeEnum.AccountSuspended` | The SES account is suspended (AWS `AccountSuspendedException`). |
62
+ | `EmailSendEmailErrorTypeEnum.SendingPaused` | Sending is paused for the account (AWS `SendingPausedException`). |
63
+ | `EmailSendEmailErrorTypeEnum.Throttled` | The send rate was exceeded (AWS `TooManyRequestsException`). |
64
+ | `EmailSendEmailErrorTypeEnum.LimitExceeded` | A sending limit was exceeded (AWS `LimitExceededException`). |
65
+ | `EmailSendEmailErrorTypeEnum.BadRequest` | The request was otherwise invalid (AWS `BadRequestException`). |
66
+
67
+ Wrap the call in [askCatch](../../../actions/core/system/ask-catch.md) to handle these without unwinding the story:
68
+
69
+ ```typescript
70
+ import { askCatch } from 'quidproquo-core';
71
+ import { askEmailSendEmail, EmailSendEmailErrorTypeEnum } from 'quidproquo-webserver';
72
+
73
+ export function* askTrySendEmail(payload) {
74
+ const outcome = yield* askCatch(askEmailSendEmail(payload));
75
+ if (!outcome.success && outcome.error.errorType === EmailSendEmailErrorTypeEnum.Throttled) {
76
+ // back off and retry later
77
+ }
78
+ }
79
+ ```
80
+
81
+ ## Related
82
+
83
+ - [defineEmailSender](../../../config/webserver/email-sender.md) — declares the verified sending domain this action requires.
84
+ - [defineEmailSenderAllowList](../../../config/config-aws/email-sender-allow-list.md) — recipient addresses allowed while the SES account is in sandbox mode.
85
+ - [askCatch](../../../actions/core/system/ask-catch.md) — catch the errors above.
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: defineEmailSenderAllowList
3
+ description: Recipient addresses a service is allowed to email while its SES account is in sandbox mode.
4
+ ---
5
+
6
+ # defineEmailSenderAllowList
7
+
8
+ Declares recipient addresses a service is allowed to send to while its SES account is still in **sandbox mode**. In sandbox, SES authorizes a send against the recipient's identity as well as the sender's, so [defineEmailSender](../webserver/email-sender.md)'s exact-ARN send grant needs each recipient identity listed too. The addresses must also be verified identities in the SES console (sandbox requires that anyway).
9
+
10
+ This is an AWS-specific concession, not a portable email concept, which is why it lives in `quidproquo-config-aws` keyed by the same `rootDomain` as `defineEmailSender` rather than on that webserver setting directly. Once the account has SES production access this setting does nothing useful and can be deleted.
11
+
12
+ ```typescript
13
+ import { defineEmailSender } from 'quidproquo-webserver';
14
+ import { defineEmailSenderAllowList } from 'quidproquo-config-aws';
15
+
16
+ export default [
17
+ defineEmailSender('example.com'),
18
+
19
+ defineEmailSenderAllowList('example.com', ['joe@external.com', 'test@external.com']),
20
+ ];
21
+ ```
22
+
23
+ ## Signature
24
+
25
+ ```typescript
26
+ function defineEmailSenderAllowList(
27
+ rootDomain: string,
28
+ allowedEmailAddresses: string[],
29
+ ): EmailSenderAllowListQPQConfigSetting;
30
+ ```
31
+
32
+ ## Parameters
33
+
34
+ ### `rootDomain` — `string` (required)
35
+
36
+ The `rootDomain` of the matching [defineEmailSender](../webserver/email-sender.md) this allow-list applies to.
37
+
38
+ ### `allowedEmailAddresses` — `string[]` (required)
39
+
40
+ Recipient addresses to grant sandbox send access to. Multiple calls for the same `rootDomain` are additive.
41
+
42
+ ## Related
43
+
44
+ - [defineEmailSender](../webserver/email-sender.md) — the sending domain this allow-list is keyed by.
45
+ - [askEmailSendEmail](../../actions/webserver/email/ask-email-send-email.md) — the action these addresses need to be reachable from.
@@ -54,6 +54,7 @@ export interface QPQConfigAdvancedLogSettings extends QPQConfigAdvancedSettings
54
54
  logRetentionDays?: number;
55
55
  coldStorageAfterDays?: number;
56
56
  services?: string[];
57
+ maintenanceWebsocketApiName?: string;
57
58
  }
58
59
  ```
59
60
 
@@ -62,6 +63,7 @@ export interface QPQConfigAdvancedLogSettings extends QPQConfigAdvancedSettings
62
63
  | `logRetentionDays` | `number` | – (no expiry) | How many days to keep the raw log objects on the logs storage drive before they are deleted. When unset, logs never expire. If `coldStorageAfterDays` is set, the effective retention is padded so that it is at least `coldStorageAfterDays + 180` days — logs always outlive the cold-storage transition window. |
63
64
  | `coldStorageAfterDays` | `number` | – (no transition) | When greater than 0, adds a lifecycle rule transitioning log objects to the `DEEP_COLD_STORAGE` tier after this many days, to cut storage cost for old logs. |
64
65
  | `services` | `string[]` | `[]` | The list of service names the admin dashboard should surface, exposed as the `qpq-serviceNames` global. Returned by the `/admin/services` route. |
66
+ | `maintenanceWebsocketApiName` | `string` | `''` | Name of the **application** [WebSocket queue](./web-socket-queue.md) (its `apiName`, not the admin's own socket) that maintenance state broadcasts on. Unset means maintenance mutations skip broadcasting. |
65
67
  | `deprecated` | `boolean` | `false` | Inherited from `QPQConfigAdvancedSettings`; marks the log storage drives and key-value stores as deprecated. |
66
68
 
67
69
  ## What it declares
@@ -72,6 +74,7 @@ export interface QPQConfigAdvancedLogSettings extends QPQConfigAdvancedSettings
72
74
  - **Log indexes** — key-value stores for correlations (indexed by `runtimeType` and `fromCorrelation`, both sorted by `startedAt`, with a `ttl` attribute), log messages, and a log list.
73
75
  - **Auth routes** — `POST /login`, `POST /refreshToken`, `POST /challenge`, authenticated against the [admin user directory](./admin-user-directory.md).
74
76
  - **Log routes** — `GET /admin/services`, `POST /log/list`, `GET /log/{correlationId}`, its `/toggle`, `/children`, `/hierarchies`, `/downloadurl`, and the log-log list — all (except the service list) requiring an admin token.
77
+ - **Maintenance collection** — an [event-doc collection](./event-doc.md) (`defineEventDoc`) mounted at `/maintenance`, admin-authenticated, storing maintenance windows as an append-only update log. Every append re-broadcasts the active maintenance set to every connection on the `maintenanceWebsocketApiName` application WebSocket queue (via its `onAppend` hook); a newly-opened connection gets the same set pre-auth through that queue's `onConnected` sync.
75
78
  - **Log chat** — a `defineEventDocAi` instance scoped to `docId` = log correlation id, with two tools (`getLogActions`, `getLogAction`) instead of the log's JSON pasted into the prompt. Runs on `AiModel.ClaudeSonnet46` over the same admin WebSocket connection, not a separate HTTP route.
76
79
  - **WebSocket sync** — an admin event bus (`qpq-admin-wsq`), a `defineWebSocketQueue`, and queues that fan out client messages (config sync, mark-log-checked, refresh-metadata) using `WebsocketAdminClientMessageEventType`.
77
80
  - **Alarms** — a `defineNotifyError` publishing to an `admin-notifier` event bus, with a queue handling its Error / Timeout / Throttle events.
@@ -100,4 +103,6 @@ export default [
100
103
  - [defineAdminUserDirectory](./admin-user-directory.md) — the directory admins authenticate against; declare it alongside these settings.
101
104
  - [defineAdminSessionEventDoc](./admin-session-event-doc.md) — the per-login admin session document, spread in by this define.
102
105
  - [askAdminGetLogs](../../actions/webserver/admin/ask-admin-get-logs.md), [askAdminGetLog](../../actions/webserver/admin/ask-admin-get-log.md), [askAdminGetLogMetadata](../../actions/webserver/admin/ask-admin-get-log-metadata.md) — the actions the admin log routes are built from.
106
+ - [defineEventDoc](./event-doc.md) — the helper the maintenance collection is declared with.
107
+ - [defineWebSocketQueue](./web-socket-queue.md) — declares the application WebSocket queue named by `maintenanceWebsocketApiName`, including its `onConnected` pre-auth sync.
103
108
  - [defineStorageDrive](../core/storage-drive.md), [defineKeyValueStore](../core/key-value-store.md), [defineQueue](../core/queue.md), [defineEventBus](../core/event-bus.md) — the core settings this feature composes.
@@ -61,6 +61,7 @@ The single `options` argument is an `EventDocRoutesOptions`:
61
61
  | `eventValidator` | `string` | – | Name of a registered inline function (see `defineInlineFunction`). When set, every append invokes it with `{ event, events }` to reject lifecycle- or payload-invalid events before they reach the log. The frontend editor runs the same rule for instant feedback. |
62
62
  | `eventRenderer` | `string` | – | Name of a registered inline function. When set, a `GET {basePath}/{id}/render` route is mounted; it invokes the renderer with the document's full `{ events }` log, which folds + renders to HTML. |
63
63
  | `onPublish` | `string` | – | Name of a registered inline function. When set, every successful append of a Publish event invokes it with `{ docId, event, summary }`, after the event is durably written and the summary re-derived. This is the seam for syncing a folded document into a materialized read model. Errors propagate to the caller: the event has landed but the side effect did not, so the caller learns the read model may be stale. |
64
+ | `onAppend` | `string` | – | Name of a registered inline function. When set, EVERY successful append (domain events and lifecycle events alike) invokes it with `{ docId, event, summary, events }`, after the event is durably written and the summary re-derived. This is the seam for reacting to any mutation (e.g. broadcasting the doc's fresh fold). Runs after `onPublish` when both fire on the same Publish event. Errors propagate to the caller: the event has landed but the side effect did not. |
64
65
  | `scopeResolver` | `string` | – | Name of a registered inline function. When set, every route invokes it with `{ event }` before running; a non-null result becomes the ambient storage scope for the whole request, transparently partitioning the collection's stores and assets (e.g. per-tenant). Null means unscoped. Omit for collections that never partition. |
65
66
  | `excludeRoutes` | `EventDocRouteName[]` | `[]` | Route names to leave unmounted (`'list' \| 'get' \| 'listEvents' \| 'render' \| 'create' \| 'appendEvent' \| 'createAsset' \| 'getAsset' \| 'remove'`). For a collection that must own one of these itself instead of using the stock behavior — e.g. a `create` that must also perform some side effect the stock controller doesn't know about. |
66
67
 
@@ -48,6 +48,7 @@ function defineEventDoc(options: EventDocRoutesOptions): QPQConfig;
48
48
  | `eventValidator` | `string` | no | Inline-function name run on every append to reject invalid events. |
49
49
  | `eventRenderer` | `string` | no | Inline-function name that folds + renders the log to HTML; mounting it adds a `GET {basePath}/{id}/render` route. |
50
50
  | `onPublish` | `string` | no | Inline-function name invoked with `{ docId, event, summary }` after every successful Publish append: the seam for syncing the folded document into a materialized read model. |
51
+ | `onAppend` | `string` | no | Inline-function name invoked with `{ docId, event, summary, events }` after EVERY successful append (domain events and lifecycle events alike): the seam for reacting to any mutation. Runs after `onPublish` when both fire on the same Publish event. |
51
52
  | `scopeResolver` | `string` | no | Inline-function name every route invokes with `{ event }` to resolve the request's ambient storage scope (e.g. per-tenant); null means unscoped. |
52
53
 
53
54
  ## Examples
@@ -68,6 +68,7 @@ export interface QPQConfigAdvancedWebsocketQueueSettings extends QPQConfigAdvanc
68
68
  owner?: CrossModuleOwnerWithNoResourceOverride;
69
69
  userDirectoryName?: string;
70
70
  connectionScopeResolver?: string;
71
+ onConnected?: string;
71
72
  }
72
73
  ```
73
74
 
@@ -75,6 +76,7 @@ export interface QPQConfigAdvancedWebsocketQueueSettings extends QPQConfigAdvanc
75
76
  | --- | --- | --- | --- |
76
77
  | `userDirectoryName` | `string` | `''` | Name of the [user directory](../core/user-directory.md) used to authenticate clients (stored in the `qpq-wsq-kvs-name-<apiName>` global). Enables the authenticate client/server message flow. |
77
78
  | `connectionScopeResolver` | `string` | `''` | Name of a registered [inline function](../core/inline-function.md) invoked with `{ userId, requestedScope }` on **every** Authenticate message, whether or not it claims a storage scope (stored in the `qpq-wsq-scope-resolver-<apiName>` global). It returns the effective scope to store on the connection (e.g. a membership-checked tenant scope for a claim, or the user's own scope for no claim), or throws to reject the authenticate. A scope claim with no resolver configured is rejected outright, leaving the connection unauthenticated. |
79
+ | `onConnected` | `string` | `''` | Name of a registered [inline function](../core/inline-function.md) invoked with `{ connectionId }` as soon as a connection opens, **before** any authenticate — so it must only push public state (e.g. broadcasting an already-active maintenance notice to a pre-login client). Runs in the websocket-owning service; failures are swallowed so a sync problem never breaks the connect. |
78
80
  | `owner` | `CrossModuleOwnerWithNoResourceOverride` | – | Declares the queue's resources as owned by another module/service, so this service references them instead of deploying its own. Applied to both the WebSocket API and the connection store. |
79
81
  | `deprecated` | `boolean` | `false` | Inherited from `QPQConfigAdvancedSettings`; when set, the underlying WebSocket API is not deployed. |
80
82
 
@@ -88,4 +90,5 @@ A `QPQConfig` array containing: three globals, the connection [key-value store](
88
90
  - [defineStateDispatchOverWebsockets](./state-dispatch-over-websockets.md) — routes core State-domain dispatches to clients through this queue.
89
91
  - [defineTenantedWebSocketQueue](./tenanted-web-socket-queue.md) — this define with the tenant connection-scope resolver pre-wired.
90
92
  - [askWebsocketSendMessage](../../actions/webserver/websocket/ask-websocket-send-message.md) — send a raw message to a connection on the queue's API.
93
+ - [askWebSocketQueueBroadcastMessage](../../actions/features/web-socket-queue/ask-web-socket-queue-broadcast-message.md), [askWebSocketQueueBroadcastServiceUpdated](../../actions/features/web-socket-queue/ask-web-socket-queue-broadcast-service-updated.md) — push a server-initiated message to every live connection on this queue's API.
91
94
  - [defineEventBus](../core/event-bus.md), [defineKeyValueStore](../core/key-value-store.md), [defineUserDirectory](../core/user-directory.md) — the underlying resources it depends on.
@@ -0,0 +1,43 @@
1
+ ---
2
+ title: defineEmailSender
3
+ description: Declare a domain a service is allowed to send email from.
4
+ ---
5
+
6
+ # defineEmailSender
7
+
8
+ Declares a domain the service can send email from with [askEmailSendEmail](../../actions/webserver/email/ask-email-send-email.md). The domain must live under a base declared with [defineDns](./dns.md) — deploy resolves it the same way (environment/feature prefixed) before creating the sending identity.
9
+
10
+ - **On AWS:** creates an SES `EmailIdentity` for the resolved domain in its Route53 hosted zone (DKIM records land there automatically), and scopes the service role's `ses:SendEmail` / `ses:SendRawEmail` grant to that identity's exact ARN. `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
+
12
+ ```typescript
13
+ import { defineDns, defineEmailSender } from 'quidproquo-webserver';
14
+
15
+ export default [
16
+ defineDns('example.com'),
17
+ defineEmailSender('example.com'),
18
+ ];
19
+ ```
20
+
21
+ ## Signature
22
+
23
+ ```typescript
24
+ function defineEmailSender(
25
+ rootDomain: string,
26
+ ): EmailSenderQPQWebServerConfigSetting;
27
+ ```
28
+
29
+ ## Parameters
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.
34
+
35
+ ## Returns
36
+
37
+ An `EmailSenderQPQWebServerConfigSetting` config entry. Deploy reads every declared entry with `qpqWebServerUtils.getEmailSenderSettings` to create the SES identities and IAM grants.
38
+
39
+ ## Related
40
+
41
+ - [askEmailSendEmail](../../actions/webserver/email/ask-email-send-email.md) — send email from a domain declared here.
42
+ - [defineDns](./dns.md) — the base domain this sender's `rootDomain` resolves against.
43
+ - [defineEmailSenderAllowList](../config-aws/email-sender-allow-list.md) — recipient addresses allowed while the SES account is in sandbox mode.
@@ -32,7 +32,6 @@
32
32
  "@chakra-ui/cli": "^3.0.0",
33
33
  "@rspack/core": "^2.1.2",
34
34
  "@sparticuz/chromium": "^112.0.2",
35
- "@types/adm-zip": "^0.5.0",
36
35
  "@types/jsonwebtoken": "^9.0.4",
37
36
  "@types/lodash": "^4.14.191",
38
37
  "@types/luxon": "^3.4.2",
@@ -62,10 +61,9 @@
62
61
  "@types/fs-extra": "^11.0.1",
63
62
  "@types/glob": "^8.1.0",
64
63
  "@xyflow/react": "^12.10.0",
65
- "adm-zip": "^0.5.10",
66
- "aws-cdk-lib": "^2.210.0",
64
+ "aws-cdk-lib": "^2.262.0",
67
65
  "chakra-react-select": "^6.1.3",
68
- "constructs": "^10.1.164",
66
+ "constructs": "^10.7.1",
69
67
  "core-js": "^3.36.1",
70
68
  "dotenv": "^16.0.3",
71
69
  "jsonata": "^2.2.1",