@el-j/google-sheet-translations 3.0.0-beta.4 → 3.0.0-beta.5
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 +501 -16
- package/dist/index.js +501 -16
- package/dist/providers/cryptpad/assetProvider.d.ts +5 -4
- package/dist/providers/cryptpad/assetProvider.d.ts.map +1 -1
- package/dist/providers/cryptpad/client.d.ts +21 -4
- package/dist/providers/cryptpad/client.d.ts.map +1 -1
- package/dist/providers/cryptpad/crypto.d.ts +5 -0
- package/dist/providers/cryptpad/crypto.d.ts.map +1 -1
- package/dist/providers/cryptpad/driveCatalogProvider.d.ts +23 -0
- package/dist/providers/cryptpad/driveCatalogProvider.d.ts.map +1 -0
- package/dist/providers/cryptpad/driveClient.d.ts +42 -0
- package/dist/providers/cryptpad/driveClient.d.ts.map +1 -0
- package/dist/providers/cryptpad/index.d.ts +13 -5
- package/dist/providers/cryptpad/index.d.ts.map +1 -1
- package/dist/providers/cryptpad/netflux.d.ts +10 -0
- package/dist/providers/cryptpad/netflux.d.ts.map +1 -1
- package/dist/providers/cryptpad/sheetOutputProvider.d.ts +34 -0
- package/dist/providers/cryptpad/sheetOutputProvider.d.ts.map +1 -0
- package/dist/providers/cryptpad/sheetParser.d.ts +27 -0
- package/dist/providers/cryptpad/sheetParser.d.ts.map +1 -1
- package/dist/providers/cryptpad/sheetProvider.d.ts.map +1 -1
- package/dist/providers/cryptpad/sheetSyncProvider.d.ts +29 -0
- package/dist/providers/cryptpad/sheetSyncProvider.d.ts.map +1 -0
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/runtime.d.ts.map +1 -1
- package/dist/setup/cryptpadCli.d.ts +2 -0
- package/dist/setup/cryptpadCli.d.ts.map +1 -0
- package/dist/utils/localeFilter.d.ts.map +1 -1
- package/dist-cli/cryptpad.mjs +1755 -0
- package/dist-cli/run-provider.mjs +501 -16
- package/package.json +21 -8
package/dist/esm/index.js
CHANGED
|
@@ -265,6 +265,10 @@ const COMMON_LOCALE_PATTERNS = [
|
|
|
265
265
|
/^[a-z]{2}-[a-z]{2}-[a-z]+$/
|
|
266
266
|
];
|
|
267
267
|
const NON_LOCALE_KEYWORDS = [
|
|
268
|
+
"var",
|
|
269
|
+
"vars",
|
|
270
|
+
"variable",
|
|
271
|
+
"variables",
|
|
268
272
|
"key",
|
|
269
273
|
"keys",
|
|
270
274
|
"id",
|
|
@@ -3269,6 +3273,10 @@ function b64Decode(str) {
|
|
|
3269
3273
|
if (pad) s += "=".repeat(4 - pad);
|
|
3270
3274
|
return new Uint8Array(Buffer.from(s, "base64"));
|
|
3271
3275
|
}
|
|
3276
|
+
/** Encodes bytes to standard base64 string. */
|
|
3277
|
+
function b64Encode(bytes) {
|
|
3278
|
+
return Buffer.from(bytes).toString("base64");
|
|
3279
|
+
}
|
|
3272
3280
|
/** Decodes UTF-8 string to Uint8Array. */
|
|
3273
3281
|
function decodeUTF8(str) {
|
|
3274
3282
|
return new Uint8Array(Buffer.from(str, "utf8"));
|
|
@@ -3353,6 +3361,16 @@ function decryptCryptPadPayload(payload, cryptKey) {
|
|
|
3353
3361
|
}
|
|
3354
3362
|
return null;
|
|
3355
3363
|
}
|
|
3364
|
+
/**
|
|
3365
|
+
* Encrypts a plaintext message payload using TweetNaCl secretbox (XSalsa20-Poly1305).
|
|
3366
|
+
* Formats output as `base64(nonce)|base64(ciphertext)`.
|
|
3367
|
+
*/
|
|
3368
|
+
function encryptCryptPadPayload(plaintext, cryptKey) {
|
|
3369
|
+
const nonce = nacl.randomBytes(24);
|
|
3370
|
+
const msgBytes = decodeUTF8(plaintext);
|
|
3371
|
+
const cipher = nacl.secretbox(msgBytes, nonce, cryptKey);
|
|
3372
|
+
return `${b64Encode(nonce)}|${b64Encode(cipher)}`;
|
|
3373
|
+
}
|
|
3356
3374
|
//#endregion
|
|
3357
3375
|
//#region src/providers/cryptpad/netflux.ts
|
|
3358
3376
|
/**
|
|
@@ -3461,6 +3479,69 @@ function fetchChannelHistory(wsUrl, channelHex, cryptKey, options = {}) {
|
|
|
3461
3479
|
};
|
|
3462
3480
|
});
|
|
3463
3481
|
}
|
|
3482
|
+
/**
|
|
3483
|
+
* Connects to the CryptPad Netflux WebSocket server, joins a channel,
|
|
3484
|
+
* encrypts the given message with TweetNaCl, and broadcasts it to all peers
|
|
3485
|
+
* in the channel (including historyKeeper).
|
|
3486
|
+
*/
|
|
3487
|
+
function broadcastChannelMessage(wsUrl, channelHex, cryptKey, message, options = {}) {
|
|
3488
|
+
const { timeoutMs = 8e3, signal } = options;
|
|
3489
|
+
return new Promise((resolve, reject) => {
|
|
3490
|
+
if (signal?.aborted) return reject(/* @__PURE__ */ new Error("Operation aborted"));
|
|
3491
|
+
const ws = new WebSocket(wsUrl);
|
|
3492
|
+
let seq = 1;
|
|
3493
|
+
let timeout = null;
|
|
3494
|
+
let sent = false;
|
|
3495
|
+
const cleanup = () => {
|
|
3496
|
+
if (timeout) clearTimeout(timeout);
|
|
3497
|
+
try {
|
|
3498
|
+
ws.close();
|
|
3499
|
+
} catch {}
|
|
3500
|
+
};
|
|
3501
|
+
timeout = setTimeout(() => {
|
|
3502
|
+
cleanup();
|
|
3503
|
+
if (sent) resolve();
|
|
3504
|
+
else reject(/* @__PURE__ */ new Error(`Timeout after ${timeoutMs}ms broadcasting message to CryptPad channel "${channelHex}".`));
|
|
3505
|
+
}, timeoutMs);
|
|
3506
|
+
if (signal) signal.addEventListener("abort", () => {
|
|
3507
|
+
cleanup();
|
|
3508
|
+
reject(/* @__PURE__ */ new Error("Operation aborted"));
|
|
3509
|
+
});
|
|
3510
|
+
ws.onopen = () => {
|
|
3511
|
+
ws.send(JSON.stringify([
|
|
3512
|
+
seq++,
|
|
3513
|
+
"JOIN",
|
|
3514
|
+
channelHex
|
|
3515
|
+
]));
|
|
3516
|
+
};
|
|
3517
|
+
ws.onmessage = (event) => {
|
|
3518
|
+
try {
|
|
3519
|
+
const raw = typeof event.data === "string" ? event.data : event.data.toString();
|
|
3520
|
+
const msg = JSON.parse(raw);
|
|
3521
|
+
if (!Array.isArray(msg)) return;
|
|
3522
|
+
const [, peerId, cmd] = msg;
|
|
3523
|
+
if (cmd === "JOIN" && typeof peerId === "string" && !sent) {
|
|
3524
|
+
sent = true;
|
|
3525
|
+
const encrypted = encryptCryptPadPayload(message, cryptKey);
|
|
3526
|
+
ws.send(JSON.stringify([
|
|
3527
|
+
seq++,
|
|
3528
|
+
"MSG",
|
|
3529
|
+
channelHex,
|
|
3530
|
+
encrypted
|
|
3531
|
+
]));
|
|
3532
|
+
setTimeout(() => {
|
|
3533
|
+
cleanup();
|
|
3534
|
+
resolve();
|
|
3535
|
+
}, 350);
|
|
3536
|
+
}
|
|
3537
|
+
} catch {}
|
|
3538
|
+
};
|
|
3539
|
+
ws.onerror = (err) => {
|
|
3540
|
+
cleanup();
|
|
3541
|
+
reject(/* @__PURE__ */ new Error(`CryptPad WebSocket broadcast error: ${String(err)}`));
|
|
3542
|
+
};
|
|
3543
|
+
});
|
|
3544
|
+
}
|
|
3464
3545
|
//#endregion
|
|
3465
3546
|
//#region src/providers/cryptpad/sheetParser.ts
|
|
3466
3547
|
/** Converts column index (0-based) to letter: 0 -> 'A', 25 -> 'Z', 26 -> 'AA'. */
|
|
@@ -3525,9 +3606,9 @@ function parseOnlyOfficeChanges(rtMessages) {
|
|
|
3525
3606
|
const strLen = buf.readUInt32LE(i + 1);
|
|
3526
3607
|
if (strLen > 0 && strLen < 4e3 && i + 5 + strLen <= buf.length) {
|
|
3527
3608
|
const str = buf.subarray(i + 5, i + 5 + strLen).toString("utf16le");
|
|
3528
|
-
const cellMatch = str.match(/^(?:
|
|
3609
|
+
const cellMatch = str.match(/^(?:([^!]+)!)?([A-Z]+)(\d+)$/);
|
|
3529
3610
|
if (cellMatch) {
|
|
3530
|
-
const cellRef = `${cellMatch[1]}${cellMatch[2]}`;
|
|
3611
|
+
const cellRef = `${cellMatch[1] ? `${cellMatch[1]}!` : ""}${cellMatch[2]}${cellMatch[3]}`;
|
|
3531
3612
|
const searchStart = i + 5 + strLen;
|
|
3532
3613
|
for (let j = searchStart; j < Math.min(searchStart + 30, buf.length - 5); j++) if (buf[j] === 8) {
|
|
3533
3614
|
const nextLen = buf.readUInt32LE(j + 1);
|
|
@@ -3577,10 +3658,67 @@ function convertCellsToSheetRows(cells) {
|
|
|
3577
3658
|
const rowCells = rowMap.get(rowIdx);
|
|
3578
3659
|
const sheetRow = {};
|
|
3579
3660
|
for (const [col, headerName] of colToHeaderName.entries()) sheetRow[headerName] = rowCells.get(col) ?? "";
|
|
3661
|
+
if (sheetRow.var !== void 0 && sheetRow.key === void 0) sheetRow.key = sheetRow.var;
|
|
3580
3662
|
if (Object.values(sheetRow).some((v) => typeof v === "string" && v.trim().length > 0)) resultRows.push(sheetRow);
|
|
3581
3663
|
}
|
|
3582
3664
|
return resultRows;
|
|
3583
3665
|
}
|
|
3666
|
+
/**
|
|
3667
|
+
* Groups a flat grid of cell coordinates by sheet tab name.
|
|
3668
|
+
* Cells without an explicit sheet prefix are assigned to `defaultSheet` (defaults to 'Sheet1').
|
|
3669
|
+
*/
|
|
3670
|
+
function groupCellsBySheet(cells, defaultSheet = "Sheet1") {
|
|
3671
|
+
const result = {};
|
|
3672
|
+
for (const [cellRef, val] of Object.entries(cells)) {
|
|
3673
|
+
const parsed = parseCellRef(cellRef);
|
|
3674
|
+
if (!parsed) continue;
|
|
3675
|
+
const sheetName = parsed.sheet && parsed.sheet.trim().length > 0 ? parsed.sheet.trim() : defaultSheet;
|
|
3676
|
+
if (!result[sheetName]) result[sheetName] = {};
|
|
3677
|
+
const flatRef = `${parsed.col}${parsed.row}`;
|
|
3678
|
+
result[sheetName][flatRef] = val;
|
|
3679
|
+
}
|
|
3680
|
+
if (Object.keys(result).length === 0 && Object.keys(cells).length > 0) result[defaultSheet] = { ...cells };
|
|
3681
|
+
return result;
|
|
3682
|
+
}
|
|
3683
|
+
/**
|
|
3684
|
+
* Converts a grid of cell references into a dictionary of SheetRow arrays keyed by sheet tab name.
|
|
3685
|
+
*/
|
|
3686
|
+
function convertCellsToMultiSheetRows(cells, defaultSheet = "Sheet1") {
|
|
3687
|
+
const grouped = groupCellsBySheet(cells, defaultSheet);
|
|
3688
|
+
const result = {};
|
|
3689
|
+
for (const [sheetName, sheetGrid] of Object.entries(grouped)) result[sheetName] = convertCellsToSheetRows(sheetGrid);
|
|
3690
|
+
return result;
|
|
3691
|
+
}
|
|
3692
|
+
/**
|
|
3693
|
+
* Encodes a single cell update record into OnlyOffice binary format (0x08 prefix + UTF-16LE string).
|
|
3694
|
+
*/
|
|
3695
|
+
function encodeOnlyOfficeCellRecord(cellRef, value) {
|
|
3696
|
+
const refBuf = Buffer.from(cellRef, "utf16le");
|
|
3697
|
+
const valBuf = Buffer.from(value, "utf16le");
|
|
3698
|
+
const refHeader = Buffer.alloc(5);
|
|
3699
|
+
refHeader[0] = 8;
|
|
3700
|
+
refHeader.writeUInt32LE(refBuf.length, 1);
|
|
3701
|
+
const valHeader = Buffer.alloc(5);
|
|
3702
|
+
valHeader[0] = 8;
|
|
3703
|
+
valHeader.writeUInt32LE(valBuf.length, 1);
|
|
3704
|
+
return Buffer.concat([
|
|
3705
|
+
refHeader,
|
|
3706
|
+
refBuf,
|
|
3707
|
+
valHeader,
|
|
3708
|
+
valBuf
|
|
3709
|
+
]);
|
|
3710
|
+
}
|
|
3711
|
+
/**
|
|
3712
|
+
* Formats a list of cell updates into an OnlyOffice change transaction JSON string.
|
|
3713
|
+
*/
|
|
3714
|
+
function buildOnlyOfficeChangePayload(updates) {
|
|
3715
|
+
const records = updates.map((u) => {
|
|
3716
|
+
const colStr = typeof u.col === "number" ? colIndexToLetter(u.col) : u.col.toUpperCase();
|
|
3717
|
+
return encodeOnlyOfficeCellRecord(`${u.sheet && u.sheet.trim().length > 0 ? `${u.sheet.trim()}!` : ""}${colStr}${u.row}`, u.value);
|
|
3718
|
+
});
|
|
3719
|
+
const changeEntry = `asc_1;${Buffer.concat(records).toString("base64")}`;
|
|
3720
|
+
return JSON.stringify({ changes: [{ change: changeEntry }] });
|
|
3721
|
+
}
|
|
3584
3722
|
//#endregion
|
|
3585
3723
|
//#region src/providers/cryptpad/client.ts
|
|
3586
3724
|
/**
|
|
@@ -3607,8 +3745,8 @@ var CryptPadClient = class {
|
|
|
3607
3745
|
return resolveCryptPadWebsocketUrl(this.parsedUrl.origin, signal);
|
|
3608
3746
|
}
|
|
3609
3747
|
/**
|
|
3610
|
-
* Fetches the complete sheet data, including raw cell coordinate mappings
|
|
3611
|
-
* and structured rows formatted for translation ingestion.
|
|
3748
|
+
* Fetches the complete sheet data, including raw cell coordinate mappings,
|
|
3749
|
+
* multi-sheet tabs, and structured rows formatted for translation ingestion.
|
|
3612
3750
|
*/
|
|
3613
3751
|
async fetchSheetData(signal) {
|
|
3614
3752
|
const wsUrl = await this.getWebsocketUrl(signal);
|
|
@@ -3622,11 +3760,22 @@ var CryptPadClient = class {
|
|
|
3622
3760
|
timeoutMs: this.timeoutMs,
|
|
3623
3761
|
signal
|
|
3624
3762
|
}));
|
|
3625
|
-
const
|
|
3763
|
+
const multiSheets = convertCellsToMultiSheetRows(cells);
|
|
3764
|
+
const sheetNames = Object.keys(multiSheets);
|
|
3765
|
+
const defaultSheet = multiSheets["Sheet1"] ? "Sheet1" : sheetNames[0];
|
|
3766
|
+
const defaultRows = defaultSheet && multiSheets[defaultSheet] ? multiSheets[defaultSheet] : convertCellsToSheetRows(cells);
|
|
3767
|
+
const groupedCells = groupCellsBySheet(cells);
|
|
3768
|
+
const sheetsResult = {};
|
|
3769
|
+
for (const name of sheetNames) sheetsResult[name] = {
|
|
3770
|
+
cells: groupedCells[name] ?? {},
|
|
3771
|
+
rows: multiSheets[name] ?? []
|
|
3772
|
+
};
|
|
3626
3773
|
return {
|
|
3627
3774
|
url: this.parsedUrl.cleanUrl,
|
|
3628
3775
|
cells,
|
|
3629
|
-
rows,
|
|
3776
|
+
rows: defaultRows,
|
|
3777
|
+
sheets: sheetsResult,
|
|
3778
|
+
sheetNames,
|
|
3630
3779
|
metadata: {
|
|
3631
3780
|
app: this.parsedUrl.app,
|
|
3632
3781
|
mode: this.parsedUrl.mode,
|
|
@@ -3637,9 +3786,83 @@ var CryptPadClient = class {
|
|
|
3637
3786
|
}
|
|
3638
3787
|
/**
|
|
3639
3788
|
* Directly fetches tabular translation rows `[ { key: '...', en: '...', de: '...' } ]`.
|
|
3789
|
+
* If `sheetName` is provided, returns rows for that specific tab.
|
|
3790
|
+
*/
|
|
3791
|
+
async fetchSheetRows(sheetName, signal) {
|
|
3792
|
+
const result = await this.fetchSheetData(signal);
|
|
3793
|
+
if (sheetName && result.sheets[sheetName]) return result.sheets[sheetName].rows;
|
|
3794
|
+
return result.rows;
|
|
3795
|
+
}
|
|
3796
|
+
/**
|
|
3797
|
+
* Broadcasts cell updates to the OnlyOffice real-time collaboration channel.
|
|
3798
|
+
*/
|
|
3799
|
+
async sendCellUpdates(updates, signal) {
|
|
3800
|
+
if (updates.length === 0) return;
|
|
3801
|
+
const rtChannel = (await this.fetchSheetData(signal)).metadata.rtChannelId;
|
|
3802
|
+
if (!rtChannel) throw new Error(`CryptPad sheet "${this.parsedUrl.cleanUrl}" does not have an active OnlyOffice RT channel.`);
|
|
3803
|
+
const wsUrl = await this.getWebsocketUrl(signal);
|
|
3804
|
+
const { cryptKey } = this.getKeys();
|
|
3805
|
+
await broadcastChannelMessage(wsUrl, rtChannel, cryptKey, buildOnlyOfficeChangePayload(updates), {
|
|
3806
|
+
timeoutMs: this.timeoutMs,
|
|
3807
|
+
signal
|
|
3808
|
+
});
|
|
3809
|
+
}
|
|
3810
|
+
/**
|
|
3811
|
+
* Updates or appends rows in a target sheet tab, creating new cells as needed.
|
|
3640
3812
|
*/
|
|
3641
|
-
async
|
|
3642
|
-
|
|
3813
|
+
async writeSheetRows(sheetName, rows, options = {}) {
|
|
3814
|
+
if (rows.length === 0) return 0;
|
|
3815
|
+
const existingRows = (await this.fetchSheetData(options.signal)).sheets[sheetName]?.rows ?? [];
|
|
3816
|
+
let keyColName = "var";
|
|
3817
|
+
const firstExisting = existingRows[0];
|
|
3818
|
+
const firstIncoming = rows[0];
|
|
3819
|
+
if (firstExisting) {
|
|
3820
|
+
if ("var" in firstExisting) keyColName = "var";
|
|
3821
|
+
else if ("key" in firstExisting) keyColName = "key";
|
|
3822
|
+
} else if (firstIncoming) {
|
|
3823
|
+
if ("var" in firstIncoming) keyColName = "var";
|
|
3824
|
+
else if ("key" in firstIncoming) keyColName = "key";
|
|
3825
|
+
}
|
|
3826
|
+
const colNamesSet = /* @__PURE__ */ new Set([keyColName]);
|
|
3827
|
+
for (const r of existingRows) for (const k of Object.keys(r)) if (k !== "key" && k !== "var") colNamesSet.add(k);
|
|
3828
|
+
for (const r of rows) for (const k of Object.keys(r)) if (k !== "key" && k !== "var") colNamesSet.add(k);
|
|
3829
|
+
const colNames = Array.from(colNamesSet);
|
|
3830
|
+
const updates = [];
|
|
3831
|
+
colNames.forEach((name, idx) => {
|
|
3832
|
+
updates.push({
|
|
3833
|
+
sheet: sheetName,
|
|
3834
|
+
col: colIndexToLetter(idx),
|
|
3835
|
+
row: 1,
|
|
3836
|
+
value: name
|
|
3837
|
+
});
|
|
3838
|
+
});
|
|
3839
|
+
const keyToRowIdx = /* @__PURE__ */ new Map();
|
|
3840
|
+
existingRows.forEach((r, idx) => {
|
|
3841
|
+
const rowKey = r.var ?? r.key ?? r[keyColName];
|
|
3842
|
+
if (rowKey) keyToRowIdx.set(rowKey, idx + 2);
|
|
3843
|
+
});
|
|
3844
|
+
let nextAvailableRow = existingRows.length + 2;
|
|
3845
|
+
for (const row of rows) {
|
|
3846
|
+
const rowKey = row.var ?? row.key ?? row[keyColName];
|
|
3847
|
+
if (!rowKey) continue;
|
|
3848
|
+
const targetRow = keyToRowIdx.get(rowKey) ?? nextAvailableRow++;
|
|
3849
|
+
keyToRowIdx.set(rowKey, targetRow);
|
|
3850
|
+
for (const [colName, val] of Object.entries(row)) {
|
|
3851
|
+
const mappedColName = colName === "key" || colName === "var" ? keyColName : colName;
|
|
3852
|
+
const colIdx = colNames.indexOf(mappedColName);
|
|
3853
|
+
if (colIdx >= 0 && val !== void 0) {
|
|
3854
|
+
const existingVal = existingRows[targetRow - 2]?.[mappedColName];
|
|
3855
|
+
if (options.override || !existingVal || existingVal.trim().length === 0) updates.push({
|
|
3856
|
+
sheet: sheetName,
|
|
3857
|
+
col: colIndexToLetter(colIdx),
|
|
3858
|
+
row: targetRow,
|
|
3859
|
+
value: String(val)
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
await this.sendCellUpdates(updates, options.signal);
|
|
3865
|
+
return updates.length;
|
|
3643
3866
|
}
|
|
3644
3867
|
};
|
|
3645
3868
|
//#endregion
|
|
@@ -3673,7 +3896,7 @@ function createCryptPadSheetInputProvider(options) {
|
|
|
3673
3896
|
const requested = new Set((request.tableNames ?? []).filter(Boolean));
|
|
3674
3897
|
const selectedSources = requested.size === 0 ? sources : sources.filter((source) => requested.has(source.tableName));
|
|
3675
3898
|
return {
|
|
3676
|
-
tables: await Promise.all(selectedSources.map(async (source) => {
|
|
3899
|
+
tables: (await Promise.all(selectedSources.map(async (source) => {
|
|
3677
3900
|
const url = source.url ?? options.url ?? process.env.CRYPTPAD_URL;
|
|
3678
3901
|
if (!url) throw new Error(`CryptPad sheet source "${source.tableName}" does not define a URL.`);
|
|
3679
3902
|
const sheetData = await new CryptPadClient({
|
|
@@ -3681,7 +3904,22 @@ function createCryptPadSheetInputProvider(options) {
|
|
|
3681
3904
|
password: source.password ?? options.password ?? process.env.CRYPTPAD_PASSWORD,
|
|
3682
3905
|
timeoutMs: options.timeoutMs
|
|
3683
3906
|
}).fetchSheetData(request.signal);
|
|
3684
|
-
|
|
3907
|
+
const sourceTables = [];
|
|
3908
|
+
if (Array.isArray(sheetData.sheetNames) && sheetData.sheetNames.length > 1) {
|
|
3909
|
+
for (const tabName of sheetData.sheetNames) if (requested.size === 0 || requested.has(tabName) || requested.has(source.tableName)) sourceTables.push({
|
|
3910
|
+
tableId: `${source.tableId ?? url}#${tabName}`,
|
|
3911
|
+
tableName: tabName,
|
|
3912
|
+
rows: sheetData.sheets[tabName]?.rows ?? [],
|
|
3913
|
+
sourcePath: url,
|
|
3914
|
+
metadata: {
|
|
3915
|
+
provider: "cryptpad-sheet",
|
|
3916
|
+
channelId: sheetData.metadata.channelId,
|
|
3917
|
+
rtChannelId: sheetData.metadata.rtChannelId,
|
|
3918
|
+
sheetTab: tabName
|
|
3919
|
+
}
|
|
3920
|
+
});
|
|
3921
|
+
}
|
|
3922
|
+
if (sourceTables.length === 0) sourceTables.push({
|
|
3685
3923
|
tableId: source.tableId ?? url,
|
|
3686
3924
|
tableName: source.tableName,
|
|
3687
3925
|
rows: sheetData.rows,
|
|
@@ -3692,8 +3930,9 @@ function createCryptPadSheetInputProvider(options) {
|
|
|
3692
3930
|
rtChannelId: sheetData.metadata.rtChannelId,
|
|
3693
3931
|
cellCount: Object.keys(sheetData.cells).length
|
|
3694
3932
|
}
|
|
3695
|
-
};
|
|
3696
|
-
|
|
3933
|
+
});
|
|
3934
|
+
return sourceTables;
|
|
3935
|
+
}))).flat(),
|
|
3697
3936
|
metadata: {
|
|
3698
3937
|
provider: "cryptpad-sheet",
|
|
3699
3938
|
sourceCount: selectedSources.length
|
|
@@ -3703,6 +3942,72 @@ function createCryptPadSheetInputProvider(options) {
|
|
|
3703
3942
|
};
|
|
3704
3943
|
}
|
|
3705
3944
|
//#endregion
|
|
3945
|
+
//#region src/providers/cryptpad/sheetOutputProvider.ts
|
|
3946
|
+
const CRYPTPAD_SHEET_OUTPUT_CAPABILITIES = createCapabilitySet({ writeTables: true });
|
|
3947
|
+
/**
|
|
3948
|
+
* Converts nested TranslationData `[locale][sheet][key] = value` into
|
|
3949
|
+
* tabular rows `Record<sheetName, SheetRow[]>`.
|
|
3950
|
+
*/
|
|
3951
|
+
function convertTranslationsToSheetRows(translations, localeMapping = {}, keyColumnName = "key") {
|
|
3952
|
+
const reverseMapping = {};
|
|
3953
|
+
for (const [header, norm] of Object.entries(localeMapping)) reverseMapping[norm] = header;
|
|
3954
|
+
const sheetRowsMap = {};
|
|
3955
|
+
for (const [locale, sheets] of Object.entries(translations)) {
|
|
3956
|
+
const colHeader = reverseMapping[locale] ?? locale;
|
|
3957
|
+
for (const [sheetName, keys] of Object.entries(sheets)) {
|
|
3958
|
+
if (!sheetRowsMap[sheetName]) sheetRowsMap[sheetName] = /* @__PURE__ */ new Map();
|
|
3959
|
+
for (const [key, value] of Object.entries(keys)) {
|
|
3960
|
+
if (!sheetRowsMap[sheetName].has(key)) sheetRowsMap[sheetName].set(key, { [keyColumnName]: key });
|
|
3961
|
+
sheetRowsMap[sheetName].get(key)[colHeader] = String(value);
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
const result = {};
|
|
3966
|
+
for (const [sheetName, keyMap] of Object.entries(sheetRowsMap)) result[sheetName] = Array.from(keyMap.values());
|
|
3967
|
+
return result;
|
|
3968
|
+
}
|
|
3969
|
+
/**
|
|
3970
|
+
* Creates a {@link TranslationOutputProvider} that writes translation data directly
|
|
3971
|
+
* into password-protected or public CryptPad spreadsheets (multi-tab OnlyOffice workbooks)
|
|
3972
|
+
* using end-to-end encrypted Netflux WebSockets.
|
|
3973
|
+
*/
|
|
3974
|
+
function createCryptPadSheetOutputProvider(options = {}) {
|
|
3975
|
+
const url = options.url ?? process.env.CRYPTPAD_URL;
|
|
3976
|
+
if (!url) throw new Error("CryptPad Sheet output provider requires a \"url\" option or CRYPTPAD_URL environment variable.");
|
|
3977
|
+
const password = options.password ?? process.env.CRYPTPAD_PASSWORD;
|
|
3978
|
+
return {
|
|
3979
|
+
kind: "output",
|
|
3980
|
+
providerId: options.providerId ?? "cryptpad-sheet",
|
|
3981
|
+
displayName: options.displayName ?? "CryptPad Sheet Output (E2EE)",
|
|
3982
|
+
capabilities: CRYPTPAD_SHEET_OUTPUT_CAPABILITIES,
|
|
3983
|
+
async writeTranslations(payload) {
|
|
3984
|
+
const client = new CryptPadClient({
|
|
3985
|
+
url,
|
|
3986
|
+
password,
|
|
3987
|
+
timeoutMs: options.timeoutMs
|
|
3988
|
+
});
|
|
3989
|
+
const effectiveMapping = options.localeMapping ?? payload.localeMapping ?? {};
|
|
3990
|
+
const sheetRowsMap = convertTranslationsToSheetRows(payload.translations, effectiveMapping, options.keyColumnName ?? "key");
|
|
3991
|
+
const updatedSheets = [];
|
|
3992
|
+
let totalUpdatedCells = 0;
|
|
3993
|
+
for (const [sheetName, rows] of Object.entries(sheetRowsMap)) {
|
|
3994
|
+
const count = await client.writeSheetRows(sheetName, rows, { override: options.override ?? false });
|
|
3995
|
+
updatedSheets.push(sheetName);
|
|
3996
|
+
totalUpdatedCells += count;
|
|
3997
|
+
}
|
|
3998
|
+
return {
|
|
3999
|
+
wroteFiles: [url],
|
|
4000
|
+
metadata: {
|
|
4001
|
+
provider: "cryptpad-sheet",
|
|
4002
|
+
url,
|
|
4003
|
+
updatedSheets,
|
|
4004
|
+
totalUpdatedCells
|
|
4005
|
+
}
|
|
4006
|
+
};
|
|
4007
|
+
}
|
|
4008
|
+
};
|
|
4009
|
+
}
|
|
4010
|
+
//#endregion
|
|
3706
4011
|
//#region src/providers/syncEngine.ts
|
|
3707
4012
|
function pathKey(locale, sheet, key) {
|
|
3708
4013
|
return `${locale}::${sheet}::${key}`;
|
|
@@ -3849,6 +4154,148 @@ function resolveSyncPlan(input, policy) {
|
|
|
3849
4154
|
};
|
|
3850
4155
|
}
|
|
3851
4156
|
//#endregion
|
|
4157
|
+
//#region src/providers/cryptpad/sheetSyncProvider.ts
|
|
4158
|
+
const CRYPTPAD_SHEET_SYNC_CAPABILITIES = createCapabilitySet({
|
|
4159
|
+
syncBack: true,
|
|
4160
|
+
writeTables: true
|
|
4161
|
+
});
|
|
4162
|
+
/**
|
|
4163
|
+
* Creates a {@link TranslationSyncProvider} that reconciles local translation changes
|
|
4164
|
+
* against a live CryptPad spreadsheet (multi-tab OnlyOffice workbook) using three-way
|
|
4165
|
+
* diffing and conflict policies, pushing only the resolved diffs back over Netflux.
|
|
4166
|
+
*/
|
|
4167
|
+
function createCryptPadSheetSyncProvider(options = {}) {
|
|
4168
|
+
const url = options.url ?? process.env.CRYPTPAD_URL;
|
|
4169
|
+
if (!url) throw new Error("CryptPad Sheet sync provider requires a \"url\" option or CRYPTPAD_URL environment variable.");
|
|
4170
|
+
const password = options.password ?? process.env.CRYPTPAD_PASSWORD;
|
|
4171
|
+
return {
|
|
4172
|
+
kind: "sync",
|
|
4173
|
+
providerId: options.providerId ?? "cryptpad-sheet",
|
|
4174
|
+
displayName: options.displayName ?? "CryptPad Sheet Sync (E2EE)",
|
|
4175
|
+
capabilities: CRYPTPAD_SHEET_SYNC_CAPABILITIES,
|
|
4176
|
+
async syncTranslations(payload) {
|
|
4177
|
+
const resolution = resolveSyncPlan({
|
|
4178
|
+
baseTranslations: payload.metadata?.baseTranslations ?? payload.remoteTranslations,
|
|
4179
|
+
localTranslations: payload.localTranslations,
|
|
4180
|
+
remoteTranslations: payload.remoteTranslations
|
|
4181
|
+
}, options.conflictPolicy ?? "manual");
|
|
4182
|
+
if (resolution.appliedLocalChanges === 0) return {
|
|
4183
|
+
changedKeys: 0,
|
|
4184
|
+
skippedKeys: resolution.skippedConflicts,
|
|
4185
|
+
metadata: {
|
|
4186
|
+
reason: "no-local-diff",
|
|
4187
|
+
url,
|
|
4188
|
+
policy: resolution.policy
|
|
4189
|
+
}
|
|
4190
|
+
};
|
|
4191
|
+
const client = new CryptPadClient({
|
|
4192
|
+
url,
|
|
4193
|
+
password,
|
|
4194
|
+
timeoutMs: options.timeoutMs
|
|
4195
|
+
});
|
|
4196
|
+
const effectiveMapping = options.localeMapping ?? {};
|
|
4197
|
+
const sheetRowsMap = convertTranslationsToSheetRows(resolution.mergedTranslations, effectiveMapping);
|
|
4198
|
+
for (const [sheetName, rows] of Object.entries(sheetRowsMap)) await client.writeSheetRows(sheetName, rows, { override: options.override ?? options.conflictPolicy === "local-wins" });
|
|
4199
|
+
return {
|
|
4200
|
+
changedKeys: resolution.appliedLocalChanges,
|
|
4201
|
+
skippedKeys: resolution.skippedConflicts,
|
|
4202
|
+
metadata: {
|
|
4203
|
+
provider: "cryptpad-sheet",
|
|
4204
|
+
url,
|
|
4205
|
+
policy: resolution.policy
|
|
4206
|
+
}
|
|
4207
|
+
};
|
|
4208
|
+
}
|
|
4209
|
+
};
|
|
4210
|
+
}
|
|
4211
|
+
//#endregion
|
|
4212
|
+
//#region src/providers/cryptpad/driveClient.ts
|
|
4213
|
+
/**
|
|
4214
|
+
* Headless client for inspecting and traversing encrypted CryptPad Drive folders.
|
|
4215
|
+
*/
|
|
4216
|
+
var CryptPadDriveClient = class {
|
|
4217
|
+
constructor(options) {
|
|
4218
|
+
if (!options.url || typeof options.url !== "string") throw new Error("CryptPadDriveClient requires a valid \"url\" option.");
|
|
4219
|
+
this.parsedUrl = parsePadUrl(options.url);
|
|
4220
|
+
this.password = options.password ?? process.env.CRYPTPAD_PASSWORD;
|
|
4221
|
+
this.passwords = options.passwords ?? {};
|
|
4222
|
+
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
4223
|
+
}
|
|
4224
|
+
/**
|
|
4225
|
+
* Fetches the drive pad's Netflux history, decrypts all changes, and parses
|
|
4226
|
+
* the hierarchical filesystem tree into a flat list of items.
|
|
4227
|
+
*/
|
|
4228
|
+
async listDriveItems(signal) {
|
|
4229
|
+
const wsUrl = await resolveCryptPadWebsocketUrl(this.parsedUrl.origin, signal);
|
|
4230
|
+
const { channelHex, cryptKey } = deriveCryptPadKeys(this.parsedUrl.seed, this.password);
|
|
4231
|
+
const messages = await fetchChannelHistory(wsUrl, channelHex, cryptKey, {
|
|
4232
|
+
timeoutMs: this.timeoutMs,
|
|
4233
|
+
signal
|
|
4234
|
+
});
|
|
4235
|
+
const items = [];
|
|
4236
|
+
for (const raw of messages) try {
|
|
4237
|
+
const parsed = JSON.parse(raw);
|
|
4238
|
+
if (parsed && typeof parsed === "object") {
|
|
4239
|
+
const fileDict = parsed.files || parsed.data?.files || parsed;
|
|
4240
|
+
if (fileDict && typeof fileDict === "object" && !Array.isArray(fileDict)) for (const [id, val] of Object.entries(fileDict)) {
|
|
4241
|
+
if (!val || typeof val !== "object") continue;
|
|
4242
|
+
const fileObj = val;
|
|
4243
|
+
const title = String(fileObj.title || fileObj.name || id);
|
|
4244
|
+
const href = String(fileObj.href || fileObj.url || "");
|
|
4245
|
+
const typeStr = String(fileObj.type || fileObj.app || (href.includes("/sheet/") ? "sheet" : "file"));
|
|
4246
|
+
const type = typeStr.includes("sheet") ? "sheet" : typeStr.includes("folder") ? "folder" : typeStr.includes("pad") ? "pad" : "file";
|
|
4247
|
+
const itemUrl = href.startsWith("http") ? href : `${this.parsedUrl.origin}${href.startsWith("/") ? "" : "/"}${href}`;
|
|
4248
|
+
const password = fileObj.password ?? this.passwords[itemUrl] ?? this.passwords[title] ?? this.password;
|
|
4249
|
+
items.push({
|
|
4250
|
+
id,
|
|
4251
|
+
title,
|
|
4252
|
+
type,
|
|
4253
|
+
url: itemUrl,
|
|
4254
|
+
password,
|
|
4255
|
+
channel: typeof fileObj.channel === "string" ? fileObj.channel : void 0,
|
|
4256
|
+
path: title,
|
|
4257
|
+
ctime: typeof fileObj.ctime === "number" ? fileObj.ctime : void 0,
|
|
4258
|
+
mtime: typeof fileObj.mtime === "number" ? fileObj.mtime : void 0
|
|
4259
|
+
});
|
|
4260
|
+
}
|
|
4261
|
+
if (Array.isArray(parsed)) for (const entry of parsed) {
|
|
4262
|
+
if (!entry || typeof entry !== "object") continue;
|
|
4263
|
+
const fileObj = entry;
|
|
4264
|
+
const title = String(fileObj.title || fileObj.name || "untitled");
|
|
4265
|
+
const href = String(fileObj.href || fileObj.url || "");
|
|
4266
|
+
const typeStr = String(fileObj.type || fileObj.app || (href.includes("/sheet/") ? "sheet" : "file"));
|
|
4267
|
+
const type = typeStr.includes("sheet") ? "sheet" : typeStr.includes("folder") ? "folder" : typeStr.includes("pad") ? "pad" : "file";
|
|
4268
|
+
const itemUrl = href.startsWith("http") ? href : `${this.parsedUrl.origin}${href.startsWith("/") ? "" : "/"}${href}`;
|
|
4269
|
+
const password = fileObj.password ?? this.passwords[itemUrl] ?? this.passwords[title] ?? this.password;
|
|
4270
|
+
items.push({
|
|
4271
|
+
id: String(fileObj.id || title),
|
|
4272
|
+
title,
|
|
4273
|
+
type,
|
|
4274
|
+
url: itemUrl,
|
|
4275
|
+
password,
|
|
4276
|
+
channel: typeof fileObj.channel === "string" ? fileObj.channel : void 0,
|
|
4277
|
+
path: title
|
|
4278
|
+
});
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
} catch {}
|
|
4282
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4283
|
+
const deduplicated = [];
|
|
4284
|
+
for (const item of items) if (item.url && !seen.has(item.url)) {
|
|
4285
|
+
seen.add(item.url);
|
|
4286
|
+
deduplicated.push(item);
|
|
4287
|
+
}
|
|
4288
|
+
return deduplicated;
|
|
4289
|
+
}
|
|
4290
|
+
/**
|
|
4291
|
+
* Filters and returns only spreadsheets found within the drive folder.
|
|
4292
|
+
*/
|
|
4293
|
+
async listSheets(signal) {
|
|
4294
|
+
return (await this.listDriveItems(signal)).filter((item) => item.type === "sheet" || item.url.includes("/sheet/"));
|
|
4295
|
+
}
|
|
4296
|
+
};
|
|
4297
|
+
createCapabilitySet({ discoverByFolder: true });
|
|
4298
|
+
//#endregion
|
|
3852
4299
|
//#region src/providers/cryptpad/fullProvider.ts
|
|
3853
4300
|
const CRYPTPAD_WORKSPACE_OUTPUT_CAPABILITIES = createCapabilitySet({ writeTables: true });
|
|
3854
4301
|
const CRYPTPAD_WORKSPACE_SYNC_CAPABILITIES = createCapabilitySet({
|
|
@@ -3995,6 +4442,9 @@ function createCryptPadWorkspaceSyncProvider(options, depsOverrides = {}) {
|
|
|
3995
4442
|
}
|
|
3996
4443
|
//#endregion
|
|
3997
4444
|
//#region src/providers/cryptpad/assetProvider.ts
|
|
4445
|
+
/**
|
|
4446
|
+
* Options for configuring a CryptPad asset synchronization provider.
|
|
4447
|
+
*/
|
|
3998
4448
|
const CRYPTPAD_ASSET_SYNC_CAPABILITIES = createCapabilitySet({
|
|
3999
4449
|
assetSync: true,
|
|
4000
4450
|
discoverByFolder: true
|
|
@@ -4080,7 +4530,16 @@ function createCryptPadAssetSyncProvider(options, depsOverrides = {}) {
|
|
|
4080
4530
|
displayName: options.displayName ?? "CryptPad Asset Sync",
|
|
4081
4531
|
capabilities: CRYPTPAD_ASSET_SYNC_CAPABILITIES,
|
|
4082
4532
|
async syncAssets(request) {
|
|
4083
|
-
|
|
4533
|
+
let manifest = [];
|
|
4534
|
+
if (options.manifestPath) manifest = await deps.readManifest(options.manifestPath);
|
|
4535
|
+
else if (options.driveUrl) manifest = (await new CryptPadDriveClient({
|
|
4536
|
+
url: options.driveUrl,
|
|
4537
|
+
password: options.password
|
|
4538
|
+
}).listDriveItems(request.signal)).filter((item) => item.type !== "folder" && !item.url.includes("/sheet/")).map((item) => ({
|
|
4539
|
+
assetId: item.id || item.title,
|
|
4540
|
+
relativePath: item.path || item.title,
|
|
4541
|
+
sourceUrl: item.url
|
|
4542
|
+
}));
|
|
4084
4543
|
const downloaded = [];
|
|
4085
4544
|
const updated = [];
|
|
4086
4545
|
const skipped = [];
|
|
@@ -4417,6 +4876,16 @@ function createInputProvider(providerId, options) {
|
|
|
4417
4876
|
function createOutputProvider(providerId, options) {
|
|
4418
4877
|
switch (providerId) {
|
|
4419
4878
|
case "google-sheets": return createGoogleSheetsOutputProvider(options);
|
|
4879
|
+
case "cryptpad-sheet":
|
|
4880
|
+
case "cryptpad": return createCryptPadSheetOutputProvider({
|
|
4881
|
+
url: typeof options.url === "string" ? options.url : void 0,
|
|
4882
|
+
password: typeof options.password === "string" ? options.password : void 0,
|
|
4883
|
+
override: options.override === true,
|
|
4884
|
+
localeMapping: typeof options.localeMapping === "object" && options.localeMapping !== null ? options.localeMapping : void 0,
|
|
4885
|
+
providerId: typeof options.providerId === "string" ? options.providerId : void 0,
|
|
4886
|
+
displayName: typeof options.displayName === "string" ? options.displayName : void 0,
|
|
4887
|
+
timeoutMs: typeof options.timeoutMs === "number" ? options.timeoutMs : void 0
|
|
4888
|
+
});
|
|
4420
4889
|
case "cryptpad-workspace":
|
|
4421
4890
|
if (typeof options.filePath !== "string" || options.filePath.trim().length === 0) throw new Error("cryptpad-workspace output provider requires a non-empty \"filePath\" option.");
|
|
4422
4891
|
return createCryptPadWorkspaceOutputProvider({
|
|
@@ -4432,6 +4901,17 @@ function createOutputProvider(providerId, options) {
|
|
|
4432
4901
|
function createSyncProvider(providerId, options) {
|
|
4433
4902
|
switch (providerId) {
|
|
4434
4903
|
case "google-sheets": return createGoogleSheetsSyncProvider(options);
|
|
4904
|
+
case "cryptpad-sheet":
|
|
4905
|
+
case "cryptpad": return createCryptPadSheetSyncProvider({
|
|
4906
|
+
url: typeof options.url === "string" ? options.url : void 0,
|
|
4907
|
+
password: typeof options.password === "string" ? options.password : void 0,
|
|
4908
|
+
override: options.override === true,
|
|
4909
|
+
conflictPolicy: options.conflictPolicy === "remote-wins" || options.conflictPolicy === "local-wins" || options.conflictPolicy === "manual" ? options.conflictPolicy : void 0,
|
|
4910
|
+
localeMapping: typeof options.localeMapping === "object" && options.localeMapping !== null ? options.localeMapping : void 0,
|
|
4911
|
+
providerId: typeof options.providerId === "string" ? options.providerId : void 0,
|
|
4912
|
+
displayName: typeof options.displayName === "string" ? options.displayName : void 0,
|
|
4913
|
+
timeoutMs: typeof options.timeoutMs === "number" ? options.timeoutMs : void 0
|
|
4914
|
+
});
|
|
4435
4915
|
case "cryptpad-workspace":
|
|
4436
4916
|
if (typeof options.filePath !== "string" || options.filePath.trim().length === 0) throw new Error("cryptpad-workspace sync provider requires a non-empty \"filePath\" option.");
|
|
4437
4917
|
return createCryptPadWorkspaceSyncProvider({
|
|
@@ -4447,13 +4927,18 @@ function createSyncProvider(providerId, options) {
|
|
|
4447
4927
|
}
|
|
4448
4928
|
function createAssetSyncProvider(providerId, options) {
|
|
4449
4929
|
switch (providerId) {
|
|
4450
|
-
case "cryptpad-assets":
|
|
4451
|
-
|
|
4930
|
+
case "cryptpad-assets": {
|
|
4931
|
+
const hasManifest = typeof options.manifestPath === "string" && options.manifestPath.trim().length > 0;
|
|
4932
|
+
const hasDrive = typeof options.driveUrl === "string" && options.driveUrl.trim().length > 0;
|
|
4933
|
+
if (!hasManifest && !hasDrive) throw new Error("cryptpad-assets sync provider requires a non-empty \"manifestPath\" option.");
|
|
4452
4934
|
return createCryptPadAssetSyncProvider({
|
|
4453
|
-
manifestPath: options.manifestPath,
|
|
4935
|
+
manifestPath: typeof options.manifestPath === "string" ? options.manifestPath : void 0,
|
|
4936
|
+
driveUrl: typeof options.driveUrl === "string" ? options.driveUrl : void 0,
|
|
4937
|
+
password: typeof options.password === "string" ? options.password : void 0,
|
|
4454
4938
|
providerId: typeof options.providerId === "string" ? options.providerId : void 0,
|
|
4455
4939
|
displayName: typeof options.displayName === "string" ? options.displayName : void 0
|
|
4456
4940
|
});
|
|
4941
|
+
}
|
|
4457
4942
|
default: throw new Error(`Unsupported asset sync provider: "${providerId}"`);
|
|
4458
4943
|
}
|
|
4459
4944
|
}
|