Skip to main content

Anchor objects in S3 / R2 / GCS / Azure Blob to BSV via Satsignal. Files stay in your bucket; portable proof sidecars land next to them.

Project description

satsignal-blob

Anchor objects in S3 / R2 / GCS / Azure Blob to BSV via Satsignal. Files stay in your bucket; portable proof sidecars (.mbnt + .proof.json) land next to them.

Keep your files where they are. Satsignal gives them independent proof.

pip install satsignal-blob[s3]    # or [gcs], [azure], [all]

What it does

For each object under a bucket prefix, satsignal-blob streams the bytes through sha256, posts the digest (not the file) to Satsignal's POST /api/v1/anchors, and writes two sidecar objects next to the original:

sibling what it is who uses it
contract.pdf.mbnt full evidence bundle (zip) — txid, canonical doc, miner acceptance, BSV chain anchor anyone who needs to verify the receipt offline against a public BSV explorer
contract.pdf.proof.json one-page summary — sha256, txid, receipt URL, bundle URL, timestamp humans, dashboards, grep / jq pipelines

The original contract.pdf is untouched. The file bytes never leave your bucket — Satsignal only sees the sha256.

CLI quickstart

Vocabulary: folder is the preferred public name; matter is a frozen legacy alias and keeps working forever. The CLI accepts --folder alongside --matter; the library accepts folder_slug= alongside matter_slug=. Either --folder or --matter is required (legacy invocations that pass --matter are unaffected). If both are set to different values the command fails loudly; equal or single is fine. The .proof.json sidecar now carries both folder_slug and matter_slug (and proof_id/bundle_id, proof_url/receipt_url). The HTTP request to the Satsignal API still sends the frozen matter_slug key, so older / self-hosted servers keep working unchanged. (Examples below still use the legacy spelling — both work.)

export SATSIGNAL_API_KEY=sk_...

# Anchor every PDF under a prefix, cap at 50 attempts.
satsignal-blob anchor s3://my-bucket/contracts/2026/ \
    --matter contracts-2026 \
    --include '*.pdf' \
    --max-files 50

# Dry-run first to preview hashes + plan without firing any anchors:
satsignal-blob anchor s3://my-bucket/contracts/2026/ \
    --matter contracts-2026 --include '*.pdf' --dry-run

A successful run prints one line per object plus a summary on stderr:

[OK]   s3://my-bucket/contracts/2026/a.pdf  sha=ab1234567890 bundle=87f9c2b4… txid=e7c4f1…
[OK]   s3://my-bucket/contracts/2026/b.pdf  sha=cd5678901234 bundle=2a91b3e5… txid=e7c4f1…
[SKIP] s3://my-bucket/contracts/2026/c.pdf  (.mbnt sidecar exists; --force to re-anchor)
satsignal-blob: anchored=2 duplicate=0 skipped_existing=1 skipped_sidecar=0 failed=0

Pass --json to emit one JSON object per line — pipe through jq / fluentd / anything line-oriented.

Supported backends

satsignal-blob resolves URIs through fsspec. Install the optional dep that matches your storage:

storage URI scheme install extra backend
AWS S3 / MinIO / Wasabi / B2 / Cloudflare R2 s3:// pip install satsignal-blob[s3] s3fs
Google Cloud Storage gs:// pip install satsignal-blob[gcs] gcsfs
Azure Blob Storage az:// pip install satsignal-blob[azure] adlfs
Local filesystem /path/ or file:/// (none) built-in

Cloudflare R2 is S3-compatible — use s3:// and configure the AWS_ENDPOINT_URL_S3 env var to point at your R2 endpoint.

Library API

from satsignal_blob import anchor_prefix

for outcome in anchor_prefix(
    "s3://my-bucket/contracts/2026/",
    api_key="sk_...",
    matter_slug="contracts-2026",
    include=["*.pdf"],
):
    if outcome.status == "anchored":
        print(outcome.receipt_url)

Or a single object:

from satsignal_blob import anchor_object

result = anchor_object(
    "s3://my-bucket/contracts/2026/contract.pdf",
    api_key="sk_...",
    matter_slug="contracts-2026",
)
print(result.txid, result.receipt_url)

