create-qpq-app 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/lib/commonjs/steps/006_applyDomain.js +3 -8
  2. package/lib/commonjs/steps/006_applyDomain.js.map +1 -1
  3. package/lib/commonjs/steps/013_printNextSteps.js +1 -1
  4. package/lib/commonjs/steps/013_printNextSteps.js.map +1 -1
  5. package/lib/esm/steps/006_applyDomain.js +3 -8
  6. package/lib/esm/steps/006_applyDomain.js.map +1 -1
  7. package/lib/esm/steps/013_printNextSteps.js +1 -1
  8. package/lib/esm/steps/013_printNextSteps.js.map +1 -1
  9. package/package.json +2 -2
  10. package/template/README.md +6 -3
  11. package/template/apps/qpqjs/account.qpq.ts +5 -13
  12. package/template/apps/qpqjs/bootstrap.qpq.ts +29 -14
  13. package/template/apps/qpqjs/deploy.config.json +0 -1
  14. package/template/apps/qpqjs/packages/constants/src/domain.ts +3 -1
  15. package/template/apps/qpqjs/packages/service-utils/src/defineQpqjsService.ts +15 -9
  16. package/template/apps/qpqjs/services/design/views/src/components/LandingPage/components/Infrastructure.tsx +1 -2
  17. package/template/apps/qpqjs/services/shell/service/src/infrastructure.ts +1 -4
  18. package/template/apps/qpqjs/services/test/models/package.json +2 -1
  19. package/template/apps/qpqjs/services/test/models/src/EchoRequest.ts +8 -0
  20. package/template/apps/qpqjs/services/test/models/src/EchoResponse.ts +9 -0
  21. package/template/apps/qpqjs/services/test/models/src/SmokeRun.ts +11 -9
  22. package/template/apps/qpqjs/services/test/models/src/SmokeRunStarted.ts +8 -0
  23. package/template/apps/qpqjs/services/test/models/src/SmokeRunSummary.ts +10 -6
  24. package/template/apps/qpqjs/services/test/models/src/SmokeRunWithSummary.ts +9 -5
  25. package/template/apps/qpqjs/services/test/models/src/SmokeTestResult.ts +11 -9
  26. package/template/apps/qpqjs/services/test/models/src/index.ts +3 -0
  27. package/template/apps/qpqjs/services/test/service/src/echo/config/defineEcho.ts +9 -0
  28. package/template/apps/qpqjs/services/test/service/src/echo/controller/echo.ts +32 -0
  29. package/template/apps/qpqjs/services/test/service/src/echo/controller/index.ts +1 -0
  30. package/template/apps/qpqjs/services/test/service/src/infrastructure.ts +3 -0
  31. package/template/apps/qpqjs/services/test/service/src/smoke/config/defineSmoke.ts +83 -18
  32. package/template/apps/qpqjs/services/test/service/src/smoke/constants/index.ts +1 -0
  33. package/template/apps/qpqjs/services/test/service/src/smoke/constants/smokeEventDoc.ts +27 -0
  34. package/template/apps/qpqjs/services/test/service/src/smoke/constants/smokeRunQueue.ts +4 -4
  35. package/template/apps/qpqjs/services/test/service/src/smoke/controller/askRunSmokeTests.ts +5 -1
  36. package/template/apps/qpqjs/services/test/service/src/smoke/data/askRecordSmokeTestResult.ts +32 -0
  37. package/template/apps/qpqjs/services/test/service/src/smoke/data/askSetSmokeRunOutcome.ts +24 -0
  38. package/template/apps/qpqjs/services/test/service/src/smoke/data/index.ts +2 -0
  39. package/template/apps/qpqjs/services/test/service/src/smoke/eventDoc/index.ts +1 -0
  40. package/template/apps/qpqjs/services/test/service/src/smoke/eventDoc/smokeProbeDocDefinition.ts +74 -0
  41. package/template/apps/qpqjs/services/test/service/src/smoke/index.ts +1 -0
  42. package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askExecuteSmokeTest.ts +97 -0
  43. package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askStartSmokeRun.ts +18 -10
  44. package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/index.ts +1 -1
  45. package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeEventDocAppendQueueEvent.ts +15 -0
  46. package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeEventDocMark.ts +8 -0
  47. package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeTestRequestedQueueEvent.ts +12 -0
  48. package/template/apps/qpqjs/services/test/service/src/smoke/models/index.ts +3 -1
  49. package/template/apps/qpqjs/services/test/service/src/smoke/queue/index.ts +2 -1
  50. package/template/apps/qpqjs/services/test/service/src/smoke/queue/onSmokeEventDocAppend.ts +64 -0
  51. package/template/apps/qpqjs/services/test/service/src/smoke/queue/onSmokeTestRequested.ts +17 -0
  52. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askAssertContiguousEventIds.ts +18 -0
  53. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askAssertSmokeProbeDocState.ts +32 -0
  54. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askAwaitSmokeEventDocLog.ts +35 -0
  55. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askRunEventDocConcurrentAppendTest.ts +82 -0
  56. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askRunEventDocInterleavedBatchAppendTest.ts +110 -0
  57. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/index.ts +5 -0
  58. package/template/apps/qpqjs/services/test/service/src/smoke/tests/index.ts +2 -0
  59. package/template/apps/qpqjs/services/test/service/src/smoke/tests/openApi/askRunEchoRoundTripTest.ts +45 -0
  60. package/template/apps/qpqjs/services/test/service/src/smoke/tests/openApi/askRunOpenApiDocumentTest.ts +42 -0
  61. package/template/apps/qpqjs/services/test/service/src/smoke/tests/openApi/index.ts +2 -0
  62. package/template/apps/qpqjs/services/test/service/src/smoke/tests/smokeTestRegistry.ts +18 -2
  63. package/template/apps/todo/deploy.config.json +0 -1
  64. package/template/apps/todo/packages/constants/src/domain.ts +2 -1
  65. package/template/apps/todo/packages/service-utils/src/defineTodoService.ts +5 -6
  66. package/template/apps/todo/services/shell/service/src/infrastructure.ts +1 -3
  67. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-upsert-with-retry.md +2 -2
  68. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-upsert.md +2 -1
  69. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-append-server-event.md +1 -1
  70. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-create.md +1 -1
  71. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-append.md +18 -12
  72. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-list.md +4 -4
  73. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-write.md +6 -6
  74. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-by-id.md +2 -2
  75. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-draft.md +2 -2
  76. package/template/docusaurus/docs/actions/webserver/dns/ask-dns-list.md +9 -8
  77. package/template/docusaurus/docs/actions/webserver/dns/ask-dns-resolve-hosts.md +42 -0
  78. package/template/docusaurus/docs/actions/webserver/open-api/_category_.json +1 -0
  79. package/template/docusaurus/docs/actions/webserver/open-api/ask-open-api-get-document.md +41 -0
  80. package/template/docusaurus/docs/config/config-aws/account-github-oidc-provider.md +31 -0
  81. package/template/docusaurus/docs/config/config-aws/domain-certificate.md +16 -32
  82. package/template/docusaurus/docs/config/config-aws/email-sender-allow-list.md +7 -14
  83. package/template/docusaurus/docs/config/config-aws/github-deploy-role.md +68 -0
  84. package/template/docusaurus/docs/config/core/user-directory.md +2 -3
  85. package/template/docusaurus/docs/config/features/admin-settings.md +2 -6
  86. package/template/docusaurus/docs/config/features/event-doc-summary.md +2 -2
  87. package/template/docusaurus/docs/config/features/open-api-routes.md +82 -0
  88. package/template/docusaurus/docs/config/features/tenanted-web-socket-queue.md +2 -3
  89. package/template/docusaurus/docs/config/features/web-socket-queue.md +1 -5
  90. package/template/docusaurus/docs/config/webserver/api.md +6 -9
  91. package/template/docusaurus/docs/config/webserver/cache.md +1 -1
  92. package/template/docusaurus/docs/config/webserver/dns.md +25 -17
  93. package/template/docusaurus/docs/config/webserver/domain-proxy.md +1 -5
  94. package/template/docusaurus/docs/config/webserver/email-sender.md +9 -15
  95. package/template/docusaurus/docs/config/webserver/route.md +1 -0
  96. package/template/docusaurus/docs/config/webserver/seo.md +1 -1
  97. package/template/docusaurus/docs/config/webserver/web-entry.md +0 -5
  98. package/template/docusaurus/docs/config/webserver/websocket.md +3 -7
  99. package/template/docusaurus/docs/domains.md +76 -0
  100. package/template/docusaurus/docs/use-cases.md +0 -6
  101. package/template/docusaurus/docusaurus.config.ts +38 -14
  102. package/template/docusaurus/sidebars.ts +5 -5
  103. package/template/docusaurus/src/components/HomepageFeatures/index.tsx +2 -2
  104. package/template/scripts/deployedSmoke.mjs +82 -23
  105. package/template/scripts/smoke/runEdgeChecks.mjs +152 -0
  106. package/template/scripts/smoke/runSmokeRun.mjs +1 -1
  107. package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askExecuteSmokeRun.ts +0 -83
  108. package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeRunRequestedQueueEvent.ts +0 -9
  109. package/template/apps/qpqjs/services/test/service/src/smoke/queue/onSmokeRunRequested.ts +0 -15
  110. package/template/docusaurus/docs/actions/webserver/open-api-spec/_category_.json +0 -1
  111. package/template/docusaurus/docs/actions/webserver/open-api-spec/ask-get-open-api-spec.md +0 -43
  112. package/template/docusaurus/docs/config/webserver/open-api.md +0 -36
