Add SDK parity and Go CLI release readiness

This commit is contained in:
Codex
2026-06-07 22:35:23 +02:00
commit 61f3a217e6
11 changed files with 2797 additions and 0 deletions

81
README.md Normal file
View File

@@ -0,0 +1,81 @@
# Attesto Go SDK
Official Go SDK for Attesto 2.0 Proofstream. The default API base URL is
`https://verify.attesto.eu`. Use it from server-side, infrastructure, security
tooling, CI, evidence exporters, and operator automation. Do not embed Attesto API keys in browser bundles, mobile apps, or public artifacts.
## Install
```shell
go get git.rotz.ai/rotzmediagroup/attesto-v1/sdk/go
```
The first release is VCS-resolved from the Attesto repository. It intentionally
uses only the Go standard library.
## Quickstart
```go
package main
import (
"context"
"fmt"
"log"
"os"
attesto "git.rotz.ai/rotzmediagroup/attesto-v1/sdk/go"
)
func main() {
ctx := context.Background()
client, err := attesto.NewClient(os.Getenv("ATTESTO_API_KEY"))
if err != nil {
log.Fatal(err)
}
stream, err := client.CreateStream(ctx, attesto.StreamCreateInput{
UseCase: "ai-governance",
PolicyID: "policy-main",
})
if err != nil {
log.Fatal(err)
}
receipt, err := client.LogEvent(ctx, stream.StreamID, attesto.EventInput{
SourceRef: "decision-42",
Payload: attesto.M{
"model": "risk-classifier",
"score": 0.92,
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(receipt.StreamEventID, receipt.EventHash)
}
```
## Verification
Remote verification uses Attesto's public `/v2/verify` API. Offline receipt
verification uses `ATTESTO-PROOFSTREAM-001` canonical JSON, domain-separated
hashes, and Ed25519 signature verification locally.
```go
report := attesto.VerifyReceiptOffline(receipt.Receipt, publicKeyHex)
if !report.OK {
log.Fatalf("receipt failed verification: %v", report.Problems)
}
```
## Operator and Admin Endpoints
System-key clients are created with `attesto.NewClient`. Tenant/operator
endpoints, including connector installation and Local Vault installation
management, use `attesto.NewBearerClient` with a tenant bearer token obtained
from the dashboard session flow.
Secrets returned once by connector creation are present only in the returned
struct and are never logged by the SDK.

561
client.go Normal file
View File

