Deploy / Betrieb (Cloudflare) — Runbook (OP-DEPLOY-1)
Stand: Live in Produktion auf
app.medidentas.com(Auto-Deploy bei Push aufmain); offen bleibt nur die organisatorische HQ-Eigentümer-Zuordnung der Domain (s. §8). Ziel: eine Subdomainapp.medidentas.combedient 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 inserver/src/index.tsaufindex.htmlzurü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.comliegt bereits im Cloudflare-Account (Zone aktiv) → der Custom-Domain-Deploy aufapp.medidentas.comist 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_IDals GitHub-Actions-Secrets → Deploy überapp-deploy.yml. Auto-Deploy bei Push aufmain(Änderungen inserver/**/client/**oder am Workflow selbst) + weiterhin manuell (workflow_dispatch) auslösbar.concurrencyverhindert überlappende Deploys. - "Sofort absichern": Betrieb hinter Cloudflare Access mit
AUTH_ENFORCED=truevon Anfang an. ⚠️ Wichtig: Access (self-hosted) braucht einen Hostnamen in eurer Zone —*.workers.devlässt sich nicht mit Access schützen.AUTH_ENFORCED=trueohne Access davor ⇒ jeder/api-Request hat keinencf-access-…-Header ⇒ 401 (App unbedienbar). Konsequenz: der abgesicherte Test läuft aufapp.medidentas.com(Zone ist da), nicht auf*.workers.dev.
3. Voraussetzungen
- ✅ Domain
medidentas.com— im Cloudflare-Account vorhanden (Zone aktiv). HQ-Eigentümer- Zuordnung: organisatorischer Folgepunkt (OP-DEPLOY-1). - 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_IDals GitHub-Actions-Secrets. Nie im Repo (RISK-8). - 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:
- Secrets setzen (GitHub → Settings → Secrets → Actions):
CLOUDFLARE_API_TOKEN,CLOUDFLARE_ACCOUNT_ID. - D1 anlegen (Schritt 4,
--location weur) +database_idinwrangler.tomleintragen. - EU-Datenresidenz für die Zone sicherstellen (Data Localization Suite: Regional Services + EU Metadata Boundary) — G-5/RISK-17.
- Custom-Domain-Route in
wrangler.tomlaktivieren:routes = [{ pattern = "app.medidentas.com", custom_domain = true }]. - Cloudflare Access Application für
app.medidentas.comanlegen (IdP/SSO), Policy auf die berechtigten E-Mails;AUTH_ENFORCED=true+AUTH_BOOTSTRAP_ADMIN=<deine-mail>als Worker-Vars. - 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. - Smoke-Test:
GET /api/health(authEnforced: true), UI lädt, Deep-Link…/mandant/xlä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 deploy
→ https://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:
- 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/leadwar bereits über/oeffentlich*abgedeckt). - Bot-Schutz (empfohlen): Cloudflare Turnstile-Widget anlegen →
TURNSTILE_SITEKEYals Worker-Var,wrangler secret put TURNSTILE_SECRET. Ohne Secret wird nicht geprüft (nur Token/Ablauf schützen). 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.- Einwilligungstext (
server/src/domain/einwilligung.ts) vor echtem Einsatz juristisch prüfen (RISK-18); bei TextänderungEINWILLIGUNG_VERSIONhochzä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_ORDNERsteht noch auf der Sandboxmedidentas-digital-test(bewusst für die Erst-Aktivierung) → vor echtem Produktivbetrieb aufMedidentas/Mandantenbzw. 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_BACKENDauskommentieren + 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-ContextCodeRabbittaucht in der UI-Auswahlliste erst auf, nachdem CodeRabbit mindestens einmal auf einem PR gegenmaingelaufen ist (commit_status: true,.coderabbit.yaml, MED-D-58). Fehlt er noch, den Namen exaktCodeRabbitmanuell 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):
- 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#1BEGIN 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 inwrangler.tomlgesetzten 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 Prodaccount.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 musssignature impersonationsein.
- Einmaliger JWT-Consent (Pflicht, sonst
- 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 jedemmain-Deploy perwrangler secret putin 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 fehlgeschlageneswrangler secret putbricht 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 (imserver/-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 mitSIGNATUR_PROVIDER=docusignund 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 ProduktionDOCUSIGN_AUTH_BASE/DOCUSIGN_REST_BASEauf 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_EMAILals 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, sobaldSIGNATUR_PROVIDER=docusignaktiv ist.
- (a) GitHub-Secrets (empfohlen, MED-D-221): dieselben 5 Werte als GitHub-Repo-Secrets
(Settings → Secrets and variables → Actions) hinterlegen —
- 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_HMAChinterlegen. Der Worker prüft damit jeden Push (HMAC-SHA256, fail-closed → 401). Der Adapter liest nurX-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 eineeventNotificationan (Per-Envelope-Push für completed · declined · voided, JSONrestv2.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/
envelopeSummaryeinschließen, keine Dokumente mitsenden, HMAC-Signatur einschließen; Recipient-Events sind nicht nötig. Doppelte Pushes (per-Envelope + Konto-Level) sind harmlos — derabgleichist 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-abgleichbleibt als Fallback für verpasste Events (RISK-6).
- 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
- 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. DEMO_MODEerst 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 (keinconsent_required), Envelope real angelegt (c4fe2244…),status/erinnern, Webhook-HMAC-Verifikation (pure), danach sauber voided. Dabei behoben:pkcs8PemToBufferlehnte 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 durchgelaufenedocusign-Vorgänge (f77e28a3…,9fca2eb1…) — Vorgangsigniert, Dokumentunterschrieben, signierte Fassung in R2 (signiert_pfad), Auditsignatur.signiert. Auslöser = Webhook bewiesen über den Audit-actor=system(setAkteurläuft nur in/api/*; der/oeffentlich-Webhook nutzt den Default-Actor; on-demand/api-abgleichschriebe 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).
- 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). - 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. - 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).
- Webhook-Write-back (Kern): den Envelope in der DocuSign-Demo signieren → der Connect-Push trifft
POST /oeffentlich/signatur/docusign-webhook, HMAC grün → Dokument wirdunterschrieben/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 liestX-DocuSign-Signature-1, primärer Key) +DOCUSIGN_WEBHOOK_URLprüfen; fail-closed → ohne/falscher Key liefert die Route 401. - Pull-Fallback (RISK-6): Webhook kurz „stören" (Key rotieren, ohne den Worker nachzuziehen) → signieren →
der bestehende Pull-
abgleichmuss den Abschluss trotzdem einholen (verpasste Events dürfen keinen Vorgang dauerhaft „versendet" einfrieren). - Dormant-Rückfall (Negativtest, deploy-sicher): ein Pflicht-Secret leeren/
wrangler secret delete→ die App fällt sichtbar auf Fake/503zurück (kein stiller Echt-Eindruck, MED-KB-5) — danach Secret wieder setzen. - 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.
- 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 auferhalten(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.