@chainlesschain/personal-data-hub 0.2.1 → 0.2.3

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.
Files changed (39) hide show
  1. package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +58 -16
  2. package/__tests__/adapters/wechat-frida-agent.test.js +132 -1
  3. package/__tests__/integration/social-bilibili-pipeline.test.js +261 -0
  4. package/__tests__/longtail-adapters.test.js +60 -14
  5. package/__tests__/messaging-qq-snapshot.test.js +294 -0
  6. package/__tests__/shopping-pinduoduo-snapshot.test.js +302 -0
  7. package/__tests__/shopping-snapshot.test.js +438 -0
  8. package/__tests__/social-adapters.test.js +91 -17
  9. package/__tests__/social-bilibili-snapshot.test.js +278 -0
  10. package/__tests__/social-douyin-snapshot.test.js +253 -0
  11. package/__tests__/social-kuaishou-snapshot.test.js +309 -0
  12. package/__tests__/social-toutiao-snapshot.test.js +314 -0
  13. package/__tests__/social-weibo-snapshot.test.js +234 -0
  14. package/__tests__/social-xiaohongshu-snapshot.test.js +232 -0
  15. package/__tests__/travel-maps-snapshot.test.js +426 -0
  16. package/__tests__/vault-driver-error.test.js +74 -0
  17. package/__tests__/wechat-adapter.test.js +118 -0
  18. package/lib/adapters/messaging-qq/index.js +498 -92
  19. package/lib/adapters/shopping-jd/index.js +228 -25
  20. package/lib/adapters/shopping-meituan/index.js +222 -26
  21. package/lib/adapters/shopping-pinduoduo/index.js +275 -0
  22. package/lib/adapters/social-bilibili/adapter.js +500 -0
  23. package/lib/adapters/social-bilibili/index.js +21 -169
  24. package/lib/adapters/social-douyin/index.js +454 -63
  25. package/lib/adapters/social-kuaishou/index.js +379 -127
  26. package/lib/adapters/social-toutiao/index.js +400 -130
  27. package/lib/adapters/social-weibo/index.js +393 -95
  28. package/lib/adapters/social-xiaohongshu/index.js +389 -49
  29. package/lib/adapters/travel-baidu-map/index.js +286 -26
  30. package/lib/adapters/travel-tencent-map/index.js +414 -0
  31. package/lib/adapters/wechat/content-parser.js +11 -2
  32. package/lib/adapters/wechat/db-reader.js +88 -10
  33. package/lib/adapters/wechat/frida-agent/loader.js +7 -0
  34. package/lib/adapters/wechat/frida-agent/wechat-key-hook.js +140 -18
  35. package/lib/adapters/wechat/key-providers/frida-key-provider.js +8 -0
  36. package/lib/adapters/wechat/normalize.js +12 -3
  37. package/lib/index.js +5 -1
  38. package/lib/vault.js +60 -8
  39. package/package.json +2 -1
@@ -32,7 +32,10 @@
32
32
  "use strict";
33
33
 
