AUTH4D-55 feature-completion contracts
This ticket freezes the wire shapes and storage for application branding, per-application device
settings, the guest access-token claim and project team management. It adds no routes. The schemas
are in packages/contracts/src/index.ts and the storage is
infra/cloudflare/d1/migrations/0008_feature_completion.sql (see docs/operations/d1-schema.md).
The tickets that implement each endpoint own its route documentation.
Branding
BrandingSchema is strict and every field is optional:
{
"accent_color": "#1d4ed8",
"support_url": "https://support.example.test/help",
"privacy_policy_url": "https://example.test/privacy"
}
accent_colormust be exactly#rrggbb.- URLs must start with
https://, contain only printable ASCII and no credentials, and be at most 2048 characters. There is no loopbackhttp:exception. - Unknown fields are rejected.
Management updates use BrandingUpdateSchema. It takes the same fields; null clears a field and
an omitted field is unchanged. At least one field is required. Hosted pages still check contrast
and ignore unknown fields (AUTH4D-23).
Device settings
DeviceSettingsSchema describes a native application’s effective settings. Omitted fields take
DEFAULT_DEVICE_SETTINGS, which equal the values used before this ticket:
| Field | Default | Bounds |
|---|---|---|
user_code_length |
8 | 8–12 |
code_ttl_seconds |
600 | 120–1800 |
poll_interval_seconds |
5 | 5–60 |
The user-code length cannot drop below the default, so codes are never easier to brute-force than
they are today. DeviceSettingsUpdateSchema uses null to reset a field to its default.
resolveDeviceSettings() maps the nullable D1 columns to effective settings.
DeviceUserCodeSchema and DeviceAuthorizationResponseSchema are unchanged and still describe the
defaults. Once per-application settings are applied, use these schemas instead:
ConfiguredDeviceUserCodeSchemaaccepts 8–12 characters fromA-Z0-9.ConfiguredDeviceAuthorizationResponseSchemaallows a boundedexpires_inandinterval.ConfiguredDeviceApprovalRequestSchemaaccepts a configured user code.
Hosted page data
LoginContextResponseSchema and DeviceVerifyResponseSchema validate the responses described
under “Required API additions” in AUTH4D-23 (repository file docs/api-spec/auth4d-23.md). Both are strict and embed
BrandingSchema. Tenant names are limited to 100 characters, application names to 80, and login
context origins to canonical origins. user_code in the device verification response accepts any
configured length.
Guest claim
Access tokens may carry "auth4_guest": true (GUEST_CLAIM) while the subject is an
un-upgraded guest. The claim is optional and boolean, and an absent claim means “not a guest”. It
is namespaced so it cannot collide with OIDC claims, and ID tokens reject it.
Project members and invitations
Project roles reuse migration 0004’s memberships.project_role: ProjectRoleSchema is
owner | admin | viewer. InvitableProjectRoleSchema is admin | viewer. It is the only role
type accepted by invitations and member role updates, so ownership can never be granted this way.
| Schema | Shape |
|---|---|
CreateProjectInvitationRequestSchema |
{ email, role }; the email is trimmed and lowercased |
ProjectInvitationResponseSchema |
{ invitation } |
ProjectInvitationListResponseSchema |
{ invitations: [...] } (at most 1000) |
AcceptProjectInvitationRequestSchema |
{ token }: 43 base64url characters, sent in the body |
AcceptProjectInvitationResponseSchema |
{ tenant, membershipId, role } |
ProjectMemberListResponseSchema |
{ members: [{ membershipId, userId, role, email?, createdAt }] } |
UpdateProjectMemberRoleRequestSchema |
{ role }, where role is admin or viewer |
ProjectMemberResponseSchema |
{ member } |
An invitation record has these fields: tenantId, invitationId (inv_*), email, role,
invitedBy, createdAt, expiresAt, and the optional acceptedAt and revokedAt. Its status
is one of pending, accepted, revoked or expired. acceptedAt and revokedAt must agree
with the status. Invitations expire within PROJECT_INVITATION_EXPIRY_SECONDS (seven days).
The raw invitation token is only ever delivered by email. It never appears in a management response, a log or an audit event, and it is never stored, because D1 keeps only its hash. Accepting an invitation is a POST with the token in the JSON body. An email link that carries the token should use a URL fragment, which the page reads and then removes, so the token never reaches a server log.
Tests
pnpm exec vitest run packages/contractsruns the schema tests. They parse the JSON examples inauth4d-23.mddirectly.pnpm --filter @auth4/db testruns the migration tests against a local D1.
Source: docs/api-spec/auth4d-55.md