@cotal-ai/cli 0.26.0 → 0.28.0-hack-20260823202734

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.
@@ -28,18 +28,22 @@
28
28
  * overlay is actually up on this machine; the same literal with the overlay down is just a CGNAT
29
29
  * address.
30
30
  *
31
- * WHERE THIS GOES NEXT, so the next editor does not relax it for the wrong reason. Measured on
32
- * this stack: the `tls` connect option verifies the certificate CHAIN AND THE HOSTNAME (Node
33
- * defaults, `rejectUnauthorized: true`, servername from the URL), so a redirected name cannot
34
- * complete the handshake and hostnames become safe — but only once something REQUIRES TLS. The
35
- * relaxation is therefore a function of recorded strictness, not of time passing, and the agreed
36
- * eventual shape is `classifyJoinTarget(url, { tlsRequired })` rather than a third verdict: one
37
- * place keeps answering "may this machine send credentials to that address", with strictness as
38
- * an input it cannot otherwise see. Two counterintuitive consequences worth keeping in view:
39
- * the overlay ranges are the AWKWARD case under TLS (CGNAT space, no public CA will issue for it,
40
- * verifying a literal needs an IP SAN, so it implies a private CA on every joiner), while the
41
- * `<host>.ts.net` names refused below are the EASY case (publicly resolvable, publicly-trusted
42
- * certs). Do not pre-relax: with nothing requiring TLS today, literals-only is the correct rule.
31
+ * WHERE THIS WENT, so the next editor knows the relaxation below is the authorized one. The
32
+ * previous header said, measured on this stack: the `tls` connect option verifies the certificate
33
+ * CHAIN AND THE HOSTNAME (Node defaults, `rejectUnauthorized: true`, servername from the URL), so
34
+ * a redirected name cannot complete the handshake and hostnames become safe — but only once
35
+ * something REQUIRES TLS. The relaxation is a function of recorded strictness, not of time
36
+ * passing, and the agreed shape is `classifyJoinTarget(url, { tlsRequired })` rather than a third
37
+ * verdict: one place keeps answering "may this machine send credentials to that address", with
38
+ * strictness as an input it cannot otherwise see. THAT PRECONDITION HAS NOW LANDED: the broker
39
+ * can serve TLS and the mesh record carries `tlsRequired`, so with `tlsRequired: true` a hostname
40
+ * or a public literal classifies as {@link JoinReach} `"public-tls"`. Two counterintuitive
41
+ * consequences remain in force: the overlay ranges are the AWKWARD case under TLS (CGNAT space,
42
+ * no public CA will issue for it, verifying a literal needs an IP SAN, so it implies a private CA
43
+ * on every joiner), while the `<host>.ts.net` names are the EASY case (publicly resolvable,
44
+ * publicly-trusted certs). With `tlsRequired: false` nothing is relaxed: literals-only stays the
45
+ * rule, verbatim. And ordinary private ranges are refused in BOTH modes — no public CA issues for
46
+ * them, so required TLS cannot make them verifiable, and a cafe LAN is private too.
43
47
  */
44
48
  /** The address class a permitted target belongs to. This is a REACHABILITY allowlist: it says the
45
49
  * address is one we are willing to consider, never that the connection is protected. What
@@ -51,7 +55,11 @@ export type JoinReach =
51
55
  /** A private-overlay literal. Permitted TODAY without required TLS, carrying a
52
56
  * {@link JoinTarget.residual} that says so; it becomes conditional on TLS in step two. The
53
57
  * address alone is not a guarantee - see {@link DialPolicy.tlsRequired}. */
