@el-j/google-sheet-translations 3.0.0-beta.8 → 3.0.0-beta.9

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/esm/index.js CHANGED
@@ -3299,11 +3299,13 @@ function parsePadUrl(url) {
3299
3299
  const match = urlObj.hash.match(/#\/(\d+)\/([^/]+)\/([^/]+)\/([^/]+)(\/p\/)?/);
3300
3300
  if (!match) throw new Error(`Unsupported CryptPad URL format: "${url}". Expected "#/<version>/<app>/<mode>/<seed>[/p/]"`);
3301
3301
  const [, versionStr, app, modeRaw, seed, passwordFlag] = match;
3302
+ const version = parseInt(versionStr, 10);
3303
+ if (version === 3) throw new Error(`CryptPad URL "${url}" is a version 3 "safe link" (hidden hash) which omits the encryption key to protect it in browser address bars. Please use the original pad share URL (version 2 format: "https://.../#/2/${app}/${modeRaw}/<secret>/..."), which can be copied via CryptPad's "Share" -> "Link" menu.`);
3302
3304
  const mode = modeRaw === "edit" ? "edit" : "view";
3303
3305
  const isPasswordProtected = Boolean(passwordFlag);
3304
3306
  return {
3305
3307
  origin: urlObj.origin,
3306
- version: parseInt(versionStr, 10),
3308
+ version,
3307
3309
  app,
3308
3310
  mode,
3309
3311
  seed,
@@ -3312,27 +3314,36 @@ function parsePadUrl(url) {
3312
3314
  };
3313
3315
  }
3314
3316
  /**
3315
- * Derives the Netflux channel ID (hex string) and symmetric encryption key (32 bytes)
3316
- * from a CryptPad pad seed and optional password according to CryptPad's createEditCryptor2 / createViewCryptor2 protocol.
3317
+ * Derives the Netflux channel ID (hex string), symmetric encryption key (32 bytes),
3318
+ * and Ed25519 signing keypair from a CryptPad pad seed and optional password
3319
+ * according to CryptPad's createEditCryptor2 / createViewCryptor2 protocol.
3317
3320
  */
3318
3321
  function deriveCryptPadKeys(seedStr, password) {
3319
3322
  const seed = b64Decode(seedStr);
3320
3323
  if (!password) {
3321
- const seed2 = crypto.createHash("sha512").update(seed).digest().subarray(32, 64);
3324
+ const hash = crypto.createHash("sha512").update(seed).digest();
3325
+ const signKp = nacl.sign.keyPair.fromSeed(hash.subarray(0, 32));
3326
+ const seed2 = hash.subarray(32, 64);
3322
3327
  const hash2 = crypto.createHash("sha512").update(seed2).digest();
3323
3328
  return {
3324
3329
  channelHex: hash2.subarray(0, 16).toString("hex"),
3325
- cryptKey: new Uint8Array(hash2.subarray(16, 48))
3330
+ cryptKey: new Uint8Array(hash2.subarray(16, 48)),
3331
+ signKey: signKp.secretKey,
3332
+ validateKey: signKp.publicKey
3326
3333
  };
3327
3334
  }
3328
3335
  const pwBytes = decodeUTF8(password);
3329
3336
  const superSeed1 = Buffer.concat([Buffer.from(pwBytes), Buffer.from(seed)]);
3330
- const seed2 = crypto.createHash("sha512").update(superSeed1).digest().subarray(32, 64);
3337
+ const hash1 = crypto.createHash("sha512").update(superSeed1).digest();
3338
+ const signKp = nacl.sign.keyPair.fromSeed(hash1.subarray(0, 32));
3339
+ const seed2 = hash1.subarray(32, 64);
3331
3340
  const superSeed2 = Buffer.concat([Buffer.from(pwBytes), Buffer.from(seed2)]);
3332
3341
  const hash2 = crypto.createHash("sha512").update(superSeed2).digest();
3333
3342
  return {
3334
3343
  channelHex: hash2.subarray(0, 16).toString("hex"),
3335
- cryptKey: new Uint8Array(hash2.subarray(16, 48))
3344
+ cryptKey: new Uint8Array(hash2.subarray(16, 48)),
3345
+ signKey: signKp.secretKey,
3346
+ validateKey: signKp.publicKey
3336
3347
  };
3337
3348
  }
3338
3349
  /**
@@ -3363,13 +3374,16 @@ function decryptCryptPadPayload(payload, cryptKey) {
3363
3374
  }
3364
3375
  /**
3365
3376
  * Encrypts a plaintext message payload using TweetNaCl secretbox (XSalsa20-Poly1305).
3366
- * Formats output as `base64(nonce)|base64(ciphertext)`.
3377
+ * If signKey is provided, signs the `nonce|ciphertext` string using TweetNaCl Ed25519
3378
+ * matching OnlyOffice / CryptPad real-time channel requirements.
3367
3379
  */
3368
- function encryptCryptPadPayload(plaintext, cryptKey) {
3380
+ function encryptCryptPadPayload(plaintext, cryptKey, signKey) {
3369
3381
  const nonce = nacl.randomBytes(24);
3370
3382
  const msgBytes = decodeUTF8(plaintext);
3371
3383
  const cipher = nacl.secretbox(msgBytes, nonce, cryptKey);
3372
- return `${b64Encode(nonce)}|${b64Encode(cipher)}`;
3384
+ const encStr = `${b64Encode(nonce)}|${b64Encode(cipher)}`;
3385
+ if (signKey) return b64Encode(nacl.sign(decodeUTF8(encStr), signKey));
3386
+ return encStr;
3373
3387
  }
3374
3388
  //#endregion
3375
3389
  //#region src/providers/cryptpad/netflux.ts
@@ -3462,11 +3476,7 @@ function fetchChannelHistory(wsUrl, channelHex, cryptKey, options = {}) {
3462
3476
  seq++,
3463
3477
  "MSG",
3464
3478
  peerId,
3465
- JSON.stringify([
3466
- "GET_HISTORY",
3467
- channelHex,
3468
- {}
3469
- ])
3479
+ JSON.stringify(["GET_FULL_HISTORY", channelHex])
3470
3480
  ]));
3471
3481
  resetQuietTimer();
3472
3482
  }
