Eine Tour durch das URnetwork SDK
Das ist der Tiefgang hinter Erste Schritte: wie jede Binding-Schicht zusammengesetzt ist, welche Standardwerte sie ausliefert und welche scharfen Kanten wir kennen. Alles läuft gegen URnetworks gehostete Plattform. Du bringst Zugangsdaten mit, keine Infrastruktur. Das Objektmodell (Device, API, View-Controller) steht in /docs/overview und /docs/api.
Ein Go-Kern implementiert den gesamten Client: Transport, Verträge, Paketpfad. Jedes Artefakt (AAR, xcframework, wasm, c-shared-Bibliothek) ist ein Binding genau dieses Kerns — deshalb ist das Verhalten über alle Plattformen hinweg identisch, und deshalb sind die Artefakte groß (jedes trägt die Go-Runtime). Der Kern trägt auch Produktregeln, nicht nur Pakete. Solana Pay lebt hier (CreatePaymentReference, BuildSolanaPaymentUrl), weil es das einst nicht tat: Die Web-App prägte die Zahlungsreferenz als Hex-UUID, wo Solana Pay einen base58-kodierten 32-Byte-Pubkey verlangt — ein Kunde konnte also zahlen und wurde nie wieder seinem Konto zugeordnet —, und Android hatte Betrag und Händleradresse hart verdrahtet. Eine Regel neu zu implementieren, der die Apps längst folgen, ist die Klasse von Bug, gegen die diese Schicht existiert.
Mobile Bindings
DeviceLocal und die App-Prozess-Aufteilung auf Apple
DeviceLocal ist das echte Ding, die laufende Client-Engine: Ihm gehören der Transport, der Vertragszustand (URnetworks Buchführung über die Bytes, die ein Konto bewegen darf) und der Paketpfad. DeviceRemote ist ein Client für ein DeviceLocal, das in einem anderen Prozess oder auf der Plattform lebt, mit derselben API-Fläche: eine Fernbedienung für das echte Device. Auf Android beherbergt ein Prozess sowohl die UI als auch den VpnService, die App hält das DeviceLocal also direkt.
Apple führt die VPN-Paketverarbeitung in einem eigenen, gesandboxten NetworkExtension-Prozess aus (jedes iOS-/macOS-VPN ist so aufgeteilt), also teilt sich das SDK mit ihm auf:
- Die
NEPacketTunnelProvider-Extension besitzt dasSdkDeviceLocalund ruft daraufsetRpcServer(serverPem, clientCertPem, hostPort)auf, um den Listener zu starten. - Der App-Prozess erstellt ein
SdkDeviceRemoteund hängt sich über eine Loopback-mTLS-Device-RPC-Verbindung an: das spiegelbildlichesetRpcServer(clientPem, serverCertPem, hostPort), wobei jede Seite zuerst ihre eigene Identität und danach das Zertifikat der Gegenseite benennt. Device-RPC ist das eigene Protokoll des SDK: Methodenaufrufe plus Subscription-Streams über Gos net/rpc, nicht gRPC. Gegenseitiges TLS auf Loopback ist keine Zeremonie. Localhost ist für jeden lokalen Prozess und jeden lokalen Nutzer erreichbar, und nur wer die passenden PEMs hält, kann sich anhängen. - Die App prägt das Key-Material, nicht die Extension.
SdkGenerateDeviceRpcKeyMaterial()gibt pro VPN-Sitzung ein frisches, selbstsigniertes Server- und Client-Schlüsselpaar zurück; die App legt beide PEMs und den host:port in die Provider-Konfiguration desNETunnelProviderProtocol, und so bekommt die Extension sie beim Start des Tunnels. Ein App-Group-Container ist nicht beteiligt. - Beobachte den
RemoteChangeListener: WennremoteConnectedzum ersten Mal auf true geht, ist dieses Material erwiesen — persistiere es also als zuletzt bekannt gut und wende es beim nächsten Start wieder an. Genau das lässt die App sich an eine bereits laufende Extension anhängen.
Rebuilds sind Routine: Die Extension baut ihr Device jedes Mal neu, wenn der Tunnel neu startet, weil der Nutzer das VPN umschaltet, das OS die Extension neu startet oder iOS sie killt, weil sie das Speicherlimit für Extensions überschritten hat. Verdrahte dieses Wieder-Anhängen (prägen, Tunnel starten, setRpcServer, den eigenen UI-Zustand wieder anwenden) am ersten Tag. Alles, was du auf einem DeviceLocal tun kannst, kannst du auf einem DeviceRemote tun; das Remote leitet Aufrufe weiter und spielt Subscriptions über Wiederverbindungen hinweg erneut ein. Zwei Stufen: Eine schlichte Wiederverbindung (dasselbe Device, die RPC-Verbindung ist abgerissen und zurückgekommen) stellt die Subscriptions für dich wieder her; eine Neuerstellung (eine neue Device-Instanz darunter) heißt, diese Sequenz erneut zu fahren. Eine Falle: DeviceRecreatedListener feuert bei einem Wechsel der Device-Generation, und nur der plattformgehostete RPC-Pfad stempelt eine — auf Loopback treibst du das Wieder-Anhängen also aus deinem eigenen Tunnel-Lebenszyklus und dem RemoteChangeListener.
IoLoop (nur Android)
Sdk.newIoLoop(device, detachedFd) ist die Paketpumpe:
- Übergib einen detachten, nicht blockierenden fd (
ParcelFileDescriptor.detachFd()). Nach diesem Aufruf gehört der fd Go, und Go wird ihn schließen; wickle ihn nie wieder aus Java ein und schließ ihn nicht. Zwei Besitzer heißt Double-Close: fd-Nummern werden sofort wiederverwendet, ein verirrtes Schließen aus Java kann also irgendeinen unbeteiligten Deskriptor zertrampeln, der die Nummer als Nächstes bekommen hat — Korruption weit weg von ihrer Ursache. - Die Schleife pumpt beide Richtungen (tun→Device und Device→tun) innerhalb von Go, was JNI-Übergänge und Pufferkopien pro Paket vermeidet. Deshalb nimmt die API einen rohen fd, statt Read-/Write-Methoden anzubieten.
- Zum Stoppen schließt du den IoLoop (nicht den fd); der Done-Callback feuert, wenn die Schleife endet. Feuert er, ohne dass du gefragt hast — weil der tun-fd EOF oder einen Fehler traf oder das Device heruntergefahren wurde —, behandle es als „der Tunnel ist weg“: Beende die
VpnService-Sitzung, dann bau ihn neu auf oder zeig „getrennt“. Der Callback trifft auf einem SDK-Thread ein, und Go schließt den fd auf dem Weg hinaus; fass ihn im Handler nie an.
Apple nutzt IoLoop nicht: Der Tunnel-Provider bewegt Pakete über packetFlow, die einzige Schnittstelle, die NetworkExtension anbietet. Jedes iOS-VPN zahlt diesen Übergang, und seine gebündelten Reads amortisieren die Kosten.
Provide-Modi, und warum Key-Material zählt
SetProvideMode steuert, ob das Device dem Netzwerk Kapazität anbietet. Der Standard ist aus: Ein frisch konstruiertes Device hat den Provide-Modus „none“ und bietet nichts an, bis du einen Modus setzt — das SDK einzubetten teilt die Bandbreite deiner Nutzer also nie still. Zwei Modi tun heute etwas: public und network, das die Bereitstellung auf andere Geräte im selben URnetwork-Konto beschränkt. Das Protokoll-Enum trägt mehr Werte, aber die Plattform löst jeden Peer außerhalb deines Netzwerks zu public auf — die echte Wahl ist also: aus, meine eigenen Geräte, oder alle.
Beim Bereitstellen zählt die Persistenz des Key-Materials: Das DeviceLocalKeyMaterial, das du bei der Konstruktion übergibst, ist die Provider-Identität des Device, ein Client-Key-Seed plus das Provide-TLS-Zertifikat und sein Schlüssel. Persistiere es im sicheren Speicher der Plattform (Keystore, Keychain) und übergib bei jedem Start dasselbe Material, sonst sieht das Netzwerk jedes Mal einen brandneuen Provider und verliert die Zuverlässigkeits-Historie, die die Auswahl bevorzugt. Flüchtiges (null) Material ist für reine Clients fein.
Wenn du das Bereitstellen anbietest, benenne den Tausch: Der Datenverkehr Fremder tritt über die IP des Nutzers aus, und die Provider-Position sieht Ziel-IPs und TLS-SNI (wie ein Internetanbieter), standardmäßig aber nicht die echte IP des ursprünglichen Nutzers. Die Sicherheit des Providers ist in der Engine konstruiert. Die quelloffene ip_security-Schicht inspiziert den eigenen Ausgang des Providers und verwirft Datenverkehr der DMCA- und der CFAA-Klasse, bevor er ihn verlässt. Das Urteil ist ein verworfenes Paket, ohne dass irgendwo ein Ziel, eine Domain oder Inhalte aufgezeichnet werden. Ein Treffer auf eine BitTorrent-Signatur sendet zusätzlich eine Missbrauchsmarkierung an den Operator, die nur die Geräte-ID des Peers und einen Boolean trägt, und der Operator liefert heute keinen Handler dafür aus; Verwürfe opak verschlüsselten Datenverkehrs bleiben still. Provider nehmen am UR Protocol teil; ur.xyz dokumentiert die Vergütung.
Laufzeit-Notizen
- GC-Pacing wird automatisch pro OS abgestimmt: ein Pacing-Faktor von 10 auf iOS (die Speicherlimits für Extensions sind brutal) gegenüber 50 auf Android und 100 anderswo. Du setzt ihn nicht.
memoryTargetByteCountist eine andere Stellschraube: ein Byte-Budget, das das Device aufteilt, um seine eigenen Puffer zu bemessen, dns 2 : client 14 : provider 4, wobei der Provider-Anteil das Client-Paar stützt, solange das Bereitstellen aus ist (Standard 20 MB). Weder eine Obergrenze noch eine GC-Einstellung: Das prozessweite weiche Footprint-Limit ist das separateSetMemoryLimit, und der harte Kill ist das Extension-Limit des OS. Setz das Target darunter; halt die Extension nahezu logikfrei.- Der gomobile-Bind ist im Build gegatet, und der Mechanismus zählt: gobind lässt still alles weg, was es nicht binden kann, und hinterlässt in den generierten Quellen nur einen
// skipped-Kommentar — der Build grept also diese Quellen und schlägt bei jeder Auslassung fehl, die nicht auf einer expliziten Allowlist steht. Die erlaubten Skips sind intern (RPC-gob-Payloads, die Proxy-/Plattform-Fläche,uint64- und[][]byte-Formen, die gomobile nicht ausdrücken kann): Absicht, keine Drift.
JavaScript
Warum es im wasm kein DeviceLocal gibt
Eine Browser-Seite kann kein tun-Interface besitzen, ein wasm-DeviceLocal hätte also nichts zu pumpen. Die JS-Schicht liefert nur die clientseitigen Hälften: die API-Fläche, die View-Controller und DeviceRemote, einen vollen Device-Client, dessen Device anderswo lebt. Die Seite hält die Steuerungsebene (Verbindungszustand, Standorte, Statistiken, Konto), während der Datenverkehrspfad von allem konsumiert wird, was den gehosteten Proxy nutzen kann. Daher die zwei Factories: createProxyDevice ist das dünne Modell, das gehostete Proxy-URLs auflöst und den Datenverkehr allem überlässt, was sie konsumiert (eine Proxy-Konfiguration einer Erweiterung, ein Fetch-Agent); createPlatformDeviceRemote ist das dicke Modell, ein echtes DeviceRemote mit der vollen Listener- und View-Controller-Fläche über ein Device, das die Plattform für dich hostet.
signedProxyId-Auth
createPlatformDeviceRemote öffnet einen Device-RPC-WebSocket zu wss:///device-rpc. Dieser WebSocket authentifiziert sich nicht mit dem Konto-JWT (byJwt, dem Bearer-Token aus dem URnetwork-Login; das „by“ ist ein Überbleibsel von BringYour). Er authentifiziert sich mit signedProxyId, dem auth_token, das der Plattform-Endpunkt /network/auth-client neben der Proxy-URL zurückgibt. Das ist bewusste Trennung der Geltungsbereiche: Die signierte Proxy-ID autorisiert genau einen WebSocket und trägt keine Konto-Autorität — das breite Token fährt also nie auf dem Socket der Datenebene mit. Behandle (proxyUrl, signedProxyId) als einen einzigen Zugangsnachweis. Fordere sie zusammen an, gib sie zusammen weiter, frisch sie zusammen auf, und fordere das Paar neu an, wenn der Proxy den Socket ablehnt, statt ein Teilstück zu cachen.
Listener-Muster und View-Controller
Jede Subscription hat dieselbe Form: add*ChangeListener(fn) gibt eine Unsubscribe-Funktion zurück. Halt sie fest und ruf sie beim Abbau auf. In React gibst du sie zurück:
useEffect(() => {
const unsub = device.addConnectChangeListener(setConnectEnabled);
return unsub;
}, [device]);Das übersteht die doppelt aufgerufenen Effects im Strict Mode von React (add, unsub, add) und lässt genau eine lebende Subscription übrig. Die View-Controller-Fläche (Connect, Standorte, Geräte, Verträge, Block-Aktionen) ist ebenfalls ins wasm gebunden und hängt am Device, wie in device.openConnectViewController() — eine Web-App nutzt also dieselbe Präsentationslogik wieder wie die mobilen Apps, statt den Zustand erneut aus rohen Listenern abzuleiten.
Wie ur.io es nutzt
Die /app-Fläche auf ur.io ist der Referenz-Konsument: Das wasm wird lazy geladen, erst wenn der Nutzer eine Verbindungs-Oberfläche erreicht, sodass Landing-Pages die rund 43 MB nie zahlen, und es gibt ein DeviceRemote pro Tab. Die Exporte sind global für die Seite, init sitzt also hinter einem Singleton.
Die Browser-Erweiterung packt dasselbe wasm ein, instanziiert es heute aber nicht: Sie treibt die Konnektivität über den Plattform-Proxy plus die eigenen Proxy-APIs des Browsers und konsumiert den anderen Einstiegspunkt des npm-Pakets, @urnetwork/sdk-js/react — schlichte fetch-basierte API-Hooks und generierte Typen, die kein wasm brauchen. Wenn du nur die REST-Fläche willst, importiere die und ruf init nie auf: Der Loader löst die wasm-URL zur Laufzeit auf statt über ein statisches new URL(..., import.meta.url), Bundler emittieren sie also nicht für Konsumenten, die sie nicht brauchen.
cgo
Die Daemon-Aufteilung
Die ausgelieferten URnetwork-Apps für Linux und Windows sind auf dieser Schicht gebaut, und beide nutzen dasselbe Zwei-Prozess-Muster — die Form, die für jede cgo-Integration zu kopieren ist, die ein tun-Device berührt. Das SDK liefert die Engine und die C-API; der Daemon ist deiner, mit diesen beiden veröffentlichten Apps als quelloffenen Referenzen:
- Ein Root-/Dienst-Prozess (systemd-Unit, Windows-Dienst) führt das
DeviceLocalaus und besitzt das tun-Interface. - Der unprivilegierte UI-Prozess führt ein
DeviceRemoteaus und hängt sich über Loopback-mTLS-Device-RPC an; die Standardadresse des SDK ist127.0.0.1:12025. - Die mTLS-PEMs werden der UI über einen Kanal übergeben, den das OS selbst autorisiert: einen per
SO_PEERCREDgeprüften Unix-Socket auf Linux (geprüft beim Accept, bevor irgendein Frame gelesen wird), eine Named Pipe auf Windows. Dieser Handshake ist die echte Autorisierungsgrenze, nicht der TCP-Port; Loopback-mTLS hält nur andere lokale Nutzer vom Port fern.
RAII-Wrapper-Semantik (urnetwork_sdk.hpp)
- Handles sind in besitzende Typen gewickelt, deren Destruktor
urnet_releaseaufruft. Aber Release ist nicht Close/Stop: Den letzten Wrapper fallen zu lassen stoppt kein Device und schließt keine Verbindung. Ruf zuerst*_close/*_stopauf (im Wrapper.close()/.stop()) und lass den Wrapper dann freigeben. Mehrere Handles können auf ein lebendes Objekt zeigen, ein Scope-Ende darf also nie still eine lebende Sitzung töten. - Subscriptions kommen als
urnet::Subzurück; der Destruktor meldet ab. - Fehler treten als
urnet::Error-Exceptions auf, die denout_error-Text tragen.
Eine harte Regel, das Gegenteil dessen, was Leute erwarten:
sub = device.addConnectChangeListener([&](bool enabled) { /* ... */ });
sub.close(); // returns immediately; a callback may still be running
// do NOT free what that lambda captured hereListener-Listen sind Copy-on-Write, das Abmelden entfernt den Eintrag also und kehrt zurück. Das ist Absicht (ein Callback darf sich aus sich selbst heraus entfernen), aber es heißt, dass ein Callback auf einem anderen Thread noch laufen kann, nachdem das Sub weg ist. Der echte Absturz ist, den Zustand zu zerstören, den dieser Listener eingefangen hat; halt ihn über den Abbau hinaus am Leben, mit einem shared_ptr oder einem Flag, das der Callback prüft.
Für Leak-Disziplin nimmst du vor einem Szenario einen Snapshot von urnet_live_handle_count(), fährst Konstruieren/Nutzen/Schließen/Freigeben durch und assertierst, dass er zur Baseline zurückkehrt. cgo/smoke tut genau das und ist das Muster zum Kopieren.
Was tatsächlich gebunden ist
Die C-ABI wird aus der Go-Fläche generiert, und der Generator schreibt cgo/coverage_report.txt: jedes exportierte Symbol, und für jedes, das nicht hinüberkam, den Grund (Go-Contexts, net.Conn-Interna, Funktionsparameter, Aufrufe mit Pool-Besitz, RPC-gob-Typen). Lies diese Datei, nicht den Go-Quellcode, um „Ist das aus C aufrufbar?“ zu beantworten. Ein neuer Go-Export erreicht den Header erst, wenn der Generator läuft, der Header wird also neben der Bibliothek ausgeliefert, mit der er geschnitten wurde. Pinne sie zusammen.
Wire-Kompatibilität
Das Device-RPC-Wire-Protokoll ist versionsgepinnt (DeviceRpcVersion, derzeit 1) und wird durchgesetzt, vom Local bei jedem Sync. Es ist bewusst nicht die Release-Version: Die gehosteten Hälften werden unabhängig deployt, sie zu koppeln würde also nach jedem Server-Deploy jeden Browser ablehnen. Eine Nichtübereinstimmung wirft nicht. Das Remote bleibt unsynchronisiert und versucht es erneut, was identisch aussieht zu „der Daemon läuft nicht“. Unterscheide beides mit GetRemoteConnected() plus GetSyncError(): Ein leerer Sync-Fehler heißt noch nicht erreichbar, während "device rpc version mismatch: ..." oder "device instance mismatch: ..." eine Ablehnung ist, die keine Wiederverbindung je behebt. Liefere Daemon und UI aus demselben SDK-Release.
Querschnittsthemen
Einstellungen, die Richtlinien tragen
Diese Flags halten Produktentscheidungen, kein Tuning, und ihre Standardwerte sind die Privacy-Haltung, die deine Nutzer bekommen. Was ein Device ab Werk ausliefert:
| Einstellung | Standard | Wirkung | Hauptkosten |
|---|---|---|---|
SetPerformanceProfile | nil (auto) | Qualitäts- und Geschwindigkeitsfenster laufen nebeneinander; Datenverkehr tritt über mehrere Provider zugleich aus (üblicherweise 3–8), mit Bindung pro Website | einen WindowType festzupinnen verengt auf ein Fenster |
AllowDirect | aus | hält den Anonymisierungs-Hop, sodass kein Provider die echte IP des Nutzers sieht | an: mehr Durchsatz, und der Provider sieht die echte IP des Nutzers |
PostQuantumEncryption | an | versiegelt die Sitzung Client↔Provider, sodass der Operator Bytes weiterleitet, die er nicht lesen kann | ein Provider, mit dem nicht versiegelt werden kann, wird übersprungen, nicht unversiegelt genutzt |
SetRouteLocal | erlauben | Datenverkehr fällt auf die lokale Route zurück, wenn der Tunnel abreißt | Verbieten ist der Kill Switch: Der Datenverkehr stoppt stattdessen |
SetProvideMode | none | das Device bietet dem Netzwerk keine Kapazität an | Bereitstellen als public oder network lässt fremden Datenverkehr über die IP des Nutzers austreten |
Vorbehalte, die zur Tabelle gehören:
AllowDirectist die Geschwindigkeitseinstellung, und die IP, die es offenlegt, ist genau der Hop, den es entfernt. Die Apps zeigen es invertiert als „Starke Anonymisierung“ (Strong Anonymization), standardmäßig an, und es wird auf gehosteten Device-Profilen erzwungen ausgeschaltet, was auch immer ein Aufrufer setzt. Präsentiere es deinen Nutzern nie als geschenkte Geschwindigkeit; präsentiere es als den Tausch, der es ist.PostQuantumEncryptionist die Ende-zu-Ende-Sitzung Client↔Provider, die die Apps als „Post-Quanten-Verschlüsselung“ (Post Quantum Encryption) ausliefern, ab Start standardmäßig an und provider-bereit: Jeder aktuelle Provider-Build aktiviert die Responder-Seite. Solange es an ist, arbeitet der Client fail-closed — er trägt keine Anwendungsdaten im Klartext, und einen Provider, mit dem er nicht versiegeln kann, überspringt er, statt ihn unversiegelt zu nutzen. Die Kosten liegen bei der Verfügbarkeit, nicht bei der Vertraulichkeit. Schalt das Flag aus, und der Datenverkehr kann wieder den Standardpfad nehmen.SetRouteLocalist das Kill-Switch-Primitiv („lokalen Datenverkehr erlauben“). Schalt es aus und gib deinen Nutzern den Schalter, den jede URnetwork-App hat, damit Datenverkehr stoppt, statt zurückzufallen, wenn der Tunnel abreißt.
Die Blindheit des Providers gegenüber der Identität deines Nutzers ist bedingungslos; die Blindheit des Operators gegenüber Inhalten ist es standardmäßig ebenfalls. Zwei Eigenschaften, beide wahr für ein Profil ab Werk — und genau das macht es zutreffend zu sagen, dass keine einzelne Partei zugleich die Identität deines Nutzers und seine Aktivität hält. Kipp eines der beiden Flags, und du gibst die zugehörige Eigenschaft weg, statt sie zu verdienen.
Die drei Modi, die diese beiden Flags auswählen, formuliert für die Nutzer, die du routest:
| Modus | Operator sieht | Provider sieht | Wie du ihn bekommst |
|---|---|---|---|
| Relay, versiegelt | Konto/Quellverbindung, Provider-Zuordnung, Chiffretext sowie Timing/Volumen | Ziel-Datenverkehr und eine Geräte-/Vertrags-ID, nicht die echte IP des Nutzers | der Standard: PostQuantumEncryption an, AllowDirect aus |
| Relay, Standard | Konto/Quellverbindung, Provider-Zuordnung, innere Ziele und Paketbytes | Ziel-Datenverkehr und eine Geräte-/Vertrags-ID, nicht die echte IP des Nutzers | nur mit PostQuantumEncryption aus |
| Direkt | weniger Relay-Beteiligung | die echte IP des Nutzers und Ziel-Datenverkehr | opt-in: AllowDirect an (auf gehosteten Profilen erzwungen aus) |
Der Standard liefert die erste Zeile bereits aus, ohne dass etwas zu setzen wäre. Für die zweite muss PostQuantumEncryption aus sein: Solange es an ist, überspringt der Client einen Provider, mit dem er nicht versiegeln kann, statt auf diese Zeile zu fallen. Es gibt keine API pro Verbindung, die meldet, auf welcher Zeile eine bestimmte Sitzung gelandet ist. Das Bedrohungsmodell spielt diese Zeilen gegen benannte Angreifer durch und benennt ausdrücklich, wo jede versagt.
Threading-Regeln
- Nicht-View-Objekte (
DeviceLocal,DeviceRemote,Api, Network Spaces) sind nebenläufigkeitssicher. Ruf sie aus jedem Thread auf. - View-Controller sind Single-Threaded, sofern einer nichts anderes dokumentiert. Treib jeden aus einem Thread (normalerweise deinem UI-Thread). Browser-JavaScript erfüllt das umsonst; auf Android, Apple und cgo beißt es, wo Callbacks auf Go-verwalteten Threads eintreffen.
- Callbacks feuern auf beliebigen SDK-verwalteten Threads. Marshalle auf deinen UI-Thread, bevor du UI anfasst, und blockiere nie in einem: Ein
DeviceRemoteserialisiert seine Callbacks durch einen einzelnen gepufferten Channel, ein langsamer Listener erzeugt also Gegendruck auf jede Subscription darauf.
Artefaktgrößen
Plane Download- und Packaging-Budgets um diese herum:
| Artefakt | Größe (komprimiert, wo vermerkt) |
|---|---|
| Android AAR | ~37 MB |
| Apple xcframework (zip) | ~116 MB |
| JS wasm | ~43,5 MB |
| Linux c-shared (zip) | ~25 MB |
| Windows c-shared (zip) | ~21 MB |
API-Stabilität
Versionen sind datumsbasiert (vYYYY.M.D-); einen SemVer-Vertrag gibt es noch nicht, und das npm-Paket ist Beta und wird nächtlich neu veröffentlicht. Behandle alle drei Flächen als in Bewegung, bis eine 1.0 kommt: Pinne exakte Versionen, lies bei jedem Sprung die Release Notes, und halte gepaarte Artefakte (Daemon und UI, Erweiterung und App) auf einem Release.