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 null to 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 ://, no www. and no http:, https:, mailto:, javascript:, data:, tel:, ftp: or file: 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