# Invite Identity Target Spec

> Status: canonical-spec
> Canonical path: `specs/identity/invite-identity.md`
> Scope: email/mobile invitation targets, strict invite identifier resolution, OIDC subject binding, invite discovery, acceptance, and mismatch behavior
> Related specs: [Human, Agent, And Service Identity Spec](./identity.md), [organization workspace access management](../organization/workspace-access-management.md), [organization governance](../organization/organization.md)

## Goal

AgentTeam invitations must be addressed to identifiers that an inviter can know before
the invitee signs in, while invite acceptance must be bound to the authenticated
AgentTeam user after sign-in.

The stable model is:

```text
create invite:
  raw identifier -> strict resolver -> email or mobile target -> copyable invite link

accept invite:
  auth subject/openid -> AgentTeam user
  AgentTeam user's verified email/mobile -> target match
```

The authentication subject is the durable post-login account binding. Email and
mobile are pre-login invitation targets and proof-of-control identifiers. They
are not delivery channels.

## Why OpenID Is Not The Invite Target

AgentTeam Auth and compatible OIDC providers expose a stable subject such as
`sub`/`open_id`. AgentTeam stores that subject on the local user account and uses it
to identify the signed-in account after login.

Inviters generally do not know another person's OIDC subject before that person
logs in. They know an email address or a mobile number. Therefore an invite row
MUST NOT require an OIDC subject as its target. It may be linked to a user after
acceptance, but the pending invite target remains the normalized email or mobile
identifier that was entered when the invite was created.

## Terms

- `auth subject`: the stable external identity subject returned by the
  authentication provider, for example OIDC `sub` / `open_id`.
- `AgentTeam user`: the local `users` row bound to the auth subject after login.
- `invite identifier`: the raw user-entered string used to address an invite.
- `invite target`: the normalized, typed target stored on the invite row.
- `invite target kind`: exactly one of `email` or `mobile`.
- `invite link`: the copyable URL that the inviter sends to the invitee through
  any channel outside the invite target model.
- `verified identity`: a AgentTeam user's email with `email_verified_at`, or mobile
  with `mobile_verified_at`.

## Required Invariants

- An invite target is exactly one normalized email or one normalized mobile.
- Invite creation MUST use one canonical server-side resolver for raw
  identifiers.
- Callers MUST NOT infer target kind from string shape in product code.
- A resolver MAY use string parsing internally, but it MUST return a typed result
  only after strict validation.
- Invite acceptance MUST require a matching verified identity on the current
  authenticated AgentTeam user.
- An auth subject proves which AgentTeam user is signed in; it does not by itself
  prove that a pending email/mobile invite belongs to that user.
- Invite creation MUST surface a copyable invite link for the inviter to share.
- Email and mobile targets MUST NOT imply automatic email or SMS delivery.
- Pending invite discovery, token acceptance, invite-id acceptance, and dismissal
  MUST use the same target-matching semantics.
- UI validation is advisory only. The server resolver is the source of truth.

## Strict Identifier Resolution

The canonical resolver has this shape:

```text
ResolveInviteIdentifier(raw):
  trimmed = trim(raw)
  reject if trimmed is empty

  email = TryNormalizeStrictEmail(trimmed)
  mobile = TryNormalizeStrictMobile(trimmed)

  reject if both are valid
  reject if neither is valid

  if email is valid:
    return { kind: "email", value: normalized_email }

  return { kind: "mobile", value: normalized_mobile }
```

This is not a product-level `if contains("@") then email else mobile` rule. Both
strict validators run, and exactly one validator must accept the value.

### Strict Email

Email normalization MUST:

- reject empty input;
- reject display-name forms such as `Alice <alice@example.com>`;
- reject input containing surrounding comments or multiple addresses;
- normalize the stored address to the canonical lowercase form used by AgentTeam's
  identity layer;
- apply length and syntax checks before returning a target.

### Strict Mobile

Mobile normalization MUST:

- reject empty input;
- reject characters and formats outside the supported phone-number grammar;
- normalize to the canonical E.164-like form used by AgentTeam's identity layer;
- only return a target when the normalized value can later be matched against a
  verified AgentTeam user mobile.

The mobile normalizer may accept deployment-specific shorthand, such as a bare
Mainland China mobile number, only if it normalizes to the same canonical stored
form used for user identity.

## Storage Contract

Invite tables that store targets SHOULD model the target explicitly:

```text
email  nullable
mobile nullable
```

Each pending invite row MUST have exactly one target populated. New schema should
enforce this with a check constraint or equivalent write-side validation. Existing
schemas that already have separate email/mobile columns must still enforce the
same invariant in the service layer until a database constraint exists.

Indexes for pending invite discovery should remain target-specific, because a
user has at most two relevant lookup keys: verified email and verified mobile.
Discovery queries MUST be bounded by those keys, not by a broad scan of pending
invites.

## API Contract

New invite creation APIs SHOULD accept a single raw field:

```json
{
  "identifier": "alice@example.com"
}
```

or:

```json
{
  "identifier": "13800138000"
}
```

The server resolves the field to a typed target. Legacy API fields such as
`email` or `mobile` may remain only as compatibility adapter inputs. They must be
converted to the same typed target before storage and must not create alternate
acceptance semantics.

