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_color must be exactly #rrggbb.
  • URLs must start with https://, contain only printable ASCII and no credentials, and be at most 2048 characters. There is no loopback http: 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:

  • ConfiguredDeviceUserCodeSchema accepts 8–12 characters from A-Z0-9.
  • ConfiguredDeviceAuthorizationResponseSchema allows a bounded expires_in and interval.
  • ConfiguredDeviceApprovalRequestSchema accepts 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/contracts runs the schema tests. They parse the JSON examples in auth4d-23.md directly.
  • pnpm --filter @auth4/db test runs the migration tests against a local D1.

Source: docs/api-spec/auth4d-55.md