this repo has no description

organize example

Changed files
+414 -387
cmd
+229
cmd/client_test/handle_auth.go
···
+
package main
+
+
import (
+
"encoding/json"
+
"fmt"
+
"net/url"
+
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
"github.com/gorilla/sessions"
+
oauth "github.com/haileyok/atproto-oauth-golang"
+
"github.com/labstack/echo-contrib/session"
+
"github.com/labstack/echo/v4"
+
"gorm.io/gorm/clause"
+
)
+
+
func (s *TestServer) handleLoginSubmit(e echo.Context) error {
+
handle := e.FormValue("handle")
+
if handle == "" {
+
return e.Redirect(302, "/login?e=handle-empty")
+
}
+
+
_, herr := syntax.ParseHandle(handle)
+
_, derr := syntax.ParseDID(handle)
+
+
if herr != nil && derr != nil {
+
return e.Redirect(302, "/login?e=handle-invalid")
+
}
+
+
var did string
+
+
if derr == nil {
+
did = handle
+
} else {
+
maybeDid, err := resolveHandle(e.Request().Context(), handle)
+
if err != nil {
+
return err
+
}
+
+
did = maybeDid
+
}
+
+
service, err := resolveService(ctx, did)
+
if err != nil {
+
return err
+
}
+
+
authserver, err := s.oauthClient.ResolvePDSAuthServer(ctx, service)
+
if err != nil {
+
return err
+
}
+
+
meta, err := s.oauthClient.FetchAuthServerMetadata(ctx, authserver)
+
if err != nil {
+
return err
+
}
+
+
dpopPrivateKey, err := oauth.GenerateKey(nil)
+
if err != nil {
+
return err
+
}
+
+
dpopPrivateKeyJson, err := json.Marshal(dpopPrivateKey)
+
if err != nil {
+
return err
+
}
+
+
parResp, err := s.oauthClient.SendParAuthRequest(
+
ctx,
+
authserver,
+
meta,
+
"",
+
scope,
+
dpopPrivateKey,
+
)
+
+
oauthRequest := &OauthRequest{
+
State: parResp.State,
+
AuthserverIss: meta.Issuer,
+
Did: did,
+
PdsUrl: service,
+
PkceVerifier: parResp.PkceVerifier,
+
DpopAuthserverNonce: parResp.DpopAuthserverNonce,
+
DpopPrivateJwk: string(dpopPrivateKeyJson),
+
}
+
+
if err := s.db.Create(oauthRequest).Error; err != nil {
+
return err
+
}
+
+
u, _ := url.Parse(meta.AuthorizationEndpoint)
+
u.RawQuery = fmt.Sprintf(
+
"client_id=%s&request_uri=%s",
+
url.QueryEscape(serverMetadataUrl),
+
parResp.Resp["request_uri"].(string),
+
)
+
+
sess, err := session.Get("session", e)
+
if err != nil {
+
return err
+
}
+
+
sess.Options = &sessions.Options{
+
Path: "/",
+
MaxAge: 300, // save for five minutes
+
HttpOnly: true,
+
}
+
+
// make sure the session is empty
+
sess.Values = map[interface{}]interface{}{}
+
sess.Values["oauth_state"] = parResp.State
+
sess.Values["oauth_did"] = did
+
+
if err := sess.Save(e.Request(), e.Response()); err != nil {
+
return err
+
}
+
+
return e.Redirect(302, u.String())
+
}
+
+
func (s *TestServer) handleCallback(e echo.Context) error {
+
resState := e.QueryParam("state")
+
resIss := e.QueryParam("iss")
+
resCode := e.QueryParam("code")
+
+
sess, err := session.Get("session", e)
+
if err != nil {
+
return err
+
}
+
+
sessState := sess.Values["oauth_state"]
+
sessDid := sess.Values["oauth_did"]
+
+
if resState == "" || resIss == "" || resCode == "" || sessState == "" || sessDid == "" {
+
return fmt.Errorf("request missing needed parameters")
+
}
+
+
if resState != sessState {
+
return fmt.Errorf("session state does not match response state")
+
}
+
+
var oauthRequest OauthRequest
+
if err := s.db.Raw("SELECT * FROM oauth_requests WHERE state = ? AND did = ?", sessState, sessDid).Scan(&oauthRequest).Error; err != nil {
+
return err
+
}
+
+
if err := s.db.Exec("DELETE FROM oauth_requests WHERE state = ? AND did = ?", sessState, sessDid).Error; err != nil {
+
return err
+
}
+
+
if resIss != oauthRequest.AuthserverIss {
+
return fmt.Errorf("incoming iss did not match authserver iss")
+
}
+
+
jwk, err := oauth.ParseKeyFromBytes([]byte(oauthRequest.DpopPrivateJwk))
+
if err != nil {
+
return err
+
}
+
+
initialTokenResp, err := s.oauthClient.InitialTokenRequest(
+
e.Request().Context(),
+
resCode,
+
resIss,
+
resIss,
+
oauthRequest.PkceVerifier,
+
oauthRequest.DpopAuthserverNonce,
+
jwk,
+
)
+
if err != nil {
+
return err
+
}
+
+
// TODO: resolve if needed
+
+
if initialTokenResp.Resp["scope"] != scope {
+
return fmt.Errorf("did not receive correct scopes from token request")
+
}
+
+
oauthSession := &OauthSession{
+
Did: oauthRequest.Did,
+
PdsUrl: oauthRequest.PdsUrl,
+
AuthserverIss: oauthRequest.AuthserverIss,
+
AccessToken: initialTokenResp.Resp["access_token"].(string),
+
RefreshToken: initialTokenResp.Resp["refresh_token"].(string),
+
DpopAuthserverNonce: initialTokenResp.DpopAuthserverNonce,
+
DpopPrivateJwk: oauthRequest.DpopPrivateJwk,
+
}
+
+
if err := s.db.Clauses(clause.OnConflict{
+
Columns: []clause.Column{{Name: "did"}},
+
UpdateAll: true,
+
}).Create(oauthSession).Error; err != nil {
+
return err
+
}
+
+
sess.Options = &sessions.Options{
+
Path: "/",
+
MaxAge: 86400 * 7,
+
HttpOnly: true,
+
}
+
+
// make sure the session is empty
+
sess.Values = map[interface{}]interface{}{}
+
sess.Values["did"] = oauthRequest.Did
+
+
if err := sess.Save(e.Request(), e.Response()); err != nil {
+
return err
+
}
+
+
return e.Redirect(302, "/")
+
}
+
+
func (s *TestServer) handleLogout(e echo.Context) error {
+
sess, err := session.Get("session", e)
+
if err != nil {
+
return err
+
}
+
+
sess.Options = &sessions.Options{
+
Path: "/",
+
MaxAge: -1,
+
HttpOnly: true,
+
}
+
+
if err := sess.Save(e.Request(), e.Response()); err != nil {
+
return err
+
}
+
+
return e.Redirect(302, "/")
+
}
+51
cmd/client_test/handle_post.go
···
+
package main
+
+
import (
+
"github.com/bluesky-social/indigo/api/atproto"
+
"github.com/bluesky-social/indigo/api/bsky"
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
"github.com/bluesky-social/indigo/lex/util"
+
"github.com/bluesky-social/indigo/xrpc"
+
"github.com/labstack/echo-contrib/session"
+
"github.com/labstack/echo/v4"
+
)
+
+
func (s *TestServer) handleMakePost(e echo.Context) error {
+
sess, err := session.Get("session", e)
+
if err != nil {
+
return err
+
}
+
+
did, ok := sess.Values["did"]
+
if !ok {
+
return e.Redirect(302, "/login")
+
}
+
+
var oauthSession OauthSession
+
if err := s.db.Raw("SELECT * FROM oauth_sessions WHERE did = ?", did).Scan(&oauthSession).Error; err != nil {
+
return err
+
}
+
+
args, err := authedReqArgsFromSession(&oauthSession)
+
if err != nil {
+
return err
+
}
+
+
post := bsky.FeedPost{
+
Text: "hello from atproto golang oauth client",
+
CreatedAt: syntax.DatetimeNow().String(),
+
}
+
+
input := atproto.RepoCreateRecord_Input{
+
Collection: "app.bsky.feed.post",
+
Repo: oauthSession.Did,
+
Record: &util.LexiconTypeDecoder{Val: &post},
+
}
+
+
var out atproto.RepoCreateRecord_Output
+
if err := s.xrpcCli.Do(e.Request().Context(), args, xrpc.Procedure, "application/json", "com.atproto.repo.createRecord", nil, input, &out); err != nil {
+
return err
+
}
+
+
return e.File(getFilePath("make-post.html"))
+
}
-377
cmd/client_test/main.go
···
import (
"context"
-
"encoding/json"
"fmt"
"html/template"
"io"
"log/slog"
-
"net"
"net/http"
-
"net/url"
"os"
-
"strings"
-
"github.com/bluesky-social/indigo/api/atproto"
-
"github.com/bluesky-social/indigo/api/bsky"
-
"github.com/bluesky-social/indigo/atproto/syntax"
-
"github.com/bluesky-social/indigo/lex/util"
-
"github.com/bluesky-social/indigo/xrpc"
"github.com/gorilla/sessions"
oauth "github.com/haileyok/atproto-oauth-golang"
_ "github.com/joho/godotenv/autoload"
···
"github.com/urfave/cli/v2"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
-
"gorm.io/gorm/clause"
)
var (
···
return e.JSON(200, s.jwksResponse)
}
-
func (s *TestServer) handleLoginSubmit(e echo.Context) error {
-
handle := e.FormValue("handle")
-
if handle == "" {
-
return e.Redirect(302, "/login?e=handle-empty")
-
}
-
-
_, herr := syntax.ParseHandle(handle)
-
_, derr := syntax.ParseDID(handle)
-
-
if herr != nil && derr != nil {
-
return e.Redirect(302, "/login?e=handle-invalid")
-
}
-
-
var did string
-
-
if derr == nil {
-
did = handle
-
} else {
-
maybeDid, err := resolveHandle(e.Request().Context(), handle)
-
if err != nil {
-
return err
-
}
-
-
did = maybeDid
-
}
-
-
service, err := resolveService(ctx, did)
-
if err != nil {
-
return err
-
}
-
-
authserver, err := s.oauthClient.ResolvePDSAuthServer(ctx, service)
-
if err != nil {
-
return err
-
}
-
-
meta, err := s.oauthClient.FetchAuthServerMetadata(ctx, authserver)
-
if err != nil {
-
return err
-
}
-
-
dpopPrivateKey, err := oauth.GenerateKey(nil)
-
if err != nil {
-
return err
-
}
-
-
dpopPrivateKeyJson, err := json.Marshal(dpopPrivateKey)
-
if err != nil {
-
return err
-
}
-
-
parResp, err := s.oauthClient.SendParAuthRequest(
-
ctx,
-
authserver,
-
meta,
-
"",
-
scope,
-
dpopPrivateKey,
-
)
-
-
oauthRequest := &OauthRequest{
-
State: parResp.State,
-
AuthserverIss: meta.Issuer,
-
Did: did,
-
PdsUrl: service,
-
PkceVerifier: parResp.PkceVerifier,
-
DpopAuthserverNonce: parResp.DpopAuthserverNonce,
-
DpopPrivateJwk: string(dpopPrivateKeyJson),
-
}
-
-
if err := s.db.Create(oauthRequest).Error; err != nil {
-
return err
-
}
-
-
u, _ := url.Parse(meta.AuthorizationEndpoint)
-
u.RawQuery = fmt.Sprintf(
-
"client_id=%s&request_uri=%s",
-
url.QueryEscape(serverMetadataUrl),
-
parResp.Resp["request_uri"].(string),
-
)
-
-
sess, err := session.Get("session", e)
-
if err != nil {
-
return err
-
}
-
-
sess.Options = &sessions.Options{
-
Path: "/",
-
MaxAge: 300, // save for five minutes
-
HttpOnly: true,
-
}
-
-
// make sure the session is empty
-
sess.Values = map[interface{}]interface{}{}
-
sess.Values["oauth_state"] = parResp.State
-
sess.Values["oauth_did"] = did
-
-
if err := sess.Save(e.Request(), e.Response()); err != nil {
-
return err
-
}
-
-
return e.Redirect(302, u.String())
-
}
-
-
func (s *TestServer) handleCallback(e echo.Context) error {
-
resState := e.QueryParam("state")
-
resIss := e.QueryParam("iss")
-
resCode := e.QueryParam("code")
-
-
sess, err := session.Get("session", e)
-
if err != nil {
-
return err
-
}
-
-
sessState := sess.Values["oauth_state"]
-
sessDid := sess.Values["oauth_did"]
-
-
if resState == "" || resIss == "" || resCode == "" || sessState == "" || sessDid == "" {
-
return fmt.Errorf("request missing needed parameters")
-
}
-
-
if resState != sessState {
-
return fmt.Errorf("session state does not match response state")
-
}
-
-
var oauthRequest OauthRequest
-
if err := s.db.Raw("SELECT * FROM oauth_requests WHERE state = ? AND did = ?", sessState, sessDid).Scan(&oauthRequest).Error; err != nil {
-
return err
-
}
-
-
if err := s.db.Exec("DELETE FROM oauth_requests WHERE state = ? AND did = ?", sessState, sessDid).Error; err != nil {
-
return err
-
}
-
-
if resIss != oauthRequest.AuthserverIss {
-
return fmt.Errorf("incoming iss did not match authserver iss")
-
}
-
-
jwk, err := oauth.ParseKeyFromBytes([]byte(oauthRequest.DpopPrivateJwk))
-
if err != nil {
-
return err
-
}
-
-
initialTokenResp, err := s.oauthClient.InitialTokenRequest(
-
e.Request().Context(),
-
resCode,
-
resIss,
-
resIss,
-
oauthRequest.PkceVerifier,
-
oauthRequest.DpopAuthserverNonce,
-
jwk,
-
)
-
if err != nil {
-
return err
-
}
-
-
// TODO: resolve if needed
-
-
if initialTokenResp.Resp["scope"] != scope {
-
return fmt.Errorf("did not receive correct scopes from token request")
-
}
-
-
oauthSession := &OauthSession{
-
Did: oauthRequest.Did,
-
PdsUrl: oauthRequest.PdsUrl,
-
AuthserverIss: oauthRequest.AuthserverIss,
-
AccessToken: initialTokenResp.Resp["access_token"].(string),
-
RefreshToken: initialTokenResp.Resp["refresh_token"].(string),
-
DpopAuthserverNonce: initialTokenResp.DpopAuthserverNonce,
-
DpopPrivateJwk: oauthRequest.DpopPrivateJwk,
-
}
-
-
if err := s.db.Clauses(clause.OnConflict{
-
Columns: []clause.Column{{Name: "did"}},
-
UpdateAll: true,
-
}).Create(oauthSession).Error; err != nil {
-
return err
-
}
-
-
sess.Options = &sessions.Options{
-
Path: "/",
-
MaxAge: 86400 * 7,
-
HttpOnly: true,
-
}
-
-
// make sure the session is empty
-
sess.Values = map[interface{}]interface{}{}
-
sess.Values["did"] = oauthRequest.Did
-
-
if err := sess.Save(e.Request(), e.Response()); err != nil {
-
return err
-
}
-
-
return e.Redirect(302, "/")
-
}
-
-
func (s *TestServer) handleLogout(e echo.Context) error {
-
sess, err := session.Get("session", e)
-
if err != nil {
-
return err
-
}
-
-
sess.Options = &sessions.Options{
-
Path: "/",
-
MaxAge: -1,
-
HttpOnly: true,
-
}
-
-
if err := sess.Save(e.Request(), e.Response()); err != nil {
-
return err
-
}
-
-
return e.Redirect(302, "/")
-
}
-
-
func (s *TestServer) handleMakePost(e echo.Context) error {
-
sess, err := session.Get("session", e)
-
if err != nil {
-
return err
-
}
-
-
did, ok := sess.Values["did"]
-
if !ok {
-
return e.Redirect(302, "/login")
-
}
-
-
var oauthSession OauthSession
-
if err := s.db.Raw("SELECT * FROM oauth_sessions WHERE did = ?", did).Scan(&oauthSession).Error; err != nil {
-
return err
-
}
-
-
args, err := authedReqArgsFromSession(&oauthSession)
-
if err != nil {
-
return err
-
}
-
-
post := bsky.FeedPost{
-
Text: "hello from atproto golang oauth client",
-
CreatedAt: syntax.DatetimeNow().String(),
-
}
-
-
input := atproto.RepoCreateRecord_Input{
-
Collection: "app.bsky.feed.post",
-
Repo: oauthSession.Did,
-
Record: &util.LexiconTypeDecoder{Val: &post},
-
}
-
-
var out atproto.RepoCreateRecord_Output
-
if err := s.xrpcCli.Do(e.Request().Context(), args, xrpc.Procedure, "application/json", "com.atproto.repo.createRecord", nil, input, &out); err != nil {
-
return err
-
}
-
-
return e.File(getFilePath("make-post.html"))
-
}
-
func authedReqArgsFromSession(session *OauthSession) (*oauth.XrpcAuthedRequestArgs, error) {
privateJwk, err := oauth.ParseKeyFromBytes([]byte(session.DpopPrivateJwk))
if err != nil {
···
DpopPdsNonce: session.DpopPdsNonce,
DpopPrivateJwk: privateJwk,
}, nil
-
}
-
-
func resolveHandle(ctx context.Context, handle string) (string, error) {
-
var did string
-
-
_, err := syntax.ParseHandle(handle)
-
if err != nil {
-
return "", err
-
}
-
-
recs, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle))
-
if err != nil {
-
return "", err
-
}
-
-
for _, rec := range recs {
-
if strings.HasPrefix(rec, "did=") {
-
did = strings.Split(rec, "did=")[1]
-
break
-
}
-
}
-
-
if did == "" {
-
req, err := http.NewRequestWithContext(
-
ctx,
-
"GET",
-
fmt.Sprintf("https://%s/.well-known/atproto-did", handle),
-
nil,
-
)
-
if err != nil {
-
return "", err
-
}
-
-
resp, err := http.DefaultClient.Do(req)
-
if err != nil {
-
return "", err
-
}
-
defer resp.Body.Close()
-
-
if resp.StatusCode != http.StatusOK {
-
io.Copy(io.Discard, resp.Body)
-
return "", fmt.Errorf("unable to resolve handle")
-
}
-
-
b, err := io.ReadAll(resp.Body)
-
if err != nil {
-
return "", err
-
}
-
-
maybeDid := string(b)
-
-
if _, err := syntax.ParseDID(maybeDid); err != nil {
-
return "", fmt.Errorf("unable to resolve handle")
-
}
-
-
did = maybeDid
-
}
-
-
return did, nil
-
}
-
-
func resolveService(ctx context.Context, did string) (string, error) {
-
type Identity struct {
-
Service []struct {
-
ID string `json:"id"`
-
Type string `json:"type"`
-
ServiceEndpoint string `json:"serviceEndpoint"`
-
} `json:"service"`
-
}
-
-
var ustr string
-
if strings.HasPrefix(did, "did:plc:") {
-
ustr = fmt.Sprintf("https://plc.directory/%s", did)
-
} else if strings.HasPrefix(did, "did:web:") {
-
ustr = fmt.Sprintf("https://%s/.well-known/did.json", did)
-
} else {
-
return "", fmt.Errorf("did was not a supported did type")
-
}
-
-
req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
-
if err != nil {
-
return "", err
-
}
-
-
resp, err := http.DefaultClient.Do(req)
-
if err != nil {
-
return "", err
-
}
-
defer resp.Body.Close()
-
-
if resp.StatusCode != 200 {
-
io.Copy(io.Discard, resp.Body)
-
return "", fmt.Errorf("could not find identity in plc registry")
-
}
-
-
var identity Identity
-
if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil {
-
return "", err
-
}
-
-
var service string
-
for _, svc := range identity.Service {
-
if svc.ID == "#atproto_pds" {
-
service = svc.ServiceEndpoint
-
}
-
}
-
-
if service == "" {
-
return "", fmt.Errorf("could not find atproto_pds service in identity services")
-
}
-
-
return service, nil
}
func getFilePath(file string) string {
+125
cmd/client_test/resolution.go
···
+
package main
+
+
import (
+
"context"
+
"encoding/json"
+
"fmt"
+
"io"
+
"net"
+
"net/http"
+
"strings"
+
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
)
+
+
func resolveHandle(ctx context.Context, handle string) (string, error) {
+
var did string
+
+
_, err := syntax.ParseHandle(handle)
+
if err != nil {
+
return "", err
+
}
+
+
recs, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle))
+
if err != nil {
+
return "", err
+
}
+
+
for _, rec := range recs {
+
if strings.HasPrefix(rec, "did=") {
+
did = strings.Split(rec, "did=")[1]
+
break
+
}
+
}
+
+
if did == "" {
+
req, err := http.NewRequestWithContext(
+
ctx,
+
"GET",
+
fmt.Sprintf("https://%s/.well-known/atproto-did", handle),
+
nil,
+
)
+
if err != nil {
+
return "", err
+
}
+
+
resp, err := http.DefaultClient.Do(req)
+
if err != nil {
+
return "", err
+
}
+
defer resp.Body.Close()
+
+
if resp.StatusCode != http.StatusOK {
+
io.Copy(io.Discard, resp.Body)
+
return "", fmt.Errorf("unable to resolve handle")
+
}
+
+
b, err := io.ReadAll(resp.Body)
+
if err != nil {
+
return "", err
+
}
+
+
maybeDid := string(b)
+
+
if _, err := syntax.ParseDID(maybeDid); err != nil {
+
return "", fmt.Errorf("unable to resolve handle")
+
}
+
+
did = maybeDid
+
}
+
+
return did, nil
+
}
+
+
func resolveService(ctx context.Context, did string) (string, error) {
+
type Identity struct {
+
Service []struct {
+
ID string `json:"id"`
+
Type string `json:"type"`
+
ServiceEndpoint string `json:"serviceEndpoint"`
+
} `json:"service"`
+
}
+
+
var ustr string
+
if strings.HasPrefix(did, "did:plc:") {
+
ustr = fmt.Sprintf("https://plc.directory/%s", did)
+
} else if strings.HasPrefix(did, "did:web:") {
+
ustr = fmt.Sprintf("https://%s/.well-known/did.json", did)
+
} else {
+
return "", fmt.Errorf("did was not a supported did type")
+
}
+
+
req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
+
if err != nil {
+
return "", err
+
}
+
+
resp, err := http.DefaultClient.Do(req)
+
if err != nil {
+
return "", err
+
}
+
defer resp.Body.Close()
+
+
if resp.StatusCode != 200 {
+
io.Copy(io.Discard, resp.Body)
+
return "", fmt.Errorf("could not find identity in plc registry")
+
}
+
+
var identity Identity
+
if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil {
+
return "", err
+
}
+
+
var service string
+
for _, svc := range identity.Service {
+
if svc.ID == "#atproto_pds" {
+
service = svc.ServiceEndpoint
+
}
+
}
+
+
if service == "" {
+
return "", fmt.Errorf("could not find atproto_pds service in identity services")
+
}
+
+
return service, nil
+
}
-1
oauth.go
···
return nil, err
}
-
// TODO: ??
dpopAuthserverNonce := ""
dpopProof, err := c.AuthServerDpopJwt("POST", parUrl, dpopAuthserverNonce, dpopPrivateKey)
if err != nil {
+9 -9
xrpc.go
···
OnDPoPNonceChanged func(did, newNonce string)
}
+
type XrpcAuthedRequestArgs struct {
+
Did string
+
PdsUrl string
+
Issuer string
+
AccessToken string
+
DpopPdsNonce string
+
DpopPrivateJwk jwk.Key
+
}
+
func (c *XrpcClient) getClient() *http.Client {
if c.Client == nil {
return util.RobustHTTPClient()
···
}
return tokenString, nil
-
}
-
-
type XrpcAuthedRequestArgs struct {
-
Did string
-
PdsUrl string
-
Issuer string
-
AccessToken string
-
DpopPdsNonce string
-
DpopPrivateJwk jwk.Key
}
func (c *XrpcClient) Do(ctx context.Context, authedArgs *XrpcAuthedRequestArgs, kind xrpc.XRPCRequestType, inpenc, method string, params map[string]any, bodyobj any, out any) error {