@agentprojectcontext/apx 1.15.4 → 1.15.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.15.4",
3
+ "version": "1.15.5",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/cli/http.js CHANGED
@@ -11,6 +11,12 @@ const __dirname = path.dirname(__filename);
11
11
  const DEFAULT_PORT = parseInt(process.env.APX_PORT || "7430", 10);
12
12
  const DEFAULT_HOST = process.env.APX_HOST || "127.0.0.1";
13
13
 
14
+ const TOKEN_PATH = path.join(os.homedir(), ".apx", "daemon.token");
15
+
16
+ function readToken() {
17
+ try { return fs.readFileSync(TOKEN_PATH, "utf8").trim(); } catch { return ""; }
18
+ }
19
+
14
20
  function baseUrl() {
15
21
  return `http://${DEFAULT_HOST}:${DEFAULT_PORT}`;
16
22
  }
@@ -71,9 +77,13 @@ async function request(method, path, body, opts = {}) {
71
77
  else if (!(await ping())) {
72
78
  throw new Error(`apx daemon not running (no response on ${baseUrl()})`);
73
79
  }
80
+ const token = readToken();
74
81
  const res = await fetch(`${baseUrl()}${path}`, {
75
82
  method,
76
- headers: body ? { "content-type": "application/json" } : {},
83
+ headers: {
84
+ ...(body ? { "content-type": "application/json" } : {}),
85
+ ...(token ? { "authorization": `Bearer ${token}` } : {}),
86
+ },
77
87
  body: body ? JSON.stringify(body) : undefined,
78
88
  signal: opts.signal,
79
89
  });
@@ -98,9 +108,13 @@ async function streamRequest(method, path, body, onEvent, opts = {}) {
98
108
  throw new Error(`apx daemon not running (no response on ${baseUrl()})`);
99
109
  }
100
110
 
111
+ const token = readToken();
101
112
  const res = await fetch(`${baseUrl()}${path}`, {
102
113
  method,
103
- headers: body ? { "content-type": "application/json" } : {},
114
+ headers: {
115
+ ...(body ? { "content-type": "application/json" } : {}),
116
+ ...(token ? { "authorization": `Bearer ${token}` } : {}),
117
+ },
104
118
  body: body ? JSON.stringify(body) : undefined,
105
119
  signal: opts.signal,
106
120
  });
@@ -8,6 +8,7 @@ export const CONFIG_PATH = path.join(APX_HOME, "config.json");
8
8
  export const PID_PATH = path.join(APX_HOME, "daemon.pid");
9
9
  export const LOG_PATH = path.join(APX_HOME, "daemon.log");
10
10
  export const TELEGRAM_STATE_PATH = path.join(APX_HOME, "telegram-state.json");
11
+ export const TOKEN_PATH = path.join(APX_HOME, "daemon.token");
11
12
  // Global channel messages (telegram, direct, whatsapp, …) live here,
12
13
  // separated from any project. Structure: ~/.apx/messages/<channel>/YYYY-MM-DD.jsonl
13
14
  export const GLOBAL_MESSAGES_DIR = path.join(APX_HOME, "messages");
package/src/daemon/api.js CHANGED
@@ -78,7 +78,7 @@ function appendSuperAgentErrorTrace(req, error, details = {}) {
78
78
  });
79
79
  }
80
80
 
81
- export function buildApi({ projects, registries, plugins, scheduler, version, startedAt, addProjectGlobally, config }) {
81
+ export function buildApi({ projects, registries, plugins, scheduler, version, startedAt, addProjectGlobally, config, token }) {
82
82
  const telegram = plugins?.get("telegram");
83
83
 
84
84
  const app = express();
@@ -89,6 +89,17 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
89
89
  next();
90
90
  });
91
91
 
