feat(P3.4): portable receipts, attestedFetch, edge-runtime lane, receipt PDF
Portable receipt export (*.attesto.json): export_receipt_file /
verify_receipt_file in Python, exportReceiptFile / verifyReceiptFile in
TypeScript, ExportReceiptFile / VerifyReceiptExport in Go, plus
`attesto verify file` in the CLI. New normative corpus
golden-vectors/sdk-parity/receipt-export.json (valid, tampered-inner,
linkage-mismatch, wrong-format, embedded-hint-only) passes identically in
all three SDKs; a Python-made export verifies through the Go CLI
end-to-end. Embedded witness keys are explicit second-class hints
(kind=receipt-export-selfcontained).
attestedFetch (TS) attests AI calls at the transport exactly like the
gateway: OpenAI-compatible paths -> attesto.model_decision with
commitments only (SSE reassembled after byte-for-byte pass-through),
anything else -> http_call; fail-open by default with onError, strict
rejects; attest() wraps any function with a commitment event +
lastReceipt. 5 emulator tests prove raw prompt/completion text never
appears in any stored object.
Edge runtimes: new guard test fails the build if any node: builtin enters
the dist/index.js module graph (FileHeadStore stays out by design), and
the receipt+export corpora now run on Bun in CI (10 cases green locally).
render_receipt_pdf ships behind the attesto[receipt-pdf] extra (fpdf2 +
qrcode, pure Python; core stays light) — one-page rendering with a QR of
{receipt_hash, event_hash} and a disclaimer that the JSON, not the PDF,
is the evidence; clean ImportError naming the extra when absent.
Also fixed a stale CI assertion: the npm package-install smoke pinned
SDK_VERSION 0.1.1; it now reads the version from package.json.
Suites: Python 106 passed, TypeScript 67+5 passed, Go green, package
policy contract green. Connectorkit already exists in all three languages
(no port needed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
161
export.go
Normal file
161
export.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package attesto
|
||||
|
||||
// [P3.4] Portable receipt export — a self-contained `*.attesto.json`.
|
||||
// Mirrors attesto.export (Python) and export.ts (TypeScript); the
|
||||
// receipt-export.json parity corpus is normative for all three.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ExportFormat = "attesto-receipt-export"
|
||||
ExportFormatVersion = 1
|
||||
)
|
||||
|
||||
// ReceiptExport is the portable envelope. Receipt is kept as raw JSON so the
|
||||
// export carries the receipt verbatim, exactly as the API returned it.
|
||||
type ReceiptExport struct {
|
||||
Format string `json:"format"`
|
||||
FormatVersion int `json:"format_version"`
|
||||
ExportedAt string `json:"exported_at"`
|
||||
StreamID any `json:"stream_id,omitempty"`
|
||||
SeqNo any `json:"seq_no,omitempty"`
|
||||
EventHash any `json:"event_hash,omitempty"`
|
||||
WitnessPublicKeyHex string `json:"witness_public_key_hex,omitempty"`
|
||||
Receipt json.RawMessage `json:"receipt"`
|
||||
Payload M `json:"payload,omitempty"`
|
||||
PayloadCommitment M `json:"payload_commitment,omitempty"`
|
||||
}
|
||||
|
||||
func exportPick(source M, keys ...string) any {
|
||||
for _, key := range keys {
|
||||
if value, ok := source[key]; ok {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExportReceiptFile builds a portable export from a receipt's raw JSON (as
|
||||
// returned by the API) and optionally writes it to path (empty = no write).
|
||||
func ExportReceiptFile(receiptJSON []byte, path string, witnessPublicKeyHex string) (ReceiptExport, error) {
|
||||
var parsed struct {
|
||||
Payload M `json:"payload"`
|
||||
}
|
||||
if err := json.Unmarshal(receiptJSON, &parsed); err != nil {
|
||||
return ReceiptExport{}, fmt.Errorf("receipt is not valid JSON: %w", err)
|
||||
}
|
||||
export := ReceiptExport{
|
||||
Format: ExportFormat,
|
||||
FormatVersion: ExportFormatVersion,
|
||||
ExportedAt: time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
|
||||
StreamID: exportPick(parsed.Payload, "stream_id", "streamId"),
|
||||
SeqNo: exportPick(parsed.Payload, "seq_no", "seqNo"),
|
||||
EventHash: exportPick(parsed.Payload, "event_hash", "eventHash"),
|
||||
WitnessPublicKeyHex: witnessPublicKeyHex,
|
||||
Receipt: json.RawMessage(receiptJSON),
|
||||
}
|
||||
if path != "" {
|
||||
raw, err := json.MarshalIndent(export, "", " ")
|
||||
if err != nil {
|
||||
return ReceiptExport{}, err
|
||||
}
|
||||
if err := os.WriteFile(path, append(raw, '\n'), 0o644); err != nil {
|
||||
return ReceiptExport{}, err
|
||||
}
|
||||
}
|
||||
return export, nil
|
||||
}
|
||||
|
||||
// VerifyReceiptExport verifies a portable export offline. publicKeyHex == ""
|
||||
// falls back to the embedded hint (self-contained mode — proves internal
|
||||
// consistency against the key the file itself names).
|
||||
func VerifyReceiptExport(exportJSON []byte, publicKeyHex string) VerifyReport {
|
||||
var export struct {
|
||||
Format string `json:"format"`
|
||||
FormatVersion int `json:"format_version"`
|
||||
StreamID any `json:"stream_id"`
|
||||
SeqNo any `json:"seq_no"`
|
||||
EventHash any `json:"event_hash"`
|
||||
WitnessPublicKeyHex string `json:"witness_public_key_hex"`
|
||||
Receipt json.RawMessage `json:"receipt"`
|
||||
Payload M `json:"payload"`
|
||||
PayloadCommitment M `json:"payload_commitment"`
|
||||
}
|
||||
kind := VerifyKind("receipt-export")
|
||||
if publicKeyHex == "" {
|
||||
kind = "receipt-export-selfcontained"
|
||||
}
|
||||
fail := func(problems ...string) VerifyReport {
|
||||
return VerifyReport{Kind: kind, OK: false, Problems: problems}
|
||||
}
|
||||
if err := json.Unmarshal(exportJSON, &export); err != nil {
|
||||
return fail("export is not valid JSON: " + err.Error())
|
||||
}
|
||||
var problems []string
|
||||
if export.Format != ExportFormat {
|
||||
problems = append(problems, "not an attesto receipt export (format field)")
|
||||
}
|
||||
if export.FormatVersion != ExportFormatVersion {
|
||||
problems = append(problems, fmt.Sprintf("unsupported export format_version: %d", export.FormatVersion))
|
||||
}
|
||||
if len(export.Receipt) == 0 {
|
||||
problems = append(problems, "export carries no receipt object")
|
||||
}
|
||||
if len(problems) > 0 {
|
||||
return fail(problems...)
|
||||
}
|
||||
|
||||
key := publicKeyHex
|
||||
if key == "" {
|
||||
key = export.WitnessPublicKeyHex
|
||||
}
|
||||
if key == "" {
|
||||
return fail("no public key supplied and no embedded hint")
|
||||
}
|
||||
var receipt SignedReceipt
|
||||
if err := json.Unmarshal(export.Receipt, &receipt); err != nil {
|
||||
return fail("receipt is not valid JSON: " + err.Error())
|
||||
}
|
||||
report := VerifyReceiptOffline(receipt, key)
|
||||
problems = append(problems, report.Problems...)
|
||||
|
||||
linkage := []struct {
|
||||
name string
|
||||
outer any
|
||||
keys []string
|
||||
}{
|
||||
{"stream_id", export.StreamID, []string{"stream_id", "streamId"}},
|
||||
{"seq_no", export.SeqNo, []string{"seq_no", "seqNo"}},
|
||||
{"event_hash", export.EventHash, []string{"event_hash", "eventHash"}},
|
||||
}
|
||||
for _, link := range linkage {
|
||||
inner := exportPick(receipt.Payload, link.keys...)
|
||||
if link.outer == nil || inner == nil {
|
||||
continue
|
||||
}
|
||||
// JSON numbers decode as float64 on both sides, so == is sound here.
|
||||
if fmt.Sprint(link.outer) != fmt.Sprint(inner) {
|
||||
problems = append(problems, "export linkage mismatch: "+link.name)
|
||||
}
|
||||
}
|
||||
|
||||
if export.Payload != nil && export.PayloadCommitment != nil {
|
||||
ok, err := VerifyPayloadCommitment(export.Payload, M{"payload_commitment": export.PayloadCommitment})
|
||||
if err != nil || !ok {
|
||||
problems = append(problems, "embedded payload does not match payload_commitment")
|
||||
}
|
||||
}
|
||||
|
||||
return VerifyReport{
|
||||
Kind: kind,
|
||||
OK: len(problems) == 0,
|
||||
ReceiptHash: report.ReceiptHash,
|
||||
EventHash: report.EventHash,
|
||||
Problems: problems,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user