@el-j/google-sheet-translations 3.0.0-beta.4 → 3.0.0-beta.6
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 +532 -16
- package/dist/index.js +532 -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 +32 -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 +1786 -0
- package/dist-cli/run-provider.mjs +532 -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,31 @@ var CryptPadClient = class {
|
|
|
3607
3745
|
return resolveCryptPadWebsocketUrl(this.parsedUrl.origin, signal);
|
|
3608
3746
|
}
|
|
3609
3747
|
/**
|
|
3610
|
-
*
|
|
3611
|
-
*
|
|
3748
|
+
* Initializes the OnlyOffice RT channel for a brand-new CryptPad sheet that has never
|
|
3749
|
+
* been opened in a browser. Generates a random 32-hex channel ID, broadcasts it as an
|
|
3750
|
+
* encrypted metadata patch on the Netflux channel (the same thing the OnlyOffice browser
|
|
3751
|
+
* client does on first open), and returns the new RT channel ID.
|
|
3752
|
+
*
|
|
3753
|
+
* This removes the requirement to open the sheet in a browser before the CLI can write to it.
|
|
3754
|
+
*/
|
|
3755
|
+
async initializeRtChannel(signal) {
|
|
3756
|
+
const wsUrl = await this.getWebsocketUrl(signal);
|
|
3757
|
+
const { channelHex, cryptKey } = this.getKeys();
|
|
3758
|
+
const newRtChannel = crypto.randomBytes(16).toString("hex");
|
|
3759
|
+
const innerJson = JSON.stringify({ content: { channel: newRtChannel } });
|
|
3760
|
+
await broadcastChannelMessage(wsUrl, channelHex, cryptKey, JSON.stringify([1, [[
|
|
3761
|
+
0,
|
|
3762
|
+
0,
|
|
3763
|
+
innerJson
|
|
3764
|
+
]]]), {
|
|
3765
|
+
timeoutMs: this.timeoutMs,
|
|
3766
|
+
signal
|
|
3767
|
+
});
|
|
3768
|
+
return newRtChannel;
|
|
3769
|
+
}
|
|
3770
|
+
/**
|
|
3771
|
+
* Fetches the complete sheet data, including raw cell coordinate mappings,
|
|
3772
|
+
* multi-sheet tabs, and structured rows formatted for translation ingestion.
|
|
3612
3773
|
*/
|
|
3613
3774
|
async fetchSheetData(signal) {
|
|
3614
3775
|
const wsUrl = await this.getWebsocketUrl(signal);
|
|
@@ -3622,11 +3783,22 @@ var CryptPadClient = class {
|
|
|
3622
3783
|
timeoutMs: this.timeoutMs,
|
|
3623
3784
|
signal
|
|
3624
3785
|
}));
|
|
3625
|
-
const
|
|
3786
|
+
const multiSheets = convertCellsToMultiSheetRows(cells);
|
|
3787
|
+
const sheetNames = Object.keys(multiSheets);
|
|
3788
|
+
const defaultSheet = multiSheets["Sheet1"] ? "Sheet1" : sheetNames[0];
|
|
3789
|
+
const defaultRows = defaultSheet && multiSheets[defaultSheet] ? multiSheets[defaultSheet] : convertCellsToSheetRows(cells);
|
|
3790
|
+
const groupedCells = groupCellsBySheet(cells);
|
|
3791
|
+
const sheetsResult = {};
|
|
3792
|
+
for (const name of sheetNames) sheetsResult[name] = {
|
|
3793
|
+
cells: groupedCells[name] ?? {},
|
|
3794
|
+
rows: multiSheets[name] ?? []
|
|
3795
|
+
};
|
|
3626
3796
|
return {
|
|
3627
3797
|
url: this.parsedUrl.cleanUrl,
|
|
3628
3798
|
cells,
|
|
3629
|
-
rows,
|
|
3799
|
+
rows: defaultRows,
|
|
3800
|
+
sheets: sheetsResult,
|
|
3801
|
+
sheetNames,
|
|
3630
3802
|
metadata: {
|
|
3631
3803
|
app: this.parsedUrl.app,
|
|
3632
3804
|
mode: this.parsedUrl.mode,
|
|
@@ -3637,9 +3809,91 @@ var CryptPadClient = class {
|
|
|
3637
3809
|
}
|
|
3638
3810
|
/**
|
|
3639
3811
|
* Directly fetches tabular translation rows `[ { key: '...', en: '...', de: '...' } ]`.
|
|
3812
|
+
* If `sheetName` is provided, returns rows for that specific tab.
|
|
3640
3813
|
*/
|
|
3641
|
-
async fetchSheetRows(signal) {
|
|
3642
|
-
|
|
3814
|
+
async fetchSheetRows(sheetName, signal) {
|
|
3815
|
+
const result = await this.fetchSheetData(signal);
|
|
3816
|
+
if (sheetName && result.sheets[sheetName]) return result.sheets[sheetName].rows;
|
|
3817
|
+
return result.rows;
|
|
3818
|
+
}
|
|
3819
|
+
/**
|
|
3820
|
+
* Broadcasts cell updates to the OnlyOffice real-time collaboration channel.
|
|
3821
|
+
* If the sheet has never been opened in a browser (RT channel is None), it is
|
|
3822
|
+
* automatically initialized headlessly — no browser required.
|
|
3823
|
+
*/
|
|
3824
|
+
async sendCellUpdates(updates, signal) {
|
|
3825
|
+
if (updates.length === 0) return;
|
|
3826
|
+
let rtChannel = (await this.fetchSheetData(signal)).metadata.rtChannelId;
|
|
3827
|
+
if (!rtChannel) {
|
|
3828
|
+
console.log("No OnlyOffice RT channel found. Initializing headlessly (no browser required)...");
|
|
3829
|
+
rtChannel = await this.initializeRtChannel(signal);
|
|
3830
|
+
console.log(`RT channel initialized: ${rtChannel}`);
|
|
3831
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
3832
|
+
}
|
|
3833
|
+
const wsUrl = await this.getWebsocketUrl(signal);
|
|
3834
|
+
const { cryptKey } = this.getKeys();
|
|
3835
|
+
const payload = buildOnlyOfficeChangePayload(updates);
|
|
3836
|
+
await broadcastChannelMessage(wsUrl, rtChannel, cryptKey, payload, {
|
|
3837
|
+
timeoutMs: this.timeoutMs,
|
|
3838
|
+
signal
|
|
3839
|
+
});
|
|
3840
|
+
}
|
|
3841
|
+
/**
|
|
3842
|
+
* Updates or appends rows in a target sheet tab, creating new cells as needed.
|
|
3843
|
+
*/
|
|
3844
|
+
async writeSheetRows(sheetName, rows, options = {}) {
|
|
3845
|
+
if (rows.length === 0) return 0;
|
|
3846
|
+
const existingRows = (await this.fetchSheetData(options.signal)).sheets[sheetName]?.rows ?? [];
|
|
3847
|
+
let keyColName = "var";
|
|
3848
|
+
const firstExisting = existingRows[0];
|
|
3849
|
+
const firstIncoming = rows[0];
|
|
3850
|
+
if (firstExisting) {
|
|
3851
|
+
if ("var" in firstExisting) keyColName = "var";
|
|
3852
|
+
else if ("key" in firstExisting) keyColName = "key";
|
|
3853
|
+
} else if (firstIncoming) {
|
|
3854
|
+
if ("var" in firstIncoming) keyColName = "var";
|
|
3855
|
+
else if ("key" in firstIncoming) keyColName = "key";
|
|
3856
|
+
}
|
|
3857
|
+
const colNamesSet = /* @__PURE__ */ new Set([keyColName]);
|
|
3858
|
+
for (const r of existingRows) for (const k of Object.keys(r)) if (k !== "key" && k !== "var") colNamesSet.add(k);
|
|
3859
|
+
for (const r of rows) for (const k of Object.keys(r)) if (k !== "key" && k !== "var") colNamesSet.add(k);
|
|
3860
|
+
const colNames = Array.from(colNamesSet);
|
|
3861
|
+
const updates = [];
|
|
3862
|
+
colNames.forEach((name, idx) => {
|
|
3863
|
+
updates.push({
|
|
3864
|
+
sheet: sheetName,
|
|
3865
|
+
col: colIndexToLetter(idx),
|
|
3866
|
+
row: 1,
|
|
3867
|
+
value: name
|
|
3868
|
+
});
|
|
3869
|
+
});
|
|
3870
|
+
const keyToRowIdx = /* @__PURE__ */ new Map();
|
|
3871
|
+
existingRows.forEach((r, idx) => {
|
|
3872
|
+
const rowKey = r.var ?? r.key ?? r[keyColName];
|
|
3873
|
+
if (rowKey) keyToRowIdx.set(rowKey, idx + 2);
|
|
3874
|
+
});
|
|
3875
|
+
let nextAvailableRow = existingRows.length + 2;
|
|
3876
|
+
for (const row of rows) {
|
|
3877
|
+
const rowKey = row.var ?? row.key ?? row[keyColName];
|
|
3878
|
+
if (!rowKey) continue;
|
|
3879
|
+
const targetRow = keyToRowIdx.get(rowKey) ?? nextAvailableRow++;
|
|
3880
|
+
keyToRowIdx.set(rowKey, targetRow);
|
|
3881
|
+
for (const [colName, val] of Object.entries(row)) {
|
|
3882
|
+
const mappedColName = colName === "key" || colName === "var" ? keyColName : colName;
|
|
3883
|
+
const colIdx = colNames.indexOf(mappedColName);
|
|
3884
|
+
if (colIdx >= 0 && val !== void 0) {
|
|
3885
|
+
const existingVal = existingRows[targetRow - 2]?.[mappedColName];
|
|
3886
|
+
if (options.override || !existingVal || existingVal.trim().length === 0) updates.push({
|
|
3887
|
+
sheet: sheetName,
|
|
3888
|
+
col: colIndexToLetter(colIdx),
|
|
3889
|
+
row: targetRow,
|
|
3890
|
+
value: String(val)
|
|
3891
|
+
});
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
await this.sendCellUpdates(updates, options.signal);
|
|
3896
|
+
return updates.length;
|
|
3643
3897
|
}
|
|
3644
3898
|
};
|
|
3645
3899
|
//#endregion
|
|
@@ -3673,7 +3927,7 @@ function createCryptPadSheetInputProvider(options) {
|
|
|
3673
3927
|
const requested = new Set((request.tableNames ?? []).filter(Boolean));
|
|
3674
3928
|
const selectedSources = requested.size === 0 ? sources : sources.filter((source) => requested.has(source.tableName));
|
|
3675
3929
|
return {
|
|
3676
|
-
tables: await Promise.all(selectedSources.map(async (source) => {
|
|
3930
|
+
tables: (await Promise.all(selectedSources.map(async (source) => {
|
|
3677
3931
|
const url = source.url ?? options.url ?? process.env.CRYPTPAD_URL;
|
|
3678
3932
|
if (!url) throw new Error(`CryptPad sheet source "${source.tableName}" does not define a URL.`);
|
|
3679
3933
|
const sheetData = await new CryptPadClient({
|
|
@@ -3681,7 +3935,22 @@ function createCryptPadSheetInputProvider(options) {
|
|
|
3681
3935
|
password: source.password ?? options.password ?? process.env.CRYPTPAD_PASSWORD,
|
|
3682
3936
|
timeoutMs: options.timeoutMs
|
|
3683
3937
|
}).fetchSheetData(request.signal);
|
|
3684
|
-
|
|
3938
|
+
const sourceTables = [];
|
|
3939
|
+
if (Array.isArray(sheetData.sheetNames) && sheetData.sheetNames.length > 1) {
|
|
3940
|
+
for (const tabName of sheetData.sheetNames) if (requested.size === 0 || requested.has(tabName) || requested.has(source.tableName)) sourceTables.push({
|
|
3941
|
+
tableId: `${source.tableId ?? url}#${tabName}`,
|
|
3942
|
+
tableName: tabName,
|
|
3943
|
+
rows: sheetData.sheets[tabName]?.rows ?? [],
|
|
3944
|
+
sourcePath: url,
|
|
3945
|
+
metadata: {
|
|
3946
|
+
provider: "cryptpad-sheet",
|
|
3947
|
+
channelId: sheetData.metadata.channelId,
|
|
3948
|
+
rtChannelId: sheetData.metadata.rtChannelId,
|
|
3949
|
+
sheetTab: tabName
|
|
3950
|
+
}
|
|
3951
|
+
});
|
|
3952
|
+
}
|
|
3953
|
+
if (sourceTables.length === 0) sourceTables.push({
|
|
3685
3954
|
tableId: source.tableId ?? url,
|
|
3686
3955
|
tableName: source.tableName,
|
|
3687
3956
|
rows: sheetData.rows,
|
|
@@ -3692,8 +3961,9 @@ function createCryptPadSheetInputProvider(options) {
|
|
|
3692
3961
|
rtChannelId: sheetData.metadata.rtChannelId,
|
|
3693
3962
|
cellCount: Object.keys(sheetData.cells).length
|
|
3694
3963
|
}
|
|
3695
|
-
};
|
|
3696
|
-
|
|
3964
|
+
});
|
|
3965
|
+
return sourceTables;
|
|
3966
|
+
}))).flat(),
|
|
3697
3967
|
metadata: {
|
|
3698
3968
|
provider: "cryptpad-sheet",
|
|
3699
3969
|
sourceCount: selectedSources.length
|
|
@@ -3703,6 +3973,72 @@ function createCryptPadSheetInputProvider(options) {
|
|
|
3703
3973
|
};
|
|
3704
3974
|
}
|
|
3705
3975
|
//#endregion
|
|
3976
|
+
//#region src/providers/cryptpad/sheetOutputProvider.ts
|
|
3977
|
+
const CRYPTPAD_SHEET_OUTPUT_CAPABILITIES = createCapabilitySet({ writeTables: true });
|
|
3978
|
+
/**
|
|
3979
|
+
* Converts nested TranslationData `[locale][sheet][key] = value` into
|
|
3980
|
+
* tabular rows `Record<sheetName, SheetRow[]>`.
|
|
3981
|
+
*/
|
|
3982
|
+
function convertTranslationsToSheetRows(translations, localeMapping = {}, keyColumnName = "key") {
|
|
3983
|
+
const reverseMapping = {};
|
|
3984
|
+
for (const [header, norm] of Object.entries(localeMapping)) reverseMapping[norm] = header;
|
|
3985
|
+
const sheetRowsMap = {};
|
|
3986
|
+
for (const [locale, sheets] of Object.entries(translations)) {
|
|
3987
|
+
const colHeader = reverseMapping[locale] ?? locale;
|
|
3988
|
+
for (const [sheetName, keys] of Object.entries(sheets)) {
|
|
3989
|
+
if (!sheetRowsMap[sheetName]) sheetRowsMap[sheetName] = /* @__PURE__ */ new Map();
|
|
3990
|
+
for (const [key, value] of Object.entries(keys)) {
|
|
3991
|
+
if (!sheetRowsMap[sheetName].has(key)) sheetRowsMap[sheetName].set(key, { [keyColumnName]: key });
|
|
3992
|
+
sheetRowsMap[sheetName].get(key)[colHeader] = String(value);
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
const result = {};
|
|
3997
|
+
for (const [sheetName, keyMap] of Object.entries(sheetRowsMap)) result[sheetName] = Array.from(keyMap.values());
|
|
3998
|
+
return result;
|
|
3999
|
+
}
|
|
4000
|
+
/**
|
|
4001
|
+
* Creates a {@link TranslationOutputProvider} that writes translation data directly
|
|
4002
|
+
* into password-protected or public CryptPad spreadsheets (multi-tab OnlyOffice workbooks)
|
|
4003
|
+
* using end-to-end encrypted Netflux WebSockets.
|
|
4004
|
+
*/
|
|
4005
|
+
function createCryptPadSheetOutputProvider(options = {}) {
|
|
4006
|
+
const url = options.url ?? process.env.CRYPTPAD_URL;
|
|
4007
|
+
if (!url) throw new Error("CryptPad Sheet output provider requires a \"url\" option or CRYPTPAD_URL environment variable.");
|
|
4008
|
+
const password = options.password ?? process.env.CRYPTPAD_PASSWORD;
|
|
4009
|
+
return {
|
|
4010
|
+
kind: "output",
|
|
4011
|
+
providerId: options.providerId ?? "cryptpad-sheet",
|
|
4012
|
+
displayName: options.displayName ?? "CryptPad Sheet Output (E2EE)",
|
|
4013
|
+
capabilities: CRYPTPAD_SHEET_OUTPUT_CAPABILITIES,
|
|
4014
|
+
async writeTranslations(payload) {
|
|
4015
|
+
const client = new CryptPadClient({
|
|
4016
|
+
url,
|
|
4017
|
+
password,
|
|
4018
|
+
timeoutMs: options.timeoutMs
|
|
4019
|
+
});
|
|
4020
|
+
const effectiveMapping = options.localeMapping ?? payload.localeMapping ?? {};
|
|
4021
|
+
const sheetRowsMap = convertTranslationsToSheetRows(payload.translations, effectiveMapping, options.keyColumnName ?? "key");
|
|
4022
|
+
const updatedSheets = [];
|
|
4023
|
+
let totalUpdatedCells = 0;
|
|
4024
|
+
for (const [sheetName, rows] of Object.entries(sheetRowsMap)) {
|
|
4025
|
+
const count = await client.writeSheetRows(sheetName, rows, { override: options.override ?? false });
|
|
4026
|
+
updatedSheets.push(sheetName);
|
|
4027
|
+
totalUpdatedCells += count;
|
|
4028
|
+
}
|
|
4029
|
+
return {
|
|
4030
|
+
wroteFiles: [url],
|
|
4031
|
+
metadata: {
|
|
4032
|
+
provider: "cryptpad-sheet",
|
|
4033
|
+
url,
|
|
4034
|
+
updatedSheets,
|
|
4035
|
+
totalUpdatedCells
|
|
4036
|
+
}
|
|
4037
|
+
};
|
|
4038
|
+
}
|
|
4039
|
+
};
|
|
4040
|
+
}
|
|
4041
|
+
//#endregion
|
|
3706
4042
|
//#region src/providers/syncEngine.ts
|
|
3707
4043
|
function pathKey(locale, sheet, key) {
|
|
3708
4044
|
return `${locale}::${sheet}::${key}`;
|
|
@@ -3849,6 +4185,148 @@ function resolveSyncPlan(input, policy) {
|
|
|
3849
4185
|
};
|
|
3850
4186
|
}
|
|
3851
4187
|
//#endregion
|
|
4188
|
+
//#region src/providers/cryptpad/sheetSyncProvider.ts
|
|
4189
|
+
const CRYPTPAD_SHEET_SYNC_CAPABILITIES = createCapabilitySet({
|
|
4190
|
+
syncBack: true,
|
|
4191
|
+
writeTables: true
|
|
4192
|
+
});
|
|
4193
|
+
/**
|
|
4194
|
+
* Creates a {@link TranslationSyncProvider} that reconciles local translation changes
|
|
4195
|
+
* against a live CryptPad spreadsheet (multi-tab OnlyOffice workbook) using three-way
|
|
4196
|
+
* diffing and conflict policies, pushing only the resolved diffs back over Netflux.
|
|
4197
|
+
*/
|
|
4198
|
+
function createCryptPadSheetSyncProvider(options = {}) {
|
|
4199
|
+
const url = options.url ?? process.env.CRYPTPAD_URL;
|
|
4200
|
+
if (!url) throw new Error("CryptPad Sheet sync provider requires a \"url\" option or CRYPTPAD_URL environment variable.");
|
|
4201
|
+
const password = options.password ?? process.env.CRYPTPAD_PASSWORD;
|
|
4202
|
+
return {
|
|
4203
|
+
kind: "sync",
|
|
4204
|
+
providerId: options.providerId ?? "cryptpad-sheet",
|
|
4205
|
+
displayName: options.displayName ?? "CryptPad Sheet Sync (E2EE)",
|
|
4206
|
+
capabilities: CRYPTPAD_SHEET_SYNC_CAPABILITIES,
|
|
4207
|
+
async syncTranslations(payload) {
|
|
4208
|
+
const resolution = resolveSyncPlan({
|
|
4209
|
+
baseTranslations: payload.metadata?.baseTranslations ?? payload.remoteTranslations,
|
|
4210
|
+
localTranslations: payload.localTranslations,
|
|
4211
|
+
remoteTranslations: payload.remoteTranslations
|
|
4212
|
+
}, options.conflictPolicy ?? "manual");
|
|
4213
|
+
if (resolution.appliedLocalChanges === 0) return {
|
|
4214
|
+
changedKeys: 0,
|
|
4215
|
+
skippedKeys: resolution.skippedConflicts,
|
|
4216
|
+
metadata: {
|
|
4217
|
+
reason: "no-local-diff",
|
|
4218
|
+
url,
|
|
4219
|
+
policy: resolution.policy
|
|
4220
|
+
}
|
|
4221
|
+
};
|
|
4222
|
+
const client = new CryptPadClient({
|
|
4223
|
+
url,
|
|
4224
|
+
password,
|
|
4225
|
+
timeoutMs: options.timeoutMs
|
|
4226
|
+
});
|
|
4227
|
+
const effectiveMapping = options.localeMapping ?? {};
|
|
4228
|
+
const sheetRowsMap = convertTranslationsToSheetRows(resolution.mergedTranslations, effectiveMapping);
|
|
4229
|
+
for (const [sheetName, rows] of Object.entries(sheetRowsMap)) await client.writeSheetRows(sheetName, rows, { override: options.override ?? options.conflictPolicy === "local-wins" });
|
|
4230
|
+
return {
|
|
4231
|
+
changedKeys: resolution.appliedLocalChanges,
|
|
4232
|
+
skippedKeys: resolution.skippedConflicts,
|
|
4233
|
+
metadata: {
|
|
4234
|
+
provider: "cryptpad-sheet",
|
|
4235
|
+
url,
|
|
4236
|
+
policy: resolution.policy
|
|
4237
|
+
}
|
|
4238
|
+
};
|
|
4239
|
+
}
|
|
4240
|
+
};
|
|
4241
|
+
}
|
|
4242
|
+
//#endregion
|
|
4243
|
+
//#region src/providers/cryptpad/driveClient.ts
|
|
4244
|
+
/**
|
|
4245
|
+
* Headless client for inspecting and traversing encrypted CryptPad Drive folders.
|
|
4246
|
+
*/
|
|
4247
|
+
var CryptPadDriveClient = class {
|
|
4248
|
+
constructor(options) {
|
|
4249
|
+
if (!options.url || typeof options.url !== "string") throw new Error("CryptPadDriveClient requires a valid \"url\" option.");
|
|
4250
|
+
this.parsedUrl = parsePadUrl(options.url);
|
|
4251
|
+
this.password = options.password ?? process.env.CRYPTPAD_PASSWORD;
|
|
4252
|
+
this.passwords = options.passwords ?? {};
|
|
4253
|
+
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
4254
|
+
}
|
|
4255
|
+
/**
|
|
4256
|
+
* Fetches the drive pad's Netflux history, decrypts all changes, and parses
|
|
4257
|
+
* the hierarchical filesystem tree into a flat list of items.
|
|
4258
|
+
*/
|
|
4259
|
+
async listDriveItems(signal) {
|
|
4260
|
+
const wsUrl = await resolveCryptPadWebsocketUrl(this.parsedUrl.origin, signal);
|
|
4261
|
+
const { channelHex, cryptKey } = deriveCryptPadKeys(this.parsedUrl.seed, this.password);
|
|
4262
|
+
const messages = await fetchChannelHistory(wsUrl, channelHex, cryptKey, {
|
|
4263
|
+
timeoutMs: this.timeoutMs,
|
|
4264
|
+
signal
|
|
4265
|
+
});
|
|
4266
|
+
const items = [];
|
|
4267
|
+
for (const raw of messages) try {
|
|
4268
|
+
const parsed = JSON.parse(raw);
|
|
4269
|
+
if (parsed && typeof parsed === "object") {
|
|
4270
|
+
const fileDict = parsed.files || parsed.data?.files || parsed;
|
|
4271
|
+
if (fileDict && typeof fileDict === "object" && !Array.isArray(fileDict)) for (const [id, val] of Object.entries(fileDict)) {
|
|
4272
|
+
if (!val || typeof val !== "object") continue;
|
|
4273
|
+
const fileObj = val;
|
|
4274
|
+
const title = String(fileObj.title || fileObj.name || id);
|
|
4275
|
+
const href = String(fileObj.href || fileObj.url || "");
|
|
4276
|
+
const typeStr = String(fileObj.type || fileObj.app || (href.includes("/sheet/") ? "sheet" : "file"));
|
|
4277
|
+
const type = typeStr.includes("sheet") ? "sheet" : typeStr.includes("folder") ? "folder" : typeStr.includes("pad") ? "pad" : "file";
|
|
4278
|
+
const itemUrl = href.startsWith("http") ? href : `${this.parsedUrl.origin}${href.startsWith("/") ? "" : "/"}${href}`;
|
|
4279
|
+
const password = fileObj.password ?? this.passwords[itemUrl] ?? this.passwords[title] ?? this.password;
|
|
4280
|
+
items.push({
|
|
4281
|
+
id,
|
|
4282
|
+
title,
|
|
4283
|
+
type,
|
|
4284
|
+
url: itemUrl,
|
|
4285
|
+
password,
|
|
4286
|
+
channel: typeof fileObj.channel === "string" ? fileObj.channel : void 0,
|
|
4287
|
+
path: title,
|
|
4288
|
+
ctime: typeof fileObj.ctime === "number" ? fileObj.ctime : void 0,
|
|
4289
|
+
mtime: typeof fileObj.mtime === "number" ? fileObj.mtime : void 0
|
|
4290
|
+
});
|
|
4291
|
+
}
|
|
4292
|
+
if (Array.isArray(parsed)) for (const entry of parsed) {
|
|
4293
|
+
if (!entry || typeof entry !== "object") continue;
|
|
4294
|
+
const fileObj = entry;
|
|
4295
|
+
const title = String(fileObj.title || fileObj.name || "untitled");
|
|
4296
|
+
const href = String(fileObj.href || fileObj.url || "");
|
|
4297
|
+
const typeStr = String(fileObj.type || fileObj.app || (href.includes("/sheet/") ? "sheet" : "file"));
|
|
4298
|
+
const type = typeStr.includes("sheet") ? "sheet" : typeStr.includes("folder") ? "folder" : typeStr.includes("pad") ? "pad" : "file";
|
|
4299
|
+
const itemUrl = href.startsWith("http") ? href : `${this.parsedUrl.origin}${href.startsWith("/") ? "" : "/"}${href}`;
|
|
4300
|
+
const password = fileObj.password ?? this.passwords[itemUrl] ?? this.passwords[title] ?? this.password;
|
|
4301
|
+
items.push({
|
|
4302
|
+
id: String(fileObj.id || title),
|
|
4303
|
+
title,
|
|
4304
|
+
type,
|
|
4305
|
+
url: itemUrl,
|
|
4306
|
+
password,
|
|
4307
|
+
channel: typeof fileObj.channel === "string" ? fileObj.channel : void 0,
|
|
4308
|
+
path: title
|
|
4309
|
+
});
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
} catch {}
|
|
4313
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4314
|
+
const deduplicated = [];
|
|
4315
|
+
for (const item of items) if (item.url && !seen.has(item.url)) {
|
|
4316
|
+
seen.add(item.url);
|
|
4317
|
+
deduplicated.push(item);
|
|
4318
|
+
}
|
|
4319
|
+
return deduplicated;
|
|
4320
|
+
}
|
|
4321
|
+
/**
|
|
4322
|
+
* Filters and returns only spreadsheets found within the drive folder.
|
|
4323
|
+
*/
|
|
4324
|
+
async listSheets(signal) {
|
|
4325
|
+
return (await this.listDriveItems(signal)).filter((item) => item.type === "sheet" || item.url.includes("/sheet/"));
|
|
4326
|
+
}
|
|
4327
|
+
};
|
|
4328
|
+
createCapabilitySet({ discoverByFolder: true });
|
|
4329
|
+
//#endregion
|
|
3852
4330
|
//#region src/providers/cryptpad/fullProvider.ts
|
|
3853
4331
|
const CRYPTPAD_WORKSPACE_OUTPUT_CAPABILITIES = createCapabilitySet({ writeTables: true });
|
|
3854
4332
|
const CRYPTPAD_WORKSPACE_SYNC_CAPABILITIES = createCapabilitySet({
|
|
@@ -3995,6 +4473,9 @@ function createCryptPadWorkspaceSyncProvider(options, depsOverrides = {}) {
|
|
|
3995
4473
|
}
|
|
3996
4474
|
//#endregion
|
|
3997
4475
|
//#region src/providers/cryptpad/assetProvider.ts
|
|
4476
|
+
/**
|
|
4477
|
+
* Options for configuring a CryptPad asset synchronization provider.
|
|
4478
|
+
*/
|
|
3998
4479
|
const CRYPTPAD_ASSET_SYNC_CAPABILITIES = createCapabilitySet({
|
|
3999
4480
|
assetSync: true,
|
|
4000
4481
|
discoverByFolder: true
|
|
@@ -4080,7 +4561,16 @@ function createCryptPadAssetSyncProvider(options, depsOverrides = {}) {
|
|
|
4080
4561
|
displayName: options.displayName ?? "CryptPad Asset Sync",
|
|
4081
4562
|
capabilities: CRYPTPAD_ASSET_SYNC_CAPABILITIES,
|
|
4082
4563
|
async syncAssets(request) {
|
|
4083
|
-
|
|
4564
|
+
let manifest = [];
|
|
4565
|
+
if (options.manifestPath) manifest = await deps.readManifest(options.manifestPath);
|
|
4566
|
+
else if (options.driveUrl) manifest = (await new CryptPadDriveClient({
|
|
4567
|
+
url: options.driveUrl,
|
|
4568
|
+
password: options.password
|
|
4569
|
+
}).listDriveItems(request.signal)).filter((item) => item.type !== "folder" && !item.url.includes("/sheet/")).map((item) => ({
|
|
4570
|
+
assetId: item.id || item.title,
|
|
4571
|
+
relativePath: item.path || item.title,
|
|
4572
|
+
sourceUrl: item.url
|
|
4573
|
+
}));
|
|
4084
4574
|
const downloaded = [];
|
|
4085
4575
|
const updated = [];
|
|
4086
4576
|
const skipped = [];
|
|
@@ -4417,6 +4907,16 @@ function createInputProvider(providerId, options) {
|
|
|
4417
4907
|
function createOutputProvider(providerId, options) {
|
|
4418
4908
|
switch (providerId) {
|
|
4419
4909
|
case "google-sheets": return createGoogleSheetsOutputProvider(options);
|
|
4910
|
+
case "cryptpad-sheet":
|
|
4911
|
+
case "cryptpad": return createCryptPadSheetOutputProvider({
|
|
4912
|
+
url: typeof options.url === "string" ? options.url : void 0,
|
|
4913
|
+
password: typeof options.password === "string" ? options.password : void 0,
|
|
4914
|
+
override: options.override === true,
|
|
4915
|
+
localeMapping: typeof options.localeMapping === "object" && options.localeMapping !== null ? options.localeMapping : void 0,
|
|
4916
|
+
providerId: typeof options.providerId === "string" ? options.providerId : void 0,
|
|
4917
|
+
displayName: typeof options.displayName === "string" ? options.displayName : void 0,
|
|
4918
|
+
timeoutMs: typeof options.timeoutMs === "number" ? options.timeoutMs : void 0
|
|
4919
|
+
});
|
|
4420
4920
|
case "cryptpad-workspace":
|
|
4421
4921
|
if (typeof options.filePath !== "string" || options.filePath.trim().length === 0) throw new Error("cryptpad-workspace output provider requires a non-empty \"filePath\" option.");
|
|
4422
4922
|
return createCryptPadWorkspaceOutputProvider({
|
|
@@ -4432,6 +4932,17 @@ function createOutputProvider(providerId, options) {
|
|
|
4432
4932
|
function createSyncProvider(providerId, options) {
|
|
4433
4933
|
switch (providerId) {
|
|
4434
4934
|
case "google-sheets": return createGoogleSheetsSyncProvider(options);
|
|
4935
|
+
case "cryptpad-sheet":
|
|
4936
|
+
case "cryptpad": return createCryptPadSheetSyncProvider({
|
|
4937
|
+
url: typeof options.url === "string" ? options.url : void 0,
|
|
4938
|
+
password: typeof options.password === "string" ? options.password : void 0,
|
|
4939
|
+
override: options.override === true,
|
|
4940
|
+
conflictPolicy: options.conflictPolicy === "remote-wins" || options.conflictPolicy === "local-wins" || options.conflictPolicy === "manual" ? options.conflictPolicy : void 0,
|
|
4941
|
+
localeMapping: typeof options.localeMapping === "object" && options.localeMapping !== null ? options.localeMapping : void 0,
|
|
4942
|
+
providerId: typeof options.providerId === "string" ? options.providerId : void 0,
|
|
4943
|
+
displayName: typeof options.displayName === "string" ? options.displayName : void 0,
|
|
4944
|
+
timeoutMs: typeof options.timeoutMs === "number" ? options.timeoutMs : void 0
|
|
4945
|
+
});
|
|
4435
4946
|
case "cryptpad-workspace":
|
|
4436
4947
|
if (typeof options.filePath !== "string" || options.filePath.trim().length === 0) throw new Error("cryptpad-workspace sync provider requires a non-empty \"filePath\" option.");
|
|
4437
4948
|
return createCryptPadWorkspaceSyncProvider({
|
|
@@ -4447,13 +4958,18 @@ function createSyncProvider(providerId, options) {
|
|
|
4447
4958
|
}
|
|
4448
4959
|
function createAssetSyncProvider(providerId, options) {
|
|
4449
4960
|
switch (providerId) {
|
|
4450
|
-
case "cryptpad-assets":
|
|
4451
|
-
|
|
4961
|
+
case "cryptpad-assets": {
|
|
4962
|
+
const hasManifest = typeof options.manifestPath === "string" && options.manifestPath.trim().length > 0;
|
|
4963
|
+
const hasDrive = typeof options.driveUrl === "string" && options.driveUrl.trim().length > 0;
|
|
4964
|
+
if (!hasManifest && !hasDrive) throw new Error("cryptpad-assets sync provider requires a non-empty \"manifestPath\" option.");
|
|
4452
4965
|
return createCryptPadAssetSyncProvider({
|
|
4453
|
-
manifestPath: options.manifestPath,
|
|
4966
|
+
manifestPath: typeof options.manifestPath === "string" ? options.manifestPath : void 0,
|
|
4967
|
+
driveUrl: typeof options.driveUrl === "string" ? options.driveUrl : void 0,
|
|
4968
|
+
password: typeof options.password === "string" ? options.password : void 0,
|
|
4454
4969
|
providerId: typeof options.providerId === "string" ? options.providerId : void 0,
|
|
4455
4970
|
displayName: typeof options.displayName === "string" ? options.displayName : void 0
|
|
4456
4971
|
});
|
|
4972
|
+
}
|
|
4457
4973
|
default: throw new Error(`Unsupported asset sync provider: "${providerId}"`);
|
|
4458
4974
|
}
|
|
4459
4975
|
}
|