Protokół synchronizacji

Ta strona to kontrakt interoperacyjności dla każdego, kto implementuje usługę synchronizacji dla OHarness. Opisuje, co wysyła klient i na jakim zachowaniu polega; nie wymaga magazynu JSONL używanego przez @oharness/sync.

Pod jednym originem działają dwa niezależne protokoły:

  • Synchronizacja sesji odzwierciedla transkrypcje, wersje artefaktów i metadane sesji przez strumień tylko do dopisywania.
  • Synchronizacja Memory uzgadnia notatki Markdown przez manifest i compare-and-swap. Jest opcjonalna. Serwer może implementować synchronizację sesji bez implementowania Memory.

Bazowy URL nie zawiera ścieżki, query, poświadczeń ani fragmentu. Klienci produkcyjni akceptują tylko HTTPS; zwykły HTTP jest akceptowany przy pracy deweloperskiej na localhost, 127.0.0.1 i [::1]. Przykłady poniżej używają https://sync.example.com.

Na tej stronie

  • [Docelowa zgodność](#compatibility-target)
  • [Uwierzytelnianie i najemcy](#authentication-and-tenancy)
  • [Model danych synchronizacji sesji](#session-sync-data-model)
  • [Endpointy sesji](#session-endpoints)
  • [Odpowiedzi z błędami sesji](#session-error-responses)
  • [Synchronizacja Memory](#memory-sync)
  • [Konfiguracja klienta OHarness](#configuring-an-oharness-client)
  • [Lista kontrolna implementacji](#implementation-checklist)
  • [Czego nie wymagamy](#non-requirements)

Docelowa zgodność

Ten dokument opisuje wersję 1 protokołu, używaną przez OHarness 0.5.x. Obecnie nie ma endpointu możliwości ani negocjacji treści. Zachowuj wsteczną zgodność /v1/: dodawaj pola opcjonalne, nie zmieniaj znaczenia istniejącego pola i nie czyń wymaganym pola, które wcześniej było opcjonalne.

Nieznane pola żądań w synchronizacji sesji powinny być ignorowane. Memory jest surowsze i odrzuca je z 400 {"code":"unknown_field"}; nowi klienci używają tej odpowiedzi, aby wykryć starszy serwer Memory i ponowić migawkę projektu bez optymalizacji have.

Wszystkie znaczniki czasu to milisekundy uniksowe. Wszystkie treści to JSON. Udane odpowiedzi używają Content-Type: application/json; klienci nie zależą od konkretnych białych znaków w JSON ani kolejności właściwości.

Uwierzytelnianie i najemcy

Każda trasa z wyjątkiem GET /v1/health wymaga:

Authorization: Bearer <token>

Prawidłowy token rozwiązuje się do dokładnie jednego stałego identyfikatora podmiotu. Serwer musi wyprowadzać właściciela ze zweryfikowanego tokenu, nigdy z identyfikatora użytkownika podanego w żądaniu. Token sesji może dodatkowo zawierać identyfikatory zespołów.

Bez team żądania sesji dotyczą osobistego strumienia podmiotu. Z team dotyczą strumienia tego zespołu, dopiero po sprawdzeniu członkostwa. Nieznany lub nieautoryzowany zespół zwraca 403; nie wolno po cichu wracać do strumienia osobistego.

Uwierzytelnianie celowo nie jest standaryzowane poza transportem Bearer. Dołączony serwer akceptuje wydawane lokalnie nieprzezroczyste tokeny, JWT HS256 oraz JWT weryfikowane przez JWKS. Inna implementacja może używać bazy danych lub innego weryfikatora tokenów, o ile obserwowalna semantyka podmiotu i zespołu pozostaje taka sama.

Memory ma silniejsze sprawdzanie tożsamości, opisane niżej. W szczególności nieprzezroczysty token wydany przez oharness-sync token wystarcza dla synchronizacji sesji, ale obecny klient Memory w OHarness używa tokenu konta Cloud.

Model danych synchronizacji sesji

Rekord

Rekord to jeden niezmienny element sesji. Dziś zwykle zawiera rekord transkrypcji lub kopertę wersji artefaktu, ale serwer musi traktować body jako nieprzezroczysty JSON.

{
  "session": "20260914_abcd",
  "id": "stable-record-id",
  "seq": 12,
  "device": "device-id",
  "at": 1789344000000,
  "body": { "opaque": "to the server" }
}
PoleTypZnaczenie
sessionstringSesja zawierająca rekord
idstringStały identyfikator używany do deduplikacji ponowień
seqnumberSekwencja Lamporta w obrębie sesji
devicestringUrządzenie, które utworzyło rekord
atnumberCzas utworzenia w milisekundach uniksowych
bodydowolny JSONŁadunek przechowywany i zwracany bez zmian

Kluczem deduplikacji jest (session, id). Ponowne wysłanie tego klucza nie może dopisać drugiego zdarzenia, nawet jeśli poprzednia odpowiedź zaginęła. Nie parsuj, nie normalizuj i nie przepisuj body.

Metadane sesji

{
  "id": "20260914_abcd",
  "gen": 1,
  "updatedAt": 1789344000000,
  "body": { "opaque": "session metadata" }
}

body jest nieprzezroczyste. deleted: true to tombstone i zwykle nie ma treści. Metadane rozstrzyga zasada last-writer-wins według tej krotki, w malejącej kolejności priorytetu:

  1. Wygrywa większe gen. Kompaktowanie lub przepisanie zwiększa generację.
  2. W obrębie jednej generacji wygrywa większe updatedAt.
  3. Równa krotka jest już przechowywana i nie dodaje zdarzenia.

Zwycięska aktualizacja, łącznie z tombstone, jest dopisywana do strumienia, aby urządzenia, które odczytały już starszą wartość, mogły zauważyć zmianę.

Kursor

Kursor to nieujemna liczba całkowita wydawana przez serwer. Identyfikuje pozycję w jednym strumieniu (owner, project). Klient traktuje go jako nieprzezroczysty: zapisuje go, odsyła jako since i oczekuje jedynie, że nigdy nie cofnie się do tyłu.

Dołączona implementacja JSONL używa rosnącego numeru zdarzenia. Implementacja bazodanowa może użyć sekwencji. Kursor nie musi być przesunięciem bajtowym, znacznikiem czasu ani seq rekordu.

Kursory należą do jednego strumienia i nie mogą być używane ponownie między użytkownikami, zespołami ani projektami. Magazyn strumieni to stan trwały. Jeśli serwer straci strumień, a klient zachowa swój kursor, klient może na stałe pozostać przed nowym strumieniem; protokół v1 nie ma handshake'u resetu kursora.

Endpointy sesji

GET /v1/health

Endpoint sprawdzania, czy serwer żyje, bez uwierzytelniania.

HTTP/1.1 200 OK
Content-Type: application/json

{"ok":true}

Dowodzi, że proces i trasa są osiągalne. Nie dowodzi, że token działa, że trwały magazyn jest zamontowany ani że Memory jest skonfigurowane.

GET /v1/projects

Listuje projekty, które mają strumień dla wybranego właściciela.

GET /v1/projects
GET /v1/projects?team=platform

Odpowiedź:

{"projects":["project-a","project-b"]}

Zwróć pustą tablicę, gdy właściciel nie ma strumieni. Identyfikatory tworzone przez obecnych klientów OHarness pasują do [A-Za-z0-9_-]{1,128} i powinny być zwracane bez zmian. Serwer może odrzucać inne dane wejściowe lub kodować je odwracalnie; nigdy nie doklejaj niezaufanego identyfikatora do ścieżki w systemie plików.

Obecne lustro per projekt nie wywołuje tej trasy w zwykłej pętli push/pull, ale jest ona częścią publicznego klienta i jest wymagana do pełnej implementacji oraz wykrywania projektów między urządzeniami.

POST /v1/push

Atomowo dopisuje nowe rekordy i zwycięskie metadane sesji.

{
  "project": "project-a",
  "team": "platform",
  "records": [
    {
      "session": "20260914_abcd",
      "id": "record-12",
      "seq": 12,
      "device": "laptop",
      "at": 1789344000000,
      "body": { "role": "user", "content": "hello" }
    }
  ],
  "sessions": [
    {
      "id": "20260914_abcd",
      "gen": 0,
      "updatedAt": 1789344000000,
      "body": { "id": "20260914_abcd", "title": "Example" }
    }
  ]
}

team, records i sessions są opcjonalne. Brakujące tablice oznaczają puste tablice. Wymagane jest co najmniej project. Starsze deklaracje typów zawierały device na najwyższym poziomie; klient OHarness go nie wysyła, a zgodne serwery nie mogą go wymagać. Tożsamość urządzenia jest zapisana w każdym rekordzie.

Odpowiedź:

{"cursor":42,"accepted":2}

cursor to bieżący koniec strumienia po tej wysyłce. accepted liczy nowe zdarzenia rekordów oraz zwycięskie zdarzenia metadanych sesji. Zduplikowane rekordy i nieaktualne lub równe metadane sesji nie są liczone.

Całe żądanie można ponawiać. Zwaliduj je przed zatwierdzeniem jakiegokolwiek wpisu. Nie przyjmuj częściowej partii: zwróć błąd dla całego żądania, jeśli którykolwiek wpis jest błędny lub przekroczono limit.

Limity używane przez obecnego klienta i dołączony serwer:

LimitWartość
Rekordy w jednej wysyłce500
Wpisy metadanych sesji w jednej wysyłce500
Zserializowana treść żądania8 MiB

Oba limity tablic są niezależne: jedno żądanie może zawierać 500 rekordów i 500 wpisów sesji.

GET /v1/pull

Czyta zdarzenia strumienia po kursorze.

GET /v1/pull?project=project-a&since=20&limit=500&wait=30&team=platform
ParametrWymaganyZnaczenie
projecttakNieprzezroczysty identyfikator projektu
sincetak, przy zwykłym użyciuZwraca zdarzenia ściśle po tym kursorze; 0 zaczyna od początku
limitnieRozmiar strony, przycinany do 1..500; domyślnie 500
waitnieSekundy long pollingu, przycinane do 0..30; domyślnie 0
teamnieStrumień zespołu zamiast strumienia podmiotu

Odpowiedź:

{
  "cursor": 22,
  "records": [],
  "sessions": [],
  "more": false
}

Zwracaj zdarzenia w kolejności strumienia i bez pól wyłącznie serwerowych. Kursor to ostatnie zwrócone zdarzenie. Jeśli nie ma nowych zdarzeń, pozostaje równy since. Ustaw more: true tylko wtedy, gdy kolejna strona jest już dostępna; klient wtedy natychmiast pobiera ponownie.

Gdy wait > 0, a strumień jest aktualny, przytrzymaj żądanie, aż zostanie dopisane zdarzenie lub upłynie czas oczekiwania. Upłynięcie czasu to udana pusta odpowiedź, a nie błąd HTTP. Rozłączenie long pollingu nie może zmienić stanu strumienia.

Odpowiedzi z błędami sesji

Błędy sesji to obiekty JSON z krótkim, czytelnym dla człowieka ciągiem error:

{"error":"missing or bad token"}

Używaj konwencjonalnych kodów statusu:

StatusKiedy
400Nieprawidłowy JSON, brak projektu, nieprawidłowy kursor lub błędny wpis
401Brakujący, wygasły lub nieprawidłowy token Bearer; dołącz WWW-Authenticate: Bearer
403Uwierzytelniony podmiot nie jest członkiem żądanego zespołu
413Liczba bajtów żądania lub wpisów przekracza limit
404Nieznana trasa
500Nieoczekiwany błąd magazynu; nie twierdź, że częściowa wysyłka się powiodła

Lustro ponawia błędy sieciowe i zwykłe awarie serwera z wycofywaniem (backoff). 401 zatrzymuje to lustro, dopóki nie zostanie uruchomione ponownie z używalnym poświadczeniem.

Synchronizacja Memory

Synchronizacja Memory jest opcjonalna i oddzielona od strumienia sesji tylko do dopisywania. Wszystkie trzy trasy to uwierzytelniane żądania POST z Content-Type: application/json:

  • /v1/memory/identity
  • /v1/memory/snapshot
  • /v1/memory/commit

Obecny klient jest wyłącznie natywny. Żądanie z nagłówkiem Origin jest odrzucane z 403 {"code":"native_client_required"}.

Tożsamość w Memory

Każde żądanie zawiera expectedPrincipal, pobrany z lokalnie zdekodowanego konta Cloud jako przypięcie zgody. Serwer musi niezależnie zwalidować token Bearer i wymagać, aby zweryfikowany podmiot był z nim zgodny. Nigdy nie autoryzuj prefiksu obiektów na podstawie samego expectedPrincipal.

Żądanie:

{"expectedPrincipal":"user-id"}

Odpowiedź:

{
  "principal": "user-id",
  "quota": {
    "maxNotes": 5000,
    "maxChanges": 500,
    "maxNoteBytes": 131072,
    "tombstoneDays": 90
  }
}

Dołączona implementacja waliduje tokeny przez Supabase GET /auth/v1/user, ale zgodna implementacja może używać innej usługi kont. Musi odrzucać konta anonimowe, usunięte, wygasłe lub w inny sposób nieprawidłowe i zwracać ten sam stały identyfikator podmiotu, którego oczekuje klient.

Zakres i format notatki

Zakres projektu wyraża się przez project bez właściwości scope:

{"project":"project-a"}

Zakres użytkownika ma scope: "user" i nie może zawierać project:

{"scope":"user"}

Protokół wymaga obu zakresów. Obecny dołączony adapter R2 ma znaną lukę w liście dozwolonych kluczy dla prefiksu użytkownika; nie zmienia to kontraktu protokołu dla niezależnych implementacji.

Notatka wygląda tak:

{
  "id": "borderless-cards-a91",
  "summary": "Pricing cards use fills, not borders",
  "body": "Use whitespace and background fills.",
  "tags": ["design"],
  "type": "feedback",
  "createdAt": 1789344000000,
  "updatedAt": 1789345000000
}

type jest opcjonalne i, jeśli występuje, ma jedną z wartości user, feedback, project lub reference. updatedAt to opcjonalne metadane, celowo wyłączone z hasha treści.

Limity walidacji są częścią kontraktu protokołu:

PoleOgraniczenie
Identyfikatory podmiotu, projektu i notatki[A-Za-z0-9_-]{1,128}; odrzucaj też __proto__, constructor i prototype
summaryPrzycięte, jedna linia, 1–500 znaków
bodyPrzycięty ciąg, najwyżej ogłoszone maxNoteBytes w UTF-8
tagsNajwyżej 30; każdy ma 1–64 znaki, bez białych znaków, przecinków i nowych linii
createdAtNieujemna bezpieczna liczba całkowita
updatedAtOpcjonalna nieujemna bezpieczna liczba całkowita

Identyfikatory aktywnych notatek są unikalne bez rozróżniania wielkości liter. Jest to wymagane, bo serwer nie może tworzyć stanu, którego popularne systemy plików klientów, nierozróżniające wielkości liter, nie potrafią odwzorować.

Hash notatki — SHA-256 zapisany małymi literami szesnastkowymi — jest liczony z JSON w UTF-8 z dokładnie tą tablicą pozycyjną:

JSON.stringify([id, summary, body, tags, createdAt])

Wtedy i tylko wtedy, gdy występuje type, dopisz je:

JSON.stringify([id, summary, body, tags, createdAt, type])

Implementacje muszą odtwarzać działanie JSON.stringify z JavaScriptu dla tych wartości. Ten wyjątek zgodności zachowuje hashe utworzone, zanim pojawiły się typy notatek.

POST /v1/memory/snapshot

Żądanie dla projektu:

{
  "expectedPrincipal": "user-id",
  "project": "project-a",
  "knownEtag": "optional-previous-etag",
  "have": {
    "borderless-cards-a91": "64-lowercase-hex-characters"
  }
}

Dla zakresu użytkownika zastąp project przez "scope":"user".

knownEtag umożliwia szybką ścieżkę bez zmian. have mapuje identyfikatory notatek na hashe, które klient już ma; serwer może pominąć treści tych notatek bez odczytywania ich z magazynu obiektów. Odrzuć mapę have z więcej niż maxNotes * 2 + 1000 kluczami.

Odpowiedź bez zmian:

{
  "principal": "user-id",
  "etag": "same-etag",
  "unchanged": true,
  "quota": {
    "maxNotes": 5000,
    "maxChanges": 500,
    "maxNoteBytes": 131072,
    "tombstoneDays": 90
  }
}

Odpowiedź pełna lub przyrostowa:

{
  "principal": "user-id",
  "etag": "current-etag-or-null",
  "entries": [
    {
      "id": "borderless-cards-a91",
      "hash": "64-lowercase-hex-characters",
      "note": { "id": "borderless-cards-a91", "summary": "...", "body": "...", "tags": [], "createdAt": 1789344000000 }
    },
    { "id": "already-held", "hash": "64-lowercase-hex-characters" },
    { "id": "deleted-note", "hash": null }
  ],
  "quota": {
    "maxNotes": 5000,
    "maxChanges": 500,
    "maxNoteBytes": 131072,
    "tombstoneDays": 90
  }
}

Aktywny wpis może pominąć note tylko wtedy, gdy have[id] jest równe jego hashowi. Tombstone ma hash: null i nie ma notatki. etag ma wartość null, gdy manifest nie istnieje.

POST /v1/memory/commit

{
  "expectedPrincipal": "user-id",
  "project": "project-a",
  "etag": "etag-from-snapshot-or-null",
  "changes": [
    {
      "id": "borderless-cards-a91",
      "hash": "64-lowercase-hex-characters",
      "note": { "id": "borderless-cards-a91", "summary": "...", "body": "...", "tags": [], "createdAt": 1789344000000 }
    },
    { "id": "deleted-note", "hash": null }
  ],
  "have": {}
}

Dla zakresu użytkownika zastąp project przez "scope":"user". Utworzenie lub aktualizacja musi zawierać notatkę, której identyfikator i obliczony hash zgadzają się ze zmianą. Usunięcie ma hash: null i nie ma note. changes musi zawierać od 1 do ogłoszonej liczby maxChanges wpisów, bez zduplikowanych identyfikatorów.

Zastosuj commit tylko wtedy, gdy etag nadal identyfikuje bieżący manifest. Przy nieaktualnym ETagu zwróć 409 {"code":"revision_conflict"} i pozostaw opublikowany manifest bez zmian. Zapisz nowe niezmienne wersje notatek przed opublikowaniem nowego manifestu i opublikuj manifest atomowo. Przegrywający współbieżny zapisujący może pozostawić nieosiągalne niezmienne obiekty do późniejszego odśmiecania, ale żaden częściowy stan nie może stać się widoczny przez manifest. Odpowiedź ma ten sam kształt co udana migawka, z nowym ETagiem i bieżącymi wpisami.

Nie odrzucaj usunięć od razu. Przechowuj tombstone'y przez ogłoszony okres, aby klienci offline mogli je zauważyć. Obniżenie limitu nie może uwięzić podmiotu powyżej niego: odrzucaj wzrost ponad maxNotes, ale nadal pozwalaj na usuwanie.

Błędy i limity Memory

Błędy Memory używają stałego, czytelnego maszynowo code:

{"code":"revision_conflict"}

Kody używane przez obecną implementację to m.in.:

StatusKody
400invalid_body, unknown_field, invalid_project, invalid_scope, invalid_have, invalid_changes, invalid_note, hash_mismatch, id_case_conflict
401sign_in_required
403native_client_required, account_mismatch
409revision_conflict
413memory_quota_exceeded
415json_required
429rate_limited
502auth_unavailable, invalid_manifest, missing_memory_object, memory_integrity_error, memory_sync_failed
503auth_not_configured, r2_not_configured

Dołączone wartości domyślne to 5000 aktywnych notatek na zakres, 500 zmian na commit, 128 KiB na treść notatki, 4 MiB na żądanie, 16 MiB zserializowanych danych wpisów na odpowiedź i 90 dni dla tombstone'ów. W protokole ogłaszane są tylko cztery wartości, na które klient może reagować, pokazane w quota. Klient stosuje też własne twarde limity bezpieczeństwa, więc podniesienie limitów serwera nie gwarantuje, że klient przyjmie dowolnie dużą odpowiedź.

Konfiguracja klienta OHarness

Umieść adres docelowy w konfiguracji użytkownika, nigdy w konfiguracji repozytorium:

{
  "sync": {
    "url": "https://sync.example.com",
    "token": "opaque-session-token",
    "team": "optional-team",
    "enabled": true
  }
}

Plikiem jest $OHARNESS_HOME/config.json, gdy ustawione jest OHARNESS_HOME, $XDG_CONFIG_HOME/oharness/config.json w Linuksie, gdy ustawione jest XDG, a w pozostałych przypadkach ~/.oharness/config.json. Po zmianie adresu docelowego zrestartuj OHarness.

Obecna kolejność pierwszeństwa:

sync.url in user config > OHARNESS_SYNC_URL > https://sync.oharness.ai
sync.token in user config > OHARNESS_SYNC_TOKEN > Cloud account token

W konsekwencji OHARNESS_SYNC_URL zastępuje wbudowaną wartość domyślną tylko wtedy, gdy konfiguracja użytkownika nie ma sync.url. OHARNESS_SYNC=0 wyłącza synchronizację sesji niezależnie od adresu docelowego i poświadczenia.

Pliki repozytorium oharness.config.json i .oharness.json nie mogą ustawiać sync.url, sync.token ani sync.team; OHarness odrzuca te wartości. Repozytorium może jedynie wyłączyć synchronizację lub dodać wykluczenia. Dzięki temu sklonowane repozytorium nie przekieruje transkrypcji użytkownika.

Ten sam rozwiązany bazowy URL jest używany dla Memory, ale obecny klient Memory używa poświadczenia konta Cloud, a nie sync.token ani OHARNESS_SYNC_TOKEN. Aby hostować Memory samodzielnie, logowanie klienta i weryfikator tożsamości serwera Memory muszą więc zgadzać się co do tego samego podmiotu.

Lista kontrolna implementacji

Zgodna usługa powinna spełniać wszystkie te zachowania:

  • Health działa bez tokenu; każda trasa z danymi bez tokenu kończy się odmową.
  • Dwa podmioty używające tego samego identyfikatora projektu nie widzą nawzajem swoich danych.
  • Strumienie osobiste i zespołowe są rozdzielne; nieautoryzowane zespoły zwracają 403.
  • Ponowienie wysyłki nie dubluje rekordów ani nieaktualnych metadanych sesji.
  • Pobieranie jest uporządkowane, stronicowane, z monotonicznym kursorem i bezpieczne dla long pollingu.
  • Treści sesji przechodzą przez push/pull bez zmian na poziomie wartości JSON.
  • Usunięcia sesji i usunięcia Memory docierają do drugiego urządzenia, które było offline.
  • Nieaktualny ETag Memory nie publikuje żadnej zmiany w manifeście.
  • Klucze obiektów Memory zawsze używają podmiotu zweryfikowanego przez serwer.
  • Niezgodne hashe, kolizje identyfikatorów notatek różniące się tylko wielkością liter i zbyt duże żądania są odrzucane przed publikacją manifestu.
  • Trwały stan strumieni i manifestów przetrwa restarty procesu i wdrożenia.

Testy sesji w repozytorium, test/sync.test.ts, oraz testy Memory, script/sync-memory.test.ts, to wykonywalne przykłady kontraktu. Obecnie tworzą dołączony serwer bezpośrednio, zamiast przyjmować zdalny URL, więc inna implementacja powinna przenieść te same przypadki lub dostarczyć mały adapter, zanim ogłosi zgodność.

Na pierwszy zdalny test dymny wydaj jednorazowy token i podstaw jego wartość poniżej. Nie używaj tokenu produkcyjnego konta w historii powłoki:

export SYNC_URL=https://sync.example.com
export SYNC_TOKEN=test-token

curl "$SYNC_URL/v1/health"

curl -X POST "$SYNC_URL/v1/push" \
  -H "Authorization: Bearer $SYNC_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"project":"conformance","records":[{"session":"s1","id":"r1","seq":1,"device":"test","at":1,"body":{"value":"unchanged"}}],"sessions":[]}'

curl "$SYNC_URL/v1/pull?project=conformance&since=0&limit=500" \
  -H "Authorization: Bearer $SYNC_TOKEN"

Wyślij tę samą treść drugi raz i sprawdź, że accepted wynosi 0; następnie pobierz od zwróconego kursora i sprawdź, że tablice są puste, a kursor się nie zmienił. Te testy potwierdzają osiągalność i podstawową idempotentność, a nie izolację najemców ani bezpieczeństwo współbieżnego CAS.

Czego nie wymagamy

Zgodny serwer nie musi:

  • uruchamiać modelu, wykonywać narzędzi ani rozumieć formatów wiadomości w transkrypcjach;
  • używać JSONL, R2, Supabase ani żadnej konkretnej bazy danych;
  • zapewniać rejestracji, logowania, wydawania tokenów ani aplikacji przeglądarkowej;
  • implementować Memory, aby implementować synchronizację sesji;
  • udostępniać CORS. Obecni klienci to natywni klienci Node/Electron.

Trzymanie tych kwestii poza protokołem pozwala, by mała usługa hostowana samodzielnie i wielodostępne wdrożenie mówiły tym samym formatem protokołu.