feat(user): TravelRule-Signatur-PDF-Cron als Ablöse des Apps-Script-Sheets#3975
Open
TaprootFreak wants to merge 13 commits into
Open
feat(user): TravelRule-Signatur-PDF-Cron als Ablöse des Apps-Script-Sheets#3975TaprootFreak wants to merge 13 commits into
TaprootFreak wants to merge 13 commits into
Conversation
b0e4f2e to
e78ac98
Compare
…heets Ersetzt den seit 18.06. still mit 403 scheiternden Sheet-Auto-Pfad durch einen nativen API-Job: - TravelRulePdfService: reines PDF-Rendering der signierten Adressnachricht - TravelRuleJobService: Minuten-Cron mit Claim-first-CAS gegen Multi-Replica- Doppel-Upload, Rollback des travelRulePdfDate und Invalidierung verwaister kyc_file-Rows bei Upload-Fehler - TravelRuleObserver: Stuck-Detektor (Backlog + Alter ältester Kandidat) als Lehre aus dem stillen Sheet-Ausfall - KycFileService.invalidateKycFile für die Waisen-Bereinigung - Prozess-Gating via Process.TRAVEL_RULE_PDF (DISABLED_PROCESSES-Notbremse)
- Signatur verbatim ins PDF rendern (kein split(';')); der ;<key>-Suffix
der Cardano-CIP-30-Faelle ist verifikationsrelevant und wird 1:1 uebernommen.
rawSignature-Helper entfernt, Spec auf verbatim-Assert inkl. Cardano-Fall.
- Fail-closed Signatur-Validierung vor PDF-Erzeugung: UUID-Ausschluss plus
konservative Allowlist bekannter Formate (EVM-Hex, Monero-base58,
Bitcoin-base64, Cardano-COSE inkl. ;key). Unbekannte Formate werden mit
logger.warn uebersprungen, travelRulePdfDate bleibt ungesetzt.
- Observer: zweite Metrik claimedWithoutFile (geclaimt ohne gueltige
AddressSignature-kycFile) fuer den Rollback-fehlgeschlagen-Fall;
oldestAgeHours kandidaten-nah ueber user.updated statt MIN(user.created);
Rollback- und Orphan-Invalidation-Fehler werden geloggt statt verschluckt.
…n beschraenken Der Job ueberspringt Signaturen, die die Format-Allowlist nicht matchen (z.B. Lightning/DeFiChain), und setzt fuer sie travelRulePdfDate nie. Der Stuck-Observer zaehlte dieselbe WHERE-Bedingung ohne Format-Filter und meldete diese dauerhaft-uebersprungenen Kandidaten als Backlog -> dauerhafte Falsch-Positiv-Stuck-Alerts. - Signatur-Format-Pruefung als geteilte Klasse TravelRuleSignature.isValid ausgelagert (DRY), in Job und Observer identisch genutzt. - Observer holt die Kandidaten-Signaturen als Spalte und teilt in-Code via derselben isValid auf: backlog/oldestAgeHours nur ueber verarbeitbare Kandidaten, separate Metrik skippedUnrecognised fuer nicht-allowlist-faehige. - oldestAgeHours ehrlich als approximativ dokumentiert (user.updated ist @UpdateDateColumn, kein echtes Stuck-Alter); backlog als robuster Primaerindikator. - travel-rule.observer.spec.ts ergaenzt (backlog/oldestAge/skippedUnrecognised/ claimedWithoutFile). M5-Kontiguitaets-Assertion im pdf.service.spec (statement+address als ein zusammenhaengender String, kein getrenntes Adressfeld).
- dedizierte travel-rule-signature.spec.ts: testet TravelRuleSignature.isValid direkt ueber alle Pfade (leere Eingabe, je ein Treffer pro Allowlist-Format, masterKey-UUID mit/ohne ;<key>-Suffix, Lightning, Artefakte) -> Branch 100% - travel-rule-job.service.spec.ts: Rollback-update-Fehler (Claim-Leak) und Orphan-invalidateKycFile-Fehler decken die fail-closed logger.error-Pfade ab - travel-rule-pdf.service.spec.ts: wirft beim Rendern -> Promise rejected
e78ac98 to
8fbbeff
Compare
Das bisherige PDF war frei erfunden. Der TravelRulePdfService rendert nun exakt die editPDF-Vorlage: grüner Haken oben links, DFX-Logo rechts, zweizeiliger Titel, Metadaten-Tabelle (Id/Date/User Data ID) und Kontrolle-Box (Address/Signatur Text/Signatur/Kontrolle-Link). Der Signatur-Text ist signMessageGeneral + Adresse verbatim (Unterstriche bleiben erhalten), die Signatur wird unverändert inkl. ;key-Suffix gerendert, der Kontrolle-Link zeigt auf verifycryptomessage.com mit url-kodierter Nachricht und Signatur. Neue Service-Signatur generatePdf(input) mit id/userDataId/address/signature/ date; Job übergibt diese Felder. drawLogo erhält eine optionale x-Override für die Rechts-Positionierung. Tests komplett neu.
Der `kf`-Alias im NOT-EXISTS-Subquery der claimedWithoutFile-Metrik ist kein TypeORM-registrierter Alias, daher quotet TypeORM die Identifier nicht. PostgreSQL faltet ungequotete Identifier zu lowercase, wodurch `kf.userDataId` und `kf.subType` zu nicht existierenden Spalten werden und der Observer alle 10 Minuten mit "column kf.userdataid does not exist" zur Laufzeit abbricht. Die camelCase-Identifier werden jetzt manuell doppelt gequotet, damit sie verbatim erhalten bleiben. Der bisherige Voll-Mock im Observer-Spec hat den Fehler verdeckt; ein neuer Regressionstest prueft den generierten SQL und schlaegt fehl, falls die Identifier nicht gequotet sind.
…ure-PDFs Vor dem Claim/Generate wird je Kandidat ein bereits vorhandenes, gueltiges AddressSignature-PDF gesucht (valid, subType ADDRESS_SIGNATURE, Name-Infix "AddressSignature-0-<userId>-" mit Trailing-Hyphen gegen Multi-Adress-Kollision). Gefunden: Datum aus dem YYYYMMDD-Dateinamen-Prefix wird als UTC-Mitternacht geparst und per CAS in travelRulePdfDate gestempelt (repliziert die Verify-Stamp- Stufe des alten Sheets), keine Neuerzeugung. Malformter Name wird als Fehler geloggt und ohne Stempel uebersprungen (fail-safe, kein geratenes Datum, kein Fallback auf now). Nicht gefunden: unveraendertes Claim-first-Verhalten. Verhindert Duplikat-PDFs mit falschem (now) Datum fuer den Sheet-Altbestand.
…l: Format-Randfälle wie EVM ohne 0x-Prefix; DeFiChain/Bitcoin werden erkannt)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Was
Ersetzt den TravelRule-Signatur-Pfad des Apps-Script-Sheets durch einen nativen API-Cron:
TravelRulePdfService— rendert das PDF als 1:1-Replik dereditPDF-Sheet-Vorlage (per echtem PDF-Export verifiziert + visuell gegengeprüft): DFX-Logo oben rechts, deutscher Titel „Travel Rule / Digitale Signatur Kontrolle“, Metadaten-Tabelle (Id / Date in UTC / User Data ID), umrahmte „Kontrolle“-Box (Address / Signatur Text =signMessageGeneral+ Adresse verbatim mit Unterstrichen / Signatur verbatim inkl.;key, keinsplit/ Kontrolle =verifycryptomessage.com-Link). Signatur in derselben Schrift (Helvetica) wie die übrigen Felder; der grüne Haken der Vorlage wurde bewusst entfernt (nicht benötigt).TravelRuleJobService—@DfxCron(EVERY_MINUTE, { process: TRAVEL_RULE_PDF, timeout: 1800 }): Query (signature NOT NULL,travelRulePdfDate IS NULL,kycLevel >= 40, kein Custody), fail-closed Signatur-Validierung, PDF erzeugen, alskyc_file(UserNotes/AddressSignature) anhängen,travelRulePdfDatesetzen.TravelRuleSignature— geteilte, fail-closed Signatur-Format-Allowlist (isValid), einzige Quelle der Wahrheit, von Job und Observer identisch genutzt (DRY).TravelRuleObserver— Stuck-Detektor (MetricObserver,EVERY_10_MINUTES): Backlog-Count nur über verarbeitbare Kandidaten, separate MetrikskippedUnrecognised, Alter des ältesten offenen Kandidaten (approximativ) und „geclaimt-aber-blob-los".KycFileService.invalidateKycFile— setzt eine verwaistekyc_file-Row aufvalid = false.Process.TRAVEL_RULE_PDF— neuer Prozess fürs Gating.Warum
Der Sheet-Auto-Pfad zeigte auf die abgebaute Azure-App und scheiterte seit 18.06. still mit 403 — niemand bemerkte es, weil der Fehler nur in ein ungelesenes Log ging. Die Migration löst diesen Pfad nativ ab und schließt zugleich die eigentliche Lücke: das stille Scheitern. Datengrundlage ist der empirische Voll-Scan der Sheet-Daten (~27k
archiv-Zeilen, 28.9kBatchArchiv) plus Repo-Verifikation (sieheopen-questions.md).Kernpunkte
;<key>-Suffix (Cardano CIP-30 COSE, 23/27298 Fälle) ist verifikationsrelevant und wird 1:1 ins PDF übernommen.user.signaturewird im Cron viauserRepo.findroh/unmaskiert gelesen (nicht der maskierte /gs-Pfad), also keine RESTRICTED-Maskierung.^[0-9a-fA-F]{8}-…-[0-9a-fA-F]{12}$) — eine masterKey-UUID kann Adress-Ownership kryptographisch nie beweisen (historisch 9 wertlose UUID-Pseudo-Belege mit Status TRUE).0x+ 130/146 hex), Monero base58, Bitcoin base64 (H…, len ~88), Cardano COSE (hex8458, optional mit;key). Unbekannte Formate werden übersprungen +logger.warn,travelRulePdfDatebleibt ungesetzt (Kandidat bleibt sichtbar). Bewusst konservativ, um valide Signaturen nicht fälschlich abzulehnen.LockClassnur prozessweit greift unduploadUserFilenicht transaktional ist, wirdtravelRulePdfDatezuerst atomar viaUPDATE … WHERE travelRulePdfDate IS NULLgesetzt. Nur beiaffected === 1wird verarbeitet.0x-Prefix; DeFiChain/Bitcoin werden erkannt), und setzt für sietravelRulePdfDatenie → sie bleiben dauerhaft offen. Würde der Observer dieselbe WHERE-Bedingung ohne Format-Filter zählen, meldete er diese dauerhaft-übersprungenen Kandidaten als Backlog → dauerhafte Falsch-Positiv-Stuck-Alerts. Fix: dieselbeTravelRuleSignature.isValidin Job und Observer. Der Observer holt die Kandidaten-Signaturen als Spalte und teilt in-Code auf —backlog/oldestAgeHoursmessen nur verarbeitbare Arbeit, die separate MetrikskippedUnrecognisedführt die nicht-allowlist-fähigen Fälle.TravelRuleObservermacht einen hängenden Job sichtbar —backlog(wachsend, robuster Primärindikator),oldestAgeHours(approximativ, siehe unten) und neuclaimedWithoutFile(travelRulePdfDate IS NOT NULLohne gültigeAddressSignature-kycFile). Letzteres fängt genau den Fall, gegen den der PR antritt: Rollback-fehlgeschlagen (Claim-Leak / Orphan-Blob).oldestAgeHours-Semantik ehrlich: Basis istuser.updated, eine@UpdateDateColumn, die bei jedem Save bumpt (nicht nur bei Signatur/Claim).oldestAgeHoursist daher „Zeit seit letzter Änderung, approximativ" — eine untere Schranke, kein exaktes Stuck-Alter. Der Backlog-Count ist der robuste, autoritative Stuck-Indikator; keine erfundene Genauigkeit.kyc_file-Row fehl, wirdtravelRulePdfDatezurückgesetzt und die Row aufvalid = falsegesetzt. Rollback- und Invalidations-Fehler werden jetztlogger.error-geloggt statt still verschluckt.originalName-Konvention:YYYYMMDD-AddressSignature-0-<userId>-HHMMSS.pdf;split('-')[0] === YYYYMMDDist per Test gegen den Download-Filter (config id 15) abgesichert.Geklärte Fragen (RESOLVED)
;<key>-Suffix: Verbatim rendern, keinsplit. Genau 23/27298 Cardano-CIP-30-COSE-Fälle, part1 immer non-empty. Jetzt umgesetzt (verbatim).POST userData/:id/kycFile, COMPLIANCE-Guard) existiert bereits.createOrUpdate()/ Spalten B/C: Toter Code. B/C in allen 28.9k Batch- + 27.5k archiv-Datenzeilen leer → No-Op-Return. Nicht portiert.txId = user.idim Dateinamen: Das-0-<userId>--Segment deroriginalName-Konvention kodiertuser.idals „txId"-Platzhalter (das0-Feld ist der historische, hier konstante txId-Slot). Empirisch aus dem Archiv-Sheet bestätigt:open-questions.mdbelegtuser.idals Spalte A desarchiv-Tabs (A=user.id, 100% befüllt), genau die ID, die der Sheet-Pfad pro Adressbeweis führte. Jetzt umgesetzt (hartkodiert-0-${userId}-, per Test gegen den Download-Filter id 15 abgesichert). Annahme empirisch gestützt — formale Tech-Lead-Bestätigung der txId-Semantik vor PRD-Aktivierung erbeten.Bewusst zurückgestellt (DECISION_NEEDED)
signMessageGeneral(defaultMessage). Nutzer, deren Signatur gegenfallbackMessage(signMessage, DeFiChain) verifiziert wurde (auth.service.ts:502-504), haben de facto einen abweichenden Statement-Wortlaut unterschrieben, als das PDF zeigt. Da der native Pfad heutesignMessageGeneralals einheitliches Statement rendert, ist dies an die Q1-Allowlist-/Blockchain-Scope-Entscheidung gebunden (welche Chains/Messages der Pfad abdeckt). Nicht in diesem PR — Compliance/Produkt entscheidet, ob DeFiChain-Belege mit ihrem eigenensignMessage-Wortlaut gerendert werden müssen.selectAll/Sort-Richtung) kollabiert pro userData auf das jeweils älteste PDF und macht 13.262 Adressbeweise unsichtbar. Selektions-/Sort-Änderung (neueste vs. älteste = Compliance-Definition) + einmaliger Backfill der 13.262 Fälle wären nötig. Nicht in diesem PR — Produkt/Compliance muss Multi-Adress-Vollständigkeit und Sort-Semantik entscheiden.user.wallet?.name === REALUNIT_WALLET_NAME) + beide Logo-Assets (PdfBrand), wäre ~1 Zeile. DFX-only ist funktional ok. Nicht in diesem PR — Produkt entscheidet, ob RealUnit-Dokumente RealUnit-gebrandet sein müssen.archiv(~27.255 Audit-Zeilen seit Jan 2025) ist persistenter Store; die 15 übrigen Tabs sind regenerierbar. Aufbewahrungspflicht des Alt-Trails ist eine Compliance-Entscheidung (Export vs. ab-jetzt-Logging in der API). Nicht in diesem PR.Tests / Gates
format:check,lint,type-check: grün (0 Fehler).jest travel-rule: 22/22 grün (3 Suites):affected === 0→ skip), catch-Rollback inkl. Waisen-Invalidierung, distinctoriginalNamepro User mitsplit('-')[0] === YYYYMMDD; fail-closed Validierung (UUID → skip, Artefakt → skip, alle Allowlist-Formate → pass, Cardano;keyverbatim durchgereicht).%PDF-Magic), Signatur verbatim inkl.;key(kein split), M5-Kontiguität (statement+address als ein zusammenhängender String, kein getrenntes Adressfeld).backlog(nur verarbeitbare),skippedUnrecognised(Format-Randfälle/UUID separat),oldestAgeHoursaus ältestem verarbeitbaren Kandidaten (skipped-Kandidaten verfälschen das Alter nicht),claimedWithoutFile, Emit an Subscriber.Rollout (DEV vor PRD, mit Notbremse)
Gemäß Design-Abschnitt 5:
develop, DEV-Deploy mitTravelRulePdfin denDISABLED_PROCESSES(Job aus).travelRulePdfDategesetzt, Download-Filter id 15 liefert das PDF, Observer-Metriken plausibel).develop→main(Auto-Release-PR, User merged), PRD-Deploy mit Job inDISABLED_PROCESSES(aus).autoOn = false), Trigger als Fallback nicht löschen.DISABLED_PROCESSESentfernen).Notbremse:
DISABLED_PROCESSES += TravelRulePdfgreift in ~30s (resyncDisabledProcesses) — kein Redeploy nötig.Backlog seit 18.06.: kein Sonder-Backfill — alle offenen Kandidaten haben
travelRulePdfDate IS NULLund werden mittake: 100/Minute,order: id ASCabgearbeitet. Vor PRD-Aktivierung Pflicht-Check, ob in der Lücke manuell erledigte Fälle bereits eineAddressSignature-Row haben (sonst Doppel-PDF) → derentravelRulePdfDatenachsetzen.Status: Draft — DEV-Verifikation steht aus, nicht mergen.
Bekannte Einschränkung (vor Alert-/PRD-Aktivierung zu klären)
0x-Prefix) werden ohnetravelRulePdfDate-Marker übersprungen und belegen Slots im festentake:100/id ASC-Fenster. Bei ≥100 unverarbeitbaren Kandidaten mit niedrigeren IDs käme ein verarbeitbarer Kandidat nicht ins Fenster. Aktuell überwacht der Observer diese Fälle aktiv viaskippedUnrecognised(kein stiller Verlust). Falls die reale Verteilung das Fenster je füllt: Cursor/Offset oder persistenter Skip-Marker nachrüsten. Zusammen mit der Q1-Allowlist-Scope-Entscheidung zu klären.Bekannte Minors (Verify-Loop, kein Merge-Gate)
claimedWithoutFilezählt Alt-/admin-gesetzte Daten als Sockel (keinkycLevel/Custody-Filter wie die Job-Query) — Monitoring-Tuning, Follow-up vor PRD.G) nicht ab — sichtbar viaskippedUnrecognised, kein stiller Verlust; Widening zu[G-K]ist Q1-Entscheidung.Idempotenz (Update) — der Cron ersetzt BEIDE Sheet-Stufen
Die Sheet-Pipeline war zweistufig: ein Generator-Sheet erzeugte das
AddressSignature-PDF, ein separates vollautomatisches Verify-Stamp-Sheet (1w0hQk) stempeltetravelRulePdfDatemit dem Datum aus dem PDF-Dateinamen, sobald das PDF im Storage lag. Gegen PRD-DB belegt: 200 von 399 Kandidaten haben bereits ein gültigesAddressSignature-PDF (nur nie gestempelt). Der Cron ist daher idempotent: existiert ein validesAddressSignature-0-<userId>-PDF, wirdtravelRulePdfDatemit dem Datum aus dem Dateinamen gestempelt (kein Duplikat, keinnow-Datum); sonst wird erzeugt. Dateinamen-Muster gegen 1188/1188 echte Backlog-Dateien bestätigt; malformierte/unmögliche Datums-Namen sind fail-safe (kein Stempel, kein Rateversuch).