54
- | "overlay";
58
+ | "overlay"
59
+ /** A hostname or public literal dialed with REQUIRED TLS ({@link DialPolicy.tlsRequired}):
60
+ * the certificate chain + hostname check is the authority, so the resolver no longer picks
61
+ * the peer. Never produced with `tlsRequired: false`. */
62
+ | "public-tls";
55
63
  export interface JoinTarget {
56
64
  /** The normalized dial URL, port defaulted. */
57
65
  server: string;
@@ -80,11 +88,11 @@ export interface DialPolicy {
80
88
  * DHCP or routing can answer a dial to it. So the address class establishes only that we are
81
89
  * willing to consider the target; requiring TLS is what makes it safe.
82
90
  *
83
- * There is no field on the mesh record to source this from yet — it arrives with the work that
84
- * teaches the broker to serve TLS — so callers pass `false` today, and with it false an overlay
85
- * literal is refused unless {@link DialPolicy.allowUnencryptedOverlay} says the operator
86
- * accepted the tunnel dependency. When this can be `true` the acceptance stops being needed:
87
- * the transport is then proven rather than promised, and the flag can go.
91
+ * The mesh record now carries this intent (`MeshEntry.tlsRequired`), so callers pass the
92
+ * record's real strictness. With it `false` an overlay literal is refused unless
93
+ * {@link DialPolicy.allowUnencryptedOverlay} says the operator accepted the tunnel dependency.
94
+ * With it `true` the acceptance stops being needed — the transport is proven rather than
95
+ * promised — and a hostname or public literal becomes registrable as `"public-tls"`.
88
96
  */
89
97
  tlsRequired: boolean;
90
98
  /**
@@ -102,6 +110,59 @@ export interface DialPolicy {
102
110
  */
103
111
  allowUnencryptedOverlay: boolean;
104
112
  }
113
+ /**
114
+ * WHICH fence refused, as a closed set rather than as prose.
115
+ *
116
+ * The union is exhaustive on purpose: a new refusal arm must name itself here or it will not
117
+ * compile, so an unlabelled refusal is a type error rather than a cell that passes for the wrong
118
+ * reason.
119
+ */
120
+ export declare const JOIN_REFUSAL_CODES: readonly ["not-a-url", "bad-scheme", "no-host", "overlay-unacked", "private-range", "unprotected-target"];
121
+ /**
122
+ * The type is DERIVED from the runtime array above, so there is exactly ONE universe of codes.
123
+ *
124
+ * A suite that restated this list would carry a second copy of the thing it measures, and copies
125
+ * drift in the direction that hurts: an arm added here but not there makes the emitted-code check
126
+ * either falsely red or, if loosened to compensate, permanently green. Exporting the array and
127
+ * deriving the type means a new arm is visible to the suite by construction, with the union no
128
+ * less closed than a hand-written one.
129
+ */
130
+ export type JoinRefusalCode = (typeof JOIN_REFUSAL_CODES)[number];
131
+ /**
132
+ * A refusal from THIS classifier, identified structurally.
133
+ *
134
+ * The suite used to decide "was this a refusal?" by matching the rendered message, which is a
135
+ * string shape standing in for a decision — the exact defect the address fences in this file exist
136
+ * to correct, reproduced in the harness that proves them. Measured: a fault firing before the
137
+ * privacy check that emitted the privacy refusal's text VERBATIM was accepted as a genuine
138
+ * private-range refusal. A tag the classifier sets deliberately cannot be produced by an unrelated
139
+ * fault, so a cell can require not merely "something refused" but "the fence I am testing refused".
140
+ *
141
+ * BOUNDARY, stated so "typed" is not read as "unspoofable": this class is EXPORTED so the suite can
142
+ * assert `instanceof`, and that same export hands the constructor to every importer — so a refusal
143
+ * is NOT unforgeable from outside. What holds is narrower, and is the property that matters:
144
+ * unforgeable BY ACCIDENT — constructible only by deliberate code that imports the class, wherever
145
+ * it lives; and `classifyJoinTarget` throws its own. The reachable threat was never a malicious
146
+ * importer manufacturing one; it was a non-privacy fault inside this module reporting as a privacy
147
+ * refusal, and a deliberate tag plus a per-cell code closes that.
148
+ */
149
+ export declare class JoinRefusal extends Error {
150
+ readonly code: JoinRefusalCode;
151
+ constructor(code: JoinRefusalCode, message: string);
152
+ }
153
+ /**
154
+ * Is this host the loopback machine, whatever spelling it arrived in?
155
+ *
156
+ * THE ANSWER COMES FROM PARSING AN ADDRESS, NEVER FROM HOW THE TEXT BEGINS. A prefix test like
157
+ * `/^127\./` matches `127.evil.com`, `127.0.0.1.nip.io` and `127.com` — all registrable by anyone
158
+ * — and misses real loopback spellings such as `0177.0.0.1` or `[::ffff:127.0.0.1]`. It is wrong
159
+ * in both directions, which is what a string standing in for an address decision usually is.
160
+ *
161
+ * Exported so every caller that needs "is this loopback" shares ONE authority, canonicalization
162
+ * included, rather than growing a second opinion. Brackets are tolerated so a caller may pass a
163
+ * URL hostname straight in.
164
+ */
165
+ export declare function isLoopbackHost(host: string): boolean;
105
166
  /**
106
167
  * Classify a registration target, or throw the reason it is refused.
107
168
  *
@@ -1 +1 @@
1
- {"version":3,"file":"join-target.d.ts","sourceRoot":"","sources":["../../src/lib/join-target.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH;;;0BAG0B;AAC1B,MAAM,MAAM,SAAS;AACnB,6DAA6D;AAC3D,UAAU;AACZ;;6EAE6E;GAC3E,SAAS,CAAC;AAEd,MAAM,WAAW,UAAU;IACzB,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,SAAS,CAAC;IACjB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,2FAA2F;AAC3F,MAAM,WAAW,UAAU;IACzB;;;;;;;;;;;;;;OAcG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB;;;;;;;;;;;;OAYG;IACH,uBAAuB,EAAE,OAAO,CAAC;CAClC;AAuCD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,UAAU,CA8C9E"}
1
+ {"version":3,"file":"join-target.d.ts","sourceRoot":"","sources":["../../src/lib/join-target.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH;;;0BAG0B;AAC1B,MAAM,MAAM,SAAS;AACnB,6DAA6D;AAC3D,UAAU;AACZ;;6EAE6E;GAC3E,SAAS;AACX;;0DAE0D;GACxD,YAAY,CAAC;AAEjB,MAAM,WAAW,UAAU;IACzB,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,SAAS,CAAC;IACjB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,2FAA2F;AAC3F,MAAM,WAAW,UAAU;IACzB;;;;;;;;;;;;;;OAcG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB;;;;;;;;;;;;OAYG;IACH,uBAAuB,EAAE,OAAO,CAAC;CAClC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,2GAarB,CAAC;AAEX;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;gBACnB,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM;CAKnD;AAKD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGpD;AA8ID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,UAAU,CAqE9E"}
@@ -28,21 +28,88 @@
28
28
  * overlay is actually up on this machine; the same literal with the overlay down is just a CGNAT
29
29
  * address.
30
30
  *
31
- * WHERE THIS GOES NEXT, so the next editor does not relax it for the wrong reason. Measured on
32
- * this stack: the `tls` connect option verifies the certificate CHAIN AND THE HOSTNAME (Node
33
- * defaults, `rejectUnauthorized: true`, servername from the URL), so a redirected name cannot
34
- * complete the handshake and hostnames become safe — but only once something REQUIRES TLS. The
35
- * relaxation is therefore a function of recorded strictness, not of time passing, and the agreed
36
- * eventual shape is `classifyJoinTarget(url, { tlsRequired })` rather than a third verdict: one
37
- * place keeps answering "may this machine send credentials to that address", with strictness as
38
- * an input it cannot otherwise see. Two counterintuitive consequences worth keeping in view:
39
- * the overlay ranges are the AWKWARD case under TLS (CGNAT space, no public CA will issue for it,
40
- * verifying a literal needs an IP SAN, so it implies a private CA on every joiner), while the
41
- * `<host>.ts.net` names refused below are the EASY case (publicly resolvable, publicly-trusted
42
- * certs). Do not pre-relax: with nothing requiring TLS today, literals-only is the correct rule.
31
+ * WHERE THIS WENT, so the next editor knows the relaxation below is the authorized one. The
32
+ * previous header said, measured on this stack: the `tls` connect option verifies the certificate
33
+ * CHAIN AND THE HOSTNAME (Node defaults, `rejectUnauthorized: true`, servername from the URL), so
34
+ * a redirected name cannot complete the handshake and hostnames become safe — but only once
35
+ * something REQUIRES TLS. The relaxation is a function of recorded strictness, not of time
36
+ * passing, and the agreed shape is `classifyJoinTarget(url, { tlsRequired })` rather than a third
37
+ * verdict: one place keeps answering "may this machine send credentials to that address", with
38
+ * strictness as an input it cannot otherwise see. THAT PRECONDITION HAS NOW LANDED: the broker
39
+ * can serve TLS and the mesh record carries `tlsRequired`, so with `tlsRequired: true` a hostname
40
+ * or a public literal classifies as {@link JoinReach} `"public-tls"`. Two counterintuitive
41
+ * consequences remain in force: the overlay ranges are the AWKWARD case under TLS (CGNAT space,
42
+ * no public CA will issue for it, verifying a literal needs an IP SAN, so it implies a private CA
43
+ * on every joiner), while the `<host>.ts.net` names are the EASY case (publicly resolvable,
44
+ * publicly-trusted certs). With `tlsRequired: false` nothing is relaxed: literals-only stays the
45
+ * rule, verbatim. And ordinary private ranges are refused in BOTH modes — no public CA issues for
46
+ * them, so required TLS cannot make them verifiable, and a cafe LAN is private too.
43
47
  */
48
+ /**
49
+ * WHICH fence refused, as a closed set rather than as prose.
50
+ *
51
+ * The union is exhaustive on purpose: a new refusal arm must name itself here or it will not
52
+ * compile, so an unlabelled refusal is a type error rather than a cell that passes for the wrong
53
+ * reason.
54
+ */
55
+ export const JOIN_REFUSAL_CODES = [
56
+ /** The string is not a URL at all. */
57
+ "not-a-url",
58
+ /** Parsed, but not a broker scheme this policy classifies. */
59
+ "bad-scheme",
60
+ /** Parsed, but carries no host to classify. */
61
+ "no-host",
62
+ /** An overlay literal without required TLS and without recorded acceptance. */
63
+ "overlay-unacked",
64
+ /** A private range, which required TLS cannot make verifiable. */
65
+ "private-range",
66
+ /** A hostname or public literal with no TLS requirement to protect it. */
67
+ "unprotected-target",
68
+ ];
69
+ /**
70
+ * A refusal from THIS classifier, identified structurally.
71
+ *
72
+ * The suite used to decide "was this a refusal?" by matching the rendered message, which is a
73
+ * string shape standing in for a decision — the exact defect the address fences in this file exist
74
+ * to correct, reproduced in the harness that proves them. Measured: a fault firing before the
75
+ * privacy check that emitted the privacy refusal's text VERBATIM was accepted as a genuine
76
+ * private-range refusal. A tag the classifier sets deliberately cannot be produced by an unrelated
77
+ * fault, so a cell can require not merely "something refused" but "the fence I am testing refused".
78
+ *
79
+ * BOUNDARY, stated so "typed" is not read as "unspoofable": this class is EXPORTED so the suite can
80
+ * assert `instanceof`, and that same export hands the constructor to every importer — so a refusal
81
+ * is NOT unforgeable from outside. What holds is narrower, and is the property that matters:
82
+ * unforgeable BY ACCIDENT — constructible only by deliberate code that imports the class, wherever
83
+ * it lives; and `classifyJoinTarget` throws its own. The reachable threat was never a malicious
84
+ * importer manufacturing one; it was a non-privacy fault inside this module reporting as a privacy
85
+ * refusal, and a deliberate tag plus a per-cell code closes that.
86
+ */
87
+ export class JoinRefusal extends Error {
88
+ code;
89
+ constructor(code, message) {
90
+ super(message);
91
+ this.name = "JoinRefusal";
92
+ this.code = code;
93
+ }
94
+ }
44
95
  /** The NATS default client port, used when the join URL omits one. */
45
96
  const DEFAULT_PORT = 4222;
97
+ /**
98
+ * Is this host the loopback machine, whatever spelling it arrived in?
99
+ *
100
+ * THE ANSWER COMES FROM PARSING AN ADDRESS, NEVER FROM HOW THE TEXT BEGINS. A prefix test like
101
+ * `/^127\./` matches `127.evil.com`, `127.0.0.1.nip.io` and `127.com` — all registrable by anyone
102
+ * — and misses real loopback spellings such as `0177.0.0.1` or `[::ffff:127.0.0.1]`. It is wrong
103
+ * in both directions, which is what a string standing in for an address decision usually is.
104
+ *
105
+ * Exported so every caller that needs "is this loopback" shares ONE authority, canonicalization
106
+ * included, rather than growing a second opinion. Brackets are tolerated so a caller may pass a
107
+ * URL hostname straight in.
108
+ */
109
+ export function isLoopbackHost(host) {
110
+ const bare = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
111
+ return isLoopbackLiteral(normalizeLegacyV4(normalizeMappedV4(bare)));
112
+ }
46
113
  /** Loopback: `127.0.0.0/8` and `::1`. LITERALS ONLY, never `localhost`, because the whole point is
47
114
  * a verdict that does not depend on a resolver an attacker could influence (a hosts-file entry or
48
115
  * a poisoned lookup would otherwise turn "loopback" into any address at all). */
@@ -72,12 +139,132 @@ function isOverlayLiteral(host) {
72
139
  }
73
140
  return /^fd7a:115c:a1e0:/i.test(host);
74
141
  }
