{
    "schema": "https://saferpage.de/schemas/operator-api-key-readiness.v2",
    "generated_at": "2026-08-06T14:15:20+00:00",
    "available": true,
    "status": "blocked_by_production_gates",
    "summary": "API-Key-Readiness: Runtime-Kontrollen 7/7 implementiert; API-Access-Storage 2/2 Tabellen; produktive Freigaben 0/7 aktiv.",
    "metrics": {
        "store_field_count": 11,
        "scope_mapping_count": 8,
        "audit_field_count": 14,
        "implementation_gate_count": 7,
        "implementation_gate_passed_count": 7,
        "implementation_gate_missing_count": 0,
        "gate_count": 7,
        "passed_gate_count": 0,
        "blocked_gate_count": 7,
        "api_access_storage_ready": 1,
        "api_access_storage_table_count": 2,
        "api_access_public_schema_create_privilege": 0,
        "api_access_preflight_check_count": 3,
        "api_access_ready_artifact_count": 0,
        "api_access_missing_required_artifact_count": 2,
        "fallback_audit_events_24h": 0,
        "write_hmac_fixture_count": 1,
        "write_hmac_fixture_negative_test_count": 4,
        "developer_quickstart_count": 4,
        "error_contract_count": 6,
        "rotation_playbook_step_count": 5,
        "client_acceptance_criterion_count": 6,
        "issuance_receipt_item_count": 6,
        "rate_limit_tier_count": 5,
        "abuse_response_step_count": 5,
        "post_issuance_watch_signal_count": 5,
        "openapi_contract_item_count": 6,
        "postman_collection_request_count": 5,
        "sdk_readiness_language_count": 5,
        "sandbox_onboarding_step_count": 6,
        "webhook_receiver_acceptance_count": 5,
        "support_slo_tier_count": 4,
        "consolidation_status_count": 4,
        "claim_boundary_count": 4,
        "api_parity_closure_plan_count": 6,
        "api_parity_closure_done_count": 0,
        "api_parity_closure_waiting_count": 6,
        "api_parity_closure_unlock_count": 18,
        "api_parity_closure_acceptance_count": 18,
        "api_parity_closure_operator_input_count": 18,
        "api_parity_closure_boundary_count": 6,
        "api_parity_closure_evidence_url_count": 6,
        "migration_signoff_item_count": 5,
        "migration_rollback_rehearsal_step_count": 4,
        "post_migration_canary_step_count": 6,
        "db_owner_handoff_step_count": 5,
        "db_owner_handoff_private_input_count": 5,
        "db_owner_handoff_public_output_count": 6,
        "db_owner_handoff_forbidden_output_count": 8,
        "db_owner_handoff_acceptance_count": 6,
        "db_owner_handoff_evidence_url_count": 6,
        "db_owner_handoff_validation_query_count": 5,
        "db_owner_handoff_safe_command_count": 4,
        "latest_smoke_available": 1,
        "latest_smoke_ok": 1,
        "latest_smoke_target_count": 9,
        "latest_smoke_failed_check_count": 0,
        "latest_smoke_blocked_expected_count": 0
    },
    "api_access_storage_status": {
        "available": true,
        "database_name": "saferpage",
        "db_user_hash": "d3234487bb1bc80ee9da6915667ce1f2",
        "api_keys_table": true,
        "api_access_audit_log_table": true,
        "scan_results_table": true,
        "public_schema_create_privilege": false,
        "api_key_store_ready": true,
        "migration_required": false,
        "admin_dsn_required": false,
        "migration_sha256": "27d802d48887d084e9132c841b943681c70638bd432746a92cab7809be621213",
        "migration_path": "infra/postgres/migrations/api-access.sql",
        "preflight_command": "scripts/run-api-access-migration-preflight.sh",
        "preflight_evidence_url": "https://saferpage.de/evidence/api-access-migration-preflight.json",
        "safe_apply_command": "SAFERPAGE_MIGRATION_DATABASE_URL='<admin-dsn-from-secure-shell>' scripts/run-api-access-migration.sh",
        "safe_next_action": "API-Access-Tabellen sind vorhanden; als nächstes Pepper, Domain-Claim, HMAC und Smoke-Tests produktiv freigeben.",
        "secret_policy": "Nur Tabellenstatus, Hashes und Privilegstatus; keine DSN, Passwörter, Roh-DB-User, Hosts, Ports, API-Keys oder Key-Hashes."
    },
    "api_access_migration_preflight": {
        "admin_dsn_required": false,
        "available": true,
        "checks": [
            {
                "id": "api_keys_table",
                "label": "API-Key-Tabelle",
                "operator_action": "infra/postgres/migrations/api-access.sql mit kurzlebigem Admin-DSN aus sicherer Shell anwenden.",
                "purpose": "Speichert nur Prefix, Hash, Scopes, Status, Ablauf und Revocation-Metadaten.",
                "ready": false,
                "required_for_key_store": true,
                "status": "missing"
            },
            {
                "id": "api_access_audit_log_table",
                "label": "API-Access-Auditlog",
                "operator_action": "infra/postgres/migrations/api-access.sql mit kurzlebigem Admin-DSN aus sicherer Shell anwenden.",
                "purpose": "Append-only Auditlog für Key-Prefix, Scope, Route, Entscheidung, Status und Hash-Evidence.",
                "ready": false,
                "required_for_key_store": true,
                "status": "missing"
            },
            {
                "id": "scan_results_table",
                "label": "Scan-Results-Basistabelle",
                "operator_action": "Basisinstallation prüfen; Scan-Results-Tabelle sollte für Reports vorhanden sein.",
                "purpose": "Vorhandene Reportbasis für Public- und Operator-Read-Flows.",
                "ready": false,
                "required_for_key_store": false,
                "status": "optional_missing"
            }
        ],
        "database_reachable": false,
        "error_class": "connection_failed",
        "generated_at": "2026-07-14T20:54:01.516354Z",
        "migration_path": "infra/postgres/migrations/api-access.sql",
        "migration_required": false,
        "migration_sha256": "27d802d48887d084e9132c841b943681c70638bd432746a92cab7809be621213",
        "missing_artifact_count": 2,
        "missing_required_artifacts": [
            "api_keys_table",
            "api_access_audit_log_table"
        ],
        "ok": false,
        "operator_runbook": [
            "Kurzlebigen Admin-DSN nur in einer sicheren Shell setzen; nicht in History, Projektdateien, systemd-Units oder Public-State speichern.",
            "scripts/run-api-access-migration-preflight.sh ausführen und missing_required_artifacts prüfen.",
            "scripts/run-api-access-migration.sh mit Admin-DSN ausführen.",
            "Preflight erneut ausführen, danach API-Key-Pepper, Domain-Claim, Write-HMAC und Deny/Allow-Smokes aktivieren."
        ],
        "ready_artifact_count": 0,
        "required_artifact_count": 2,
        "safe_apply_command": "SAFERPAGE_MIGRATION_DATABASE_URL='<admin-dsn-from-secure-shell>' scripts/run-api-access-migration.sh",
        "safe_combined_apply_command": "SAFERPAGE_MIGRATION_DATABASE_URL='<admin-dsn-from-secure-shell>' scripts/run-storage-migrations.sh",
        "safe_next_action": "Restore database connectivity and rerun this preflight before applying the migration.",
        "safe_preflight_command": "scripts/run-api-access-migration-preflight.sh",
        "schema": "https://saferpage.de/schemas/operator-api-access-migration-preflight.v1",
        "secret_policy": "No DSN, password, raw database user, host, port, key hash or API key is exported.",
        "preflight_evidence_url": "https://saferpage.de/evidence/api-access-migration-preflight.json"
    },
    "latest_smoke_result": {
        "available": true,
        "url": "https://saferpage.de/evidence/api-key-readiness-smoke.json",
        "schema": "https://saferpage.de/schemas/operator-api-key-readiness-smoke.v1",
        "status": "ok",
        "ok": true,
        "generated_at": "2026-06-15T00:27:41+00:00",
        "summary": "No-Secret-Smoke für API-Key-Readiness, Migration-Preflight, Runtime-Gates und Operator-Go-live-Verknuepfung.",
        "claim_boundary": "Dieser Smoke erzeugt keine API-Keys, setzt keine Env-Gates, wendet keine Migration an und prüft keine privaten Zielsysteme.",
        "metrics": {
            "target_count": 9,
            "http_passed_count": 9,
            "http_failed_count": 0,
            "check_count": 21,
            "failed_check_count": 0,
            "blocked_expected_count": 0,
            "api_access_storage_table_count": 2,
            "missing_required_artifact_count": 0,
            "consolidation_status_count": 0,
            "claim_boundary_count": 0,
            "api_parity_closure_plan_count": 6,
            "api_parity_closure_done_count": 0,
            "api_parity_closure_waiting_count": 6,
            "api_parity_closure_unlock_count": 18,
            "api_parity_closure_acceptance_count": 18,
            "api_parity_closure_operator_input_count": 18,
            "api_parity_closure_boundary_count": 6,
            "api_parity_closure_evidence_url_count": 6,
            "migration_signoff_item_count": 0,
            "migration_rollback_rehearsal_step_count": 0,
            "post_migration_canary_step_count": 0
        },
        "checks": [
            {
                "id": "public_routes_http_200",
                "label": "Öffentliche API-Key-Readiness-Routen erreichbar",
                "status": "passed",
                "evidence": "9/9 Route(s) liefern HTTP 200.",
                "operator_action": "Weiter mit Preflight/Go-live-Gates."
            },
            {
                "id": "migration_preflight_public",
                "label": "Migration-Preflight öffentlich und sanitisiert",
                "status": "passed",
                "evidence": "missing_required_artifacts=0, admin_dsn_required=no.",
                "operator_action": "Tabellen vorhanden; Runtime-Gates abnehmen."
            },
            {
                "id": "migration_admin_dsn_gate_explicit",
                "label": "API-Key-Store-Migration braucht DB-Admin-DSN der verantwortlichen Rolle statt Web-DB-User",
                "status": "passed",
                "evidence": "required_artifacts=2, ready_artifacts=2, missing_artifacts=0, admin_dsn_required=no.",
                "operator_action": "API-Key-Pepper, Domain-Claim und HMAC-Gates abnehmen."
            },
            {
                "id": "runtime_controls_documented",
                "label": "API-Runtime-Kontrollen dokumentiert",
                "status": "passed",
                "evidence": "manifest_ok=yes, controls=8/8, missing=0, secrets=0, implementation=7/7.",
                "operator_action": "Deny-/Allow-/Revocation-Smokes nach Storage-Migration erneut ausführen."
            },
            {
                "id": "runtime_gate_probe_contract",
                "label": "Runtime-Gate-Probe beschreibt Public Reads, Protected Contracts, Deny-Fixtures und Write-HMAC",
                "status": "passed",
                "evidence": "public_routes=4, protected_routes=7, denied_fixtures=4, live_denied=0, hmac_negative_tests=4, implementation=7/7, systemd=1.",
                "operator_action": "Runtime-Gate-Probe, HMAC-Testfixture, Protected Contracts oder systemd-Service-Evidence reparieren."
            },
            {
                "id": "live_missing_authorization_deny_smoke",
                "label": "Live-Deny-Smoke verweigert fehlende Authorization und schreibt sanitisiertes Audit",
                "status": "passed",
                "evidence": "ok=yes, status_code=401, decision=deny, audit_storage=postgres, fallback_events_24h=0.",
                "operator_action": "scripts/run-api-runtime-deny-smoke.sh prüfen; /api/operator/probe muss ohne Bearer-Key 401 liefern und Audit-Evidence schreiben."
            },
            {
                "id": "developer_client_contract_ready",
                "label": "Developer-Quickstarts, Fehlervertrag, Rotation und Client-Abnahme sind no-secret belegbar",
                "status": "passed",
                "evidence": "quickstarts=4, errors=6, rotation_steps=5, acceptance=6.",
                "operator_action": "Client-Onboarding, Fehlervertrag, Rotation und Abnahmekriterien im API-Key-Dossier pflegen; keine echten Keys oder Secrets in Public-Exports aufnehmen."
            },
            {
                "id": "api_key_issuance_receipt_ready",
                "label": "API-Key-Ausgabe hat Issuance-Receipt, Rate-Limits, Abuse-Stop und Nachlauf",
                "status": "passed",
                "evidence": "issuance_items=6, rate_limit_tiers=5, abuse_steps=5, watch_signals=5.",
                "operator_action": "Key-Issuance-Receipt, Rate-Limit-Tiers, Abuse-Response und Post-Issuance-Watch im API-Dossier pflegen; keine Raw-Keys, Hashes oder Betreiberidentitäten exportieren."
            },
            {
                "id": "api_integration_launch_kit_ready",
                "label": "API-Integration-Launch-Kit deckt OpenAPI, Postman, SDKs, Sandbox, Webhook-Abnahme und Support ab",
                "status": "passed",
                "evidence": "openapi=6, postman=5, sdks=5, sandbox=6, webhook=5, slo=4.",
                "operator_action": "OpenAPI-Vertrag, Postman-Smokes, SDK-Matrix, Sandbox-Onboarding, Webhook-Receiver-Abnahme und Support-SLOs im API-Dossier pflegen; echte Keys und Zielsystem-Secrets bleiben privat."
            },
            {
                "id": "api_access_page_developer_launch_kit_visible",
                "label": "API-Hauptseite zeigt Developer-Launch-Kit mit OpenAPI, Postman, SDK, Sandbox und Nicht-live-Grenzen",
                "status": "passed",
                "evidence": "api_access_url=https://saferpage.de/api-zugriff, visible=yes.",
                "operator_action": "API-Hauptseite muss Developer-Readiness und Nicht-live-Grenzen zeigen; Details bleiben im Key-Readiness-Dossier."
            },
            {
                "id": "api_access_openapi_export_contract",
                "label": "API-Hauptseite liefert no-secret OpenAPI 3.1 Vertrag mit BearerAuth, Pfaden und Claim-Grenze",
                "status": "passed",
                "evidence": "openapi=3.1.0, paths=6, bearer=yes, no_secret=yes.",
                "operator_action": "OpenAPI-Export muss Pfade, Security-Scheme, Fehlervertrag und No-Secret-Grenze zeigen; keine echten Keys, Servicezusage oder Write-Live-Zusage exportieren."
            },
            {
                "id": "api_access_postman_collection_contract",
                "label": "API-Hauptseite liefert no-secret Postman/Smoke Collection mit Placeholder-Key",
                "status": "passed",
                "evidence": "schema=yes, requests=7, bearer_placeholder=yes, no_secret=yes, forbidden_hits=0.",
                "operator_action": "Postman-Export muss v2.1-Schema, Smoke-Requests, Bearer-Placeholder und No-Secret-Grenze zeigen; keine echten Keys, privaten URLs oder Empfänger exportieren."
            }
        ],
        "failed_checks": [],
        "no_secret_policy": {
            "contains_secrets": false,
            "contains_private_target_urls": false,
            "contains_recipients": false,
            "contains_private_documents": false,
            "contains_visitor_logs": false,
            "sql_secret_pattern_hits": []
        }
    },
    "key_store_contract": {
        "hash_algorithm": "Argon2id für direkt verifizierte Keys oder HMAC-SHA-256 mit serverseitigem Pepper für Prefix-Lookups.",
        "one_time_display": true,
        "raw_key_storage_allowed": false,
        "max_lifetime_days": 90,
        "prefix_format": "sp_live_<8_char_prefix>",
        "fields": [
            {
                "field": "key_id",
                "type": "uuid",
                "purpose": "Interne stabile ID; nie als Auth-Secret nutzen.",
                "public_export": "allowed"
            },
            {
                "field": "key_prefix",
                "type": "text",
                "purpose": "Kurzer Prefix für Support, Audit und Revocation.",
                "public_export": "allowed_if_not_unique_secret"
            },
            {
                "field": "key_hash",
                "type": "argon2id_or_hmac_sha256",
                "purpose": "Klartext-Key serverseitig verifizieren; Roh-Key nie speichern.",
                "public_export": "forbidden"
            },
            {
                "field": "operator_id",
                "type": "uuid_or_external_subject",
                "purpose": "Betreiberkonto oder OIDC-Subject.",
                "public_export": "forbidden"
            },
            {
                "field": "domain_scope",
                "type": "text[]",
                "purpose": "Zulaessige Domains oder Portfolios.",
                "public_export": "redacted"
            },
            {
                "field": "scopes",
                "type": "text[]",
                "purpose": "Least-Privilege-Berechtigungen.",
                "public_export": "allowed"
            },
            {
                "field": "status",
                "type": "active|rotating|revoked|expired",
                "purpose": "Serverseitige Sperrentscheidung.",
                "public_export": "allowed"
            },
            {
                "field": "created_at",
                "type": "timestamptz",
                "purpose": "Ausstellungszeitpunkt.",
                "public_export": "allowed"
            },
            {
                "field": "expires_at",
                "type": "timestamptz",
                "purpose": "Maximal 90 Tage für produktive Keys.",
                "public_export": "allowed"
            },
            {
                "field": "last_used_at",
                "type": "timestamptz",
                "purpose": "Missbrauchs- und Aufraeumkontrolle.",
                "public_export": "redacted"
            },
            {
                "field": "revoked_at",
                "type": "timestamptz",
                "purpose": "Widerrufsnachweis.",
                "public_export": "allowed"
            }
        ]
    },
    "scope_endpoint_matrix": [
        {
            "scope": "reports.public:read",
            "tier": "public",
            "method": "GET",
            "endpoint_pattern": "/{domain}, /{domain}/share-card-json, /badge/{domain}",
            "risk": "niedrig",
            "decision": "ohne Key oder Public-Key mit Cache erlaubt"
        },
        {
            "scope": "schemas:read",
            "tier": "public",
            "method": "GET",
            "endpoint_pattern": "/schemas, /schemas/{schema}.v1",
            "risk": "niedrig",
            "decision": "öffentlich cachebar"
        },
        {
            "scope": "reports:read",
            "tier": "operator_read",
            "method": "GET",
            "endpoint_pattern": "/{domain}/module-export, /report-pack/{domain}/export",
            "risk": "mittel",
            "decision": "Domain-Claim und Operator-Key erforderlich"
        },
        {
            "scope": "portfolio:read",
            "tier": "operator_read",
            "method": "GET",
            "endpoint_pattern": "/portfolio/export, /portfolio/audit-json, /portfolio/schedule-json",
            "risk": "mittel",
            "decision": "Portfolio-Zuordnung prüfen"
        },
        {
            "scope": "evidence:read",
            "tier": "operator_read",
            "method": "GET",
            "endpoint_pattern": "/nachweise/{domain}/export, /api/report/export",
            "risk": "hoch",
            "decision": "Sanitization, Domain-Claim und Auditlog erforderlich"
        },
        {
            "scope": "nachweise:write",
            "tier": "operator_write",
            "method": "POST",
            "endpoint_pattern": "Nachweispositions-Delivery",
            "risk": "hoch",
            "decision": "HMAC, Idempotency-Key und Zielsystem-Dry-Run erforderlich"
        },
        {
            "scope": "dispatch:write",
            "tier": "operator_write",
            "method": "POST",
            "endpoint_pattern": "Scan-Dispatch und Alert-Dispatch",
            "risk": "hoch",
            "decision": "Betreiberfreigabe, Rate-Limit und Stop-Conditions erforderlich"
        },
        {
            "scope": "keys:rotate",
            "tier": "admin",
            "method": "POST",
            "endpoint_pattern": "Key-Rotation und Revocation",
            "risk": "kritisch",
            "decision": "OIDC/MFA, Vier-Augen-Gate und Auditlog erforderlich"
        }
    ],
    "access_audit_event_schema": {
        "retention_days": 400,
        "append_only": true,
        "fields": [
            {
                "field": "event_id",
                "type": "uuid",
                "policy": "required"
            },
            {
                "field": "request_id",
                "type": "uuid",
                "policy": "required"
            },
            {
                "field": "key_prefix",
                "type": "text",
                "policy": "allowed"
            },
            {
                "field": "key_hash",
                "type": "never_export",
                "policy": "forbidden"
            },
            {
                "field": "operator_subject_hash",
                "type": "sha256",
                "policy": "redacted"
            },
            {
                "field": "domain_scope_hash",
                "type": "sha256_or_domain_if_public",
                "policy": "redacted"
            },
            {
                "field": "scope",
                "type": "text",
                "policy": "required"
            },
            {
                "field": "endpoint",
                "type": "route_id",
                "policy": "required"
            },
            {
                "field": "method",
                "type": "GET|POST|PATCH|DELETE",
                "policy": "required"
            },
            {
                "field": "decision",
                "type": "allow|deny|rate_limited|revoked|expired",
                "policy": "required"
            },
            {
                "field": "status_code",
                "type": "int",
                "policy": "required"
            },
            {
                "field": "ip_hash",
                "type": "sha256_with_rotation_salt",
                "policy": "redacted"
            },
            {
                "field": "user_agent_hash",
                "type": "sha256",
                "policy": "redacted"
            },
            {
                "field": "created_at",
                "type": "timestamptz",
                "policy": "required"
            }
        ]
    },
    "runtime_implementation_gates": [
        {
            "id": "hashed_key_store_runtime",
            "label": "Key-Store- und Hash-Vertrag im Backend vorhanden",
            "status": "implemented",
            "passed": true,
            "evidence": "Migration deklariert api_keys mit Prefix/Hash/Scopes/Status; Storage liest Key-Records ohne Roh-Key-Export.",
            "next_action": "Mit Migration, Secret-Referenzen und Smoke-Tests produktiv verifizieren.",
            "export_policy": "Nur Implementierungsstatus und Dateievidenz ausgeben; keine Secrets, Roh-Keys, Hashes oder personenbezogenen Rohdaten."
        },
        {
            "id": "scope_enforcement_runtime",
            "label": "Scope-Enforcement im Operator-Probe vorhanden",
            "status": "implemented",
            "passed": true,
            "evidence": "/api/operator/probe vergleicht angeforderten Scope mit dem Key-Record und auditiert Deny-Entscheidungen.",
            "next_action": "Mit Migration, Secret-Referenzen und Smoke-Tests produktiv verifizieren.",
            "export_policy": "Nur Implementierungsstatus und Dateievidenz ausgeben; keine Secrets, Roh-Keys, Hashes oder personenbezogenen Rohdaten."
        },
        {
            "id": "access_audit_runtime",
            "label": "Access-Audit mit Fallback vorhanden",
            "status": "implemented",
            "passed": true,
            "evidence": "Postgres-Audit und File-Fallback schreiben sanitisierte Events; Fallback-Events letzte 24h: 0.",
            "next_action": "Mit Migration, Secret-Referenzen und Smoke-Tests produktiv verifizieren.",
            "export_policy": "Nur Implementierungsstatus und Dateievidenz ausgeben; keine Secrets, Roh-Keys, Hashes oder personenbezogenen Rohdaten."
        },
        {
            "id": "rate_limit_runtime",
            "label": "Rate-Limit-Gate im Operator-Probe vorhanden",
            "status": "implemented",
            "passed": true,
            "evidence": "Operator-Probe prüft ein serverseitiges Rate-Limit vor Auth-/Scope-Entscheidung und auditiert 429.",
            "next_action": "Mit Migration, Secret-Referenzen und Smoke-Tests produktiv verifizieren.",
            "export_policy": "Nur Implementierungsstatus und Dateievidenz ausgeben; keine Secrets, Roh-Keys, Hashes oder personenbezogenen Rohdaten."
        },
        {
            "id": "revocation_runtime",
            "label": "Revocation- und Expiry-Checks vorhanden",
            "status": "implemented",
            "passed": true,
            "evidence": "Key-Status revoked/expired und expires_at werden vor Allow-Entscheidung geprüft.",
            "next_action": "Mit Migration, Secret-Referenzen und Smoke-Tests produktiv verifizieren.",
            "export_policy": "Nur Implementierungsstatus und Dateievidenz ausgeben; keine Secrets, Roh-Keys, Hashes oder personenbezogenen Rohdaten."
        },
        {
            "id": "domain_claim_runtime",
            "label": "Domain-Scope-Gate vorhanden",
            "status": "implemented",
            "passed": true,
            "evidence": "Operator-Probe erzwingt Domain-Scope für Operator-Scopes und auditiert Domain-Scope-Mismatches nur mit Hash-Evidence.",
            "next_action": "Mit Migration, Secret-Referenzen und Smoke-Tests produktiv verifizieren.",
            "export_policy": "Nur Implementierungsstatus und Dateievidenz ausgeben; keine Secrets, Roh-Keys, Hashes oder personenbezogenen Rohdaten."
        },
        {
            "id": "write_hmac_runtime",
            "label": "Write-HMAC- und Idempotency-Gate vorhanden",
            "status": "implemented",
            "passed": true,
            "evidence": "Write-Scopes verlangen X-SaferPage-Signature und X-SaferPage-Idempotency-Key; Secret bleibt Server-Env.",
            "next_action": "Mit Migration, Secret-Referenzen und Smoke-Tests produktiv verifizieren.",
            "export_policy": "Nur Implementierungsstatus und Dateievidenz ausgeben; keine Secrets, Roh-Keys, Hashes oder personenbezogenen Rohdaten."
        }
    ],
    "write_hmac_test_fixture": {
        "purpose": "Öffentlicher Testfall für Operator-API-Write-Clients. Nicht als produktives Secret verwenden.",
        "algorithm": "HMAC-SHA256",
        "signature_header": "X-SaferPage-Signature",
        "idempotency_header": "X-SaferPage-Idempotency-Key",
        "secret_ref": "SAFERPAGE_API_WRITE_HMAC_SECRET",
        "test_secret": "saferpage_api_write_test_secret_do_not_use_in_production",
        "method": "POST",
        "endpoint": "/api/operator/probe",
        "scope": "nachweise:write",
        "idempotency_key": "sp-api-write-test-fixture",
        "canonical_string": "POST\n/api/operator/probe\nnachweise:write\nsp-api-write-test-fixture",
        "canonical_string_sha256": "16e4a7e04cf94f5ad6bc986e4214f0e600c06100b9cef6b40fbee419f5382049",
        "expected_signature": "sha256=aa2e6130e9eeb1bd9d3b92f4d2b883ece08b2b76389a01f61d99b55fed7a3b1d",
        "expected_signature_header": "X-SaferPage-Signature: sha256=aa2e6130e9eeb1bd9d3b92f4d2b883ece08b2b76389a01f61d99b55fed7a3b1d",
        "positive_test": "Client berechnet über exakt diesen kanonischen String dieselbe Signatur und sendet sie mit Idempotency-Key.",
        "negative_tests": [
            "Methode von POST auf GET ändern: Signatur muss abgelehnt werden.",
            "Scope von nachweise:write auf reports:read ändern: Signatur muss abgelehnt werden.",
            "Idempotency-Key ändern oder wiederverwenden: Payload muss abgelehnt oder als Duplikat behandelt werden.",
            "Signature-Prefix entfernen oder falschen Algorithmus nutzen: Request muss abgelehnt werden."
        ],
        "sample_commands": {
            "canonical_string_sha256": "printf %s \"$CANONICAL\" | sha256sum",
            "expected_signature": "printf %s \"$CANONICAL\" | openssl dgst -sha256 -hmac \"saferpage_api_write_test_secret_do_not_use_in_production\" -binary | xxd -p -c 256",
            "curl_dry_run": "curl -X POST \"https://saferpage.de/api/operator/probe?scope=nachweise%3Awrite&domain=anrufer.info\" -H \"Authorization: Bearer sp_live_testfixture.invalid\" -H \"X-SaferPage-Idempotency-Key: sp-api-write-test-fixture\" -H \"X-SaferPage-Signature: sha256=aa2e6130e9eeb1bd9d3b92f4d2b883ece08b2b76389a01f61d99b55fed7a3b1d\""
        }
    },
    "developer_quickstarts": [
        {
            "id": "curl_read_probe",
            "label": "Read-Probe mit Bearer-Key",
            "language": "curl",
            "scope": "reports:read",
            "endpoint": "GET /api/operator/probe?scope=reports:read&domain=anrufer.info",
            "secret_handling": "Bearer-Key nur aus Secret Manager oder Server-Env laden; nie in Browser, Logs oder Public Config schreiben.",
            "sample": "curl -fsS \"https://saferpage.de/api/operator/probe?scope=reports:read&domain=anrufer.info\" -H \"Authorization: Bearer ${SAFERPAGE_API_KEY}\"",
            "expected_failure_before_go_live": "401 ohne Authorization oder 403 bei fehlendem Scope/Domain-Claim."
        },
        {
            "id": "node_fetch_hmac",
            "label": "Node.js Write-HMAC",
            "language": "node",
            "scope": "nachweise:write",
            "endpoint": "POST /api/operator/probe",
            "secret_handling": "HMAC-Secret serverseitig halten; Idempotency-Key pro logischem Auftrag stabil setzen.",
            "sample": "crypto.createHmac(\"sha256\", process.env.SAFERPAGE_API_WRITE_HMAC_SECRET).update(canonical).digest(\"hex\")",
            "expected_failure_before_go_live": "401/403/409 bei fehlender Signatur, falschem Scope oder wiederverwendetem Idempotency-Key."
        },
        {
            "id": "php_server_client",
            "label": "PHP Server-Client",
            "language": "php",
            "scope": "portfolio:read",
            "endpoint": "GET /api/operator/probe?scope=portfolio:read",
            "secret_handling": "Key aus getenv lesen; Exception-Handler darf Authorization-Header nicht loggen.",
            "sample": "$headers = [\"Authorization: Bearer \" . getenv(\"SAFERPAGE_API_KEY\")];",
            "expected_failure_before_go_live": "401 bis Key-Store, Domain-Claim und Scope-Gate produktiv freigegeben sind."
        },
        {
            "id": "ci_dry_run",
            "label": "CI-Dry-run ohne echten Key",
            "language": "ci",
            "scope": "schemas:read",
            "endpoint": "GET /api-zugriff/runtime-gate-probe-json",
            "secret_handling": "CI nutzt nur Public Probe und Fixture-Signaturen; keine Live-Keys in Pull Requests.",
            "sample": "curl -fsS https://saferpage.de/api-zugriff/runtime-gate-probe-json | jq .metrics",
            "expected_failure_before_go_live": "Protected Routen bleiben im CI ohne Key denied."
        }
    ],
    "error_contract": [
        {
            "status_code": 400,
            "code": "bad_request",
            "meaning": "Pflichtparameter, Domain oder Scope fehlt.",
            "client_action": "Request lokal validieren; keine Wiederholung ohne Korrektur."
        },
        {
            "status_code": 401,
            "code": "missing_or_invalid_authorization",
            "meaning": "Bearer-Key fehlt, ist falsch formatiert oder kann nicht verifiziert werden.",
            "client_action": "Secret-Quelle prüfen; Key nicht in Logs ausgeben."
        },
        {
            "status_code": 403,
            "code": "scope_or_domain_denied",
            "meaning": "Key ist gültig, aber Scope, Domain-Claim oder Status erlaubt den Zugriff nicht.",
            "client_action": "Scope-Matrix und Domain-Freigabe prüfen."
        },
        {
            "status_code": 409,
            "code": "idempotency_conflict",
            "meaning": "Write-Request kollidiert mit vorhandenem Idempotency-Key oder Payload.",
            "client_action": "Duplikat behandeln; keinen neuen Key für denselben Auftrag erzeugen."
        },
        {
            "status_code": 429,
            "code": "rate_limited",
            "meaning": "Key-, Scope- oder IP-bezogenes Limit erreicht.",
            "client_action": "Retry-After beachten, Backoff mit Jitter verwenden."
        },
        {
            "status_code": 500,
            "code": "server_error_sanitized",
            "meaning": "Serverfehler ohne Secret- oder Rohpayload-Details.",
            "client_action": "Request-ID an Support geben; keine Secrets mitsenden."
        }
    ],
    "rotation_playbook": [
        {
            "step": 1,
            "label": "Neuen Key vorbereiten",
            "owner": "IT/Security",
            "action": "Neuen Key mit identischen oder reduzierten Scopes und maximal 90 Tagen Laufzeit erstellen.",
            "evidence": "key_prefix, scopes, expires_at, reviewer"
        },
        {
            "step": 2,
            "label": "Dual-run testen",
            "owner": "Integration-Verantwortung",
            "action": "Read- und Write-Dry-runs mit neuem Key ausführen, ohne alten Key zu widerrufen.",
            "evidence": "Deny-/Allow-Smoke, HMAC-Fixture, Audit-Event"
        },
        {
            "step": 3,
            "label": "Client umstellen",
            "owner": "Platform/DevOps",
            "action": "Secret Manager aktualisieren und Dienste kontrolliert neu starten.",
            "evidence": "Deploy-ID, Key-Prefix, Zeitpunkt"
        },
        {
            "step": 4,
            "label": "Alten Key widerrufen",
            "owner": "IT/Security",
            "action": "Alten Key auf revoked setzen und sicherstellen, dass der nächste Request denied wird.",
            "evidence": "revoked_at, deny_audit_event"
        },
        {
            "step": 5,
            "label": "Nachlauf prüfen",
            "owner": "Compliance/IT",
            "action": "Auditlog auf Nutzung alter Prefixe, Rate-Limits und Scope-Mismatches prüfen.",
            "evidence": "24h Audit-Auszug ohne Roh-Keys"
        }
    ],
    "client_acceptance": [
        {
            "id": "no_browser_key",
            "label": "Kein API-Key im Browser",
            "criterion": "Keys werden nur serverseitig genutzt; Frontend, HTML, JS und Mobile-Bundles enthalten keine Secrets.",
            "status": "required_before_live"
        },
        {
            "id": "least_privilege_scope",
            "label": "Least-Privilege-Scopes",
            "criterion": "Jeder Client bekommt nur benötigte Scopes und Domain-/Portfolio-Grenzen.",
            "status": "required_before_live"
        },
        {
            "id": "audit_request_id",
            "label": "Request-ID und Audit",
            "criterion": "Jeder Client sendet oder akzeptiert Request-ID; Deny/Allow wird sanitisiert auditiert.",
            "status": "required_before_live"
        },
        {
            "id": "retry_backoff",
            "label": "Retry/Backoff",
            "criterion": "Clients behandeln 409/429 deterministisch und wiederholen nicht unkontrolliert.",
            "status": "required_before_live"
        },
        {
            "id": "write_hmac_idempotency",
            "label": "Write-HMAC und Idempotency",
            "criterion": "Schreibende Clients signieren Payload-Kontext und setzen Idempotency-Key.",
            "status": "required_before_write_live"
        },
        {
            "id": "rotation_rehearsed",
            "label": "Rotation geprobt",
            "criterion": "Rotation und Revocation sind vor dem ersten Produktiv-Key mit Fixture-Key dokumentiert.",
            "status": "required_before_live"
        }
    ],
    "issuance_receipt": [
        {
            "id": "requester_verified",
            "label": "Requester und Betreiberrolle verifiziert",
            "owner": "Customer Success/Security",
            "evidence": "OIDC/MFA, Betreiberkonto und Domain-Claim sind vor Key-Erzeugung bestätigt.",
            "status": "required_before_key"
        },
        {
            "id": "scope_approved",
            "label": "Scopes und Domain-Grenzen freigegeben",
            "owner": "Security/Programm-Verantwortung",
            "evidence": "Scope-Matrix, Domain-Scope und Zweckbindung sind dokumentiert.",
            "status": "required_before_key"
        },
        {
            "id": "expiry_set",
            "label": "Ablaufdatum gesetzt",
            "owner": "Integration-Verantwortung",
            "evidence": "Produktive Keys maximal 90 Tage; Test-Keys kürzer.",
            "status": "required_before_key"
        },
        {
            "id": "display_once_ack",
            "label": "One-time Display akzeptiert",
            "owner": "Requester",
            "evidence": "Raw-Key wird nur einmal angezeigt, nie gespeichert und nie exportiert.",
            "status": "required_before_key"
        },
        {
            "id": "deny_allow_smoke",
            "label": "Deny-/Allow-Smoke geplant",
            "owner": "Platform",
            "evidence": "401/403/429/Revocation-Fixtures und Request-ID werden vor Go-live geprüft.",
            "status": "required_before_live"
        },
        {
            "id": "rotation_owner_set",
            "label": "Rotationsverantwortung gesetzt",
            "owner": "Security/Integration-Verantwortung",
            "evidence": "Verantwortlichkeit, Rotationstermin, Revocation-Pfad und Notfallkontakt sind bekannt.",
            "status": "required_before_live"
        }
    ],
    "rate_limit_tiers": [
        {
            "tier": "public_read",
            "scope_pattern": "reports.public:read, schemas:read",
            "limit": "cachebar, pro IP begrenzt",
            "burst": "hoch",
            "abuse_action": "429 und Cache/Edge-Regel prüfen"
        },
        {
            "tier": "operator_read",
            "scope_pattern": "reports:read, portfolio:read",
            "limit": "pro Key und Domain moderat",
            "burst": "mittel",
            "abuse_action": "Key-Prefix, Domain-Hash und Request-ID auditieren"
        },
        {
            "tier": "evidence_read",
            "scope_pattern": "evidence:read",
            "limit": "streng pro Key und Report-Pack",
            "burst": "niedrig",
            "abuse_action": "Sanitization prüfen und Key temporär pausieren"
        },
        {
            "tier": "operator_write",
            "scope_pattern": "nachweise:write, dispatch:write",
            "limit": "sehr streng mit Idempotency",
            "burst": "niedrig",
            "abuse_action": "Write-HMAC, Idempotency und Zielsystem-Dry-run prüfen"
        },
        {
            "tier": "admin",
            "scope_pattern": "keys:rotate",
            "limit": "MFA/Vier-Augen, kein Burst",
            "burst": "keiner",
            "abuse_action": "Break-glass-Review und Audit-Export erzeugen"
        }
    ],
    "abuse_response_playbook": [
        {
            "step": 1,
            "label": "Anomalie erkennen",
            "trigger": "429-Spike, Scope-Mismatch, unbekannter User-Agent oder viele Deny-Events.",
            "action": "Key-Prefix, Scope, Domain-Hash und Request-ID ohne Raw-Key prüfen."
        },
        {
            "step": 2,
            "label": "Key pausieren",
            "trigger": "Verdacht auf Fehlkonfiguration oder Leak.",
            "action": "status=revoked oder rotating setzen; keine Roh-Keys exportieren."
        },
        {
            "step": 3,
            "label": "Client informieren",
            "trigger": "Betreiberkontakt vorhanden.",
            "action": "Sanitisierte Fehlermeldung mit Prefix, Zeitraum und Request-IDs senden."
        },
        {
            "step": 4,
            "label": "Rotation erzwingen",
            "trigger": "Client bestätigt Wechsel.",
            "action": "Neuen Key mit reduzierten Scopes ausstellen und alten Key denied testen."
        },
        {
            "step": 5,
            "label": "Nachlauf auditieren",
            "trigger": "24 Stunden nach Pausierung.",
            "action": "Auditlog auf alte Prefixe, Rate-Limits und Scope-Mismatches prüfen."
        }
    ],
    "post_issuance_watch": [
        {
            "signal": "first_successful_request",
            "window": "erste 30 Minuten",
            "expected": "Genau erwartete Domain/Scope-Kombination, Request-ID vorhanden."
        },
        {
            "signal": "denied_request_ratio",
            "window": "erste 24 Stunden",
            "expected": "Deny-Anteil plausibel; keine wiederholten 401/403-Schleifen."
        },
        {
            "signal": "rate_limit_events",
            "window": "erste 24 Stunden",
            "expected": "429 selten; Client beachtet Retry-After und Backoff."
        },
        {
            "signal": "write_idempotency_conflicts",
            "window": "erste 24 Stunden",
            "expected": "Keine unkontrollierten 409-Wiederholungen."
        },
        {
            "signal": "old_prefix_usage_after_rotation",
            "window": "nach Rotation 24 Stunden",
            "expected": "Alter Prefix erzeugt nur Deny-Audit, keine Allow-Events."
        }
    ],
    "openapi_contract": [
        {
            "id": "security_scheme",
            "label": "Bearer-Auth als Security Scheme",
            "status": "documented_no_secret",
            "detail": "Authorization: Bearer ${SAFERPAGE_API_KEY}; Roh-Key nie in Beispiele oder Public-Exports schreiben."
        },
        {
            "id": "scope_extension",
            "label": "Scopes als x-saferpage-scopes",
            "status": "documented_no_secret",
            "detail": "Jede geschützte Route nennt erlaubte Scopes, Domain-Scope und Risiko-Tier."
        },
        {
            "id": "error_schema",
            "label": "Sanitisierte Fehlerstruktur",
            "status": "documented_no_secret",
            "detail": "code, message, request_id, retry_after und decision; keine Secrets, Header oder Rohpayloads."
        },
        {
            "id": "rate_limit_headers",
            "label": "Rate-Limit-Header",
            "status": "documented_no_secret",
            "detail": "Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining und X-RateLimit-Reset als Client-Vertrag."
        },
        {
            "id": "write_hmac_extension",
            "label": "Write-HMAC Extension",
            "status": "documented_no_secret",
            "detail": "x-saferpage-hmac mit Canonical-String, Idempotency-Key und Negativtests aus Fixture."
        },
        {
            "id": "audit_correlation",
            "label": "Request-ID und Audit-Korrelation",
            "status": "documented_no_secret",
            "detail": "Clients senden oder übernehmen X-Request-ID; Audit exportiert nur Prefix, Scope, Entscheidung und Hashes."
        }
    ],
    "postman_collection": [
        {
            "id": "runtime_gate_probe",
            "method": "GET",
            "path": "/api-zugriff/runtime-gate-probe-json",
            "purpose": "Public Runtime-Vertrag und Deny-Fixtures abrufen.",
            "secret_policy": "kein Key erforderlich"
        },
        {
            "id": "missing_authorization_deny",
            "method": "GET",
            "path": "/api/operator/probe?scope=reports:read&domain=anrufer.info",
            "purpose": "401-Deny ohne Authorization reproduzieren.",
            "secret_policy": "kein Key im Request"
        },
        {
            "id": "operator_read_probe",
            "method": "GET",
            "path": "/api/operator/probe?scope=reports:read&domain=anrufer.info",
            "purpose": "Read-Key nach Go-live prüfen.",
            "secret_policy": "Bearer aus Environment-Variable"
        },
        {
            "id": "operator_write_hmac_probe",
            "method": "POST",
            "path": "/api/operator/probe",
            "purpose": "Write-HMAC und Idempotency testen.",
            "secret_policy": "HMAC-Secret nur serverseitig"
        },
        {
            "id": "revocation_probe",
            "method": "GET",
            "path": "/api/operator/probe?scope=reports:read&domain=anrufer.info",
            "purpose": "Widerrufener oder abgelaufener Key muss 403 liefern.",
            "secret_policy": "nur Test-Key nach Freigabe"
        }
    ],
    "sdk_readiness_matrix": [
        {
            "language": "Node.js",
            "status": "contract_ready",
            "minimum": "fetch oder undici, crypto.createHmac, AbortController",
            "owner": "Developer Experience",
            "first_test": "Runtime-Gate-Probe und HMAC-Fixture."
        },
        {
            "language": "PHP",
            "status": "contract_ready",
            "minimum": "curl/ext-openssl, getenv, PSR-3 Redaction",
            "owner": "Developer Experience",
            "first_test": "Read-Probe mit serverseitigem Bearer-Key."
        },
        {
            "language": "Python",
            "status": "contract_ready",
            "minimum": "requests/httpx, hmac, uuid, backoff",
            "owner": "Developer Experience",
            "first_test": "429/Retry-After und Request-ID prüfen."
        },
        {
            "language": "Go",
            "status": "contract_ready",
            "minimum": "net/http, crypto/hmac, context timeout",
            "owner": "Developer Experience",
            "first_test": "Idempotency und Timeout-Handling."
        },
        {
            "language": "CI/curl",
            "status": "contract_ready",
            "minimum": "curl, jq, masked secrets",
            "owner": "Platform",
            "first_test": "Deny-Smoke ohne Key und Public Schema-Check."
        }
    ],
    "sandbox_onboarding_plan": [
        {
            "step": 1,
            "label": "Public Contract lesen",
            "action": "Runtime-Gate-Probe, Scope-Matrix, Fehlervertrag und No-Secret-Grenzen prüfen.",
            "exit_criterion": "Client versteht 401/403/409/429 und Request-ID."
        },
        {
            "step": 2,
            "label": "Deny-Smoke ausführen",
            "action": "Ohne Authorization 401 und sanitisiertes Audit erwarten.",
            "exit_criterion": "Kein Secret, kein Raw-Key, Audit-Storage sichtbar."
        },
        {
            "step": 3,
            "label": "Domain-Claim vorbereiten",
            "action": "Betreiberrolle, Domain-Scope und Zweckbindung vor Key-Ausgabe dokumentieren.",
            "exit_criterion": "Domain-Claim-Receipt liegt vor."
        },
        {
            "step": 4,
            "label": "Read-Key testen",
            "action": "Test-Key einmal anzeigen, in Secret Manager legen und Read-Probe ausführen.",
            "exit_criterion": "Nur erlaubte Domain/Scope-Kombination liefert allow."
        },
        {
            "step": 5,
            "label": "Write-HMAC testen",
            "action": "Canonical-String, Signatur und Idempotency-Key gegen Fixture prüfen.",
            "exit_criterion": "Positive und negative HMAC-Tests dokumentiert."
        },
        {
            "step": 6,
            "label": "Rotation proben",
            "action": "Dual-run, Client-Switch, Revocation und 24h-Nachlauf durchführen.",
            "exit_criterion": "Alter Prefix erzeugt nur Deny-Audit."
        }
    ],
    "webhook_receiver_acceptance": [
        {
            "id": "hmac_required",
            "label": "Receiver validiert HMAC",
            "criterion": "X-SaferPage-Signature wird mit konstantzeitlichem Vergleich geprüft.",
            "status": "required_before_write_live"
        },
        {
            "id": "idempotency_required",
            "label": "Receiver speichert Idempotency-Key",
            "criterion": "Doppelte Write-Requests erzeugen keine zweite externe Aktion.",
            "status": "required_before_write_live"
        },
        {
            "id": "retry_safe",
            "label": "Retry ist nebenwirkungsarm",
            "criterion": "409/429/5xx werden mit Backoff und Jitter behandelt.",
            "status": "required_before_write_live"
        },
        {
            "id": "payload_minimized",
            "label": "Payload minimiert",
            "criterion": "Keine Roh-Cookies, Authorization-Header, Besucherlogs oder private Dokument-URLs.",
            "status": "required_before_write_live"
        },
        {
            "id": "receiver_logs_redacted",
            "label": "Receiver-Logs redacted",
            "criterion": "Logs enthalten Request-ID, Prefix und Entscheidung, aber keine Secrets.",
            "status": "required_before_write_live"
        }
    ],
    "support_slo_matrix": [
        {
            "severity": "sev1_key_leak_suspected",
            "response": "15 Minuten",
            "action": "Key pausieren, Prefix auditieren, Rotation erzwingen.",
            "customer_output": "Sanitisierter Incident-Receipt ohne Roh-Key."
        },
        {
            "severity": "sev2_write_delivery_blocked",
            "response": "4 Stunden",
            "action": "HMAC, Idempotency, 409/429 und Receiver-Antwort prüfen.",
            "customer_output": "Request-ID-Liste und nächster Testschritt."
        },
        {
            "severity": "sev3_scope_or_domain_denied",
            "response": "1 Arbeitstag",
            "action": "Scope-Matrix, Domain-Claim und Betreiberrolle prüfen.",
            "customer_output": "Freigabe- oder Ablehnungsgrund."
        },
        {
            "severity": "sev4_docs_or_sdk_question",
            "response": "2 Arbeitstage",
            "action": "Quickstart, OpenAPI/Postman oder SDK-Beispiel aktualisieren.",
            "customer_output": "Dokumentationslink und Beispielrequest."
        }
    ],
    "fallback_audit_status": {
        "available": true,
        "events_24h": 0,
        "last_event_at": "2026-06-12T10:46:25+00:00"
    },
    "readiness_gates": [
        {
            "id": "hashed_key_store",
            "label": "Gehashter Key-Store produktiv",
            "env_ref": "SAFERPAGE_API_KEY_STORE_READY",
            "status": "required",
            "passed": false,
            "owner": "IT/Security",
            "evidence": "Server-Freigabe SAFERPAGE_API_KEY_STORE_READY ist nicht aktiv.",
            "operator_action": "Key-Store mit Hashing, Prefix und Revocation-Feldern bereitstellen.",
            "export_policy": "Nur Referenzname und Status ausgeben; keine API-Keys, Hashes, Salts, IPs oder personenbezogenen Rohdaten."
        },
        {
            "id": "scope_enforcement",
            "label": "Serverseitige Scope-Prüfung",
            "env_ref": "SAFERPAGE_API_SCOPE_ENFORCEMENT_READY",
            "status": "required",
            "passed": false,
            "owner": "Backend",
            "evidence": "Server-Freigabe SAFERPAGE_API_SCOPE_ENFORCEMENT_READY ist nicht aktiv.",
            "operator_action": "Middleware für Scope, Domain-Claim und Method-Policy aktivieren.",
            "export_policy": "Nur Referenzname und Status ausgeben; keine API-Keys, Hashes, Salts, IPs oder personenbezogenen Rohdaten."
        },
        {
            "id": "access_audit_log",
            "label": "Access-Auditlog aktiv",
            "env_ref": "SAFERPAGE_API_ACCESS_AUDIT_READY",
            "status": "required",
            "passed": false,
            "owner": "Compliance/IT",
            "evidence": "Server-Freigabe SAFERPAGE_API_ACCESS_AUDIT_READY ist nicht aktiv.",
            "operator_action": "Append-only Auditlog mit Request-ID, Scope, Entscheidung und Statuscode anbinden.",
            "export_policy": "Nur Referenzname und Status ausgeben; keine API-Keys, Hashes, Salts, IPs oder personenbezogenen Rohdaten."
        },
        {
            "id": "rate_limit",
            "label": "Rate-Limits je Key und Scope",
            "env_ref": "SAFERPAGE_API_RATE_LIMIT_READY",
            "status": "required",
            "passed": false,
            "owner": "Platform",
            "evidence": "Server-Freigabe SAFERPAGE_API_RATE_LIMIT_READY ist nicht aktiv.",
            "operator_action": "Rate-Limit-Store und Stop-Condition für Schreibpfade aktivieren.",
            "export_policy": "Nur Referenzname und Status ausgeben; keine API-Keys, Hashes, Salts, IPs oder personenbezogenen Rohdaten."
        },
        {
            "id": "revocation",
            "label": "Sofortige Sperrung und Rotation",
            "env_ref": "SAFERPAGE_API_REVOCATION_READY",
            "status": "required",
            "passed": false,
            "owner": "IT/Security",
            "evidence": "Server-Freigabe SAFERPAGE_API_REVOCATION_READY ist nicht aktiv.",
            "operator_action": "Revocation-Check vor Scope-Entscheidung ausführen und Rotation dokumentieren.",
            "export_policy": "Nur Referenzname und Status ausgeben; keine API-Keys, Hashes, Salts, IPs oder personenbezogenen Rohdaten."
        },
        {
            "id": "domain_claim_required",
            "label": "Domain-Claim vor Operator-Zugriff",
            "env_ref": "SAFERPAGE_API_DOMAIN_CLAIM_READY",
            "status": "required",
            "passed": false,
            "owner": "Programm-Verantwortung",
            "evidence": "Server-Freigabe SAFERPAGE_API_DOMAIN_CLAIM_READY ist nicht aktiv.",
            "operator_action": "Domain-Verifizierung und Rollenfreigabe vor Key-Ausstellung erzwingen.",
            "export_policy": "Nur Referenzname und Status ausgeben; keine API-Keys, Hashes, Salts, IPs oder personenbezogenen Rohdaten."
        },
        {
            "id": "write_hmac",
            "label": "HMAC für Schreib- und Webhook-Pfade",
            "env_ref": "SAFERPAGE_API_WRITE_HMAC_READY",
            "status": "required",
            "passed": false,
            "owner": "IT/Security",
            "evidence": "Server-Freigabe SAFERPAGE_API_WRITE_HMAC_READY ist nicht aktiv.",
            "operator_action": "X-SaferPage-Signature und X-SaferPage-Idempotency-Key für Write-Scopes erzwingen.",
            "export_policy": "Nur Referenzname und Status ausgeben; keine API-Keys, Hashes, Salts, IPs oder personenbezogenen Rohdaten."
        }
    ],
    "consolidation_status": [
        {
            "id": "runtime_controls",
            "label": "Runtime-Kontrollen vorbereitet",
            "state": "erreicht",
            "evidence": "7/7 Implementierungs-Gates sind durch Code- oder Control-Evidence belegt.",
            "boundary": "Das ersetzt keine produktive Key-Ausgabe und kein echtes Betreiber-Signoff.",
            "next_action": "Nach Storage-Migration Deny-, Allow-, Revocation-, Rate-Limit- und HMAC-Smokes erneut ausführen."
        },
        {
            "id": "api_storage",
            "label": "API-Access-Storage",
            "state": "erreicht",
            "evidence": "api_keys/api_access_audit_log: 2/2 Tabellen vorhanden.",
            "boundary": "Tabellenstatus sagt noch nichts über echte Keys, Pepper, Rollenfreigabe oder Rotation.",
            "next_action": "Pepper, Domain-Claim und Smoke-Gates produktiv signieren."
        },
        {
            "id": "public_evidence",
            "label": "Öffentliche Evidence und No-Secret-Exports",
            "state": "erreicht",
            "evidence": "Readiness-Smoke: ok; Secret-Policy bleibt öffentlich prüfbar.",
            "boundary": "Öffentliche Evidence darf keine DSN, Roh-Keys, Hashes, Betreiberidentitäten oder private Zielsysteme enthalten.",
            "next_action": "Smokes nach jeder Änderung neu laufen lassen und erst dann Claims in Parity-/Go-live-Ansichten übernehmen."
        },
        {
            "id": "operator_claims",
            "label": "Claims für Betreiber konsolidieren",
            "state": "begrenzt",
            "evidence": "Dossier trennt technische Vorbereitung, Produktiv-Gates, Signoff, Rollback und Canary-Checks.",
            "boundary": "Nicht behaupten: echte API-Keys sind live, solange Storage, Secrets, Domain-Claim und Gates nicht zusammen grün sind.",
            "next_action": "Darstellung auf klare Betreiber-Entscheidungen ausrichten: was ist fertig, was ist blockiert, was ist der nächste sichere Schritt."
        }
    ],
    "api_claim_boundaries": [
        {
            "id": "no_key_issuance_claim",
            "label": "Keine produktive Key-Ausgabe behaupten",
            "claim_allowed": "API-Key-Prozess ist vorbereitet und no-secret dokumentiert.",
            "claim_forbidden": "Produktive API-Keys sind verfügbar.",
            "reason": "Storage-Tabellen, Pepper, Betreiberrolle, Domain-Claim und Runtime-Gates müssen gemeinsam freigegeben sein.",
            "evidence_url": "https://saferpage.de/api-zugriff/key-readiness-json"
        },
        {
            "id": "migration_boundary",
            "label": "Migration ist Übergabepaket, kein Auto-Apply",
            "claim_allowed": "SQL, Hash und Preflight sind für die DB-Verantwortung prüfbar.",
            "claim_forbidden": "Die Migration wurde produktiv angewendet.",
            "reason": "Der aktuelle Web-DB-User hat kein CREATE-Recht; Admin-DSN darf nicht öffentlich gespeichert werden.",
            "evidence_url": "https://saferpage.de/evidence/api-access-migration-preflight.json"
        },
        {
            "id": "smoke_boundary",
            "label": "Smokes prüfen Grenzen, nicht private Zielsysteme",
            "claim_allowed": "Öffentliche Routen, No-Secret-Regeln und Deny-Verhalten sind prüfbar.",
            "claim_forbidden": "Alle Kundenintegrationen sind produktiv abgenommen.",
            "reason": "Private Empfänger, Betreiberidentitäten, echte Keys und Webhook-Secrets bleiben außerhalb öffentlicher Evidence.",
            "evidence_url": "https://saferpage.de/evidence/api-key-readiness-smoke.json"
        },
        {
            "id": "operator_wording",
            "label": "Betreiber-Wording statt Technikbehauptung",
            "claim_allowed": "Betreiber sehen nächste sichere Schritte und klare Stop-Bedingungen.",
            "claim_forbidden": "Score oder API-Reife als pauschales Sicherheitsurteil darstellen.",
            "reason": "Automatisierte Checks können Kontext wie Rollen, Consent, Paywall oder manuelle Betreiberfreigaben nicht vollständig bewerten.",
            "evidence_url": "https://saferpage.de/vergleich/parity-readiness-json"
        }
    ],
    "api_parity_closure_plan": [
        {
            "id": "key_store_migration_pepper",
            "label": "Key-Store-Migration, Pepper und One-time Display",
            "status": "waiting_for_secret_signoff",
            "owner": "DBA/IT-Security",
            "operator_input": [
                "DB-Verantwortung bestätigt SQL-Hash und Apply-Zeitfenster.",
                "Pepper/Key-Hash-Secret wird nur im Secret Manager oder Server-Environment gesetzt.",
                "One-time-Display-Regel für Raw-Key wird durch Security freigegeben."
            ],
            "unlocks": [
                "Echte API-Keys können sicher gehasht und widerrufbar gespeichert werden.",
                "Support kann nur mit Prefix arbeiten, ohne Roh-Key oder Hash zu sehen.",
                "Migration kann als belegbarer Schritt im Go-live-Center geschlossen werden."
            ],
            "acceptance_criteria": [
                "api_keys und api_access_audit_log existieren und Preflight zeigt 2/2 Tabellen.",
                "Public-Exports enthalten keinen Roh-Key, keinen Hash, keinen Pepper und keine DSN.",
                "Test-Key wird nur einmal angezeigt und danach nur über Prefix/Audit nachgewiesen."
            ],
            "competitor_parity_effect": "Schließt die Basislücke zu Plattformen mit API-Key-Lifecycle, Secret-Handling und Audit-Trail.",
            "evidence_url": "https://saferpage.de/evidence/api-access-migration-preflight.json",
            "boundary": "Vor Secret-Signoff keine produktive Key-Ausgabe behaupten.",
            "do_not_claim_until_done": "Produktive API-Keys sind verfügbar."
        },
        {
            "id": "domain_claim_operator_auth",
            "label": "Betreiber-Auth, Domain-Claim und Rollenfreigabe",
            "status": "waiting_for_operator_auth",
            "owner": "Customer Success/Programm-Verantwortung",
            "operator_input": [
                "Betreiberkonto mit MFA/OIDC ist bestätigt.",
                "Domain-Claim oder Portfolio-Zuordnung ist belegt.",
                "Anfragende Rolle und Zweck der API-Nutzung sind dokumentiert."
            ],
            "unlocks": [
                "Operator-Scopes können auf verifizierte Domains begrenzt werden.",
                "Private Reports und Portfolio-Exports bleiben gegen fremde Domains gesperrt.",
                "Key-Ausgabe wird als Betreiberentscheidung statt technischer Default behandelt."
            ],
            "acceptance_criteria": [
                "Kein Operator-Key ohne verifizierte Domain oder Portfolio-Zuordnung.",
                "Scope-Matrix nennt für jede geschützte Route Domain-Claim-Anforderung.",
                "Deny-Smoke liefert 403 bei Domain-Scope-Mismatch."
            ],
            "competitor_parity_effect": "Schließt die Trust-/Operator-Grenze gegen Plattformen mit Customer Portal und rollenbasiertem API-Zugang.",
            "evidence_url": "https://saferpage.de/betreiber/go-live-json",
            "boundary": "Public Evidence darf keine Betreiberidentitäten oder privaten Domain-Claims offenlegen.",
            "do_not_claim_until_done": "Jeder Betreiber kann selbststaendig API-Keys ausstellen."
        },
        {
            "id": "scope_runtime_rate_limit",
            "label": "Scopes, Runtime-Gates, Rate-Limits und Revocation",
            "status": "waiting_for_runtime_signoff",
            "owner": "Backend/Platform",
            "operator_input": [
                "Scope-Matrix für Read-, Evidence-, Write- und Admin-Routen freigeben.",
                "Rate-Limit-Tiers und Retry-After-Vertrag abnehmen.",
                "Revocation-/Rotation-Probe mit Test-Key durchführen."
            ],
            "unlocks": [
                "Clients erhalten planbare 401/403/409/429-Fehler statt unklarer API-Antworten.",
                "Missbrauch kann über Prefix, Scope, Domain-Hash und Request-ID eingegrenzt werden.",
                "Alte oder kompromittierte Keys können vor dem nächsten Request gesperrt werden."
            ],
            "acceptance_criteria": [
                "Runtime-Gate-Probe zeigt alle Implementierungs-Gates gründlich belegt.",
                "Rate-Limit- und Revocation-Fixtures sind im Smoke ohne Secret-Leak prüfbar.",
                "OpenAPI/Postman-Doku beschreibt Fehlervertrag, Retry und Request-ID."
            ],
            "competitor_parity_effect": "Schließt die Integrationsreife zu API-first Compliance- und Trust-Plattformen.",
            "evidence_url": "https://saferpage.de/api-zugriff/runtime-gate-probe-json",
            "boundary": "Runtime-Verträge beweisen kein echtes Allow für Kunden, solange keine produktiven Keys ausgegeben wurden.",
            "do_not_claim_until_done": "Operator-API ist voll produktiv nutzbar."
        },
        {
            "id": "write_hmac_webhook_receivers",
            "label": "Write-HMAC, Idempotency und Webhook-Receiver-Abnahme",
            "status": "waiting_for_hmac_secret",
            "owner": "IT-Security/Integration-Verantwortung",
            "operator_input": [
                "Write-HMAC-Secret wird serverseitig gesetzt und rotiert.",
                "Receiver validiert Signatur konstantzeitlich und speichert Idempotency-Key.",
                "Retry-, 409- und 429-Verhalten wird mit Test-Fixture abgenommen."
            ],
            "unlocks": [
                "Schreibende Aktionen können nebenwirkungsarm und nachvollziehbar ausgeführt werden.",
                "Doppelte Delivery oder Ticket-Erzeugung wird verhindert.",
                "Webhook-/Ticket-Integrationen lassen sich gegen Konkurrenzprodukte als kontrollierter Write-Pfad erklären."
            ],
            "acceptance_criteria": [
                "Positive und negative HMAC-Fixture-Tests sind dokumentiert.",
                "Receiver-Logs enthalten keine Secrets, keine Authorization-Header und keine Rohpayloads.",
                "Idempotency-Konflikte liefern 409 statt Mehrfachaktion."
            ],
            "competitor_parity_effect": "Schließt Write-/Webhook-Lücken gegen Plattformen mit Jira, Slack, Teams, Webhook und Ticket-Automation.",
            "evidence_url": "https://saferpage.de/api-zugriff/key-readiness-json",
            "boundary": "Fixture ist öffentlich und kein produktives Secret; echte Receiver bleiben privat.",
            "do_not_claim_until_done": "Schreibende Kundenintegrationen sind produktiv abgenommen."
        },
        {
            "id": "developer_onboarding_contracts",
            "label": "Developer-Onboarding, OpenAPI, Postman, SDKs und Sandbox",
            "status": "contract_ready_waiting_for_live_keys",
            "owner": "Developer Experience",
            "operator_input": [
                "OpenAPI-Security-Schemes, Scopes und Fehlerobjekte final freigeben.",
                "Postman-Smokes und CI-Dry-run ohne echte Keys veröffentlichen.",
                "SDK-Minimums für Node, PHP, Python, Go und CI/curl bestätigen."
            ],
            "unlocks": [
                "Integratoren können vor Key-Ausgabe sicher gegen Public- und Deny-Fixtures entwickeln.",
                "Support kann Kundenfehler über Request-ID, Code und Retry-Vertrag einordnen.",
                "Sandbox-Onboarding wird als produktnahe, aber no-secret Variante kommunizierbar."
            ],
            "acceptance_criteria": [
                "OpenAPI-Vertrag enthält Bearer-Auth, Scopes, Rate-Limits und HMAC-Extension.",
                "Postman-Collection deckt Public Probe, 401, HMAC und Revocation-Fall ab.",
                "SDK-Beispiele laden Keys nur serverseitig aus Env/Secret Manager."
            ],
            "competitor_parity_effect": "Schließt die Developer-Experience-Lücke zu API-first Wettbewerbern mit Docs, Collections und SDK-Pfaden.",
            "evidence_url": "https://saferpage.de/api-zugriff/key-readiness-md",
            "boundary": "Docs und Fixtures sind bereit; ohne Live-Key-Ausgabe bleibt es eine Integrationsvorbereitung.",
            "do_not_claim_until_done": "Self-service API-Onboarding ist produktiv live."
        },
        {
            "id": "support_slo_abuse_go_live",
            "label": "Support-SLO, Abuse-Stop, Go-live-Signoff und Nachlauf",
            "status": "waiting_for_operator_go_live_signoff",
            "owner": "Support/Security/Programm-Verantwortung",
            "operator_input": [
                "SLO-Tiers für Key-Leak, Write-Blocker, Scope-Deny und Docs-Fragen abnehmen.",
                "Abuse-Response mit Pause, Rotation und sanitisierter Kundenkommunikation freigeben.",
                "Post-Issuance-Watch für erste 30 Minuten und 24 Stunden aktivieren."
            ],
            "unlocks": [
                "Produktive API-Keys können mit Stop-Bedingungen statt nur Technikfreigabe gestartet werden.",
                "Fehlkonfigurationen und Leaks haben klare Reaktionswege.",
                "Go-live-Center kann API-Parität als kontrollierten Betreiberprozess abbilden."
            ],
            "acceptance_criteria": [
                "Go-live-Dossier enthält Evidence-URLs, Stop-Bedingungen und No-Secret-Checkliste.",
                "Erste 24h nach Key-Ausgabe werden Deny-Quote, 429, 409 und alte Prefixe beobachtet.",
                "Kundenoutput enthält nur Prefix, Request-ID, Zeitraum und sanitisierte Entscheidung."
            ],
            "competitor_parity_effect": "Schließt die Betriebslücke zu reifen Plattformen mit Support-Ziel, Incident-Prozess und Post-Go-live-Monitoring.",
            "evidence_url": "https://saferpage.de/betreiber/go-live-json",
            "boundary": "Ohne Betreiber-Signoff bleibt API-Reife ein vorbereitetes Dossier, kein Live-Service-Versprechen.",
            "do_not_claim_until_done": "API-Betrieb ist mit produktiver Servicezusage und Support freigegeben."
        }
    ],
    "db_owner_handoff_packet": {
        "status": "storage_ready_after_apply",
        "summary": "API-Access-Tabellen sind vorhanden; DB-Verantwortungs-Handoff bleibt als Nachweis für Hash, Rollback und Canary erhalten.",
        "handoff_steps": [
            {
                "id": "verify_public_sql_hash",
                "label": "SQL-Hash vergleichen",
                "owner": "DB-Verantwortung",
                "action": "Public SHA-256 mit der lokal angewendeten Datei vergleichen.",
                "acceptance": "Hash stimmt exakt mit dem Readiness-Export überein.",
                "evidence_url": "https://saferpage.de/api-zugriff/key-readiness-json",
                "boundary": "Hash beweist nur Dateigleichheit, nicht erfolgreiche Migration."
            },
            {
                "id": "reserve_apply_window",
                "label": "Apply-Fenster und Backup festlegen",
                "owner": "DB-Verantwortung/Platform",
                "action": "Zeitfenster, Backup-Stand und Pause-Entscheidung privat dokumentieren.",
                "acceptance": "Zeitfenster und Rollback-Verantwortung sind vor Apply bestätigt.",
                "evidence_url": "https://saferpage.de/api-zugriff/key-readiness-md",
                "boundary": "Private Backup-Details werden nicht öffentlich exportiert."
            },
            {
                "id": "apply_with_short_lived_admin_dsn",
                "label": "Kurzlebigen Admin-DSN nutzen",
                "owner": "DB-Verantwortung",
                "action": "Migration nur aus sicherer Shell mit kurzlebigem Admin-DSN ausführen und DSN danach entfernen.",
                "acceptance": "DSN taucht in keinem Public-Export, Repo, Log oder Smoke auf.",
                "evidence_url": "https://saferpage.de/evidence/api-access-migration-preflight.json",
                "boundary": "Ohne Admin-DSN bleibt der Web-DB-User erwartbar blockiert."
            },
            {
                "id": "rerun_preflight_and_runtime_smokes",
                "label": "Preflight und Runtime-Smokes wiederholen",
                "owner": "Platform/Security",
                "action": "Preflight, Deny-Smoke, Runtime-Probe und API-Key-Readiness-Smoke nach Apply erneut ausführen.",
                "acceptance": "api_access_storage_table_count=2 und failed_check_count=0; offene Produktiv-Gates bleiben als Blocker sichtbar.",
                "evidence_url": "https://saferpage.de/evidence/api-key-readiness-smoke.json",
                "boundary": "Smokes erzeugen keine produktiven API-Keys."
            },
            {
                "id": "activate_only_after_secret_signoff",
                "label": "Runtime-Gates erst nach Secret-Signoff aktivieren",
                "owner": "Security/Operator",
                "action": "API-Key-Pepper, Write-HMAC-Secret, Domain-Claim und Betreiberrolle privat abnehmen.",
                "acceptance": "Alle Produktiv-Gates sind mit privatem Signoff und öffentlicher No-Secret-Evidence belegt.",
                "evidence_url": "https://saferpage.de/betreiber/go-live-json",
                "boundary": "Storage allein erlaubt noch keine Key-Ausgabe."
            }
        ],
        "private_inputs_required": [
            {
                "id": "admin_dsn",
                "label": "Kurzlebiger Admin-DSN",
                "reason": "Nur die DB-Verantwortung kann Tabellen/Indizes/Trigger sicher anlegen.",
                "public_handling": "Nie öffentlich ausgeben."
            },
            {
                "id": "db_owner_approval",
                "label": "DB-Verantwortungsfreigabe",
                "reason": "DDL-Änderung braucht Verantwortlichen und Zeitfenster.",
                "public_handling": "Nur Status, Zeitpunktklasse und SQL-Hash öffentlich zeigen."
            },
            {
                "id": "backup_or_pause_owner",
                "label": "Backup-/Pause-Verantwortung",
                "reason": "Rollback oder Pause muss vor Apply feststehen.",
                "public_handling": "Keine Backup-Pfade oder internen Hostnamen exportieren."
            },
            {
                "id": "api_key_pepper_secret_ref",
                "label": "API-Key-Pepper-Secret-Referenz",
                "reason": "Hash-Verifikation braucht serverseitiges Secret.",
                "public_handling": "Nur Vorhandensein als Gate zeigen, nie Wert oder Secret-Manager-Pfad."
            },
            {
                "id": "write_hmac_secret_ref",
                "label": "Write-HMAC-Secret-Referenz",
                "reason": "Schreibende Aufrufe brauchen Signaturpruefung.",
                "public_handling": "Nur Fixture und Gate-Status exportieren."
            }
        ],
        "public_outputs_allowed": [
            {
                "id": "migration_sql_sha256",
                "label": "Migration-SHA-256",
                "reason": "DB-Verantwortung kann Datei vor Apply prüfen."
            },
            {
                "id": "table_readiness_counts",
                "label": "Tabellen-Readiness 0/2 oder 2/2",
                "reason": "Öffentlicher Fortschritt ohne DSN/Tabellendaten."
            },
            {
                "id": "redacted_db_user_hash",
                "label": "Redaktierter DB-User-Hash",
                "reason": "Preflight kann Rollenwechsel erkennen, ohne Usernamen zu nennen."
            },
            {
                "id": "command_names",
                "label": "Sichere Command-Namen",
                "reason": "Runbook bleibt reproduzierbar ohne private Parameter."
            },
            {
                "id": "evidence_urls",
                "label": "SaferPage-Evidence-URLs",
                "reason": "Abnahme ist verlinkbar und maschinenlesbar."
            },
            {
                "id": "smoke_status_counts",
                "label": "Smoke-Status und Zähler",
                "reason": "Produktreife ist prüfbar, ohne Secrets zu leaken."
            }
        ],
        "forbidden_outputs": [
            "Admin-DSN oder Passwort",
            "DB-Host, Port oder interner Username",
            "Roh-API-Key oder vollständiger Key-Prefix außer Testfixture",
            "Key-Hash, Pepper oder Secret-Manager-Pfad",
            "Authorization-Header oder Bearer-Token",
            "Write-HMAC-Secret oder echte Signatur für Live-Payloads",
            "Private Webhook-, Slack-, Teams- oder Kundenziel-URLs",
            "Besucherlogs, Request-Payloads oder personenbezogene Auditdetails"
        ],
        "validation_queries": [
            "select coalesce(to_regclass('public.api_keys')::text, 'missing');",
            "select coalesce(to_regclass('public.api_access_audit_log')::text, 'missing');",
            "select count(*) from information_schema.columns where table_schema = 'public' and table_name = 'api_keys';",
            "select count(*) from information_schema.columns where table_schema = 'public' and table_name = 'api_access_audit_log';",
            "select tgname from pg_trigger where tgrelid = 'public.api_keys'::regclass and not tgisinternal;"
        ],
        "safe_commands": [
            "scripts/run-api-access-migration-preflight.sh",
            "scripts/run-api-access-migration.sh",
            "scripts/run-api-runtime-deny-smoke.sh",
            "scripts/run-api-key-readiness-smoke.sh"
        ],
        "acceptance_criteria": [
            "DB-Verantwortung bestätigt SQL-Hash und Apply-Fenster privat.",
            "api_keys und api_access_audit_log existieren nach Apply.",
            "Public Evidence enthält keine DSN-, Passwort-, Key-, Hash-, Header- oder Ziel-URL-Werte.",
            "Deny-Smoke liefert 401/deny und schreibt nur sanitisiertes Audit.",
            "Produktive Key-Ausgabe bleibt blockiert, bis Pepper, HMAC, Domain-Claim und Betreiberrolle privat freigegeben sind.",
            "Go-live-Dossier nennt Rollback, Stop-Bedingungen und Nachlaufbeobachtung."
        ],
        "evidence_urls": [
            {
                "id": "preflight",
                "label": "Migration-Preflight",
                "url": "https://saferpage.de/evidence/api-access-migration-preflight.json"
            },
            {
                "id": "readiness",
                "label": "API-Key-Readiness",
                "url": "https://saferpage.de/api-zugriff/key-readiness-json"
            },
            {
                "id": "runtime_probe",
                "label": "Runtime-Gate-Probe",
                "url": "https://saferpage.de/api-zugriff/runtime-gate-probe-json"
            },
            {
                "id": "smoke",
                "label": "API-Key-Smoke",
                "url": "https://saferpage.de/evidence/api-key-readiness-smoke.json"
            },
            {
                "id": "migration_sql",
                "label": "Migration-SQL",
                "url": "https://saferpage.de/api-zugriff/access-migration.sql"
            },
            {
                "id": "operator_go_live",
                "label": "Operator-Go-live",
                "url": "https://saferpage.de/betreiber/go-live-json"
            }
        ]
    },
    "migration_package": {
        "schema_version": "api-access-2026-06-09",
        "requires_role": "PostgreSQL-Rolle mit Besitz- oder Adminrechten sowie CREATE TABLE, CREATE INDEX und CREATE TRIGGER im Schema public.",
        "does_not_require_secret_values": true,
        "sql_url": "https://saferpage.de/api-zugriff/access-migration.sql",
        "preflight_evidence_url": "https://saferpage.de/evidence/api-access-migration-preflight.json",
        "migration_required": false,
        "admin_dsn_required": false,
        "safe_next_action": "API-Access-Tabellen sind vorhanden; als nächstes Pepper, Domain-Claim, HMAC und Smoke-Tests produktiv freigeben.",
        "sql_sha256": "27d802d48887d084e9132c841b943681c70638bd432746a92cab7809be621213",
        "sql_bytes": 2211,
        "preflight_commands": [
            "scripts/run-api-access-migration-preflight.sh",
            "psql -d saferpage -Atq -c \"select current_database(), current_user;\"",
            "psql -d saferpage -Atq -c \"select coalesce(to_regclass('public.api_keys')::text,'missing'), coalesce(to_regclass('public.api_access_audit_log')::text,'missing');\""
        ],
        "migration_command": "SAFERPAGE_MIGRATION_DATABASE_URL='<admin-dsn-from-secure-shell>' scripts/run-api-access-migration.sh",
        "download_and_apply_command": "curl -fsS https://saferpage.de/api-zugriff/access-migration.sql -o /tmp/saferpage-api-access.sql && export SAFERPAGE_MIGRATION_DATABASE_URL='<admin-dsn-from-secure-shell>' && psql \"$SAFERPAGE_MIGRATION_DATABASE_URL\" -v ON_ERROR_STOP=1 -f /tmp/saferpage-api-access.sql",
        "smoke_test_commands": [
            "scripts/run-api-access-migration-preflight.sh",
            "curl -fsS https://saferpage.de/api-zugriff/key-readiness-json | python3 -m json.tool",
            "scripts/run-api-runtime-deny-smoke.sh",
            "scripts/run-api-service-smoke.sh"
        ],
        "activation_order": [
            "DDL-Migration für api_keys und api_access_audit_log anwenden.",
            "API-Readiness erneut prüfen: api_access_storage_table_count muss 2 zeigen.",
            "API-Key-Pepper und Write-HMAC-Secret nur im Server-Environment oder Secret Manager setzen.",
            "Domain-Claim und Betreiberrolle verifizieren.",
            "Test-Key nur einmal anzeigen, danach Deny-/Allow-/Revocation-Smokes ausführen.",
            "Produktive Gates erst nach erfolgreicher Smoke-Evidence aktivieren."
        ],
        "rollback_or_pause": [
            "Sofortige Pause ohne DDL-Rollback: alle SAFERPAGE_API_*_READY Freigaben entfernen und API-Service neu starten.",
            "Keys bei Verdacht auf Fehlkonfiguration auf status=revoked setzen statt Rohdaten zu exportieren.",
            "DDL-Drop nur nach Backup-, Audit- und Retention-Freigabe ausführen."
        ],
        "acceptance_criteria": [
            "api_keys und api_access_audit_log existieren.",
            "api_keys enthält nur Prefix/Hash/Scopes/Status und keinen Roh-Key.",
            "api_access_audit_log schreibt sanitisierte Deny-/Allow-Events ohne Authorization-Header.",
            "Runtime-Gate-Probe bestätigt 401/403/429/Revocation-Fixtures.",
            "Write-HMAC-Test-Fixture ist gegen Client/Receiver verifiziert."
        ],
        "signoff_pack": [
            {
                "id": "ddl_hash_pinned",
                "label": "DDL-Hash fixiert",
                "status": "ready",
                "evidence": "27d802d48887d084e9132c841b943681c70638bd432746a92cab7809be621213",
                "operator_action": "Hash vor Apply mit Public-Export und lokaler Datei vergleichen."
            },
            {
                "id": "preflight_before_after",
                "label": "Preflight vor/nach Migration",
                "status": "ready",
                "evidence": "https://saferpage.de/evidence/api-access-migration-preflight.json",
                "operator_action": "Preflight unmittelbar vor und nach Apply ausführen und Ergebnis publizieren."
            },
            {
                "id": "short_lived_admin_dsn",
                "label": "Kurzlebiger Admin-DSN",
                "status": "not_required_after_apply",
                "evidence": "DSN wird nie in Public-State, Repo, Logs oder Smoke-Ergebnis ausgegeben.",
                "operator_action": "DSN nur in sicherer Shell setzen und nach Apply entfernen."
            },
            {
                "id": "owner_approval",
                "label": "DB-Verantwortungsfreigabe",
                "status": "ready",
                "evidence": "Freigabe muss verantwortliche DB-Rolle, Zeitpunkt, SQL-Hash und Rollback/Pause-Entscheidung nennen.",
                "operator_action": "Signoff außerhalb des Public-Exports dokumentieren; öffentlich nur Status/Hash zeigen."
            },
            {
                "id": "service_restart_plan",
                "label": "API-Service Restart geplant",
                "status": "ready",
                "evidence": "Service-Restart erst nach erfolgreichem Preflight und Secret-Referenzen.",
                "operator_action": "Restart-Fenster, Fallback und erneute Smokes festlegen."
            }
        ],
        "rollback_rehearsal": [
            {
                "step": 1,
                "label": "Key-Ausgabe pausieren",
                "action": "SAFERPAGE_API_*_READY Freigaben entfernen und API-Service neu starten.",
                "evidence": "Protected Routen bleiben denied."
            },
            {
                "step": 2,
                "label": "Keine Rohdaten exportieren",
                "action": "Bei Fehlern nur Prefix, Request-ID, Scope, Entscheidung und Hash-Evidence nutzen.",
                "evidence": "No-Secret-Policy im Smoke bleibt grün."
            },
            {
                "step": 3,
                "label": "Test-Keys widerrufen",
                "action": "status=revoked setzen, bevor echte Betreiberzugriffe wieder erlaubt werden.",
                "evidence": "Revocation-Smoke liefert Deny."
            },
            {
                "step": 4,
                "label": "DDL nur nach Backup droppen",
                "action": "Tabellen-Drop nur mit Backup-, Audit- und Retention-Freigabe ausführen.",
                "evidence": "DB-Verantwortungsentscheidung außerhalb Public-Export dokumentiert."
            }
        ],
        "post_migration_canary_sequence": [
            {
                "step": 1,
                "label": "Preflight nach Apply",
                "command": "scripts/run-api-access-migration-preflight.sh",
                "expected": "api_access_storage_table_count=2 und missing_required_artifact_count=0."
            },
            {
                "step": 2,
                "label": "Readiness JSON prüfen",
                "command": "curl -fsS https://saferpage.de/api-zugriff/key-readiness-json",
                "expected": "Storage grün, Produktiv-Gates nur bei echten Env-Freigaben grün."
            },
            {
                "step": 3,
                "label": "Deny-Smoke ohne Key",
                "command": "scripts/run-api-runtime-deny-smoke.sh",
                "expected": "401 deny und sanitisiertes Audit."
            },
            {
                "step": 4,
                "label": "Runtime-Gate-Probe",
                "command": "curl -fsS https://saferpage.de/api-zugriff/runtime-gate-probe-json",
                "expected": "Public/Protected/HMAC/Revocation-Vertrag vollständig."
            },
            {
                "step": 5,
                "label": "No-Secret-Smoke",
                "command": "SAFERPAGE_BASE_URL=https://saferpage.de scripts/run-api-key-readiness-smoke.sh",
                "expected": "failed_check_count=0; blocked_expected nur für bewusst offene Gates."
            },
            {
                "step": 6,
                "label": "Public Evidence erneut deployen",
                "command": "./scripts/install-system-nginx.sh",
                "expected": "Öffentliche Evidence zeigt neue Smoke-Zeit und keine Secrets."
            }
        ]
    },
    "operator_sequence": [
        "Migration-Preflight ausführen: scripts/run-api-access-migration-preflight.sh.",
        "Wenn api_keys/api_access_audit_log fehlen, Migration mit kurzlebigem Admin-DSN aus sicherer Shell anwenden.",
        "Domain-Claim und Operator-Rolle verifizieren.",
        "Key nur einmal anzeigen, Prefix und Hash speichern, Ablaufdatum maximal 90 Tage setzen.",
        "Scopes minimal vergeben und Endpoint-Matrix gegen reale Rolle prüfen.",
        "Read-/Write-/Admin-Limits getrennt aktivieren.",
        "Schreibende Aufrufe nur mit HMAC, Idempotency-Key und Audit-Event erlauben.",
        "Rotation, Revocation und Notfall-Sperrung mit Test-Key dokumentieren."
    ],
    "example_redacted_key_record": {
        "key_id": "00000000-0000-0000-0000-000000000000",
        "key_prefix": "sp_live_ab12cd34",
        "key_hash": "__FORBIDDEN_IN_EXPORT__",
        "scopes": [
            "reports:read",
            "portfolio:read"
        ],
        "status": "active",
        "expires_at": "2026-11-04T14:15:20+00:00",
        "raw_key": "__DISPLAY_ONCE_ONLY_NEVER_EXPORT__"
    },
    "links": {
        "html": "https://saferpage.de/api-zugriff/key-readiness",
        "json": "https://saferpage.de/api-zugriff/key-readiness-json",
        "csv": "https://saferpage.de/api-zugriff/key-readiness-csv",
        "markdown": "https://saferpage.de/api-zugriff/key-readiness-md",
        "api_access": "https://saferpage.de/api-zugriff/export",
        "runtime_gate_probe": "https://saferpage.de/api-zugriff/runtime-gate-probe-json",
        "migration_sql": "https://saferpage.de/api-zugriff/access-migration.sql",
        "latest_smoke_result": "https://saferpage.de/evidence/api-key-readiness-smoke.json",
        "trust_api": "https://saferpage.de/trust/anrufer.info/api-json",
        "domain_verification": "https://saferpage.de/betreiber/anrufer.info/verifizierung-json",
        "integration_setup": "https://saferpage.de/integrationen/setup-json",
        "schema": "https://saferpage.de/schemas/operator-api-key-readiness.v2"
    },
    "disclaimer": "Dieses Dossier erzeugt keine echten API-Keys, speichert keine Hashes und gibt keine Secret-Werte aus. Produktive Key-Ausstellung braucht Betreiber-Auth, Domain-Claim und Server-Gates."
}