@@ -0,0 +1,561 @@
package attesto
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"
)
type Client struct {
baseURL string
bearer string
httpClient *http.Client
maxRetries int
userAgent string
validateKey bool
}
type Option func(*Client) error
var apiKeyPattern = regexp.MustCompile(`^atto_(?:live|test)_[0-9a-f]{32}$`)
func NewClient(apiKey string, opts ...Option) (*Client, error) {
if !apiKeyPattern.MatchString(apiKey) {
return nil, errors.New("api key must match atto_live_<32 lowercase hex chars> or atto_test_<32 lowercase hex chars>")
}
return newClient(apiKey, true, opts...)
}
func NewBearerClient(token string, opts ...Option) (*Client, error) {
if strings.TrimSpace(token) == "" {
return nil, errors.New("bearer token must not be blank")
}
return newClient(token, false, opts...)
}
func newClient(bearer string, validateKey bool, opts ...Option) (*Client, error) {
client := &Client{
baseURL: DefaultBaseURL,
bearer: bearer,
httpClient: &http.Client{Timeout: 10 * time.Second},
maxRetries: 3,
userAgent: "attesto-go/" + SDKVersion,
validateKey: validateKey,
}
for _, opt := range opts {
if err := opt(client); err != nil {
return nil, err
}
}
if _, err := url.ParseRequestURI(client.baseURL); err != nil {
return nil, fmt.Errorf("base url must be an absolute URL: %w", err)
}
client.baseURL = strings.TrimRight(client.baseURL, "/")
return client, nil
}
func WithBaseURL(baseURL string) Option {
return func(c *Client) error {
baseURL = strings.TrimSpace(strings.TrimRight(baseURL, "/"))
if baseURL == "" {
return errors.New("base url must not be blank")
}
parsed, err := url.Parse(baseURL)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return errors.New("base url must be an absolute URL")
}
if parsed.Scheme != "https" && parsed.Scheme != "http" {
return errors.New("base url must use http or https")
}
c.baseURL = baseURL
return nil
}
}
func WithHTTPClient(httpClient *http.Client) Option {
return func(c *Client) error {
if httpClient == nil {
return errors.New("http client must not be nil")
}
c.httpClient = httpClient
return nil
}
}
func WithMaxRetries(maxRetries int) Option {
return func(c *Client) error {
if maxRetries < 1 {
return errors.New("max retries must be at least 1")
}
c.maxRetries = maxRetries
return nil
}
}
func WithUserAgent(userAgent string) Option {
return func(c *Client) error {
if strings.TrimSpace(userAgent) == "" {
return errors.New("user agent must not be blank")
}
c.userAgent = userAgent
return nil
}
}
func (c *Client) CreateStream(ctx context.Context, input StreamCreateInput, options ...RequestOptions) (*Stream, error) {
var out Stream
if input.Metadata == nil {
input.Metadata = M{}
}
err := c.requestJSON(ctx, http.MethodPost, "/v2/streams", nil, input, idempotency(options), &out)
return &out, err
}
func (c *Client) GetStreamHead(ctx context.Context, streamID string) (*StreamHead, error) {
var out StreamHead
err := c.requestJSON(ctx, http.MethodGet, "/v2/streams/"+url.PathEscape(streamID)+"/head", nil, nil, "", &out)
return &out, err
}
func (c *Client) ListTenantStreams(ctx context.Context, systemID string, limit, offset int) ([]TenantStream, error) {
values := url.Values{}
if systemID != "" {
values.Set("systemId", systemID)
}
setPaging(values, limit, offset)
var out []TenantStream
err := c.requestJSON(ctx, http.MethodGet, "/v2/tenant/streams", values, nil, "", &out)
return out, err
}
func (c *Client) GetTenantStream(ctx context.Context, streamID string) (*TenantStream, error) {
offset := 0
for {
streams, err := c.ListTenantStreams(ctx, "", 500, offset)
if err != nil {
return nil, err
}
for _, stream := range streams {
if stream.StreamID == streamID {
return &stream, nil
}
}
if len(streams) < 500 {
return nil, fmt.Errorf("stream not found: %s", streamID)
}
offset += 500
}
}
func (c *Client) LogEvent(ctx context.Context, streamID string, input EventInput, options ...RequestOptions) (*EventReceipt, error) {
if input.EventType == "" {
input.EventType = "inference"
}
if input.SourceKind == "" {
input.SourceKind = "sdk"
}
if input.Payload == nil {
input.Payload = M{}
}
if input.Metadata == nil {
input.Metadata = M{}
}
var out EventReceipt
err := c.requestJSON(ctx, http.MethodPost, "/v2/streams/"+url.PathEscape(streamID)+"/events", nil, input, idempotency(options), &out)
return &out, err
}
func (c *Client) LogEvents(ctx context.Context, streamID string, events []EventInput, options ...RequestOptions) (*EventBatchResponse, error) {
if len(events) > 1000 {
return nil, errors.New("max 1000 events per batch")
}
for i := range events {
if events[i].EventType == "" {
events[i].EventType = "inference"
}
if events[i].SourceKind == "" {
events[i].SourceKind = "sdk"
}
if events[i].Payload == nil {
events[i].Payload = M{}
}
if events[i].Metadata == nil {
events[i].Metadata = M{}
}
}
body := M{"events": events}
var out EventBatchResponse
err := c.requestJSON(ctx, http.MethodPost, "/v2/streams/"+url.PathEscape(streamID)+"/events/batch", nil, body, idempotency(options), &out)
return &out, err
}
func (c *Client) GetReceipt(ctx context.Context, streamEventID string) (*EventReceipt, error) {
var out EventReceipt
err := c.requestJSON(ctx, http.MethodGet, "/v2/receipts/"+url.PathEscape(streamEventID), nil, nil, "", &out)
return &out, err
}
func (c *Client) GetWindow(ctx context.Context, windowID string) (M, error) {
return c.getObject(ctx, "/v2/windows/"+url.PathEscape(windowID), nil)
}
func (c *Client) GetCheckpoint(ctx context.Context, checkpointID string) (M, error) {
return c.getObject(ctx, "/v2/checkpoints/"+url.PathEscape(checkpointID), nil)
}
func (c *Client) GetCheckpointConsistency(ctx context.Context, checkpointID, fromCheckpointID string) (M, error) {
values := url.Values{"from": []string{fromCheckpointID}}
return c.getObject(ctx, "/v2/checkpoints/"+url.PathEscape(checkpointID)+"/consistency", values)
}
func (c *Client) GetWitnessPolicy(ctx context.Context, policyID string) (*WitnessPolicy, error) {
var out WitnessPolicy
err := c.requestJSON(ctx, http.MethodGet, "/v2/witness/policies/"+url.PathEscape(policyID), nil, nil, "", &out)
return &out, err
}
func (c *Client) GetAnchorEpoch(ctx context.Context, anchorEpochID string) (M, error) {
return c.getObject(ctx, "/v2/anchors/"+url.PathEscape(anchorEpochID), nil)
}
func (c *Client) GetIVCEpoch(ctx context.Context, ivcEpochID string) (M, error) {
return c.getObject(ctx, "/v2/ivc/epochs/"+url.PathEscape(ivcEpochID), nil)
}
func (c *Client) BuildVerifierBundle(ctx context.Context, fromCheckpointID, toCheckpointID string, options ...RequestOptions) (*VerifierBundle, error) {
body := M{"fromCheckpointId": fromCheckpointID, "toCheckpointId": toCheckpointID}
var out VerifierBundle
err := c.requestJSON(ctx, http.MethodPost, "/v2/audit/packs", nil, body, idempotency(options), &out)
return &out, err
}
func (c *Client) VerifyReceiptRemote(ctx context.Context, input ReceiptVerifyInput, options ...RequestOptions) (*VerifyReport, error) {
var out VerifyReport
err := c.requestJSON(ctx, http.MethodPost, "/v2/verify/receipt", nil, input, idempotency(options), &out)
return &out, err
}
func (c *Client) VerifyObjectRemote(ctx context.Context, input OfflineVerifyInput, options ...RequestOptions) (*VerifyReport, error) {
var out VerifyReport
err := c.requestJSON(ctx, http.MethodPost, "/v2/verify", nil, input, idempotency(options), &out)
return &out, err
}
func (c *Client) ListTenantStreamEvents(ctx context.Context, streamID string, limit, offset int) ([]M, error) {
return c.getList(ctx, "/v2/tenant/streams/"+url.PathEscape(streamID)+"/events", limit, offset)
}
func (c *Client) ListTenantWindows(ctx context.Context, streamID string, limit, offset int) ([]M, error) {
return c.getList(ctx, "/v2/tenant/streams/"+url.PathEscape(streamID)+"/windows", limit, offset)
}
func (c *Client) ListTenantCheckpoints(ctx context.Context, streamID string, limit, offset int) ([]M, error) {
return c.getList(ctx, "/v2/tenant/streams/"+url.PathEscape(streamID)+"/checkpoints", limit, offset)
}
func (c *Client) ListForkEvidence(ctx context.Context, streamID string, limit, offset int) ([]M, error) {
return c.getList(ctx, "/v2/tenant/streams/"+url.PathEscape(streamID)+"/forks", limit, offset)
}
func (c *Client) GetTenantProofState(ctx context.Context, streamID string) (M, error) {
return c.getObject(ctx, "/v2/tenant/streams/"+url.PathEscape(streamID)+"/proof-state", nil)
}
func (c *Client) ListTenantIVCEpochs(ctx context.Context, streamID string, limit, offset int) ([]M, error) {
return c.getList(ctx, "/v2/tenant/streams/"+url.PathEscape(streamID)+"/ivc/epochs", limit, offset)
}
func (c *Client) BuildTenantAuditPack(ctx context.Context, fromCheckpointID, toCheckpointID string, options ...RequestOptions) (*VerifierBundle, error) {
body := M{"fromCheckpointId": fromCheckpointID, "toCheckpointId": toCheckpointID}
var out VerifierBundle
err := c.requestJSON(ctx, http.MethodPost, "/v2/tenant/audit/packs", nil, body, idempotency(options), &out)
return &out, err
}
func (c *Client) ListSignedWebhookConnectors(ctx context.Context, limit, offset int) ([]Connector, error) {
return c.listConnectors(ctx, "/v2/tenant/connectors/signed-webhooks", limit, offset)
}
func (c *Client) CreateSignedWebhookConnector(ctx context.Context, input ConnectorCreateInput, options ...RequestOptions) (*Connector, error) {
var out Connector
err := c.requestJSON(ctx, http.MethodPost, "/v2/tenant/connectors/signed-webhooks", nil, input, idempotency(options), &out)
return &out, err
}
func (c *Client) RevokeSignedWebhookConnector(ctx context.Context, connectorID string) error {
return c.requestNoBody(ctx, http.MethodDelete, "/v2/tenant/connectors/signed-webhooks/"+url.PathEscape(connectorID))
}
func (c *Client) ListS3ObjectConnectors(ctx context.Context, limit, offset int) ([]Connector, error) {
return c.listConnectors(ctx, "/v2/tenant/connectors/s3-objects", limit, offset)
}
func (c *Client) CreateS3ObjectConnector(ctx context.Context, input S3ConnectorCreateInput, options ...RequestOptions) (*Connector, error) {
var out Connector
err := c.requestJSON(ctx, http.MethodPost, "/v2/tenant/connectors/s3-objects", nil, input, idempotency(options), &out)
return &out, err
}
func (c *Client) CommitS3Object(ctx context.Context, connectorID string, body M, options ...RequestOptions) (*EventReceipt, error) {
var out EventReceipt
err := c.requestJSON(ctx, http.MethodPost, "/v2/tenant/connectors/s3-objects/"+url.PathEscape(connectorID)+"/commit", nil, body, idempotency(options), &out)
return &out, err
}
func (c *Client) RevokeS3ObjectConnector(ctx context.Context, connectorID string) error {
return c.requestNoBody(ctx, http.MethodDelete, "/v2/tenant/connectors/s3-objects/"+url.PathEscape(connectorID))
}
func (c *Client) ListRepositoryWebhookConnectors(ctx context.Context, limit, offset int) ([]Connector, error) {
return c.listConnectors(ctx, "/v2/tenant/connectors/repository-webhooks", limit, offset)
}
func (c *Client) CreateRepositoryWebhookConnector(ctx context.Context, input RepositoryConnectorCreateInput, options ...RequestOptions) (*Connector, error) {
var out Connector
err := c.requestJSON(ctx, http.MethodPost, "/v2/tenant/connectors/repository-webhooks", nil, input, idempotency(options), &out)
return &out, err
}
func (c *Client) RevokeRepositoryWebhookConnector(ctx context.Context, connectorID string) error {
return c.requestNoBody(ctx, http.MethodDelete, "/v2/tenant/connectors/repository-webhooks/"+url.PathEscape(connectorID))
}
func (c *Client) IngestSignedWebhookEvent(ctx context.Context, connectorID string, event EventInput, secret string) (*EventReceipt, error) {
raw, err := json.Marshal(event)
if err != nil {
return nil, err
}
headers := SignedConnectorWebhookHeaders(secret, raw, 0)
var out EventReceipt
err = c.requestRaw(ctx, http.MethodPost, "/v2/connectors/signed-webhooks/"+url.PathEscape(connectorID)+"/events", nil, raw, headers, "", &out)
return &out, err
}
func (c *Client) IngestRepositoryWebhookEvent(ctx context.Context, connectorID string, rawBody []byte, headers map[string]string) (*EventReceipt, error) {
var out EventReceipt
err := c.requestRaw(ctx, http.MethodPost, "/v2/connectors/repository-webhooks/"+url.PathEscape(connectorID)+"/events", nil, rawBody, headers, "", &out)
return &out, err
}
func (c *Client) ListLocalVaultInstallations(ctx context.Context, limit, offset int) ([]LocalVaultInstallation, error) {
values := url.Values{}
setPaging(values, limit, offset)
var out []LocalVaultInstallation
err := c.requestJSON(ctx, http.MethodGet, "/v2/tenant/local-vault/installations", values, nil, "", &out)
return out, err
}
func (c *Client) CreateLocalVaultInstallation(ctx context.Context, input LocalVaultInstallationCreateInput, options ...RequestOptions) (*LocalVaultInstallation, error) {
var out LocalVaultInstallation
err := c.requestJSON(ctx, http.MethodPost, "/v2/tenant/local-vault/installations", nil, input, idempotency(options), &out)
return &out, err
}
func (c *Client) RevokeLocalVaultInstallation(ctx context.Context, installationID string) error {
return c.requestNoBody(ctx, http.MethodDelete, "/v2/tenant/local-vault/installations/"+url.PathEscape(installationID))
}
func (c *Client) RelayLocalVaultEvent(ctx context.Context, installationID string, envelope M, payload M, envelopeHash, signatureHex, publicKeyHex string) (*EventReceipt, error) {
body := M{"envelope": envelope, "payload": payload}
headers := map[string]string{
"X-Attesto-Local-Vault-Envelope-Hash": envelopeHash,
"X-Attesto-Local-Vault-Signature": signatureHex,
"X-Attesto-Local-Vault-Public-Key": publicKeyHex,
}
var out EventReceipt
err := c.requestRaw(ctx, http.MethodPost, "/v2/local-vault/installations/"+url.PathEscape(installationID)+"/events", nil, mustJSON(body), headers, "", &out)
return &out, err
}
func (c *Client) SubmitLocalVaultWitnessReceipt(ctx context.Context, installationID string, receipt M, options ...RequestOptions) (M, error) {
return c.postObject(ctx, "/v2/local-vault/installations/"+url.PathEscape(installationID)+"/witness/checkpoints", M{"receipt": receipt}, idempotency(options))
}
func (c *Client) SubmitLocalVaultForkEvidence(ctx context.Context, installationID string, forkEvidence M, options ...RequestOptions) (M, error) {
return c.postObject(ctx, "/v2/local-vault/installations/"+url.PathEscape(installationID)+"/witness/checkpoints", M{"forkEvidence": forkEvidence}, idempotency(options))
}
func (c *Client) getObject(ctx context.Context, path string, values url.Values) (M, error) {
var out M
err := c.requestJSON(ctx, http.MethodGet, path, values, nil, "", &out)
return out, err
}
func (c *Client) postObject(ctx context.Context, path string, body M, idempotencyKey string) (M, error) {
var out M
err := c.requestJSON(ctx, http.MethodPost, path, nil, body, idempotencyKey, &out)
return out, err
}
func (c *Client) getList(ctx context.Context, path string, limit, offset int) ([]M, error) {
values := url.Values{}
setPaging(values, limit, offset)
var out []M
err := c.requestJSON(ctx, http.MethodGet, path, values, nil, "", &out)
return out, err
}
func (c *Client) listConnectors(ctx context.Context, path string, limit, offset int) ([]Connector, error) {
values := url.Values{}
setPaging(values, limit, offset)
var out []Connector
err := c.requestJSON(ctx, http.MethodGet, path, values, nil, "", &out)
return out, err
}
func (c *Client) requestNoBody(ctx context.Context, method, path string) error {
return c.requestRaw(ctx, method, path, nil, nil, nil, "", nil)
}
func (c *Client) requestJSON(ctx context.Context, method, path string, values url.Values, body any, idempotencyKey string, out any) error {
var raw []byte
var err error
if body != nil {
raw, err = json.Marshal(body)
if err != nil {
return err
}
}
return c.requestRaw(ctx, method, path, values, raw, nil, idempotencyKey, out)
}
func (c *Client) requestRaw(ctx context.Context, method, path string, values url.Values, body []byte, extraHeaders map[string]string, idempotencyKey string, out any) error {
if values != nil && len(values) > 0 {
path += "?" + values.Encode()
}
var lastErr error
for attempt := 1; attempt <= c.maxRetries; attempt++ {
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.bearer)
req.Header.Set("User-Agent", c.userAgent)
req.Header.Set("X-Attesto-SDK", c.userAgent)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if idempotencyKey != "" {
req.Header.Set("Idempotency-Key", idempotencyKey)
}
for key, value := range extraHeaders {
req.Header.Set(key, value)
}
resp, err := c.httpClient.Do(req)
if err != nil {
lastErr = err
if attempt == c.maxRetries {
break
}
sleep(attempt)
continue
}
err = decodeResponse(resp, out)
if err == nil {
return nil
}
lastErr = err
var apiErr *APIError
if errors.As(err, &apiErr) && !retryable(apiErr.StatusCode) {
return err
}
if attempt < c.maxRetries {
sleep(attempt)
}
}
return lastErr
}
type APIError struct {
StatusCode int
Message string
}
func (e *APIError) Error() string {
return fmt.Sprintf("attesto api error: status=%d message=%s", e.StatusCode, e.Message)
}
func decodeResponse(resp *http.Response, out any) error {
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent {
return nil
}
raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return err
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
if out == nil || len(raw) == 0 {
return nil
}
return json.Unmarshal(raw, out)
}
message := http.StatusText(resp.StatusCode)
var parsed M
if json.Unmarshal(raw, &parsed) == nil {
if detail, ok := parsed["detail"].(string); ok && detail != "" {
message = detail
}
}
return &APIError{StatusCode: resp.StatusCode, Message: sanitizeMessage(message)}
}
func retryable(status int) bool {
switch status {
case http.StatusRequestTimeout, http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
return true
default:
return false
}
}
func idempotency(options []RequestOptions) string {
if len(options) > 0 && options[0].IdempotencyKey != "" {
return options[0].IdempotencyKey
}
var raw [16]byte
if _, err := rand.Read(raw[:]); err != nil {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
return hex.EncodeToString(raw[:])
}
func setPaging(values url.Values, limit, offset int) {
if limit > 0 {
values.Set("limit", fmt.Sprintf("%d", limit))
}
if offset > 0 {
values.Set("offset", fmt.Sprintf("%d", offset))
}
}
func sleep(attempt int) {
time.Sleep(time.Duration(100*attempt) * time.Millisecond)
}
func mustJSON(value any) []byte {
raw, err := json.Marshal(value)
if err != nil {
panic(err)
}
return raw
}
func sanitizeMessage(message string) string {
for _, marker := range []string{"sk_live_", "sk_test_", "pk_live_", "pk_test_", "npm_", "pypi-", "atto_live_", "atto_test_"} {
if strings.Contains(message, marker) {
return "redacted"
}
}
return message
}

100
client_test.go Normal file
View File

@@ -0,0 +1,100 @@
package attesto
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
const testAPIKey = "atto_test_0123456789abcdef0123456789abcdef"
func TestClientCallsProductionV2Endpoints(t *testing.T) {
var seen []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seen = append(seen, r.Method+" "+r.URL.String())
if r.Header.Get("Authorization") != "Bearer "+testAPIKey {
t.Fatalf("authorization header missing")
}
if !strings.HasPrefix(r.Header.Get("X-Attesto-SDK"), "attesto-go/") {
t.Fatalf("sdk header missing: %s", r.Header.Get("X-Attesto-SDK"))
}
w.Header().Set("Content-Type", "application/json")
switch {
case r.Method == http.MethodPost && r.URL.Path == "/v2/streams":
json.NewEncoder(w).Encode(Stream{
StreamID: "str_123", SystemID: "sys_123", UseCase: "ai-governance", PolicyID: "policy-main", Status: "active", Created: true,
})
case r.Method == http.MethodPost && r.URL.Path == "/v2/streams/str_123/events":
if r.Header.Get("Idempotency-Key") == "" {
t.Fatalf("idempotency key missing")
}
json.NewEncoder(w).Encode(EventReceipt{
StreamID: "str_123", StreamEventID: "evt_123", SeqNo: 1, EventHash: strings.Repeat("a", 64), StreamHeadHash: strings.Repeat("b", 64),
Receipt: SignedReceipt{Payload: M{}, ReceiptHash: strings.Repeat("c", 64), Signature: ReceiptSignature{Alg: "ed25519"}},
})
case r.Method == http.MethodPost && r.URL.Path == "/v2/verify":
json.NewEncoder(w).Encode(VerifyReport{Kind: VerifyReceipt, OK: true, Problems: []string{}, Result: "accepted"})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
}
}))
defer server.Close()
client, err := NewClient(testAPIKey, WithBaseURL(server.URL), WithMaxRetries(1))
if err != nil {
t.Fatalf("client: %v", err)
}
ctx := context.Background()
stream, err := client.CreateStream(ctx, StreamCreateInput{UseCase: "ai-governance", PolicyID: "policy-main"})
if err != nil {
t.Fatalf("create stream: %v", err)
}
if stream.StreamID != "str_123" {
t.Fatalf("stream id mismatch: %s", stream.StreamID)
}
receipt, err := client.LogEvent(ctx, "str_123", EventInput{SourceRef: "go-test"})
if err != nil {
t.Fatalf("log event: %v", err)
}
if receipt.StreamEventID != "evt_123" {
t.Fatalf("receipt id mismatch: %s", receipt.StreamEventID)
}
report, err := client.VerifyObjectRemote(ctx, OfflineVerifyInput{Kind: VerifyReceipt, Object: M{"receipt": "object"}})
if err != nil {
t.Fatalf("verify object: %v", err)
}
if !report.OK {
t.Fatalf("verify report failed: %#v", report)
}
if len(seen) != 3 {
t.Fatalf("request count mismatch: %v", seen)
}
}
func TestBearerClientCanCallTenantEndpoints(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer tenant-token" {
t.Fatalf("tenant bearer header missing")
}
if r.URL.Path != "/v2/tenant/streams" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode([]TenantStream{{StreamID: "str_tenant", SystemID: "sys_123", UseCase: "audit", PolicyID: "policy", Status: "active"}})
}))
defer server.Close()
client, err := NewBearerClient("tenant-token", WithBaseURL(server.URL), WithMaxRetries(1))
if err != nil {
t.Fatalf("client: %v", err)
}
streams, err := client.ListTenantStreams(context.Background(), "", 100, 0)
if err != nil {
t.Fatalf("list streams: %v", err)
}
if len(streams) != 1 || streams[0].StreamID != "str_tenant" {
t.Fatalf("unexpected streams: %#v", streams)
}
}

