@remembrance-ai/opencode-plugin 0.1.47 → 0.1.48
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/README.md +9 -3
- package/opencode.json +2 -2
- package/package.json +1 -1
- package/scripts/hook-core.mjs +283 -36
- package/servers/remembrance-mcp.mjs +1200 -170
- package/skills/remembrancer/SKILL.md +34 -19
- package/skills/remembrancer/references/remembrance-payloads.md +8 -7
- package/skills/remembrancer/references/remembrance-setup.md +66 -25
- package/src/index.mjs +2 -2
package/README.md
CHANGED
|
@@ -49,9 +49,10 @@ chmod 600 ~/.config/remembrance/config.json
|
|
|
49
49
|
local MCP server both read `~/.config/remembrance/config.json`, an unset
|
|
50
50
|
`REMEMBRANCE_API_KEY` does **not** prove this install is anonymous.
|
|
51
51
|
|
|
52
|
-
After restarting,
|
|
53
|
-
`
|
|
54
|
-
|
|
52
|
+
After restarting, run MCP `run_connection_doctor` and require
|
|
53
|
+
`safe_to_query: true`. It verifies the active connection and gives one exact
|
|
54
|
+
next step if attention is required. Use `get_connection_status` only for lower-
|
|
55
|
+
level fields. Never infer scope from `REMEMBRANCE_API_KEY` alone, from an environment
|
|
55
56
|
variable, or from an anonymous REST/browser probe — the diagnostic verifies the
|
|
56
57
|
process that will actually serve opencode's tools, without exposing the key.
|
|
57
58
|
|
|
@@ -66,6 +67,11 @@ printf '{"apiKey":"YOUR_ORG_KEY","apiUrl":"https://dev.remembrance.dev"}\n' \
|
|
|
66
67
|
> ~/.config/remembrance/config.json
|
|
67
68
|
```
|
|
68
69
|
|
|
70
|
+
This paired file binds the key to that destination. If environment variables
|
|
71
|
+
are used instead, set `REMEMBRANCE_API_KEY_ORIGIN` equal to the exact custom
|
|
72
|
+
`REMEMBRANCE_API_URL`. Remote registries require HTTPS; a trusted private or
|
|
73
|
+
link-local self-host also requires `REMEMBRANCE_ALLOW_PRIVATE_REGISTRY=true`.
|
|
74
|
+
|
|
69
75
|
## What the plugin does
|
|
70
76
|
|
|
71
77
|
| Event | Behavior |
|
package/opencode.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"command": [
|
|
7
7
|
"npx",
|
|
8
8
|
"-y",
|
|
9
|
-
"@remembrance-ai/mcp-server@0.1.
|
|
9
|
+
"@remembrance-ai/mcp-server@0.1.48"
|
|
10
10
|
],
|
|
11
11
|
"enabled": true,
|
|
12
12
|
"environment": {
|
|
@@ -15,6 +15,6 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"plugin": [
|
|
18
|
-
"@remembrance-ai/opencode-plugin@0.1.
|
|
18
|
+
"@remembrance-ai/opencode-plugin@0.1.48"
|
|
19
19
|
]
|
|
20
20
|
}
|
package/package.json
CHANGED
package/scripts/hook-core.mjs
CHANGED
|
@@ -17,15 +17,23 @@
|
|
|
17
17
|
|
|
18
18
|
import { createHash, randomBytes } from "node:crypto";
|
|
19
19
|
import {
|
|
20
|
+
chmodSync,
|
|
21
|
+
closeSync,
|
|
22
|
+
constants,
|
|
20
23
|
existsSync,
|
|
24
|
+
fstatSync,
|
|
25
|
+
lstatSync,
|
|
21
26
|
mkdirSync,
|
|
27
|
+
openSync,
|
|
22
28
|
readFileSync,
|
|
29
|
+
readSync,
|
|
23
30
|
readdirSync,
|
|
24
31
|
renameSync,
|
|
25
32
|
rmSync,
|
|
26
33
|
statSync,
|
|
27
34
|
writeFileSync,
|
|
28
35
|
} from "node:fs";
|
|
36
|
+
import { isIP } from "node:net";
|
|
29
37
|
import { homedir, tmpdir } from "node:os";
|
|
30
38
|
import { dirname, join } from "node:path";
|
|
31
39
|
import process from "node:process";
|
|
@@ -43,7 +51,8 @@ const VALUE_EPISODE_MARKER_LIMIT = 20;
|
|
|
43
51
|
const VALUE_EPISODE_MARKER_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
44
52
|
const DIRECT_SELECTION_MARKER_LIMIT = 20;
|
|
45
53
|
const DIRECT_SELECTION_MARKER_TTL_MS = 24 * 60 * 60 * 1000;
|
|
46
|
-
const
|
|
54
|
+
const MAX_LOCAL_CONFIG_BYTES = 64 * 1024;
|
|
55
|
+
const MAX_LOCAL_HEALTH_MARKER_BYTES = 16 * 1024;
|
|
47
56
|
const PLUGIN_HEALTH_COMPONENTS = new Set([
|
|
48
57
|
"session_start",
|
|
49
58
|
"prompt_hook",
|
|
@@ -2281,7 +2290,7 @@ export function contributeDisabled(value) {
|
|
|
2281
2290
|
function pluginHealthDir(env = process.env) {
|
|
2282
2291
|
return env?.REMEMBRANCE_PLUGIN_HEALTH_DIR
|
|
2283
2292
|
? String(env.REMEMBRANCE_PLUGIN_HEALTH_DIR)
|
|
2284
|
-
: join(
|
|
2293
|
+
: join(homedir(), ".cache", "remembrance", "plugin-health");
|
|
2285
2294
|
}
|
|
2286
2295
|
|
|
2287
2296
|
function normalizedPluginHealthSurface(value) {
|
|
@@ -2317,7 +2326,9 @@ export function readPluginLifecycleHealth(
|
|
|
2317
2326
|
const path = pluginHealthSessionPath(surface, sessionId, env);
|
|
2318
2327
|
if (!path) return null;
|
|
2319
2328
|
try {
|
|
2320
|
-
const parsed = JSON.parse(
|
|
2329
|
+
const parsed = JSON.parse(
|
|
2330
|
+
readSecureHookFile(path, MAX_LOCAL_HEALTH_MARKER_BYTES),
|
|
2331
|
+
);
|
|
2321
2332
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
2322
2333
|
? parsed
|
|
2323
2334
|
: null;
|
|
@@ -2329,7 +2340,7 @@ export function readPluginLifecycleHealth(
|
|
|
2329
2340
|
// Record that a native lifecycle component actually executed. The marker is
|
|
2330
2341
|
// intentionally local and content-free: it contains only surface/version,
|
|
2331
2342
|
// component timestamps, and the credential source category. Local MCP reads
|
|
2332
|
-
// it through
|
|
2343
|
+
// it through run_connection_doctor so partial activation cannot look healthy.
|
|
2333
2344
|
export function recordPluginLifecycleHealth(
|
|
2334
2345
|
{
|
|
2335
2346
|
surface,
|
|
@@ -2390,6 +2401,11 @@ export function recordPluginLifecycleHealth(
|
|
|
2390
2401
|
// healthy for as long as the old marker remains fresh.
|
|
2391
2402
|
const currentComponents =
|
|
2392
2403
|
normalizedComponent === "session_start" ? {} : existingComponents;
|
|
2404
|
+
const apiConfiguration = resolveApiConfiguration(env);
|
|
2405
|
+
const apiDestinationFingerprint = createHash("sha256")
|
|
2406
|
+
.update(apiConfiguration.apiUrl.replace(/\/+$/, ""), "utf8")
|
|
2407
|
+
.digest("hex")
|
|
2408
|
+
.slice(0, 16);
|
|
2393
2409
|
const payload = {
|
|
2394
2410
|
schema_version: 1,
|
|
2395
2411
|
surface: normalizedSurface,
|
|
@@ -2414,6 +2430,8 @@ export function recordPluginLifecycleHealth(
|
|
|
2414
2430
|
)
|
|
2415
2431
|
? existing.credential_source
|
|
2416
2432
|
: "none",
|
|
2433
|
+
api_destination_source: apiConfiguration.source,
|
|
2434
|
+
api_destination_fingerprint: apiDestinationFingerprint,
|
|
2417
2435
|
components: {
|
|
2418
2436
|
...currentComponents,
|
|
2419
2437
|
[normalizedComponent]: now,
|
|
@@ -2422,9 +2440,10 @@ export function recordPluginLifecycleHealth(
|
|
|
2422
2440
|
};
|
|
2423
2441
|
const temporaryPath = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
2424
2442
|
try {
|
|
2425
|
-
|
|
2443
|
+
ensureSecureHookDirectory(dirname(path));
|
|
2426
2444
|
writeFileSync(temporaryPath, `${JSON.stringify(payload)}\n`, {
|
|
2427
2445
|
mode: 0o600,
|
|
2446
|
+
flag: "wx",
|
|
2428
2447
|
});
|
|
2429
2448
|
renameSync(temporaryPath, path);
|
|
2430
2449
|
prunePluginHealthSessionMarkers(normalizedSurface, path, env);
|
|
@@ -2481,7 +2500,9 @@ export function remembranceConfigPath(env = process.env) {
|
|
|
2481
2500
|
|
|
2482
2501
|
export function readRemembranceConfig(env = process.env) {
|
|
2483
2502
|
try {
|
|
2484
|
-
const parsed = JSON.parse(
|
|
2503
|
+
const parsed = JSON.parse(
|
|
2504
|
+
readSecureHookFile(remembranceConfigPath(env), MAX_LOCAL_CONFIG_BYTES),
|
|
2505
|
+
);
|
|
2485
2506
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
2486
2507
|
? parsed
|
|
2487
2508
|
: {};
|
|
@@ -2497,32 +2518,59 @@ export function resolveApiCredential(env = process.env) {
|
|
|
2497
2518
|
}
|
|
2498
2519
|
const environmentKey = String(env.REMEMBRANCE_API_KEY ?? "").trim();
|
|
2499
2520
|
if (environmentKey) {
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2521
|
+
const explicitBinding = String(
|
|
2522
|
+
env.REMEMBRANCE_API_KEY_ORIGIN ?? "",
|
|
2523
|
+
).trim();
|
|
2524
|
+
const binding = explicitBinding
|
|
2525
|
+
? normalizeApiUrl(explicitBinding, env)
|
|
2526
|
+
: { apiUrl: DEFAULT_API_URL, issue: null };
|
|
2527
|
+
return credentialForApiDestination(
|
|
2528
|
+
binding.apiUrl && !binding.issue
|
|
2529
|
+
? {
|
|
2530
|
+
apiKey: environmentKey,
|
|
2531
|
+
source: "environment",
|
|
2532
|
+
boundApiUrl: binding.apiUrl,
|
|
2533
|
+
}
|
|
2534
|
+
: unusableDestinationCredential(),
|
|
2535
|
+
apiConfiguration,
|
|
2536
|
+
);
|
|
2504
2537
|
}
|
|
2505
2538
|
const path = remembranceConfigPath(env);
|
|
2506
2539
|
if (!existsSync(path)) {
|
|
2507
2540
|
return { apiKey: "", source: "none" };
|
|
2508
2541
|
}
|
|
2509
2542
|
try {
|
|
2510
|
-
const parsed = JSON.parse(
|
|
2543
|
+
const parsed = JSON.parse(
|
|
2544
|
+
readSecureHookFile(path, MAX_LOCAL_CONFIG_BYTES),
|
|
2545
|
+
);
|
|
2511
2546
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2512
|
-
return
|
|
2547
|
+
return unusableSharedConfigCredential();
|
|
2513
2548
|
}
|
|
2514
2549
|
if (
|
|
2515
2550
|
Object.prototype.hasOwnProperty.call(parsed, "apiKey") &&
|
|
2516
2551
|
(typeof parsed.apiKey !== "string" || !parsed.apiKey.trim())
|
|
2517
2552
|
) {
|
|
2518
|
-
return
|
|
2553
|
+
return unusableSharedConfigCredential();
|
|
2519
2554
|
}
|
|
2520
2555
|
const fromFile = parsed.apiKey;
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2556
|
+
if (typeof fromFile !== "string" || !fromFile.trim()) {
|
|
2557
|
+
return { apiKey: "", source: "none" };
|
|
2558
|
+
}
|
|
2559
|
+
const binding = Object.prototype.hasOwnProperty.call(parsed, "apiUrl")
|
|
2560
|
+
? normalizeApiUrl(parsed.apiUrl, env)
|
|
2561
|
+
: { apiUrl: DEFAULT_API_URL, issue: null };
|
|
2562
|
+
return credentialForApiDestination(
|
|
2563
|
+
binding.apiUrl && !binding.issue
|
|
2564
|
+
? {
|
|
2565
|
+
apiKey: fromFile.trim(),
|
|
2566
|
+
source: "shared_config",
|
|
2567
|
+
boundApiUrl: binding.apiUrl,
|
|
2568
|
+
}
|
|
2569
|
+
: unusableSharedConfigCredential(),
|
|
2570
|
+
apiConfiguration,
|
|
2571
|
+
);
|
|
2524
2572
|
} catch {
|
|
2525
|
-
return
|
|
2573
|
+
return unusableSharedConfigCredential();
|
|
2526
2574
|
}
|
|
2527
2575
|
}
|
|
2528
2576
|
|
|
@@ -2538,8 +2586,10 @@ export function sharedConfigCredentialNotice(env = process.env) {
|
|
|
2538
2586
|
return (
|
|
2539
2587
|
"Remembrance setup needs attention: REMEMBRANCE_API_URL is invalid. " +
|
|
2540
2588
|
"Remote Remembrance calls are paused locally instead of changing " +
|
|
2541
|
-
"destinations. Set it to an absolute
|
|
2542
|
-
"
|
|
2589
|
+
"destinations. Set it to an absolute HTTPS registry URL (HTTP is allowed " +
|
|
2590
|
+
"only for loopback development) without credentials, query parameters, " +
|
|
2591
|
+
"or fragments, or " +
|
|
2592
|
+
"intentionally remove it, then run run_connection_doctor."
|
|
2543
2593
|
);
|
|
2544
2594
|
}
|
|
2545
2595
|
if (source === "unusable_shared_config") {
|
|
@@ -2547,7 +2597,16 @@ export function sharedConfigCredentialNotice(env = process.env) {
|
|
|
2547
2597
|
"Remembrance setup needs attention: the shared config file exists but " +
|
|
2548
2598
|
"is unreadable or invalid. Remote Remembrance calls are paused locally " +
|
|
2549
2599
|
"instead of falling back to anonymous scope. Fix or intentionally remove " +
|
|
2550
|
-
"~/.config/remembrance/config.json, then
|
|
2600
|
+
"~/.config/remembrance/config.json, then run run_connection_doctor."
|
|
2601
|
+
);
|
|
2602
|
+
}
|
|
2603
|
+
if (source === "unusable_destination_binding") {
|
|
2604
|
+
return (
|
|
2605
|
+
"Remembrance setup needs attention: the API key is not bound to the " +
|
|
2606
|
+
"configured registry destination. For a custom registry, store apiKey " +
|
|
2607
|
+
"and apiUrl together in the shared config, or set " +
|
|
2608
|
+
"REMEMBRANCE_API_KEY_ORIGIN to the exact REMEMBRANCE_API_URL value. " +
|
|
2609
|
+
"Remote calls are paused so the key cannot be forwarded elsewhere."
|
|
2551
2610
|
);
|
|
2552
2611
|
}
|
|
2553
2612
|
if (source !== "shared_config") {
|
|
@@ -2558,24 +2617,30 @@ export function sharedConfigCredentialNotice(env = process.env) {
|
|
|
2558
2617
|
`the shared Remembrance config file (normally ` +
|
|
2559
2618
|
`~/.config/remembrance/config.json). REMEMBRANCE_API_KEY may be unset; do not ` +
|
|
2560
2619
|
`use an anonymous REST/browser probe to infer this hook's scope. Call ` +
|
|
2561
|
-
`
|
|
2620
|
+
`run_connection_doctor for the MCP transport you will use.`
|
|
2562
2621
|
);
|
|
2563
2622
|
}
|
|
2564
2623
|
|
|
2565
2624
|
export function resolveApiConfiguration(env = process.env) {
|
|
2566
2625
|
const environmentUrl = String(env.REMEMBRANCE_API_URL ?? "").trim();
|
|
2567
2626
|
if (environmentUrl) {
|
|
2568
|
-
const normalized = normalizeApiUrl(environmentUrl);
|
|
2569
|
-
return normalized
|
|
2570
|
-
? { apiUrl: normalized, source: "environment" }
|
|
2571
|
-
: {
|
|
2627
|
+
const normalized = normalizeApiUrl(environmentUrl, env);
|
|
2628
|
+
return normalized.apiUrl && !normalized.issue
|
|
2629
|
+
? { apiUrl: normalized.apiUrl, source: "environment" }
|
|
2630
|
+
: {
|
|
2631
|
+
apiUrl: DEFAULT_API_URL,
|
|
2632
|
+
source: "unusable_environment",
|
|
2633
|
+
issue: normalized.issue ?? "invalid_url",
|
|
2634
|
+
};
|
|
2572
2635
|
}
|
|
2573
2636
|
const path = remembranceConfigPath(env);
|
|
2574
2637
|
if (!existsSync(path)) {
|
|
2575
2638
|
return { apiUrl: DEFAULT_API_URL, source: "default" };
|
|
2576
2639
|
}
|
|
2577
2640
|
try {
|
|
2578
|
-
const parsed = JSON.parse(
|
|
2641
|
+
const parsed = JSON.parse(
|
|
2642
|
+
readSecureHookFile(path, MAX_LOCAL_CONFIG_BYTES),
|
|
2643
|
+
);
|
|
2579
2644
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2580
2645
|
return {
|
|
2581
2646
|
apiUrl: DEFAULT_API_URL,
|
|
@@ -2585,10 +2650,14 @@ export function resolveApiConfiguration(env = process.env) {
|
|
|
2585
2650
|
if (!Object.prototype.hasOwnProperty.call(parsed, "apiUrl")) {
|
|
2586
2651
|
return { apiUrl: DEFAULT_API_URL, source: "default" };
|
|
2587
2652
|
}
|
|
2588
|
-
const normalized = normalizeApiUrl(parsed.apiUrl);
|
|
2589
|
-
return normalized
|
|
2590
|
-
? { apiUrl: normalized, source: "shared_config" }
|
|
2591
|
-
: {
|
|
2653
|
+
const normalized = normalizeApiUrl(parsed.apiUrl, env);
|
|
2654
|
+
return normalized.apiUrl && !normalized.issue
|
|
2655
|
+
? { apiUrl: normalized.apiUrl, source: "shared_config" }
|
|
2656
|
+
: {
|
|
2657
|
+
apiUrl: DEFAULT_API_URL,
|
|
2658
|
+
source: "unusable_shared_config",
|
|
2659
|
+
issue: normalized.issue ?? "invalid_url",
|
|
2660
|
+
};
|
|
2592
2661
|
} catch {
|
|
2593
2662
|
return { apiUrl: DEFAULT_API_URL, source: "unusable_shared_config" };
|
|
2594
2663
|
}
|
|
@@ -2598,8 +2667,10 @@ function apiUrl(env) {
|
|
|
2598
2667
|
return resolveApiConfiguration(env).apiUrl;
|
|
2599
2668
|
}
|
|
2600
2669
|
|
|
2601
|
-
function normalizeApiUrl(value) {
|
|
2602
|
-
if (typeof value !== "string" || !value.trim())
|
|
2670
|
+
function normalizeApiUrl(value, env = process.env) {
|
|
2671
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
2672
|
+
return { apiUrl: null, issue: "invalid_url" };
|
|
2673
|
+
}
|
|
2603
2674
|
const candidate = value.trim();
|
|
2604
2675
|
try {
|
|
2605
2676
|
const parsed = new URL(candidate);
|
|
@@ -2610,17 +2681,193 @@ function normalizeApiUrl(value) {
|
|
|
2610
2681
|
parsed.search ||
|
|
2611
2682
|
parsed.hash
|
|
2612
2683
|
) {
|
|
2613
|
-
return null;
|
|
2684
|
+
return { apiUrl: null, issue: "invalid_url" };
|
|
2685
|
+
}
|
|
2686
|
+
const loopback = isLoopbackHostname(parsed.hostname);
|
|
2687
|
+
const privateDestination = isPrivateDestinationHostname(parsed.hostname);
|
|
2688
|
+
if (parsed.protocol === "http:" && !loopback) {
|
|
2689
|
+
return { apiUrl: null, issue: "insecure_remote_http" };
|
|
2690
|
+
}
|
|
2691
|
+
if (
|
|
2692
|
+
!loopback &&
|
|
2693
|
+
privateDestination &&
|
|
2694
|
+
env.REMEMBRANCE_ALLOW_PRIVATE_REGISTRY !== "true"
|
|
2695
|
+
) {
|
|
2696
|
+
return {
|
|
2697
|
+
apiUrl: null,
|
|
2698
|
+
issue: "private_destination_requires_opt_in",
|
|
2699
|
+
};
|
|
2614
2700
|
}
|
|
2615
|
-
return candidate.replace(/\/+$/, "");
|
|
2701
|
+
return { apiUrl: candidate.replace(/\/+$/, ""), issue: null };
|
|
2616
2702
|
} catch {
|
|
2617
|
-
return null;
|
|
2703
|
+
return { apiUrl: null, issue: "invalid_url" };
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
function credentialForApiDestination(credential, configuration) {
|
|
2708
|
+
if (!credential.apiKey || isUnusableConfigurationSource(configuration.source)) {
|
|
2709
|
+
return credential.apiKey
|
|
2710
|
+
? unusableDestinationCredential()
|
|
2711
|
+
: { apiKey: "", source: credential.source };
|
|
2618
2712
|
}
|
|
2713
|
+
return credential.boundApiUrl === configuration.apiUrl
|
|
2714
|
+
? { apiKey: credential.apiKey, source: credential.source }
|
|
2715
|
+
: unusableDestinationCredential();
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
function unusableSharedConfigCredential() {
|
|
2719
|
+
return {
|
|
2720
|
+
apiKey: "",
|
|
2721
|
+
source: "unusable_shared_config",
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
function unusableDestinationCredential() {
|
|
2726
|
+
return {
|
|
2727
|
+
apiKey: "",
|
|
2728
|
+
source: "unusable_destination_binding",
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
function isLoopbackHostname(hostname) {
|
|
2733
|
+
const normalized = normalizeHostname(hostname);
|
|
2734
|
+
if (normalized === "localhost" || normalized.endsWith(".localhost")) {
|
|
2735
|
+
return true;
|
|
2736
|
+
}
|
|
2737
|
+
if (normalized === "::1") return true;
|
|
2738
|
+
if (isIP(normalized) === 4) {
|
|
2739
|
+
return Number(normalized.split(".")[0]) === 127;
|
|
2740
|
+
}
|
|
2741
|
+
return false;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
function isPrivateDestinationHostname(hostname) {
|
|
2745
|
+
const normalized = normalizeHostname(hostname);
|
|
2746
|
+
if (
|
|
2747
|
+
normalized.endsWith(".local") ||
|
|
2748
|
+
normalized.endsWith(".internal") ||
|
|
2749
|
+
normalized === "localhost"
|
|
2750
|
+
) {
|
|
2751
|
+
return true;
|
|
2752
|
+
}
|
|
2753
|
+
const version = isIP(normalized);
|
|
2754
|
+
if (version === 4) {
|
|
2755
|
+
const [a, b] = normalized.split(".").map(Number);
|
|
2756
|
+
return (
|
|
2757
|
+
a === 0 ||
|
|
2758
|
+
a === 10 ||
|
|
2759
|
+
a === 127 ||
|
|
2760
|
+
(a === 100 && b >= 64 && b <= 127) ||
|
|
2761
|
+
(a === 169 && b === 254) ||
|
|
2762
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
2763
|
+
(a === 192 && b === 168) ||
|
|
2764
|
+
(a === 198 && (b === 18 || b === 19)) ||
|
|
2765
|
+
a >= 224
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
if (version === 6) {
|
|
2769
|
+
return (
|
|
2770
|
+
normalized === "::" ||
|
|
2771
|
+
normalized === "::1" ||
|
|
2772
|
+
normalized.startsWith("fc") ||
|
|
2773
|
+
normalized.startsWith("fd") ||
|
|
2774
|
+
/^fe[89ab]/.test(normalized)
|
|
2775
|
+
);
|
|
2776
|
+
}
|
|
2777
|
+
return false;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
function normalizeHostname(hostname) {
|
|
2781
|
+
return String(hostname).replace(/^\[|\]$/g, "").toLowerCase();
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
function readSecureHookFile(path, maxBytes) {
|
|
2785
|
+
const parent = lstatSync(dirname(path));
|
|
2786
|
+
assertSecureHookDirectory(parent);
|
|
2787
|
+
const before = lstatSync(path);
|
|
2788
|
+
assertSecureHookFile(before, maxBytes);
|
|
2789
|
+
const descriptor = openSync(
|
|
2790
|
+
path,
|
|
2791
|
+
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),
|
|
2792
|
+
);
|
|
2793
|
+
try {
|
|
2794
|
+
const opened = fstatSync(descriptor);
|
|
2795
|
+
assertSecureHookFile(opened, maxBytes);
|
|
2796
|
+
if (before.dev !== opened.dev || before.ino !== opened.ino) {
|
|
2797
|
+
throw new Error("Remembrance local state changed while opening.");
|
|
2798
|
+
}
|
|
2799
|
+
return readBoundedHookDescriptor(descriptor, maxBytes);
|
|
2800
|
+
} finally {
|
|
2801
|
+
closeSync(descriptor);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
function ensureSecureHookDirectory(path) {
|
|
2806
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
2807
|
+
const metadata = lstatSync(path);
|
|
2808
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
2809
|
+
throw new Error("Remembrance health state is not a regular directory.");
|
|
2810
|
+
}
|
|
2811
|
+
assertCurrentHookUser(metadata.uid);
|
|
2812
|
+
if (isPosixHookRuntime() && (metadata.mode & 0o077) !== 0) {
|
|
2813
|
+
chmodSync(path, 0o700);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
function assertSecureHookDirectory(metadata) {
|
|
2818
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
2819
|
+
throw new Error("Remembrance local state parent is unsafe.");
|
|
2820
|
+
}
|
|
2821
|
+
assertCurrentHookUser(metadata.uid);
|
|
2822
|
+
if (isPosixHookRuntime() && (metadata.mode & 0o022) !== 0) {
|
|
2823
|
+
throw new Error("Remembrance local state parent is writable by others.");
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
function assertSecureHookFile(metadata, maxBytes) {
|
|
2828
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
2829
|
+
throw new Error("Remembrance local state is not a regular file.");
|
|
2830
|
+
}
|
|
2831
|
+
assertCurrentHookUser(metadata.uid);
|
|
2832
|
+
if (metadata.size > maxBytes) {
|
|
2833
|
+
throw new Error("Remembrance local state exceeds its size limit.");
|
|
2834
|
+
}
|
|
2835
|
+
if (isPosixHookRuntime() && (metadata.mode & 0o077) !== 0) {
|
|
2836
|
+
throw new Error("Remembrance local state permissions are not private.");
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
function assertCurrentHookUser(uid) {
|
|
2841
|
+
if (isPosixHookRuntime() && uid !== process.getuid()) {
|
|
2842
|
+
throw new Error("Remembrance local state is owned by another user.");
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
function readBoundedHookDescriptor(descriptor, maxBytes) {
|
|
2847
|
+
const chunks = [];
|
|
2848
|
+
let total = 0;
|
|
2849
|
+
while (total <= maxBytes) {
|
|
2850
|
+
const chunk = Buffer.allocUnsafe(Math.min(8 * 1024, maxBytes + 1 - total));
|
|
2851
|
+
const bytesRead = readSync(descriptor, chunk, 0, chunk.length, null);
|
|
2852
|
+
if (bytesRead === 0) break;
|
|
2853
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
2854
|
+
total += bytesRead;
|
|
2855
|
+
}
|
|
2856
|
+
if (total > maxBytes) {
|
|
2857
|
+
throw new Error("Remembrance local state exceeds its size limit.");
|
|
2858
|
+
}
|
|
2859
|
+
return Buffer.concat(chunks, total).toString("utf8");
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
function isPosixHookRuntime() {
|
|
2863
|
+
return typeof process.getuid === "function";
|
|
2619
2864
|
}
|
|
2620
2865
|
|
|
2621
2866
|
function isUnusableConfigurationSource(source) {
|
|
2622
2867
|
return (
|
|
2623
|
-
source === "unusable_environment" ||
|
|
2868
|
+
source === "unusable_environment" ||
|
|
2869
|
+
source === "unusable_shared_config" ||
|
|
2870
|
+
source === "unusable_destination_binding"
|
|
2624
2871
|
);
|
|
2625
2872
|
}
|
|
2626
2873
|
|