Magma 1.0: signiert, notarisiert, und was Apple dafür verlangt hat

Magma 1.0 ist da: signiertes und notarisiertes DMG für macOS. Was Apple dafür verlangt und welche drei Stolpersteine je eine Debug-Runde gekostet haben."

Magma 1.0.0 liegt seit heute auf GitHub. Der sichtbarste Unterschied zu den sechs Versionen davor steht nicht im Changelog der Features: Das DMG ist mit einem Developer-ID-Zertifikat signiert und von Apple notarisiert. Wer es installiert, zieht die App in den Programme-Ordner, doppelklickt und arbeitet. Kein Rechtsklick, kein „Öffnen“ im Kontextmenü, kein zweites „Öffnen“ im Dialog danach.

Downloads: github.com/7H3-CH053N/Magma/releases

Der Dokumente-Ordner fragt jetzt nur noch einmal

Die zwei Klicks beim ersten Start waren erklärbar, das stand seit v0.1.0 so in den Release Notes. Teurer war etwas anderes, und das habe ich vorher unterschätzt: macOS bindet erteilte Berechtigungen an die Identität einer App. Ohne Signatur hat Magma keine stabile Identität. Also sah jeder Start wie eine neue App aus, und die Systemabfrage für den Dokumente-Ordner kam wieder. Bei einer Notiz-App, deren Vault genau dort liegt, ist das kein Schönheitsfehler, sondern ein Dialog zwischen mir und meiner Arbeit, jeden einzelnen Tag.

Mit Signatur fragt macOS einmal.

Alles bis einschließlich v0.1.5 bleibt unsigniert. Ich baue keine alten Tags neu.

Sechs Secrets, alle optional

Gesteuert wird das Signieren ausschließlich über sechs Repository-Secrets:

SecretInhalt
APPLE_CERTIFICATEDie Developer-ID-.p12, base64-kodiert
APPLE_CERTIFICATE_PASSWORDDas beim Export vergebene Passwort
APPLE_SIGNING_IDENTITYDeveloper ID Application: Alex Januschewsky (7QR7KTK28M)
APPLE_API_KEYDie Key ID aus App Store Connect
APPLE_API_ISSUERDie Issuer ID aus App Store Connect
APPLE_API_KEY_P8Die AuthKey_*.p8, base64-kodiert

Alle sechs sind optional, genau wie der Updater-Schlüssel. Ist keines gesetzt, entsteht trotzdem ein installierbares DMG, eben das unsignierte. Ein Fork braucht davon nichts, und niemand muss ein Apple-Konto haben, um Magma selbst zu bauen. Das war mir wichtiger als ein schlanker Workflow: Ein Repo, das ohne meine Zertifikate nicht mehr baut, ist kein offenes Repo.

Die Variablennamen sind Tauris eigene. Ich habe sie aus der macOS-CLI-Binary der gepinnten Version gezogen, per npm pack und strings, statt sie aus dem Gedächtnis zu schreiben. hardened_runtime steht in tauri-utils 2.9.3 ohnehin auf true, da war nichts zu tun.

Drei Stolpersteine, die in keiner Anleitung stehen

Die .p12 muss aus „Meine Zertifikate“ in der Schlüsselbundverwaltung exportiert werden. Nur unter diesem Punkt hängt der private Schlüssel mit dran. Der Export aus der Ansicht „Zertifikate“ ergibt eine Datei, die aussieht wie eine .p12, sich importieren lässt und nichts signiert.

security find-identity -v -p codesigning meldete zweimal 0 valid identities found, bei sichtbar installiertem Zertifikat, aus zwei verschiedenen Gründen nacheinander. Erst lag das Zertifikat im System-Schlüsselbund und der private Schlüssel in Anmeldung. Beide müssen im selben liegen. Danach fehlte Apples Zwischenzertifikat Developer ID – G2, das man sich bei apple.com/certificateauthority holt. Ohne das lässt sich die Kette nicht prüfen, und die Identity gilt als ungültig. Ohne installiertes Xcode ist es oft nicht da. Der Befehl, der die beiden Fälle trennt, ist security find-identity ohne -v: Dort taucht die Identity dann mit CSSMERR_TP_NOT_TRUSTED auf, statt einfach zu fehlen.

