Axowl.Sdk.Integrity.Client 0.2.1

dotnet add package Axowl.Sdk.Integrity.Client --version 0.2.1
                    
NuGet\Install-Package Axowl.Sdk.Integrity.Client -Version 0.2.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Axowl.Sdk.Integrity.Client" Version="0.2.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Axowl.Sdk.Integrity.Client" Version="0.2.1" />
                    
Directory.Packages.props
<PackageReference Include="Axowl.Sdk.Integrity.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Axowl.Sdk.Integrity.Client --version 0.2.1
                    
#r "nuget: Axowl.Sdk.Integrity.Client, 0.2.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Axowl.Sdk.Integrity.Client@0.2.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Axowl.Sdk.Integrity.Client&version=0.2.1
                    
Install as a Cake Addin
#tool nuget:?package=Axowl.Sdk.Integrity.Client&version=0.2.1
                    
Install as a Cake Tool

Axowl.Sdk.Integrity.Client

Axowl SDK for hash-chain integrity sealing. gRPC default + REST fallback, single-line transport selection.

Install

dotnet add package Axowl.Sdk.Integrity.Client
dotnet add package Axowl.Sdk.Integrity.Hashing   # client-side hash computation

Quickstart (gRPC, default)

using Axowl.Sdk.Integrity.Abstractions.Contracts;
using Axowl.Sdk.Integrity.Abstractions.Interfaces;
using Axowl.Sdk.Integrity.Client;
using Axowl.Sdk.Integrity.Hashing;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

services.AddAxowlIntegrityClient(opts =>
{
    opts.ApiKey = configuration["Axowl:ApiKey"]!;
    // Defaults — set these only to point somewhere else
    // opts.ServerAddress     = "https://testgrpc.axowl.com";
    // opts.RestServerAddress = "https://testapi.axowl.com";
});

services.AddSingleton<IHashCalculator, Sha256Hasher>();

await using var provider = services.BuildServiceProvider();

var client = provider.GetRequiredService<IAxowlIntegrityClient>();
var hasher = provider.GetRequiredService<IHashCalculator>();

var entityId = Guid.NewGuid();
var orgId    = Guid.Parse("...");  // your Axowl org id
var prevHash = new string('0', 64);
var payload  = """{"cashBalance":100,"creditBalance":50}""";

var clientHash = hasher.ComputeHash("Wallet", entityId, payload, prevHash, sequenceNumber: 1);

var resp = await client.SealAsync(new SealRequest(
    EntityType:         "Wallet",
    EntityId:           entityId,
    OrganizationId:     orgId,
    SequenceNumber:     1,
    ClientComputedHash: clientHash,
    PreviousHash:       prevHash,
    OccurredAt:         DateTime.UtcNow));

Console.WriteLine($"Sealed: {resp.ServerSealedHash}");
Console.WriteLine($"Anchor: {resp.ChainAnchorId}");

Transport selection

Switch the entire client behavior with one option:

services.AddAxowlIntegrityClient(opts =>
{
    opts.ApiKey    = "...";
    opts.Transport = TransportMode.GrpcWithFallback;   // ← here
});
Mode Behavior When to use
TransportMode.Grpc (default) gRPC over HTTP/2 + TLS only. Throws on transport failure. Normal use, lowest latency, full streaming support
TransportMode.GrpcWithFallback gRPC first. On transport-level failure (CDN 403 HTML, channel unavailable, HTTP/2 blocked), automatically falls back to REST. B2B SDK in unknown customer environments
TransportMode.RestOnly REST over HTTP/1.1 only. Corporate firewalls or proxies that block HTTP/2 / gRPC

What triggers fallback

