unoverse 0.1.37 → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/create.mjs +37 -31
- package/operator/lib/db-setup.sh +6 -0
- package/operator/lib/init.sh +33 -6
- package/operator/lib/start.sh +3 -1
- package/package.json +1 -1
package/lib/create.mjs
CHANGED
|
@@ -33,38 +33,44 @@ async function ask(rl, q) {
|
|
|
33
33
|
* credential gets refused, so detect it and pass it through untouched. */
|
|
34
34
|
/** The raw dop_v1 token, whichever shape was pasted. docker login needs the raw form,
|
|
35
35
|
* so the base64 user:token blob is decoded before anything downstream sees it. */
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
36
|
+
/** The credential AS A PAIR. The downloaded Docker credentials file carries
|
|
37
|
+
* base64("email:token") — the username is the ACCOUNT EMAIL, and logging in with the
|
|
38
|
+
* token as username (right for API tokens) gets "unauthorized" for it. So nothing here
|
|
39
|
+
* reduces to a bare token any more: whatever shape is pasted, the exact user:password
|
|
40
|
+
* pair comes out and is used verbatim.
|
|
41
|
+
* raw dop_v1 → token:token
|
|
42
|
+
* base64("u:p") → u:p (email:token, or :token → token:token)
|
|
43
|
+
* the whole JSON file → its auth value, decoded the same way
|
|
44
|
+
*/
|
|
45
|
+
function parseCredential(input) {
|
|
46
|
+
const t = input.trim().replace(/^["']|["']$/g, "");
|
|
47
|
+
const pair = (d) => {
|
|
48
|
+
const m = /^([\x20-\x7e]*):([\x20-\x7e]+)$/.exec(d);
|
|
49
|
+
return m ? { user: m[1] || m[2], pass: m[2] } : null;
|
|
50
|
+
};
|
|
51
|
+
const auth = /"auth"\s*:\s*"([A-Za-z0-9+/=]+)"/.exec(t);
|
|
52
|
+
if (auth) {
|
|
53
|
+
const p = pair(Buffer.from(auth[1], "base64").toString("utf8"));
|
|
54
|
+
if (p) return p;
|
|
55
|
+
}
|
|
56
|
+
if (/^[A-Za-z0-9+/]+=*$/.test(t)) {
|
|
57
|
+
const p = pair(Buffer.from(t, "base64").toString("utf8"));
|
|
58
|
+
if (p) return p;
|
|
52
59
|
}
|
|
53
|
-
|
|
60
|
+
const inline = /dop_v1_[0-9a-f]{64}/.exec(t);
|
|
61
|
+
if (inline) return { user: inline[0], pass: inline[0] };
|
|
62
|
+
return { user: t, pass: t };
|
|
54
63
|
}
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (/^[\x20-\x7e]+:[\x20-\x7e]+$/.test(decoded)) return token;
|
|
60
|
-
}
|
|
61
|
-
return Buffer.from(`${token}:${token}`).toString("base64");
|
|
65
|
+
|
|
66
|
+
function registryAuth(cred) {
|
|
67
|
+
return Buffer.from(`${cred.user}:${cred.pass}`).toString("base64");
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
/** The gate: prove the token opens the registry before handing over the kit.
|
|
65
71
|
* Docker registries use the bearer-realm flow: /v2/ answers 401 and names the
|
|
66
72
|
* auth realm; presenting the token there yields 200 (valid) or 401 (refused). */
|
|
67
|
-
async function validateRegistryToken(
|
|
73
|
+
async function validateRegistryToken(cred) {
|
|
68
74
|
try {
|
|
69
75
|
const probe = await fetch(`https://${REGISTRY_HOST}/v2/`);
|
|
70
76
|
const wa = probe.headers.get("www-authenticate") ?? "";
|
|
@@ -73,7 +79,7 @@ async function validateRegistryToken(token) {
|
|
|
73
79
|
if (!realm) return probe.status === 200;
|
|
74
80
|
const res = await fetch(
|
|
75
81
|
`${realm}?service=${encodeURIComponent(service)}&scope=registry:catalog:*`,
|
|
76
|
-
{ headers: { Authorization: `Basic ${registryAuth(
|
|
82
|
+
{ headers: { Authorization: `Basic ${registryAuth(cred)}` } },
|
|
77
83
|
);
|
|
78
84
|
return res.ok;
|
|
79
85
|
} catch {
|
|
@@ -124,9 +130,9 @@ function dockerUp() {
|
|
|
124
130
|
* the same credential two minutes later — the tool approving a token with a weaker
|
|
125
131
|
* check than the one that counts. When Docker is up, validation IS docker login (and
|
|
126
132
|
* the credential lands in the keychain, so nothing downstream logs in again). */
|
|
127
|
-
function dockerLogin(
|
|
128
|
-
const r = spawnSync("docker", ["login", REGISTRY_HOST, "-u",
|
|
129
|
-
input:
|
|
133
|
+
function dockerLogin(cred) {
|
|
134
|
+
const r = spawnSync("docker", ["login", REGISTRY_HOST, "-u", cred.user, "--password-stdin"], {
|
|
135
|
+
input: cred.pass,
|
|
130
136
|
encoding: "utf8",
|
|
131
137
|
});
|
|
132
138
|
const err = (r.stderr || "").split("\n").filter((l) => l && !/^WARNING/i.test(l)).pop() || "";
|
|
@@ -282,8 +288,8 @@ export async function create(nameArg) {
|
|
|
282
288
|
// A refused token re-asks RIGHT HERE. Being thrown out of the wizard to run
|
|
283
289
|
// create again is its own bug.
|
|
284
290
|
for (;;) {
|
|
285
|
-
token =
|
|
286
|
-
if (!token) {
|
|
291
|
+
token = parseCredential(await ask(rl, "Registry access token: "));
|
|
292
|
+
if (!token.pass) {
|
|
287
293
|
fail("no token. The universe kit is available to licensed operators");
|
|
288
294
|
process.exit(1);
|
|
289
295
|
}
|
|
@@ -332,7 +338,7 @@ export async function create(nameArg) {
|
|
|
332
338
|
const r = spawnSync("bash", [operatorScript, "init"], {
|
|
333
339
|
stdio: "inherit",
|
|
334
340
|
cwd: name,
|
|
335
|
-
env: { ...process.env, UNOVERSE_DOCR_TOKEN:
|
|
341
|
+
env: { ...process.env, UNOVERSE_DOCR_TOKEN: token.pass, UNOVERSE_DOCR_USER: token.user },
|
|
336
342
|
});
|
|
337
343
|
if (r.status !== 0) {
|
|
338
344
|
fail(`setup did not finish. Run 'unoverse start'${name === "." ? "" : ` in ./${name}`} to pick it back up`);
|
package/operator/lib/db-setup.sh
CHANGED
|
@@ -34,11 +34,16 @@ cmd_db_setup() {
|
|
|
34
34
|
echo ""
|
|
35
35
|
echo " Running migrations (local dev)..."
|
|
36
36
|
|
|
37
|
+
# --no-check-order: databases from before the 2026-07-28 squash have
|
|
38
|
+
# 002-018 recorded in pgmigrations with no matching files (they live inside
|
|
39
|
+
# 001_baseline now), which trips the order check on ANY new migration.
|
|
40
|
+
# Ordering still holds by filename; applied names are never re-run.
|
|
37
41
|
NODE_TLS_REJECT_UNAUTHORIZED=0 DATABASE_URL="$db_url" \
|
|
38
42
|
npx node-pg-migrate up \
|
|
39
43
|
--migrations-dir "$ROOT/apps/unoverse/engine/migrations" \
|
|
40
44
|
--migration-file-language sql \
|
|
41
45
|
--no-lock \
|
|
46
|
+
--no-check-order \
|
|
42
47
|
2>&1 | sed 's/^/ /' || {
|
|
43
48
|
fail "Migrations failed"
|
|
44
49
|
exit 1
|
|
@@ -73,6 +78,7 @@ cmd_db_setup() {
|
|
|
73
78
|
--migrations-dir /app/apps/unoverse/engine/migrations \
|
|
74
79
|
--migration-file-language sql \
|
|
75
80
|
--no-lock \
|
|
81
|
+
--no-check-order \
|
|
76
82
|
2>&1 | sed 's/^/ /' || {
|
|
77
83
|
fail "Migrations failed"
|
|
78
84
|
exit 1
|
package/operator/lib/init.sh
CHANGED
|
@@ -140,10 +140,12 @@ cmd_init() {
|
|
|
140
140
|
# twice minutes apart. Typed here only when init is run on its own.
|
|
141
141
|
if [ -n "${UNOVERSE_DOCR_TOKEN:-}" ]; then
|
|
142
142
|
DOCR_TOKEN="$UNOVERSE_DOCR_TOKEN"
|
|
143
|
+
DOCR_USER="${UNOVERSE_DOCR_USER:-$UNOVERSE_DOCR_TOKEN}"
|
|
143
144
|
ok "Registry token carried over from create"
|
|
144
145
|
else
|
|
145
146
|
local cur_token
|
|
146
147
|
cur_token=$(_env_cur DOCR_TOKEN)
|
|
148
|
+
DOCR_USER=$(_env_cur DOCR_USER)
|
|
147
149
|
while true; do
|
|
148
150
|
if [ -n "$cur_token" ]; then
|
|
149
151
|
read -p " DOCR Token [keep current]: " DOCR_TOKEN || { fail "no input (end of stream). Run unoverse init interactively"; exit 1; }
|
|
@@ -151,14 +153,19 @@ cmd_init() {
|
|
|
151
153
|
else
|
|
152
154
|
read -p " DOCR Token (from your Unoverse admin): " DOCR_TOKEN || { fail "no input (end of stream). Run unoverse init interactively"; exit 1; }
|
|
153
155
|
fi
|
|
154
|
-
# Accept the credential in any shape
|
|
155
|
-
#
|
|
156
|
-
#
|
|
156
|
+
# Accept the credential in any shape, AS A PAIR. The downloaded Docker
|
|
157
|
+
# credentials wrap base64("email:token") — the username is the email, and
|
|
158
|
+
# logging in token-as-username gets "unauthorized" for exactly that shape.
|
|
159
|
+
DOCR_USER=""
|
|
157
160
|
if [[ "$DOCR_TOKEN" != dop_v1_* ]]; then
|
|
158
161
|
local decoded
|
|
159
|
-
decoded=$(printf '%s' "$DOCR_TOKEN" | base64 -d 2>/dev/null | tr -d '\0')
|
|
160
|
-
|
|
162
|
+
decoded=$(printf '%s' "$DOCR_TOKEN" | sed -E 's/.*"auth"[^"]*"([A-Za-z0-9+\/=]+)".*/\1/' | base64 -d 2>/dev/null | tr -d '\0')
|
|
163
|
+
[ -n "$decoded" ] || decoded=$(printf '%s' "$DOCR_TOKEN" | base64 -d 2>/dev/null | tr -d '\0')
|
|
164
|
+
case "$decoded" in
|
|
165
|
+
*:dop_v1_*) DOCR_USER="${decoded%%:*}"; DOCR_TOKEN="dop_v1_${decoded##*dop_v1_}";;
|
|
166
|
+
esac
|
|
161
167
|
fi
|
|
168
|
+
DOCR_USER="${DOCR_USER:-$DOCR_TOKEN}"
|
|
162
169
|
if [[ "$DOCR_TOKEN" == dop_v1_* ]]; then
|
|
163
170
|
break
|
|
164
171
|
fi
|
|
@@ -169,7 +176,7 @@ cmd_init() {
|
|
|
169
176
|
# THE DOWNLOAD STARTS NOW (see the block at the top of this file).
|
|
170
177
|
if [ "$DOCKER_OK" = "1" ]; then
|
|
171
178
|
local login_err
|
|
172
|
-
if login_err=$(echo "$DOCR_TOKEN" | docker login "$DOCR_REGISTRY" -u "$DOCR_TOKEN" --password-stdin 2>&1 >/dev/null); then
|
|
179
|
+
if login_err=$(echo "$DOCR_TOKEN" | docker login "$DOCR_REGISTRY" -u "${DOCR_USER:-$DOCR_TOKEN}" --password-stdin 2>&1 >/dev/null); then
|
|
173
180
|
start_background_pull
|
|
174
181
|
echo ""
|
|
175
182
|
echo -e " ${CYAN}⬇${NC} ${DIM}Platform images are downloading in the background while you configure${NC}"
|
|
@@ -343,10 +350,23 @@ cmd_init() {
|
|
|
343
350
|
read -p " SEARCHAPI_KEY (web-search nodes, blank to skip): " SEARCHAPI_KEY
|
|
344
351
|
fi
|
|
345
352
|
|
|
353
|
+
# Silent facts, not questions. The encryption key is GENERATED (a human never types
|
|
354
|
+
# one), kept on re-runs (a new key orphans every stored credential), and the pool
|
|
355
|
+
# sizes are present-but-empty purely so compose stops warning about them.
|
|
356
|
+
CREDENTIAL_ENCRYPTION_KEY=$(_env_cur CREDENTIAL_ENCRYPTION_KEY)
|
|
357
|
+
if [ -z "$CREDENTIAL_ENCRYPTION_KEY" ]; then
|
|
358
|
+
CREDENTIAL_ENCRYPTION_KEY=$(openssl rand -hex 32 2>/dev/null || head -c32 /dev/urandom | xxd -p -c64)
|
|
359
|
+
ok "Credential encryption key generated"
|
|
360
|
+
fi
|
|
361
|
+
DB_POOL_ENGINE=$(_env_cur DB_POOL_ENGINE)
|
|
362
|
+
DB_POOL_ENGINE_LEGACY=$(_env_cur DB_POOL_ENGINE_LEGACY)
|
|
363
|
+
DB_POOL_MEMORY=$(_env_cur DB_POOL_MEMORY)
|
|
364
|
+
|
|
346
365
|
# Write .env
|
|
347
366
|
cat > "$ROOT/.env" << ENVEOF
|
|
348
367
|
# Generated by unoverse init
|
|
349
368
|
DOCR_TOKEN=${DOCR_TOKEN}
|
|
369
|
+
DOCR_USER=${DOCR_USER:-${DOCR_TOKEN}}
|
|
350
370
|
DATABASE_URL=${DATABASE_URL}
|
|
351
371
|
REDIS_HOST=${REDIS_HOST}
|
|
352
372
|
REDIS_PORT=${REDIS_PORT}
|
|
@@ -359,6 +379,13 @@ AUTH_CLIENT_ID=${AUTH_CLIENT_ID}
|
|
|
359
379
|
AUTH_AUDIENCE=${AUTH_AUDIENCE}
|
|
360
380
|
API_URL=${API_URL}
|
|
361
381
|
OPENAI_API_KEY=${OPENAI_API_KEY}
|
|
382
|
+
# Generated at setup: encrypts credentials stored by nodes. Losing it orphans them —
|
|
383
|
+
# back it up with the database. Kept on re-runs.
|
|
384
|
+
CREDENTIAL_ENCRYPTION_KEY=${CREDENTIAL_ENCRYPTION_KEY}
|
|
385
|
+
# Present-but-empty so compose does not warn; the platform applies its own defaults.
|
|
386
|
+
DB_POOL_ENGINE=${DB_POOL_ENGINE}
|
|
387
|
+
DB_POOL_ENGINE_LEGACY=${DB_POOL_ENGINE_LEGACY}
|
|
388
|
+
DB_POOL_MEMORY=${DB_POOL_MEMORY}
|
|
362
389
|
HYPERBROWSER_API_KEY=${HYPERBROWSER_API_KEY}
|
|
363
390
|
SEARCHAPI_KEY=${SEARCHAPI_KEY}
|
|
364
391
|
DOMAIN=
|
package/operator/lib/start.sh
CHANGED
|
@@ -55,8 +55,10 @@ cmd_start() {
|
|
|
55
55
|
# Login to registry if DOCR_TOKEN is set
|
|
56
56
|
local docr_token
|
|
57
57
|
docr_token=$(grep "^DOCR_TOKEN=" "$ROOT/.env" 2>/dev/null | cut -d'=' -f2-)
|
|
58
|
+
local docr_user
|
|
59
|
+
docr_user=$(grep "^DOCR_USER=" "$ROOT/.env" 2>/dev/null | cut -d'=' -f2-)
|
|
58
60
|
if [ -n "$docr_token" ]; then
|
|
59
|
-
echo "$docr_token" | docker login registry.digitalocean.com -u "$docr_token" --password-stdin >/dev/null 2>&1 || true
|
|
61
|
+
echo "$docr_token" | docker login registry.digitalocean.com -u "${docr_user:-$docr_token}" --password-stdin >/dev/null 2>&1 || true
|
|
60
62
|
fi
|
|
61
63
|
|
|
62
64
|
# `start --pull` refreshes the images first. This is where refreshing a LOCAL universe
|
package/package.json
CHANGED