mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-31 05:07:09 +00:00
99 lines
3.5 KiB
Go
99 lines
3.5 KiB
Go
// Package testpds provides an in-process fake ATProto PDS for integration
|
|
// tests. It implements the minimal XRPC surface that AppView and Hold call
|
|
// during push/pull (createSession, getServiceAuth, repo.put/get/list/delete
|
|
// Record, identity.resolveHandle), plus an identity.Directory implementation
|
|
// so DID resolution short-circuits the network.
|
|
package testpds
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/bluesky-social/indigo/atproto/atcrypto"
|
|
"github.com/bluesky-social/indigo/atproto/identity"
|
|
"github.com/bluesky-social/indigo/atproto/syntax"
|
|
)
|
|
|
|
// Identity is a single account on the fake PDS. Each identity has its own
|
|
// K-256 signing keypair used to sign service-auth JWTs the Hold service
|
|
// verifies.
|
|
type Identity struct {
|
|
DID syntax.DID
|
|
Handle syntax.Handle
|
|
SigningKey *atcrypto.PrivateKeyK256
|
|
Password string // synthetic app-password; opaque to tests
|
|
AccessToken string // synthetic accessJwt returned by createSession
|
|
pdsURL string // base URL of the fake PDS serving this identity
|
|
}
|
|
|
|
// newIdentity allocates an identity with a fresh keypair. didHostEscaped is
|
|
// the percent-encoded host:port that did:web will encode in the DID, e.g.
|
|
// "127.0.0.1%3A45123".
|
|
func newIdentity(pdsURL, didHostEscaped, handle string) (*Identity, error) {
|
|
priv, err := atcrypto.GeneratePrivateKeyK256()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("generate K-256: %w", err)
|
|
}
|
|
hdl, err := syntax.ParseHandle(handle)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid handle %q: %w", handle, err)
|
|
}
|
|
didStr := fmt.Sprintf("did:web:%s:user:%s", didHostEscaped, hdl.String())
|
|
did, err := syntax.ParseDID(didStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid synthesized DID %q: %w", didStr, err)
|
|
}
|
|
rb := make([]byte, 16)
|
|
if _, err := rand.Read(rb); err != nil {
|
|
return nil, err
|
|
}
|
|
return &Identity{
|
|
DID: did,
|
|
Handle: hdl,
|
|
SigningKey: priv,
|
|
Password: "test-pass-" + hex.EncodeToString(rb[:4]),
|
|
AccessToken: "test-access-" + hex.EncodeToString(rb),
|
|
pdsURL: pdsURL,
|
|
}, nil
|
|
}
|
|
|
|
// toIndigoIdentity returns the indigo identity.Identity view of this account.
|
|
// It contains the same fields the production code reads when resolving DIDs:
|
|
// AlsoKnownAs (for handle), Services["atproto_pds"] (for the PDS URL), and
|
|
// Keys["atproto"] (for service-auth JWT verification).
|
|
func (i *Identity) toIndigoIdentity() (*identity.Identity, error) {
|
|
pub, err := i.SigningKey.PublicKey()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("derive public key: %w", err)
|
|
}
|
|
pubK256, ok := pub.(*atcrypto.PublicKeyK256)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected K-256 public key, got %T", pub)
|
|
}
|
|
pubMultibase := pubK256.Multibase()
|
|
return &identity.Identity{
|
|
DID: i.DID,
|
|
Handle: i.Handle,
|
|
AlsoKnownAs: []string{"at://" + i.Handle.String()},
|
|
Services: map[string]identity.ServiceEndpoint{
|
|
"atproto_pds": {Type: "AtprotoPersonalDataServer", URL: i.pdsURL},
|
|
},
|
|
Keys: map[string]identity.VerificationMethod{
|
|
"atproto": {Type: "Multikey", PublicKeyMultibase: pubMultibase},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// didWebForHost builds the percent-encoded did:web host component for the
|
|
// given listener address. did:web requires the port colon be encoded as %3A.
|
|
func didWebForHost(addr string) string {
|
|
host := strings.TrimPrefix(addr, "http://")
|
|
host = strings.TrimPrefix(host, "https://")
|
|
// url.QueryEscape would encode dots and slashes; we only need to swap the
|
|
// port colon. did:web also forbids userinfo, paths beyond the optional
|
|
// path segment, etc., which we don't generate.
|
|
return strings.Replace(host, ":", "%3A", 1)
|
|
}
|