@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.
- package/dist/commands/meshes-add.d.ts +76 -10
- package/dist/commands/meshes-add.d.ts.map +1 -1
- package/dist/commands/meshes-add.js +214 -49
- package/dist/commands/meshes-add.js.map +1 -1
- package/dist/commands/meshes-wizard.d.ts.map +1 -1
- package/dist/commands/meshes-wizard.js +67 -7
- package/dist/commands/meshes-wizard.js.map +1 -1
- package/dist/commands/meshes.d.ts +16 -2
- package/dist/commands/meshes.d.ts.map +1 -1
- package/dist/commands/meshes.js +170 -8
- package/dist/commands/meshes.js.map +1 -1
- package/dist/commands/personas.d.ts.map +1 -1
- package/dist/commands/personas.js +12 -2
- package/dist/commands/personas.js.map +1 -1
- package/dist/commands/up.d.ts +3 -0
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +34 -5
- package/dist/commands/up.js.map +1 -1
- package/dist/lib/auth-proc.d.ts +4 -0
- package/dist/lib/auth-proc.d.ts.map +1 -1
- package/dist/lib/auth-proc.js +3 -3
- package/dist/lib/auth-proc.js.map +1 -1
- package/dist/lib/join-target.d.ts +79 -18
- package/dist/lib/join-target.d.ts.map +1 -1
- package/dist/lib/join-target.js +226 -21
- package/dist/lib/join-target.js.map +1 -1
- package/dist/seed/store.d.ts.map +1 -1
- package/dist/seed/store.js +23 -2
- package/dist/seed/store.js.map +1 -1
- package/package.json +3 -3
|
@@ -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
|
|
32
|
-
* this stack: the `tls` connect option verifies the certificate
|
|
33
|
-
* defaults, `rejectUnauthorized: true`, servername from the URL), so
|
|
34
|
-
* complete the handshake and hostnames become safe — but only once
|
|
35
|
-
* relaxation is
|
|
36
|
-
*
|
|
37
|
-
* place keeps answering "may this machine send credentials to that address", with
|
|
38
|
-
* an input it cannot otherwise see.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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
|
|
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"}
|
package/dist/lib/join-target.js
CHANGED
|
@@ -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
|
|
32
|
-
* this stack: the `tls` connect option verifies the certificate
|
|
33
|
-
* defaults, `rejectUnauthorized: true`, servername from the URL), so
|
|
34
|
-
* complete the handshake and hostnames become safe — but only once
|
|
35
|
-
* relaxation is
|
|
36
|
-
*
|
|
37
|
-
* place keeps answering "may this machine send credentials to that address", with
|
|
38
|
-
* an input it cannot otherwise see.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
|
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
|
|
97
|
-
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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
|
-
|
|
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":"AAAA
|
|
1
|
+
{"version":3,"file":"join-target.js","sourceRoot":"","sources":["../../src/lib/join-target.ts"],"names":[],"mappings":"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"}
|
package/dist/seed/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/seed/store.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/seed/store.js
CHANGED
|
@@ -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 (
|
|
55
|
-
|
|
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
|
package/dist/seed/store.js.map
CHANGED
|
@@ -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,
|
|
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.
|
|
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/
|
|
32
|
-
"@cotal-ai/
|
|
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",
|