User API
Accounts and access tokens: signing up with an emailed code, logging in, carrying the token, and what your account can then do in the registry.
Reads are public: the catalog, the versions and the downloads need no credential at all, so an IDE that only installs extensions never has to ask anyone to sign in. Everything on this page is for the other case — publishing. You need an account to put an extension into the registry, and this is how you get one and prove it is yours. For what you then do with it, see the Extension API.
Two kinds of request
| credential | how | what it reaches |
|---|---|---|
| none | — | every read: the catalog, versions, resource downloads |
Authorization: Bearer <jwt> |
an access token from login | your account, and the extensions it owns — publishing included |
Reads need nothing at all. A token is what turns a caller into somebody: it identifies your account, and through it the extensions you own. See the Extension API for what you can then do with them.
1. Sign up
Anyone can create an account, but it is not usable until the address on it is confirmed. Sign-up mails a four-digit code; entering that code is what turns the account on — and logs it in, since by then its holder has proved both the password and the address.
curl -X POST "https://tucan.amlang.net/api/users/signup" \
-H 'Content-Type: application/json' -d '{
"username": "anders",
"email": "anders@example.com",
"password": "correct horse battery staple",
"displayName": "Anders"
}'
{"username":"anders","email":"anders@example.com","verification":"sent","expiresIn":1800}
Then, with the code from the mail — the response is the same token a login returns:
curl -X POST "https://tucan.amlang.net/api/users/verify" \
-H 'Content-Type: application/json' -d '{"username":"anders","code":"2452"}'
# a fresh code, which replaces the outstanding one
curl -X POST "https://tucan.amlang.net/api/users/verify/resend" \
-H 'Content-Type: application/json' -d '{"username":"anders"}'
-
usernameis 3-32 ofA-Za-z0-9._-, because it ends up in URLs;passwordis at least 8 characters;emailmust be unique. A taken username or address is a409— sign-up cannot hide whether a name is free, since that answer is the point of the call. - The code lives 30 minutes and dies after five wrong guesses. Four digits is 10,000 possibilities, so the cap is what makes it safe: a locked code is dead rather than merely disfavoured, and the way out is a new one, which is a new number to find. Resends are limited to one a minute per account, sign-ups to five an hour per IP address.
-
Until the code is entered the account grants nothing. It
holds no roles, owns no extensions, and its password is not a credential:
/users/loginanswers403with"verification":"required"rather than a token.
Sign-up answers 503 on a deployment with no mail or no signing
key configured, rather than creating accounts whose codes go nowhere, and
403 where it has been switched off
(registry.signup_enabled=false). Neither affects the admin path
below.
2. Log in
curl -X POST "https://tucan.amlang.net/api/users/login" \
-H 'Content-Type: application/json' \
-d '{"username":"anders","password":"correct horse battery staple"}'
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9…",
"tokenType": "Bearer",
"expiresIn": 86400,
"refreshToken": "Yy0xQk9…",
"refreshExpiresIn": 31536000,
"username": "anders",
"roles": ["Administrator"]
}
Two tokens, with two jobs. The access token goes on every request and lasts 24 hours; it cannot be called back, which is why it is short. The refresh token lasts a year, is sent to one endpoint only, and can be called back — so a lost machine is a revoked session rather than a password change.
Every failure — unknown user, wrong password, disabled account, no password set —
returns the same 401 and the same message. That is deliberate:
distinguishing them tells an attacker which usernames exist. The one exception is
an unverified account, which answers 403 with
"verification":"required": that is only reachable with the right
password, so it reveals nothing the caller did not just prove they knew, and it is
the difference between retrying the password and going to read the mail.
3. Stay logged in
curl -X POST "https://tucan.amlang.net/api/users/token/refresh" \
-H 'Content-Type: application/json' -d '{"refreshToken":"Yy0xQk9…"}'
The reply is the same shape as a login: a new access token and a new refresh token. Store both — the one you sent is spent.
- Refresh on a schedule, not on a 401. Halfway through the access token's life is a good rule: twice a day against a 24-hour token leaves a wide margin for a machine that was switched off, or a clock that is off.
-
Rotation is the point. Every refresh consumes the token and
issues its replacement, so a stolen copy works only until you next refresh. When a
spent token is presented again, the whole session is revoked on the spot — one of
the two callers holding it is not you, and there is no telling which, so both stop.
A
401with"refresh":"rejected"means log in again. - A retry is safe. If the reply is lost in transit, sending the same token again within two minutes counts as the same refresh rather than as reuse, so a dropped connection is not a forced logout.
- Keep it like a password. It is a bearer credential with a year on it: store it where you would store one, never in a project folder that might be committed, and never log it.
To end a session deliberately, and to see what is signed in:
curl -X POST "https://tucan.amlang.net/api/users/logout" \
-H 'Content-Type: application/json' -d '{"refreshToken":"Yy0xQk9…"}'
curl "https://tucan.amlang.net/api/users/me/sessions" -H "Authorization: Bearer $ACCESS_TOKEN"
curl -X DELETE "https://tucan.amlang.net/api/users/me/sessions/12" -H "Authorization: Bearer $ACCESS_TOKEN"
curl -X DELETE "https://tucan.amlang.net/api/users/me/sessions" -H "Authorization: Bearer $ACCESS_TOKEN"
One row per machine signed in as you: what asked for it, where it was last used from,
when it expires. The name shown is the User-Agent the login came with, so
send something a person will recognise. An access token already issued stays valid
until it runs out — nothing can recall it — which is exactly why its life is measured
in hours.
4. Pass the token
curl "https://tucan.amlang.net/api/users/me" -H "Authorization: Bearer $ACCESS_TOKEN"
# and what that account participates in, invitations included
curl "https://tucan.amlang.net/api/users/me/extensions" -H "Authorization: Bearer $ACCESS_TOKEN"
The Bearer prefix is optional but conventional, and a token
cookie is accepted as a fallback for browser clients.
GET /users/me returns the account the token names — the cheapest way to
check that a token is still good and see its roles.
What is in the token
HS256, signed by the registry.
{
"sub": "75",
"preferred_username": "anders",
"roles": ["Administrator"],
"iat": 1788282039,
"exp": 1788325239,
"iss": "https://tucan.amlang.net/api",
"aud": "amlang-registry"
}
-
preferred_usernameis the link to the account. It is matched againstUser.username; if no account matches, the token authenticates but resolves to nobody, and account-scoped endpoints return404. - Roles are a snapshot. They are copied in at login, so changing someone's roles takes effect on their next token, not immediately. Keep lifetimes short if that matters to you.
-
Expiry is enforced, 24 hours by default
(
expiresInis in seconds). Past that, refresh rather than log in again. - An access token cannot be revoked: ending a session stops it being renewed, and the token already issued runs out its 24 hours. Treat one the way you would a password.
What each code means
| code | meaning | what to do |
|---|---|---|
401 |
no usable credential: missing, malformed or expired token | log in again |
403 |
a real token, but this account is not allowed — you do not own this extension, or, on login, your address is not verified yet | check verification in the body: required
means enter the code. Otherwise it is not a token problem |
404 |
on an account call, the token names an account that does not exist here; on an extension, it exists but is not yours to see | sign up, or check the id |
409 |
on sign-up: the username or address is taken | pick another |
429 |
too many wrong codes, resends, or sign-ups from one address | ask for a fresh code, or wait |
503 |
login or sign-up is unavailable on this deployment | nothing a client can fix; tell whoever runs the registry |
The 401/403 split is the useful one: 401 means try again with a
better token, 403 means the token is fine, the account is not
allowed. An unapproved extension answers 404 to everyone but its
owner, so a 404 on something you know exists means it is not yours.
What your account can do
Once you are signed in and verified, the registry is open to you: you can create an extension, and you own what you create.
| you can | where |
|---|---|
| create an extension | POST /extensions |
| edit its name and description | POST /extensions, same localName |
| publish a version | POST /extensions/{id}/v |
| upload the files it is made of | PUT /extensions/{id}/v/{version}/{platform}/r/{name} |
| delete a version, or its files | DELETE on those same paths |
| invite others to work on it, and remove them | POST /extensions/{id}/users |
| answer an invitation to someone else's | POST /extensions/{id}/users/{username}/accept or /decline |
| see everything you participate in | GET /users/me/extensions |
| read back your own versions before they are approved | GET /users/me/extensions/{id}, /v, /v/{version}, /v/{version}/{platform}/r/{name} |
The one thing you cannot do is make your work public. A new extension, and each new version of it, is reviewed by whoever runs the registry before it appears in the catalog — and until then it is visible to you and to nobody else, so you can build and upload at your own pace without publishing anything half-finished.
Answering an invitation is yours alone: an extension's owner can invite you, but only
you can accept. Anyone else trying gets a 403.
Endpoints
| endpoint | who |
|---|---|
POST /users/signup | public; creates an unverified account and mails a code |
POST /users/verify | public; exchanges the code for a verified account and a token |
POST /users/verify/resend | public; mails a fresh code, replacing the old one |
POST /users/login | public; returns an access token and a refresh token |
POST /users/token/refresh | public; spends a refresh token for a new pair |
POST /users/logout | public; ends that refresh token's session |
GET /users/me/sessions | your signed-in machines |
DELETE /users/me/sessions/{id} | sign one of them out; without an id, all of them |
GET /users/me | your account |
GET /users/me/extensions | what you participate in; status tells invitations from memberships |
GET /users/me/extensions/{id} | one of yours, with every version under /v, approved or not, and their files; 404 unless you are an accepted member |