+5
appview/config/config.go
+5
appview/config/config.go
···
28
Jwks string `env:"JWKS"`
29
}
30
31
type JetstreamConfig struct {
32
Endpoint string `env:"ENDPOINT, default=wss://jetstream1.us-east.bsky.network/subscribe"`
33
}
···
103
Avatar AvatarConfig `env:",prefix=TANGLED_AVATAR_"`
104
OAuth OAuthConfig `env:",prefix=TANGLED_OAUTH_"`
105
Redis RedisConfig `env:",prefix=TANGLED_REDIS_"`
106
Pds PdsConfig `env:",prefix=TANGLED_PDS_"`
107
Cloudflare Cloudflare `env:",prefix=TANGLED_CLOUDFLARE_"`
108
}
···
28
Jwks string `env:"JWKS"`
29
}
30
31
+
type PlcConfig struct {
32
+
PLCURL string `env:"URL, default=https://plc.directory"`
33
+
}
34
+
35
type JetstreamConfig struct {
36
Endpoint string `env:"ENDPOINT, default=wss://jetstream1.us-east.bsky.network/subscribe"`
37
}
···
107
Avatar AvatarConfig `env:",prefix=TANGLED_AVATAR_"`
108
OAuth OAuthConfig `env:",prefix=TANGLED_OAUTH_"`
109
Redis RedisConfig `env:",prefix=TANGLED_REDIS_"`
110
+
Plc PlcConfig `env:",prefix=TANGLED_PLC_"`
111
Pds PdsConfig `env:",prefix=TANGLED_PDS_"`
112
Cloudflare Cloudflare `env:",prefix=TANGLED_CLOUDFLARE_"`
113
}
+2
-2
appview/state/state.go
+2
-2
appview/state/state.go
···
70
return nil, fmt.Errorf("failed to create enforcer: %w", err)
71
}
72
73
-
res, err := idresolver.RedisResolver(config.Redis.ToURL())
74
if err != nil {
75
logger.Error("failed to create redis resolver", "err", err)
76
-
res = idresolver.DefaultResolver()
77
}
78
79
posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint})
···
70
return nil, fmt.Errorf("failed to create enforcer: %w", err)
71
}
72
73
+
res, err := idresolver.RedisResolver(config.Redis.ToURL(), config.Plc.PLCURL)
74
if err != nil {
75
logger.Error("failed to create redis resolver", "err", err)
76
+
res = idresolver.DefaultResolver(config.Plc.PLCURL)
77
}
78
79
posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint})
+9
-3
guard/guard.go
+9
-3
guard/guard.go
···
17
"github.com/urfave/cli/v3"
18
"tangled.org/core/idresolver"
19
"tangled.org/core/log"
20
)
21
22
func Command() *cli.Command {
···
56
57
func Run(ctx context.Context, cmd *cli.Command) error {
58
l := log.FromContext(ctx)
59
60
incomingUser := cmd.String("user")
61
gitDir := cmd.String("git-dir")
···
122
}
123
124
didOrHandle := components[0]
125
-
identity := resolveIdentity(ctx, l, didOrHandle)
126
did := identity.DID.String()
127
repoName := components[1]
128
qualifiedRepoName, _ := securejoin.SecureJoin(did, repoName)
···
195
return nil
196
}
197
198
-
func resolveIdentity(ctx context.Context, l *slog.Logger, didOrHandle string) *identity.Identity {
199
-
resolver := idresolver.DefaultResolver()
200
ident, err := resolver.ResolveIdent(ctx, didOrHandle)
201
if err != nil {
202
l.Error("Error resolving handle", "error", err, "handle", didOrHandle)
···
17
"github.com/urfave/cli/v3"
18
"tangled.org/core/idresolver"
19
"tangled.org/core/log"
20
+
"tangled.org/core/knotserver/config"
21
)
22
23
func Command() *cli.Command {
···
57
58
func Run(ctx context.Context, cmd *cli.Command) error {
59
l := log.FromContext(ctx)
60
+
61
+
c, err := config.Load(ctx)
62
+
if err != nil {
63
+
return fmt.Errorf("failed to load config: %w", err)
64
+
}
65
66
incomingUser := cmd.String("user")
67
gitDir := cmd.String("git-dir")
···
128
}
129
130
didOrHandle := components[0]
131
+
identity := resolveIdentity(ctx, c, l, didOrHandle)
132
did := identity.DID.String()
133
repoName := components[1]
134
qualifiedRepoName, _ := securejoin.SecureJoin(did, repoName)
···
201
return nil
202
}
203
204
+
func resolveIdentity(ctx context.Context, c *config.Config, l *slog.Logger, didOrHandle string) *identity.Identity {
205
+
resolver := idresolver.DefaultResolver(c.Server.PlcUrl)
206
ident, err := resolver.ResolveIdent(ctx, didOrHandle)
207
if err != nil {
208
l.Error("Error resolving handle", "error", err, "handle", didOrHandle)
+17
-8
idresolver/resolver.go
+17
-8
idresolver/resolver.go
···
17
directory identity.Directory
18
}
19
20
-
func BaseDirectory() identity.Directory {
21
base := identity.BaseDirectory{
22
-
PLCURL: identity.DefaultPLCURL,
23
HTTPClient: http.Client{
24
Timeout: time.Second * 10,
25
Transport: &http.Transport{
···
42
return &base
43
}
44
45
-
func RedisDirectory(url string) (identity.Directory, error) {
46
hitTTL := time.Hour * 24
47
errTTL := time.Second * 30
48
invalidHandleTTL := time.Minute * 5
49
-
return redisdir.NewRedisDirectory(BaseDirectory(), url, hitTTL, errTTL, invalidHandleTTL, 10000)
50
}
51
52
-
func DefaultResolver() *Resolver {
53
return &Resolver{
54
-
directory: identity.DefaultDirectory(),
55
}
56
}
57
58
-
func RedisResolver(redisUrl string) (*Resolver, error) {
59
-
directory, err := RedisDirectory(redisUrl)
60
if err != nil {
61
return nil, err
62
}
···
17
directory identity.Directory
18
}
19
20
+
func BaseDirectory(plcUrl string) identity.Directory {
21
base := identity.BaseDirectory{
22
+
PLCURL: plcUrl,
23
HTTPClient: http.Client{
24
Timeout: time.Second * 10,
25
Transport: &http.Transport{
···
42
return &base
43
}
44
45
+
func RedisDirectory(url, plcUrl string) (identity.Directory, error) {
46
hitTTL := time.Hour * 24
47
errTTL := time.Second * 30
48
invalidHandleTTL := time.Minute * 5
49
+
return redisdir.NewRedisDirectory(
50
+
BaseDirectory(plcUrl),
51
+
url,
52
+
hitTTL,
53
+
errTTL,
54
+
invalidHandleTTL,
55
+
10000,
56
+
)
57
}
58
59
+
func DefaultResolver(plcUrl string) *Resolver {
60
+
base := BaseDirectory(plcUrl)
61
+
cached := identity.NewCacheDirectory(base, 250_000, time.Hour*24, time.Minute*2, time.Minute*5)
62
return &Resolver{
63
+
directory: &cached,
64
}
65
}
66
67
+
func RedisResolver(redisUrl, plcUrl string) (*Resolver, error) {
68
+
directory, err := RedisDirectory(redisUrl, plcUrl)
69
if err != nil {
70
return nil, err
71
}
+1
knotserver/config/config.go
+1
knotserver/config/config.go
···
19
InternalListenAddr string `env:"INTERNAL_LISTEN_ADDR, default=127.0.0.1:5444"`
20
DBPath string `env:"DB_PATH, default=knotserver.db"`
21
Hostname string `env:"HOSTNAME, required"`
22
JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"`
23
Owner string `env:"OWNER, required"`
24
LogDids bool `env:"LOG_DIDS, default=true"`
···
19
InternalListenAddr string `env:"INTERNAL_LISTEN_ADDR, default=127.0.0.1:5444"`
20
DBPath string `env:"DB_PATH, default=knotserver.db"`
21
Hostname string `env:"HOSTNAME, required"`
22
+
PlcUrl string `env:"PLC_URL, default=plc.directory"`
23
JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"`
24
Owner string `env:"OWNER, required"`
25
LogDids bool `env:"LOG_DIDS, default=true"`
+3
-7
knotserver/ingester.go
+3
-7
knotserver/ingester.go
···
16
"github.com/bluesky-social/jetstream/pkg/models"
17
securejoin "github.com/cyphar/filepath-securejoin"
18
"tangled.org/core/api/tangled"
19
-
"tangled.org/core/idresolver"
20
"tangled.org/core/knotserver/db"
21
"tangled.org/core/knotserver/git"
22
"tangled.org/core/log"
···
120
}
121
122
// resolve this aturi to extract the repo record
123
-
resolver := idresolver.DefaultResolver()
124
-
ident, err := resolver.ResolveIdent(ctx, repoAt.Authority().String())
125
if err != nil || ident.Handle.IsInvalidHandle() {
126
return fmt.Errorf("failed to resolve handle: %w", err)
127
}
···
233
return err
234
}
235
236
-
resolver := idresolver.DefaultResolver()
237
-
238
-
subjectId, err := resolver.ResolveIdent(ctx, record.Subject)
239
if err != nil || subjectId.Handle.IsInvalidHandle() {
240
return err
241
}
242
243
// TODO: fix this for good, we need to fetch the record here unfortunately
244
// resolve this aturi to extract the repo record
245
-
owner, err := resolver.ResolveIdent(ctx, repoAt.Authority().String())
246
if err != nil || owner.Handle.IsInvalidHandle() {
247
return fmt.Errorf("failed to resolve handle: %w", err)
248
}
···
16
"github.com/bluesky-social/jetstream/pkg/models"
17
securejoin "github.com/cyphar/filepath-securejoin"
18
"tangled.org/core/api/tangled"
19
"tangled.org/core/knotserver/db"
20
"tangled.org/core/knotserver/git"
21
"tangled.org/core/log"
···
119
}
120
121
// resolve this aturi to extract the repo record
122
+
ident, err := h.resolver.ResolveIdent(ctx, repoAt.Authority().String())
123
if err != nil || ident.Handle.IsInvalidHandle() {
124
return fmt.Errorf("failed to resolve handle: %w", err)
125
}
···
231
return err
232
}
233
234
+
subjectId, err := h.resolver.ResolveIdent(ctx, record.Subject)
235
if err != nil || subjectId.Handle.IsInvalidHandle() {
236
return err
237
}
238
239
// TODO: fix this for good, we need to fetch the record here unfortunately
240
// resolve this aturi to extract the repo record
241
+
owner, err := h.resolver.ResolveIdent(ctx, repoAt.Authority().String())
242
if err != nil || owner.Handle.IsInvalidHandle() {
243
return fmt.Errorf("failed to resolve handle: %w", err)
244
}
+1
-1
knotserver/internal.go
+1
-1
knotserver/internal.go
···
145
146
func (h *InternalHandle) replyCompare(line git.PostReceiveLine, repoOwner string, gitRelativeDir string, repoName string, ctx context.Context) ([]string, error) {
147
l := h.l.With("handler", "replyCompare")
148
-
userIdent, err := idresolver.DefaultResolver().ResolveIdent(ctx, repoOwner)
149
user := repoOwner
150
if err != nil {
151
l.Error("Failed to fetch user identity", "err", err)
···
145
146
func (h *InternalHandle) replyCompare(line git.PostReceiveLine, repoOwner string, gitRelativeDir string, repoName string, ctx context.Context) ([]string, error) {
147
l := h.l.With("handler", "replyCompare")
148
+
userIdent, err := idresolver.DefaultResolver(h.c.Server.PlcUrl).ResolveIdent(ctx, repoOwner)
149
user := repoOwner
150
if err != nil {
151
l.Error("Failed to fetch user identity", "err", err)
+1
-1
knotserver/router.go
+1
-1
knotserver/router.go
+54
local-infra/Caddyfile
+54
local-infra/Caddyfile
···
···
1
+
{
2
+
storage file_system /data/
3
+
debug
4
+
pki {
5
+
ca localtangled {
6
+
name "LocalTangledCA"
7
+
}
8
+
}
9
+
}
10
+
11
+
plc.tngl.boltless.dev {
12
+
tls {
13
+
issuer internal {
14
+
ca localtangled
15
+
}
16
+
}
17
+
reverse_proxy http://plc:8080
18
+
}
19
+
20
+
*.pds.tngl.boltless.dev, pds.tngl.boltless.dev {
21
+
tls {
22
+
issuer internal {
23
+
ca localtangled
24
+
}
25
+
}
26
+
reverse_proxy http://pds:3000
27
+
}
28
+
29
+
jetstream.tngl.boltless.dev {
30
+
tls {
31
+
issuer internal {
32
+
ca localtangled
33
+
}
34
+
}
35
+
reverse_proxy http://jetstream:6008
36
+
}
37
+
38
+
knot.tngl.boltless.dev {
39
+
tls {
40
+
issuer internal {
41
+
ca localtangled
42
+
}
43
+
}
44
+
reverse_proxy http://host.docker.internal:6000
45
+
}
46
+
47
+
spindle.tngl.boltless.dev {
48
+
tls {
49
+
issuer internal {
50
+
ca localtangled
51
+
}
52
+
}
53
+
reverse_proxy http://host.docker.internal:6555
54
+
}
+78
local-infra/docker-compose.yml
+78
local-infra/docker-compose.yml
···
···
1
+
name: tangled-local-infra
2
+
services:
3
+
caddy:
4
+
container_name: caddy
5
+
image: caddy:2
6
+
depends_on:
7
+
- pds
8
+
restart: unless-stopped
9
+
cap_add:
10
+
- NET_ADMIN
11
+
ports:
12
+
- "80:80"
13
+
- "443:443"
14
+
- "443:443/udp"
15
+
volumes:
16
+
- ./Caddyfile:/etc/caddy/Caddyfile
17
+
- caddy_data:/data
18
+
- caddy_config:/config
19
+
20
+
plc:
21
+
image: ghcr.io/bluesky-social/did-method-plc:plc-f2ab7516bac5bc0f3f86842fa94e996bd1b3815b
22
+
# did-method-plc only provides linux/amd64
23
+
platform: linux/amd64
24
+
container_name: plc
25
+
restart: unless-stopped
26
+
ports:
27
+
- "4000:8080"
28
+
depends_on:
29
+
- plc_db
30
+
environment:
31
+
DEBUG_MODE: 1
32
+
LOG_ENABLED: "true"
33
+
LOG_LEVEL: "debug"
34
+
LOG_DESTINATION: 1
35
+
DB_CREDS_JSON: &DB_CREDS_JSON '{"username":"pg","password":"password","host":"plc_db","port":5432}'
36
+
DB_MIGRATE_CREDS_JSON: *DB_CREDS_JSON
37
+
PLC_VERSION: 0.0.1
38
+
PORT: 8080
39
+
40
+
plc_db:
41
+
image: postgres:14.4-alpine
42
+
container_name: plc_db
43
+
environment:
44
+
- POSTGRES_USER=pg
45
+
- POSTGRES_PASSWORD=password
46
+
- PGPORT=5432
47
+
volumes:
48
+
- plc:/var/lib/postgresql/data
49
+
50
+
pds:
51
+
container_name: pds
52
+
image: ghcr.io/bluesky-social/pds:0.4
53
+
restart: unless-stopped
54
+
ports:
55
+
- "4001:3000"
56
+
volumes:
57
+
- pds:/pds
58
+
env_file:
59
+
- ./pds.env
60
+
61
+
jetstream:
62
+
container_name: jetstream
63
+
image: ghcr.io/bluesky-social/jetstream:sha-0ab10bd
64
+
restart: unless-stopped
65
+
volumes:
66
+
- jetstream:/data
67
+
environment:
68
+
- JETSTREAM_DATA_DIR=/data
69
+
# livness check interval to restart when no events are received (default: 15sec)
70
+
- JETSTREAM_LIVENESS_TTL=300s
71
+
- JETSTREAM_WS_URL=ws://pds:3000/xrpc/com.atproto.sync.subscribeRepos
72
+
73
+
volumes:
74
+
caddy_config:
75
+
caddy_data:
76
+
plc:
77
+
pds:
78
+
jetstream:
+17
local-infra/pds.env
+17
local-infra/pds.env
···
···
1
+
PDS_JWT_SECRET=8cae8bffcc73d9932819650791e4e89a
2
+
PDS_ADMIN_PASSWORD=d6a902588cd93bee1af83f924f60cfd3
3
+
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=2e92e336a50a618458e1097d94a1db86ec3fd8829d7735020cbae80625c761d7
4
+
5
+
LOG_ENABLED=true
6
+
7
+
# PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app
8
+
# PDS_BSKY_APP_VIEW_URL=https://api.bsky.app
9
+
10
+
PDS_DATA_DIRECTORY=/pds
11
+
PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks
12
+
13
+
# PDS_DID_PLC_URL=http://plc:8080
14
+
PDS_HOSTNAME=pds.tngl.boltless.dev
15
+
16
+
# PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac
17
+
# PDS_REPORT_SERVICE_URL=https://mod.bsky.app
+14
local-infra/readme.md
+14
local-infra/readme.md
···
···
1
+
run compose
2
+
```
3
+
docker compose up -d
4
+
```
5
+
6
+
copy the self-signed certificate to host machine
7
+
```
8
+
docker cp caddy:/data/pki/authorities/localtangled/root.crt localtangled.crt
9
+
```
10
+
11
+
trust the cert (macOS)
12
+
```
13
+
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ./localtangled.crt
14
+
```
+63
local-infra/scripts/create-test-account.sh
+63
local-infra/scripts/create-test-account.sh
···
···
1
+
#!/bin/bash
2
+
set -o errexit
3
+
set -o nounset
4
+
set -o pipefail
5
+
6
+
source "$(dirname "$0")/../pds.env"
7
+
8
+
# curl a URL and fail if the request fails.
9
+
function curl_cmd_get {
10
+
curl --fail --silent --show-error "$@"
11
+
}
12
+
13
+
# curl a URL and fail if the request fails.
14
+
function curl_cmd_post {
15
+
curl --fail --silent --show-error --request POST --header "Content-Type: application/json" "$@"
16
+
}
17
+
18
+
# curl a URL but do not fail if the request fails.
19
+
function curl_cmd_post_nofail {
20
+
curl --silent --show-error --request POST --header "Content-Type: application/json" "$@"
21
+
}
22
+
23
+
USERNAME="${1:-}"
24
+
25
+
if [[ "${USERNAME}" == "" ]]; then
26
+
read -p "Enter a username: " USERNAME
27
+
fi
28
+
29
+
if [[ "${USERNAME}" == "" ]]; then
30
+
echo "ERROR: missing USERNAME parameter." >/dev/stderr
31
+
echo "Usage: $0 ${SUBCOMMAND} <USERNAME>" >/dev/stderr
32
+
exit 1
33
+
fi
34
+
35
+
PASSWORD="password"
36
+
INVITE_CODE="$(curl_cmd_post \
37
+
--user "admin:${PDS_ADMIN_PASSWORD}" \
38
+
--data '{"useCount": 1}' \
39
+
"https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createInviteCode" | jq --raw-output '.code'
40
+
)"
41
+
RESULT="$(curl_cmd_post_nofail \
42
+
--data "{\"email\":\"${USERNAME}@${PDS_HOSTNAME}\", \"handle\":\"${USERNAME}.${PDS_HOSTNAME}\", \"password\":\"${PASSWORD}\", \"inviteCode\":\"${INVITE_CODE}\"}" \
43
+
"https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createAccount"
44
+
)"
45
+
46
+
DID="$(echo $RESULT | jq --raw-output '.did')"
47
+
if [[ "${DID}" != did:* ]]; then
48
+
ERR="$(echo ${RESULT} | jq --raw-output '.message')"
49
+
echo "ERROR: ${ERR}" >/dev/stderr
50
+
echo "Usage: $0 <EMAIL> <HANDLE>" >/dev/stderr
51
+
exit 1
52
+
fi
53
+
54
+
echo
55
+
echo "Account created successfully!"
56
+
echo "-----------------------------"
57
+
echo "Handle : ${USERNAME}.${PDS_HOSTNAME}"
58
+
echo "DID : ${DID}"
59
+
echo "Password : ${PASSWORD}"
60
+
echo "-----------------------------"
61
+
echo "This is a test account with an insecure password."
62
+
echo "Make sure it's only used for development."
63
+
echo
+1
spindle/config/config.go
+1
spindle/config/config.go
···
13
DBPath string `env:"DB_PATH, default=spindle.db"`
14
Hostname string `env:"HOSTNAME, required"`
15
JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"`
16
Dev bool `env:"DEV, default=false"`
17
Owner string `env:"OWNER, required"`
18
Secrets Secrets `env:",prefix=SECRETS_"`
···
13
DBPath string `env:"DB_PATH, default=spindle.db"`
14
Hostname string `env:"HOSTNAME, required"`
15
JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"`
16
+
PlcUrl string `env:"PLC_URL, default=plc.directory"`
17
Dev bool `env:"DEV, default=false"`
18
Owner string `env:"OWNER, required"`
19
Secrets Secrets `env:",prefix=SECRETS_"`
+3
-7
spindle/ingester.go
+3
-7
spindle/ingester.go
···
9
10
"tangled.org/core/api/tangled"
11
"tangled.org/core/eventconsumer"
12
-
"tangled.org/core/idresolver"
13
"tangled.org/core/rbac"
14
"tangled.org/core/spindle/db"
15
···
142
func (s *Spindle) ingestRepo(ctx context.Context, e *models.Event) error {
143
var err error
144
did := e.Did
145
-
resolver := idresolver.DefaultResolver()
146
147
l := s.l.With("component", "ingester", "record", tangled.RepoNSID)
148
···
190
}
191
192
// add collaborators to rbac
193
-
owner, err := resolver.ResolveIdent(ctx, did)
194
if err != nil || owner.Handle.IsInvalidHandle() {
195
return err
196
}
···
225
return err
226
}
227
228
-
resolver := idresolver.DefaultResolver()
229
-
230
-
subjectId, err := resolver.ResolveIdent(ctx, record.Subject)
231
if err != nil || subjectId.Handle.IsInvalidHandle() {
232
return err
233
}
···
240
241
// TODO: get rid of this entirely
242
// resolve this aturi to extract the repo record
243
-
owner, err := resolver.ResolveIdent(ctx, repoAt.Authority().String())
244
if err != nil || owner.Handle.IsInvalidHandle() {
245
return fmt.Errorf("failed to resolve handle: %w", err)
246
}
···
9
10
"tangled.org/core/api/tangled"
11
"tangled.org/core/eventconsumer"
12
"tangled.org/core/rbac"
13
"tangled.org/core/spindle/db"
14
···
141
func (s *Spindle) ingestRepo(ctx context.Context, e *models.Event) error {
142
var err error
143
did := e.Did
144
145
l := s.l.With("component", "ingester", "record", tangled.RepoNSID)
146
···
188
}
189
190
// add collaborators to rbac
191
+
owner, err := s.res.ResolveIdent(ctx, did)
192
if err != nil || owner.Handle.IsInvalidHandle() {
193
return err
194
}
···
223
return err
224
}
225
226
+
subjectId, err := s.res.ResolveIdent(ctx, record.Subject)
227
if err != nil || subjectId.Handle.IsInvalidHandle() {
228
return err
229
}
···
236
237
// TODO: get rid of this entirely
238
// resolve this aturi to extract the repo record
239
+
owner, err := s.res.ResolveIdent(ctx, repoAt.Authority().String())
240
if err != nil || owner.Handle.IsInvalidHandle() {
241
return fmt.Errorf("failed to resolve handle: %w", err)
242
}