@chainlesschain/personal-data-hub 0.4.29 → 0.4.31
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/lib/forensics/qq-nt-collect.js +190 -0
- package/lib/prompt-builder.js +15 -1
- package/package.json +8 -3
- package/__tests__/adapter-guide.test.js +0 -47
- package/__tests__/adapter-spec.test.js +0 -78
- package/__tests__/adapters/ai-chat-cookie-capture-spec.test.js +0 -211
- package/__tests__/adapters/ai-chat-health-checker.test.js +0 -262
- package/__tests__/adapters/ai-chat-history.test.js +0 -396
- package/__tests__/adapters/ai-chat-http-client.test.js +0 -242
- package/__tests__/adapters/ai-chat-vendors.test.js +0 -874
- package/__tests__/adapters/alipay-bill-adapter.test.js +0 -538
- package/__tests__/adapters/apple-health.test.js +0 -95
- package/__tests__/adapters/bank-family.test.js +0 -125
- package/__tests__/adapters/biz-tianyancha.test.js +0 -159
- package/__tests__/adapters/browser-history-chrome.test.js +0 -377
- package/__tests__/adapters/browser-history-edge.test.js +0 -159
- package/__tests__/adapters/car-mercedesme.test.js +0 -74
- package/__tests__/adapters/doc-baidu-netdisk.test.js +0 -102
- package/__tests__/adapters/doc-camscanner.test.js +0 -147
- package/__tests__/adapters/doc-platforms.test.js +0 -177
- package/__tests__/adapters/edu-huawei-learning-live.test.js +0 -198
- package/__tests__/adapters/edu-zuoyebang-live.test.js +0 -226
- package/__tests__/adapters/email-adapter-snapshot.test.js +0 -237
- package/__tests__/adapters/email-adapter.test.js +0 -742
- package/__tests__/adapters/email-classifier.test.js +0 -347
- package/__tests__/adapters/email-imap-session.test.js +0 -334
- package/__tests__/adapters/email-parser.test.js +0 -244
- package/__tests__/adapters/email-pdf-extractor.test.js +0 -529
- package/__tests__/adapters/email-providers.test.js +0 -84
- package/__tests__/adapters/email-retry-progress.test.js +0 -294
- package/__tests__/adapters/email-templates.test.js +0 -822
- package/__tests__/adapters/family-23-collectors-scaffold.test.js +0 -182
- package/__tests__/adapters/finance-alipay-live.test.js +0 -258
- package/__tests__/adapters/finance-dcep.test.js +0 -74
- package/__tests__/adapters/fitness-joyrun.test.js +0 -82
- package/__tests__/adapters/game-genshin-live.test.js +0 -238
- package/__tests__/adapters/game-genshin-scaffold.test.js +0 -108
- package/__tests__/adapters/game-honor-of-kings-live.test.js +0 -230
- package/__tests__/adapters/git-activity.test.js +0 -222
- package/__tests__/adapters/gov-12123.test.js +0 -103
- package/__tests__/adapters/gov-ixiamen.test.js +0 -150
- package/__tests__/adapters/gov-tax.test.js +0 -135
- package/__tests__/adapters/health-meiyou.test.js +0 -125
- package/__tests__/adapters/local-files.test.js +0 -264
- package/__tests__/adapters/local-im-pc.test.js +0 -154
- package/__tests__/adapters/messaging-whatsapp.test.js +0 -289
- package/__tests__/adapters/music-kugou.test.js +0 -187
- package/__tests__/adapters/music-qq.test.js +0 -112
- package/__tests__/adapters/netease-music-live.test.js +0 -244
- package/__tests__/adapters/netease-music.test.js +0 -74
- package/__tests__/adapters/pc-local-discovery.test.js +0 -141
- package/__tests__/adapters/qq-pc-direct-read.test.js +0 -227
- package/__tests__/adapters/reading-family.test.js +0 -108
- package/__tests__/adapters/recruit-boss.test.js +0 -180
- package/__tests__/adapters/shell-history.test.js +0 -180
- package/__tests__/adapters/shopping-base.test.js +0 -179
- package/__tests__/adapters/shopping-dianping.test.js +0 -239
- package/__tests__/adapters/social-bilibili-adb-api-client.test.js +0 -721
- package/__tests__/adapters/social-bilibili-adb-chromium-cookies-reader.test.js +0 -346
- package/__tests__/adapters/social-bilibili-adb-collector.test.js +0 -284
- package/__tests__/adapters/social-bilibili-adb-cookies-extension.test.js +0 -343
- package/__tests__/adapters/social-bilibili-adb-snapshot-builder.test.js +0 -296
- package/__tests__/adapters/social-csdn.test.js +0 -175
- package/__tests__/adapters/social-dongchedi.test.js +0 -165
- package/__tests__/adapters/social-douyin-adb-aweme-detail.test.js +0 -165
- package/__tests__/adapters/social-douyin-adb-collector.test.js +0 -254
- package/__tests__/adapters/social-douyin-adb-db-extension.test.js +0 -114
- package/__tests__/adapters/social-douyin-adb-im-db-parser.test.js +0 -304
- package/__tests__/adapters/social-douyin-adb-snapshot-builder.test.js +0 -216
- package/__tests__/adapters/social-douyin-adb-usage-profile.test.js +0 -229
- package/__tests__/adapters/social-douyin-adb-watch-history.test.js +0 -269
- package/__tests__/adapters/social-kuaishou-adb-api-client.test.js +0 -496
- package/__tests__/adapters/social-kuaishou-adb-collector.test.js +0 -276
- package/__tests__/adapters/social-kuaishou-adb-cookies-extension.test.js +0 -152
- package/__tests__/adapters/social-kuaishou-adb-snapshot-builder.test.js +0 -178
- package/__tests__/adapters/social-toutiao-adb-account-reader.test.js +0 -135
- package/__tests__/adapters/social-toutiao-adb-api-client.test.js +0 -626
- package/__tests__/adapters/social-toutiao-adb-article.test.js +0 -155
- package/__tests__/adapters/social-toutiao-adb-collector.test.js +0 -378
- package/__tests__/adapters/social-toutiao-adb-cookies-extension.test.js +0 -193
- package/__tests__/adapters/social-toutiao-adb-snapshot-builder.test.js +0 -196
- package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +0 -311
- package/__tests__/adapters/social-weibo-adb-api-client.test.js +0 -362
- package/__tests__/adapters/social-weibo-adb-collector.test.js +0 -201
- package/__tests__/adapters/social-weibo-adb-cookies-extension.test.js +0 -167
- package/__tests__/adapters/social-weibo-adb-snapshot-builder.test.js +0 -189
- package/__tests__/adapters/social-xiaohongshu-adb-api-client.test.js +0 -431
- package/__tests__/adapters/social-xiaohongshu-adb-collector.test.js +0 -207
- package/__tests__/adapters/social-xiaohongshu-adb-cookies-extension.test.js +0 -0
- package/__tests__/adapters/social-xiaohongshu-adb-sign-provider-injection.test.js +0 -351
- package/__tests__/adapters/social-xiaohongshu-adb-sign.test.js +0 -130
- package/__tests__/adapters/social-xiaohongshu-adb-snapshot-builder.test.js +0 -200
- package/__tests__/adapters/social-zhihu.test.js +0 -246
- package/__tests__/adapters/system-data-adapter.test.js +0 -443
- package/__tests__/adapters/system-data-android-ingest.test.js +0 -144
- package/__tests__/adapters/system-data-android.test.js +0 -519
- package/__tests__/adapters/system-data-disclosure.test.js +0 -153
- package/__tests__/adapters/travel-12306.test.js +0 -512
- package/__tests__/adapters/travel-amap.test.js +0 -219
- package/__tests__/adapters/travel-baidu-map.test.js +0 -305
- package/__tests__/adapters/travel-base.test.js +0 -205
- package/__tests__/adapters/travel-ctrip.test.js +0 -377
- package/__tests__/adapters/travel-didi-consumer.test.js +0 -66
- package/__tests__/adapters/travel-didi.test.js +0 -204
- package/__tests__/adapters/travel-tencent-map.test.js +0 -207
- package/__tests__/adapters/travel-tongcheng.test.js +0 -289
- package/__tests__/adapters/video-platforms.test.js +0 -152
- package/__tests__/adapters/video-xigua.test.js +0 -106
- package/__tests__/adapters/vscode.test.js +0 -299
- package/__tests__/adapters/wechat-bootstrap.test.js +0 -240
- package/__tests__/adapters/wechat-env-probe.test.js +0 -162
- package/__tests__/adapters/wechat-frida-agent.test.js +0 -322
- package/__tests__/adapters/wechat-frida-integration.test.js +0 -149
- package/__tests__/adapters/wechat-frida-key-provider.test.js +0 -188
- package/__tests__/adapters/wechat-md5-key-provider.test.js +0 -101
- package/__tests__/adapters/wechat-pc-direct-read.test.js +0 -365
- package/__tests__/adapters/wechat-pc-group-topic.test.js +0 -63
- package/__tests__/adapters/wechat-pc-v4-sidecar.test.js +0 -72
- package/__tests__/adapters/weread.test.js +0 -123
- package/__tests__/adapters/wework-pc.test.js +0 -124
- package/__tests__/adapters/win-recent.test.js +0 -192
- package/__tests__/analysis-skills.test.js +0 -754
- package/__tests__/analysis.test.js +0 -1845
- package/__tests__/audio-ximalaya-snapshot.test.js +0 -279
- package/__tests__/batch.test.js +0 -133
- package/__tests__/bridges-cc-kg.test.js +0 -231
- package/__tests__/bridges-cc-llm.test.js +0 -191
- package/__tests__/bridges-cc-rag.test.js +0 -162
- package/__tests__/categories.test.js +0 -92
- package/__tests__/e2e/ai-chat-cross-source-journey.test.js +0 -213
- package/__tests__/e2e/full-user-journey.test.js +0 -188
- package/__tests__/e2e/local-data-adapters-cli.e2e.test.js +0 -146
- package/__tests__/entity-resolver-ingest-hook.test.js +0 -177
- package/__tests__/entity-resolver-stages.test.js +0 -411
- package/__tests__/entity-resolver-vault.test.js +0 -249
- package/__tests__/entity-resolver.test.js +0 -526
- package/__tests__/fitness-keep-snapshot.test.js +0 -224
- package/__tests__/fixtures/entity-resolver-200-mock.json +0 -96
- package/__tests__/ids.test.js +0 -45
- package/__tests__/integration/ai-chat-history-registry.test.js +0 -228
- package/__tests__/integration/aichat-wizard-end-to-end.test.js +0 -282
- package/__tests__/integration/cross-adapter-pipelines.test.js +0 -396
- package/__tests__/integration/local-data-adapters-pipeline.test.js +0 -373
- package/__tests__/integration/social-bilibili-pipeline.test.js +0 -261
- package/__tests__/integration/wechat-bootstrap-end-to-end.test.js +0 -390
- package/__tests__/key-providers.test.js +0 -126
- package/__tests__/kg-derive.test.js +0 -219
- package/__tests__/llm-client.test.js +0 -122
- package/__tests__/longtail-adapters.test.js +0 -281
- package/__tests__/messaging-qq-snapshot.test.js +0 -294
- package/__tests__/mobile-extractor-encrypted.test.js +0 -460
- package/__tests__/mobile-extractor.test.js +0 -288
- package/__tests__/mock-adapter.test.js +0 -93
- package/__tests__/prompt-builder.test.js +0 -249
- package/__tests__/query-parser.test.js +0 -365
- package/__tests__/rag-derive.test.js +0 -169
- package/__tests__/registry-readiness.test.js +0 -292
- package/__tests__/registry.test.js +0 -420
- package/__tests__/salvage-ingest.test.js +0 -97
- package/__tests__/schemas.test.js +0 -331
- package/__tests__/shopping-adapters.test.js +0 -392
- package/__tests__/shopping-eleme-snapshot.test.js +0 -454
- package/__tests__/shopping-pinduoduo-snapshot.test.js +0 -484
- package/__tests__/shopping-snapshot.test.js +0 -438
- package/__tests__/shopping-vipshop-snapshot.test.js +0 -425
- package/__tests__/shopping-xianyu-snapshot.test.js +0 -451
- package/__tests__/sidecar-contacts-cross-validate.test.js +0 -186
- package/__tests__/sidecar-supervisor.test.js +0 -128
- package/__tests__/sign-providers.test.js +0 -62
- package/__tests__/social-adapters.test.js +0 -280
- package/__tests__/social-bilibili-snapshot.test.js +0 -278
- package/__tests__/social-douban-snapshot.test.js +0 -351
- package/__tests__/social-douyin-im-direct-read.test.js +0 -377
- package/__tests__/social-douyin-salvage-collector.test.js +0 -98
- package/__tests__/social-douyin-salvage-mapper.test.js +0 -90
- package/__tests__/social-douyin-snapshot.test.js +0 -256
- package/__tests__/social-kuaishou-snapshot.test.js +0 -362
- package/__tests__/social-toutiao-snapshot.test.js +0 -366
- package/__tests__/social-weibo-snapshot.test.js +0 -234
- package/__tests__/social-weibo-sqlite-device.test.js +0 -174
- package/__tests__/social-xiaohongshu-snapshot.test.js +0 -232
- package/__tests__/sqlite-leaf-salvage.test.js +0 -97
- package/__tests__/travel-adapters.test.js +0 -483
- package/__tests__/travel-maps-snapshot.test.js +0 -426
- package/__tests__/vault-driver-error.test.js +0 -74
- package/__tests__/vault-search-helpers.test.js +0 -104
- package/__tests__/vault-search.test.js +0 -423
- package/__tests__/vault.test.js +0 -767
- package/__tests__/wechat-adapter.test.js +0 -594
- package/__tests__/whatsapp-adapter.test.js +0 -138
- package/scripts/_make-fixture-all.js +0 -126
- package/scripts/_make-fixture-contacts.js +0 -84
- package/scripts/evaluate-entity-resolver.js +0 -213
- package/scripts/run-native-tests-sandbox.sh +0 -55
- package/scripts/smoke-phase-5-5.js +0 -196
- package/scripts/smoke-phase-5-7.js +0 -181
- package/scripts/smoke-system-data-contacts.js +0 -309
- package/scripts/smoke-system-data.js +0 -312
- package/vitest.config.js +0 -88
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* qq-nt-collect — on-device-ready QQNT (nt_msg.db) decrypt + protobuf-parse.
|
|
4
|
+
*
|
|
5
|
+
* Pure Node (crypto + a caller-provided better-sqlite3 ctor) — NO frida, NO adb.
|
|
6
|
+
* This is the bundle-shipped core behind `cc hub collect-qq`, which the Android
|
|
7
|
+
* `CollectQqNativeTool` invokes after su-staging the encrypted DB + uid list. The
|
|
8
|
+
* exact same logic runs on PC (USB pull) and on-device (app su-read).
|
|
9
|
+
*
|
|
10
|
+
* Method (key DERIVED, no frida): `key = MD5(MD5(uid) + rand)` → SQLCipher
|
|
11
|
+
* PBKDF2-HMAC-SHA512/1 (iter 4000) → AES-256-CBC. `rand` is read from the 1024-byte
|
|
12
|
+
* plaintext header; `uid` (QQNT `u_...`) is brute-forced from candidates — page 1
|
|
13
|
+
* decrypting to a valid SQLite header identifies the self uid. (See
|
|
14
|
+
* scripts/android/pdh-qq-android-decrypt.mjs for the standalone PC tool this was
|
|
15
|
+
* extracted from; behaviour is byte-identical.)
|
|
16
|
+
*/
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
const md5 = (s) => crypto.createHash('md5').update(s).digest('hex');
|
|
19
|
+
|
|
20
|
+
/** Read `rand` (protobuf field after "QQ_NT DB") from the plaintext header. */
|
|
21
|
+
function extractRand(raw) {
|
|
22
|
+
const head = raw.subarray(0, 256).toString('latin1');
|
|
23
|
+
const i = head.indexOf('QQ_NT DB');
|
|
24
|
+
if (i < 0) return null;
|
|
25
|
+
const m = head.slice(i + 8, i + 40).match(/[A-Za-z0-9]{6,12}/);
|
|
26
|
+
return m ? m[0] : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Detect HMAC algo named in the header (informational). */
|
|
30
|
+
function headerHmac(raw) {
|
|
31
|
+
return (/HMAC_SHA\d+/.exec(raw.subarray(0, 256).toString('latin1')) || [])[0] || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// decrypt one page slice. p1=true → page 1 (first 16 bytes = salt, skipped).
|
|
35
|
+
function decPage(page, encKey, reserve, ivOff, p1) {
|
|
36
|
+
const ro = page.length - reserve;
|
|
37
|
+
const ct = page.subarray(p1 ? 16 : 0, ro);
|
|
38
|
+
const iv = page.subarray(ro + ivOff, ro + ivOff + 16);
|
|
39
|
+
if (iv.length < 16) return null;
|
|
40
|
+
try {
|
|
41
|
+
const d = crypto.createDecipheriv('aes-256-cbc', encKey, iv);
|
|
42
|
+
d.setAutoPadding(false);
|
|
43
|
+
return Buffer.concat([d.update(ct), d.final()]);
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const validHdr = (pt) => pt && pt.length > 8 && pt[5] === 64 && pt[6] === 32 && pt[7] === 32;
|
|
49
|
+
const CFG = [[4096, 48, 0], [4096, 80, 0], [4096, 48, 20]];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Brute the key over uid candidates, then decrypt every page.
|
|
53
|
+
* @returns {{decrypted: Buffer, uid: string, kdf: string}|null} null = no uid matched.
|
|
54
|
+
*/
|
|
55
|
+
function deriveAndDecrypt(raw, uids, rand) {
|
|
56
|
+
if (!raw || raw.length <= 1024 || !rand || !uids || !uids.length) return null;
|
|
57
|
+
const body = raw.subarray(1024);
|
|
58
|
+
const salt = body.subarray(0, 16);
|
|
59
|
+
let hit = null;
|
|
60
|
+
for (const uid of uids) {
|
|
61
|
+
const pass = md5(md5(uid) + rand);
|
|
62
|
+
for (const algo of ['sha512', 'sha1']) {
|
|
63
|
+
const ek = crypto.pbkdf2Sync(Buffer.from(pass, 'utf8'), salt, 4000, 32, algo);
|
|
64
|
+
for (const [pg, rs, iv] of CFG) {
|
|
65
|
+
if (validHdr(decPage(body.subarray(0, pg), ek, rs, iv, true))) {
|
|
66
|
+
hit = { uid, algo, page: pg, reserve: rs, ivOff: iv, ek };
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (hit) break;
|
|
71
|
+
}
|
|
72
|
+
if (hit) break;
|
|
73
|
+
}
|
|
74
|
+
if (!hit) return null;
|
|
75
|
+
const { ek, page, reserve, ivOff } = hit;
|
|
76
|
+
const n = Math.floor(body.length / page);
|
|
77
|
+
const out = [];
|
|
78
|
+
for (let i = 0; i < n; i++) {
|
|
79
|
+
const pt = decPage(body.subarray(i * page, (i + 1) * page), ek, reserve, ivOff, i === 0);
|
|
80
|
+
const full = Buffer.alloc(page);
|
|
81
|
+
if (i === 0) {
|
|
82
|
+
Buffer.from('SQLite format 3\0').copy(full, 0);
|
|
83
|
+
if (pt) pt.copy(full, 16);
|
|
84
|
+
} else if (pt) {
|
|
85
|
+
pt.copy(full, 0);
|
|
86
|
+
}
|
|
87
|
+
out.push(full);
|
|
88
|
+
}
|
|
89
|
+
return { decrypted: Buffer.concat(out), uid: hit.uid, kdf: hit.algo };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── protobuf message-body → readable text (from pdh-qq-ingest.mjs) ──────────
|
|
93
|
+
function readVarint(buf, p) {
|
|
94
|
+
let shift = 0, r = 0n;
|
|
95
|
+
while (p < buf.length) {
|
|
96
|
+
const b = buf[p++];
|
|
97
|
+
r |= BigInt(b & 0x7f) << BigInt(shift);
|
|
98
|
+
if (!(b & 0x80)) break;
|
|
99
|
+
shift += 7;
|
|
100
|
+
}
|
|
101
|
+
return [r, p];
|
|
102
|
+
}
|
|
103
|
+
function* fields(buf) {
|
|
104
|
+
let p = 0;
|
|
105
|
+
while (p < buf.length) {
|
|
106
|
+
let tag;[tag, p] = readVarint(buf, p); tag = Number(tag);
|
|
107
|
+
const wire = tag & 7;
|
|
108
|
+
if (wire === 0) { let v;[v, p] = readVarint(buf, p); } else if (wire === 2) {
|
|
109
|
+
let len;[len, p] = readVarint(buf, p); len = Number(len);
|
|
110
|
+
const data = buf.subarray(p, p + len); p += len; yield data;
|
|
111
|
+
} else if (wire === 5) p += 4; else if (wire === 1) p += 8; else return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const readable = (s) => s && /[一-鿿 -ヿA-Za-z0-9]/.test(s) && !/[�]/.test(s) &&
|
|
115
|
+
[...s].every((c) => c.charCodeAt(0) >= 32 || c === '\n');
|
|
116
|
+
function extractTexts(buf, depth, out) {
|
|
117
|
+
if (depth > 6 || !buf || buf.length === 0) return;
|
|
118
|
+
for (const data of fields(buf)) {
|
|
119
|
+
if (!data || data.length === 0) continue;
|
|
120
|
+
let s = null; try { s = data.toString('utf8'); } catch {}
|
|
121
|
+
if (s && Buffer.from(s, 'utf8').equals(data) && readable(s) && s.length <= 1000 &&
|
|
122
|
+
!/^https?:\/\/\S+$/.test(s)) out.push(s);
|
|
123
|
+
if (data.length >= 2) extractTexts(data, depth + 1, out);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function bodyText(blob) {
|
|
127
|
+
if (!Buffer.isBuffer(blob)) return null;
|
|
128
|
+
const out = []; extractTexts(blob, 0, out);
|
|
129
|
+
const cjk = out.filter((t) => /[一-鿿]/.test(t)).sort((a, b) => b.length - a.length);
|
|
130
|
+
return cjk[0] || out.sort((a, b) => b.length - a.length)[0] || null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Parse a DECRYPTED nt_msg.db into vault events (qq-pc adapter shape).
|
|
135
|
+
* @param Database a better-sqlite3 constructor (injected by the caller/bundle)
|
|
136
|
+
* @param dbPath path to the decrypted nt_msg.db
|
|
137
|
+
* @param self the user's own QQ number (attribution fallback)
|
|
138
|
+
* @returns {Array} event objects ready for vault.putEvent
|
|
139
|
+
*/
|
|
140
|
+
function parseEvents(Database, dbPath, self) {
|
|
141
|
+
const src = new Database(dbPath, { readonly: true });
|
|
142
|
+
const events = [];
|
|
143
|
+
const num = (v) => (typeof v === 'bigint' ? Number(v) : v);
|
|
144
|
+
const ingestTable = (table, isGroup) => {
|
|
145
|
+
let rows;
|
|
146
|
+
try {
|
|
147
|
+
rows = src.prepare(
|
|
148
|
+
`SELECT [40001] msgId,[40020] uid,[40011] type,[40033] sender,[40021] peer,` +
|
|
149
|
+
`[40050] t,[40800] body FROM ${table}`,
|
|
150
|
+
).safeIntegers().all();
|
|
151
|
+
} catch { return; }
|
|
152
|
+
for (const r of rows) {
|
|
153
|
+
const type = num(r.type);
|
|
154
|
+
if (type === 5 || type === 9) continue; // grey-bar / system
|
|
155
|
+
const text = bodyText(r.body);
|
|
156
|
+
if (!text || !/[一-鿿A-Za-z0-9]/.test(text)) continue;
|
|
157
|
+
// QQ service/gray-tip config messages (e.g. com.tencent.* push configs) —
|
|
158
|
+
// not real chat; drop so the vault holds conversation, not service noise.
|
|
159
|
+
if (text.includes('com.tencent.') || text.includes('public desc')) continue;
|
|
160
|
+
const msgId = typeof r.msgId === 'bigint' ? r.msgId.toString() : String(r.msgId);
|
|
161
|
+
const sender = String(num(r.sender) || '');
|
|
162
|
+
const peer = String(num(r.peer) || '');
|
|
163
|
+
const occurredAt = num(r.t) * 1000;
|
|
164
|
+
if (!occurredAt) continue;
|
|
165
|
+
const actor = sender ? `person-qq-${sender}` : `person-qq-${self}`;
|
|
166
|
+
const participants = [actor];
|
|
167
|
+
participants.push(isGroup ? `group-qq-${peer}` : `person-qq-${peer}`);
|
|
168
|
+
events.push({
|
|
169
|
+
type: 'event', subtype: 'message', id: `qq:${table}:${msgId}`,
|
|
170
|
+
occurredAt, actor, participants,
|
|
171
|
+
content: { text: isGroup ? `[群${peer}] ${text}` : text },
|
|
172
|
+
topics: isGroup ? [`group-qq-${peer}`] : undefined,
|
|
173
|
+
source: {
|
|
174
|
+
adapter: 'qq-pc', adapterVersion: '0.1.0', originalId: `${table}:${msgId}`,
|
|
175
|
+
capturedAt: occurredAt, capturedBy: 'sqlite',
|
|
176
|
+
},
|
|
177
|
+
ingestedAt: Date.now(),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
try {
|
|
182
|
+
ingestTable('c2c_msg_table', false);
|
|
183
|
+
ingestTable('group_msg_table', true);
|
|
184
|
+
} finally {
|
|
185
|
+
src.close();
|
|
186
|
+
}
|
|
187
|
+
return events;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { extractRand, headerHmac, deriveAndDecrypt, bodyText, parseEvents };
|
package/lib/prompt-builder.js
CHANGED
|
@@ -48,11 +48,25 @@ const CROSS_APP_HEADER = "CROSS_APP_OVERVIEW (跨 app 汇聚画像 — 各 app
|
|
|
48
48
|
* Trim an event down to the fields the LLM actually needs. Saves tokens +
|
|
49
49
|
* reduces prompt injection surface (no raw `extra` blob).
|
|
50
50
|
*/
|
|
51
|
+
// Local-time "YYYY-MM-DD HH:mm" for the LLM. Passing the raw epoch-ms integer
|
|
52
|
+
// (e.g. 1781706182375) made the model unreliable on "when did I…" questions —
|
|
53
|
+
// it can't dependably convert epoch ms to a date. buildPrompt runs on the
|
|
54
|
+
// user's own machine (cc hub / desktop), so local getters are the user's TZ.
|
|
55
|
+
function fmtLocalDateTime(ms) {
|
|
56
|
+
const d = new Date(ms);
|
|
57
|
+
if (!Number.isFinite(d.getTime())) return null;
|
|
58
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
59
|
+
return (
|
|
60
|
+
`${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ` +
|
|
61
|
+
`${p(d.getHours())}:${p(d.getMinutes())}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
51
65
|
function summarizeEvent(e) {
|
|
52
66
|
const out = {
|
|
53
67
|
id: e.id,
|
|
54
68
|
type: e.subtype,
|
|
55
|
-
at: e.occurredAt,
|
|
69
|
+
at: fmtLocalDateTime(e.occurredAt) || e.occurredAt,
|
|
56
70
|
source: e.source && e.source.adapter,
|
|
57
71
|
};
|
|
58
72
|
if (e.actor) out.actor = e.actor;
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chainlesschain/personal-data-hub",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.31",
|
|
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",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib/"
|
|
9
|
+
],
|
|
7
10
|
"exports": {
|
|
8
11
|
".": "./lib/index.js",
|
|
9
12
|
"./constants": "./lib/constants.js",
|
|
@@ -70,7 +73,8 @@
|
|
|
70
73
|
"./adapters/messaging-whatsapp": "./lib/adapters/messaging-whatsapp/index.js",
|
|
71
74
|
"./sidecar": "./lib/sidecar/index.js",
|
|
72
75
|
"./forensics/leaf-salvage": "./lib/forensics/leaf-salvage.js",
|
|
73
|
-
"./forensics/salvage-ingest": "./lib/forensics/salvage-ingest.js"
|
|
76
|
+
"./forensics/salvage-ingest": "./lib/forensics/salvage-ingest.js",
|
|
77
|
+
"./forensics/qq-nt-collect": "./lib/forensics/qq-nt-collect.js"
|
|
74
78
|
},
|
|
75
79
|
"scripts": {
|
|
76
80
|
"test": "vitest run",
|
|
@@ -101,7 +105,8 @@
|
|
|
101
105
|
"optionalDependencies": {
|
|
102
106
|
"imapflow": "^1.0.183",
|
|
103
107
|
"adm-zip": "^0.5.16",
|
|
104
|
-
"iconv-lite": "^0.6.3"
|
|
108
|
+
"iconv-lite": "^0.6.3",
|
|
109
|
+
"pdf-parse": "^1.1.1"
|
|
105
110
|
},
|
|
106
111
|
"devDependencies": {
|
|
107
112
|
"vitest": "^4.1.5"
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const { getAdapterGuide, ADAPTER_OVERRIDES } = require("../lib/adapter-guide");
|
|
6
|
-
|
|
7
|
-
describe("adapter-guide", () => {
|
|
8
|
-
it("wechat-pc guide reflects the 4.0 one-click reality (no manual PyWxDump as primary)", () => {
|
|
9
|
-
const g = getAdapterGuide("wechat-pc", "device");
|
|
10
|
-
// primary method is the automatic one-click, not manual decryption
|
|
11
|
-
const primary = g.methods[0];
|
|
12
|
-
expect(primary.recommended).toBe(true);
|
|
13
|
-
expect(primary.label).toMatch(/一键|自动/);
|
|
14
|
-
expect(primary.steps.join(" ")).toMatch(/一键采集|自动/);
|
|
15
|
-
// summary mentions the full coverage we now capture
|
|
16
|
-
expect(g.summary).toMatch(/公众号/);
|
|
17
|
-
expect(g.summary).toMatch(/朋友圈/);
|
|
18
|
-
expect(g.summary).toMatch(/收藏/);
|
|
19
|
-
// manual 3.x path is still offered as a fallback
|
|
20
|
-
expect(g.methods.some((m) => /3\.x|PyWxDump|手动/.test(m.label + m.steps.join(" ")))).toBe(true);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("the 6 social platforms all have a tailored one-click ADB guide", () => {
|
|
24
|
-
for (const name of [
|
|
25
|
-
"social-bilibili",
|
|
26
|
-
"social-weibo",
|
|
27
|
-
"social-douyin",
|
|
28
|
-
"social-xiaohongshu",
|
|
29
|
-
"social-toutiao",
|
|
30
|
-
"social-kuaishou",
|
|
31
|
-
]) {
|
|
32
|
-
expect(ADAPTER_OVERRIDES[name]).toBeTruthy();
|
|
33
|
-
const g = getAdapterGuide(name, "device");
|
|
34
|
-
const primary = g.methods[0];
|
|
35
|
-
expect(primary.recommended).toBe(true);
|
|
36
|
-
// recommended path is root-phone + one-click, not "go log in on the web"
|
|
37
|
-
expect(primary.label + primary.steps.join(" ")).toMatch(/一键|ADB|USB|root/i);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("unknown adapter falls back to a category guide without throwing", () => {
|
|
42
|
-
const g = getAdapterGuide("totally-unknown", "snapshot");
|
|
43
|
-
expect(g.category).toBe("snapshot");
|
|
44
|
-
expect(Array.isArray(g.methods)).toBe(true);
|
|
45
|
-
expect(g.methods.length).toBeGreaterThan(0);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const { assertAdapter, SENSITIVITY_LEVELS } = require("../lib/adapter-spec");
|
|
6
|
-
const { MockAdapter } = require("../lib/mock-adapter");
|
|
7
|
-
|
|
8
|
-
describe("assertAdapter", () => {
|
|
9
|
-
it("accepts a fully-valid adapter (MockAdapter)", () => {
|
|
10
|
-
const r = assertAdapter(new MockAdapter());
|
|
11
|
-
expect(r.ok).toBe(true);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("rejects non-object input", () => {
|
|
15
|
-
expect(assertAdapter(null).ok).toBe(false);
|
|
16
|
-
expect(assertAdapter(undefined).ok).toBe(false);
|
|
17
|
-
expect(assertAdapter("oops").ok).toBe(false);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("rejects missing required fields (collects all errors, no throw)", () => {
|
|
21
|
-
const r = assertAdapter({});
|
|
22
|
-
expect(r.ok).toBe(false);
|
|
23
|
-
// Many fields missing — at least name + version + capabilities + dataDisclosure + methods.
|
|
24
|
-
expect(r.errors.length).toBeGreaterThan(4);
|
|
25
|
-
expect(r.errors.some((e) => e.includes("name"))).toBe(true);
|
|
26
|
-
expect(r.errors.some((e) => e.includes("version"))).toBe(true);
|
|
27
|
-
expect(r.errors.some((e) => e.includes("authenticate"))).toBe(true);
|
|
28
|
-
expect(r.errors.some((e) => e.includes("sync"))).toBe(true);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("rejects invalid sensitivity", () => {
|
|
32
|
-
const a = new MockAdapter();
|
|
33
|
-
a.dataDisclosure = { ...a.dataDisclosure, sensitivity: "extreme" };
|
|
34
|
-
const r = assertAdapter(a);
|
|
35
|
-
expect(r.ok).toBe(false);
|
|
36
|
-
expect(r.errors.some((e) => e.includes("sensitivity"))).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("rejects non-boolean legalGate", () => {
|
|
40
|
-
const a = new MockAdapter();
|
|
41
|
-
a.dataDisclosure = { ...a.dataDisclosure, legalGate: "yes" };
|
|
42
|
-
const r = assertAdapter(a);
|
|
43
|
-
expect(r.ok).toBe(false);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("rejects non-array capabilities", () => {
|
|
47
|
-
const a = new MockAdapter();
|
|
48
|
-
a.capabilities = "sync";
|
|
49
|
-
const r = assertAdapter(a);
|
|
50
|
-
expect(r.ok).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("rejects rateLimits with negative value", () => {
|
|
54
|
-
const a = new MockAdapter();
|
|
55
|
-
a.rateLimits = { perMinute: -1 };
|
|
56
|
-
expect(assertAdapter(a).ok).toBe(false);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("accepts adapter without rateLimits (optional field)", () => {
|
|
60
|
-
const a = new MockAdapter();
|
|
61
|
-
delete a.rateLimits;
|
|
62
|
-
expect(assertAdapter(a).ok).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("rejects non-function authenticate / sync / normalize / healthCheck", () => {
|
|
66
|
-
const a = new MockAdapter();
|
|
67
|
-
a.authenticate = "not a function";
|
|
68
|
-
expect(assertAdapter(a).ok).toBe(false);
|
|
69
|
-
|
|
70
|
-
const b = new MockAdapter();
|
|
71
|
-
b.normalize = 42;
|
|
72
|
-
expect(assertAdapter(b).ok).toBe(false);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("SENSITIVITY_LEVELS lists low/medium/high", () => {
|
|
76
|
-
expect(SENSITIVITY_LEVELS).toEqual(["low", "medium", "high"]);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
COOKIE_SPEC_VERSION,
|
|
7
|
-
KNOWN_VENDORS,
|
|
8
|
-
COOKIE_CAPTURE_SPECS,
|
|
9
|
-
getSpec,
|
|
10
|
-
listVendors,
|
|
11
|
-
classifyProbedCookies,
|
|
12
|
-
validateCookieCaptureSpec,
|
|
13
|
-
_internal,
|
|
14
|
-
} = require("../../lib/adapters/ai-chat-history/cookie-capture-spec");
|
|
15
|
-
|
|
16
|
-
describe("cookie-capture-spec — Phase 10.3.1 matrix", () => {
|
|
17
|
-
it("exposes a positive integer COOKIE_SPEC_VERSION", () => {
|
|
18
|
-
expect(Number.isInteger(COOKIE_SPEC_VERSION)).toBe(true);
|
|
19
|
-
expect(COOKIE_SPEC_VERSION).toBeGreaterThanOrEqual(1);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("KNOWN_VENDORS contains exactly the 9 wired vendors", () => {
|
|
23
|
-
expect(KNOWN_VENDORS).toEqual([
|
|
24
|
-
"deepseek",
|
|
25
|
-
"kimi",
|
|
26
|
-
"tongyi",
|
|
27
|
-
"zhipu",
|
|
28
|
-
"hunyuan",
|
|
29
|
-
"qianfan",
|
|
30
|
-
"coze",
|
|
31
|
-
"dreamina",
|
|
32
|
-
"doubao",
|
|
33
|
-
]);
|
|
34
|
-
// Defensive — frozen so contributors don't accidentally mutate at runtime.
|
|
35
|
-
expect(Object.isFrozen(KNOWN_VENDORS)).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("ships 9 specs, one per KNOWN_VENDORS entry, no duplicates", () => {
|
|
39
|
-
expect(COOKIE_CAPTURE_SPECS.length).toBe(KNOWN_VENDORS.length);
|
|
40
|
-
const seen = new Set();
|
|
41
|
-
for (const s of COOKIE_CAPTURE_SPECS) {
|
|
42
|
-
expect(KNOWN_VENDORS).toContain(s.vendor);
|
|
43
|
-
expect(seen.has(s.vendor)).toBe(false);
|
|
44
|
-
seen.add(s.vendor);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("validates the shipped spec set without errors", () => {
|
|
49
|
-
const r = validateCookieCaptureSpec(undefined, { throwOnError: false });
|
|
50
|
-
expect(r.errors).toEqual([]);
|
|
51
|
-
expect(r.ok).toBe(true);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("each loginUrl host matches at least one cookieDomains entry", () => {
|
|
55
|
-
for (const s of COOKIE_CAPTURE_SPECS) {
|
|
56
|
-
const host = new URL(s.loginUrl).host;
|
|
57
|
-
const matched = s.cookieDomains.some((d) =>
|
|
58
|
-
d.startsWith(".") ? host.endsWith(d.slice(1)) : host === d,
|
|
59
|
-
);
|
|
60
|
-
expect({ vendor: s.vendor, host, matched }).toEqual({ vendor: s.vendor, host, matched: true });
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("each spec has non-empty requiredCookies + postLoginPathHints + positive maxAge", () => {
|
|
65
|
-
for (const s of COOKIE_CAPTURE_SPECS) {
|
|
66
|
-
expect(Array.isArray(s.requiredCookies)).toBe(true);
|
|
67
|
-
expect(s.requiredCookies.length).toBeGreaterThan(0);
|
|
68
|
-
expect(Array.isArray(s.postLoginPathHints)).toBe(true);
|
|
69
|
-
expect(s.postLoginPathHints.length).toBeGreaterThan(0);
|
|
70
|
-
expect(Number.isInteger(s.cookieMaxAgeHintDays)).toBe(true);
|
|
71
|
-
expect(s.cookieMaxAgeHintDays).toBeGreaterThan(0);
|
|
72
|
-
expect(typeof s.notes).toBe("string");
|
|
73
|
-
expect(s.notes.length).toBeGreaterThan(0);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("getSpec returns the right spec for a known vendor and null for unknown", () => {
|
|
78
|
-
const ds = getSpec("deepseek");
|
|
79
|
-
expect(ds).toBeTruthy();
|
|
80
|
-
expect(ds.vendor).toBe("deepseek");
|
|
81
|
-
expect(getSpec("notarealvendor")).toBeNull();
|
|
82
|
-
expect(getSpec("")).toBeNull();
|
|
83
|
-
expect(getSpec(undefined)).toBeNull();
|
|
84
|
-
expect(getSpec(null)).toBeNull();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("listVendors returns a copy (mutation does not affect KNOWN_VENDORS)", () => {
|
|
88
|
-
const arr = listVendors();
|
|
89
|
-
expect(arr).toEqual([...KNOWN_VENDORS]);
|
|
90
|
-
arr.push("hacked");
|
|
91
|
-
expect(KNOWN_VENDORS.includes("hacked")).toBe(false);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe("classifyProbedCookies — required vs optional vs missing", () => {
|
|
96
|
-
it("returns ok=true when all required cookies present (object input)", () => {
|
|
97
|
-
const r = classifyProbedCookies("deepseek", {
|
|
98
|
-
userToken: "abc",
|
|
99
|
-
"intercom-session-deepseek": "xyz",
|
|
100
|
-
});
|
|
101
|
-
expect(r.ok).toBe(true);
|
|
102
|
-
expect(r.foundRequired).toEqual(["userToken"]);
|
|
103
|
-
expect(r.missingRequired).toEqual([]);
|
|
104
|
-
expect(r.foundOptional).toEqual(["intercom-session-deepseek"]);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("returns ok=false when a required cookie is missing", () => {
|
|
108
|
-
const r = classifyProbedCookies("kimi", { refresh_token: "rt", session_id: "sid" });
|
|
109
|
-
expect(r.ok).toBe(false);
|
|
110
|
-
expect(r.missingRequired).toEqual(["access_token"]);
|
|
111
|
-
expect(r.foundOptional.sort()).toEqual(["refresh_token", "session_id"]);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("accepts Electron Cookie[] shape (array of { name, value })", () => {
|
|
115
|
-
const r = classifyProbedCookies("zhipu", [
|
|
116
|
-
{ name: "chatglm_token", value: "tok" },
|
|
117
|
-
{ name: "cgsessionid", value: "sid" },
|
|
118
|
-
{ name: "unrelated", value: "x" },
|
|
119
|
-
]);
|
|
120
|
-
expect(r.ok).toBe(true);
|
|
121
|
-
expect(r.foundRequired).toEqual(["chatglm_token"]);
|
|
122
|
-
expect(r.foundOptional).toContain("cgsessionid");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("accepts raw 'k=v; k=v' string (web-shell paste fallback)", () => {
|
|
126
|
-
const raw = "sessionid=abc; sid_guard=xyz; passport_csrf_token=csrf; ;junk";
|
|
127
|
-
const r = classifyProbedCookies("doubao", raw);
|
|
128
|
-
expect(r.ok).toBe(true);
|
|
129
|
-
expect(r.foundRequired).toEqual(["sessionid"]);
|
|
130
|
-
expect(r.foundOptional.sort()).toEqual(["passport_csrf_token", "sid_guard"]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("string parser tolerates values containing '=' (e.g. base64)", () => {
|
|
134
|
-
// sessionid is base64 with '=' padding; only the FIRST '=' is the delimiter.
|
|
135
|
-
const raw = "sessionid=YWJjZGVmZ2g=; sid_guard=v1=";
|
|
136
|
-
const r = classifyProbedCookies("coze", raw);
|
|
137
|
-
expect(r.ok).toBe(true);
|
|
138
|
-
expect(r.foundRequired).toEqual(["sessionid"]);
|
|
139
|
-
// raw cookie value must be preserved verbatim including the trailing '='
|
|
140
|
-
const jar = _internal._normalizeCookieJar(raw);
|
|
141
|
-
expect(jar.sessionid).toBe("YWJjZGVmZ2g=");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("empty string / null / undefined / wrong type all produce ok=false", () => {
|
|
145
|
-
for (const input of ["", null, undefined, 42, true]) {
|
|
146
|
-
const r = classifyProbedCookies("doubao", input);
|
|
147
|
-
expect(r.ok).toBe(false);
|
|
148
|
-
expect(r.missingRequired).toEqual(["sessionid"]);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("returns UNKNOWN_VENDOR reason for an unregistered vendor name", () => {
|
|
153
|
-
const r = classifyProbedCookies("notarealvendor", { anything: "x" });
|
|
154
|
-
expect(r.ok).toBe(false);
|
|
155
|
-
expect(r.reason).toBe("UNKNOWN_VENDOR");
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("treats empty-string cookie value as missing (not present)", () => {
|
|
159
|
-
const r = classifyProbedCookies("deepseek", { userToken: "" });
|
|
160
|
-
expect(r.ok).toBe(false);
|
|
161
|
-
expect(r.foundRequired).toEqual([]);
|
|
162
|
-
expect(r.missingRequired).toEqual(["userToken"]);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe("validateCookieCaptureSpec — defensive guard catches malformed specs", () => {
|
|
167
|
-
it("flags unknown vendor", () => {
|
|
168
|
-
const { ok, errors } = validateCookieCaptureSpec(
|
|
169
|
-
[{ ...COOKIE_CAPTURE_SPECS[0], vendor: "ghostvendor" }],
|
|
170
|
-
{ throwOnError: false },
|
|
171
|
-
);
|
|
172
|
-
expect(ok).toBe(false);
|
|
173
|
-
expect(errors.join(" ")).toMatch(/unknown vendor/);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("flags loginUrl host not matching any cookieDomain", () => {
|
|
177
|
-
const broken = {
|
|
178
|
-
...COOKIE_CAPTURE_SPECS[0],
|
|
179
|
-
loginUrl: "https://malicious.example.com/",
|
|
180
|
-
};
|
|
181
|
-
const { ok, errors } = validateCookieCaptureSpec([broken], { throwOnError: false });
|
|
182
|
-
expect(ok).toBe(false);
|
|
183
|
-
expect(errors.join(" ")).toMatch(/does not match any cookieDomain/);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("flags empty requiredCookies / postLoginPathHints / invalid maxAge", () => {
|
|
187
|
-
const broken = {
|
|
188
|
-
...COOKIE_CAPTURE_SPECS[0],
|
|
189
|
-
requiredCookies: [],
|
|
190
|
-
postLoginPathHints: [],
|
|
191
|
-
cookieMaxAgeHintDays: 0,
|
|
192
|
-
};
|
|
193
|
-
const { ok, errors } = validateCookieCaptureSpec([broken], { throwOnError: false });
|
|
194
|
-
expect(ok).toBe(false);
|
|
195
|
-
const joined = errors.join(" ");
|
|
196
|
-
expect(joined).toMatch(/requiredCookies/);
|
|
197
|
-
expect(joined).toMatch(/postLoginPathHints/);
|
|
198
|
-
expect(joined).toMatch(/cookieMaxAgeHintDays/);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("flags duplicate vendor entries", () => {
|
|
202
|
-
const dup = [COOKIE_CAPTURE_SPECS[0], { ...COOKIE_CAPTURE_SPECS[0] }];
|
|
203
|
-
const { ok, errors } = validateCookieCaptureSpec(dup, { throwOnError: false });
|
|
204
|
-
expect(ok).toBe(false);
|
|
205
|
-
expect(errors.join(" ")).toMatch(/duplicate vendor/);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("throws by default when malformed (no opts)", () => {
|
|
209
|
-
expect(() => validateCookieCaptureSpec([{ vendor: "ghost" }])).toThrow(/Invalid cookie capture spec/);
|
|
210
|
-
});
|
|
211
|
-
});
|