Skip to content

feat(btcindexer): secure block ingestion with bearer auth#351

Merged
sczembor merged 9 commits intomasterfrom
stan/authentication-block-ingestor
Feb 10, 2026
Merged

feat(btcindexer): secure block ingestion with bearer auth#351
sczembor merged 9 commits intomasterfrom
stan/authentication-block-ingestor

Conversation

@sczembor
Copy link
Contributor

@sczembor sczembor commented Feb 9, 2026

Description

Closes: #339


Author Checklist

All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.

I have...

  • included the correct type prefix in the PR title
  • added ! to the type prefix if API or client breaking change
  • added appropriate labels to the PR
  • provided a link to the relevant issue or specification
  • added a changelog entry to CHANGELOG.md
  • included doc comments for public functions
  • updated the relevant documentation or specification
  • reviewed "Files changed" and left comments if necessary

Summary by Sourcery

Secure the Bitcoin block ingestion endpoint with bearer-token authentication and align clients with the new authenticated /bitcoin/blocks API.

New Features:

  • Add bearer-token authentication for the /bitcoin/blocks ingestion endpoint using a secret configured in the worker environment.
  • Extend the TypeScript and Go BTC indexer clients to optionally include a bearer token when sending block ingestion requests.

Enhancements:

  • Change the block ingestion REST path to /bitcoin/blocks and switch the HTTP method from POST to PUT for block submissions.
  • Export the block-ingestor router for reuse and update the content type for msgpack payloads.
  • Add a RELAYER_AUTH_TOKEN secret to the worker environment configuration for authenticated relayers.

Tests:

  • Adjust the BTC indexer API integration test to construct the client with an explicit auth token parameter.

…nit tests

Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
@sczembor sczembor requested a review from a team as a code owner February 9, 2026 12:58
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 9, 2026

Reviewer's Guide

Adds bearer-token based authentication to the block ingestor /bitcoin/blocks endpoint and its clients, updates HTTP method and path conventions, and wires the auth secret into the worker environment and Go client.

Sequence diagram for authenticated PUT /bitcoin/blocks ingest flow

sequenceDiagram
    actor Relayer
    participant Client as BtcIndexerClient
    participant Worker as BlockIngestorWorker
    participant Auth as AuthModule
    participant Secret as RELAYER_AUTH_TOKEN
    participant KV as BtcBlocks
    participant Queue as BlockQueue

    Relayer->>Client: putBlocks(blocks)
    Client->>Client: PutBlocksReq.encode(blocks)
    Client->>Worker: HTTP PUT /bitcoin/blocks
    activate Worker
    Worker->>Auth: isAuthorized(request, env)
    activate Auth
    Auth->>Secret: get()
    Secret-->>Auth: secretValue
    Auth-->>Worker: authorized or unauthorized
    deactivate Auth

    alt unauthorized
        Worker-->>Client: 401 Unauthorized
    else authorized
        Worker->>Worker: PutBlocksReq.decode(body)
        Worker->>KV: store blocks
        Worker->>Queue: enqueue block events
        Worker-->>Client: 2xx Success
    end
    deactivate Worker

    Client-->>Relayer: return (void or error)
Loading

Class diagram for updated block ingestor clients and auth

classDiagram
    class BtcIndexerClient {
        - string url
        - string authToken
        + BtcIndexerClient(url string, authToken string)
        + putBlocks(blocks PutBlock[]) Promise~void~
    }

    class PutBlocksReqTS {
        + encode(blocks PutBlock[]) Uint8Array
        + decode(buffer ArrayBuffer) PutBlock[]
    }

    class Env {
        + BtcBlocks KVNamespace
        + RELAYER_AUTH_TOKEN SecretsStoreSecret
        + BtcIndexer Service
        + BlockQueue Queue
    }

    class AuthModule {
        + isAuthorized(request Request, env Env) Promise~boolean~
    }

    class GoClient {
        - string baseUrl
        - string authToken
        - http.Client c
        + NewClient(workerUrl string, authToken string) GoClient
        + PutBlocks(putBlocks PutBlocksReq) *http.Response, error
    }

    class PutBlocksReqGo {
        + MsgpackEncode() ([]byte, error)
    }

    BtcIndexerClient --> PutBlocksReqTS : uses
    GoClient --> PutBlocksReqGo : uses
    AuthModule --> Env : reads
    BtcIndexerClient --> Env : targets worker using env config
    GoClient --> Env : targets worker using env config