@@ -3476,7 +3486,11 @@ function fetchChannelHistory(wsUrl, channelHex, cryptKey, options = {}) {
3476
3486
  if (!encStr && typeof msg[3] === "string" && msg[3].length > 50) encStr = msg[3];
3477
3487
  try {
3478
3488
  const parsed = JSON.parse(encStr);
3479
- if (Array.isArray(parsed) && parsed[2] === "MSG" && typeof parsed[4] === "string") encStr = parsed[4];
3489
+ if (Array.isArray(parsed) && parsed[0] === "FULL_HISTORY" && Array.isArray(parsed[1])) encStr = parsed[1][4];
3490
+ else if (Array.isArray(parsed) && parsed[0] === "FULL_HISTORY_END") {
3491
+ finish(decryptedMessages);
3492
+ return;
3493
+ } else if (Array.isArray(parsed) && parsed[2] === "MSG" && typeof parsed[4] === "string") encStr = parsed[4];
3480
3494
  } catch {}
3481
3495
  if (typeof encStr === "string" && encStr.length > 30) {
3482
3496
  const decrypted = decryptCryptPadPayload(encStr, cryptKey);
@@ -3490,7 +3504,9 @@ function fetchChannelHistory(wsUrl, channelHex, cryptKey, options = {}) {
3490
3504
  };
3491
3505
  ws.onerror = (err) => {
3492
3506
  cleanup();
3493
- reject(/* @__PURE__ */ new Error(`CryptPad WebSocket error: ${String(err)}`));
3507
+ const errObj = err;
3508
+ const detail = errObj?.message || errObj?.error?.message || (errObj?.error ? String(errObj.error) : String(err));
3509
+ reject(/* @__PURE__ */ new Error(`CryptPad WebSocket error: ${detail}`));
3494
3510
  };
3495
3511
  });
