create-agent-rig 0.9.1 → 0.10.0

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 (234) hide show
  1. package/CHANGELOG.md +419 -3
  2. package/README.md +267 -305
  3. package/package.json +8 -14
  4. package/packages/cli/dist/commands/create.js +56 -88
  5. package/packages/cli/dist/commands/doctor.js +213 -0
  6. package/packages/cli/dist/commands/init.js +111 -50
  7. package/packages/cli/dist/commands/integrations.js +468 -0
  8. package/packages/cli/dist/commands/setup-wizard.js +61 -0
  9. package/packages/cli/dist/commands/uninstall.js +1223 -0
  10. package/packages/cli/dist/commands/upgrade.js +472 -90
  11. package/packages/cli/dist/index.js +651 -46
  12. package/packages/cli/dist/integrations/declaration.js +158 -0
  13. package/packages/cli/dist/integrations/doctor-guards.js +126 -0
  14. package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
  15. package/packages/cli/dist/integrations/mcp-json.js +107 -0
  16. package/packages/cli/dist/integrations/memory-doctor.js +124 -0
  17. package/packages/cli/dist/integrations/registry.js +22 -0
  18. package/packages/cli/dist/integrations/spawn.js +228 -0
  19. package/packages/cli/dist/integrations/spec-kit.js +280 -0
  20. package/packages/cli/dist/integrations/verify.js +193 -0
  21. package/packages/cli/dist/integrations/windows-job.js +275 -0
  22. package/packages/cli/dist/lib/elevated-paths.js +74 -0
  23. package/packages/cli/dist/lib/install-set.js +1 -44
  24. package/packages/cli/dist/lib/manifest.js +64 -5
  25. package/packages/cli/dist/lib/prompts.js +0 -23
  26. package/packages/cli/dist/lib/safe-path.js +193 -2
  27. package/packages/cli/dist/lib/safe-text.js +78 -0
  28. package/packages/cli/dist/lib/substitute.js +1 -52
  29. package/packages/cli/dist/lib/summary.js +5 -5
  30. package/packages/cli/dist/templates.js +4 -14
  31. package/scripts/prepare.mjs +28 -12
  32. package/templates/agent-os/subagent-routing.json +4 -4
  33. package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +13 -9
  34. package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
  35. package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
  36. package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
  37. package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
  38. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
  39. package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
  40. package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
  41. package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
  42. package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -2
  43. package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
  44. package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
  45. package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
  46. package/templates/agent-os/universal/.claude/rules/workflow.md +47 -22
  47. package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
  48. package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
  49. package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
  50. package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
  51. package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
  52. package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +4 -3
  53. package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
  54. package/templates/agent-os/universal/.claude/settings.json +0 -8
  55. package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +13 -9
  56. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
  57. package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
  58. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
  59. package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
  60. package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
  61. package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
  62. package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
  63. package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
  64. package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
  65. package/templates/agent-os/universal/.codex/hooks.json +0 -10
  66. package/templates/agent-os/universal/AGENTS.md +208 -107
  67. package/templates/agent-os/universal/CLAUDE.md +15 -162
  68. package/templates/agent-os/universal/PLAN.md +17 -10
  69. package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
  70. package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
  71. package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
  72. package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
  73. package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
  74. package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
  75. package/templates/agent-os/universal/layers.json +25 -30
  76. package/templates/hash-history.json +8 -4
  77. package/templates/release-ledger.json +2 -1
  78. package/packages/cli/dist/lib/composition.js +0 -20
  79. package/packages/cli/dist/lib/targets.js +0 -28
  80. package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
  81. package/packages/cli/dist/policy/core/adapter.js +0 -18
  82. package/packages/cli/dist/policy/core/coverage.js +0 -253
  83. package/packages/cli/dist/policy/core/decision-record.js +0 -287
  84. package/packages/cli/dist/policy/core/declaration.js +0 -127
  85. package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
  86. package/packages/cli/dist/policy/core/probe.js +0 -442
  87. package/packages/cli/dist/policy/core/registry.js +0 -115
  88. package/packages/cli/dist/policy/core/validation.js +0 -275
  89. package/packages/cli/dist/policy/core/vocabulary.js +0 -123
  90. package/packages/cli/dist/policy/harness/claude.js +0 -47
  91. package/packages/cli/dist/policy/harness/codex.js +0 -87
  92. package/packages/cli/dist/policy/harness/index.js +0 -15
  93. package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
  94. package/packages/cli/dist/policy/index.js +0 -17
  95. package/templates/agent-os/init/AGENTS.md +0 -201
  96. package/templates/agent-os/init/CLAUDE.md +0 -201
  97. package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
  98. package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
  99. package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
  100. package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
  101. package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
  102. package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
  103. package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
  104. package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
  105. package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
  106. package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
  107. package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
  108. package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
  109. package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
  110. package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
  111. package/templates/skeleton/aws-serverless/README.md +0 -181
  112. package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
  113. package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
  114. package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
  115. package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
  116. package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
  117. package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
  118. package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
  119. package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
  120. package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
  121. package/templates/skeleton/aws-serverless/gitignore +0 -56
  122. package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
  123. package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
  124. package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
  125. package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
  126. package/templates/skeleton/aws-serverless/infra/package.json +0 -18
  127. package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
  128. package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
  129. package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
  130. package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
  131. package/templates/skeleton/aws-serverless/package.json +0 -30
  132. package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
  133. package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
  134. package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
  135. package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
  136. package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
  137. package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
  138. package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
  139. package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
  140. package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
  141. package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
  142. package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
  143. package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
  144. package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
  145. package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
  146. package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
  147. package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
  148. package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
  149. package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
  150. package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
  151. package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
  152. package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
  153. package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
  154. package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
  155. package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
  156. package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
  157. package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
  158. package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
  159. package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
  160. package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
  161. package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
  162. package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
  163. package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
  164. package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
  165. package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
  166. package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
  167. package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
  168. package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
  169. package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
  170. package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
  171. package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
  172. package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
  173. package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
  174. package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
  175. package/templates/skeleton/node-service/README.md +0 -104
  176. package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
  177. package/templates/skeleton/node-service/apps/web/package.json +0 -19
  178. package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
  179. package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
  180. package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
  181. package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
  182. package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
  183. package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
  184. package/templates/skeleton/node-service/eslint.config.mjs +0 -20
  185. package/templates/skeleton/node-service/gitignore +0 -54
  186. package/templates/skeleton/node-service/package.json +0 -30
  187. package/templates/skeleton/node-service/packages/core/package.json +0 -11
  188. package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
  189. package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
  190. package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
  191. package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
  192. package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
  193. package/templates/skeleton/node-service/packages/db/package.json +0 -12
  194. package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
  195. package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
  196. package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
  197. package/templates/skeleton/node-service/packages/shared/package.json +0 -11
  198. package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
  199. package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
  200. package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
  201. package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
  202. package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
  203. package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
  204. package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
  205. package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
  206. package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
  207. package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
  208. package/templates/skeleton/node-service/services/api/package.json +0 -17
  209. package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
  210. package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
  211. package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
  212. package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
  213. package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
  214. package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
  215. package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
  216. package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
  217. package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
  218. package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
  219. package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
  220. package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
  221. package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
  222. package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
  223. package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
  224. package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
  225. package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
  226. package/templates/skeleton/node-service/services/worker/package.json +0 -16
  227. package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
  228. package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
  229. package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
  230. package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
  231. package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
  232. package/templates/skeleton/node-service/tsconfig.base.json +0 -15
  233. package/templates/skeleton/node-service/tsconfig.json +0 -13
  234. package/templates/skeleton/node-service/vitest.config.ts +0 -12
