---
id: ATS-PFM-SEC-008
scope: CAP-PFM-SEC-008
product_area: platform
module: security
revision: 2
status: proposed
approved_by: null
approved_at: null
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
scenarios:
  - id: ATS-PFM-SEC-008-01
    title: Check invitation customer boundaries
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-01
    preconditions: Prepare two customers with memberships.
    steps: >-
      Invite into the other customer and act on its member and on a missing
      identity.
    expected: >-
      Out-of-boundary and missing cases return 404 without creating a person;
      public acceptance endpoints declare no permission.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        an invitation into a customer the caller does not reach, as 404, and
        leaves nothing behind
      - TST-PFM-SEC-060
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        to act on a user outside the caller's scope and an unknown one, telling
        the two apart for nobody
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#declares
        the lifecycle routes with the one permission that gives access, and the
        public ones with none
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
      an invitation into a customer the caller does not reach, as 404, and
      leaves nothing behind
    category: boundary
    test_references:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        an invitation into a customer the caller does not reach, as 404, and
        leaves nothing behind
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        to act on a user outside the caller's scope and an unknown one, telling
        the two apart for nobody
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#declares
        the lifecycle routes with the one permission that gives access, and the
        public ones with none
  - id: ATS-PFM-SEC-008-02
    title: Check address requirements and normalization
    level: unit
    proves:
      - BR-CAP-PFM-SEC-008-02
    preconditions: Prepare otherwise valid invitation input.
    steps: 'Try missing, invalid and mixed-case addresses.'
    expected: >-
      Missing and invalid addresses fail without creation; the accepted address
      is stored canonically.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invite-user.spec.js#refuses
        an invalid e-mail address as ObjectInvalid naming email_address,
        creating nobody
      - TST-PFM-SEC-210
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invite-user.spec.js#refuses
        an invitation with no e-mail address, because nothing could ever be
        delivered
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invite-user.spec.js#invites
        the person under the canonical form of the address it was given
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/services/invitations/invite-user.spec.js#refuses
      an invalid e-mail address as ObjectInvalid naming email_address, creating
      nobody
    category: validation
    test_references:
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invite-user.spec.js#refuses
        an invalid e-mail address as ObjectInvalid naming email_address,
        creating nobody
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invite-user.spec.js#refuses
        an invitation with no e-mail address, because nothing could ever be
        delivered
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invite-user.spec.js#invites
        the person under the canonical form of the address it was given
  - id: ATS-PFM-SEC-008-03
    title: Check duplicate and returning memberships
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-03
    preconditions: 'Prepare active, invited and revoked members.'
    steps: Invite each identity again.
    expected: >-
      Active and invited members receive their respective existing-member codes
      and guidance; the revoked member reuses the existing record.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        an invitation to somebody who has already accepted, pointing at their
        roles instead
      - TST-PFM-SEC-060
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        a second invitation to somebody already invited here, telling the
        administrator to resend
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#invites
        a revoked member back onto the row and the account they already had
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
      an invitation to somebody who has already accepted, pointing at their
      roles instead
    category: transition
    test_references:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        an invitation to somebody who has already accepted, pointing at their
        roles instead
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        a second invitation to somebody already invited here, telling the
        administrator to resend
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#invites
        a revoked member back onto the row and the account they already had
  - id: ATS-PFM-SEC-008-04
    title: Check committed records survive delivery failure
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-04
    preconditions: Prepare a taken sign-in alias and a failing mail provider.
    steps: 'Invite in both conditions, then restore the provider and resend.'
    expected: >-
      Delivery reports refusal or failure and its reason without exposing a
      token; committed records persist and resend can deliver.
    negative: false
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#creates
        the user, provisions the account and e-mails the token in the request,
        answering that it was sent and no token
      - TST-PFM-SEC-060
      - >-
        services-platform:modules/security/tests/integration/lifecycle/user-provisioning/user-provisioning.integration.spec.js#answers
        the delivery as refused
      - TST-PFM-SEC-084
      - >-
        services-platform:modules/security/tests/integration/lifecycle/user-provisioning/user-provisioning.integration.spec.js#still
        answers the user and the invitation it committed
      - >-
        services-platform:modules/security/tests/integration/lifecycle/user-provisioning/user-provisioning.integration.spec.js#creates
        the account the invite could not
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#creates
      the user, provisions the account and e-mails the token in the request,
      answering that it was sent and no token
    category: failure
    test_references:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#creates
        the user, provisions the account and e-mails the token in the request,
        answering that it was sent and no token
      - >-
        services-platform:modules/security/tests/integration/lifecycle/user-provisioning/user-provisioning.integration.spec.js#answers
        the delivery as refused
      - >-
        services-platform:modules/security/tests/integration/lifecycle/user-provisioning/user-provisioning.integration.spec.js#still
        answers the user and the invitation it committed
      - >-
        services-platform:modules/security/tests/integration/lifecycle/user-provisioning/user-provisioning.integration.spec.js#creates
        the account the invite could not
  - id: ATS-PFM-SEC-008-05
    title: Check role delegation and self-invitation
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-05
    preconditions: Prepare an inviter allowed a limited set of permissions.
    steps: >-
      Offer an allowed role, a role exceeding those permissions and a role to
      the inviter’s own account.
    expected: >-
      The allowed offer succeeds; excessive delegation names missing keys and
      self-invitation fails.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#lets
        a caller grant a role whose every key they are allowed where they are
        granting it
      - TST-PFM-SEC-060
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        a role that grants more than the caller may do there, and names what was
        missing
      - >-
        services-platform:modules/security/tests/unit/services/invitations/widening.spec.ts#refuses
        a caller granting to their own user, whatever they hold
      - TST-PFM-SEC-213
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#lets
      a caller grant a role whose every key they are allowed where they are
      granting it
    category: permission
    test_references:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#lets
        a caller grant a role whose every key they are allowed where they are
        granting it
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        a role that grants more than the caller may do there, and names what was
        missing
      - >-
        services-platform:modules/security/tests/unit/services/invitations/widening.spec.ts#refuses
        a caller granting to their own user, whatever they hold
  - id: ATS-PFM-SEC-008-06
    title: Check pending access and membership scope
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-06
    preconditions: Prepare an invited membership.
    steps: >-
      Read context before acceptance and offer grants outside the customer or
      beyond a portal member’s workspace.
    expected: >-
      Pre-acceptance access is refused and invalid grant boundaries fail before
      a new member is created.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        the seeded invited user every signed request until it accepts
      - TST-PFM-SEC-060
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invite-user.spec.js#refuses
        a grant that reaches outside the wall the user is being created in,
        before a user exists
      - TST-PFM-SEC-210
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invite-user.spec.js#refuses
        a member's grant that is not tenant-scoped, or names another tenant
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        a grant that reaches outside the wall the user is being created in
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
      the seeded invited user every signed request until it accepts
    category: invariant
    test_references:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        the seeded invited user every signed request until it accepts
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invite-user.spec.js#refuses
        a grant that reaches outside the wall the user is being created in,
        before a user exists
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invite-user.spec.js#refuses
        a member's grant that is not tenant-scoped, or names another tenant
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        a grant that reaches outside the wall the user is being created in
  - id: ATS-PFM-SEC-008-07
    title: Check resend invalidates the preceding link
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-07
      - INV-DOM-PFM-SEC-003-03
    preconditions: Prepare invited and accepted memberships.
    steps: Resend each invitation.
    expected: >-
      The invited member receives a new usable link and the old link fails; the
      accepted member receives recovery guidance.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#supersedes
        the old token on a resend, so only the newest link works
      - TST-PFM-SEC-060
      - >-
        services-platform:modules/security/tests/unit/services/invitations/resend-invitation.spec.js#refuses
        an account that has already accepted, telling the administrator to reset
        the password instead
      - TST-PFM-SEC-211
      - >-
        react-console:e2e/security/invitations.spec.ts#resending from the
        console invalidates the link it replaces
      - TST-PFM-SEC-031
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#supersedes
      the old token on a resend, so only the newest link works
    category: transition
    test_references:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#supersedes
        the old token on a resend, so only the newest link works
      - >-
        services-platform:modules/security/tests/unit/services/invitations/resend-invitation.spec.js#refuses
        an account that has already accepted, telling the administrator to reset
        the password instead
      - >-
        react-console:e2e/security/invitations.spec.ts#resending from the
        console invalidates the link it replaces
  - id: ATS-PFM-SEC-008-08
    title: Check withdrawal prevents acceptance
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-08
    preconditions: Prepare a member with an outstanding link.
    steps: Withdraw membership and attempt to use the link.
    expected: Acceptance fails and cannot restore the revoked membership.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#revoking
        a membership stops the link already in the inbox, and the account cannot
        come back by accepting it
      - TST-PFM-SEC-060
      - >-
        react-console:e2e/security/invitations.spec.ts#revoking from the console
        withdraws the link
      - TST-PFM-SEC-031
      - >-
        react-console:e2e/dev/invitations.dev.spec.ts#says an invitation was
        withdrawn when the administrator revoked it
      - TST-PFM-SEC-013
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#revoking
      a membership stops the link already in the inbox, and the account cannot
      come back by accepting it
    category: transition
    test_references:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#revoking
        a membership stops the link already in the inbox, and the account cannot
        come back by accepting it
      - >-
        react-console:e2e/security/invitations.spec.ts#revoking from the console
        withdraws the link
      - >-
        react-console:e2e/dev/invitations.dev.spec.ts#says an invitation was
        withdrawn when the administrator revoked it
  - id: ATS-PFM-SEC-008-09
    title: Check direct activation prerequisites
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-10
    preconditions: Prepare never-invited linked and currently invited memberships.
    steps: Request activation for both.
    expected: >-
      The linked never-invited membership activates and can read its own record;
      the invited membership is refused.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#links
        the identity from the pool, activates without an invitation, and the
        user then reaches GET /users/me
      - TST-PFM-SEC-060
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        to activate a user with an invitation in flight (the person accepts it,
        or has it resent), and one outside the scope
      - >-
        services-platform:modules/security/tests/unit/services/invitations/activate-user.spec.js#refuses
        a user that was invited (accept or resend instead), an unprovisioned one
        and a disabled one, writing nothing
      - TST-PFM-SEC-206
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#links
      the identity from the pool, activates without an invitation, and the user
      then reaches GET /users/me
    category: validation
    test_references:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#links
        the identity from the pool, activates without an invitation, and the
        user then reaches GET /users/me
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        to activate a user with an invitation in flight (the person accepts it,
        or has it resent), and one outside the scope
      - >-
        services-platform:modules/security/tests/unit/services/invitations/activate-user.spec.js#refuses
        a user that was invited (accept or resend instead), an unprovisioned one
        and a disabled one, writing nothing
  - id: ATS-PFM-SEC-008-10
    title: Check safe invitation-message rendering
    level: unit
    proves:
      - BR-CAP-PFM-SEC-008-12
    preconditions: Prepare display-name markup and newline inputs.
    steps: Render the invitation message.
    expected: >-
      The HTML escapes injected markup, From stays on one line, the subject
      omits the token and the available expiry appears in the body.
    negative: true
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invitation-email.spec.js#lets
        no raw angle bracket of the display name reach the HTML body
      - TST-PFM-SEC-208
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invitation-email.spec.js#strips
        the quote and the newline of an injected display name, leaving one
        header line
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invitation-email.spec.js#keeps
        the raw token out of the subject
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invitation-email.spec.js#names
        the expiry date in the text body when the invitation has one
    provenance: test
    source: >-
      services-platform:modules/security/tests/unit/services/invitations/invitation-email.spec.js#lets
      no raw angle bracket of the display name reach the HTML body
    category: validation
    test_references:
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invitation-email.spec.js#lets
        no raw angle bracket of the display name reach the HTML body
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invitation-email.spec.js#strips
        the quote and the newline of an injected display name, leaving one
        header line
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invitation-email.spec.js#keeps
        the raw token out of the subject
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invitation-email.spec.js#names
        the expiry date in the text body when the invitation has one
  - id: ATS-PFM-SEC-008-11
    title: Check invitation actions from the user list
    level: browser
    proves:
      - AS-JNY-PFM-SEC-003-01
    preconditions: Sign in as a security administrator.
    steps: >-
      Invite a colleague, select the Invited filter and use resend and revoke
      row actions.
    expected: >-
      The new row shows Invited and the two management actions update the
      invitation state.
    negative: false
    data_needs: Use the records described in the setup.
    bound_to:
      - >-
        react-console:e2e/security/invitations.spec.ts#invites a new identity
        from the Users screen
      - TST-PFM-SEC-031
      - >-
        react-console:e2e/mock/security-admin.mock.spec.ts#a new invitation
        appears in the list as Invited
      - TST-PFM-SEC-024
      - >-
        react-console:e2e/mock/security-admin.mock.spec.ts#resends and revokes
        an invitation from a row's actions
    provenance: test
    source: >-
      react-console:e2e/security/invitations.spec.ts#invites a new identity from
      the Users screen
    category: interaction
    test_references:
      - >-
        react-console:e2e/security/invitations.spec.ts#invites a new identity
        from the Users screen
      - >-
        react-console:e2e/mock/security-admin.mock.spec.ts#a new invitation
        appears in the list as Invited
      - >-
        react-console:e2e/mock/security-admin.mock.spec.ts#resends and revokes
        an invitation from a row's actions
  - id: ATS-PFM-SEC-008-12
    title: 'Membership status: invited to active'
    category: transition
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-10
    state_model: STATE-PFM-SEC-004
    transition:
      from: invited
      to: active
      index: 1
    preconditions: >-
      Use an isolated membership status in invited state, with the required
      authority and guard fixtures.
    steps:
      - do: >-
          Directly activate a linked invited member that has never received an
          invitation.
        expect: >-
          Read the resulting membership status as active; inspect the associated
          records and recorded history for this transition.
        provenance: proposed
        source: &ref_0
          - >-
            services-platform:modules/security/services/invitations/activate-user.ts
          - >-
            services-platform at ad3f6a65: modules/security/db/users.table.ts;
            services/invitations/accept-invitation.ts, activate-user.ts,
            invite-user.ts, revoke-user.ts, suspend-user.ts,
            resend-invitation.ts, user-deletion.ts;
            services/membership/lockout.ts, last-administrator.ts.
    expected: >-
      Read the resulting membership status as active; inspect the associated
      records and recorded history for this transition.
    negative: false
    data_needs: >-
      A disposable customer, workspace and named actor; a controllable clock
      where the transition depends on time.
    bound_to: []
    provenance: proposed
    source: *ref_0
    guard_cases: >-
      The invited membership has an active linked identity, no sent invitation
      and the caller has access-management authority.
    side_effects_to_verify:
      - Record activation without marking an invitation accepted
    transition_assertion:
      state: active
      provenance: proposed
      source: *ref_0
  - id: ATS-PFM-SEC-008-13
    title: 'Membership status: invited to revoked'
    category: transition
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-08
    state_model: STATE-PFM-SEC-004
    transition:
      from: invited
      to: revoked
      index: 2
    preconditions: >-
      Use an isolated membership status in invited state, with the required
      authority and guard fixtures.
    steps:
      - do: >-
          Withdraw the invited membership and inspect its offered assignments
          and removal work.
        expect: >-
          Read the resulting membership status as revoked; inspect the
          associated records and recorded history for this transition.
        provenance: proposed
        source: &ref_1
          - >-
            services-platform:modules/security/services/invitations/revoke-user.ts
          - >-
            services-platform at ad3f6a65: modules/security/db/users.table.ts;
            services/invitations/accept-invitation.ts, activate-user.ts,
            invite-user.ts, revoke-user.ts, suspend-user.ts,
            resend-invitation.ts, user-deletion.ts;
            services/membership/lockout.ts, last-administrator.ts.
    expected: >-
      Read the resulting membership status as revoked; inspect the associated
      records and recorded history for this transition.
    negative: false
    data_needs: >-
      A disposable customer, workspace and named actor; a controllable clock
      where the transition depends on time.
    bound_to: []
    provenance: proposed
    source: *ref_1
    guard_cases: >-
      The target is reachable, is not the caller and the caller has
      access-management authority.
    side_effects_to_verify:
      - Revoke outstanding invitations and all assignments
      - >-
        Queue provider disable atomically, then nudge dispatch and submit a
        flush
    transition_assertion:
      state: revoked
      provenance: proposed
      source: *ref_1
  - id: ATS-PFM-SEC-008-14
    title: 'Membership status: revoked to invited'
    category: transition
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-03
    state_model: STATE-PFM-SEC-004
    transition:
      from: revoked
      to: invited
      index: 7
    preconditions: >-
      Use an isolated membership status in revoked state, with the required
      authority and guard fixtures.
    steps:
      - do: >-
          Invite the revoked address again and compare membership identity and
          invitation identity.
        expect: >-
          Read the resulting membership status as invited; inspect the
          associated records and recorded history for this transition.
        provenance: proposed
        source: &ref_2
          - >-
            services-platform:modules/security/services/persistence/users.ts:reinviteRevokedUser
          - >-
            services-platform at ad3f6a65: modules/security/db/users.table.ts;
            services/invitations/accept-invitation.ts, activate-user.ts,
            invite-user.ts, revoke-user.ts, suspend-user.ts,
            resend-invitation.ts, user-deletion.ts;
            services/membership/lockout.ts, last-administrator.ts.
    expected: >-
      Read the resulting membership status as invited; inspect the associated
      records and recorded history for this transition.
    negative: false
    data_needs: >-
      A disposable customer, workspace and named actor; a controllable clock
      where the transition depends on time.
    bound_to: []
    provenance: proposed
    source: *ref_2
    guard_cases: Delegation checks permit the newly offered assignments.
    side_effects_to_verify:
      - >-
        Reuse membership identity and history while creating a new invitation
        and pending grants
    transition_assertion:
      state: invited
      provenance: proposed
      source: *ref_2
  - id: ATS-PFM-SEC-008-15
    title: 'Invitation: issued to live'
    category: transition
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-04
    state_model: STATE-PFM-SEC-005
    transition:
      from: issued
      to: live
      index: 0
    preconditions: >-
      Use an isolated invitation in issued state, with the required authority
      and guard fixtures.
    steps:
      - do: >-
          Prepare the provider account and issue the invitation token before
          delivery.
        expect: >-
          Read the resulting invitation as live; inspect the associated records
          and recorded history for this transition.
        provenance: proposed
        source: &ref_3
          - >-
            services-platform:modules/security/services/persistence/invitations.ts:issueInvitationToken
          - >-
            services-platform at ad3f6a65:
            modules/security/db/user-invitations.table.ts;
            services/persistence/invitations.ts;
            services/invitations/invitation-lifetime.ts, resend-invitation.ts,
            accept-invitation.ts; services/provisioning/provision-user.ts.
    expected: >-
      Read the resulting invitation as live; inspect the associated records and
      recorded history for this transition.
    negative: false
    data_needs: >-
      A disposable customer, workspace and named actor; a controllable clock
      where the transition depends on time.
    bound_to: []
    provenance: proposed
    source: *ref_3
    guard_cases: >-
      The pool account has been prepared; delivery-policy approval is checked
      after token issuance.
    side_effects_to_verify:
      - Store the token digest and record the delivery attempt separately
    transition_assertion:
      state: live
      provenance: proposed
      source: *ref_3
  - id: ATS-PFM-SEC-008-16
    title: 'Invitation: live to superseded'
    category: transition
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-07
    state_model: STATE-PFM-SEC-005
    transition:
      from: live
      to: superseded
      index: 3
    preconditions: >-
      Use an isolated invitation in live state, with the required authority and
      guard fixtures.
    steps:
      - do: Resend the invitation and attempt to use the replaced link.
        expect: >-
          Read the resulting invitation as superseded; inspect the associated
          records and recorded history for this transition.
        provenance: proposed
        source: &ref_4
          - >-
            services-platform:modules/security/services/persistence/invitations.ts:supersedeOutstandingInvitations
          - >-
            services-platform at ad3f6a65:
            modules/security/db/user-invitations.table.ts;
            services/persistence/invitations.ts;
            services/invitations/invitation-lifetime.ts, resend-invitation.ts,
            accept-invitation.ts; services/provisioning/provision-user.ts.
    expected: >-
      Read the resulting invitation as superseded; inspect the associated
      records and recorded history for this transition.
    negative: false
    data_needs: >-
      A disposable customer, workspace and named actor; a controllable clock
      where the transition depends on time.
    bound_to: []
    provenance: proposed
    source: *ref_4
    guard_cases: The membership remains invited.
    side_effects_to_verify:
      - Replace the invitation in one transaction
    transition_assertion:
      state: superseded
      provenance: proposed
      source: *ref_4
  - id: ATS-PFM-SEC-008-17
    title: 'Invitation: expired to superseded'
    category: transition
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-07
    state_model: STATE-PFM-SEC-005
    transition:
      from: expired
      to: superseded
      index: 4
    preconditions: >-
      Use an isolated invitation in expired state, with the required authority
      and guard fixtures.
    steps:
      - do: Replace the expired invitation and compare the new validity window.
        expect: >-
          Read the resulting invitation as superseded; inspect the associated
          records and recorded history for this transition.
        provenance: proposed
        source: &ref_5
          - >-
            services-platform:modules/security/services/invitations/resend-invitation.ts
          - >-
            services-platform at ad3f6a65:
            modules/security/db/user-invitations.table.ts;
            services/persistence/invitations.ts;
            services/invitations/invitation-lifetime.ts, resend-invitation.ts,
            accept-invitation.ts; services/provisioning/provision-user.ts.
    expected: >-
      Read the resulting invitation as superseded; inspect the associated
      records and recorded history for this transition.
    negative: false
    data_needs: >-
      A disposable customer, workspace and named actor; a controllable clock
      where the transition depends on time.
    bound_to: []
    provenance: proposed
    source: *ref_5
    guard_cases: The membership remains invited.
    side_effects_to_verify:
      - Create a replacement with a fresh seven-day deadline
    transition_assertion:
      state: superseded
      provenance: proposed
      source: *ref_5
  - id: ATS-PFM-SEC-008-18
    title: 'Invitation: issued to superseded'
    category: transition
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-07
    state_model: STATE-PFM-SEC-005
    transition:
      from: issued
      to: superseded
      index: 5
    preconditions: >-
      Use an isolated invitation in issued state, with the required authority
      and guard fixtures.
    steps:
      - do: >-
          Resend an invitation whose provider preparation has not yet produced a
          token.
        expect: >-
          Read the resulting invitation as superseded; inspect the associated
          records and recorded history for this transition.
        provenance: proposed
        source: &ref_6
          - >-
            services-platform:modules/security/services/invitations/resend-invitation.ts
          - >-
            services-platform at ad3f6a65:
            modules/security/db/user-invitations.table.ts;
            services/persistence/invitations.ts;
            services/invitations/invitation-lifetime.ts, resend-invitation.ts,
            accept-invitation.ts; services/provisioning/provision-user.ts.
    expected: >-
      Read the resulting invitation as superseded; inspect the associated
      records and recorded history for this transition.
    negative: false
    data_needs: >-
      A disposable customer, workspace and named actor; a controllable clock
      where the transition depends on time.
    bound_to: []
    provenance: proposed
    source: *ref_6
    guard_cases: The membership remains invited.
    side_effects_to_verify:
      - Create a new invitation and run post-commit preparation
    transition_assertion:
      state: superseded
      provenance: proposed
      source: *ref_6
  - id: ATS-PFM-SEC-008-19
    title: 'Role assignment: pending to revoked'
    category: transition
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-08
    state_model: STATE-PFM-SEC-006
    transition:
      from: pending
      to: revoked
      index: 5
    preconditions: >-
      Use an isolated role assignment in pending state, with the required
      authority and guard fixtures.
    steps:
      - do: >-
          Withdraw the invitation and inspect the pending assignment’s retained
          row.
        expect: >-
          Read the resulting role assignment as revoked; inspect the associated
          records and recorded history for this transition.
        provenance: proposed
        source: &ref_7
          - >-
            services-platform:modules/security/services/persistence/assignments.ts:revokeAssignmentsOfUser
          - >-
            services-platform at ad3f6a65:
            modules/security/db/security-assignments.table.ts;
            services/persistence/assignments.ts;
            services/membership/manage-assignments.ts;
            services/invitations/widening.ts.
    expected: >-
      Read the resulting role assignment as revoked; inspect the associated
      records and recorded history for this transition.
    negative: false
    data_needs: >-
      A disposable customer, workspace and named actor; a controllable clock
      where the transition depends on time.
    bound_to: []
    provenance: proposed
    source: *ref_7
    guard_cases: The owning membership capability performs withdrawal.
    side_effects_to_verify: []
    transition_assertion:
      state: revoked
      provenance: proposed
      source: *ref_7
  - id: ATS-PFM-SEC-008-20
    title: Offer and manage an invitation with join through an invitation
    category: interaction
    level: api
    proves:
      - BR-CAP-PFM-SEC-005-03
      - BR-CAP-PFM-SEC-008-07
    exercises:
      - CAP-PFM-SEC-008
      - CAP-PFM-SEC-005
    preconditions: An invited person retains the first invitation link.
    steps: >-
      Resend the invitation, attempt acceptance through the old link, then
      accept through the replacement.
    expected: The old link is superseded; only the replacement activates the membership.
    negative: true
    data_needs: >-
      An isolated customer with two workspaces, ordinary and staff identities,
      and records owned by this run.
    bound_to: []
    provenance: proposed
    source:
      - >-
        services-platform:modules/security/tests/unit/services/invitations/accept-invitation.spec.js#answers
        an expired, a revoked and a superseded token with the same sentence, and
        records which it was
      - >-
        services-platform:modules/security/tests/unit/services/invitations/resend-invitation.spec.js#supersedes
        the outstanding invitations and issues a new one with a new token, in
        one transaction
  - id: ATS-PFM-SEC-008-21
    title: Offer and manage an invitation with maintain console memberships
    category: interaction
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-03
      - BR-CAP-PFM-SEC-009-05
    exercises:
      - CAP-PFM-SEC-008
      - CAP-PFM-SEC-009
    preconditions: A console member has a pending invitation and offered assignments.
    steps: >-
      Revoke the membership, inspect the old offer, and invite the same address
      again.
    expected: >-
      The old offer cannot activate access; reinvitation reuses membership
      identity with a new offer.
    negative: true
    data_needs: >-
      An isolated customer with two workspaces, ordinary and staff identities,
      and records owned by this run.
    bound_to: []
    provenance: proposed
    source:
      - >-
        services-platform:modules/security/tests/unit/services/invitations/invite-user.spec.js#refuses
        somebody already invited to that wall as already_invited, telling the
        administrator to resend
      - >-
        services-platform:modules/security/services/invitations/suspend-user.ts:reinstateUser
  - id: ATS-PFM-SEC-008-22
    title: Offer and manage an invitation with assign roles within a controlled scope
    category: interaction
    level: api
    proves:
      - BR-CAP-PFM-SEC-008-05
      - BR-CAP-PFM-SEC-010-02
    exercises:
      - CAP-PFM-SEC-008
      - CAP-PFM-SEC-010
    preconditions: An access manager can delegate one role but lacks another role’s powers.
    steps: >-
      Invite with the permitted role, then attempt another invitation carrying
      the unavailable role.
    expected: >-
      The first creates pending assignments; the second creates neither
      invitation nor assignment.
    negative: true
    data_needs: >-
      An isolated customer with two workspaces, ordinary and staff identities,
      and records owned by this run.
    bound_to: []
    provenance: proposed
    source:
      - >-
        services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#refuses
        a role that grants more than the caller may do there, and names what was
        missing
      - >-
        services-platform:modules/security/tests/unit/services/invitations/widening.spec.ts#refuses
        a scope the caller does not manage, and says which scope rather than
        which role
