feat!: Token-Schluessel und Wer-bin-ich entfernt, Version 1.0

Beides brachte allein einen Anzeigenamen. Seit der Benutzer als Widget auf dem
Schreibtisch steht, kommt der aus der HoneyOS-Anmeldung — keine App muss mehr
etwas dafuer bereitstellen, und beide Leisten tragen ueberall dasselbe.

Der Token-Schluessel hatte zudem eine scharfe Kante: Zeigte das Feld
Wer-bin-ich auf eine fremde Adresse, ging der Ausweis der App dorthin.

Weg sind damit auch checkAuth, askMe und die Anmeldezeile auf den Kacheln. Alte
honeyos.json-Dateien werden weiter gelesen; die Felder verfallen beim naechsten
Speichern still.

1.0, weil die Bedienung sich aendert und der Stand rund ist: zwei Leisten mit
gleichem Inhalt, Widgets fuer Benutzer und Wetter, die HoneyBar auch in fremden
Apps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-29 20:17:18 +02:00
1 parent b999a66fed
commit fc4dd12c5e
7 files changed
+22 -168

No files matched your search

+7 -15
View File
@@ -131,7 +131,6 @@ Solange das so bleibt, kann HoneyOS **keine InkNote-Daten anzeigen**. Der Weg
wäre, dass InkNote seine Sitzung zusätzlich als `HttpOnly`-Cookie setzt; der wäre, dass InkNote seine Sitzung zusätzlich als `HttpOnly`-Cookie setzt; der
Token-Weg kann daneben bleiben, damit PWA und Offline-Betrieb unangetastet sind. Token-Weg kann daneben bleiben, damit PWA und Offline-Betrieb unangetastet sind.
Erledigt zugleich den Token-Schlüssel (siehe „Offene Entscheidungen").
### 1.5b HoneyOS selbst als PWA ### 1.5b HoneyOS selbst als PWA
@@ -356,23 +355,16 @@ Aether oder InkNote neu zu bauen.
→ **Erst in einer App einbauen, eine Woche benutzen, dann entscheiden.** Nicht → **Erst in einer App einbauen, eine Woche benutzen, dann entscheiden.** Nicht
in beide gleichzeitig. in beide gleichzeitig.
### Token-Schlüssel behalten oder löschen? ### Token-Schlüssel behalten oder löschen? — ✅ gelöscht (29.08.2026)
**Empfehlung: löschen.** Gründe: Mitsamt dem Feld „Wer-bin-ich". Die Begründung von damals hat sich gehalten: Es
brachte allein den Anzeigenamen, wirkte an zwei Stellen unterschiedlich, und ein
Token an eine fremde Adresse zu schicken war eine Falle, die niemand braucht.
1. Er wirkt nur an einer von zwei Stellen — in der HoneyBar ja, auf dem Desktop Den Namen zeigt jetzt das Benutzer-Widget auf dem Schreibtisch, aus der
nie. Das ist schwer zu erklären und noch schwerer zu merken. HoneyOS-Anmeldung — dort, wo er hingehört, und ohne dass eine App etwas dafür
2. Er bringt allein die Namensanzeige. Kein Funktionsunterschied. tun muss.
3. Sobald 1.3 gebaut ist, ist er ohnehin überflüssig.
4. **Scharfe Kante:** Das Feld „Wer-bin-ich" darf auch eine vollständige fremde
Adresse sein. Steht dort etwas Fremdes *und* ein Token-Schlüssel, schickt die
HoneyBar den InkNote-Token dorthin. Das setzt voraus, dass jemand Angemeldetes
das selbst einträgt — bei einer Ein-Personen-Instanz also kein akuter
Notstand, aber eine Falle, die niemand braucht.
Wird er doch behalten, gehört mindestens eingebaut: **mit Token-Schlüssel nur
relative Pfade zulassen**, damit ein Ausweis nie an einen fremden Ursprung geht.
Drei Zeilen.
### Persönliche Apps je Nutzer? ### Persönliche Apps je Nutzer?
+12 -18
View File
@@ -109,8 +109,6 @@ entfällt der Schlüssel und alles funktioniert ohne Einschränkung.
| Adresse | wohin der Start springt | | Adresse | wohin der Start springt |
| Symbol | cloud · note · box · chip · folder · gear · globe · disk | | Symbol | cloud · note · box · chip · folder · gear · globe · disk |
| Taste | Zifferntaste im Terminal-Modus | | Taste | Zifferntaste im Terminal-Modus |
| Wer-bin-ich | optional, siehe unten |
| Token-Schlüssel | nur für Apps mit Bearer-Token im localStorage statt Cookie |
Gespeichert wird die Liste als Ganzes; der Server prüft sie noch einmal und antwortet mit Gespeichert wird die Liste als Ganzes; der Server prüft sie noch einmal und antwortet mit
dem, was er wirklich behalten hat. Eine Adresse ohne `https://` kommt als Fehlermeldung dem, was er wirklich behalten hat. Eine Adresse ohne `https://` kommt als Fehlermeldung
@@ -388,25 +386,21 @@ nur, wessen App selbst eine schickt.
- **Andere Proxys:** Caddy braucht ein Zusatzmodul (`replace-response`), - **Andere Proxys:** Caddy braucht ein Zusatzmodul (`replace-response`),
Traefik ein Plugin für den Antwortkörper. nginx kann es ohne Erweiterung. Traefik ein Plugin für den Antwortkörper. nginx kann es ohne Erweiterung.
## Wer ist wo angemeldet ## Wer angemeldet ist
Das Feld **Wer-bin-ich** ist der Pfad, unter dem eine App „wer bin ich" beantwortet Steht auf dem Schreibtisch, als eigenes Widget: Bild, Name und Begrüßung, aus
(relativ zur Adresse oder absolut). Aus der Antwort wird der Anzeigename gefischt — der HoneyOS-Anmeldung. In den Apps steht es **nicht** — die HoneyBar dort trägt
`display_name`, `displayName`, `name`, `username`, auch verschachtelt unter `user`. genau dasselbe wie die Menüleiste hier, sonst merkt man beim Wechsel, dass es
zwei verschiedene Leisten sind.
Gelesen wird das an zwei Stellen, und die verhalten sich **unterschiedlich**: Früher konnte jede App einen „Wer-bin-ich"-Pfad und einen Token-Schlüssel
mitbringen; die HoneyBar fragte damit die App nach dem Anzeigenamen. Beides ist
seit dem 29.08.2026 **entfernt**. Es brachte allein einen Namen, wirkte je nach
Domain unterschiedlich, und der Token-Schlüssel hatte eine scharfe Kante: Zeigte
das Wer-bin-ich-Feld auf eine fremde Adresse, ging der Ausweis der App dorthin.
Wer die Felder in seiner `honeyos.json` noch stehen hat, braucht nichts zu tun —
sie werden beim nächsten Speichern still verworfen.
1. **Die HoneyBar in der App** fragt ihre eigene App. Gleicher Ursprung → funktioniert
sofort, ohne Änderung an Aether oder InkNote.
2. **Der Desktop auf `os.…`** fragt über die Domaingrenze. Dafür muss die App diesen
Ursprung erlauben:
- InkNote: `ALLOWED_ORIGINS=https://os.honeyy.duckdns.org`
- Aether: CORS mit `Access-Control-Allow-Credentials` — noch nicht vorhanden
Fehlt das, bleibt die Anzeige leer und sonst passiert nichts.
**Voraussetzung:** alle Adressen unter derselben Domain (`*.honeyy.duckdns.org`). Sonst
behandelt der Browser sie als fremde Site und schickt die Sitzung gar nicht erst mit.
## Entwickeln ## Entwickeln
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -28,7 +28,7 @@ const localSub = "local"
// Version ist die einzige Stelle, an der sie steht. build.sh liest sie hier aus // Version ist die einzige Stelle, an der sie steht. build.sh liest sie hier aus
// und vergibt denselben Docker-Tag; die Oberfläche holt sie über /api/public. // und vergibt denselben Docker-Tag; die Oberfläche holt sie über /api/public.
// Vorher stand sie zusätzlich in os.js und lief mit den Image-Tags auseinander. // Vorher stand sie zusätzlich in os.js und lief mit den Image-Tags auseinander.
const Version = "0.9" const Version = "1.0"
type Config struct { type Config struct {
Addr string Addr string
+1 -5
View File
@@ -24,8 +24,6 @@ type App struct {
URL string `json:"url"` URL string `json:"url"`
Glyph string `json:"glyph,omitempty"` Glyph string `json:"glyph,omitempty"`
Key string `json:"key,omitempty"` Key string `json:"key,omitempty"`
Me string `json:"me,omitempty"`
TokenKey string `json:"tokenKey,omitempty"`
} }
type User struct { type User struct {
@@ -117,8 +115,6 @@ func (s *Store) SetApps(apps []App) error {
a.URL = strings.TrimSpace(a.URL) a.URL = strings.TrimSpace(a.URL)
a.Glyph = strings.TrimSpace(a.Glyph) a.Glyph = strings.TrimSpace(a.Glyph)
a.Key = strings.TrimSpace(a.Key) a.Key = strings.TrimSpace(a.Key)
a.Me = strings.TrimSpace(a.Me)
a.TokenKey = strings.TrimSpace(a.TokenKey)
if !idPattern.MatchString(a.ID) { if !idPattern.MatchString(a.ID) {
return fmt.Errorf("App %d: ID muss klein geschrieben sein und mit Buchstabe oder Ziffer beginnen", i+1) return fmt.Errorf("App %d: ID muss klein geschrieben sein und mit Buchstabe oder Ziffer beginnen", i+1)
@@ -141,7 +137,7 @@ func (s *Store) SetApps(apps []App) error {
if len([]rune(a.Key)) > 1 { if len([]rune(a.Key)) > 1 {
return fmt.Errorf("App %q: Taste ist ein einzelnes Zeichen", a.ID) return fmt.Errorf("App %q: Taste ist ein einzelnes Zeichen", a.ID)
} }
for _, f := range []string{a.Name, a.Subtitle, a.URL, a.Me, a.TokenKey} { for _, f := range []string{a.Name, a.Subtitle, a.URL} {
if len(f) > maxFieldLen { if len(f) > maxFieldLen {
return fmt.Errorf("App %q: ein Feld ist länger als %d Zeichen", a.ID, maxFieldLen) return fmt.Errorf("App %q: ein Feld ist länger als %d Zeichen", a.ID, maxFieldLen)
} }
-40
View File
@@ -153,8 +153,6 @@
background: var(--p); border-color: var(--p); color: var(--bg); text-shadow: none; background: var(--p); border-color: var(--p); color: var(--bg); text-shadow: none;
} }
.right { display: flex; align-items: center; gap: 8px; } .right { display: flex; align-items: center; gap: 8px; }
.user { color: var(--p); max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user[hidden] { display: none; }
.clock { color: var(--p-mid); font-variant-numeric: tabular-nums; } .clock { color: var(--p-mid); font-variant-numeric: tabular-nums; }
/* Anzeige, kein Knopf: Wer darauf klickt, erwartet, dass etwas passiert. /* Anzeige, kein Knopf: Wer darauf klickt, erwartet, dass etwas passiert.
Eingestellt wird das Wetter dort, wo alles eingestellt wird. */ Eingestellt wird das Wetter dort, wo alles eingestellt wird. */
@@ -187,7 +185,6 @@
<a class="home" id="home" title="HoneyOS">&#9632; HONEY<b>OS</b></a> <a class="home" id="home" title="HoneyOS">&#9632; HONEY<b>OS</b></a>
<nav class="apps" id="apps"></nav> <nav class="apps" id="apps"></nav>
<div class="right"> <div class="right">
<span class="user" id="user" hidden></span>
<span class="wx" id="wx" hidden></span> <span class="wx" id="wx" hidden></span>
<span class="clock" id="clock"></span> <span class="clock" id="clock"></span>
<button class="menu-btn" id="menuBtn" aria-haspopup="true" aria-expanded="false">&#9662;</button> <button class="menu-btn" id="menuBtn" aria-haspopup="true" aria-expanded="false">&#9662;</button>
@@ -279,41 +276,6 @@
el.hidden = false; el.hidden = false;
} }
/**
* Zeigt an, wer angemeldet ist. Die HoneyBar steckt in der App selbst, der
* Aufruf geht also an denselben Ursprung — kein CORS, keine Freigabe nötig.
* Antwortet die App nicht wie erwartet, bleibt das Feld leer und niemand
* merkt etwas: die Anzeige ist eine Zugabe, keine Zusicherung.
*/
async function showUser() {
const me = apps.find((a) => a.id === current);
if (!me || !me.me) return;
try {
const headers = {};
if (me.tokenKey) {
const tok = localStorage.getItem(me.tokenKey);
if (tok) headers.Authorization = "Bearer " + tok;
}
const res = await fetch(url(me, me.me), { credentials: "same-origin", cache: "no-store", headers });
if (!res.ok) return;
const data = await res.json();
const u = (data && (data.user || data.profile)) || data || {};
const name = u.display_name || u.displayName || u.name || u.username || u.preferred_username;
if (!name) return;
const el = root.getElementById("user");
el.textContent = name;
el.hidden = false;
} catch {
/* nicht angemeldet, kein Endpunkt, kein Netz — alles kein Grund zu lärmen */
}
}
function url(app, path) {
if (!path) return app.url;
if (/^https?:\/\//i.test(path)) return path;
return app.url.replace(/\/+$/, "") + (path.startsWith("/") ? path : "/" + path);
}
function clock() { function clock() {
root.getElementById("clock").textContent = new Date().toLocaleTimeString("de-DE", { root.getElementById("clock").textContent = new Date().toLocaleTimeString("de-DE", {
hour: "2-digit", hour: "2-digit",
@@ -427,7 +389,6 @@
nub.remove(); nub.remove();
nub = null; nub = null;
mount(); mount();
showUser();
}); });
document.body.appendChild(nub); document.body.appendChild(nub);
} }
@@ -492,7 +453,6 @@
if (document.body) mount(); if (document.body) mount();
else document.addEventListener("DOMContentLoaded", mount, { once: true }); else document.addEventListener("DOMContentLoaded", mount, { once: true });
showUser();
wetter(); wetter();
} }
+1 -89
View File
@@ -43,7 +43,6 @@ const state = {
oidc: false, oidc: false,
settings: { ...DEFAULT_SETTINGS }, settings: { ...DEFAULT_SETTINGS },
probes: {}, // appId -> "up" | "down" | "unknown" probes: {}, // appId -> "up" | "down" | "unknown"
auth: {}, // appId -> {status: "in"|"out"|"blind", name}
wins: {}, // id -> {el, task, min} wins: {}, // id -> {el, task, min}
zTop: 10, zTop: 10,
}; };
@@ -650,7 +649,6 @@ async function probeApps() {
); );
paintProbes(); paintProbes();
renderStats(); renderStats();
checkAuth();
} }
async function probe(url) { async function probe(url) {
@@ -666,91 +664,11 @@ async function probe(url) {
} }
} }
/* ── Anmeldestand ──────────────────────────────────────────────────────────
* Wenn eine App einen "me"-Endpunkt in der Konfiguration hat, fragen wir ihn
* mit den Keksen des Browsers. Das funktioniert nur, weil alle Adressen unter
* derselben Domain liegen — sonst schickt der Browser die Sitzung nicht mit.
*
* Antwortet die App nicht oder verbietet CORS, ist der Stand "blind": dann
* zeigt HoneyOS eben nur die Lampe wie bisher. Kein Fehler, keine Meldung —
* die Anzeige ist eine Zugabe, nichts, worauf sich jemand verlassen muss.
*/
async function checkAuth() {
await Promise.all(
state.cfg.apps
.filter((a) => a.me)
.map(async (app) => {
state.auth[app.id] = await askMe(app);
})
);
paintProbes();
applyAuthName();
}
async function askMe(app) {
const ctl = new AbortController();
const t = setTimeout(() => ctl.abort(), 5000);
try {
const headers = {};
// Apps mit Bearer-Token statt Cookie: der Schlüssel steht in der
// Konfiguration, der Wert im localStorage — und der ist pro Domain.
// Von hier aus lesbar ist er nur, wenn HoneyOS dieselbe Domain ist.
if (app.tokenKey) {
try {
const tok = localStorage.getItem(app.tokenKey);
if (tok) headers.Authorization = `Bearer ${tok}`;
} catch {
/* kein Speicherzugriff — dann ohne */
}
}
const res = await fetch(resolveUrl(app, app.me), {
credentials: "include",
cache: "no-store",
headers,
signal: ctl.signal,
});
if (res.status === 401 || res.status === 403) return { status: "out" };
if (!res.ok) return { status: "blind" };
return { status: "in", name: extractName(await res.json()) };
} catch {
return { status: "blind" };
} finally {
clearTimeout(t);
}
}
/** Relative Pfade beziehen sich auf die App-Adresse, absolute gelten wie sie sind. */
function resolveUrl(app, path) {
if (!path) return app.url;
if (/^https?:\/\//i.test(path)) return path;
return app.url.replace(/\/+$/, "") + (path.startsWith("/") ? path : "/" + path);
}
/** Jede App antwortet anders — hier wird der Anzeigename herausgefischt. */
function extractName(data) {
const u = (data && (data.user || data.profile)) || data || {};
return u.display_name || u.displayName || u.name || u.username || u.preferred_username || "";
}
/** Nennt eine App einen Namen, gilt der — er kommt vom Anbieter, die
* Einstellung ist nur der Platzhalter, solange niemand antwortet. */
function applyAuthName() {
const named = Object.values(state.auth).find((a) => a.status === "in" && a.name);
if (named) {
state.cfg.user = named.name;
renderUser();
renderStats();
}
}
const STATE_TEXT = { up: "online", down: "keine antwort", unknown: "prüfe …" }; const STATE_TEXT = { up: "online", down: "keine antwort", unknown: "prüfe …" };
const AUTH_TEXT = { in: "angemeldet", out: "nicht angemeldet" };
function stateLine(appId) { function stateLine(appId) {
const s = state.probes[appId] || "unknown"; const s = state.probes[appId] || "unknown";
const a = state.auth[appId]; return { s, text: STATE_TEXT[s] };
const extra = a && AUTH_TEXT[a.status] ? ` · ${a.status === "in" && a.name ? a.name : AUTH_TEXT[a.status]}` : "";
return { s, text: STATE_TEXT[s] + (s === "up" ? extra : "") };
} }
function paintProbes() { function paintProbes() {
@@ -1211,12 +1129,6 @@ function openApps() {
</select></label> </select></label>
<label class="ae-field"><span>Taste</span> <label class="ae-field"><span>Taste</span>
<input data-i="${i}" data-f="key" value="${escapeAttr(a.key || "")}" maxlength="1" placeholder="1" /></label> <input data-i="${i}" data-f="key" value="${escapeAttr(a.key || "")}" maxlength="1" placeholder="1" /></label>
<label class="ae-field"><span>Wer-bin-ich</span>
<input data-i="${i}" data-f="me" value="${escapeAttr(a.me || "")}" placeholder="/api/v1/auth/me" spellcheck="false" /></label>
<p class="ae-hint">optional — zeigt an, ob du dort angemeldet bist</p>
<label class="ae-field"><span>Token-Schlüssel</span>
<input data-i="${i}" data-f="tokenKey" value="${escapeAttr(a.tokenKey || "")}" placeholder="leer lassen" spellcheck="false" /></label>
<p class="ae-hint">nur für Apps mit Bearer-Token im localStorage statt Cookie</p>
<div class="ae-actions"> <div class="ae-actions">
<button class="btn btn-danger" data-del="${i}">löschen</button> <button class="btn btn-danger" data-del="${i}">löschen</button>
</div> </div>