142
+ /** Ordinary private ranges: RFC1918, link-local, and the non-overlay ULA/link-local v6 space.
143
+ * Refused in BOTH modes — a cafe LAN is private too, and no public CA issues certificates for
144
+ * these ranges, so required TLS cannot make an address here verifiable. */
145
+ function isPrivateLiteral(host) {
146
+ const v4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
147
+ if (v4) {
148
+ const p = v4.slice(1).map(Number);
149
+ if (p.some((n) => n > 255))
150
+ return false; // not an IP literal at all; treated as a hostname
151
+ if (p[0] === 10)
152
+ return true; // 10.0.0.0/8
153
+ if (p[0] === 172 && p[1] >= 16 && p[1] <= 31)
154
+ return true; // 172.16.0.0/12
155
+ if (p[0] === 192 && p[1] === 168)
156
+ return true; // 192.168.0.0/16
157
+ if (p[0] === 169 && p[1] === 254)
158
+ return true; // 169.254.0.0/16 link-local
159
+ return false;
160
+ }
161
+ // ULA fc00::/7 and link-local fe80::/10; the overlay's fd7a:115c:a1e0::/48 is classified first.
162
+ return /^(f[cd][0-9a-f]{2}:|fe[89ab][0-9a-f]{0,2}:)/i.test(host);
163
+ }
75
164
  /** A URL's hostname with an IPv6 literal's brackets removed (`[::1]` -> `::1`). */