evidence:
  - >-
    Pinned services-platform references for the structured rows:
    modules/security/tests/e2e/invitations.e2e.spec.js;
    modules/security/tests/integration/lifecycle/user-provisioning/user-provisioning.integration.spec.js;
    modules/security/tests/unit/services/invitations/activate-user.spec.js;
    modules/security/tests/unit/services/invitations/invitation-email.spec.js;
    modules/security/tests/unit/services/invitations/invite-user.spec.js;
    modules/security/tests/unit/services/invitations/resend-invitation.spec.js;
    modules/security/tests/unit/services/invitations/widening.spec.ts.
  - >-
    Pinned react-console references for the structured rows:
    e2e/dev/invitations.dev.spec.ts; e2e/mock/security-admin.mock.spec.ts;
    e2e/security/invitations.spec.ts.
  - >-
    Testing back-fill reviews the pinned suite assertions, related capabilities,
    domain invariants, state transitions and workflow failure boundaries. New
    unbound rows remain proposed.
coverage:
  rule: 'gap: FND-PFM-SEC-065'
  transition: covered
  validation: covered
  permission: covered
  edge: 'gap: FND-PFM-SEC-065'
  failure: covered
  boundary: covered
  concurrency: 'gap: FND-PFM-SEC-065'
  invariant: covered
  interaction: covered