92
+ // Token auth — skip only /health so the CLI can ping before reading the token.
93
+ if (token) {
94
+ app.use((req, res, next) => {
95
+ if (req.path === "/health") return next();
96
+ const auth = req.get("authorization") || "";
97
+ const provided = auth.startsWith("Bearer ") ? auth.slice(7) : "";
98
+ if (provided !== token) return res.status(401).json({ error: "unauthorized" });
99
+ next();
100
+ });
101
+ }
102
+
92
103
  // ---- Tool routers (fetch / browser / search / glob / grep / registry) ----
93
104
  // fetch = native HTTP, no Chromium → fast, cheap, default for REST/HTML
94
105
  // browser = Puppeteer-backed → heavy, lazy-launched, for JS-rendered pages
@@ -3,6 +3,7 @@
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { randomBytes } from "node:crypto";
6
7
  import {
7
8
  readConfig,
8
9
  writeConfig,
@@ -12,6 +13,7 @@ import {
12
13
  PID_PATH,
13
14
  LOG_PATH,
14
15
  APX_HOME,
16
+ TOKEN_PATH,
15
17
  } from "../core/config.js";
16
18
  import { ProjectManager } from "./db.js";
17
19
  import { McpRegistry } from "./mcp-runner.js";
@@ -76,6 +78,12 @@ function clearPid() {
76
78
  } catch {}
77
79
  }
78
80
 
81
+ function generateToken() {
82
+ const token = randomBytes(32).toString("hex");
83
+ fs.writeFileSync(TOKEN_PATH, token, { mode: 0o600 });
84
+ return token;
85
+ }
86
+
79
87
  class RegistryCache {
80
88
  constructor() {
81
89
  this.byProjectId = new Map();
@@ -98,6 +106,7 @@ class RegistryCache {
98
106
  async function main() {
99
107
  ensureHome();
100
108
  claimSingleton();
109
+ const token = generateToken();
101
110
 
102
111
  const cfg = readConfig();
103
112
  const host = effectiveHost(cfg);
@@ -138,6 +147,7 @@ async function main() {
138
147
  plugins,
139
148
  scheduler,
140
149
  config: cfg,
150
+ token,
141
151
  version: PKG.version,
142
152
  startedAt,
143
153
  addProjectGlobally: (absPath) => {
@@ -33,6 +33,22 @@ async function getFetch() {
33
33
  const DEFAULT_TIMEOUT = 30000;
34
34
  const MAX_BODY_BYTES = 5 * 1024 * 1024; // 5MB
35
35
 
36
+ // Block private/link-local ranges and cloud metadata endpoints to prevent SSRF.
37
+ const BLOCKED_HOST_RE = /^(localhost|metadata\.google\.internal\.?)$/i;
38
+ const PRIVATE_IP_RE = /^(127\.\d+\.\d+\.\d+|0\.0\.0\.0|::1|10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|169\.254\.\d+\.\d+|fd[0-9a-f]{2}:)/i;
39
+
40
+ function validateUrl(rawUrl) {
41
+ let parsed;
42
+ try { parsed = new URL(rawUrl); } catch { throw new Error("Invalid URL"); }
43
+ if (!["http:", "https:"].includes(parsed.protocol)) {
44
+ throw new Error(`Protocol "${parsed.protocol}" is not allowed; use http or https`);
45
+ }
46
+ const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
47
+ if (BLOCKED_HOST_RE.test(host) || PRIVATE_IP_RE.test(host)) {
48
+ throw new Error(`Requests to private or link-local addresses are blocked`);
49
+ }
50
+ }
51
+
36
52
  async function readBody(response, jsonHint) {
37
53
  const ctype = response.headers.get("content-type") || "";
38
54
  const wantsJson = jsonHint || ctype.includes("application/json");
@@ -57,6 +73,7 @@ async function readBody(response, jsonHint) {
57
73
 
58
74
  async function doRequest({ url, method = "GET", headers = {}, body = null, timeout_ms = DEFAULT_TIMEOUT, json = false } = {}) {
59
75
  if (!url) throw new Error("url required");
76
+ validateUrl(url);
60
77
  const fetch = await getFetch();
61
78
 
62
79
  const controller = new AbortController();