create-qpq-app 0.1.9 → 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.
Files changed (36) hide show
  1. package/package.json +2 -2
  2. package/template/apps/qpqjs/deploy.config.json +2 -2
  3. package/template/apps/qpqjs/services/design/views/src/components/LandingPage/components/NavBar.tsx +5 -17
  4. package/template/apps/qpqjs/services/design/views/src/components/LandingPage/components/Packages.tsx +1 -2
  5. package/template/apps/qpqjs/services/design/views/src/components/LandingPage/landing.css +44 -0
  6. package/template/apps/qpqjs/services/shell/views/src/index.html +1 -1
  7. package/template/docusaurus/docs/actions/core/file/ask-file-generate-temporary-upload-secure-url.md +3 -1
  8. package/template/docusaurus/docs/actions/core/user-directory/ask-user-directory-revoke-refresh-token.md +56 -0
  9. package/template/docusaurus/docs/actions/core/user-directory/ask-user-directory-sign-out-user.md +52 -0
  10. package/template/docusaurus/docs/actions/features/event-doc/ask-apply-event-doc-event.md +6 -4
  11. package/template/docusaurus/docs/actions/features/event-doc/ask-apply-transient-event-doc-event.md +53 -0
  12. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-generate-asset-upload-url.md +3 -1
  13. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-read-identity.md +45 -0
  14. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-read-state.md +46 -0
  15. package/template/docusaurus/docs/actions/features/web-socket-queue/ask-web-socket-queue-broadcast-message.md +50 -0
  16. package/template/docusaurus/docs/actions/features/web-socket-queue/ask-web-socket-queue-broadcast-service-updated.md +41 -0
  17. package/template/docusaurus/docs/actions/webserver/email/_category_.json +1 -0
  18. package/template/docusaurus/docs/actions/webserver/email/ask-email-send-email.md +85 -0
  19. package/template/docusaurus/docs/actions/webserver/service-function/ask-service-function-execute.md +19 -13
  20. package/template/docusaurus/docs/config/config-aws/email-sender-allow-list.md +45 -0
  21. package/template/docusaurus/docs/config/core/bundle-options.md +1 -1
  22. package/template/docusaurus/docs/config/core/user-directory.md +3 -1
  23. package/template/docusaurus/docs/config/features/admin-settings.md +5 -0
  24. package/template/docusaurus/docs/config/features/event-doc-routes.md +1 -0
  25. package/template/docusaurus/docs/config/features/event-doc.md +1 -0
  26. package/template/docusaurus/docs/config/features/web-socket-queue.md +4 -1
  27. package/template/docusaurus/docs/config/webserver/auth-system.md +9 -1
  28. package/template/docusaurus/docs/config/webserver/email-sender.md +43 -0
  29. package/template/docusaurus/docs/core-concepts.md +35 -1
  30. package/template/docusaurus/docs/eslint-plugin.md +86 -0
  31. package/template/docusaurus/docs/getting-started.md +3 -1
  32. package/template/docusaurus/sidebars.ts +1 -0
  33. package/template/docusaurus/src/css/custom.css +44 -0
  34. package/template/docusaurus/static/img/favicon.svg +6 -2
  35. package/template/docusaurus/static/img/qpq-logo.svg +4 -3
  36. package/template/package.json +2 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-qpq-app",
3
- "version": "0.1.9",
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.9"
54
+ "quidproquo-tsconfig": "0.1.11"
55
55
  },
56
56
  "bin": {
57
57
  "create-qpq-app": "./lib/commonjs/bin/createQpqApp.js"
@@ -9,13 +9,13 @@
9
9
  },
10
10
  "staging": {
11
11
  "platform": "aws",
12
- "accountId": "761018864142",
12
+ "accountId": "061039804449",
13
13
  "region": "us-east-1"
14
14
  },
15
15
  "production": {
16
16
  "platform": "aws",
17
17
  "accountId": "761018864142",
18
- "region": "us-east-1"
18
+ "region": "ap-southeast-2"
19
19
  },
