@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.
- package/LICENSE +21 -0
- package/NOTICE +81 -0
- package/PRIVACY.md +39 -0
- package/README.md +659 -0
- package/dist/auth/credential-store.js +395 -0
- package/dist/auth/keychain.js +256 -0
- package/dist/cli/login.js +142 -0
- package/dist/line/auth/protocol/e2ee-bootstrap.js +87 -0
- package/dist/line/auth/protocol/index.js +3 -0
- package/dist/line/auth/protocol/login-metadata.js +159 -0
- package/dist/line/auth/protocol/pwless-login-flow.js +83 -0
- package/dist/line/auth/pwless/index.js +163 -0
- package/dist/line/auth/pwless/steps.js +213 -0
- package/dist/line/client/auth-service/client.js +240 -0
- package/dist/line/client/e2ee-service/client.js +69 -0
- package/dist/line/client/e2ee-service/requests.js +92 -0
- package/dist/line/client/e2ee-service/shared-key-log.js +24 -0
- package/dist/line/client/e2ee-service/shared-key-payload.js +35 -0
- package/dist/line/client/index.js +213 -0
- package/dist/line/client/obs-media-client.js +140 -0
- package/dist/line/client/parsers.js +69 -0
- package/dist/line/client/shop-service/client.js +63 -0
- package/dist/line/client/sticker-meta.js +46 -0
- package/dist/line/client/sync-service/client.js +317 -0
- package/dist/line/client/talk-service/chat-query.js +122 -0
- package/dist/line/client/talk-service/command-client.js +242 -0
- package/dist/line/client/talk-service/contact-query.js +77 -0
- package/dist/line/client/talk-service/message-box-query.js +137 -0
- package/dist/line/client/talk-service/message-id-query.js +241 -0
- package/dist/line/client/talk-service/query-client.js +219 -0
- package/dist/line/client/talk-service/requests.js +383 -0
- package/dist/line/client/transport.js +138 -0
- package/dist/line/core/auth-session-logout.js +55 -0
- package/dist/line/core/auth-session-runtime.js +231 -0
- package/dist/line/core/auth-session-service.js +85 -0
- package/dist/line/core/chat-runtime-service.js +343 -0
- package/dist/line/core/client-runtime.js +17 -0
- package/dist/line/core/config.js +67 -0
- package/dist/line/core/constants.js +56 -0
- package/dist/line/core/contact-query-service.js +20 -0
- package/dist/line/core/directory-runtime-service.js +83 -0
- package/dist/line/core/e2ee/bootstrap/keychain.js +41 -0
- package/dist/line/core/e2ee/crypto/crypto-primitives.js +98 -0
- package/dist/line/core/e2ee/index.js +6 -0
- package/dist/line/core/e2ee/keys/decrypt.js +110 -0
- package/dist/line/core/e2ee/keys/encrypt.js +117 -0
- package/dist/line/core/e2ee/keys/group-key.js +260 -0
- package/dist/line/core/e2ee/keys/key-manager.js +231 -0
- package/dist/line/core/e2ee/keys/key-payload.js +130 -0
- package/dist/line/core/e2ee/keys/key-types.js +1 -0
- package/dist/line/core/e2ee/keys/message-crypto.js +146 -0
- package/dist/line/core/e2ee/keys/message-decrypt.js +390 -0
- package/dist/line/core/e2ee/keys/peer-key.js +191 -0
- package/dist/line/core/e2ee/recovery/bootstrap-keys.js +161 -0
- package/dist/line/core/e2ee/types.js +4 -0
- package/dist/line/core/group-query-service.js +20 -0
- package/dist/line/core/media-decrypt.js +78 -0
- package/dist/line/core/media-encrypt.js +97 -0
- package/dist/line/core/mention.js +89 -0
- package/dist/line/core/message-command-service.js +454 -0
- package/dist/line/core/message-query-service.js +161 -0
- package/dist/line/core/service.js +134 -0
- package/dist/line/core/session-state/index.js +243 -0
- package/dist/line/core/session-state/persist.js +192 -0
- package/dist/line/core/store.js +8 -0
- package/dist/line/core/thrift/binary/binary-reader.js +151 -0
- package/dist/line/core/thrift/binary/binary-writer.js +157 -0
- package/dist/line/core/thrift/binary/type-codec-dsl.js +42 -0
- package/dist/line/core/thrift/compact/compact-reader.js +180 -0
- package/dist/line/core/thrift/compact/compact-writer.js +161 -0
- package/dist/line/core/thrift/compact/type-codec-dsl.js +66 -0
- package/dist/line/core/thrift/fields/builders.js +124 -0
- package/dist/line/core/thrift/index.js +12 -0
- package/dist/line/core/thrift/message/decoder.js +55 -0
- package/dist/line/core/thrift/message/encoder.js +25 -0
- package/dist/line/core/thrift/primitives/varint.js +64 -0
- package/dist/line/core/thrift/struct/reader.js +172 -0
- package/dist/line/core/thrift/struct/writer.js +189 -0
- package/dist/line/core/thrift/types.js +23 -0
- package/dist/line/core/video-thumbnail.js +104 -0
- package/dist/mcp/handlers/contacts.js +166 -0
- package/dist/mcp/handlers/conversations.js +193 -0
- package/dist/mcp/handlers/groups.js +123 -0
- package/dist/mcp/handlers/index.js +15 -0
- package/dist/mcp/handlers/login.js +473 -0
- package/dist/mcp/handlers/media.js +69 -0
- package/dist/mcp/handlers/message-actions.js +62 -0
- package/dist/mcp/handlers/scope.js +120 -0
- package/dist/mcp/handlers/search.js +196 -0
- package/dist/mcp/handlers/send.js +523 -0
- package/dist/mcp/handlers/shared.js +84 -0
- package/dist/mcp/handlers/stickers.js +87 -0
- package/dist/mcp/media.js +228 -0
- package/dist/mcp/names.js +133 -0
- package/dist/mcp/phi-guard.js +105 -0
- package/dist/mcp/policy.js +34 -0
- package/dist/mcp/server.js +301 -0
- package/dist/mcp/tools.js +761 -0
- package/dist/mcp/ui/capability.js +26 -0
- package/dist/mcp/ui/login-app-protocol.js +118 -0
- package/dist/mcp/ui/login-app-styles.js +142 -0
- package/dist/mcp/ui/login-app-view.js +361 -0
- package/dist/mcp/ui/login-app.js +72 -0
- package/dist/mcp/ui/login-structured-content.js +1 -0
- package/dist/mcp/ui/resource.js +35 -0
- package/dist/mcp/ui/tools-with-ui.js +40 -0
- package/dist/search/bigram.js +86 -0
- package/dist/search/capture.js +140 -0
- package/dist/search/collector.js +176 -0
- package/dist/search/default-embedder.js +101 -0
- package/dist/search/embedder.js +13 -0
- package/dist/search/scope.js +142 -0
- package/dist/search/sqlite.js +141 -0
- package/dist/search/store.js +469 -0
- package/dist/util/data-dir.js +55 -0
- package/dist/util/log.js +86 -0
- package/dist/version.js +7 -0
- package/package.json +63 -0
- package/run.mjs +71 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LINE Passwordless Login Protocol Steps.
|
|
3
|
+
*
|
|
4
|
+
* Flow: createSession → verifyLoginCertificate → requestPinCodeVerif →
|
|
5
|
+
* checkPinCodeVerified → putExchangeKey → requestPaakAuth →
|
|
6
|
+
* checkPaakAuthenticated → loginV2
|
|
7
|
+
*
|
|
8
|
+
* Field layouts verified against CHRLINE reference + live test (2026-03-27).
|
|
9
|
+
*/
|
|
10
|
+
import { boolField, encodeCallMessage, mapField, stringField, structField, } from '../../core/thrift/index.js';
|
|
11
|
+
/**
|
|
12
|
+
* Number of long-poll hops `checkPinCodeVerified` makes before giving up.
|
|
13
|
+
* Each hop is bounded by `PWLESS_POLL_TIMEOUT_MS` (../index.ts) — the two
|
|
14
|
+
* multiplied together give the real client-side ceiling for this step, which
|
|
15
|
+
* is minutes, not the duration of a single hop.
|
|
16
|
+
*/
|
|
17
|
+
export const PIN_VERIFY_POLL_ATTEMPTS = 15;
|
|
18
|
+
/**
|
|
19
|
+
* Number of long-poll hops `checkPaakAuthenticated` makes before giving up.
|
|
20
|
+
* Same relationship to `PWLESS_POLL_TIMEOUT_MS` as `PIN_VERIFY_POLL_ATTEMPTS`.
|
|
21
|
+
*/
|
|
22
|
+
export const PAAK_AUTH_POLL_ATTEMPTS = 12;
|
|
23
|
+
/**
|
|
24
|
+
* Step 1: Create a pwless login session with phone number.
|
|
25
|
+
*
|
|
26
|
+
* @param ctx - Pwless login context
|
|
27
|
+
* @param phone - E.164 phone number (e.g. +886912345678)
|
|
28
|
+
* @param region - Region code (e.g. TW, JP)
|
|
29
|
+
* @returns Session ID string
|
|
30
|
+
*/
|
|
31
|
+
export async function createSession(ctx, phone, region) {
|
|
32
|
+
const fields = [
|
|
33
|
+
structField(1, [stringField(1, phone), stringField(2, region)]),
|
|
34
|
+
];
|
|
35
|
+
const data = encodeCallMessage('createSession', ctx.seq++, fields);
|
|
36
|
+
const result = await ctx.sendPwless(data);
|
|
37
|
+
const sid = result?.fields?.[0]?.[1];
|
|
38
|
+
if (!sid) {
|
|
39
|
+
throw new Error(`createSession failed: ${JSON.stringify(result)}`);
|
|
40
|
+
}
|
|
41
|
+
return sid;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Step 2: Verify login certificate (skip PIN if cert valid).
|
|
45
|
+
*
|
|
46
|
+
* @param ctx - Pwless login context
|
|
47
|
+
* @param certificate - Previously saved certificate, or empty string
|
|
48
|
+
* @returns True if certificate is valid (PIN can be skipped)
|
|
49
|
+
*/
|
|
50
|
+
export async function verifyLoginCertificate(ctx, certificate) {
|
|
51
|
+
const fields = [
|
|
52
|
+
structField(1, [
|
|
53
|
+
stringField(1, ctx.sessionId),
|
|
54
|
+
stringField(2, certificate || ''),
|
|
55
|
+
]),
|
|
56
|
+
];
|
|
57
|
+
const data = encodeCallMessage('verifyLoginCertificate', ctx.seq++, fields);
|
|
58
|
+
const result = await ctx.sendPwless(data);
|
|
59
|
+
return !!(result?.fields?.[0] && !result?.fields?.[1]);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Step 3a: Request PIN code verification.
|
|
63
|
+
*
|
|
64
|
+
* @param ctx - Pwless login context
|
|
65
|
+
* @returns PIN code string to display to user
|
|
66
|
+
*/
|
|
67
|
+
export async function requestPinCodeVerif(ctx) {
|
|
68
|
+
const fields = [
|
|
69
|
+
structField(1, [stringField(1, ctx.sessionId)]),
|
|
70
|
+
];
|
|
71
|
+
const data = encodeCallMessage('requestPinCodeVerif', ctx.seq++, fields);
|
|
72
|
+
const result = await ctx.sendPwless(data);
|
|
73
|
+
const pin = result?.fields?.[0]?.[1];
|
|
74
|
+
if (!pin) {
|
|
75
|
+
throw new Error(`requestPinCodeVerif failed: ${JSON.stringify(result)}`);
|
|
76
|
+
}
|
|
77
|
+
return pin;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Step 3b: Long-poll until PIN is verified on phone.
|
|
81
|
+
*
|
|
82
|
+
* @param ctx - Pwless login context
|
|
83
|
+
* @returns True if PIN verified, false if timed out
|
|
84
|
+
*/
|
|
85
|
+
export async function checkPinCodeVerified(ctx) {
|
|
86
|
+
for (let i = 0; i < PIN_VERIFY_POLL_ATTEMPTS; i++) {
|
|
87
|
+
if (ctx.aborted) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const fields = [
|
|
91
|
+
structField(1, [stringField(1, ctx.sessionId)]),
|
|
92
|
+
];
|
|
93
|
+
const data = encodeCallMessage('checkPinCodeVerified', ctx.seq++, fields);
|
|
94
|
+
const result = await ctx.sendPwlessLongPoll(data);
|
|
95
|
+
if (result.error === 'timeout' || result.error) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (result.fields && !result.fields[1]) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Step 4: Exchange E2EE public key with server.
|
|
106
|
+
* Uses MAP type for key-value exchange.
|
|
107
|
+
*
|
|
108
|
+
* @param ctx - Pwless login context
|
|
109
|
+
* @param publicKeyBase64 - NaCl X25519 public key in base64
|
|
110
|
+
*/
|
|
111
|
+
export async function putExchangeKey(ctx, publicKeyBase64) {
|
|
112
|
+
const fields = [
|
|
113
|
+
structField(1, [
|
|
114
|
+
stringField(1, ctx.sessionId),
|
|
115
|
+
mapField(2, 11, 11, {
|
|
116
|
+
e2eeVersion: '1',
|
|
117
|
+
temporalPublicKey: publicKeyBase64,
|
|
118
|
+
}),
|
|
119
|
+
]),
|
|
120
|
+
];
|
|
121
|
+
const data = encodeCallMessage('putExchangeKey', ctx.seq++, fields);
|
|
122
|
+
await ctx.sendPwless(data);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Step 5: Request PAAK (biometric) authentication.
|
|
126
|
+
*
|
|
127
|
+
* @param ctx - Pwless login context
|
|
128
|
+
*/
|
|
129
|
+
export async function requestPaakAuth(ctx) {
|
|
130
|
+
const fields = [
|
|
131
|
+
structField(1, [stringField(1, ctx.sessionId)]),
|
|
132
|
+
];
|
|
133
|
+
const data = encodeCallMessage('requestPaakAuth', ctx.seq++, fields);
|
|
134
|
+
await ctx.sendPwless(data);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Step 6: Long-poll until biometric is confirmed on phone.
|
|
138
|
+
*
|
|
139
|
+
* @param ctx - Pwless login context
|
|
140
|
+
* @returns True if PAAK confirmed
|
|
141
|
+
*/
|
|
142
|
+
export async function checkPaakAuthenticated(ctx) {
|
|
143
|
+
for (let i = 0; i < PAAK_AUTH_POLL_ATTEMPTS; i++) {
|
|
144
|
+
if (ctx.aborted) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const fields = [
|
|
148
|
+
structField(1, [
|
|
149
|
+
stringField(1, ctx.sessionId),
|
|
150
|
+
stringField(2, 'CHANNELGW'),
|
|
151
|
+
boolField(3, true),
|
|
152
|
+
]),
|
|
153
|
+
];
|
|
154
|
+
const data = encodeCallMessage('checkPaakAuthenticated', ctx.seq++, fields);
|
|
155
|
+
const result = await ctx.sendPwlessLongPoll(data);
|
|
156
|
+
if (result.error === 'timeout' || result.error) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (result.fields?.[0] && !result.fields?.[1]) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Step 7: Retrieve E2EE key from server.
|
|
167
|
+
* Must be called after PAAK and before loginV2 to complete the session properly.
|
|
168
|
+
*
|
|
169
|
+
* @param ctx - Pwless login context
|
|
170
|
+
* @returns E2EE key response (encryptedKeyChain, publicKey, etc.)
|
|
171
|
+
*/
|
|
172
|
+
export async function getE2eeKey(ctx) {
|
|
173
|
+
const fields = [
|
|
174
|
+
structField(1, [stringField(1, ctx.sessionId)]),
|
|
175
|
+
];
|
|
176
|
+
const data = encodeCallMessage('getE2eeKey', ctx.seq++, fields);
|
|
177
|
+
const result = await ctx.sendPwless(data);
|
|
178
|
+
return result?.fields?.[0] ?? null;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Step 8: Final login call. Returns authToken, certificate, mid.
|
|
182
|
+
*
|
|
183
|
+
* @param ctx - Pwless login context
|
|
184
|
+
* @param systemName - Device hostname
|
|
185
|
+
* @returns Login result with authToken, refreshToken, certificate, mid
|
|
186
|
+
*/
|
|
187
|
+
export async function loginV2(ctx, systemName) {
|
|
188
|
+
const fields = [
|
|
189
|
+
structField(1, [
|
|
190
|
+
stringField(1, ctx.sessionId),
|
|
191
|
+
boolField(2, true),
|
|
192
|
+
stringField(3, systemName),
|
|
193
|
+
stringField(4, 'CHANNELGW'),
|
|
194
|
+
]),
|
|
195
|
+
];
|
|
196
|
+
const data = encodeCallMessage('loginV2', ctx.seq++, fields);
|
|
197
|
+
const result = await ctx.sendPwless(data);
|
|
198
|
+
if (result?.fields?.[1]) {
|
|
199
|
+
const exc = result.fields[1];
|
|
200
|
+
throw new Error(`loginV2 failed: code=${exc[1]} msg="${exc[2]}"`);
|
|
201
|
+
}
|
|
202
|
+
const rp = result?.fields?.[0];
|
|
203
|
+
if (!rp) {
|
|
204
|
+
throw new Error(`loginV2: unexpected response`);
|
|
205
|
+
}
|
|
206
|
+
const tokenInfo = rp[3];
|
|
207
|
+
return {
|
|
208
|
+
authToken: tokenInfo?.[1],
|
|
209
|
+
refreshToken: tokenInfo?.[2],
|
|
210
|
+
certificate: rp[2],
|
|
211
|
+
mid: rp[5],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LINE AuthService client capability.
|
|
3
|
+
*
|
|
4
|
+
* Keep auth-service calls separate from TalkService calls so the client mirrors
|
|
5
|
+
* linejs more closely: Talk handles messaging and contacts, Auth handles token
|
|
6
|
+
* lifecycle and credential/bootstrap flows on /AS4, /RS4, and refresh paths.
|
|
7
|
+
*/
|
|
8
|
+
import { Buffer } from 'node:buffer';
|
|
9
|
+
import { createCliLogger } from '../../../util/log.js';
|
|
10
|
+
import { LINE_APP_CONFIG } from '../../core/config.js';
|
|
11
|
+
import { encodeCallMessage } from '../../core/thrift/index.js';
|
|
12
|
+
import { sendRequest } from '../transport.js';
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the active LINE client logger, preserving startup indentation.
|
|
15
|
+
*
|
|
16
|
+
* @param client - LINE client instance.
|
|
17
|
+
* @returns Logger for auth transport diagnostics.
|
|
18
|
+
*/
|
|
19
|
+
function getLineClientLog(client) {
|
|
20
|
+
if (client?.startupFlowLogger) {
|
|
21
|
+
return client.startupFlowLogger;
|
|
22
|
+
}
|
|
23
|
+
if (client?.logger?.info) {
|
|
24
|
+
return client.logger;
|
|
25
|
+
}
|
|
26
|
+
return client?.logger || createCliLogger('LINE');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Normalize LINE auth-service success/error responses.
|
|
30
|
+
*
|
|
31
|
+
* @param method - AuthService method name for diagnostics.
|
|
32
|
+
* @param result - Decoded thrift response.
|
|
33
|
+
* @returns Success payload or boolean true for void methods.
|
|
34
|
+
*/
|
|
35
|
+
function unwrapAuthSuccess(method, result) {
|
|
36
|
+
if (result.fields?.[1]) {
|
|
37
|
+
const err = result.fields[1];
|
|
38
|
+
throw new Error(`${method} failed: code=${err?.[1]} msg="${err?.[2] || 'Unknown error'}"`);
|
|
39
|
+
}
|
|
40
|
+
return result.fields?.[0] ?? true;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create the auth capability bound to one LINE client runtime.
|
|
44
|
+
*
|
|
45
|
+
* @param runtime - Mutable LINE client runtime.
|
|
46
|
+
* @returns Auth service methods bound to the runtime.
|
|
47
|
+
*/
|
|
48
|
+
export function createAuthClient(runtime) {
|
|
49
|
+
return {
|
|
50
|
+
/**
|
|
51
|
+
* Send a generic AuthService request to LINE.
|
|
52
|
+
*
|
|
53
|
+
* This helper mirrors linejs AuthService.request() behavior for the subset of
|
|
54
|
+
* methods implemented directly here. All callers should pass thrift field
|
|
55
|
+
* tuples that match the upstream struct layout for the target method.
|
|
56
|
+
*
|
|
57
|
+
* @param method - AuthService method name.
|
|
58
|
+
* @param args - TCompact thrift field tuples for the request body.
|
|
59
|
+
* @param path - Override service path when the method is not served from /AS4.
|
|
60
|
+
* @returns Decoded thrift response.
|
|
61
|
+
*/
|
|
62
|
+
async sendAuth(method, args = [], path = LINE_APP_CONFIG.authPath) {
|
|
63
|
+
const data = encodeCallMessage(method, runtime.seq++, args);
|
|
64
|
+
const result = await sendRequest(runtime.host, path, data, { 'X-Line-Access': runtime.authToken }, 30000, { logger: getLineClientLog(runtime) });
|
|
65
|
+
if (result.nextToken) {
|
|
66
|
+
runtime.authToken = result.nextToken;
|
|
67
|
+
runtime.emit('tokenRotated', result.nextToken);
|
|
68
|
+
}
|
|
69
|
+
if (result.fields?.[1] && typeof result.fields[1] === 'object') {
|
|
70
|
+
const exc = result.fields[1];
|
|
71
|
+
const msg = typeof exc[2] === 'string' ? exc[2] : JSON.stringify(exc);
|
|
72
|
+
console.error(`[LINE] ${method} exception: ${msg}`);
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
},
|
|
76
|
+
/**
|
|
77
|
+
* Refresh the authentication token through LINE's refresh endpoint.
|
|
78
|
+
*
|
|
79
|
+
* @param refreshToken - The refresh token.
|
|
80
|
+
* @returns Refresh payload with auth token and refresh timing metadata.
|
|
81
|
+
*/
|
|
82
|
+
async refreshAuthToken(refreshToken) {
|
|
83
|
+
// stderr-only logger — stdout is the MCP JSON-RPC channel and this runs
|
|
84
|
+
// inside the stdio server (token refresh on resume/rotation).
|
|
85
|
+
const lineLog = getLineClientLog(runtime);
|
|
86
|
+
lineLog.info('auth.refresh.call', {
|
|
87
|
+
auth: `${String(runtime.authToken || '').slice(0, 16)}...`,
|
|
88
|
+
refresh: `${String(refreshToken || '').slice(0, 12)}...`,
|
|
89
|
+
});
|
|
90
|
+
const result = await runtime.sendAuth('refresh', [[12, 1, [[11, 1, refreshToken]]]], LINE_APP_CONFIG.tokenRefreshPath);
|
|
91
|
+
const resp = result.fields?.[0];
|
|
92
|
+
if (resp?.[1]) {
|
|
93
|
+
runtime.authToken = resp[1];
|
|
94
|
+
runtime.emit('tokenRotated', resp[1]);
|
|
95
|
+
const durationUntilRefreshInSec = typeof resp[2] === 'bigint' ? Number(resp[2]) : resp[2] || 0;
|
|
96
|
+
const tokenIssueTimeEpochSec = typeof resp[3] === 'bigint' ? Number(resp[3]) : resp[3] || null;
|
|
97
|
+
lineLog.info('auth.refresh.success', {
|
|
98
|
+
auth: `${String(resp[1]).slice(0, 16)}...`,
|
|
99
|
+
next_in_hours: Math.round(durationUntilRefreshInSec / 3600),
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
authToken: resp[1],
|
|
103
|
+
durationUntilRefreshInSec,
|
|
104
|
+
tokenIssueTimeEpochSec,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
console.error('[LINE] refresh() exception payload:', JSON.stringify(result, (_key, val) => typeof val === 'bigint' ? val.toString() : val));
|
|
108
|
+
throw new Error(`Token refresh failed: ${JSON.stringify(result, (_key, val) => (typeof val === 'bigint' ? val.toString() : val))}`);
|
|
109
|
+
},
|
|
110
|
+
/**
|
|
111
|
+
* Report a freshly rotated access token back to LINE's auth service.
|
|
112
|
+
*
|
|
113
|
+
* @param authToken - The refreshed access token issued by LINE.
|
|
114
|
+
* @returns Raw decoded auth-service success payload.
|
|
115
|
+
*/
|
|
116
|
+
async reportRefreshedAccessToken(authToken) {
|
|
117
|
+
const result = await runtime.sendAuth('reportRefreshedAccessToken', [
|
|
118
|
+
[12, 1, [[11, 1, authToken]]],
|
|
119
|
+
]);
|
|
120
|
+
return unwrapAuthSuccess('reportRefreshedAccessToken', result);
|
|
121
|
+
},
|
|
122
|
+
/**
|
|
123
|
+
* Open a generic auth session on /AS4.
|
|
124
|
+
*
|
|
125
|
+
* @param metaData - Optional auth session metadata map.
|
|
126
|
+
* @returns Auth session ID string.
|
|
127
|
+
*/
|
|
128
|
+
async openAuthSession(metaData = {}) {
|
|
129
|
+
const result = await runtime.sendAuth('openAuthSession', [
|
|
130
|
+
[12, 2, [[13, 1, [11, 11, metaData]]]],
|
|
131
|
+
]);
|
|
132
|
+
return unwrapAuthSuccess('openAuthSession', result);
|
|
133
|
+
},
|
|
134
|
+
/**
|
|
135
|
+
* Open a legacy auth session using the same metadata-map shape linejs sends.
|
|
136
|
+
*
|
|
137
|
+
* @param metaData - Optional auth session metadata map.
|
|
138
|
+
* @returns Open session response payload.
|
|
139
|
+
*/
|
|
140
|
+
async openSession(metaData = {}) {
|
|
141
|
+
const result = await runtime.sendAuth('openSession', [
|
|
142
|
+
[12, 1, [[13, 1, [11, 11, metaData]]]],
|
|
143
|
+
]);
|
|
144
|
+
return unwrapAuthSuccess('openSession', result);
|
|
145
|
+
},
|
|
146
|
+
/**
|
|
147
|
+
* Fetch the RSA challenge key for identifier/password-style auth flows.
|
|
148
|
+
*
|
|
149
|
+
* @param authSessionId - Session ID returned by openAuthSession/openSession.
|
|
150
|
+
* @param identityProvider - LINE identity-provider enum value.
|
|
151
|
+
* @returns RSA key payload used for upstream credential encryption.
|
|
152
|
+
*/
|
|
153
|
+
async getAuthRSAKey(authSessionId, identityProvider) {
|
|
154
|
+
const result = await runtime.sendAuth('getAuthRSAKey', [
|
|
155
|
+
[11, 2, authSessionId],
|
|
156
|
+
[8, 3, identityProvider],
|
|
157
|
+
]);
|
|
158
|
+
return unwrapAuthSuccess('getAuthRSAKey', result);
|
|
159
|
+
},
|
|
160
|
+
/**
|
|
161
|
+
* Issue a V3 access token for a primary-device style bootstrap flow.
|
|
162
|
+
*
|
|
163
|
+
* @param request - Device identification payload.
|
|
164
|
+
* @returns Access token, refresh token, MID, and refresh timing metadata.
|
|
165
|
+
*/
|
|
166
|
+
async issueV3TokenForPrimary(request) {
|
|
167
|
+
const result = await runtime.sendAuth('issueV3TokenForPrimary', [
|
|
168
|
+
[
|
|
169
|
+
12,
|
|
170
|
+
1,
|
|
171
|
+
[
|
|
172
|
+
[11, 1, request?.udid],
|
|
173
|
+
[11, 2, request?.systemDisplayName],
|
|
174
|
+
[11, 3, request?.modelName],
|
|
175
|
+
],
|
|
176
|
+
],
|
|
177
|
+
]);
|
|
178
|
+
return unwrapAuthSuccess('issueV3TokenForPrimary', result);
|
|
179
|
+
},
|
|
180
|
+
/**
|
|
181
|
+
* Create an enforced E2EE key backup blob on the auth service.
|
|
182
|
+
*
|
|
183
|
+
* @param request - Backup request payload.
|
|
184
|
+
* @returns Backup creation response payload.
|
|
185
|
+
*/
|
|
186
|
+
async createE2EEKeyBackupEnforced(request) {
|
|
187
|
+
const result = await runtime.sendAuth('createE2EEKeyBackupEnforced', [
|
|
188
|
+
[
|
|
189
|
+
12,
|
|
190
|
+
2,
|
|
191
|
+
[
|
|
192
|
+
[11, 1, request?.blobHeader],
|
|
193
|
+
[11, 2, request?.blobPayload],
|
|
194
|
+
[8, 3, request?.reason],
|
|
195
|
+
],
|
|
196
|
+
],
|
|
197
|
+
]);
|
|
198
|
+
return unwrapAuthSuccess('createE2EEKeyBackupEnforced', result);
|
|
199
|
+
},
|
|
200
|
+
/**
|
|
201
|
+
* Restore an E2EE key backup from a server-issued restore claim.
|
|
202
|
+
*
|
|
203
|
+
* @param restoreClaim - Restore claim/token issued by LINE.
|
|
204
|
+
* @returns Restore payload containing the encrypted backup blob.
|
|
205
|
+
*/
|
|
206
|
+
async restoreE2EEKeyBackup(restoreClaim) {
|
|
207
|
+
const result = await runtime.sendAuth('restoreE2EEKeyBackup', [
|
|
208
|
+
[12, 2, [[11, 1, restoreClaim]]],
|
|
209
|
+
]);
|
|
210
|
+
return unwrapAuthSuccess('restoreE2EEKeyBackup', result);
|
|
211
|
+
},
|
|
212
|
+
/**
|
|
213
|
+
* Revoke the current LINE session on the server via logoutZ.
|
|
214
|
+
*
|
|
215
|
+
* @returns Whether the server acknowledged logout success.
|
|
216
|
+
*/
|
|
217
|
+
async logoutZ() {
|
|
218
|
+
const payload = Buffer.from([
|
|
219
|
+
0x82, 0x21, 0x00, 0x07, 0x6c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x5a, 0x00,
|
|
220
|
+
]);
|
|
221
|
+
const result = await sendRequest(runtime.host, LINE_APP_CONFIG.revokePath, payload, { 'X-Line-Access': runtime.authToken }, 30000, { logger: getLineClientLog(runtime) });
|
|
222
|
+
if (result.nextToken) {
|
|
223
|
+
runtime.authToken = result.nextToken;
|
|
224
|
+
runtime.emit('tokenRotated', result.nextToken);
|
|
225
|
+
}
|
|
226
|
+
if (result.fields?.[1]) {
|
|
227
|
+
const err = result.fields[1];
|
|
228
|
+
const code = String(err?.[1] || '');
|
|
229
|
+
const message = String(err?.[2] || '');
|
|
230
|
+
if (code !== 'MUST_REFRESH_V3_TOKEN' &&
|
|
231
|
+
!message.includes('MUST_REFRESH_V3_TOKEN')) {
|
|
232
|
+
throw new Error(`logoutZ failed: code=${code} msg="${message || 'Unknown error'}"`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
runtime.polling = false;
|
|
236
|
+
runtime.aborted = true;
|
|
237
|
+
return result.fields?.[0] ?? true;
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LINE Client E2EE transport capability.
|
|
3
|
+
*/
|
|
4
|
+
import { buildGetE2EEMessageInfoRequest, buildGetLastGroupSharedKeyRequest, buildGetLastPublicKeysRequest, buildGetPublicKeysRequest, buildNegotiatePublicKeyRequest, buildRegisterGroupKeyRequest, buildRegisterPublicKeyRequest, } from './requests.js';
|
|
5
|
+
import { buildSharedKeyEventLog } from './shared-key-log.js';
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a logger suitable for E2EE client events.
|
|
8
|
+
*
|
|
9
|
+
* @param client - LINE client instance.
|
|
10
|
+
* @returns Logger-like object when available.
|
|
11
|
+
*/
|
|
12
|
+
function getE2EEClientLog(client) {
|
|
13
|
+
if (client?.startupFlowLogger) {
|
|
14
|
+
return client.startupFlowLogger;
|
|
15
|
+
}
|
|
16
|
+
if (client?.logger?.info) {
|
|
17
|
+
return client.logger;
|
|
18
|
+
}
|
|
19
|
+
return client?.logger;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create the E2EE transport capability bound to one LINE client runtime.
|
|
23
|
+
*
|
|
24
|
+
* @param runtime - Mutable LINE client runtime.
|
|
25
|
+
* @returns E2EE transport methods bound to the runtime.
|
|
26
|
+
*/
|
|
27
|
+
export function createE2EEClient(runtime) {
|
|
28
|
+
return {
|
|
29
|
+
async getE2EEPublicKeys() {
|
|
30
|
+
const result = await runtime.sendTalk('getE2EEPublicKeys', []);
|
|
31
|
+
return result.fields?.[0] || [];
|
|
32
|
+
},
|
|
33
|
+
async registerE2EEPublicKey(version, keyId, keyData, timestamp) {
|
|
34
|
+
const result = await runtime.sendTalk('registerE2EEPublicKey', buildRegisterPublicKeyRequest(version, keyId, keyData, timestamp));
|
|
35
|
+
return result.fields?.[0] || null;
|
|
36
|
+
},
|
|
37
|
+
async negotiateE2EEPublicKey(mid) {
|
|
38
|
+
const result = await runtime.sendTalk('negotiateE2EEPublicKey', buildNegotiatePublicKeyRequest(mid));
|
|
39
|
+
return result.fields?.[0] || null;
|
|
40
|
+
},
|
|
41
|
+
async getE2EEPublicKeysEx(mids) {
|
|
42
|
+
const result = await runtime.sendTalk('getE2EEPublicKeysEx', buildGetPublicKeysRequest(mids));
|
|
43
|
+
return result.fields?.[0] || [];
|
|
44
|
+
},
|
|
45
|
+
async getE2EEMessageInfo(mid, messageId, receiverKeyId) {
|
|
46
|
+
const result = await runtime.sendTalk('getE2EEMessageInfo', buildGetE2EEMessageInfoRequest(mid, messageId, receiverKeyId));
|
|
47
|
+
return result.fields?.[0] || null;
|
|
48
|
+
},
|
|
49
|
+
async getLastE2EEGroupSharedKey(keyVersion, chatMid) {
|
|
50
|
+
const result = await runtime.sendTalk('getLastE2EEGroupSharedKey', buildGetLastGroupSharedKeyRequest(keyVersion, chatMid));
|
|
51
|
+
const shared = result.fields?.[0] || null;
|
|
52
|
+
getE2EEClientLog(runtime)?.info?.('e2ee.group_shared_key.response', buildSharedKeyEventLog(chatMid, shared));
|
|
53
|
+
return shared;
|
|
54
|
+
},
|
|
55
|
+
async getLastE2EEPublicKeys(chatMid) {
|
|
56
|
+
const result = await runtime.sendTalk('getLastE2EEPublicKeys', buildGetLastPublicKeysRequest(chatMid));
|
|
57
|
+
return result.fields?.[0] || {};
|
|
58
|
+
},
|
|
59
|
+
async registerE2EEGroupKey(keyVersion, chatMid, members, keyIds, encryptedSharedKeys) {
|
|
60
|
+
const result = await runtime.sendTalk('registerE2EEGroupKey', buildRegisterGroupKeyRequest(keyVersion, chatMid, members, keyIds, encryptedSharedKeys));
|
|
61
|
+
const shared = result.fields?.[0] || null;
|
|
62
|
+
getE2EEClientLog(runtime)?.info?.('e2ee.group_shared_key.registered', buildSharedKeyEventLog(chatMid, shared, {
|
|
63
|
+
members: members.length,
|
|
64
|
+
key_ids: keyIds.length,
|
|
65
|
+
}));
|
|
66
|
+
return shared;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { i32Field, i64Field, listField, stringField, structField, } from '../../core/thrift/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build the registerE2EEPublicKey request fields.
|
|
4
|
+
*
|
|
5
|
+
* @param version - Public key version.
|
|
6
|
+
* @param keyId - Assigned key identifier.
|
|
7
|
+
* @param keyData - Public key payload.
|
|
8
|
+
* @param timestamp - Registration timestamp.
|
|
9
|
+
* @returns Thrift request fields.
|
|
10
|
+
*/
|
|
11
|
+
export function buildRegisterPublicKeyRequest(version, keyId, keyData, timestamp) {
|
|
12
|
+
return [
|
|
13
|
+
i32Field(1, 0),
|
|
14
|
+
structField(2, [
|
|
15
|
+
i32Field(1, version),
|
|
16
|
+
i32Field(2, keyId),
|
|
17
|
+
stringField(4, keyData),
|
|
18
|
+
i64Field(5, timestamp),
|
|
19
|
+
]),
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build the negotiateE2EEPublicKey request fields.
|
|
24
|
+
*
|
|
25
|
+
* @param mid - Target MID.
|
|
26
|
+
* @returns Thrift request fields.
|
|
27
|
+
*/
|
|
28
|
+
export function buildNegotiatePublicKeyRequest(mid) {
|
|
29
|
+
return [stringField(2, mid)];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build the getE2EEPublicKeysEx request fields.
|
|
33
|
+
*
|
|
34
|
+
* @param mids - Target MIDs.
|
|
35
|
+
* @returns Thrift request fields.
|
|
36
|
+
*/
|
|
37
|
+
export function buildGetPublicKeysRequest(mids) {
|
|
38
|
+
return [listField(2, 11, mids)];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the getE2EEMessageInfo request fields.
|
|
42
|
+
*
|
|
43
|
+
* @param mid - Peer or chat MID.
|
|
44
|
+
* @param messageId - LINE message id.
|
|
45
|
+
* @param receiverKeyId - Receiver key id from the E2EE envelope.
|
|
46
|
+
* @returns Thrift request fields.
|
|
47
|
+
*/
|
|
48
|
+
export function buildGetE2EEMessageInfoRequest(mid, messageId, receiverKeyId) {
|
|
49
|
+
return [
|
|
50
|
+
stringField(2, mid),
|
|
51
|
+
stringField(3, messageId),
|
|
52
|
+
i32Field(4, Number(receiverKeyId)),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the getLastE2EEGroupSharedKey request fields.
|
|
57
|
+
*
|
|
58
|
+
* @param keyVersion - Shared key version.
|
|
59
|
+
* @param chatMid - Target chat MID.
|
|
60
|
+
* @returns Thrift request fields.
|
|
61
|
+
*/
|
|
62
|
+
export function buildGetLastGroupSharedKeyRequest(keyVersion, chatMid) {
|
|
63
|
+
return [i32Field(2, keyVersion), stringField(3, chatMid)];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the getLastE2EEPublicKeys request fields.
|
|
67
|
+
*
|
|
68
|
+
* @param chatMid - Target chat MID.
|
|
69
|
+
* @returns Thrift request fields.
|
|
70
|
+
*/
|
|
71
|
+
export function buildGetLastPublicKeysRequest(chatMid) {
|
|
72
|
+
return [stringField(2, chatMid)];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build the registerE2EEGroupKey request fields.
|
|
76
|
+
*
|
|
77
|
+
* @param keyVersion - Shared key version.
|
|
78
|
+
* @param chatMid - Target chat MID.
|
|
79
|
+
* @param members - Group member MIDs.
|
|
80
|
+
* @param keyIds - Public key IDs.
|
|
81
|
+
* @param encryptedSharedKeys - Encrypted shared keys.
|
|
82
|
+
* @returns Thrift request fields.
|
|
83
|
+
*/
|
|
84
|
+
export function buildRegisterGroupKeyRequest(keyVersion, chatMid, members, keyIds, encryptedSharedKeys) {
|
|
85
|
+
return [
|
|
86
|
+
i32Field(2, keyVersion),
|
|
87
|
+
stringField(3, chatMid),
|
|
88
|
+
listField(4, 11, members),
|
|
89
|
+
listField(5, 8, keyIds),
|
|
90
|
+
listField(6, 11, encryptedSharedKeys),
|
|
91
|
+
];
|
|
92
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { extractSharedKeyIdentity, readSharedKeyResponseField, } from './shared-key-payload.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build a normalized log payload for group shared-key transport events.
|
|
5
|
+
*
|
|
6
|
+
* @param chatMid - Group or room id.
|
|
7
|
+
* @param shared - Shared-key response payload.
|
|
8
|
+
* @param extra - Additional fields to merge into the payload.
|
|
9
|
+
* @returns Log payload object.
|
|
10
|
+
*/
|
|
11
|
+
export function buildSharedKeyEventLog(chatMid, shared, extra = {}) {
|
|
12
|
+
const encryptedSharedKey = readSharedKeyResponseField(shared, 'encryptedSharedKey', 7);
|
|
13
|
+
return {
|
|
14
|
+
chat: chatMid,
|
|
15
|
+
...extractSharedKeyIdentity(shared),
|
|
16
|
+
encrypted_shared_key_bytes: Buffer.isBuffer(encryptedSharedKey) ||
|
|
17
|
+
encryptedSharedKey instanceof Uint8Array
|
|
18
|
+
? encryptedSharedKey.length
|
|
19
|
+
: typeof encryptedSharedKey === 'string'
|
|
20
|
+
? encryptedSharedKey.length
|
|
21
|
+
: 0,
|
|
22
|
+
...extra,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LINE shared-key response payload normalization.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Resolve one shared-key field from either a named property or raw tuple form.
|
|
6
|
+
*
|
|
7
|
+
* @param shared - Shared-key response payload.
|
|
8
|
+
* @param namedKey - Preferred named property.
|
|
9
|
+
* @param tupleIndex - Fallback tuple index.
|
|
10
|
+
* @returns Resolved field value or null.
|
|
11
|
+
*/
|
|
12
|
+
export function readSharedKeyResponseField(shared, namedKey, tupleIndex) {
|
|
13
|
+
if (shared?.[namedKey] !== undefined && shared?.[namedKey] !== null) {
|
|
14
|
+
return shared[namedKey];
|
|
15
|
+
}
|
|
16
|
+
if (shared?.[tupleIndex] !== undefined && shared?.[tupleIndex] !== null) {
|
|
17
|
+
return shared[tupleIndex];
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Extract normalized shared-key identity fields from a LINE response payload.
|
|
23
|
+
*
|
|
24
|
+
* @param shared - Shared-key response payload.
|
|
25
|
+
* @returns Normalized shared-key fields used by downstream logging/diagnostics.
|
|
26
|
+
*/
|
|
27
|
+
export function extractSharedKeyIdentity(shared) {
|
|
28
|
+
return {
|
|
29
|
+
creator: readSharedKeyResponseField(shared, 'creator', 3),
|
|
30
|
+
creator_key_id: readSharedKeyResponseField(shared, 'creatorKeyId', 4),
|
|
31
|
+
receiver_key_id: readSharedKeyResponseField(shared, 'receiverKeyId', 6),
|
|
32
|
+
group_key_id: readSharedKeyResponseField(shared, 'groupKeyId', 2),
|
|
33
|
+
has_encrypted_shared_key: Boolean(readSharedKeyResponseField(shared, 'encryptedSharedKey', 7)),
|
|
34
|
+
};
|
|
35
|
+
}
|