AUTH4D-166 hosted page text
Each public application (browser or native) can set three optional plain-text fields that the hosted login, guest upgrade and device approval pages show:
| Field | Limit | Where it appears |
|---|---|---|
headline |
60 characters | Above the page heading, under the app name |
intro |
280 characters | Under the headline |
footer_note |
200 characters | In the page footer, above support/privacy links |
The fields are stored in the clients columns page_headline, page_intro and
page_footer_note (migration 0029), keyed on tenant_id and client_id. Verification emails are
unchanged; their wording is AUTH4D-150 (repository file docs/api-spec/auth4d-150.md).
Rules
PageTextSchema in packages/contracts validates every field. The value is trimmed, then must:
- be 1 to the limit above in characters (an all-space value is rejected; send
nullto clear); - be one line: no control, line-separator, bidi-override, isolate or byte-order-mark characters;
- contain no
<,>,{or}, so it cannot carry HTML or template placeholders; - contain no link: no
://, nowww.and nohttp:,https:,mailto:,javascript:,data:,tel:,ftp:orfile:scheme. The support and privacy URLs remain the only links; - contain no run of six or more digits, so it cannot look like a sign-in code.
The text never replaces built-in wording. Page headings, user codes, scopes, warnings and error messages stay fixed.
Management API
The fields are part of branding in the AUTH4D-56 application document and its update
(BrandingSchema, BrandingUpdateSchema; both additive).
PATCH /management/tenants/{tenantId}/clients/{clientId} (scope client:write, project role
owner or admin)
{
"branding": {
"headline": "Welcome back, pilot",
"intro": "Sign in to sync your hangar and saves across devices.",
"footer_note": null
}
}
null clears a field; omitted fields, including the other branding fields, are unchanged. A value
that breaks a rule fails with 400 validation_failed and details[].field such as
branding.headline, and nothing is written. Re-sending the stored values is a no-op.
GET and PATCH return the application document with the configured fields in branding
(no-store). Unset fields are omitted, and a stored value that no longer passes the contract is
omitted rather than served.
Each change writes one management.client.presentation.updated audit event. Its
changed_fields names the fields (branding.headline, branding.intro, branding.footer_note)
but never their text.
Hosted pages
GET /t/{tenantId}/login/context and GET /device/verify (with Accept: application/json) add the
configured fields to branding:
{
"branding": {
"accent_color": "#1d4ed8",
"headline": "Welcome back, pilot",
"intro": "Sign in to sync your hangar and saves across devices.",
"footer_note": "Starfall is made by a small studio in Cape Town."
}
}
The lookup is keyed on both tenant and client and only covers customer tenants. On a database without migration 0029, the responses carry no page text.
apps/login checks each field again. It removes control and format characters, collapses
whitespace, drops values that are too long or not strings, and renders them with textContent
through h(). The headline and intro go in a .page-text block before the single h1, and the
footer note goes in the footer. The strict CSP is unchanged.
Source: docs/api-spec/auth4d-166.md