@vex-chat/libvex 1.1.0 → 4.0.0
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/README.md +104 -41
- package/dist/Client.d.ts +473 -560
- package/dist/Client.d.ts.map +1 -0
- package/dist/Client.js +1486 -1551
- package/dist/Client.js.map +1 -1
- package/dist/Storage.d.ts +111 -0
- package/dist/Storage.d.ts.map +1 -0
- package/dist/Storage.js +2 -0
- package/dist/Storage.js.map +1 -0
- package/dist/__tests__/harness/memory-storage.d.ts +29 -27
- package/dist/__tests__/harness/memory-storage.d.ts.map +1 -0
- package/dist/__tests__/harness/memory-storage.js +120 -109
- package/dist/__tests__/harness/memory-storage.js.map +1 -1
- package/dist/codec.d.ts +44 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +51 -0
- package/dist/codec.js.map +1 -0
- package/dist/codecs.d.ts +201 -0
- package/dist/codecs.d.ts.map +1 -0
- package/dist/codecs.js +67 -0
- package/dist/codecs.js.map +1 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/keystore/memory.d.ts +5 -4
- package/dist/keystore/memory.d.ts.map +1 -0
- package/dist/keystore/memory.js +9 -7
- package/dist/keystore/memory.js.map +1 -1
- package/dist/keystore/node.d.ts +8 -6
- package/dist/keystore/node.d.ts.map +1 -0
- package/dist/keystore/node.js +47 -22
- package/dist/keystore/node.js.map +1 -1
- package/dist/preset/common.d.ts +7 -0
- package/dist/preset/common.d.ts.map +1 -0
- package/dist/preset/common.js +2 -0
- package/dist/preset/common.js.map +1 -0
- package/dist/preset/node.d.ts +4 -7
- package/dist/preset/node.d.ts.map +1 -0
- package/dist/preset/node.js +4 -11
- package/dist/preset/node.js.map +1 -1
- package/dist/preset/test.d.ts +4 -5
- package/dist/preset/test.d.ts.map +1 -0
- package/dist/preset/test.js +3 -20
- package/dist/preset/test.js.map +1 -1
- package/dist/storage/node.d.ts +3 -3
- package/dist/storage/node.d.ts.map +1 -0
- package/dist/storage/node.js +4 -10
- package/dist/storage/node.js.map +1 -1
- package/dist/storage/schema.d.ts +55 -54
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/sqlite.d.ts +41 -28
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +339 -297
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/transport/types.d.ts +17 -16
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/websocket.d.ts +26 -0
- package/dist/transport/websocket.d.ts.map +1 -0
- package/dist/transport/websocket.js +83 -0
- package/dist/transport/websocket.js.map +1 -0
- package/dist/types/crypto.d.ts +38 -0
- package/dist/types/crypto.d.ts.map +1 -0
- package/dist/types/crypto.js +9 -0
- package/dist/types/crypto.js.map +1 -0
- package/dist/types/identity.d.ts +22 -0
- package/dist/types/identity.d.ts.map +1 -0
- package/dist/types/identity.js +6 -0
- package/dist/types/identity.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/capitalize.d.ts +1 -0
- package/dist/utils/capitalize.d.ts.map +1 -0
- package/dist/utils/formatBytes.d.ts +1 -0
- package/dist/utils/formatBytes.d.ts.map +1 -0
- package/dist/utils/formatBytes.js +3 -1
- package/dist/utils/formatBytes.js.map +1 -1
- package/dist/utils/sqlSessionToCrypto.d.ts +4 -2
- package/dist/utils/sqlSessionToCrypto.d.ts.map +1 -0
- package/dist/utils/sqlSessionToCrypto.js +5 -5
- package/dist/utils/sqlSessionToCrypto.js.map +1 -1
- package/dist/utils/uint8uuid.d.ts +1 -4
- package/dist/utils/uint8uuid.d.ts.map +1 -0
- package/dist/utils/uint8uuid.js +1 -7
- package/dist/utils/uint8uuid.js.map +1 -1
- package/package.json +74 -91
- package/src/Client.ts +3086 -0
- package/{dist/IStorage.d.ts → src/Storage.ts} +70 -62
- package/src/__tests__/codec.test.ts +256 -0
- package/src/__tests__/ghost.png +0 -0
- package/src/__tests__/harness/fixtures.ts +22 -0
- package/src/__tests__/harness/memory-storage.ts +254 -0
- package/src/__tests__/harness/platform-transports.ts +4 -0
- package/src/__tests__/harness/poison-node-imports.ts +107 -0
- package/src/__tests__/harness/shared-suite.ts +426 -0
- package/src/__tests__/platform-browser.test.ts +14 -0
- package/src/__tests__/platform-node.test.ts +9 -0
- package/src/__tests__/triggered.png +0 -0
- package/src/codec.ts +68 -0
- package/src/codecs.ts +101 -0
- package/src/index.ts +40 -0
- package/src/keystore/memory.ts +30 -0
- package/src/keystore/node.ts +102 -0
- package/src/preset/common.ts +7 -0
- package/src/preset/node.ts +18 -0
- package/src/preset/test.ts +20 -0
- package/src/storage/node.ts +22 -0
- package/src/storage/schema.ts +94 -0
- package/src/storage/sqlite.ts +655 -0
- package/src/transport/types.ts +22 -0
- package/src/transport/websocket.ts +106 -0
- package/src/types/crypto.ts +42 -0
- package/src/types/identity.ts +23 -0
- package/src/types/index.ts +9 -0
- package/src/utils/capitalize.ts +6 -0
- package/src/utils/formatBytes.ts +15 -0
- package/src/utils/sqlSessionToCrypto.ts +16 -0
- package/src/utils/uint8uuid.ts +7 -0
- package/dist/IStorage.js +0 -2
- package/dist/IStorage.js.map +0 -1
- package/dist/keystore/types.d.ts +0 -4
- package/dist/keystore/types.js +0 -2
- package/dist/keystore/types.js.map +0 -1
- package/dist/preset/expo.d.ts +0 -2
- package/dist/preset/expo.js +0 -39
- package/dist/preset/expo.js.map +0 -1
- package/dist/preset/tauri.d.ts +0 -2
- package/dist/preset/tauri.js +0 -36
- package/dist/preset/tauri.js.map +0 -1
- package/dist/preset/types.d.ts +0 -14
- package/dist/preset/types.js +0 -2
- package/dist/preset/types.js.map +0 -1
- package/dist/storage/expo.d.ts +0 -3
- package/dist/storage/expo.js +0 -18
- package/dist/storage/expo.js.map +0 -1
- package/dist/storage/tauri.d.ts +0 -3
- package/dist/storage/tauri.js +0 -21
- package/dist/storage/tauri.js.map +0 -1
- package/dist/transport/browser.d.ts +0 -17
- package/dist/transport/browser.js +0 -56
- package/dist/transport/browser.js.map +0 -1
- package/dist/utils/constants.d.ts +0 -8
- package/dist/utils/constants.js +0 -9
- package/dist/utils/constants.js.map +0 -1
- package/dist/utils/createLogger.d.ts +0 -5
- package/dist/utils/createLogger.js +0 -27
- package/dist/utils/createLogger.js.map +0 -1
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { Message } from "../../index.js";
|
|
2
|
+
import type { Storage } from "../../Storage.js";
|
|
3
|
+
import type {
|
|
4
|
+
PreKeysCrypto,
|
|
5
|
+
SessionCrypto,
|
|
6
|
+
UnsavedPreKey,
|
|
7
|
+
} from "../../types/index.js";
|
|
8
|
+
import type { Device, PreKeysSQL, SessionSQL } from "@vex-chat/types";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type KeyPair,
|
|
12
|
+
xBoxKeyPairFromSecret,
|
|
13
|
+
XKeyConvert,
|
|
14
|
+
xSecretbox,
|
|
15
|
+
xSecretboxOpen,
|
|
16
|
+
xSignKeyPairFromSecret,
|
|
17
|
+
XUtils,
|
|
18
|
+
} from "@vex-chat/crypto";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Minimal in-memory Storage for browser/RN platform tests.
|
|
22
|
+
*
|
|
23
|
+
* Uses eventemitter3 (browser-safe) instead of Node's events module.
|
|
24
|
+
* No persistence — just enough for the register/login/connect/DM test flow.
|
|
25
|
+
*/
|
|
26
|
+
import { EventEmitter } from "eventemitter3";
|
|
27
|
+
|
|
28
|
+
export class MemoryStorage extends EventEmitter implements Storage {
|
|
29
|
+
public ready = false;
|
|
30
|
+
private readonly devices: Device[] = [];
|
|
31
|
+
private readonly idKeys: KeyPair;
|
|
32
|
+
private messages: Message[] = [];
|
|
33
|
+
private nextOtkIndex = 1;
|
|
34
|
+
private nextPreKeyIndex = 1;
|
|
35
|
+
private oneTimeKeys: any[] = [];
|
|
36
|
+
private preKeys: any[] = [];
|
|
37
|
+
private sessions: SessionSQL[] = [];
|
|
38
|
+
|
|
39
|
+
constructor(SK: string) {
|
|
40
|
+
super();
|
|
41
|
+
const idKeys = XKeyConvert.convertKeyPair(
|
|
42
|
+
xSignKeyPairFromSecret(XUtils.decodeHex(SK)),
|
|
43
|
+
);
|
|
44
|
+
if (!idKeys) throw new Error("Can't convert SK!");
|
|
45
|
+
this.idKeys = idKeys;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
close(): Promise<void> {
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
deleteHistory(channelOrUserID: string): Promise<void> {
|
|
53
|
+
this.messages = this.messages.filter(
|
|
54
|
+
(m) =>
|
|
55
|
+
m.group !== channelOrUserID &&
|
|
56
|
+
m.authorID !== channelOrUserID &&
|
|
57
|
+
m.readerID !== channelOrUserID,
|
|
58
|
+
);
|
|
59
|
+
return Promise.resolve();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
deleteMessage(mailID: string): Promise<void> {
|
|
63
|
+
this.messages = this.messages.filter((m) => m.mailID !== mailID);
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
deleteOneTimeKey(index: number): Promise<void> {
|
|
68
|
+
this.oneTimeKeys = this.oneTimeKeys.filter((k) => k.index !== index);
|
|
69
|
+
return Promise.resolve();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getAllSessions(): Promise<SessionSQL[]> {
|
|
73
|
+
return Promise.resolve(
|
|
74
|
+
this.sessions.map((s) => ({
|
|
75
|
+
...s,
|
|
76
|
+
verified: s.verified,
|
|
77
|
+
})),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getDevice(deviceID: string): Promise<Device | null> {
|
|
82
|
+
return Promise.resolve(
|
|
83
|
+
this.devices.find((d) => d.deviceID === deviceID) ?? null,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getGroupHistory(channelID: string): Promise<Message[]> {
|
|
88
|
+
return Promise.resolve(
|
|
89
|
+
this.messages
|
|
90
|
+
.filter((m) => m.group === channelID)
|
|
91
|
+
.map((m) => this.decryptMessage(m)),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getMessageHistory(userID: string): Promise<Message[]> {
|
|
96
|
+
return Promise.resolve(
|
|
97
|
+
this.messages
|
|
98
|
+
.filter(
|
|
99
|
+
(m) =>
|
|
100
|
+
(m.direction === "incoming" &&
|
|
101
|
+
m.authorID === userID &&
|
|
102
|
+
!m.group) ||
|
|
103
|
+
(m.direction === "outgoing" &&
|
|
104
|
+
m.readerID === userID &&
|
|
105
|
+
!m.group),
|
|
106
|
+
)
|
|
107
|
+
.map((m) => this.decryptMessage(m)),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getOneTimeKey(index: number): Promise<null | PreKeysCrypto> {
|
|
112
|
+
const otk = this.oneTimeKeys.find((k) => k.index === index);
|
|
113
|
+
if (!otk || !otk.privateKey) return Promise.resolve(null);
|
|
114
|
+
return Promise.resolve({
|
|
115
|
+
index: otk.index,
|
|
116
|
+
keyPair: xBoxKeyPairFromSecret(XUtils.decodeHex(otk.privateKey)),
|
|
117
|
+
signature: XUtils.decodeHex(otk.signature),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getPreKeys(): Promise<null | PreKeysCrypto> {
|
|
122
|
+
if (this.preKeys.length === 0) return Promise.resolve(null);
|
|
123
|
+
const pk = this.preKeys[0];
|
|
124
|
+
if (!pk.privateKey) return Promise.resolve(null);
|
|
125
|
+
return Promise.resolve({
|
|
126
|
+
index: pk.index,
|
|
127
|
+
keyPair: xBoxKeyPairFromSecret(XUtils.decodeHex(pk.privateKey)),
|
|
128
|
+
signature: XUtils.decodeHex(pk.signature),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getSessionByDeviceID(deviceID: string): Promise<null | SessionCrypto> {
|
|
133
|
+
const s = this.sessions.find((s) => s.deviceID === deviceID);
|
|
134
|
+
if (!s) return Promise.resolve(null);
|
|
135
|
+
return Promise.resolve(this.sqlToCrypto(s));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
getSessionByPublicKey(
|
|
139
|
+
publicKey: Uint8Array,
|
|
140
|
+
): Promise<null | SessionCrypto> {
|
|
141
|
+
const hex = XUtils.encodeHex(publicKey);
|
|
142
|
+
const s = this.sessions.find((s) => s.publicKey === hex);
|
|
143
|
+
if (!s) return Promise.resolve(null);
|
|
144
|
+
return Promise.resolve(this.sqlToCrypto(s));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
init(): Promise<void> {
|
|
148
|
+
this.ready = true;
|
|
149
|
+
this.emit("ready");
|
|
150
|
+
return Promise.resolve();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
markSessionUsed(sessionID: string): Promise<void> {
|
|
154
|
+
const s = this.sessions.find((s) => s.sessionID === sessionID);
|
|
155
|
+
if (s) s.lastUsed = new Date().toISOString();
|
|
156
|
+
return Promise.resolve();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
markSessionVerified(sessionID: string): Promise<void> {
|
|
160
|
+
const s = this.sessions.find((s) => s.sessionID === sessionID);
|
|
161
|
+
if (s) s.verified = true;
|
|
162
|
+
return Promise.resolve();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
purgeHistory(): Promise<void> {
|
|
166
|
+
this.messages = [];
|
|
167
|
+
return Promise.resolve();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
purgeKeyData(): Promise<void> {
|
|
171
|
+
this.sessions = [];
|
|
172
|
+
this.preKeys = [];
|
|
173
|
+
this.oneTimeKeys = [];
|
|
174
|
+
this.messages = [];
|
|
175
|
+
return Promise.resolve();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
saveDevice(device: Device): Promise<void> {
|
|
179
|
+
if (!this.devices.find((d) => d.deviceID === device.deviceID)) {
|
|
180
|
+
this.devices.push(device);
|
|
181
|
+
}
|
|
182
|
+
return Promise.resolve();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
saveMessage(message: Message): Promise<void> {
|
|
186
|
+
const copy = { ...message };
|
|
187
|
+
copy.message = XUtils.encodeHex(
|
|
188
|
+
xSecretbox(
|
|
189
|
+
XUtils.decodeUTF8(message.message),
|
|
190
|
+
XUtils.decodeHex(message.nonce),
|
|
191
|
+
this.idKeys.secretKey,
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
this.messages.push(copy);
|
|
195
|
+
return Promise.resolve();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
savePreKeys(
|
|
199
|
+
preKeys: UnsavedPreKey[],
|
|
200
|
+
oneTime: boolean,
|
|
201
|
+
): Promise<PreKeysSQL[]> {
|
|
202
|
+
const added: PreKeysSQL[] = [];
|
|
203
|
+
for (const pk of preKeys) {
|
|
204
|
+
const idx = oneTime ? this.nextOtkIndex++ : this.nextPreKeyIndex++;
|
|
205
|
+
const row = {
|
|
206
|
+
index: idx,
|
|
207
|
+
privateKey: XUtils.encodeHex(pk.keyPair.secretKey),
|
|
208
|
+
publicKey: XUtils.encodeHex(pk.keyPair.publicKey),
|
|
209
|
+
signature: XUtils.encodeHex(pk.signature),
|
|
210
|
+
};
|
|
211
|
+
if (oneTime) this.oneTimeKeys.push(row);
|
|
212
|
+
else this.preKeys.push(row);
|
|
213
|
+
// Return without privateKey (matches real Storage behavior)
|
|
214
|
+
added.push({
|
|
215
|
+
index: idx,
|
|
216
|
+
publicKey: row.publicKey,
|
|
217
|
+
signature: row.signature,
|
|
218
|
+
} as PreKeysSQL);
|
|
219
|
+
}
|
|
220
|
+
return Promise.resolve(added);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
saveSession(session: SessionSQL): Promise<void> {
|
|
224
|
+
if (!this.sessions.find((s) => s.SK === session.SK)) {
|
|
225
|
+
this.sessions.push(session);
|
|
226
|
+
}
|
|
227
|
+
return Promise.resolve();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private decryptMessage(msg: Message): Message {
|
|
231
|
+
const copy = { ...msg };
|
|
232
|
+
if (copy.decrypted) {
|
|
233
|
+
const dec = xSecretboxOpen(
|
|
234
|
+
XUtils.decodeHex(copy.message),
|
|
235
|
+
XUtils.decodeHex(copy.nonce),
|
|
236
|
+
this.idKeys.secretKey,
|
|
237
|
+
);
|
|
238
|
+
if (dec) copy.message = XUtils.encodeUTF8(dec);
|
|
239
|
+
}
|
|
240
|
+
return copy;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private sqlToCrypto(s: SessionSQL): SessionCrypto {
|
|
244
|
+
return {
|
|
245
|
+
fingerprint: XUtils.decodeHex(s.fingerprint),
|
|
246
|
+
lastUsed: s.lastUsed,
|
|
247
|
+
mode: s.mode,
|
|
248
|
+
publicKey: XUtils.decodeHex(s.publicKey),
|
|
249
|
+
sessionID: s.sessionID,
|
|
250
|
+
SK: XUtils.decodeHex(s.SK),
|
|
251
|
+
userID: s.userID,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Vite plugin that catches Node builtin imports AND Node-only globals
|
|
5
|
+
* in library source (src/) during transformation.
|
|
6
|
+
*
|
|
7
|
+
* Vitest runs in Node where builtins resolve natively and globals like
|
|
8
|
+
* Buffer/process exist — so tests pass even when the code would crash
|
|
9
|
+
* in a browser or Tauri webview. This plugin catches those at transform
|
|
10
|
+
* time, which vitest does invoke.
|
|
11
|
+
*
|
|
12
|
+
* Uses Node's own builtinModules list — no manual maintenance needed.
|
|
13
|
+
*/
|
|
14
|
+
import { builtinModules } from "node:module";
|
|
15
|
+
|
|
16
|
+
const nodeBuiltins = new Set([
|
|
17
|
+
...builtinModules,
|
|
18
|
+
...builtinModules.map((m) => `node:${m}`),
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
// Node-only globals that don't exist in browsers/Tauri/RN.
|
|
22
|
+
// \bBuffer\b won't match ArrayBuffer or SharedArrayBuffer (no word boundary).
|
|
23
|
+
const NODE_GLOBALS = ["Buffer", "process", "__dirname", "__filename"];
|
|
24
|
+
|
|
25
|
+
// Matches: import ... from "events" or import ... from 'node:os'
|
|
26
|
+
// Also: export ... from "events"
|
|
27
|
+
const IMPORT_RE = /(?:import|export)\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
|
|
28
|
+
// Matches: await import("events")
|
|
29
|
+
const DYNAMIC_IMPORT_RE = /import\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
30
|
+
|
|
31
|
+
type Violation = { kind: "global" | "import"; line: number; name: string };
|
|
32
|
+
|
|
33
|
+
export function poisonNodeImports(): Plugin {
|
|
34
|
+
return {
|
|
35
|
+
enforce: "pre",
|
|
36
|
+
name: "poison-node-imports",
|
|
37
|
+
transform(code: string, id: string) {
|
|
38
|
+
// Only check library source — not tests or dependencies
|
|
39
|
+
if (!id.includes("/src/")) return null;
|
|
40
|
+
if (id.includes("__tests__")) return null;
|
|
41
|
+
if (id.includes("node_modules")) return null;
|
|
42
|
+
if (isNodeOnlyFile(id)) return null;
|
|
43
|
+
|
|
44
|
+
const violations = findViolations(code);
|
|
45
|
+
if (violations.length === 0) return null;
|
|
46
|
+
|
|
47
|
+
const file = id.replace(/^.*\/src\//, "src/");
|
|
48
|
+
const msgs = violations
|
|
49
|
+
.map((v) => ` line ${String(v.line)}: ${v.name} (${v.kind})`)
|
|
50
|
+
.join("\n");
|
|
51
|
+
throw new Error(
|
|
52
|
+
`[platform-guard] Node-only code in ${file}:\n${msgs}\n` +
|
|
53
|
+
`These would crash in browser/RN/Tauri. Use browser-safe alternatives or dynamic imports.`,
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function findViolations(code: string): Violation[] {
|
|
60
|
+
const results: Violation[] = [];
|
|
61
|
+
const lines = code.split("\n");
|
|
62
|
+
const strippedLines = stripComments(code).split("\n");
|
|
63
|
+
|
|
64
|
+
// Check imports against the original code (comments don't affect import syntax)
|
|
65
|
+
for (let i = 0; i < lines.length; i++) {
|
|
66
|
+
const lineText = lines[i];
|
|
67
|
+
for (const re of [IMPORT_RE, DYNAMIC_IMPORT_RE]) {
|
|
68
|
+
re.lastIndex = 0;
|
|
69
|
+
let match;
|
|
70
|
+
while ((match = re.exec(lineText ?? "")) !== null) {
|
|
71
|
+
const mod = match[1]?.replace(/\.js$/, "").replace(/\.ts$/, "");
|
|
72
|
+
if (mod !== undefined && nodeBuiltins.has(mod)) {
|
|
73
|
+
results.push({
|
|
74
|
+
kind: "import",
|
|
75
|
+
line: i + 1,
|
|
76
|
+
name: match[1] ?? "",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check globals against comment-stripped code
|
|
84
|
+
for (let i = 0; i < strippedLines.length; i++) {
|
|
85
|
+
const lineText = strippedLines[i] ?? "";
|
|
86
|
+
for (const g of NODE_GLOBALS) {
|
|
87
|
+
const re = new RegExp(`\\b${g}\\b`);
|
|
88
|
+
if (re.test(lineText)) {
|
|
89
|
+
results.push({ kind: "global", line: i + 1, name: g });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isNodeOnlyFile(id: string): boolean {
|
|
98
|
+
// These files are only loaded via dynamic import on the Node path.
|
|
99
|
+
if (id.includes("/storage/node")) return true;
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Strip comments to avoid false positives on globals in JSDoc / inline comments. */
|
|
104
|
+
function stripComments(code: string): string {
|
|
105
|
+
// Remove single-line comments, but not URLs (://), and multi-line comments
|
|
106
|
+
return code.replace(/\/\/(?!:).*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
107
|
+
}
|