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
<PackageReference Include="Axowl.Sdk.Integrity.Client" Version="0.2.1" />
<PackageVersion Include="Axowl.Sdk.Integrity.Client" Version="0.2.1" />
<PackageReference Include="Axowl.Sdk.Integrity.Client" />
paket add Axowl.Sdk.Integrity.Client --version 0.2.1
#r "nuget: Axowl.Sdk.Integrity.Client, 0.2.1"
#:package Axowl.Sdk.Integrity.Client@0.2.1
#addin nuget:?package=Axowl.Sdk.Integrity.Client&version=0.2.1
#tool nuget:?package=Axowl.Sdk.Integrity.Client&version=0.2.1
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:
RpcExceptionwith statusUnavailable(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 codeorBad 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: 0asks about the latest anchor, which is what a re-sealed (edited) record wants. Pin a number to audit one specific version.LatestSequenceNumbercomes back either way, so a caller pinning an old position can tell it is holding a stale one.SealedAtis 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.ServerSealedHashis 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.
Related packages
Axowl.Sdk.Integrity.Hashing— client-side SHA-256 + Merkle root computation. Use the same algorithm as the server.Axowl.Sdk.Integrity.Abstractions— DTOs + interfaces, infrastructure-free.
Links
- Architecture & transport layer details: docs/2026-05-26-sdk-transport-gap-and-a-path.md
- API surface: docs/2026-05-26-axowl-sdk-api-surface.md
| Product | Versions 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. |
-
net10.0
- Axowl.Sdk.Integrity.Abstractions (>= 0.2.0)
- Axowl.Sdk.Integrity.Hashing (>= 0.3.1)
- Google.Api.CommonProtos (>= 2.16.0)
- Google.Protobuf (>= 3.28.2)
- Grpc.Net.Client (>= 2.66.0)
- Grpc.Net.ClientFactory (>= 2.66.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Http (>= 10.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
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 | 51 | 9/26/2026 |