Für die Notarisierung reicht ein App-Store-Connect-Teamschlüssel mit der Rolle „Entwickler“. App Manager braucht es nicht. Die .p8 lässt sich genau einmal herunterladen, danach ist sie weg.

Zwei Zeilen im Workflow, die je eine Debug-Runde gekostet haben

Die Variablen gehen über GITHUB_ENV, nicht über das env: des Schritts. Mit env: wird ein fehlendes Secret zur leeren Zeichenkette, und ein leeres APPLE_CERTIFICATE lässt die CLI mit nichts signieren wollen. Das ist exakt die Fehlerform, die mir der fehlende Updater-Schlüssel schon einmal beschert hat: nicht „Secret fehlt“, sondern ein Signaturfehler mitten im Build. Über GITHUB_ENV bedeutet ein fehlendes Secret eine fehlende Variable, und Tauri überspringt das Signieren.

Das Zertifikat wird per Heredoc geschrieben. Eine base64-kodierte .p12 kann Zeilenumbrüche enthalten, und eine KEY=value-Zeile in GITHUB_ENV schneidet am ersten davon ab. Der Notarisierungsschlüssel landet in einer Datei, weil Tauri einen Pfad erwartet, ein Secret aber nur Inhalt halten kann.

36 Minuten in Apples Warteschlange

Der erste Lauf brauchte 36 Minuten zwischen „Notarizing“ und „Finished with status Accepted“. Derselbe Job unsigniert: acht. Das ist kein Fehler, das ist Apples Queue, und man sieht währenddessen nichts, weil GitHub die Logs eines laufenden Jobs nicht herausgibt (HTTP 404).

Ein brauchbarer Zwischenbeweis von außen: In App Store Connect unter Benutzer und Zugriff → Integrationen zeigt der Schlüssel ein „Zuletzt verwendet“. Steht dort das heutige Datum, ist die Einreichung angekommen und man wartet nur noch.

Der bundle-Job hatte kein Zeitlimit, also galt GitHubs Standard von sechs Stunden. Auf einem macOS-Runner, der zehnfach abgerechnet wird. Steht jetzt auf timeout-minutes: 60. Ein Release dauert damit rund 45 Minuten statt acht, und das ist der Preis, den ich ab jetzt bei jeder Version zahle.

Windows bleibt unsigniert

Die MSI baue ich weiter mit, jede Version, und ich teste sie auch. Aber SmartScreen meldet sich dort weiterhin mit „Weitere Informationen → Trotzdem ausführen“. Ein Windows-Code-Signing-Zertifikat ist ein eigenes Produkt, separat gekauft und jährlich verlängert. Für ein Nebenprojekt, das ich selbst auf dem Mac benutze, gebe ich das Geld nicht doppelt aus. Der Fokus liegt auf Apple.

Und dann waren da noch die Bugs

Die Farbwahl im Graph wurde nie exakt übernommen. Helles Rot wurde volles Rot, Grau wurde Rot. Schuld war eine Funktion: hueFromHex behielt den Farbton und warf Sättigung und Helligkeit weg, alles danach baute sich auf festen 66 % und 56 % neu auf. Grau ist der schlimmste Fall, weil es gar keinen Farbton hat, also griff if (d === 0) return 0. Und 0 ist Rot. Der Picker selbst war nie falsch, er liest den gespeicherten Hex direkt. Nur die Zeichenfläche widersprach ihm, was die Meldung „die Farbe wird nicht übernommen“ erklärt statt „das Bedienelement ist kaputt“. Die Arithmetik liegt jetzt in src/lib/graphColors.ts und ist ohne DOM testbar.

Meine eigene Regression, zweimal falsch gelöst. Die Unterordner-Schattierung, die ich mit dem Farbfix eingebaut hatte, kollabierte. Ein Fächer, der pro Schritt breiter wurde und an der Bandgrenze klemmte, legte jeden Schritt ab dem vierten auf eine der beiden Kanten. Ein Ordner mit zwölf Unterordnern kam als zwei flache Blöcke heraus. Der zweite Versuch scheiterte anders: Eine symmetrische Leiter ist bei halber Bandbreite kongruent, also landeten Schritt 5 und 6 auf demselben Wert. Beide Versuche bestanden einen Test, der nur die ersten vier Schritte ansah. Ein Test mit zu kleinem N ist ein Freispruch, den man sich selbst ausstellt.