Loading

File-Level Changes

Change Details Files
Introduce bearer-token authentication for the block ingestor HTTP endpoint and enforce it on PUT /bitcoin/blocks requests.
  • Export the itty-router instance and register a PUT /bitcoin/blocks route that checks authorization before decoding and ingesting blocks
  • Return HTTP 401 Unauthorized when the incoming request is not authorized
  • Remove the previous TODO about adding authentication from the Worker fetch handler
packages/block-ingestor/src/index.ts
Add an isAuthorized helper that validates Authorization headers against a secret stored in the Worker environment.
  • Implement isAuthorized that extracts and normalizes the Bearer token from the Authorization header
  • Fetch the RELAYER_AUTH_TOKEN secret from the environment and compare it to the presented token
  • Return a boolean to gate access to protected endpoints
packages/block-ingestor/src/auth.ts
Update TypeScript worker environment types to include the RELAYER_AUTH_TOKEN secret used for authentication.
  • Extend Env typing with RELAYER_AUTH_TOKEN: SecretsStoreSecret so auth logic can access it safely
  • Regenerate Wrangler type header metadata comment
packages/block-ingestor/worker-configuration.d.ts
Update the JavaScript BtcIndexerClient to use the new authenticated PUT /bitcoin/blocks API.
  • Change RestPath.blocks from root path to /bitcoin/blocks
  • Change the HTTP method from POST to PUT for block ingestion requests
  • Adjust Content-Type from application/vnd.msgpack to application/msgpack
  • Extend BtcIndexerClient constructor to accept an optional auth token and store it privately
  • Include Authorization: Bearer header when an auth token is configured
packages/block-ingestor/src/api/client.ts
Update the Go btcindexer client to support bearer-token authentication while sending msgpack block ingestion requests.
  • Add an authToken field to the Client struct and plumb it through NewClient
  • Set Content-Type to application/msgpack on PutBlocks requests
  • Conditionally set Authorization: Bearer when the authToken is non-empty
  • Update the integration test to construct the Client with an empty auth token argument
api/btcindexer/api.go
api/btcindexer/api_test.go

Assessment against linked issues

Issue Objective Addressed Explanation
#339 Implement authentication on the block-ingestor worker's block ingestion endpoint so that only authorized requests can ingest blocks.
#339 Update the relayer/client code used to send block ingestion requests so that it supports and uses the new authentication mechanism.

Possibly linked issues

  • #unknown: PR implements bearer token auth and env secret validation for Block Ingestor, matching the authentication issue
  • worker: Authentication for block ingestion #339: PR adds bearer auth on /bitcoin/blocks and updates relayer/client, exactly implementing the requested worker authentication.
  • #unknown: PR adds Bearer auth to btcindexer PUT /bitcoin/blocks and clients, matching issue’s authentication requirement albeit stricter config.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sczembor
Copy link
Contributor Author

sczembor commented Feb 9, 2026

@sourcery-ai title

@sourcery-ai sourcery-ai bot changed the title Stan/authentication block ingestor feat(btcindexer): secure block ingestion with bearer auth Feb 9, 2026
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The request path, method, and Content-Type are now duplicated across the TypeScript client, Go client, and worker handler; consider centralizing these API contract details in a shared definition to avoid future divergence.
  • In isAuthorized, a misconfigured or missing RELAYER_AUTH_TOKEN will cause all requests to be rejected without any signal; consider surfacing this as a clear startup/runtime error or logging a configuration warning so it’s easier to diagnose.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The request path, method, and `Content-Type` are now duplicated across the TypeScript client, Go client, and worker handler; consider centralizing these API contract details in a shared definition to avoid future divergence.
- In `isAuthorized`, a misconfigured or missing `RELAYER_AUTH_TOKEN` will cause all requests to be rejected without any signal; consider surfacing this as a clear startup/runtime error or logging a configuration warning so it’s easier to diagnose.

## Individual Comments

### Comment 1
<location> `api/btcindexer/api_test.go:48` </location>
<code_context>
 	pb := PutBlock{Network: NetworkRegtest, Height: 156, Block: blockBz}

-	c := NewClient("http://localhost:8787")
+	c := NewClient("http://localhost:8787", "")
 	resp, err := c.PutBlocks(PutBlocksReq{pb})
 	assert.NilError(t, err)
