@nanhara/hara 0.123.1 → 0.124.1

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.
@@ -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
- const weixinDir = () => join(homedir(), ".hara", "weixin");
190
- const credsFile = () => join(weixinDir(), "creds.json");
191
- export function loadWeixinCreds() {
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
- return existsSync(credsFile()) ? JSON.parse(readFileSync(credsFile(), "utf8")) : null;
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 null;
214
+ return [];
197
215
  }
198
216
  }
199
- function saveWeixinCreds(c) {
200
- mkdirSync(weixinDir(), { recursive: true, mode: 0o700 });
217
+ export function loadWeixinCreds() {
201
218
  try {
202
- chmodSync(weixinDir(), 0o700);
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 { /* best effort */ }
205
- writeFileSync(credsFile(), JSON.stringify(c, null, 2), { mode: 0o600 });
206
- try {
207
- chmodSync(credsFile(), 0o600);
223
+ catch {
224
+ return null;
208
225
  }
209
- catch { /* best effort */ }
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 f = join(weixinDir(), `${accountId}.cursor`);
215
- return existsSync(f) ? readFileSync(f, "utf8") : "";
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
- mkdirSync(weixinDir(), { recursive: true, mode: 0o700 });
224
- chmodSync(weixinDir(), 0o700);
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 exists = existsSync(this.file());
241
- const parsed = exists ? JSON.parse(readFileSync(this.file(), "utf8")) : {};
242
- if (exists)
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
- file() {
256
- return join(weixinDir(), `${this.accountId}.context-tokens.json`);
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
- mkdirSync(weixinDir(), { recursive: true, mode: 0o700 });
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);