[{"data":1,"prerenderedAt":679},["ShallowReactive",2],{"search-de":3,"content-de-developer\u002Fplugin-connected-apps":4,"surround-de-\u002Fdeveloper\u002Fplugin-connected-apps":670},[],{"id":5,"title":6,"body":7,"description":662,"extension":663,"meta":664,"navigation":665,"path":666,"seo":667,"stem":668,"__hash__":669},"content_de\u002F3.developer\u002F46.plugin-connected-apps.md","Connected Apps & signierte Hooks",{"type":8,"value":9,"toc":648},"minimark",[10,19,24,125,128,132,143,158,164,168,183,193,236,240,243,325,330,336,341,348,435,438,444,451,454,458,539,546,550,553,616,620,623,635,638,642],[11,12,13,14,18],"p",{},"Eine ",[15,16,17],"strong",{},"Connected App"," ist ein externer HTTPS-Service, der an eine Plugin-Id gebunden ist. Sie läuft außerhalb des Prozesses und führt niemals Code innerhalb von Convex oder Nuxt aus, weshalb sie sofort installiert ist — kein Rebuild, kein Redeploy. Im Gegenzug gilt alles, was sie sagt, als nicht vertrauenswürdig.",[20,21,23],"h2",{"id":22},"der-datensatz","Der Datensatz",[25,26,27,40],"table",{},[28,29,30],"thead",{},[31,32,33,37],"tr",{},[34,35,36],"th",{},"Feld",[34,38,39],{},"Hinweise",[41,42,43,55,65,83,93,117],"tbody",{},[31,44,45,52],{},[46,47,48],"td",{},[49,50,51],"code",{},"pluginId",[46,53,54],{},"Das gebündelte Plugin, an das diese App gebunden ist. Sein Manifest ist die Obergrenze der Capabilities",[31,56,57,62],{},[46,58,59],{},[49,60,61],{},"name",[46,63,64],{},"Anzeigename",[31,66,67,72],{},[46,68,69],{},[49,70,71],{},"endpointUrl",[46,73,74,75,78,79,82],{},"Muss absolutes ",[15,76,77],{},"https"," sein, mit Hostname und ohne eingebettete ",[49,80,81],{},"user:pass@","-Zugangsdaten",[31,84,85,90],{},[46,86,87],{},[49,88,89],{},"grantedCapabilities",[46,91,92],{},"Eine duplikatfreie Teilmenge der vom gebundenen Plugin deklarierten Capabilities. Die Anforderung von etwas, das das Manifest nicht deklariert, wird bei der Registrierung abgelehnt",[31,94,95,100],{},[46,96,97],{},[49,98,99],{},"status",[46,101,102,105,106,109,110,112,113,116],{},[49,103,104],{},"enabled"," → ",[49,107,108],{},"disabled"," ⇄ ",[49,111,104],{}," oder ",[49,114,115],{},"revoked"," (terminal)",[31,118,119,122],{},[46,120,121],{},"Shared Secret",[46,123,124],{},"Versiegelter AES-256-GCM-Umschlag; wird nach dem Erzeugen niemals im Klartext gespeichert, geloggt oder zurückgegeben",[11,126,127],{},"Lesepfade liefern eine einzige Projektion, die die versiegelten Secret-Spalten konstruktionsbedingt auslässt, sodass eine neue Query den Ciphertext nicht versehentlich zutage fördern kann.",[20,129,131],{"id":130},"registrierung-und-secret-austausch","Registrierung und Secret-Austausch",[11,133,134,135,138,139,142],{},"Die Registrierung erzeugt ein 256-Bit-Secret mit dem wiedererkennbaren Präfix ",[49,136,137],{},"cah_"," und versiegelt es unter einem Schlüssel, der per HKDF-SHA256 aus ",[49,140,141],{},"INSTANCE_SECRET"," mit connected-app-spezifischen Salt- und Info-Labels abgeleitet wird — ein Connected-App-Secret kann niemals unter dem Schlüsselkontext eines anderen Consumers geöffnet werden.",[144,145,148],"callout",{"title":146,"type":147},"Das Klartext-Secret wird genau einmal angezeigt","warning",[11,149,150,153,154,157],{},[49,151,152],{},"register"," und ",[49,155,156],{},"rotateSecret"," geben den Klartext an den Aufrufer zurück und sonst nirgendwohin. Speichern Sie ihn sofort in Ihrer App; geht er verloren, rotieren Sie ihn.",[11,159,160,163],{},[49,161,162],{},"testConnection"," prüft den konfigurierten Endpoint (eine unsignierte Erreichbarkeitsprüfung), sodass ein Operator das Setup validieren kann, bevor er irgendetwas gewährt. Deaktivieren, Widerrufen und Löschen sind getrennte Operationen; das Widerrufen ist terminal.",[20,165,167],{"id":166},"api-scopes-und-plugin-gebundene-keys","API-Scopes und plugin-gebundene Keys",[11,169,170,171,174,175,178,179,182],{},"Eine Connected App authentifiziert sich gegenüber der v1-HTTP-API mit einem API-Key, der an ihr Plugin ",[15,172,173],{},"gebunden"," ist (",[49,176,177],{},"apiKeys.pluginId","). Der Scope-String ist zugleich der Plugin-Capability-String, und die effektive Scope-Menge wird bei ",[15,180,181],{},"jedem"," Request neu abgeleitet:",[184,185,191],"pre",{"className":186,"code":188,"language":189,"meta":190},[187],"language-text","effective scopes = key scopes ∩ plugin manifest capabilities ∩ operator grants ∩ (plugin flag enabled)\n","text","",[49,192,188],{"__ignoreMap":190},[11,194,195,196,199,200,199,203,199,206,199,209,212,213,199,216,199,219,199,222,199,225,199,228,231,232,235],{},"Das Deaktivieren des Plugins oder das Widerrufen einer Gewährung neutralisiert den Key daher sofort. Endpoint-Scopes (",[49,197,198],{},"contacts:read",", ",[49,201,202],{},"contacts:write",[49,204,205],{},"events:write",[49,207,208],{},"transactional:send",[49,210,211],{},"topics:write",") sind auch auf eigenständigen Operator-Keys gültig. Die reinen Tier-2-Scopes (",[49,214,215],{},"campaigns:read",[49,217,218],{},"mail:read",[49,220,221],{},"knowledge:read",[49,223,224],{},"webhooks:manage",[49,226,227],{},"plugin-storage:read",[49,229,230],{},"plugin-storage:write",") haben eigenständig keine Bedeutung und werden bei der Erzeugung auf einem ungebundenen Key abgelehnt. Keys sind konstruktionsbedingt least-privilege: Das Anlegen erfordert eine explizite, nicht leere Scope-Liste, und eine Legacy-Zeile ohne ",[49,233,234],{},"scopes","-Spalte ist deny-all.",[20,237,239],{"id":238},"signierte-synchrone-hooks","Signierte synchrone Hooks",[11,241,242],{},"Das Hook-Protokoll definiert drei Entscheidungspunkte in der Pipeline, die eine Connected App beantworten kann:",[25,244,245,258],{},[28,246,247],{},[31,248,249,252,255],{},[34,250,251],{},"Hook",[34,253,254],{},"Was die App zurückgibt",[34,256,257],{},"Fail-Richtung",[41,259,260,283,299],{},[31,261,262,267,273],{},[46,263,264],{},[49,265,266],{},"draft",[46,268,269,272],{},[49,270,271],{},"{ draft: string }"," — ein vorgeschlagener Antworttext",[46,274,275,278,279,282],{},[15,276,277],{},"Offen",": Rückfall auf die eingebaute ",[49,280,281],{},"default","-Draft-Strategie",[31,284,285,290,293],{},[46,286,287],{},[49,288,289],{},"gate",[46,291,292],{},"Ein ausschließlich restriktives Verdikt",[46,294,295,298],{},[15,296,297],{},"Geschlossen",": Rückfall auf einen Vorsichts-Einwand (menschliche Prüfung)",[31,300,301,306,320],{},[46,302,303],{},[49,304,305],{},"score",[46,307,308,311,312,316,317],{},[49,309,310],{},"{ score: number }"," in ",[313,314,315],"span",{},"0, 1",", optional ",[49,318,319],{},"reason",[46,321,322,324],{},[15,323,277],{},": Rückfall auf \"kein Score\"",[11,326,13,327,329],{},[49,328,289],{},"-Antwort ist strukturell außerstande zuzustimmen: Im Schema existiert kein Accept-Wert. Eine Connected App kann Vorsicht oder Arbeit hinzufügen; sie kann niemals genehmigen, entsperren oder einen Versand erzwingen.",[144,331,333],{"title":332,"type":147},"Nur Protokoll: Runtime-Adapter wird nicht ausgeliefert",[11,334,335],{},"Das Wire-Protokoll für signierte Hooks ist spezifiziert, aber Owlat liefert dafür weder einen Convex-Runtime-Adapter noch eine Aufrufstelle in der Pipeline aus. Die Draft-, Route-Gate- und Scoring-Stufen führen ausschließlich Core- und Tier-1-Contributions aus, sodass das Registrieren einer Connected App nicht dazu führt, dass Owlat sie an einem Entscheidungspunkt aufruft. Der aktive Teil von Tier 2 sind plugin-gebundene API-Keys (Scope-Schnittmenge pro Request neu abgeleitet) und die Registrierung von Connected Apps mit ihrem versiegelten Shared Secret. Alles unterhalb von \"Wire-Contract\" beschreibt den Vertrag, den eine künftige Implementierung erfüllen muss; erwarten Sie heute keinen eingehenden Hook-Traffic.",[337,338,340],"h3",{"id":339},"wire-contract","Wire-Contract",[11,342,343,344,347],{},"Protokollversion ",[49,345,346],{},"v1",". Request-Header:",[25,349,350,360],{},[28,351,352],{},[31,353,354,357],{},[34,355,356],{},"Header",[34,358,359],{},"Wert",[41,361,362,378,389,399,413,423],{},[31,363,364,369],{},[46,365,366],{},[49,367,368],{},"x-owlat-hook",[46,370,371,373,374,373,376],{},[49,372,266],{}," | ",[49,375,289],{},[49,377,305],{},[31,379,380,385],{},[46,381,382],{},[49,383,384],{},"x-owlat-hook-version",[46,386,387],{},[49,388,346],{},[31,390,391,396],{},[46,392,393],{},[49,394,395],{},"x-owlat-hook-app",[46,397,398],{},"Connected-App-Id, damit der Empfänger das richtige Secret auswählt",[31,400,401,406],{},[46,402,403],{},[49,404,405],{},"x-owlat-hook-timestamp",[46,407,408,409,412],{},"Unix-",[15,410,411],{},"Sekunden",", signiert",[31,414,415,420],{},[46,416,417],{},[49,418,419],{},"x-owlat-hook-nonce",[46,421,422],{},"128-Bit-Base64url-Nonce pro Request, signiert",[31,424,425,430],{},[46,426,427],{},[49,428,429],{},"x-owlat-hook-signature",[46,431,432],{},[49,433,434],{},"v1=\u003Chex hmac>",[11,436,437],{},"Beide Richtungen sind HMAC-SHA256 über einen mit Zeilenumbrüchen verbundenen kanonischen String mit fester Feldreihenfolge und einem richtungsspezifischen Domain-Tag:",[184,439,442],{"className":440,"code":441,"language":189,"meta":190},[187],"request:                          response:\n  owlat.hook.request.v1             owlat.hook.response.v1\n  \u003ChookKind>                        \u003ChookKind>\n  \u003CconnectedAppId>                  \u003CconnectedAppId>\n  \u003CtimestampSeconds>                \u003Cnonce>            ← echoes the REQUEST nonce\n  \u003Cnonce>                           \u003CtimestampSeconds>\n  \u003Csha256Hex(bodyBytes)>            \u003Csha256Hex(bodyBytes)>\n",[49,443,441],{"__ignoreMap":190},[11,445,446,447,450],{},"Der Body ist über seinen SHA-256 gebunden, sodass Manipulation die Signatur ungültig macht. Das Richtungs-Tag bedeutet, dass eine Request-Signatur niemals als Response-Signatur wiedereingespielt werden kann. Die ",[15,448,449],{},"Request-Nonce fließt in den Signierstring der Response ein",", sodass eine abgefangene Response nicht gegen einen anderen Request wiedereingespielt werden kann. Die Verifikation erfolgt in konstanter Zeit.",[11,452,453],{},"Ihre App muss die Request-Signatur verifizieren und ein eigenes Frische-Fenster auf dem signierten Zeitstempel durchsetzen, dann ihre Antwort auf dieselbe Weise signieren und dabei die Request-Nonce zurückspiegeln. Owlat weist eine Antwort ab, deren signierter Zeitstempel außerhalb einer Toleranz von 30 Sekunden liegt.",[337,455,457],{"id":456},"was-owlat-bei-jedem-aufruf-erzwingt","Was Owlat bei jedem Aufruf erzwingt",[25,459,460,469],{},[28,461,462],{},[31,463,464,467],{},[34,465,466],{},"Kontrolle",[34,468,359],{},[41,470,471,479,487,495,503,515,523,531],{},[31,472,473,476],{},[46,474,475],{},"Deadline",[46,477,478],{},"5 s, danach wird der Fetch abgebrochen",[31,480,481,484],{},[46,482,483],{},"Obergrenze Request-Body",[46,485,486],{},"64 KiB",[31,488,489,492],{},[46,490,491],{},"Obergrenze Response-Body",[46,493,494],{},"64 KiB (bis zur Obergrenze geleert; oberhalb der Grenze fail-closed)",[31,496,497,500],{},[46,498,499],{},"Frische der Response",[46,501,502],{},"30 s Toleranz",[31,504,505,508],{},[46,506,507],{},"Transport",[46,509,510,511,514],{},"ausschließlich https, SSRF-Schutz mit einer Blockliste privater\u002Finterner Ziele — sowohl vorab ",[15,512,513],{},"als auch"," zum Verbindungszeitpunkt angewandt; Redirects werden verweigert",[31,516,517,520],{},[46,518,519],{},"Akzeptierter Draft-Text",[46,521,522],{},"injektionsbereinigt und auf 65 536 Codepoints begrenzt",[31,524,525,528],{},[46,526,527],{},"Akzeptierter Reason-Text",[46,529,530],{},"injektionsbereinigt und auf 300 Codepoints begrenzt",[31,532,533,536],{},[46,534,535],{},"Circuit Breaker",[46,537,538],{},"5 aufeinanderfolgende Fehlschläge pro (App, Kind) öffnen ihn; nach 60 s Cooldown ist ein halboffener Versuch erlaubt",[11,540,541,542,545],{},"Die Runtime löst zuerst App und Circuit-Zustand auf und ",[15,543,544],{},"bricht kurz — ohne das Secret zu öffnen und ohne Netzwerkaufruf — ab"," bei einer fehlenden, deaktivierten oder widerrufenen App, einer Hook-Art, die der Operator nicht gewährt hat, oder einem offenen Breaker. Antworten werden strikt auf ihre Form validiert: ein zusätzlicher Key, ein falscher Typ oder ein leerer String wird abgelehnt, und der deklarierte Fallback greift.",[337,547,549],{"id":548},"fehler-taxonomie","Fehler-Taxonomie",[11,551,552],{},"Jede Auflösung protokolliert genau einen festen Code — niemals freien Text und niemals die Meldung der App selbst:",[11,554,555,199,558,199,561,199,564,199,567,199,570,199,573,199,576,199,579,199,582,199,585,199,588,199,591,199,594,199,597,199,600,199,603,199,606,199,609,199,612,615],{},[49,556,557],{},"request_too_large",[49,559,560],{},"blocked_ssrf",[49,562,563],{},"redirect_refused",[49,565,566],{},"timeout",[49,568,569],{},"network_error",[49,571,572],{},"bad_status",[49,574,575],{},"response_too_large",[49,577,578],{},"signature_missing",[49,580,581],{},"signature_mismatch",[49,583,584],{},"stale_response",[49,586,587],{},"invalid_json",[49,589,590],{},"invalid_response",[49,592,593],{},"app_not_found",[49,595,596],{},"app_disabled",[49,598,599],{},"app_revoked",[49,601,602],{},"capability_denied",[49,604,605],{},"circuit_open",[49,607,608],{},"secret_unavailable",[49,610,611],{},"output_rejected",[49,613,614],{},"unexpected_error",".",[20,617,619],{"id":618},"zustell-logs","Zustell-Logs",[11,621,622],{},"Jede Auflösung schreibt neben den ausgehenden Webhook-Logs eine redigierte, tenant-gescopte Zustell-Log-Zeile: Hook-Art, ob ein Netzwerkaufruf versucht wurde, ob der Wert der App oder der Fallback gewonnen hat, der feste Fallback-Grund und die Netzwerkdauer.",[11,624,625,626,629,630,634],{},"Die Redigierung ist ",[15,627,628],{},"strukturell",": Es gibt keine Spalte für die Payload, den zurückgegebenen Draft-\u002FGate-\u002FScore-Text, das Shared Secret oder eine der beiden Signaturen. Eine geloggte Zustellung kann daher nicht ",[631,632,633],"em",{},"aus"," dem Log wiedereingespielt werden. Die einzige Wiederholung besteht darin, dass die Pipeline den Hook erneut aufruft — mit frisch signiertem Zeitstempel und frischer Nonce und einem erneuten Durchlauf des vollständigen, ausschließlich restriktiven Umschlags. Ein erneut ausgeführtes Gate kann also weiterhin nur Vorsicht hinzufügen.",[11,636,637],{},"Lesezugriffe sind org-gescopt und begrenzt (Standard 50 Zeilen, Maximum 200, filterbar nach App, Art und Quelle). Zeilen verfallen mit der Audit-Log-Aufbewahrung von 30 Tagen.",[20,639,641],{"id":640},"referenzimplementierung","Referenzimplementierung",[11,643,644,647],{},[49,645,646],{},"examples\u002Fplugins\u002Fslack-approvals"," ist die gepflegte Tier-2-Referenz: ein ausschließlich restriktives Hold-Gate plus Automations-Benachrichtigungen, mit authentifizierten Slack-Callbacks, Ablauf, Modellierung von Doppelabstimmungen und Quorum — samt Tests, die belegen, dass Slack weder eine Genehmigung erzwingen noch die Core-Gates von Owlat umgehen kann.",{"title":190,"searchDepth":649,"depth":649,"links":650},2,[651,652,653,654,660,661],{"id":22,"depth":649,"text":23},{"id":130,"depth":649,"text":131},{"id":166,"depth":649,"text":167},{"id":238,"depth":649,"text":239,"children":655},[656,658,659],{"id":339,"depth":657,"text":340},3,{"id":456,"depth":657,"text":457},{"id":548,"depth":657,"text":549},{"id":618,"depth":649,"text":619},{"id":640,"depth":649,"text":641},"Tier 2: externe Services, die über plugin-gebundene API-Keys, gescopte Webhooks und signierte synchrone Draft-\u002FGate-\u002FScore-Hooks angesprochen werden.","md",{},true,"\u002Fdeveloper\u002Fplugin-connected-apps",{"title":6,"description":662},"3.developer\u002F46.plugin-connected-apps","oD4ROOqXo5bXeh_ERshW-SiUoMIFknv4kt6eYbPewLk",[671,675],{"title":672,"path":673,"stem":674,"children":-1},"Die owlat-plugins-CLI","\u002Fdeveloper\u002Fplugin-cli","3.developer\u002F45.plugin-cli",{"title":676,"path":677,"stem":678,"children":-1},"Sandboxed Plugin-Jobs","\u002Fdeveloper\u002Fplugin-sandboxed-jobs","3.developer\u002F47.plugin-sandboxed-jobs",1786915109637]