Files
MijnBesluit/docs/Socketprotocol.md
jfandClaude Sonnet 5.5 5446dd4170 Versiehistorie in een tabel, voor site en app
De versiehistorie stond als tekst in zowel de app als de website. Nu staat hij in
tabel version_history, met een beheerpagina (Beheer > Versiehistorie) op de site.
De site leest de tabel rechtstreeks, de app via de nieuwe actie app.versiehistorie.
Startrijen: db/handmatig/22b_versiehistorie_startrijen.sql (als root).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014MGLqt1uKfNARQHut9ifcx
2026-10-20 11:47:06 +00:00

8.2 KiB

Het socketprotocol van MijnBesluit

De Android- en iOS-app praten met MijnBesluit.SocketServer over een eigen TCP-verbinding. Geen IIS, geen webservice.

De vorm van een bericht

[4 bytes lengte, big-endian][UTF-8 JSON]

De lengte staat er dus vóór. De ontvanger weet daardoor exact wanneer een bericht af is.

Waarom niet de **BYE**-markering uit de RenH-socketserver? Omdat TCP geen berichtgrenzen kent. Wie in de datastroom naar een markering zoekt, loopt vast zodra die tekencombinatie toevallig in de inhoud voorkomt — precies het probleem dat in RHSocketClient op 10-09-2026 is gerepareerd, toen een leesgrens midden in een groot antwoord op een fragment "BYE" viel. Met een lengte vooraan kan dat niet gebeuren, en het scheelt ook het base64-coderen van alles wat er over de lijn gaat. SelfCheck bevat een controle die precies dit randgeval nabootst: een bericht met **BYE** en BYE in de inhoud, 400 kB groot, dat ongeschonden aankomt.

Eén verbinding, meerdere verzoeken

RHSocketClient zet per aanroep een nieuwe verbinding op. Hier blijft één verbinding open:

  • verzoeken mogen elkaar overlappen; antwoorden worden teruggekoppeld op requestId;
  • de server kan uit zichzelf een bericht sturen (een bericht dat binnenkomt, een overlijdensmelding over jouw profiel) — de app herkent dat aan het ontbreken van een bijbehorend openstaand verzoek;
  • er is niet bij elk verzoek opnieuw een TCP- en TLS-handdruk nodig.

Verzoek

{
  "requestId": "8f2c...",
  "action": "vault.list",
  "token": "<sessietoken>",
  "version": 1,
  "device": "Pixel 8",
  "payload": { "category": 3 }
}

Antwoord

{
  "requestId": "8f2c...",
  "status": "Ok",
  "message": null,
  "payload": [ { "id": 12, "title": "Bank", "category": 3 } ]
}

status is "Ok" of "Error" — dezelfde opzet als je RenH-socketserver, zodat het vertrouwd aanvoelt. Bij "Error" staat in message een tekst die je zo aan de gebruiker kunt tonen.

Acties

Actie Sessie nodig Wat het doet
ping nee Controle of de server leeft; meldt ook of de verbinding versleuteld is
auth.request-code nee Stuurt een inlogcode naar een bestaand account
auth.signin nee Inloggen met e-mailadres en code; geeft een sessietoken
auth.request-register-code nee Stuurt een code om een account aan te maken; naam gaat mee
auth.register nee Maakt het account aan met de ontvangen code
auth.accept-invitation nee Uitnodiging aannemen en het profiel claimen (geen code nodig)
auth.signout ja Sessie beëindigen
family.members ja Familieleden met hun eigendomsstatus
family.add-member ja Iemand toevoegen
family.invite ja Uitnodiging per e-mail versturen
vault.list ja Titels en categorieën (geen inhoud)
vault.open ja Eén item ontsleutelen
vault.save ja Item versleuteld opslaan
vault.categories ja De soorten kluisitems die jij te zien krijgt: de actieve plus die waar je al een item van hebt, met naam, uitleg, "bespreken met" en bij een eigen soort de velden van het formulier
vault.category ja Eén soort op nummer, ook als hij niet actief is (nodig om het item van een ander te lezen); alleen de beschrijving, nooit inhoud
app.versiehistorie ja De zichtbare regels van de versiehistorie (datum jjjj-mm-dd, versie, kop, tekst), nieuwste dag bovenaan; staat in tabel version_history, zie docs/Versiehistorie.md
message.inbox / message.outbox ja Berichten voor en door jou
message.read ja Bericht lezen en als gelezen markeren
message.compose ja Bericht klaarzetten
legacy.declare-death ja Overlijden melden
legacy.object ja Een melding over jezelf tegenspreken
legacy.status ja Stand van de procedure
home.advertentie ja De advertentie voor het overzicht, of leeg. Het opvragen telt de vertoning mee
legacy.appoint-manager ja Nalatenschapsbeheerder aanwijzen
legacy.remove-manager ja Een beheerder weer weghalen