Fallback only happens on transport-level failures, not application logic:

  • RpcException with status Unavailable (TCP / TLS / HTTP/2 negotiation failed)
  • Error detail contains text/html (CDN / proxy returned an HTML error page instead of gRPC)
  • Error detail contains HTTP status code or Bad gRPC response
  • HttpRequestException (DNS failure, TCP refused, etc — gRPC channel's HTTP-layer wrapper)

Application failures (Unauthenticated, PermissionDenied with proper gRPC trailers, InvalidArgument, etc) propagate as-is — they aren't network errors, retrying via REST would produce the same result.

Configuration reference

public sealed class AxowlIntegrityClientOptions
{
    public string ServerAddress     { get; set; } = "https://testgrpc.axowl.com";  // gRPC endpoint
    public string RestServerAddress { get; set; } = "https://testapi.axowl.com";   // REST endpoint
    public string ApiKey            { get; set; } = "";                            // required
    public TransportMode Transport  { get; set; } = TransportMode.Grpc;
}

The defaults point at the environment Axowl serves today, so you do not have to set them.

Verifying (VerifyAsync)

Sealing alone does not prove anything to anyone. To answer "is this record genuine?" you have to recompute the hash from the row in front of you and compare it to what we anchored — and until 0.1.0-rc.12 there was no way to ask us the second half. Do not answer that question from your own status column: the database that stores the row also stores the column.

// 1. Recompute from the row AS IT STANDS NOW, over the same canonical fields you sealed with.
var canonical = JsonSerializer.Serialize(new { order.Id, order.Total, order.Status });
var recomputed = hasher.ComputeHash(
    "Order", order.Id, canonical, order.PreviousHash, order.SequenceNumber, actor: null);

// 2. Ask what we hold.
var check = await client.VerifyAsync(new VerifyRequest(
    EntityType:         "Order",
    EntityId:           order.Id,
    OrganizationId:     orgId,
    ClientComputedHash: recomputed,
    SequenceNumber:     order.SequenceNumber));   // 0 = latest anchor

if (!check.IsGenuine) { /* do not treat this record as proven */ }

IsGenuine is true for exactly one verdict, Match. Read it rather than comparing hashes yourself — the other three are all "not proven", for different reasons:

Verdict Means Do not read as
Match The anchored hash equals what you just computed. —
Mismatch An anchor exists and differs. The row changed after sealing. —
NotAnchored Nothing was ever anchored here. "valid" — it is unproven
NotChecked You sent no hash, so nothing was compared. "valid" — silence is not agreement

Notes:

  • SequenceNumber: 0 asks about the latest anchor, which is what a re-sealed (edited) record wants. Pin a number to audit one specific version. LatestSequenceNumber comes back either way, so a caller pinning an old position can tell it is holding a stale one.
  • SealedAt is our clock, not yours. That is what makes back-dating impossible: a record forged today cannot be given an anchor from last year.
  • The comparison is against the hash you anchored, reported as AnchoredHash. ServerSealedHash is our own derivation and is returned for the record only.
  • Read-only: verifying never creates or moves a chain position, and never costs you one.

REST endpoint surface

The REST path is auto-generated from the proto via Microsoft.AspNetCore.Grpc.JsonTranscoding. Direct curl example:

curl -X POST https://testapi.axowl.com/v1/integrity/seal \
  -H "Authorization: Bearer $AXOWL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entityType": "Wallet",
    "entityId": "00000000000000000000000000000001",
    "organizationId": "<your-org-id-no-hyphens>",
    "sequenceNumber": 1,
    "clientComputedHash": "<sha256-hex>",
    "previousHash": "<sha256-hex or 64-zeros>",
    "occurredAt": "2026-05-26T12:00:00Z"
  }'

And the verify side of it:

curl -X POST https://testapi.axowl.com/v1/integrity/verify \
  -H "Authorization: Bearer $AXOWL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entityType": "Wallet",
    "entityId": "00000000000000000000000000000001",
    "organizationId": "<your-org-id-no-hyphens>",
    "clientComputedHash": "<sha256-hex recomputed from the row now>",
    "sequenceNumber": 0
  }'
{
  "verdict": "VERIFY_VERDICT_MATCH",
  "anchoredHash": "<sha256-hex>",
  "serverSealedHash": "<sha256-hex>",
  "sealedAt": "2026-08-30T04:15:22Z",
  "chainAnchorId": "…",
  "sequenceNumber": "1",
  "latestSequenceNumber": "1"
}

⚠️ Transcoding omits proto3 default values, so a field you expect can simply be absent — notably verdict is missing when it is VERIFY_VERDICT_UNSPECIFIED, and sequenceNumber arrives as a string (int64). Treat a missing verdict as not-proven, never as a pass.

Error JSON shape (REST mapping of gRPC status):

{"code": 16, "message": "Missing or malformed Authorization header...", "details": []}

gRPC status codes: 0=OK, 7=PermissionDenied, 16=Unauthenticated, 3=InvalidArgument.

Hybrid scope (optional)

API keys can be scoped to an AppGroup or Application. When you create such a key, pass the matching ID:

await client.SealAsync(new SealRequest(
    EntityType: "Wallet",
    EntityId: entityId,
    OrganizationId: orgId,
    SequenceNumber: 1,
    ClientComputedHash: clientHash,
    PreviousHash: prevHash,
    OccurredAt: DateTime.UtcNow,
    AppGroupId: yourAppGroupId,        // required if key is AppGroup-scoped
    ApplicationId: yourApplicationId)); // required if key is Application-scoped

Server rejects with PermissionDenied if scope doesn't match.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.1 56 9/26/2026