34
34
  (function () {
35
- var TARGET_MODULE = "libwcdb.so";
35
+ // sjqz-verified module name is `libWCDB.so` (uppercase); some WeChat
36
+ // builds ship lowercase. Try both — first match wins, no extra cost
37
+ // because Process.findModuleByName is a cheap lookup.
38
+ var TARGET_MODULES = ["libWCDB.so", "libwcdb.so"];
36
39
  // Primary symbol per §18.3. Add fallbacks below — version drift will
37
40
  // shift the export name; host treats first hit as authoritative.
38
41
  var SYMBOLS = [
@@ -60,63 +63,182 @@
60
63
  // the host detaches quickly (anti-detection §18.6 #4).
61
64
  var fired = false;
62
65
 
66
+ // Sig-aware arg index map. The host treats the first 'key' event as
67
+ // authoritative, so picking the wrong index for v2 = host gets the
68
+ // database NAME pointer (e.g. "main") and DB opens fail silently.
69
+ // sqlite3_key(sqlite3 *db, const void *pKey, int nKey)
70
+ // args[0]=db, args[1]=key, args[2]=len
71
+ // sqlite3_key_v2(sqlite3 *db, const char *zDbName, const void *pKey, int nKey)
72
+ // args[0]=db, args[1]=name, args[2]=key, args[3]=len
73
+ // wcdb_setkey / WCDBKeyDerive: unknown sig — assume sqlite3_key shape
74
+ // Mangled C++: WCDB::Database::setCipherKey(*this, const std::string&)
75
+ // args[0]=this, args[1]=&string (length needs .size()) — not handled
76
+ // here; emit error so the host falls back to MD5 path.
77
+ function argIndicesFor(symbolName) {
78
+ if (symbolName === "sqlite3_key_v2") {
79
+ return { key: 2, len: 3, sig: "v2" };
80
+ }
81
+ if (symbolName.indexOf("_ZN4WCDB") === 0) {
82
+ return { key: -1, len: -1, sig: "mangled-cpp" };
83
+ }
84
+ return { key: 1, len: 2, sig: "v1" };
85
+ }
86
+
87
+ // sjqz extract_wechat_key.py uses Memory.readCString(args[1]) for the
88
+ // key — meaning some WeChat builds pass the key as a NUL-terminated
89
+ // 64-char ASCII hex string. Other builds (and the original SQLCipher
90
+ // contract) pass 32 raw bytes. We can disambiguate by `len`:
91
+ // - len === 32 → raw 32-byte key → readByteArray + bytesToHex
92
+ // - len === 64 → ASCII hex string → readCString
93
+ // - anything else → emit error, host falls back to MD5 path
63
94
  function makeHook(symbolName) {
95
+ var idx = argIndicesFor(symbolName);
64
96
  return {
65
97
  onEnter: function (args) {
66
98
  if (fired) return;
99
+ if (idx.key < 0) {
100
+ send({
101
+ kind: "error",
102
+ message:
103
+ "unsupported symbol signature: " +
104
+ symbolName +
105
+ " — host should fall back to MD5(IMEI+UIN) key path",
106
+ });
107
+ return;
108
+ }
67
109
  try {
68
- // sqlite3_key signature: int sqlite3_key(sqlite3 *db, const void *pKey, int nKey)
69
- // args[1] = key bytes, args[2] = key length
70
- var len = args[2].toInt32();
110
+ var len = args[idx.len].toInt32();
71
111
  if (len <= 0 || len > 256) {
72
- send({ kind: "error", message: "implausible key length " + len + " at " + symbolName });
112
+ send({
113
+ kind: "error",
114
+ message:
115
+ "implausible key length " + len + " at " + symbolName,
116
+ });
117
+ return;
118
+ }
119
+ var hex;
120
+ var format;
121
+ if (len === 64) {
122
+ // ASCII hex string (sjqz-verified path on WeChat 7.x/8.0 libWCDB)
123
+ var s = Memory.readCString(args[idx.key], len);
124
+ if (!s || s.length === 0) {
125
+ send({
126
+ kind: "error",
127
+ message: "readCString returned empty at " + symbolName,
128
+ });
129
+ return;
130
+ }
131
+ hex = s.toLowerCase();
132
+ format = "ascii-hex";
133
+ } else if (len === 32) {
134
+ // Raw 32-byte key — convert to 64-char hex
135
+ var buf = args[idx.key].readByteArray(len);
136
+ hex = bytesToHex(buf);
137
+ format = "raw-bytes";
138
+ } else {
139
+ // Ambiguous length — could be either. Emit both interpretations
140
+ // and let the host try each against the DB until one succeeds.
141
+ var bufAmb = args[idx.key].readByteArray(len);
142
+ var hexFromBytes = bytesToHex(bufAmb);
143
+ var hexFromString = null;
144
+ try {
145
+ var sAmb = Memory.readCString(args[idx.key], len);
146
+ if (sAmb) hexFromString = sAmb.toLowerCase();
147
+ } catch (_e) {
148
+ // readCString may fault on non-NUL-terminated bytes; ignore.
149
+ }
150
+ fired = true;
151
+ send({
152
+ kind: "key",
153
+ hex: hexFromBytes,
154
+ alt: hexFromString,
155
+ source: symbolName,
156
+ sig: idx.sig,
157
+ format: "ambiguous",
158
+ length: len,
159
+ });
73
160
  return;
74
161
  }
75
- var buf = args[1].readByteArray(len);
76
- var hex = bytesToHex(buf);
77
162
  if (!hex) {
78
- send({ kind: "error", message: "empty key buffer at " + symbolName });
163
+ send({
164
+ kind: "error",
165
+ message: "empty key buffer at " + symbolName,
166
+ });
79
167
  return;
80
168
  }
81
169
  fired = true;
82
- send({ kind: "key", hex: hex, source: symbolName });
170
+ send({
171
+ kind: "key",
172
+ hex: hex,
173
+ source: symbolName,
174
+ sig: idx.sig,
175
+ format: format,
176
+ length: len,
177
+ });
83
178
  } catch (e) {
84
- send({ kind: "error", message: "hook exception at " + symbolName + ": " + (e && e.message ? e.message : String(e)) });
179
+ send({
180
+ kind: "error",
181
+ message:
182
+ "hook exception at " +
183
+ symbolName +
184
+ ": " +
185
+ (e && e.message ? e.message : String(e)),
186
+ });
85
187
  }
86
188
  },
87
189
  };
88
190
  }
89
191
 
90
- function tryAttach() {
91
- var mod = Process.findModuleByName(TARGET_MODULE);
192
+ function tryAttachOnModule(moduleName) {
193
+ var mod = Process.findModuleByName(moduleName);
92
194
  if (!mod) return false;
93
195
  var attached = 0;
94
196
  for (var i = 0; i < SYMBOLS.length; i++) {
95
- var addr = Module.findExportByName(TARGET_MODULE, SYMBOLS[i]);
197
+ var addr = Module.findExportByName(moduleName, SYMBOLS[i]);
96
198
  if (!addr) continue;
97
199
  try {
98
200
  Interceptor.attach(addr, makeHook(SYMBOLS[i]));
99
- send({ kind: "hooked", symbol: SYMBOLS[i], module: TARGET_MODULE });
201
+ send({ kind: "hooked", symbol: SYMBOLS[i], module: moduleName });
100
202
  attached++;
101
203
  } catch (e) {
102
- send({ kind: "error", message: "Interceptor.attach failed for " + SYMBOLS[i] + ": " + (e && e.message ? e.message : String(e)) });
204
+ send({
205
+ kind: "error",
206
+ message:
207
+ "Interceptor.attach failed for " +
208
+ SYMBOLS[i] +
209
+ ": " +
210
+ (e && e.message ? e.message : String(e)),
211
+ });
103
212
  }
104
213
  }
105
214
  return attached > 0;
106
215
  }
107
216
 
217
+ function tryAttach() {
218
+ for (var i = 0; i < TARGET_MODULES.length; i++) {
219
+ if (tryAttachOnModule(TARGET_MODULES[i])) {
220
+ return true;
221
+ }
222
+ }
223
+ return false;
224
+ }
225
+
108
226
  // Module-load polling — §18.6 #1 "hook at module-load time before
109
- // anti-detection thread runs". WeChat lazy-loads libwcdb when the
227
+ // anti-detection thread runs". WeChat lazy-loads libWCDB when the
110
228
  // first DB opens, so we can't always find it at script start.
111
229
  if (!tryAttach()) {
112
- send({ kind: "module-waiting", module: TARGET_MODULE });
230
+ send({ kind: "module-waiting", module: TARGET_MODULES.join("|") });
113
231
  var attempts = 0;
114
232
  var poll = function () {
115
233
  attempts++;
116
234
  if (tryAttach()) return;
117
235
  if (attempts >= 60) {
118
236
  // 60 attempts × 500ms = 30s ceiling, matches host timeoutMs
119
- send({ kind: "error", message: TARGET_MODULE + " did not load within 30s" });
237
+ send({
238
+ kind: "error",
239
+ message:
240
+ TARGET_MODULES.join("|") + " did not load within 30s",
241
+ });
120
242
  return;
121
243
  }
122
244
  setTimeout(poll, 500);
@@ -191,6 +191,14 @@ class FridaKeyProvider extends KeyProvider {
191
191
  if (evt.kind === "key") {
192
192
  settled = true;
193
193
  telemetry.keySource = evt.source;
194
+ // Phase 12.6 (post-sjqz audit) — capture sig/format/length so a
195
+ // failed DB open can be diagnosed: ascii-hex vs raw-bytes
196
+ // determines whether sqlite3_key got the expected key bytes,
197
+ // and sig=v1/v2 confirms args index resolution.
198
+ telemetry.keyFormat = evt.format || null;
199
+ telemetry.keySig = evt.sig || null;
200
+ telemetry.keyLength = evt.length || null;
201
+ telemetry.keyAlt = evt.alt || null;
194
202
  telemetry.durationMs = Date.now() - telemetry.startedAt;
195
203
  cleanup().then(() => resolve(String(evt.hex || "").toLowerCase()));
196
204
  return;
@@ -203,11 +203,20 @@ function contactDisplayName(byUsername, wxid) {
203
203
  function guessContactSubtype(row) {
204
204
  // rcontact.type bits: official accounts / group / regular contact /
205
205
  // black list. Detailed mapping in WeChat reverse-eng community —
206
- // for v0.5 we keep it simple: anything that's not the user's self is
207
- // "contact". Phase 12.6 will refine with full bit mapping.
208
- if (typeof row.username === "string" && row.username.endsWith("@chatroom")) {
206
+ // for v0.5 we keep it simple: chatroom unknown (not a Person),
207
+ // `gh_*` username merchant (公众号 / Official Account — brand /
208
+ // business pushing content; closest enum match), rest → contact.
209
+ // Phase 12.6 will refine with full bit mapping + rcontact.type bits.
210
+ // (sjqz parity wechat.py:282 — get_friends() excludes gh_* from
211
+ // friends view but keeps them in contacts; we keep as Person with
212
+ // distinct subtype so Ask flow / EntityResolver can filter cleanly.)
213
+ if (typeof row.username !== "string") return "contact";
214
+ if (row.username.endsWith("@chatroom")) {
209
215
  return "unknown"; // chat group, not a Person
210
216
  }
217
+ if (row.username.startsWith("gh_")) {
218
+ return "merchant"; // 公众号 / Official Account
219
+ }
211
220
  return "contact";
212
221
  }
213
222
 
package/lib/index.js CHANGED
@@ -39,10 +39,12 @@ const { Train12306Adapter } = require("./adapters/travel-12306");
39
39
  const { CtripAdapter } = require("./adapters/travel-ctrip");
40
40
  const { AmapAdapter } = require("./adapters/travel-amap");
41
41
  const { BaiduMapAdapter } = require("./adapters/travel-baidu-map");
42
+ const { TencentMapAdapter } = require("./adapters/travel-tencent-map");
42
43
  const shoppingBase = require("./adapters/shopping-base");
43
44
  const { TaobaoAdapter } = require("./adapters/shopping-taobao");
44
45
  const { JdAdapter } = require("./adapters/shopping-jd");
45
46
  const { MeituanAdapter } = require("./adapters/shopping-meituan");
47
+ const { PinduoduoAdapter } = require("./adapters/shopping-pinduoduo");
46
48
  const { BilibiliAdapter } = require("./adapters/social-bilibili");
47
49
  const { WeiboAdapter } = require("./adapters/social-weibo");
48
50
  const { DouyinAdapter } = require("./adapters/social-douyin");
@@ -221,13 +223,14 @@ module.exports = {
221
223
  wxidToWeChatPersonId: wechatAdapter.wxidToWeChatPersonId,
222
224
  WECHAT_PRAGMA_PROFILES: wechatAdapter.WECHAT_PRAGMA_PROFILES,
223
225
 
224
- // Phase 9 — Travel four-pack
226
+ // Phase 9 + §2.5b 地图三联 — Travel five-pack
225
227
  normalizeTravelRecord: travelBase.normalizeTravelRecord,
226
228
  parseChineseDateTime: travelBase.parseChineseDateTime,
227
229
  Train12306Adapter,
228
230
  CtripAdapter,
229
231
  AmapAdapter,
230
232
  BaiduMapAdapter,
233
+ TencentMapAdapter,
231
234
 
232
235
  // Phase 7 — Shopping three-pack
233
236
  normalizeOrderRecord: shoppingBase.normalizeOrderRecord,
@@ -235,6 +238,7 @@ module.exports = {
235
238
  TaobaoAdapter,
236
239
  JdAdapter,
237
240
  MeituanAdapter,
241
+ PinduoduoAdapter,
238
242
 
239
243
  // Phase 13+ — long-tail social + messaging (借 sjqz parsers)
240
244
  BilibiliAdapter,
package/lib/vault.js CHANGED
@@ -44,19 +44,71 @@ function newGroupId() {
44
44
  return `mg-${r()}${r()}-${Date.now().toString(36)}`;
45
45
  }
46
46
 
47
+ /**
48
+ * Translate a bs3mc load-failure error into an actionable, user-readable
49
+ * message. Detects NODE_MODULE_VERSION mismatch (the single most common
50
+ * failure: Node 23/24/25 has no prebuild — bs3mc upstream only ships for
51
+ * Node LTS ABIs 108/115/127). See memory `node_23_native_dep_trap.md`.
52
+ *
53
+ * Pure function so it can be unit-tested without poisoning require cache.
54
+ *
55
+ * @param {Error|unknown} err Original throw from `require("better-sqlite3-multiple-ciphers")`.
56
+ * @param {string} [nodeVer] process.versions.node (override for tests).
57
+ * @returns {Error} Wrapped Error with `cause` and (when ABI-related) `code: "BS3MC_ABI_MISMATCH"`.
58
+ */
59
+ function formatDriverLoadError(err, nodeVer) {
60
+ const originalMsg = err && err.message ? err.message : String(err);
61
+ const runtimeNodeVer = nodeVer || process.versions.node;
62
+
63
+ const abiMatch = originalMsg.match(
64
+ /NODE_MODULE_VERSION\s+(\d+)[\s\S]+?requires\s+NODE_MODULE_VERSION\s+(\d+)/,
65
+ );
66
+ if (abiMatch) {
67
+ const compiledAbi = abiMatch[1];
68
+ const runtimeAbi = abiMatch[2];
69
+ const lines = [
70
+ "better-sqlite3-multiple-ciphers ABI mismatch — Node " +
71
+ runtimeNodeVer +
72
+ " has ABI " +
73
+ runtimeAbi +
74
+ " but bs3mc prebuild is ABI " +
75
+ compiledAbi +
76
+ ".",
77
+ "",
78
+ "修法(任选其一):",
79
+ " 1. 切 Node 22 LTS (推荐) — nvm-windows: `nvm install 22.12.0 && nvm use 22.12.0`",
80
+ " 2. 源码重编 — `npm rebuild better-sqlite3-multiple-ciphers --build-from-source`",
81
+ " (需要本机有 Visual Studio Build Tools / node-gyp toolchain,慢且不推荐)",
82
+ "",
83
+ "为什么 bs3mc 没 ABI " + runtimeAbi + " prebuild:",
84
+ " bs3mc 上游只 ship 主流 Node LTS 的 prebuild (ABI 108/115/127)。",
85
+ " Node 23/24/25 是 Current 系列,上游不给 prebuild。",
86
+ "",
87
+ "项目 engines.node 允许 >=22.12 是为了兼容未来 LTS,但实际推荐 22.x。",
88
+ ];
89
+ const wrapped = new Error(lines.join("\n"));
90
+ wrapped.cause = err;
91
+ wrapped.code = "BS3MC_ABI_MISMATCH";
92
+ return wrapped;
93
+ }
94
+
95
+ const wrapped = new Error(
96
+ "Failed to load better-sqlite3-multiple-ciphers. " +
97
+ "Install it as a workspace dep or pin the version in your package. " +
98
+ "Original error: " +
99
+ originalMsg,
100
+ );
101
+ wrapped.cause = err;
102
+ return wrapped;
103
+ }
104
+
47
105
  function loadDriver() {
48
106
  // Lazy require so consumers that only need schemas don't pay for the
49
107
  // native binding load. Errors surface here with a precise message.
50
108
  try {
51
109
  return require("better-sqlite3-multiple-ciphers");
52
110
  } catch (err) {
53
- const msg =
54
- "Failed to load better-sqlite3-multiple-ciphers. " +
55
- "Install it as a workspace dep or pin the version in your package. " +
56
- "Original error: " + (err && err.message ? err.message : String(err));
57
- const wrapped = new Error(msg);
58
- wrapped.cause = err;
59
- throw wrapped;
111
+ throw formatDriverLoadError(err);
60
112
  }
61
113
  }
62
114
 
@@ -1223,4 +1275,4 @@ class LocalVault {
1223
1275
  }
1224
1276
  }
1225
1277
 
1226
- module.exports = { LocalVault };
1278
+ module.exports = { LocalVault, _internal: { loadDriver, formatDriverLoadError } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chainlesschain/personal-data-hub",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Personal Data Hub — UnifiedSchema + validators + KG ingest helpers for the data-back-to-the-individual middleware",
5
5
  "type": "commonjs",
6
6
  "main": "lib/index.js",
@@ -46,6 +46,7 @@
46
46
  "./adapters/travel-ctrip": "./lib/adapters/travel-ctrip/index.js",
47
47
  "./adapters/travel-amap": "./lib/adapters/travel-amap/index.js",
48
48
  "./adapters/travel-baidu-map": "./lib/adapters/travel-baidu-map/index.js",
49
+ "./adapters/travel-tencent-map": "./lib/adapters/travel-tencent-map/index.js",
49
50
  "./adapters/shopping-base": "./lib/adapters/shopping-base/index.js",
50
51
  "./adapters/shopping-taobao": "./lib/adapters/shopping-taobao/index.js",
51
52
  "./adapters/shopping-jd": "./lib/adapters/shopping-jd/index.js",