Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion db_github.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Package dalgo2ghingitdb provides a DALgo database adapter for reading inGitDB repositories from GitHub using the GitHub API.
// It supports read-only access to public repositories with no authentication required.
// Future versions will support authentication and write operations for private repositories.
// Authenticated access is configured either with a static Config.Token or, for
// rotating credentials such as short-lived GitHub App installation tokens,
// with a Config.TokenProvider that is consulted on every request.
package dalgo2ghingitdb

import (
Expand Down
51 changes: 29 additions & 22 deletions file_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,20 @@ import (

// Config defines connection settings for reading an inGitDB repository from GitHub.
type Config struct {
Owner string
Repo string
Ref string
Token string
Owner string
Repo string
Ref string

// Token is a static GitHub token (e.g. a classic/fine-grained PAT).
// If TokenProvider is nil and Token is non-empty, Token is wrapped in
// StaticTokenProvider automatically, preserving legacy behavior.
Token string

// TokenProvider, when set, supplies the token per request and takes
// precedence over Token. Use it to inject rotating credentials such as
// short-lived GitHub App installation tokens.
TokenProvider TokenProvider

APIBaseURL string
HTTPClient *http.Client
}
Expand All @@ -30,6 +40,19 @@ func (c Config) validate() error {
return nil
}

// tokenProvider resolves the effective TokenProvider: an explicit
// Config.TokenProvider wins; otherwise a non-empty Config.Token is wrapped in
// a StaticTokenProvider; nil means unauthenticated requests.
func (c Config) tokenProvider() TokenProvider {
if c.TokenProvider != nil {
return c.TokenProvider
}
if c.Token != "" {
return StaticTokenProvider(c.Token)
}
return nil
}

// FileReader reads repository files by path from GitHub.
type FileReader interface {
ReadFile(ctx context.Context, path string) (content []byte, found bool, err error)
Expand All @@ -46,25 +69,9 @@ func NewGitHubFileReader(cfg Config) (FileReader, error) {
if err != nil {
return nil, err
}
opts := make([]github.ClientOptionsFunc, 0, 3) // max 3: HTTPClient + Token + APIBaseURL
httpClient := cfg.HTTPClient
if httpClient == nil {
httpClient = http.DefaultClient
}
opts = append(opts, github.WithHTTPClient(httpClient))
if cfg.Token != "" {
opts = append(opts, github.WithAuthToken(cfg.Token))
}
if cfg.APIBaseURL != "" {
baseURL := cfg.APIBaseURL
if !strings.HasSuffix(baseURL, "/") {
baseURL += "/"
}
opts = append(opts, github.WithEnterpriseURLs(baseURL, baseURL))
}
client, err := github.NewClient(opts...)
client, err := newGitHubAPIClient(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create github client: %w", err)
return nil, err
}
return &githubFileReader{cfg: cfg, client: client}, nil
}
Expand Down
50 changes: 50 additions & 0 deletions github_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package dalgo2ghingitdb

import (
"fmt"
"net/http"
"strings"

"github.com/google/go-github/v88/github"
)

// newGitHubAPIClient builds the go-github client from a Config. Shared by
// NewGitHubFileReader and NewTreeWriter (which previously duplicated this
// construction logic inline).
//
// Fable refactoring: authorization moved from a construction-time
// github.WithAuthToken(cfg.Token) to the per-request tokenProviderTransport,
// so tokens can rotate per operation (short-lived GitHub App installation
// tokens; see TokenProvider). A bare Config.Token keeps working unchanged —
// Config.tokenProvider() wraps it in StaticTokenProvider.
func newGitHubAPIClient(cfg Config) (*github.Client, error) {
httpClient := cfg.HTTPClient
if httpClient == nil {
httpClient = http.DefaultClient
}
if provider := cfg.tokenProvider(); provider != nil {
base := httpClient.Transport
if base == nil {
base = http.DefaultTransport
}
// Shallow-copy the client so a caller-supplied http.Client is never
// mutated by the adapter.
authClient := *httpClient
authClient.Transport = tokenProviderTransport{base: base, provider: provider}
httpClient = &authClient
}
opts := make([]github.ClientOptionsFunc, 0, 2) // max 2: HTTPClient + APIBaseURL
opts = append(opts, github.WithHTTPClient(httpClient))
if cfg.APIBaseURL != "" {
baseURL := cfg.APIBaseURL
if !strings.HasSuffix(baseURL, "/") {
baseURL += "/"
}
opts = append(opts, github.WithEnterpriseURLs(baseURL, baseURL))
}
client, err := github.NewClient(opts...)
if err != nil {
return nil, fmt.Errorf("failed to create github client: %w", err)
}
return client, nil
}
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoG
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/dal-go/dalgo v0.62.2 h1:rA+k9QCgS0UnZV0oonvDtA+QoM3Wj1doVBWo1higBTw=
github.com/dal-go/dalgo v0.62.2/go.mod h1:wwaibB/UlzwAY1r9KZw5JCis+aroIwAu5WQJ/aoWZiU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
Expand Down Expand Up @@ -41,6 +42,7 @@ github.com/strongo/random v0.0.1/go.mod h1:/pSI+SjBNLBkjljNtVdYr6ERddA+LqSa87o0/
go.starlark.net v0.0.0-20260613233743-8ba36ccb83fb h1:NGUBN0jbH0IR3msRslALnoxlySm+6YvVKvVDjdDJrlA=
go.starlark.net v0.0.0-20260613233743-8ba36ccb83fb/go.mod h1:Iue6g6iirlfLoVi/DYCi5/x0h/bAOuWF3dULTKpt2Vo=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
67 changes: 67 additions & 0 deletions token_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package dalgo2ghingitdb

import (
"context"
"fmt"
"net/http"
)

// TokenProvider supplies the GitHub token used to authorize API calls.
// It is invoked once per outgoing HTTP request, so implementations can rotate
// tokens (e.g. mint short-lived GitHub App installation tokens on demand)
// without rebuilding the adapter. Returning an empty token with a nil error
// sends the request unauthenticated (fine for public-repo reads).
//
// See docs/roadmaps/ovdb-access-tokens-grants.md in sneat-co/backstage
// (Decision 3.4): the sneat-go backend injects a provider that mints 1-hour
// installation tokens with an in-memory cache; CLI/dev injects
// StaticTokenProvider with a PAT.
type TokenProvider interface {
Token(ctx context.Context) (string, error)
}

// TokenProviderFunc adapts a plain function to the TokenProvider interface.
type TokenProviderFunc func(ctx context.Context) (string, error)

// Token implements TokenProvider.
func (f TokenProviderFunc) Token(ctx context.Context) (string, error) {
return f(ctx)
}

// StaticTokenProvider returns a TokenProvider that always yields the given
// fixed token (e.g. a personal access token for CLI/dev usage). It preserves
// the legacy Config.Token behavior: a non-empty Config.Token is wrapped in a
// StaticTokenProvider automatically when Config.TokenProvider is nil.
func StaticTokenProvider(token string) TokenProvider {
return staticTokenProvider(token)
}

type staticTokenProvider string

func (p staticTokenProvider) Token(context.Context) (string, error) {
return string(p), nil
}

// tokenProviderTransport is an http.RoundTripper that asks the TokenProvider
// for a token on every request and injects it as a Bearer Authorization
// header. Provider errors abort the request and surface to the caller as
// regular operation errors (wrapped by the http.Client, then by
// wrapGitHubError at the call sites).
type tokenProviderTransport struct {
base http.RoundTripper
provider TokenProvider
}

func (t tokenProviderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
token, err := t.provider.Token(req.Context())
if err != nil {
return nil, fmt.Errorf("token provider failed: %w", err)
}
if token != "" {
// RoundTrippers must not mutate the caller's request; clone before
// setting the header.
req = req.Clone(req.Context())
req.Header.Set("Authorization", "Bearer "+token)
}
return t.base.RoundTrip(req)
}
Loading