@mushi-mushi/core 0.1.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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/index.cjs +680 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +330 -0
- package/dist/index.d.ts +330 -0
- package/dist/index.js +668 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
// src/api-client.ts
|
|
2
|
+
var DEFAULT_API_ENDPOINT = "https://api.mushimushi.dev";
|
|
3
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
4
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
5
|
+
function createApiClient(options) {
|
|
6
|
+
const {
|
|
7
|
+
projectId,
|
|
8
|
+
apiKey,
|
|
9
|
+
apiEndpoint = DEFAULT_API_ENDPOINT,
|
|
10
|
+
timeout = DEFAULT_TIMEOUT,
|
|
11
|
+
maxRetries = DEFAULT_MAX_RETRIES
|
|
12
|
+
} = options;
|
|
13
|
+
const baseUrl = apiEndpoint.replace(/\/$/, "");
|
|
14
|
+
async function request(method, path, body, retries = maxRetries) {
|
|
15
|
+
const url = `${baseUrl}${path}`;
|
|
16
|
+
const controller = new AbortController();
|
|
17
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetch(url, {
|
|
20
|
+
method,
|
|
21
|
+
headers: {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
"X-Mushi-Api-Key": apiKey,
|
|
24
|
+
"X-Mushi-Project": projectId
|
|
25
|
+
},
|
|
26
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
27
|
+
signal: controller.signal
|
|
28
|
+
});
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
32
|
+
if (response.status >= 500 && retries > 0) {
|
|
33
|
+
await sleep(getBackoffDelay(maxRetries - retries));
|
|
34
|
+
return request(method, path, body, retries - 1);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
error: {
|
|
39
|
+
code: `HTTP_${response.status}`,
|
|
40
|
+
message: errorBody.message || `HTTP ${response.status} error`
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
return { ok: true, data };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
if (retries > 0 && isRetryable(error)) {
|
|
49
|
+
await sleep(getBackoffDelay(maxRetries - retries));
|
|
50
|
+
return request(method, path, body, retries - 1);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
error: {
|
|
55
|
+
code: "NETWORK_ERROR",
|
|
56
|
+
message: error instanceof Error ? error.message : "Unknown network error"
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
async submitReport(report) {
|
|
63
|
+
return request("POST", "/v1/reports", report);
|
|
64
|
+
},
|
|
65
|
+
async getReportStatus(reportId) {
|
|
66
|
+
return request("GET", `/v1/reports/${reportId}/status`);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function sleep(ms) {
|
|
71
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
72
|
+
}
|
|
73
|
+
function getBackoffDelay(attempt) {
|
|
74
|
+
return Math.min(1e3 * 2 ** attempt + Math.random() * 500, 1e4);
|
|
75
|
+
}
|
|
76
|
+
function isRetryable(error) {
|
|
77
|
+
if (error instanceof DOMException && error.name === "AbortError") return true;
|
|
78
|
+
if (error instanceof TypeError) return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/pre-filter.ts
|
|
83
|
+
var DEFAULT_MIN_LENGTH = 10;
|
|
84
|
+
var DEFAULT_MAX_LENGTH = 2e3;
|
|
85
|
+
var SPAM_PATTERNS = [
|
|
86
|
+
/^(.)\1{10,}$/,
|
|
87
|
+
// repeated single character
|
|
88
|
+
/^[A-Z\s!?]{20,}$/,
|
|
89
|
+
// all caps shouting
|
|
90
|
+
/^[\d\s]+$/,
|
|
91
|
+
// numbers only
|
|
92
|
+
/^[^a-zA-Z\u00C0-\u024F\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF]{10,}$/,
|
|
93
|
+
// no real letters
|
|
94
|
+
/\b(test|asdf|qwerty|lorem ipsum)\b/i
|
|
95
|
+
// common test strings
|
|
96
|
+
];
|
|
97
|
+
var GIBBERISH_PATTERN = /^[bcdfghjklmnpqrstvwxz]{6,}/i;
|
|
98
|
+
function createPreFilter(config = {}) {
|
|
99
|
+
const {
|
|
100
|
+
enabled = true,
|
|
101
|
+
blockObviousSpam = true,
|
|
102
|
+
minDescriptionLength = DEFAULT_MIN_LENGTH,
|
|
103
|
+
maxDescriptionLength = DEFAULT_MAX_LENGTH
|
|
104
|
+
} = config;
|
|
105
|
+
function check(description) {
|
|
106
|
+
if (!enabled) {
|
|
107
|
+
return { passed: true };
|
|
108
|
+
}
|
|
109
|
+
const trimmed = description.trim();
|
|
110
|
+
if (trimmed.length < minDescriptionLength) {
|
|
111
|
+
return { passed: false, reason: `Too short (min ${minDescriptionLength} characters)` };
|
|
112
|
+
}
|
|
113
|
+
if (trimmed.length > maxDescriptionLength) {
|
|
114
|
+
return { passed: false, reason: `Too long (max ${maxDescriptionLength} characters)` };
|
|
115
|
+
}
|
|
116
|
+
if (blockObviousSpam) {
|
|
117
|
+
for (const pattern of SPAM_PATTERNS) {
|
|
118
|
+
if (pattern.test(trimmed)) {
|
|
119
|
+
return { passed: false, reason: "Detected as spam" };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (GIBBERISH_PATTERN.test(trimmed)) {
|
|
123
|
+
return { passed: false, reason: "Detected as gibberish" };
|
|
124
|
+
}
|
|
125
|
+
const words = trimmed.split(/\s+/).filter((w) => w.length > 1);
|
|
126
|
+
if (words.length < 2) {
|
|
127
|
+
return { passed: false, reason: "Description needs at least 2 words" };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { passed: true };
|
|
131
|
+
}
|
|
132
|
+
function truncate(description) {
|
|
133
|
+
const trimmed = description.trim();
|
|
134
|
+
if (trimmed.length <= maxDescriptionLength) return trimmed;
|
|
135
|
+
return trimmed.slice(0, maxDescriptionLength) + "...";
|
|
136
|
+
}
|
|
137
|
+
return { check, truncate };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/logger.ts
|
|
141
|
+
var LEVEL_VALUE = {
|
|
142
|
+
debug: 10,
|
|
143
|
+
info: 20,
|
|
144
|
+
warn: 30,
|
|
145
|
+
error: 40,
|
|
146
|
+
fatal: 50,
|
|
147
|
+
silent: 99
|
|
148
|
+
};
|
|
149
|
+
var LEVEL_LABEL = {
|
|
150
|
+
debug: "DBG",
|
|
151
|
+
info: "INF",
|
|
152
|
+
warn: "WRN",
|
|
153
|
+
error: "ERR",
|
|
154
|
+
fatal: "FTL"
|
|
155
|
+
};
|
|
156
|
+
var ANSI = {
|
|
157
|
+
reset: "\x1B[0m",
|
|
158
|
+
dim: "\x1B[2m",
|
|
159
|
+
bold: "\x1B[1m",
|
|
160
|
+
cyan: "\x1B[36m",
|
|
161
|
+
green: "\x1B[32m",
|
|
162
|
+
yellow: "\x1B[33m",
|
|
163
|
+
red: "\x1B[31m",
|
|
164
|
+
white: "\x1B[37m",
|
|
165
|
+
bgRed: "\x1B[41m"
|
|
166
|
+
};
|
|
167
|
+
var LEVEL_COLOR = {
|
|
168
|
+
debug: ANSI.dim,
|
|
169
|
+
info: ANSI.green,
|
|
170
|
+
warn: ANSI.yellow,
|
|
171
|
+
error: ANSI.red,
|
|
172
|
+
fatal: `${ANSI.bgRed}${ANSI.white}${ANSI.bold}`
|
|
173
|
+
};
|
|
174
|
+
function detectFormat() {
|
|
175
|
+
try {
|
|
176
|
+
if (typeof globalThis.Deno !== "undefined") return "json";
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
const proc = typeof globalThis.process !== "undefined" ? globalThis.process : void 0;
|
|
180
|
+
if (proc?.env) {
|
|
181
|
+
if (proc.env.NODE_ENV === "production") return "json";
|
|
182
|
+
if (proc.env.LOG_FORMAT === "json") return "json";
|
|
183
|
+
if (proc.env.LOG_FORMAT === "pretty") return "pretty";
|
|
184
|
+
if (proc.stdout?.isTTY) return "pretty";
|
|
185
|
+
}
|
|
186
|
+
if (typeof globalThis.window !== "undefined") return "pretty";
|
|
187
|
+
return "json";
|
|
188
|
+
}
|
|
189
|
+
function flattenMeta(meta) {
|
|
190
|
+
const parts = [];
|
|
191
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
192
|
+
if (v === void 0 || v === null) continue;
|
|
193
|
+
if (typeof v === "object") {
|
|
194
|
+
parts.push(`${k}=${JSON.stringify(v)}`);
|
|
195
|
+
} else {
|
|
196
|
+
parts.push(`${k}=${String(v)}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return parts.join(" ");
|
|
200
|
+
}
|
|
201
|
+
function formatPretty(entry) {
|
|
202
|
+
const { ts, level, scope, msg, ...rest } = entry;
|
|
203
|
+
const time = ts.slice(11, 23);
|
|
204
|
+
const color = LEVEL_COLOR[level] ?? "";
|
|
205
|
+
const label = LEVEL_LABEL[level] ?? level.toUpperCase();
|
|
206
|
+
const metaStr = Object.keys(rest).length > 0 ? ` ${ANSI.dim}${flattenMeta(rest)}${ANSI.reset}` : "";
|
|
207
|
+
return `${ANSI.dim}${time}${ANSI.reset} ${color}${label}${ANSI.reset} ${ANSI.cyan}[${scope}]${ANSI.reset} ${msg}${metaStr}`;
|
|
208
|
+
}
|
|
209
|
+
function formatJson(entry) {
|
|
210
|
+
return JSON.stringify(entry);
|
|
211
|
+
}
|
|
212
|
+
function emit(level, formatted) {
|
|
213
|
+
switch (level) {
|
|
214
|
+
case "error":
|
|
215
|
+
case "fatal":
|
|
216
|
+
console.error(formatted);
|
|
217
|
+
break;
|
|
218
|
+
case "warn":
|
|
219
|
+
console.warn(formatted);
|
|
220
|
+
break;
|
|
221
|
+
default:
|
|
222
|
+
console.log(formatted);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function buildLogger(scope, minLevel, baseMeta, formatter) {
|
|
226
|
+
let currentLevel = minLevel;
|
|
227
|
+
function log(level, msg, meta) {
|
|
228
|
+
if (LEVEL_VALUE[level] < LEVEL_VALUE[currentLevel]) return;
|
|
229
|
+
const entry = {
|
|
230
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
231
|
+
level,
|
|
232
|
+
scope,
|
|
233
|
+
msg,
|
|
234
|
+
...baseMeta,
|
|
235
|
+
...meta
|
|
236
|
+
};
|
|
237
|
+
emit(level, formatter(entry));
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
debug: (msg, meta) => log("debug", msg, meta),
|
|
241
|
+
info: (msg, meta) => log("info", msg, meta),
|
|
242
|
+
warn: (msg, meta) => log("warn", msg, meta),
|
|
243
|
+
error: (msg, meta) => log("error", msg, meta),
|
|
244
|
+
fatal: (msg, meta) => log("fatal", msg, meta),
|
|
245
|
+
child(childScope, childMeta) {
|
|
246
|
+
return buildLogger(
|
|
247
|
+
`${scope}:${childScope}`,
|
|
248
|
+
currentLevel,
|
|
249
|
+
{ ...baseMeta, ...childMeta },
|
|
250
|
+
formatter
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
setLevel(level) {
|
|
254
|
+
currentLevel = level;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function createLogger(options) {
|
|
259
|
+
const {
|
|
260
|
+
scope,
|
|
261
|
+
level = "info",
|
|
262
|
+
meta = {},
|
|
263
|
+
format = "auto"
|
|
264
|
+
} = options;
|
|
265
|
+
const resolvedFormat = format === "auto" ? detectFormat() : format;
|
|
266
|
+
const formatter = resolvedFormat === "json" ? formatJson : formatPretty;
|
|
267
|
+
return buildLogger(scope, level, meta, formatter);
|
|
268
|
+
}
|
|
269
|
+
var noopLogger = {
|
|
270
|
+
debug: () => {
|
|
271
|
+
},
|
|
272
|
+
info: () => {
|
|
273
|
+
},
|
|
274
|
+
warn: () => {
|
|
275
|
+
},
|
|
276
|
+
error: () => {
|
|
277
|
+
},
|
|
278
|
+
fatal: () => {
|
|
279
|
+
},
|
|
280
|
+
child: () => noopLogger,
|
|
281
|
+
setLevel: () => {
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// src/queue.ts
|
|
286
|
+
var queueLog = createLogger({ scope: "mushi:queue", level: "warn" });
|
|
287
|
+
var DB_NAME = "mushi-mushi";
|
|
288
|
+
var STORE_NAME = "offline-reports";
|
|
289
|
+
var DB_VERSION = 1;
|
|
290
|
+
var LS_KEY = "mushi_offline_queue";
|
|
291
|
+
var BATCH_SIZE = 10;
|
|
292
|
+
var MAX_BACKOFF_MS = 6e4;
|
|
293
|
+
function createOfflineQueue(config = {}) {
|
|
294
|
+
const { enabled = true, maxQueueSize = 50, syncOnReconnect = true } = config;
|
|
295
|
+
let syncCleanup = null;
|
|
296
|
+
let backendType = null;
|
|
297
|
+
function detectBackend() {
|
|
298
|
+
if (backendType) return backendType;
|
|
299
|
+
if (typeof indexedDB !== "undefined") {
|
|
300
|
+
backendType = "indexeddb";
|
|
301
|
+
} else if (typeof localStorage !== "undefined") {
|
|
302
|
+
backendType = "localstorage";
|
|
303
|
+
} else {
|
|
304
|
+
backendType = "none";
|
|
305
|
+
}
|
|
306
|
+
return backendType;
|
|
307
|
+
}
|
|
308
|
+
function openDb() {
|
|
309
|
+
return new Promise((resolve, reject) => {
|
|
310
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
311
|
+
request.onupgradeneeded = () => {
|
|
312
|
+
const db = request.result;
|
|
313
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
314
|
+
db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
request.onsuccess = () => resolve(request.result);
|
|
318
|
+
request.onerror = () => {
|
|
319
|
+
backendType = "localstorage";
|
|
320
|
+
reject(request.error);
|
|
321
|
+
};
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
async function idbEnqueue(report) {
|
|
325
|
+
const db = await openDb();
|
|
326
|
+
return new Promise((resolve, reject) => {
|
|
327
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
328
|
+
tx.objectStore(STORE_NAME).put({ ...report, queuedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
329
|
+
tx.oncomplete = () => resolve();
|
|
330
|
+
tx.onerror = () => reject(tx.error);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
async function idbGetAll() {
|
|
334
|
+
const db = await openDb();
|
|
335
|
+
return new Promise((resolve, reject) => {
|
|
336
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
337
|
+
const request = tx.objectStore(STORE_NAME).getAll();
|
|
338
|
+
request.onsuccess = () => resolve(request.result);
|
|
339
|
+
request.onerror = () => reject(request.error);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
async function idbDelete(id) {
|
|
343
|
+
const db = await openDb();
|
|
344
|
+
return new Promise((resolve, reject) => {
|
|
345
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
346
|
+
tx.objectStore(STORE_NAME).delete(id);
|
|
347
|
+
tx.oncomplete = () => resolve();
|
|
348
|
+
tx.onerror = () => reject(tx.error);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
async function idbSize() {
|
|
352
|
+
const db = await openDb();
|
|
353
|
+
return new Promise((resolve, reject) => {
|
|
354
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
355
|
+
const request = tx.objectStore(STORE_NAME).count();
|
|
356
|
+
request.onsuccess = () => resolve(request.result);
|
|
357
|
+
request.onerror = () => reject(request.error);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
async function idbClear() {
|
|
361
|
+
const db = await openDb();
|
|
362
|
+
return new Promise((resolve, reject) => {
|
|
363
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
364
|
+
tx.objectStore(STORE_NAME).clear();
|
|
365
|
+
tx.oncomplete = () => resolve();
|
|
366
|
+
tx.onerror = () => reject(tx.error);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
function lsRead() {
|
|
370
|
+
try {
|
|
371
|
+
const raw = localStorage.getItem(LS_KEY);
|
|
372
|
+
return raw ? JSON.parse(raw) : [];
|
|
373
|
+
} catch {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function lsWrite(reports) {
|
|
378
|
+
try {
|
|
379
|
+
localStorage.setItem(LS_KEY, JSON.stringify(reports));
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function lsEnqueue(report) {
|
|
384
|
+
const reports = lsRead();
|
|
385
|
+
reports.push({ ...report, queuedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
386
|
+
lsWrite(reports);
|
|
387
|
+
}
|
|
388
|
+
function lsDelete(id) {
|
|
389
|
+
const reports = lsRead().filter((r) => r.id !== id);
|
|
390
|
+
lsWrite(reports);
|
|
391
|
+
}
|
|
392
|
+
async function enqueue(report) {
|
|
393
|
+
if (!enabled) return;
|
|
394
|
+
const currentSize = await size();
|
|
395
|
+
if (currentSize >= maxQueueSize) {
|
|
396
|
+
queueLog.warn("Offline queue full \u2014 dropping report", { maxQueueSize });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const backend = detectBackend();
|
|
400
|
+
if (backend === "indexeddb") {
|
|
401
|
+
try {
|
|
402
|
+
await idbEnqueue(report);
|
|
403
|
+
return;
|
|
404
|
+
} catch {
|
|
405
|
+
backendType = "localstorage";
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (backend === "localstorage" || backendType === "localstorage") {
|
|
409
|
+
lsEnqueue(report);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function getBackoffDelay2(attempt) {
|
|
414
|
+
return Math.min(1e3 * 2 ** attempt + Math.random() * 500, MAX_BACKOFF_MS);
|
|
415
|
+
}
|
|
416
|
+
function sleep2(ms) {
|
|
417
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
418
|
+
}
|
|
419
|
+
async function flush(client) {
|
|
420
|
+
if (!enabled) return { sent: 0, failed: 0 };
|
|
421
|
+
let reports;
|
|
422
|
+
const backend = detectBackend();
|
|
423
|
+
if (backend === "indexeddb") {
|
|
424
|
+
try {
|
|
425
|
+
reports = await idbGetAll();
|
|
426
|
+
} catch {
|
|
427
|
+
reports = lsRead();
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
reports = lsRead();
|
|
431
|
+
}
|
|
432
|
+
const batch = reports.slice(0, BATCH_SIZE);
|
|
433
|
+
let sent = 0;
|
|
434
|
+
let failed = 0;
|
|
435
|
+
for (let i = 0; i < batch.length; i++) {
|
|
436
|
+
const report = batch[i];
|
|
437
|
+
const result = await client.submitReport(report);
|
|
438
|
+
if (result.ok) {
|
|
439
|
+
try {
|
|
440
|
+
if (backend === "indexeddb") await idbDelete(report.id);
|
|
441
|
+
else lsDelete(report.id);
|
|
442
|
+
} catch {
|
|
443
|
+
lsDelete(report.id);
|
|
444
|
+
}
|
|
445
|
+
sent++;
|
|
446
|
+
} else {
|
|
447
|
+
failed++;
|
|
448
|
+
if (i < batch.length - 1) {
|
|
449
|
+
await sleep2(getBackoffDelay2(i));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return { sent, failed };
|
|
454
|
+
}
|
|
455
|
+
async function size() {
|
|
456
|
+
const backend = detectBackend();
|
|
457
|
+
if (backend === "indexeddb") {
|
|
458
|
+
try {
|
|
459
|
+
return await idbSize();
|
|
460
|
+
} catch {
|
|
461
|
+
return lsRead().length;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return lsRead().length;
|
|
465
|
+
}
|
|
466
|
+
async function clear() {
|
|
467
|
+
const backend = detectBackend();
|
|
468
|
+
if (backend === "indexeddb") {
|
|
469
|
+
try {
|
|
470
|
+
await idbClear();
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
localStorage.removeItem(LS_KEY);
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function startAutoSync(client) {
|
|
480
|
+
if (!enabled || !syncOnReconnect || typeof window === "undefined") return;
|
|
481
|
+
const handler = () => {
|
|
482
|
+
if (navigator.onLine) {
|
|
483
|
+
flush(client).catch(() => {
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
window.addEventListener("online", handler);
|
|
488
|
+
syncCleanup = () => window.removeEventListener("online", handler);
|
|
489
|
+
}
|
|
490
|
+
function stopAutoSync() {
|
|
491
|
+
syncCleanup?.();
|
|
492
|
+
syncCleanup = null;
|
|
493
|
+
}
|
|
494
|
+
return { enqueue, flush, size, clear, startAutoSync, stopAutoSync };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/environment.ts
|
|
498
|
+
function captureEnvironment() {
|
|
499
|
+
const nav = typeof navigator !== "undefined" ? navigator : void 0;
|
|
500
|
+
const win = typeof window !== "undefined" ? window : void 0;
|
|
501
|
+
const doc = typeof document !== "undefined" ? document : void 0;
|
|
502
|
+
const connection = nav && "connection" in nav ? nav.connection : void 0;
|
|
503
|
+
return {
|
|
504
|
+
userAgent: nav?.userAgent ?? "unknown",
|
|
505
|
+
platform: nav?.platform ?? "unknown",
|
|
506
|
+
language: nav?.language ?? "en",
|
|
507
|
+
viewport: {
|
|
508
|
+
width: win?.innerWidth ?? 0,
|
|
509
|
+
height: win?.innerHeight ?? 0
|
|
510
|
+
},
|
|
511
|
+
url: win?.location?.href ?? "",
|
|
512
|
+
referrer: doc?.referrer ?? "",
|
|
513
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
514
|
+
timezone: Intl?.DateTimeFormat?.()?.resolvedOptions?.()?.timeZone ?? "UTC",
|
|
515
|
+
connection: connection ? {
|
|
516
|
+
effectiveType: connection.effectiveType,
|
|
517
|
+
downlink: connection.downlink,
|
|
518
|
+
rtt: connection.rtt
|
|
519
|
+
} : void 0,
|
|
520
|
+
deviceMemory: nav?.deviceMemory,
|
|
521
|
+
hardwareConcurrency: nav?.hardwareConcurrency
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/reporter-token.ts
|
|
526
|
+
var STORAGE_KEY = "mushi_reporter_token";
|
|
527
|
+
function getReporterToken() {
|
|
528
|
+
if (typeof localStorage !== "undefined") {
|
|
529
|
+
const existing = localStorage.getItem(STORAGE_KEY);
|
|
530
|
+
if (existing) return existing;
|
|
531
|
+
}
|
|
532
|
+
const token = generateToken();
|
|
533
|
+
if (typeof localStorage !== "undefined") {
|
|
534
|
+
try {
|
|
535
|
+
localStorage.setItem(STORAGE_KEY, token);
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return token;
|
|
540
|
+
}
|
|
541
|
+
function generateToken() {
|
|
542
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
543
|
+
return `mushi_${crypto.randomUUID()}`;
|
|
544
|
+
}
|
|
545
|
+
const bytes = new Uint8Array(16);
|
|
546
|
+
if (typeof crypto !== "undefined") {
|
|
547
|
+
crypto.getRandomValues(bytes);
|
|
548
|
+
} else {
|
|
549
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
550
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
554
|
+
return `mushi_${hex}`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/session.ts
|
|
558
|
+
var SESSION_KEY = "mushi_session_id";
|
|
559
|
+
var cachedSessionId = null;
|
|
560
|
+
function getSessionId() {
|
|
561
|
+
if (cachedSessionId) return cachedSessionId;
|
|
562
|
+
if (typeof sessionStorage !== "undefined") {
|
|
563
|
+
const existing = sessionStorage.getItem(SESSION_KEY);
|
|
564
|
+
if (existing) {
|
|
565
|
+
cachedSessionId = existing;
|
|
566
|
+
return existing;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const id = generateSessionId();
|
|
570
|
+
cachedSessionId = id;
|
|
571
|
+
if (typeof sessionStorage !== "undefined") {
|
|
572
|
+
try {
|
|
573
|
+
sessionStorage.setItem(SESSION_KEY, id);
|
|
574
|
+
} catch {
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return id;
|
|
578
|
+
}
|
|
579
|
+
function generateSessionId() {
|
|
580
|
+
const timestamp = Date.now().toString(36);
|
|
581
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
582
|
+
return `ms_${timestamp}_${random}`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/rate-limiter.ts
|
|
586
|
+
var DEFAULT_MAX_BURST = 10;
|
|
587
|
+
var DEFAULT_REFILL_RATE = 1;
|
|
588
|
+
var DEFAULT_REFILL_INTERVAL_MS = 5e3;
|
|
589
|
+
function createRateLimiter(config = {}) {
|
|
590
|
+
const {
|
|
591
|
+
maxBurst = DEFAULT_MAX_BURST,
|
|
592
|
+
refillRate = DEFAULT_REFILL_RATE,
|
|
593
|
+
refillIntervalMs = DEFAULT_REFILL_INTERVAL_MS
|
|
594
|
+
} = config;
|
|
595
|
+
let tokens = maxBurst;
|
|
596
|
+
let lastRefill = Date.now();
|
|
597
|
+
function refill() {
|
|
598
|
+
const now = Date.now();
|
|
599
|
+
const elapsed = now - lastRefill;
|
|
600
|
+
const refills = Math.floor(elapsed / refillIntervalMs);
|
|
601
|
+
if (refills > 0) {
|
|
602
|
+
tokens = Math.min(maxBurst, tokens + refills * refillRate);
|
|
603
|
+
lastRefill = now;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function tryConsume() {
|
|
607
|
+
refill();
|
|
608
|
+
if (tokens > 0) {
|
|
609
|
+
tokens--;
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
function reset() {
|
|
615
|
+
tokens = maxBurst;
|
|
616
|
+
lastRefill = Date.now();
|
|
617
|
+
}
|
|
618
|
+
function availableTokens() {
|
|
619
|
+
refill();
|
|
620
|
+
return tokens;
|
|
621
|
+
}
|
|
622
|
+
return { tryConsume, reset, availableTokens };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/pii-scrubber.ts
|
|
626
|
+
var ORDERED_PATTERNS = [
|
|
627
|
+
{ key: "ssns", regex: /\b\d{3}-\d{2}-\d{4}\b/g, replacement: "[REDACTED_SSN]" },
|
|
628
|
+
{ key: "creditCards", regex: /\b(?:\d[ -]*){12,18}\d\b/g, replacement: "[REDACTED_CC]" },
|
|
629
|
+
{ key: "emails", regex: /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g, replacement: "[REDACTED_EMAIL]" },
|
|
630
|
+
{ key: "phones", regex: /(?:\+\d{1,3}[\s.-])?\(?\d{2,4}\)?[\s.-]\d{3,4}[\s.-]\d{3,4}\b/g, replacement: "[REDACTED_PHONE]" },
|
|
631
|
+
{ key: "ipAddresses", regex: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[REDACTED_IP]" }
|
|
632
|
+
];
|
|
633
|
+
var DEFAULT_CONFIG = {
|
|
634
|
+
emails: true,
|
|
635
|
+
phones: true,
|
|
636
|
+
creditCards: true,
|
|
637
|
+
ssns: true,
|
|
638
|
+
ipAddresses: false
|
|
639
|
+
};
|
|
640
|
+
function createPiiScrubber(config = {}) {
|
|
641
|
+
const merged = { ...DEFAULT_CONFIG, ...config };
|
|
642
|
+
const activePatterns = ORDERED_PATTERNS.filter((p) => merged[p.key]);
|
|
643
|
+
function scrub(text) {
|
|
644
|
+
if (!text) return text;
|
|
645
|
+
let result = text;
|
|
646
|
+
for (const { regex, replacement } of activePatterns) {
|
|
647
|
+
result = result.replace(new RegExp(regex.source, regex.flags), replacement);
|
|
648
|
+
}
|
|
649
|
+
return result;
|
|
650
|
+
}
|
|
651
|
+
function scrubObject(obj, keys) {
|
|
652
|
+
const copy = { ...obj };
|
|
653
|
+
for (const key of keys) {
|
|
654
|
+
if (typeof copy[key] === "string") {
|
|
655
|
+
copy[key] = scrub(copy[key]);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return copy;
|
|
659
|
+
}
|
|
660
|
+
return { scrub, scrubObject };
|
|
661
|
+
}
|
|
662
|
+
function scrubPii(text, config) {
|
|
663
|
+
return createPiiScrubber(config).scrub(text);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export { captureEnvironment, createApiClient, createLogger, createOfflineQueue, createPiiScrubber, createPreFilter, createRateLimiter, getReporterToken, getSessionId, noopLogger, scrubPii };
|
|
667
|
+
//# sourceMappingURL=index.js.map
|
|
668
|
+
//# sourceMappingURL=index.js.map
|