{"record":{"id":"1e6f4fb0f249393c","repo":"paperclipai/paperclip","slug":"createos-api-url-must-be-an-https-origin-optionally-ending","errorCode":null,"errorMessage":"CreateOS API URL must be an HTTPS origin (optionally ending in /v1); HTTP is allowed on loopback only.","messagePattern":"CreateOS API URL must be an HTTPS origin \\(optionally ending in /v1\\); HTTP is allowed on loopback only\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/plugins/sandbox-providers/createos/src/config.ts","lineNumber":30,"sourceCode":"  const text = (key: string): string | null => {\n    const value = raw[key];\n    if (value == null) return null;\n    if (typeof value !== \"string\" || !value.trim() || value.includes(\"\\0\")) {\n      throw new Error(`${key} must be a non-empty string.`);\n    }\n    return value.trim();\n  };\n  const apiUrl = text(\"apiUrl\");\n  if (!apiUrl) throw new Error(\"CreateOS requires an API URL.\");\n  let url: URL;\n  try { url = new URL(apiUrl); } catch { throw new Error(\"CreateOS API URL is invalid.\"); }\n  // Configuration is board-owned, but never follow redirects with the API key.\n  // Plain HTTP is useful for a loopback development server only.\n  const loopback = [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(url.hostname);\n  if ((url.protocol !== \"https:\" && !(url.protocol === \"http:\" && loopback)) ||\n      url.username || url.password || url.search || url.hash ||\n      ![\"\", \"/\", \"/v1\", \"/v1/\"].includes(url.pathname)) {\n    throw new Error(\"CreateOS API URL must be an HTTPS origin (optionally ending in /v1); HTTP is allowed on loopback only.\");\n  }\n  const shape = text(\"shape\");\n  if (!shape) throw new Error(\"CreateOS requires a shape from its shape catalog.\");\n  const timeoutMs = raw.timeoutMs ?? 300_000;\n  if (typeof timeoutMs !== \"number\" || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 86_400_000) {\n    throw new Error(\"timeoutMs must be an integer between 1 and 86400000.\");\n  }\n  if (raw.reuseLease != null && typeof raw.reuseLease !== \"boolean\") {\n    throw new Error(\"reuseLease must be a boolean.\");\n  }\n  return {\n    apiUrl: url.origin,\n    apiKey: text(\"apiKey\"),\n    shape,\n    rootfs: text(\"rootfs\"),\n    region: text(\"region\"),\n    timeoutMs,\n    reuseLease: raw.reuseLease === true,","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/plugins/sandbox-providers/createos/src/config.ts#L12-L48","documentation":"parseConfig in the CreateOS sandbox plugin validates the configured apiUrl before the plugin ever talks to the remote service. The URL must be an HTTPS origin (optionally with a trailing /v1 path), must not embed credentials, query strings, or fragments, and plain HTTP is accepted only for loopback hosts (localhost, 127.0.0.1, [::1]). This guard prevents accidentally sending the API key over unencrypted transport or to a redirecting/non-origin URL.","triggerScenarios":"Calling parseConfig (directly or via client/config construction, or the onEnvironmentValidateConfig/onEnvironmentProbe hooks) with a config whose apiUrl is http:// on a non-loopback host, uses ftp/other schemes, contains ?query, #fragment, user:pass@ credentials, or a pathname other than '', '/', '/v1', or '/v1/'.","commonSituations":"Local dev server on a LAN IP (http://192.168.x.x) instead of localhost; pointing at a self-hosted CreateOS instance behind plain HTTP; pasting a URL with a trailing query string or embedded basic-auth credentials; pointing at a proxy path like /api/v2.","solutions":["Change the apiUrl scheme to https:// (e.g. https://api.sb.createos.sh or https://your-host/v1).","If developing locally, serve on localhost, 127.0.0.1, or [::1] so HTTP loopback is allowed.","Strip credentials, query strings, and fragments from the URL; put the key in the environment config or CREATEOS_API_KEY instead.","Normalize the path to just the origin or origin + /v1; put any other path prefix behind a reverse proxy on that origin."],"exampleFix":"// before\n{ \"apiUrl\": \"http://sandbox.internal:8080/api/v1?env=dev\" }\n// after\n{ \"apiUrl\": \"https://sandbox.internal:8080/v1\" }","handlingStrategy":"validation","validationCode":"function isValidCreateosUrl(raw) {\n  try {\n    const u = new URL(raw);\n    const loopback = [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(u.hostname);\n    return (u.protocol === \"https:\" || (u.protocol === \"http:\" && loopback)) &&\n      !u.username && !u.password && !u.search && !u.hash &&\n      [\"\", \"/\", \"/v1\", \"/v1/\"].includes(u.pathname);\n  } catch { return false; }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always use https:// origins in stored configs","Keep the URL path to '' , '/', or '/v1' and proxy anything else at the host","Never embed credentials in the URL; use the apiKey/env var"],"tags":["config","url-validation","https","security"],"backgroundTag":"invalid-url-format","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T10:30:35.592Z"}