# Invitation access — local application repair

The owner’s Sales-only invitation was accepted successfully, but the new user received User instead of Sales/LeadEditor. The runtime logged `Failed to add user to group $or argument must be a non-empty array`; the failure was swallowed and the invitation marked completed. Root captured the live reproduction during the roles course. Existing Maya Ito’s record is being repaired separately through owner UI; this source change does not mutate it.

## Repair

`appengine/src/users/users.service.ts` now validates the stored invitation’s access before creating an account. Group references resolve within the invitation organisation; direct and inherited roles are checked for existence and root-tier restrictions, including ID aliases. The public acceptance body cannot supply grants. Malformed or missing grants fail before any user creation or invitation completion.

Validated groups and explicit direct roles are part of the initial signup record. Sales-only means groups `[Sales]`, roles `[]`: no broad User default. Direct role invitations retain those direct roles without adding User. Legacy invitations with no explicit access retain the established User default; workspace-only invitations persist guest status with empty org groups/roles.

Acceptance confirms the stored grants before marking the invitation completed. Existing accounts are no longer falsely marked completed; strict invitation signup also refuses an account found between the initial lookup and signup and propagates lookup failures. Account-write and invitation-write failures return errors.

The subsequent `customerDashboardAuth` helper previously persisted its enriched lookup record, converting inherited group roles into permanent direct roles. It now writes only the generated magic token. Group grants remain inherited, and encrypted credentials/direct roles remain intact.

## Tests and proof

`src/users/users.invitation.spec.ts` exercises actual acceptance, signup, permission resolution and invitation authentication methods with a scoped in-memory repository. Combined with `users.permissions.spec.ts`, **46 tests passed** (including six subsequent direct-role removal regressions). Cases cover exact Sales membership, ignored forged request grants, custom direct roles, group/role ID canonicalisation, legacy arrays, missing/foreign/root/malformed grants, already-registered accounts, strict signup races, failed lookup/create/readback/completion writes, and no false success.

Coordinated full 8GB build passed, including the stock agent’s latest `productItems` fallback. Private environment files were restored byte-for-byte. Fresh Nora Ellis invitation acceptance was completed through the actual local browser UI. The authenticated session contains Sales and LeadEditor, without User, and CRM → Leads opens the two existing training leads. Evidence: `assets/local-roles/24-nora-acceptance-ready.png`, `25-nora-inherited-role-after-signup.png`, `26-nora-leads-access.png`, and `nora-authenticated-grants.json`. The owner then opened Nora’s refreshed profile: Sales remained assigned and **No direct roles** was displayed (`27-nora-owner-profile-raw-grants.png`). This confirms the stored direct grants stayed empty while the signed-in session inherited LeadEditor. The owner separately removed the legacy direct User grant from Maya Ito using the new confirmation control; a full reload/reopen showed Sales and No direct roles (captures 28–31). Local captured invitation email only; no production deployment.
