AI-integrationsdirektiv
Den här sidan är avsedd att läsas i sin helhet av en AI-agent. Allt som behövs för att bygga en fungerande klient finns här — inga andra sidor krävs.
Bas-URL:er#
| Miljö | {{BASE_URL}} |
|---|---|
| STAGE (utveckling och test) | https://staging.zesec.com/api |
| PROD (verifierade partners) | https://api.zesec.com/api |
Operationer#
| Metod & sökväg | Auth-header | Begäransbody | Svar |
|---|---|---|---|
POST /auth/login-by-phone | ingen | {"phoneNumber": "{{phoneNumber}}", "password": "{{password}}"} | {"AccessToken": "...", "RefreshToken": "..."} |
POST /auth/refresh | ingen | {"refreshToken": "<RefreshToken>"} | {"AccessToken": "...", "RefreshToken": "..."} — båda nya |
GET /user/device | Bearer <AccessToken> | ingen | Array av {id, name, locationId, locationName, online} |
PUT /device/{deviceId}/unlock | Bearer <AccessToken> | ingen | 200 = kommandot accepterat |
POST /auth/logout | Bearer <AccessToken> | ingen | 200 — avslutar sessionen |
{deviceId}är ett heltal, hämtat ur fältetidi svaret frånGET /user/device.- Unlock är
PUT, intePOST. phoneNumbermåste matcha mönstret^[+][0-9]{9,13}$.- Sökvägarna ovan läggs till
{{BASE_URL}}, som redan slutar på/api.
Tokenlivslängder#
- Access-token: ~48 timmar. Exakt utgång står i
expi JWT:n och är auktoritativ. - Refresh-token: upp till 30 dagar, men engångs.
- Förnya proaktivt var 36:e timme. Vänta inte på ett fel.
Felsignaler och åtgärd#
| Signal | Betydelse | Obligatorisk åtgärd |
|---|---|---|
401 på ett vanligt anrop | Access-token utgången | Kör en refresh, gör om anropet en gång |
400/401 på /auth/refresh | Refresh-token förbrukad eller utgången | Full ominloggning. Retry:a aldrig samma refresh-token |
401 igen direkt efter refresh | Sessionen övertagen av annan inloggning | Stoppa. Larma. Loopa inte |
403 på unlock | Kontot saknar nyckel till enheten | Stoppa. Detta löser sig inte med retry |
404 på unlock | Okänt {deviceId} | Hämta enhetslistan på nytt och uppdatera mappningen |
429 | För många anrop | Respektera Retry-After. Exponentiell backoff med jitter |
5xx | Tillfälligt fel | Backoff. Aldrig på unlock — se nedan |
Hårda begränsningar#
- En aktiv session per konto. Implementera tokenlagret som en singleton med delad lagring. Två inloggningar med samma konto avlivar varandras sessioner.
- Refresh-token är engångs. Ett samtidigt anropspar utan lås förbrukar två tokens och dödar sessionen. Single-flight-lås krävs.
- Skriv båda tokens atomiskt. En halv skrivning förlorar sessionen permanent.
- Logga in server-side, aldrig per öppning. Öppningsögonblicket ska innehålla exakt ett API-anrop.
- Retry:a aldrig unlock automatiskt. Ett upprepat anrop kan öppna dörren igen. Visa felet för en människa.
- Hårdkoda aldrig `{deviceId}`. Hämta och cachelagra mappningen.
- Tokens lämnar aldrig servern. Inte till webbklient, inte till mobilapp, inte till loggar.
Referensimplementation#
Reference implementation (language-agnostic)
STATE session = { accessToken, refreshToken, rotatedAt } # ONE per API account, server-side
CONST PROACTIVE_REFRESH = 36 hours
LOCK refreshLock # single-flight
FUNCTION getValidAccessToken():
IF session EXISTS AND now() - session.rotatedAt < PROACTIVE_REFRESH:
RETURN session.accessToken
ACQUIRE refreshLock # concurrent callers wait, never race
IF session EXISTS:
TRY:
RETURN rotate(session.refreshToken)
CATCH RefreshFailed: # 400/401 => token spent or expired
RETURN login()
ELSE:
RETURN login()
RELEASE refreshLock
FUNCTION rotate(refreshToken):
res = POST {{BASE_URL}}/auth/refresh body {"refreshToken": refreshToken}
IF res.status IN (400, 401): THROW RefreshFailed # do NOT retry this token, ever
IF res.status >= 500: BACKOFF once, THEN THROW RefreshFailed
session = { accessToken: res.AccessToken,
refreshToken: res.RefreshToken, # BOTH, in ONE atomic write
rotatedAt: now() }
RETURN session.accessToken
FUNCTION login():
res = POST {{BASE_URL}}/auth/login-by-phone body {"phoneNumber": ..., "password": ...}
IF NOT res.ok: ALERT operator; STOP # never loop on a failed login
session = { accessToken: res.AccessToken, refreshToken: res.RefreshToken, rotatedAt: now() }
RETURN session.accessToken
FUNCTION apiCall(method, path, body = null):
res = REQUEST method, {{BASE_URL}} + path, Authorization: "Bearer " + getValidAccessToken()
IF res.status == 401:
invalidate session.accessToken
res = REQUEST method, {{BASE_URL}} + path, Authorization: "Bearer " + getValidAccessToken()
RETURN res # still 401 => STOP, alert; do not loop
FUNCTION unlock(internalDoorId):
ASSERT callerIsAuthorized(internalDoorId) # YOUR rules, not KiiOn's
deviceId = cachedMapping[internalDoorId] # from syncDevices(), never hard-coded
RETURN apiCall("PUT", "/device/" + deviceId + "/unlock") # exactly one call, no retry loopMaskinläsbara källor#
Kopiera sidan som Markdown: Visa som Markdown