@rlynicrisis/link 0.0.1 → 0.0.2

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.
@@ -1,6 +1,7 @@
1
1
  import { Buffer } from 'buffer';
2
2
  import { CmdCodes } from './constants.js';
3
3
  import { EmbMessage, EmbCommand } from './types.js';
4
+ import { emb } from './message.js';
4
5
 
5
6
  const HEAD = 0x5;
6
7
  const OPT = 0x0;
@@ -12,26 +13,41 @@ export class ProtocolError extends Error {
12
13
  }
13
14
  }
14
15
 
15
- // Simple encryption from Java client: ~byte then Base64
16
- export function encryptContent(raw: string): string {
17
- if (!raw) return raw;
18
- const buffer = Buffer.from(raw, 'utf-8');
16
+ // Simple encryption from Java client: Base64
17
+ export function encryptContent(raw: string | Buffer): string {
18
+ if (!raw) return raw as string;
19
+ let buffer: Buffer;
20
+ if (Buffer.isBuffer(raw)) {
21
+ buffer = Buffer.from(raw); // Copy to avoid modifying original
22
+ } else {
23
+ buffer = Buffer.from(raw, 'utf-8');
24
+ }
25
+
26
+ // Restore bitwise NOT (~) to match provided Java code
27
+ // It seems sending requires encryption, while receiving might not (or depends on msg type)
19
28
  for (let i = 0; i < buffer.length; i++) {
20
29
  buffer[i] = ~buffer[i];
21
30
  }
22
31
  return buffer.toString('base64');
23
32
  }
24
33
 
25
- export function decryptContent(encoded: string): string {
26
- if (!encoded) return encoded;
34
+ export function decryptContentBuffer(encoded: string): Buffer {
35
+ if (!encoded) return Buffer.alloc(0);
27
36
  const buffer = Buffer.from(encoded, 'base64');
37
+
28
38
  for (let i = 0; i < buffer.length; i++) {
29
39
  buffer[i] = ~buffer[i];
30
40
  }
41
+
42
+ return buffer;
43
+ }
44
+
45
+ export function decryptContent(encoded: string): string {
46
+ const buffer = decryptContentBuffer(encoded);
31
47
  return buffer.toString('utf-8');
32
48
  }
33
49
 
34
- export function encodePacket(cmdCode: number, body?: string): Buffer {
50
+ export function encodePacket(cmdCode: number, body?: string | Buffer): Buffer {
35
51
  const header = Buffer.alloc(4);
36
52
  header[0] = HEAD;
37
53
  header[1] = cmdCode;
@@ -43,14 +59,21 @@ export function encodePacket(cmdCode: number, body?: string): Buffer {
43
59
  }
44
60
 
45
61
  header[3] = 1; // Body count = 1
46
- const bodyBuffer = Buffer.from(body, 'utf-8');
62
+
63
+ let bodyBuffer: Buffer;
64
+ if (Buffer.isBuffer(body)) {
65
+ bodyBuffer = body;
66
+ } else {
67
+ bodyBuffer = Buffer.from(body, 'utf-8');
68
+ }
69
+
47
70
  const lenBuffer = Buffer.alloc(4);
48
71
  lenBuffer.writeUInt32BE(bodyBuffer.length, 0);
49
72
 
50
73
  return Buffer.concat([header, lenBuffer, bodyBuffer]);
51
74
  }
52
75
 
53
- export function decodePacket(buffer: Buffer): { cmdCode: number; body?: string; consumed: number } {
76
+ export function decodePacket(buffer: Buffer): { cmdCode: number; body?: Buffer; consumed: number } {
54
77
  if (buffer.length < 4) {
55
78
  throw new ProtocolError('Incomplete header');
56
79
  }
@@ -75,221 +98,192 @@ export function decodePacket(buffer: Buffer): { cmdCode: number; body?: string;
75
98
  if (buffer.length < 8 + bodyLen) {
76
99
  throw new ProtocolError('Incomplete body');
77
100
  }
78
- const body = buffer.toString('utf-8', 8, 8 + bodyLen);
101
+ const body = buffer.subarray(8, 8 + bodyLen);
79
102
  return { cmdCode, body, consumed: 8 + bodyLen };
80
103
  }
81
104
 
82
105
  throw new ProtocolError(`Unsupported body count: ${bodyCount}`);
83
106
  }
84
107
 
85
- export function encodeMessage(msg: EmbMessage): string {
86
- // Map fields to JSON keys as per Java implementation
87
- const json = {
88
- msg_id: msg.id,
89
- msg_type: msg.type,
90
- content: msg.sign ? msg.content : encryptContent(msg.content),
91
- from_type: msg.fromType,
92
- from_id: msg.fromId,
93
- from_name: msg.fromName,
94
- from_company: msg.fromCompany,
95
- to_type: msg.toType,
96
- to_id: msg.toId,
97
- to_name: msg.toName,
98
- to_company: msg.toCompany,
99
- send_time: msg.sendTime,
100
- is_read: msg.read ? 1 : 0,
101
- sign: msg.sign
108
+ export function encodeMessage(msg: EmbMessage): Buffer {
109
+ // If protocol version is >= 3.0, server expects Protobuf encoded message in payload,
110
+ // NOT JSON string.
111
+
112
+ // Convert EmbMessage to Protobuf Message object
113
+ // Note: content field in Protobuf is bytes.
114
+ // We need to handle content encryption here:
115
+ // Raw Content -> ~ (Bitwise NOT) -> Buffer -> Protobuf content field
116
+
117
+ let contentBuffer: Buffer;
118
+ if (Buffer.isBuffer(msg.content)) {
119
+ contentBuffer = Buffer.from(msg.content);
120
+ } else {
121
+ contentBuffer = Buffer.from(msg.content, 'utf-8');
122
+ }
123
+
124
+ // Apply encryption (~)
125
+ for (let i = 0; i < contentBuffer.length; i++) {
126
+ contentBuffer[i] = ~contentBuffer[i];
127
+ }
128
+
129
+ const messagePayload = {
130
+ msgId: msg.id,
131
+ type: msg.type,
132
+ sendTime: msg.sendTime,
133
+ content: contentBuffer,
134
+ from: {
135
+ fromId: msg.fromId,
136
+ fromType: msg.fromType,
137
+ fromName: msg.fromName,
138
+ fromCompany: msg.fromCompany,
139
+ // fromDevice: ... (filled by server session)
140
+ },
141
+ to: {
142
+ toId: msg.toId,
143
+ toType: msg.toType,
144
+ toName: msg.toName,
145
+ toCompany: msg.toCompany
146
+ },
147
+ options: {
148
+ isRead: msg.read
149
+ }
102
150
  };
103
- return JSON.stringify(json);
151
+
152
+ const message = emb.message.format.Message.create(messagePayload);
153
+ return Buffer.from(emb.message.format.Message.encode(message).finish());
104
154
  }
105
155
 
106
- export function decodeMessage(jsonStr: string): EmbMessage {
156
+ export function decodeMessage(body: Buffer | string): EmbMessage {
157
+ // Try to decode as string first to check for JSON
158
+ let jsonStr = '';
159
+ if (Buffer.isBuffer(body)) {
160
+ jsonStr = body.toString('utf-8');
161
+ } else {
162
+ jsonStr = body;
163
+ }
164
+
107
165
  // Try to fix malformed JSON if it has leading garbage
108
166
  let cleanJsonStr = jsonStr;
109
167
 
110
168
  // Find first '{'
111
169
  const braceIndex = jsonStr.indexOf('{');
170
+ // Check if it looks like JSON
112
171
  if (braceIndex >= 0) {
113
172
  cleanJsonStr = jsonStr.substring(braceIndex);
114
- } else {
115
- // No JSON object start found?
116
- throw new Error("No JSON object found in message body");
117
- }
173
+
174
+ // Find last '}' to handle trailing garbage if any
175
+ const lastBraceIndex = cleanJsonStr.lastIndexOf('}');
176
+ if (lastBraceIndex >= 0 && lastBraceIndex < cleanJsonStr.length - 1) {
177
+ cleanJsonStr = cleanJsonStr.substring(0, lastBraceIndex + 1);
178
+ }
118
179
 
119
- // Find last '}' to handle trailing garbage if any
120
- const lastBraceIndex = cleanJsonStr.lastIndexOf('}');
121
- if (lastBraceIndex >= 0 && lastBraceIndex < cleanJsonStr.length - 1) {
122
- cleanJsonStr = cleanJsonStr.substring(0, lastBraceIndex + 1);
180
+ try {
181
+ const json = JSON.parse(cleanJsonStr);
182
+ const sign = json.sign;
183
+ // Return Buffer if encrypted, otherwise string
184
+ const content = sign ? json.content : decryptContentBuffer(json.content);
185
+
186
+ return {
187
+ id: json.msg_id,
188
+ type: json.msg_type,
189
+ content: content,
190
+ fromType: json.from_type,
191
+ fromId: json.from_id,
192
+ fromName: json.from_name,
193
+ fromCompany: json.from_company,
194
+ toType: json.to_type,
195
+ toId: json.to_id,
196
+ toName: json.to_name,
197
+ toCompany: json.to_company,
198
+ sendTime: json.send_time,
199
+ read: json.is_read === 1,
200
+ sign: sign
201
+ };
202
+ } catch (e) {
203
+ // Ignore JSON parse error and fall through to Protobuf check
204
+ }
123
205
  }
124
206
 
125
- // Attempt to remove non-JSON prefix if '{' check wasn't enough (e.g. if body starts with garbage but contains {)
126
- // The logs show bodies starting with $UUID... then JSON content?
127
- // Actually the logs show: $UUID... and then some binary/garbage? Or maybe the body IS just that string?
128
- // Wait, the logs show: "$c8b8dca6-..." ... is not valid JSON
129
- // And Raw Body: $c8b8dca6-... followed by some binary chars.
130
-
131
- // It seems the body received is NOT JSON.
132
- // The Java code suggests:
133
- // protected EmbMessage decode(JsonObject json) { ... }
134
- // And TcpProtocolV2.java:
135
- // public EmbCommand decodeCommandWithBody(byte[] body) {
136
- // if(bodyCount == 1) {
137
- // EmbCommandWithBody cmd = newInstance();
138
- // cmd.setCmdBody(Strings.newStringUtf8(body));
139
- // return cmd;
140
- // }
141
- // }
142
-
143
- // However, `SendMessage` (Cmd 0x3) is `EmbCommandWithMsgBase`.
144
- // Wait, `SendMessage` extends `EmbCommandWithMsgBase`.
145
- // `EmbCommandWithMsgBase` implements `EmbCommandWithMsg`.
146
- // `EmbCommandWithMsg` extends `EmbCommandWithBody`.
147
-
148
- // The `TcpProtocolV2` decodes the body as a UTF-8 string.
149
- // Then `MessageProtocol` decodes that string into an `EmbMessage`.
150
- // `MessageProtocolV3.decode(JsonObject json)` expects a JSON object.
151
-
152
- // BUT the logs show the body content starting with a UUID (maybe msgId?) and NOT a JSON object.
153
- // Example: $c8b8dca6-a423-48e1-8c15-911eebc316b0c...
154
-
155
- // Is it possible the server is sending V2 protocol or a different format?
156
- // Or maybe the body is encrypted?
157
- // `MessageProtocolV2` has `encrypt`/`decrypt`.
158
- // But `decode` calls `JsonObject.parse(body)`.
159
-
160
- // Let's look closely at the Java code again.
161
- // TcpProtocolV2:
162
- // byte[] body = ...
163
- // cmd.setCmdBody(Strings.newStringUtf8(body));
164
-
165
- // If the server sends `SEND_MSG` (0x3), the client receives it.
166
- // The body SHOULD be a JSON string representing the message.
167
-
168
- // Why does the log show `$UUID...`?
169
- // Maybe the `TcpProtocolV2` logic for parsing the packet is slightly off?
170
- // Packet structure:
171
- // HEAD(1) + CMD(1) + OPT(1) + BODY_COUNT(1)
172
- // If BODY_COUNT=1: + LENGTH(4) + BODY(LENGTH)
173
-
174
- // My implementation:
175
- // header[0] = HEAD;
176
- // header[1] = cmdCode;
177
- // header[3] = bodyCount;
178
- // ...
179
- // const bodyLen = buffer.readUInt32BE(4);
180
-
181
- // Let's verify if I am reading the length correctly.
182
- // If I read length wrong, I might be reading into the next packet or reading garbage.
183
-
184
- // In the logs:
185
- // Raw body: $c8b8dca6-a423-48e1-8c15-911eebc316b0c...
186
- // This looks like a UUID.
187
- // Maybe the body IS just a UUID? But SEND_MSG expects a full message.
188
-
189
- // Is it possible that `0x3` (SEND_MSG) from server to client has a different format?
190
- // `SendMessage` class:
191
- // public byte getCmdCode() { return CmdCodes.SEND_MSG; }
192
-
193
- // Wait, `SendMessage` is typically Client -> Server.
194
- // Server -> Client message push is also `SEND_MSG`?
195
- // Java client `TcpClient` handles received messages.
196
- // It decodes the command.
197
-
198
- // If the server is sending `SEND_MSG`, it should contain the message.
199
- // The logs show what looks like a message ID (UUID).
200
-
201
- // Maybe the packet has multiple bodies?
202
- // My code throws "Unsupported body count" if != 0 or 1.
203
- // It didn't throw that. So bodyCount is 1.
204
-
205
- // Maybe the length includes something else?
206
- // `TcpProtocolV2`: `Bytes.bytes4ToInt(bytesLen)` (Big Endian usually).
207
-
208
- // Let's try to parse the body as if it might be encrypted or formatted differently?
209
- // Or maybe I should log the hex of the body to see what's going on.
210
-
211
- // If I look at the "Raw body" output again:
212
- // $c8b8dca6-a423-48e1-8c15-911eebc316b0c...
213
- // It starts with `$`.
214
- // A UUID is 36 chars.
215
- // The log shows more chars after it.
216
- // It looks like `UUID` + `Content`?
217
-
218
- // Wait! In Java `MessageProtocolV2`:
219
- // encode: writes JSON.
220
- // decode: parses JSON.
221
-
222
- // Is it possible the server is using a custom serialization that is NOT JSON?
223
- // Or maybe I am connecting to a server version that uses a different protocol?
224
-
225
- // Let's relax the JSON requirement and return a raw message if JSON fails, just to see what happens.
226
-
227
- try {
228
- const json = JSON.parse(cleanJsonStr);
229
- const sign = json.sign;
230
- const content = sign ? json.content : decryptContent(json.content);
231
-
232
- return {
233
- id: json.msg_id,
234
- type: json.msg_type,
235
- content: content,
236
- fromType: json.from_type,
237
- fromId: json.from_id,
238
- fromName: json.from_name,
239
- fromCompany: json.from_company,
240
- toType: json.to_type,
241
- toId: json.to_id,
242
- toName: json.to_name,
243
- toCompany: json.to_company,
244
- sendTime: json.send_time,
245
- read: json.is_read === 1,
246
- sign: sign
247
- };
248
- } catch (e) {
249
- // If parsing fails, use regex to extract key fields as a fallback
250
- // The raw body seems to have format: $UUID...{JSON content}...binary?
251
- // Or maybe: $UUID...JSON...
252
-
253
- // Let's try to extract JSON-like structure using regex if standard parsing fails
254
- // This is risky but might work for the mixed content we are seeing
255
- const jsonMatch = jsonStr.match(/(\{.*\})/s);
256
- if (jsonMatch) {
257
- try {
258
- const json = JSON.parse(jsonMatch[1]);
259
- const sign = json.sign;
260
- const content = sign ? json.content : decryptContent(json.content);
261
- return {
262
- id: json.msg_id,
263
- type: json.msg_type,
264
- content: content,
265
- fromType: json.from_type,
266
- fromId: json.from_id,
267
- fromName: json.from_name,
268
- fromCompany: json.from_company,
269
- toType: json.to_type,
270
- toId: json.to_id,
271
- toName: json.to_name,
272
- toCompany: json.to_company,
273
- sendTime: json.send_time,
274
- read: json.is_read === 1,
275
- sign: sign
276
- };
277
- } catch (innerE) {
278
- // ignore
279
- }
207
+ // Fallback: Try Protobuf decoding
208
+ if (Buffer.isBuffer(body)) {
209
+ try {
210
+ // Check if body starts with $UUID... (Legacy/Custom framing?)
211
+ // If so, we might need to strip it.
212
+ // But let's try to decode the whole body first.
213
+ const message = emb.message.format.Message.decode(body);
214
+ return convertProtoMessage(message);
215
+ } catch (e) {
216
+ // Maybe it has a prefix?
217
+ // Log observed: $UUID...
218
+ // $ is 0x24.
219
+ if (body[0] === 0x24) { // '$'
220
+ // Try to find where the actual protobuf starts.
221
+ // A UUID is 36 chars.
222
+ // $ + 36 chars = 37 bytes.
223
+ // Maybe the protobuf starts after that?
224
+ if (body.length > 37) {
225
+ try {
226
+ const protoBody = body.subarray(37);
227
+ const message = emb.message.format.Message.decode(protoBody);
228
+ return convertProtoMessage(message);
229
+ } catch (innerE) {
230
+ // Ignore
231
+ }
232
+ }
233
+ }
280
234
  }
235
+ }
236
+
237
+ // Fallback: return raw content for debugging
238
+ // Try to extract ID if it looks like a UUID (e.g. $UUID...)
239
+ let id = 'unknown';
240
+ const uuidMatch = jsonStr.match(/\$([0-9a-fA-F-]{36})/);
241
+ if (uuidMatch) {
242
+ id = uuidMatch[1];
243
+ }
244
+
245
+ return {
246
+ id: id,
247
+ type: 1, // TEXT
248
+ content: jsonStr, // Return original string
249
+ fromType: 1,
250
+ fromId: 'unknown',
251
+ toType: 1,
252
+ toId: 'unknown',
253
+ sendTime: Date.now(),
254
+ read: false
255
+ };
256
+ }
281
257
 
282
- // Fallback: return raw content for debugging
283
- return {
284
- id: 'unknown',
285
- type: 1, // TEXT
286
- content: jsonStr, // Return original string
287
- fromType: 1,
288
- fromId: 'unknown',
289
- toType: 1,
290
- toId: 'unknown',
291
- sendTime: Date.now(),
292
- read: false
293
- };
258
+ function convertProtoMessage(protoMsg: any): EmbMessage {
259
+ let content: string | Buffer = '';
260
+ if (protoMsg.content && protoMsg.content.length > 0) {
261
+ // Protobuf content is bytes.
262
+ // Keep it as Buffer
263
+ content = Buffer.from(protoMsg.content);
264
+
265
+ // Decrypt content using bitwise NOT (~)
266
+ // This matches the encryption logic provided: raw -> ~ -> Base64 (for JSON)
267
+ // For Protobuf bytes, we just reverse the ~ operation.
268
+ for (let i = 0; i < content.length; i++) {
269
+ content[i] = ~content[i];
270
+ }
294
271
  }
272
+
273
+ return {
274
+ id: protoMsg.msgId || 'unknown',
275
+ type: protoMsg.type || 1,
276
+ content: content,
277
+ fromType: protoMsg.from?.fromType || 1,
278
+ fromId: protoMsg.from?.fromId || 'unknown',
279
+ fromName: protoMsg.from?.fromName || undefined,
280
+ fromCompany: protoMsg.from?.fromCompany || undefined,
281
+ toType: protoMsg.to?.toType || 1,
282
+ toId: protoMsg.to?.toId || 'unknown',
283
+ toName: protoMsg.to?.toName || undefined,
284
+ toCompany: protoMsg.to?.toCompany || undefined,
285
+ sendTime: typeof protoMsg.sendTime === 'number' ? protoMsg.sendTime : (protoMsg.sendTime?.toNumber() || Date.now()),
286
+ read: protoMsg.options?.isRead || false,
287
+ // Add other fields if needed
288
+ };
295
289
  }
package/src/link/types.ts CHANGED
@@ -3,7 +3,7 @@ import { MsgType, ParticipantType } from './constants.js';
3
3
  export interface EmbMessage {
4
4
  id: string;
5
5
  type: MsgType;
6
- content: string; // Encrypted or plain? Java code suggests encryption.
6
+ content: string | Buffer; // Encrypted or plain? Java code suggests encryption.
7
7
  fromType: ParticipantType;
8
8
  fromId: string;
9
9
  fromName?: string;
@@ -14,7 +14,7 @@ export interface EmbMessage {
14
14
  toCompany?: string;
15
15
  sendTime: number;
16
16
  read: boolean;
17
- sign?: string;
17
+ sign?: boolean | string;
18
18
  }
19
19
 
20
20
  export interface ClientVerifyInfo {
package/src/types.ts CHANGED
@@ -3,8 +3,10 @@ import { ClientVerifyInfo } from "./link/types.js";
3
3
 
4
4
  export interface LinkConfig {
5
5
  host: string;
6
- port: number;
6
+ // port: number; // merged into host
7
7
  accessToken: string;
8
+ refreshToken?: string;
9
+ ssoUrl?: string;
8
10
  verifyInfo?: Partial<ClientVerifyInfo>;
9
11
  heartbeatIntervalMs?: number;
10
12
  protocol?: "tcp" | "ws"; // Default tcp