Een actie die een sessie vereist en die zonder geldig token binnenkomt, krijgt altijd: Je sessie is verlopen. Log opnieuw in., met "code": "sessie-verlopen" erbij.

Dat code-veld is er voor de gevallen waarin de app iets anders moet doen dan de melding tonen. Bij sessie-verlopen gooit de client het bewaarde token weg en stuurt de schil je naar het inlogscherm. Op de tekst van de melding sturen zou ook werken, tot iemand er een komma in zet.

De app krijgt sessie-verlopen alleen als de server de sessie echt niet meer kent. Een mislukt verzoek is iets anders: geen bereik, een server die net opstart. Dan blijft het token staan en probeert de app het de volgende keer gewoon opnieuw. Dat onderscheid is het verschil tussen een hik en een e-mail met een code.

Gebruik vanuit de app

MijnBesluit.Protocol heeft geen enkel extern package nodig en kan daardoor rechtstreeks in het MAUI-project worden opgenomen.

var verbinding = new MijnBesluitSocketClient("socket.mijnbesluit.nl", 5200, useTls: true);

verbinding.PushReceived += melding =>
    Console.WriteLine($"Van de server: {melding.Message}");

// Stap 1: code aanvragen. Die komt per e-mail binnen.
await verbinding.SendAsync(SocketActions.RequestLoginCode, new { email = "jan@example.nl" });

// Stap 2: inloggen met de code die de gebruiker heeft ingetypt.
var inloggen = await verbinding.SendAsync(SocketActions.SignIn,
    new { email = "jan@example.nl", code = ingevuldeCode });

if (inloggen.Status == "Ok")
{
    verbinding.Token = /* token uit inloggen.Payload */;
    var kluis = await verbinding.SendAsync(SocketActions.VaultList);
}

Tijdens ontwikkelen mag AcceptAnyCertificate = true om met een zelfondertekend certificaat te werken. Zet dat in een uitgeleverde app nooit aan: dan is TLS zijn waarde kwijt.

Threads

De server accepteert verbindingen op één thread, geeft elke verbinding een eigen thread met een blokkerende leeslus, en handelt elk binnengekomen verzoek af op zijn eigen thread. Antwoorden kunnen daardoor in een andere volgorde terugkomen dan de verzoeken binnenkwamen; daar is requestId voor. Het wegschrijven is geserialiseerd, dus twee antwoorden raken nooit door elkaar.

Zie docs/SocketServer-installeren.md voor de instellingen en de grenzen.

Sessies

Een sessie leeft veertien dagen en schuift op bij elk verzoek. Ze staan in het geheugen én in de tabel sessies (db/16_sessies.sql).

Het geheugen is er voor de snelheid: een gewoon verzoek raakt geen database. De tabel is er om een herstart te overleven. Dat laatste is geen luxe: de app blijft aangemeld en gaat daarna open met een vingerafdruk of de code van de telefoon. Stonden de sessies alleen in het geheugen, dan was elke uitrol, elke Windows-update 's nachts en elke reboot genoeg om iedereen weer een code per e-mail te laten opvragen - het toestel wist dan nog wie je was, de server niet meer. De database wordt alleen geraadpleegd bij een token dat niet in het geheugen staat, dus in de praktijk bij het eerste verzoek van elk toestel na een herstart.

In de tabel staat de SHA-256 van het token, nooit het token zelf: uit een hash valt geen token terug te rekenen. Dezelfde afspraak als bij de inlogcodes.

De vervaltijd schuift op bij gebruik, maar wordt niet bij elk verzoek weggeschreven - zie SessionStore.SchuifopDrempel. Bij een termijn van veertien dagen maakt een uur speling niets uit, en het scheelt een schrijfactie per aanroep.

Gaat de database niet, dan komt die fout naar buiten als een gewone fout en niet als "je sessie is verlopen". Ook dat is het verschil tussen een hik en een e-mail met een code.

De tabel refresh_tokens uit het oorspronkelijke schema wordt nergens gebruikt; sessies is niet hetzelfde (die heeft de persoon erbij, en er is hier geen apart toegangstoken).

Poort en certificaat

Standaard poort 5200. In te stellen in appsettings.json:

"Socket": {
  "Address": "0.0.0.0",
  "Port": 5200,
  "CertificateFile": "D:\\MijnBesluit\\certificaat.pfx",
  "CertificatePassword": "..."
}

Of met de vingerafdruk van een certificaat uit de Windows-certificaatopslag (CertificateThumbprint), bijvoorbeeld een certificaat dat je al met Let's Encrypt vernieuwt.