feat(multi-site): wire selfUrl through setup.sh so joins register live

Extends the shipped CFG_MASTER_DIRECTORY_URL/JOIN_KEY join flow with the
selfUrl a spoke needs to register itself for live catalog replication
(theta-directory v2.4.0's POST /api/site/spokes) -- without this, every
spoke was permanently limited to the one-time join snapshot even after
the master gained the ability to push live updates.

setup.sh already computes CFG_SSO_HOST before this point in the script;
passes https://$CFG_SSO_HOST as bootstrap/site-join.js's third argument,
which forwards it as `selfUrl` in the POST /api/site/join body.
This commit is contained in:
2026-08-10 16:50:07 -04:00
parent 0367c33542
commit f42b69c084
3 changed files with 23 additions and 5 deletions
+12 -3
View File
@@ -5,7 +5,13 @@
* setup.env sets CFG_MASTER_DIRECTORY_URL + CFG_MASTER_DIRECTORY_JOIN_KEY:
*
* docker compose exec sso-manager node /bootstrap/site-join.js \
* https://sso.master.example.com stj_9f2e...
* https://sso.master.example.com stj_9f2e... https://sso.this-site.example.com
*
* The third argument (selfUrl, optional) is this site's own public SSO host
* (setup.sh passes https://$CFG_SSO_HOST) -- without it the join still
* succeeds, it just registers for one-time adoption only: the master has no
* way to reach this spoke to push live replication resync pings at it (see
* theta-directory's docs/site-join.md and utils/site_replicate.js).
*
* Self-contained (Node built-ins + global fetch), same rule as bootstrap.js —
* it does NOT require the SSO's internal models. It logs in as the bootstrap
@@ -26,6 +32,7 @@ const SSO_INTERNAL = 'http://localhost:3001';
const masterUrl = process.argv[2];
const joinKey = process.argv[3];
const selfUrl = process.argv[4] || '';
function log(msg) { console.error('[site-join] ' + msg); }
@@ -52,7 +59,7 @@ async function main() {
const res = await fetch(`${SSO_INTERNAL}/api/site/join`, {
method: 'POST',
headers: { 'auth-token': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ masterUrl, joinKey }),
body: JSON.stringify({ masterUrl, joinKey, ...(selfUrl ? { selfUrl } : {}) }),
});
const text = await res.text().catch(() => '');
let data = null;
@@ -68,11 +75,13 @@ async function main() {
}
log(`Joined master site ${masterUrl} as ${data.siteSlug || '?'}`);
log(`Live replication: ${(data.replication && data.replication.note) || 'unknown'}`);
console.log([
`JOINED=yes`,
`SITE_SLUG=${data.siteSlug || ''}`,
`RESOURCES=${(data.resources && data.resources.created) || 0}`,
`LDAP=${(data.ldap && data.ldap.note) || ''}`
`LDAP=${(data.ldap && data.ldap.note) || ''}`,
`LIVE_REPLICATION=${(data.replication && data.replication.live) ? 'yes' : 'no'}`
].join(' '));
}
+6 -1
View File
@@ -55,7 +55,12 @@ CFG_DOMAIN=example.com
# -> Mint key). Honored ONLY on a first-run bring-up (before ./config/ exists),
# so it can never merge an already-populated directory; re-runs ignore it.
# The spoke adopts the master's users/groups/resources and persists its spoke
# role in ./config/site.json (isMaster=false, masterUrl, siteSlug).
# role in ./config/site.json (isMaster=false, masterUrl, siteSlug). It also
# registers itself with the master (using this site's own CFG_SSO_HOST) so
# future catalog changes on the master get pushed here live instead of this
# being a one-time snapshot -- the master must be able to reach THIS site's
# CFG_SSO_HOST for that part to work; if it can't (this site has no inbound
# path), the join still succeeds, it just never receives live updates.
#CFG_MASTER_DIRECTORY_URL=https://sso.master.example.com
#CFG_MASTER_DIRECTORY_JOIN_KEY=stj_9f2e...
+5 -1
View File
@@ -1161,8 +1161,12 @@ fi
# joined reports "already a spoke" and setup continues.
if [[ -n "${CFG_MASTER_DIRECTORY_URL:-}" && -n "${CFG_MASTER_DIRECTORY_JOIN_KEY:-}" ]]; then
info "Joining master site ${CFG_MASTER_DIRECTORY_URL} (CFG_MASTER_DIRECTORY_*)..."
# selfUrl (https://$CFG_SSO_HOST, already derived above) registers this
# spoke for LIVE replication -- without it the join still succeeds, but
# the master has no way to reach this spoke to push resync pings, so it
# only ever gets the one-time snapshot from the moment it joined.
if ! "${COMPOSE[@]}" exec -T sso-manager node /bootstrap/site-join.js \
"$CFG_MASTER_DIRECTORY_URL" "$CFG_MASTER_DIRECTORY_JOIN_KEY"; then
"$CFG_MASTER_DIRECTORY_URL" "$CFG_MASTER_DIRECTORY_JOIN_KEY" "https://$CFG_SSO_HOST"; then
die "site join failed — check the master URL + site join key (mint one on the master's Site Join Keys card)."
fi
else