20
20
  "local": {
21
21
  "platform": "docker"
@@ -15,25 +15,13 @@ export function NavBar() {
15
15
  <header className="nav">
16
16
  <div className="nav__inner">
17
17
  <a className="nav__brand" href="#top">
18
- <svg aria-hidden="true" className="nav__mark" viewBox="0 0 32 32">
19
- <rect
20
- fill="rgba(34,211,238,0.08)"
21
- height="26"
22
- rx="7"
23
- stroke="#22d3ee"
24
- strokeWidth="1.6"
25
- width="26"
26
- x="3"
27
- y="3"
28
- />
18
+ <svg aria-hidden="true" className="nav__mark" viewBox="24.4 24.4 465.3 465.3">
29
19
  <path
30
- d="M12 9l-4 7 4 7M20 9l4 7-4 7"
31
- fill="none"
32
- stroke="#8df6ff"
33
- strokeLinecap="round"
34
- strokeLinejoin="round"
35
- strokeWidth="2.2"
20
+ d="M271.42,489.64h-10.72c-128.68,0-235.74-102.93-236.29-231.61S128.97,23.86,258.03,24.41c128.68.55,231.6,107.61,231.6,236.29v11.61c0,.09-.07.16-.16.16h-109.69c-.09,0-.16-.07-.16-.16v-12.61c0-65.9-53.82-123.71-119.7-125.24-69.89-1.62-127.08,55.57-125.46,125.46,1.53,65.89,59.34,119.71,125.24,119.71h11.72c.09,0,.16.07.16.16v109.69c0,.09-.07.16-.16.16Z"
21
+ fill="#22d3ee"
36
22
  />
23
+ <rect fill="#8df6ff" height="106.91" width="106.91" x="268.31" y="273.22" />
24
+ <rect fill="#8df6ff" height="106.91" width="106.91" x="375.62" y="380.53" />
37
25
  </svg>
38
26
  <span className="nav__name">
39
27
  quid<span className="nav__name-accent">pro</span>quo
@@ -40,8 +40,7 @@ export function Packages() {
40
40
  <p className="section__kicker">the grid</p>
41
41
  <h2 className="section__title">A package for every layer</h2>
42
42
  <p className="section__sub">
43
- Core defines the actions. Everything else is an implementation you plug
44
- in.
43
+ Every layer is its own package, so you only pull in what you need.
45
44
  </p>
46
45
 
47
46
  <div className="package-groups">
@@ -355,6 +355,7 @@ em {
355
355
  }
356
356
 
357
357
  .nav__name {
358
+ position: relative;
358
359
  font-family: var(--font-display);
359
360
  font-size: 19px;
360
361
  font-weight: 600;
@@ -366,6 +367,49 @@ em {
366
367
  color: var(--cyan);
367
368
  }
368
369
 
370
+ /* Glint overlay: the same glyphs stacked over the title, transparent except
371
+ for a narrow band clipped to the letterforms. It rests off-canvas, then
372
+ makes one eased sweep across per cycle. The band is two-tone so it reads
373
+ on both letter colors: the white half is invisible on the near-white
374
+ quid/quo but lights up the cyan pro, and the cyan half does the reverse.
375
+ The "/ ''" marks the duplicated text as decorative; browsers without that
376
+ syntax skip the overlay entirely, which just means no shimmer. */
377
+ .nav__name::after {
378
+ content: 'quidproquo' / '';
379
+ position: absolute;
380
+ inset: 0;
381
+ color: transparent;
382
+ background-image: linear-gradient(
383
+ 105deg,
384
+ rgba(255, 255, 255, 0) 38%,
385
+ rgba(255, 255, 255, 0.85) 45%,
386
+ rgba(34, 211, 238, 0.85) 53%,
387
+ rgba(34, 211, 238, 0) 60%
388
+ );
389
+ background-size: 250% 100%;
390
+ background-repeat: no-repeat;
391
+ background-position: 130% 0;
392
+ -webkit-background-clip: text;
393
+ background-clip: text;
394
+ animation: nav-name-shine 7s ease-in-out infinite;
395
+ }
396
+
397
+ @keyframes nav-name-shine {
398
+ 0%,
399
+ 72% {
400
+ background-position: 130% 0;
401
+ }
402
+ 100% {
403
+ background-position: -130% 0;
404
+ }
405
+ }
406
+
407
+ @media (prefers-reduced-motion: reduce) {
408
+ .nav__name::after {
409
+ animation: none;
410
+ }
411
+ }
412
+
369
413
  .nav__links {
370
414
  display: flex;
371
415
  gap: 26px;
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <link
6
6
  rel="icon"
7
- href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='3' y='3' width='26' height='26' rx='6' fill='%23071018' stroke='%2322d3ee' stroke-width='2'/%3E%3Cpath d='M12 9l-4 7 4 7M20 9l4 7-4 7' stroke='%237ff4ff' stroke-width='2.4' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"
7
+ href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='1' y='1' width='30' height='30' rx='6' fill='%23071018'/%3E%3Cg transform='translate(4.5 4.5) scale(0.0494) translate(-24.4 -24.4)'%3E%3Cpath fill='%2322d3ee' d='M271.42,489.64h-10.72c-128.68,0-235.74-102.93-236.29-231.61S128.97,23.86,258.03,24.41c128.68.55,231.6,107.61,231.6,236.29v11.61c0,.09-.07.16-.16.16h-109.69c-.09,0-.16-.07-.16-.16v-12.61c0-65.9-53.82-123.71-119.7-125.24-69.89-1.62-127.08,55.57-125.46,125.46,1.53,65.89,59.34,119.71,125.24,119.71h11.72c.09,0,.16.07.16.16v109.69c0,.09-.07.16-.16.16Z'/%3E%3Crect fill='%238df6ff' x='268.31' y='273.22' width='106.91' height='106.91'/%3E%3Crect fill='%238df6ff' x='375.62' y='380.53' width='106.91' height='106.91'/%3E%3C/g%3E%3C/svg%3E"
8
8
  />
9
9
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10
10
  <meta
@@ -8,7 +8,7 @@ description: Generate a time-limited, signed URL that lets a client upload a fil
8
8
  Generates a temporary, signed URL that lets a client **upload** a single file directly to a [storage drive](../../../config/core/storage-drive.md). The bytes go straight to storage instead of through your service, which is ideal for large uploads from a browser. The URL grants write access to just that one path and stops working once it expires.
9
9
 
10
10
  - **Action type:** `FileActionType.GenerateTemporaryUploadSecureUrl`
11
- - **On AWS:** returns a SigV4 presigned S3 `PutObject` URL for the object. The expiry cannot exceed **7 days** — the maximum lifetime of a SigV4 presigned URL. If `contentType` is supplied it is bound into the signature, so the upload's `Content-Type` header must match.
11
+ - **On AWS:** returns a SigV4 presigned S3 `PutObject` URL for the object. The expiry cannot exceed **7 days** — the maximum lifetime of a SigV4 presigned URL. If `contentType` and/or `contentDisposition` are supplied they are bound into the signature, so the upload's `Content-Type` / `Content-Disposition` headers must match, and S3 stores them so later downloads serve the object with the same headers.
12
12
 
13
13
  ```typescript
14
14
  import { askFileGenerateTemporaryUploadSecureUrl } from 'quidproquo-core';
@@ -34,6 +34,7 @@ function* askFileGenerateTemporaryUploadSecureUrl(
34
34
  expirationMs: number,
35
35
  advancedOptions?: {
36
36
  contentType?: string;
37
+ contentDisposition?: string;
37
38
  },
38
39
  scope?: string,
39
40
  ): AskResponse<string>;
@@ -54,6 +55,7 @@ function* askFileGenerateTemporaryUploadSecureUrl(
54
55
  | Property | Type | Default | Description |
55
56
  | --- | --- | --- | --- |
56
57
  | `contentType` | `string` | – | Pins the `Content-Type` of the upload into the signature. When set, the client must send a matching `Content-Type` header or the `PUT` is rejected. |
58
+ | `contentDisposition` | `string` | – | Pins the `Content-Disposition` of the upload into the signature (e.g. `inline` so a stored PDF previews in an `<iframe>` instead of force-downloading). When set, the client must send a matching `Content-Disposition` header, and S3 stores it so later downloads serve it back. |
57
59
 
58
60
  ## Returns
59
61
 
@@ -0,0 +1,56 @@
1
+ ---
2
+ title: askUserDirectoryRevokeRefreshToken
3
+ description: Revoke a single refresh token — the "log out this device" primitive.
4
+ ---
5
+
6
+ # askUserDirectoryRevokeRefreshToken
7
+
8
+ Revokes a single refresh token (and every access token minted from it) against a [user directory](../../../config/core/user-directory.md). This is the "log out this device" primitive: other sessions belonging to the same user, holding different refresh tokens, are unaffected.
9
+
10
+ - **Action type:** `UserDirectoryActionType.RevokeRefreshToken`
11
+ - **On AWS:** issues Cognito `RevokeToken`. Requires token revocation to be enabled on the app client (the CDK default); a disabled client surfaces as an unmapped error.
12
+
13
+ ```typescript
14
+ import { askUserDirectoryRevokeRefreshToken } from 'quidproquo-core';
15
+
16
+ export function* askLogOutThisDevice(refreshToken: string) {
17
+ yield* askUserDirectoryRevokeRefreshToken('app-users', refreshToken);
18
+ }
19
+ ```
20
+
21
+ ## Signature
22
+
23
+ ```typescript
24
+ function* askUserDirectoryRevokeRefreshToken(
25
+ userDirectoryName: string,
26
+ refreshToken: string,
27
+ ): AskResponse<void>;
28
+ ```
29
+
30
+ ## Parameters
31
+
32
+ | Parameter | Type | Description |
33
+ | --- | --- | --- |
34
+ | `userDirectoryName` | `string` | Name of the directory — must match a directory declared with [defineUserDirectory](../../../config/core/user-directory.md). |
35
+ | `refreshToken` | `string` | The refresh token to revoke (from `AuthenticationInfo.refreshToken`). |
36
+
37
+ ## Returns
38
+
39
+ `void` — the story resumes once the token has been revoked.
40
+
41
+ ## Errors
42
+
43
+ | Error | Meaning |
44
+ | --- | --- |
45
+ | `UserDirectoryRevokeRefreshTokenErrorTypeEnum.Unauthorized` | The refresh token is invalid or already revoked, or revocation is disabled on the app client. |
46
+ | `UserDirectoryRevokeRefreshTokenErrorTypeEnum.LimitExceeded` | Too many attempts; back off and retry later. |
47
+
48
+ ## Notes
49
+
50
+ Access tokens already minted from the revoked refresh token remain valid until they expire — they're stateless JWTs and can't be individually invalidated. Keep `accessTokenValidityMinutes` (see [defineUserDirectory](../../../config/core/user-directory.md#parameters)) short to shrink that window.
51
+
52
+ ## Related
53
+
54
+ - [askUserDirectorySignOutUser](./ask-user-directory-sign-out-user.md) — revokes **every** refresh token for the user, not just one.
55
+ - [askUserDirectoryRefreshToken](./ask-user-directory-refresh-token.md) — exchanges a refresh token for fresh tokens.
56
+ - [defineUserDirectory](../../../config/core/user-directory.md) — declares the directory.
@@ -0,0 +1,52 @@
1
+ ---
2
+ title: askUserDirectorySignOutUser
3
+ description: Globally sign a user out by revoking every refresh token issued to them.
4
+ ---
5
+
6
+ # askUserDirectorySignOutUser
7
+
8
+ Globally signs a user out: revokes **every** refresh token issued to the owner of the supplied access token, against a [user directory](../../../config/core/user-directory.md). Use this for a "sign out of all devices" action or as a compromise response, since a stolen/persisted refresh token stops working for every session at once.
9
+
10
+ - **Action type:** `UserDirectoryActionType.SignOutUser`
11
+ - **On AWS:** issues Cognito `GlobalSignOut`. Authorized by the access token itself — no user directory name or IAM is needed to identify the user, Cognito resolves it from the token.
12
+
13
+ ```typescript
14
+ import { askUserDirectorySignOutUser } from 'quidproquo-core';
15
+
16
+ export function* askLogOutEverywhere(accessToken: string) {
17
+ yield* askUserDirectorySignOutUser(accessToken);
18
+ }
19
+ ```
20
+
21
+ ## Signature
22
+
23
+ ```typescript
24
+ function* askUserDirectorySignOutUser(accessToken: string): AskResponse<void>;
25
+ ```
26
+
27
+ ## Parameters
28
+
29
+ | Parameter | Type | Description |
30
+ | --- | --- | --- |
31
+ | `accessToken` | `string` | The signed-in user's access token, identifying whose sessions to sign out. |
32
+
33
+ ## Returns
34
+
35
+ `void` — the story resumes once every session has been revoked.
36
+
37
+ ## Errors
38
+
39
+ | Error | Meaning |
40
+ | --- | --- |
41
+ | `UserDirectorySignOutUserErrorTypeEnum.Unauthorized` | The access token is missing, invalid, or expired — there is nothing to revoke. |
42
+ | `UserDirectorySignOutUserErrorTypeEnum.LimitExceeded` | Too many attempts; back off and retry later. |
43
+
44
+ ## Notes
45
+
46
+ Access tokens already minted remain valid until they expire — they're stateless JWTs and can't be individually invalidated. Keep `accessTokenValidityMinutes` (see [defineUserDirectory](../../../config/core/user-directory.md#parameters)) short to shrink that window.
47
+
48
+ ## Related
49
+
50
+ - [askUserDirectoryRevokeRefreshToken](./ask-user-directory-revoke-refresh-token.md) — revokes only a single session, leaving the user's other devices signed in.
51
+ - [askUserDirectoryReadAccessToken](./ask-user-directory-read-access-token.md) / [askUserDirectoryDecodeAccessToken](./ask-user-directory-decode-access-token.md) — inspect an access token.
52
+ - [defineUserDirectory](../../../config/core/user-directory.md) — declares the directory.
@@ -15,22 +15,24 @@ A **purely declarative** way to apply an event-doc event: the story yields an `e
15
15
  import { askApplyEventDocEvent } from 'quidproquo-features';
16
16
 
17
17
  export function* askTenantSetBrand(data: TenantSetBrandData) {
18
- yield* askApplyEventDocEvent(TenantEffect.setBrand, data);
18
+ yield* askApplyEventDocEvent<SetBrandEffect>(TenantEffect.setBrand, data);
19
19
  }
20
20
  ```
21
21
 
22
22
  ## Signature
23
23
 
24
24
  ```typescript
25
- function* askApplyEventDocEvent(eventType: string, data: unknown): AskResponse<void>;
25
+ function* askApplyEventDocEvent<E extends Effect<string, any>>(eventType: E['type'], data: E['payload']): AskResponse<void>;
26
26
  ```
27
27
 
28
+ Typed like [askStateDispatchEffect](../../core/state/ask-state-dispatch.md#effect-and-askstatedispatcheffect): an event-doc event IS a special kind of effect (`Effect<type, data>`), so an action creator passes its effect type as `E` and gets `data` checked against it.
29
+
28
30
  ## Parameters
29
31
 
30
32
  | Parameter | Type | Description |
31
33
  | --- | --- | --- |
32
- | `eventType` | `string` | The effect/event type discriminant, matched by the reducer that folds the document and by the registered processor. |
33
- | `data` | `unknown` | The typed domain data for the event, opaque to this action itself. |
34
+ | `eventType` | `E['type']` | The effect/event type discriminant, matched by the reducer that folds the document and by the registered processor. |
35
+ | `data` | `E['payload']` | The typed domain data for the event, checked against `E`, opaque to this action itself. |
34
36
 
35
37
  ## Returns
36
38
 
@@ -0,0 +1,53 @@
1
+ ---
2
+ title: askApplyTransientEventDocEvent
3
+ description: Declaratively apply an event-doc event into a never-saved transient group, dropped wholesale by key instead of persisted.
4
+ ---
5
+
6
+ # askApplyTransientEventDocEvent
7
+
8
+ The never-saved sibling of [askApplyEventDocEvent](./ask-apply-event-doc-event.md): the story yields an `eventType` and its `data` under a `transientKey`, and the event lands in a transient group that is never persisted, only dropped wholesale by that key. It is the event-sourced way to represent observations that must not survive past their source, such as websocket progress messages, where dropping the key (usually a connection id) reverts the folded view reactively instead of leaving a stuck UI state behind.
9
+
10
+ - **Action type:** `EventDocActionType.ApplyTransientEvent`
11
+ - Transient applies are client-runtime-only by definition: a server "authors" them only via messages a client story processes, never a backend `ApplyTransientEvent` processor. An unbound or backend apply fails loudly, the same as an unbound `ApplyEvent`.
12
+ - The version stamped on the resulting event is not passed by the caller — the binding that applies it stamps its own configured schema version, the same as `askApplyEventDocEvent`.
13
+
14
+ ```typescript
15
+ import { askApplyTransientEventDocEvent } from 'quidproquo-features';
16
+
17
+ export function* askZipReportProgress(connectionId: string, data: ZipProgressData) {
18
+ yield* askApplyTransientEventDocEvent<ZipProgressEffect>(connectionId, ZipEffect.Progress, data);
19
+ }
20
+ ```
21
+
22
+ ## Signature
23
+
24
+ ```typescript
25
+ function* askApplyTransientEventDocEvent<E extends Effect<string, any>>(
26
+ transientKey: string,
27
+ eventType: E['type'],
28
+ data: E['payload'],
29
+ ): AskResponse<void>;
30
+ ```
31
+
32
+ Typed like [askApplyEventDocEvent](./ask-apply-event-doc-event.md): an event-doc event IS a special kind of effect (`Effect<type, data>`), so an action creator passes its effect type as `E` and gets `data` checked against it.
33
+
34
+ ## Parameters
35
+
36
+ | Parameter | Type | Description |
37
+ | --- | --- | --- |
38
+ | `transientKey` | `string` | Names the drop unit for this event, usually a websocket connection id. Dropping a key clears every event committed under it. |
39
+ | `eventType` | `E['type']` | The effect/event type discriminant, matched by the reducer that folds the document. |
40
+ | `data` | `E['payload']` | The typed domain data for the event, checked against `E`, opaque to this action itself. |
41
+
42
+ ## Returns
43
+
44
+ `AskResponse<void>` — applying a transient event never returns a value; a binding that fails to apply it surfaces the failure as its own runtime's error state rather than throwing back through this call.
45
+
46
+ ## Notes
47
+
48
+ - This is the contract only — it carries no default behavior on its own. In the event-doc workspace, the binding registered for a slot routes `ApplyTransientEvent` into that slot's transient group under `transientKey`; outside a workspace bind, calling it fails loudly.
49
+ - Transient events never influence the persistable log or validation: they are excluded from the log every save and validator reads, and are dropped wholesale by `transientKey` rather than removed one at a time.
50
+
51
+ ## Related
52
+
53
+ - [askApplyEventDocEvent](./ask-apply-event-doc-event.md) — the persisted sibling this action mirrors.
@@ -14,7 +14,7 @@ These three helpers cover the two flows: presigned URLs for a browser to upload/
14
14
 
15
15
  ## askEventDocGenerateAssetUploadUrl
16
16
 
17
- Mints a guid and a short-lived presigned `PUT` URL the client uploads the bytes to. Returns the guid (as `assetId`) so the caller can record it in a domain event. This is one complete storage operation — no bytes pass through your service.
17
+ Mints a guid and a short-lived presigned `PUT` URL the client uploads the bytes to. Returns the guid (as `assetId`) so the caller can record it in a domain event. This is one complete storage operation — no bytes pass through your service. An optional `contentDisposition` is baked into the upload (the client must `PUT` the matching header) so the stored object serves with it, e.g. `inline` so a rendered PDF previews in an `<iframe>` instead of downloading.
18
18
 
19
19
  ```typescript
20
20
  import { askEventDocGenerateAssetUploadUrl } from 'quidproquo-features';
@@ -32,6 +32,7 @@ export function* startImageUpload(docId: string) {
32
32
  function* askEventDocGenerateAssetUploadUrl(
33
33
  docId: string,
34
34
  contentType: string,
35
+ contentDisposition?: string,
35
36
  ): AskResponse<EventDocAssetUploadUrl>;
36
37
  ```
37
38
 
@@ -41,6 +42,7 @@ function* askEventDocGenerateAssetUploadUrl(
41
42
  | --- | --- | --- |
42
43
  | `docId` | `string` | The document the asset belongs to — determines the `<docId>/assets/<guid>` blob key. |
43
44
  | `contentType` | `string` | The MIME type the client will upload; pinned into the presigned `PUT` URL. |
45
+ | `contentDisposition` | `string` | Optional. Pinned into the presigned `PUT` URL so the stored asset serves with it, e.g. `inline` to preview instead of force-download. |
44
46
 
45
47
  ### Returns
46
48
 
@@ -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." } }