Add SDK parity and Go CLI release readiness

This commit is contained in:
Codex
2026-06-07 22:41:32 +02:00
commit 61f3a217e6
11 changed files with 2797 additions and 0 deletions
+81
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
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
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
View File
File diff suppressed because it is too large Load Diff
+122
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 ""
}
}
+36
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
View File
@@ -0,0 +1,3 @@
module git.rotz.ai/rotzmediagroup/attesto-v1/sdk/go
go 1.24
+254
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
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
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
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"
)