@@ -154,12 +154,6 @@ export const api = createApi({
154
154
  });
155
155
  ```
156
156
 
157
- **Automatic OpenAPI Documentation**
158
- - Generate OpenAPI specs from your route definitions
159
- - Type-safe request/response contracts
160
- - Automatic validation against schemas
161
- - Interactive API documentation
162
-
163
157
  **Built-in Authentication & Authorization**
164
158
  ```typescript
165
159
  function* protectedRoute(request: HTTPRequest) {
@@ -1,6 +1,6 @@
1
+ import type { PrismTheme } from 'prism-react-renderer';
1
2
  import type * as Preset from '@docusaurus/preset-classic';
2
- import type {Config} from '@docusaurus/types';
3
- import type {PrismTheme} from 'prism-react-renderer';
3
+ import type { Config } from '@docusaurus/types';
4
4
 
5
5
  // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
6
6
 
@@ -12,16 +12,36 @@ const qpqPrismTheme: PrismTheme = {
12
12
  backgroundColor: 'transparent',
13
13
  },
14
14
  styles: [
15
- {types: ['keyword', 'operator', 'atrule'], style: {color: '#7dd3fc'}},
16
- {types: ['function', 'function-variable'], style: {color: '#8df6ff'}},
17
- {types: ['string', 'char', 'attr-value', 'url', 'regex'], style: {color: '#fbbf77'}},
18
- {types: ['class-name', 'maybe-class-name', 'builtin', 'namespace'], style: {color: '#86efac'}},
19
- {types: ['comment', 'prolog', 'doctype', 'cdata'], style: {color: '#5c7181', fontStyle: 'italic' as const}},
20
- {types: ['number', 'boolean', 'constant', 'symbol', 'property', 'attr-name', 'variable'], style: {color: '#c4b5fd'}},
21
- {types: ['punctuation'], style: {color: '#8fa8b8'}},
22
- {types: ['tag', 'selector'], style: {color: '#7dd3fc'}},
23
- {types: ['deleted'], style: {color: '#ff5f57'}},
24
- {types: ['inserted'], style: {color: '#28c840'}},
15
+ { types: ['keyword', 'operator', 'atrule'], style: { color: '#7dd3fc' } },
16
+ { types: ['function', 'function-variable'], style: { color: '#8df6ff' } },
17
+ {
18
+ types: ['string', 'char', 'attr-value', 'url', 'regex'],
19
+ style: { color: '#fbbf77' },
20
+ },
21
+ {
22
+ types: ['class-name', 'maybe-class-name', 'builtin', 'namespace'],
23
+ style: { color: '#86efac' },
24
+ },
25
+ {
26
+ types: ['comment', 'prolog', 'doctype', 'cdata'],
27
+ style: { color: '#5c7181', fontStyle: 'italic' as const },
28
+ },
29
+ {
30
+ types: [
31
+ 'number',
32
+ 'boolean',
33
+ 'constant',
34
+ 'symbol',
35
+ 'property',
36
+ 'attr-name',
37
+ 'variable',
38
+ ],
39
+ style: { color: '#c4b5fd' },
40
+ },
41
+ { types: ['punctuation'], style: { color: '#8fa8b8' } },
42
+ { types: ['tag', 'selector'], style: { color: '#7dd3fc' } },
43
+ { types: ['deleted'], style: { color: '#ff5f57' } },
44
+ { types: ['inserted'], style: { color: '#28c840' } },
25
45
  ],
26
46
  };
