@agentunion/fastaun 0.2.20 → 0.3.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +63 -23
  2. package/_packed_docs/CHANGELOG.md +63 -23
  3. package/_packed_docs/design/2026-05-22-aun-rpc-trace-enhancement.md +542 -0
  4. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1 -24
  5. package/_packed_docs/protocol/15-/347/246/273/347/272/277/346/216/250/351/200/201/351/200/232/347/237/245/345/215/217/350/256/256.md +419 -0
  6. package/_packed_docs/protocol/index.md +13 -3
  7. package/_packed_docs/python-sdk-v2-only-changelog.md +189 -0
  8. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +39 -16
  9. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +131 -39
  10. package/_packed_docs/sdk/09-message-rpc-manual.md +30 -67
  11. package/dist/auth.js +26 -7
  12. package/dist/auth.js.map +1 -1
  13. package/dist/client.d.ts +117 -166
  14. package/dist/client.js +2130 -3419
  15. package/dist/client.js.map +1 -1
  16. package/dist/config.d.ts +0 -4
  17. package/dist/config.js +0 -4
  18. package/dist/config.js.map +1 -1
  19. package/dist/e2ee.d.ts +5 -139
  20. package/dist/e2ee.js +4 -1151
  21. package/dist/e2ee.js.map +1 -1
  22. package/dist/errors.d.ts +0 -8
  23. package/dist/errors.js +0 -14
  24. package/dist/errors.js.map +1 -1
  25. package/dist/index.d.ts +9 -5
  26. package/dist/index.js +6 -3
  27. package/dist/index.js.map +1 -1
  28. package/dist/keystore/aid-db.d.ts +12 -61
  29. package/dist/keystore/aid-db.js +41 -539
  30. package/dist/keystore/aid-db.js.map +1 -1
  31. package/dist/keystore/file.d.ts +5 -41
  32. package/dist/keystore/file.js +8 -64
  33. package/dist/keystore/file.js.map +1 -1
  34. package/dist/keystore/index.d.ts +1 -49
  35. package/dist/namespaces/auth.d.ts +8 -0
  36. package/dist/namespaces/auth.js +169 -2
  37. package/dist/namespaces/auth.js.map +1 -1
  38. package/dist/protected-headers.d.ts +13 -0
  39. package/dist/protected-headers.js +47 -0
  40. package/dist/protected-headers.js.map +1 -0
  41. package/dist/seq-tracker.d.ts +7 -2
  42. package/dist/seq-tracker.js +33 -13
  43. package/dist/seq-tracker.js.map +1 -1
  44. package/dist/transport.d.ts +11 -1
  45. package/dist/transport.js +255 -6
  46. package/dist/transport.js.map +1 -1
  47. package/dist/types.d.ts +0 -56
  48. package/dist/v2/crypto/aead.d.ts +20 -0
  49. package/dist/v2/crypto/aead.js +59 -0
  50. package/dist/v2/crypto/aead.js.map +1 -0
  51. package/dist/v2/crypto/canonical.d.ts +20 -0
  52. package/dist/v2/crypto/canonical.js +119 -0
  53. package/dist/v2/crypto/canonical.js.map +1 -0
  54. package/dist/v2/crypto/dh-path.d.ts +39 -0
  55. package/dist/v2/crypto/dh-path.js +55 -0
  56. package/dist/v2/crypto/dh-path.js.map +1 -0
  57. package/dist/v2/crypto/ecdh.d.ts +29 -0
  58. package/dist/v2/crypto/ecdh.js +122 -0
  59. package/dist/v2/crypto/ecdh.js.map +1 -0
  60. package/dist/v2/crypto/ecdsa.d.ts +29 -0
  61. package/dist/v2/crypto/ecdsa.js +120 -0
  62. package/dist/v2/crypto/ecdsa.js.map +1 -0
  63. package/dist/v2/crypto/hkdf.d.ts +19 -0
  64. package/dist/v2/crypto/hkdf.js +47 -0
  65. package/dist/v2/crypto/hkdf.js.map +1 -0
  66. package/dist/v2/crypto/index.d.ts +8 -0
  67. package/dist/v2/crypto/index.js +8 -0
  68. package/dist/v2/crypto/index.js.map +1 -0
  69. package/dist/v2/crypto/recipients.d.ts +32 -0
  70. package/dist/v2/crypto/recipients.js +183 -0
  71. package/dist/v2/crypto/recipients.js.map +1 -0
  72. package/dist/v2/e2ee/decrypt.d.ts +29 -0
  73. package/dist/v2/e2ee/decrypt.js +159 -0
  74. package/dist/v2/e2ee/decrypt.js.map +1 -0
  75. package/dist/v2/e2ee/encrypt-group.d.ts +17 -0
  76. package/dist/v2/e2ee/encrypt-group.js +143 -0
  77. package/dist/v2/e2ee/encrypt-group.js.map +1 -0
  78. package/dist/v2/e2ee/encrypt-p2p.d.ts +31 -0
  79. package/dist/v2/e2ee/encrypt-p2p.js +190 -0
  80. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -0
  81. package/dist/v2/e2ee/index.d.ts +9 -0
  82. package/dist/v2/e2ee/index.js +9 -0
  83. package/dist/v2/e2ee/index.js.map +1 -0
  84. package/dist/v2/e2ee/metadata-auth.d.ts +15 -0
  85. package/dist/v2/e2ee/metadata-auth.js +50 -0
  86. package/dist/v2/e2ee/metadata-auth.js.map +1 -0
  87. package/dist/v2/e2ee/types.d.ts +57 -0
  88. package/dist/v2/e2ee/types.js +7 -0
  89. package/dist/v2/e2ee/types.js.map +1 -0
  90. package/dist/v2/session/index.d.ts +4 -0
  91. package/dist/v2/session/index.js +3 -0
  92. package/dist/v2/session/index.js.map +1 -0
  93. package/dist/v2/session/keystore.d.ts +50 -0
  94. package/dist/v2/session/keystore.js +138 -0
  95. package/dist/v2/session/keystore.js.map +1 -0
  96. package/dist/v2/session/session.d.ts +124 -0
  97. package/dist/v2/session/session.js +318 -0
  98. package/dist/v2/session/session.js.map +1 -0
  99. package/dist/v2/state/commitment.d.ts +58 -0
  100. package/dist/v2/state/commitment.js +85 -0
  101. package/dist/v2/state/commitment.js.map +1 -0
  102. package/dist/v2/state/index.d.ts +2 -0
  103. package/dist/v2/state/index.js +2 -0
  104. package/dist/v2/state/index.js.map +1 -0
  105. package/package.json +4 -3
