@rikaidev/yomi 0.2.6

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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +81 -0
  3. package/PRIVACY.md +39 -0
  4. package/README.md +659 -0
  5. package/dist/auth/credential-store.js +395 -0
  6. package/dist/auth/keychain.js +256 -0
  7. package/dist/cli/login.js +142 -0
  8. package/dist/line/auth/protocol/e2ee-bootstrap.js +87 -0
  9. package/dist/line/auth/protocol/index.js +3 -0
  10. package/dist/line/auth/protocol/login-metadata.js +159 -0
  11. package/dist/line/auth/protocol/pwless-login-flow.js +83 -0
  12. package/dist/line/auth/pwless/index.js +163 -0
  13. package/dist/line/auth/pwless/steps.js +213 -0
  14. package/dist/line/client/auth-service/client.js +240 -0
  15. package/dist/line/client/e2ee-service/client.js +69 -0
  16. package/dist/line/client/e2ee-service/requests.js +92 -0
  17. package/dist/line/client/e2ee-service/shared-key-log.js +24 -0
  18. package/dist/line/client/e2ee-service/shared-key-payload.js +35 -0
  19. package/dist/line/client/index.js +213 -0
  20. package/dist/line/client/obs-media-client.js +140 -0
  21. package/dist/line/client/parsers.js +69 -0
  22. package/dist/line/client/shop-service/client.js +63 -0
  23. package/dist/line/client/sticker-meta.js +46 -0
  24. package/dist/line/client/sync-service/client.js +317 -0
  25. package/dist/line/client/talk-service/chat-query.js +122 -0
  26. package/dist/line/client/talk-service/command-client.js +242 -0
  27. package/dist/line/client/talk-service/contact-query.js +77 -0
  28. package/dist/line/client/talk-service/message-box-query.js +137 -0
  29. package/dist/line/client/talk-service/message-id-query.js +241 -0
  30. package/dist/line/client/talk-service/query-client.js +219 -0
  31. package/dist/line/client/talk-service/requests.js +383 -0
  32. package/dist/line/client/transport.js +138 -0
  33. package/dist/line/core/auth-session-logout.js +55 -0
  34. package/dist/line/core/auth-session-runtime.js +231 -0
  35. package/dist/line/core/auth-session-service.js +85 -0
  36. package/dist/line/core/chat-runtime-service.js +343 -0
  37. package/dist/line/core/client-runtime.js +17 -0
  38. package/dist/line/core/config.js +67 -0
  39. package/dist/line/core/constants.js +56 -0
  40. package/dist/line/core/contact-query-service.js +20 -0
  41. package/dist/line/core/directory-runtime-service.js +83 -0
  42. package/dist/line/core/e2ee/bootstrap/keychain.js +41 -0
  43. package/dist/line/core/e2ee/crypto/crypto-primitives.js +98 -0
  44. package/dist/line/core/e2ee/index.js +6 -0
  45. package/dist/line/core/e2ee/keys/decrypt.js +110 -0
  46. package/dist/line/core/e2ee/keys/encrypt.js +117 -0
  47. package/dist/line/core/e2ee/keys/group-key.js +260 -0
  48. package/dist/line/core/e2ee/keys/key-manager.js +231 -0
  49. package/dist/line/core/e2ee/keys/key-payload.js +130 -0
  50. package/dist/line/core/e2ee/keys/key-types.js +1 -0
  51. package/dist/line/core/e2ee/keys/message-crypto.js +146 -0
  52. package/dist/line/core/e2ee/keys/message-decrypt.js +390 -0
  53. package/dist/line/core/e2ee/keys/peer-key.js +191 -0
  54. package/dist/line/core/e2ee/recovery/bootstrap-keys.js +161 -0
  55. package/dist/line/core/e2ee/types.js +4 -0
  56. package/dist/line/core/group-query-service.js +20 -0
  57. package/dist/line/core/media-decrypt.js +78 -0
  58. package/dist/line/core/media-encrypt.js +97 -0
  59. package/dist/line/core/mention.js +89 -0
  60. package/dist/line/core/message-command-service.js +454 -0
  61. package/dist/line/core/message-query-service.js +161 -0
  62. package/dist/line/core/service.js +134 -0
  63. package/dist/line/core/session-state/index.js +243 -0
  64. package/dist/line/core/session-state/persist.js +192 -0
  65. package/dist/line/core/store.js +8 -0
  66. package/dist/line/core/thrift/binary/binary-reader.js +151 -0
  67. package/dist/line/core/thrift/binary/binary-writer.js +157 -0
  68. package/dist/line/core/thrift/binary/type-codec-dsl.js +42 -0
  69. package/dist/line/core/thrift/compact/compact-reader.js +180 -0
  70. package/dist/line/core/thrift/compact/compact-writer.js +161 -0
  71. package/dist/line/core/thrift/compact/type-codec-dsl.js +66 -0
  72. package/dist/line/core/thrift/fields/builders.js +124 -0
  73. package/dist/line/core/thrift/index.js +12 -0
  74. package/dist/line/core/thrift/message/decoder.js +55 -0
  75. package/dist/line/core/thrift/message/encoder.js +25 -0
  76. package/dist/line/core/thrift/primitives/varint.js +64 -0
  77. package/dist/line/core/thrift/struct/reader.js +172 -0
  78. package/dist/line/core/thrift/struct/writer.js +189 -0
  79. package/dist/line/core/thrift/types.js +23 -0
  80. package/dist/line/core/video-thumbnail.js +104 -0
  81. package/dist/mcp/handlers/contacts.js +166 -0
  82. package/dist/mcp/handlers/conversations.js +193 -0
  83. package/dist/mcp/handlers/groups.js +123 -0
  84. package/dist/mcp/handlers/index.js +15 -0
  85. package/dist/mcp/handlers/login.js +473 -0
  86. package/dist/mcp/handlers/media.js +69 -0
  87. package/dist/mcp/handlers/message-actions.js +62 -0
  88. package/dist/mcp/handlers/scope.js +120 -0
  89. package/dist/mcp/handlers/search.js +196 -0
  90. package/dist/mcp/handlers/send.js +523 -0
  91. package/dist/mcp/handlers/shared.js +84 -0
  92. package/dist/mcp/handlers/stickers.js +87 -0
  93. package/dist/mcp/media.js +228 -0
  94. package/dist/mcp/names.js +133 -0
  95. package/dist/mcp/phi-guard.js +105 -0
  96. package/dist/mcp/policy.js +34 -0
  97. package/dist/mcp/server.js +301 -0
  98. package/dist/mcp/tools.js +761 -0
  99. package/dist/mcp/ui/capability.js +26 -0
  100. package/dist/mcp/ui/login-app-protocol.js +118 -0
  101. package/dist/mcp/ui/login-app-styles.js +142 -0
  102. package/dist/mcp/ui/login-app-view.js +361 -0
  103. package/dist/mcp/ui/login-app.js +72 -0
  104. package/dist/mcp/ui/login-structured-content.js +1 -0
  105. package/dist/mcp/ui/resource.js +35 -0
  106. package/dist/mcp/ui/tools-with-ui.js +40 -0
  107. package/dist/search/bigram.js +86 -0
  108. package/dist/search/capture.js +140 -0
  109. package/dist/search/collector.js +176 -0
  110. package/dist/search/default-embedder.js +101 -0
  111. package/dist/search/embedder.js +13 -0
  112. package/dist/search/scope.js +142 -0
  113. package/dist/search/sqlite.js +141 -0
  114. package/dist/search/store.js +469 -0
  115. package/dist/util/data-dir.js +55 -0
  116. package/dist/util/log.js +86 -0
  117. package/dist/version.js +7 -0
  118. package/package.json +63 -0
  119. package/run.mjs +71 -0
