Add SDK parity and Go CLI release readiness
This commit is contained in:
81
README.md
Normal file
81
README.md
Normal 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
561
client.go
Normal 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
100
client_test.go
Normal 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
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
122
cmd/attesto/main_test.go
Normal 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
examples/proofstream/main.go
Normal file
36
examples/proofstream/main.go
Normal 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
3
go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module git.rotz.ai/rotzmediagroup/attesto-v1/sdk/go
|
||||||
|
|
||||||
|
go 1.24
|
||||||
254
proofstream.go
Normal file
254
proofstream.go
Normal 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
81
proofstream_test.go
Normal 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
271
types.go
Normal 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
8
version.go
Normal 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"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user