Geisterknoten in der Legende, mit Einträgen wie missing:[ai.rs](http:. Link-Ziele ohne Notiz dahinter tragen eine synthetische ID statt eines Pfades, und wenn das Ziel selbst ein Markdown-Link ist, steckt darin eine URL. Die Ordner-Erkennung las den Schrägstrich in http:// als Ordnergrenze. Gefiltert wird jetzt über das missing-Flag, nicht durch Schnüffeln am String.

Farbschemata gibt es jetzt pro Modus, sieben Tokens für hell und sieben für dunkel, beide gleichzeitig sichtbar statt dem aktuellen Modus folgend. Einen dunklen Akzent zu bearbeiten, während man das helle Schema ansieht, ist der Normalfall. Dazu eine Kontrastwarnung nach WCAG 2.1, AA für normalen Text. Geprüft wird nur Text gegen seinen Hintergrund. Akzentfarbe und die KI-Markierung sind bewusst ausgenommen, weil sie Marker und Button-Füllungen sind: Am Fließtext-Maßstab gemessen würden schon die ausgelieferten Standardwerte durchfallen, und eine Warnung, die auf einer unveränderten Installation feuert, bringt Leuten bei, Warnungen zu ignorieren.

Der Blog-Import blieb fünf Minuten auf „593 Beiträge gefunden“ stehen. Er lief, und der Großteil der Arbeit war sinnlos: Der RSS-Rückfall geht bis zu dreißig Feed-Seiten einzeln durch, um Autorennamen zu finden, die die REST-API nicht liefert. Er lief vor dem Anwenden des eingetragenen Autors, also wurde jeder gefundene Name eine Zeile später überschrieben. Der Autor wird jetzt vorher gelesen und durchgereicht, bei gesetztem Autor fasse ich den Feed gar nicht mehr an. Ein Test pinnt die Reihenfolge, weil genau so etwas ein Refactor wieder einbaut, ohne dass irgendwas fehlschlägt.

Der erste Frontend-Test überhaupt

36 Vitest-Tests in graphColors.test.ts, theme.test.ts und contrast.test.ts. npm test existierte vorher als Skript, das nie jemand aufrief, auch die CI nicht. Ein Frontend-Test hätte also geschrieben und still nie ausgeführt werden können. Das ist das Hausmuster dieses Projekts in Reinform: Die gefährlichen Fehler in Magma waren durchweg solche, die nichts kaputt machen, was ein gewöhnlicher Test bemerkt. Der checks-Job ruft npm test jetzt auf.

Zwei meiner eigenen Kontrast-Tests behaupteten übrigens das Gegenteil dessen, was die Arithmetik tut. Ich habe nachgemessen, statt sie anzupassen, bis sie grün waren.

Was offen bleibt

Das Auto-Update ist eingebaut, aber nicht scharf. Es hängt am Minisign-Schlüssel, den nur ich als Repo-Inhaber erzeugen kann, und der ausgelieferte pubkey ist noch ein Platzhalter. Tauris Updater-Schlüssel hat mit dem Apple-Zertifikat nichts zu tun, das ist ein eigenes Paar, kostenlos und selbst erzeugt. Ich hatte das lange falsch im Kopf und beides in einen Topf geworfen.

Und Issue #18 steht weiter: kein Index, kein Watcher, die Oberfläche pollt alle vier Sekunden. Gemessen an 8000 Notizen liest list_notes kalt 705 ms. Spürbar, aber kein Fehler. Der In-Memory-Index kommt, wenn ich die lokalen Embeddings angehe, denn ohne ihn haben sie keine Grundlage.

share that. please.

LinkedIn
Facebook
Threads
Telegram
WhatsApp
Email
Alex Januschewsky, KI-Berater und Prompt Rocker
Über den Autor
Alex Januschewsky
KI-Berater & Prompt Rocker // Salzburg, AT

Ich arbeite seit 1989 in Kommunikation und Werbung, heute als KI-Berater unter der Marke Prompt Rocker. Ich baue Dinge selbst: mit Claude Code, n8n, WordPress und Coolify auf Hetzner. Vibe Coding ist für mich kein Begriff, sondern tägliche Praxis. Auf vibecraft.rocks dokumentiere ich, was dabei entsteht.

ship safe.