@@ -1,10 +1,8 @@
1
1
  /**
2
2
  * Per-AID SQLite 数据库。
3
3
  *
4
- * SQLite SDK 统一规则:
5
- * - schema Python / Go 完全一致
6
- * - 敏感字段写入时一律字段级加密
7
- * - 读取时兼容密文 JSON 与历史明文
4
+ * E2EE V2 ONLY:这里不再提供旧版 E2EE 密钥材料存储。
5
+ * V2 设备密钥由 `v2/session/keystore.ts` 通过同一个 SQLite 句柄维护。
8
6
  */
9
7
  import { mkdirSync } from 'node:fs';
10
8
  import { createRequire } from 'node:module';
@@ -20,95 +18,44 @@ function configureDatabase(db, busyTimeoutMs) {
20
18
  }
21
19
  db.exec('PRAGMA synchronous = NORMAL');
22
20
  }
23
- function runImmediateTransaction(db, fn) {
24
- db.exec('BEGIN IMMEDIATE');
25
- try {
26
- const result = fn();
27
- db.exec('COMMIT');
28
- return result;
29
- }
30
- catch (err) {
31
- try {
32
- if (db.isTransaction)
33
- db.exec('ROLLBACK');
34
- }
35
- catch {
36
- /* ignore rollback failure */
37
- }
38
- throw err;
39
- }
40
- }
41
21
  const DDL_STATEMENTS = [
42
- `CREATE TABLE IF NOT EXISTS _schema_version (
43
- id INTEGER PRIMARY KEY CHECK (id = 1),
44
- version INTEGER NOT NULL
45
- )`,
46
- `CREATE TABLE IF NOT EXISTS tokens (
47
- key TEXT PRIMARY KEY,
48
- value TEXT NOT NULL,
49
- updated_at INTEGER NOT NULL
50
- )`,
51
- `CREATE TABLE IF NOT EXISTS prekeys (
52
- prekey_id TEXT NOT NULL,
53
- device_id TEXT NOT NULL DEFAULT '',
54
- private_key_enc TEXT NOT NULL DEFAULT '',
55
- data TEXT NOT NULL DEFAULT '{}',
56
- created_at INTEGER,
57
- updated_at INTEGER NOT NULL,
58
- expires_at INTEGER,
59
- PRIMARY KEY (prekey_id, device_id)
22
+ `CREATE TABLE IF NOT EXISTS _schema_version (
23
+ id INTEGER PRIMARY KEY CHECK (id = 1),
24
+ version INTEGER NOT NULL
60
25
  )`,
61
- `CREATE INDEX IF NOT EXISTS idx_prekeys_device ON prekeys (device_id, created_at)`,
62
- `CREATE TABLE IF NOT EXISTS group_current (
63
- group_id TEXT PRIMARY KEY,
64
- epoch INTEGER NOT NULL,
65
- secret_enc TEXT NOT NULL DEFAULT '',
66
- data TEXT NOT NULL DEFAULT '{}',
67
- updated_at INTEGER NOT NULL
26
+ `CREATE TABLE IF NOT EXISTS tokens (
27
+ key TEXT PRIMARY KEY,
28
+ value TEXT NOT NULL,
29
+ updated_at INTEGER NOT NULL
68
30
  )`,
69
- `CREATE TABLE IF NOT EXISTS group_old_epochs (
70
- group_id TEXT NOT NULL,
71
- epoch INTEGER NOT NULL,
72
- secret_enc TEXT NOT NULL DEFAULT '',
73
- data TEXT NOT NULL DEFAULT '{}',
74
- updated_at INTEGER NOT NULL,
75
- expires_at INTEGER,
76
- PRIMARY KEY (group_id, epoch)
31
+ `CREATE TABLE IF NOT EXISTS instance_state (
32
+ device_id TEXT NOT NULL,
33
+ slot_id TEXT NOT NULL DEFAULT '_singleton',
34
+ data TEXT NOT NULL DEFAULT '{}',
35
+ updated_at INTEGER NOT NULL,
36
+ PRIMARY KEY (device_id, slot_id)
77
37
  )`,
78
- `CREATE INDEX IF NOT EXISTS idx_group_old_expires ON group_old_epochs (group_id, expires_at)`,
79
- `CREATE TABLE IF NOT EXISTS e2ee_sessions (
80
- session_id TEXT PRIMARY KEY,
81
- data_enc TEXT NOT NULL DEFAULT '{}',
82
- updated_at INTEGER NOT NULL
38
+ `CREATE TABLE IF NOT EXISTS seq_tracker (
39
+ device_id TEXT NOT NULL,
40
+ slot_id TEXT NOT NULL DEFAULT '_singleton',
41
+ namespace TEXT NOT NULL,
42
+ contiguous_seq INTEGER NOT NULL DEFAULT 0,
43
+ updated_at INTEGER NOT NULL,
44
+ PRIMARY KEY (device_id, slot_id, namespace)
83
45
  )`,
84
- `CREATE TABLE IF NOT EXISTS instance_state (
85
- device_id TEXT NOT NULL,
86
- slot_id TEXT NOT NULL DEFAULT '_singleton',
87
- data TEXT NOT NULL DEFAULT '{}',
88
- updated_at INTEGER NOT NULL,
89
- PRIMARY KEY (device_id, slot_id)
46
+ `CREATE TABLE IF NOT EXISTS metadata_kv (
47
+ key TEXT PRIMARY KEY,
48
+ value TEXT NOT NULL,
49
+ updated_at INTEGER NOT NULL
90
50
  )`,
91
- `CREATE TABLE IF NOT EXISTS seq_tracker (
92
- device_id TEXT NOT NULL,
93
- slot_id TEXT NOT NULL DEFAULT '_singleton',
94
- namespace TEXT NOT NULL,
95
- contiguous_seq INTEGER NOT NULL DEFAULT 0,
96
- updated_at INTEGER NOT NULL,
97
- PRIMARY KEY (device_id, slot_id, namespace)
98
- )`,
99
- `CREATE TABLE IF NOT EXISTS metadata_kv (
100
- key TEXT PRIMARY KEY,
101
- value TEXT NOT NULL,
102
- updated_at INTEGER NOT NULL
103
- )`,
104
- `CREATE TABLE IF NOT EXISTS group_state (
105
- group_id TEXT PRIMARY KEY,
106
- state_version INTEGER NOT NULL DEFAULT 0,
107
- state_hash TEXT NOT NULL DEFAULT '',
108
- key_epoch INTEGER NOT NULL DEFAULT 0,
109
- membership_json TEXT NOT NULL DEFAULT '',
110
- policy_json TEXT NOT NULL DEFAULT '',
111
- updated_at INTEGER NOT NULL DEFAULT 0
51
+ `CREATE TABLE IF NOT EXISTS group_state (
52
+ group_id TEXT PRIMARY KEY,
53
+ state_version INTEGER NOT NULL DEFAULT 0,
54
+ state_hash TEXT NOT NULL DEFAULT '',
55
+ key_epoch INTEGER NOT NULL DEFAULT 0,
56
+ membership_json TEXT NOT NULL DEFAULT '',
57
+ policy_json TEXT NOT NULL DEFAULT '',
58
+ updated_at INTEGER NOT NULL DEFAULT 0
112
59
  )`,
113
60
  ];
114
61
  function jsonParseObject(value) {
@@ -123,14 +70,12 @@ function jsonParseObject(value) {
123
70
  export class AIDDatabase {
124
71
  _db;
125
72
  _dbPath;
126
- _secretStore;
127
73
  _scope;
128
74
  _log;
129
- constructor(dbPath, secretStore, scope, logger) {
75
+ constructor(dbPath, _secretStore, scope, logger) {
130
76
  mkdirSync(dirname(dbPath), { recursive: true });
131
77
  const absPath = resolve(dbPath);
132
78
  this._dbPath = absPath;
133
- this._secretStore = secretStore ?? null;
134
79
  this._scope = scope ?? dirname(absPath).split(/[\\/]/).pop() ?? '';
135
80
  this._log = logger ?? { error: () => { }, warn: () => { }, info: () => { }, debug: () => { } };
136
81
  const cached = _dbPool.get(absPath);
@@ -146,6 +91,10 @@ export class AIDDatabase {
146
91
  this._initSchema();
147
92
  this._log.debug(`AIDDatabase ready: path=${this._dbPath} scope=${this._scope}`);
148
93
  }
94
+ /** 暴露底层 SQLite 句柄,供 V2KeyStore 共享同一 DB 连接。 */
95
+ getSqliteHandle() {
96
+ return this._db;
97
+ }
149
98
  close() {
150
99
  const cached = _dbPool.get(this._dbPath);
151
100
  if (cached) {
@@ -166,50 +115,10 @@ export class AIDDatabase {
166
115
  _initSchema() {
167
116
  for (const ddl of DDL_STATEMENTS)
168
117
  this._db.exec(ddl);
169
- this._migrateLegacyColumns();
170
118
  const row = this._db.prepare('SELECT version FROM _schema_version WHERE id = 1').get();
171
119
  if (!row)
172
120
  this._db.prepare('INSERT INTO _schema_version (id, version) VALUES (1, ?)').run(SCHEMA_VERSION);
173
121
  }
174
- _migrateLegacyColumns() {
175
- this._renameColumnIfExists('prekeys', 'private_key_pem', 'private_key_enc');
176
- this._renameColumnIfExists('group_current', 'secret', 'secret_enc');
177
- this._renameColumnIfExists('group_old_epochs', 'secret', 'secret_enc');
178
- this._renameColumnIfExists('e2ee_sessions', 'data', 'data_enc');
179
- }
180
- _renameColumnIfExists(table, oldName, newName) {
181
- const rows = this._db.prepare(`PRAGMA table_info(${table})`).all();
182
- const names = new Set(rows.map((row) => row.name));
183
- if (names.has(oldName) && !names.has(newName)) {
184
- this._db.exec(`ALTER TABLE ${table} RENAME COLUMN ${oldName} TO ${newName}`);
185
- }
186
- }
187
- _protectText(name, plaintext) {
188
- if (!this._secretStore || !plaintext)
189
- return plaintext;
190
- try {
191
- return JSON.stringify(this._secretStore.protect(this._scope, name, Buffer.from(plaintext, 'utf-8')));
192
- }
193
- catch (exc) {
194
- this._log.error(`field encryption failed (scope=${this._scope}, name=${name}), degrading to plaintext: ${exc instanceof Error ? exc.message : String(exc)}`, exc instanceof Error ? exc : undefined);
195
- return plaintext;
196
- }
197
- }
198
- _revealText(name, stored) {
199
- if (!this._secretStore || !stored)
200
- return stored;
201
- try {
202
- const record = JSON.parse(stored);
203
- if (record?.scheme !== 'file_aes')
204
- return stored;
205
- const plain = this._secretStore.reveal(this._scope, name, record);
206
- return plain ? plain.toString('utf-8') : stored;
207
- }
208
- catch (exc) {
209
- this._log.error(`field decryption failed (scope=${this._scope}, name=${name}): ${exc instanceof Error ? exc.message : String(exc)}`, exc instanceof Error ? exc : undefined);
210
- return stored;
211
- }
212
- }
213
122
  getToken(key) {
214
123
  const row = this._db.prepare('SELECT value FROM tokens WHERE key = ?').get(key);
215
124
  return row?.value ?? null;
@@ -227,412 +136,6 @@ export class AIDDatabase {
227
136
  result[row.key] = row.value;
228
137
  return result;
229
138
  }
230
- savePrekey(prekeyId, privateKeyPem, deviceId = '', createdAt, expiresAt, extraData) {
231
- const now = Date.now();
232
- this._db.prepare(`INSERT INTO prekeys (prekey_id, device_id, private_key_enc, data, created_at, updated_at, expires_at)
233
- VALUES (?, ?, ?, ?, ?, ?, ?)
234
- ON CONFLICT(prekey_id, device_id) DO UPDATE SET
235
- private_key_enc=excluded.private_key_enc, data=excluded.data,
236
- updated_at=excluded.updated_at, expires_at=excluded.expires_at`).run(prekeyId, deviceId, this._protectText(`prekey/${prekeyId}`, privateKeyPem), JSON.stringify(extraData ?? {}), createdAt ?? now, now, expiresAt ?? null);
237
- }
238
- loadPrekeys(deviceId = '') {
239
- const rows = this._db.prepare('SELECT prekey_id, private_key_enc, data, created_at, updated_at, expires_at FROM prekeys WHERE device_id = ?').all(deviceId);
240
- const result = {};
241
- for (const row of rows) {
242
- const entry = {
243
- private_key_pem: this._revealText(`prekey/${row.prekey_id}`, row.private_key_enc),
244
- };
245
- if (row.created_at != null)
246
- entry.created_at = row.created_at;
247
- if (row.updated_at != null)
248
- entry.updated_at = row.updated_at;
249
- if (row.expires_at != null)
250
- entry.expires_at = row.expires_at;
251
- Object.assign(entry, jsonParseObject(row.data));
252
- result[row.prekey_id] = entry;
253
- }
254
- return result;
255
- }
256
- /**
257
- * 按 prekey_id 单点查询(WHERE prekey_id = ? LIMIT 1)。
258
- * 相比 loadPrekeys 的全量加载(O(N)),单条查询是 O(1)。
259
- * 解密入站消息时信封里有 prekey_id,应优先走这条路径。
260
- */
261
- loadPrekeyById(prekeyId) {
262
- const row = this._db.prepare('SELECT prekey_id, private_key_enc, data, created_at, updated_at, expires_at FROM prekeys WHERE prekey_id = ? LIMIT 1').get(prekeyId);
263
- if (!row)
264
- return null;
265
- const entry = {
266
- private_key_pem: this._revealText(`prekey/${row.prekey_id}`, row.private_key_enc),
267
- };
268
- if (row.created_at != null)
269
- entry.created_at = row.created_at;
270
- if (row.updated_at != null)
271
- entry.updated_at = row.updated_at;
272
- if (row.expires_at != null)
273
- entry.expires_at = row.expires_at;
274
- Object.assign(entry, jsonParseObject(row.data));
275
- return entry;
276
- }
277
- deletePrekey(prekeyId, deviceId = '') {
278
- this._db.prepare('DELETE FROM prekeys WHERE prekey_id = ? AND device_id = ?').run(prekeyId, deviceId);
279
- }
280
- cleanupPrekeys(deviceId, cutoffMs, keepLatest) {
281
- const rows = this._db.prepare('SELECT prekey_id, created_at FROM prekeys WHERE device_id = ? ORDER BY created_at DESC').all(deviceId);
282
- const latestIds = new Set(rows.slice(0, keepLatest).map((row) => row.prekey_id));
283
- const toDelete = rows
284
- .filter((row) => !latestIds.has(row.prekey_id) && row.created_at != null && row.created_at < cutoffMs)
285
- .map((row) => row.prekey_id);
286
- if (toDelete.length > 0) {
287
- const del = this._db.prepare('DELETE FROM prekeys WHERE device_id = ? AND prekey_id = ?');
288
- runImmediateTransaction(this._db, () => {
289
- for (const id of toDelete)
290
- del.run(deviceId, id);
291
- });
292
- }
293
- return toDelete;
294
- }
295
- saveGroupCurrent(groupId, epoch, secret, data) {
296
- this._db.prepare(`INSERT INTO group_current (group_id, epoch, secret_enc, data, updated_at)
297
- VALUES (?, ?, ?, ?, ?)
298
- ON CONFLICT(group_id) DO UPDATE SET epoch=excluded.epoch, secret_enc=excluded.secret_enc, data=excluded.data, updated_at=excluded.updated_at`).run(groupId, epoch, this._protectText(`group/${groupId}/current`, secret), JSON.stringify(data), Date.now());
299
- }
300
- loadGroupCurrent(groupId) {
301
- const row = this._db.prepare('SELECT epoch, secret_enc, data, updated_at FROM group_current WHERE group_id = ?').get(groupId);
302
- if (!row)
303
- return null;
304
- return {
305
- group_id: groupId,
306
- epoch: row.epoch,
307
- secret: this._revealText(`group/${groupId}/current`, row.secret_enc),
308
- ...jsonParseObject(row.data),
309
- updated_at: row.updated_at,
310
- };
311
- }
312
- loadGroupSecretEpoch(groupId, epoch) {
313
- const current = this.loadGroupCurrent(groupId);
314
- if (epoch === undefined || epoch === null)
315
- return current;
316
- if (current && Number(current.epoch ?? 0) === Number(epoch))
317
- return current;
318
- const row = this._db.prepare('SELECT epoch, secret_enc, data, updated_at, expires_at FROM group_old_epochs WHERE group_id = ? AND epoch = ?').get(groupId, epoch);
319
- if (!row)
320
- return null;
321
- const entry = {
322
- epoch: row.epoch,
323
- secret: this._revealText(`group/${groupId}/epoch/${row.epoch}`, row.secret_enc),
324
- ...jsonParseObject(row.data),
325
- updated_at: row.updated_at,
326
- };
327
- if (row.expires_at != null)
328
- entry.expires_at = row.expires_at;
329
- return entry;
330
- }
331
- loadGroupSecretEpochs(groupId) {
332
- const result = [];
333
- const current = this.loadGroupCurrent(groupId);
334
- if (current)
335
- result.push(current);
336
- const rows = this._db.prepare('SELECT epoch, secret_enc, data, updated_at, expires_at FROM group_old_epochs WHERE group_id = ? ORDER BY epoch ASC').all(groupId);
337
- for (const row of rows) {
338
- const entry = {
339
- epoch: row.epoch,
340
- secret: this._revealText(`group/${groupId}/epoch/${row.epoch}`, row.secret_enc),
341
- ...jsonParseObject(row.data),
342
- updated_at: row.updated_at,
343
- };
344
- if (row.expires_at != null)
345
- entry.expires_at = row.expires_at;
346
- result.push(entry);
347
- }
348
- return result;
349
- }
350
- loadAllGroupCurrent() {
351
- const rows = this._db.prepare('SELECT group_id, epoch, secret_enc, data, updated_at FROM group_current').all();
352
- const result = {};
353
- for (const row of rows) {
354
- result[row.group_id] = {
355
- group_id: row.group_id,
356
- epoch: row.epoch,
357
- secret: this._revealText(`group/${row.group_id}/current`, row.secret_enc),
358
- ...jsonParseObject(row.data),
359
- updated_at: row.updated_at,
360
- };
361
- }
362
- return result;
363
- }
364
- deleteGroupCurrent(groupId) {
365
- this._db.prepare('DELETE FROM group_current WHERE group_id = ?').run(groupId);
366
- }
367
- saveGroupOldEpoch(groupId, epoch, secret, data, updatedAt, expiresAt) {
368
- this._db.prepare(`INSERT INTO group_old_epochs (group_id, epoch, secret_enc, data, updated_at, expires_at)
369
- VALUES (?, ?, ?, ?, ?, ?)
370
- ON CONFLICT(group_id, epoch) DO UPDATE SET secret_enc=excluded.secret_enc, data=excluded.data, updated_at=excluded.updated_at, expires_at=excluded.expires_at`).run(groupId, epoch, this._protectText(`group/${groupId}/epoch/${epoch}`, secret), JSON.stringify(data), updatedAt ?? Date.now(), expiresAt ?? null);
371
- }
372
- loadGroupOldEpochs(groupId) {
373
- const rows = this._db.prepare('SELECT epoch, secret_enc, data, updated_at, expires_at FROM group_old_epochs WHERE group_id = ? ORDER BY epoch ASC').all(groupId);
374
- return rows.map((row) => {
375
- const entry = {
376
- epoch: row.epoch,
377
- secret: this._revealText(`group/${groupId}/epoch/${row.epoch}`, row.secret_enc),
378
- ...jsonParseObject(row.data),
379
- updated_at: row.updated_at,
380
- };
381
- if (row.expires_at != null)
382
- entry.expires_at = row.expires_at;
383
- return entry;
384
- });
385
- }
386
- deleteAllGroupOldEpochs(groupId) {
387
- this._db.prepare('DELETE FROM group_old_epochs WHERE group_id = ?').run(groupId);
388
- }
389
- loadAllGroupIdsWithOldEpochs() {
390
- const rows = this._db.prepare('SELECT DISTINCT group_id FROM group_old_epochs').all();
391
- return rows.map((row) => row.group_id);
392
- }
393
- cleanupGroupOldEpochs(groupId, cutoffMs) {
394
- const result = this._db.prepare('DELETE FROM group_old_epochs WHERE group_id = ? AND updated_at <= ?').run(groupId, cutoffMs);
395
- return Number(result.changes);
396
- }
397
- storeGroupSecretTransition(groupId, opts) {
398
- return Boolean(runImmediateTransaction(this._db, () => {
399
- const now = Date.now();
400
- const epoch = Number(opts.epoch);
401
- const members = [...(opts.memberAids ?? [])].map((item) => String(item)).sort();
402
- const pendingRotationId = String(opts.pendingRotationId ?? '').trim();
403
- const current = this._db.prepare('SELECT epoch, secret_enc, data, updated_at FROM group_current WHERE group_id = ?').get(groupId);
404
- if (current) {
405
- const localEpoch = Number(current.epoch);
406
- const currentSecret = this._revealText(`group/${groupId}/current`, current.secret_enc);
407
- const currentData = jsonParseObject(current.data);
408
- if (epoch < localEpoch)
409
- return false;
410
- if (epoch === localEpoch && currentSecret) {
411
- if (currentSecret !== opts.secret) {
412
- if (String(currentData.pending_rotation_id ?? '').trim()) {
413
- this._upsertGroupCurrent(groupId, epoch, opts.secret, this._buildGroupCurrentData(opts, members, now), now);
414
- return true;
415
- }
416
- return false;
417
- }
418
- const updated = { ...currentData };
419
- let changed = false;
420
- const oldMembers = Array.isArray(updated.member_aids) ? updated.member_aids.map(String).sort() : [];
421
- if (members.length > 0 && JSON.stringify(oldMembers) !== JSON.stringify(members)) {
422
- updated.member_aids = members;
423
- updated.commitment = opts.commitment;
424
- changed = true;
425
- }
426
- if (opts.epochChain !== undefined && updated.epoch_chain !== opts.epochChain) {
427
- updated.epoch_chain = opts.epochChain;
428
- changed = true;
429
- }
430
- if (opts.epochChainUnverified === true) {
431
- if (updated.epoch_chain_unverified !== true) {
432
- updated.epoch_chain_unverified = true;
433
- changed = true;
434
- }
435
- if (opts.epochChainUnverifiedReason && updated.epoch_chain_unverified_reason !== opts.epochChainUnverifiedReason) {
436
- updated.epoch_chain_unverified_reason = opts.epochChainUnverifiedReason;
437
- changed = true;
438
- }
439
- }
440
- else if (opts.epochChainUnverified === false) {
441
- if ('epoch_chain_unverified' in updated || 'epoch_chain_unverified_reason' in updated) {
442
- delete updated.epoch_chain_unverified;
443
- delete updated.epoch_chain_unverified_reason;
444
- changed = true;
445
- }
446
- }
447
- if (pendingRotationId && updated.pending_rotation_id !== pendingRotationId) {
448
- updated.pending_rotation_id = pendingRotationId;
449
- updated.pending_created_at = now;
450
- changed = true;
451
- }
452
- if (!pendingRotationId && updated.pending_rotation_id) {
453
- delete updated.pending_rotation_id;
454
- delete updated.pending_created_at;
455
- changed = true;
456
- }
457
- if (changed)
458
- this._upsertGroupCurrent(groupId, epoch, currentSecret, updated, now);
459
- return true;
460
- }
461
- if (localEpoch !== epoch) {
462
- const expiresAt = Number(current.updated_at || now) + opts.oldEpochRetentionMs;
463
- this._upsertGroupOldEpoch(groupId, localEpoch, currentSecret, currentData, Number(current.updated_at || now), expiresAt);
464
- }
465
- else {
466
- // epoch === localEpoch 但 currentSecret 为空:合并 data,不覆盖已有字段
467
- const merged = { ...currentData, ...this._buildGroupCurrentData(opts, members, now) };
468
- // 保留已有的 epoch_chain(如果新值为空)
469
- if (!opts.epochChain && currentData.epoch_chain) {
470
- merged.epoch_chain = currentData.epoch_chain;
471
- }
472
- if (currentData.pending_rotation_id && !opts.pendingRotationId) {
473
- merged.pending_rotation_id = currentData.pending_rotation_id;
474
- if (currentData.pending_created_at)
475
- merged.pending_created_at = currentData.pending_created_at;
476
- }
477
- this._upsertGroupCurrent(groupId, epoch, opts.secret, merged, now);
478
- return true;
479
- }
480
- }
481
- this._upsertGroupCurrent(groupId, epoch, opts.secret, this._buildGroupCurrentData(opts, members, now), now);
482
- return true;
483
- }));
484
- }
485
- storeGroupSecretEpoch(groupId, opts) {
486
- return Boolean(runImmediateTransaction(this._db, () => {
487
- const now = Date.now();
488
- const epoch = Number(opts.epoch);
489
- const members = [...(opts.memberAids ?? [])].map((item) => String(item)).sort();
490
- const pendingRotationId = String(opts.pendingRotationId ?? '').trim();
491
- const data = this._buildGroupCurrentData(opts, members, now);
492
- const current = this._db.prepare('SELECT epoch, secret_enc, data, updated_at FROM group_current WHERE group_id = ?').get(groupId);
493
- if (!current) {
494
- this._upsertGroupCurrent(groupId, epoch, opts.secret, data, now);
495
- return true;
496
- }
497
- const localEpoch = Number(current.epoch);
498
- if (epoch > localEpoch) {
499
- // 归档旧 epoch 到 old_epochs,然后用新 epoch 更新 current
500
- const oldSecret = this._revealText(`group/${groupId}/current`, current.secret_enc);
501
- const oldData = jsonParseObject(current.data);
502
- const expiresAt = now + (opts.oldEpochRetentionMs ?? 604800_000);
503
- this._upsertGroupOldEpoch(groupId, localEpoch, oldSecret, oldData, current.updated_at, expiresAt);
504
- this._upsertGroupCurrent(groupId, epoch, opts.secret, data, now);
505
- return true;
506
- }
507
- if (epoch === localEpoch) {
508
- const currentSecret = this._revealText(`group/${groupId}/current`, current.secret_enc);
509
- const currentData = jsonParseObject(current.data);
510
- if (currentSecret && currentSecret !== opts.secret) {
511
- if (!String(currentData.pending_rotation_id ?? '').trim())
512
- return false;
513
- this._upsertGroupCurrent(groupId, epoch, opts.secret, data, now);
514
- return true;
515
- }
516
- const updated = { ...currentData };
517
- let changed = false;
518
- const oldMembers = Array.isArray(updated.member_aids) ? updated.member_aids.map(String).sort() : [];
519
- if (members.length > 0 && JSON.stringify(oldMembers) !== JSON.stringify(members)) {
520
- updated.member_aids = members;
521
- updated.commitment = opts.commitment;
522
- changed = true;
523
- }
524
- if (opts.epochChain !== undefined && updated.epoch_chain !== opts.epochChain) {
525
- updated.epoch_chain = opts.epochChain;
526
- changed = true;
527
- }
528
- if (opts.epochChainUnverified === true) {
529
- if (updated.epoch_chain_unverified !== true) {
530
- updated.epoch_chain_unverified = true;
531
- changed = true;
532
- }
533
- if (opts.epochChainUnverifiedReason && updated.epoch_chain_unverified_reason !== opts.epochChainUnverifiedReason) {
534
- updated.epoch_chain_unverified_reason = opts.epochChainUnverifiedReason;
535
- changed = true;
536
- }
537
- }
538
- else if (opts.epochChainUnverified === false) {
539
- if ('epoch_chain_unverified' in updated || 'epoch_chain_unverified_reason' in updated) {
540
- delete updated.epoch_chain_unverified;
541
- delete updated.epoch_chain_unverified_reason;
542
- changed = true;
543
- }
544
- }
545
- if (pendingRotationId && updated.pending_rotation_id !== pendingRotationId) {
546
- updated.pending_rotation_id = pendingRotationId;
547
- updated.pending_created_at = now;
548
- changed = true;
549
- }
550
- if (!pendingRotationId && updated.pending_rotation_id) {
551
- delete updated.pending_rotation_id;
552
- delete updated.pending_created_at;
553
- changed = true;
554
- }
555
- if (changed)
556
- this._upsertGroupCurrent(groupId, epoch, currentSecret, updated, now);
557
- return true;
558
- }
559
- const old = this._db.prepare('SELECT secret_enc FROM group_old_epochs WHERE group_id = ? AND epoch = ?').get(groupId, epoch);
560
- if (old) {
561
- const oldSecret = this._revealText(`group/${groupId}/epoch/${epoch}`, old.secret_enc);
562
- if (oldSecret && oldSecret !== opts.secret)
563
- return false;
564
- }
565
- this._upsertGroupOldEpoch(groupId, epoch, opts.secret, data, now, now + opts.oldEpochRetentionMs);
566
- return true;
567
- }));
568
- }
569
- discardPendingGroupSecretState(groupId, epoch, rotationId) {
570
- return Boolean(runImmediateTransaction(this._db, () => {
571
- const rid = String(rotationId ?? '').trim();
572
- if (!rid)
573
- return false;
574
- const current = this._db.prepare('SELECT epoch, data FROM group_current WHERE group_id = ?').get(groupId);
575
- if (!current || Number(current.epoch) !== Number(epoch))
576
- return false;
577
- const data = jsonParseObject(current.data);
578
- if (String(data.pending_rotation_id ?? '').trim() !== rid)
579
- return false;
580
- const old = this._db.prepare('SELECT epoch, secret_enc, data, updated_at, expires_at FROM group_old_epochs WHERE group_id = ? AND epoch < ? ORDER BY epoch DESC LIMIT 1').get(groupId, epoch);
581
- if (!old) {
582
- this._db.prepare('DELETE FROM group_current WHERE group_id = ?').run(groupId);
583
- this._db.prepare('DELETE FROM group_old_epochs WHERE group_id = ?').run(groupId);
584
- return true;
585
- }
586
- const secret = this._revealText(`group/${groupId}/epoch/${old.epoch}`, old.secret_enc);
587
- this._upsertGroupCurrent(groupId, Number(old.epoch), secret, jsonParseObject(old.data), Date.now());
588
- this._db.prepare('DELETE FROM group_old_epochs WHERE group_id = ? AND epoch = ?').run(groupId, old.epoch);
589
- return true;
590
- }));
591
- }
592
- _buildGroupCurrentData(opts, memberAids, now) {
593
- const data = {
594
- commitment: opts.commitment,
595
- member_aids: memberAids,
596
- };
597
- if (opts.epochChain !== undefined)
598
- data.epoch_chain = opts.epochChain;
599
- const pendingRotationId = String(opts.pendingRotationId ?? '').trim();
600
- if (pendingRotationId) {
601
- data.pending_rotation_id = pendingRotationId;
602
- data.pending_created_at = now;
603
- }
604
- if (opts.epochChainUnverified === true) {
605
- data.epoch_chain_unverified = true;
606
- if (opts.epochChainUnverifiedReason)
607
- data.epoch_chain_unverified_reason = opts.epochChainUnverifiedReason;
608
- }
609
- return data;
610
- }
611
- _upsertGroupCurrent(groupId, epoch, secret, data, updatedAt) {
612
- this._db.prepare(`INSERT INTO group_current (group_id, epoch, secret_enc, data, updated_at)
613
- VALUES (?, ?, ?, ?, ?)
614
- ON CONFLICT(group_id) DO UPDATE SET epoch=excluded.epoch, secret_enc=excluded.secret_enc, data=excluded.data, updated_at=excluded.updated_at`).run(groupId, epoch, this._protectText(`group/${groupId}/current`, secret), JSON.stringify(data), updatedAt);
615
- }
616
- _upsertGroupOldEpoch(groupId, epoch, secret, data, updatedAt, expiresAt) {
617
- this._db.prepare(`INSERT INTO group_old_epochs (group_id, epoch, secret_enc, data, updated_at, expires_at)
618
- VALUES (?, ?, ?, ?, ?, ?)
619
- ON CONFLICT(group_id, epoch) DO UPDATE SET secret_enc=excluded.secret_enc, data=excluded.data, updated_at=excluded.updated_at, expires_at=excluded.expires_at`).run(groupId, epoch, this._protectText(`group/${groupId}/epoch/${epoch}`, secret), JSON.stringify(data), updatedAt, expiresAt ?? null);
620
- }
621
- saveSession(sessionId, data) {
622
- const dataJson = JSON.stringify(data);
623
- this._db.prepare('INSERT INTO e2ee_sessions (session_id, data_enc, updated_at) VALUES (?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET data_enc=excluded.data_enc, updated_at=excluded.updated_at').run(sessionId, this._protectText(`session/${sessionId}`, dataJson), Date.now());
624
- }
625
- loadAllSessions() {
626
- const rows = this._db.prepare('SELECT session_id, data_enc, updated_at FROM e2ee_sessions').all();
627
- return rows.map((row) => ({
628
- ...jsonParseObject(this._revealText(`session/${row.session_id}`, row.data_enc)),
629
- session_id: row.session_id,
630
- updated_at: row.updated_at,
631
- }));
632
- }
633
- deleteSession(sessionId) {
634
- this._db.prepare('DELETE FROM e2ee_sessions WHERE session_id = ?').run(sessionId);
635
- }
636
139
  saveInstanceState(deviceId, slotId, state) {
637
140
  const slot = slotId || '_singleton';
638
141
  this._db.prepare('INSERT INTO instance_state (device_id, slot_id, data, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(device_id, slot_id) DO UPDATE SET data=excluded.data, updated_at=excluded.updated_at').run(deviceId, slot, JSON.stringify(state), Date.now());
@@ -680,10 +183,9 @@ export class AIDDatabase {
680
183
  result[row.key] = row.value;
681
184
  return result;
682
185
  }
683
- // ── Group State ─────────────────────────────────────────────
684
186
  saveGroupState(groupId, stateVersion, stateHash, keyEpoch, membershipJson, policyJson) {
685
- this._db.prepare(`INSERT INTO group_state (group_id, state_version, state_hash, key_epoch, membership_json, policy_json, updated_at)
686
- VALUES (?, ?, ?, ?, ?, ?, ?)
187
+ this._db.prepare(`INSERT INTO group_state (group_id, state_version, state_hash, key_epoch, membership_json, policy_json, updated_at)
188
+ VALUES (?, ?, ?, ?, ?, ?, ?)
687
189
  ON CONFLICT(group_id) DO UPDATE SET state_version=excluded.state_version, state_hash=excluded.state_hash, key_epoch=excluded.key_epoch, membership_json=excluded.membership_json, policy_json=excluded.policy_json, updated_at=excluded.updated_at`).run(groupId, stateVersion, stateHash, keyEpoch, membershipJson, policyJson, Date.now());
688
190
  }
689
191
  loadGroupState(groupId) {