sdk(P1.2): payload commitment + safe-number preflight in all three SDKs
Adds payload_commitment / metadata_commitment / verify_payload_commitment and assert_commitment_safe_numbers to the Python, TypeScript, and Go SDKs, each building on the frozen canonical_json/domain_hash primitives (no change to their byte output). The number preflight is a byte-for-byte port of the backend assert_commitment_safe_numbers (floats rejected, |int| > 2^53-1 rejected, bool exempt) and is wired into the v2 log_event / log_events send path, raising a typed AttestoUnsafeNumberError with the JSON path so the rule fails at dev time rather than as a production 422; preflight=False / SkipPreflight defers to the server. New shared corpus golden-vectors/sdk-parity/canonical-numbers.json (15 accept + 8 reject), accept-hashes generated from the backend _commitment. Proven: Python = TypeScript = Go = backend produce byte-identical commitment hashes for every accept vector and identical reject paths (the Go float64-vs-Python- int serialization parity holds). READMEs updated per SDK. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
23
client.go
23
client.go
@@ -161,6 +161,16 @@ func (c *Client) LogEvent(ctx context.Context, streamID string, input EventInput
|
||||
if err := normalizeEventInput(&input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Reject commitment-unsafe numbers locally so the developer sees the rule at
|
||||
// dev time rather than as a production 422; SkipPreflight defers to the server.
|
||||
if !skipPreflight(options) {
|
||||
if err := AssertCommitmentSafeNumbers(input.Payload, "$.payload"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := AssertCommitmentSafeNumbers(input.Metadata, "$.metadata"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var out EventReceipt
|
||||
err := c.requestJSON(ctx, http.MethodPost, "/v2/streams/"+url.PathEscape(streamID)+"/events", nil, input, idempotency(options), &out)
|
||||
return &out, err
|
||||
@@ -170,10 +180,19 @@ func (c *Client) LogEvents(ctx context.Context, streamID string, events []EventI
|
||||
if len(events) > 1000 {
|
||||
return nil, errors.New("max 1000 events per batch")
|
||||
}
|
||||
preflight := !skipPreflight(options)
|
||||
for i := range events {
|
||||
if err := normalizeEventInput(&events[i]); err != nil {
|
||||
return nil, fmt.Errorf("event %d: %w", i, err)
|
||||
}
|
||||
if preflight {
|
||||
if err := AssertCommitmentSafeNumbers(events[i].Payload, fmt.Sprintf("$.events[%d].payload", i)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := AssertCommitmentSafeNumbers(events[i].Metadata, fmt.Sprintf("$.events[%d].metadata", i)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
body := M{"events": events}
|
||||
var out EventBatchResponse
|
||||
@@ -558,6 +577,10 @@ func retryable(status int) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func skipPreflight(options []RequestOptions) bool {
|
||||
return len(options) > 0 && options[0].SkipPreflight
|
||||
}
|
||||
|
||||
func idempotency(options []RequestOptions) string {
|
||||
if len(options) > 0 && options[0].IdempotencyKey != "" {
|
||||
return options[0].IdempotencyKey
|
||||
|
||||
Reference in New Issue
Block a user