@nanhara/hara 0.123.0 → 0.124.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 +42 -0
- package/README.md +12 -9
- package/dist/agent/compact.js +98 -18
- package/dist/agent/context-budget.js +180 -0
- package/dist/agent/evolution.js +37 -0
- package/dist/agent/failover.js +3 -4
- package/dist/agent/loop.js +39 -4
- package/dist/config.js +27 -90
- package/dist/desk.js +8 -14
- package/dist/fs-read.js +26 -7
- package/dist/gateway/weixin.js +44 -42
- package/dist/index.js +240 -54
- package/dist/org-fleet/enroll.js +17 -22
- package/dist/plugins/plugins.js +7 -9
- package/dist/profile/profile.js +29 -56
- package/dist/providers/qwen-oauth.js +6 -19
- package/dist/security/private-state.js +285 -5
- package/dist/serve/server.js +59 -35
- package/dist/serve/sessions.js +12 -4
- package/dist/session/store.js +4 -0
- package/dist/session/task.js +70 -8
- package/dist/tools/memory.js +18 -3
- package/dist/tui/App.js +43 -17
- package/dist/tui/bracketed-paste.js +9 -1
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { homedir } from "node:os";
|
|
3
2
|
import { join, dirname, resolve } from "node:path";
|
|
4
|
-
import {
|
|
3
|
+
import { existsSync, lstatSync, realpathSync } from "node:fs";
|
|
5
4
|
import { agentMaxRounds, agentRunTimeoutMs } from "./agent/limits.js";
|
|
6
|
-
import { ensurePrivateHaraState } from "./security/private-state.js";
|
|
5
|
+
import { bindPrivateHaraStateFile, ensurePrivateHaraState, readPrivateStateFileSnapshotSync, writePrivateStateFileSync, } from "./security/private-state.js";
|
|
7
6
|
import { readVerifiedRegularFileSnapshotSync } from "./fs-read.js";
|
|
8
7
|
import { projectRepositoryTrustedAtStartup } from "./security/project-trust.js";
|
|
9
8
|
import { isHomeWorkspace } from "./context/workspace-scope.js";
|
|
@@ -109,15 +108,11 @@ function configRecord(value) {
|
|
|
109
108
|
}
|
|
110
109
|
export function readRawConfig() {
|
|
111
110
|
ensurePrivateHaraState();
|
|
112
|
-
const p = configPath();
|
|
113
|
-
if (!existsSync(p))
|
|
114
|
-
return {};
|
|
115
111
|
try {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
});
|
|
112
|
+
const binding = bindPrivateHaraStateFile(homedir(), [], "config.json");
|
|
113
|
+
const snapshot = readPrivateStateFileSnapshotSync(binding.path, MAX_GLOBAL_CONFIG_BYTES);
|
|
114
|
+
if (!snapshot)
|
|
115
|
+
return {};
|
|
121
116
|
return configRecord(JSON.parse(snapshot.text));
|
|
122
117
|
}
|
|
123
118
|
catch {
|
|
@@ -239,91 +234,33 @@ function readProjectConfig(cwd) {
|
|
|
239
234
|
}
|
|
240
235
|
return {};
|
|
241
236
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const info = lstatSync(path);
|
|
245
|
-
if (info.isSymbolicLink())
|
|
246
|
-
throw new Error(`refusing global config write: '${path}' is a symbolic link`);
|
|
247
|
-
if (!info.isFile())
|
|
248
|
-
throw new Error(`refusing global config write: '${path}' is not a regular file`);
|
|
249
|
-
if (info.nlink !== 1)
|
|
250
|
-
throw new Error(`refusing global config write: '${path}' is hard-linked`);
|
|
251
|
-
return { dev: info.dev, ino: info.ino };
|
|
252
|
-
}
|
|
253
|
-
catch (error) {
|
|
254
|
-
if (error?.code === "ENOENT")
|
|
255
|
-
return null;
|
|
256
|
-
throw error;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
/** Atomically replace the global 0600 config without opening its current directory entry for write. */
|
|
260
|
-
function persistConfig(p, cfg) {
|
|
237
|
+
/** Atomically replace the global 0600 config through the shared private-state CAS boundary. */
|
|
238
|
+
function persistConfig(cfg) {
|
|
261
239
|
ensurePrivateHaraState();
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const temp = join(parent, `.config-${process.pid}-${randomUUID()}.tmp`);
|
|
271
|
-
const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
|
|
272
|
-
let fd;
|
|
273
|
-
try {
|
|
274
|
-
fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollow, 0o600);
|
|
275
|
-
writeFileSync(fd, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
276
|
-
fsyncSync(fd);
|
|
277
|
-
const staged = fstatSync(fd);
|
|
278
|
-
if (!staged.isFile() || staged.nlink !== 1)
|
|
279
|
-
throw new Error("refusing global config write: unsafe staging inode");
|
|
280
|
-
closeSync(fd);
|
|
281
|
-
fd = undefined;
|
|
282
|
-
const parentAfter = lstatSync(parent);
|
|
283
|
-
if (!parentAfter.isDirectory() || parentAfter.isSymbolicLink() ||
|
|
284
|
-
parentAfter.dev !== parentInfo.dev || parentAfter.ino !== parentInfo.ino ||
|
|
285
|
-
realpathSync.native(parent) !== canonicalParent)
|
|
286
|
-
throw new Error(`refusing global config write: '${parent}' changed before commit`);
|
|
287
|
-
const current = existingConfigIdentity(p);
|
|
288
|
-
if ((existing === null) !== (current === null) || (existing && current && (existing.dev !== current.dev || existing.ino !== current.ino))) {
|
|
289
|
-
throw new Error(`refusing global config write: '${p}' changed before commit`);
|
|
290
|
-
}
|
|
291
|
-
// rename replaces the path entry; it never follows an alias planted after the last identity check.
|
|
292
|
-
renameSync(temp, p);
|
|
293
|
-
const committed = lstatSync(p);
|
|
294
|
-
if (!committed.isFile() || committed.isSymbolicLink() || committed.dev !== staged.dev || committed.ino !== staged.ino || committed.nlink !== 1) {
|
|
295
|
-
throw new Error(`global config changed during commit: '${p}'`);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
finally {
|
|
299
|
-
if (fd !== undefined)
|
|
300
|
-
try {
|
|
301
|
-
closeSync(fd);
|
|
302
|
-
}
|
|
303
|
-
catch { /* preserve original error */ }
|
|
304
|
-
try {
|
|
305
|
-
rmSync(temp, { force: true });
|
|
306
|
-
}
|
|
307
|
-
catch { /* preserve original error */ }
|
|
308
|
-
}
|
|
240
|
+
const binding = bindPrivateHaraStateFile(homedir(), [], "config.json");
|
|
241
|
+
writePrivateStateFileSync(binding, JSON.stringify(cfg, null, 2) + "\n");
|
|
242
|
+
}
|
|
243
|
+
/** Mutate global config without exposing a second direct-write implementation to feature modules. */
|
|
244
|
+
export function updateRawConfig(mutate) {
|
|
245
|
+
const config = readRawConfig();
|
|
246
|
+
const replacement = mutate(config);
|
|
247
|
+
persistConfig(replacement ?? config);
|
|
309
248
|
}
|
|
310
249
|
export function writeConfigValue(key, value) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
persistConfig(p, cfg);
|
|
250
|
+
updateRawConfig((config) => {
|
|
251
|
+
config[key] = value;
|
|
252
|
+
});
|
|
315
253
|
}
|
|
316
254
|
/** Record (or clear, with cap=null) a confirmed per-model vision capability in `modelVision`. */
|
|
317
255
|
export function setModelVisionOverride(model, cap) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
persistConfig(p, cfg);
|
|
256
|
+
updateRawConfig((config) => {
|
|
257
|
+
const map = config.modelVision && typeof config.modelVision === "object" ? config.modelVision : {};
|
|
258
|
+
if (cap === null)
|
|
259
|
+
delete map[model];
|
|
260
|
+
else
|
|
261
|
+
map[model] = cap;
|
|
262
|
+
config.modelVision = map;
|
|
263
|
+
});
|
|
327
264
|
}
|
|
328
265
|
/**
|
|
329
266
|
* Effective config. Precedence (high→low): env vars > allowed/trusted project `.hara/config.json` >
|
package/dist/desk.js
CHANGED
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Credentials live in ~/.hara/desk.json (0600): the desk URL + this agent's token, written once at
|
|
6
6
|
// `hara desk register`. Every later call reads them back. The token is a bearer secret — never logged.
|
|
7
|
-
import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
8
7
|
import { homedir } from "node:os";
|
|
9
|
-
import {
|
|
10
|
-
const credsPath = () => join(homedir(), ".hara", "desk.json");
|
|
8
|
+
import { bindPrivateHaraStateFile, readPrivateStateFileSnapshotSync, writePrivateStateFileSync, } from "./security/private-state.js";
|
|
11
9
|
export function loadCreds() {
|
|
12
10
|
try {
|
|
13
|
-
const
|
|
11
|
+
const binding = bindPrivateHaraStateFile(homedir(), [], "desk.json");
|
|
12
|
+
const snapshot = readPrivateStateFileSnapshotSync(binding.path, 1024 * 1024);
|
|
13
|
+
if (!snapshot)
|
|
14
|
+
return null;
|
|
15
|
+
const c = JSON.parse(snapshot.text);
|
|
14
16
|
return c.url && c.token ? c : null;
|
|
15
17
|
}
|
|
16
18
|
catch {
|
|
@@ -18,16 +20,8 @@ export function loadCreds() {
|
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
export function saveCreds(c) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const p = credsPath();
|
|
24
|
-
writeFileSync(p, JSON.stringify(c, null, 2));
|
|
25
|
-
try {
|
|
26
|
-
chmodSync(p, 0o600);
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
/* best-effort on non-posix */
|
|
30
|
-
}
|
|
23
|
+
const binding = bindPrivateHaraStateFile(homedir(), [], "desk.json");
|
|
24
|
+
writePrivateStateFileSync(binding, JSON.stringify(c, null, 2) + "\n");
|
|
31
25
|
}
|
|
32
26
|
/** One HTTP call to the desk. Auth via bearer token when creds are present. Throws on non-2xx with
|
|
33
27
|
* the server's error message (so the CLI can surface a real reason, not "request failed"). */
|
package/dist/fs-read.js
CHANGED
|
@@ -10,6 +10,27 @@ export class BinaryFileError extends Error {
|
|
|
10
10
|
this.name = "BinaryFileError";
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
export class InvalidUtf8FileError extends Error {
|
|
14
|
+
code = "HARA_INVALID_UTF8";
|
|
15
|
+
constructor(path) {
|
|
16
|
+
super(`${path} is not valid UTF-8; refusing a lossy text operation. ` +
|
|
17
|
+
"Use a binary-aware tool or explicitly transcode the file first");
|
|
18
|
+
this.name = "InvalidUtf8FileError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Decode a complete text-file snapshot without replacing invalid bytes or dropping a UTF-8 BOM. */
|
|
22
|
+
export function decodeUtf8Strict(bytes, path) {
|
|
23
|
+
if (bytes.includes(0))
|
|
24
|
+
throw new BinaryFileError(path);
|
|
25
|
+
try {
|
|
26
|
+
// ignoreBOM=true means the BOM is emitted as U+FEFF. A later UTF-8 rewrite therefore preserves the
|
|
27
|
+
// original bytes instead of silently stripping the marker while editing otherwise valid text.
|
|
28
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new InvalidUtf8FileError(path);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
13
34
|
export class NonRegularFileError extends Error {
|
|
14
35
|
code = "HARA_NOT_REGULAR_FILE";
|
|
15
36
|
constructor(path) {
|
|
@@ -44,7 +65,7 @@ export class ProtectedContextFileError extends Error {
|
|
|
44
65
|
export class HardLinkedFileError extends Error {
|
|
45
66
|
code = "HARA_HARD_LINKED_FILE";
|
|
46
67
|
constructor(path) {
|
|
47
|
-
super(`${path} has multiple hard links; its protected identity cannot be established safely`);
|
|
68
|
+
super(`${path} is hard-linked (has multiple hard links); its protected identity cannot be established safely`);
|
|
48
69
|
this.name = "HardLinkedFileError";
|
|
49
70
|
}
|
|
50
71
|
}
|
|
@@ -143,7 +164,7 @@ export async function readVerifiedRegularFileSnapshot(path, maxBytes = MAX_EDIT_
|
|
|
143
164
|
|| latest.ctimeMs !== info.ctimeMs)
|
|
144
165
|
throw new Error(`refusing to read ${path}: file changed while reading it`);
|
|
145
166
|
return {
|
|
146
|
-
text: Buffer.concat(chunks, total)
|
|
167
|
+
text: decodeUtf8Strict(Buffer.concat(chunks, total), path),
|
|
147
168
|
dev: info.dev,
|
|
148
169
|
ino: info.ino,
|
|
149
170
|
mode: info.mode & 0o777,
|
|
@@ -196,7 +217,7 @@ export function readVerifiedRegularFileSnapshotSync(path, maxBytes = MAX_EDIT_RE
|
|
|
196
217
|
|| latest.ctimeMs !== info.ctimeMs)
|
|
197
218
|
throw new Error(`refusing to read ${path}: file changed while reading it`);
|
|
198
219
|
return {
|
|
199
|
-
text: bytes
|
|
220
|
+
text: decodeUtf8Strict(bytes, path),
|
|
200
221
|
dev: info.dev,
|
|
201
222
|
ino: info.ino,
|
|
202
223
|
mode: info.mode & 0o777,
|
|
@@ -278,9 +299,7 @@ export function readModelContextFileSync(path, maxBytes) {
|
|
|
278
299
|
const bytes = readFdBytesSync(fd, Math.min(limit + 1, size + 1));
|
|
279
300
|
if (bytes.length > limit)
|
|
280
301
|
throw new FileReadLimitError(path, limit);
|
|
281
|
-
|
|
282
|
-
throw new BinaryFileError(path);
|
|
283
|
-
return bytes.toString("utf8");
|
|
302
|
+
return decodeUtf8Strict(bytes, path);
|
|
284
303
|
});
|
|
285
304
|
}
|
|
286
305
|
/** Safe bounded-prefix counterpart for @file expansion; it never materializes the remainder of a huge file. */
|
|
@@ -361,7 +380,7 @@ async function readRegularFileSnapshotWithFlags(path, maxBytes, flags) {
|
|
|
361
380
|
}
|
|
362
381
|
if (total > limit)
|
|
363
382
|
throw new FileReadLimitError(path, limit);
|
|
364
|
-
return { text: Buffer.concat(chunks, total)
|
|
383
|
+
return { text: decodeUtf8Strict(Buffer.concat(chunks, total), path), dev: info.dev, ino: info.ino, mode: info.mode & 0o777, nlink: info.nlink };
|
|
365
384
|
}
|
|
366
385
|
finally {
|
|
367
386
|
await handle.close().catch(() => { });
|
package/dist/gateway/weixin.js
CHANGED
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
// `qrcode-terminal` dep (graceful fallback to printing the URL). No encryption is needed on the text path
|
|
5
5
|
// (iLink only uses crypto for media upload/download, which v1 doesn't do).
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import { chmodSync, readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
9
7
|
import { randomBytes, randomUUID, createHash, createCipheriv, createDecipheriv } from "node:crypto";
|
|
10
8
|
import { chunkText } from "./telegram.js";
|
|
11
9
|
import { INBOUND_MEDIA_MAX_BYTES, INBOUND_MEDIA_TIMEOUT_MS, InboundMediaBudget, cleanupTransientMedia, readResponseBytesLimited, savePrivateMediaBytes, } from "./media.js";
|
|
10
|
+
import { bindPrivateHaraStateFile, readPrivateStateFileSnapshotSync, writePrivateStateFileSync, } from "../security/private-state.js";
|
|
12
11
|
const ILINK_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
13
12
|
const CHANNEL_VERSION = "2.2.0";
|
|
14
13
|
const ILINK_APP_ID = "bot";
|
|
@@ -186,33 +185,53 @@ async function apiGet(baseUrl, endpoint, timeoutMs) {
|
|
|
186
185
|
throw new Error(`iLink GET ${endpoint} HTTP ${res.status}: ${raw.slice(0, 200)}`);
|
|
187
186
|
return JSON.parse(raw);
|
|
188
187
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
function weixinStateFile(filename) {
|
|
189
|
+
return bindPrivateHaraStateFile(homedir(), ["weixin"], filename);
|
|
190
|
+
}
|
|
191
|
+
function accountStateFilename(accountId, suffix) {
|
|
192
|
+
const normalized = String(accountId ?? "").trim();
|
|
193
|
+
const key = /^[A-Za-z0-9_-]{1,128}$/.test(normalized)
|
|
194
|
+
? normalized
|
|
195
|
+
: `account-${createHash("sha256").update(normalized).digest("hex")}`;
|
|
196
|
+
return `${key}${suffix}`;
|
|
197
|
+
}
|
|
198
|
+
/** Return peer identifiers without exposing token values or bypassing the private-state reader. */
|
|
199
|
+
export function weixinKnownPeers(accountId) {
|
|
192
200
|
try {
|
|
193
|
-
|
|
201
|
+
const binding = weixinStateFile(accountStateFilename(accountId, ".context-tokens.json"));
|
|
202
|
+
const snapshot = readPrivateStateFileSnapshotSync(binding.path, 8 * 1024 * 1024);
|
|
203
|
+
if (!snapshot)
|
|
204
|
+
return [];
|
|
205
|
+
const parsed = JSON.parse(snapshot.text);
|
|
206
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
207
|
+
return [];
|
|
208
|
+
return Object.entries(parsed)
|
|
209
|
+
.slice(-1000)
|
|
210
|
+
.filter(([peer, token]) => Boolean(peer) && typeof token === "string" && Boolean(token))
|
|
211
|
+
.map(([peer]) => peer);
|
|
194
212
|
}
|
|
195
213
|
catch {
|
|
196
|
-
return
|
|
214
|
+
return [];
|
|
197
215
|
}
|
|
198
216
|
}
|
|
199
|
-
function
|
|
200
|
-
mkdirSync(weixinDir(), { recursive: true, mode: 0o700 });
|
|
217
|
+
export function loadWeixinCreds() {
|
|
201
218
|
try {
|
|
202
|
-
|
|
219
|
+
const binding = weixinStateFile("creds.json");
|
|
220
|
+
const snapshot = readPrivateStateFileSnapshotSync(binding.path, 1024 * 1024);
|
|
221
|
+
return snapshot ? JSON.parse(snapshot.text) : null;
|
|
203
222
|
}
|
|
204
|
-
catch {
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
chmodSync(credsFile(), 0o600);
|
|
223
|
+
catch {
|
|
224
|
+
return null;
|
|
208
225
|
}
|
|
209
|
-
|
|
226
|
+
}
|
|
227
|
+
function saveWeixinCreds(c) {
|
|
228
|
+
writePrivateStateFileSync(weixinStateFile("creds.json"), JSON.stringify(c, null, 2) + "\n");
|
|
210
229
|
}
|
|
211
230
|
// get_updates_buf cursor — persisted so a restart resumes the message stream where it left off.
|
|
212
231
|
function loadCursor(accountId) {
|
|
213
232
|
try {
|
|
214
|
-
const
|
|
215
|
-
return
|
|
233
|
+
const binding = weixinStateFile(accountStateFilename(accountId, ".cursor"));
|
|
234
|
+
return readPrivateStateFileSnapshotSync(binding.path, 4 * 1024 * 1024)?.text ?? "";
|
|
216
235
|
}
|
|
217
236
|
catch {
|
|
218
237
|
return "";
|
|
@@ -220,11 +239,8 @@ function loadCursor(accountId) {
|
|
|
220
239
|
}
|
|
221
240
|
function saveCursor(accountId, buf) {
|
|
222
241
|
try {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const file = join(weixinDir(), `${accountId}.cursor`);
|
|
226
|
-
writeFileSync(file, buf, { mode: 0o600 });
|
|
227
|
-
chmodSync(file, 0o600);
|
|
242
|
+
const binding = weixinStateFile(accountStateFilename(accountId, ".cursor"));
|
|
243
|
+
writePrivateStateFileSync(binding, buf);
|
|
228
244
|
}
|
|
229
245
|
catch {
|
|
230
246
|
/* best-effort */
|
|
@@ -237,10 +253,9 @@ class TokenStore {
|
|
|
237
253
|
constructor(accountId) {
|
|
238
254
|
this.accountId = accountId;
|
|
239
255
|
try {
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
chmodSync(this.file(), 0o600);
|
|
256
|
+
const binding = this.binding();
|
|
257
|
+
const snapshot = readPrivateStateFileSnapshotSync(binding.path, 8 * 1024 * 1024);
|
|
258
|
+
const parsed = snapshot ? JSON.parse(snapshot.text) : {};
|
|
244
259
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
245
260
|
for (const [peer, token] of Object.entries(parsed).slice(-1000)) {
|
|
246
261
|
if (peer && typeof token === "string" && token)
|
|
@@ -252,32 +267,19 @@ class TokenStore {
|
|
|
252
267
|
this.cache.clear();
|
|
253
268
|
}
|
|
254
269
|
}
|
|
255
|
-
|
|
256
|
-
return
|
|
270
|
+
binding() {
|
|
271
|
+
return weixinStateFile(accountStateFilename(this.accountId, ".context-tokens.json"));
|
|
257
272
|
}
|
|
258
273
|
peerKey(peer) {
|
|
259
274
|
return String(peer ?? "").trim().slice(0, 256);
|
|
260
275
|
}
|
|
261
276
|
persist() {
|
|
262
|
-
const temporary = `${this.file()}.${process.pid}.${randomUUID()}.tmp`;
|
|
263
277
|
try {
|
|
264
|
-
|
|
265
|
-
chmodSync(weixinDir(), 0o700);
|
|
266
|
-
writeFileSync(temporary, JSON.stringify(Object.fromEntries(this.cache)), { mode: 0o600, flag: "wx" });
|
|
267
|
-
renameSync(temporary, this.file());
|
|
268
|
-
chmodSync(this.file(), 0o600);
|
|
278
|
+
writePrivateStateFileSync(this.binding(), JSON.stringify(Object.fromEntries(this.cache)) + "\n");
|
|
269
279
|
}
|
|
270
280
|
catch {
|
|
271
281
|
/* best-effort */
|
|
272
282
|
}
|
|
273
|
-
finally {
|
|
274
|
-
try {
|
|
275
|
-
rmSync(temporary, { force: true });
|
|
276
|
-
}
|
|
277
|
-
catch {
|
|
278
|
-
/* best-effort */
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
283
|
}
|
|
282
284
|
get(peer) {
|
|
283
285
|
const key = this.peerKey(peer);
|