@@ -0,0 +1,260 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { aesDecrypt, computeSharedSecret, deriveKeyAndIV, } from '../crypto/crypto-primitives.js';
3
+ import { getPeerPublicKey } from './peer-key.js';
4
+ /**
5
+ * Read one field from a raw shared-key payload using thrift or named access.
6
+ *
7
+ * @param shared - Raw shared-key payload.
8
+ * @param tupleField - Numeric thrift field id.
9
+ * @param namedField - Named object field.
10
+ * @returns Raw field value or null.
11
+ */
12
+ function readSharedField(shared, tupleField, namedField) {
13
+ return shared?.[tupleField] ?? shared?.[namedField] ?? null;
14
+ }
15
+ /**
16
+ * Read one shared-key field and coerce it into a string with fallback support.
17
+ *
18
+ * @param shared - Raw shared-key payload.
19
+ * @param tupleField - Numeric thrift field id.
20
+ * @param namedField - Named object field.
21
+ * @param fallback - Fallback string when the field is absent.
22
+ * @returns String value.
23
+ */
24
+ function readSharedString(shared, tupleField, namedField, fallback = '') {
25
+ return String(readSharedField(shared, tupleField, namedField) ?? fallback);
26
+ }
27
+ /**
28
+ * Extract structured fields from a raw LINE group shared key payload.
29
+ * Accepts both thrift tuple and named-property shapes.
30
+ *
31
+ * @param shared - Raw shared key payload
32
+ * @param receiverKeyId - Fallback receiver key ID
33
+ * @param senderMid - Fallback creator MID
34
+ * @param senderKeyId - Fallback creator key ID
35
+ * @returns Normalized fields needed for key derivation
36
+ */
37
+ function extractSharedKeyFields(shared, receiverKeyId, senderMid, senderKeyId) {
38
+ const sharedReceiverKeyId = readSharedString(shared, 6, 'receiverKeyId', receiverKeyId);
39
+ const groupKeyId = readSharedString(shared, 2, 'groupKeyId', sharedReceiverKeyId);
40
+ const rawCreator = readSharedField(shared, 3, 'creator');
41
+ const creator = rawCreator == null ? senderMid : String(rawCreator);
42
+ const creatorKeyId = readSharedString(shared, 4, 'creatorKeyId', senderKeyId ?? '');
43
+ const encryptedSharedKey = readSharedField(shared, 7, 'encryptedSharedKey');
44
+ return {
45
+ creator,
46
+ creatorKeyId,
47
+ sharedReceiverKeyId,
48
+ encryptedSharedKey,
49
+ groupKeyId,
50
+ };
51
+ }
52
+ /**
53
+ * Check memory and persisted store for a cached group key.
54
+ *
55
+ * @param ctx - KeyManager context
56
+ * @param chatMid - Group chat MID
57
+ * @param receiverKeyId - Expected key ID
58
+ * @returns Cached group key or null
59
+ */
60
+ async function lookupCachedGroupKey(ctx, chatMid, receiverKeyId) {
61
+ // Epoch-aware cache: keys are stored per (chat, groupKeyId) and never
62
+ // overwritten, so once this device has seen a group-key epoch it keeps
63
+ // decrypting that epoch's messages even after the group rekeys. Without a
64
+ // specific epoch (the send path asks for "latest"), skip the cache and let
65
+ // the caller fetch the current key from LINE.
66
+ if (!receiverKeyId) {
67
+ return null;
68
+ }
69
+ const cacheKey = `${chatMid}:${receiverKeyId}`;
70
+ const cached = ctx.groupKeys.get(cacheKey);
71
+ if (cached) {
72
+ ctx.logGroupKeyEvent('e2ee.group_key.cache_hit', {
73
+ chat: chatMid,
74
+ source: 'memory',
75
+ receiver_key_id: receiverKeyId,
76
+ group_key_id: cached.keyId,
77
+ });
78
+ return cached;
79
+ }
80
+ const store = ctx.getStore();
81
+ const persisted = await store?.get?.(`line_e2ee_group_by_mid:${cacheKey}`);
82
+ if (!persisted) {
83
+ return null;
84
+ }
85
+ const parsed = JSON.parse(persisted);
86
+ const groupKey = {
87
+ keyId: String(parsed.keyId),
88
+ privateKey: Buffer.from(parsed.privateKey, 'base64'),
89
+ };
90
+ ctx.groupKeys.set(cacheKey, groupKey);
91
+ ctx.logGroupKeyEvent('e2ee.group_key.cache_hit', {
92
+ chat: chatMid,
93
+ source: 'persisted',
94
+ receiver_key_id: receiverKeyId,
95
+ group_key_id: groupKey.keyId,
96
+ });
97
+ return groupKey;
98
+ }
99
+ /**
100
+ * Return a stable fetch key for one group-key resolution request.
101
+ *
102
+ * @param chatMid - Group chat MID
103
+ * @param receiverKeyId - Expected receiver key id
104
+ * @returns Stable in-flight fetch key
105
+ */
106
+ function getGroupKeyFetchKey(chatMid, receiverKeyId) {
107
+ return `${chatMid}:${receiverKeyId}`;
108
+ }
109
+ /**
110
+ * Derive and cache a group key from the shared key payload.
111
+ *
112
+ * @param ctx - KeyManager context
113
+ * @param chatMid - Group chat MID
114
+ * @param shared - Raw shared key payload from LINE
115
+ * @param receiverKeyId - Receiver key ID fallback
116
+ * @param senderMid - Sender MID fallback
117
+ * @param senderKeyId - Sender key ID fallback
118
+ * @param resolutionSource - Label used in diagnostic logs
119
+ * @returns Derived group key
120
+ */
121
+ async function deriveAndCacheGroupKey(ctx, chatMid, shared, receiverKeyId, senderMid, senderKeyId, resolutionSource) {
122
+ const { creator, creatorKeyId, sharedReceiverKeyId, encryptedSharedKey, groupKeyId, } = extractSharedKeyFields(shared, receiverKeyId, senderMid, senderKeyId);
123
+ const selfKey = ctx.getSelfKeyById(sharedReceiverKeyId);
124
+ if (!selfKey) {
125
+ ctx.raiseWarning('missing_self_key', {
126
+ receiverKeyId: sharedReceiverKeyId,
127
+ chatMid,
128
+ });
129
+ throw new Error(`Missing self E2EE key for receiverKeyId=${sharedReceiverKeyId}`);
130
+ }
131
+ if (!creator || !creatorKeyId || !encryptedSharedKey) {
132
+ throw new Error('Incomplete group shared key payload');
133
+ }
134
+ const selfMid = ctx.getProfileMid();
135
+ const creatorPublicKey = creator === selfMid
136
+ ? selfKey.publicKey
137
+ : await getPeerPublicKey(ctx, creator, creatorKeyId);
138
+ const aesSecret = computeSharedSecret(selfKey.privateKey, creatorPublicKey);
139
+ const { key, iv } = deriveKeyAndIV(aesSecret);
140
+ const raw = encryptedSharedKey;
141
+ const encrypted = Buffer.isBuffer(raw)
142
+ ? raw
143
+ : Buffer.from(raw, typeof raw === 'string' ? 'base64' : undefined);
144
+ const plainText = aesDecrypt(encrypted, key, iv);
145
+ const groupKey = { keyId: groupKeyId, privateKey: plainText };
146
+ // Cache under (chat, groupKeyId) — only ever add, never overwrite a
147
+ // different epoch, so a later rekey cannot strand messages this device can
148
+ // already read.
149
+ const cacheKey = `${chatMid}:${groupKey.keyId}`;
150
+ ctx.groupKeys.set(cacheKey, groupKey);
151
+ const store = ctx.getStore();
152
+ await store?.set?.(`line_e2ee_group_by_mid:${cacheKey}`, JSON.stringify({
153
+ keyId: groupKey.keyId,
154
+ privateKey: groupKey.privateKey.toString('base64'),
155
+ }));
156
+ ctx.logGroupKeyEvent('e2ee.group_key.resolved', {
157
+ chat: chatMid,
158
+ source: resolutionSource,
159
+ receiver_key_id: sharedReceiverKeyId,
160
+ creator,
161
+ creator_key_id: creatorKeyId,
162
+ group_key_id: groupKeyId,
163
+ encrypted_shared_key_bytes: encrypted.length,
164
+ });
165
+ return groupKey;
166
+ }
167
+ /**
168
+ * Resolve and cache the group shared key for a chat.
169
+ *
170
+ * Yomi is read/send-only over group keys: it resolves an EXISTING shared key
171
+ * or returns undefined. It NEVER registers (mints) a group key. Minting is an
172
+ * account-visible write that rotates the group's shared secret for every
173
+ * member; every message encrypted under the previous secret — all of it, for
174
+ * every sender — then fails to decrypt, and members who don't adopt the new
175
+ * secret are stranded too. Group-key lifecycle belongs to the official LINE
176
+ * clients / the primary device, never to Yomi.
177
+ *
178
+ * @param ctx - KeyManager context
179
+ * @param chatMid - Group chat MID
180
+ * @param receiverKeyId - Local receiver key ID
181
+ * @param senderMid - Sender MID
182
+ * @param senderKeyId - Sender key ID
183
+ * @returns Group key or undefined when unavailable
184
+ */
185
+ export async function getGroupKey(ctx, chatMid, receiverKeyId, senderMid, senderKeyId) {
186
+ if (!receiverKeyId) {
187
+ const cachedLatest = await lookupCachedGroupKey(ctx, chatMid, null);
188
+ if (cachedLatest) {
189
+ return cachedLatest;
190
+ }
191
+ }
192
+ const cached = await lookupCachedGroupKey(ctx, chatMid, receiverKeyId);
193
+ if (cached) {
194
+ return cached;
195
+ }
196
+ const resolvedReceiverKeyId = receiverKeyId || 'latest';
197
+ const fetchKey = getGroupKeyFetchKey(chatMid, resolvedReceiverKeyId);
198
+ const inflight = ctx.groupKeyFetches.get(fetchKey);
199
+ if (inflight) {
200
+ ctx.logGroupKeyEvent('e2ee.group_key.fetch_join', {
201
+ chat: chatMid,
202
+ receiver_key_id: receiverKeyId,
203
+ inflight_receiver_key_id: inflight.receiverKeyId,
204
+ });
205
+ return inflight.promise;
206
+ }
207
+ const fetchPromise = (async () => {
208
+ const client = ctx.getClient();
209
+ let shared;
210
+ let resolutionSource = 'server_fetch';
211
+ try {
212
+ shared = await client?.getLastE2EEGroupSharedKey?.(2, chatMid);
213
+ }
214
+ catch (error) {
215
+ const code = typeof error?.data?.code === 'string' ? error.data.code : null;
216
+ const message = error?.message || String(error);
217
+ const shouldFallback = code === 'NOT_FOUND' ||
218
+ message.toLowerCase().includes('no valid group key');
219
+ if (!shouldFallback) {
220
+ ctx.logGroupKeyEvent('e2ee.group_key.fetch_failed', {
221
+ chat: chatMid,
222
+ receiver_key_id: receiverKeyId,
223
+ code,
224
+ error: message,
225
+ });
226
+ throw error;
227
+ }
228
+ resolutionSource = 'register_fallback';
229
+ }
230
+ if (!shared) {
231
+ // Yomi never mints. LINE has no readable group key for this
232
+ // (receiverKeyId, chat) — fail cleanly rather than register one and
233
+ // rotate the group's secret out from under every other member.
234
+ ctx.logGroupKeyEvent('e2ee.group_key.mint_suppressed', {
235
+ chat: chatMid,
236
+ receiver_key_id: receiverKeyId,
237
+ });
238
+ }
239
+ if (!shared) {
240
+ ctx.logGroupKeyEvent('e2ee.group_key.unavailable', {
241
+ chat: chatMid,
242
+ receiver_key_id: receiverKeyId,
243
+ source: resolutionSource,
244
+ });
245
+ return undefined;
246
+ }
247
+ return deriveAndCacheGroupKey(ctx, chatMid, shared, resolvedReceiverKeyId, senderMid, senderKeyId, resolutionSource);
248
+ })();
249
+ ctx.groupKeyFetches.set(fetchKey, {
250
+ receiverKeyId: resolvedReceiverKeyId,
251
+ promise: fetchPromise,
252
+ });
253
+ try {
254
+ return await fetchPromise;
255
+ }
256
+ finally {
257
+ ctx.groupKeyFetches.delete(fetchKey);
258
+ }
259
+ }
260
+ export { normalizeGroupPublicKeys } from './key-payload.js';
@@ -0,0 +1,231 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { generateKeyPairSync } from 'node:crypto';
3
+ import { createCliLogger } from '../../../../util/log.js';
4
+ import { tryDecrypt as tryDecryptFn } from './decrypt.js';
5
+ import { encryptE2EEMessage as encryptE2EEMessageFn } from './encrypt.js';
6
+ import { toChunkKeyId } from './message-crypto.js';
7
+ const e2eeLog = createCliLogger('E2EE');
8
+ /**
9
+ * KeyManager handles E2EE key generation and storage.
10
+ * Implements {@link KeyManagerContext} so it can be passed as `ctx` to module-level functions.
11
+ */
12
+ export class KeyManager {
13
+ keys = new Map();
14
+ importedKeys = new Map();
15
+ selfKeyByMid = new Map();
16
+ peerPublicKeys = new Map();
17
+ groupKeys = new Map();
18
+ groupKeyFetches = new Map();
19
+ runtimeGetClient;
20
+ runtimeGetStore;
21
+ runtimeGetProfileMid;
22
+ emitWarning;
23
+ /**
24
+ * Generate a new X25519 key pair for E2EE.
25
+ * @returns Generated key pair
26
+ */
27
+ generateKeyPair() {
28
+ const keyPair = generateKeyPairSync('x25519');
29
+ return {
30
+ publicKey: keyPair.publicKey
31
+ .export({ type: 'spki', format: 'der' })
32
+ .slice(-32),
33
+ privateKey: keyPair.privateKey
34
+ .export({ type: 'pkcs8', format: 'der' })
35
+ .slice(-32),
36
+ };
37
+ }
38
+ /**
39
+ * Store a key pair for a user.
40
+ * @param userId - User identifier
41
+ * @param keyPair - Key pair to store
42
+ */
43
+ storeKey(userId, keyPair) {
44
+ this.keys.set(userId, keyPair);
45
+ }
46
+ /**
47
+ * Get a user's stored key pair.
48
+ * @param userId - User identifier.
49
+ * @returns Stored key pair when present.
50
+ */
51
+ getKey(userId) {
52
+ return this.keys.get(userId);
53
+ }
54
+ /**
55
+ * Check if a user has a stored key.
56
+ * @param userId - User identifier.
57
+ * @returns True when a key exists for the user.
58
+ */
59
+ hasKey(userId) {
60
+ return this.keys.has(userId);
61
+ }
62
+ /**
63
+ * Import E2EE keys decoded from the login keychain.
64
+ * Called after successful login with the decrypted key list.
65
+ * @param keys - Array of key objects from decryptKeyChain
66
+ */
67
+ importKeys(keys) {
68
+ for (const k of keys) {
69
+ const keyId = String(k.keyId);
70
+ const importedKey = {
71
+ keyId,
72
+ version: k.version != null ? String(k.version) : undefined,
73
+ createdTime: k.createdTime != null ? Number(k.createdTime) : null,
74
+ publicKey: Buffer.isBuffer(k.publicKey)
75
+ ? k.publicKey
76
+ : Buffer.from(k.publicKey),
77
+ privateKey: Buffer.isBuffer(k.privateKey)
78
+ ? k.privateKey
79
+ : Buffer.from(k.privateKey),
80
+ };
81
+ this.importedKeys.set(keyId, importedKey);
82
+ if (k.mid) {
83
+ this.selfKeyByMid.set(String(k.mid), importedKey);
84
+ }
85
+ }
86
+ this.resolveLogger().info('e2ee.keys.imported', {
87
+ count: keys.length,
88
+ });
89
+ }
90
+ /**
91
+ * Associate an imported self key with the authenticated user's MID.
92
+ * @param mid - Authenticated LINE MID
93
+ */
94
+ bindSelfKeysToMid(mid) {
95
+ const latest = [...this.importedKeys.values()].sort((a, b) => (b.createdTime ?? 0) - (a.createdTime ?? 0))[0];
96
+ if (latest) {
97
+ this.selfKeyByMid.set(mid, latest);
98
+ }
99
+ }
100
+ /**
101
+ * Return a self key by stored key ID.
102
+ * @param keyId - Imported self key identifier.
103
+ * @returns Matching imported key when present.
104
+ */
105
+ getSelfKeyById(keyId) {
106
+ return this.importedKeys.get(String(keyId));
107
+ }
108
+ /**
109
+ * Return the authenticated user's current self key by MID.
110
+ * @param mid - Authenticated LINE MID.
111
+ * @returns Matching imported key when present.
112
+ */
113
+ getSelfKeyByMid(mid) {
114
+ return this.selfKeyByMid.get(String(mid));
115
+ }
116
+ /**
117
+ * Clear only runtime-discovered E2EE caches while preserving imported self keys.
118
+ *
119
+ * This is used by session-level recovery: peer public keys and group shared
120
+ * keys may have become stale or incomplete, but the authenticated self key
121
+ * material should survive across retries until the user explicitly logs out.
122
+ */
123
+ resetTransientCaches() {
124
+ this.peerPublicKeys.clear();
125
+ this.groupKeys.clear();
126
+ this.groupKeyFetches.clear();
127
+ }
128
+ /**
129
+ * Bind runtime dependencies required for async E2EE key resolution.
130
+ * @param deps - Runtime dependency providers.
131
+ * @param deps.getClient - Returns the active LINE client.
132
+ * @param deps.getStore - Returns the credential store.
133
+ * @param deps.getProfileMid - Returns the authenticated profile MID.
134
+ * @param deps.emitWarning - Emits a recovery warning to the service.
135
+ */
136
+ setRuntime(deps) {
137
+ this.runtimeGetClient = deps.getClient;
138
+ this.runtimeGetStore = deps.getStore;
139
+ this.runtimeGetProfileMid = deps.getProfileMid;
140
+ this.emitWarning = deps.emitWarning;
141
+ }
142
+ /**
143
+ * Return the active LINE client when runtime dependencies are bound.
144
+ * @returns Active LINE client.
145
+ */
146
+ getClient() {
147
+ return this.runtimeGetClient?.();
148
+ }
149
+ /**
150
+ * Return the credential store when runtime dependencies are bound.
151
+ * @returns Credential store.
152
+ */
153
+ getStore() {
154
+ return this.runtimeGetStore?.();
155
+ }
156
+ /**
157
+ * Return the authenticated LINE MID when runtime dependencies are bound.
158
+ * @returns Authenticated LINE MID.
159
+ */
160
+ getProfileMid() {
161
+ return this.runtimeGetProfileMid?.();
162
+ }
163
+ /**
164
+ * Emit a recovery warning if the service is available.
165
+ * @param reason - Warning reason code.
166
+ * @param details - Additional warning metadata.
167
+ */
168
+ raiseWarning(reason, details = {}) {
169
+ this.emitWarning?.({ active: true, reason, ...details });
170
+ }
171
+ /**
172
+ * Emit one visible LINE runtime log for group-key diagnostics.
173
+ * @param event - Event name
174
+ * @param context - Structured payload
175
+ */
176
+ logGroupKeyEvent(event, context = {}) {
177
+ this.resolveLogger().info(event, context);
178
+ }
179
+ /**
180
+ * Emit one warning-level E2EE log through the shared CLI logger.
181
+ *
182
+ * @param event - Event name
183
+ * @param context - Structured payload
184
+ */
185
+ logE2EEWarning(event, context = {}) {
186
+ this.resolveLogger().warn(event, context);
187
+ }
188
+ /**
189
+ * Resolve the active E2EE logger using the shared logging contract.
190
+ *
191
+ * @returns Startup-aware CLI logger.
192
+ */
193
+ resolveLogger() {
194
+ const client = this.getClient();
195
+ return client?.startupFlowLogger || client?.logger || e2eeLog;
196
+ }
197
+ /**
198
+ * Attempt to decrypt an E2EE message using stored keys.
199
+ * Returns plaintext on success, or a no-op result for plaintext messages.
200
+ * @param msg - Parsed message object (from parsers.ts)
201
+ * @returns Decryption result with optional plaintext
202
+ */
203
+ async tryDecrypt(msg) {
204
+ return tryDecryptFn(this, msg);
205
+ }
206
+ /**
207
+ * Prepare an outbound LINE E2EE payload using the same chunk layout as linejs.
208
+ *
209
+ * The returned structure is transport-ready for TalkService sendMessage:
210
+ * `chunks` contains `[salt, ciphertext+tag, nonce, senderKeyIdBytes, receiverKeyIdBytes]`
211
+ * and `contentMetadata` includes the E2EE markers LINE expects for version 2.
212
+ *
213
+ * @param to - Target LINE MID
214
+ * @param data - Text or structured payload to encrypt
215
+ * @param contentType - LINE content type of the payload
216
+ * @returns Message chunks plus metadata required by LINE
217
+ */
218
+ async encryptE2EEMessage(to, data, contentType = 0) {
219
+ return encryptE2EEMessageFn(this, to, data, contentType);
220
+ }
221
+ /**
222
+ * Public wrapper used by logging/initializer layers to format key IDs from
223
+ * raw message chunks without duplicating chunk parsing logic outside E2EE.
224
+ *
225
+ * @param value - Raw LINE chunk value
226
+ * @returns Parsed decimal key ID or null
227
+ */
228
+ readChunkKeyId(value) {
229
+ return toChunkKeyId(value);
230
+ }
231
+ }
@@ -0,0 +1,130 @@
1
+ import { Buffer } from 'node:buffer';
2
+ /**
3
+ * Read one normalized field from a negotiated public-key candidate payload.
4
+ *
5
+ * @param candidate - Raw candidate payload.
6
+ * @param fieldId - Target thrift field id.
7
+ * @returns Raw field value or null.
8
+ */
9
+ function readCandidateField(candidate, fieldId) {
10
+ if (!candidate) {
11
+ return null;
12
+ }
13
+ if (fieldId === 2) {
14
+ return (candidate[2] ??
15
+ candidate.keyId ??
16
+ candidate.publicKey?.[2] ??
17
+ candidate.publicKey?.keyId ??
18
+ null);
19
+ }
20
+ return (candidate[4] ??
21
+ candidate.keyData ??
22
+ candidate.publicKey?.[4] ??
23
+ candidate.publicKey?.keyData ??
24
+ null);
25
+ }
26
+ /**
27
+ * Select the nested object that actually contains public key fields.
28
+ *
29
+ * @param raw - Raw negotiation response from LINE.
30
+ * @returns Normalized container object.
31
+ */
32
+ function readPublicKeyContainer(raw) {
33
+ return raw?.[2] || raw?.publicKey || raw?.fields?.[2] || raw || null;
34
+ }
35
+ /**
36
+ * Read one normalized field from the selected negotiated public-key container.
37
+ *
38
+ * @param publicKey - Selected public-key container.
39
+ * @param raw - Raw negotiation response.
40
+ * @param fieldId - Target thrift field id.
41
+ * @returns Raw field value or null.
42
+ */
43
+ function readNegotiatedValue(publicKey, raw, fieldId) {
44
+ if (fieldId === 2) {
45
+ return publicKey?.[2] ?? publicKey?.keyId ?? raw?.keyId ?? null;
46
+ }
47
+ return publicKey?.[4] ?? publicKey?.keyData ?? raw?.keyData ?? null;
48
+ }
49
+ /**
50
+ * Normalize raw key payloads into Buffer instances.
51
+ *
52
+ * @param keyData - Raw key payload from thrift or object responses.
53
+ * @returns Buffer instance or null when unsupported.
54
+ */
55
+ export function toKeyBuffer(keyData) {
56
+ if (Buffer.isBuffer(keyData)) {
57
+ return keyData;
58
+ }
59
+ if (typeof keyData === 'string') {
60
+ return Buffer.from(keyData, 'base64');
61
+ }
62
+ if (keyData instanceof Uint8Array) {
63
+ return Buffer.from(keyData);
64
+ }
65
+ if (keyData instanceof ArrayBuffer) {
66
+ return Buffer.from(new Uint8Array(keyData));
67
+ }
68
+ return null;
69
+ }
70
+ /**
71
+ * Normalize the getLastE2EEPublicKeys response into a member->public-key map.
72
+ *
73
+ * linejs treats this response as an object keyed by member MID. Depending on
74
+ * our thrift decoder shape it may arrive as a plain object or as nested field
75
+ * tuples, so this helper accepts both and extracts only the fields required to
76
+ * register a new group key.
77
+ *
78
+ * @param raw - Raw getLastE2EEPublicKeys response
79
+ * @returns Map of member MID to key metadata
80
+ */
81
+ export function normalizeGroupPublicKeys(raw) {
82
+ const result = new Map();
83
+ const entries = raw && typeof raw === 'object' ? Object.entries(raw) : [];
84
+ for (const [mid, value] of entries) {
85
+ const candidate = value;
86
+ const keyId = readCandidateField(candidate, 2);
87
+ const keyData = readCandidateField(candidate, 4);
88
+ if (keyId == null || !keyData) {
89
+ continue;
90
+ }
91
+ const normalizedKeyData = toKeyBuffer(keyData);
92
+ if (!normalizedKeyData) {
93
+ continue;
94
+ }
95
+ result.set(String(mid), {
96
+ keyId: String(keyId),
97
+ keyData: normalizedKeyData,
98
+ });
99
+ }
100
+ return result;
101
+ }
102
+ /**
103
+ * Normalize a negotiateE2EEPublicKey response into the key shape used by the
104
+ * local runtime. LINE returns slightly different nested structs depending on
105
+ * endpoint version and thrift decoder shape; this helper accepts either.
106
+ *
107
+ * @param raw - Raw negotiation response from the LINE Talk service
108
+ * @returns Normalized key payload or null
109
+ */
110
+ export function normalizeNegotiatedPublicKey(raw) {
111
+ const publicKey = readPublicKeyContainer(raw);
112
+ const keyId = readNegotiatedValue(publicKey, raw, 2);
113
+ const keyData = readNegotiatedValue(publicKey, raw, 4);
114
+ if (keyId == null || keyData == null) {
115
+ return null;
116
+ }
117
+ if (!Buffer.isBuffer(keyData) &&
118
+ typeof keyData !== 'string' &&
119
+ !(keyData instanceof Uint8Array)) {
120
+ return null;
121
+ }
122
+ return {
123
+ keyId: String(keyId),
124
+ keyData: Buffer.isBuffer(keyData)
125
+ ? keyData
126
+ : typeof keyData === 'string'
127
+ ? Buffer.from(keyData, 'base64')
128
+ : Buffer.from(keyData),
129
+ };
130
+ }
@@ -0,0 +1 @@
1
+ export {};