@zi2/relay-sdk 1.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/dist/index.js ADDED
@@ -0,0 +1,2537 @@
1
+ // src/constants.ts
2
+ var RELAY_MESSAGE_TYPES = {
3
+ AUTH: "auth",
4
+ SEND_SMS: "send_sms",
5
+ SMS_ACK: "sms_ack",
6
+ DELIVERY_RECEIPT: "delivery_receipt",
7
+ PONG: "pong",
8
+ STATUS: "status",
9
+ REKEY: "rekey",
10
+ REKEY_ACK: "rekey_ack"
11
+ };
12
+ var RELAY_STATUS = {
13
+ ACTIVE: "active",
14
+ INACTIVE: "inactive",
15
+ REVOKED: "revoked",
16
+ DEGRADED: "degraded"
17
+ };
18
+ var RELAY_MESSAGE_STATUS = {
19
+ PENDING: "pending",
20
+ SENT_TO_DEVICE: "sent_to_device",
21
+ DELIVERED: "delivered",
22
+ FAILED: "failed",
23
+ EXPIRED: "expired"
24
+ };
25
+ var RELAY_DELIVERY_STATUS = {
26
+ SENT: "sent",
27
+ DELIVERED: "delivered",
28
+ FAILED: "failed"
29
+ };
30
+ var RELAY_PAIRING_STATUS = {
31
+ PENDING: "pending",
32
+ COMPLETED: "completed",
33
+ EXPIRED: "expired"
34
+ };
35
+ var RELAY_LIMITS = {
36
+ AUTH_TIMEOUT_MS: 5e3,
37
+ HEARTBEAT_INTERVAL_MS: 3e4,
38
+ PAIRING_EXPIRY_MINUTES: 5,
39
+ MESSAGE_EXPIRY_HOURS: 24,
40
+ QUEUE_DRAIN_DELAY_MS: 3e3,
41
+ DEFAULT_SMS_TIMEOUT_MS: 3e4,
42
+ MAX_SMS_PER_MINUTE: 20,
43
+ MAX_SMS_PER_HOUR: 200,
44
+ MAX_SMS_PER_DAY: 1e3,
45
+ DEGRADED_THRESHOLD_MS: 5 * 60 * 1e3
46
+ };
47
+ var RELAY_PLATFORMS = {
48
+ ANDROID: "android",
49
+ IOS: "ios"
50
+ };
51
+
52
+ // src/core/queue-service.ts
53
+ import { EventEmitter } from "events";
54
+
55
+ // src/core/crypto/e2e.ts
56
+ import crypto2 from "crypto";
57
+ var E2E_HKDF_INFO = "zi2-relay-e2e-v1";
58
+ function zeroBuffer(buf) {
59
+ buf.fill(0);
60
+ }
61
+ function generateX25519KeyPair() {
62
+ const { publicKey, privateKey } = crypto2.generateKeyPairSync("x25519");
63
+ return {
64
+ publicKey: publicKey.export({ type: "spki", format: "der" }).toString("hex"),
65
+ privateKey: privateKey.export({ type: "pkcs8", format: "der" }).toString("hex")
66
+ };
67
+ }
68
+ function deriveSharedKey(privateKeyHex, publicKeyHex) {
69
+ const privateKey = crypto2.createPrivateKey({
70
+ key: Buffer.from(privateKeyHex, "hex"),
71
+ format: "der",
72
+ type: "pkcs8"
73
+ });
74
+ const publicKey = crypto2.createPublicKey({
75
+ key: Buffer.from(publicKeyHex, "hex"),
76
+ format: "der",
77
+ type: "spki"
78
+ });
79
+ const sharedSecret = crypto2.diffieHellman({ privateKey, publicKey });
80
+ const derived = Buffer.from(
81
+ crypto2.hkdfSync("sha256", sharedSecret, Buffer.alloc(0), E2E_HKDF_INFO, 32)
82
+ );
83
+ zeroBuffer(sharedSecret);
84
+ return derived;
85
+ }
86
+ function encryptE2E(plaintext, sharedKey) {
87
+ const iv = crypto2.randomBytes(12);
88
+ const cipher = crypto2.createCipheriv("aes-256-gcm", sharedKey, iv);
89
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
90
+ const authTag = cipher.getAuthTag();
91
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
92
+ }
93
+ function decryptE2E(ciphertext, sharedKey) {
94
+ const [ivHex, authTagHex, encryptedHex] = ciphertext.split(":");
95
+ if (!ivHex || !authTagHex || !encryptedHex) throw new Error("Invalid E2E ciphertext format");
96
+ const iv = Buffer.from(ivHex, "hex");
97
+ const authTag = Buffer.from(authTagHex, "hex");
98
+ const encrypted = Buffer.from(encryptedHex, "hex");
99
+ const decipher = crypto2.createDecipheriv("aes-256-gcm", sharedKey, iv);
100
+ decipher.setAuthTag(authTag);
101
+ return decipher.update(encrypted).toString("utf8") + decipher.final("utf8");
102
+ }
103
+
104
+ // src/core/connections.ts
105
+ var relayConnections = /* @__PURE__ */ new Map();
106
+ function getRelaySocket(relayId) {
107
+ return relayConnections.get(relayId);
108
+ }
109
+ function setRelaySocket(relayId, ws) {
110
+ relayConnections.set(relayId, ws);
111
+ }
112
+ function deleteRelaySocket(relayId) {
113
+ relayConnections.delete(relayId);
114
+ }
115
+ function isRelayOnline(relayId) {
116
+ const ws = relayConnections.get(relayId);
117
+ return !!ws && ws.readyState === 1;
118
+ }
119
+
120
+ // src/core/queue-service.ts
121
+ function createQueueService(deps) {
122
+ const { db, encryption, logger, events, limits } = deps;
123
+ const ackEmitter = new EventEmitter();
124
+ ackEmitter.setMaxListeners(1e4);
125
+ async function enqueueAndWait(options) {
126
+ const { relayId, orgId, to, body, timeoutMs = limits.DEFAULT_SMS_TIMEOUT_MS } = options;
127
+ const relay = await db.findRelay(relayId, orgId);
128
+ if (!relay || relay.status !== RELAY_STATUS.ACTIVE && relay.status !== RELAY_STATUS.DEGRADED) {
129
+ throw new Error("Relay not found or inactive");
130
+ }
131
+ let sharedKeyHex;
132
+ try {
133
+ sharedKeyHex = encryption.decrypt(relay.sharedKeyEncrypted);
134
+ } catch (err) {
135
+ throw new Error(`Failed to decrypt relay shared key: ${err}`);
136
+ }
137
+ const sharedKey = Buffer.from(sharedKeyHex, "hex");
138
+ const payload = JSON.stringify({ to, body });
139
+ const encryptedPayload = encryptE2E(payload, sharedKey);
140
+ const message = await db.createMessage({
141
+ relayId,
142
+ organizationId: orgId,
143
+ encryptedPayload,
144
+ expiresAt: new Date(Date.now() + limits.MESSAGE_EXPIRY_HOURS * 60 * 60 * 1e3)
145
+ });
146
+ if (!isRelayOnline(relayId)) {
147
+ return { messageId: message.id, status: "queued" };
148
+ }
149
+ const ws = getRelaySocket(relayId);
150
+ if (!ws || ws.readyState !== 1) {
151
+ return { messageId: message.id, status: "queued" };
152
+ }
153
+ ws.send(JSON.stringify({
154
+ type: "send_sms",
155
+ messageId: message.id,
156
+ encryptedPayload
157
+ }));
158
+ await db.updateMessage(message.id, {
159
+ status: RELAY_MESSAGE_STATUS.SENT_TO_DEVICE,
160
+ sentToDeviceAt: /* @__PURE__ */ new Date()
161
+ });
162
+ return new Promise((resolve) => {
163
+ const timer = setTimeout(() => {
164
+ ackEmitter.removeAllListeners(`ack:${message.id}`);
165
+ resolve({ messageId: message.id, status: "sent_to_device" });
166
+ }, timeoutMs);
167
+ ackEmitter.once(`ack:${message.id}`, (ackStatus) => {
168
+ clearTimeout(timer);
169
+ resolve({ messageId: message.id, status: ackStatus });
170
+ });
171
+ });
172
+ }
173
+ async function handleAck(messageId, status, nativeMessageId, errorMessage) {
174
+ const updateData = {
175
+ status: status === "sent" ? RELAY_MESSAGE_STATUS.DELIVERED : RELAY_MESSAGE_STATUS.FAILED,
176
+ ackedAt: /* @__PURE__ */ new Date()
177
+ };
178
+ if (nativeMessageId) updateData.nativeMessageId = nativeMessageId;
179
+ if (errorMessage) updateData.errorMessage = errorMessage;
180
+ await db.updateMessage(messageId, updateData);
181
+ const message = await db.findMessage(messageId).catch(() => null);
182
+ try {
183
+ if (message) {
184
+ if (status === "sent") {
185
+ await db.incrementRelayStat(message.relayId, "totalSmsSent");
186
+ await db.incrementRelayStat(message.relayId, "dailySmsSent");
187
+ } else {
188
+ await db.incrementRelayStat(message.relayId, "totalSmsFailed");
189
+ }
190
+ }
191
+ } catch (err) {
192
+ logger.error("Failed to update relay stats after ack", { messageId, error: String(err) });
193
+ }
194
+ ackEmitter.emit(`ack:${messageId}`, status);
195
+ if (message) {
196
+ if (status === "sent") {
197
+ events.emit("message:delivered", messageId, message.relayId);
198
+ } else {
199
+ events.emit("message:failed", messageId, message.relayId, errorMessage || "Unknown error");
200
+ }
201
+ }
202
+ }
203
+ async function handleDeliveryReceipt(messageId, deliveryStatus) {
204
+ await db.updateMessage(messageId, { deliveryStatus });
205
+ }
206
+ async function drainQueue(relayId) {
207
+ const pendingMessages = await db.findPendingMessages(relayId, 50);
208
+ const ws = getRelaySocket(relayId);
209
+ if (!ws || ws.readyState !== 1) return;
210
+ for (const msg of pendingMessages) {
211
+ ws.send(JSON.stringify({
212
+ type: "send_sms",
213
+ messageId: msg.id,
214
+ encryptedPayload: msg.encryptedPayload
215
+ }));
216
+ await db.updateMessage(msg.id, {
217
+ status: RELAY_MESSAGE_STATUS.SENT_TO_DEVICE,
218
+ sentToDeviceAt: /* @__PURE__ */ new Date(),
219
+ attempts: msg.attempts + 1
220
+ });
221
+ await new Promise((r) => setTimeout(r, limits.QUEUE_DRAIN_DELAY_MS));
222
+ if (ws.readyState !== 1) break;
223
+ }
224
+ }
225
+ return { enqueueAndWait, handleAck, handleDeliveryReceipt, drainQueue };
226
+ }
227
+
228
+ // src/core/crypto/token.ts
229
+ import crypto3 from "crypto";
230
+ function hashToken(token) {
231
+ return crypto3.createHash("sha256").update(token).digest("hex");
232
+ }
233
+ function generateSecureToken(bytes = 64) {
234
+ return crypto3.randomBytes(bytes).toString("hex");
235
+ }
236
+ function timingSafeCompare(a, b) {
237
+ try {
238
+ return crypto3.timingSafeEqual(Buffer.from(a), Buffer.from(b));
239
+ } catch {
240
+ return false;
241
+ }
242
+ }
243
+
244
+ // src/errors.ts
245
+ var RelayError = class extends Error {
246
+ constructor(code, statusCode, message, i18nKey) {
247
+ super(message);
248
+ this.code = code;
249
+ this.statusCode = statusCode;
250
+ this.i18nKey = i18nKey;
251
+ this.name = "RelayError";
252
+ }
253
+ /** Safe serialization that never leaks stack traces to clients */
254
+ toJSON() {
255
+ return {
256
+ error: {
257
+ code: this.code,
258
+ message: this.message,
259
+ i18nKey: this.i18nKey
260
+ }
261
+ };
262
+ }
263
+ };
264
+ var RELAY_ERRORS = {
265
+ RELAY_NOT_FOUND: { code: "RELAY_NOT_FOUND", status: 404, message: "Relay device not found", i18nKey: "errors.relayNotFound" },
266
+ RELAY_INACTIVE: { code: "RELAY_INACTIVE", status: 409, message: "Relay is not active", i18nKey: "errors.relayInactive" },
267
+ RELAY_OFFLINE: { code: "RELAY_OFFLINE", status: 503, message: "Relay device is offline", i18nKey: "errors.relayOffline" },
268
+ PAIRING_NOT_FOUND: { code: "PAIRING_NOT_FOUND", status: 404, message: "Pairing session not found", i18nKey: "errors.pairingNotFound" },
269
+ PAIRING_EXPIRED: { code: "PAIRING_EXPIRED", status: 410, message: "Pairing session has expired", i18nKey: "errors.pairingExpired" },
270
+ PAIRING_INVALID_TOKEN: { code: "PAIRING_INVALID_TOKEN", status: 403, message: "Invalid pairing token", i18nKey: "errors.pairingInvalidToken" },
271
+ AUTH_TIMEOUT: { code: "AUTH_TIMEOUT", status: 408, message: "WebSocket authentication timeout", i18nKey: "errors.authTimeout" },
272
+ AUTH_FAILED: { code: "AUTH_FAILED", status: 401, message: "Invalid relay credentials", i18nKey: "errors.authFailed" },
273
+ RATE_LIMITED: { code: "RATE_LIMITED", status: 429, message: "Rate limit exceeded", i18nKey: "errors.rateLimited" },
274
+ ENCRYPTION_FAILED: { code: "ENCRYPTION_FAILED", status: 500, message: "Encryption operation failed", i18nKey: "errors.encryptionFailed" },
275
+ REKEY_FAILED: { code: "REKEY_FAILED", status: 500, message: "Key rotation failed", i18nKey: "errors.rekeyFailed" },
276
+ SMS_SEND_FAILED: { code: "SMS_SEND_FAILED", status: 502, message: "SMS delivery failed", i18nKey: "errors.smsSendFailed" },
277
+ SMS_TIMEOUT: { code: "SMS_TIMEOUT", status: 504, message: "SMS acknowledgement timeout", i18nKey: "errors.smsTimeout" }
278
+ };
279
+ var RELAY_ERRORS_EXTENDED = {
280
+ ...RELAY_ERRORS,
281
+ AUTH_LOCKED: { code: "AUTH_LOCKED", status: 423, message: "Too many failed attempts. Try again later.", i18nKey: "errors.authLocked" },
282
+ TLS_REQUIRED: { code: "TLS_REQUIRED", status: 426, message: "TLS/SSL connection required", i18nKey: "errors.tlsRequired" },
283
+ ORG_MISMATCH: { code: "ORG_MISMATCH", status: 403, message: "Organization access denied", i18nKey: "errors.orgMismatch" }
284
+ };
285
+ function createRelayError(key) {
286
+ const def = RELAY_ERRORS[key];
287
+ return new RelayError(def.code, def.status, def.message, def.i18nKey);
288
+ }
289
+
290
+ // src/core/pairing-service.ts
291
+ function createPairingService(deps) {
292
+ const { db, encryption, logger, limits } = deps;
293
+ async function initiatePairing(orgId, userId) {
294
+ await db.deleteExpiredPairings();
295
+ const { publicKey, privateKey } = generateX25519KeyPair();
296
+ const pairingToken = generateSecureToken(64);
297
+ const pairing = await db.createPairing({
298
+ organizationId: orgId,
299
+ pairingToken,
300
+ serverPublicKey: publicKey,
301
+ serverPrivateKeyEncrypted: encryption.encrypt(privateKey),
302
+ status: RELAY_PAIRING_STATUS.PENDING,
303
+ initiatedById: userId,
304
+ expiresAt: new Date(Date.now() + limits.PAIRING_EXPIRY_MINUTES * 60 * 1e3)
305
+ });
306
+ const wsUrl = deps.wsUrl || deps.apiUrl.replace(/^https:/, "wss:").replace(/^http:/, "ws:") + "/ws/relay";
307
+ return {
308
+ pairingId: pairing.id,
309
+ serverPublicKey: publicKey,
310
+ pairingToken,
311
+ wsUrl
312
+ };
313
+ }
314
+ async function completePairing(pairingId, pairingToken, devicePublicKey, deviceName, platform, phoneNumber) {
315
+ const pairing = await db.findPairing(pairingId);
316
+ if (!pairing) throw createRelayError("PAIRING_NOT_FOUND");
317
+ if (pairing.status !== RELAY_PAIRING_STATUS.PENDING) throw createRelayError("PAIRING_EXPIRED");
318
+ if (!timingSafeCompare(pairing.pairingToken, pairingToken)) {
319
+ throw createRelayError("PAIRING_INVALID_TOKEN");
320
+ }
321
+ if (/* @__PURE__ */ new Date() > pairing.expiresAt) {
322
+ await db.updatePairingStatus(pairingId, RELAY_PAIRING_STATUS.EXPIRED);
323
+ throw createRelayError("PAIRING_EXPIRED");
324
+ }
325
+ const serverPrivateKey = encryption.decrypt(pairing.serverPrivateKeyEncrypted);
326
+ const sharedKey = deriveSharedKey(serverPrivateKey, devicePublicKey);
327
+ const authToken = generateSecureToken(64);
328
+ const authTokenHashed = hashToken(authToken);
329
+ const relay = await db.createRelay({
330
+ organizationId: pairing.organizationId,
331
+ deviceName,
332
+ platform,
333
+ phoneNumber: phoneNumber || null,
334
+ devicePublicKey,
335
+ sharedKeyEncrypted: encryption.encrypt(sharedKey.toString("hex")),
336
+ authTokenHash: authTokenHashed,
337
+ status: "active",
338
+ lastKeyRotation: null,
339
+ lastSeenAt: null,
340
+ lastIpAddress: null,
341
+ batteryLevel: null,
342
+ signalStrength: null,
343
+ dailyResetAt: null,
344
+ maxSmsPerMinute: limits.MAX_SMS_PER_MINUTE,
345
+ maxSmsPerHour: limits.MAX_SMS_PER_HOUR,
346
+ maxSmsPerDay: limits.MAX_SMS_PER_DAY,
347
+ pairedById: pairing.initiatedById
348
+ });
349
+ await db.updatePairingStatus(pairingId, RELAY_PAIRING_STATUS.COMPLETED);
350
+ try {
351
+ await deps.onPairingComplete?.({ relayId: relay.id, orgId: pairing.organizationId, deviceName, phoneNumber });
352
+ } catch (err) {
353
+ deps.logger.error("onPairingComplete callback failed", { relayId: relay.id, error: String(err) });
354
+ }
355
+ return { relayId: relay.id, authToken };
356
+ }
357
+ async function revokeRelay(relayId, orgId) {
358
+ const relay = await db.findRelay(relayId, orgId);
359
+ if (!relay) throw createRelayError("RELAY_NOT_FOUND");
360
+ const ws = getRelaySocket(relayId);
361
+ if (ws) ws.close(1e3, "Relay revoked");
362
+ await db.deleteMessagesByRelay(relayId);
363
+ await db.deleteRelay(relayId);
364
+ try {
365
+ await deps.onRelayRevoked?.({ relayId, orgId });
366
+ } catch (err) {
367
+ deps.logger.error("onRelayRevoked callback failed", { relayId, error: String(err) });
368
+ }
369
+ }
370
+ async function rotateKeys(relayId, orgId) {
371
+ const relay = await db.findRelay(relayId, orgId);
372
+ if (!relay) throw createRelayError("RELAY_NOT_FOUND");
373
+ const ws = getRelaySocket(relayId);
374
+ if (!ws || ws.readyState !== 1) {
375
+ throw createRelayError("RELAY_OFFLINE");
376
+ }
377
+ const { publicKey, privateKey } = generateX25519KeyPair();
378
+ ws.send(JSON.stringify({ type: "rekey", serverPublicKey: publicKey }));
379
+ await db.updateRelay(relayId, {
380
+ sharedKeyEncrypted: encryption.encrypt(JSON.stringify({
381
+ pending: true,
382
+ serverPrivateKey: privateKey,
383
+ serverPublicKey: publicKey,
384
+ previousSharedKey: relay.sharedKeyEncrypted
385
+ }))
386
+ });
387
+ }
388
+ return { initiatePairing, completePairing, revokeRelay, rotateKeys };
389
+ }
390
+
391
+ // src/core/health-service.ts
392
+ function createHealthService(deps) {
393
+ const { db, logger, limits } = deps;
394
+ let healthInterval = null;
395
+ let dailyResetTimeout = null;
396
+ let dailyResetInterval = null;
397
+ let isRunning = false;
398
+ let isStopped = false;
399
+ async function runHealthCheck() {
400
+ if (isRunning || isStopped) {
401
+ return { expiredMessages: 0, degradedRelays: 0, cleanedPairings: 0 };
402
+ }
403
+ isRunning = true;
404
+ try {
405
+ const [expiredMessages, cleanedPairings] = await Promise.all([
406
+ db.expireStaleMessages(),
407
+ db.deleteExpiredPairings()
408
+ ]);
409
+ const threshold = new Date(Date.now() - limits.DEGRADED_THRESHOLD_MS);
410
+ const degradedRelays = await db.markDegradedRelays(threshold);
411
+ return { expiredMessages, degradedRelays, cleanedPairings };
412
+ } finally {
413
+ isRunning = false;
414
+ }
415
+ }
416
+ function startHealthService() {
417
+ if (healthInterval) return;
418
+ healthInterval = setInterval(async () => {
419
+ try {
420
+ await runHealthCheck();
421
+ } catch (err) {
422
+ logger.error("Relay health service error", { error: String(err) });
423
+ }
424
+ }, 6e4);
425
+ const now = /* @__PURE__ */ new Date();
426
+ const tomorrow = new Date(now);
427
+ tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);
428
+ tomorrow.setUTCHours(0, 0, 0, 0);
429
+ const msUntilMidnight = tomorrow.getTime() - now.getTime();
430
+ dailyResetTimeout = setTimeout(() => {
431
+ db.resetDailyCounters().catch((err) => logger.error("Daily reset failed", { error: String(err) }));
432
+ dailyResetInterval = setInterval(() => {
433
+ db.resetDailyCounters().catch((err) => logger.error("Daily reset failed", { error: String(err) }));
434
+ }, 24 * 60 * 60 * 1e3);
435
+ }, msUntilMidnight);
436
+ }
437
+ function stopHealthService() {
438
+ isStopped = true;
439
+ if (healthInterval) {
440
+ clearInterval(healthInterval);
441
+ healthInterval = null;
442
+ }
443
+ if (dailyResetTimeout) {
444
+ clearTimeout(dailyResetTimeout);
445
+ dailyResetTimeout = null;
446
+ }
447
+ if (dailyResetInterval) {
448
+ clearInterval(dailyResetInterval);
449
+ dailyResetInterval = null;
450
+ }
451
+ }
452
+ return { runHealthCheck, startHealthService, stopHealthService };
453
+ }
454
+
455
+ // src/core/event-emitter.ts
456
+ import { EventEmitter as EventEmitter2 } from "events";
457
+ var RelayEventEmitter = class {
458
+ emitter = new EventEmitter2();
459
+ constructor() {
460
+ this.emitter.setMaxListeners(1e3);
461
+ }
462
+ on(event, listener) {
463
+ this.emitter.on(event, listener);
464
+ return () => {
465
+ this.emitter.removeListener(event, listener);
466
+ };
467
+ }
468
+ once(event, listener) {
469
+ this.emitter.once(event, listener);
470
+ }
471
+ emit(event, ...args) {
472
+ this.emitter.emit(event, ...args);
473
+ }
474
+ removeAllListeners(event) {
475
+ if (event) {
476
+ this.emitter.removeAllListeners(event);
477
+ } else {
478
+ this.emitter.removeAllListeners();
479
+ }
480
+ }
481
+ };
482
+
483
+ // src/adapters/broadcast.ts
484
+ var NoopBroadcast = class {
485
+ broadcast(_orgId, _message) {
486
+ }
487
+ };
488
+
489
+ // src/adapters/logger.ts
490
+ var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
491
+ "token",
492
+ "authToken",
493
+ "pairingToken",
494
+ "authTokenHash",
495
+ "sharedKey",
496
+ "sharedKeyEncrypted",
497
+ "privateKey",
498
+ "serverPrivateKey",
499
+ "serverPrivateKeyEncrypted",
500
+ "devicePublicKey",
501
+ "serverPublicKey",
502
+ "encryptedPayload",
503
+ "password",
504
+ "secret",
505
+ "credentials",
506
+ "authorization",
507
+ "cookie",
508
+ "body"
509
+ // SMS body is PII
510
+ ]);
511
+ function redact(data) {
512
+ const safe = {};
513
+ for (const [key, value] of Object.entries(data)) {
514
+ if (SENSITIVE_KEYS.has(key) || SENSITIVE_KEYS.has(key.toLowerCase())) {
515
+ safe[key] = "[REDACTED]";
516
+ } else if (value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)) {
517
+ safe[key] = redact(value);
518
+ } else {
519
+ safe[key] = value;
520
+ }
521
+ }
522
+ return safe;
523
+ }
524
+ var ConsoleLogger = class {
525
+ debug(msg, data) {
526
+ if (data) console.debug(`[relay-sdk] ${msg}`, redact(data));
527
+ else console.debug(`[relay-sdk] ${msg}`);
528
+ }
529
+ info(msg, data) {
530
+ if (data) console.info(`[relay-sdk] ${msg}`, redact(data));
531
+ else console.info(`[relay-sdk] ${msg}`);
532
+ }
533
+ warn(msg, data) {
534
+ if (data) console.warn(`[relay-sdk] ${msg}`, redact(data));
535
+ else console.warn(`[relay-sdk] ${msg}`);
536
+ }
537
+ error(msg, data) {
538
+ if (data) console.error(`[relay-sdk] ${msg}`, redact(data));
539
+ else console.error(`[relay-sdk] ${msg}`);
540
+ }
541
+ };
542
+ function withRedaction(logger) {
543
+ return {
544
+ debug: (msg, data) => logger.debug(msg, data ? redact(data) : void 0),
545
+ info: (msg, data) => logger.info(msg, data ? redact(data) : void 0),
546
+ warn: (msg, data) => logger.warn(msg, data ? redact(data) : void 0),
547
+ error: (msg, data) => logger.error(msg, data ? redact(data) : void 0)
548
+ };
549
+ }
550
+
551
+ // src/adapters/audit.ts
552
+ var NoopAudit = class {
553
+ async log(_entry) {
554
+ }
555
+ };
556
+ var ConsoleAudit = class {
557
+ async log(entry) {
558
+ console.info(`[relay-sdk:audit] ${entry.action}`, {
559
+ timestamp: entry.timestamp.toISOString(),
560
+ organizationId: entry.organizationId,
561
+ relayId: entry.relayId,
562
+ userId: entry.userId,
563
+ ip: entry.ip,
564
+ // Never log sensitive metadata values
565
+ metadata: entry.metadata ? Object.keys(entry.metadata) : void 0
566
+ });
567
+ }
568
+ };
569
+
570
+ // src/providers/phone-relay.ts
571
+ var PhoneRelayProvider = class {
572
+ name = "phone-relay";
573
+ relayId = "";
574
+ orgId = "";
575
+ queue;
576
+ constructor(queue) {
577
+ this.queue = queue;
578
+ }
579
+ initialize(credentials) {
580
+ if (!credentials.relayId) throw new Error("Phone Relay relayId is required");
581
+ if (!credentials.orgId) throw new Error("Phone Relay orgId is required");
582
+ this.relayId = credentials.relayId;
583
+ this.orgId = credentials.orgId;
584
+ }
585
+ async sendSms(to, body) {
586
+ const result = await this.queue.enqueueAndWait({
587
+ relayId: this.relayId,
588
+ orgId: this.orgId,
589
+ to,
590
+ body
591
+ });
592
+ return { messageId: result.messageId, success: result.status !== "failed" };
593
+ }
594
+ async validateCredentials(credentials) {
595
+ return isRelayOnline(credentials.relayId || this.relayId);
596
+ }
597
+ };
598
+
599
+ // src/providers/fallback.ts
600
+ var FallbackProvider = class {
601
+ name = "relay-fallback";
602
+ config;
603
+ constructor(config) {
604
+ this.config = config;
605
+ }
606
+ initialize(_credentials) {
607
+ }
608
+ async sendSms(to, body) {
609
+ if (this.config.isOnline()) {
610
+ try {
611
+ const result = await this.config.primary.sendSms(to, body);
612
+ if (result.success) return result;
613
+ } catch {
614
+ }
615
+ }
616
+ for (const fb of this.config.fallbacks) {
617
+ try {
618
+ const result = await fb.sendSms(to, body);
619
+ if (result.success) {
620
+ this.config.onFallback?.(fb.name);
621
+ return result;
622
+ }
623
+ } catch {
624
+ continue;
625
+ }
626
+ }
627
+ return { success: false };
628
+ }
629
+ async validateCredentials(_credentials) {
630
+ return this.config.isOnline() || this.config.fallbacks.length > 0;
631
+ }
632
+ };
633
+
634
+ // src/server/websocket-handler.ts
635
+ var RATE_LIMIT_WINDOW_MS = 1e4;
636
+ var RATE_LIMIT_MAX_MESSAGES = 100;
637
+ function createWebSocketHandler(deps) {
638
+ const { db, encryption, broadcast, logger, events, queue, limits } = deps;
639
+ return function handleWebSocket(socket, request) {
640
+ const log = request.log || logger;
641
+ let authenticated = false;
642
+ let relayId = null;
643
+ let relayOrgId = null;
644
+ let heartbeatInterval = null;
645
+ let messageCount = 0;
646
+ let rateLimitWindowStart = Date.now();
647
+ const ip = request.ip;
648
+ log.debug("Relay WebSocket connection opened", { ip });
649
+ const authTimeout = setTimeout(() => {
650
+ if (!authenticated) {
651
+ log.debug("Relay WebSocket auth timeout", { ip });
652
+ socket.close(4001, "Authentication timeout");
653
+ }
654
+ }, limits.AUTH_TIMEOUT_MS);
655
+ socket.on("message", async (raw) => {
656
+ const now = Date.now();
657
+ if (now - rateLimitWindowStart > RATE_LIMIT_WINDOW_MS) {
658
+ messageCount = 0;
659
+ rateLimitWindowStart = now;
660
+ }
661
+ messageCount++;
662
+ if (messageCount > RATE_LIMIT_MAX_MESSAGES) {
663
+ log.warn("Relay WebSocket rate limit exceeded", { relayId, ip });
664
+ socket.close(4008, "Rate limit exceeded");
665
+ return;
666
+ }
667
+ let message;
668
+ try {
669
+ message = JSON.parse(raw.toString());
670
+ } catch {
671
+ return;
672
+ }
673
+ if (!authenticated) {
674
+ if (message.type !== RELAY_MESSAGE_TYPES.AUTH) {
675
+ socket.close(4002, "Must authenticate first");
676
+ return;
677
+ }
678
+ const { relayId: rid, token } = message;
679
+ if (!rid || !token) {
680
+ socket.close(4003, "Missing relayId or token");
681
+ return;
682
+ }
683
+ const tokenHash = hashToken(token);
684
+ const verifiedRelay = await db.findRelayByTokenHash(
685
+ rid,
686
+ tokenHash,
687
+ [RELAY_STATUS.ACTIVE, RELAY_STATUS.DEGRADED]
688
+ ).catch(() => null);
689
+ if (!verifiedRelay) {
690
+ log.warn("Relay WebSocket auth failed", { relayId: rid });
691
+ socket.close(4004, "Invalid credentials");
692
+ return;
693
+ }
694
+ clearTimeout(authTimeout);
695
+ authenticated = true;
696
+ relayId = rid;
697
+ relayOrgId = verifiedRelay.organizationId;
698
+ log.debug("Relay WebSocket authenticated", { relayId: rid });
699
+ const existing = getRelaySocket(rid);
700
+ if (existing && existing.readyState === 1) {
701
+ existing.close(4005, "Replaced by new connection");
702
+ }
703
+ setRelaySocket(rid, socket);
704
+ await db.updateRelay(rid, {
705
+ lastSeenAt: /* @__PURE__ */ new Date(),
706
+ lastIpAddress: ip,
707
+ status: RELAY_STATUS.ACTIVE
708
+ });
709
+ socket.send(JSON.stringify({ type: "auth_ok" }));
710
+ broadcast.broadcast(verifiedRelay.organizationId, {
711
+ type: "relay:status_change",
712
+ relayId: rid,
713
+ online: true
714
+ });
715
+ events.emit("relay:online", rid, verifiedRelay.organizationId);
716
+ let pongReceived = true;
717
+ socket.on("pong", () => {
718
+ pongReceived = true;
719
+ });
720
+ heartbeatInterval = setInterval(() => {
721
+ if (socket.readyState !== 1) return;
722
+ if (!pongReceived) {
723
+ log.warn("Relay pong timeout \u2014 closing stale connection", { relayId: rid });
724
+ socket.close(1e3, "Pong timeout");
725
+ return;
726
+ }
727
+ pongReceived = false;
728
+ socket.ping();
729
+ }, limits.HEARTBEAT_INTERVAL_MS);
730
+ queue.drainQueue(rid).catch(() => {
731
+ });
732
+ return;
733
+ }
734
+ const isValidStr = (v, maxLen = 200) => typeof v === "string" && v.length > 0 && v.length <= maxLen;
735
+ switch (message.type) {
736
+ case RELAY_MESSAGE_TYPES.SMS_ACK:
737
+ if (isValidStr(message.messageId)) {
738
+ const status = isValidStr(message.status, 50) ? message.status : "sent";
739
+ const nativeId = isValidStr(message.nativeMessageId) ? message.nativeMessageId : void 0;
740
+ const errorMsg = isValidStr(message.errorMessage, 500) ? message.errorMessage : void 0;
741
+ log.debug("Relay SMS ACK", { relayId, messageId: message.messageId, status });
742
+ queue.handleAck(message.messageId, status, nativeId, errorMsg).catch(() => {
743
+ });
744
+ }
745
+ break;
746
+ case RELAY_MESSAGE_TYPES.DELIVERY_RECEIPT:
747
+ if (isValidStr(message.messageId) && isValidStr(message.deliveryStatus, 50)) {
748
+ queue.handleDeliveryReceipt(message.messageId, message.deliveryStatus).catch(() => {
749
+ });
750
+ }
751
+ break;
752
+ case RELAY_MESSAGE_TYPES.PONG:
753
+ if (relayId) {
754
+ db.updateRelay(relayId, { lastSeenAt: /* @__PURE__ */ new Date(), lastIpAddress: ip }).catch(() => {
755
+ });
756
+ }
757
+ break;
758
+ case RELAY_MESSAGE_TYPES.STATUS:
759
+ if (relayId) {
760
+ const battery = typeof message.batteryLevel === "number" && Number.isFinite(message.batteryLevel) ? Math.max(0, Math.min(100, Math.round(message.batteryLevel))) : null;
761
+ const signal = typeof message.signalStrength === "number" && Number.isFinite(message.signalStrength) ? Math.max(0, Math.min(100, Math.round(message.signalStrength))) : null;
762
+ const statusUpdate = { lastSeenAt: /* @__PURE__ */ new Date() };
763
+ if (battery !== null) statusUpdate.batteryLevel = battery;
764
+ if (signal !== null) statusUpdate.signalStrength = signal;
765
+ db.updateRelay(relayId, statusUpdate).catch(() => {
766
+ });
767
+ }
768
+ break;
769
+ case RELAY_MESSAGE_TYPES.REKEY_ACK:
770
+ if (relayId && isValidStr(message.devicePublicKey, 500)) {
771
+ try {
772
+ const relay = await db.findRelay(relayId);
773
+ if (!relay) break;
774
+ const rekeyData = JSON.parse(encryption.decrypt(relay.sharedKeyEncrypted));
775
+ if (!rekeyData.pending) break;
776
+ const newSharedKey = deriveSharedKey(rekeyData.serverPrivateKey, message.devicePublicKey);
777
+ await db.updateRelay(relayId, {
778
+ sharedKeyEncrypted: encryption.encrypt(newSharedKey.toString("hex")),
779
+ devicePublicKey: message.devicePublicKey,
780
+ keyVersion: relay.keyVersion + 1,
781
+ lastKeyRotation: /* @__PURE__ */ new Date()
782
+ });
783
+ socket.send(JSON.stringify({ type: "rekey_complete" }));
784
+ } catch {
785
+ log.warn("Rekey failed, notifying device", { relayId });
786
+ socket.send(JSON.stringify({ type: "rekey_failed" }));
787
+ }
788
+ }
789
+ break;
790
+ }
791
+ });
792
+ socket.on("close", async (_code, _reason) => {
793
+ log.debug("Relay WebSocket connection closed", { relayId: relayId || "unknown" });
794
+ clearTimeout(authTimeout);
795
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
796
+ if (relayId) {
797
+ deleteRelaySocket(relayId);
798
+ try {
799
+ if (relayOrgId) {
800
+ broadcast.broadcast(relayOrgId, {
801
+ type: "relay:status_change",
802
+ relayId,
803
+ online: false
804
+ });
805
+ events.emit("relay:offline", relayId, relayOrgId);
806
+ } else {
807
+ const relay = await db.findRelay(relayId);
808
+ if (relay) {
809
+ broadcast.broadcast(relay.organizationId, {
810
+ type: "relay:status_change",
811
+ relayId,
812
+ online: false
813
+ });
814
+ events.emit("relay:offline", relayId, relay.organizationId);
815
+ }
816
+ }
817
+ } catch {
818
+ }
819
+ }
820
+ });
821
+ socket.on("error", async () => {
822
+ clearTimeout(authTimeout);
823
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
824
+ if (relayId) {
825
+ deleteRelaySocket(relayId);
826
+ try {
827
+ if (relayOrgId) {
828
+ broadcast.broadcast(relayOrgId, {
829
+ type: "relay:status_change",
830
+ relayId,
831
+ online: false
832
+ });
833
+ events.emit("relay:offline", relayId, relayOrgId);
834
+ } else {
835
+ const relay = await db.findRelay(relayId);
836
+ if (relay) {
837
+ broadcast.broadcast(relay.organizationId, {
838
+ type: "relay:status_change",
839
+ relayId,
840
+ online: false
841
+ });
842
+ events.emit("relay:offline", relayId, relay.organizationId);
843
+ }
844
+ }
845
+ } catch {
846
+ }
847
+ }
848
+ });
849
+ };
850
+ }
851
+
852
+ // src/core/auth-limiter.ts
853
+ var AuthLimiter = class {
854
+ attempts = /* @__PURE__ */ new Map();
855
+ maxFailures;
856
+ windowMs;
857
+ lockoutMs;
858
+ cleanupInterval;
859
+ /**
860
+ * @param maxFailures Max auth failures before lockout (default: 10)
861
+ * @param windowMs Time window for counting failures in ms (default: 15 min)
862
+ * @param lockoutMs Duration of lockout after max failures (default: 30 min)
863
+ */
864
+ constructor(maxFailures = 10, windowMs = 15 * 60 * 1e3, lockoutMs = 30 * 60 * 1e3) {
865
+ this.maxFailures = maxFailures;
866
+ this.windowMs = windowMs;
867
+ this.lockoutMs = lockoutMs;
868
+ this.cleanupInterval = setInterval(() => this.cleanup(), 5 * 60 * 1e3);
869
+ }
870
+ /** Returns true if the IP is currently locked out */
871
+ isLocked(ip) {
872
+ const attempt = this.attempts.get(ip);
873
+ if (!attempt) return false;
874
+ if (attempt.lockedUntil > Date.now()) return true;
875
+ if (attempt.lockedUntil > 0) {
876
+ this.attempts.delete(ip);
877
+ return false;
878
+ }
879
+ return false;
880
+ }
881
+ /** Record a failed auth attempt. Returns remaining attempts before lockout. */
882
+ recordFailure(ip) {
883
+ const now = Date.now();
884
+ let attempt = this.attempts.get(ip);
885
+ if (!attempt || now - attempt.firstFailure > this.windowMs) {
886
+ attempt = { failures: 0, firstFailure: now, lockedUntil: 0 };
887
+ }
888
+ attempt.failures++;
889
+ if (attempt.failures >= this.maxFailures) {
890
+ attempt.lockedUntil = now + this.lockoutMs;
891
+ }
892
+ this.attempts.set(ip, attempt);
893
+ return Math.max(0, this.maxFailures - attempt.failures);
894
+ }
895
+ /** Clear failure count for an IP after successful auth */
896
+ recordSuccess(ip) {
897
+ this.attempts.delete(ip);
898
+ }
899
+ /** Get remaining lockout time in seconds (0 if not locked) */
900
+ getLockoutRemaining(ip) {
901
+ const attempt = this.attempts.get(ip);
902
+ if (!attempt || attempt.lockedUntil <= Date.now()) return 0;
903
+ return Math.ceil((attempt.lockedUntil - Date.now()) / 1e3);
904
+ }
905
+ cleanup() {
906
+ const now = Date.now();
907
+ for (const [ip, attempt] of this.attempts) {
908
+ if (attempt.lockedUntil > 0 && attempt.lockedUntil < now) {
909
+ this.attempts.delete(ip);
910
+ } else if (now - attempt.firstFailure > this.windowMs && attempt.lockedUntil === 0) {
911
+ this.attempts.delete(ip);
912
+ }
913
+ }
914
+ }
915
+ destroy() {
916
+ clearInterval(this.cleanupInterval);
917
+ this.attempts.clear();
918
+ }
919
+ };
920
+
921
+ // src/schemas.ts
922
+ import { z } from "zod";
923
+ var completePairingSchema = z.object({
924
+ pairingId: z.string().uuid(),
925
+ pairingToken: z.string().min(1),
926
+ devicePublicKey: z.string().min(44).max(5e3),
927
+ deviceName: z.string().min(1).max(200),
928
+ platform: z.enum(["android", "ios"]),
929
+ phoneNumber: z.string().max(30).optional()
930
+ });
931
+ var updatePhoneRelaySchema = z.object({
932
+ deviceName: z.string().min(1).max(200).optional(),
933
+ maxSmsPerMinute: z.number().int().min(1).max(60).optional(),
934
+ maxSmsPerHour: z.number().int().min(1).max(1e3).optional(),
935
+ maxSmsPerDay: z.number().int().min(1).max(1e4).optional()
936
+ });
937
+ var testRelaySmsSchema = z.object({
938
+ to: z.string().min(1),
939
+ body: z.string().min(1).max(160).optional()
940
+ });
941
+ var relayMessagesQuerySchema = z.object({
942
+ limit: z.coerce.number().int().min(1).max(100).default(50),
943
+ offset: z.coerce.number().int().min(0).default(0)
944
+ });
945
+
946
+ // src/core/crypto/encryption.ts
947
+ import crypto4 from "crypto";
948
+ var ALGORITHM = "aes-256-gcm";
949
+ var IV_LENGTH = 12;
950
+ var AUTH_TAG_LENGTH = 16;
951
+ var IV_HEX_LENGTH = IV_LENGTH * 2;
952
+ var AUTH_TAG_HEX_LENGTH = AUTH_TAG_LENGTH * 2;
953
+ var AesGcmEncryption = class {
954
+ keyBuffer;
955
+ allowPlaintextFallback;
956
+ /**
957
+ * @param key 64-char hex string OR 32-byte Buffer for AES-256
958
+ * @param options.allowPlaintextFallback Allow decrypting legacy unencrypted values.
959
+ * Set to false (default) for PCI DSS v4 compliance. Set to true only during migration.
960
+ */
961
+ constructor(key, options) {
962
+ if (Buffer.isBuffer(key)) {
963
+ if (key.length !== 32) {
964
+ throw new Error("Encryption key Buffer must be exactly 32 bytes per PCI DSS v4 Req 3.7.1");
965
+ }
966
+ this.keyBuffer = Buffer.from(key);
967
+ key.fill(0);
968
+ } else {
969
+ if (!key || key.length !== 64) {
970
+ throw new Error(
971
+ "Encryption key must be exactly 64 hex characters (32 bytes) per PCI DSS v4 Req 3.7.1"
972
+ );
973
+ }
974
+ if (!/^[0-9a-fA-F]+$/.test(key)) {
975
+ throw new Error("Encryption key must be valid hexadecimal");
976
+ }
977
+ this.keyBuffer = Buffer.from(key, "hex");
978
+ }
979
+ this.allowPlaintextFallback = options?.allowPlaintextFallback ?? false;
980
+ }
981
+ encrypt(plaintext) {
982
+ const iv = crypto4.randomBytes(IV_LENGTH);
983
+ const cipher = crypto4.createCipheriv(ALGORITHM, this.keyBuffer, iv);
984
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
985
+ const authTag = cipher.getAuthTag();
986
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
987
+ }
988
+ decrypt(ciphertext) {
989
+ const parts = ciphertext.split(":");
990
+ if (parts.length !== 3 || parts[0].length !== IV_HEX_LENGTH || parts[1].length !== AUTH_TAG_HEX_LENGTH) {
991
+ if (this.allowPlaintextFallback) {
992
+ return ciphertext;
993
+ }
994
+ throw new Error(
995
+ "Invalid ciphertext format. Plaintext fallback is disabled (PCI DSS v4 Req 3.4.1). If migrating legacy data, set allowPlaintextFallback: true temporarily."
996
+ );
997
+ }
998
+ const [ivHex, authTagHex, encryptedHex] = parts;
999
+ const iv = Buffer.from(ivHex, "hex");
1000
+ const authTag = Buffer.from(authTagHex, "hex");
1001
+ const encrypted = Buffer.from(encryptedHex, "hex");
1002
+ const decipher = crypto4.createDecipheriv(ALGORITHM, this.keyBuffer, iv);
1003
+ decipher.setAuthTag(authTag);
1004
+ try {
1005
+ return decipher.update(encrypted).toString("utf8") + decipher.final("utf8");
1006
+ } catch {
1007
+ throw new Error(
1008
+ "Decryption failed \u2014 possible key rotation issue or data tampering (PCI DSS v4 Req 3.5.1)"
1009
+ );
1010
+ }
1011
+ }
1012
+ /**
1013
+ * Securely destroy the key material (PCI DSS v4 Req 3.5.1).
1014
+ * Call when the SDK instance is being shut down.
1015
+ */
1016
+ destroy() {
1017
+ this.keyBuffer.fill(0);
1018
+ }
1019
+ };
1020
+
1021
+ // src/adapters/redis-broker.ts
1022
+ var RedisBroker = class {
1023
+ pub;
1024
+ // ioredis instance
1025
+ sub;
1026
+ // ioredis instance
1027
+ handlers = /* @__PURE__ */ new Map();
1028
+ prefix;
1029
+ constructor(pubClient, subClient, options) {
1030
+ this.pub = pubClient;
1031
+ this.sub = subClient;
1032
+ this.prefix = options?.prefix ?? "zi2:relay:";
1033
+ this.sub.on("message", (channel, message) => {
1034
+ const handler = this.handlers.get(channel);
1035
+ if (handler) handler(message);
1036
+ });
1037
+ }
1038
+ async publish(channel, message) {
1039
+ await this.pub.publish(`${this.prefix}${channel}`, message);
1040
+ }
1041
+ async subscribe(channel, handler) {
1042
+ const fullChannel = `${this.prefix}${channel}`;
1043
+ this.handlers.set(fullChannel, handler);
1044
+ await this.sub.subscribe(fullChannel);
1045
+ }
1046
+ async unsubscribe(channel) {
1047
+ const fullChannel = `${this.prefix}${channel}`;
1048
+ this.handlers.delete(fullChannel);
1049
+ await this.sub.unsubscribe(fullChannel);
1050
+ }
1051
+ async destroy() {
1052
+ this.handlers.clear();
1053
+ await this.sub.quit();
1054
+ await this.pub.quit();
1055
+ }
1056
+ };
1057
+
1058
+ // src/adapters/memory-adapter.ts
1059
+ var MemoryAdapter = class {
1060
+ relays = /* @__PURE__ */ new Map();
1061
+ pairings = /* @__PURE__ */ new Map();
1062
+ messages = /* @__PURE__ */ new Map();
1063
+ // ── PhoneRelay CRUD ─────────────────────────────────────────────────
1064
+ async findRelay(id, orgId) {
1065
+ const r = this.relays.get(id);
1066
+ if (!r) return null;
1067
+ if (orgId && r.organizationId !== orgId) return null;
1068
+ return structuredClone(r);
1069
+ }
1070
+ async findRelayByTokenHash(id, tokenHash, statuses) {
1071
+ const r = this.relays.get(id);
1072
+ if (!r) return null;
1073
+ if (r.authTokenHash !== tokenHash) return null;
1074
+ if (!statuses.includes(r.status)) return null;
1075
+ return structuredClone(r);
1076
+ }
1077
+ async findRelays(orgId, excludeStatus) {
1078
+ const results = [];
1079
+ for (const r of this.relays.values()) {
1080
+ if (r.organizationId !== orgId) continue;
1081
+ if (excludeStatus && r.status === excludeStatus) continue;
1082
+ results.push(structuredClone(r));
1083
+ }
1084
+ return results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
1085
+ }
1086
+ async createRelay(data) {
1087
+ const now = /* @__PURE__ */ new Date();
1088
+ const record = {
1089
+ id: crypto.randomUUID(),
1090
+ ...data,
1091
+ keyVersion: 1,
1092
+ totalSmsSent: 0,
1093
+ totalSmsFailed: 0,
1094
+ dailySmsSent: 0,
1095
+ createdAt: now,
1096
+ updatedAt: now
1097
+ };
1098
+ this.relays.set(record.id, record);
1099
+ return structuredClone(record);
1100
+ }
1101
+ async updateRelay(id, data) {
1102
+ const r = this.relays.get(id);
1103
+ if (!r) return;
1104
+ Object.assign(r, data, { updatedAt: /* @__PURE__ */ new Date() });
1105
+ }
1106
+ async updateRelayByOrg(id, orgId, data) {
1107
+ const r = this.relays.get(id);
1108
+ if (!r || r.organizationId !== orgId) return;
1109
+ Object.assign(r, data, { updatedAt: /* @__PURE__ */ new Date() });
1110
+ }
1111
+ async deleteRelay(id) {
1112
+ this.relays.delete(id);
1113
+ }
1114
+ async incrementRelayStat(id, field, amount = 1) {
1115
+ const r = this.relays.get(id);
1116
+ if (!r) return;
1117
+ r[field] += amount;
1118
+ r.updatedAt = /* @__PURE__ */ new Date();
1119
+ }
1120
+ async countRelays(orgId) {
1121
+ let count = 0;
1122
+ for (const r of this.relays.values()) {
1123
+ if (r.organizationId === orgId && r.status !== RELAY_STATUS.REVOKED) {
1124
+ count++;
1125
+ }
1126
+ }
1127
+ return count;
1128
+ }
1129
+ async markDegradedRelays(threshold) {
1130
+ let count = 0;
1131
+ for (const r of this.relays.values()) {
1132
+ if (r.status === RELAY_STATUS.ACTIVE && r.lastSeenAt && r.lastSeenAt < threshold) {
1133
+ r.status = RELAY_STATUS.DEGRADED;
1134
+ r.updatedAt = /* @__PURE__ */ new Date();
1135
+ count++;
1136
+ }
1137
+ }
1138
+ return count;
1139
+ }
1140
+ async resetDailyCounters() {
1141
+ const now = /* @__PURE__ */ new Date();
1142
+ let count = 0;
1143
+ for (const r of this.relays.values()) {
1144
+ if (r.status === RELAY_STATUS.ACTIVE || r.status === RELAY_STATUS.DEGRADED) {
1145
+ r.dailySmsSent = 0;
1146
+ r.dailyResetAt = now;
1147
+ r.updatedAt = now;
1148
+ count++;
1149
+ }
1150
+ }
1151
+ return count;
1152
+ }
1153
+ // ── PhoneRelayPairing ───────────────────────────────────────────────
1154
+ async findPairing(id) {
1155
+ const p = this.pairings.get(id);
1156
+ return p ? structuredClone(p) : null;
1157
+ }
1158
+ async createPairing(data) {
1159
+ const record = {
1160
+ id: crypto.randomUUID(),
1161
+ ...data,
1162
+ createdAt: /* @__PURE__ */ new Date()
1163
+ };
1164
+ this.pairings.set(record.id, record);
1165
+ return structuredClone(record);
1166
+ }
1167
+ async updatePairingStatus(id, status) {
1168
+ const p = this.pairings.get(id);
1169
+ if (p) p.status = status;
1170
+ }
1171
+ async deleteExpiredPairings() {
1172
+ const now = /* @__PURE__ */ new Date();
1173
+ let count = 0;
1174
+ for (const [id, p] of this.pairings) {
1175
+ if (p.expiresAt < now && p.status === "pending") {
1176
+ this.pairings.delete(id);
1177
+ count++;
1178
+ }
1179
+ }
1180
+ return count;
1181
+ }
1182
+ // ── RelayMessageQueue ───────────────────────────────────────────────
1183
+ async findMessage(id) {
1184
+ const m = this.messages.get(id);
1185
+ return m ? structuredClone(m) : null;
1186
+ }
1187
+ async createMessage(data) {
1188
+ const now = /* @__PURE__ */ new Date();
1189
+ const record = {
1190
+ id: crypto.randomUUID(),
1191
+ relayId: data.relayId,
1192
+ organizationId: data.organizationId,
1193
+ encryptedPayload: data.encryptedPayload,
1194
+ status: "pending",
1195
+ attempts: 0,
1196
+ maxAttempts: 3,
1197
+ nativeMessageId: null,
1198
+ errorMessage: null,
1199
+ deliveryStatus: null,
1200
+ sentToDeviceAt: null,
1201
+ ackedAt: null,
1202
+ expiresAt: data.expiresAt,
1203
+ createdAt: now,
1204
+ updatedAt: now
1205
+ };
1206
+ this.messages.set(record.id, record);
1207
+ return structuredClone(record);
1208
+ }
1209
+ async updateMessage(id, data) {
1210
+ const m = this.messages.get(id);
1211
+ if (!m) return;
1212
+ Object.assign(m, data, { updatedAt: /* @__PURE__ */ new Date() });
1213
+ }
1214
+ async findPendingMessages(relayId, limit) {
1215
+ const now = /* @__PURE__ */ new Date();
1216
+ return Array.from(this.messages.values()).filter((m) => m.relayId === relayId && m.status === "pending" && m.expiresAt > now).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, limit).map((m) => structuredClone(m));
1217
+ }
1218
+ async findMessages(relayId, limit, offset) {
1219
+ const filtered = Array.from(this.messages.values()).filter((m) => m.relayId === relayId).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
1220
+ const total = filtered.length;
1221
+ const messages = filtered.slice(offset, offset + limit).map((m) => structuredClone(m));
1222
+ return { messages, total };
1223
+ }
1224
+ async deleteMessagesByRelay(relayId) {
1225
+ let count = 0;
1226
+ for (const [id, m] of this.messages) {
1227
+ if (m.relayId === relayId) {
1228
+ this.messages.delete(id);
1229
+ count++;
1230
+ }
1231
+ }
1232
+ return count;
1233
+ }
1234
+ async expireStaleMessages() {
1235
+ const now = /* @__PURE__ */ new Date();
1236
+ let count = 0;
1237
+ for (const m of this.messages.values()) {
1238
+ if (m.expiresAt < now && (m.status === "pending" || m.status === "sent_to_device")) {
1239
+ m.status = "expired";
1240
+ m.updatedAt = /* @__PURE__ */ new Date();
1241
+ count++;
1242
+ }
1243
+ }
1244
+ return count;
1245
+ }
1246
+ // ── Batch operations ────────────────────────────────────────────────
1247
+ async createMessages(batch) {
1248
+ const results = [];
1249
+ for (const data of batch) {
1250
+ results.push(await this.createMessage(data));
1251
+ }
1252
+ return results;
1253
+ }
1254
+ async updateMessages(ids, data) {
1255
+ let count = 0;
1256
+ for (const id of ids) {
1257
+ const m = this.messages.get(id);
1258
+ if (m) {
1259
+ Object.assign(m, data, { updatedAt: /* @__PURE__ */ new Date() });
1260
+ count++;
1261
+ }
1262
+ }
1263
+ return count;
1264
+ }
1265
+ // ── Test helpers ──────────────────────────────────────────────────
1266
+ clear() {
1267
+ this.relays.clear();
1268
+ this.pairings.clear();
1269
+ this.messages.clear();
1270
+ }
1271
+ };
1272
+
1273
+ // src/i18n/locales/en.ts
1274
+ var en = {
1275
+ // Dashboard
1276
+ "relay.title": "ZI2 SMS Relay",
1277
+ "relay.connected": "Connected",
1278
+ "relay.connecting": "Connecting...",
1279
+ "relay.disconnected": "Disconnected",
1280
+ "relay.uptime": "Uptime",
1281
+ "relay.establishingConnection": "Establishing secure connection...",
1282
+ "relay.tapReconnect": "Tap reconnect to retry",
1283
+ "relay.live": "LIVE",
1284
+ "relay.e2eEncrypted": "E2E Encrypted",
1285
+ "relay.reconnect": "Reconnect",
1286
+ "relay.sent": "Sent",
1287
+ "relay.failed": "Failed",
1288
+ "relay.remaining": "Remaining",
1289
+ "relay.successRate": "Success Rate",
1290
+ "relay.thisSession": "this session",
1291
+ "relay.ofPerDay": "of {{limit}}/day",
1292
+ "relay.total": "Total",
1293
+ "relay.dailyUsage": "Daily Usage",
1294
+ "relay.perMin": "{{limit}}/min",
1295
+ "relay.perHr": "{{limit}}/hr",
1296
+ "relay.perDay": "{{limit}}/day",
1297
+ "relay.quickControls": "Quick Controls",
1298
+ "relay.forceReconnect": "Force Reconnect",
1299
+ "relay.forceReconnectDesc": "Re-establish WebSocket connection",
1300
+ "relay.pauseRelay": "Pause Relay",
1301
+ "relay.pauseRelayDesc": "Temporarily stop receiving SMS",
1302
+ "relay.resumeRelay": "Resume Relay",
1303
+ "relay.resumeRelayDesc": "Start receiving SMS again",
1304
+ "relay.sendDeviceStatus": "Send Device Status",
1305
+ "relay.sendDeviceStatusDesc": "Report battery & signal to server",
1306
+ "relay.messageLog": "Message Log",
1307
+ "relay.messagesRecorded": "{{count}} messages recorded",
1308
+ "relay.connectionInfo": "Connection Info",
1309
+ "relay.notPaired": "Not paired",
1310
+ "relay.noServer": "No server",
1311
+ "relay.recentActivity": "Recent Activity",
1312
+ "relay.viewAll": "View All",
1313
+ "relay.noMessagesYet": "No messages yet",
1314
+ "relay.smsWillAppear": "SMS will appear here as they are relayed",
1315
+ "relay.delivered": "Delivered",
1316
+ "relay.smsPermissionRequired": "SMS Permission Required",
1317
+ "relay.smsPermissionDesc": "Grant SMS permission in device settings to send messages",
1318
+ "relay.retry": "Retry",
1319
+ // Pairing
1320
+ "relay.pairingTitle": "ZI2 SMS Relay",
1321
+ "relay.pairingInstruction": "Point the camera at the QR code on your screen to pair.",
1322
+ "relay.pairingWithServer": "Pairing with server...",
1323
+ "relay.pairingFailed": "Pairing failed",
1324
+ "relay.tryAgain": "Try Again",
1325
+ "relay.cameraAccessDenied": "Camera access denied",
1326
+ // Settings
1327
+ "relay.settings": "Settings",
1328
+ "relay.relayInformation": "Relay Information",
1329
+ "relay.relayId": "Relay ID",
1330
+ "relay.server": "Server",
1331
+ "relay.status": "Status",
1332
+ "relay.online": "Online",
1333
+ "relay.offline": "Offline",
1334
+ "relay.encryption": "Encryption",
1335
+ "relay.sessionStatistics": "Session Statistics",
1336
+ "relay.messagesSent": "Messages Sent",
1337
+ "relay.messagesFailed": "Messages Failed",
1338
+ "relay.totalLogged": "Total Logged",
1339
+ "relay.rateLimits": "Rate Limits",
1340
+ "relay.perMinute": "Per Minute",
1341
+ "relay.perHour": "Per Hour",
1342
+ "relay.perDayLabel": "Per Day",
1343
+ "relay.messageExpiry": "Message Expiry",
1344
+ "relay.expiryHours": "{{hours}}h",
1345
+ "relay.about": "About",
1346
+ "relay.version": "ZI2 SMS Relay v1.0.0",
1347
+ "relay.aboutDescription": "Relays SMS messages from your ZI2 server through your phone's native SIM card. All messages are end-to-end encrypted using X25519 key exchange and AES-256-GCM. The server never sees plaintext message content.",
1348
+ "relay.dangerZone": "Danger Zone",
1349
+ "relay.dangerZoneDesc": "Clear all data and unpair this device from the server. This will require re-pairing via QR code.",
1350
+ "relay.clearDataUnpair": "Clear Data & Unpair",
1351
+ "relay.confirmClearData": "Confirm - Clear All Data & Unpair",
1352
+ // Message Log
1353
+ "relay.messageLogTitle": "Message Log",
1354
+ "relay.totalLabel": "total",
1355
+ "relay.all": "All",
1356
+ "relay.noFilteredMessages": "No {{filter}} messages",
1357
+ "relay.messagesWillAppear": "Messages will appear here as SMS are relayed",
1358
+ "relay.sentLabel": "SENT",
1359
+ "relay.failLabel": "FAIL",
1360
+ "relay.e2eFooter": "Message content is E2E encrypted and not stored on device",
1361
+ "relay.copied": "Copied!",
1362
+ "relay.na": "N/A",
1363
+ "relay.cancel": "Cancel",
1364
+ // Error codes
1365
+ "errors.relayNotFound": "Relay device not found",
1366
+ "errors.relayInactive": "Relay is not active",
1367
+ "errors.relayOffline": "Relay device is offline",
1368
+ "errors.pairingNotFound": "Pairing session not found",
1369
+ "errors.pairingExpired": "Pairing session has expired",
1370
+ "errors.pairingInvalidToken": "Invalid pairing token",
1371
+ "errors.authTimeout": "WebSocket authentication timeout",
1372
+ "errors.authFailed": "Invalid relay credentials",
1373
+ "errors.rateLimited": "Rate limit exceeded",
1374
+ "errors.encryptionFailed": "Encryption operation failed",
1375
+ "errors.rekeyFailed": "Key rotation failed",
1376
+ "errors.smsSendFailed": "SMS delivery failed",
1377
+ "errors.smsTimeout": "SMS acknowledgement timeout"
1378
+ };
1379
+
1380
+ // src/i18n/locales/fr.ts
1381
+ var fr = {
1382
+ // Dashboard
1383
+ "relay.title": "ZI2 Relais SMS",
1384
+ "relay.connected": "Connect\xE9",
1385
+ "relay.connecting": "Connexion...",
1386
+ "relay.disconnected": "D\xE9connect\xE9",
1387
+ "relay.uptime": "Disponibilit\xE9",
1388
+ "relay.establishingConnection": "\xC9tablissement de la connexion s\xE9curis\xE9e...",
1389
+ "relay.tapReconnect": "Appuyez sur reconnexion pour r\xE9essayer",
1390
+ "relay.live": "EN DIRECT",
1391
+ "relay.e2eEncrypted": "Chiffrement E2E",
1392
+ "relay.reconnect": "Reconnecter",
1393
+ "relay.sent": "Envoy\xE9s",
1394
+ "relay.failed": "\xC9chou\xE9s",
1395
+ "relay.remaining": "Restants",
1396
+ "relay.successRate": "Taux de r\xE9ussite",
1397
+ "relay.thisSession": "cette session",
1398
+ "relay.ofPerDay": "sur {{limit}}/jour",
1399
+ "relay.total": "Total",
1400
+ "relay.dailyUsage": "Utilisation quotidienne",
1401
+ "relay.perMin": "{{limit}}/min",
1402
+ "relay.perHr": "{{limit}}/h",
1403
+ "relay.perDay": "{{limit}}/jour",
1404
+ "relay.quickControls": "Contr\xF4les rapides",
1405
+ "relay.forceReconnect": "Forcer la reconnexion",
1406
+ "relay.forceReconnectDesc": "R\xE9tablir la connexion WebSocket",
1407
+ "relay.pauseRelay": "Suspendre le relais",
1408
+ "relay.pauseRelayDesc": "Arr\xEAter temporairement la r\xE9ception de SMS",
1409
+ "relay.resumeRelay": "Reprendre le relais",
1410
+ "relay.resumeRelayDesc": "Reprendre la r\xE9ception de SMS",
1411
+ "relay.sendDeviceStatus": "Envoyer l'\xE9tat de l'appareil",
1412
+ "relay.sendDeviceStatusDesc": "Signaler la batterie et le signal au serveur",
1413
+ "relay.messageLog": "Journal des messages",
1414
+ "relay.messagesRecorded": "{{count}} messages enregistr\xE9s",
1415
+ "relay.connectionInfo": "Informations de connexion",
1416
+ "relay.notPaired": "Non appari\xE9",
1417
+ "relay.noServer": "Aucun serveur",
1418
+ "relay.recentActivity": "Activit\xE9 r\xE9cente",
1419
+ "relay.viewAll": "Voir tout",
1420
+ "relay.noMessagesYet": "Aucun message pour le moment",
1421
+ "relay.smsWillAppear": "Les SMS appara\xEEtront ici lorsqu'ils seront relay\xE9s",
1422
+ "relay.delivered": "Livr\xE9",
1423
+ "relay.smsPermissionRequired": "Permission SMS requise",
1424
+ "relay.smsPermissionDesc": "Accordez la permission SMS dans les param\xE8tres de l'appareil pour envoyer des messages",
1425
+ "relay.retry": "R\xE9essayer",
1426
+ // Pairing
1427
+ "relay.pairingTitle": "ZI2 Relais SMS",
1428
+ "relay.pairingInstruction": "Pointez la cam\xE9ra vers le code QR sur votre \xE9cran pour appairer.",
1429
+ "relay.pairingWithServer": "Appairage avec le serveur...",
1430
+ "relay.pairingFailed": "\xC9chec de l'appairage",
1431
+ "relay.tryAgain": "R\xE9essayer",
1432
+ "relay.cameraAccessDenied": "Acc\xE8s \xE0 la cam\xE9ra refus\xE9",
1433
+ // Settings
1434
+ "relay.settings": "Param\xE8tres",
1435
+ "relay.relayInformation": "Informations du relais",
1436
+ "relay.relayId": "ID du relais",
1437
+ "relay.server": "Serveur",
1438
+ "relay.status": "Statut",
1439
+ "relay.online": "En ligne",
1440
+ "relay.offline": "Hors ligne",
1441
+ "relay.encryption": "Chiffrement",
1442
+ "relay.sessionStatistics": "Statistiques de session",
1443
+ "relay.messagesSent": "Messages envoy\xE9s",
1444
+ "relay.messagesFailed": "Messages \xE9chou\xE9s",
1445
+ "relay.totalLogged": "Total enregistr\xE9",
1446
+ "relay.rateLimits": "Limites de d\xE9bit",
1447
+ "relay.perMinute": "Par minute",
1448
+ "relay.perHour": "Par heure",
1449
+ "relay.perDayLabel": "Par jour",
1450
+ "relay.messageExpiry": "Expiration des messages",
1451
+ "relay.expiryHours": "{{hours}}h",
1452
+ "relay.about": "\xC0 propos",
1453
+ "relay.version": "ZI2 Relais SMS v1.0.0",
1454
+ "relay.aboutDescription": "Relaye les messages SMS depuis votre serveur ZI2 via la carte SIM native de votre t\xE9l\xE9phone. Tous les messages sont chiffr\xE9s de bout en bout avec l'\xE9change de cl\xE9s X25519 et AES-256-GCM. Le serveur ne voit jamais le contenu en clair des messages.",
1455
+ "relay.dangerZone": "Zone de danger",
1456
+ "relay.dangerZoneDesc": "Effacer toutes les donn\xE9es et d\xE9sappairer cet appareil du serveur. Cela n\xE9cessitera un r\xE9-appairage via code QR.",
1457
+ "relay.clearDataUnpair": "Effacer les donn\xE9es et d\xE9sappairer",
1458
+ "relay.confirmClearData": "Confirmer - Effacer toutes les donn\xE9es et d\xE9sappairer",
1459
+ // Message Log
1460
+ "relay.messageLogTitle": "Journal des messages",
1461
+ "relay.totalLabel": "total",
1462
+ "relay.all": "Tous",
1463
+ "relay.noFilteredMessages": "Aucun message {{filter}}",
1464
+ "relay.messagesWillAppear": "Les messages appara\xEEtront ici lorsque les SMS seront relay\xE9s",
1465
+ "relay.sentLabel": "ENVOY\xC9",
1466
+ "relay.failLabel": "\xC9CHEC",
1467
+ "relay.e2eFooter": "Le contenu des messages est chiffr\xE9 E2E et non stock\xE9 sur l'appareil",
1468
+ "relay.copied": "Copi\xE9 !",
1469
+ "relay.na": "N/A",
1470
+ "relay.cancel": "Annuler",
1471
+ // Error codes
1472
+ "errors.relayNotFound": "Appareil relais introuvable",
1473
+ "errors.relayInactive": "Le relais n'est pas actif",
1474
+ "errors.relayOffline": "L'appareil relais est hors ligne",
1475
+ "errors.pairingNotFound": "Session d'appairage introuvable",
1476
+ "errors.pairingExpired": "La session d'appairage a expir\xE9",
1477
+ "errors.pairingInvalidToken": "Jeton d'appairage invalide",
1478
+ "errors.authTimeout": "D\xE9lai d'authentification WebSocket d\xE9pass\xE9",
1479
+ "errors.authFailed": "Identifiants du relais invalides",
1480
+ "errors.rateLimited": "Limite de d\xE9bit d\xE9pass\xE9e",
1481
+ "errors.encryptionFailed": "\xC9chec de l'op\xE9ration de chiffrement",
1482
+ "errors.rekeyFailed": "\xC9chec de la rotation des cl\xE9s",
1483
+ "errors.smsSendFailed": "\xC9chec de la livraison du SMS",
1484
+ "errors.smsTimeout": "D\xE9lai d'accus\xE9 de r\xE9ception du SMS d\xE9pass\xE9"
1485
+ };
1486
+
1487
+ // src/i18n/locales/es.ts
1488
+ var es = {
1489
+ // Dashboard
1490
+ "relay.title": "ZI2 Rel\xE9 SMS",
1491
+ "relay.connected": "Conectado",
1492
+ "relay.connecting": "Conectando...",
1493
+ "relay.disconnected": "Desconectado",
1494
+ "relay.uptime": "Tiempo activo",
1495
+ "relay.establishingConnection": "Estableciendo conexi\xF3n segura...",
1496
+ "relay.tapReconnect": "Toque reconectar para reintentar",
1497
+ "relay.live": "EN VIVO",
1498
+ "relay.e2eEncrypted": "Cifrado E2E",
1499
+ "relay.reconnect": "Reconectar",
1500
+ "relay.sent": "Enviados",
1501
+ "relay.failed": "Fallidos",
1502
+ "relay.remaining": "Restantes",
1503
+ "relay.successRate": "Tasa de \xE9xito",
1504
+ "relay.thisSession": "esta sesi\xF3n",
1505
+ "relay.ofPerDay": "de {{limit}}/d\xEDa",
1506
+ "relay.total": "Total",
1507
+ "relay.dailyUsage": "Uso diario",
1508
+ "relay.perMin": "{{limit}}/min",
1509
+ "relay.perHr": "{{limit}}/h",
1510
+ "relay.perDay": "{{limit}}/d\xEDa",
1511
+ "relay.quickControls": "Controles r\xE1pidos",
1512
+ "relay.forceReconnect": "Forzar reconexi\xF3n",
1513
+ "relay.forceReconnectDesc": "Restablecer la conexi\xF3n WebSocket",
1514
+ "relay.pauseRelay": "Pausar rel\xE9",
1515
+ "relay.pauseRelayDesc": "Dejar de recibir SMS temporalmente",
1516
+ "relay.resumeRelay": "Reanudar rel\xE9",
1517
+ "relay.resumeRelayDesc": "Volver a recibir SMS",
1518
+ "relay.sendDeviceStatus": "Enviar estado del dispositivo",
1519
+ "relay.sendDeviceStatusDesc": "Informar bater\xEDa y se\xF1al al servidor",
1520
+ "relay.messageLog": "Registro de mensajes",
1521
+ "relay.messagesRecorded": "{{count}} mensajes registrados",
1522
+ "relay.connectionInfo": "Informaci\xF3n de conexi\xF3n",
1523
+ "relay.notPaired": "No emparejado",
1524
+ "relay.noServer": "Sin servidor",
1525
+ "relay.recentActivity": "Actividad reciente",
1526
+ "relay.viewAll": "Ver todo",
1527
+ "relay.noMessagesYet": "Sin mensajes a\xFAn",
1528
+ "relay.smsWillAppear": "Los SMS aparecer\xE1n aqu\xED cuando sean retransmitidos",
1529
+ "relay.delivered": "Entregado",
1530
+ "relay.smsPermissionRequired": "Permiso de SMS requerido",
1531
+ "relay.smsPermissionDesc": "Otorgue permiso de SMS en la configuraci\xF3n del dispositivo para enviar mensajes",
1532
+ "relay.retry": "Reintentar",
1533
+ // Pairing
1534
+ "relay.pairingTitle": "ZI2 Rel\xE9 SMS",
1535
+ "relay.pairingInstruction": "Apunte la c\xE1mara al c\xF3digo QR en su pantalla para emparejar.",
1536
+ "relay.pairingWithServer": "Emparejando con el servidor...",
1537
+ "relay.pairingFailed": "Error en el emparejamiento",
1538
+ "relay.tryAgain": "Intentar de nuevo",
1539
+ "relay.cameraAccessDenied": "Acceso a la c\xE1mara denegado",
1540
+ // Settings
1541
+ "relay.settings": "Configuraci\xF3n",
1542
+ "relay.relayInformation": "Informaci\xF3n del rel\xE9",
1543
+ "relay.relayId": "ID del rel\xE9",
1544
+ "relay.server": "Servidor",
1545
+ "relay.status": "Estado",
1546
+ "relay.online": "En l\xEDnea",
1547
+ "relay.offline": "Fuera de l\xEDnea",
1548
+ "relay.encryption": "Cifrado",
1549
+ "relay.sessionStatistics": "Estad\xEDsticas de sesi\xF3n",
1550
+ "relay.messagesSent": "Mensajes enviados",
1551
+ "relay.messagesFailed": "Mensajes fallidos",
1552
+ "relay.totalLogged": "Total registrado",
1553
+ "relay.rateLimits": "L\xEDmites de velocidad",
1554
+ "relay.perMinute": "Por minuto",
1555
+ "relay.perHour": "Por hora",
1556
+ "relay.perDayLabel": "Por d\xEDa",
1557
+ "relay.messageExpiry": "Expiraci\xF3n de mensajes",
1558
+ "relay.expiryHours": "{{hours}}h",
1559
+ "relay.about": "Acerca de",
1560
+ "relay.version": "ZI2 Rel\xE9 SMS v1.0.0",
1561
+ "relay.aboutDescription": "Retransmite mensajes SMS desde su servidor ZI2 a trav\xE9s de la tarjeta SIM nativa de su tel\xE9fono. Todos los mensajes est\xE1n cifrados de extremo a extremo usando intercambio de claves X25519 y AES-256-GCM.",
1562
+ "relay.dangerZone": "Zona de peligro",
1563
+ "relay.dangerZoneDesc": "Borrar todos los datos y desemparejar este dispositivo del servidor. Esto requerir\xE1 un nuevo emparejamiento v\xEDa c\xF3digo QR.",
1564
+ "relay.clearDataUnpair": "Borrar datos y desemparejar",
1565
+ "relay.confirmClearData": "Confirmar - Borrar todos los datos y desemparejar",
1566
+ // Message Log
1567
+ "relay.messageLogTitle": "Registro de mensajes",
1568
+ "relay.totalLabel": "total",
1569
+ "relay.all": "Todos",
1570
+ "relay.noFilteredMessages": "Sin mensajes {{filter}}",
1571
+ "relay.messagesWillAppear": "Los mensajes aparecer\xE1n aqu\xED cuando los SMS sean retransmitidos",
1572
+ "relay.sentLabel": "ENVIADO",
1573
+ "relay.failLabel": "FALLO",
1574
+ "relay.e2eFooter": "El contenido del mensaje est\xE1 cifrado E2E y no se almacena en el dispositivo",
1575
+ "relay.copied": "\xA1Copiado!",
1576
+ "relay.na": "N/A",
1577
+ "relay.cancel": "Cancelar",
1578
+ // Error codes
1579
+ "errors.relayNotFound": "Dispositivo rel\xE9 no encontrado",
1580
+ "errors.relayInactive": "El rel\xE9 no est\xE1 activo",
1581
+ "errors.relayOffline": "El dispositivo rel\xE9 est\xE1 fuera de l\xEDnea",
1582
+ "errors.pairingNotFound": "Sesi\xF3n de emparejamiento no encontrada",
1583
+ "errors.pairingExpired": "La sesi\xF3n de emparejamiento ha expirado",
1584
+ "errors.pairingInvalidToken": "Token de emparejamiento inv\xE1lido",
1585
+ "errors.authTimeout": "Tiempo de autenticaci\xF3n WebSocket agotado",
1586
+ "errors.authFailed": "Credenciales del rel\xE9 inv\xE1lidas",
1587
+ "errors.rateLimited": "L\xEDmite de velocidad excedido",
1588
+ "errors.encryptionFailed": "Error en la operaci\xF3n de cifrado",
1589
+ "errors.rekeyFailed": "Error en la rotaci\xF3n de claves",
1590
+ "errors.smsSendFailed": "Error en la entrega del SMS",
1591
+ "errors.smsTimeout": "Tiempo de acuse de recibo del SMS agotado"
1592
+ };
1593
+
1594
+ // src/i18n/locales/de.ts
1595
+ var de = {
1596
+ // Dashboard
1597
+ "relay.title": "ZI2 SMS-Relay",
1598
+ "relay.connected": "Verbunden",
1599
+ "relay.connecting": "Verbindung wird hergestellt...",
1600
+ "relay.disconnected": "Getrennt",
1601
+ "relay.uptime": "Betriebszeit",
1602
+ "relay.establishingConnection": "Sichere Verbindung wird hergestellt...",
1603
+ "relay.tapReconnect": "Tippen Sie auf Verbinden, um es erneut zu versuchen",
1604
+ "relay.live": "LIVE",
1605
+ "relay.e2eEncrypted": "E2E-verschl\xFCsselt",
1606
+ "relay.reconnect": "Neu verbinden",
1607
+ "relay.sent": "Gesendet",
1608
+ "relay.failed": "Fehlgeschlagen",
1609
+ "relay.remaining": "Verbleibend",
1610
+ "relay.successRate": "Erfolgsrate",
1611
+ "relay.thisSession": "diese Sitzung",
1612
+ "relay.ofPerDay": "von {{limit}}/Tag",
1613
+ "relay.total": "Gesamt",
1614
+ "relay.dailyUsage": "T\xE4gliche Nutzung",
1615
+ "relay.perMin": "{{limit}}/min",
1616
+ "relay.perHr": "{{limit}}/Std",
1617
+ "relay.perDay": "{{limit}}/Tag",
1618
+ "relay.quickControls": "Schnellsteuerung",
1619
+ "relay.forceReconnect": "Verbindung erzwingen",
1620
+ "relay.forceReconnectDesc": "WebSocket-Verbindung wiederherstellen",
1621
+ "relay.pauseRelay": "Relay pausieren",
1622
+ "relay.pauseRelayDesc": "SMS-Empfang vor\xFCbergehend stoppen",
1623
+ "relay.resumeRelay": "Relay fortsetzen",
1624
+ "relay.resumeRelayDesc": "SMS-Empfang wieder aufnehmen",
1625
+ "relay.sendDeviceStatus": "Ger\xE4testatus senden",
1626
+ "relay.sendDeviceStatusDesc": "Akku und Signal an Server melden",
1627
+ "relay.messageLog": "Nachrichtenprotokoll",
1628
+ "relay.messagesRecorded": "{{count}} Nachrichten aufgezeichnet",
1629
+ "relay.connectionInfo": "Verbindungsinformationen",
1630
+ "relay.notPaired": "Nicht gekoppelt",
1631
+ "relay.noServer": "Kein Server",
1632
+ "relay.recentActivity": "Letzte Aktivit\xE4t",
1633
+ "relay.viewAll": "Alle anzeigen",
1634
+ "relay.noMessagesYet": "Noch keine Nachrichten",
1635
+ "relay.smsWillAppear": "SMS werden hier angezeigt, wenn sie weitergeleitet werden",
1636
+ "relay.delivered": "Zugestellt",
1637
+ "relay.smsPermissionRequired": "SMS-Berechtigung erforderlich",
1638
+ "relay.smsPermissionDesc": "Erteilen Sie die SMS-Berechtigung in den Ger\xE4teeinstellungen, um Nachrichten zu senden",
1639
+ "relay.retry": "Erneut versuchen",
1640
+ // Pairing
1641
+ "relay.pairingTitle": "ZI2 SMS-Relay",
1642
+ "relay.pairingInstruction": "Richten Sie die Kamera auf den QR-Code auf Ihrem Bildschirm, um zu koppeln.",
1643
+ "relay.pairingWithServer": "Kopplung mit Server...",
1644
+ "relay.pairingFailed": "Kopplung fehlgeschlagen",
1645
+ "relay.tryAgain": "Erneut versuchen",
1646
+ "relay.cameraAccessDenied": "Kamerazugriff verweigert",
1647
+ // Settings
1648
+ "relay.settings": "Einstellungen",
1649
+ "relay.relayInformation": "Relay-Informationen",
1650
+ "relay.relayId": "Relay-ID",
1651
+ "relay.server": "Server",
1652
+ "relay.status": "Status",
1653
+ "relay.online": "Online",
1654
+ "relay.offline": "Offline",
1655
+ "relay.encryption": "Verschl\xFCsselung",
1656
+ "relay.sessionStatistics": "Sitzungsstatistiken",
1657
+ "relay.messagesSent": "Gesendete Nachrichten",
1658
+ "relay.messagesFailed": "Fehlgeschlagene Nachrichten",
1659
+ "relay.totalLogged": "Gesamt protokolliert",
1660
+ "relay.rateLimits": "Ratenbegrenzungen",
1661
+ "relay.perMinute": "Pro Minute",
1662
+ "relay.perHour": "Pro Stunde",
1663
+ "relay.perDayLabel": "Pro Tag",
1664
+ "relay.messageExpiry": "Nachrichtenablauf",
1665
+ "relay.expiryHours": "{{hours}}h",
1666
+ "relay.about": "\xDCber",
1667
+ "relay.version": "ZI2 SMS-Relay v1.0.0",
1668
+ "relay.aboutDescription": "Leitet SMS-Nachrichten von Ihrem ZI2-Server \xFCber die native SIM-Karte Ihres Telefons weiter. Alle Nachrichten sind Ende-zu-Ende mit X25519-Schl\xFCsselaustausch und AES-256-GCM verschl\xFCsselt.",
1669
+ "relay.dangerZone": "Gefahrenzone",
1670
+ "relay.dangerZoneDesc": "Alle Daten l\xF6schen und dieses Ger\xE4t vom Server entkoppeln. Dies erfordert eine erneute Kopplung \xFCber QR-Code.",
1671
+ "relay.clearDataUnpair": "Daten l\xF6schen & entkoppeln",
1672
+ "relay.confirmClearData": "Best\xE4tigen - Alle Daten l\xF6schen & entkoppeln",
1673
+ // Message Log
1674
+ "relay.messageLogTitle": "Nachrichtenprotokoll",
1675
+ "relay.totalLabel": "gesamt",
1676
+ "relay.all": "Alle",
1677
+ "relay.noFilteredMessages": "Keine {{filter}} Nachrichten",
1678
+ "relay.messagesWillAppear": "Nachrichten erscheinen hier, wenn SMS weitergeleitet werden",
1679
+ "relay.sentLabel": "GESENDET",
1680
+ "relay.failLabel": "FEHLER",
1681
+ "relay.e2eFooter": "Nachrichteninhalt ist E2E-verschl\xFCsselt und wird nicht auf dem Ger\xE4t gespeichert",
1682
+ "relay.copied": "Kopiert!",
1683
+ "relay.na": "N/A",
1684
+ "relay.cancel": "Abbrechen",
1685
+ // Error codes
1686
+ "errors.relayNotFound": "Relay-Ger\xE4t nicht gefunden",
1687
+ "errors.relayInactive": "Relay ist nicht aktiv",
1688
+ "errors.relayOffline": "Relay-Ger\xE4t ist offline",
1689
+ "errors.pairingNotFound": "Kopplungssitzung nicht gefunden",
1690
+ "errors.pairingExpired": "Kopplungssitzung ist abgelaufen",
1691
+ "errors.pairingInvalidToken": "Ung\xFCltiges Kopplungstoken",
1692
+ "errors.authTimeout": "WebSocket-Authentifizierungszeit\xFCberschreitung",
1693
+ "errors.authFailed": "Ung\xFCltige Relay-Anmeldedaten",
1694
+ "errors.rateLimited": "Ratenbegrenzung \xFCberschritten",
1695
+ "errors.encryptionFailed": "Verschl\xFCsselungsvorgang fehlgeschlagen",
1696
+ "errors.rekeyFailed": "Schl\xFCsselrotation fehlgeschlagen",
1697
+ "errors.smsSendFailed": "SMS-Zustellung fehlgeschlagen",
1698
+ "errors.smsTimeout": "SMS-Best\xE4tigungszeit\xFCberschreitung"
1699
+ };
1700
+
1701
+ // src/i18n/locales/ja.ts
1702
+ var ja = {
1703
+ // Dashboard
1704
+ "relay.title": "ZI2 SMS\u30EA\u30EC\u30FC",
1705
+ "relay.connected": "\u63A5\u7D9A\u6E08\u307F",
1706
+ "relay.connecting": "\u63A5\u7D9A\u4E2D...",
1707
+ "relay.disconnected": "\u5207\u65AD\u6E08\u307F",
1708
+ "relay.uptime": "\u7A3C\u50CD\u6642\u9593",
1709
+ "relay.establishingConnection": "\u5B89\u5168\u306A\u63A5\u7D9A\u3092\u78BA\u7ACB\u4E2D...",
1710
+ "relay.tapReconnect": "\u518D\u63A5\u7D9A\u3092\u30BF\u30C3\u30D7\u3057\u3066\u30EA\u30C8\u30E9\u30A4",
1711
+ "relay.live": "\u30E9\u30A4\u30D6",
1712
+ "relay.e2eEncrypted": "E2E\u6697\u53F7\u5316",
1713
+ "relay.reconnect": "\u518D\u63A5\u7D9A",
1714
+ "relay.sent": "\u9001\u4FE1\u6E08\u307F",
1715
+ "relay.failed": "\u5931\u6557",
1716
+ "relay.remaining": "\u6B8B\u308A",
1717
+ "relay.successRate": "\u6210\u529F\u7387",
1718
+ "relay.thisSession": "\u4ECA\u56DE\u306E\u30BB\u30C3\u30B7\u30E7\u30F3",
1719
+ "relay.ofPerDay": "{{limit}}/\u65E5\u306E\u3046\u3061",
1720
+ "relay.total": "\u5408\u8A08",
1721
+ "relay.dailyUsage": "\u65E5\u6B21\u4F7F\u7528\u91CF",
1722
+ "relay.perMin": "{{limit}}/\u5206",
1723
+ "relay.perHr": "{{limit}}/\u6642",
1724
+ "relay.perDay": "{{limit}}/\u65E5",
1725
+ "relay.quickControls": "\u30AF\u30A4\u30C3\u30AF\u30B3\u30F3\u30C8\u30ED\u30FC\u30EB",
1726
+ "relay.forceReconnect": "\u5F37\u5236\u518D\u63A5\u7D9A",
1727
+ "relay.forceReconnectDesc": "WebSocket\u63A5\u7D9A\u3092\u518D\u78BA\u7ACB",
1728
+ "relay.pauseRelay": "\u30EA\u30EC\u30FC\u3092\u4E00\u6642\u505C\u6B62",
1729
+ "relay.pauseRelayDesc": "SMS\u53D7\u4FE1\u3092\u4E00\u6642\u7684\u306B\u505C\u6B62",
1730
+ "relay.resumeRelay": "\u30EA\u30EC\u30FC\u3092\u518D\u958B",
1731
+ "relay.resumeRelayDesc": "SMS\u53D7\u4FE1\u3092\u518D\u958B",
1732
+ "relay.sendDeviceStatus": "\u30C7\u30D0\u30A4\u30B9\u30B9\u30C6\u30FC\u30BF\u30B9\u3092\u9001\u4FE1",
1733
+ "relay.sendDeviceStatusDesc": "\u30D0\u30C3\u30C6\u30EA\u30FC\u3068\u96FB\u6CE2\u3092\u30B5\u30FC\u30D0\u30FC\u306B\u5831\u544A",
1734
+ "relay.messageLog": "\u30E1\u30C3\u30BB\u30FC\u30B8\u30ED\u30B0",
1735
+ "relay.messagesRecorded": "{{count}}\u4EF6\u306E\u30E1\u30C3\u30BB\u30FC\u30B8\u3092\u8A18\u9332",
1736
+ "relay.connectionInfo": "\u63A5\u7D9A\u60C5\u5831",
1737
+ "relay.notPaired": "\u672A\u30DA\u30A2\u30EA\u30F3\u30B0",
1738
+ "relay.noServer": "\u30B5\u30FC\u30D0\u30FC\u306A\u3057",
1739
+ "relay.recentActivity": "\u6700\u8FD1\u306E\u30A2\u30AF\u30C6\u30A3\u30D3\u30C6\u30A3",
1740
+ "relay.viewAll": "\u3059\u3079\u3066\u8868\u793A",
1741
+ "relay.noMessagesYet": "\u30E1\u30C3\u30BB\u30FC\u30B8\u306F\u307E\u3060\u3042\u308A\u307E\u305B\u3093",
1742
+ "relay.smsWillAppear": "\u30EA\u30EC\u30FC\u3055\u308C\u305FSMS\u304C\u3053\u3053\u306B\u8868\u793A\u3055\u308C\u307E\u3059",
1743
+ "relay.delivered": "\u914D\u4FE1\u6E08\u307F",
1744
+ "relay.smsPermissionRequired": "SMS\u6A29\u9650\u304C\u5FC5\u8981",
1745
+ "relay.smsPermissionDesc": "\u30E1\u30C3\u30BB\u30FC\u30B8\u3092\u9001\u4FE1\u3059\u308B\u306B\u306F\u30C7\u30D0\u30A4\u30B9\u8A2D\u5B9A\u3067SMS\u6A29\u9650\u3092\u8A31\u53EF\u3057\u3066\u304F\u3060\u3055\u3044",
1746
+ "relay.retry": "\u30EA\u30C8\u30E9\u30A4",
1747
+ // Pairing
1748
+ "relay.pairingTitle": "ZI2 SMS\u30EA\u30EC\u30FC",
1749
+ "relay.pairingInstruction": "\u753B\u9762\u306EQR\u30B3\u30FC\u30C9\u306B\u30AB\u30E1\u30E9\u3092\u5411\u3051\u3066\u30DA\u30A2\u30EA\u30F3\u30B0\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
1750
+ "relay.pairingWithServer": "\u30B5\u30FC\u30D0\u30FC\u3068\u30DA\u30A2\u30EA\u30F3\u30B0\u4E2D...",
1751
+ "relay.pairingFailed": "\u30DA\u30A2\u30EA\u30F3\u30B0\u5931\u6557",
1752
+ "relay.tryAgain": "\u518D\u8A66\u884C",
1753
+ "relay.cameraAccessDenied": "\u30AB\u30E1\u30E9\u30A2\u30AF\u30BB\u30B9\u304C\u62D2\u5426\u3055\u308C\u307E\u3057\u305F",
1754
+ // Settings
1755
+ "relay.settings": "\u8A2D\u5B9A",
1756
+ "relay.relayInformation": "\u30EA\u30EC\u30FC\u60C5\u5831",
1757
+ "relay.relayId": "\u30EA\u30EC\u30FCID",
1758
+ "relay.server": "\u30B5\u30FC\u30D0\u30FC",
1759
+ "relay.status": "\u30B9\u30C6\u30FC\u30BF\u30B9",
1760
+ "relay.online": "\u30AA\u30F3\u30E9\u30A4\u30F3",
1761
+ "relay.offline": "\u30AA\u30D5\u30E9\u30A4\u30F3",
1762
+ "relay.encryption": "\u6697\u53F7\u5316",
1763
+ "relay.sessionStatistics": "\u30BB\u30C3\u30B7\u30E7\u30F3\u7D71\u8A08",
1764
+ "relay.messagesSent": "\u9001\u4FE1\u30E1\u30C3\u30BB\u30FC\u30B8",
1765
+ "relay.messagesFailed": "\u5931\u6557\u30E1\u30C3\u30BB\u30FC\u30B8",
1766
+ "relay.totalLogged": "\u5408\u8A08\u30ED\u30B0",
1767
+ "relay.rateLimits": "\u30EC\u30FC\u30C8\u5236\u9650",
1768
+ "relay.perMinute": "\u6BCE\u5206",
1769
+ "relay.perHour": "\u6BCE\u6642",
1770
+ "relay.perDayLabel": "\u6BCE\u65E5",
1771
+ "relay.messageExpiry": "\u30E1\u30C3\u30BB\u30FC\u30B8\u6709\u52B9\u671F\u9650",
1772
+ "relay.expiryHours": "{{hours}}\u6642\u9593",
1773
+ "relay.about": "\u30D0\u30FC\u30B8\u30E7\u30F3\u60C5\u5831",
1774
+ "relay.version": "ZI2 SMS\u30EA\u30EC\u30FC v1.0.0",
1775
+ "relay.aboutDescription": "ZI2\u30B5\u30FC\u30D0\u30FC\u304B\u3089\u306ESMS\u30E1\u30C3\u30BB\u30FC\u30B8\u3092\u304A\u4F7F\u3044\u306E\u643A\u5E2F\u96FB\u8A71\u306E\u30CD\u30A4\u30C6\u30A3\u30D6SIM\u30AB\u30FC\u30C9\u3092\u901A\u3058\u3066\u30EA\u30EC\u30FC\u3057\u307E\u3059\u3002\u3059\u3079\u3066\u306E\u30E1\u30C3\u30BB\u30FC\u30B8\u306FX25519\u9375\u4EA4\u63DB\u3068AES-256-GCM\u3067\u7AEF\u672B\u9593\u6697\u53F7\u5316\u3055\u308C\u3066\u3044\u307E\u3059\u3002",
1776
+ "relay.dangerZone": "\u5371\u967A\u30BE\u30FC\u30F3",
1777
+ "relay.dangerZoneDesc": "\u3059\u3079\u3066\u306E\u30C7\u30FC\u30BF\u3092\u6D88\u53BB\u3057\u3001\u3053\u306E\u30C7\u30D0\u30A4\u30B9\u3092\u30B5\u30FC\u30D0\u30FC\u304B\u3089\u30DA\u30A2\u30EA\u30F3\u30B0\u89E3\u9664\u3057\u307E\u3059\u3002QR\u30B3\u30FC\u30C9\u3067\u306E\u518D\u30DA\u30A2\u30EA\u30F3\u30B0\u304C\u5FC5\u8981\u306B\u306A\u308A\u307E\u3059\u3002",
1778
+ "relay.clearDataUnpair": "\u30C7\u30FC\u30BF\u6D88\u53BB\uFF06\u30DA\u30A2\u30EA\u30F3\u30B0\u89E3\u9664",
1779
+ "relay.confirmClearData": "\u78BA\u8A8D - \u3059\u3079\u3066\u306E\u30C7\u30FC\u30BF\u3092\u6D88\u53BB\uFF06\u30DA\u30A2\u30EA\u30F3\u30B0\u89E3\u9664",
1780
+ // Message Log
1781
+ "relay.messageLogTitle": "\u30E1\u30C3\u30BB\u30FC\u30B8\u30ED\u30B0",
1782
+ "relay.totalLabel": "\u5408\u8A08",
1783
+ "relay.all": "\u3059\u3079\u3066",
1784
+ "relay.noFilteredMessages": "{{filter}}\u30E1\u30C3\u30BB\u30FC\u30B8\u306F\u3042\u308A\u307E\u305B\u3093",
1785
+ "relay.messagesWillAppear": "SMS\u304C\u30EA\u30EC\u30FC\u3055\u308C\u308B\u3068\u3053\u3053\u306B\u30E1\u30C3\u30BB\u30FC\u30B8\u304C\u8868\u793A\u3055\u308C\u307E\u3059",
1786
+ "relay.sentLabel": "\u9001\u4FE1",
1787
+ "relay.failLabel": "\u5931\u6557",
1788
+ "relay.e2eFooter": "\u30E1\u30C3\u30BB\u30FC\u30B8\u5185\u5BB9\u306FE2E\u6697\u53F7\u5316\u3055\u308C\u30C7\u30D0\u30A4\u30B9\u306B\u306F\u4FDD\u5B58\u3055\u308C\u307E\u305B\u3093",
1789
+ "relay.copied": "\u30B3\u30D4\u30FC\u3057\u307E\u3057\u305F\uFF01",
1790
+ "relay.na": "N/A",
1791
+ "relay.cancel": "\u30AD\u30E3\u30F3\u30BB\u30EB",
1792
+ // Error codes
1793
+ "errors.relayNotFound": "\u30EA\u30EC\u30FC\u30C7\u30D0\u30A4\u30B9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093",
1794
+ "errors.relayInactive": "\u30EA\u30EC\u30FC\u306F\u30A2\u30AF\u30C6\u30A3\u30D6\u3067\u306F\u3042\u308A\u307E\u305B\u3093",
1795
+ "errors.relayOffline": "\u30EA\u30EC\u30FC\u30C7\u30D0\u30A4\u30B9\u306F\u30AA\u30D5\u30E9\u30A4\u30F3\u3067\u3059",
1796
+ "errors.pairingNotFound": "\u30DA\u30A2\u30EA\u30F3\u30B0\u30BB\u30C3\u30B7\u30E7\u30F3\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093",
1797
+ "errors.pairingExpired": "\u30DA\u30A2\u30EA\u30F3\u30B0\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059",
1798
+ "errors.pairingInvalidToken": "\u7121\u52B9\u306A\u30DA\u30A2\u30EA\u30F3\u30B0\u30C8\u30FC\u30AF\u30F3",
1799
+ "errors.authTimeout": "WebSocket\u8A8D\u8A3C\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8",
1800
+ "errors.authFailed": "\u7121\u52B9\u306A\u30EA\u30EC\u30FC\u8A8D\u8A3C\u60C5\u5831",
1801
+ "errors.rateLimited": "\u30EC\u30FC\u30C8\u5236\u9650\u3092\u8D85\u904E",
1802
+ "errors.encryptionFailed": "\u6697\u53F7\u5316\u64CD\u4F5C\u306B\u5931\u6557",
1803
+ "errors.rekeyFailed": "\u9375\u30ED\u30FC\u30C6\u30FC\u30B7\u30E7\u30F3\u306B\u5931\u6557",
1804
+ "errors.smsSendFailed": "SMS\u914D\u4FE1\u306B\u5931\u6557",
1805
+ "errors.smsTimeout": "SMS\u78BA\u8A8D\u5FDC\u7B54\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8"
1806
+ };
1807
+
1808
+ // src/i18n/locales/ar.ts
1809
+ var ar = {
1810
+ // Dashboard
1811
+ "relay.title": "ZI2 \u0645\u064F\u0631\u062D\u0651\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644",
1812
+ "relay.connected": "\u0645\u062A\u0635\u0644",
1813
+ "relay.connecting": "\u062C\u0627\u0631\u064D \u0627\u0644\u0627\u062A\u0635\u0627\u0644...",
1814
+ "relay.disconnected": "\u063A\u064A\u0631 \u0645\u062A\u0635\u0644",
1815
+ "relay.uptime": "\u0648\u0642\u062A \u0627\u0644\u062A\u0634\u063A\u064A\u0644",
1816
+ "relay.establishingConnection": "\u062C\u0627\u0631\u064D \u0625\u0646\u0634\u0627\u0621 \u0627\u062A\u0635\u0627\u0644 \u0622\u0645\u0646...",
1817
+ "relay.tapReconnect": "\u0627\u0636\u063A\u0637 \u0639\u0644\u0649 \u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0627\u062A\u0635\u0627\u0644 \u0644\u0644\u0645\u062D\u0627\u0648\u0644\u0629 \u0645\u0631\u0629 \u0623\u062E\u0631\u0649",
1818
+ "relay.live": "\u0645\u0628\u0627\u0634\u0631",
1819
+ "relay.e2eEncrypted": "\u0645\u0634\u0641\u0631 E2E",
1820
+ "relay.reconnect": "\u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0627\u062A\u0635\u0627\u0644",
1821
+ "relay.sent": "\u0645\u064F\u0631\u0633\u0644\u0629",
1822
+ "relay.failed": "\u0641\u0627\u0634\u0644\u0629",
1823
+ "relay.remaining": "\u0645\u062A\u0628\u0642\u064A\u0629",
1824
+ "relay.successRate": "\u0645\u0639\u062F\u0644 \u0627\u0644\u0646\u062C\u0627\u062D",
1825
+ "relay.thisSession": "\u0647\u0630\u0647 \u0627\u0644\u062C\u0644\u0633\u0629",
1826
+ "relay.ofPerDay": "\u0645\u0646 {{limit}}/\u064A\u0648\u0645",
1827
+ "relay.total": "\u0627\u0644\u0625\u062C\u0645\u0627\u0644\u064A",
1828
+ "relay.dailyUsage": "\u0627\u0644\u0627\u0633\u062A\u062E\u062F\u0627\u0645 \u0627\u0644\u064A\u0648\u0645\u064A",
1829
+ "relay.perMin": "{{limit}}/\u062F\u0642\u064A\u0642\u0629",
1830
+ "relay.perHr": "{{limit}}/\u0633\u0627\u0639\u0629",
1831
+ "relay.perDay": "{{limit}}/\u064A\u0648\u0645",
1832
+ "relay.quickControls": "\u0623\u062F\u0648\u0627\u062A \u0627\u0644\u062A\u062D\u0643\u0645 \u0627\u0644\u0633\u0631\u064A\u0639",
1833
+ "relay.forceReconnect": "\u0625\u062C\u0628\u0627\u0631 \u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0627\u062A\u0635\u0627\u0644",
1834
+ "relay.forceReconnectDesc": "\u0625\u0639\u0627\u062F\u0629 \u0625\u0646\u0634\u0627\u0621 \u0627\u062A\u0635\u0627\u0644 WebSocket",
1835
+ "relay.pauseRelay": "\u0625\u064A\u0642\u0627\u0641 \u0627\u0644\u0645\u064F\u0631\u062D\u0651\u0644 \u0645\u0624\u0642\u062A\u0627\u064B",
1836
+ "relay.pauseRelayDesc": "\u0625\u064A\u0642\u0627\u0641 \u0627\u0633\u062A\u0642\u0628\u0627\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0645\u0624\u0642\u062A\u0627\u064B",
1837
+ "relay.resumeRelay": "\u0627\u0633\u062A\u0626\u0646\u0627\u0641 \u0627\u0644\u0645\u064F\u0631\u062D\u0651\u0644",
1838
+ "relay.resumeRelayDesc": "\u0627\u0633\u062A\u0626\u0646\u0627\u0641 \u0627\u0633\u062A\u0642\u0628\u0627\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644",
1839
+ "relay.sendDeviceStatus": "\u0625\u0631\u0633\u0627\u0644 \u062D\u0627\u0644\u0629 \u0627\u0644\u062C\u0647\u0627\u0632",
1840
+ "relay.sendDeviceStatusDesc": "\u0625\u0628\u0644\u0627\u063A \u0627\u0644\u0628\u0637\u0627\u0631\u064A\u0629 \u0648\u0627\u0644\u0625\u0634\u0627\u0631\u0629 \u0644\u0644\u062E\u0627\u062F\u0645",
1841
+ "relay.messageLog": "\u0633\u062C\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644",
1842
+ "relay.messagesRecorded": "{{count}} \u0631\u0633\u0627\u0644\u0629 \u0645\u0633\u062C\u0644\u0629",
1843
+ "relay.connectionInfo": "\u0645\u0639\u0644\u0648\u0645\u0627\u062A \u0627\u0644\u0627\u062A\u0635\u0627\u0644",
1844
+ "relay.notPaired": "\u063A\u064A\u0631 \u0645\u0642\u062A\u0631\u0646",
1845
+ "relay.noServer": "\u0644\u0627 \u064A\u0648\u062C\u062F \u062E\u0627\u062F\u0645",
1846
+ "relay.recentActivity": "\u0627\u0644\u0646\u0634\u0627\u0637 \u0627\u0644\u0623\u062E\u064A\u0631",
1847
+ "relay.viewAll": "\u0639\u0631\u0636 \u0627\u0644\u0643\u0644",
1848
+ "relay.noMessagesYet": "\u0644\u0627 \u062A\u0648\u062C\u062F \u0631\u0633\u0627\u0626\u0644 \u0628\u0639\u062F",
1849
+ "relay.smsWillAppear": "\u0633\u062A\u0638\u0647\u0631 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0647\u0646\u0627 \u0639\u0646\u062F \u062A\u0631\u062D\u064A\u0644\u0647\u0627",
1850
+ "relay.delivered": "\u062A\u0645 \u0627\u0644\u062A\u0633\u0644\u064A\u0645",
1851
+ "relay.smsPermissionRequired": "\u0625\u0630\u0646 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0645\u0637\u0644\u0648\u0628",
1852
+ "relay.smsPermissionDesc": "\u0627\u0645\u0646\u062D \u0625\u0630\u0646 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0641\u064A \u0625\u0639\u062F\u0627\u062F\u0627\u062A \u0627\u0644\u062C\u0647\u0627\u0632 \u0644\u0625\u0631\u0633\u0627\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644",
1853
+ "relay.retry": "\u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629",
1854
+ // Pairing
1855
+ "relay.pairingTitle": "ZI2 \u0645\u064F\u0631\u062D\u0651\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644",
1856
+ "relay.pairingInstruction": "\u0648\u062C\u0651\u0647 \u0627\u0644\u0643\u0627\u0645\u064A\u0631\u0627 \u0625\u0644\u0649 \u0631\u0645\u0632 QR \u0639\u0644\u0649 \u0634\u0627\u0634\u062A\u0643 \u0644\u0644\u0627\u0642\u062A\u0631\u0627\u0646.",
1857
+ "relay.pairingWithServer": "\u062C\u0627\u0631\u064D \u0627\u0644\u0627\u0642\u062A\u0631\u0627\u0646 \u0628\u0627\u0644\u062E\u0627\u062F\u0645...",
1858
+ "relay.pairingFailed": "\u0641\u0634\u0644 \u0627\u0644\u0627\u0642\u062A\u0631\u0627\u0646",
1859
+ "relay.tryAgain": "\u062D\u0627\u0648\u0644 \u0645\u0631\u0629 \u0623\u062E\u0631\u0649",
1860
+ "relay.cameraAccessDenied": "\u062A\u0645 \u0631\u0641\u0636 \u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u0627\u0644\u0643\u0627\u0645\u064A\u0631\u0627",
1861
+ // Settings
1862
+ "relay.settings": "\u0627\u0644\u0625\u0639\u062F\u0627\u062F\u0627\u062A",
1863
+ "relay.relayInformation": "\u0645\u0639\u0644\u0648\u0645\u0627\u062A \u0627\u0644\u0645\u064F\u0631\u062D\u0651\u0644",
1864
+ "relay.relayId": "\u0645\u0639\u0631\u0651\u0641 \u0627\u0644\u0645\u064F\u0631\u062D\u0651\u0644",
1865
+ "relay.server": "\u0627\u0644\u062E\u0627\u062F\u0645",
1866
+ "relay.status": "\u0627\u0644\u062D\u0627\u0644\u0629",
1867
+ "relay.online": "\u0645\u062A\u0635\u0644",
1868
+ "relay.offline": "\u063A\u064A\u0631 \u0645\u062A\u0635\u0644",
1869
+ "relay.encryption": "\u0627\u0644\u062A\u0634\u0641\u064A\u0631",
1870
+ "relay.sessionStatistics": "\u0625\u062D\u0635\u0627\u0626\u064A\u0627\u062A \u0627\u0644\u062C\u0644\u0633\u0629",
1871
+ "relay.messagesSent": "\u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0631\u0633\u0644\u0629",
1872
+ "relay.messagesFailed": "\u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0641\u0627\u0634\u0644\u0629",
1873
+ "relay.totalLogged": "\u0625\u062C\u0645\u0627\u0644\u064A \u0627\u0644\u0645\u0633\u062C\u0644",
1874
+ "relay.rateLimits": "\u062D\u062F\u0648\u062F \u0627\u0644\u0645\u0639\u062F\u0644",
1875
+ "relay.perMinute": "\u0641\u064A \u0627\u0644\u062F\u0642\u064A\u0642\u0629",
1876
+ "relay.perHour": "\u0641\u064A \u0627\u0644\u0633\u0627\u0639\u0629",
1877
+ "relay.perDayLabel": "\u0641\u064A \u0627\u0644\u064A\u0648\u0645",
1878
+ "relay.messageExpiry": "\u0627\u0646\u062A\u0647\u0627\u0621 \u0635\u0644\u0627\u062D\u064A\u0629 \u0627\u0644\u0631\u0633\u0627\u0626\u0644",
1879
+ "relay.expiryHours": "{{hours}} \u0633\u0627\u0639\u0629",
1880
+ "relay.about": "\u062D\u0648\u0644",
1881
+ "relay.version": "ZI2 \u0645\u064F\u0631\u062D\u0651\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 v1.0.0",
1882
+ "relay.aboutDescription": "\u064A\u0631\u062D\u0651\u0644 \u0631\u0633\u0627\u0626\u0644 SMS \u0645\u0646 \u062E\u0627\u062F\u0645 ZI2 \u0639\u0628\u0631 \u0628\u0637\u0627\u0642\u0629 SIM \u0627\u0644\u0623\u0635\u0644\u064A\u0629 \u0644\u0647\u0627\u062A\u0641\u0643. \u062C\u0645\u064A\u0639 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0645\u0634\u0641\u0631\u0629 \u0645\u0646 \u0637\u0631\u0641 \u0625\u0644\u0649 \u0637\u0631\u0641 \u0628\u0627\u0633\u062A\u062E\u062F\u0627\u0645 \u062A\u0628\u0627\u062F\u0644 \u0645\u0641\u0627\u062A\u064A\u062D X25519 \u0648 AES-256-GCM.",
1883
+ "relay.dangerZone": "\u0645\u0646\u0637\u0642\u0629 \u0627\u0644\u062E\u0637\u0631",
1884
+ "relay.dangerZoneDesc": "\u0645\u0633\u062D \u062C\u0645\u064A\u0639 \u0627\u0644\u0628\u064A\u0627\u0646\u0627\u062A \u0648\u0625\u0644\u063A\u0627\u0621 \u0627\u0642\u062A\u0631\u0627\u0646 \u0647\u0630\u0627 \u0627\u0644\u062C\u0647\u0627\u0632 \u0645\u0646 \u0627\u0644\u062E\u0627\u062F\u0645. \u0633\u064A\u062A\u0637\u0644\u0628 \u0630\u0644\u0643 \u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0627\u0642\u062A\u0631\u0627\u0646 \u0639\u0628\u0631 \u0631\u0645\u0632 QR.",
1885
+ "relay.clearDataUnpair": "\u0645\u0633\u062D \u0627\u0644\u0628\u064A\u0627\u0646\u0627\u062A \u0648\u0625\u0644\u063A\u0627\u0621 \u0627\u0644\u0627\u0642\u062A\u0631\u0627\u0646",
1886
+ "relay.confirmClearData": "\u062A\u0623\u0643\u064A\u062F - \u0645\u0633\u062D \u062C\u0645\u064A\u0639 \u0627\u0644\u0628\u064A\u0627\u0646\u0627\u062A \u0648\u0625\u0644\u063A\u0627\u0621 \u0627\u0644\u0627\u0642\u062A\u0631\u0627\u0646",
1887
+ // Message Log
1888
+ "relay.messageLogTitle": "\u0633\u062C\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644",
1889
+ "relay.totalLabel": "\u0625\u062C\u0645\u0627\u0644\u064A",
1890
+ "relay.all": "\u0627\u0644\u0643\u0644",
1891
+ "relay.noFilteredMessages": "\u0644\u0627 \u062A\u0648\u062C\u062F \u0631\u0633\u0627\u0626\u0644 {{filter}}",
1892
+ "relay.messagesWillAppear": "\u0633\u062A\u0638\u0647\u0631 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0647\u0646\u0627 \u0639\u0646\u062F \u062A\u0631\u062D\u064A\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644",
1893
+ "relay.sentLabel": "\u0645\u064F\u0631\u0633\u0644",
1894
+ "relay.failLabel": "\u0641\u0627\u0634\u0644",
1895
+ "relay.e2eFooter": "\u0645\u062D\u062A\u0648\u0649 \u0627\u0644\u0631\u0633\u0627\u0644\u0629 \u0645\u0634\u0641\u0631 E2E \u0648\u0644\u0627 \u064A\u062A\u0645 \u062A\u062E\u0632\u064A\u0646\u0647 \u0639\u0644\u0649 \u0627\u0644\u062C\u0647\u0627\u0632",
1896
+ "relay.copied": "\u062A\u0645 \u0627\u0644\u0646\u0633\u062E!",
1897
+ "relay.na": "\u063A/\u0645",
1898
+ "relay.cancel": "\u0625\u0644\u063A\u0627\u0621",
1899
+ // Error codes
1900
+ "errors.relayNotFound": "\u0644\u0645 \u064A\u062A\u0645 \u0627\u0644\u0639\u062B\u0648\u0631 \u0639\u0644\u0649 \u062C\u0647\u0627\u0632 \u0627\u0644\u0645\u064F\u0631\u062D\u0651\u0644",
1901
+ "errors.relayInactive": "\u0627\u0644\u0645\u064F\u0631\u062D\u0651\u0644 \u063A\u064A\u0631 \u0646\u0634\u0637",
1902
+ "errors.relayOffline": "\u062C\u0647\u0627\u0632 \u0627\u0644\u0645\u064F\u0631\u062D\u0651\u0644 \u063A\u064A\u0631 \u0645\u062A\u0635\u0644",
1903
+ "errors.pairingNotFound": "\u0644\u0645 \u064A\u062A\u0645 \u0627\u0644\u0639\u062B\u0648\u0631 \u0639\u0644\u0649 \u062C\u0644\u0633\u0629 \u0627\u0644\u0627\u0642\u062A\u0631\u0627\u0646",
1904
+ "errors.pairingExpired": "\u0627\u0646\u062A\u0647\u062A \u0635\u0644\u0627\u062D\u064A\u0629 \u062C\u0644\u0633\u0629 \u0627\u0644\u0627\u0642\u062A\u0631\u0627\u0646",
1905
+ "errors.pairingInvalidToken": "\u0631\u0645\u0632 \u0627\u0642\u062A\u0631\u0627\u0646 \u063A\u064A\u0631 \u0635\u0627\u0644\u062D",
1906
+ "errors.authTimeout": "\u0627\u0646\u062A\u0647\u062A \u0645\u0647\u0644\u0629 \u0645\u0635\u0627\u062F\u0642\u0629 WebSocket",
1907
+ "errors.authFailed": "\u0628\u064A\u0627\u0646\u0627\u062A \u0627\u0639\u062A\u0645\u0627\u062F \u0627\u0644\u0645\u064F\u0631\u062D\u0651\u0644 \u063A\u064A\u0631 \u0635\u0627\u0644\u062D\u0629",
1908
+ "errors.rateLimited": "\u062A\u0645 \u062A\u062C\u0627\u0648\u0632 \u062D\u062F \u0627\u0644\u0645\u0639\u062F\u0644",
1909
+ "errors.encryptionFailed": "\u0641\u0634\u0644\u062A \u0639\u0645\u0644\u064A\u0629 \u0627\u0644\u062A\u0634\u0641\u064A\u0631",
1910
+ "errors.rekeyFailed": "\u0641\u0634\u0644 \u062A\u062F\u0648\u064A\u0631 \u0627\u0644\u0645\u0641\u062A\u0627\u062D",
1911
+ "errors.smsSendFailed": "\u0641\u0634\u0644 \u062A\u0633\u0644\u064A\u0645 \u0627\u0644\u0631\u0633\u0627\u0644\u0629",
1912
+ "errors.smsTimeout": "\u0627\u0646\u062A\u0647\u062A \u0645\u0647\u0644\u0629 \u062A\u0623\u0643\u064A\u062F \u0627\u0633\u062A\u0644\u0627\u0645 \u0627\u0644\u0631\u0633\u0627\u0644\u0629"
1913
+ };
1914
+
1915
+ // src/i18n/locales/zh.ts
1916
+ var zh = {
1917
+ // Dashboard
1918
+ "relay.title": "ZI2 \u77ED\u4FE1\u4E2D\u7EE7",
1919
+ "relay.connected": "\u5DF2\u8FDE\u63A5",
1920
+ "relay.connecting": "\u8FDE\u63A5\u4E2D...",
1921
+ "relay.disconnected": "\u5DF2\u65AD\u5F00",
1922
+ "relay.uptime": "\u8FD0\u884C\u65F6\u95F4",
1923
+ "relay.establishingConnection": "\u6B63\u5728\u5EFA\u7ACB\u5B89\u5168\u8FDE\u63A5...",
1924
+ "relay.tapReconnect": "\u70B9\u51FB\u91CD\u65B0\u8FDE\u63A5\u4EE5\u91CD\u8BD5",
1925
+ "relay.live": "\u5728\u7EBF",
1926
+ "relay.e2eEncrypted": "\u7AEF\u5230\u7AEF\u52A0\u5BC6",
1927
+ "relay.reconnect": "\u91CD\u65B0\u8FDE\u63A5",
1928
+ "relay.sent": "\u5DF2\u53D1\u9001",
1929
+ "relay.failed": "\u5931\u8D25",
1930
+ "relay.remaining": "\u5269\u4F59",
1931
+ "relay.successRate": "\u6210\u529F\u7387",
1932
+ "relay.thisSession": "\u672C\u6B21\u4F1A\u8BDD",
1933
+ "relay.ofPerDay": "{{limit}}/\u5929 \u4E2D\u7684",
1934
+ "relay.total": "\u603B\u8BA1",
1935
+ "relay.dailyUsage": "\u6BCF\u65E5\u7528\u91CF",
1936
+ "relay.perMin": "{{limit}}/\u5206\u949F",
1937
+ "relay.perHr": "{{limit}}/\u5C0F\u65F6",
1938
+ "relay.perDay": "{{limit}}/\u5929",
1939
+ "relay.quickControls": "\u5FEB\u6377\u63A7\u5236",
1940
+ "relay.forceReconnect": "\u5F3A\u5236\u91CD\u8FDE",
1941
+ "relay.forceReconnectDesc": "\u91CD\u65B0\u5EFA\u7ACBWebSocket\u8FDE\u63A5",
1942
+ "relay.pauseRelay": "\u6682\u505C\u4E2D\u7EE7",
1943
+ "relay.pauseRelayDesc": "\u6682\u65F6\u505C\u6B62\u63A5\u6536\u77ED\u4FE1",
1944
+ "relay.resumeRelay": "\u6062\u590D\u4E2D\u7EE7",
1945
+ "relay.resumeRelayDesc": "\u91CD\u65B0\u5F00\u59CB\u63A5\u6536\u77ED\u4FE1",
1946
+ "relay.sendDeviceStatus": "\u53D1\u9001\u8BBE\u5907\u72B6\u6001",
1947
+ "relay.sendDeviceStatusDesc": "\u5411\u670D\u52A1\u5668\u62A5\u544A\u7535\u6C60\u548C\u4FE1\u53F7\u72B6\u6001",
1948
+ "relay.messageLog": "\u6D88\u606F\u65E5\u5FD7",
1949
+ "relay.messagesRecorded": "\u5DF2\u8BB0\u5F55 {{count}} \u6761\u6D88\u606F",
1950
+ "relay.connectionInfo": "\u8FDE\u63A5\u4FE1\u606F",
1951
+ "relay.notPaired": "\u672A\u914D\u5BF9",
1952
+ "relay.noServer": "\u65E0\u670D\u52A1\u5668",
1953
+ "relay.recentActivity": "\u6700\u8FD1\u6D3B\u52A8",
1954
+ "relay.viewAll": "\u67E5\u770B\u5168\u90E8",
1955
+ "relay.noMessagesYet": "\u6682\u65E0\u6D88\u606F",
1956
+ "relay.smsWillAppear": "\u77ED\u4FE1\u4E2D\u7EE7\u540E\u5C06\u5728\u6B64\u663E\u793A",
1957
+ "relay.delivered": "\u5DF2\u9001\u8FBE",
1958
+ "relay.smsPermissionRequired": "\u9700\u8981\u77ED\u4FE1\u6743\u9650",
1959
+ "relay.smsPermissionDesc": "\u8BF7\u5728\u8BBE\u5907\u8BBE\u7F6E\u4E2D\u6388\u4E88\u77ED\u4FE1\u6743\u9650\u4EE5\u53D1\u9001\u6D88\u606F",
1960
+ "relay.retry": "\u91CD\u8BD5",
1961
+ // Pairing
1962
+ "relay.pairingTitle": "ZI2 \u77ED\u4FE1\u4E2D\u7EE7",
1963
+ "relay.pairingInstruction": "\u5C06\u6444\u50CF\u5934\u5BF9\u51C6\u5C4F\u5E55\u4E0A\u7684\u4E8C\u7EF4\u7801\u4EE5\u914D\u5BF9\u3002",
1964
+ "relay.pairingWithServer": "\u6B63\u5728\u4E0E\u670D\u52A1\u5668\u914D\u5BF9...",
1965
+ "relay.pairingFailed": "\u914D\u5BF9\u5931\u8D25",
1966
+ "relay.tryAgain": "\u91CD\u8BD5",
1967
+ "relay.cameraAccessDenied": "\u6444\u50CF\u5934\u8BBF\u95EE\u88AB\u62D2\u7EDD",
1968
+ // Settings
1969
+ "relay.settings": "\u8BBE\u7F6E",
1970
+ "relay.relayInformation": "\u4E2D\u7EE7\u4FE1\u606F",
1971
+ "relay.relayId": "\u4E2D\u7EE7ID",
1972
+ "relay.server": "\u670D\u52A1\u5668",
1973
+ "relay.status": "\u72B6\u6001",
1974
+ "relay.online": "\u5728\u7EBF",
1975
+ "relay.offline": "\u79BB\u7EBF",
1976
+ "relay.encryption": "\u52A0\u5BC6",
1977
+ "relay.sessionStatistics": "\u4F1A\u8BDD\u7EDF\u8BA1",
1978
+ "relay.messagesSent": "\u5DF2\u53D1\u9001\u6D88\u606F",
1979
+ "relay.messagesFailed": "\u5931\u8D25\u6D88\u606F",
1980
+ "relay.totalLogged": "\u603B\u8BB0\u5F55\u6570",
1981
+ "relay.rateLimits": "\u901F\u7387\u9650\u5236",
1982
+ "relay.perMinute": "\u6BCF\u5206\u949F",
1983
+ "relay.perHour": "\u6BCF\u5C0F\u65F6",
1984
+ "relay.perDayLabel": "\u6BCF\u5929",
1985
+ "relay.messageExpiry": "\u6D88\u606F\u8FC7\u671F",
1986
+ "relay.expiryHours": "{{hours}}\u5C0F\u65F6",
1987
+ "relay.about": "\u5173\u4E8E",
1988
+ "relay.version": "ZI2 \u77ED\u4FE1\u4E2D\u7EE7 v1.0.0",
1989
+ "relay.aboutDescription": "\u901A\u8FC7\u624B\u673A\u539F\u751FSIM\u5361\u4E2D\u7EE7ZI2\u670D\u52A1\u5668\u7684\u77ED\u4FE1\u3002\u6240\u6709\u6D88\u606F\u5747\u4F7F\u7528X25519\u5BC6\u94A5\u4EA4\u6362\u548CAES-256-GCM\u8FDB\u884C\u7AEF\u5230\u7AEF\u52A0\u5BC6\u3002\u670D\u52A1\u5668\u6C38\u8FDC\u4E0D\u4F1A\u770B\u5230\u660E\u6587\u6D88\u606F\u5185\u5BB9\u3002",
1990
+ "relay.dangerZone": "\u5371\u9669\u533A\u57DF",
1991
+ "relay.dangerZoneDesc": "\u6E05\u9664\u6240\u6709\u6570\u636E\u5E76\u53D6\u6D88\u6B64\u8BBE\u5907\u4E0E\u670D\u52A1\u5668\u7684\u914D\u5BF9\u3002\u8FD9\u5C06\u9700\u8981\u901A\u8FC7\u4E8C\u7EF4\u7801\u91CD\u65B0\u914D\u5BF9\u3002",
1992
+ "relay.clearDataUnpair": "\u6E05\u9664\u6570\u636E\u5E76\u53D6\u6D88\u914D\u5BF9",
1993
+ "relay.confirmClearData": "\u786E\u8BA4 - \u6E05\u9664\u6240\u6709\u6570\u636E\u5E76\u53D6\u6D88\u914D\u5BF9",
1994
+ // Message Log
1995
+ "relay.messageLogTitle": "\u6D88\u606F\u65E5\u5FD7",
1996
+ "relay.totalLabel": "\u603B\u8BA1",
1997
+ "relay.all": "\u5168\u90E8",
1998
+ "relay.noFilteredMessages": "\u6CA1\u6709{{filter}}\u6D88\u606F",
1999
+ "relay.messagesWillAppear": "\u77ED\u4FE1\u4E2D\u7EE7\u540E\u6D88\u606F\u5C06\u5728\u6B64\u663E\u793A",
2000
+ "relay.sentLabel": "\u5DF2\u53D1\u9001",
2001
+ "relay.failLabel": "\u5931\u8D25",
2002
+ "relay.e2eFooter": "\u6D88\u606F\u5185\u5BB9\u5DF2\u7AEF\u5230\u7AEF\u52A0\u5BC6\uFF0C\u4E0D\u4F1A\u5B58\u50A8\u5728\u8BBE\u5907\u4E0A",
2003
+ "relay.copied": "\u5DF2\u590D\u5236\uFF01",
2004
+ "relay.na": "N/A",
2005
+ "relay.cancel": "\u53D6\u6D88",
2006
+ // Error codes
2007
+ "errors.relayNotFound": "\u672A\u627E\u5230\u4E2D\u7EE7\u8BBE\u5907",
2008
+ "errors.relayInactive": "\u4E2D\u7EE7\u672A\u6FC0\u6D3B",
2009
+ "errors.relayOffline": "\u4E2D\u7EE7\u8BBE\u5907\u79BB\u7EBF",
2010
+ "errors.pairingNotFound": "\u672A\u627E\u5230\u914D\u5BF9\u4F1A\u8BDD",
2011
+ "errors.pairingExpired": "\u914D\u5BF9\u4F1A\u8BDD\u5DF2\u8FC7\u671F",
2012
+ "errors.pairingInvalidToken": "\u65E0\u6548\u7684\u914D\u5BF9\u4EE4\u724C",
2013
+ "errors.authTimeout": "WebSocket\u8BA4\u8BC1\u8D85\u65F6",
2014
+ "errors.authFailed": "\u65E0\u6548\u7684\u4E2D\u7EE7\u51ED\u636E",
2015
+ "errors.rateLimited": "\u8D85\u51FA\u901F\u7387\u9650\u5236",
2016
+ "errors.encryptionFailed": "\u52A0\u5BC6\u64CD\u4F5C\u5931\u8D25",
2017
+ "errors.rekeyFailed": "\u5BC6\u94A5\u8F6E\u6362\u5931\u8D25",
2018
+ "errors.smsSendFailed": "\u77ED\u4FE1\u53D1\u9001\u5931\u8D25",
2019
+ "errors.smsTimeout": "\u77ED\u4FE1\u786E\u8BA4\u8D85\u65F6"
2020
+ };
2021
+
2022
+ // src/i18n/locales/pt.ts
2023
+ var pt = {
2024
+ // Dashboard
2025
+ "relay.title": "ZI2 Relay SMS",
2026
+ "relay.connected": "Conectado",
2027
+ "relay.connecting": "Conectando...",
2028
+ "relay.disconnected": "Desconectado",
2029
+ "relay.uptime": "Tempo ativo",
2030
+ "relay.establishingConnection": "Estabelecendo conex\xE3o segura...",
2031
+ "relay.tapReconnect": "Toque em reconectar para tentar novamente",
2032
+ "relay.live": "AO VIVO",
2033
+ "relay.e2eEncrypted": "Criptografia E2E",
2034
+ "relay.reconnect": "Reconectar",
2035
+ "relay.sent": "Enviadas",
2036
+ "relay.failed": "Falharam",
2037
+ "relay.remaining": "Restantes",
2038
+ "relay.successRate": "Taxa de sucesso",
2039
+ "relay.thisSession": "esta sess\xE3o",
2040
+ "relay.ofPerDay": "de {{limit}}/dia",
2041
+ "relay.total": "Total",
2042
+ "relay.dailyUsage": "Uso di\xE1rio",
2043
+ "relay.perMin": "{{limit}}/min",
2044
+ "relay.perHr": "{{limit}}/h",
2045
+ "relay.perDay": "{{limit}}/dia",
2046
+ "relay.quickControls": "Controles r\xE1pidos",
2047
+ "relay.forceReconnect": "For\xE7ar reconex\xE3o",
2048
+ "relay.forceReconnectDesc": "Restabelecer conex\xE3o WebSocket",
2049
+ "relay.pauseRelay": "Pausar relay",
2050
+ "relay.pauseRelayDesc": "Parar temporariamente de receber SMS",
2051
+ "relay.resumeRelay": "Retomar relay",
2052
+ "relay.resumeRelayDesc": "Voltar a receber SMS",
2053
+ "relay.sendDeviceStatus": "Enviar status do dispositivo",
2054
+ "relay.sendDeviceStatusDesc": "Reportar bateria e sinal ao servidor",
2055
+ "relay.messageLog": "Registro de mensagens",
2056
+ "relay.messagesRecorded": "{{count}} mensagens registradas",
2057
+ "relay.connectionInfo": "Informa\xE7\xF5es de conex\xE3o",
2058
+ "relay.notPaired": "N\xE3o pareado",
2059
+ "relay.noServer": "Sem servidor",
2060
+ "relay.recentActivity": "Atividade recente",
2061
+ "relay.viewAll": "Ver tudo",
2062
+ "relay.noMessagesYet": "Nenhuma mensagem ainda",
2063
+ "relay.smsWillAppear": "SMS aparecer\xE3o aqui quando forem retransmitidos",
2064
+ "relay.delivered": "Entregue",
2065
+ "relay.smsPermissionRequired": "Permiss\xE3o de SMS necess\xE1ria",
2066
+ "relay.smsPermissionDesc": "Conceda permiss\xE3o de SMS nas configura\xE7\xF5es do dispositivo para enviar mensagens",
2067
+ "relay.retry": "Tentar novamente",
2068
+ // Pairing
2069
+ "relay.pairingTitle": "ZI2 Relay SMS",
2070
+ "relay.pairingInstruction": "Aponte a c\xE2mera para o c\xF3digo QR na sua tela para parear.",
2071
+ "relay.pairingWithServer": "Pareando com o servidor...",
2072
+ "relay.pairingFailed": "Falha no pareamento",
2073
+ "relay.tryAgain": "Tentar novamente",
2074
+ "relay.cameraAccessDenied": "Acesso \xE0 c\xE2mera negado",
2075
+ // Settings
2076
+ "relay.settings": "Configura\xE7\xF5es",
2077
+ "relay.relayInformation": "Informa\xE7\xF5es do relay",
2078
+ "relay.relayId": "ID do relay",
2079
+ "relay.server": "Servidor",
2080
+ "relay.status": "Status",
2081
+ "relay.online": "Online",
2082
+ "relay.offline": "Offline",
2083
+ "relay.encryption": "Criptografia",
2084
+ "relay.sessionStatistics": "Estat\xEDsticas da sess\xE3o",
2085
+ "relay.messagesSent": "Mensagens enviadas",
2086
+ "relay.messagesFailed": "Mensagens com falha",
2087
+ "relay.totalLogged": "Total registrado",
2088
+ "relay.rateLimits": "Limites de taxa",
2089
+ "relay.perMinute": "Por minuto",
2090
+ "relay.perHour": "Por hora",
2091
+ "relay.perDayLabel": "Por dia",
2092
+ "relay.messageExpiry": "Expira\xE7\xE3o de mensagens",
2093
+ "relay.expiryHours": "{{hours}}h",
2094
+ "relay.about": "Sobre",
2095
+ "relay.version": "ZI2 Relay SMS v1.0.0",
2096
+ "relay.aboutDescription": "Retransmite mensagens SMS do seu servidor ZI2 atrav\xE9s do cart\xE3o SIM nativo do seu telefone. Todas as mensagens s\xE3o criptografadas de ponta a ponta usando troca de chaves X25519 e AES-256-GCM.",
2097
+ "relay.dangerZone": "Zona de perigo",
2098
+ "relay.dangerZoneDesc": "Limpar todos os dados e desparear este dispositivo do servidor. Isso exigir\xE1 um novo pareamento via c\xF3digo QR.",
2099
+ "relay.clearDataUnpair": "Limpar dados e desparear",
2100
+ "relay.confirmClearData": "Confirmar - Limpar todos os dados e desparear",
2101
+ // Message Log
2102
+ "relay.messageLogTitle": "Registro de mensagens",
2103
+ "relay.totalLabel": "total",
2104
+ "relay.all": "Todos",
2105
+ "relay.noFilteredMessages": "Nenhuma mensagem {{filter}}",
2106
+ "relay.messagesWillAppear": "Mensagens aparecer\xE3o aqui quando SMS forem retransmitidos",
2107
+ "relay.sentLabel": "ENVIADO",
2108
+ "relay.failLabel": "FALHA",
2109
+ "relay.e2eFooter": "O conte\xFAdo da mensagem \xE9 criptografado E2E e n\xE3o \xE9 armazenado no dispositivo",
2110
+ "relay.copied": "Copiado!",
2111
+ "relay.na": "N/A",
2112
+ "relay.cancel": "Cancelar",
2113
+ // Error codes
2114
+ "errors.relayNotFound": "Dispositivo relay n\xE3o encontrado",
2115
+ "errors.relayInactive": "O relay n\xE3o est\xE1 ativo",
2116
+ "errors.relayOffline": "Dispositivo relay est\xE1 offline",
2117
+ "errors.pairingNotFound": "Sess\xE3o de pareamento n\xE3o encontrada",
2118
+ "errors.pairingExpired": "A sess\xE3o de pareamento expirou",
2119
+ "errors.pairingInvalidToken": "Token de pareamento inv\xE1lido",
2120
+ "errors.authTimeout": "Tempo limite de autentica\xE7\xE3o WebSocket",
2121
+ "errors.authFailed": "Credenciais do relay inv\xE1lidas",
2122
+ "errors.rateLimited": "Limite de taxa excedido",
2123
+ "errors.encryptionFailed": "Opera\xE7\xE3o de criptografia falhou",
2124
+ "errors.rekeyFailed": "Rota\xE7\xE3o de chaves falhou",
2125
+ "errors.smsSendFailed": "Entrega de SMS falhou",
2126
+ "errors.smsTimeout": "Tempo limite de confirma\xE7\xE3o de SMS"
2127
+ };
2128
+
2129
+ // src/i18n/locales/ko.ts
2130
+ var ko = {
2131
+ // Dashboard
2132
+ "relay.title": "ZI2 SMS \uB9B4\uB808\uC774",
2133
+ "relay.connected": "\uC5F0\uACB0\uB428",
2134
+ "relay.connecting": "\uC5F0\uACB0 \uC911...",
2135
+ "relay.disconnected": "\uC5F0\uACB0 \uB04A\uAE40",
2136
+ "relay.uptime": "\uAC00\uB3D9 \uC2DC\uAC04",
2137
+ "relay.establishingConnection": "\uBCF4\uC548 \uC5F0\uACB0 \uC124\uC815 \uC911...",
2138
+ "relay.tapReconnect": "\uC7AC\uC5F0\uACB0\uC744 \uD0ED\uD558\uC5EC \uC7AC\uC2DC\uB3C4",
2139
+ "relay.live": "\uB77C\uC774\uBE0C",
2140
+ "relay.e2eEncrypted": "E2E \uC554\uD638\uD654",
2141
+ "relay.reconnect": "\uC7AC\uC5F0\uACB0",
2142
+ "relay.sent": "\uBC1C\uC1A1\uB428",
2143
+ "relay.failed": "\uC2E4\uD328",
2144
+ "relay.remaining": "\uB0A8\uC740 \uC218",
2145
+ "relay.successRate": "\uC131\uACF5\uB960",
2146
+ "relay.thisSession": "\uC774\uBC88 \uC138\uC158",
2147
+ "relay.ofPerDay": "{{limit}}/\uC77C \uC911",
2148
+ "relay.total": "\uD569\uACC4",
2149
+ "relay.dailyUsage": "\uC77C\uC77C \uC0AC\uC6A9\uB7C9",
2150
+ "relay.perMin": "{{limit}}/\uBD84",
2151
+ "relay.perHr": "{{limit}}/\uC2DC\uAC04",
2152
+ "relay.perDay": "{{limit}}/\uC77C",
2153
+ "relay.quickControls": "\uBE60\uB978 \uC81C\uC5B4",
2154
+ "relay.forceReconnect": "\uAC15\uC81C \uC7AC\uC5F0\uACB0",
2155
+ "relay.forceReconnectDesc": "WebSocket \uC5F0\uACB0 \uC7AC\uC124\uC815",
2156
+ "relay.pauseRelay": "\uB9B4\uB808\uC774 \uC77C\uC2DC \uC911\uC9C0",
2157
+ "relay.pauseRelayDesc": "\uC77C\uC2DC\uC801\uC73C\uB85C SMS \uC218\uC2E0 \uC911\uC9C0",
2158
+ "relay.resumeRelay": "\uB9B4\uB808\uC774 \uC7AC\uAC1C",
2159
+ "relay.resumeRelayDesc": "SMS \uC218\uC2E0 \uC7AC\uAC1C",
2160
+ "relay.sendDeviceStatus": "\uAE30\uAE30 \uC0C1\uD0DC \uC804\uC1A1",
2161
+ "relay.sendDeviceStatusDesc": "\uBC30\uD130\uB9AC \uBC0F \uC2E0\uD638\uB97C \uC11C\uBC84\uC5D0 \uBCF4\uACE0",
2162
+ "relay.messageLog": "\uBA54\uC2DC\uC9C0 \uB85C\uADF8",
2163
+ "relay.messagesRecorded": "{{count}}\uAC1C\uC758 \uBA54\uC2DC\uC9C0 \uAE30\uB85D\uB428",
2164
+ "relay.connectionInfo": "\uC5F0\uACB0 \uC815\uBCF4",
2165
+ "relay.notPaired": "\uD398\uC5B4\uB9C1 \uC548 \uB428",
2166
+ "relay.noServer": "\uC11C\uBC84 \uC5C6\uC74C",
2167
+ "relay.recentActivity": "\uCD5C\uADFC \uD65C\uB3D9",
2168
+ "relay.viewAll": "\uBAA8\uB450 \uBCF4\uAE30",
2169
+ "relay.noMessagesYet": "\uC544\uC9C1 \uBA54\uC2DC\uC9C0 \uC5C6\uC74C",
2170
+ "relay.smsWillAppear": "\uB9B4\uB808\uC774\uB41C SMS\uAC00 \uC5EC\uAE30\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4",
2171
+ "relay.delivered": "\uC804\uB2EC\uB428",
2172
+ "relay.smsPermissionRequired": "SMS \uAD8C\uD55C \uD544\uC694",
2173
+ "relay.smsPermissionDesc": "\uBA54\uC2DC\uC9C0\uB97C \uBCF4\uB0B4\uB824\uBA74 \uAE30\uAE30 \uC124\uC815\uC5D0\uC11C SMS \uAD8C\uD55C\uC744 \uD5C8\uC6A9\uD558\uC138\uC694",
2174
+ "relay.retry": "\uC7AC\uC2DC\uB3C4",
2175
+ // Pairing
2176
+ "relay.pairingTitle": "ZI2 SMS \uB9B4\uB808\uC774",
2177
+ "relay.pairingInstruction": "\uD654\uBA74\uC758 QR \uCF54\uB4DC\uC5D0 \uCE74\uBA54\uB77C\uB97C \uAC00\uB9AC\uCF1C \uD398\uC5B4\uB9C1\uD558\uC138\uC694.",
2178
+ "relay.pairingWithServer": "\uC11C\uBC84\uC640 \uD398\uC5B4\uB9C1 \uC911...",
2179
+ "relay.pairingFailed": "\uD398\uC5B4\uB9C1 \uC2E4\uD328",
2180
+ "relay.tryAgain": "\uB2E4\uC2DC \uC2DC\uB3C4",
2181
+ "relay.cameraAccessDenied": "\uCE74\uBA54\uB77C \uC561\uC138\uC2A4 \uAC70\uBD80\uB428",
2182
+ // Settings
2183
+ "relay.settings": "\uC124\uC815",
2184
+ "relay.relayInformation": "\uB9B4\uB808\uC774 \uC815\uBCF4",
2185
+ "relay.relayId": "\uB9B4\uB808\uC774 ID",
2186
+ "relay.server": "\uC11C\uBC84",
2187
+ "relay.status": "\uC0C1\uD0DC",
2188
+ "relay.online": "\uC628\uB77C\uC778",
2189
+ "relay.offline": "\uC624\uD504\uB77C\uC778",
2190
+ "relay.encryption": "\uC554\uD638\uD654",
2191
+ "relay.sessionStatistics": "\uC138\uC158 \uD1B5\uACC4",
2192
+ "relay.messagesSent": "\uBC1C\uC1A1\uB41C \uBA54\uC2DC\uC9C0",
2193
+ "relay.messagesFailed": "\uC2E4\uD328\uD55C \uBA54\uC2DC\uC9C0",
2194
+ "relay.totalLogged": "\uCD1D \uAE30\uB85D",
2195
+ "relay.rateLimits": "\uC18D\uB3C4 \uC81C\uD55C",
2196
+ "relay.perMinute": "\uBD84\uB2F9",
2197
+ "relay.perHour": "\uC2DC\uAC04\uB2F9",
2198
+ "relay.perDayLabel": "\uC77C\uB2F9",
2199
+ "relay.messageExpiry": "\uBA54\uC2DC\uC9C0 \uB9CC\uB8CC",
2200
+ "relay.expiryHours": "{{hours}}\uC2DC\uAC04",
2201
+ "relay.about": "\uC815\uBCF4",
2202
+ "relay.version": "ZI2 SMS \uB9B4\uB808\uC774 v1.0.0",
2203
+ "relay.aboutDescription": "ZI2 \uC11C\uBC84\uC758 SMS \uBA54\uC2DC\uC9C0\uB97C \uD734\uB300\uD3F0\uC758 \uAE30\uBCF8 SIM \uCE74\uB4DC\uB97C \uD1B5\uD574 \uB9B4\uB808\uC774\uD569\uB2C8\uB2E4. \uBAA8\uB4E0 \uBA54\uC2DC\uC9C0\uB294 X25519 \uD0A4 \uAD50\uD658 \uBC0F AES-256-GCM\uC73C\uB85C \uC885\uB2E8 \uAC04 \uC554\uD638\uD654\uB429\uB2C8\uB2E4.",
2204
+ "relay.dangerZone": "\uC704\uD5D8 \uAD6C\uC5ED",
2205
+ "relay.dangerZoneDesc": "\uBAA8\uB4E0 \uB370\uC774\uD130\uB97C \uC9C0\uC6B0\uACE0 \uC774 \uAE30\uAE30\uC758 \uC11C\uBC84 \uD398\uC5B4\uB9C1\uC744 \uD574\uC81C\uD569\uB2C8\uB2E4. QR \uCF54\uB4DC\uB85C \uB2E4\uC2DC \uD398\uC5B4\uB9C1\uD574\uC57C \uD569\uB2C8\uB2E4.",
2206
+ "relay.clearDataUnpair": "\uB370\uC774\uD130 \uC0AD\uC81C \uBC0F \uD398\uC5B4\uB9C1 \uD574\uC81C",
2207
+ "relay.confirmClearData": "\uD655\uC778 - \uBAA8\uB4E0 \uB370\uC774\uD130 \uC0AD\uC81C \uBC0F \uD398\uC5B4\uB9C1 \uD574\uC81C",
2208
+ // Message Log
2209
+ "relay.messageLogTitle": "\uBA54\uC2DC\uC9C0 \uB85C\uADF8",
2210
+ "relay.totalLabel": "\uD569\uACC4",
2211
+ "relay.all": "\uC804\uCCB4",
2212
+ "relay.noFilteredMessages": "{{filter}} \uBA54\uC2DC\uC9C0 \uC5C6\uC74C",
2213
+ "relay.messagesWillAppear": "SMS\uAC00 \uB9B4\uB808\uC774\uB418\uBA74 \uC5EC\uAE30\uC5D0 \uBA54\uC2DC\uC9C0\uAC00 \uD45C\uC2DC\uB429\uB2C8\uB2E4",
2214
+ "relay.sentLabel": "\uBC1C\uC1A1",
2215
+ "relay.failLabel": "\uC2E4\uD328",
2216
+ "relay.e2eFooter": "\uBA54\uC2DC\uC9C0 \uB0B4\uC6A9\uC740 E2E \uC554\uD638\uD654\uB418\uC5B4 \uAE30\uAE30\uC5D0 \uC800\uC7A5\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4",
2217
+ "relay.copied": "\uBCF5\uC0AC\uB428!",
2218
+ "relay.na": "N/A",
2219
+ "relay.cancel": "\uCDE8\uC18C",
2220
+ // Error codes
2221
+ "errors.relayNotFound": "\uB9B4\uB808\uC774 \uAE30\uAE30\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
2222
+ "errors.relayInactive": "\uB9B4\uB808\uC774\uAC00 \uD65C\uC131 \uC0C1\uD0DC\uAC00 \uC544\uB2D9\uB2C8\uB2E4",
2223
+ "errors.relayOffline": "\uB9B4\uB808\uC774 \uAE30\uAE30\uAC00 \uC624\uD504\uB77C\uC778\uC785\uB2C8\uB2E4",
2224
+ "errors.pairingNotFound": "\uD398\uC5B4\uB9C1 \uC138\uC158\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
2225
+ "errors.pairingExpired": "\uD398\uC5B4\uB9C1 \uC138\uC158\uC774 \uB9CC\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4",
2226
+ "errors.pairingInvalidToken": "\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uD398\uC5B4\uB9C1 \uD1A0\uD070",
2227
+ "errors.authTimeout": "WebSocket \uC778\uC99D \uC2DC\uAC04 \uCD08\uACFC",
2228
+ "errors.authFailed": "\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uB9B4\uB808\uC774 \uC790\uACA9 \uC99D\uBA85",
2229
+ "errors.rateLimited": "\uC18D\uB3C4 \uC81C\uD55C \uCD08\uACFC",
2230
+ "errors.encryptionFailed": "\uC554\uD638\uD654 \uC791\uC5C5 \uC2E4\uD328",
2231
+ "errors.rekeyFailed": "\uD0A4 \uB85C\uD14C\uC774\uC158 \uC2E4\uD328",
2232
+ "errors.smsSendFailed": "SMS \uC804\uB2EC \uC2E4\uD328",
2233
+ "errors.smsTimeout": "SMS \uD655\uC778 \uC2DC\uAC04 \uCD08\uACFC"
2234
+ };
2235
+
2236
+ // src/i18n/locales/it.ts
2237
+ var it = {
2238
+ // Dashboard
2239
+ "relay.title": "ZI2 Relay SMS",
2240
+ "relay.connected": "Connesso",
2241
+ "relay.connecting": "Connessione...",
2242
+ "relay.disconnected": "Disconnesso",
2243
+ "relay.uptime": "Tempo attivo",
2244
+ "relay.establishingConnection": "Stabilendo connessione sicura...",
2245
+ "relay.tapReconnect": "Tocca riconnetti per riprovare",
2246
+ "relay.live": "LIVE",
2247
+ "relay.e2eEncrypted": "Crittografia E2E",
2248
+ "relay.reconnect": "Riconnetti",
2249
+ "relay.sent": "Inviati",
2250
+ "relay.failed": "Falliti",
2251
+ "relay.remaining": "Rimanenti",
2252
+ "relay.successRate": "Tasso di successo",
2253
+ "relay.thisSession": "questa sessione",
2254
+ "relay.ofPerDay": "di {{limit}}/giorno",
2255
+ "relay.total": "Totale",
2256
+ "relay.dailyUsage": "Utilizzo giornaliero",
2257
+ "relay.perMin": "{{limit}}/min",
2258
+ "relay.perHr": "{{limit}}/ora",
2259
+ "relay.perDay": "{{limit}}/giorno",
2260
+ "relay.quickControls": "Controlli rapidi",
2261
+ "relay.forceReconnect": "Forza riconnessione",
2262
+ "relay.forceReconnectDesc": "Ristabilisci la connessione WebSocket",
2263
+ "relay.pauseRelay": "Metti in pausa il relay",
2264
+ "relay.pauseRelayDesc": "Interrompi temporaneamente la ricezione di SMS",
2265
+ "relay.resumeRelay": "Riprendi relay",
2266
+ "relay.resumeRelayDesc": "Riprendi la ricezione di SMS",
2267
+ "relay.sendDeviceStatus": "Invia stato dispositivo",
2268
+ "relay.sendDeviceStatusDesc": "Segnala batteria e segnale al server",
2269
+ "relay.messageLog": "Registro messaggi",
2270
+ "relay.messagesRecorded": "{{count}} messaggi registrati",
2271
+ "relay.connectionInfo": "Informazioni connessione",
2272
+ "relay.notPaired": "Non associato",
2273
+ "relay.noServer": "Nessun server",
2274
+ "relay.recentActivity": "Attivit\xE0 recente",
2275
+ "relay.viewAll": "Vedi tutto",
2276
+ "relay.noMessagesYet": "Nessun messaggio ancora",
2277
+ "relay.smsWillAppear": "Gli SMS appariranno qui quando vengono inoltrati",
2278
+ "relay.delivered": "Consegnato",
2279
+ "relay.smsPermissionRequired": "Permesso SMS richiesto",
2280
+ "relay.smsPermissionDesc": "Concedi il permesso SMS nelle impostazioni del dispositivo per inviare messaggi",
2281
+ "relay.retry": "Riprova",
2282
+ // Pairing
2283
+ "relay.pairingTitle": "ZI2 Relay SMS",
2284
+ "relay.pairingInstruction": "Punta la fotocamera verso il codice QR sullo schermo per associare.",
2285
+ "relay.pairingWithServer": "Associazione con il server...",
2286
+ "relay.pairingFailed": "Associazione fallita",
2287
+ "relay.tryAgain": "Riprova",
2288
+ "relay.cameraAccessDenied": "Accesso alla fotocamera negato",
2289
+ // Settings
2290
+ "relay.settings": "Impostazioni",
2291
+ "relay.relayInformation": "Informazioni relay",
2292
+ "relay.relayId": "ID relay",
2293
+ "relay.server": "Server",
2294
+ "relay.status": "Stato",
2295
+ "relay.online": "Online",
2296
+ "relay.offline": "Offline",
2297
+ "relay.encryption": "Crittografia",
2298
+ "relay.sessionStatistics": "Statistiche sessione",
2299
+ "relay.messagesSent": "Messaggi inviati",
2300
+ "relay.messagesFailed": "Messaggi falliti",
2301
+ "relay.totalLogged": "Totale registrato",
2302
+ "relay.rateLimits": "Limiti di frequenza",
2303
+ "relay.perMinute": "Al minuto",
2304
+ "relay.perHour": "All'ora",
2305
+ "relay.perDayLabel": "Al giorno",
2306
+ "relay.messageExpiry": "Scadenza messaggi",
2307
+ "relay.expiryHours": "{{hours}}h",
2308
+ "relay.about": "Informazioni",
2309
+ "relay.version": "ZI2 Relay SMS v1.0.0",
2310
+ "relay.aboutDescription": "Inoltra i messaggi SMS dal tuo server ZI2 attraverso la scheda SIM nativa del tuo telefono. Tutti i messaggi sono crittografati end-to-end con scambio chiavi X25519 e AES-256-GCM.",
2311
+ "relay.dangerZone": "Zona pericolosa",
2312
+ "relay.dangerZoneDesc": "Cancella tutti i dati e dissocia questo dispositivo dal server. Sar\xE0 necessaria una nuova associazione tramite codice QR.",
2313
+ "relay.clearDataUnpair": "Cancella dati e dissocia",
2314
+ "relay.confirmClearData": "Conferma - Cancella tutti i dati e dissocia",
2315
+ // Message Log
2316
+ "relay.messageLogTitle": "Registro messaggi",
2317
+ "relay.totalLabel": "totale",
2318
+ "relay.all": "Tutti",
2319
+ "relay.noFilteredMessages": "Nessun messaggio {{filter}}",
2320
+ "relay.messagesWillAppear": "I messaggi appariranno qui quando gli SMS vengono inoltrati",
2321
+ "relay.sentLabel": "INVIATO",
2322
+ "relay.failLabel": "FALLITO",
2323
+ "relay.e2eFooter": "Il contenuto del messaggio \xE8 crittografato E2E e non viene memorizzato sul dispositivo",
2324
+ "relay.copied": "Copiato!",
2325
+ "relay.na": "N/D",
2326
+ "relay.cancel": "Annulla",
2327
+ // Error codes
2328
+ "errors.relayNotFound": "Dispositivo relay non trovato",
2329
+ "errors.relayInactive": "Il relay non \xE8 attivo",
2330
+ "errors.relayOffline": "Il dispositivo relay \xE8 offline",
2331
+ "errors.pairingNotFound": "Sessione di associazione non trovata",
2332
+ "errors.pairingExpired": "La sessione di associazione \xE8 scaduta",
2333
+ "errors.pairingInvalidToken": "Token di associazione non valido",
2334
+ "errors.authTimeout": "Timeout autenticazione WebSocket",
2335
+ "errors.authFailed": "Credenziali relay non valide",
2336
+ "errors.rateLimited": "Limite di frequenza superato",
2337
+ "errors.encryptionFailed": "Operazione di crittografia fallita",
2338
+ "errors.rekeyFailed": "Rotazione chiavi fallita",
2339
+ "errors.smsSendFailed": "Consegna SMS fallita",
2340
+ "errors.smsTimeout": "Timeout conferma ricezione SMS"
2341
+ };
2342
+
2343
+ // src/i18n/index.ts
2344
+ var locales = { en, fr, es, de, ja, ar, zh, pt, ko, it };
2345
+ function getTranslation(locale, key) {
2346
+ const dict = locales[locale] || locales.en;
2347
+ return dict[key] || locales.en[key] || key;
2348
+ }
2349
+ function getSupportedLocales() {
2350
+ return Object.keys(locales);
2351
+ }
2352
+ function getRelayTranslations(locale) {
2353
+ return locales[locale] || locales.en;
2354
+ }
2355
+
2356
+ // src/index.ts
2357
+ function createRelay(config) {
2358
+ const logger = config.logger || new ConsoleLogger();
2359
+ const broadcast = config.broadcast || new NoopBroadcast();
2360
+ const audit = config.audit || new NoopAudit();
2361
+ const authLimiter = new AuthLimiter(config.maxAuthFailuresPerIp ?? 10);
2362
+ const enforceTls = config.enforceTls ?? true;
2363
+ const limits = { ...RELAY_LIMITS, ...config.limits };
2364
+ const events = new RelayEventEmitter();
2365
+ const queue = createQueueService({
2366
+ db: config.db,
2367
+ encryption: config.encryption,
2368
+ logger,
2369
+ events,
2370
+ limits
2371
+ });
2372
+ const pairing = createPairingService({
2373
+ db: config.db,
2374
+ encryption: config.encryption,
2375
+ logger,
2376
+ apiUrl: config.apiUrl || "http://localhost:3000",
2377
+ wsUrl: config.wsUrl || "",
2378
+ limits,
2379
+ onPairingComplete: config.onPairingComplete,
2380
+ onRelayRevoked: config.onRelayRevoked
2381
+ });
2382
+ const health = createHealthService({
2383
+ db: config.db,
2384
+ logger,
2385
+ limits
2386
+ });
2387
+ const handleWs = createWebSocketHandler({
2388
+ db: config.db,
2389
+ encryption: config.encryption,
2390
+ broadcast,
2391
+ logger,
2392
+ events,
2393
+ queue,
2394
+ limits
2395
+ });
2396
+ const sdk = {
2397
+ // Pairing
2398
+ async initiatePairing(orgId, userId) {
2399
+ return pairing.initiatePairing(orgId, userId);
2400
+ },
2401
+ async completePairing(input) {
2402
+ return pairing.completePairing(
2403
+ input.pairingId,
2404
+ input.pairingToken,
2405
+ input.devicePublicKey,
2406
+ input.deviceName,
2407
+ input.platform,
2408
+ input.phoneNumber
2409
+ );
2410
+ },
2411
+ // Relay management
2412
+ async listRelays(orgId) {
2413
+ const relays = await config.db.findRelays(orgId, "revoked");
2414
+ const todayUTC = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2415
+ return relays.map(({ dailyResetAt, ...relay }) => ({
2416
+ ...relay,
2417
+ dailySmsSent: dailyResetAt && dailyResetAt.toISOString().slice(0, 10) === todayUTC ? relay.dailySmsSent : 0,
2418
+ isOnline: isRelayOnline(relay.id)
2419
+ }));
2420
+ },
2421
+ async getRelay(id, orgId) {
2422
+ const relay = await config.db.findRelay(id, orgId);
2423
+ if (!relay) return null;
2424
+ const todayUTC = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2425
+ const isToday = relay.dailyResetAt && relay.dailyResetAt.toISOString().slice(0, 10) === todayUTC;
2426
+ return {
2427
+ ...relay,
2428
+ dailySmsSent: isToday ? relay.dailySmsSent : 0,
2429
+ isOnline: isRelayOnline(relay.id)
2430
+ };
2431
+ },
2432
+ async updateRelay(id, orgId, data) {
2433
+ await config.db.updateRelayByOrg(id, orgId, data);
2434
+ },
2435
+ async revokeRelay(id, orgId) {
2436
+ return pairing.revokeRelay(id, orgId);
2437
+ },
2438
+ async rotateKeys(id, orgId) {
2439
+ return pairing.rotateKeys(id, orgId);
2440
+ },
2441
+ // Messaging
2442
+ async sendSMS(options) {
2443
+ return queue.enqueueAndWait(options);
2444
+ },
2445
+ async getMessages(relayId, orgId, limit = 50, offset = 0) {
2446
+ const relay = await config.db.findRelay(relayId, orgId);
2447
+ if (!relay) throw new Error("Relay not found");
2448
+ const { messages, total } = await config.db.findMessages(relayId, limit, offset);
2449
+ return { messages, pagination: { total, limit, offset } };
2450
+ },
2451
+ // Connection status
2452
+ isRelayOnline(relayId) {
2453
+ return isRelayOnline(relayId);
2454
+ },
2455
+ getRelaySocket(relayId) {
2456
+ return getRelaySocket(relayId);
2457
+ },
2458
+ // WebSocket handler
2459
+ handleWebSocket(socket, request) {
2460
+ handleWs(socket, request);
2461
+ },
2462
+ // Health
2463
+ async runHealthCheck() {
2464
+ return health.runHealthCheck();
2465
+ },
2466
+ startHealthService() {
2467
+ health.startHealthService();
2468
+ },
2469
+ stopHealthService() {
2470
+ health.stopHealthService();
2471
+ },
2472
+ // Provider factory
2473
+ createProvider(relayId, orgId) {
2474
+ const provider = new PhoneRelayProvider(queue);
2475
+ provider.initialize({ relayId, orgId });
2476
+ return provider;
2477
+ },
2478
+ createFallbackProvider(fallbackConfig) {
2479
+ return new FallbackProvider(fallbackConfig);
2480
+ },
2481
+ // Event subscriptions
2482
+ onRelayOnline(callback) {
2483
+ return events.on("relay:online", callback);
2484
+ },
2485
+ onRelayOffline(callback) {
2486
+ return events.on("relay:offline", callback);
2487
+ },
2488
+ onMessageDelivered(callback) {
2489
+ return events.on("message:delivered", callback);
2490
+ },
2491
+ onMessageFailed(callback) {
2492
+ return events.on("message:failed", callback);
2493
+ }
2494
+ };
2495
+ return sdk;
2496
+ }
2497
+ export {
2498
+ AesGcmEncryption,
2499
+ AuthLimiter,
2500
+ ConsoleAudit,
2501
+ ConsoleLogger,
2502
+ FallbackProvider,
2503
+ MemoryAdapter,
2504
+ NoopAudit,
2505
+ NoopBroadcast,
2506
+ PhoneRelayProvider,
2507
+ RELAY_DELIVERY_STATUS,
2508
+ RELAY_ERRORS,
2509
+ RELAY_ERRORS_EXTENDED,
2510
+ RELAY_LIMITS,
2511
+ RELAY_MESSAGE_STATUS,
2512
+ RELAY_MESSAGE_TYPES,
2513
+ RELAY_PAIRING_STATUS,
2514
+ RELAY_PLATFORMS,
2515
+ RELAY_STATUS,
2516
+ RedisBroker,
2517
+ RelayError,
2518
+ completePairingSchema,
2519
+ createRelay,
2520
+ createRelayError,
2521
+ decryptE2E,
2522
+ deriveSharedKey,
2523
+ encryptE2E,
2524
+ generateSecureToken,
2525
+ generateX25519KeyPair,
2526
+ getRelaySocket,
2527
+ getRelayTranslations,
2528
+ getSupportedLocales,
2529
+ getTranslation,
2530
+ hashToken,
2531
+ isRelayOnline,
2532
+ relayMessagesQuerySchema,
2533
+ testRelaySmsSchema,
2534
+ timingSafeCompare,
2535
+ updatePhoneRelaySchema,
2536
+ withRedaction
2537
+ };