</code_context>

<issue_to_address>
**suggestion (testing):** Add tests to verify that `NewClient` with a non-empty auth token sets the `Authorization` header in `PutBlocks` requests.

This update only adapts to the new `NewClient` signature and still passes an empty token, so the new auth behavior isn’t actually tested. Please add a test (or table-driven tests) that builds a `Client` with both non-empty and empty `authToken` values, calls `PutBlocks`, and asserts that:

- `Authorization: Bearer <token>` is set when the token is non-empty
- The `Authorization` header is omitted when the token is empty

You can do this by injecting an `http.Client` with a custom `Transport` that captures the outgoing request instead of performing a real HTTP call.

Suggested implementation:

```golang
	pb := PutBlock{Network: NetworkRegtest, Height: 156, Block: blockBz}

	c := NewClient("http://localhost:8787", "")
	resp, err := c.PutBlocks(PutBlocksReq{pb})
	assert.NilError(t, err)
	respBody, err := io.ReadAll(resp.Body)

}

func TestClientPutBlocksAuthorizationHeader(t *testing.T) {
	tests := []struct {
		name     string
		token    string
		wantAuth string
	}{
		{
			name:     "no auth token omits Authorization header",
			token:    "",
			wantAuth: "",
		},
		{
			name:     "non-empty auth token sets Authorization header",
			token:    "my-secret-token",
			wantAuth: "Bearer my-secret-token",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var capturedReq *http.Request

			// Custom RoundTripper that captures the request instead of doing a real HTTP call.
			rt := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
				capturedReq = req

				// Return a minimal successful HTTP response so PutBlocks can proceed.
				return &http.Response{
					StatusCode: http.StatusOK,
					Body:       io.NopCloser(strings.NewReader(`{}`)),
					Header:     make(http.Header),
					Request:    req,
				}, nil
			})

			httpClient := &http.Client{
				Transport: rt,
			}

			client := NewClient("http://localhost:8787", tt.token)

			// Inject our custom HTTP client so we can capture the outgoing request.
			client.httpClient = httpClient

			_, err := client.PutBlocks(PutBlocksReq{})
			assert.NilError(t, err)

			if capturedReq == nil {
				t.Fatalf("expected request to be captured")
			}

			gotAuth := capturedReq.Header.Get("Authorization")
			if tt.wantAuth == "" {
				if gotAuth != "" {
					t.Fatalf("expected no Authorization header, got %q", gotAuth)
				}
			} else {
				if gotAuth != tt.wantAuth {
					t.Fatalf("expected Authorization header %q, got %q", tt.wantAuth, gotAuth)
				}
			}
		})
	}

```

1. Ensure the following helper type is defined in this file (at top-level, outside any function), to satisfy `http.RoundTripper`:

```go
type roundTripperFunc func(*http.Request) (*http.Response, error)

func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
	return f(req)
}
```

2. Make sure the `Client` type used by `NewClient` exposes a way to inject a custom `*http.Client`. The test above assumes a field `httpClient`:
   ```go
   type Client struct {
       baseURL    string
       authToken  string
       httpClient *http.Client
   }
   ```
   If this field is not currently exported/available, either:
   - Add it to `Client`, initialized in `NewClient` (e.g., `httpClient: http.DefaultClient`), and keep using it inside `PutBlocks`, or
   - Provide a setter method (e.g., `func (c *Client) SetHTTPClient(hc *http.Client)`) and update the test to call that instead of assigning the field directly.

3. Confirm the following imports exist at the top of `api/btcindexer/api_test.go`:
   ```go
   import (
       "io"
       "net/http"
       "strings"
       "testing"

       "gotest.tools/v3/assert"
   )
   ```
   Adjust to match existing import style and any additional imports already present.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Signed-off-by: sczembor <stanislaw.czembor@gmail.com>
Copy link
Contributor

@robert-zaremba robert-zaremba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pre-approved

@sczembor sczembor enabled auto-merge (squash) February 10, 2026 10:44
@sczembor sczembor merged commit 22a77a9 into master Feb 10, 2026
12 checks passed
@sczembor sczembor disabled auto-merge February 10, 2026 10:46
@sczembor sczembor deleted the stan/authentication-block-ingestor branch February 10, 2026 10:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

worker: Authentication for block ingestion

2 participants