@el-j/google-sheet-translations 3.0.0-beta.7 → 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 +304 -80
- package/dist/index.js +304 -80
- package/dist/migration/migrateV3.d.ts +3 -0
- package/dist/migration/migrateV3.d.ts.map +1 -1
- package/dist/providers/cryptpad/client.d.ts +48 -7
- package/dist/providers/cryptpad/client.d.ts.map +1 -1
- package/dist/providers/cryptpad/crypto.d.ts +8 -4
- package/dist/providers/cryptpad/crypto.d.ts.map +1 -1
- package/dist/providers/cryptpad/fullProvider.d.ts +2 -4
- package/dist/providers/cryptpad/fullProvider.d.ts.map +1 -1
- package/dist/providers/cryptpad/netflux.d.ts +1 -0
- package/dist/providers/cryptpad/netflux.d.ts.map +1 -1
- package/dist/providers/cryptpad/sheetOutputProvider.d.ts.map +1 -1
- package/dist/providers/cryptpad/sheetParser.d.ts +25 -2
- package/dist/providers/cryptpad/sheetParser.d.ts.map +1 -1
- package/dist/providers/google/providers.d.ts +1 -0
- package/dist/providers/google/providers.d.ts.map +1 -1
- package/dist/providers/runtime.d.ts.map +1 -1
- package/dist/providers/syncEngine.d.ts +1 -0
- package/dist/providers/syncEngine.d.ts.map +1 -1
- package/dist-cli/cryptpad.mjs +308 -77
- package/dist-cli/run-provider.mjs +304 -80
- package/package.json +4 -1
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
|
|
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)
|
|
3316
|
-
* from a CryptPad pad seed and optional password
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
@@ -3403,30 +3417,45 @@ function fetchChannelHistory(wsUrl, channelHex, cryptKey, options = {}) {
|
|
|
3403
3417
|
let quietTimer = null;
|
|
3404
3418
|
let overallTimeout = null;
|
|
3405
3419
|
let requestedHistory = false;
|
|
3420
|
+
let abortListener = null;
|
|
3421
|
+
let settled = false;
|
|
3406
3422
|
const cleanup = () => {
|
|
3407
3423
|
if (quietTimer) clearTimeout(quietTimer);
|
|
3408
3424
|
if (overallTimeout) clearTimeout(overallTimeout);
|
|
3425
|
+
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
3409
3426
|
try {
|
|
3410
3427
|
ws.close();
|
|
3411
3428
|
} catch {}
|
|
3412
3429
|
};
|
|
3413
|
-
const finish = () => {
|
|
3430
|
+
const finish = (result) => {
|
|
3431
|
+
if (settled) return;
|
|
3432
|
+
settled = true;
|
|
3414
3433
|
cleanup();
|
|
3415
|
-
resolve(
|
|
3434
|
+
resolve(result);
|
|
3435
|
+
};
|
|
3436
|
+
const rejectOnce = (error) => {
|
|
3437
|
+
if (settled) return;
|
|
3438
|
+
settled = true;
|
|
3439
|
+
cleanup();
|
|
3440
|
+
reject(error);
|
|
3416
3441
|
};
|
|
3417
3442
|
const resetQuietTimer = () => {
|
|
3418
3443
|
if (quietTimer) clearTimeout(quietTimer);
|
|
3419
|
-
quietTimer = setTimeout(
|
|
3444
|
+
quietTimer = setTimeout(() => {
|
|
3445
|
+
if (decryptedMessages.length > 0) finish(decryptedMessages);
|
|
3446
|
+
else rejectOnce(/* @__PURE__ */ new Error(`Timeout after ${timeoutMs}ms waiting for CryptPad channel "${channelHex}" history.`));
|
|
3447
|
+
}, quietPeriodMs);
|
|
3420
3448
|
};
|
|
3421
3449
|
overallTimeout = setTimeout(() => {
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
else reject(/* @__PURE__ */ new Error(`Timeout after ${timeoutMs}ms waiting for CryptPad channel "${channelHex}" history.`));
|
|
3450
|
+
if (decryptedMessages.length > 0) finish(decryptedMessages);
|
|
3451
|
+
else rejectOnce(/* @__PURE__ */ new Error(`Timeout after ${timeoutMs}ms waiting for CryptPad channel "${channelHex}" history.`));
|
|
3425
3452
|
}, timeoutMs);
|
|
3426
|
-
if (signal)
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3453
|
+
if (signal) {
|
|
3454
|
+
abortListener = () => {
|
|
3455
|
+
rejectOnce(/* @__PURE__ */ new Error("Operation aborted"));
|
|
3456
|
+
};
|
|
3457
|
+
signal.addEventListener("abort", abortListener);
|
|
3458
|
+
}
|
|
3430
3459
|
ws.onopen = () => {
|
|
3431
3460
|
ws.send(JSON.stringify([
|
|
3432
3461
|
seq++,
|
|
@@ -3447,11 +3476,7 @@ function fetchChannelHistory(wsUrl, channelHex, cryptKey, options = {}) {
|
|
|
3447
3476
|
seq++,
|
|
3448
3477
|
"MSG",
|
|
3449
3478
|
peerId,
|
|
3450
|
-
JSON.stringify([
|
|
3451
|
-
"GET_HISTORY",
|
|
3452
|
-
channelHex,
|
|
3453
|
-
{}
|
|
3454
|
-
])
|
|
3479
|
+
JSON.stringify(["GET_FULL_HISTORY", channelHex])
|
|
3455
3480
|
]));
|
|
3456
3481
|
resetQuietTimer();
|
|
3457
3482
|
}
|
|
@@ -3461,7 +3486,11 @@ function fetchChannelHistory(wsUrl, channelHex, cryptKey, options = {}) {
|
|
|
3461
3486
|
if (!encStr && typeof msg[3] === "string" && msg[3].length > 50) encStr = msg[3];
|
|
3462
3487
|
try {
|
|
3463
3488
|
const parsed = JSON.parse(encStr);
|
|
3464
|
-
if (Array.isArray(parsed) && parsed[
|
|
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];
|
|
3465
3494
|
} catch {}
|
|
3466
3495
|
if (typeof encStr === "string" && encStr.length > 30) {
|
|
3467
3496
|
const decrypted = decryptCryptPadPayload(encStr, cryptKey);
|
|
@@ -3475,7 +3504,9 @@ function fetchChannelHistory(wsUrl, channelHex, cryptKey, options = {}) {
|
|
|
3475
3504
|
};
|
|
3476
3505
|
ws.onerror = (err) => {
|
|
3477
3506
|
cleanup();
|
|
3478
|
-
|
|
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}`));
|
|
3479
3510
|
};
|
|
3480
3511
|
});
|
|
3481
3512
|
}
|
|
@@ -3492,21 +3523,37 @@ function broadcastChannelMessage(wsUrl, channelHex, cryptKey, message, options =
|
|
|
3492
3523
|
let seq = 1;
|
|
3493
3524
|
let timeout = null;
|
|
3494
3525
|
let sent = false;
|
|
3526
|
+
let abortListener = null;
|
|
3527
|
+
let settled = false;
|
|
3495
3528
|
const cleanup = () => {
|
|
3496
3529
|
if (timeout) clearTimeout(timeout);
|
|
3530
|
+
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
3497
3531
|
try {
|
|
3498
3532
|
ws.close();
|
|
3499
3533
|
} catch {}
|
|
3500
3534
|
};
|
|
3501
|
-
|
|
3535
|
+
const resolveOnce = () => {
|
|
3536
|
+
if (settled) return;
|
|
3537
|
+
settled = true;
|
|
3502
3538
|
cleanup();
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3539
|
+
resolve();
|
|
3540
|
+
};
|
|
3541
|
+
const rejectOnce = (error) => {
|
|
3542
|
+
if (settled) return;
|
|
3543
|
+
settled = true;
|
|
3507
3544
|
cleanup();
|
|
3508
|
-
reject(
|
|
3509
|
-
}
|
|
3545
|
+
reject(error);
|
|
3546
|
+
};
|
|
3547
|
+
timeout = setTimeout(() => {
|
|
3548
|
+
if (sent) resolveOnce();
|
|
3549
|
+
else rejectOnce(/* @__PURE__ */ new Error(`Timeout after ${timeoutMs}ms broadcasting message to CryptPad channel "${channelHex}".`));
|
|
3550
|
+
}, timeoutMs);
|
|
3551
|
+
if (signal) {
|
|
3552
|
+
abortListener = () => {
|
|
3553
|
+
rejectOnce(/* @__PURE__ */ new Error("Operation aborted"));
|
|
3554
|
+
};
|
|
3555
|
+
signal.addEventListener("abort", abortListener);
|
|
3556
|
+
}
|
|
3510
3557
|
ws.onopen = () => {
|
|
3511
3558
|
ws.send(JSON.stringify([
|
|
3512
3559
|
seq++,
|
|
@@ -3522,7 +3569,7 @@ function broadcastChannelMessage(wsUrl, channelHex, cryptKey, message, options =
|
|
|
3522
3569
|
const [, peerId, cmd] = msg;
|
|
3523
3570
|
if (cmd === "JOIN" && typeof peerId === "string" && !sent) {
|
|
3524
3571
|
sent = true;
|
|
3525
|
-
const encrypted = encryptCryptPadPayload(message, cryptKey);
|
|
3572
|
+
const encrypted = encryptCryptPadPayload(message, cryptKey, options.signKey);
|
|
3526
3573
|
ws.send(JSON.stringify([
|
|
3527
3574
|
seq++,
|
|
3528
3575
|
"MSG",
|
|
@@ -3530,15 +3577,15 @@ function broadcastChannelMessage(wsUrl, channelHex, cryptKey, message, options =
|
|
|
3530
3577
|
encrypted
|
|
3531
3578
|
]));
|
|
3532
3579
|
setTimeout(() => {
|
|
3533
|
-
|
|
3534
|
-
resolve();
|
|
3580
|
+
resolveOnce();
|
|
3535
3581
|
}, 350);
|
|
3536
3582
|
}
|
|
3537
3583
|
} catch {}
|
|
3538
3584
|
};
|
|
3539
3585
|
ws.onerror = (err) => {
|
|
3540
|
-
|
|
3541
|
-
|
|
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}`));
|
|
3542
3589
|
};
|
|
3543
3590
|
});
|
|
3544
3591
|
}
|
|
@@ -3554,6 +3601,12 @@ function colIndexToLetter(colIndex) {
|
|
|
3554
3601
|
}
|
|
3555
3602
|
return letter;
|
|
3556
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
|
+
}
|
|
3557
3610
|
/**
|
|
3558
3611
|
* Parses cell coordinates from a reference like "A1", "Sheet1!B2", or "$C$5".
|
|
3559
3612
|
*/
|
|
@@ -3572,18 +3625,51 @@ function parseCellRef(ref) {
|
|
|
3572
3625
|
function extractOnlyOfficeChannelId(metadataMessages) {
|
|
3573
3626
|
for (const raw of metadataMessages) try {
|
|
3574
3627
|
const parsed = JSON.parse(raw);
|
|
3628
|
+
if (parsed?.content?.channel && typeof parsed.content.channel === "string") return parsed.content.channel;
|
|
3575
3629
|
if (Array.isArray(parsed) && Array.isArray(parsed[1])) {
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
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
|
+
}
|
|
3580
3637
|
}
|
|
3581
|
-
if (
|
|
3582
|
-
|
|
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
|
+
}
|
|
3583
3648
|
return null;
|
|
3584
3649
|
}
|
|
3585
3650
|
/**
|
|
3586
3651
|
* Parses OnlyOffice incremental binary change records into cell coordinates and text values.
|
|
3652
|
+
*
|
|
3653
|
+
* ### OnlyOffice Document Server / CryptPad Binary Protocol Specification
|
|
3654
|
+
* OnlyOffice collaborative document changes are broadcast over Netflux real-time channels.
|
|
3655
|
+
* Each message contains a `changes` array of transaction objects.
|
|
3656
|
+
*
|
|
3657
|
+
* Inside each transaction, `change` is formatted with a command prefix followed by base64 binary:
|
|
3658
|
+
* `asc_<version>;<base64_binary_payload>` (e.g., `asc_1;<base64>`).
|
|
3659
|
+
*
|
|
3660
|
+
* The decoded binary stream represents OnlyOffice document AST changes:
|
|
3661
|
+
* - Text and identifiers are serialized as length-prefixed UTF-16LE strings.
|
|
3662
|
+
* - The marker byte `0x08` indicates the start of a UTF-16LE string entry, followed by a 4-byte
|
|
3663
|
+
* little-endian unsigned integer (`UInt32LE`) specifying byte length, followed by the UTF-16LE payload.
|
|
3664
|
+
*
|
|
3665
|
+
* Two layout patterns are supported:
|
|
3666
|
+
* - **Case A (Explicit coordinate reference)**: A string matching coordinate syntax (e.g. `A1` or `sheet!B2`)
|
|
3667
|
+
* followed closely (within 30 bytes) by another `0x08` marker holding the cell's UTF-16LE text value.
|
|
3668
|
+
* - **Case B (Binary header coordinates)**: Header packets store 0-based column index `c1` at byte 14
|
|
3669
|
+
* and row index `r1` at byte 18 as `UInt32LE`, followed by string values starting at byte 40+.
|
|
3670
|
+
*
|
|
3671
|
+
* @param rtMessages - Decrypted raw change messages retrieved from the OnlyOffice RT Netflux channel.
|
|
3672
|
+
* @returns A mapping of cell coordinates (`A1` or `sheet!A1`) to cell text values.
|
|
3587
3673
|
*/
|
|
3588
3674
|
function parseOnlyOfficeChanges(rtMessages) {
|
|
3589
3675
|
const cells = {};
|
|
@@ -3616,12 +3702,20 @@ function parseOnlyOfficeChanges(rtMessages) {
|
|
|
3616
3702
|
break;
|
|
3617
3703
|
}
|
|
3618
3704
|
}
|
|
3619
|
-
if (buf.length >=
|
|
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 {
|
|
3620
3714
|
const c1 = buf.readUInt32LE(14);
|
|
3621
3715
|
const r1 = buf.readUInt32LE(18);
|
|
3622
3716
|
if (r1 < 1e5 && c1 < 200) {
|
|
3623
3717
|
const cellRef = `${colIndexToLetter(c1)}${r1 + 1}`;
|
|
3624
|
-
if (
|
|
3718
|
+
if (str.trim().length > 0 && !str.includes("!")) cells[cellRef] = str;
|
|
3625
3719
|
}
|
|
3626
3720
|
} catch {}
|
|
3627
3721
|
}
|
|
@@ -3690,11 +3784,70 @@ function convertCellsToMultiSheetRows(cells, defaultSheet = "Sheet1") {
|
|
|
3690
3784
|
return result;
|
|
3691
3785
|
}
|
|
3692
3786
|
/**
|
|
3693
|
-
* Encodes a single cell update record into OnlyOffice binary format
|
|
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.
|
|
3694
3790
|
*/
|
|
3695
|
-
function encodeOnlyOfficeCellRecord(cellRef, value) {
|
|
3696
|
-
|
|
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
|
+
}
|
|
3697
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");
|
|
3698
3851
|
const refHeader = Buffer.alloc(5);
|
|
3699
3852
|
refHeader[0] = 8;
|
|
3700
3853
|
refHeader.writeUInt32LE(refBuf.length, 1);
|
|
@@ -3702,6 +3855,7 @@ function encodeOnlyOfficeCellRecord(cellRef, value) {
|
|
|
3702
3855
|
valHeader[0] = 8;
|
|
3703
3856
|
valHeader.writeUInt32LE(valBuf.length, 1);
|
|
3704
3857
|
return Buffer.concat([
|
|
3858
|
+
nativeBuf,
|
|
3705
3859
|
refHeader,
|
|
3706
3860
|
refBuf,
|
|
3707
3861
|
valHeader,
|
|
@@ -3712,12 +3866,19 @@ function encodeOnlyOfficeCellRecord(cellRef, value) {
|
|
|
3712
3866
|
* Formats a list of cell updates into an OnlyOffice change transaction JSON string.
|
|
3713
3867
|
*/
|
|
3714
3868
|
function buildOnlyOfficeChangePayload(updates) {
|
|
3869
|
+
const txOpen = Buffer.from("0a0000000129000000ff00000000", "hex");
|
|
3715
3870
|
const records = updates.map((u) => {
|
|
3716
3871
|
const colStr = typeof u.col === "number" ? colIndexToLetter(u.col) : u.col.toUpperCase();
|
|
3717
3872
|
return encodeOnlyOfficeCellRecord(`${u.sheet && u.sheet.trim().length > 0 ? `${u.sheet.trim()}!` : ""}${colStr}${u.row}`, u.value);
|
|
3718
3873
|
});
|
|
3719
|
-
const changeEntry = `asc_1;${Buffer.concat(records).toString("base64")}`;
|
|
3720
|
-
return JSON.stringify({
|
|
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
|
+
});
|
|
3721
3882
|
}
|
|
3722
3883
|
//#endregion
|
|
3723
3884
|
//#region src/providers/cryptpad/client.ts
|
|
@@ -3732,36 +3893,73 @@ var CryptPadClient = class {
|
|
|
3732
3893
|
this.password = options.password ?? process.env.CRYPTPAD_PASSWORD;
|
|
3733
3894
|
this.websocketUrl = options.websocketUrl;
|
|
3734
3895
|
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
3896
|
+
this.onProgress = options.onProgress;
|
|
3735
3897
|
if (this.parsedUrl.isPasswordProtected && !this.password) throw new Error(`CryptPad pad "${this.parsedUrl.cleanUrl}" is password protected, but no password was provided. Set "password" option or CRYPTPAD_PASSWORD environment variable.`);
|
|
3736
3898
|
}
|
|
3737
|
-
/**
|
|
3899
|
+
/**
|
|
3900
|
+
* Gets or derives the symmetric key and primary Netflux channel ID.
|
|
3901
|
+
*
|
|
3902
|
+
* @returns The derived CryptPad channel hex ID and 32-byte TweetNaCl secretbox key.
|
|
3903
|
+
*/
|
|
3738
3904
|
getKeys() {
|
|
3739
3905
|
if (!this.derivedKeys) this.derivedKeys = deriveCryptPadKeys(this.parsedUrl.seed, this.password);
|
|
3740
3906
|
return this.derivedKeys;
|
|
3741
3907
|
}
|
|
3742
|
-
/**
|
|
3908
|
+
/**
|
|
3909
|
+
* Resolves the Netflux WebSocket endpoint to connect to.
|
|
3910
|
+
*
|
|
3911
|
+
* @param signal - Optional AbortSignal to cancel connection discovery.
|
|
3912
|
+
* @returns The WebSocket endpoint URL (wss:// or ws://).
|
|
3913
|
+
*/
|
|
3743
3914
|
async getWebsocketUrl(signal) {
|
|
3744
3915
|
if (this.websocketUrl) return this.websocketUrl;
|
|
3745
3916
|
return resolveCryptPadWebsocketUrl(this.parsedUrl.origin, signal);
|
|
3746
3917
|
}
|
|
3747
3918
|
/**
|
|
3748
3919
|
* Initializes the OnlyOffice RT channel for a brand-new CryptPad sheet that has never
|
|
3749
|
-
* been opened in a browser.
|
|
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.
|
|
3920
|
+
* been opened in a browser.
|
|
3752
3921
|
*
|
|
3753
|
-
*
|
|
3922
|
+
* ### CryptPad Protocol Details
|
|
3923
|
+
* CryptPad pads use Netflux channels for real-time collaboration. The main pad channel
|
|
3924
|
+
* stores document metadata. When a spreadsheet pad is opened in OnlyOffice, the OnlyOffice
|
|
3925
|
+
* web wrapper creates a secondary Netflux channel for real-time document change frames
|
|
3926
|
+
* and registers it in the pad's metadata channel.
|
|
3927
|
+
*
|
|
3928
|
+
* The metadata update is formatted as an edit sequence:
|
|
3929
|
+
* `[sequenceNumber, [[offset, length, innerJsonString]]]`
|
|
3930
|
+
* where `sequenceNumber` is 1, and `innerJsonString` is:
|
|
3931
|
+
* `{"content":{"channel":"<32-hex-channel-id>"}}`.
|
|
3932
|
+
*
|
|
3933
|
+
* Broadcasting this envelope over the main channel headlessly replicates OnlyOffice's
|
|
3934
|
+
* first-open handshake without requiring a browser or headless browser session.
|
|
3935
|
+
*
|
|
3936
|
+
* @param signal - Optional AbortSignal to cancel the initialization.
|
|
3937
|
+
* @returns The newly allocated 32-character hexadecimal RT channel ID.
|
|
3754
3938
|
*/
|
|
3755
3939
|
async initializeRtChannel(signal) {
|
|
3756
3940
|
const wsUrl = await this.getWebsocketUrl(signal);
|
|
3757
3941
|
const { channelHex, cryptKey } = this.getKeys();
|
|
3758
3942
|
const newRtChannel = crypto.randomBytes(16).toString("hex");
|
|
3759
|
-
const innerJson = JSON.stringify({
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
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
|
+
]), {
|
|
3765
3963
|
timeoutMs: this.timeoutMs,
|
|
3766
3964
|
signal
|
|
3767
3965
|
});
|
|
@@ -3770,6 +3968,9 @@ var CryptPadClient = class {
|
|
|
3770
3968
|
/**
|
|
3771
3969
|
* Fetches the complete sheet data, including raw cell coordinate mappings,
|
|
3772
3970
|
* multi-sheet tabs, and structured rows formatted for translation ingestion.
|
|
3971
|
+
*
|
|
3972
|
+
* @param signal - Optional AbortSignal to cancel the fetch.
|
|
3973
|
+
* @returns The structured {@link CryptPadSheetResult} containing grids, rows, and metadata.
|
|
3773
3974
|
*/
|
|
3774
3975
|
async fetchSheetData(signal) {
|
|
3775
3976
|
const wsUrl = await this.getWebsocketUrl(signal);
|
|
@@ -3810,6 +4011,10 @@ var CryptPadClient = class {
|
|
|
3810
4011
|
/**
|
|
3811
4012
|
* Directly fetches tabular translation rows `[ { key: '...', en: '...', de: '...' } ]`.
|
|
3812
4013
|
* If `sheetName` is provided, returns rows for that specific tab.
|
|
4014
|
+
*
|
|
4015
|
+
* @param sheetName - Optional tab/sheet name to extract rows from.
|
|
4016
|
+
* @param signal - Optional AbortSignal to cancel the operation.
|
|
4017
|
+
* @returns An array of {@link SheetRow} objects.
|
|
3813
4018
|
*/
|
|
3814
4019
|
async fetchSheetRows(sheetName, signal) {
|
|
3815
4020
|
const result = await this.fetchSheetData(signal);
|
|
@@ -3820,30 +4025,42 @@ var CryptPadClient = class {
|
|
|
3820
4025
|
* Broadcasts cell updates to the OnlyOffice real-time collaboration channel.
|
|
3821
4026
|
* If the sheet has never been opened in a browser (RT channel is None), it is
|
|
3822
4027
|
* automatically initialized headlessly — no browser required.
|
|
4028
|
+
*
|
|
4029
|
+
* @param updates - Array of cell updates containing coordinates and text values.
|
|
4030
|
+
* @param signal - Optional AbortSignal to cancel the broadcast.
|
|
4031
|
+
* @param existingRtChannel - Optional pre-fetched RT channel ID to avoid refetching sheet data.
|
|
3823
4032
|
*/
|
|
3824
|
-
async sendCellUpdates(updates, signal) {
|
|
4033
|
+
async sendCellUpdates(updates, signal, existingRtChannel) {
|
|
3825
4034
|
if (updates.length === 0) return;
|
|
3826
|
-
let rtChannel =
|
|
4035
|
+
let rtChannel = existingRtChannel;
|
|
4036
|
+
if (!rtChannel) rtChannel = (await this.fetchSheetData(signal)).metadata.rtChannelId;
|
|
3827
4037
|
if (!rtChannel) {
|
|
3828
|
-
|
|
4038
|
+
this.onProgress?.("No OnlyOffice RT channel found. Initializing headlessly (no browser required)...");
|
|
3829
4039
|
rtChannel = await this.initializeRtChannel(signal);
|
|
3830
|
-
|
|
4040
|
+
this.onProgress?.(`RT channel initialized: ${rtChannel}`);
|
|
3831
4041
|
await new Promise((r) => setTimeout(r, 800));
|
|
3832
4042
|
}
|
|
3833
4043
|
const wsUrl = await this.getWebsocketUrl(signal);
|
|
3834
|
-
const { cryptKey } = this.getKeys();
|
|
4044
|
+
const { cryptKey, signKey } = this.getKeys();
|
|
3835
4045
|
const payload = buildOnlyOfficeChangePayload(updates);
|
|
3836
4046
|
await broadcastChannelMessage(wsUrl, rtChannel, cryptKey, payload, {
|
|
3837
4047
|
timeoutMs: this.timeoutMs,
|
|
3838
|
-
signal
|
|
4048
|
+
signal,
|
|
4049
|
+
signKey
|
|
3839
4050
|
});
|
|
3840
4051
|
}
|
|
3841
4052
|
/**
|
|
3842
4053
|
* Updates or appends rows in a target sheet tab, creating new cells as needed.
|
|
4054
|
+
*
|
|
4055
|
+
* @param sheetName - Target sheet tab name (e.g. 'i18n' or 'common').
|
|
4056
|
+
* @param rows - Array of translation rows to write.
|
|
4057
|
+
* @param options - Write options: override existing non-empty values and optional signal.
|
|
4058
|
+
* @returns The total number of cell update records sent.
|
|
3843
4059
|
*/
|
|
3844
4060
|
async writeSheetRows(sheetName, rows, options = {}) {
|
|
3845
4061
|
if (rows.length === 0) return 0;
|
|
3846
|
-
const
|
|
4062
|
+
const data = await this.fetchSheetData(options.signal);
|
|
4063
|
+
const existingRows = data.sheets[sheetName]?.rows ?? [];
|
|
3847
4064
|
let keyColName = "var";
|
|
3848
4065
|
const firstExisting = existingRows[0];
|
|
3849
4066
|
const firstIncoming = rows[0];
|
|
@@ -3892,7 +4109,7 @@ var CryptPadClient = class {
|
|
|
3892
4109
|
}
|
|
3893
4110
|
}
|
|
3894
4111
|
}
|
|
3895
|
-
await this.sendCellUpdates(updates, options.signal);
|
|
4112
|
+
await this.sendCellUpdates(updates, options.signal, data.metadata.rtChannelId);
|
|
3896
4113
|
return updates.length;
|
|
3897
4114
|
}
|
|
3898
4115
|
};
|
|
@@ -3979,7 +4196,7 @@ const CRYPTPAD_SHEET_OUTPUT_CAPABILITIES = createCapabilitySet({ writeTables: tr
|
|
|
3979
4196
|
* Converts nested TranslationData `[locale][sheet][key] = value` into
|
|
3980
4197
|
* tabular rows `Record<sheetName, SheetRow[]>`.
|
|
3981
4198
|
*/
|
|
3982
|
-
function convertTranslationsToSheetRows(translations, localeMapping = {}, keyColumnName = "
|
|
4199
|
+
function convertTranslationsToSheetRows(translations, localeMapping = {}, keyColumnName = "var") {
|
|
3983
4200
|
const reverseMapping = {};
|
|
3984
4201
|
for (const [header, norm] of Object.entries(localeMapping)) reverseMapping[norm] = header;
|
|
3985
4202
|
const sheetRowsMap = {};
|
|
@@ -4018,13 +4235,14 @@ function createCryptPadSheetOutputProvider(options = {}) {
|
|
|
4018
4235
|
timeoutMs: options.timeoutMs
|
|
4019
4236
|
});
|
|
4020
4237
|
const effectiveMapping = options.localeMapping ?? payload.localeMapping ?? {};
|
|
4021
|
-
const sheetRowsMap = convertTranslationsToSheetRows(payload.translations, effectiveMapping, options.keyColumnName ?? "
|
|
4238
|
+
const sheetRowsMap = convertTranslationsToSheetRows(payload.translations, effectiveMapping, options.keyColumnName ?? "var");
|
|
4022
4239
|
const updatedSheets = [];
|
|
4023
4240
|
let totalUpdatedCells = 0;
|
|
4024
4241
|
for (const [sheetName, rows] of Object.entries(sheetRowsMap)) {
|
|
4025
4242
|
const count = await client.writeSheetRows(sheetName, rows, { override: options.override ?? false });
|
|
4026
4243
|
updatedSheets.push(sheetName);
|
|
4027
4244
|
totalUpdatedCells += count;
|
|
4245
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
4028
4246
|
}
|
|
4029
4247
|
return {
|
|
4030
4248
|
wroteFiles: [url],
|
|
@@ -4400,7 +4618,7 @@ function createCryptPadWorkspaceOutputProvider(options, depsOverrides = {}) {
|
|
|
4400
4618
|
displayName: options.displayName ?? "CryptPad Workspace Output",
|
|
4401
4619
|
capabilities: CRYPTPAD_WORKSPACE_OUTPUT_CAPABILITIES,
|
|
4402
4620
|
async writeTranslations(payload) {
|
|
4403
|
-
const snapshot = await deps.readSnapshot(options.filePath
|
|
4621
|
+
const snapshot = await deps.readSnapshot(options.filePath);
|
|
4404
4622
|
assertRevision(snapshot, options.expectedRevision);
|
|
4405
4623
|
const merged = mergeTranslations$1(snapshot.translations, payload.translations);
|
|
4406
4624
|
const nextRevision = snapshot.revision + 1;
|
|
@@ -4411,7 +4629,7 @@ function createCryptPadWorkspaceOutputProvider(options, depsOverrides = {}) {
|
|
|
4411
4629
|
...snapshot.metadata,
|
|
4412
4630
|
lastWriteProvider: "cryptpad-workspace-output"
|
|
4413
4631
|
}
|
|
4414
|
-
}
|
|
4632
|
+
});
|
|
4415
4633
|
return {
|
|
4416
4634
|
wroteFiles: [options.filePath],
|
|
4417
4635
|
metadata: {
|
|
@@ -4422,6 +4640,14 @@ function createCryptPadWorkspaceOutputProvider(options, depsOverrides = {}) {
|
|
|
4422
4640
|
}
|
|
4423
4641
|
};
|
|
4424
4642
|
}
|
|
4643
|
+
/**
|
|
4644
|
+
* Builds the {@link BuildSyncPlanInput} from a {@link TranslationSyncPayload}.
|
|
4645
|
+
* Falls back to using `payload.remoteTranslations` as the base if `payload.metadata.baseTranslations`
|
|
4646
|
+
* is not supplied.
|
|
4647
|
+
*
|
|
4648
|
+
* @param payload - The incoming sync payload containing local, remote, and optional base translations.
|
|
4649
|
+
* @returns The structured three-way sync plan input.
|
|
4650
|
+
*/
|
|
4425
4651
|
function buildSyncInput(payload) {
|
|
4426
4652
|
return {
|
|
4427
4653
|
baseTranslations: payload.metadata?.baseTranslations ?? payload.remoteTranslations,
|
|
@@ -4447,7 +4673,7 @@ function createCryptPadWorkspaceSyncProvider(options, depsOverrides = {}) {
|
|
|
4447
4673
|
displayName: options.displayName ?? "CryptPad Workspace Sync",
|
|
4448
4674
|
capabilities: CRYPTPAD_WORKSPACE_SYNC_CAPABILITIES,
|
|
4449
4675
|
async syncTranslations(payload) {
|
|
4450
|
-
const snapshot = await deps.readSnapshot(options.filePath
|
|
4676
|
+
const snapshot = await deps.readSnapshot(options.filePath);
|
|
4451
4677
|
assertRevision(snapshot, options.expectedRevision ?? (Number.isFinite(payload.metadata?.expectedRevision) ? Number(payload.metadata?.expectedRevision) : void 0));
|
|
4452
4678
|
const resolution = resolveSyncPlan(buildSyncInput(payload), options.conflictPolicy ?? "manual");
|
|
4453
4679
|
const nextRevision = snapshot.revision + 1;
|
|
@@ -4459,7 +4685,7 @@ function createCryptPadWorkspaceSyncProvider(options, depsOverrides = {}) {
|
|
|
4459
4685
|
lastSyncProvider: "cryptpad-workspace-sync",
|
|
4460
4686
|
policy: resolution.policy
|
|
4461
4687
|
}
|
|
4462
|
-
}
|
|
4688
|
+
});
|
|
4463
4689
|
return {
|
|
4464
4690
|
changedKeys: resolution.appliedLocalChanges,
|
|
4465
4691
|
skippedKeys: resolution.skippedConflicts,
|
|
@@ -4921,7 +5147,6 @@ function createOutputProvider(providerId, options) {
|
|
|
4921
5147
|
if (typeof options.filePath !== "string" || options.filePath.trim().length === 0) throw new Error("cryptpad-workspace output provider requires a non-empty \"filePath\" option.");
|
|
4922
5148
|
return createCryptPadWorkspaceOutputProvider({
|
|
4923
5149
|
filePath: options.filePath,
|
|
4924
|
-
authToken: typeof options.authToken === "string" ? options.authToken : void 0,
|
|
4925
5150
|
expectedRevision: typeof options.expectedRevision === "number" ? options.expectedRevision : void 0,
|
|
4926
5151
|
providerId: typeof options.providerId === "string" ? options.providerId : void 0,
|
|
4927
5152
|
displayName: typeof options.displayName === "string" ? options.displayName : void 0
|
|
@@ -4947,7 +5172,6 @@ function createSyncProvider(providerId, options) {
|
|
|
4947
5172
|
if (typeof options.filePath !== "string" || options.filePath.trim().length === 0) throw new Error("cryptpad-workspace sync provider requires a non-empty \"filePath\" option.");
|
|
4948
5173
|
return createCryptPadWorkspaceSyncProvider({
|
|
4949
5174
|
filePath: options.filePath,
|
|
4950
|
-
authToken: typeof options.authToken === "string" ? options.authToken : void 0,
|
|
4951
5175
|
expectedRevision: typeof options.expectedRevision === "number" ? options.expectedRevision : void 0,
|
|
4952
5176
|
conflictPolicy: options.conflictPolicy === "remote-wins" || options.conflictPolicy === "local-wins" || options.conflictPolicy === "manual" ? options.conflictPolicy : void 0,
|
|
4953
5177
|
providerId: typeof options.providerId === "string" ? options.providerId : void 0,
|