1280
cmd/attesto/main.go Normal file

File diff suppressed because it is too large Load Diff

122
cmd/attesto/main_test.go Normal file
View File

@@ -0,0 +1,122 @@
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
const cliTestAPIKey = "atto_test_0123456789abcdef0123456789abcdef"
func TestVersionJSON(t *testing.T) {
var stdout, stderr bytes.Buffer
code := run([]string{"--json", "version"}, &stdout, &stderr, testEnv(t, nil))
if code != 0 {
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
}
var out map[string]any
if err := json.Unmarshal(stdout.Bytes(), &out); err != nil {
t.Fatalf("json: %v", err)
}
if out["name"] != "attesto" || out["version"] == "" {
t.Fatalf("unexpected version output: %s", stdout.String())
}
}
func TestReceiptsVerifyOfflineGoldenVector(t *testing.T) {
vector := loadVector(t)
dir := t.TempDir()
receiptFile := filepath.Join(dir, "receipt.json")
rawReceipt, _ := json.Marshal(vector["receipt"])
if err := os.WriteFile(receiptFile, rawReceipt, 0o600); err != nil {
t.Fatal(err)
}
publicKey := vector["signing"].(map[string]any)["public_key_hex"].(string)
var stdout, stderr bytes.Buffer
code := run([]string{"--json", "receipts", "verify", "--file", receiptFile, "--public-key-hex", publicKey}, &stdout, &stderr, testEnv(t, nil))
if code != 0 {
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
}
var out map[string]any
if err := json.Unmarshal(stdout.Bytes(), &out); err != nil {
t.Fatalf("json: %v", err)
}
if out["ok"] != true {
t.Fatalf("receipt did not verify: %s", stdout.String())
}
}
func TestConfigSetRedactsSecrets(t *testing.T) {
dir := t.TempDir()
config := filepath.Join(dir, "config.json")
env := testEnv(t, map[string]string{
"ATTESTO_CONFIG": config,
"ATT_API_KEY": cliTestAPIKey,
"ATT_TOKEN": "tenant-token-secret",
})
var stdout, stderr bytes.Buffer
code := run([]string{"--json", "config", "set", "--api-key-env", "ATT_API_KEY", "--token-env", "ATT_TOKEN"}, &stdout, &stderr, env)
if code != 0 {
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
}
if strings.Contains(stdout.String(), cliTestAPIKey) || strings.Contains(stdout.String(), "tenant-token-secret") {
t.Fatalf("secret leaked in output: %s", stdout.String())
}
raw, err := os.ReadFile(config)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(raw), cliTestAPIKey) {
t.Fatalf("config did not persist api key")
}
}
func TestStreamsCreateCallsAPI(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v2/streams" || r.Method != http.MethodPost {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
if r.Header.Get("Authorization") != "Bearer "+cliTestAPIKey {
t.Fatalf("missing auth")
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"streamId":"str_cli","systemId":"sys_cli","useCase":"audit","policyId":"policy","status":"active","lastSeqNo":0,"created":true}`))
}))
defer server.Close()
var stdout, stderr bytes.Buffer
code := run([]string{"--json", "--base-url", server.URL, "--api-key-env", "ATT_API_KEY", "streams", "create", "--use-case", "audit", "--policy-id", "policy"}, &stdout, &stderr, testEnv(t, map[string]string{"ATT_API_KEY": cliTestAPIKey}))
if code != 0 {
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
}
if !strings.Contains(stdout.String(), "str_cli") {
t.Fatalf("unexpected stdout: %s", stdout.String())
}
}
func loadVector(t *testing.T) map[string]any {
t.Helper()
raw, err := os.ReadFile(filepath.Join("..", "..", "..", "..", "golden-vectors", "proofstream-v0.1-alpha", "one-stream-two-events.json"))
if err != nil {
t.Fatal(err)
}
var out map[string]any
if err := json.Unmarshal(raw, &out); err != nil {
t.Fatal(err)
}
return out
}
func testEnv(t *testing.T, values map[string]string) func(string) string {
t.Helper()
return func(key string) string {
if value, ok := values[key]; ok {
return value
}
return ""
}
}

View File

@@ -0,0 +1,36 @@
package main
import (
"context"
"fmt"
"log"
"os"
attesto "git.rotz.ai/rotzmediagroup/attesto-v1/sdk/go"
)
func main() {
ctx := context.Background()
client, err := attesto.NewClient(os.Getenv("ATTESTO_API_KEY"))
if err != nil {
log.Fatal(err)
}
stream, err := client.CreateStream(ctx, attesto.StreamCreateInput{
UseCase: "ai-governance",
PolicyID: "policy-main",
})
if err != nil {
log.Fatal(err)
}
receipt, err := client.LogEvent(ctx, stream.StreamID, attesto.EventInput{
SourceRef: "example-decision",
Payload: attesto.M{
"decision": "approved",
"reason": "policy-threshold-met",
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s %s\n", receipt.StreamEventID, receipt.EventHash)
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module git.rotz.ai/rotzmediagroup/attesto-v1/sdk/go
go 1.24

254
proofstream.go Normal file
View File

@@ -0,0 +1,254 @@
package attesto
import (
"bytes"
"crypto/ed25519"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math"
"sort"
"strings"
"time"
)
var ProofstreamDomains = map[string]string{
"anchor": "attesto.v2.anchor",
"bundle": "attesto.v2.bundle",
"checkpoint": "attesto.v2.checkpoint",
"consistency": "attesto.v2.consistency",
"event": "attesto.v2.event",
"fork": "attesto.v2.fork",
"ivc": "attesto.v2.ivc",
"receipt": "attesto.v2.receipt",
"stream": "attesto.v2.stream",
"window": "attesto.v2.window",
"witness": "attesto.v2.witness",
"witness_policy": "attesto.v2.witness_policy",
}
func CanonicalJSON(value any) ([]byte, error) {
normalized, err := normalizeCanonical(value)
if err != nil {
return nil, err
}
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
if err := encoder.Encode(normalized); err != nil {
return nil, err
}
return bytes.TrimSuffix(buf.Bytes(), []byte("\n")), nil
}
func CanonicalJSONHex(value any) (string, error) {
raw, err := CanonicalJSON(value)
if err != nil {
return "", err
}
return hex.EncodeToString(raw), nil
}
func DomainHashHex(domain string, value any) (string, error) {
raw, err := CanonicalJSON(value)
if err != nil {
return "", err
}
h := sha256.New()
h.Write([]byte(domain))
h.Write([]byte{0})
h.Write(raw)
return hex.EncodeToString(h.Sum(nil)), nil
}
func SHA256Hex(value []byte) string {
sum := sha256.Sum256(value)
return hex.EncodeToString(sum[:])
}
func SignConnectorWebhookPayload(secret string, body []byte, timestamp int64) (string, string) {
if timestamp == 0 {
timestamp = time.Now().Unix()
}
ts := fmt.Sprintf("%d", timestamp)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(ts))
mac.Write([]byte("."))
mac.Write(body)
return ts, hex.EncodeToString(mac.Sum(nil))
}
func SignedConnectorWebhookHeaders(secret string, body []byte, timestamp int64) map[string]string {
ts, sig := SignConnectorWebhookPayload(secret, body, timestamp)
return map[string]string{
"X-Attesto-Connector-Timestamp": ts,
"X-Attesto-Connector-Signature": sig,
}
}
func VerifyReceiptOffline(receipt SignedReceipt, publicKeyHex string) VerifyReport {
problems := make([]string, 0)
hash, err := DomainHashHex(ProofstreamDomains["receipt"], receipt.Payload)
if err != nil {
problems = append(problems, "receipt payload is not canonical-json compatible")
} else if hash != receipt.ReceiptHash {
problems = append(problems, "receipt hash mismatch")
}
if strings.ToLower(receipt.Signature.Alg) != "ed25519" {
problems = append(problems, "unsupported receipt signature algorithm")
}
signatureHex := receipt.Signature.SignatureHex
if signatureHex == "" {
problems = append(problems, "receipt signature missing")
}
if err == nil && signatureHex != "" {
publicKey, keyErr := hex.DecodeString(strings.TrimPrefix(strings.ToLower(publicKeyHex), "0x"))
signature, sigErr := hex.DecodeString(strings.TrimPrefix(strings.ToLower(signatureHex), "0x"))
payloadBytes, payloadErr := CanonicalJSON(receipt.Payload)
if keyErr != nil || len(publicKey) != ed25519.PublicKeySize {
problems = append(problems, "invalid public key")
} else if sigErr != nil || len(signature) != ed25519.SignatureSize {
problems = append(problems, "invalid receipt signature")
} else if payloadErr != nil {
problems = append(problems, "receipt payload is not canonical-json compatible")
} else {
message := make([]byte, 0, len(ProofstreamDomains["receipt"])+1+len(payloadBytes))
message = append(message, []byte(ProofstreamDomains["receipt"])...)
message = append(message, 0)
message = append(message, payloadBytes...)
if !ed25519.Verify(ed25519.PublicKey(publicKey), message, signature) {
problems = append(problems, "receipt signature mismatch")
}
}
}
ok := len(problems) == 0
result := "failed"
if ok {
result = "accepted"
}
eventHash, _ := receipt.Payload["event_hash"].(string)
if eventHash == "" {
eventHash, _ = receipt.Payload["eventHash"].(string)
}
return VerifyReport{
Kind: VerifyReceipt,
OK: ok,
ReceiptHash: hash,
EventHash: eventHash,
Problems: problems,
Protocol: ProofstreamProtocol,
ProtocolVersion: ProtocolVersionAlpha,
Subject: subjectFromPayload(receipt.Payload),
Result: result,
Checks: []VerificationCheck{{
Name: "receipt",
Result: result,
Details: M{"problems": problems},
}},
}
}
func normalizeCanonical(value any) (any, error) {
switch v := value.(type) {
case nil, string, bool:
return v, nil
case time.Time:
return v.UTC().Format("2006-01-02T15:04:05.000Z"), nil
case []byte:
return hex.EncodeToString(v), nil
case int:
return v, nil
case int8:
return int64(v), nil
case int16:
return int64(v), nil
case int32:
return int64(v), nil
case int64:
return v, nil
case uint:
return v, nil
case uint8:
return uint64(v), nil
case uint16:
return uint64(v), nil
case uint32:
return uint64(v), nil
case uint64:
return v, nil
case float32:
if math.IsInf(float64(v), 0) || math.IsNaN(float64(v)) {
return nil, errors.New("canonical JSON cannot encode non-finite numbers")
}
return v, nil
case float64:
if math.IsInf(v, 0) || math.IsNaN(v) {
return nil, errors.New("canonical JSON cannot encode non-finite numbers")
}
return v, nil
case []any:
out := make([]any, len(v))
for i, item := range v {
normalized, err := normalizeCanonical(item)
if err != nil {
return nil, err
}
out[i] = normalized
}
return out, nil
case []M:
out := make([]any, len(v))
for i, item := range v {
normalized, err := normalizeCanonical(item)
if err != nil {
return nil, err
}
out[i] = normalized
}
return out, nil
case map[string]any:
return normalizeMap(v)
case M:
return normalizeMap(map[string]any(v))
default:
raw, err := json.Marshal(v)
if err != nil {
return nil, err
}
var decoded any
if err := json.Unmarshal(raw, &decoded); err != nil {
return nil, err
}
return normalizeCanonical(decoded)
}
}
func normalizeMap(in map[string]any) (map[string]any, error) {
out := make(map[string]any, len(in))
keys := make([]string, 0, len(in))
for key := range in {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
normalized, err := normalizeCanonical(in[key])
if err != nil {
return nil, err
}
out[key] = normalized
}
return out, nil
}
func subjectFromPayload(payload M) M {
subject := M{}
for _, key := range []string{"tenant_id", "system_id", "stream_id", "stream_event_id", "seq_no", "from_seq_no", "to_seq_no", "checkpoint_id", "from_checkpoint_id", "to_checkpoint_id"} {
if value, ok := payload[key]; ok {
subject[key] = value
}
}
return subject
}

81
proofstream_test.go Normal file
View File

@@ -0,0 +1,81 @@
package attesto
import (
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"testing"
)
func TestGoldenVectorCanonicalJSONAndHashes(t *testing.T) {
vector := loadGoldenVector(t)
events := vector["events"].([]any)
first := events[0].(map[string]any)
envelope := M(first["envelope"].(map[string]any))
gotCanonicalHex, err := CanonicalJSONHex(envelope)
if err != nil {
t.Fatalf("canonical json: %v", err)
}
if gotCanonicalHex != first["canonical_json_hex"].(string) {
t.Fatalf("canonical json hex mismatch\ngot %s\nwant %s", gotCanonicalHex, first["canonical_json_hex"])
}
domains := vector["domains"].(map[string]any)
gotHash, err := DomainHashHex(domains["event"].(string), envelope)
if err != nil {
t.Fatalf("domain hash: %v", err)
}
if gotHash != first["event_hash"].(string) {
t.Fatalf("event hash mismatch\ngot %s\nwant %s", gotHash, first["event_hash"])
}
}
func TestGoldenVectorReceiptOfflineVerification(t *testing.T) {
vector := loadGoldenVector(t)
rawReceipt, err := json.Marshal(vector["receipt"])
if err != nil {
t.Fatal(err)
}
var receipt SignedReceipt
if err := json.Unmarshal(rawReceipt, &receipt); err != nil {
t.Fatalf("unmarshal receipt: %v", err)
}
signing := vector["signing"].(map[string]any)
report := VerifyReceiptOffline(receipt, signing["public_key_hex"].(string))
if !report.OK {
t.Fatalf("receipt should verify: %#v", report.Problems)
}
receipt.Payload["event_hash"] = "00" + receipt.Payload["event_hash"].(string)[2:]
tampered := VerifyReceiptOffline(receipt, signing["public_key_hex"].(string))
if tampered.OK {
t.Fatal("tampered receipt unexpectedly verified")
}
}
func TestSignedConnectorWebhookHeaders(t *testing.T) {
headers := SignedConnectorWebhookHeaders("secret", []byte(`{"eventType":"decision"}`), 1710000000)
if headers["X-Attesto-Connector-Timestamp"] != "1710000000" {
t.Fatalf("timestamp mismatch: %v", headers)
}
signature := headers["X-Attesto-Connector-Signature"]
if _, err := hex.DecodeString(signature); err != nil {
t.Fatalf("signature is not hex: %v", err)
}
if len(signature) != 64 {
t.Fatalf("signature length mismatch: %d", len(signature))
}
}
func loadGoldenVector(t *testing.T) map[string]any {
t.Helper()
path := filepath.Join("..", "..", "golden-vectors", "proofstream-v0.1-alpha", "one-stream-two-events.json")
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read golden vector: %v", err)
}
var data map[string]any
if err := json.Unmarshal(raw, &data); err != nil {
t.Fatalf("parse golden vector: %v", err)
}
return data
}

271
types.go Normal file
View File

@@ -0,0 +1,271 @@
package attesto
import "encoding/json"
type M map[string]any
type RequestOptions struct {
IdempotencyKey string
}
type StreamCreateInput struct {
UseCase string `json:"useCase"`
PolicyID string `json:"policyId"`
Metadata M `json:"metadata,omitempty"`
}
type Stream struct {
StreamID string `json:"streamId"`
SystemID string `json:"systemId"`
UseCase string `json:"useCase"`
PolicyID string `json:"policyId"`
Status string `json:"status"`
LastSeqNo int64 `json:"lastSeqNo"`
LastEventHash string `json:"lastEventHash,omitempty"`
LastStreamHeadHash string `json:"lastStreamHeadHash,omitempty"`
Created bool `json:"created"`
}
type TenantStream struct {
StreamID string `json:"streamId"`
SystemID string `json:"systemId"`
SystemName string `json:"systemName,omitempty"`
UseCase string `json:"useCase"`
PolicyID string `json:"policyId"`
Status string `json:"status"`
LastSeqNo int64 `json:"lastSeqNo"`
LastEventHash string `json:"lastEventHash,omitempty"`
LastStreamHeadHash string `json:"lastStreamHeadHash,omitempty"`
OpenedAt string `json:"openedAt"`
UpdatedAt string `json:"updatedAt"`
}
type StreamHead struct {
StreamID string `json:"streamId"`
SystemID string `json:"systemId"`
Status string `json:"status"`
LastSeqNo int64 `json:"lastSeqNo"`
LastEventHash string `json:"lastEventHash,omitempty"`
LastStreamHeadHash string `json:"lastStreamHeadHash,omitempty"`
}
type EventInput struct {
EventType string `json:"eventType,omitempty"`
OccurredAt string `json:"occurredAt,omitempty"`
SourceKind string `json:"sourceKind,omitempty"`
SourceRef string `json:"sourceRef"`
Payload M `json:"payload,omitempty"`
Metadata M `json:"metadata,omitempty"`
}
type ReceiptSignature struct {
Alg string `json:"alg"`
KID string `json:"kid"`
KeyEpoch string `json:"keyEpoch,omitempty"`
SignatureHex string `json:"signatureHex,omitempty"`
}
type SignedReceipt struct {
Payload M `json:"payload"`
ReceiptHash string `json:"receiptHash"`
Signature ReceiptSignature `json:"signature"`
}
func (s *ReceiptSignature) UnmarshalJSON(raw []byte) error {
var data map[string]any
if err := json.Unmarshal(raw, &data); err != nil {
return err
}
s.Alg, _ = data["alg"].(string)
s.KID, _ = data["kid"].(string)
if value, ok := data["keyEpoch"].(string); ok {
s.KeyEpoch = value
} else if value, ok := data["key_epoch"].(string); ok {
s.KeyEpoch = value
}
if value, ok := data["signatureHex"].(string); ok {
s.SignatureHex = value
} else if value, ok := data["signature_hex"].(string); ok {
s.SignatureHex = value
}
return nil
}
func (r *SignedReceipt) UnmarshalJSON(raw []byte) error {
var data map[string]json.RawMessage
if err := json.Unmarshal(raw, &data); err != nil {
return err
}
if payload, ok := data["payload"]; ok {
if err := json.Unmarshal(payload, &r.Payload); err != nil {
return err
}
}
if receiptHash, ok := data["receiptHash"]; ok {
if err := json.Unmarshal(receiptHash, &r.ReceiptHash); err != nil {
return err
}
} else if receiptHash, ok := data["receipt_hash"]; ok {
if err := json.Unmarshal(receiptHash, &r.ReceiptHash); err != nil {
return err
}
}
if signature, ok := data["signature"]; ok {
if err := json.Unmarshal(signature, &r.Signature); err != nil {
return err
}
}
return nil
}
type EventReceipt struct {
StreamID string `json:"streamId"`
StreamEventID string `json:"streamEventId"`
SeqNo int64 `json:"seqNo"`
EventHash string `json:"eventHash"`
PrevEventHash string `json:"prevEventHash,omitempty"`
StreamHeadHash string `json:"streamHeadHash"`
Receipt SignedReceipt `json:"receipt"`
LocalVaultAck M `json:"localVaultAck,omitempty"`
}
type EventBatchResponse struct {
Accepted int `json:"accepted"`
Receipts []EventReceipt `json:"receipts"`
}
type VerifyKind string
const (
VerifyReceipt VerifyKind = "receipt"
VerifyStream VerifyKind = "stream"
VerifyWindow VerifyKind = "window"
VerifyCheckpoint VerifyKind = "checkpoint"
VerifyConsistency VerifyKind = "consistency"
VerifyAnchor VerifyKind = "anchor"
VerifyIVC VerifyKind = "ivc"
VerifyBundle VerifyKind = "bundle"
)
type ReceiptVerifyInput struct {
Receipt any `json:"receipt"`
PublicKeyHex string `json:"publicKeyHex"`
StreamEventID string `json:"streamEventId,omitempty"`
}
type OfflineVerifyInput struct {
Kind VerifyKind `json:"kind"`
Object M `json:"object"`
PublicKeyHex string `json:"publicKeyHex,omitempty"`
}
type VerificationCheck struct {
Name string `json:"name"`
Result string `json:"result"`
Details M `json:"details,omitempty"`
}
type VerifyReport struct {
Kind VerifyKind `json:"kind,omitempty"`
OK bool `json:"ok"`
ReceiptHash string `json:"receiptHash,omitempty"`
EventHash string `json:"eventHash,omitempty"`
Problems []string `json:"problems"`
Protocol string `json:"protocol,omitempty"`
ProtocolVersion string `json:"protocolVersion,omitempty"`
VerifiedAt string `json:"verifiedAt,omitempty"`
Subject M `json:"subject,omitempty"`
Result string `json:"result,omitempty"`
Checks []VerificationCheck `json:"checks,omitempty"`
Evidence []M `json:"evidence,omitempty"`
}
type WitnessPolicy struct {
PolicyID string `json:"policyId"`
TenantID string `json:"tenantId"`
Mode string `json:"mode"`
Status string `json:"status"`
Enforced bool `json:"enforced"`
ManagedOnly bool `json:"managedOnly"`
QuorumThreshold int `json:"quorumThreshold"`
WitnessKeys []string `json:"witnessKeys"`
Payload M `json:"payload"`
PolicyHash string `json:"policyHash"`
}
type VerifierBundle struct {
BundleID string `json:"bundleId"`
BundleHash string `json:"bundleHash"`
Payload M `json:"payload"`
}
type ConnectorCreateInput struct {
StreamID string `json:"streamId"`
Label string `json:"label"`
Metadata M `json:"metadata,omitempty"`
}
type S3ConnectorCreateInput struct {
StreamID string `json:"streamId"`
Label string `json:"label"`
Bucket string `json:"bucket"`
AccessKeyID string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
Region string `json:"region,omitempty"`
EndpointURL string `json:"endpointUrl,omitempty"`
Provider string `json:"provider,omitempty"`
AddressingStyle string `json:"addressingStyle,omitempty"`
Metadata M `json:"metadata,omitempty"`
}
type RepositoryConnectorCreateInput struct {
StreamID string `json:"streamId"`
Label string `json:"label"`
Provider string `json:"provider"`
RepositoryURL string `json:"repositoryUrl,omitempty"`
Metadata M `json:"metadata,omitempty"`
}
type Connector struct {
ConnectorID string `json:"connectorId"`
StreamID string `json:"streamId"`
SystemID string `json:"systemId"`
ConnectorType string `json:"connectorType"`
Label string `json:"label"`
Status string `json:"status"`
EndpointPath string `json:"endpointPath,omitempty"`
CommitEndpointPath string `json:"commitEndpointPath,omitempty"`
Provider string `json:"provider,omitempty"`
RepositoryURL string `json:"repositoryUrl,omitempty"`
Bucket string `json:"bucket,omitempty"`
Region string `json:"region,omitempty"`
EndpointURL string `json:"endpointUrl,omitempty"`
AddressingStyle string `json:"addressingStyle,omitempty"`
AccessKeyIDMasked string `json:"accessKeyIdMasked,omitempty"`
LastEventAt string `json:"lastEventAt,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
Secret string `json:"secret,omitempty"`
}
type LocalVaultInstallationCreateInput struct {
StreamID string `json:"streamId"`
Label string `json:"label"`
KeyID string `json:"keyId"`
PublicKeyHex string `json:"publicKeyHex"`
Metadata M `json:"metadata,omitempty"`
}
type LocalVaultInstallation struct {
InstallationID string `json:"installationId"`
StreamID string `json:"streamId"`
SystemID string `json:"systemId"`
Label string `json:"label"`
Status string `json:"status"`
KeyID string `json:"keyId"`
PublicKeyHex string `json:"publicKeyHex"`
EndpointPath string `json:"endpointPath"`
LastEventAt string `json:"lastEventAt,omitempty"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}

8
version.go Normal file
View File

@@ -0,0 +1,8 @@
package attesto
const (
SDKVersion = "0.2.0"
DefaultBaseURL = "https://verify.attesto.eu"
ProofstreamProtocol = "ATTESTO-PROOFSTREAM-001"
ProtocolVersionAlpha = "0.1-alpha"
)