> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wristler.eu/llms.txt
> Use this file to discover all available pages before exploring further.

# How it holds together

> Who may act for which business, where clients and grants are managed, and which fields are ours.

The notes we keep for ourselves about the pieces underneath. Nothing here is a
secret — it is how the API decides what a token may reach — but it is written for
whoever maintains it rather than for somebody building against it.

## Who may act for which business

`App\Modules\API\Auth\SellerScope` answers it, and it is the only place that does.

| Who | Which businesses |
| - | - |
| A seller's own people | The ones on their `seller_user` rows |
| Our own staff (`view_administration`) | Every business — they already edit any seller's watches and orders in the administration, and an account that can do that through one door and not the other is a rule nobody can explain |
| A partner | The businesses that granted them access, from `seller_integration_grants` — never the businesses the person who approved happens to work at |

That last difference is the whole reason the table exists.

The grant also says *what* was allowed, and that is checked per request by the
`granted:` middleware rather than by Passport's `scopes:`. A token is issued to a
client; a permission is given by a business, and a client several businesses
connected holds one token carrying the union. Without the second check, a partner
with read-only access to one shop and write access to another could write to the
first — the token said so, and nothing asked whether that shop had agreed.

Our own app and our own staff hold no grants: they act as the person signed in, so
the token is the whole answer and `granted:` falls through to it.

Being staff widens *which* businesses you reach. It does not widen what a Connect
feed owns: a feed listing is refused to everyone.

> [!WARNING]
>
> Every permission in this application is registered for the `web` guard, and
> `$user->can()` asks the guard the request is running on. On the API that is
> `passport`, so the same account with the same permissions answered yes on the
> website and no here — silently, and it looked like a missing permission rather
> than a guard mismatch. `App\Modules\Auth\Permissions::allows()` asks the guard
> they are defined for, and both `SellerScope` and `Actor` go through it.

## Where clients and grants are managed

> [accordion] Admin → API clients
>
> Registers an application and decides what kind it is. That is the lock in front
> of every scope; registering grants nothing on its own. Revoking withdraws every
> token the client issued and keeps the row: tokens already out there point at it,
> and losing it would leave them unexplainable rather than refused. Only a client
> already revoked can then be deleted for good, and its tokens, codes and the
> sellers' grants go with it.

> [accordion] Trader → Settings → Integrations
>
> Where a seller sees which applications may act for them and withdraws any of
> them, and where they create a key for a script they run themselves. Owning that
> key is the permission, so its grant row names nobody as approver.

A secret is shown exactly once in both screens. We store only the hash, so the only
way back from losing it is a new key — which the screens say, because otherwise
somebody will assume we can look it up.

## Fields the app sees and a partner does not

`App\Modules\API\Auth\InternalFields` answers it for fields a partner should not even
see, keyed on the client being ours.

What sits behind it is not secret: a secret — a password, MFA material — is never
serialised at any scope, which is a stronger guarantee than a scope check. These
are things an outside integration has no business building against, because the
moment they can, the field becomes a contract we have to keep.

* `descriptionTranslations` on a listing, and only when you name it in
  `fields[watches]`. A translator maintains them after the fact; nobody writes them
  through the API. The website serves them to anyone, so there is nothing hidden here —
  only unpromised. `translatedDescription` is the one you want, and it is promised to
  everybody.
* `purpose` on a thread. Free text with five values in practice and null for older
  threads, so it is analytics rather than a contract.
* `translatedBody`, `translationLocale`, `translationSourceLocale` and `translationPending` on a message.

One pair is withheld rather than hidden: `buyerEmail` and `buyerPhone` on an order read
under a seller are in every client's document, and null for anyone but our own app
(`OrderSchema::buyerDetail()`). They are a person's contact details, which a shop's
people may see on its dashboard and nobody agreed to hand to a partner's system.

Add a field to that list by giving it
`->hidden(fn ($request) => InternalFields::hiddenFrom($request))`, not by inventing
a second definition of "is this our app".

## Why the reference describes what a partner cannot reach

The published reference documents the catalogue, the pages people read, messaging,
uploads, checkout and everything under `/v1/me`, and its OAuth flows do not offer
their scopes. That is deliberate: a scope a
partner can read about but never obtain is honest, while one they can ask for and
are always refused reads as a bug in our API rather than in their request. And a
reference that leaves out an endpoint that exists is a reference you cannot trust.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.