@@ -1,301 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { Annotations as CdkAnnotations, App } from 'aws-cdk-lib';
5
- import { Annotations, Match } from 'aws-cdk-lib/assertions';
6
- import { describe, expect, it } from 'vitest';
7
- import {
8
- AppStack,
9
- type AppStackProps,
10
- nothingGiven,
11
- resolveAllowedOrigins,
12
- } from '../lib/app-stack.js';
13
-
14
- // Who may call the API is a security decision with exactly two safe outcomes:
15
- // the right origins, or a refusal loud enough that somebody fixes it. An empty
16
- // allow-list is neither — it synthesises, deploys, and blocks every call.
17
- //
18
- // Resolving that list is a pure function of (props, context), so it is tested
19
- // as one. The previous shape of this file built a whole AppStack per case —
20
- // twelve synths, each bundling three Lambdas with esbuild — which cost ~9.3s
21
- // and left the generated project's `beforeAll` hooks racing vitest's 10s
22
- // default hookTimeout on a saturated CPU. Only the CDK-level behaviour below
23
- // (the wildcard annotation) needs a stack, and it builds one per case.
24
-
25
- const resolved = (given: { props?: string[]; context?: unknown } = {}): string[] =>
26
- resolveAllowedOrigins(given.props, given.context);
27
-
28
- const thrownBy = (run: () => unknown): unknown => {
29
- try {
30
- run();
31
- } catch (error) {
32
- return error;
33
- }
34
- return undefined;
35
- };
36
-
37
- const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
38
-
39
- /**
40
- * The port `apps/web` is actually started on, read from the script that starts it.
41
- *
42
- * The localhost default in `app-stack.ts` is a restated copy of a number owned
43
- * by another file, and a restated constant drifts: it shipped `:3000` while the
44
- * only dev server in this repository has always been launched with
45
- * `--port 3001`, so the default named an origin nothing here ever sends —
46
- * configured-looking, and working for nobody. Reading the source of truth is
47
- * what stops that happening a second time.
48
- *
49
- * The read is deliberately loud. A `dev` script this cannot parse fails the
50
- * suite rather than falling back to a port of its own, because a correspondence
51
- * test that quietly supplies both sides of the correspondence checks nothing.
52
- */
53
- const webDevServerPort = (): string => {
54
- const manifest = path.join(workspaceRoot, 'apps', 'web', 'package.json');
55
- const scripts = (JSON.parse(readFileSync(manifest, 'utf8')) as { scripts?: Record<string, string> })
56
- .scripts;
57
- const dev = scripts?.dev;
58
- expect(dev, 'apps/web/package.json has no dev script to take the origin port from').toBeTypeOf(
59
- 'string',
60
- );
61
- const port = /--port[=\s]+(\d+)/.exec(dev as string)?.[1];
62
- expect(
63
- port,
64
- `the dev script ${JSON.stringify(dev)} names no --port, so nothing pins the default to it`,
65
- ).toBeTypeOf('string');
66
- return port as string;
67
- };
68
-
69
- /** Every refusal must name the flag that carried the bad value, and be an Error. */
70
- const refusalFrom = (run: () => unknown): Error => {
71
- const error = thrownBy(run);
72
- expect(error, 'the value was accepted instead of refused').toBeInstanceOf(Error);
73
- expect(
74
- (error as Error).message,
75
- 'the message must name the flag that was wrong',
76
- ).toMatch(/allowedOrigins/);
77
- return error as Error;
78
- };
79
-
80
- describe('choosing the allowed origins', () => {
81
- it('falls back to localhost only when nothing at all was given', () => {
82
- expect(resolved()).toEqual(['http://localhost:3001']);
83
- });
84
-
85
- it('defaults to the port the web dev server is actually started on', () => {
86
- // The correspondence, not a second copy of the number. `apps/web` owns the
87
- // port; a default that restates it is only correct until somebody changes
88
- // one of the two, which is exactly how it came to name :3000.
89
- expect(resolved()).toEqual([`http://localhost:${webDevServerPort()}`]);
90
- });
91
-
92
- it('prefers the origins passed in props over the localhost default', () => {
93
- expect(resolved({ props: ['https://app.example.com'] })).toEqual(['https://app.example.com']);
94
- });
95
-
96
- it('prefers the origins passed in props over a context value', () => {
97
- // Both are "given"; props are the programmatic wiring and win. `bin/app.ts`
98
- // depends on this ordering being decided in one place.
99
- expect(
100
- resolved({ props: ['https://app.example.com'], context: 'https://ctx.example.com' }),
101
- ).toEqual(['https://app.example.com']);
102
- });
103
-
104
- it('reads a comma-separated -c allowedOrigins as one origin each, trimmed', () => {
105
- expect(resolved({ context: ' https://a.example.com , https://b.example.com ' })).toEqual([
106
- 'https://a.example.com',
107
- 'https://b.example.com',
108
- ]);
109
- });
110
-
111
- it('treats a null context as nothing given, exactly as bin/app.ts does', () => {
112
- // A JSON `null` is writable in `cdk.json`, `cdk.context.json` or
113
- // `CDK_CONTEXT_JSON`. If the two files disagree about it, the entrypoint
114
- // drops the wired CloudFront origin while the stack falls back to a
115
- // localhost no deploy is served from — green synth, blocked browser.
116
- expect(nothingGiven(null)).toBe(true);
117
- expect(nothingGiven(undefined)).toBe(true);
118
- expect(resolved({ context: null })).toEqual(['http://localhost:3001']);
119
- });
120
- });
121
-
122
- describe('an allow-list that names nobody is a broken deploy, not a default', () => {
123
- it('refuses a context value that parses to no origin at all', () => {
124
- // `-c allowedOrigins=" , , "` is a typo, and an empty allow-list means
125
- // every browser call fails — whose only obvious repair is putting `*` back.
126
- refusalFrom(() => resolved({ context: ' , , ' }));
127
- });
128
-
129
- it('refuses an explicitly empty allowedOrigins prop for the same reason', () => {
130
- // `?? DEFAULT` does not catch `[]` — an empty array is not nullish, so it
131
- // travels all the way to CorsConfiguration.
132
- refusalFrom(() => resolved({ props: [] }));
133
- });
134
-
135
- it('refuses a non-string context value instead of crashing inside the stack', () => {
136
- // `cdk.json` holds JSON, so `"allowedOrigins": ["https://a"]` is a natural
137
- // thing to write. Chosen behaviour: REFUSE it and name the flag — one code
138
- // path for "the flag was given something unusable", rather than quietly
139
- // widening what the stack accepts.
140
- const error = refusalFrom(() => resolved({ context: ['https://a.example.com'] }));
141
- expect(error.message, 'a TypeError from .split is a crash, not a refusal').not.toMatch(
142
- /is not a function/,
143
- );
144
- });
145
- });
146
-
147
- // A browser's `Origin` header is always `scheme://host[:port]` — so an entry
148
- // without a scheme cannot match one, ever, and the failure surfaces in a
149
- // browser console days later, far from the flag that caused it.
150
- describe('an origin no browser could ever send is refused at synth', () => {
151
- it.each([
152
- // The likeliest operator typo: the host as it appears in the address bar.
153
- ['a host with the scheme left off', 'app.example.com'],
154
- // `-c allowedOrigins=null` is a string; the JSON null this branch also
155
- // handles goes through a different path entirely.
156
- ['the literal string null', 'null'],
157
- // A quoting artifact from a shell or a CI variable: non-empty after trim,
158
- // and meaningless as an origin.
159
- ['a quoting artifact left over from the shell', '" "'],
160
- ])('refuses %s and names the flag that carried it', (_case, value) => {
161
- refusalFrom(() => resolved({ context: value }));
162
- });
163
-
164
- it('refuses the same shapes when they arrive through props', () => {
165
- // Two entry points, one standard: a prop is not a trusted channel just
166
- // because it is typed `string[]`.
167
- refusalFrom(() => resolved({ props: ['app.example.com'] }));
168
- });
169
-
170
- it('accepts the origins a browser can actually send', () => {
171
- // The refusal above must stay narrow: a real deploy origin and the local
172
- // dev one are the two shapes this project is built around. The second is
173
- // spelled as THIS project's dev origin on purpose — an arbitrary port would
174
- // prove the same thing about the parser while quietly reading as "the dev
175
- // origin" to the next person, which is the confusion that produced the
176
- // :3000 default in the first place.
177
- expect(resolved({ context: 'https://a.example.com' })).toEqual(['https://a.example.com']);
178
- expect(resolved({ context: 'http://localhost:3001' })).toEqual(['http://localhost:3001']);
179
- });
180
- });
181
-
182
- // An entry that synthesises but can never equal a browser's `Origin` header is
183
- // the same failure as a typo, only quieter: CORS compares the header to the
184
- // allow-list byte for byte. The near-misses below all survive `new URL()`, so
185
- // nothing downstream objects — the API just refuses every call.
186
- describe('an origin is stored the way a browser would actually send it', () => {
187
- it.each([
188
- // Copying the site's root URL out of the address bar adds the slash. This
189
- // is the likeliest of the five by a wide margin.
190
- ['a trailing slash', 'https://app.example.com/', 'https://app.example.com'],
191
- ['an uppercase scheme', 'HTTPS://app.example.com', 'https://app.example.com'],
192
- ['a path left on the end', 'https://app.example.com/notes', 'https://app.example.com'],
193
- // A browser resolves an IDN to punycode before it builds the header, so
194
- // the unicode spelling never appears on the wire.
195
- ['a unicode host', 'https://héllo.example.com', 'https://xn--hllo-bpa.example.com'],
196
- // Also strips a credential that has no business in a synthesised template.
197
- ['userinfo', 'https://user:secret@app.example.com', 'https://app.example.com'],
198
- // Browsers omit the default port; a literal `:443` never matches.
199
- ['a redundant default port', 'https://app.example.com:443/', 'https://app.example.com'],
200
- ])('normalises %s to what the Origin header carries', (_case, given, expected) => {
201
- expect(resolved({ context: given }), 'through -c allowedOrigins').toEqual([expected]);
202
- expect(resolved({ props: [given] }), 'and through props, identically').toEqual([expected]);
203
- });
204
-
205
- it('keeps a non-default port, which a browser does send', () => {
206
- expect(resolved({ context: 'https://app.example.com:8443/' })).toEqual([
207
- 'https://app.example.com:8443',
208
- ]);
209
- });
210
-
211
- it('trims a props entry the way it trims a context entry', () => {
212
- // The context branch trims before checking; the props branch never did, so
213
- // a stray space from a shell array reached CorsConfiguration intact.
214
- expect(resolved({ props: [' https://a.example.com ', 'https://b.example.com\t'] })).toEqual([
215
- 'https://a.example.com',
216
- 'https://b.example.com',
217
- ]);
218
- });
219
-
220
- it('never stores the literal string null, whatever it was handed', () => {
221
- // `URL.origin` is the string "null" for every opaque scheme. An allow-list
222
- // entry of `null` matches the `Origin: null` a sandboxed iframe sends —
223
- // granting exactly the caller nobody meant to grant.
224
- for (const value of ['null', 'file:///tmp/x', 'data:text/html,x']) {
225
- expect(thrownBy(() => resolved({ context: value })), value).toBeInstanceOf(Error);
226
- }
227
- });
228
- });
229
-
230
- // These are real `Origin` header values — a Chrome extension, and the two
231
- // WebView schemes Capacitor and Ionic use on device. Refusing them is a
232
- // DECISION, recorded here rather than left to be rediscovered: the http(s)
233
- // check is what makes the scheme-less typo above catchable, the refusal is
234
- // loud and names the flag, and an app that genuinely serves a WebView client
235
- // widens `checkedOrigin` in one place. Changing this must change this test.
236
- describe('a non-http(s) origin is refused, and that is a decision on record', () => {
237
- it.each([
238
- ['a browser extension', 'chrome-extension://abcdefghijklmnopabcdefghijklmnop'],
239
- ['a Capacitor WebView', 'capacitor://localhost'],
240
- ['an Ionic WebView', 'ionic://localhost'],
241
- ['a blob URL whose inner origin looks real', 'blob:https://example.com/uuid'],
242
- ])('refuses %s, saying which schemes it does take', (_case, value) => {
243
- const error = refusalFrom(() => resolved({ context: value }));
244
- expect(error.message, 'the refusal must say what it would have accepted').toMatch(/http/);
245
- });
246
- });
247
-
248
- // `*` is a decision, not a defect: an operator who types it into a flag has
249
- // chosen it, and refusing outright would push people to edit the stack instead
250
- // — a worse outcome, invisible to this test file. So it is accepted AND it is
251
- // loud.
252
- describe('a wildcard allow-list is honoured, but never silently', () => {
253
- const namesTheWildcardRisk = Match.stringLikeRegexp('allowedOrigins');
254
- /** Acknowledgeable warnings need a stable id; changing it is a breaking change. */
255
- const wildcardWarningId = '@app/allowed-origins:wildcard';
256
-
257
- it('keeps the wildcard the operator asked for', () => {
258
- expect(resolved({ context: '*' })).toEqual(['*']);
259
- expect(resolved({ props: ['*'] })).toEqual(['*']);
260
- });
261
-
262
- it('keeps a wildcard hidden among real origins, rather than dropping it', () => {
263
- // `https://a.example.com,*` reads like an allow-list; the `*` makes every
264
- // other entry decorative, which is exactly why it must not slip past.
265
- expect(resolved({ context: 'https://a.example.com,*' })).toEqual([
266
- 'https://a.example.com',
267
- '*',
268
- ]);
269
- });
270
-
271
- // Everything below builds a real AppStack — three esbuild bundles and a synth
272
- // apiece. One stack per case, and nothing in a `beforeAll`: the generated
273
- // project's vitest config raises `testTimeout`, not `hookTimeout`, so a hook
274
- // that synthesises is a 10s fuse on a loaded machine.
275
- const stackWith = (options: { context?: Record<string, unknown>; props?: AppStackProps }) =>
276
- new AppStack(new App({ context: options.context }), 'TestStack', options.props);
277
-
278
- it('warns about a wildcard from -c allowedOrigins, acknowledgeably', () => {
279
- // "Honoured, but loud" only holds if the operator can then proceed. A bare
280
- // `addWarning` cannot be acknowledged, so `cdk synth --strict` — the flag
281
- // CI reaches for — turns a deliberate `*` into a hard failure with no way
282
- // past it short of editing the stack: the outcome this design refuses.
283
- // Emission and acknowledgeability are one behaviour and share one stack;
284
- // a second one costs another three esbuild bundles.
285
- const stack = stackWith({ context: { allowedOrigins: '*' } });
286
- Annotations.fromStack(stack).hasWarning('*', namesTheWildcardRisk);
287
-
288
- CdkAnnotations.of(stack).acknowledgeWarning(wildcardWarningId);
289
-
290
- Annotations.fromStack(stack).hasNoWarning('*', namesTheWildcardRisk);
291
- });
292
-
293
- it('warns just the same when the wildcard arrives through props', () => {
294
- // `bin/app.ts` passes props, so this is the path a composed app takes —
295
- // and it was the untested half: a wildcard wired in code warned nobody.
296
- Annotations.fromStack(stackWith({ props: { allowedOrigins: ['*'] } })).hasWarning(
297
- '*',
298
- namesTheWildcardRisk,
299
- );
300
- });
301
- });
@@ -1,137 +0,0 @@
1
- import type { App, Stack } from 'aws-cdk-lib';
2
- import { Template } from 'aws-cdk-lib/assertions';
3
- import { describe, expect, it } from 'vitest';
4
- import { resolveAllowedOrigins } from '../lib/app-stack.js';
5
-
6
- // `bin/app.ts` is what every `cdk deploy` and every CI run actually builds, so
7
- // the CORS allow-list has to be correct THERE — a stack that can be handed the
8
- // right origin, by an entrypoint that hands it none, ships an app whose own
9
- // browser calls are blocked. The README's `curl` health check sends no `Origin`
10
- // header, so nothing downstream notices.
11
- //
12
- // The composition is exercised through the entrypoint rather than rebuilt here:
13
- // a test that re-wires the stacks itself passes just as happily while
14
- // `bin/app.ts` stays broken.
15
-
16
- const originsOfComposedApp = async (): Promise<unknown[]> => {
17
- const { createApp } = (await import('../bin/app.js')) as { createApp: () => App };
18
- const app = createApp();
19
- const template = Template.fromStack(app.node.findChild('AppStack') as Stack);
20
- const apis = template.findResources('AWS::ApiGatewayV2::Api');
21
- return Object.values(apis).flatMap(
22
- (api) =>
23
- (api.Properties as { CorsConfiguration?: { AllowOrigins?: unknown[] } }).CorsConfiguration
24
- ?.AllowOrigins ?? [],
25
- );
26
- };
27
-
28
- /**
29
- * `createApp()` builds its own `App`, so `new App({ context })` is not a seam
30
- * here. CDK's own is: the CLI passes `-c key=value` to a synth through
31
- * `CDK_CONTEXT_JSON`, which the `App` constructor reads — so setting it around
32
- * one `createApp()` call reproduces `cdk deploy -c allowedOrigins=…` exactly,
33
- * without the test knowing anything about how the stacks are wired.
34
- *
35
- * `process.env` is process-global, so this seam is not concurrency-safe: an
36
- * `it.concurrent` in this file would have one case's context leak into
37
- * another's synth. Keep these cases sequential.
38
- */
39
- const withCdkContext = async <T>(context: Record<string, unknown>, run: () => Promise<T>) => {
40
- const previous = process.env.CDK_CONTEXT_JSON;
41
- process.env.CDK_CONTEXT_JSON = JSON.stringify(context);
42
- try {
43
- return await run();
44
- } finally {
45
- if (previous === undefined) delete process.env.CDK_CONTEXT_JSON;
46
- else process.env.CDK_CONTEXT_JSON = previous;
47
- }
48
- };
49
-
50
- /**
51
- * The default composition is synthesised once and shared — but deliberately
52
- * NOT from `beforeAll`. This project's `vitest.config.ts` raises `testTimeout`
53
- * for exactly this cost and leaves `hookTimeout` at vitest's 10s default, so
54
- * work put in a hook runs on a fuse a quarter the length of the one the config
55
- * meant to give it. Synthesising here bundles three Lambdas with esbuild while
56
- * sibling workers do the same; that contention is what made this suite fail
57
- * intermittently with `Hook timed out in 10000ms`.
58
- */
59
- let defaultComposition: Promise<unknown[]> | undefined;
60
- const defaultComposedOrigins = (): Promise<unknown[]> =>
61
- (defaultComposition ??= originsOfComposedApp());
62
-
63
- /** What CDK emits for a cross-stack reference: a Join around an ImportValue. */
64
- const referencesTheWebDistribution = (origin: unknown): boolean => {
65
- if (typeof origin === 'string') return false; // a hard-coded origin, whatever it says
66
- const rendered = JSON.stringify(origin);
67
- return /Fn::ImportValue/.test(rendered) && /WebStack/.test(rendered);
68
- };
69
-
70
- describe('the deployed app allows the origin it is actually served from', () => {
71
- it('takes its allowed origin from the web distribution, not from a localhost no deploy uses', async () => {
72
- // `distributionDomainName` is a synth-time token; CDK resolves it across
73
- // stacks as an `Fn::ImportValue`, and `AllowOrigins` accepts it. So the
74
- // entrypoint has no excuse to leave the two stacks unwired.
75
- const appStackOrigins = await defaultComposedOrigins();
76
- expect(appStackOrigins.length).toBeGreaterThan(0);
77
- expect(appStackOrigins.some(referencesTheWebDistribution)).toBe(true);
78
- });
79
-
80
- it('still names who may call it: no wildcard reaches the composed app', async () => {
81
- const appStackOrigins = await defaultComposedOrigins();
82
- expect(appStackOrigins).not.toContain('*');
83
- expect(JSON.stringify(appStackOrigins)).not.toContain('"*"');
84
- });
85
-
86
- it('carries the https scheme, without which no browser Origin header can match', async () => {
87
- const appStackOrigins = await defaultComposedOrigins();
88
- const distributionOrigin = appStackOrigins.find(referencesTheWebDistribution);
89
- expect(JSON.stringify(distributionOrigin)).toContain('https://');
90
- });
91
- });
92
-
93
- // The README and `AppStackProps.allowedOrigins` both advertise
94
- // `-c allowedOrigins=https://…` as the way to point the API at a custom domain.
95
- // It once did not work: the entrypoint passed `allowedOrigins` unconditionally
96
- // and props win, so the flag was discarded on synth with no error and no
97
- // warning — a green deploy, a CORS-blocked browser, and `allowOrigins: ['*']`
98
- // as the obvious repair for a hurried reader. That is the regression the rest
99
- // of this file exists to prevent, arriving through a different door, which is
100
- // why the door is pinned here.
101
- describe('the documented -c escape hatch reaches the composed app', () => {
102
- it('serves the origin the flag names instead of discarding it for the CloudFront import', async () => {
103
- const origins = await withCdkContext({ allowedOrigins: 'https://custom.example.com' }, () =>
104
- originsOfComposedApp(),
105
- );
106
-
107
- expect(origins).toContain('https://custom.example.com');
108
- expect(
109
- origins.some(referencesTheWebDistribution),
110
- 'a custom domain that still allows only the CloudFront import is a blocked browser',
111
- ).toBe(false);
112
- });
113
- });
114
-
115
- // Two files read the same context key, and they once disagreed by one value:
116
- // the entrypoint asked `override === undefined` while `resolveAllowedOrigins`
117
- // treated `undefined` OR `null` as "nothing given". A `null` — writable in
118
- // `cdk.json`, `cdk.context.json` or `CDK_CONTEXT_JSON`, though never producible
119
- // by the `-c` flag — landed in the gap, and the entrypoint dropped the wired
120
- // origin as if an override existed while the stack fell back to a localhost no
121
- // deploy is served from. Green synth, blocked browser: this branch's whole
122
- // failure class. One predicate now, imported; this pins that it stays one.
123
- describe('a context that names no origin is not an override', () => {
124
- it('keeps the wired CloudFront origin when allowedOrigins is present but null', async () => {
125
- const origins = await withCdkContext({ allowedOrigins: null }, () => originsOfComposedApp());
126
-
127
- expect(
128
- origins.some(referencesTheWebDistribution),
129
- 'a null context must compose exactly like no context at all',
130
- ).toBe(true);
131
- // Asked of the resolver rather than spelled out: a literal copy of the
132
- // localhost default passes vacuously the moment the default changes — it
133
- // then forbids an origin nothing could have produced, and the leak this
134
- // line exists to catch walks straight through.
135
- expect(origins).not.toContain(resolveAllowedOrigins(undefined, undefined)[0]);
136
- });
137
- });
@@ -1,120 +0,0 @@
1
- import { App } from 'aws-cdk-lib';
2
- import { Match, Template } from 'aws-cdk-lib/assertions';
3
- import { describe, expect, it } from 'vitest';
4
- import { AppStack } from '../lib/app-stack.js';
5
-
6
- // `Template.fromStack` bundles the Lambda entries with esbuild — slow-ish, so
7
- // it runs once and is shared. Deliberately not in `beforeAll`: this project's
8
- // `vitest.config.ts` raises `testTimeout` for exactly this cost and leaves
9
- // `hookTimeout` at vitest's 10s default, so the same work in a hook runs on a
10
- // much shorter fuse — and it is contended, because sibling workers are
11
- // bundling too.
12
- let synthesised: Template | undefined;
13
- const template = (): Template =>
14
- (synthesised ??= Template.fromStack(new AppStack(new App(), 'TestStack')));
15
-
16
- describe('storage', () => {
17
- it('creates the single table with pk/sk and on-demand billing', () => {
18
- template().hasResourceProperties('AWS::DynamoDB::Table', {
19
- KeySchema: [
20
- { AttributeName: 'pk', KeyType: 'HASH' },
21
- { AttributeName: 'sk', KeyType: 'RANGE' },
22
- ],
23
- BillingMode: 'PAY_PER_REQUEST',
24
- });
25
- template().resourceCountIs('AWS::DynamoDB::Table', 1);
26
- });
27
- });
28
-
29
- describe('queue discipline', () => {
30
- it('wires the DLQ with maxReceiveCount 3', () => {
31
- template().hasResourceProperties('AWS::SQS::Queue', {
32
- RedrivePolicy: Match.objectLike({ maxReceiveCount: 3 }),
33
- });
34
- });
35
-
36
- it('alarms as soon as one message reaches the DLQ', () => {
37
- template().hasResourceProperties('AWS::CloudWatch::Alarm', {
38
- MetricName: 'ApproximateNumberOfMessagesVisible',
39
- Threshold: 1,
40
- EvaluationPeriods: 1,
41
- ComparisonOperator: 'GreaterThanOrEqualToThreshold',
42
- });
43
- });
44
-
45
- it('feeds the worker one message at a time', () => {
46
- template().hasResourceProperties('AWS::Lambda::EventSourceMapping', {
47
- BatchSize: 1,
48
- });
49
- });
50
- });
51
-
52
- describe('functions and routes', () => {
53
- it('deploys exactly the create, list and worker functions — one purpose each', () => {
54
- template().resourceCountIs('AWS::Lambda::Function', 3);
55
- });
56
-
57
- it('passes table and queue to the api via environment', () => {
58
- template().hasResourceProperties('AWS::Lambda::Function', {
59
- Environment: {
60
- Variables: Match.objectLike({
61
- TABLE_NAME: Match.anyValue(),
62
- QUEUE_URL: Match.anyValue(),
63
- }),
64
- },
65
- });
66
- });
67
-
68
- it('exposes exactly two routes: POST /notes and GET /notes', () => {
69
- template().hasResourceProperties('AWS::ApiGatewayV2::Route', {
70
- RouteKey: 'POST /notes',
71
- });
72
- template().hasResourceProperties('AWS::ApiGatewayV2::Route', {
73
- RouteKey: 'GET /notes',
74
- });
75
- template().resourceCountIs('AWS::ApiGatewayV2::Route', 2);
76
- });
77
-
78
- it('allows the browser origin in: CORS is configured', () => {
79
- template().hasResourceProperties('AWS::ApiGatewayV2::Api', {
80
- CorsConfiguration: Match.objectLike({
81
- AllowMethods: Match.arrayWith(['GET', 'POST']),
82
- }),
83
- });
84
- });
85
-
86
- it('never allows every origin: the api names who may call it', () => {
87
- const apis = template().findResources('AWS::ApiGatewayV2::Api');
88
- const origins = Object.values(apis).flatMap(
89
- (api) =>
90
- (api.Properties as { CorsConfiguration?: { AllowOrigins?: unknown[] } }).CorsConfiguration
91
- ?.AllowOrigins ?? [],
92
- );
93
-
94
- // A starter multiplies whatever it ships: `*` here becomes the default of
95
- // every project generated from it. The web bundle has a known origin.
96
- expect(origins.length).toBeGreaterThan(0);
97
- expect(origins).not.toContain('*');
98
- });
99
- });
100
-
101
- describe('least-privilege IAM', () => {
102
- it('grants writes to the creator, reads to the lister, queue send — nothing broad', () => {
103
- const policies = template().findResources('AWS::IAM::Policy');
104
- const statements = Object.values(policies).flatMap(
105
- (policy) =>
106
- (policy.Properties as { PolicyDocument: { Statement: Array<Record<string, unknown>> } })
107
- .PolicyDocument.Statement,
108
- );
109
- const actions = statements.flatMap((s) =>
110
- Array.isArray(s.Action) ? (s.Action as string[]) : [s.Action as string],
111
- );
112
-
113
- expect(actions).toContain('dynamodb:PutItem');
114
- expect(actions).toContain('dynamodb:Scan'); // the list function reads
115
- expect(actions).toContain('sqs:SendMessage');
116
- // Least privilege: nothing broad.
117
- expect(actions).not.toContain('dynamodb:*');
118
- expect(actions.some((a) => a === '*')).toBe(false);
119
- });
120
- });
@@ -1,44 +0,0 @@
1
- import { App } from 'aws-cdk-lib';
2
- import { Match, Template } from 'aws-cdk-lib/assertions';
3
- import { describe, it } from 'vitest';
4
- import { WebStack } from '../lib/web-stack.js';
5
-
6
- // The web stack is stateless serving only: a private bucket behind CloudFront.
7
- // The bundle itself is synced by the deploy step (see README) — synth stays
8
- // independent of `next build`.
9
- //
10
- // Synthesised lazily rather than in a `beforeAll`, like its neighbours in this
11
- // directory: `vitest.config.ts` raises `testTimeout` and leaves `hookTimeout`
12
- // at 10 s, so a hook is the one place in the suite running on a short fuse.
13
- // This stack bundles no Lambda and takes ~370 ms — the reason to do it anyway
14
- // is that the next person copies whatever shape is here.
15
- let cached: Template | undefined;
16
- const template = () => (cached ??= Template.fromStack(new WebStack(new App(), 'TestWebStack')));
17
-
18
- describe('web serving stack', () => {
19
- it('keeps the bucket fully private', () => {
20
- template().hasResourceProperties('AWS::S3::Bucket', {
21
- PublicAccessBlockConfiguration: Match.objectLike({
22
- BlockPublicAcls: true,
23
- BlockPublicPolicy: true,
24
- IgnorePublicAcls: true,
25
- RestrictPublicBuckets: true,
26
- }),
27
- });
28
- });
29
-
30
- it('serves through CloudFront with origin access control', () => {
31
- template().resourceCountIs('AWS::CloudFront::Distribution', 1);
32
- template().resourceCountIs('AWS::CloudFront::OriginAccessControl', 1);
33
- template().hasResourceProperties('AWS::CloudFront::Distribution', {
34
- DistributionConfig: Match.objectLike({
35
- DefaultRootObject: 'index.html',
36
- }),
37
- });
38
- });
39
-
40
- it('exports the distribution domain and bucket name for the deploy step', () => {
41
- template().hasOutput('WebBucketName', {});
42
- template().hasOutput('WebUrl', {});
43
- });
44
- });
@@ -1,30 +0,0 @@
1
- {
2
- "name": "@app/root",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "engines": {
7
- "node": ">=20"
8
- },
9
- "scripts": {
10
- "test": "vitest run",
11
- "lint": "eslint .",
12
- "typecheck": "tsc -p tsconfig.json && tsc -p apps/web/tsconfig.json",
13
- "build:web": "pnpm --filter @app/web build",
14
- "synth": "pnpm --filter @app/infra synth",
15
- "deploy": "pnpm --filter @app/infra deploy",
16
- "check": "pnpm lint && pnpm typecheck && pnpm test && pnpm build:web && pnpm synth"
17
- },
18
- "devDependencies": {
19
- "@eslint/js": "^10.0.1",
20
- "@types/aws-lambda": "^8.10.162",
21
- "@types/node": "^26.1.1",
22
- "esbuild": "^0.28.1",
23
- "eslint": "^10.7.0",
24
- "globals": "^17.7.0",
25
- "tsx": "^4.23.1",
26
- "typescript": "^6.0.3",
27
- "typescript-eslint": "^8.65.0",
28
- "vitest": "^4.1.10"
29
- }
30
- }
@@ -1,11 +0,0 @@
1
- {
2
- "name": "@app/core",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "main": "src/index.ts",
7
- "types": "src/index.ts",
8
- "dependencies": {
9
- "zod": "^4.4.3"
10
- }
11
- }
@@ -1,14 +0,0 @@
1
- // Domain event vocabulary. The schema is the contract between the API (producer)
2
- // and the worker (consumer) — both sides validate against it.
3
- import { z } from 'zod';
4
- import { NoteSchema, type Note } from './note.js';
5
-
6
- export const NoteCreatedEventSchema = z.object({
7
- type: z.literal('note.created'),
8
- note: NoteSchema,
9
- });
10
- export type NoteCreatedEvent = z.infer<typeof NoteCreatedEventSchema>;
11
-
12
- export function makeNoteCreatedEvent(note: Note): NoteCreatedEvent {
13
- return { type: 'note.created', note };
14
- }