coverage_notes:
  rule: >-
    No dedicated scenario isolates this category; the coverage finding records
    the missing proof.
  edge: >-
    No dedicated scenario isolates this category; the coverage finding records
    the missing proof.
  concurrency: >-
    No dedicated scenario isolates this category; the coverage finding records
    the missing proof.
invariant_proofs:
  - scenario: ATS-PFM-SEC-008-07
    proves:
      - INV-DOM-PFM-SEC-003-03
    provenance: test
    source: >-
      services-platform:modules/security/tests/e2e/invitations.e2e.spec.js#supersedes
      the old token on a resend, so only the newest link works
permission_proof_review:
  - key: platform.security-access.manage
    scenario_candidates:
      - ATS-PFM-SEC-008-01
      - ATS-PFM-SEC-008-05
      - ATS-PFM-SEC-008-22
    qualification: >-
      Permission-rule scenarios cover the declared authority boundaries;
      exhaustive operation-by-key combinations remain an executable coverage
      gap.
    gap: FND-PFM-SEC-065
  - key: platform.users.read
    scenario_candidates:
      - ATS-PFM-SEC-008-01
      - ATS-PFM-SEC-008-05
      - ATS-PFM-SEC-008-22
    qualification: >-
      Permission-rule scenarios cover the declared authority boundaries;
      exhaustive operation-by-key combinations remain an executable coverage
      gap.
    gap: FND-PFM-SEC-065
