Zum Hauptinhalt springen

Deploy / Betrieb (Cloudflare) — Runbook (OP-DEPLOY-1)

Stand: Live in Produktion auf app.medidentas.com (Auto-Deploy bei Push auf main); offen bleibt nur die organisatorische HQ-Eigentümer-Zuordnung der Domain (s. §8). Ziel: eine Subdomain app.medidentas.com bedient UI + API über einen Worker (Workers + Static Assets). Begründung: ein Origin → Cloudflare Access (R7) schützt UI und API in einer Policy, kein CORS, ein TLS-Zertifikat. Quelle des Stacks: Stack.md (A-4).

1. Architektur des Deploys

app.medidentas.com (Cloudflare Access davor, R7/OP-AUTH-1)

┌──────┴───────────────┐
Static Assets│ Worker medidentas-api│ (server/, Hono)
(Angular UI) │ /api/* → Hono │
dist/browser │ sonst → ASSETS │ ── D1 (medidentas) · Cron (R5)
└───────────────────────┘
  • UI: Angular-Build (client/dist/browser) als Static Assets desselben Workers (wrangler.toml [assets], binding = "ASSETS"). SPA-Deep-Links fallen in server/src/index.ts auf index.html zurück.
  • API: alles unter /api/* → Hono-App (unverändert).
  • DB: D1 medidentas (idempotente Auto-Migration beim Start, db/migrate.ts).
  • Cron: stündliche Wiedervorlage-Erinnerungen (R5, [triggers]).

2. Entscheidungen (28.06.2026)

  • Domain verfügbar: medidentas.com liegt bereits im Cloudflare-Account (Zone aktiv) → der Custom-Domain-Deploy auf app.medidentas.com ist technisch nicht mehr blockiert. Die im OP genannte Übertragung an HQ ist eine organisatorische Folgeaufgabe (Eigentümer-/Account-Zuordnung), kein Deploy-Blocker mehr.
  • EU-Datenresidenz = Pflicht (G-5, OP-DSGVO-1, RISK-17): alle personenbezogenen Daten in der EU: D1 mit EU-Location-Hint anlegen (--location weur ⟶ Western Europe), NextCloud EU-gehostet (A-1), Cloudflare Data Localization Suite (Regional Services + EU Metadata Boundary) für die Zone, keine US-Sub-Prozessoren ohne AVV/SCC (RISK-4/RISK-15). Vor Produktiv-Go-live verbindlich prüfen.
  • Deploy-Weg = CI: CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID als GitHub-Actions-Secrets → Deploy über app-deploy.yml. Auto-Deploy bei Push auf main (Änderungen in server/**/client/** oder am Workflow selbst) + weiterhin manuell (workflow_dispatch) auslösbar. concurrency verhindert überlappende Deploys.
  • "Sofort absichern": Betrieb hinter Cloudflare Access mit AUTH_ENFORCED=true von Anfang an. ⚠️ Wichtig: Access (self-hosted) braucht einen Hostnamen in eurer Zone — *.workers.dev lässt sich nicht mit Access schützen. AUTH_ENFORCED=true ohne Access davor ⇒ jeder /api-Request hat keinen cf-access-…-Header ⇒ 401 (App unbedienbar). Konsequenz: der abgesicherte Test läuft auf app.medidentas.com (Zone ist da), nicht auf *.workers.dev.

3. Voraussetzungen

  1. Domain medidentas.comim Cloudflare-Account vorhanden (Zone aktiv). HQ-Eigentümer- Zuordnung: organisatorischer Folgepunkt (OP-DEPLOY-1).
  2. Deploy-Credential: CLOUDFLARE_API_TOKEN (Scopes: Workers Scripts: Edit, D1: Edit, Workers KV/Assets, bei Custom Domain zusätzlich Zone: DNS/Workers Routes) + CLOUDFLARE_ACCOUNT_ID als GitHub-Actions-Secrets. Nie im Repo (RISK-8).
  3. Secrets (per wrangler secret put, nicht im Repo): NEXTCLOUD_URL/USER/APP_PASSWORD (R3/R9, EU- Hosting), SIGNATUR_PROVIDER + Provider-Keys (R4/OP-SIGN-1; DocuSign s. §7d, MED-D-220).

4. D1-Datenbank anlegen (einmalig, EU-Residenz)

cd server
npx wrangler d1 create medidentas --location weur # EU (Western Europe) — Pflicht (G-5)
# → gibt eine database_id aus; in wrangler.toml unter [[d1_databases]] eintragen
# (Platzhalter REPLACE_WITH_D1_ID ersetzen). Die Auto-Migration läuft beim ersten Request.

5. Empfohlener Weg — abgesichert auf app.medidentas.com (EU)

"Sofort absichern" gewählt + Zone ist verfügbar ⇒ direkt auf die Subdomain (Access schützt keine *.workers.dev). Reihenfolge:

  1. Secrets setzen (GitHub → Settings → Secrets → Actions): CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID.
  2. D1 anlegen (Schritt 4, --location weur) + database_id in wrangler.toml eintragen.
  3. EU-Datenresidenz für die Zone sicherstellen (Data Localization Suite: Regional Services + EU Metadata Boundary) — G-5/RISK-17.
  4. Custom-Domain-Route in wrangler.toml aktivieren: routes = [{ pattern = "app.medidentas.com", custom_domain = true }].
  5. Cloudflare Access Application für app.medidentas.com anlegen (IdP/SSO), Policy auf die berechtigten E-Mails; AUTH_ENFORCED=true + AUTH_BOOTSTRAP_ADMIN=<deine-mail> als Worker-Vars.
  6. Deploy via GitHub Actions: automatisch bei Push/Merge auf main (App-Code) — oder Workflow App Deploy manuell starten (workflow_dispatch), z. B. für den Erst-Rollout.
  7. Smoke-Test: GET /api/health (authEnforced: true), UI lädt, Deep-Link …/mandant/x lädt (SPA-Fallback), Mandant anlegen → Audit-Eintrag mit echter Identität.

6. Optional: ungeschützter Vorab-Smoke-Test auf *.workers.dev

Da die Zone medidentas.com bereits verfügbar ist, ist dieser Schritt i. d. R. nicht nötig — der abgesicherte Weg (§5) ist direkt möglich. Falls dennoch ein schneller Funktions-Check gewünscht ist: ohne Access (Access schützt keine *.workers.dev), daher AUTH_ENFORCED nicht auf true (Dev-Akteur = admin). Kein abgesicherter Betrieb, nur Dummy-Daten, keine produktiven Provider-Secrets/PII. cd client && npm run build && cd ../server && npx wrangler deployhttps://medidentas-api.<account>.workers.dev.

7. CI-Deploy-Workflow

.github/workflows/app-deploy.yml baut Client + Worker und ruft wrangler deploy, sobald CLOUDFLARE_API_TOKEN/CLOUDFLARE_ACCOUNT_ID als Secrets gesetzt sind. Trigger: Auto-Deploy bei Push auf main (Pfade server/**, client/**, der Workflow selbst) + manuell (workflow_dispatch). concurrency: app-deploy mit cancel-in-progress verhindert überlappende Deploys (neuer Push bricht einen laufenden Deploy ab). Läuft getrennt vom CI Gate (blockt es nicht). Die D1-Migration (migrate.ts, additiv/idempotent) läuft beim ersten Request nach dem Deploy. Analog zum Doku-Auto-Deploy (docs-deploy.yml) — App und Doku sind damit beide bei Merge auf main selbst-aktualisierend.

Deploy-Status-Meldung (Variante B, D-47): Ein abschließender Workflow-Schritt (if: always()) schreibt den Job-Ausgang ({status,sha,runUrl,ranAt}) via wrangler d1 execute --remote in den einstellung-Store (Schlüssel deploy_status) — mit dem ohnehin vorhandenen CLOUDFLARE_API_TOKEN (D1:Edit), also kein GitHub-Token im Worker und kein Cloudflare-Access-Bypass nötig. Die App liest das read-only über GET /api/deploy-status (domain/deploy-status.ts) und zeigt "✓/✗ Deploy vor Xm" dezent im Header. Reine Ops-Telemetrie (kein PII, G-5); scheitert der Schreibschritt, bleibt der zuletzt gemeldete Stand stehen.

7a. Self-Service-Onboarding freischalten (Slice 9, OP-ONBOARD-1)

Die öffentliche Formularseite läuft außerhalb Cloudflare Access. Damit der Einladungslink nicht im Access-Login landet:

  1. Cloudflare Access → Application Medidentas App: eine Bypass-Policy (Action "Bypass", Include "Everyone") nur für die Pfade /onboarding*, /fragebogen* (Dentmarking T5), /formular* (Ausfüll-Formulare, MED-D-63), /lead* (öffentliches Lead-Formular, MED-D-72) und /oeffentlich* hinzufügen. (Alle anderen Pfade bleiben durch "HQ + MK" geschützt.) — ✅ erledigt 2026-07-06: /formular* in die Bypass-Policy aufgenommen. ✅ erledigt 2026-07-07 (MED-D-72): /lead* in die Bypass-Policy aufgenommen (Betreiber) — die öffentliche Lead-Formularseite ist damit ohne Login erreichbar (die Daten-Route /oeffentlich/lead war bereits über /oeffentlich* abgedeckt).
  2. Bot-Schutz (empfohlen): Cloudflare Turnstile-Widget anlegen → TURNSTILE_SITEKEY als Worker-Var, wrangler secret put TURNSTILE_SECRET. Ohne Secret wird nicht geprüft (nur Token/Ablauf schützen).
  3. DEMO_MODE (wrangler.toml [vars]) ist aktuell "true" → Einwilligung end-to-end testbar mit Fakes (NextCloud/Signatur), ohne echte eIDAS-Rechtswirkung. Auf "false" stellen, sobald echte NextCloud-/Signatur-Secrets gesetzt sind.
  4. Einwilligungstext (server/src/domain/einwilligung.ts) vor echtem Einsatz juristisch prüfen (RISK-18); bei Textänderung EINWILLIGUNG_VERSION hochzählen.

7a-2. Geschlossene Ablage R2 scharfgeschaltet (MED-D-61)

Die Datei-Ablage läuft über Cloudflare R2 (Eigenverwahrung, Zugriff nur über die App — MED-D-54/58). Aktiviert in server/wrangler.toml: ABLAGE_BACKEND = "r2" + Binding [[r2_buckets]]ABLAGE_BUCKET (Bucket medidentas-ablage, --jurisdiction eu, G-5). Hinweise:

  • Die öffentliche Upload-Route /oeffentlich/ablage/upload/* liegt bereits im bestehenden /oeffentlich*-Access-Bypass (§7a Schritt 1) — kein neuer Access-Schritt nötig.
  • Ablage-Wurzel: NEXTCLOUD_BASIS_ORDNER steht noch auf der Sandbox medidentas-digital-test (bewusst für die Erst-Aktivierung) → vor echtem Produktivbetrieb auf Medidentas/Mandanten bzw. einen eigenen [env.production]-Block umstellen.
  • Aufbewahrung/GoBD (optional): WORM per npx wrangler r2 bucket lock add medidentas-ablage --name aufbewahrung --retention-days 3650.
  • Zurückdrehen: ABLAGE_BACKEND auskommentieren + neu deployen (bereits in R2 abgelegte Dateien liegen dann nicht in NextCloud — bei echtem Wechsel vorher migrieren).

7b. CI-/Deploy-Runner: self-hosted (D-29)

Alle GitHub-Actions-Workflows (ci.yml, app-deploy.yml, docs-deploy.yml) laufen per Default auf dem self-hosted Runner der Organisation (runs-on: ${{ vars.CI_RUNNER || 'self-hosted' }}) — umschaltbar ohne Commit über die Repo-/Org-Variable CI_RUNNER (z. B. ubuntu-latest für GitHub-gehostete Runner). Hintergrund: Am 2026-07-04 startete GitHub Actions für die Org keine gehosteten Jobs mehr (Billing/Limit); der lokale Runner macht CI + Deploys davon unabhängig.

Fork-Gate (Sicherheit): In ci.yml laufen PRs aus Forks nie auf dem self-hosted Runner (github.event.pull_request.head.repo.fork → erzwungen ubuntu-latest) — fremder Code erreicht den lokalen Runner nicht; die Deploy-Workflows laufen ohnehin nur bei Push auf main/manuell.

Anforderungen an den Runner-Host: Label self-hosted (Standard), Linux, git + bash + Netzzugang; Node liefert actions/setup-node selbst. Sicherheit (RISK-24): Der Runner führt Repo-Code aus und sieht die Cloudflare-Secrets — nur für dieses private Repo registrieren (nie für öffentliche Repos), Host aktuell halten. Ist der Runner offline, bleiben Läufe in "Queued", bis er wieder verfügbar ist.

7c. Repo-Setup (einmalig): Merge-Regeln, Auto-Merge/Auto-Delete & Branch-Protection

Diese Admin-Ebenen-Toggles sind nicht über den GitHub-MCP-/Agenten-Zugang setzbar (der Server hat keine Repo-Admin-/Branch-Protection-/Ruleset-Tools, und der Agenten-Token trägt bewusst kein Administration-Recht — eine Automation, die Gates umbauen kann, würde sie auch abschalten können). Daher: einmalig manuell — in der UI (Settings → General bzw. Settings → Branches) oder per gh mit einem eigenen Admin-PAT (Classic-Scope repo, bzw. Fine-grained Administration: Read and write). Verifikation dieser Punkte ist Agenten-Pflicht (agents.md §6.5) — gesetzt werden sie hier.

(1) Merge-Methoden (nur Squash, OP-PM-1) + Auto-Merge + Auto-Delete-Head:

gh api -X PATCH repos/drkv-com/medidentas \
-F allow_squash_merge=true \
-F allow_merge_commit=false \
-F allow_rebase_merge=false \
-F allow_auto_merge=true \
-F delete_branch_on_merge=true
# (booleans mit -F/typed, nicht -f/string). UI-Pendant: Settings → General → "Pull Requests".

(2) Required Checks auf main = CI Gate + CodeRabbit (MED-D-58/59). Zwei gleichwertige Wege:

Variante A — modernes Ruleset (empfohlen):

gh api -X POST repos/drkv-com/medidentas/rulesets --input - <<'JSON'
{
"name": "main-protection",
"target": "branch",
"enforcement": "active",
"conditions": { "ref_name": { "include": ["refs/heads/main"], "exclude": [] } },
"rules": [
{ "type": "pull_request" },
{ "type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": true,
"required_status_checks": [ { "context": "CI Gate" }, { "context": "CodeRabbit" } ]
}
}
]
}
JSON
# WICHTIG: enforcement MUSS "active" sein (nicht "evaluate" = nur Testmodus → erzwingt nichts).

Variante B — klassische Branch-Protection (PUT legt das volle Objekt an):

gh api -X PUT repos/drkv-com/medidentas/branches/main/protection --input - <<'JSON'
{
"required_status_checks": { "strict": true, "checks": [ { "context": "CI Gate" }, { "context": "CodeRabbit" } ] },
"enforce_admins": false,
"required_pull_request_reviews": null,
"restrictions": null
}
JSON

(3) Verifizieren (auch für Agenten, §6.5):

gh api repos/drkv-com/medidentas --jq '{squash:.allow_squash_merge, merge:.allow_merge_commit, rebase:.allow_rebase_merge, automerge:.allow_auto_merge, autodelete:.delete_branch_on_merge}'
gh api repos/drkv-com/medidentas/branches/main/protection --jq '.required_status_checks.checks' # klassisch
gh api repos/drkv-com/medidentas/rulesets --jq '.[].name' # bzw. Rulesets

Stolperstein CodeRabbit-Check: Der Status-Context CodeRabbit taucht in der UI-Auswahlliste erst auf, nachdem CodeRabbit mindestens einmal auf einem PR gegen main gelaufen ist (commit_status: true, .coderabbit.yaml, MED-D-58). Fehlt er noch, den Namen exakt CodeRabbit manuell eintippen/eintragen. Rate-Limit-/Deadlock-Schutz liegt in .coderabbit.yaml (fail_commit_status: false, MED-D-58) + im Draft-PR-Workflow (MED-D-60): Draft → "ready" → 1× Review → Auto-Merge (Squash) → Auto-Delete.

7d. DocuSign scharfschalten (MED-D-220, OP-SIGN-1)

Der DocuSign-HTTP-Adapter (server/src/signatur/docusign.ts) ist gebaut, aber dormant ohne Secret — ohne vollständige Konfiguration fällt der Signatur-Provider automatisch auf fake zurück (deploy-sicher). Zum Scharfschalten (Reihenfolge):

  1. DocuSign-Konto/Integration: im DocuSign-Admin eine JWT-Grant-App (Integration Key) anlegen, RSA-Keypair erzeugen (privater Schlüssel als PKCS#8-PEM — -----BEGIN PRIVATE KEY-----, nicht PKCS#1 BEGIN RSA PRIVATE KEY, sonst lehnt WebCrypto ab), API-accountId + userId (GUID) notieren. Wichtig — richtige Umgebung: Demo/Developer (account-d.docusign.com / demo.docusign.net) und Produktion (account.docusign.com / account-spez. *.docusign.net, z. B. na3.docusign.net) sind getrennte Welten mit eigenen Apps/Consents/IDs. Die App + der Consent müssen in derselben Umgebung liegen wie die in wrangler.toml gesetzten Hosts.
    • Einmaliger JWT-Consent (Pflicht, sonst consent_required): eine Redirect-URI in der App registrieren (Additional settings → Redirect URIs), dann als API-User eingeloggt diese URL im Browser öffnen und „Allow" klicken (Demo-Host; für Prod account.docusign.com): https://account-d.docusign.com/oauth/auth?response_type=code&scope=signature%20impersonation&client_id=<INTEGRATION_KEY>&redirect_uri=<REGISTRIERTE_REDIRECT_URI>. Der Redirect darf auf einer 404-Seite landen — der Consent ist damit gesetzt. Einmalig je (API-User × Integration Key × Scopes); Scope muss signature impersonation sein.
  2. Credentials setzen — zwei Wege (nie im Repo, RISK-8/G-5):
    • (a) GitHub-Secrets (empfohlen, MED-D-221): dieselben 5 Werte als GitHub-Repo-Secrets (Settings → Secrets and variables → Actions) hinterlegen — DOCUSIGN_INTEGRATION_KEY (GUID der JWT-App) · DOCUSIGN_USER_ID (API-User-GUID, impersoniert) · DOCUSIGN_ACCOUNT_ID (API-Account-GUID) · DOCUSIGN_PRIVATE_KEY (PKCS#8-PEM, mehrzeilig einfügen) · DOCUSIGN_WEBHOOK_HMAC (Key für Connect). Der App-Deploy-Workflow (.github/workflows/app-deploy.yml) synct sie bei jedem main-Deploy per wrangler secret put in den Worker — genau wie die NextCloud-Secrets (idempotent; leere werden übersprungen). Kein manueller CLI-Schritt, kein lokaler Cloudflare-Login nötig. Der Sync ist fehlertolerant (MED-D-222): ein fehlgeschlagenes wrangler secret put bricht den Lauf nicht ab, sondern wird gesammelt → der Schritt endet dann sichtbar rot (::error::), statt eine partielle Credential-Menge stumm durchgehen zu lassen.
    • (b) Manuell (einmalig, ohne CI): npx wrangler secret put DOCUSIGN_INTEGRATION_KEY … für jeden der 5 Namen (im server/-Verzeichnis, lokaler Cloudflare-Login erforderlich).
    • Widerruf (kein Auto-Delete, MED-D-222): Ein leeres/entferntes GitHub-Secret wird nicht synchronisiert und lässt das zuvor deployte Worker-Secret bestehen — Entfernen allein widerruft den Zugriff also nicht (bewusst: ein versehentlich leeres GitHub-Secret darf keine funktionierende Produktiv-Credential destruktiv löschen). Echter Widerruf ist ein bewusster Handgriff: cd server && npx wrangler secret delete <NAME> (bzw. den Integration-Key im DocuSign-Admin sperren).
    • Wichtig: Die Credentials allein schalten nichts scharf — sie machen den Adapter nur konfigurierbar. Der Go-Live-Schalter ist der nicht-geheime Provider-Toggle in Schritt 2b unten. 2b. Provider aktivieren (Go-Live-Schalter, Everything-as-Code): in server/wrangler.toml [vars] die auskommentierten Zeilen aktivieren — SIGNATUR_PROVIDER = "docusign" + DOCUSIGN_WEBHOOK_URL = "…" — und committen/deployen. Der Toggle liegt bewusst im Repo (nicht als verstecktes GitHub-Secret), damit Go-Live ein sichtbarer, reviewbarer Commit ist. Erst mit SIGNATUR_PROVIDER=docusign und den 5 Credentials spricht die App den echten DocuSign-Adapter an — davor Fake (dormant). Demo/Prod-Host: Default ist die Demo-Umgebung (account-d.docusign.com / demo.docusign.net); für Produktion DOCUSIGN_AUTH_BASE/DOCUSIGN_REST_BASE auf die Prod-Hosts setzen (auch in [vars]). 2c. In-Person-/Vor-Ort-Signatur (MED-D-236/237): die echte eingebettete Vor-Ort-Strecke (Kunde unterschreibt auf dem Berater-Gerät, kein E-Mail-Versand) nutzt als Host standardmäßig den eingeloggten Berater (Name = benutzer.anzeigename, E-Mail = Access-Identität, MED-D-237) — Voraussetzung: jede Berater-E-Mail, die vor Ort hosten soll, muss ein Nutzer desselben DocuSign-Accounts sein (sonst lehnt DocuSign den In-Person-Host ab). Fallback (optional): DOCUSIGN_HOST_NAME + DOCUSIGN_HOST_EMAIL als GitHub-Secrets/wrangler secret — greift, wenn keine Berater-Identität vorliegt (Dev) oder als dediziertes Sammel-Host-Konto. Fehlen sowohl Berater als auch Fallback, liefert die App bei „Vor Ort unterschreiben" einen klaren Fehler (der Remote-/E-Mail-Weg bleibt unberührt). Kein neuer Toggle nötig — greift, sobald SIGNATUR_PROVIDER=docusign aktiv ist.
  3. Connect-Webhook (Push):
    • HMAC-Key: DocuSign-Admin → Settings → Connect → Connect Keys„Add Secret Key". DocuSign erzeugt den Key (du wählst ihn nicht selbst) → den generierten Wert kopieren und als GitHub-Secret DOCUSIGN_WEBHOOK_HMAC hinterlegen. Der Worker prüft damit jeden Push (HMAC-SHA256, fail-closed → 401). Der Adapter liest nur X-DocuSign-Signature-1 → bei mehreren Keys den ersten/primären nehmen.
    • URL: dieselbe Route als DOCUSIGN_WEBHOOK_URL (wrangler.toml [vars], https://app.medidentas.com/oeffentlich/signatur/docusign-webhook) setzen — dann hängt der Adapter jedem Envelope eine eventNotification an (Per-Envelope-Push für completed · declined · voided, JSON restv2.1, includeData: envelopeSummary, ohne Dokumente — die signierte Fassung holt der Worker selbst per API, G-6). Ohne diese Var kein Per-Envelope-Push (nur Konto-Level-Connect + Pull-Fallback).
    • Falls stattdessen eine Konto-Level-Connect-Konfiguration genutzt wird: Format JSON (nicht Legacy-XML), Envelope-Events Completed (Pflicht — löst den Write-back aus) · Declined · Voided aktivieren (optional Sent/Delivered für Zwischenstatus), Envelope-Data/envelopeSummary einschließen, keine Dokumente mitsenden, HMAC-Signatur einschließen; Recipient-Events sind nicht nötig. Doppelte Pushes (per-Envelope + Konto-Level) sind harmlos — der abgleich ist idempotent. Die Route liegt bereits im /oeffentlich*-Access-Bypass (§7a) — kein neuer Access-Schritt; die Signatur wird server-seitig HMAC-verifiziert (fail-closed ohne Key → 401). Der Pull-abgleich bleibt als Fallback für verpasste Events (RISK-6).
  4. Compliance-Riegel (zwingend vor dem ersten echten Envelope): AVV mit DocuSign + US-Transfer- Mechanismus (DPF/SCC) + EU-Residenz-Option (RISK-4, Compliance.md); QES-Dokumente laufen bis zur CSP-Anbindung als AES mit sichtbarem Hinweis (RISK-2) — nicht für echte QES-Rechtswirkung nutzen.
  5. DEMO_MODE erst auf "false", wenn NextCloud/R2 und DocuSign echt konfiguriert sind.

7d-Live-Verifikation (Smoke-Test gegen Demo, OP-SIGN-1)

✅ Verifiziert 2026-07-25 (MED-D-238/239). (A) Demo-E2E (Schritte 1–3, MED-D-238): mit Demo-Credentials aus dem Session-Environment (account 108a57f9… / demo.docusign.net) lief der echte Adapter gegen die DocuSign-Demo — JWT-Grant + Consent (kein consent_required), Envelope real angelegt (c4fe2244…), status/erinnern, Webhook-HMAC-Verifikation (pure), danach sauber voided. Dabei behoben: pkcs8PemToBuffer lehnte einen Key mit verstümmeltem PEM-Footer (4 statt 5 Trailing-Hyphens) mit "Invalid character" ab → JWT scheiterte still; Parser jetzt delimiter-/\n-tolerant. (B) Webhook-Write-back in Produktion (Schritte 4–5, MED-D-239): statt gestelltem Test die echte Produktion belegt (Cloudflare-MCP-D1-Query am deployten Worker + Endpoint-Probe): Endpoint erreichbar + fail-closed (401 ohne HMAC); die produktive D1 hält 2 vollständig durchgelaufene docusign-Vorgänge (f77e28a3…, 9fca2eb1…) — Vorgang signiert, Dokument unterschrieben, signierte Fassung in R2 (signiert_pfad), Audit signatur.signiert. Auslöser = Webhook bewiesen über den Audit-actor=system (setAkteur läuft nur in /api/*; der /oeffentlich-Webhook nutzt den Default-Actor; on-demand /api-abgleich schriebe die Nutzer-Mail; der Cron macht keinen Signatur-abgleich) + 54-s-Push-Timing (12:04:34→12:05:28 UTC, kein stündlicher Cron). Der deployte Private-Key ist wohlgeformt. Offen bleiben Schritt 6 (Dormant-Negativtest) + 7 (QES-Hinweis), die Prod-Umschaltung für echte Rechtswirkung (Consent/Hosts/DEMO_MODE=false) und — separat — die embedded/captive In-Person-Strecke (MED-D-236): deren iFrame-Klickdurchlauf gegen echtes DocuSign ist noch nicht verifiziert und nicht als produktionsreifer Rechtssignatur-Pfad zu werten (der hier belegte Kern deckt den E-Mail-Envelope- + Webhook-Weg ab, nicht die embedded Signieransicht).

Ziel: jede Schicht einmal echt durchlaufen, bevor Prod scharfgeht — die App hat bewusst keinen Provider-Health-Endpoint (kein PII/Secret-Leak über einen Diagnose-Pfad, G-5/G-6), daher ist der erste echte anfordern der Test. Reihenfolge im Demo-Account (account-d.docusign.com / demo.docusign.net); Voraussetzung: die 5 Secrets + SIGNATUR_PROVIDER=docusign sind deployt (Schritte 2/2b), und der Signer ist ein Test-Mandant mit einer dir zugänglichen E-Mail-Adresse (DocuSign schickt die Signier-Einladung selbst — dieser Mailversand ist echt und unabhängig von OP-DM-8, das nur die App-eigenen Benachrichtigungen fakt).

  1. Konfiguration greift: beim Mandanten einen Signatur-Request auslösen (Unterschriften-Assistent → Prüfen → Versenden„Per E-Mail senden"). Erscheint kein Attrappe-Banner mehr bzw. entsteht ein echter Envelope → der Provider-Toggle steht. Kommt 503/Fake → Secret unvollständig (docuSignKonfigAusEnv = null).
  2. JWT/Consent: gelingt der Envelope-Create ohne consent_required/401, sind Integration-Key + PKCS#8-Key + Consent korrekt. consent_required → Consent (Schritt 1) in derselben Umgebung nachholen; Key-/invalid_grant-Fehler → PKCS#8-PEM (nicht PKCS#1) + USER_ID/ACCOUNT_ID-GUIDs prüfen.
  3. Envelope/Zustellung: die Signier-E-Mail trifft an der Signer-Adresse ein; der Vorgang steht beim Mandanten auf „zur Unterschrift versendet" (DocumentRow „wartet auf Signatur", abgeleitet aus dem offenen Vorgang).
  4. Webhook-Write-back (Kern): den Envelope in der DocuSign-Demo signieren → der Connect-Push trifft POST /oeffentlich/signatur/docusign-webhook, HMAC grün → Dokument wird unterschrieben/geprüft, die kombinierte signierte Fassung landet in R2, ein Audit-Eintrag entsteht. Alles beim Mandanten prüfbar. Bleibt es hängen → DOCUSIGN_WEBHOOK_HMAC (Adapter liest X-DocuSign-Signature-1, primärer Key) + DOCUSIGN_WEBHOOK_URL prüfen; fail-closed → ohne/falscher Key liefert die Route 401.
  5. Pull-Fallback (RISK-6): Webhook kurz „stören" (Key rotieren, ohne den Worker nachzuziehen) → signieren → der bestehende Pull-abgleich muss den Abschluss trotzdem einholen (verpasste Events dürfen keinen Vorgang dauerhaft „versendet" einfrieren).
  6. Dormant-Rückfall (Negativtest, deploy-sicher): ein Pflicht-Secret leeren/wrangler secret delete → die App fällt sichtbar auf Fake/503 zurück (kein stiller Echt-Eindruck, MED-KB-5) — danach Secret wieder setzen.
  7. QES-Hinweis (Rechtswirkung): einen QES-Dokumenttyp senden → der „als AES"-Hinweis muss erscheinen (keine stille Herabstufung, RISK-2); nicht für echte QES-Rechtswirkung nutzen, bis der CSP-Vertrag steht.
  8. In-Person/Vor-Ort (MED-D-236, wenn DOCUSIGN_HOST_* gesetzt): im Assistenten Versenden„Vor Ort starten" → „An Kunden übergeben" → die DocuSign-Signaturansicht lädt im iFrame (kein E-Mail-Versand; captive recipient) → signieren → der Redirect landet auf /oeffentlich/signatur/vor-ort-fertig?event=signing_complete → das Dokument geht auf erhalten (Prüfung offen). Bei mehreren Dokumenten läuft je Envelope ein iFrame nacheinander. Fehlt der Host → klarer Fehler statt iFrame; kein 409 mehr (der 409 war der alte Attrappen-Weg gegen den echten Provider).

Erst wenn 1–7 (bzw. 8 falls In-Person genutzt) grün: Consent in der Prod-Umgebung erneut setzen, DOCUSIGN_AUTH_BASE/_REST_BASE auf Prod-Hosts, DEMO_MODE=false (Schritt 1/2b), Envelope-ID + Datum + Ergebnis in HANDOFF.md/OP-SIGN-1 vermerken → damit ist die Live-Verifikation von OP-SIGN-1 abgehakt (danach bleibt nur noch PandaDoc-Fallback

  • QES-CSP + AVV-Formalie). Solange 1–7 nicht durchlaufen sind, bleibt OP-SIGN-1 „Adapter gebaut, nicht live verifiziert".

8. Offene Punkte

  • OP-DEPLOY-1 — 🟡 Live in Produktion seit mehreren Wochen (D1 EU, Cloudflare Access, Auto-Deploy); offen bleibt nur die HQ-Eigentümer-Zuordnung der Domain (organisatorisch). S. Lastenheft §11.
  • EU-Datenresidenz (Pflicht, G-5/RISK-17): D1 --location weur, NextCloud EU-gehostet, Cloudflare Data Localization Suite (Regional Services + EU Metadata Boundary), keine US-Sub-Prozessoren ohne SCC.
  • Subdomain-Schema final: app. (Anwendung) — www./Apex bleibt für eine spätere Marketing-Seite frei.