Skip to content

Commit 56e902e

Browse files
committed
docs: add componentSecrets support and document PaymentField secrets
Adds a named componentSecrets feature to the doc generation script so components can declare what per-form secrets they require. Generates a standard "Required secrets" section with a getFormSecret blurb and a secrets table. PaymentField is the first consumer. Also clarifies in custom-services.md that getFormSecret is per-form scoped, distinct from global plugin options such as ordnanceSurveyApiKey, and safe to leave unimplemented when no secret-using components are present.
1 parent 7d03705 commit 56e902e

4 files changed

Lines changed: 49 additions & 2 deletions

File tree

docs/features/code-based/custom-services.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ interface FormsService {
1515
}
1616
```
1717

18-
`getFormMetadata` is called on every request and should be fast. `getFormDefinition` is only called when the metadata signals the definition has changed, so it can be slower. `getFormMetadataById` is called by the status page to retrieve the submitted form's name by its ID — this allows the confirmation panel to display the correct form name even when the current URL belongs to a different form (for example, a shared feedback form). `getFormSecret` is used by components that need secrets (such as API keys) stored outside the form definition.
18+
`getFormMetadata` is called on every request and should be fast. `getFormDefinition` is only called when the metadata signals the definition has changed, so it can be slower. `getFormMetadataById` is called by the status page to retrieve the submitted form's name by its ID — this allows the confirmation panel to display the correct form name even when the current URL belongs to a different form (for example, a shared feedback form).
19+
20+
`getFormSecret` retrieves a secret that belongs to a specific form — for example, a payment API key scoped to that form. This is distinct from global secrets such as `ordnanceSurveyApiKey` and `ordnanceSurveyApiSecret`, which are passed once at plugin registration and apply across all forms. If your forms do not use any components that require per-form secrets, this method will never be called and you can safely return `undefined` (or leave it unimplemented). See the documentation for each component to find out whether it requires secrets and what names it requests.
1921

2022
### Loading forms from files
2123

scripts/component-metadata.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,18 @@
115115
"This component renders an inline Ordnance Survey map that lets users click a location to auto-populate the coordinate inputs across multiple coordinate formats. The map requires the `ordnanceSurveyApiKey` and `ordnanceSurveyApiSecret` [plugin options](../../plugin-options.md#geospatial-map) to be set — without them the component falls back to plain text inputs."
116116
]
117117
},
118+
"componentSecrets": {
119+
"PaymentField": [
120+
{
121+
"name": "payment-test-api-key",
122+
"description": "GOV.UK Pay API key used when the form is in test or draft mode."
123+
},
124+
{
125+
"name": "payment-live-api-key",
126+
"description": "GOV.UK Pay API key used when the form is in live mode."
127+
}
128+
]
129+
},
118130
"pageProperties": {
119131
"components": "Array of component definitions rendered on the page.",
120132
"condition": "Name of a condition that controls whether this page is shown.",

scripts/generate-component-docs.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,7 @@ export function generateComponentMd(
733733
const { options = [], schema = [], props = [] } = interfaceData
734734

735735
const links = metadata.componentLinks?.[componentName] ?? []
736+
const secrets = metadata.componentSecrets?.[componentName] ?? []
736737

737738
// leading '' ensures a blank line between frontmatter and the import
738739
// Level 1 components require client-side JavaScript to render and can't be statically previewed
@@ -821,6 +822,20 @@ export function generateComponentMd(
821822
lines.push(``)
822823
}
823824

825+
if (secrets.length > 0) {
826+
lines.push(`## Required secrets`, ``)
827+
lines.push(
828+
`This component retrieves secrets at runtime via [\`getFormSecret\`](../../code-based/custom-services.md#formsservice) on your \`formsService\`. Implement it to return the correct value from your secrets store — do not use environment variables or plugin options for per-form secrets.`,
829+
``
830+
)
831+
lines.push(`| Secret name | Description |`)
832+
lines.push(`|---|---|`)
833+
for (const secret of secrets) {
834+
lines.push(`| \`${secret.name}\` | ${secret.description} |`)
835+
}
836+
lines.push(``)
837+
}
838+
824839
return lines.join('\n')
825840
}
826841

scripts/generate-component-docs.test.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jest.mock('fs', () => ({
4747
readdirSync: jest.fn(),
4848
readFileSync: jest.fn().mockImplementation((filePath) => {
4949
if (String(filePath ?? '').includes('component-metadata.json')) {
50-
return '{"components":{"TextField":"Single-line text input."},"pages":{"PageController":"The default page type.","RepeatPageController":"Allows repeated answers.","SummaryPageController":"Summary page type."},"properties":{"rows":"Number of rows for the textarea."},"pageProperties":{"repeat.options.name":"Identifier for the repeatable section."}}'
50+
return '{"components":{"TextField":"Single-line text input.","PaymentField":"Redirects the user to GOV.UK Pay."},"pages":{"PageController":"The default page type.","RepeatPageController":"Allows repeated answers.","SummaryPageController":"Summary page type."},"properties":{"rows":"Number of rows for the textarea."},"pageProperties":{"repeat.options.name":"Identifier for the repeatable section."},"componentSecrets":{"PaymentField":[{"name":"payment-test-api-key","description":"GOV.UK Pay API key for test mode."},{"name":"payment-live-api-key","description":"GOV.UK Pay API key for live mode."}]}}'
5151
}
5252
return ''
5353
}),
@@ -436,6 +436,24 @@ describe('Component Documentation Generator', () => {
436436
})
437437
})
438438

439+
describe('generateComponentMd with componentSecrets', () => {
440+
const interfaceData = { options: [], schema: [], props: [] }
441+
442+
it('renders a Required secrets section with blurb and table when secrets are defined', () => {
443+
const result = generateComponentMd('PaymentField', interfaceData, 1)
444+
expect(result).toContain('## Required secrets')
445+
expect(result).toContain('`getFormSecret`')
446+
expect(result).toContain('`payment-test-api-key`')
447+
expect(result).toContain('`payment-live-api-key`')
448+
})
449+
450+
it('omits Required secrets section for components with no secrets', () => {
451+
const result = generateComponentMd('TextField', interfaceData, 1)
452+
expect(result).not.toContain('## Required secrets')
453+
expect(result).not.toContain('getFormSecret')
454+
})
455+
})
456+
439457
describe('buildJsNotice', () => {
440458
it('Level 1: renders a GOV.UK notification banner with banner structure', () => {
441459
const result = buildJsNotice(1, 'Notice text.')

0 commit comments

Comments
 (0)