@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,134 @@
1
+ /**
2
+ * LINE Protocol Service - Yomi's read-only session/query core.
3
+ *
4
+ * Orchestrates session resume (with auto token refresh) and E2EE
5
+ * decryption. Login and continuous polling live in the vendored auth
6
+ * modules but are never invoked by Yomi's MCP server — see README.md.
7
+ */
8
+ import { EventEmitter } from 'node:events';
9
+ import { CredentialStore } from '../../auth/credential-store.js';
10
+ import { yomiDataPath } from '../../util/data-dir.js';
11
+ import { createCliLogger } from '../../util/log.js';
12
+ import { createAuthSessionService } from './auth-session-service.js';
13
+ import { createChatRuntimeService } from './chat-runtime-service.js';
14
+ import { createDirectoryRuntimeService } from './directory-runtime-service.js';
15
+ import { E2EEKeyManager } from './e2ee/index.js';
16
+ import { LineSessionState } from './session-state/index.js';
17
+ export const STATE = {
18
+ DISCONNECTED: 'disconnected',
19
+ LOGGING_IN: 'logging_in',
20
+ CONNECTED: 'connected',
21
+ POLLING: 'polling',
22
+ ERROR: 'error',
23
+ };
24
+ /**
25
+ * High-level LINE service coordinating all aspects of the protocol.
26
+ * Used by Adapter and Agent layers.
27
+ */
28
+ export class LineProtocolService extends EventEmitter {
29
+ options;
30
+ state;
31
+ client;
32
+ profile;
33
+ startupFlowLogger;
34
+ logger;
35
+ credentialStore;
36
+ sessionState;
37
+ nameCache;
38
+ chatCache;
39
+ e2eeManager;
40
+ e2eeWarning;
41
+ loginRequired;
42
+ // Why loginRequired was set. 'revoked' = LINE invalidated the token (a
43
+ // competing login elsewhere); 'expired' = the token aged out and the silent
44
+ // refresh failed. Both recover via `login`, but they are different stories to
45
+ // tell the user, so the cause has to survive the trip to the MCP gate.
46
+ // Always cleared back to null wherever loginRequired goes false.
47
+ loginReason;
48
+ recentFetchState;
49
+ // The following are mixed in at construction time via Object.assign from
50
+ // createAuthSessionService / createDirectoryRuntimeService /
51
+ // createChatRuntimeService (see constructor below) — TS cannot see that
52
+ // assignment, so these use definite-assignment assertions. Yomi's MCP
53
+ // server calls resumeSession/sendMessage/getRecentMessages/
54
+ // getPreviousMessages/download* only; it never calls startPwlessLogin
55
+ // (still present for compile completeness).
56
+ startPwlessLogin;
57
+ logout;
58
+ invalidateSession;
59
+ tryRefreshToken;
60
+ resumeSession;
61
+ resolveName;
62
+ getContact;
63
+ getGroup;
64
+ listStickerPackages;
65
+ searchStickerPackages;
66
+ sendMessage;
67
+ sendImage;
68
+ sendFile;
69
+ sendAudio;
70
+ sendVideo;
71
+ sendContact;
72
+ sendSticker;
73
+ sendLocation;
74
+ getRecentMessages;
75
+ getPreviousMessages;
76
+ downloadMessageContent;
77
+ downloadMessageContentPreview;
78
+ markChatRead;
79
+ renameGroup;
80
+ inviteToGroup;
81
+ kickFromGroup;
82
+ leaveGroup;
83
+ createGroup;
84
+ reactToMessage;
85
+ cancelReaction;
86
+ unsendMessage;
87
+ addFriend;
88
+ blockContact;
89
+ unblockContact;
90
+ acceptInvitation;
91
+ constructor(options = {}) {
92
+ super();
93
+ this.options = { autoStart: true, pollInterval: 10000, ...options };
94
+ this.state = STATE.DISCONNECTED;
95
+ this.client = null;
96
+ this.profile = null;
97
+ this.logger = options.logger || createCliLogger('LINE');
98
+ this.startupFlowLogger = null;
99
+ this.credentialStore =
100
+ options.credentialStore ||
101
+ new CredentialStore('line',
102
+ // Absolute, per-user (see ../../util/data-dir.ts). This was the
103
+ // relative string 'data/line-credentials.json', which CredentialStore
104
+ // also derives the E2EE cache path from — so under Claude Desktop, whose
105
+ // cwd is `/`, the cache aimed at /data/ and silently never persisted.
106
+ this.options.credentialPath ?? yomiDataPath('line-credentials.json'));
107
+ this.sessionState = new LineSessionState(this.credentialStore);
108
+ this.nameCache = new Map();
109
+ this.chatCache = new Map();
110
+ this.e2eeManager = options.e2eeManager || new E2EEKeyManager();
111
+ this.e2eeWarning = false;
112
+ this.loginRequired = false;
113
+ this.loginReason = null;
114
+ this.recentFetchState = new Map();
115
+ this.e2eeManager.setRuntime({
116
+ getClient: () => this.client,
117
+ getStore: () => this.credentialStore,
118
+ getProfileMid: () => this.profile?.mid,
119
+ emitWarning: (payload) => {
120
+ this.e2eeWarning = payload.active;
121
+ this.emit('e2eeWarning', payload);
122
+ },
123
+ });
124
+ Object.assign(this, createAuthSessionService(this, STATE), createDirectoryRuntimeService(this), createChatRuntimeService(this));
125
+ }
126
+ /**
127
+ * Set the current service state and emit an event.
128
+ * @param newState - The new state to set.
129
+ */
130
+ setState(newState) {
131
+ this.state = newState;
132
+ this.emit('state', newState);
133
+ }
134
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * LINE session state manager.
3
+ *
4
+ * Acts as the single state owner for LINE auth/session persistence. All
5
+ * token/revision writes should flow through this class instead of writing
6
+ * directly to the credential store from scattered call sites.
7
+ */
8
+ import { loadStoredSessionFields, parseStoredNumber, persistLoginBootstrap, persistLoginCredentials, persistLoginCryptoMaterial, } from './persist.js';
9
+ /**
10
+ * Own persisted and in-memory LINE session state.
11
+ */
12
+ export class LineSessionState {
13
+ credentialStore;
14
+ client;
15
+ authToken;
16
+ refreshToken;
17
+ tokenIssueTimeEpochSec;
18
+ durationUntilRefreshInSec;
19
+ revision;
20
+ globalRevision;
21
+ individualRevision;
22
+ mid;
23
+ /**
24
+ * Create the LINE session state manager.
25
+ * @param credentialStore - Persistent credential store for LINE session data
26
+ */
27
+ constructor(credentialStore) {
28
+ this.credentialStore = credentialStore;
29
+ this.client = null;
30
+ this.authToken = null;
31
+ this.refreshToken = null;
32
+ this.tokenIssueTimeEpochSec = null;
33
+ this.durationUntilRefreshInSec = null;
34
+ this.revision = -1;
35
+ this.globalRevision = 0;
36
+ this.individualRevision = 0;
37
+ this.mid = null;
38
+ }
39
+ /**
40
+ * Bind the active runtime client to the current session snapshot.
41
+ * @param client - Active LINE client instance
42
+ */
43
+ bindClient(client) {
44
+ this.client = client;
45
+ if (this.authToken) {
46
+ client.authToken = this.authToken;
47
+ }
48
+ client.revision = this.revision;
49
+ client.globalRevision = this.globalRevision;
50
+ client.individualRevision = this.individualRevision;
51
+ }
52
+ /**
53
+ * Load persisted LINE session state from the credential store.
54
+ * @returns Promise that resolves when state has been hydrated
55
+ */
56
+ async loadFromStore() {
57
+ const stored = await loadStoredSessionFields(this.credentialStore);
58
+ this.authToken = stored.authToken;
59
+ this.refreshToken = stored.refreshToken;
60
+ this.mid = stored.mid;
61
+ this.tokenIssueTimeEpochSec = parseStoredNumber(stored.tokenIssueTimeEpochSec, null);
62
+ this.durationUntilRefreshInSec = parseStoredNumber(stored.durationUntilRefreshInSec, null);
63
+ this.revision = parseStoredNumber(stored.revision, -1) ?? -1;
64
+ this.globalRevision = parseStoredNumber(stored.globalRevision, 0) ?? 0;
65
+ this.individualRevision =
66
+ parseStoredNumber(stored.individualRevision, 0) ?? 0;
67
+ if (this.client) {
68
+ this.bindClient(this.client);
69
+ }
70
+ }
71
+ /**
72
+ * Initialize session state from a fresh login result.
73
+ * @param loginResult - Login payload returned by the LINE auth flow
74
+ * @returns Promise that resolves when state has been persisted
75
+ */
76
+ async initializeFromLogin(loginResult) {
77
+ this.authToken = loginResult.authToken ?? this.authToken;
78
+ this.refreshToken = loginResult.refreshToken ?? this.refreshToken;
79
+ this.mid = loginResult.mid ?? this.mid;
80
+ this.tokenIssueTimeEpochSec =
81
+ loginResult.tokenIssueTimeEpochSec ?? this.tokenIssueTimeEpochSec;
82
+ this.durationUntilRefreshInSec =
83
+ loginResult.durationUntilRefreshInSec ?? this.durationUntilRefreshInSec;
84
+ await persistLoginCredentials(this.credentialStore, loginResult);
85
+ await persistLoginCryptoMaterial(this.credentialStore, loginResult);
86
+ await persistLoginBootstrap(this.credentialStore, loginResult);
87
+ // Read-back verification: a write that is never read back is an
88
+ // unverified claim, not a fact. `persistLoginCredentials` above can
89
+ // report success while the underlying store still drops the key (a
90
+ // keychain write failure swallowed by `CredentialStore.persistBlob`'s
91
+ // catch, a race with another writer, etc.) — the only way to know the
92
+ // session actually survives a restart is to read it back through the
93
+ // same store the next process boot will use.
94
+ const persistedAuthToken = await this.credentialStore.get('line_auth_token');
95
+ const persistedMid = await this.credentialStore.get('line_mid');
96
+ if (!persistedAuthToken || !persistedMid) {
97
+ throw new Error(`LineSessionState.initializeFromLogin: login succeeded but session did not persist ` +
98
+ `(line_auth_token=${persistedAuthToken ? 'present' : 'MISSING'}, ` +
99
+ `line_mid=${persistedMid ? 'present' : 'MISSING'}). ` +
100
+ 'The next restart would require a fresh phone-PIN login.');
101
+ }
102
+ if (this.client) {
103
+ this.bindClient(this.client);
104
+ }
105
+ }
106
+ /**
107
+ * Apply a refreshed auth token result to the session.
108
+ * @param refreshResult - Refresh payload containing a new auth token
109
+ * @returns Promise that resolves when the new token is persisted
110
+ */
111
+ async applyRefreshResult(refreshResult) {
112
+ this.authToken = refreshResult.authToken;
113
+ this.tokenIssueTimeEpochSec =
114
+ refreshResult.tokenIssueTimeEpochSec ?? this.tokenIssueTimeEpochSec;
115
+ this.durationUntilRefreshInSec =
116
+ refreshResult.durationUntilRefreshInSec ?? this.durationUntilRefreshInSec;
117
+ await this.credentialStore.set('line_auth_token', refreshResult.authToken);
118
+ if (refreshResult.tokenIssueTimeEpochSec != null) {
119
+ await this.credentialStore.set('line_token_issue_time_epoch_sec', String(refreshResult.tokenIssueTimeEpochSec));
120
+ }
121
+ if (refreshResult.durationUntilRefreshInSec != null) {
122
+ await this.credentialStore.set('line_token_duration_until_refresh_sec', String(refreshResult.durationUntilRefreshInSec));
123
+ }
124
+ if (this.client) {
125
+ this.client.authToken = refreshResult.authToken;
126
+ }
127
+ }
128
+ /**
129
+ * Apply an auth token observed at runtime, such as token rotation.
130
+ * @param authToken - Latest auth token
131
+ * @returns Promise that resolves when the token is persisted
132
+ */
133
+ async applyRuntimeToken(authToken) {
134
+ this.authToken = authToken;
135
+ await this.credentialStore.set('line_auth_token', authToken);
136
+ if (this.client) {
137
+ this.client.authToken = authToken;
138
+ }
139
+ }
140
+ /**
141
+ * Persist the latest LINE sync revisions.
142
+ * @param revision - Main operation revision
143
+ * @param globalRevision - Global sync revision
144
+ * @param individualRevision - Individual sync revision
145
+ * @returns Promise that resolves when revisions are persisted
146
+ */
147
+ async setRevisions(revision, globalRevision, individualRevision) {
148
+ this.revision = revision;
149
+ await this.credentialStore.set('line_revision', String(revision));
150
+ if (globalRevision != null) {
151
+ this.globalRevision = globalRevision;
152
+ await this.credentialStore.set('line_global_revision', String(globalRevision));
153
+ }
154
+ if (individualRevision != null) {
155
+ this.individualRevision = individualRevision;
156
+ await this.credentialStore.set('line_individual_revision', String(individualRevision));
157
+ }
158
+ if (this.client) {
159
+ this.client.revision = revision;
160
+ this.client.globalRevision = this.globalRevision;
161
+ this.client.individualRevision = this.individualRevision;
162
+ }
163
+ }
164
+ /**
165
+ * Persist decrypted E2EE self keys for future session restore.
166
+ * @param keys - Decrypted E2EE self keys
167
+ * @returns Promise that resolves when keys are persisted
168
+ */
169
+ async saveE2EEKeys(keys) {
170
+ // Only the `line_e2ee_keys` blob is ever read back (auth-session-runtime
171
+ // restore). The former per-keyId and per-mid entries had no reader, so they
172
+ // only churned the OS keychain and emitted one store-complete debug log per
173
+ // key on every login/token refresh — dropped.
174
+ await this.credentialStore.set('line_e2ee_keys', JSON.stringify(keys));
175
+ }
176
+ /**
177
+ * Load persisted recent-fetch checkpoints for warm history planning.
178
+ *
179
+ * @returns Recent fetch checkpoint map.
180
+ */
181
+ async loadRecentFetchState() {
182
+ const raw = await this.credentialStore.get('line_recent_fetch_state');
183
+ if (!raw) {
184
+ return new Map();
185
+ }
186
+ try {
187
+ const parsed = JSON.parse(raw);
188
+ const entries = Object.entries(parsed || {}).map(([chatId, state]) => [
189
+ chatId,
190
+ {
191
+ lastCheckedAt: Number(state?.lastCheckedAt || 0),
192
+ lastDeliveredMessageId: state?.lastDeliveredMessageId
193
+ ? String(state.lastDeliveredMessageId)
194
+ : null,
195
+ lastDeliveredTime: Number(state?.lastDeliveredTime || 0),
196
+ },
197
+ ]);
198
+ return new Map(entries);
199
+ }
200
+ catch {
201
+ return new Map();
202
+ }
203
+ }
204
+ /**
205
+ * Persist recent-fetch checkpoints for warm history planning.
206
+ *
207
+ * @param recentFetchState - Current recent fetch checkpoint map.
208
+ * @returns Promise that resolves when checkpoints are persisted.
209
+ */
210
+ async saveRecentFetchState(recentFetchState) {
211
+ const payload = Object.fromEntries(Array.from(recentFetchState.entries())
212
+ .sort((left, right) => right[1].lastCheckedAt - left[1].lastCheckedAt)
213
+ .slice(0, 500));
214
+ await this.credentialStore.set('line_recent_fetch_state', JSON.stringify(payload));
215
+ }
216
+ /**
217
+ * Clear auth credentials for the current LINE session.
218
+ *
219
+ * This intentionally preserves non-auth LINE state such as saved phone,
220
+ * E2EE key material, peer/group key caches, and recent-fetch checkpoints.
221
+ * Those fields may belong to a freshly logged-in sibling process or be useful
222
+ * for in-session E2EE recovery after a token failure.
223
+ *
224
+ * @returns Promise that resolves when credentials are removed
225
+ */
226
+ async clearAuth() {
227
+ this.authToken = null;
228
+ this.refreshToken = null;
229
+ this.tokenIssueTimeEpochSec = null;
230
+ this.durationUntilRefreshInSec = null;
231
+ this.mid = null;
232
+ this.revision = -1;
233
+ this.globalRevision = 0;
234
+ this.individualRevision = 0;
235
+ await this.credentialStore.delete('line_auth_token');
236
+ await this.credentialStore.delete('line_refresh_token');
237
+ await this.credentialStore.delete('line_token_issue_time_epoch_sec');
238
+ await this.credentialStore.delete('line_token_duration_until_refresh_sec');
239
+ await this.credentialStore.delete('line_revision');
240
+ await this.credentialStore.delete('line_global_revision');
241
+ await this.credentialStore.delete('line_individual_revision');
242
+ }
243
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Persistence helpers for LINE session state.
3
+ * Read/write individual credential fields from the credential store.
4
+ */
5
+ import { Buffer } from 'node:buffer';
6
+ import { createCliLogger } from '../../../util/log.js';
7
+ import { extractE2EEInfo } from '../../auth/protocol/login-metadata.js';
8
+ const persistLog = createCliLogger('SESSION-PERSIST');
9
+ /**
10
+ * Normalize binary credential values before persistence.
11
+ * @param value - Raw credential value.
12
+ * @returns Persistable string value.
13
+ */
14
+ export function toPersistedKeyValue(value) {
15
+ if (!value) {
16
+ return null;
17
+ }
18
+ return Buffer.isBuffer(value) ? value.toString('base64') : String(value);
19
+ }
20
+ /**
21
+ * Parse an optional numeric credential value.
22
+ * @param value - Raw persisted value.
23
+ * @param fallback - Fallback when the value is missing.
24
+ * @returns Parsed number or fallback.
25
+ */
26
+ export function parseStoredNumber(value, fallback) {
27
+ return value ? Number(value) : fallback;
28
+ }
29
+ /**
30
+ * Extract persisted token/revision fields from the credential store.
31
+ * @param credentialStore - Persistent LINE credential store.
32
+ * @returns Hydrated session fields.
33
+ */
34
+ export async function loadStoredSessionFields(credentialStore) {
35
+ const authToken = await credentialStore.get('line_auth_token');
36
+ const refreshToken = await credentialStore.get('line_refresh_token');
37
+ const mid = await credentialStore.get('line_mid');
38
+ const tokenIssueTimeEpochSec = await credentialStore.get('line_token_issue_time_epoch_sec');
39
+ const durationUntilRefreshInSec = await credentialStore.get('line_token_duration_until_refresh_sec');
40
+ const revision = await credentialStore.get('line_revision');
41
+ const globalRevision = await credentialStore.get('line_global_revision');
42
+ const individualRevision = await credentialStore.get('line_individual_revision');
43
+ return {
44
+ authToken,
45
+ refreshToken,
46
+ mid,
47
+ tokenIssueTimeEpochSec,
48
+ durationUntilRefreshInSec,
49
+ revision,
50
+ globalRevision,
51
+ individualRevision,
52
+ };
53
+ }
54
+ /**
55
+ * Persist LINE auth/session fields provided by a fresh login result.
56
+ *
57
+ * `authToken` is never optional: a login result without it is not a
58
+ * usable session, so it throws instead of writing a half-persisted
59
+ * credential set. The other fields are genuinely optional (a field the
60
+ * LINE login response legitimately omits must not throw), but every
61
+ * call logs exactly which keys were written and which were skipped so
62
+ * an unexpected omission is visible immediately instead of surfacing as
63
+ * "session lost" three hours later.
64
+ *
65
+ * @param credentialStore - Persistent LINE credential store.
66
+ * @param loginResult - Login payload returned by the auth flow.
67
+ */
68
+ export async function persistLoginCredentials(credentialStore, loginResult) {
69
+ if (!loginResult.authToken) {
70
+ throw new Error('persistLoginCredentials: loginResult.authToken is missing — cannot persist a session without an auth token');
71
+ }
72
+ const written = [];
73
+ const skipped = [];
74
+ await credentialStore.set('line_auth_token', loginResult.authToken);
75
+ written.push('line_auth_token');
76
+ if (loginResult.refreshToken) {
77
+ await credentialStore.set('line_refresh_token', loginResult.refreshToken);
78
+ written.push('line_refresh_token');
79
+ }
80
+ else {
81
+ skipped.push('line_refresh_token');
82
+ }
83
+ if (loginResult.certificate) {
84
+ await credentialStore.set('line_certificate', loginResult.certificate);
85
+ written.push('line_certificate');
86
+ }
87
+ else {
88
+ skipped.push('line_certificate');
89
+ }
90
+ if (loginResult.mid) {
91
+ await credentialStore.set('line_mid', loginResult.mid);
92
+ written.push('line_mid');
93
+ }
94
+ else {
95
+ skipped.push('line_mid');
96
+ }
97
+ if (loginResult.tokenIssueTimeEpochSec != null) {
98
+ await credentialStore.set('line_token_issue_time_epoch_sec', String(loginResult.tokenIssueTimeEpochSec));
99
+ written.push('line_token_issue_time_epoch_sec');
100
+ }
101
+ else {
102
+ skipped.push('line_token_issue_time_epoch_sec');
103
+ }
104
+ if (loginResult.durationUntilRefreshInSec != null) {
105
+ await credentialStore.set('line_token_duration_until_refresh_sec', String(loginResult.durationUntilRefreshInSec));
106
+ written.push('line_token_duration_until_refresh_sec');
107
+ }
108
+ else {
109
+ skipped.push('line_token_duration_until_refresh_sec');
110
+ }
111
+ persistLog.info('login-credentials-persisted', {
112
+ written: written.join(','),
113
+ skipped: skipped.join(',') || 'none',
114
+ });
115
+ }
116
+ /**
117
+ * Persist LINE NaCl/E2EE bootstrap credentials when present.
118
+ *
119
+ * All three fields are optional here (a fresh login may or may not carry
120
+ * new crypto bootstrap material), so a missing field never throws — but
121
+ * every call logs exactly which keys were written and which were skipped.
122
+ *
123
+ * @param credentialStore - Persistent LINE credential store.
124
+ * @param loginResult - Login payload returned by the auth flow.
125
+ */
126
+ export async function persistLoginCryptoMaterial(credentialStore, loginResult) {
127
+ const secretKey = toPersistedKeyValue(loginResult.secretKey);
128
+ const publicKey = toPersistedKeyValue(loginResult.publicKey);
129
+ const written = [];
130
+ const skipped = [];
131
+ if (secretKey) {
132
+ await credentialStore.set('line_nacl_secret_key', secretKey);
133
+ written.push('line_nacl_secret_key');
134
+ }
135
+ else {
136
+ skipped.push('line_nacl_secret_key');
137
+ }
138
+ if (publicKey) {
139
+ await credentialStore.set('line_nacl_public_key', publicKey);
140
+ written.push('line_nacl_public_key');
141
+ }
142
+ else {
143
+ skipped.push('line_nacl_public_key');
144
+ }
145
+ if (loginResult.nonce) {
146
+ await credentialStore.set('line_nonce', loginResult.nonce);
147
+ written.push('line_nonce');
148
+ }
149
+ else {
150
+ skipped.push('line_nonce');
151
+ }
152
+ persistLog.info('login-crypto-material-persisted', {
153
+ written: written.join(',') || 'none',
154
+ skipped: skipped.join(',') || 'none',
155
+ });
156
+ }
157
+ /**
158
+ * Persist the E2EE bootstrap payload extracted from login metadata.
159
+ *
160
+ * This field is optional (a login result may carry no E2EE bootstrap
161
+ * metadata at all — e.g. a device that already has keys), so a missing
162
+ * field never throws — but every call logs whether the key was written
163
+ * or skipped, and why, so an unexpected omission is visible immediately.
164
+ *
165
+ * @param credentialStore - Persistent LINE credential store.
166
+ * @param loginResult - Login payload returned by the auth flow.
167
+ */
168
+ export async function persistLoginBootstrap(credentialStore, loginResult) {
169
+ const e2eeInfo = extractE2EEInfo(loginResult.e2eeInfo || loginResult.metaData || null);
170
+ const secretKey = toPersistedKeyValue(loginResult.secretKey);
171
+ if (!e2eeInfo?.encryptedKeyChain ||
172
+ !e2eeInfo?.serverPublicKey ||
173
+ !secretKey) {
174
+ persistLog.info('login-bootstrap-persisted', {
175
+ written: 'none',
176
+ skipped: 'line_e2ee_bootstrap',
177
+ reason: 'no encryptedKeyChain/serverPublicKey/secretKey in login result',
178
+ });
179
+ return;
180
+ }
181
+ await credentialStore.set('line_e2ee_bootstrap', JSON.stringify({
182
+ encryptedKeyChain: e2eeInfo.encryptedKeyChain.toString('base64'),
183
+ secretKey,
184
+ serverPublicKey: e2eeInfo.serverPublicKey.toString('base64'),
185
+ keyId: e2eeInfo.keyId,
186
+ e2eeVersion: e2eeInfo.e2eeVersion,
187
+ }));
188
+ persistLog.info('login-bootstrap-persisted', {
189
+ written: 'line_e2ee_bootstrap',
190
+ skipped: 'none',
191
+ });
192
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * LINE Credential Stores — re-exports
3
+ *
4
+ * Both stores live in Yomi's vendored auth/credential-store module.
5
+ * This module re-exports them under their original names for
6
+ * compatibility with the LINE protocol core.
7
+ */
8
+ export { CredentialStore as FileCredentialStore, InMemoryStore, } from '../../auth/credential-store.js';