3496
3512
  }
@@ -3553,7 +3569,7 @@ function broadcastChannelMessage(wsUrl, channelHex, cryptKey, message, options =
3553
3569
  const [, peerId, cmd] = msg;
3554
3570
  if (cmd === "JOIN" && typeof peerId === "string" && !sent) {
3555
3571
  sent = true;
3556
- const encrypted = encryptCryptPadPayload(message, cryptKey);
3572
+ const encrypted = encryptCryptPadPayload(message, cryptKey, options.signKey);
3557
3573
  ws.send(JSON.stringify([
3558
3574
  seq++,
3559
3575
  "MSG",
@@ -3567,7 +3583,9 @@ function broadcastChannelMessage(wsUrl, channelHex, cryptKey, message, options =
3567
3583
  } catch {}
3568
3584
  };
3569
3585
  ws.onerror = (err) => {
3570
- rejectOnce(/* @__PURE__ */ new Error(`CryptPad WebSocket broadcast error: ${String(err)}`));
3586
+ const errObj = err;
3587
+ const detail = errObj?.message || errObj?.error?.message || (errObj?.error ? String(errObj.error) : String(err));
3588
+ rejectOnce(/* @__PURE__ */ new Error(`CryptPad WebSocket broadcast error: ${detail}`));
3571
3589
  };
3572
3590
  });
3573
3591
  }
@@ -3583,6 +3601,12 @@ function colIndexToLetter(colIndex) {
3583
3601
  }
3584
3602
  return letter;
3585
3603
  }
3604
+ /** Converts column letter to 0-based index: 'A' -> 0, 'Z' -> 25, 'AA' -> 26. */
3605
+ function letterToColIndex(letter) {
3606
+ let index = 0;
3607
+ for (let i = 0; i < letter.length; i++) index = index * 26 + (letter.charCodeAt(i) - 64);
3608
+ return index - 1;
3609
+ }
3586
3610
  /**
3587
3611
  * Parses cell coordinates from a reference like "A1", "Sheet1!B2", or "$C$5".
3588
3612
  */
@@ -3601,14 +3625,26 @@ function parseCellRef(ref) {
3601
3625
  function extractOnlyOfficeChannelId(metadataMessages) {
3602
3626
  for (const raw of metadataMessages) try {
3603
3627
  const parsed = JSON.parse(raw);
3628
+ if (parsed?.content?.channel && typeof parsed.content.channel === "string") return parsed.content.channel;
3604
3629
  if (Array.isArray(parsed) && Array.isArray(parsed[1])) {
3605
- for (const edit of parsed[1]) if (Array.isArray(edit) && typeof edit[2] === "string") try {
3606
- const inner = JSON.parse(edit[2]);
3607
- if (inner?.content?.channel && typeof inner.content.channel === "string") return inner.content.channel;
3608
- } catch {}
3630
+ const candidateContainers = [parsed[1], Array.isArray(parsed[1][0]) ? parsed[1][0] : null].filter(Boolean);
3631
+ for (const container of candidateContainers) for (const item of container) if (Array.isArray(item)) {
3632
+ for (const elem of item) if (typeof elem === "string" && elem.includes("channel")) try {
3633
+ const inner = JSON.parse(elem);
3634
+ if (inner?.content?.channel && typeof inner.content.channel === "string") return inner.content.channel;
3635
+ } catch {}
3636
+ }
3609
3637
  }
3610
- if (parsed?.content?.channel && typeof parsed.content.channel === "string") return parsed.content.channel;
3611
- } catch {}
3638
+ if (typeof raw === "string") {
3639
+ const match = raw.match(/\\?"channel\\?"\s*:\s*\\?"([a-f0-9]{32})\\?"/i);
3640
+ if (match) return match[1];
3641
+ }
3642
+ } catch {
3643
+ if (typeof raw === "string") {
3644
+ const match = raw.match(/\\?"channel\\?"\s*:\s*\\?"([a-f0-9]{32})\\?"/i);
3645
+ if (match) return match[1];
3646
+ }
3647
+ }
3612
3648
  return null;
3613
3649
  }
