create-qpq-app 0.1.9 → 0.1.10

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 (24) 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/webserver/service-function/ask-service-function-execute.md +19 -13
  14. package/template/docusaurus/docs/config/core/bundle-options.md +1 -1
  15. package/template/docusaurus/docs/config/core/user-directory.md +3 -1
  16. package/template/docusaurus/docs/config/features/web-socket-queue.md +1 -1
  17. package/template/docusaurus/docs/config/webserver/auth-system.md +9 -1
  18. package/template/docusaurus/docs/core-concepts.md +35 -1
  19. package/template/docusaurus/docs/eslint-plugin.md +86 -0
  20. package/template/docusaurus/docs/getting-started.md +3 -1
  21. package/template/docusaurus/sidebars.ts +1 -0
  22. package/template/docusaurus/src/css/custom.css +44 -0
  23. package/template/docusaurus/static/img/favicon.svg +6 -2
  24. package/template/docusaurus/static/img/qpq-logo.svg +4 -3
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.10",
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.10"
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
 
@@ -13,14 +13,20 @@ Invokes a [service function](../../../config/webserver/service-function.md) —
13
13
  ```typescript
14
14
  import { askServiceFunctionExecute } from 'quidproquo-webserver';
15
15
 
16
- interface ResizeRequest { drive: string; filepath: string; width: number; }
17
- interface ResizeResult { thumbnailPath: string; }
16
+ interface ResizeRequest {
17
+ drive: string;
18
+ filepath: string;
19
+ width: number;
20
+ }
21
+ interface ResizeResult {
22
+ thumbnailPath: string;
23
+ }
18
24
 
19
25
  export function* askMakeThumbnail(filepath: string) {
20
26
  const result = yield* askServiceFunctionExecute<ResizeResult, ResizeRequest>(
21
- 'media', // service name
22
- 'resizeImage', // function name
23
- { drive: 'uploads', filepath, width: 256 },
27
+ 'media', // service name
28
+ 'resizeImage', // function name
29
+ { drive: 'uploads', filepath, width: 256 }
24
30
  );
25
31
 
26
32
  return result.thumbnailPath;
@@ -34,18 +40,18 @@ function* askServiceFunctionExecute<R, T>(
34
40
  service: string,
35
41
  functionName: string,
36
42
  payload: T,
37
- isAsync?: boolean,
43
+ isAsync?: boolean
38
44
  ): AskResponse<R>;
39
45
  ```
40
46
 
41
47
  ## Parameters
42
48
 
43
- | Parameter | Type | Default | Description |
44
- | --- | --- | --- | --- |
45
- | `service` | `string` | – | Name of the service that owns the target function. |
46
- | `functionName` | `string` | – | Name of the [service function](../../../config/webserver/service-function.md) to invoke (its `functionName`, which defaults to the story name). |
47
- | `payload` | `T` | – | The typed payload passed to the function's story. |
48
- | `isAsync` | `boolean` | `false` | When `false`, wait for and return the result. When `true`, invoke fire-and-forget and resolve without a result. |
49
+ | Parameter | Type | Default | Description |
50
+ | -------------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | `service` | `string` | – | Name of the service that owns the target function. |
52
+ | `functionName` | `string` | – | Name of the [service function](../../../config/webserver/service-function.md) to invoke (its `functionName`, which defaults to the story name). |
53
+ | `payload` | `T` | – | The typed payload passed to the function's story. |
54
+ | `isAsync` | `boolean` | `false` | When `false`, wait for and return the result. When `true`, invoke fire-and-forget and resolve without a result. |
49
55
 
50
56
  ## Returns
51
57
 
