Skip to content

Commit e06a6e5

Browse files
committed
Merge remote-tracking branch 'origin/rps-feature' into RPS-1172-check-your-answers
2 parents f20b744 + 6fb16b1 commit e06a6e5

35 files changed

Lines changed: 971 additions & 705 deletions

.github/copilot-instructions.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copilot Instructions
2+
3+
## 🚫 Sensitive Files — Do Not Use for Context
4+
5+
The following files contain sensitive or secret information and must NEVER be used
6+
as context for suggestions, completions, or prompts:
7+
8+
- .env
9+
- \*.env
10+
- .env.\*
11+
- secrets.\*
12+
- credentials.\*
13+
- private.key
14+
- \*.pem
15+
16+
These files may contain:
17+
18+
- API keys
19+
- Tokens
20+
- Passwords
21+
- Private credentials
22+
23+
Copilot must ignore these files entirely.
24+
25+
---
26+
27+
## ✅ Safe Alternatives
28+
29+
When generating code or examples:
30+
31+
- Use placeholder values instead of real secrets
32+
- Refer to `.env.example` for variable names and structure
33+
- Never infer or suggest real-looking secrets
34+
35+
Example:
36+
37+
```env
38+
API_KEY=your-api-key-here
39+
DATABASE_URL=your-database-url-here
40+
```

.gitignore

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,53 @@
1+
# OS files
12
.DS_Store
2-
.vscode
3-
node_modules
4-
npm-debug.log
3+
4+
# Editor
55
.idea
6-
.nyc_output
7-
unit-test.html
6+
7+
# Dependencies
8+
node_modules
89
package-lock.json
9-
/.yarn/cache/
10-
*/.env
11-
.env.local
12-
*/test-results/*
13-
*/test-coverage/*
14-
/runner/tmp.pdf
15-
/model/module/
10+
11+
# Logs
12+
npm-debug.log
13+
14+
# Environment variables
15+
**/.env
16+
**/.env.local
17+
**/.env_mysql
18+
19+
# Build output
20+
**/dist/
21+
**/.dist/
1622
/model/dist/
17-
smoke-tests/designer/reports/
18-
*/dist/
19-
*/.dist/
23+
/model/module/
24+
/queue-model/dist
25+
/queue-model/module
2026
tsconfig.tsbuildinfo
27+
28+
# Generated / Prisma
29+
/queue-model/src/prisma/generated
30+
docs/**/typedoc
31+
32+
# Testing
33+
**/test-results/*
34+
**/test-coverage/*
35+
unit-test.html
36+
.nyc_output
2137
.lighthouseci
38+
/e2e/cypress/screenshots/
39+
smoke-tests/designer/reports/
40+
41+
# Misc artifacts
42+
/runner/tmp.pdf
2243

44+
# Yarn (PnP / Berry)
45+
.pnp.*
2346
.yarn/*
2447
!.yarn/releases
2548
!.yarn/plugins
2649
!.yarn/sdks
2750
!.yarn/versions
28-
.pnp.*
2951
.yarn/build-state.yml
3052
.yarn/install-state.gz
31-
docs/**/typedoc
32-
33-
/e2e/cypress/screenshots/
34-
.env_mysql
35-
/queue-model/dist
36-
/queue-model/module
37-
/queue-model/src/prisma/generated
53+
/.yarn/cache/

.vscode/extensions.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"recommendations": ["esbenp.prettier-vscode"]
3+
}

.vscode/settings.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
// Editor Settings
3+
"editor.formatOnSave": true,
4+
"editor.quickSuggestions": {
5+
"strings": true
6+
},
7+
"editor.suggest.insertMode": "replace",
8+
9+
// Formatters
10+
"[typescript]": {
11+
"editor.defaultFormatter": "esbenp.prettier-vscode"
12+
},
13+
"[javascript]": {
14+
"editor.defaultFormatter": "esbenp.prettier-vscode"
15+
},
16+
"[json]": {
17+
"editor.defaultFormatter": "esbenp.prettier-vscode"
18+
},
19+
"[jsonc]": {
20+
"editor.defaultFormatter": "esbenp.prettier-vscode"
21+
}
22+
}

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# XGov Digital Form Builder
22

