unoverse 0.1.36 → 0.1.38

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,27 +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(token) {
37
- if (/^[A-Za-z0-9+/]+=*$/.test(token) && !token.startsWith("dop_v1_")) {
38
- const decoded = Buffer.from(token, "base64").toString("utf8");
39
- const m = /^([\x20-\x7e]+):[\x20-\x7e]+$/.exec(decoded);
40
- if (m) return m[1];
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;
41
59
  }
42
- 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 };
43
63
  }
44
64
 
45
- function registryAuth(token) {
46
- if (/^[A-Za-z0-9+/]+=*$/.test(token)) {
47
- const decoded = Buffer.from(token, "base64").toString("utf8");
48
- if (/^[\x20-\x7e]+:[\x20-\x7e]+$/.test(decoded)) return token;
49
- }
50
- return Buffer.from(`${token}:${token}`).toString("base64");
65
+
66
+ function registryAuth(cred) {
67
+ return Buffer.from(`${cred.user}:${cred.pass}`).toString("base64");
51
68
  }
52
69
 
53
70
  /** The gate: prove the token opens the registry before handing over the kit.
54
71
  * Docker registries use the bearer-realm flow: /v2/ answers 401 and names the
55
72
  * auth realm; presenting the token there yields 200 (valid) or 401 (refused). */
56
- async function validateRegistryToken(token) {
73
+ async function validateRegistryToken(cred) {
57
74
  try {
58
75
  const probe = await fetch(`https://${REGISTRY_HOST}/v2/`);
59
76
  const wa = probe.headers.get("www-authenticate") ?? "";
@@ -62,7 +79,7 @@ async function validateRegistryToken(token) {
62
79
  if (!realm) return probe.status === 200;
63
80
  const res = await fetch(
64
81
  `${realm}?service=${encodeURIComponent(service)}&scope=registry:catalog:*`,
65
- { headers: { Authorization: `Basic ${registryAuth(token)}` } },
82
+ { headers: { Authorization: `Basic ${registryAuth(cred)}` } },
66
83
  );
67
84
  return res.ok;
68
85
  } catch {
@@ -113,9 +130,9 @@ function dockerUp() {
113
130
  * the same credential two minutes later — the tool approving a token with a weaker
114
131
  * check than the one that counts. When Docker is up, validation IS docker login (and
115
132
  * the credential lands in the keychain, so nothing downstream logs in again). */
116
- function dockerLogin(token) {
117
- const r = spawnSync("docker", ["login", REGISTRY_HOST, "-u", token, "--password-stdin"], {
118
- input: token,
133
+ function dockerLogin(cred) {
134
+ const r = spawnSync("docker", ["login", REGISTRY_HOST, "-u", cred.user, "--password-stdin"], {
135
+ input: cred.pass,
119
136
  encoding: "utf8",
120
137
  });
121
138
  const err = (r.stderr || "").split("\n").filter((l) => l && !/^WARNING/i.test(l)).pop() || "";
@@ -271,8 +288,8 @@ export async function create(nameArg) {
271
288
  // A refused token re-asks RIGHT HERE. Being thrown out of the wizard to run
272
289
  // create again is its own bug.
273
290
  for (;;) {
274
- token = rawToken(await ask(rl, "Registry access token: "));
275
- if (!token) {
291
+ token = parseCredential(await ask(rl, "Registry access token: "));
292
+ if (!token.pass) {
276
293
  fail("no token. The universe kit is available to licensed operators");
277
294
  process.exit(1);
278
295
  }
@@ -283,7 +300,7 @@ export async function create(nameArg) {
283
300
  if (res.ok) { ok("token accepted"); break; }
284
301
  fail("the registry refused it:");
285
302
  console.log(` ${dim(res.err)}`);
286
- console.log(` ${dim("Paste the registry token from your admin (dop_v1_…), or Ctrl-C to stop.")}\n`);
303
+ console.log(` ${dim("Paste the credential exactly as your admin sent it (any shape works), or Ctrl-C to stop.")}\n`);
287
304
  } else {
288
305
  const valid = await validateRegistryToken(token);
289
306
  console.log("");
@@ -321,7 +338,7 @@ export async function create(nameArg) {
321
338
  const r = spawnSync("bash", [operatorScript, "init"], {
322
339
  stdio: "inherit",
323
340
  cwd: name,
324
- env: { ...process.env, UNOVERSE_DOCR_TOKEN: rawToken(token) },
341
+ env: { ...process.env, UNOVERSE_DOCR_TOKEN: token.pass, UNOVERSE_DOCR_USER: token.user },
325
342
  });
326
343
  if (r.status !== 0) {
327
344
  fail(`setup did not finish. Run 'unoverse start'${name === "." ? "" : ` in ./${name}`} to pick it back up`);
@@ -35,7 +35,7 @@ cmd_db_verify() {
35
35
  workflows: [
36
36
  "id", "name", "description", "nodes", "edges", "active",
37
37
  "execution_mode", "test_inputs", "umap_settings", "viewport",
38
- "mcp_schema", "memory_config", "created_at", "updated_at"
38
+ "mcp_schema", "memory_config", "content_taxonomy", "created_at", "updated_at"
39
39
  ],
40
40
  workflow_executions: [
41
41
  "execution_id", "workflow_id", "status", "start_time", "end_time",
@@ -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,17 +153,30 @@ 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
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=""
160
+ if [[ "$DOCR_TOKEN" != dop_v1_* ]]; then
161
+ local decoded
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
167
+ fi
168
+ DOCR_USER="${DOCR_USER:-$DOCR_TOKEN}"
154
169
  if [[ "$DOCR_TOKEN" == dop_v1_* ]]; then
155
170
  break
156
171
  fi
157
- fail "Token should start with dop_v1_"
172
+ fail "That does not look like a registry credential. Paste it exactly as it was sent"
158
173
  done
159
174
  fi
160
175
 
161
176
  # THE DOWNLOAD STARTS NOW (see the block at the top of this file).
162
177
  if [ "$DOCKER_OK" = "1" ]; then
163
178
  local login_err
164
- 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
165
180
  start_background_pull
166
181
  echo ""
167
182
  echo -e " ${CYAN}⬇${NC} ${DIM}Platform images are downloading in the background while you configure${NC}"
@@ -339,6 +354,7 @@ cmd_init() {
339
354
  cat > "$ROOT/.env" << ENVEOF
340
355
  # Generated by unoverse init
341
356
  DOCR_TOKEN=${DOCR_TOKEN}
357
+ DOCR_USER=${DOCR_USER:-${DOCR_TOKEN}}
342
358
  DATABASE_URL=${DATABASE_URL}
343
359
  REDIS_HOST=${REDIS_HOST}
344
360
  REDIS_PORT=${REDIS_PORT}
@@ -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.36",
3
+ "version": "0.1.38",
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",