76
165
  function bareHost(url) {
77
166
  return url.hostname.startsWith("[") && url.hostname.endsWith("]")
78
167
  ? url.hostname.slice(1, -1)
79
168
  : url.hostname;
80
169
  }
170
+ /**
171
+ * Collapse ANY legacy IPv4 spelling to canonical dotted-quad, so that one address has ONE
172
+ * verdict however it is written.
173
+ *
174
+ * `inet_aton` — which the C library, Node's resolver and every OS dialer accept — does not
175
+ * require four decimal octets. It takes 1, 2, 3 or 4 parts, each of which may be decimal, octal
176
+ * (leading `0`) or hex (leading `0x`), and a short form's final part absorbs the remaining bytes.
177
+ * All of these are 192.168.1.10, verified against `dns.lookup` on this machine:
178
+ *
179
+ * 3232235786 (decimal dword) -> 192.168.1.10
180
+ * 0300.0250.01.012 (octal dotted) -> 192.168.1.10
181
+ * 0xC0A8010A (hex dword) -> 192.168.1.10
182
+ * 0177.0.0.1 (octal, loopback) -> 127.0.0.1
183
+ * 192.168.257 (3-part shorthand) -> 192.168.1.1
184
+ *
185
+ * A classifier that only matched four decimal octets treated each of these as a HOSTNAME, so it
186
+ * fell through the private-range fence and registered as a public address, while the dotted form
187
+ * of the very same host was refused. Canonicalize first; classify once.
188
+ *
189
+ * Returns the dotted form, or the input unchanged when it is not an IPv4 literal in any spelling
190
+ * (a real hostname, or a v6 literal handled below).
191
+ */
192
+ function normalizeLegacyV4(host) {
193
+ if (!/^[0-9a-fx.]+$/i.test(host) || host.endsWith("."))
194
+ return host;
195
+ const parts = host.split(".");
196
+ if (parts.length > 4)
197
+ return host;
198
+ const nums = [];
199
+ for (const part of parts) {
200
+ if (part === "")
201
+ return host;
202
+ let n;
203
+ if (/^0[xX][0-9a-fA-F]+$/.test(part))
204
+ n = parseInt(part.slice(2), 16);
205
+ else if (/^0[0-7]+$/.test(part))
206
+ n = parseInt(part.slice(1), 8);
207
+ else if (/^(0|[1-9][0-9]*)$/.test(part))
208
+ n = Number(part);
209
+ else
210
+ return host; // e.g. 09 or 0xZ: not a number in any base, so not an IPv4 literal
211
+ if (!Number.isSafeInteger(n) || n < 0)
212
+ return host;
213
+ nums.push(n);
214
+ }
215
+ // The last part absorbs the bytes the earlier parts did not name (inet_aton's short forms).
216
+ const leading = nums.slice(0, -1);
217
+ const last = nums[nums.length - 1];
218
+ if (leading.some((n) => n > 255))
219
+ return host;
220
+ const width = 4 - leading.length;
221
+ if (last > (width === 4 ? 0xffffffff : Math.pow(256, width) - 1))
222
+ return host;
223
+ const bytes = [...leading];
224
+ for (let i = width - 1; i >= 0; i--)
225
+ bytes.push((last >>> (8 * i)) & 0xff);
226
+ return bytes.join(".");
227
+ }
228
+ /**
229
+ * Collapse an IPv4-MAPPED IPv6 literal to its dotted IPv4 form, so that one address has ONE
230
+ * verdict however it is spelled.
231
+ *
232
+ * `::ffff:192.168.1.10` and `::ffff:c0a8:010a` are the same 192.168.1.10, and the kernel dials
233
+ * them to the same host. RFC 4291 mapped form is exactly `::ffff:a.b.c.d` (equivalently
234
+ * `::ffff:xxxx:xxxx`) and NOTHING ELSE: `::ffff:0:127.0.0.1` looks similar but is a different
235
+ * address, which a socket proves — it dials ENETUNREACH where `::ffff:127.0.0.1` connects to
236
+ * loopback. Collapsing it would be this fence's own bug in mirror image, granting a loopback,
237
+ * overlay or public verdict to an address that is none of those. Classifying the v6 spelling separately meant the private-range check ran
238
+ * a v4 regex that could not match, the address fell through to "not private", and
239
+ * `--tls` registered a LAN broker that the dotted form correctly refuses. Every classifier below
240
+ * therefore sees the normalized form, which keeps loopback/overlay/private answers identical
241
+ * across spellings rather than fixing only the range that was reported.
242
+ *
243
+ * Anything that is not a v4-mapped literal is returned unchanged.
244
+ */
245
+ function normalizeMappedV4(host) {
246
+ const m = host.match(/^::ffff:([0-9a-f.:]+)$/i);
247
+ if (!m)
248
+ return host;
249
+ const tail = m[1];
250
+ const dotted = tail.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
251
+ if (dotted) {
252
+ const p = dotted.slice(1).map(Number);
253
+ return p.some((n) => n > 255) ? host : p.join(".");
254
+ }
255
+ // Only the two RFC 4291 tails are mapped. A legacy-spelled tail (`::ffff:3232235786`) is not
256
+ // handled here and does not need to be: `new URL()` rejects it as an invalid IPv6 literal
257
+ // before this function is ever called, so the target is refused as "not a URL" — a stricter
258
+ // answer than any collapse. Adding a branch for it would be unreachable code that no test
259
+ // could kill, and the address does not resolve anyway (a socket reports ENOTFOUND).
260
+ const hex = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
261
+ if (hex) {
262
+ const hi = parseInt(hex[1], 16);
263
+ const lo = parseInt(hex[2], 16);
264
+ return [hi >> 8, hi & 0xff, lo >> 8, lo & 0xff].join(".");
265
+ }
266
+ return host;
267
+ }
81
268
  /**
82
269
  * Classify a registration target, or throw the reason it is refused.
83
270
  *
@@ -90,15 +277,23 @@ export function classifyJoinTarget(raw, policy) {
90
277
  url = new URL(raw);
91
278
  }
92
279
  catch {
93
- throw new Error(`${JSON.stringify(raw)} is not a URL - pass a broker address like nats://100.64.0.1:4222`);
280
+ throw new JoinRefusal("not-a-url", `${JSON.stringify(raw)} is not a URL - pass a broker address like nats://100.64.0.1:4222`);
94
281
  }
95
- if (url.protocol !== "nats:" && url.protocol !== "tls:")
96
- throw new Error(`${JSON.stringify(raw)} must be a nats:// or tls:// URL, not ${url.protocol}//`);
97
- const host = bareHost(url);
282
+ if (url.protocol !== "nats:" && url.protocol !== "tls:" && url.protocol !== "ws:" && url.protocol !== "wss:")
283
+ throw new JoinRefusal("bad-scheme", `${JSON.stringify(raw)} must be a nats://, tls://, ws:// or wss:// URL, not ${url.protocol}//`);
284
+ // ONE address, ONE verdict: every legacy IPv4 spelling (decimal dword, octal, hex, short form)
285
+ // and every v4-mapped v6 literal is collapsed to canonical dotted form BEFORE any classifier
286
+ // runs, so an alternate spelling cannot walk past the private-range fence.
287
+ const host = normalizeLegacyV4(normalizeMappedV4(bareHost(url)));
98
288
  if (!host)
99
- throw new Error(`${JSON.stringify(raw)} has no host`);
100
- const port = url.port || String(DEFAULT_PORT);
101
- const server = `${url.protocol}//${url.hostname}:${port}`;
289
+ throw new JoinRefusal("no-host", `${JSON.stringify(raw)} has no host`);
290
+ // A websocket broker rides the web's ports and may live under a PATH (`wss://host/mesh-ws`
291
+ // behind an HTTPS edge) - the path is part of the address and must survive into the record,
292
+ // where a plain NATS URL has no path to keep.
293
+ const ws = url.protocol === "ws:" || url.protocol === "wss:";
294
+ const port = url.port || String(ws ? (url.protocol === "wss:" ? 443 : 80) : DEFAULT_PORT);
295
+ const path = ws && url.pathname !== "/" ? url.pathname : "";
296
+ const server = `${url.protocol}//${url.hostname}:${port}${path}`;
102
297
  // Loopback is the one class that needs no transport guarantee: nothing leaves the machine, so
103
298
  // there is nothing on a wire for anyone to sit on.
104
299
  if (isLoopbackLiteral(host))
@@ -107,7 +302,7 @@ export function classifyJoinTarget(raw, policy) {
107
302
  if (policy.tlsRequired)
108
303
  return { server, reach: "overlay" };
109
304
  if (!policy.allowUnencryptedOverlay)
110
- throw new Error(`${JSON.stringify(raw)} refused: ${host} is a private-overlay address, and this build cannot require TLS on the connection.\n` +
305
+ throw new JoinRefusal("overlay-unacked", `${JSON.stringify(raw)} refused: ${host} is a private-overlay address, and this build cannot require TLS on the connection.\n` +
111
306
  ` That is safe while the overlay tunnel is up, and NOT safe if it is down: the range is then ordinary carrier-grade\n` +
112
307
  ` NAT, and whoever answers the dial receives the credentials this machine sends.\n` +
113
308
  ` Only you can know whether the tunnel is up, so accept it explicitly with --allow-unencrypted-overlay, which records\n` +
@@ -122,7 +317,17 @@ export function classifyJoinTarget(raw, policy) {
122
317
  ` become a refusal once the broker can be served over TLS and the record can say TLS is required.`,
123
318
  };
124
319
  }
125
- throw new Error(`${JSON.stringify(raw)} refused: this build cannot protect a connection to ${host}, and a machine that registers a mesh sends its agent credentials to that broker.\n` +
320
+ // Recorded strictness relaxes the fence, exactly as the header pre-authorized: with TLS
321
+ // REQUIRED, the `tls` connect option verifies the certificate chain and the hostname, so a
322
+ // hostname or a public literal is safe — the resolver stops choosing the peer. Private ranges
323
+ // stay refused: no public CA issues for them, and a cafe LAN is private too.
324
+ if (policy.tlsRequired) {
325
+ if (!isPrivateLiteral(host))
326
+ return { server, reach: "public-tls" };
327
+ throw new JoinRefusal("private-range", `${JSON.stringify(raw)} refused: ${host} is a private-range address, and requiring TLS does not make it verifiable - no public CA issues certificates for RFC1918 or link-local space, and a cafe LAN is private, not yours.\n` +
328
+ ` Register the broker by its public name or address, or run it on this machine and use a loopback literal.`);
329
+ }
330
+ throw new JoinRefusal("unprotected-target", `${JSON.stringify(raw)} refused: this build cannot protect a connection to ${host}, and a machine that registers a mesh sends its agent credentials to that broker.\n` +
126
331
  ` Only a loopback literal (127.0.0.0/8, ::1) or a private-overlay literal (100.64.0.0/10, fd7a:115c:a1e0::/48) may be registered. An overlay literal additionally needs --allow-unencrypted-overlay, which records that you accepted its tunnel dependency.\n` +
127
332
  ` Ordinary private ranges are refused too: a cafe network is private, and private is not the same as yours.\n` +
128
333
  ` A hostname is refused even when it resolves somewhere permitted - otherwise whoever answers the lookup chooses which machine receives the credentials. Pass the address itself.`);
@@ -1 +1 @@
1
- {"version":3,"file":"join-target.js","sourceRoot":"","sources":["../../src/lib/join-target.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"join-target.js","sourceRoot":"","sources":["../../src/lib/join-target.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/seed/store.ts"],"names":[],"mappings":"AAqBA;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CAWzG;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAS5F"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/seed/store.ts"],"names":[],"mappings":"AAsBA;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CAyBzG;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAe5F"}
@@ -1,5 +1,6 @@
1
1
  import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
2
2
  import { dirname, join, relative, sep } from "node:path";
3
+ import { provenance } from "@cotal-ai/workspace";
3
4
  import { seedStoreDir, seedStorePath, shippedSourceDir } from "./paths.js";
4
5
  /**
5
6
  * The durable seed store: a stable, per-generation copy of each shipped connector payload that
@@ -34,6 +35,20 @@ export function stageSeedPayload(generation, name, opts = {}) {
34
35
  mkdirSync(dirname(dest), { recursive: true });
35
36
  cpSync(src, staging, { recursive: true, filter: (from) => payloadFilter(src, from) });
36
37
  renameSync(staging, dest); // atomic within the store: the final path only ever holds a complete payload
38
+ // Announce the write on the provenance channel. This store is operator-global (a sibling of the
39
+ // shared `extensions/` prefix, moved only by `XDG_CONFIG_HOME`), so re-seeding it from a non-released
40
+ // checkout silently makes those bytes the machine-wide payload for that generation key; naming the
41
+ // path here lets the line read as the machine-wide action it is. Only on a real materialization: the
42
+ // idempotent early return above (an intact prior copy) writes nothing and stays silent.
43
+ //
44
+ // THE BOUND, stated where the claim is made: this announce is AFTER the commit and rides the
45
+ // provenance channel, which is a plain stderr write with no failure policy. If stderr is closed or
46
+ // erroring, the payload is still written and the line is lost, so the guarantee is "a materialization
47
+ // that reaches a working stderr is named", not "no materialization is ever unannounced". Announcing
48
+ // BEFORE the rename would trade that for a worse lie (a named write that then failed), and throwing
49
+ // on a lost line would fail a seed for a disclosure fault. The channel-wide fix belongs to the
50
+ // provenance layer rather than to this call site.
51
+ provenance.wrote(`operator-global seed store payload (${name})`, dest);
37
52
  return dest;
38
53
  }
39
54
  /**
@@ -51,8 +66,14 @@ export function gcSeedStore(keepGeneration, referencedSpecs) {
51
66
  continue;
52
67
  const genDir = join(root, gen);
53
68
  const referenced = referencedSpecs.some((spec) => spec === genDir || spec.startsWith(genDir + sep));
54
- if (!referenced)
55
- rmSync(genDir, { recursive: true, force: true });
69
+ if (referenced)
70
+ continue;
71
+ rmSync(genDir, { recursive: true, force: true });
72
+ // Announce the DELETE for the same reason the write above is announced: this store is
73
+ // operator-global, so dropping a generation from it is a machine-wide act performed by a command
74
+ // the operator ran for a local reason. It is announced AFTER the removal, so the line reports
75
+ // what happened rather than what was intended.
76
+ provenance.removed(`operator-global seed store generation (${gen})`, genDir);
56
77
  }
57
78
  }
58
79
  //# sourceMappingURL=store.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/seed/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE3E;;;;;;GAMG;AAEH;;;kGAGkG;AAClG,SAAS,aAAa,CAAC,IAAY,EAAE,IAAY;IAC/C,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC1E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAE,IAAY,EAAE,OAA4B,EAAE;IAC/F,MAAM,IAAI,GAAG,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAC7C,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACvE,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,GAAG,IAAI,UAAU,CAAC;IAClC,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/C,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IACtF,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,6EAA6E;IACxG,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,cAAsB,EAAE,eAAkC;IACpF,MAAM,IAAI,GAAG,YAAY,EAAE,CAAC;IAC5B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO;IAC9B,KAAK,MAAM,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,IAAI,GAAG,KAAK,cAAc;YAAE,SAAS;QACrC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC/B,MAAM,UAAU,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC;QACpG,IAAI,CAAC,UAAU;YAAE,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpE,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/seed/store.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cotal-ai/cli",
3
3
  "description": "Cotal mesh CLI: up, join, watch, console, spawn, mint, channels, history, ext.",
4
- "version": "0.26.0",
4
+ "version": "0.28.0-hack-20260823202734",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -28,8 +28,8 @@
28
28
  "yaml": "^2.9.0",
29
29
  "zod": "^4.4.3",
30
30
  "ws": "^8.21.0",
31
- "@cotal-ai/workspace": "0.26.0",
32
- "@cotal-ai/core": "0.26.0"
31
+ "@cotal-ai/core": "0.28.0-hack-20260823202734",
32
+ "@cotal-ai/workspace": "0.28.0-hack-20260823202734"
33
33
  },
34
34
  "optionalDependencies": {
35
35
  "@eplightning/nats-server-darwin-arm64": "^2.14.0",