@@ -54,5 +60,5 @@ function* askServiceFunctionExecute<R, T>(
54
60
  ## Related
55
61
 
56
62
  - [defineServiceFunction](../../../config/webserver/service-function.md) — declares the function this action invokes.
57
- - [askServiceRequest](../service/ask-service-request.md) — a related request to another service, dispatched by method name.
63
+ - [askServiceRequest](../../features/web-socket-queue/ask-service-request.md) — a related request to another service, dispatched by method name.
58
64
  - [askCatch](../../core/system/ask-catch.md) — catch errors thrown by the invoked function.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: defineBackendBundleOptions / defineFrontendBundleOptions
3
- description: Tune how the service's backend and frontend bundles are built: externals, ignored modules, suppressed warnings, and shared frontend singletons.
3
+ description: "Tune how the service's backend and frontend bundles are built: externals, ignored modules, suppressed warnings, and shared frontend singletons."
4
4
  ---
5
5
 
6
6
  # defineBackendBundleOptions / defineFrontendBundleOptions
@@ -43,6 +43,7 @@ The name of the directory. This is the name you pass as the `userDirectoryName`
43
43
  | `dnsRecord` | `AuthDirectoryDnsRecord` | – | Serves the pool's hosted UI / OAuth endpoints from a custom domain. See [DNS record](#dns-record). |
44
44
  | `customAuthRuntime` | `CustomAuthRuntime` | – | Enables Cognito's `CUSTOM_AUTH` flow, wiring story functions to the define/create/verify auth-challenge Lambda triggers. See [Custom auth runtime](#custom-auth-runtime). |
45
45
  | `mfa` | `UserDirectoryMfaSettings` | `{ mode: off, secondFactors: [totp] }` | Multi-factor authentication configuration. See [MFA](#mfa). |
46
+ | `accessTokenValidityMinutes` | `number` | `60` (Cognito default) | Access/ID token (JWT) lifetime in minutes (Cognito allows 5–1440). Shorter shrinks the window in which a revoked session's still-valid access token keeps working, since access tokens are stateless and can't be revoked before they expire — see [askUserDirectoryRevokeRefreshToken](../../actions/core/user-directory/ask-user-directory-revoke-refresh-token.md) / [askUserDirectorySignOutUser](../../actions/core/user-directory/ask-user-directory-sign-out-user.md). Keep it comfortably above however long the client waits before refreshing. |
46
47
  | `deprecated` | `boolean` | `false` | Inherited from `QPQConfigAdvancedSettings`; marks the setting as deprecated. |
47
48
 
48
49
  ## MFA
@@ -132,7 +133,7 @@ The `QpqInfCoreUserDirectoryConstruct` deploys:
132
133
  - **Lambda triggers** (only when configured):
133
134
  - `CUSTOM_MESSAGE` — when any `emailTemplates` handler is set.
134
135
  - `DEFINE_AUTH_CHALLENGE`, and optionally `CREATE_AUTH_CHALLENGE` / `VERIFY_AUTH_CHALLENGE_RESPONSE` — when `customAuthRuntime` is set.
135
- - **An app client** with a generated secret, `ADMIN_USER_PASSWORD_AUTH` enabled, and `CUSTOM_AUTH` enabled when `customAuthRuntime` is set.
136
+ - **An app client** with a generated secret, `ADMIN_USER_PASSWORD_AUTH` enabled, `CUSTOM_AUTH` enabled when `customAuthRuntime` is set, and access/ID token validity set from `accessTokenValidityMinutes` (Cognito's 60-minute default when omitted).
136
137
  - **A custom domain + Route 53 alias** — when `dnsRecord` is set (certificate from `us-east-1`).
137
138
 
138
139
  Services that own a directory get IAM for the admin Cognito operations the actions use (create user, initiate/respond to auth, list/get users, update attributes, set password, describe pool/client, etc.). Services that only reference a **foreign** directory (via `owner`) get no Cognito IAM — token validation runs against the pool's public JWKs over HTTPS and needs none.
@@ -183,6 +184,7 @@ export default [
183
184
  - **Passwords:** [askUserDirectoryChangePassword](../../actions/core/user-directory/ask-user-directory-change-password.md), [askUserDirectoryForgotPassword](../../actions/core/user-directory/ask-user-directory-forgot-password.md), [askUserDirectorySetPassword](../../actions/core/user-directory/ask-user-directory-set-password.md).
184
185
  - **MFA:** [askUserDirectoryAssociateSoftwareToken](../../actions/core/user-directory/ask-user-directory-associate-software-token.md).
185
186
  - **Tokens:** [askUserDirectoryReadAccessToken](../../actions/core/user-directory/ask-user-directory-read-access-token.md), [askUserDirectoryDecodeAccessToken](../../actions/core/user-directory/ask-user-directory-decode-access-token.md).
187
+ - **Signing out:** [askUserDirectoryRevokeRefreshToken](../../actions/core/user-directory/ask-user-directory-revoke-refresh-token.md) (single session), [askUserDirectorySignOutUser](../../actions/core/user-directory/ask-user-directory-sign-out-user.md) (every session).
186
188
  - **All User Directory actions:** see the [User Directory actions](../../actions/core/user-directory/ask-user-directory-authenticate-user.md) group.
187
189
  - **Turn-key auth:** [defineAuthSystem](../webserver/auth-system.md) (quidproquo-webserver) creates a user directory and the login / refresh / password-recovery HTTP routes in one call.
188
190
  - **AWS tuning:** [defineAwsDataStoreRemovalPolicy](../config-aws/aws-data-store-removal-policy.md) — retain vs destroy the Cognito user pool on teardown.
@@ -80,7 +80,7 @@ export interface QPQConfigAdvancedWebsocketQueueSettings extends QPQConfigAdvanc
80
80
 
81
81
  ## Returns
82
82
 
83
- A `QPQConfig` array containing: three globals, the connection [key-value store](../core/key-value-store.md), and the [WebSocket API](./websocket.md) with the queue's built-in handlers.
83
+ A `QPQConfig` array containing: three globals, the connection [key-value store](../core/key-value-store.md), and the [WebSocket API](../webserver/websocket.md) with the queue's built-in handlers.
84
84
 
85
85
  ## Related
86
86
 
@@ -57,6 +57,7 @@ The name of the [user directory](../../config/core/user-directory.md) to create
57
57
  | `mfa` | `UserDirectoryMfaSettings` | – | Forwarded to the user directory: multi-factor authentication configuration. See [MFA](../../config/core/user-directory.md#mfa). |
58
58
  | `basePath` | `string` | `''` | A path prefix applied to every auth route. E.g. `'/auth'` produces `/auth/login`, `/auth/refreshToken`, and so on. |
59
59
  | `allowedOrigins` | `string[]` | – | Allowed browser origins (CORS) applied to every generated auth route. |
60
+ | `accessTokenValidityMinutes` | `number` | `60` (Cognito default) | Forwarded to the user directory: access/ID token (JWT) lifetime in minutes (Cognito allows 5–1440). Shorter shrinks the window in which a revoked session's access token keeps working. Keep it comfortably above however long the client waits before refreshing (the stock web client refreshes ~10 minutes before expiry). See [defineUserDirectory](../../config/core/user-directory.md#parameters). |
60
61
 
61
62
  ## Generated routes
62
63
 
@@ -66,6 +67,8 @@ With `basePath` = `''` (the default), `defineAuthSystem` registers these `POST`
66
67
  | --- | --- | --- |
67
68
  | `/login` | `login` | No |
68
69
  | `/refreshToken` | `refreshToken` | No |
70
+ | `/logout` | `logout` | No |
71
+ | `/logoutEverywhere` | `logoutEverywhere` | No |
69
72
  | `/challenge` | `respondToAuthChallenge` | No |
70
73
  | `/associateSoftwareToken` | `associateSoftwareToken` | No |
71
74
  | `/forgotPassword` | `forgotPassword` | No |
@@ -74,6 +77,11 @@ With `basePath` = `''` (the default), `defineAuthSystem` registers these `POST`
74
77
 
75
78
  Each path is prefixed with `basePath` when provided (e.g. `/auth/login`). The handler stories correspond to the [User Directory actions](../../actions/core/user-directory/ask-user-directory-authenticate-user.md) — for example `login` authenticates a user, `refreshToken` refreshes an access token, and `respondToAuthChallenge` completes a challenge/MFA flow.
76
79
 
80
+ `/logout` and `/logoutEverywhere` are unauthenticated routes that are each self-authorizing by the token they carry, and both are best-effort — they always respond `{ success: true }` even if the token was already invalid, since a client's local logout must never fail:
81
+
82
+ - `/logout` takes `{ refreshToken }` in the body and revokes just that one session ([askUserDirectoryRevokeRefreshToken](../../actions/core/user-directory/ask-user-directory-revoke-refresh-token.md)) — "log out this device."
83
+ - `/logoutEverywhere` reads the access token from the request headers and revokes every session for that user ([askUserDirectorySignOutUser](../../actions/core/user-directory/ask-user-directory-sign-out-user.md)) — "sign out of all devices."
84
+
77
85
  ## How auth ties routes to the user directory
78
86
 
79
87
  A route becomes authenticated by setting `routeAuthSettings.userDirectoryName` (which `defineAuthSystem` does for `/changePassword`). At runtime the webserver validates the caller's access token against that directory before invoking the handler — decoding the token, checking it is valid, and making its claims/roles available. Any route in the application (not just the ones generated here) can opt into the same protection by referencing this directory's name, because the directory is declared ungated at the top level.
@@ -106,5 +114,5 @@ export default [
106
114
 
107
115
  - [defineUserDirectory](../../config/core/user-directory.md) — the user directory `defineAuthSystem` creates; documents the directory options that `phoneRequired`, `selfSignUpEnabled`, `emailTemplates`, and `mfa` forward to, plus the AWS Cognito infrastructure.
108
116
  - [defineApiKey](./api-key.md) — the complementary auth mechanism (shared-secret / machine callers) for routes.
109
- - **User Directory actions** — the auth operations the generated handlers wrap: [askUserDirectoryAuthenticateUser](../../actions/core/user-directory/ask-user-directory-authenticate-user.md), [askUserDirectoryRefreshToken](../../actions/core/user-directory/ask-user-directory-refresh-token.md), [askUserDirectoryRespondToAuthChallenge](../../actions/core/user-directory/ask-user-directory-respond-to-auth-challenge.md), [askUserDirectoryChangePassword](../../actions/core/user-directory/ask-user-directory-change-password.md), [askUserDirectoryForgotPassword](../../actions/core/user-directory/ask-user-directory-forgot-password.md), [askUserDirectoryConfirmForgotPassword](../../actions/core/user-directory/ask-user-directory-confirm-forgot-password.md), [askUserDirectoryAssociateSoftwareToken](../../actions/core/user-directory/ask-user-directory-associate-software-token.md).
117
+ - **User Directory actions** — the auth operations the generated handlers wrap: [askUserDirectoryAuthenticateUser](../../actions/core/user-directory/ask-user-directory-authenticate-user.md), [askUserDirectoryRefreshToken](../../actions/core/user-directory/ask-user-directory-refresh-token.md), [askUserDirectoryRevokeRefreshToken](../../actions/core/user-directory/ask-user-directory-revoke-refresh-token.md), [askUserDirectorySignOutUser](../../actions/core/user-directory/ask-user-directory-sign-out-user.md), [askUserDirectoryRespondToAuthChallenge](../../actions/core/user-directory/ask-user-directory-respond-to-auth-challenge.md), [askUserDirectoryChangePassword](../../actions/core/user-directory/ask-user-directory-change-password.md), [askUserDirectoryForgotPassword](../../actions/core/user-directory/ask-user-directory-forgot-password.md), [askUserDirectoryConfirmForgotPassword](../../actions/core/user-directory/ask-user-directory-confirm-forgot-password.md), [askUserDirectoryAssociateSoftwareToken](../../actions/core/user-directory/ask-user-directory-associate-software-token.md).
110
118
  - **Routes and APIs** — the generated endpoints are `defineRoute` settings; see the route / `defineApi` config settings in quidproquo-webserver to add and protect your own routes with this directory.
@@ -20,4 +20,38 @@ Platform-specific implementations that execute actions.
20
20
 
21
21
  ## Runtime
22
22
 
23
- The orchestration layer that executes stories by processing yielded actions through the appropriate processors.
23
+ The orchestration layer that executes stories by processing yielded actions through the appropriate processors.
24
+
25
+ ## Why every function starts with `ask`
26
+
27
+ Every quidproquo function you call from a story starts with `ask`: `askDateNow`, `askFileReadTextContents`, `askRunParallel`. That prefix isn't decoration, it tells you exactly what kind of function you're holding. Here's the whole of `askDateNow`:
28
+
29
+ ```typescript
30
+ export function* askDateNow(): DateNowActionRequester {
31
+ return yield { type: DateActionType.Now };
32
+ }
33
+ ```
34
+
35
+ Notice it never calls `new Date()`. It yields a plain action object and suspends. The runtime catches that action, hands it to the platform's `DateNow` processor (which actually reads the clock), then resumes the generator with the result. The `ask` prefix captures three things about that arrangement:
36
+
37
+ ### 1. It's a request, not an execution
38
+
39
+ Calling `askDateNow()` does nothing on its own. It builds a generator that *describes* what it wants, and the runtime decides how to fulfil it. That's what keeps stories pure and platform-agnostic: the "what" lives in your story, the "how" lives in the `quidproquo-actionprocessor-*` packages. Your story doesn't know whether the clock is a Lambda, a Node process, or a test fixture.
40
+
41
+ ### 2. It's a question with an answer
42
+
43
+ The return type is literally `AskResponse<T>`. You ask, the runtime responds, and the response arrives as the generator's return value:
44
+
45
+ ```typescript
46
+ const now = yield* askDateNow(); // now: QpqIsoDateTime
47
+ ```
48
+
49
+ Every action pairs a **requester** (the `ask` function your story calls) with a **processor** (the platform implementation that produces the answer). They're two halves of the same conversation.
50
+
51
+ ### 3. It signals the calling convention
52
+
53
+ Anything starting with `ask` must be invoked with `yield*` from inside another generator. The prefix works like a syntax marker, the same way `use` marks a React hook: see `ask`, write `yield*`. Higher-level stories that compose other ask functions follow the same rule and get the same prefix (`askGetCurrentEpochMs`, `askRunParallel`, `askCatch`).
54
+
55
+ The practical payoff is testability and portability. Because `askDateNow` only describes intent, a test can run the same story with a fake processor that returns a fixed timestamp, and the identical story code runs on Lambda, Node, or the browser just by swapping the processor set.
56
+
57
+ Both halves of this contract (always `yield*` an ask call, never name a plain function `ask*`) are enforced at edit time by the [quidproquo ESLint plugin](./eslint-plugin.md), which ships with the shared lint config and auto-fixes missing `yield*`s.
@@ -0,0 +1,86 @@
1
+ ---
2
+ sidebar_position: 5
3
+ ---
4
+
5
+ # ESLint Plugin
6
+
7
+ The `ask` prefix is a contract: [every function that starts with `ask` is a story](./core-concepts.md#why-every-function-starts-with-ask), and every story must be run with `yield*`. Contracts are only useful if something enforces them, so quidproquo ships an ESLint plugin that does. It catches the two mistakes everyone makes at least once:
8
+
9
+ ```typescript
10
+ // error: creates a generator that never runs
11
+ const now = askDateNow();
12
+
13
+ // error: hands the generator to the runtime as a value instead of running it
14
+ const now = yield askDateNow();
15
+
16
+ // correct
17
+ const now = yield* askDateNow();
18
+ ```
19
+
20
+ ## Setup
21
+
22
+ If you use the shared config, you already have it. `quidproquo-eslint-config` wires the plugin in, so every `create-qpq-app` project and every package extending that config gets both rules as errors with no extra setup.
23
+
24
+ To use the plugin on its own, without the rest of the shared config:
25
+
26
+ ```js title="eslint.config.mjs"
27
+ import { qpqPlugin } from 'quidproquo-eslint-config';
28
+
29
+ export default [
30
+ qpqPlugin.configs.recommended,
31
+ // or pick rules individually:
32
+ // { plugins: { qpq: qpqPlugin }, rules: { 'qpq/require-yield-star': 'error' } },
33
+ ];
34
+ ```
35
+
36
+ ## `qpq/require-yield-star`
37
+
38
+ Inside a generator, any call to an `ask*` function (including member calls like `authLogic.askLogin()` and tagged templates like `` askLog`...` ``) must be delegated with `yield*`.
39
+
40
+ The rule is auto-fixable. Run `eslint --fix` and it will:
41
+
42
+ - insert `yield*` in front of a bare call
43
+ - upgrade a plain `yield` to `yield*`
44
+ - replace a wrong `await` with `yield*`
45
+ - add parentheses where a bare yield expression is not allowed, for example `!askUserExists(id)` becomes `!(yield* askUserExists(id))`
46
+
47
+ Two patterns stay legal, because they are how stories are meant to be composed and tested:
48
+
49
+ ```typescript
50
+ // ask calls as arguments build generators for the outer ask to run
51
+ yield* askRunParallel([askRandomNumber(), askDateNow()]);
52
+
53
+ // outside a generator you cannot yield; tests and forwarding
54
+ // wrappers create generators and hand them off
55
+ const result = runStory(askDateNow(), mocks);
56
+ ```
57
+
58
+ ## `qpq/ask-prefix-generator-only`
59
+
60
+ The reverse direction: `ask` is reserved for stories, the same way `use` is reserved for React hooks. A plain function named `ask*` breaks the contract, because callers will `yield*` something that is not a generator.
61
+
62
+ ```typescript
63
+ // error: not a generator
64
+ function askUserName() {
65
+ return 'joe';
66
+ }
67
+
68
+ // correct
69
+ function* askUserName(): AskResponse<string> {
70
+ return yield* askConfigGetParameter('user-name');
71
+ }
72
+ ```
73
+
74
+ The rule checks every kind of function literal: declarations, arrows, object methods, class methods and class properties. Aliases and factory results are fine, since the right-hand side already is (or produces) a story:
75
+
76
+ ```typescript
77
+ // both legal: not function literals
78
+ const askLog = askLogTemplateLiteral;
79
+ const askActiveTenantRead = createContextReader(tenantContext);
80
+ ```
81
+
82
+ There is no auto-fix for this one. Whether the function should become a generator or lose the prefix is a judgement call.
83
+
84
+ ## Why bother
85
+
86
+ TypeScript alone cannot catch a missing `yield*`. Calling `askDateNow()` without yielding is perfectly well-typed, it just returns a generator that nobody ever runs, and the action inside it silently never happens. That failure mode produces no error, no log line, and no clue. A lint rule at edit time is the cheapest place to catch it.
@@ -96,7 +96,9 @@ export const health = dynamicRoute(
96
96
  The story never reads the clock itself — it *asks* for the time by yielding
97
97
  an action, and the runtime answers. On your machine that's the dev server;
98
98
  in production it's whatever platform the app is deployed to. Same story,
99
- zero changes.
99
+ zero changes. That's why every quidproquo function starts with `ask`: see
100
+ [why every function starts with ask](./core-concepts.md#why-every-function-starts-with-ask)
101
+ for the full story.
100
102
 
101
103
  Add your own route by dropping a file next to `health.ts`, exporting it from
102
104
  the controller `index.ts`, and saving — the dev server rebuilds and restarts
@@ -7,6 +7,7 @@ const sidebars: SidebarsConfig = {
7
7
  'core-concepts',
8
8
  'use-cases',
9
9
  'architecture-overview',
10
+ 'eslint-plugin',
10
11
  ],
11
12
 
12
13
  apiSidebar: [
@@ -104,12 +104,56 @@ body {
104
104
  }
105
105
 
106
106
  .navbar__title {
107
+ position: relative;
107
108
  font-family: var(--qpq-font-display);
108
109
  font-size: 19px;
109
110
  font-weight: 600;
110
111
  letter-spacing: 0.02em;
111
112
  }
112
113
 
114
+ /* Glint overlay: the same glyphs stacked over the title, transparent except
115
+ for a narrow band clipped to the letterforms. It rests off-canvas, then
116
+ makes one eased sweep across per cycle. The band is two-tone (white into
117
+ cyan) so it stays visible whatever the letter color underneath: the cyan
118
+ half carries the shimmer on this all-light title. The "/ ''" marks the
119
+ duplicated text as decorative; browsers without that syntax skip the
120
+ overlay entirely, which just means no shimmer. */
121
+ .navbar__title::after {
122
+ content: 'quidproquo' / '';
123
+ position: absolute;
124
+ inset: 0;
125
+ color: transparent;
126
+ background-image: linear-gradient(
127
+ 105deg,
128
+ rgba(255, 255, 255, 0) 38%,
129
+ rgba(255, 255, 255, 0.85) 45%,
130
+ rgba(34, 211, 238, 0.85) 53%,
131
+ rgba(34, 211, 238, 0) 60%
132
+ );
133
+ background-size: 250% 100%;
134
+ background-repeat: no-repeat;
135
+ background-position: 130% 0;
136
+ -webkit-background-clip: text;
137
+ background-clip: text;
138
+ animation: qpq-title-shine 7s ease-in-out infinite;
139
+ }
140
+
141
+ @keyframes qpq-title-shine {
142
+ 0%,
143
+ 72% {
144
+ background-position: 130% 0;
145
+ }
146
+ 100% {
147
+ background-position: -130% 0;
148
+ }
149
+ }
150
+
151
+ @media (prefers-reduced-motion: reduce) {
152
+ .navbar__title::after {
153
+ animation: none;
154
+ }
155
+ }
156
+
113
157
  .navbar__logo {
114
158
  filter: drop-shadow(0 0 10px rgba(34, 211, 238, 0.5));
115
159
  }
@@ -1,4 +1,8 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
- <rect x="3" y="3" width="26" height="26" rx="6" fill="#071018" stroke="#22d3ee" stroke-width="2"/>
3
- <path d="M12 9l-4 7 4 7M20 9l4 7-4 7" stroke="#7ff4ff" stroke-width="2.4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
2
+ <rect x="1" y="1" width="30" height="30" rx="6" fill="#071018"/>
3
+ <g transform="translate(4.5 4.5) scale(0.0494) translate(-24.4 -24.4)">
4
+ <path fill="#22d3ee" 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"/>
5
+ <rect fill="#8df6ff" x="268.31" y="273.22" width="106.91" height="106.91"/>
6
+ <rect fill="#8df6ff" x="375.62" y="380.53" width="106.91" height="106.91"/>
7
+ </g>
4
8
  </svg>
@@ -1,4 +1,5 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
- <rect x="3" y="3" width="26" height="26" rx="7" fill="rgba(34,211,238,0.08)" stroke="#22d3ee" stroke-width="1.6"/>
3
- <path d="M12 9l-4 7 4 7M20 9l4 7-4 7" stroke="#8df6ff" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="24.4 24.4 465.3 465.3">
2
+ <path fill="#22d3ee" 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"/>
3
+ <rect fill="#8df6ff" x="268.31" y="273.22" width="106.91" height="106.91"/>
4
+ <rect fill="#8df6ff" x="375.62" y="380.53" width="106.91" height="106.91"/>
4
5
  </svg>