3+
## AI Evaluation
4+
5+
![AI Ready](https://img.shields.io/badge/AI-Ready-brightgreen)
6+
37
## Support
48

59
- Join the [community on slack](https://join.slack.com/t/xgov-digital-form-bld/shared_invite/zt-xn5ltztf-3_oBzZaziV4sCpDDOGuP6Q)
@@ -52,6 +56,19 @@ To learn more about workspaces, check these links:
5256
- [Workspaces in Yarn](https://classic.yarnpkg.com/blog/2017/08/02/introducing-workspaces/)
5357
- [Workspaces](https://classic.yarnpkg.com/en/docs/workspaces)
5458

59+
### Docker
60+
61+
For local development purposes, we use docker to run the application stack.
62+
63+
Run the following command to build and start your local development environment:
64+
65+
```bash
66+
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
67+
```
68+
69+
> Note: this process requires docker and docker compose v2 to be installed on your machine.
70+
> If you don't have docker installed, you can use podman instead. Just switch `docker <COMMAND` with `podman <COMMAND`.
71+
5572
### I want to...
5673

5774
#### run a specific workspaces' script

docs/contents.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Features and configuration options for the runner.
2929
as on the [renew or replace your adult passport pages](https://www.gov.uk/renew-adult-passport/renew)
3030
- [redirects](runner/redirects.md)
3131
- Redirect users to a different page in the runner or external URL
32+
- [error page resolution](runner/error-page-resolution.md)
33+
- Serve project or group-specific error pages instead of the generic runner error pages
3234
- [queues](runner/submission-queue.md)
3335
- Submit data to a queue after the user has completed the form
3436
- [summary details transforms](runner/summary-details-transforms.md)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Error page resolution
2+
3+
The runner can serve different error pages per-project (and per group of projects), instead of
4+
falling back to one generic error page for every form hosted by the runner.
5+
6+
## Throwing an application error
7+
8+
Two error classes are available from [`errors.ts`](../../runner/src/server/plugins/engine/errors.ts)
9+
for code that needs to control how an error is displayed:
10+
11+
- `RenderingError`: thrown by the engine itself (e.g. no form or page found for a given id/path).
12+
Takes a `code` (HTTP status code).
13+
- `ControllerError`: thrown from page controllers when a server-side action fails (e.g. a lookup
14+
in a page's `onGet`/`onPost` handler). Takes a `code`, and an optional `page` which names a
15+
specific view to render instead of the generic status-code view.
16+
17+
```ts
18+
import { ControllerError } from "server/plugins/engine/errors";
19+
20+
throw new ControllerError("Failed to find address", {
21+
code: 500,
22+
page: "example-error",
23+
});
24+
```
25+
26+
Both classes are Boom errors under the hood, so throwing one still results in a Boom response
27+
that hapi's `onPreResponse` extension can intercept.
28+
29+
## Resolving the view
30+
31+
The resolution logic lives in [`errorPages.ts`](../../runner/src/server/plugins/errorPages.ts),
32+
registered as the `error-pages` plugin's `onPreResponse` handler. For every Boom response it:
33+
34+
1. Extracts the **form id** from the first segment of the request path
35+
(`extractFormIdFromPath`), e.g. `/my-form/page-one``my-form`.
36+
2. Looks up `server.app.forms[formId]` to get the form's model, and reads its **group** from
37+
`form.def.formGroup` (the optional `formGroup` field on the form JSON).
38+
3. If the thrown error's message identifies it as a `ControllerError` or `RenderingError`, it is
39+
treated as an **application error** and handled by `handleApplicationError`, which resolves a
40+
view by checking, in order, most-to-least specific:
41+
- `page` view (from `ControllerErrorMetadata.page`, if provided) in the **form** folder,
42+
then the **group** folder, then the generic `views/` folder.
43+
- if no `page` view exists (or none was set on the error), the same lookup is repeated using
44+
the numeric status **code** (e.g. `404`) as the view name.
45+
- if none of those exist, the status code itself is used as the view name, which resolves to
46+
the existing top-level view (e.g. `views/404.html`).
47+
4. Any other Boom error falls back to the pre-existing behaviour:
48+
- a `403` renders `csrf-protection` (CSRF token failures), passing the form id and form
49+
name into the view.
50+
- anything else renders the generic `500` view, passing the form name (or the configured
51+
`serviceName`) into the view.
52+
5. If a form cannot be resolved for the form id (e.g. the path doesn't map to a known form),
53+
resolution falls back to the plain `500` view with no form context.
54+
55+
## View lookup order
56+
57+
Given a form id, an optional group, and a view name (either a `page` name or a status code),
58+
`findView` checks folders under `runner/src/server/views/` in this order and returns the first
59+
one that exists on disk:
60+
61+
```
62+
views/<PROJECT_ID>/<name>.html
63+
views/<GROUP>/<name>.html
64+
views/<name>.html
65+
```
66+
67+
For example, a `RenderingError` with `code: 404` thrown while serving a form with id
68+
`my-form` and form group `my-form-group` will look for:
69+
70+
```
71+
views/my-form/404.html
72+
views/my-form-group/404.html
73+
views/404.html
74+
```
75+
76+
A `ControllerError` with `code: 500, page: "example-error"` thrown in the same context will
77+
first look for:
78+
79+
```
80+
views/my-form/example-error.html
81+
views/my-form-group/example-error.html
82+
views/example-error.html
83+
```
84+
85+
and only fall back to the `500` lookup above if none of the `example-error.html` files exist.
86+
87+
## Adding a form or form group error page
88+
89+
Add an `.html` view under a folder matching the `formId` or `formGroup` value, using the same name
90+
(`<code>.html` or a custom page name) as an existing generic view. No registration is required;
91+
`findView` checks for the file's existence on disk at error time via `fs.existsSync`.
92+
93+
```
94+
runner/src/server/views/
95+
├── 404.html # generic fallback
96+
├── my-form-group/
97+
│ └── 404.html # shared by all forms with form group: "my-form-group"
98+
└── my-form/
99+
└── 404.html # used only by the "my-form" project
100+
```

model/src/data-model/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,13 +277,14 @@ export interface SummaryConfig {
277277
* `FormDefinition` is a typescript representation of `Schema`
278278
*/
279279
export type FormDefinition = {
280+
formGroup?: string;
281+
name?: string | undefined;
280282
pages: Array<Page | RepeatingFieldPage>;
281283
conditions: ConditionRawData[];
282284
lists: List[];
283285
sections: Section[];
284286
startPage?: Page["path"] | undefined;
285287
authentication?: boolean | undefined;
286-
name?: string | undefined;
287288
feedback?: Feedback;
288289
phaseBanner?: PhaseBanner;
289290
fees: Fee[];
@@ -310,5 +311,6 @@ export type FormDefinition = {
310311
confirmationSessionTimeout: number | undefined;
311312
returnTo?: boolean | undefined;
312313
addressLookupConfig?: AddressLookupConfig;
314+
error500ContactEmail?: string | undefined;
313315
summaryConfig?: SummaryConfig;
314316
};

model/src/schema/schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ export const Schema = joi
412412
.object()
413413
.required()
414414
.keys({
415+
formGroup: localisedString.optional(),
415416
name: localisedString.optional(),
416417
feedback: feedbackSchema,
417418
startPage: joi.string().required(),
@@ -453,6 +454,7 @@ export const Schema = joi
453454
returnTo: joi.boolean().optional(),
454455
secureFormSubmissionConfig: secureFormSubmissionConfig.optional(),
455456
addressLookupConfig: addressLookupConfigSchema.optional(),
457+
error500ContactEmail: joi.string().optional(),
456458
summaryConfig: summaryConfigSchema.optional(),
457459
});
458460

runner/config/test.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"enforceCsrf": true,
1010
"httpsCookieSecureAttribute": true,
1111
"initialisedSessionKey": "predictable-key",
12+
"sessionCookiePassword": "test-only-session-cookie-password-not-a-secret",
1213
"env": "test",
1314
"documentUploadApiUrl": "http://localhost:9000"
1415
}

0 commit comments

Comments
 (0)