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 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
- function rawToken(input) {
37
- // Whatever shape arrives, the dop_v1 token inside comes out:
38
- // - the raw token
39
- // - base64("token:token") (API-token docker auth)
40
- // - base64(":token") (DO's READ-ONLY credential: EMPTY username
41
- // the shape the downloaded Docker Credentials file actually carries, and the one
42
- // a required-username decoder silently passed through, so docker was handed the
43
- // whole blob as a username and answered "unauthorized")
44
- // - the whole credentials JSON pasted as-is
45
- const token = input.trim().replace(/^["']|["']$/g, "");
46
- const inline = /dop_v1_[0-9a-f]{64}/.exec(token);
47
- if (inline) return inline[0];
48
- if (/^[A-Za-z0-9+/]+=*$/.test(token)) {
49
- const decoded = Buffer.from(token, "base64").toString("utf8");
50
- const m = /^([\x20-\x7e]*):([\x20-\x7e]+)$/.exec(decoded);
51
- if (m) return m[1] || m[2];
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
- return token;
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
- function registryAuth(token) {
57
- if (/^[A-Za-z0-9+/]+=*$/.test(token)) {
58
- const decoded = Buffer.from(token, "base64").toString("utf8");
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(token) {
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(token)}` } },
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(token) {
128
- const r = spawnSync("docker", ["login", REGISTRY_HOST, "-u", token, "--password-stdin"], {
129
- input: token,
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 = rawToken(await ask(rl, "Registry access 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: rawToken(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`);
@@ -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
@@ -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: raw dop_v1, or the base64 blob from DO's
155
- # downloaded Docker credentials (which does NOT start with dop_v1 it wraps the
156
- # token behind an empty username). Extract rather than lecture about the format.
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
- case "$decoded" in *dop_v1_*) DOCR_TOKEN="dop_v1_${decoded##*dop_v1_}";; esac
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=
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "The Unoverse front door — create a Studio project, a universe, or a client app, and launch Studio.",
5
5
  "license": "SEE LICENSE IN README.md",
6
6
  "type": "module",