Response payloads should expose safe presentation fields:

- creation responses should include a copyable `inviteLink`;
- email targets may expose `invitedEmail`;
- mobile targets should expose a redacted `invitedMobileHint`;
- administrative surfaces may expose `targetKind` when it improves clarity.

Raw unredacted mobile values should not be returned to normal invite landing
pages or pending-invite banners.

## Acceptance Contract

All invite acceptance paths must share one target matcher:

```text
InviteMatchesUser(invite, user):
  if invite.email is populated:
    return user.email is verified and user.email == invite.email

  if invite.mobile is populated:
    return user.mobile is verified and user.mobile == invite.mobile

  return false
```

This matcher applies to:

- token-based invite links;
- stable pending invite id links;
- authenticated pending invite lists;
- pending invite dismissal;
- account mismatch checks on invite landing pages.

Acceptance may be idempotent for a user who is already a member of the target
scope, but idempotency does not weaken identity matching.

## Workspace Invites

Workspace invites use the shared invite target model. A workspace invite:

- targets one email or one mobile;
- grants only the invited workspace access after acceptance;
- may create the owning organization membership only when the workspace invite
  policy explicitly allows that side effect;
- must materialize the workspace human member and `#all` scope projection through
  the shared workspace membership path.

Workspace invite token links and pending invite id links are two entry points to
the same acceptance semantics. They must not diverge into email-only and
mobile-only paths.

## Organization Invites

Organization invites may use the same target model. An organization invite:

- targets one email or one mobile;
- grants organization membership after acceptance;
- may project default team or workspace access only according to organization
  invitation policy;
- must require the current AgentTeam user to own the matching verified target.

Organization invites share the same copy-link interaction. Email and mobile
targets identify who may accept the invite; they do not define how the link is
delivered to the invitee.

Automatic email, SMS, or provider-specific notifications may be added as separate
delivery integrations, but those integrations must not be required for invite
creation, discovery, or acceptance semantics.

## Registration Boundary

Auth-side registration invitations and AgentTeam workspace/organization invites are
different products.

Auth-side registration invitations decide whether a person may create or access
an account in the identity provider. AgentTeam invites decide whether an already
authenticated AgentTeam user may join a workspace or organization.

A AgentTeam invite MUST NOT assume that a matching email/mobile target can bypass the
identity provider's registration policy. If an external user cannot create or
access an auth account, the AgentTeam invite remains pending until the auth-side
policy permits sign-in.

## UI Contract

Invite creation UI may use one input labeled as an email or mobile identifier.
The client may provide format hints, but it must not be the authority for target
kind.

After successful creation, the UI must show a copyable invite link. Primary
actions should be worded around link creation and copying, for example
`Generate invite link` or `Copy invite link`, not around sending email or SMS.
Any external message is the inviter's responsibility unless a separate delivery
integration is explicitly enabled.

Invite landing pages and pending invite banners must explain mismatches in terms
of the target identifier:

- email invite mismatch: the signed-in account does not have the invited verified
  email;
- mobile invite mismatch: the signed-in account does not have the invited
  verified mobile.

The UI should offer account switching or identity verification where the product
supports it. It should not tell users to accept an invite with an unverified
email or mobile.

### Account Identity Settings

Profile or general account settings must make invite-eligible identities visible
to the signed-in user. The surface should answer one question clearly: which
email or mobile identifiers can this account use to accept pending invites?

The account identity section should show both identity kinds when the deployment
can receive them from local auth or an external user center:

- Email:
  - show the current email value, or an explicit empty/unknown state;
  - show `verified` only when the AgentTeam user has a verified email;
  - show `unverified` when an email is present but cannot match email-targeted
    invites yet.
- Mobile:
  - show the current mobile value or a redacted/display-safe mobile value;
  - show `verified` only when the AgentTeam user has a verified mobile;
  - show `unverified` or `not set` when it cannot match mobile-targeted invites.

When identity is managed by an external user center, these fields are read-only
inside AgentTeam. AgentTeam may show a management hint or link to the user center, but it
must not present local editing controls that cannot change the upstream
identity. When local password auth or local verification flows are enabled, AgentTeam
may expose local verification controls for the identifiers it owns.

Invite-related mismatch UI may link users to this settings surface when the
needed identity exists but is unverified, or when users need to confirm which
email/mobile belongs to the current signed-in account.

## Non-Goals

- Do not require inviters to know invitee auth subjects.
- Do not add an auth-service reverse lookup from email/mobile to openid as part
  of normal invite creation.
- Do not let raw string shape decide business behavior.
- Do not treat email or mobile targets as automatic delivery channels.
- Do not use unverified profile fields to accept invites.
- Do not make auth-side registration invitations equivalent to workspace or
  organization access grants.

## Implementation Checklist

Implementations that add or change email/mobile invite behavior must verify:

- one canonical strict resolver is used at every invite creation entry point;
- write paths populate exactly one target column;
- creation responses expose a copyable invite link;
- pending discovery considers verified email and verified mobile when present;
- token accept, pending-id accept, and dismiss share the same target matcher;
- mismatch UI uses the same server target semantics;
- mobile targets are redacted in normal user-facing responses;
- query cost is bounded by the current user's verified identifiers or the
  explicitly requested invite id/token.
