Class: Invoance::Resources::Attestations

Inherits:
Object
  • Object
show all
Defined in:
lib/invoance/resources/attestations.rb

Overview

AI Attestations resource — client.attestations.*

Instance Method Summary collapse

Constructor Details

#initialize(http) ⇒ Attestations

Returns a new instance of Attestations.

Parameters:



14
15
16
# File 'lib/invoance/resources/attestations.rb', line 14

def initialize(http)
  @http = http
end

Instance Method Details

#get(attestation_id) ⇒ Hash

GET /ai/attestations/:id — Retrieve a single attestation.

Returns:

  • (Hash)

    AiAttestation



78
79
80
# File 'lib/invoance/resources/attestations.rb', line 78

def get(attestation_id)
  @http.get("/ai/attestations/#{attestation_id}")
end

#get_raw(attestation_id) ⇒ Hash

GET /ai/attestations/:id/raw — Retrieve the canonical JSON payload.

Returns:

  • (Hash)


92
93
94
# File 'lib/invoance/resources/attestations.rb', line 92

def get_raw(attestation_id)
  @http.get_raw("/ai/attestations/#{attestation_id}/raw")
end

#ingest(type:, input:, output:, model_provider: nil, model_name: nil, model_version: nil, subject: nil, trace_id: nil, idempotency_key: nil) ⇒ Hash

POST /ai/attestations — Anchor an AI attestation.

The body is NESTED and field order matters for the server's hashing (type, payload, context, subject) — do not reorder.

Parameters:

  • type (String)
  • input (Object)
  • output (Object)
  • model_provider (String, nil) (defaults to: nil)
  • model_name (String, nil) (defaults to: nil)
  • model_version (String, nil) (defaults to: nil)
  • subject (Hash, nil) (defaults to: nil)

    user_id/session_id + arbitrary extra keys

  • trace_id (String, nil) (defaults to: nil)
  • idempotency_key (String, nil) (defaults to: nil)

Returns:

  • (Hash)

    IngestAttestationResponse



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/invoance/resources/attestations.rb', line 33

def ingest(type:, input:, output:, model_provider: nil, model_name: nil,
           model_version: nil, subject: nil, trace_id: nil, idempotency_key: nil)
  body = {
    "type" => type,
    "payload" => { "input" => input, "output" => output },
    "context" => {
      "model_provider" => model_provider,
      "model_name" => model_name,
      "model_version" => model_version
    }
  }

  unless subject.nil?
    s = symbolize_subject(subject)
    user_id = s.delete(:user_id)
    session_id = s.delete(:session_id)
    subj = {}
    subj["user_id"] = user_id unless user_id.nil?
    subj["session_id"] = session_id unless session_id.nil?
    # Pass through any remaining extra keys, preserving their string form.
    s.each { |k, v| subj[k.to_s] = v }
    body["subject"] = subj unless subj.empty?
  end

  body["trace_id"] = trace_id unless trace_id.nil?

  @http.post("/ai/attestations", body, idempotency_key)
end

#list(page: nil, limit: nil, date_from: nil, date_to: nil, attestation_type: nil, model_provider: nil) ⇒ Hash

GET /ai/attestations — Paginated attestation listing.

Returns:

  • (Hash)

    ListAttestationsResponse



64
65
66
67
68
69
70
71
72
73
74
# File 'lib/invoance/resources/attestations.rb', line 64

def list(page: nil, limit: nil, date_from: nil, date_to: nil,
         attestation_type: nil, model_provider: nil)
  @http.get("/ai/attestations", {
              "page" => page,
              "limit" => limit,
              "date_from" => date_from,
              "date_to" => date_to,
              "attestation_type" => attestation_type,
              "model_provider" => model_provider
            })
end

#verify(attestation_id, content_hash:) ⇒ Hash

POST /ai/attestations/:id/verify — Hash verification.

Returns:

  • (Hash)

    VerifyAttestationResponse



84
85
86
87
88
# File 'lib/invoance/resources/attestations.rb', line 84

def verify(attestation_id, content_hash:)
  Validate.assert_sha256_hex("content_hash", content_hash)
  @http.post("/ai/attestations/#{attestation_id}/verify",
             { "content_hash" => content_hash })
end

#verify_payload(attestation_id, payload) ⇒ Hash

Verify by raw payload — hashes client-side, then calls verify.

Accepts the canonical JSON stored in Invoance as a raw String or a Hash. Key ORDER is PRESERVED (not sorted) because the backend hashes using serde_json struct field order (type, payload, context, subject).

  • String input: JSON.parse preserves object key order into an insertion-ordered Hash, and JSON.generate preserves it — giving compact order-preserving JSON (= JS JSON.stringify(JSON.parse(s))).
  • Hash input: JSON.generate preserves insertion order — pass the keys already in the correct order.

Parameters:

  • attestation_id (String)
  • payload (String, Hash)

Returns:

  • (Hash)

    VerifyAttestationResponse



111
112
113
114
115
116
117
118
119
120
121
# File 'lib/invoance/resources/attestations.rb', line 111

def verify_payload(attestation_id, payload)
  canonical =
    if payload.is_a?(String)
      JSON.generate(JSON.parse(payload))
    else
      JSON.generate(payload)
    end

  content_hash = Digest::SHA256.hexdigest(canonical)
  verify(attestation_id, content_hash: content_hash)
end

#verify_signature(attestation_id) ⇒ Hash

Verify the Ed25519 signature of an attestation — fully client-side.

Fetches the attestation, then verifies the signature against signed_payload using public_key. Requires no trust in the server.

Parameters:

  • attestation_id (String)

Returns:

  • (Hash)

    { "valid" =>, "reason" =>, "attestation" =>, "signed_data" => }



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/invoance/resources/attestations.rb', line 130

def verify_signature(attestation_id)
  att = get(attestation_id)

  signed_payload_bytes = hex_to_bytes(att["signed_payload"].to_s)
  signature_bytes = hex_to_bytes(att["signature"].to_s)
  public_key_bytes = hex_to_bytes(att["public_key"].to_s)

  valid = false
  reason = nil
  begin
    valid = AuditVerify.ed25519_verify(signed_payload_bytes, signature_bytes, public_key_bytes)
    reason = "Signature does not match signed_payload + public_key" unless valid
  rescue StandardError => e
    valid = false
    reason = e.message
  end

  signed_data =
    begin
      JSON.parse(signed_payload_bytes.dup.force_encoding(Encoding::UTF_8))
    rescue JSON::ParserError
      nil
    end

  { "valid" => valid, "reason" => reason, "attestation" => att, "signed_data" => signed_data }
end