3614
3650
  /**
@@ -3666,12 +3702,20 @@ function parseOnlyOfficeChanges(rtMessages) {
3666
3702
  break;
3667
3703
  }
3668
3704
  }
3669
- if (buf.length >= 30 && i >= 40) try {
3705
+ if (buf.length >= 31 && buf.readUInt32BE(4) === 19468289 && buf[14] === 1) try {
3706
+ const c1 = buf.readUInt32LE(15);
3707
+ const r1 = buf.readUInt32LE(19);
3708
+ if (r1 < 1e5 && c1 < 200) {
3709
+ const cellRef = `${colIndexToLetter(c1)}${r1 + 1}`;
3710
+ if (str.trim().length > 0 && !str.includes("!")) cells[cellRef] = str;
3711
+ }
3712
+ } catch {}
3713
+ else if (buf.length >= 30 && i >= 40 && buf.readUInt32BE(4) === 0) try {
3670
3714
  const c1 = buf.readUInt32LE(14);
3671
3715
  const r1 = buf.readUInt32LE(18);
3672
3716
  if (r1 < 1e5 && c1 < 200) {
3673
3717
  const cellRef = `${colIndexToLetter(c1)}${r1 + 1}`;
3674
- if (!cells[cellRef] && str.trim().length > 0 && !str.includes("!")) cells[cellRef] = str;
3718
+ if (str.trim().length > 0 && !str.includes("!")) cells[cellRef] = str;
3675
3719
  }
3676
3720
  } catch {}
3677
3721
  }
@@ -3740,11 +3784,70 @@ function convertCellsToMultiSheetRows(cells, defaultSheet = "Sheet1") {
3740
3784
  return result;
3741
3785
  }
3742
3786
  /**
3743
- * Encodes a single cell update record into OnlyOffice binary format (0x08 prefix + UTF-16LE string).
3787
+ * Encodes a single cell update record into OnlyOffice binary format:
3788
+ * - Native AscCH.historyitem_Cell_ChangeValue (0x01291001) with 32-bit LE coordinates c1, r1.
3789
+ * - Explicit UTF-16LE cell reference tags for backward-compatibility and multi-sheet routing.
3744
3790
  */
