@cabane/companion 0.6.237 → 0.6.239

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/dist/launch.js CHANGED
@@ -45,7 +45,8 @@ import { dirname as dirname3, isAbsolute, join as join4 } from "path";
45
45
  import { z as z2 } from "zod";
46
46
 
47
47
  // src/pairing.ts
48
- function isAllowedBaseUrl(raw) {
48
+ var ALLOW_PLAIN_HTTP_HOST_ENV = "CABANE_ALLOW_PLAIN_HTTP_HOST";
49
+ function isAllowedBaseUrl(raw, env = process.env) {
49
50
  let url;
50
51
  try {
51
52
  url = new URL(raw);
@@ -55,10 +56,41 @@ function isAllowedBaseUrl(raw) {
55
56
  if (url.protocol === "https:") return true;
56
57
  if (url.protocol === "http:") {
57
58
  const host = url.hostname.toLowerCase();
58
- return host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "::1" || host === "[::1]";
59
+ if (host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "::1" || host === "[::1]")
60
+ return true;
61
+ const allowed = plainHttpHost(env);
62
+ return allowed !== null && host === allowed;
59
63
  }
60
64
  return false;
61
65
  }
66
+ function plainHttpHost(env = process.env) {
67
+ const raw = env[ALLOW_PLAIN_HTTP_HOST_ENV]?.trim().toLowerCase();
68
+ if (!raw) return null;
69
+ if (isSingleLabel(raw) || isPrivateIpv4(raw)) return raw;
70
+ const bare = raw.startsWith("[") && raw.endsWith("]") ? raw.slice(1, -1) : raw;
71
+ return privateIpv6(bare);
72
+ }
73
+ function isSingleLabel(host) {
74
+ return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(host) && !/^\d+$/.test(host);
75
+ }
76
+ function isPrivateIpv4(host) {
77
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
78
+ if (!m) return false;
79
+ const [a, b, c, d] = m.slice(1).map(Number);
80
+ if ([a, b, c, d].some((n) => n > 255)) return false;
81
+ return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
82
+ }
83
+ function privateIpv6(host) {
84
+ if (!host.includes(":")) return null;
85
+ let bracketed;
86
+ try {
87
+ bracketed = new URL(`http://[${host}]/`).hostname;
88
+ } catch {
89
+ return null;
90
+ }
91
+ const first = Number.parseInt(bracketed.slice(1).split(":")[0] || "0", 16);
92
+ return (first & 65024) === 64512 ? bracketed : null;
93
+ }
62
94
 
63
95
  // src/prepare-hook.ts
64
96
  import { spawn } from "child_process";
@@ -121,10 +153,10 @@ function isRootPath(value) {
121
153
  return isAbsolute(value) || value === "~" || value.startsWith("~/");
122
154
  }
123
155
  var companionConfigSchema = z2.object({
124
- // https-enforced (loopback exempt), so a hand-edited config can't put the
125
- // device token and prompt on a plaintext channel.
126
- baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
127
- message: "baseUrl must be https (loopback http is allowed for local dev only)"
156
+ // https-enforced (loopback, and the one private host a person names, exempt), so a
157
+ // hand-edited config can't put the device token and prompt on a plaintext channel.
158
+ baseUrl: z2.string().url().refine((url) => isAllowedBaseUrl(url), {
159
+ message: `baseUrl must be https (plain http only to this machine, or to the private host ${ALLOW_PLAIN_HTTP_HOST_ENV} names)`
128
160
  }),
129
161
  // The `cabdev_` device token plaintext — the companion's one durable credential,
130
162
  // presented as `Authorization: Bearer <token>` on the device-facing pull +
@@ -133,7 +165,7 @@ var companionConfigSchema = z2.object({
133
165
  // the rest of the config; `pair` always writes one.
134
166
  deviceToken: z2.string().optional(),
135
167
  // Identity hints, learned from the device flow and refreshed on the first
136
- // assignments pull. Cosmetic — used for `status`/dashboard display only.
168
+ // assignments pull. Cosmetic — used for `status` display only.
137
169
  deviceId: z2.string().optional(),
138
170
  deviceLabel: z2.string().optional(),
139
171
  // Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
@@ -145,9 +177,8 @@ var companionConfigSchema = z2.object({
145
177
  // silent roster: an agent nobody listed gets no hook and no error, and starts
146
178
  // in the operator's `$HOME`.
147
179
  prepareHook: prepareHookSchema.optional(),
148
- // The CLI ignores both dashboard settings. Desktop uses `dashboardPort`;
149
- // `autoOpen` remains parseable for older config files but no longer controls
150
- // startup. `logLevel` is pino's level, live-editable.
180
+ // Nothing reads either dashboard setting any more; both stay parseable so an
181
+ // older config file still loads. `logLevel` is pino's level, live-editable.
151
182
  dashboardPort: z2.number().int().min(1).max(65535).optional(),
152
183
  autoOpen: z2.boolean().optional(),
153
184
  logLevel: z2.enum(["warn", "info", "debug"]).optional(),
@@ -92,7 +92,8 @@ var ConfigError = class extends CompanionError {
92
92
  };
93
93
 
94
94
  // src/pairing.ts
95
- function isAllowedBaseUrl(raw) {
95
+ var ALLOW_PLAIN_HTTP_HOST_ENV = "CABANE_ALLOW_PLAIN_HTTP_HOST";
96
+ function isAllowedBaseUrl(raw, env = process.env) {
96
97
  let url;
97
98
  try {
98
99
  url = new URL(raw);
@@ -102,10 +103,41 @@ function isAllowedBaseUrl(raw) {
102
103
  if (url.protocol === "https:") return true;
103
104
  if (url.protocol === "http:") {
104
105
  const host = url.hostname.toLowerCase();
105
- return host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "::1" || host === "[::1]";
106
+ if (host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "::1" || host === "[::1]")
107
+ return true;
108
+ const allowed = plainHttpHost(env);
109
+ return allowed !== null && host === allowed;
106
110
  }
107
111
  return false;
108
112
  }
113
+ function plainHttpHost(env = process.env) {
114
+ const raw = env[ALLOW_PLAIN_HTTP_HOST_ENV]?.trim().toLowerCase();
115
+ if (!raw) return null;
116
+ if (isSingleLabel(raw) || isPrivateIpv4(raw)) return raw;
117
+ const bare = raw.startsWith("[") && raw.endsWith("]") ? raw.slice(1, -1) : raw;
118
+ return privateIpv6(bare);
119
+ }
120
+ function isSingleLabel(host) {
121
+ return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(host) && !/^\d+$/.test(host);
122
+ }
123
+ function isPrivateIpv4(host) {
124
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
125
+ if (!m) return false;
126
+ const [a, b, c, d] = m.slice(1).map(Number);
127
+ if ([a, b, c, d].some((n) => n > 255)) return false;
128
+ return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
129
+ }
130
+ function privateIpv6(host) {
131
+ if (!host.includes(":")) return null;
132
+ let bracketed;
133
+ try {
134
+ bracketed = new URL(`http://[${host}]/`).hostname;
135
+ } catch {
136
+ return null;
137
+ }
138
+ const first = Number.parseInt(bracketed.slice(1).split(":")[0] || "0", 16);
139
+ return (first & 65024) === 64512 ? bracketed : null;
140
+ }
109
141
 
110
142
  // src/prepare-hook.ts
111
143
  import { spawn } from "child_process";
@@ -171,10 +203,10 @@ function isRootPath(value) {
171
203
  return isAbsolute(value) || value === "~" || value.startsWith("~/");
172
204
  }
173
205
  var companionConfigSchema = z2.object({
174
- // https-enforced (loopback exempt), so a hand-edited config can't put the
175
- // device token and prompt on a plaintext channel.
176
- baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
177
- message: "baseUrl must be https (loopback http is allowed for local dev only)"
206
+ // https-enforced (loopback, and the one private host a person names, exempt), so a
207
+ // hand-edited config can't put the device token and prompt on a plaintext channel.
208
+ baseUrl: z2.string().url().refine((url) => isAllowedBaseUrl(url), {
209
+ message: `baseUrl must be https (plain http only to this machine, or to the private host ${ALLOW_PLAIN_HTTP_HOST_ENV} names)`
178
210
  }),
179
211
  // The `cabdev_` device token plaintext — the companion's one durable credential,
180
212
  // presented as `Authorization: Bearer <token>` on the device-facing pull +
@@ -183,7 +215,7 @@ var companionConfigSchema = z2.object({
183
215
  // the rest of the config; `pair` always writes one.
184
216
  deviceToken: z2.string().optional(),
185
217
  // Identity hints, learned from the device flow and refreshed on the first
186
- // assignments pull. Cosmetic — used for `status`/dashboard display only.
218
+ // assignments pull. Cosmetic — used for `status` display only.
187
219
  deviceId: z2.string().optional(),
188
220
  deviceLabel: z2.string().optional(),
189
221
  // Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
@@ -195,9 +227,8 @@ var companionConfigSchema = z2.object({
195
227
  // silent roster: an agent nobody listed gets no hook and no error, and starts
196
228
  // in the operator's `$HOME`.
197
229
  prepareHook: prepareHookSchema.optional(),
198
- // The CLI ignores both dashboard settings. Desktop uses `dashboardPort`;
199
- // `autoOpen` remains parseable for older config files but no longer controls
200
- // startup. `logLevel` is pino's level, live-editable.
230
+ // Nothing reads either dashboard setting any more; both stay parseable so an
231
+ // older config file still loads. `logLevel` is pino's level, live-editable.
201
232
  dashboardPort: z2.number().int().min(1).max(65535).optional(),
202
233
  autoOpen: z2.boolean().optional(),
203
234
  logLevel: z2.enum(["warn", "info", "debug"]).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.237",
3
+ "version": "0.6.239",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect an agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",
@@ -16,8 +16,6 @@
16
16
  "main": "./dist/cli.js",
17
17
  "exports": {
18
18
  ".": "./dist/cli.js",
19
- "./runtime": "./dist/runtime.js",
20
- "./pairing": "./dist/pairing-config.js",
21
19
  "./credential-broker": "./dist/credential-broker.js",
22
20
  "./package.json": "./package.json",
23
21
  "./*": "./*"