27
47
 
@@ -65,11 +85,15 @@ const config: Config = {
65
85
  headTags: [
66
86
  {
67
87
  tagName: 'link',
68
- attributes: {rel: 'preconnect', href: 'https://fonts.googleapis.com'},
88
+ attributes: { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
69
89
  },
70
90
  {
71
91
  tagName: 'link',
72
- attributes: {rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: 'anonymous'},
92
+ attributes: {
93
+ rel: 'preconnect',
94
+ href: 'https://fonts.gstatic.com',
95
+ crossorigin: 'anonymous',
96
+ },
73
97
  },
74
98
  ],
75
99
  stylesheets: [
@@ -1,4 +1,4 @@
1
- import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
1
+ import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';
2
2
 
3
3
  const sidebars: SidebarsConfig = {
4
4
  tutorialSidebar: [
@@ -9,20 +9,20 @@ const sidebars: SidebarsConfig = {
9
9
  'architecture-overview',
10
10
  'eslint-plugin',
11
11
  ],
12
-
12
+
13
13
  apiSidebar: [
14
14
  'api/index',
15
15
  {
16
16
  type: 'category',
17
17
  label: 'Config',
18
- items: [{type: 'autogenerated', dirName: 'config'}],
18
+ items: [{ type: 'autogenerated', dirName: 'config' }],
19
19
  },
20
20
  {
21
21
  type: 'category',
22
22
  label: 'Action Requesters',
23
- items: [{type: 'autogenerated', dirName: 'actions'}],
23
+ items: [{ type: 'autogenerated', dirName: 'actions' }],
24
24
  },
25
25
  ],
26
26
  };
27
27
 
28
- export default sidebars;
28
+ export default sidebars;
@@ -1,5 +1,5 @@
1
1
  import clsx from 'clsx';
2
- import type {ReactNode} from 'react';
2
+ import type { ReactNode } from 'react';
3
3
  import Heading from '@theme/Heading';
4
4
 
5
5
  import styles from './styles.module.css';
@@ -43,7 +43,7 @@ const FeatureList: FeatureItem[] = [
43
43
  },
44
44
  ];
45
45
 
46
- function Feature({title, Svg, description}: FeatureItem) {
46
+ function Feature({ title, Svg, description }: FeatureItem) {
47
47
  return (
48
48
  <div className={clsx('col col--4')}>
49
49
  <div className="text--center">
@@ -1,29 +1,34 @@
1
1
  #!/usr/bin/env node
2
- // Runs the smoke suite against a just-deployed test service and exits non-zero
3
- // if it does not pass. The run itself lives in scripts/smoke/runSmokeRun.mjs,
4
- // shared with localSmoke.mjs; this file only resolves which api to point it at.
2
+ // Runs the smoke suite against a just-deployed test service on EVERY root
3
+ // domain and exits non-zero if any root does not pass. Per root it runs the
4
+ // edge checks (scripts/smoke/runEdgeChecks.mjs: aliases, certs, cross-root
5
+ // cors, no token) and then the api smoke run (scripts/smoke/runSmokeRun.mjs,
6
+ // shared with localSmoke.mjs); this file only resolves where to point them.
5
7
  //
6
8
  // Runs inside the deploy workflow's deploy job, which supplies:
7
9
  // SMOKE_ENVIRONMENT the environment that was just deployed (development, staging)
8
10
  // ACTIONS_ID_TOKEN_REQUEST_URL provided by Actions when the job has id-token: write
9
11
  // ACTIONS_ID_TOKEN_REQUEST_TOKEN provided by Actions when the job has id-token: write
12
+ // SMOKE_ROOTS optional comma list to run a subset of the roots
10
13
  //
11
- // The api url is derived, not configured: the apex domain comes from the app's
12
- // deploy.config.json, non-production environments are a subdomain of it, and
13
- // the api gateway maps each service under its own base path (see the
14
- // CfnBasePathMapping in quidproquo-deploy-awscdk's api construct):
15
- // https://api.<environment>.<domain>/<service>
14
+ // Urls are derived, not configured: the roots come from the app's constants
15
+ // package (the same list every service's defineDns reads), hosts follow the
16
+ // app's default domain shape (see getRootHosts), and the api gateway maps each
17
+ // service under its own base path:
18
+ // https://api.<environment>.<root>/<service>
16
19
 
17
- import { readFileSync } from 'node:fs';
20
+ import { createRequire } from 'node:module';
18
21
  import { dirname, join } from 'node:path';
19
22
  import { fileURLToPath } from 'node:url';
20
23
 
24
+ import { getRootHosts, runEdgeChecks } from './smoke/runEdgeChecks.mjs';
21
25
  import { runSmokeRun } from './smoke/runSmokeRun.mjs';
22
26
 
23
27
  const SERVICE_NAME = 'test';
24
- const DEPLOY_CONFIG_PATH = join(
28
+ // The built constants package: the deploy job builds the app before this runs.
29
+ const DOMAIN_CONSTANTS_PATH = join(
25
30
  dirname(fileURLToPath(import.meta.url)),
26
- '../apps/qpqjs/deploy.config.json'
31
+ '../apps/qpqjs/packages/constants/dist/src/domain.js'
27
32
  );
28
33
 
29
34
  const log = (message) => console.log(`deployed-smoke: ${message}`);
@@ -41,21 +46,75 @@ const requireEnv = (name) => {
41
46
  return value;
42
47
  };
43
48
 
44
- const deriveApiUrl = (environment) => {
45
- const { domain } = JSON.parse(readFileSync(DEPLOY_CONFIG_PATH, 'utf8'));
46
- if (!domain) {
47
- fail(`no "domain" in ${DEPLOY_CONFIG_PATH}`);
49
+ const getRoots = () => {
50
+ const { QPQJS_DOMAINS } = createRequire(import.meta.url)(
51
+ DOMAIN_CONSTANTS_PATH
52
+ );
53
+ if (!QPQJS_DOMAINS?.length) {
54
+ fail(`no QPQJS_DOMAINS in ${DOMAIN_CONSTANTS_PATH}; build the app first`);
48
55
  }
49
- const envDomain =
50
- environment === 'production' ? domain : `${environment}.${domain}`;
51
- return `https://api.${envDomain}/${SERVICE_NAME}`;
56
+
57
+ const subset = process.env.SMOKE_ROOTS?.split(',')
58
+ .map((root) => root.trim())
59
+ .filter(Boolean);
60
+ if (!subset?.length) {
61
+ return QPQJS_DOMAINS;
62
+ }
63
+
64
+ const unknown = subset.filter((root) => !QPQJS_DOMAINS.includes(root));
65
+ if (unknown.length > 0) {
66
+ fail(`SMOKE_ROOTS names roots not in QPQJS_DOMAINS: ${unknown.join(', ')}`);
67
+ }
68
+ return subset;
69
+ };
70
+
71
+ const printTable = (root, results) => {
72
+ for (const result of results) {
73
+ log(
74
+ ` [${root}] ${result.ok ? 'pass' : 'FAIL'} ${result.name.padEnd(36)} ${result.detail}`
75
+ );
76
+ }
77
+ };
78
+
79
+ // Every root runs to the end, so one report covers all of them.
80
+ const smokeRoot = async (hosts, allHosts) => {
81
+ const rootLog = (message) => log(`[${hosts.root}] ${message}`);
82
+ rootLog(`edge checks against ${hosts.site}`);
83
+
84
+ const edge = await runEdgeChecks(hosts, allHosts);
85
+ printTable(hosts.root, edge);
86
+ const edgeOk = edge.every((result) => result.ok);
87
+
88
+ const apiUrl = `https://${hosts.api}/${SERVICE_NAME}`;
89
+ rootLog(`api smoke against ${apiUrl}`);
90
+ let apiOk = true;
91
+ try {
92
+ await runSmokeRun(apiUrl, rootLog);
93
+ } catch (error) {
94
+ apiOk = false;
95
+ rootLog(
96
+ `api smoke failed: ${error instanceof Error ? error.message : String(error)}`
97
+ );
98
+ }
99
+
100
+ return edgeOk && apiOk;
52
101
  };
53
102
 
54
103
  const environment = requireEnv('SMOKE_ENVIRONMENT');
55
- const apiUrl = deriveApiUrl(environment);
104
+ const roots = getRoots();
105
+ const allHosts = roots.map((root) => getRootHosts(root, environment));
56
106
 
57
- log(`environment=${environment} api=${apiUrl}`);
107
+ log(`environment=${environment} roots=${roots.join(', ')}`);
58
108
 
59
- runSmokeRun(apiUrl, log).catch((error) =>
60
- fail(error instanceof Error ? error.message : String(error))
61
- );
109
+ const outcomes = [];
110
+ for (const hosts of allHosts) {
111
+ outcomes.push({ root: hosts.root, ok: await smokeRoot(hosts, allHosts) });
112
+ }
113
+
114
+ for (const outcome of outcomes) {
115
+ log(`${outcome.ok ? 'PASS' : 'FAIL'} ${outcome.root}`);
116
+ }
117
+
118
+ if (outcomes.some((outcome) => !outcome.ok)) {
119
+ fail(`${outcomes.filter((outcome) => !outcome.ok).length} root(s) failed`);
120
+ }
@@ -0,0 +1,152 @@
1
+ // Edge checks for one root domain: what the in-lambda smoke suite cannot see
2
+ // from inside, namely the CloudFront aliases, the api gateway custom domain,
3
+ // the certificates and the cross-root CORS listing. Plain fetches, no token.
4
+ //
5
+ // Every check returns { name, ok, detail } rather than throwing, so a root
6
+ // reports its whole table before the run decides pass or fail.
7
+
8
+ const TIMEOUT_MS = 15 * 1000;
9
+
10
+ const fetchWithTimeout = (url, init = {}) =>
11
+ fetch(url, {
12
+ ...init,
13
+ signal: AbortSignal.timeout(TIMEOUT_MS),
14
+ redirect: 'manual',
15
+ });
16
+
17
+ // Hosts for one root under the app's default shape (`<sub>.<env>.<root>`, no
18
+ // environment label in production). If the app ever adds a domain resolver
19
+ // pointer, derive these with quidproquo-webserver's resolveHosts instead.
20
+ export const getRootHosts = (root, environment) => {
21
+ const site = environment === 'production' ? root : `${environment}.${root}`;
22
+
23
+ return {
24
+ root,
25
+ site,
26
+ api: `api.${site}`,
27
+ views: `views.${site}`,
28
+ docs: `docs.${site}`,
29
+ };
30
+ };
31
+
32
+ const check = async (name, run) => {
33
+ try {
34
+ const detail = await run();
35
+ return { name, ok: true, detail };
36
+ } catch (error) {
37
+ return {
38
+ name,
39
+ ok: false,
40
+ detail: error instanceof Error ? error.message : String(error),
41
+ };
42
+ }
43
+ };
44
+
45
+ const expectStatus = async (url, statuses) => {
46
+ const response = await fetchWithTimeout(url);
47
+ if (!statuses.includes(response.status)) {
48
+ throw new Error(`${url} -> ${response.status}`);
49
+ }
50
+ return `${response.status}`;
51
+ };
52
+
53
+ // CloudFront serves index.html for the site root and the docs; the views host
54
+ // is a module-federation remote root, whose own root may 403 while its manifest
55
+ // serves, so accept either a 200 or a redirect there.
56
+ const checkServed = (hosts) => [
57
+ check('site root served', () =>
58
+ expectStatus(`https://${hosts.site}/`, [200, 301, 302])
59
+ ),
60
+ check('docs served', () =>
61
+ expectStatus(`https://${hosts.docs}/`, [200, 301, 302])
62
+ ),
63
+ check('views served', () =>
64
+ expectStatus(`https://${hosts.views}/`, [200, 301, 302, 403, 404])
65
+ ),
66
+ check('api health', () =>
67
+ expectStatus(`https://${hosts.api}/shell/v1/health`, [200])
68
+ ),
69
+ ];
70
+
71
+ // The OpenAPI servers list is the runtime's own view of the roots: it proves
72
+ // the deployed lambda resolves every root, not just the one it was reached on.
73
+ const checkOpenApiServers = (hosts, allHosts) =>
74
+ check('openapi lists every root', async () => {
75
+ const response = await fetchWithTimeout(
76
+ `https://${hosts.api}/test/v1/docs/openapi.json`
77
+ );
78
+ if (!response.ok) {
79
+ throw new Error(`openapi.json -> ${response.status}`);
80
+ }
81
+ const servers = ((await response.json()).servers ?? []).map(
82
+ (server) => server.url
83
+ );
84
+ const missing = allHosts
85
+ .map((other) => other.api)
86
+ .filter((api) => !servers.some((url) => url.includes(api)));
87
+ if (missing.length > 0) {
88
+ throw new Error(
89
+ `servers ${JSON.stringify(servers)} missing ${missing.join(', ')}`
90
+ );
91
+ }
92
+ return `${servers.length} servers`;
93
+ });
94
+
95
+ // A page on root B calls the api on root A: the preflight must echo B's origin.
96
+ const checkCrossRootCors = (hosts, allHosts) =>
97
+ allHosts
98
+ .filter((other) => other.root !== hosts.root)
99
+ .map((other) =>
100
+ check(`api cors allows ${other.site}`, async () => {
101
+ const origin = `https://${other.site}`;
102
+ const response = await fetchWithTimeout(
103
+ `https://${hosts.api}/shell/v1/health`,
104
+ {
105
+ method: 'OPTIONS',
106
+ headers: { Origin: origin, 'Access-Control-Request-Method': 'GET' },
107
+ }
108
+ );
109
+ const allowed = response.headers.get('access-control-allow-origin');
110
+ if (allowed !== origin && allowed !== '*') {
111
+ throw new Error(
112
+ `allow-origin ${allowed ?? '(none)'} for origin ${origin}`
113
+ );
114
+ }
115
+ return allowed;
116
+ })
117
+ );
118
+
119
+ // The module-federation remotes are baked to the primary root, so a page on
120
+ // an alternate root loads them cross-origin; the views web entry's cors policy
121
+ // must allow that origin.
122
+ const checkViewsAssetCors = (hosts, allHosts) =>
123
+ allHosts
124
+ .filter((other) => other.root !== hosts.root)
125
+ .map((other) =>
126
+ check(`views cors allows ${other.site}`, async () => {
127
+ const origin = `https://${other.site}`;
128
+ const response = await fetchWithTimeout(
129
+ `https://${hosts.views}/shell/mf-manifest.json`,
130
+ { headers: { Origin: origin } }
131
+ );
132
+ const allowed = response.headers.get('access-control-allow-origin');
133
+ if (!allowed) {
134
+ throw new Error(
135
+ `no allow-origin header (status ${response.status}) for origin ${origin}`
136
+ );
137
+ }
138
+ return allowed;
139
+ })
140
+ );
141
+
142
+ /**
143
+ * Run every edge check for one root against the full root list and resolve the
144
+ * results, never throwing; `ok` is false on every row that failed.
145
+ */
146
+ export const runEdgeChecks = async (hosts, allHosts) =>
147
+ Promise.all([
148
+ ...checkServed(hosts),
149
+ checkOpenApiServers(hosts, allHosts),
150
+ ...checkCrossRootCors(hosts, allHosts),
151
+ ...checkViewsAssetCors(hosts, allHosts),
152
+ ]);
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // Target-agnostic on purpose. The two callers differ only in the api url they
6
6
  // resolve, which each derives for itself:
7
- // deployedSmoke.mjs the just-deployed environment, from deploy.config.json
7
+ // deployedSmoke.mjs the just-deployed environment, from the app domain constant
8
8
  // localSmoke.mjs a dev server it started, on localhost
9
9
  //
10
10
  // Both talk to the same routes over the same protocol with the same token, so
@@ -1,83 +0,0 @@
1
- import {
2
- askCatch,
3
- askDateNow,
4
- AskResponse,
5
- askThrowError,
6
- ErrorTypeEnum,
7
- } from 'quidproquo';
8
-
9
- import {
10
- SmokeRun,
11
- SmokeRunStatus,
12
- SmokeTestResult,
13
- SmokeTestStatus,
14
- } from '@qpqjs/test-models';
15
-
16
- import { askGetSmokeRunById } from '../../data/askGetSmokeRunById';
17
- import { askSaveSmokeRun } from '../../data/askSaveSmokeRun';
18
- import { SmokeTestDefinition } from '../../tests/SmokeTestDefinition';
19
- import { smokeTestRegistry } from '../../tests/smokeTestRegistry';
20
-
21
- // Runs one registered test, returning its completed result entry.
22
- function* askRunSmokeTest(
23
- test: SmokeTestDefinition,
24
- pending: SmokeTestResult,
25
- runId: string
26
- ): AskResponse<SmokeTestResult> {
27
- const startedAt = yield* askDateNow();
28
- const outcome = yield* askCatch(test.askRun(runId));
29
- const finishedAt = yield* askDateNow();
30
-
31
- return {
32
- ...pending,
33
- status: outcome.success ? SmokeTestStatus.passed : SmokeTestStatus.failed,
34
- message: outcome.success ? 'passed' : outcome.error.errorText,
35
- startedAt,
36
- finishedAt,
37
- };
38
- }
39
-
40
- // Executes every registered test for an existing run (created by
41
- // askStartSmokeRun), persisting the record after each test so a poll sees
42
- // progress, then finalizes the run status.
43
- export function* askExecuteSmokeRun(runId: string): AskResponse<SmokeRun> {
44
- const existing = yield* askGetSmokeRunById(runId);
45
- if (!existing) {
46
- return yield* askThrowError(
47
- ErrorTypeEnum.NotFound,
48
- `no smoke run [${runId}] to execute`
49
- );
50
- }
51
-
52
- let smokeRun: SmokeRun = existing;
53
-
54
- for (let index = 0; index < smokeTestRegistry.length; index += 1) {
55
- const result = yield* askRunSmokeTest(
56
- smokeTestRegistry[index],
57
- smokeRun.tests[index],
58
- runId
59
- );
60
-
61
- smokeRun = {
62
- ...smokeRun,
63
- tests: smokeRun.tests.map((t) => (t.id === result.id ? result : t)),
64
- };
65
-
66
- yield* askSaveSmokeRun(smokeRun);
67
- }
68
-
69
- const finishedAt = yield* askDateNow();
70
- const anyFailed = smokeRun.tests.some(
71
- (t) => t.status !== SmokeTestStatus.passed
72
- );
73
-
74
- smokeRun = {
75
- ...smokeRun,
76
- status: anyFailed ? SmokeRunStatus.failed : SmokeRunStatus.passed,
77
- finishedAt,
78
- };
79
-
80
- yield* askSaveSmokeRun(smokeRun);
81
-
82
- return smokeRun;
83
- }
@@ -1,9 +0,0 @@
1
- import { QueueEvent, QueueMessage } from 'quidproquo';
2
-
3
- export type SmokeRunRequestedPayload = {
4
- runId: string;
5
- };
6
-
7
- export type SmokeRunRequestedQueueEvent = QueueEvent<
8
- QueueMessage<SmokeRunRequestedPayload>
9
- >;
@@ -1,15 +0,0 @@
1
- import { AskResponse, QueueEventResponse } from 'quidproquo';
2
-
3
- import { askExecuteSmokeRun } from '../logic/smokeRun/askExecuteSmokeRun';
4
- import { SmokeRunRequestedQueueEvent } from '../models/SmokeRunRequestedQueueEvent';
5
-
6
- // Queue entry for a requested smoke run: executes the registered tests
7
- // against the run record askStartSmokeRun created. A throw here (e.g. the
8
- // record is missing) fails the message so the platform can redeliver it.
9
- export function* onSmokeRunRequested(
10
- event: SmokeRunRequestedQueueEvent
11
- ): AskResponse<QueueEventResponse> {
12
- yield* askExecuteSmokeRun(event.message.payload.runId);
13
-
14
- return true;
15
- }
@@ -1 +0,0 @@
1
- { "label": "OpenAPI Spec", "link": { "type": "generated-index", "description": "Read the service's generated OpenAPI spec from within a story." } }
@@ -1,43 +0,0 @@
1
- ---
2
- title: askGetOpenApiSpec
3
- description: Return the service's generated OpenAPI spec as a string.
4
- ---
5
-
6
- # askGetOpenApiSpec
7
-
8
- Returns the service's **OpenAPI spec** as a string. Use it in a route handler to serve the spec (for documentation UIs or client generation) directly from the running service.
9
-
10
- - **Action type:** `OpenApiSpecActionType.GetOpenApiSpec`
11
-
12
- ```typescript
13
- import { askGetOpenApiSpec } from 'quidproquo-webserver';
14
-
15
- export function* getOpenApiRoute() {
16
- const spec = yield* askGetOpenApiSpec();
17
-
18
- return {
19
- status: 200,
20
- headers: { 'content-type': 'application/json' },
21
- body: spec,
22
- };
23
- }
24
- ```
25
-
26
- ## Signature
27
-
28
- ```typescript
29
- function* askGetOpenApiSpec(): AskResponse<string>;
30
- ```
31
-
32
- ## Parameters
33
-
34
- None.
35
-
36
- ## Returns
37
-
38
- `string` — the OpenAPI spec. Associate a spec with the service using [defineOpenApi](../../../config/webserver/open-api.md).
39
-
40
- ## Related
41
-
42
- - [defineOpenApi](../../../config/webserver/open-api.md) — declares the spec this action returns.
43
- - [defineRoute](../../../config/webserver/route.md) — expose the spec at an HTTP path.
@@ -1,36 +0,0 @@
1
- ---
2
- title: defineOpenApi
3
- description: Point a service at an OpenAPI spec file describing its HTTP API.
4
- ---
5
-
6
- # defineOpenApi
7
-
8
- Registers an **OpenAPI spec** for the service by pointing at a spec file on disk. This associates a machine-readable description of the service's HTTP API with the config.
9
-
10
- - **On AWS:** no infrastructure of its own — it records the spec path in the config. The generated spec is retrievable at runtime with [askGetOpenApiSpec](../../actions/webserver/open-api-spec/ask-get-open-api-spec.md).
11
-
12
- ```typescript
13
- import { defineOpenApi } from 'quidproquo-webserver';
14
-
15
- export default [
16
- defineOpenApi('./openapi.json'),
17
- ];
18
- ```
19
-
20
- ## Signature
21
-
22
- ```typescript
23
- function defineOpenApi(openApiSpecPath: string): OpenApiQPQWebServerConfigSetting;
24
- ```
25
-
26
- ## Parameters
27
-
28
- ### `openApiSpecPath` — `string` (required)
29
-
30
- Path to the OpenAPI spec file for the service. This value is also the setting's `uniqueKey`.
31
-
32
- ## Related
33
-
34
- - [askGetOpenApiSpec](../../actions/webserver/open-api-spec/ask-get-open-api-spec.md) — read the OpenAPI spec from within a story.
35
- - [defineApi](./api.md) — the HTTP API the spec describes.
36
- - [defineRoute](./route.md) — the routes that make up the API surface.