Module: Legion::Data::AuditRecord
- Extended by:
- Logging::Helper
- Defined in:
- lib/legion/data/audit_record.rb
Constant Summary collapse
- GENESIS_HASH =
('0' * 64).freeze
Class Method Summary collapse
-
.append(chain_id:, content_type:, content_hash:, metadata: {}, sign: false) ⇒ Object
Append a new record to the named chain.
-
.compute_chain_hash(parent_hash, content_hash, timestamp, content_type) ⇒ Object
SHA-256 of “parent_hash:content_hash:unix_ts_ns:content_type”.
-
.query_by_type(content_type:, since: nil, limit: 100) ⇒ Object
Return records filtered by content_type across all chains.
-
.verify(chain_id:) ⇒ Hash
Walk all records in the chain ordered by creation time and verify that each record’s stored chain_hash matches a freshly computed one.
-
.walk(chain_id:, since: nil, limit: 1000) ⇒ Object
Return all records for a chain as deserialised hashes.
Methods included from Logging::Helper
Class Method Details
.append(chain_id:, content_type:, content_hash:, metadata: {}, sign: false) ⇒ Object
Append a new record to the named chain. Returns the persisted record hash on success, or an error hash when the database is unavailable.
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/legion/data/audit_record.rb', line 22 def append(chain_id:, content_type:, content_hash:, metadata: {}, sign: false) return { error: 'db unavailable' } unless db_ready? conn = Legion::Data.connection conn.transaction do parent_hash = latest_chain_hash(conn, chain_id) ts = Time.now ch = compute_chain_hash(parent_hash, content_hash, ts, content_type) sig = sign ? sign_record(ch) : nil = .empty? ? nil : Legion::JSON.dump() id = conn[:audit_records].insert( chain_id: chain_id, content_type: content_type, content_hash: content_hash, parent_hash: parent_hash, chain_hash: ch, signature: sig, metadata: , created_at: ts ) log.debug "AuditRecord append: chain=#{chain_id} type=#{content_type} id=#{id}" { id: id, chain_id: chain_id, chain_hash: ch, parent_hash: parent_hash } end end |
.compute_chain_hash(parent_hash, content_hash, timestamp, content_type) ⇒ Object
SHA-256 of “parent_hash:content_hash:unix_ts_ns:content_type”.
The timestamp is normalised to nanoseconds-since-epoch so the hash is independent of time zone, string formatting, and database type. Exposed as a public method so callers can independently verify a hash without querying the database.
113 114 115 116 |
# File 'lib/legion/data/audit_record.rb', line 113 def compute_chain_hash(parent_hash, content_hash, , content_type) ts_ns = () Digest::SHA256.hexdigest("#{parent_hash}:#{content_hash}:#{ts_ns}:#{content_type}") end |
.query_by_type(content_type:, since: nil, limit: 100) ⇒ Object
Return records filtered by content_type across all chains.
99 100 101 102 103 104 105 |
# File 'lib/legion/data/audit_record.rb', line 99 def query_by_type(content_type:, since: nil, limit: 100) return [] unless db_ready? ds = Legion::Data.connection[:audit_records].where(content_type: content_type) ds = ds.where { created_at >= since } if since ds.order(Sequel.desc(:created_at)).limit(limit).all.map { |r| deserialize(r) } end |
.verify(chain_id:) ⇒ Hash
Walk all records in the chain ordered by creation time and verify that each record’s stored chain_hash matches a freshly computed one.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/legion/data/audit_record.rb', line 54 def verify(chain_id:) return { valid: false, error: 'db unavailable' } unless db_ready? records = Legion::Data.connection[:audit_records] .where(chain_id: chain_id) .order(:created_at, :id) .all prev_hash = GENESIS_HASH records.each do |r| unless r[:parent_hash] == prev_hash log.warn "AuditRecord chain broken: chain=#{chain_id} id=#{r[:id]}" return { valid: false, broken_at: r[:id], reason: :parent_mismatch } end expected = compute_chain_hash(prev_hash, r[:content_hash], r[:created_at], r[:content_type]) unless r[:chain_hash] == expected log.warn "AuditRecord hash mismatch: chain=#{chain_id} id=#{r[:id]}" return { valid: false, broken_at: r[:id], reason: :hash_mismatch } end prev_hash = r[:chain_hash] end { valid: true, length: records.size } end |
.walk(chain_id:, since: nil, limit: 1000) ⇒ Object
Return all records for a chain as deserialised hashes.
86 87 88 89 90 91 92 |
# File 'lib/legion/data/audit_record.rb', line 86 def walk(chain_id:, since: nil, limit: 1000) return [] unless db_ready? ds = Legion::Data.connection[:audit_records].where(chain_id: chain_id) ds = ds.where { created_at >= since } if since ds.order(:created_at, :id).limit(limit).all.map { |r| deserialize(r) } end |