Bootstrap
A tenant, its first user and its signing key are created by the server's seed command — the only way to create the first of anything, since the admin API needs an administrator who needs a tenant. Everything after that first row has two more doors: the admin API, and, for a client on a tenant that opens registration to it, dynamic client registration. This document seeds, because a seed command is reproducible from a shell with no token in it; the admin paths walks the same ground through the API.
Pick how you are running it
Run it from a checkout describes three ways to run this: everything in Docker, Postgres in Docker with the server on your host, and everything on your host against a PostgreSQL you operate. They differ in exactly one thing this document depends on — how the seed command is invoked — because the container runs a built bundle and a host run has the source.
So the rest of this document calls the CLI through a shell function named
odudu. Define it once for the way you are running, from the repository
root, and every command below is the same command in all three.
Everything in Docker. The CLI is the bundle inside the running
container, and the configuration both it and the server read is
infra/docker/.env; the repository-root .env plays no part.
cd infra/docker && docker compose up -d --build
until curl -fsS http://localhost:3000/health/ready; do sleep 2; done
cd ../..
odudu() { docker compose -f infra/docker/compose.yaml exec -T odudu node dist/main.js "$@"; }
{"status":"ok","checks":{"database":"ok"}}
Server on your host, against either Postgres. Node runs the TypeScript
source directly, and --env-file hands it the repository-root .env — the
same file the dev script loads, so the CLI and the server it is seeding
for read one configuration.
pnpm --filter @odudu/server dev # leave running in another terminal
until curl -fsS http://localhost:3000/health/ready; do sleep 2; done
odudu() { node --env-file=.env apps/server/src/main.ts "$@"; }
{"status":"ok","checks":{"database":"ok"}}
What else the choice settles:
| What it settles | Everything in Docker | Server on your host |
|---|---|---|
| What the CLI executes | the bundle built into the image | the TypeScript source, types stripped by Node |
| Configuration the server and the CLI both read | infra/docker/.env | the repository-root .env |
| Base URL of every endpoint | http://localhost:3000 | http://localhost:3000 |
| PostgreSQL | compose's, published on port 5442 | compose's on 5442, or one you run yourself |
The base URL is the same in all three, so no URL below is mode-specific:
compose publishes 127.0.0.1:3000:3000, and a host run defaults to the same
port. Run only one of them at a time — they compete for it.
The readiness loop is not decoration, in either mode. docker compose up -d
returns when the container has started, which is before it has finished
applying migrations, and the dev script returns the shell immediately for
the same reason. The seed command does not run migrations; it expects a
schema the server put there on boot, and a seed run in that gap fails with
relation "tenants" does not exist.
Running everything on your host needs the database and the restricted serving
role to exist before any of this — one CREATE DATABASE and one CREATE USER,
in Run it from a
checkout,
against a PostgreSQL whose owner role can CREATE ROLE. Nothing after that
bootstrap differs from the second column above. The no-Docker run below used a
throwaway cluster made for it — initdb -D /tmp/odudu-pg -U postgres, started
with pg_ctl on port 5433, PostgreSQL 18.6 — rather than the compose stack, so
that "no Docker at all" meant it.
The key that has to be the same on both sides
ODUDU_KEK wraps each tenant's private signing key. It is read from whichever
configuration the process holds, so the thing that seeded a tenant and the
thing serving it must hold the same value — and the two files ship
different ones: infra/docker/.env.example carries a throwaway key, and
the root .env.example leaves it empty for you to generate.
Seeding under compose and then serving from your host is the easy accident:
both processes are pointed at the same database, and only the key differs.
It does not fail where you would look for it. Discovery and JWKS answer 200,
/authorize renders the login form, the login POST returns a code — and
then:
HTTP/1.1 500 Internal Server Error
content-type: application/json; charset=utf-8
{"statusCode":500,"error":"Internal Server Error","message":"Unsupported state or unable to authenticate data"}
(That response is from a host run serving the tenant the compose stack had
seeded, with the root .env holding a key generated for it rather than the
stack's.)
That is the private key failing to unwrap, and it names neither the key nor
the reason. Before seeding anything, copy the ODUDU_KEK line from
infra/docker/.env into the root .env if you intend to move between the
two. Changing the value afterwards strands every key already wrapped with
the old one — a tenant seeded under one key cannot be served under another,
and there is no rotation path yet.
A public client, with a user
A public client has no secret. It proves itself with PKCE alone, which is what a browser or mobile application should be.
odudu seed \
--tenant demo --client demo-spa \
--redirect-uri http://localhost:8080/callback \
--user ada --password correct-horse-battery --email ada@example.com
{
"created": true,
"tenant": "demo",
"tenantId": "01a0cb07-…",
"clientId": "demo-spa",
"userSubjectId": "01a0cb07-…"
}
(tenantId and userSubjectId are generated identifiers; shortened here.
userSubjectId is present whenever --user names one, whether this run
created it or found it already seeded — Address verification
below is what it is for.)
--redirect-uri may be repeated. It is matched by exact string comparison
when a request arrives — no trailing-slash tolerance, no case folding, no
ignoring the query string, because every normalization widens what an
attacker can aim at. --email is optional; without it the email and
email_verified claims are omitted together rather than an empty address
being asserted with a verification status.
Seeding asserts a whole desired state, so a re-run with identical arguments reports that it changed nothing:
odudu seed \
--tenant demo --client demo-spa \
--redirect-uri http://localhost:8080/callback \
--user ada --password correct-horse-battery --email ada@example.com
{
"created": false,
"tenant": "demo",
"tenantId": "01a0cb07-…",
"clientId": "demo-spa",
"userSubjectId": "01a0cb07-…"
}
while a re-run that disagrees with what is stored refuses rather than overwriting or silently ignoring the difference:
odudu seed \
--tenant demo --client demo-spa \
--redirect-uri http://localhost:8080/callback \
--user ada --password wrong-password --email ada@example.com
OduduError: user ada already exists with a different password
code: 'seed_conflict'
(exit status 1; the Node stack trace between those two lines is elided.)
A confidential client
A confidential client has a secret and is registered for exactly one way of
presenting it. --token-endpoint-auth-method picks that way; omitted, it is
client_secret_basic.
odudu seed \
--tenant demo --client demo-backend --client-secret demo-backend-secret \
--token-endpoint-auth-method client_secret_basic \
--redirect-uri http://localhost:8080/callback
{ "created": true, "tenant": "demo", "tenantId": "01a0cb07-…", "clientId": "demo-backend" }
odudu seed \
--tenant demo --client demo-post --client-secret demo-post-secret \
--token-endpoint-auth-method client_secret_post \
--redirect-uri http://localhost:8080/callback
{ "created": true, "tenant": "demo", "tenantId": "01a0cb07-…", "clientId": "demo-post" }
A confidential client is also given a service-account subject, which is what
makes client_credentials possible for it (Path C). A public client gets
none, and asking for that grant with one is refused.
The method is registered, not negotiated: demo-post must send its secret
in the body and demo-backend must send it in the Authorization header.
Each is refused if it uses the other's method, even with the right secret —
see the /token table in The branches.
Both seed runs above create the tenant's first signing key if it has none.
--user and --password go together or not at all, and --email needs a
user to belong to. Seeding never adds a user to a client that already
exists — it would exit 0 having done nothing, which is worse than refusing.
Two caveats on the command itself. --client-secret and --password are
plain command-line flags, so they land in ps output and shell history:
that is acceptable for a local bootstrap run by an operator who already
controls the machine, and is not something to carry into CI. And the seeded
values above are demonstration values in this document; choose your own.
The shell variables the rest of this document uses
Every command below is a real command, and several need a value produced by
an earlier one — $CODE, $VERIFIER, $ACCESS_TOKEN. Paste this into the
shell you are working in and they are all defined. It is Path A, run
without a browser: the browser's only job in that flow is to follow a
redirect and submit a form, and curl does both.
BASE=http://localhost:3000/tenants/demo/protocol/openid-connect
LOGIN=http://localhost:3000/tenants/demo/login-actions/authenticate
VERIFIER=$(openssl rand -hex 32)
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -binary -sha256 \
| openssl base64 | tr '+/' '-_' | tr -d '=')
AUTH_SESSION_ID=$(curl -sS --get \
--data-urlencode 'response_type=code' \
--data-urlencode 'client_id=demo-spa' \
--data-urlencode 'redirect_uri=http://localhost:8080/callback' \
--data-urlencode 'scope=openid profile email' \
--data-urlencode 'state=xyz-123' \
--data-urlencode 'nonce=n-0S6_WzA2Mj' \
--data-urlencode "code_challenge=$CHALLENGE" \
--data-urlencode 'code_challenge_method=S256' \
"$BASE/auth" | sed -n '/name="auth_session_id"/{s/.*value="\([^"]*\)".*/\1/p;q;}')
CODE=$(curl -sS -D - -o /dev/null \
--data-urlencode "auth_session_id=$AUTH_SESSION_ID" \
--data-urlencode 'username=ada' \
--data-urlencode 'password=correct-horse-battery' \
"$LOGIN" | sed -n 's/.*[?&]code=\([^&[:space:]]*\).*/\1/p' | tr -d '\r')
TOKENS=$(curl -sS \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode "code=$CODE" \
--data-urlencode 'redirect_uri=http://localhost:8080/callback' \
--data-urlencode 'client_id=demo-spa' \
--data-urlencode "code_verifier=$VERIFIER" "$BASE/token")
ACCESS_TOKEN=$(printf '%s' "$TOKENS" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
ID_TOKEN=$(printf '%s' "$TOKENS" | sed -n 's/.*"id_token":"\([^"]*\)".*/\1/p')
REFRESH_TOKEN=$(printf '%s' "$TOKENS" | sed -n 's/.*"refresh_token":"\([^"]*\)".*/\1/p')
Two things to know before reusing it. A $CODE is spent by the successful
redemption above and lives only 60 seconds anyway, so every experiment on
/token that needs an unspent one needs the first three blocks run again.
And $AUTH_SESSION_ID is consumed by a successful login, so the same goes
for experiments on the login form.
infra/docker/smoke.sh is the same sequence with assertions after each
step; read it if you would rather have something that fails loudly. It
brings the compose stack up and tears it down again, volumes included, so it
is the all-Docker way of running whichever one you picked above — and it
will take port 3000 and the stack's database with it.
Dynamic client registration
This section runs against a tenant of its own, so nothing it registers lands
in demo. seed tenant creates one with every setting at its default:
odudu seed tenant --name reg-demo
{ "command": "tenant", "created": true, "tenant": "reg-demo", "tenantId": "01a0cb08-2747-…" }
curl -sS -o /dev/null -w '%{http_code}\n' \
http://localhost:3000/tenants/reg-demo/.well-known/openid-configuration
200
Registration accepts, beside the redirect and authentication metadata the
transcripts below register, RFC 7591's client_uri, policy_uri and tos_uri
(absolute https, or http on a loopback host, no fragment — the consent screen
links them, The client's own
pages) and OpenID Connect
Dynamic Client Registration §2's id_token_signed_response_alg (an algorithm a
non-retired key of the tenant produces), default_max_age and
require_auth_time; a response echoes each one registered, and none that was
not. No transcript here registers them; integration tests do.
client_registration_policy is disabled on every tenant by default (ADR
0026). Registering before it is opened, or against a tenant that does not
exist, answers the same way — an enumeration oracle costs nothing to close
here, the same reasoning /token and /userinfo apply to a disabled
tenant. The discovery 200 above is what tells the two apart: the first
refusal below is the closed policy, not an absent tenant.
curl -sS -o /dev/null -w '%{http_code}\n' -X POST \
http://localhost:3000/tenants/reg-demo/clients-registrations/openid-connect \
-H 'content-type: application/json' \
-d '{"redirect_uris":["https://rp.example/cb"]}'
404
curl -sS -o /dev/null -w '%{http_code}\n' -X POST \
http://localhost:3000/tenants/no-such-tenant/clients-registrations/openid-connect \
-H 'content-type: application/json' \
-d '{"redirect_uris":["https://rp.example/cb"]}'
404
Opening it is a tenant setting like any other:
odudu seed tenant --name reg-demo --set client_registration_policy=open
{
"command": "tenant",
"created": false,
"tenant": "reg-demo",
"tenantId": "01a0cb08-2747-…",
"settings": ["client_registration_policy"]
}
Discovery now advertises the endpoint, and any request registers a client —
the open policy is RFC 7591 §3.1's anonymous case. The server assigns
client_id; a client_secret is generated for a confidential client (the
default — token_endpoint_auth_method defaults to client_secret_basic)
and returned exactly once, in this response. clients.secret_hash
stores its Argon2id hash; there is no way to retrieve the plaintext again.
curl -sS -X POST http://localhost:3000/tenants/reg-demo/clients-registrations/openid-connect \
-H 'content-type: application/json' \
-d '{"redirect_uris":["https://rp.example/cb"],"client_name":"Example RP"}'
{
"client_id": "01a0cb08-2958-…",
"client_id_issued_at": 1790112704,
"client_secret": "BvE93j7HNH4luJx0BBMi9k5sPlUXNafEsyCQPKkEPRM",
"client_secret_expires_at": 0,
"redirect_uris": ["https://rp.example/cb"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "client_secret_basic",
"client_name": "Example RP"
}
This registration presented no credential, so registration_origin is
'anonymous' and consent_required defaults true on the row it wrote
(ADR 0027) — an anonymous registrant is not the operator vouching for a
client the way seed client or a token-authorized registration is.
The token policy is stricter: a request with no bearer credential is
refused before anything is validated, with WWW-Authenticate naming the
scheme and no error parameter (RFC 6750 §3.1's distinction between an
absent credential and a rejected one).
odudu seed tenant --name reg-demo --set client_registration_policy=token
curl -sS -D - -o /dev/null -X POST \
http://localhost:3000/tenants/reg-demo/clients-registrations/openid-connect \
-H 'content-type: application/json' -d '{"redirect_uris":["https://rp.example/cb"]}' \
| grep -iE '^HTTP|www-authenticate'
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="client-registration"
seed registration-token mints the credential a registrant presents:
--uses bounds how many registrations it is good for, --ttl its lifetime
in seconds, and both are required since the schema has no default for
either. Every other seed subcommand answers with a line of JSON; this one
answers with the token alone, so TOKEN=$(odudu seed registration-token …)
captures exactly the credential and nothing else. The admin API mints the
same way, for an operator who would rather not shell into the container:
POST /admin/tenants/{tenant}/registration-tokens (manage-clients),
documented in the admin paths.
TOKEN=$(odudu seed registration-token --tenant reg-demo --uses 1 --ttl 3600)
j4B_nsJYIJSe8AgjA9xVsFU28J6L67yYzVinAYXXP4w
It is stored as its SHA-256 digest, the same shape the account action tokens
use, and spent by one UPDATE … RETURNING so two concurrent registrations
against a one-use token cannot both win — answering the request and consuming
the token happen in the one transaction that inserts the client, never earlier.
Presenting it registers a client whose registration_origin is 'token', with
consent_required defaulting false: an initial access token is an operator's
own authorization, as much as seed client naming a client directly is.
curl -sS -X POST http://localhost:3000/tenants/reg-demo/clients-registrations/openid-connect \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"redirect_uris":["https://rp2.example/cb"],"token_endpoint_auth_method":"none"}'
{
"client_id": "01a0cb08-2c63-…",
"client_id_issued_at": 1790112705,
"redirect_uris": ["https://rp2.example/cb"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "none"
}
token_endpoint_auth_method: "none" names a public client, so no secret is
generated or returned — RFC 6749 §10.1 gives no client that cannot keep one
confidential a credential to keep.
A tenant at its max_clients cap refuses further registration with 403 and
invalid_client_metadata, taken under SELECT … FOR UPDATE on the tenant row
before the count so two concurrent registrations cannot both observe room that
only one of them will actually get.
A registered backchannel_logout_uri is stored, echoed back in the registration
response, and now read: discovery advertises backchannel_logout_supported and
its front-channel twin for every tenant (see
discovery above), and
ending a session delivers to it (see front-channel and back-channel
logout below).
userinfo_signed_response_alg is stored, echoed, and now read: a client that
registers RS256 or ES256 gets a signed /userinfo response, and one that
registers none gets a JWT /userinfo response that is not signed — an RFC
7519 §6 unsecured JWT (alg: "none", no signature). Both are application/jwt
and carry iss and aud (OIDC Core §5.3.2 for the "If signed" case; OIDC
Registration §2 and Discovery §3 for why none still serializes as a JWT at all
— see the OIDC Core reading
note).
Registration refuses a value the tenant's own active signing key cannot
produce, and discovery's userinfo_signing_alg_values_supported is that same
tenant's answer — [key.alg, "none"] — never a fixed pair advertised to every
tenant regardless of which key it actually holds (a tenant holds exactly one).
userinfo_encrypted_response_alg and _enc are stored, echoed and now read too
— see Encrypted and nested UserInfo
responses
below for the transcript.
The metadata checks below are shown anonymously, so the policy goes back to
open first — under token every one of them would answer the 401 above
before any metadata was read, and would demonstrate the credential rather
than the check:
odudu seed tenant --name reg-demo --set client_registration_policy=open
{
"command": "tenant",
"created": false,
"tenant": "reg-demo",
"tenantId": "01a0cb08-2747-…",
"settings": ["client_registration_policy"]
}
A non-HTTP redirect_uri has to look like RFC 8252 §7.1's reverse-DNS
custom scheme (ADR 0032): the scheme names at least one ., which is what
tells com.example.app:/cb apart from javascript:, data: and file:
without an enumerable denylist of dangerous ones. A dotless custom scheme —
common in the wild, and otherwise harmless — is refused all the same:
curl -sS -X POST http://localhost:3000/tenants/reg-demo/clients-registrations/openid-connect \
-H 'content-type: application/json' -d '{"redirect_uris":["myapp://cb"]}'
{
"error": "invalid_redirect_uri",
"error_description": "redirect_uris entry myapp://cb is not valid"
}
curl -sS -X POST http://localhost:3000/tenants/reg-demo/clients-registrations/openid-connect \
-H 'content-type: application/json' -d '{"redirect_uris":["com.example.app:/cb"]}'
{
"client_id": "01a0cb08-2e42-…",
"client_id_issued_at": 1790112706,
"client_secret": "of3NqFMwFsEOjqcvz46q-L18sz4EwP2Cge7LtyRRr8A",
"client_secret_expires_at": 0,
"redirect_uris": ["com.example.app:/cb"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "client_secret_basic"
}
A client holds at most 200 redirect URIs, the bound every list on a client
shares (the admin paths); a longer list is
refused with the count and the limit, and 200 exactly is registered. In the
tenant client-reach-demo, with registration opened:
curl -sS -X POST http://localhost:3082/tenants/client-reach-demo/clients-registrations/openid-connect \
-H 'content-type: application/json' -d @reg-over.json
reg-over.json held 201 URIs, https://rp.example/cb/0 to …/cb/200:
{"error":"invalid_redirect_uri","error_description":"redirect_uris holds 201 entries, at most 200"}
frontchannel_logout_uri gets the same https/absolute/no-fragment policy
backchannel_logout_uri already had — it is the src of an iframe the
logout page now renders
(front-channel and back-channel logout),
the sink a bare http: or javascript: value would otherwise reach:
curl -sS -X POST http://localhost:3000/tenants/reg-demo/clients-registrations/openid-connect \
-H 'content-type: application/json' \
-d '{"redirect_uris":["https://rp.example/cb"],"frontchannel_logout_uri":"http://rp.example/fc"}'
{
"error": "invalid_client_metadata",
"error_description": "frontchannel_logout_uri must be an absolute https URI with no fragment"
}
Front-Channel Logout 1.0 §2 also requires a registered frontchannel_logout_uri's
domain, port and scheme to match one of the client's own redirect_uris —
an unauthenticated inbound surface is only as trustworthy as an origin the
client already proved it controls. Matching any one of several registered
redirect URIs satisfies it; a client with none registered has nothing to
match against and is refused the same way:
curl -sS -X POST http://localhost:3000/tenants/reg-demo/clients-registrations/openid-connect \
-H 'content-type: application/json' \
-d '{"redirect_uris":["https://rp.example/cb"],"frontchannel_logout_uri":"https://evil.example/fc"}'
{
"error": "invalid_client_metadata",
"error_description": "frontchannel_logout_uri must share its domain, port and scheme with a registered redirect_uri"
}
frontchannel_logout_session_required is accepted alongside it, stored and
echoed back the same way backchannel_logout_session_required already is,
defaulting to false:
curl -sS -X POST http://localhost:3000/tenants/reg-demo/clients-registrations/openid-connect \
-H 'content-type: application/json' \
-d '{"redirect_uris":["https://rp.example/cb"],"frontchannel_logout_uri":"https://rp.example/fc","frontchannel_logout_session_required":true}'
{
"client_id": "01a0cb08-2e9a-…",
"client_id_issued_at": 1790112706,
"client_secret": "cznVu4B8j-I63JVMDPbEUePpMSyV8EvDiwhOD0HR7Pk",
"client_secret_expires_at": 0,
"redirect_uris": ["https://rp.example/cb"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "client_secret_basic",
"frontchannel_logout_uri": "https://rp.example/fc",
"frontchannel_logout_session_required": true
}
A registered jwks carries public keys only. A key holding any private member —
d, p, q, dp, dq, qi or k — is refused, on registration here and on
POST and PATCH /admin/tenants/{tenant}/clients alike, since all three
validate through the same client metadata check: a stored private half would be
handed back by every read and export of the client. These three calls are from a
different stack, published on port 3080, in a tenant jwks-demo created through
POST /admin/tenants with client_registration_policy set to open through
PATCH /settings, so the refusal is the key's and not the policy's. A private
key is refused by registration and by the admin API, and the first key without
its private member then registers:
curl -sS -X POST http://localhost:3080/tenants/jwks-demo/clients-registrations/openid-connect \
-H 'content-type: application/json' \
-d '{"redirect_uris":["https://rp.example/cb"],"token_endpoint_auth_method":"private_key_jwt","jwks":{"keys":[{"kty":"EC","crv":"P-256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0","d":"jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI"}]}}'
curl -sS -X POST -H "Authorization: Bearer $ADMIN_TOKEN" -H 'content-type: application/json' \
-d '{"client_id":"signer-app","grant_types":["client_credentials"],"token_endpoint_auth_method":"private_key_jwt","jwks":{"keys":[{"kty":"oct","k":"c3ltbWV0cmljLXNlY3JldA"}]}}' \
http://localhost:3080/admin/tenants/jwks-demo/clients
curl -sS -X POST http://localhost:3080/tenants/jwks-demo/clients-registrations/openid-connect \
-H 'content-type: application/json' \
-d '{"redirect_uris":["https://rp.example/cb"],"token_endpoint_auth_method":"private_key_jwt","jwks":{"keys":[{"kty":"EC","crv":"P-256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"}]}}'
{"error":"invalid_client_metadata","error_description":"jwks.keys[0] carries the private member d; register public keys only"}
{"type":"about:blank","title":"Bad Request","status":400,"detail":"jwks.keys[0] carries the private member k; register public keys only","errors":[{"path":"jwks","message":"jwks.keys[0] carries the private member k; register public keys only"}],"instance":"01a0ea63-bbfe-7bcd-9234-5a96705e5f51"}
{"client_id":"01a0ea63-bc2e-76fa-b646-56b5121f644e","client_id_issued_at":1790638799,"client_secret":"_YZPORl5uy3NcYXEzMq8PSjdCnddn4XutbW7T4-QR4Y","client_secret_expires_at":0,"redirect_uris":["https://rp.example/cb"],"grant_types":["authorization_code"],"token_endpoint_auth_method":"private_key_jwt","jwks":{"keys":[{"kty":"EC","crv":"P-256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"}]}}