3745
- function encodeOnlyOfficeCellRecord(cellRef, value) {
3746
- const refBuf = Buffer.from(cellRef, "utf16le");
3791
+ function encodeOnlyOfficeCellRecord(cellRef, value, sheetId = 6) {
3792
+ let c1 = 0;
3793
+ let r1 = 0;
3794
+ const parsed = parseCellRef(cellRef);
3795
+ if (parsed) {
3796
+ c1 = letterToColIndex(parsed.col);
3797
+ r1 = Math.max(0, parsed.row - 1);
3798
+ }
3747
3799
  const valBuf = Buffer.from(value, "utf16le");
3800
+ const L = valBuf.length;
3801
+ const tail = Buffer.from([
3802
+ 1,
3803
+ 0,
3804
+ 2,
3805
+ 0,
3806
+ 3,
3807
+ 2,
3808
+ 1,
3809
+ 2,
3810
+ 0,
3811
+ 3,
3812
+ 1
3813
+ ]);
3814
+ const body = Buffer.alloc(60 + L + tail.length);
3815
+ body.writeUInt32BE(19468289, 0);
3816
+ body.writeUInt32LE(2, 4);
3817
+ body[8] = 48 + sheetId % 10;
3818
+ body[9] = 0;
3819
+ body[10] = 1;
3820
+ body.writeUInt32LE(c1, 11);
3821
+ body.writeUInt32LE(r1, 15);
3822
+ body.writeUInt32LE(c1, 19);
3823
+ body.writeUInt32LE(r1, 23);
3824
+ body[27] = 0;
3825
+ body.writeUInt32LE(39 + L, 28);
3826
+ body[32] = 0;
3827
+ body[33] = 2;
3828
+ body[34] = r1 & 255;
3829
+ body[35] = 1;
3830
+ body[36] = 2;
3831
+ body[37] = c1 & 255;
3832
+ body[38] = 2;
3833
+ body[39] = 9;
3834
+ body[40] = 3;
3835
+ body.writeUInt32LE(26 + L, 41);
3836
+ body[45] = 0;
3837
+ body[46] = 0;
3838
+ body[47] = 1;
3839
+ body[48] = 9;
3840
+ body[49] = 1;
3841
+ body.writeUInt32LE(13 + L, 50);
3842
+ body[54] = 0;
3843
+ body[55] = 8;
3844
+ body.writeUInt32LE(L, 56);
3845
+ valBuf.copy(body, 60);
3846
+ tail.copy(body, 60 + L);
3847
+ const header = Buffer.alloc(4);
3848
+ header.writeUInt32LE(body.length, 0);
3849
+ const nativeBuf = Buffer.concat([header, body]);
3850
+ const refBuf = Buffer.from(cellRef, "utf16le");
3748
3851
  const refHeader = Buffer.alloc(5);
3749
3852
  refHeader[0] = 8;
3750
3853
  refHeader.writeUInt32LE(refBuf.length, 1);
@@ -3752,6 +3855,7 @@ function encodeOnlyOfficeCellRecord(cellRef, value) {
3752
3855
  valHeader[0] = 8;
3753
3856
  valHeader.writeUInt32LE(valBuf.length, 1);
3754
3857
  return Buffer.concat([
3858
+ nativeBuf,
3755
3859
  refHeader,
3756
3860
  refBuf,
3757
3861
  valHeader,
@@ -3762,12 +3866,19 @@ function encodeOnlyOfficeCellRecord(cellRef, value) {
3762
3866
  * Formats a list of cell updates into an OnlyOffice change transaction JSON string.
3763
3867
  */
3764
3868
  function buildOnlyOfficeChangePayload(updates) {
3869
+ const txOpen = Buffer.from("0a0000000129000000ff00000000", "hex");
3765
3870
  const records = updates.map((u) => {
3766
3871
  const colStr = typeof u.col === "number" ? colIndexToLetter(u.col) : u.col.toUpperCase();
3767
3872
  return encodeOnlyOfficeCellRecord(`${u.sheet && u.sheet.trim().length > 0 ? `${u.sheet.trim()}!` : ""}${colStr}${u.row}`, u.value);
3768
3873
  });
3769
- const changeEntry = `asc_1;${Buffer.concat(records).toString("base64")}`;
3770
- return JSON.stringify({ changes: [{ change: changeEntry }] });
3874
+ const changeEntry = `asc_1;${Buffer.concat([txOpen, ...records]).toString("base64")}`;
3875
+ return JSON.stringify({
3876
+ type: "saveChanges",
3877
+ changes: [{ change: changeEntry }],
3878
+ startSaveChanges: true,
3879
+ endSaveChanges: true,
3880
+ isExcel: true
3881
+ });
3771
3882
  }
3772
3883
  //#endregion
3773
3884
  //#region src/providers/cryptpad/client.ts
@@ -3829,12 +3940,26 @@ var CryptPadClient = class {
3829
3940
  const wsUrl = await this.getWebsocketUrl(signal);
3830
3941
  const { channelHex, cryptKey } = this.getKeys();
3831
3942
  const newRtChannel = crypto.randomBytes(16).toString("hex");
3832
- const innerJson = JSON.stringify({ content: { channel: newRtChannel } });
3833
- await broadcastChannelMessage(wsUrl, channelHex, cryptKey, JSON.stringify([1, [[
3834
- 0,
3835
- 0,
3836
- innerJson
3837
- ]]]), {
3943
+ const innerJson = JSON.stringify({
3944
+ content: {
3945
+ hashes: {},
3946
+ ids: {},
3947
+ mediasSources: {},
3948
+ originalVersion: 9,
3949
+ version: 9,
3950
+ channel: newRtChannel
3951
+ },
3952
+ metadata: { type: "oo" }
3953
+ });
3954
+ await broadcastChannelMessage(wsUrl, channelHex, cryptKey, JSON.stringify([
3955
+ 2,
3956
+ [[[
3957
+ 0,
3958
+ 0,
3959
+ innerJson
3960
+ ]], "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],
3961
+ "a4b411975be1d48a91f0ebfcd967add000f69564b2dc5ee644b87bf2bbfd786f"
3962
+ ]), {
3838
3963
  timeoutMs: this.timeoutMs,
3839
3964
  signal
3840
3965
  });
@@ -3903,10 +4028,12 @@ var CryptPadClient = class {
3903
4028
  *
3904
4029
  * @param updates - Array of cell updates containing coordinates and text values.
3905
4030
  * @param signal - Optional AbortSignal to cancel the broadcast.
4031
+ * @param existingRtChannel - Optional pre-fetched RT channel ID to avoid refetching sheet data.
3906
4032
  */
3907
- async sendCellUpdates(updates, signal) {
4033
+ async sendCellUpdates(updates, signal, existingRtChannel) {
3908
4034
  if (updates.length === 0) return;
3909
- let rtChannel = (await this.fetchSheetData(signal)).metadata.rtChannelId;
4035
+ let rtChannel = existingRtChannel;
4036
+ if (!rtChannel) rtChannel = (await this.fetchSheetData(signal)).metadata.rtChannelId;
3910
4037
  if (!rtChannel) {
3911
4038
  this.onProgress?.("No OnlyOffice RT channel found. Initializing headlessly (no browser required)...");
3912
4039
  rtChannel = await this.initializeRtChannel(signal);
@@ -3914,11 +4041,12 @@ var CryptPadClient = class {
3914
4041
  await new Promise((r) => setTimeout(r, 800));
3915
4042
  }
3916
4043
  const wsUrl = await this.getWebsocketUrl(signal);
3917
- const { cryptKey } = this.getKeys();
4044
+ const { cryptKey, signKey } = this.getKeys();
3918
4045
  const payload = buildOnlyOfficeChangePayload(updates);
3919
4046
  await broadcastChannelMessage(wsUrl, rtChannel, cryptKey, payload, {
3920
4047
  timeoutMs: this.timeoutMs,
3921
- signal
4048
+ signal,
4049
+ signKey
3922
4050
  });
3923
4051
  }
3924
4052
  /**
@@ -3931,7 +4059,8 @@ var CryptPadClient = class {
3931
4059
  */
3932
4060
  async writeSheetRows(sheetName, rows, options = {}) {
3933
4061
  if (rows.length === 0) return 0;
3934
- const existingRows = (await this.fetchSheetData(options.signal)).sheets[sheetName]?.rows ?? [];
4062
+ const data = await this.fetchSheetData(options.signal);
4063
+ const existingRows = data.sheets[sheetName]?.rows ?? [];
3935
4064
  let keyColName = "var";
3936
4065
  const firstExisting = existingRows[0];
3937
4066
  const firstIncoming = rows[0];
@@ -3980,7 +4109,7 @@ var CryptPadClient = class {
3980
4109
  }
3981
4110
  }
3982
4111
  }
3983
- await this.sendCellUpdates(updates, options.signal);
4112
+ await this.sendCellUpdates(updates, options.signal, data.metadata.rtChannelId);
3984
4113
  return updates.length;
3985
4114
  }
3986
4115
  };
@@ -4113,6 +4242,7 @@ function createCryptPadSheetOutputProvider(options = {}) {
4113
4242
  const count = await client.writeSheetRows(sheetName, rows, { override: options.override ?? false });
4114
4243
  updatedSheets.push(sheetName);
4115
4244
  totalUpdatedCells += count;
4245
+ await new Promise((r) => setTimeout(r, 400));
4116
4246
  }
4117
4247
  return {
4118
4248
  wroteFiles: [url],