@indigoai-us/hq-cli 5.196.0 → 5.197.0
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/CHANGELOG.md +12 -0
- package/dist/lib/flag-registry-worker.js +12 -1
- package/dist/lib/flag-registry.js +72 -4
- package/dist/lib/lanes/story-ledger.js +52 -11
- package/dist/node-network-compat-flag.d.ts +19 -0
- package/dist/node-network-compat-flag.js +87 -0
- package/dist/node-network-compat.d.ts +13 -16
- package/dist/node-network-compat.js +74 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.197.0] — 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Process-wide hq-flags gates now resolve with the active company's identifier when a company is bound.
|
|
10
|
+
- Concurrent `hq lanes` updates now wait for an in-progress project story-ledger
|
|
11
|
+
lock receipt, avoiding transient `story_ledger_lock_invalid` errors.
|
|
12
|
+
|
|
5
13
|
## [5.196.0] — 2026-09-25
|
|
6
14
|
|
|
7
15
|
## [5.195.1] — 2026-09-25
|
|
@@ -31,6 +39,10 @@
|
|
|
31
39
|
full name (for example "Chaudhry Hassaan Saleem" and "Hassaan Saleem"), is
|
|
32
40
|
held back and reported as a suspect duplicate.
|
|
33
41
|
|
|
42
|
+
- `hq auth` and other CLI fetches can opt into IPv4-first networking through the
|
|
43
|
+
default-off `cli.network.force-ipv4` hq-flags gate. The canary is bounded and
|
|
44
|
+
keeps explicit Node network options in control.
|
|
45
|
+
|
|
34
46
|
## [5.194.0] — 2026-09-25
|
|
35
47
|
|
|
36
48
|
### Added
|
|
@@ -2,13 +2,24 @@
|
|
|
2
2
|
import { parentPort, workerData } from "node:worker_threads";
|
|
3
3
|
import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
4
4
|
import { createFlagClient } from "@indigoai-us/hq-flags-client";
|
|
5
|
-
const
|
|
5
|
+
const data = workerData;
|
|
6
|
+
const endpoint = data.endpoint;
|
|
7
|
+
const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
|
|
8
|
+
const COMPANY_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
6
9
|
async function refresh() {
|
|
7
10
|
if (!parentPort || typeof endpoint !== "string" || endpoint.trim() === "")
|
|
8
11
|
return;
|
|
9
12
|
try {
|
|
13
|
+
const companyUid = typeof data.companyUid === "string" && COMPANY_UID_RE.test(data.companyUid)
|
|
14
|
+
? data.companyUid
|
|
15
|
+
: undefined;
|
|
16
|
+
const companyIdentifiers = Array.isArray(data.companyIdentifiers)
|
|
17
|
+
? data.companyIdentifiers.filter((value) => typeof value === "string" && COMPANY_SLUG_RE.test(value))
|
|
18
|
+
: [];
|
|
10
19
|
const client = createFlagClient({
|
|
11
20
|
endpoint,
|
|
21
|
+
...(companyUid ? { companyUid } : {}),
|
|
22
|
+
...(companyIdentifiers.length ? { companyIdentifiers } : {}),
|
|
12
23
|
getToken: () => loadCachedTokens()?.idToken ?? "",
|
|
13
24
|
refreshIntervalMs: 0,
|
|
14
25
|
onError: () => { },
|
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Worker } from "node:worker_threads";
|
|
9
9
|
import { isFlagSnapshot, readLocalOverride, resolveFlag, } from "@indigoai-us/hq-flags-client";
|
|
10
|
+
import { readMachineCompanyBinding } from "./work-context/company.js";
|
|
11
|
+
import { readSessionState } from "./work-context/state.js";
|
|
12
|
+
import { workContextRoot } from "./work-context/paths.js";
|
|
13
|
+
import { readCompaniesManifestMap } from "./work-context/repo-remote.js";
|
|
14
|
+
import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
|
|
10
15
|
/** Dedicated endpoint configuration; this is intentionally not HQ_PRO_API_URL. */
|
|
11
16
|
export const FLAG_REGISTRY_ENDPOINT_ENV = "HQ_FLAGS_API_URL";
|
|
17
|
+
const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
|
|
18
|
+
const COMPANY_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
12
19
|
let heldSnapshot = null;
|
|
13
20
|
let processReader = null;
|
|
14
21
|
function processLocalOverride(flagKey) {
|
|
@@ -17,14 +24,66 @@ function processLocalOverride(flagKey) {
|
|
|
17
24
|
function localOverrideOrFallback(flagKey, legacyFallback) {
|
|
18
25
|
return processLocalOverride(flagKey) ?? legacyFallback();
|
|
19
26
|
}
|
|
20
|
-
function
|
|
27
|
+
function normalizedCompanyUid(value) {
|
|
28
|
+
const candidate = value?.trim() ?? "";
|
|
29
|
+
return COMPANY_UID_RE.test(candidate) ? candidate : undefined;
|
|
30
|
+
}
|
|
31
|
+
function normalizedCompanySlug(value) {
|
|
32
|
+
const candidate = value?.trim() ?? "";
|
|
33
|
+
return COMPANY_SLUG_RE.test(candidate) ? candidate : undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve only an explicit machine/session company binding. Device defaults
|
|
37
|
+
* and cwd/repository heuristics do not scope process-wide feature flags.
|
|
38
|
+
*/
|
|
39
|
+
function processCompanyContext(env = process.env) {
|
|
40
|
+
const explicitUid = normalizedCompanyUid(env.HQ_COMPANY_UID);
|
|
41
|
+
const explicitSlug = normalizedCompanySlug(env.HQ_COMPANY_SLUG);
|
|
42
|
+
if (explicitUid || explicitSlug) {
|
|
43
|
+
return { companyUid: explicitUid, companySlug: explicitSlug };
|
|
44
|
+
}
|
|
45
|
+
const machine = readMachineCompanyBinding(env);
|
|
46
|
+
const spawnValue = env.HQ_SPAWN_COMPANY?.trim();
|
|
47
|
+
const spawnSlug = normalizedCompanySlug(spawnValue);
|
|
48
|
+
const machineContext = spawnValue && COMPANY_UID_RE.test(spawnValue)
|
|
49
|
+
? { companyUid: spawnValue }
|
|
50
|
+
: spawnSlug
|
|
51
|
+
? { companySlug: spawnSlug }
|
|
52
|
+
: {
|
|
53
|
+
companyUid: normalizedCompanyUid(machine?.uid),
|
|
54
|
+
companySlug: normalizedCompanySlug(machine?.slug),
|
|
55
|
+
};
|
|
56
|
+
if (machineContext.companyUid || machineContext.companySlug)
|
|
57
|
+
return machineContext;
|
|
58
|
+
const sessionId = env.HQ_SESSION_ID?.trim();
|
|
59
|
+
if (!sessionId)
|
|
60
|
+
return {};
|
|
61
|
+
const session = readSessionState(sessionId, workContextRoot(undefined, env));
|
|
62
|
+
if (session?.contextStatus !== "bound" || session.companyConfidence === "device_default") {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
companyUid: normalizedCompanyUid(session.companyUid),
|
|
67
|
+
companySlug: normalizedCompanySlug(session.companySlug),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function companyUidForContext(context, env = process.env) {
|
|
71
|
+
if (context.companyUid)
|
|
72
|
+
return context.companyUid;
|
|
73
|
+
if (!context.companySlug)
|
|
74
|
+
return undefined;
|
|
75
|
+
const hqRoot = resolveDefaultHqRoot({ env });
|
|
76
|
+
const manifestUid = readCompaniesManifestMap(hqRoot)?.[context.companySlug]?.cloud_uid;
|
|
77
|
+
return typeof manifestUid === "string" ? normalizedCompanyUid(manifestUid) : undefined;
|
|
78
|
+
}
|
|
79
|
+
function processSnapshotReader(companyIdentifiers) {
|
|
21
80
|
return {
|
|
22
81
|
isEnabled(flagKey, lookup = {}) {
|
|
23
82
|
return resolveFlag({
|
|
24
83
|
flagKey,
|
|
25
84
|
env: process.env,
|
|
26
85
|
snapshot: heldSnapshot,
|
|
27
|
-
companyIdentifiers
|
|
86
|
+
companyIdentifiers,
|
|
28
87
|
lookup,
|
|
29
88
|
}).value;
|
|
30
89
|
},
|
|
@@ -45,9 +104,18 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
45
104
|
const endpoint = (dependencies.endpoint ?? process.env[FLAG_REGISTRY_ENDPOINT_ENV] ?? "").trim();
|
|
46
105
|
if (!endpoint)
|
|
47
106
|
return null;
|
|
48
|
-
|
|
107
|
+
const company = processCompanyContext();
|
|
108
|
+
const companyUid = companyUidForContext(company);
|
|
109
|
+
const companyIdentifiers = [companyUid, company.companySlug].filter((value) => Boolean(value));
|
|
110
|
+
processReader = processSnapshotReader(companyIdentifiers);
|
|
49
111
|
try {
|
|
50
|
-
const worker = (dependencies.createWorker ?? ((filename, options) => new Worker(filename, options)))(new URL("./flag-registry-worker.js", import.meta.url), {
|
|
112
|
+
const worker = (dependencies.createWorker ?? ((filename, options) => new Worker(filename, options)))(new URL("./flag-registry-worker.js", import.meta.url), {
|
|
113
|
+
workerData: {
|
|
114
|
+
endpoint,
|
|
115
|
+
...(companyUid ? { companyUid } : {}),
|
|
116
|
+
...(company.companySlug ? { companyIdentifiers: [company.companySlug] } : {}),
|
|
117
|
+
},
|
|
118
|
+
});
|
|
51
119
|
worker.on("message", (value) => {
|
|
52
120
|
if (isFlagSnapshot(value))
|
|
53
121
|
heldSnapshot = value;
|
|
@@ -122,6 +122,11 @@ function lockPathFor(filePath) {
|
|
|
122
122
|
function lockOwnerPath(lockPath) {
|
|
123
123
|
return path.join(lockPath, "owner.json");
|
|
124
124
|
}
|
|
125
|
+
function isInvalidLockOwnerReceipt(err, lockPath) {
|
|
126
|
+
return err instanceof LanesError &&
|
|
127
|
+
err.error === "story_ledger_lock_invalid" &&
|
|
128
|
+
err.details.path === lockOwnerPath(lockPath);
|
|
129
|
+
}
|
|
125
130
|
function readLockOwner(lockPath) {
|
|
126
131
|
const ownerPath = lockOwnerPath(lockPath);
|
|
127
132
|
let contents;
|
|
@@ -157,15 +162,17 @@ function lockSleep(deps, ms) {
|
|
|
157
162
|
}
|
|
158
163
|
function restoreMovedLockWithoutReplacement(lockPath, stalePath) {
|
|
159
164
|
const entries = fs.readdirSync(stalePath);
|
|
160
|
-
|
|
165
|
+
const isOwnerTemp = (entry) => /^\.owner-[a-f0-9]{24}\.tmp$/.test(entry);
|
|
166
|
+
if (entries.some((entry) => entry !== "owner.json" && !isOwnerTemp(entry))) {
|
|
161
167
|
throw new Error("Unexpected entry in moved story ledger lock");
|
|
162
168
|
}
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
:
|
|
169
|
+
const files = entries.map((entry) => ({
|
|
170
|
+
name: entry,
|
|
171
|
+
contents: fs.readFileSync(path.join(stalePath, entry), "utf8"),
|
|
172
|
+
}));
|
|
166
173
|
fs.mkdirSync(lockPath);
|
|
167
|
-
|
|
168
|
-
fs.writeFileSync(
|
|
174
|
+
for (const file of files) {
|
|
175
|
+
fs.writeFileSync(path.join(lockPath, file.name), file.contents, {
|
|
169
176
|
encoding: "utf8",
|
|
170
177
|
flag: "wx",
|
|
171
178
|
mode: 0o600,
|
|
@@ -186,10 +193,18 @@ function clearStaleLock(lockPath, deps) {
|
|
|
186
193
|
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
187
194
|
throw new LanesError("story_ledger_lock_invalid", "Story ledger lock path is not a regular directory: " + lockPath, { path: lockPath });
|
|
188
195
|
}
|
|
189
|
-
|
|
190
|
-
|
|
196
|
+
let ownerRead;
|
|
197
|
+
try {
|
|
198
|
+
ownerRead = readLockOwner(lockPath);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
if (!isInvalidLockOwnerReceipt(err, lockPath) || lockNow(deps) - stat.mtimeMs >= LOCK_STALE_MS) {
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
191
206
|
let lockAt = stat.mtimeMs;
|
|
192
|
-
if (
|
|
207
|
+
if (hasLockOwnerToken(ownerRead) && typeof ownerRead.owner.at === "string") {
|
|
193
208
|
const parsedAt = Date.parse(ownerRead.owner.at);
|
|
194
209
|
if (Number.isFinite(parsedAt))
|
|
195
210
|
lockAt = parsedAt;
|
|
@@ -207,7 +222,26 @@ function clearStaleLock(lockPath, deps) {
|
|
|
207
222
|
throw err;
|
|
208
223
|
}
|
|
209
224
|
const moved = fs.lstatSync(stalePath);
|
|
210
|
-
|
|
225
|
+
let movedOwnerRead;
|
|
226
|
+
try {
|
|
227
|
+
movedOwnerRead = readLockOwner(stalePath);
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
if (!isInvalidLockOwnerReceipt(err, stalePath))
|
|
231
|
+
throw err;
|
|
232
|
+
const stillFresh = lockNow(deps) - moved.mtimeMs < LOCK_STALE_MS;
|
|
233
|
+
try {
|
|
234
|
+
// Put the moved directory back before retrying or reporting the invalid receipt.
|
|
235
|
+
restoreMovedLockWithoutReplacement(lockPath, stalePath);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
throw new LanesError("story_ledger_lock_race", "A replacement story ledger lock was preserved at " + stalePath +
|
|
239
|
+
" because it could not safely be restored to " + lockPath, { path: lockPath, preserved_path: stalePath });
|
|
240
|
+
}
|
|
241
|
+
if (stillFresh)
|
|
242
|
+
return false;
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
211
245
|
let movedLockAt = moved.mtimeMs;
|
|
212
246
|
if (hasLockOwnerToken(movedOwnerRead) && typeof movedOwnerRead.owner.at === "string") {
|
|
213
247
|
const parsedAt = Date.parse(movedOwnerRead.owner.at);
|
|
@@ -239,7 +273,14 @@ function acquireLedgerLock(filePath, deps) {
|
|
|
239
273
|
while (lockNow(deps) - started < timeoutMs) {
|
|
240
274
|
try {
|
|
241
275
|
fs.mkdirSync(lockPath);
|
|
242
|
-
|
|
276
|
+
const ownerTempPath = path.join(lockPath, ".owner-" + token + ".tmp");
|
|
277
|
+
try {
|
|
278
|
+
fs.writeFileSync(ownerTempPath, JSON.stringify({ pid: process.pid, token, at: new Date(lockNow(deps)).toISOString() }) + "\n", { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
279
|
+
fs.renameSync(ownerTempPath, lockOwnerPath(lockPath));
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
fs.rmSync(ownerTempPath, { force: true });
|
|
283
|
+
}
|
|
243
284
|
return { lockPath, token };
|
|
244
285
|
}
|
|
245
286
|
catch (err) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FlagClient, FlagClientOptions } from "@indigoai-us/hq-flags-client";
|
|
2
|
+
export declare const NODE_NETWORK_FORCE_IPV4_FLAG = "cli.network.force-ipv4";
|
|
3
|
+
export declare const NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK = false;
|
|
4
|
+
export declare const NODE_NETWORK_FORCE_IPV4_FLAG_TIMEOUT_MS = 1500;
|
|
5
|
+
type NetworkFlagClient = Pick<FlagClient, "ready" | "isEnabled" | "close">;
|
|
6
|
+
export interface NodeNetworkForceIpv4Dependencies {
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
createClient?: (options: FlagClientOptions) => NetworkFlagClient;
|
|
9
|
+
getToken?: () => string | Promise<string>;
|
|
10
|
+
reportFailure?: (error: unknown) => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Read the opt-in hq-flags gate before CLI dispatch. Local hq-flags overrides
|
|
14
|
+
* work without a token or endpoint; service decisions require a company
|
|
15
|
+
* identity and use a short request deadline so startup remains bounded.
|
|
16
|
+
*/
|
|
17
|
+
export declare function nodeNetworkForceIpv4Enabled(dependencies?: NodeNetworkForceIpv4Dependencies): Promise<boolean>;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=node-network-compat-flag.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createFlagClient, readLocalOverride } from "@indigoai-us/hq-flags-client";
|
|
2
|
+
export const NODE_NETWORK_FORCE_IPV4_FLAG = "cli.network.force-ipv4";
|
|
3
|
+
export const NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK = false;
|
|
4
|
+
// Node waits up to 1s before trying the next address family. Keep this request
|
|
5
|
+
// deadline above that wait so a dead IPv6 path can reach the usable IPv4 host.
|
|
6
|
+
export const NODE_NETWORK_FORCE_IPV4_FLAG_TIMEOUT_MS = 1_500;
|
|
7
|
+
function safeErrorClass(error) {
|
|
8
|
+
const name = error instanceof Error ? error.name : "UnknownError";
|
|
9
|
+
return /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(name)
|
|
10
|
+
? name
|
|
11
|
+
: "UnknownError";
|
|
12
|
+
}
|
|
13
|
+
function reportFlagFailure(error) {
|
|
14
|
+
process.stderr.write("HQ network IPv4 flag lookup failed (" +
|
|
15
|
+
safeErrorClass(error) +
|
|
16
|
+
"); using default off.\n");
|
|
17
|
+
}
|
|
18
|
+
async function loadCachedIdToken() {
|
|
19
|
+
const { loadCachedTokens } = await import("@indigoai-us/hq-cloud");
|
|
20
|
+
return loadCachedTokens()?.idToken ?? "";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Read the opt-in hq-flags gate before CLI dispatch. Local hq-flags overrides
|
|
24
|
+
* work without a token or endpoint; service decisions require a company
|
|
25
|
+
* identity and use a short request deadline so startup remains bounded.
|
|
26
|
+
*/
|
|
27
|
+
export async function nodeNetworkForceIpv4Enabled(dependencies = {}) {
|
|
28
|
+
const env = dependencies.env ?? process.env;
|
|
29
|
+
const localOverride = readLocalOverride(NODE_NETWORK_FORCE_IPV4_FLAG, env);
|
|
30
|
+
if (localOverride !== null)
|
|
31
|
+
return localOverride;
|
|
32
|
+
const endpoint = env.HQ_FLAGS_API_URL?.trim() || "";
|
|
33
|
+
const companyUid = env.HQ_COMPANY_UID?.trim() || "";
|
|
34
|
+
if (!endpoint ||
|
|
35
|
+
!/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid)) {
|
|
36
|
+
return NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK;
|
|
37
|
+
}
|
|
38
|
+
const companySlug = env.HQ_COMPANY_SLUG?.trim() || "";
|
|
39
|
+
let client;
|
|
40
|
+
let failed = false;
|
|
41
|
+
let failureReported = false;
|
|
42
|
+
let enabled = NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK;
|
|
43
|
+
const reportFailureOnce = (error) => {
|
|
44
|
+
failed = true;
|
|
45
|
+
if (failureReported)
|
|
46
|
+
return;
|
|
47
|
+
failureReported = true;
|
|
48
|
+
(dependencies.reportFailure ?? reportFlagFailure)(error);
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
const token = await (dependencies.getToken ?? loadCachedIdToken)();
|
|
52
|
+
if (!token)
|
|
53
|
+
return NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK;
|
|
54
|
+
const createClient = dependencies.createClient ?? createFlagClient;
|
|
55
|
+
client = createClient({
|
|
56
|
+
endpoint,
|
|
57
|
+
companyUid,
|
|
58
|
+
...(companySlug && /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/.test(companySlug)
|
|
59
|
+
? { companyIdentifiers: [companySlug] }
|
|
60
|
+
: {}),
|
|
61
|
+
env: {},
|
|
62
|
+
getToken: () => token,
|
|
63
|
+
refreshIntervalMs: 0,
|
|
64
|
+
requestTimeoutMs: NODE_NETWORK_FORCE_IPV4_FLAG_TIMEOUT_MS,
|
|
65
|
+
onError: reportFailureOnce,
|
|
66
|
+
});
|
|
67
|
+
await client.ready();
|
|
68
|
+
if (!failed) {
|
|
69
|
+
enabled = client.isEnabled(NODE_NETWORK_FORCE_IPV4_FLAG, {
|
|
70
|
+
fallback: NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
reportFailureOnce(error);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
try {
|
|
79
|
+
client?.close();
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
reportFailureOnce(error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return failed ? NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK : enabled;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=node-network-compat-flag.js.map
|
|
@@ -1,31 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Node network defaults for HQ CLI entry points.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* window fixes the connection while retaining Node's automatic family choice.
|
|
4
|
+
* The compatibility timeout remains the default. On a canary where IPv6 is
|
|
5
|
+
* advertised but unusable, the default-off hq-flags gate can also prefer IPv4
|
|
6
|
+
* and disable automatic family racing before the CLI issues its first request.
|
|
8
7
|
*
|
|
9
|
-
* Keep this module
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* neither is present.
|
|
13
|
-
*
|
|
14
|
-
* Use a namespace import of `node:net` (not a static named import of
|
|
15
|
-
* `setDefaultAutoSelectFamilyAttemptTimeout`). Named ESM imports are linked
|
|
16
|
-
* before any entry-module body runs — including `node-preflight` — so importing
|
|
17
|
-
* a Node 18.13+/20.4+ API by name would turn unsupported runtimes into a
|
|
18
|
-
* module-link SyntaxError instead of the preflight's upgrade message.
|
|
8
|
+
* Keep this module safe to import immediately after the Node version preflight.
|
|
9
|
+
* The hq-flags reader loads dynamically so an unsupported runtime exits through
|
|
10
|
+
* the preflight rather than failing during module linking.
|
|
19
11
|
*/
|
|
20
12
|
export declare const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
|
|
21
13
|
export interface NodeNetworkCompatibilityOptions {
|
|
22
14
|
nodeOptions?: string;
|
|
23
15
|
execArgv?: readonly string[];
|
|
16
|
+
forceIpv4?: boolean;
|
|
24
17
|
/**
|
|
25
|
-
* Inject the net timeout setter (tests). Pass
|
|
26
|
-
* that lacks
|
|
18
|
+
* Inject the net timeout setter (tests). Pass null to simulate a runtime
|
|
19
|
+
* that lacks setDefaultAutoSelectFamilyAttemptTimeout.
|
|
27
20
|
*/
|
|
28
21
|
setAttemptTimeout?: ((milliseconds: number) => void) | null;
|
|
22
|
+
/** Inject Node's automatic family selection setter (tests). */
|
|
23
|
+
setAutoSelectFamily?: ((enabled: boolean) => void) | null;
|
|
24
|
+
/** Inject Node's DNS result-order setter (tests). */
|
|
25
|
+
setDefaultResultOrder?: ((order: "ipv4first") => void) | null;
|
|
29
26
|
}
|
|
30
27
|
export declare function configureNodeNetworkCompatibility(options?: NodeNetworkCompatibilityOptions): boolean;
|
|
31
28
|
//# sourceMappingURL=node-network-compat.d.ts.map
|
|
@@ -1,30 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Node network defaults for HQ CLI entry points.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* window fixes the connection while retaining Node's automatic family choice.
|
|
4
|
+
* The compatibility timeout remains the default. On a canary where IPv6 is
|
|
5
|
+
* advertised but unusable, the default-off hq-flags gate can also prefer IPv4
|
|
6
|
+
* and disable automatic family racing before the CLI issues its first request.
|
|
8
7
|
*
|
|
9
|
-
* Keep this module
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* neither is present.
|
|
13
|
-
*
|
|
14
|
-
* Use a namespace import of `node:net` (not a static named import of
|
|
15
|
-
* `setDefaultAutoSelectFamilyAttemptTimeout`). Named ESM imports are linked
|
|
16
|
-
* before any entry-module body runs — including `node-preflight` — so importing
|
|
17
|
-
* a Node 18.13+/20.4+ API by name would turn unsupported runtimes into a
|
|
18
|
-
* module-link SyntaxError instead of the preflight's upgrade message.
|
|
8
|
+
* Keep this module safe to import immediately after the Node version preflight.
|
|
9
|
+
* The hq-flags reader loads dynamically so an unsupported runtime exits through
|
|
10
|
+
* the preflight rather than failing during module linking.
|
|
19
11
|
*/
|
|
12
|
+
import * as nodeDns from "node:dns";
|
|
20
13
|
import * as nodeNet from "node:net";
|
|
21
14
|
export const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
|
|
22
15
|
const ATTEMPT_TIMEOUT_OPTION = "--network-family-autoselection-attempt-timeout";
|
|
23
16
|
function hasExplicitAttemptTimeout(nodeOptions, execArgv) {
|
|
24
|
-
const optionPattern = new RegExp(
|
|
17
|
+
const optionPattern = new RegExp("(?:^|\\s)" + ATTEMPT_TIMEOUT_OPTION + "(?:=|\\s|$)");
|
|
25
18
|
return (optionPattern.test(nodeOptions) ||
|
|
26
19
|
execArgv.some((argument) => argument === ATTEMPT_TIMEOUT_OPTION ||
|
|
27
|
-
argument.startsWith(
|
|
20
|
+
argument.startsWith(ATTEMPT_TIMEOUT_OPTION + "=")));
|
|
21
|
+
}
|
|
22
|
+
function hasExplicitFamilySelection(nodeOptions, execArgv) {
|
|
23
|
+
const optionPattern = /(?:^|\s)(?:--network-family-autoselection|--enable-network-family-autoselection|--no-network-family-autoselection|--dns-result-order)(?:=|\s|$)/;
|
|
24
|
+
return (optionPattern.test(nodeOptions) ||
|
|
25
|
+
execArgv.some((argument) => argument === "--network-family-autoselection" ||
|
|
26
|
+
argument === "--enable-network-family-autoselection" ||
|
|
27
|
+
argument === "--no-network-family-autoselection" ||
|
|
28
|
+
argument === "--dns-result-order" ||
|
|
29
|
+
argument.startsWith("--network-family-autoselection=") ||
|
|
30
|
+
argument.startsWith("--enable-network-family-autoselection=") ||
|
|
31
|
+
argument.startsWith("--no-network-family-autoselection=") ||
|
|
32
|
+
argument.startsWith("--dns-result-order=")));
|
|
28
33
|
}
|
|
29
34
|
function resolveSetAttemptTimeout(override) {
|
|
30
35
|
if (override === null)
|
|
@@ -34,19 +39,69 @@ function resolveSetAttemptTimeout(override) {
|
|
|
34
39
|
const fn = nodeNet.setDefaultAutoSelectFamilyAttemptTimeout;
|
|
35
40
|
return typeof fn === "function" ? fn.bind(nodeNet) : undefined;
|
|
36
41
|
}
|
|
42
|
+
function resolveSetAutoSelectFamily(override) {
|
|
43
|
+
if (override === null)
|
|
44
|
+
return undefined;
|
|
45
|
+
if (override)
|
|
46
|
+
return override;
|
|
47
|
+
const fn = nodeNet.setDefaultAutoSelectFamily;
|
|
48
|
+
return typeof fn === "function" ? fn.bind(nodeNet) : undefined;
|
|
49
|
+
}
|
|
50
|
+
function resolveSetDefaultResultOrder(override) {
|
|
51
|
+
if (override === null)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (override)
|
|
54
|
+
return override;
|
|
55
|
+
const fn = nodeDns.setDefaultResultOrder;
|
|
56
|
+
return typeof fn === "function" ? fn.bind(nodeDns) : undefined;
|
|
57
|
+
}
|
|
58
|
+
function applyIpv4Preference(options, nodeOptions, execArgv) {
|
|
59
|
+
if (!options.forceIpv4 || hasExplicitFamilySelection(nodeOptions, execArgv)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const setAutoSelectFamily = resolveSetAutoSelectFamily(options.setAutoSelectFamily);
|
|
63
|
+
const setDefaultResultOrder = resolveSetDefaultResultOrder(options.setDefaultResultOrder);
|
|
64
|
+
if (!setAutoSelectFamily || !setDefaultResultOrder)
|
|
65
|
+
return false;
|
|
66
|
+
setDefaultResultOrder("ipv4first");
|
|
67
|
+
setAutoSelectFamily(false);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
37
70
|
export function configureNodeNetworkCompatibility(options = {}) {
|
|
38
71
|
const nodeOptions = options.nodeOptions ?? process.env.NODE_OPTIONS ?? "";
|
|
39
72
|
const execArgv = options.execArgv ?? process.execArgv;
|
|
73
|
+
const ipv4Applied = applyIpv4Preference(options, nodeOptions, execArgv);
|
|
40
74
|
if (hasExplicitAttemptTimeout(nodeOptions, execArgv)) {
|
|
41
|
-
return
|
|
75
|
+
return ipv4Applied;
|
|
42
76
|
}
|
|
43
77
|
const setAttemptTimeout = resolveSetAttemptTimeout(options.setAttemptTimeout);
|
|
44
78
|
if (!setAttemptTimeout) {
|
|
45
|
-
// Runtime lacks the API
|
|
46
|
-
return
|
|
79
|
+
// Runtime lacks the API; the Node preflight should already have exited.
|
|
80
|
+
return ipv4Applied;
|
|
47
81
|
}
|
|
48
82
|
setAttemptTimeout(HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS);
|
|
49
83
|
return true;
|
|
50
84
|
}
|
|
85
|
+
async function applyHqFlaggedIpv4Preference() {
|
|
86
|
+
try {
|
|
87
|
+
const { nodeNetworkForceIpv4Enabled } = await import("./node-network-compat-flag.js");
|
|
88
|
+
if (nodeNetworkForceIpv4Enabled &&
|
|
89
|
+
(await nodeNetworkForceIpv4Enabled())) {
|
|
90
|
+
configureNodeNetworkCompatibility({ forceIpv4: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const rawName = error instanceof Error ? error.name : "UnknownError";
|
|
95
|
+
const errorName = /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(rawName)
|
|
96
|
+
? rawName
|
|
97
|
+
: "UnknownError";
|
|
98
|
+
process.stderr.write("HQ network IPv4 flag lookup failed (" +
|
|
99
|
+
errorName +
|
|
100
|
+
"); using default off.\n");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Preserve the historical timeout before any flag request. If the hq-flags
|
|
104
|
+
// lookup succeeds, the IPv4 preference is applied before command dispatch.
|
|
51
105
|
configureNodeNetworkCompatibility();
|
|
106
|
+
await applyHqFlaggedIpv4Preference();
|
|
52
107
|
//# sourceMappingURL=node-network-compat.js.map
|