---
# Offer and manage an invitation: Capability Test Specification

## Coverage

| Rule | Proved by |
|---|---|
| `BR-CAP-PFM-SEC-008-01` | `ATS-PFM-SEC-008-01` |
| `BR-CAP-PFM-SEC-008-02` | `ATS-PFM-SEC-008-02` |
| `BR-CAP-PFM-SEC-008-03` | `ATS-PFM-SEC-008-03`, `ATS-PFM-SEC-008-14`, `ATS-PFM-SEC-008-21` |
| `BR-CAP-PFM-SEC-008-04` | `ATS-PFM-SEC-008-04`, `ATS-PFM-SEC-008-15` |
| `BR-CAP-PFM-SEC-008-05` | `ATS-PFM-SEC-008-05`, `ATS-PFM-SEC-008-22` |
| `BR-CAP-PFM-SEC-008-06` | `ATS-PFM-SEC-008-06` |
| `BR-CAP-PFM-SEC-008-07` | `ATS-PFM-SEC-008-07`, `ATS-PFM-SEC-008-16`, `ATS-PFM-SEC-008-17`, `ATS-PFM-SEC-008-18`, `ATS-PFM-SEC-008-20` |
| `BR-CAP-PFM-SEC-008-08` | `ATS-PFM-SEC-008-08`, `ATS-PFM-SEC-008-13`, `ATS-PFM-SEC-008-19` |
| `BR-CAP-PFM-SEC-008-10` | `ATS-PFM-SEC-008-09`, `ATS-PFM-SEC-008-12` |
| `BR-CAP-PFM-SEC-008-12` | `ATS-PFM-SEC-008-10` |

The coverage map distinguishes a dedicated scenario from an unresolved category. Proposed scenarios define proof to build; bindings retain the exact assertion references and identify their executable suite.

## Interactions With Related Capabilities

- Join through an invitation: Resend the invitation, attempt acceptance through the old link, then accept through the replacement.
- Maintain console memberships: Revoke the membership, inspect the old offer, and invite the same address again.
- Assign roles within a controlled scope: Invite with the permitted role, then attempt another invitation carrying the unavailable role.