Event-driven (S3 → Lambda)

For real-time anchoring on every PUT, wire an S3 ObjectCreated:* event to a Lambda that calls anchor_object. Copy examples/lambda_handler.py into a Python 3.11 Lambda, set three env vars, and you're done:

import os, urllib.parse
from satsignal_blob import anchor_object

API_KEY = os.environ["SATSIGNAL_API_KEY"]
WORKSPACE_SLUG = os.environ["SATSIGNAL_WORKSPACE_SLUG"]
MATTER_SLUG = os.environ["SATSIGNAL_MATTER_SLUG"]


def lambda_handler(event, context):
    results = []
    for record in event.get("Records", []):
        bucket = record["s3"]["bucket"]["name"]
        key = urllib.parse.unquote_plus(record["s3"]["object"]["key"])
        if key.endswith((".mbnt", ".proof.json")):
            continue  # don't anchor sidecars
        outcome = anchor_object(
            f"s3://{bucket}/{key}",
            api_key=API_KEY,
            matter_slug=MATTER_SLUG,
        )
        results.append({"key": key, "status": outcome.status,
                        "bundle_id": outcome.bundle_id})
    return {"results": results}

Same pattern works for Cloud Functions (GCS finalize) and Azure Functions (blob trigger) — just swap the scheme and the event shape.

What this proves (and what it doesn't)

A Satsignal anchor over sha256(file) proves:

  • The operator of this Satsignal workspace knew this sha256 by the block time of the on-chain anchor. Anyone with the file can later recompute the sha and verify against the chain entry — without trusting Satsignal or your storage provider.

It does not prove:

  • That the file existed before the anchor. The chain only records when the sha entered the ledger.
  • The identity of whoever uploaded it. The receipt is "this sha was anchored by this workspace at this time" — not "this person did it."

For workflows that need before-the-anchor existence (e.g. press- embargo proofs), pair satsignal-blob with a commit-reveal flow via satsignal-cli or the /commit_reveal.py helper.

Threat model

  • sha-only. File bytes never leave your process. The Satsignal API only sees the digest + size + optional label + filename.
  • Untrusted attribute fields. label and filename are clipped server-side at 256 chars and validated for control substrings. Still — don't put secrets in either.
  • Fail-open per object. One file's API failure does not abort the walk. The exit code is 1 if any file failed; the per-file error is in the JSON output.
  • No bytes on chain. Satsignal never anchors file contents — only the digest. This is by design and is not a config option.

Related packages

  • satsignal-cli — general-purpose anchor + verify CLI; can verify any .mbnt file written by satsignal-blob.
  • satsignal-mcp — MCP server exposing the same 5 primitives to MCP-aware agents.
  • satsignal-otel — anchor OpenTelemetry spans (failed evals, release gates).
  • satsignal-action — GitHub Action: anchor build artifacts on every release.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

satsignal_blob-0.2.0.tar.gz (25.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

satsignal_blob-0.2.0-py3-none-any.whl (19.0 kB view details)

Uploaded Python 3

File details

Details for the file satsignal_blob-0.2.0.tar.gz.

File metadata

  • Download URL: satsignal_blob-0.2.0.tar.gz
  • Upload date:
  • Size: 25.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for satsignal_blob-0.2.0.tar.gz
Algorithm Hash digest
SHA256 0e4066f230c72d20d97a09a5d28774b083ac761dd3a76ddf4600357f7b5238dc
MD5 cfca8d49e16a661c852eef0941d29284
BLAKE2b-256 18cfce0f740f4a64fde9a6ad3eb5e75447a4ec55efffeb3405d8c1c593972274

See more details on using hashes here.

File details

Details for the file satsignal_blob-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: satsignal_blob-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 19.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for satsignal_blob-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b5973dd765b7650d4e9ac68c64e01e284a91e50273d2886d0cbc5d73e840ad1d
MD5 bc241121f14a13a27aa285534d62b5ab
BLAKE2b-256 28952121082098e5cccedabcf947dfdbb9f2197260919c49129e951c3ec52a60

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page