@remnic/core 9.50.1 → 9.50.2
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/access-admin-ops-surface.js +3 -3
- package/dist/access-authorization-probe.js +4 -4
- package/dist/access-boundary.js +3 -3
- package/dist/access-cli.js +13 -13
- package/dist/access-extraction-force-flush.js +3 -3
- package/dist/access-http-query.js +3 -3
- package/dist/access-http.js +9 -9
- package/dist/access-identity-continuity-surface.js +3 -3
- package/dist/access-lcm-surface.js +3 -3
- package/dist/access-mcp.js +7 -7
- package/dist/access-namespace-preflight.js +3 -3
- package/dist/access-observe-write-surface.js +3 -3
- package/dist/access-operations-batch.js +4 -4
- package/dist/access-operations.js +6 -6
- package/dist/access-recall-concurrency.js +3 -3
- package/dist/access-recall-response.js +3 -3
- package/dist/access-recall-surface.js +3 -3
- package/dist/access-service.js +3 -3
- package/dist/{chunk-UWIDGVQM.js → chunk-5C2HNJMO.js} +3 -3
- package/dist/{chunk-X4IDNPOS.js → chunk-HAEU3ZW7.js} +2 -2
- package/dist/{chunk-NZ6UDRUA.js → chunk-IK6X5QPO.js} +5 -5
- package/dist/{chunk-P7ADRREJ.js → chunk-KDG2KTWW.js} +3 -3
- package/dist/{chunk-IB25FBMX.js → chunk-LEJPSMKM.js} +2 -2
- package/dist/{chunk-ZDAYWJV6.js → chunk-NO37MR45.js} +3 -3
- package/dist/{chunk-ZCO5F5FW.js → chunk-PLKFO2HJ.js} +2 -2
- package/dist/{chunk-LOW66JRO.js → chunk-PYKKWQZ2.js} +2 -2
- package/dist/{chunk-T6RATUV2.js → chunk-QW3WFHKU.js} +2 -2
- package/dist/{chunk-PNRA7FAD.js → chunk-RZRUV6N2.js} +104 -62
- package/dist/chunk-RZRUV6N2.js.map +1 -0
- package/dist/{chunk-D73B6HMN.js → chunk-S3AYIQCU.js} +2 -2
- package/dist/{chunk-HS4M6BZM.js → chunk-TQFHQPWD.js} +5 -5
- package/dist/{chunk-JFNRA7YJ.js → chunk-V7X767RT.js} +4 -4
- package/dist/cli.js +10 -10
- package/dist/connectors/index.d.ts +1 -0
- package/dist/connectors/index.js +1 -1
- package/dist/external-wiki-access.js +4 -4
- package/dist/extraction.js +4 -4
- package/dist/index.js +13 -13
- package/dist/orchestrator.js +13 -13
- package/dist/source-agent-qualifier.js +3 -3
- package/package.json +2 -2
- package/src/connectors/index.ts +40 -112
- package/src/runtime/daemon-health.ts +53 -0
- package/src/runtime/http-transport.ts +25 -0
- package/dist/chunk-PNRA7FAD.js.map +0 -1
- /package/dist/{chunk-UWIDGVQM.js.map → chunk-5C2HNJMO.js.map} +0 -0
- /package/dist/{chunk-X4IDNPOS.js.map → chunk-HAEU3ZW7.js.map} +0 -0
- /package/dist/{chunk-NZ6UDRUA.js.map → chunk-IK6X5QPO.js.map} +0 -0
- /package/dist/{chunk-P7ADRREJ.js.map → chunk-KDG2KTWW.js.map} +0 -0
- /package/dist/{chunk-IB25FBMX.js.map → chunk-LEJPSMKM.js.map} +0 -0
- /package/dist/{chunk-ZDAYWJV6.js.map → chunk-NO37MR45.js.map} +0 -0
- /package/dist/{chunk-ZCO5F5FW.js.map → chunk-PLKFO2HJ.js.map} +0 -0
- /package/dist/{chunk-LOW66JRO.js.map → chunk-PYKKWQZ2.js.map} +0 -0
- /package/dist/{chunk-T6RATUV2.js.map → chunk-QW3WFHKU.js.map} +0 -0
- /package/dist/{chunk-D73B6HMN.js.map → chunk-S3AYIQCU.js.map} +0 -0
- /package/dist/{chunk-HS4M6BZM.js.map → chunk-TQFHQPWD.js.map} +0 -0
- /package/dist/{chunk-JFNRA7YJ.js.map → chunk-V7X767RT.js.map} +0 -0
package/src/connectors/index.ts
CHANGED
|
@@ -12,10 +12,10 @@ import { createRequire } from "node:module";
|
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
|
|
14
14
|
import { generateToken, revokeToken, buildTokenEntry, commitTokenEntry, loadTokenStore, saveTokenStore } from "../tokens.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { checkDaemonHealth } from "../runtime/daemon-health.js";
|
|
16
|
+
import { readEnvVar, resolveHomeDir } from "../runtime/env.js";
|
|
17
17
|
import { expandTildePath } from "../utils/path.js";
|
|
18
|
-
import { coerceInstallExtension } from "./coerce.js";
|
|
18
|
+
import { coerceBool, coerceInstallExtension } from "./coerce.js";
|
|
19
19
|
import {
|
|
20
20
|
assertConfigComponentsNotSymlinked,
|
|
21
21
|
hermesShimPath,
|
|
@@ -988,10 +988,12 @@ export function installConnector(options: InstallOptions): InstallResult {
|
|
|
988
988
|
let hermesSavedProfile: string | undefined;
|
|
989
989
|
let hermesSavedHost: string | undefined;
|
|
990
990
|
let hermesSavedPort: number | undefined;
|
|
991
|
+
let hermesSavedAllowInsecureHttp: boolean | undefined;
|
|
991
992
|
// Resolved values (used both in resolvedConfig and in the YAML update below)
|
|
992
993
|
let hermesResolvedProfile: string | undefined;
|
|
993
994
|
let hermesResolvedHost: string | undefined;
|
|
994
995
|
let hermesResolvedPort: number | undefined;
|
|
996
|
+
let hermesResolvedAllowInsecureHttp = false;
|
|
995
997
|
if (options.connectorId === "hermes") {
|
|
996
998
|
if (fs.existsSync(configPath)) {
|
|
997
999
|
try {
|
|
@@ -1022,6 +1024,9 @@ export function installConnector(options: InstallOptions): InstallResult {
|
|
|
1022
1024
|
// Invalid saved port — fall through to default
|
|
1023
1025
|
}
|
|
1024
1026
|
}
|
|
1027
|
+
if (prev?.allow_insecure_http != null) {
|
|
1028
|
+
hermesSavedAllowInsecureHttp = coerceBool(prev.allow_insecure_http, "allow_insecure_http");
|
|
1029
|
+
}
|
|
1025
1030
|
} catch {
|
|
1026
1031
|
// Could not read existing config — fall through to defaults
|
|
1027
1032
|
}
|
|
@@ -1030,6 +1035,7 @@ export function installConnector(options: InstallOptions): InstallResult {
|
|
|
1030
1035
|
// and applied in the sanitization block below (single point of validation).
|
|
1031
1036
|
hermesResolvedProfile = hermesSavedProfile ?? "default";
|
|
1032
1037
|
hermesResolvedHost = hermesSavedHost ?? "127.0.0.1";
|
|
1038
|
+
hermesResolvedAllowInsecureHttp = hermesSavedAllowInsecureHttp ?? false;
|
|
1033
1039
|
|
|
1034
1040
|
// Issue C: wrap sanitizeHermesPort (and profile/host) in try-catch so
|
|
1035
1041
|
// that invalid user-supplied values return a clean error result.
|
|
@@ -1071,6 +1077,17 @@ export function installConnector(options: InstallOptions): InstallResult {
|
|
|
1071
1077
|
};
|
|
1072
1078
|
}
|
|
1073
1079
|
}
|
|
1080
|
+
if (options.config?.allow_insecure_http !== undefined) {
|
|
1081
|
+
const allowInsecureHttp = coerceBool(options.config.allow_insecure_http, "allow_insecure_http");
|
|
1082
|
+
if (allowInsecureHttp === undefined) {
|
|
1083
|
+
return {
|
|
1084
|
+
connectorId: options.connectorId,
|
|
1085
|
+
status: "error",
|
|
1086
|
+
message: "Invalid Hermes config: allow_insecure_http must be true or false",
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
hermesResolvedAllowInsecureHttp = allowInsecureHttp;
|
|
1090
|
+
}
|
|
1074
1091
|
}
|
|
1075
1092
|
|
|
1076
1093
|
// Generate a per-connector auth token so the daemon can authenticate
|
|
@@ -1182,6 +1199,7 @@ export function installConnector(options: InstallOptions): InstallResult {
|
|
|
1182
1199
|
profile: hermesResolvedProfile,
|
|
1183
1200
|
host: hermesResolvedHost,
|
|
1184
1201
|
port: hermesResolvedPort,
|
|
1202
|
+
allow_insecure_http: hermesResolvedAllowInsecureHttp,
|
|
1185
1203
|
} : {}),
|
|
1186
1204
|
};
|
|
1187
1205
|
|
|
@@ -1260,6 +1278,7 @@ export function installConnector(options: InstallOptions): InstallResult {
|
|
|
1260
1278
|
host: hermesHost,
|
|
1261
1279
|
port: hermesPort,
|
|
1262
1280
|
token: tokenEntry.token,
|
|
1281
|
+
allowInsecureHttp: hermesResolvedAllowInsecureHttp,
|
|
1263
1282
|
});
|
|
1264
1283
|
} catch (err) {
|
|
1265
1284
|
// upsertHermesConfig threw — old token preserved, connector.json unchanged.
|
|
@@ -1458,6 +1477,7 @@ export function installConnector(options: InstallOptions): InstallResult {
|
|
|
1458
1477
|
host: hermesHost,
|
|
1459
1478
|
port: hermesPort,
|
|
1460
1479
|
token: tokenEntry.token,
|
|
1480
|
+
allowInsecureHttp: hermesResolvedAllowInsecureHttp,
|
|
1461
1481
|
});
|
|
1462
1482
|
if (refresh.updated) {
|
|
1463
1483
|
hermesPriorConfigMutations.push({ mutatedPath: priorConfigPath, priorContent: beforeRefresh });
|
|
@@ -1756,7 +1776,12 @@ export function installConnector(options: InstallOptions): InstallResult {
|
|
|
1756
1776
|
// (we returned early on failure above) but the explicit guard is kept for
|
|
1757
1777
|
// clarity and future robustness.
|
|
1758
1778
|
if (committed && tokenEntry) {
|
|
1759
|
-
const daemonOk = checkDaemonHealth(
|
|
1779
|
+
const daemonOk = checkDaemonHealth(
|
|
1780
|
+
hermesHost,
|
|
1781
|
+
hermesPort,
|
|
1782
|
+
tokenEntry.token,
|
|
1783
|
+
hermesResolvedAllowInsecureHttp,
|
|
1784
|
+
);
|
|
1760
1785
|
if (daemonOk) {
|
|
1761
1786
|
notes.push("Daemon health check: OK");
|
|
1762
1787
|
} else {
|
|
@@ -2883,6 +2908,7 @@ export function upsertHermesConfig(opts: {
|
|
|
2883
2908
|
host: string;
|
|
2884
2909
|
port: number;
|
|
2885
2910
|
token: string;
|
|
2911
|
+
allowInsecureHttp?: boolean;
|
|
2886
2912
|
}): HermesConfigResult {
|
|
2887
2913
|
return upsertHermesConfigAt(hermesConfigPath(opts.profile), opts);
|
|
2888
2914
|
}
|
|
@@ -2895,7 +2921,7 @@ export function upsertHermesConfig(opts: {
|
|
|
2895
2921
|
*/
|
|
2896
2922
|
function upsertHermesConfigAt(
|
|
2897
2923
|
cfgPath: string,
|
|
2898
|
-
opts: { host: string; port: number; token: string },
|
|
2924
|
+
opts: { host: string; port: number; token: string; allowInsecureHttp?: boolean },
|
|
2899
2925
|
): HermesConfigResult {
|
|
2900
2926
|
// Symlinked components below the Hermes root must not redirect the
|
|
2901
2927
|
// token-bearing write (Codex P1 on PR #1938, round 19).
|
|
@@ -2907,6 +2933,7 @@ function upsertHermesConfigAt(
|
|
|
2907
2933
|
// that could break out of the scalar context.
|
|
2908
2934
|
const safeHost = sanitizeHermesHost(opts.host);
|
|
2909
2935
|
const safePort = sanitizeHermesPort(opts.port);
|
|
2936
|
+
const allowInsecureHttp = opts.allowInsecureHttp ?? false;
|
|
2910
2937
|
// Token is generated by randomBytes + a fixed alphabetic prefix, so it's
|
|
2911
2938
|
// already safe for YAML scalar interpolation. We still guard against an
|
|
2912
2939
|
// unexpectedly malformed token reaching this function.
|
|
@@ -2928,6 +2955,7 @@ function upsertHermesConfigAt(
|
|
|
2928
2955
|
` host: "${safeHost}"`,
|
|
2929
2956
|
` port: ${safePort}`,
|
|
2930
2957
|
` token: "${opts.token}"`,
|
|
2958
|
+
` allow_insecure_http: ${allowInsecureHttp}`,
|
|
2931
2959
|
].join("\n");
|
|
2932
2960
|
|
|
2933
2961
|
if (!fs.existsSync(cfgPath)) {
|
|
@@ -2973,7 +3001,7 @@ function upsertHermesConfigAt(
|
|
|
2973
3001
|
let blockWritten = false;
|
|
2974
3002
|
|
|
2975
3003
|
// Track which sub-keys we've emitted
|
|
2976
|
-
const written = { host: false, port: false, token: false };
|
|
3004
|
+
const written = { host: false, port: false, token: false, allowInsecureHttp: false };
|
|
2977
3005
|
|
|
2978
3006
|
for (let i = 0; i < lines.length; i++) {
|
|
2979
3007
|
const line = lines[i];
|
|
@@ -2993,13 +3021,14 @@ function upsertHermesConfigAt(
|
|
|
2993
3021
|
if (!written.host) newLines.push(` host: "${safeHost}"`);
|
|
2994
3022
|
if (!written.port) newLines.push(` port: ${safePort}`);
|
|
2995
3023
|
if (!written.token) newLines.push(` token: "${opts.token}"`);
|
|
3024
|
+
if (!written.allowInsecureHttp) newLines.push(` allow_insecure_http: ${allowInsecureHttp}`);
|
|
2996
3025
|
blockWritten = true;
|
|
2997
3026
|
inRemnicBlock = false;
|
|
2998
3027
|
newLines.push(line);
|
|
2999
3028
|
continue;
|
|
3000
3029
|
}
|
|
3001
3030
|
|
|
3002
|
-
// Replace
|
|
3031
|
+
// Replace managed transport fields and preserve other sub-keys.
|
|
3003
3032
|
if (/^\s+host:/.test(line)) {
|
|
3004
3033
|
newLines.push(` host: "${safeHost}"`);
|
|
3005
3034
|
written.host = true;
|
|
@@ -3009,6 +3038,9 @@ function upsertHermesConfigAt(
|
|
|
3009
3038
|
} else if (/^\s+token:/.test(line)) {
|
|
3010
3039
|
newLines.push(` token: "${opts.token}"`);
|
|
3011
3040
|
written.token = true;
|
|
3041
|
+
} else if (/^\s+allow_insecure_http:/.test(line)) {
|
|
3042
|
+
newLines.push(` allow_insecure_http: ${allowInsecureHttp}`);
|
|
3043
|
+
written.allowInsecureHttp = true;
|
|
3012
3044
|
} else {
|
|
3013
3045
|
newLines.push(line);
|
|
3014
3046
|
}
|
|
@@ -3023,6 +3055,7 @@ function upsertHermesConfigAt(
|
|
|
3023
3055
|
if (!written.host) newLines.push(` host: "${safeHost}"`);
|
|
3024
3056
|
if (!written.port) newLines.push(` port: ${safePort}`);
|
|
3025
3057
|
if (!written.token) newLines.push(` token: "${opts.token}"`);
|
|
3058
|
+
if (!written.allowInsecureHttp) newLines.push(` allow_insecure_http: ${allowInsecureHttp}`);
|
|
3026
3059
|
}
|
|
3027
3060
|
|
|
3028
3061
|
// Always write exactly one trailing newline, matching the create and append paths.
|
|
@@ -3207,111 +3240,6 @@ function removeHermesConfigFile(cfgPath: string): HermesConfigResult {
|
|
|
3207
3240
|
return { updated: true, skipped: false, configPath: cfgPath };
|
|
3208
3241
|
}
|
|
3209
3242
|
|
|
3210
|
-
// ── Daemon health check (synchronous, non-fatal) ────────────────────────────
|
|
3211
|
-
|
|
3212
|
-
/**
|
|
3213
|
-
* Probe exit-code contract (used by checkDaemonHealth):
|
|
3214
|
-
* 0 — HTTP 200 (healthy)
|
|
3215
|
-
* 2 — HTTP 401 (token cache miss: retry after TTL)
|
|
3216
|
-
* 1 — any other HTTP status or network error
|
|
3217
|
-
*/
|
|
3218
|
-
const HEALTH_EXIT_OK = 0;
|
|
3219
|
-
const HEALTH_EXIT_UNAUTHORIZED = 2;
|
|
3220
|
-
|
|
3221
|
-
/**
|
|
3222
|
-
* Ping /engram/v1/health synchronously.
|
|
3223
|
-
* Returns true if the daemon responds with HTTP 200, false otherwise.
|
|
3224
|
-
* Uses a synchronous helper to run a one-liner Node script so that the existing
|
|
3225
|
-
* installConnector() flow does not need to become async.
|
|
3226
|
-
*
|
|
3227
|
-
* Data (host, port, token) are passed via environment variables — NOT
|
|
3228
|
-
* interpolated into the script string — to prevent injection from
|
|
3229
|
-
* user-supplied config values.
|
|
3230
|
-
*
|
|
3231
|
-
* /engram/v1/health is protected by bearer auth in the access HTTP server,
|
|
3232
|
-
* so the caller must pass the connector token (or the configured server
|
|
3233
|
-
* token) or the probe will always return 401 and report the daemon as
|
|
3234
|
-
* unreachable even when it is running.
|
|
3235
|
-
*
|
|
3236
|
-
* 401 handling: the daemon caches valid tokens with a 5-second TTL
|
|
3237
|
-
* (getAllValidTokensCached). A freshly-rotated token may not appear in the
|
|
3238
|
-
* cache for up to 5 s after rotation. We tolerate a single 401 by sleeping
|
|
3239
|
-
* one cache TTL (6000 ms = 5 s TTL + 1 s buffer) and retrying exactly once.
|
|
3240
|
-
*/
|
|
3241
|
-
function checkDaemonHealth(host: string, port: number, authToken?: string): boolean {
|
|
3242
|
-
try {
|
|
3243
|
-
// Validate port: must be an integer in [1, 65535].
|
|
3244
|
-
// This guards against user config supplying a non-numeric string.
|
|
3245
|
-
const safePort = Math.trunc(Number(port));
|
|
3246
|
-
if (!Number.isFinite(safePort) || safePort < 1 || safePort > 65535) {
|
|
3247
|
-
return false;
|
|
3248
|
-
}
|
|
3249
|
-
// Finding 7 fix: Node's http.get({ host }) expects an unbracketed IPv6
|
|
3250
|
-
// literal (e.g. "::1"), but sanitizeHermesHost permits bracketed form
|
|
3251
|
-
// "[::1]" (required for URL contexts). Strip the brackets here so that
|
|
3252
|
-
// http.get receives the bare address and doesn't fail to connect.
|
|
3253
|
-
// IPv4 and hostname strings are unaffected (no brackets to strip).
|
|
3254
|
-
const bareHost = host.startsWith("[") && host.endsWith("]")
|
|
3255
|
-
? host.slice(1, -1)
|
|
3256
|
-
: host;
|
|
3257
|
-
|
|
3258
|
-
// Data (host, port, token) are passed via env vars, never interpolated
|
|
3259
|
-
// into the script string, preventing any code-injection from malformed
|
|
3260
|
-
// config values.
|
|
3261
|
-
// Exit codes: 0 = 200 OK, 2 = 401 Unauthorized, 1 = other error.
|
|
3262
|
-
const script = [
|
|
3263
|
-
"const http = require('http');",
|
|
3264
|
-
"const env = process['env'];",
|
|
3265
|
-
"const headers = {};",
|
|
3266
|
-
"if (env.REMNIC_HEALTH_TOKEN) {",
|
|
3267
|
-
" headers['authorization'] = 'Bearer ' + env.REMNIC_HEALTH_TOKEN;",
|
|
3268
|
-
"}",
|
|
3269
|
-
"const req = http.get({",
|
|
3270
|
-
" host: env.REMNIC_HEALTH_HOST,",
|
|
3271
|
-
" port: parseInt(env.REMNIC_HEALTH_PORT, 10),",
|
|
3272
|
-
" path: '/engram/v1/health',",
|
|
3273
|
-
" headers,",
|
|
3274
|
-
" timeout: 3000,",
|
|
3275
|
-
"}, (res) => { process.exit(res.statusCode === 200 ? 0 : res.statusCode === 401 ? 2 : 1); });",
|
|
3276
|
-
"req.on('error', () => process.exit(1));",
|
|
3277
|
-
"req.on('timeout', () => { req.destroy(); process.exit(1); });",
|
|
3278
|
-
].join("\n");
|
|
3279
|
-
const env: NodeJS.ProcessEnv = mergeEnv({
|
|
3280
|
-
REMNIC_HEALTH_HOST: bareHost,
|
|
3281
|
-
REMNIC_HEALTH_PORT: String(safePort),
|
|
3282
|
-
});
|
|
3283
|
-
if (authToken) {
|
|
3284
|
-
env.REMNIC_HEALTH_TOKEN = authToken;
|
|
3285
|
-
}
|
|
3286
|
-
const processPath = process.execPath;
|
|
3287
|
-
const launchOptions = { timeout: 4000, env };
|
|
3288
|
-
const result = launchProcessSync(processPath, ["-e", script], launchOptions);
|
|
3289
|
-
|
|
3290
|
-
if (result.status === HEALTH_EXIT_OK) {
|
|
3291
|
-
return true;
|
|
3292
|
-
}
|
|
3293
|
-
|
|
3294
|
-
if (result.status === HEALTH_EXIT_UNAUTHORIZED) {
|
|
3295
|
-
// The daemon's token cache (5 s TTL) has not yet picked up the freshly
|
|
3296
|
-
// rotated token. Sleep one TTL + buffer and retry exactly once.
|
|
3297
|
-
console.error(
|
|
3298
|
-
"[remnic/connectors] health probe got 401 — retrying after token cache TTL...",
|
|
3299
|
-
);
|
|
3300
|
-
// Synchronous sleep without making the caller async.
|
|
3301
|
-
launchProcessSync(processPath, ["-e", "setTimeout(() => {}, 6000)"], {
|
|
3302
|
-
timeout: 7000,
|
|
3303
|
-
env: {},
|
|
3304
|
-
});
|
|
3305
|
-
const retry = launchProcessSync(processPath, ["-e", script], launchOptions);
|
|
3306
|
-
return retry.status === HEALTH_EXIT_OK;
|
|
3307
|
-
}
|
|
3308
|
-
|
|
3309
|
-
return false;
|
|
3310
|
-
} catch {
|
|
3311
|
-
return false;
|
|
3312
|
-
}
|
|
3313
|
-
}
|
|
3314
|
-
|
|
3315
3243
|
// ── Doctor ────────────────────────────────────────────────────────────────────
|
|
3316
3244
|
|
|
3317
3245
|
export async function doctorConnector(connectorId: string): Promise<DoctorResult> {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { launchProcessSync } from "./child-process.js";
|
|
2
|
+
import { mergeEnv } from "./env.js";
|
|
3
|
+
import { httpProtocolForHost } from "./http-transport.js";
|
|
4
|
+
|
|
5
|
+
const HEALTH_EXIT_OK = 0;
|
|
6
|
+
const HEALTH_EXIT_UNAUTHORIZED = 2;
|
|
7
|
+
|
|
8
|
+
export function checkDaemonHealth(
|
|
9
|
+
host: string,
|
|
10
|
+
port: number,
|
|
11
|
+
authToken?: string,
|
|
12
|
+
allowInsecureHttp = false,
|
|
13
|
+
): boolean {
|
|
14
|
+
try {
|
|
15
|
+
const safePort = Math.trunc(Number(port));
|
|
16
|
+
if (!Number.isFinite(safePort) || safePort < 1 || safePort > 65535) return false;
|
|
17
|
+
|
|
18
|
+
const bareHost = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
19
|
+
const script = [
|
|
20
|
+
"const env = process['env'];",
|
|
21
|
+
"const protocol = env.REMNIC_HEALTH_PROTOCOL;",
|
|
22
|
+
"if (protocol !== 'http' && protocol !== 'https') process.exit(1);",
|
|
23
|
+
"const transport = require(protocol);",
|
|
24
|
+
"const headers = {};",
|
|
25
|
+
"if (env.REMNIC_HEALTH_TOKEN) headers['authorization'] = 'Bearer ' + env.REMNIC_HEALTH_TOKEN;",
|
|
26
|
+
"const req = transport.get({",
|
|
27
|
+
" host: env.REMNIC_HEALTH_HOST,",
|
|
28
|
+
" port: parseInt(env.REMNIC_HEALTH_PORT, 10),",
|
|
29
|
+
" path: '/engram/v1/health', headers, timeout: 3000,",
|
|
30
|
+
"}, (res) => { process.exit(res.statusCode === 200 ? 0 : res.statusCode === 401 ? 2 : 1); });",
|
|
31
|
+
"req.on('error', () => process.exit(1));",
|
|
32
|
+
"req.on('timeout', () => { req.destroy(); process.exit(1); });",
|
|
33
|
+
].join("\n");
|
|
34
|
+
const env: NodeJS.ProcessEnv = mergeEnv({
|
|
35
|
+
REMNIC_HEALTH_HOST: bareHost,
|
|
36
|
+
REMNIC_HEALTH_PORT: String(safePort),
|
|
37
|
+
REMNIC_HEALTH_PROTOCOL: httpProtocolForHost(host, allowInsecureHttp),
|
|
38
|
+
});
|
|
39
|
+
if (authToken) env.REMNIC_HEALTH_TOKEN = authToken;
|
|
40
|
+
|
|
41
|
+
const launchOptions = { timeout: 4000, env };
|
|
42
|
+
const result = launchProcessSync(process.execPath, ["-e", script], launchOptions);
|
|
43
|
+
if (result.status === HEALTH_EXIT_OK) return true;
|
|
44
|
+
if (result.status !== HEALTH_EXIT_UNAUTHORIZED) return false;
|
|
45
|
+
|
|
46
|
+
console.error("[remnic/connectors] health probe got 401 — retrying after token cache TTL...");
|
|
47
|
+
launchProcessSync(process.execPath, ["-e", "setTimeout(() => {}, 6000)"], { timeout: 7000, env: {} });
|
|
48
|
+
const retry = launchProcessSync(process.execPath, ["-e", script], launchOptions);
|
|
49
|
+
return retry.status === HEALTH_EXIT_OK;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
|
|
3
|
+
function normalizedIp(host: string): string {
|
|
4
|
+
const value = host.trim().replace(/^\[/, "").replace(/\]\.?$/, "").replace(/\.$/, "");
|
|
5
|
+
if (isIP(value) === 4) return value;
|
|
6
|
+
if (isIP(value) !== 6) return "";
|
|
7
|
+
return new URL(`http://[${value}]/`).hostname.slice(1, -1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isLoopbackHost(host: string): boolean {
|
|
11
|
+
const normalized = host.trim().replace(/^\[/, "").replace(/\]\.?$/, "").replace(/\.$/, "").toLowerCase();
|
|
12
|
+
if (normalized === "localhost" || normalized.endsWith(".localhost")) return true;
|
|
13
|
+
|
|
14
|
+
const ip = normalizedIp(normalized);
|
|
15
|
+
if (ip === "::1") return true;
|
|
16
|
+
if (isIP(ip) === 4) return ip.startsWith("127.");
|
|
17
|
+
if (!ip.startsWith("::ffff:")) return false;
|
|
18
|
+
|
|
19
|
+
const [high, low] = ip.slice("::ffff:".length).split(":");
|
|
20
|
+
return Number.parseInt(high, 16) >>> 8 === 127 || (high === "0" && Number.parseInt(low, 16) >>> 8 === 127);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function httpProtocolForHost(host: string, allowInsecureHttp = false): "http" | "https" {
|
|
24
|
+
return isLoopbackHost(host) || allowInsecureHttp ? "http" : "https";
|
|
25
|
+
}
|