@el-j/google-sheet-translations 3.0.0-beta.3 → 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 +953 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +964 -4
- package/dist/providers/catalog.d.ts.map +1 -1
- 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 +67 -0
- package/dist/providers/cryptpad/client.d.ts.map +1 -0
- package/dist/providers/cryptpad/crypto.d.ts +42 -0
- package/dist/providers/cryptpad/crypto.d.ts.map +1 -0
- 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 +18 -1
- package/dist/providers/cryptpad/index.d.ts.map +1 -1
- package/dist/providers/cryptpad/netflux.d.ts +26 -0
- package/dist/providers/cryptpad/netflux.d.ts.map +1 -0
- 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 +62 -0
- package/dist/providers/cryptpad/sheetParser.d.ts.map +1 -0
- package/dist/providers/cryptpad/sheetProvider.d.ts +38 -0
- package/dist/providers/cryptpad/sheetProvider.d.ts.map +1 -0
- 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 +946 -8
- package/package.json +23 -9
package/dist/esm/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { Readable } from "node:stream";
|
|
|
10
10
|
import fs$1 from "node:fs/promises";
|
|
11
11
|
import { parse } from "csv-parse/sync";
|
|
12
12
|
import crypto from "node:crypto";
|
|
13
|
+
import nacl from "tweetnacl";
|
|
13
14
|
//#region src/utils/validateEnv.ts
|
|
14
15
|
/**
|
|
15
16
|
* Validates that Google service-account credentials are present.
|
|
@@ -264,6 +265,10 @@ const COMMON_LOCALE_PATTERNS = [
|
|
|
264
265
|
/^[a-z]{2}-[a-z]{2}-[a-z]+$/
|
|
265
266
|
];
|
|
266
267
|
const NON_LOCALE_KEYWORDS = [
|
|
268
|
+
"var",
|
|
269
|
+
"vars",
|
|
270
|
+
"variable",
|
|
271
|
+
"variables",
|
|
267
272
|
"key",
|
|
268
273
|
"keys",
|
|
269
274
|
"id",
|
|
@@ -3260,6 +3265,749 @@ function createCryptPadCsvInputProvider(options, depsOverrides = {}) {
|
|
|
3260
3265
|
};
|
|
3261
3266
|
}
|
|
3262
3267
|
//#endregion
|
|
3268
|
+
//#region src/providers/cryptpad/crypto.ts
|
|
3269
|
+
/** Decodes CryptPad base64 string (which may have '-' instead of '/' and no padding). */
|
|
3270
|
+
function b64Decode(str) {
|
|
3271
|
+
let s = str.replace(/-/g, "/");
|
|
3272
|
+
const pad = s.length % 4;
|
|
3273
|
+
if (pad) s += "=".repeat(4 - pad);
|
|
3274
|
+
return new Uint8Array(Buffer.from(s, "base64"));
|
|
3275
|
+
}
|
|
3276
|
+
/** Encodes bytes to standard base64 string. */
|
|
3277
|
+
function b64Encode(bytes) {
|
|
3278
|
+
return Buffer.from(bytes).toString("base64");
|
|
3279
|
+
}
|
|
3280
|
+
/** Decodes UTF-8 string to Uint8Array. */
|
|
3281
|
+
function decodeUTF8(str) {
|
|
3282
|
+
return new Uint8Array(Buffer.from(str, "utf8"));
|
|
3283
|
+
}
|
|
3284
|
+
/** Encodes Uint8Array to UTF-8 string. */
|
|
3285
|
+
function encodeUTF8(bytes) {
|
|
3286
|
+
return Buffer.from(bytes).toString("utf8");
|
|
3287
|
+
}
|
|
3288
|
+
/**
|
|
3289
|
+
* Parses a CryptPad URL and extracts the app type, mode, seed, and whether it requires a password.
|
|
3290
|
+
* Format: https://{host}/{app}/#/{version}/{app}/{mode}/{seed}[/p/]
|
|
3291
|
+
*/
|
|
3292
|
+
function parsePadUrl(url) {
|
|
3293
|
+
let urlObj;
|
|
3294
|
+
try {
|
|
3295
|
+
urlObj = new URL(url);
|
|
3296
|
+
} catch {
|
|
3297
|
+
throw new Error(`Invalid CryptPad URL: "${url}"`);
|
|
3298
|
+
}
|
|
3299
|
+
const match = urlObj.hash.match(/#\/(\d+)\/([^/]+)\/([^/]+)\/([^/]+)(\/p\/)?/);
|
|
3300
|
+
if (!match) throw new Error(`Unsupported CryptPad URL format: "${url}". Expected "#/<version>/<app>/<mode>/<seed>[/p/]"`);
|
|
3301
|
+
const [, versionStr, app, modeRaw, seed, passwordFlag] = match;
|
|
3302
|
+
const mode = modeRaw === "edit" ? "edit" : "view";
|
|
3303
|
+
const isPasswordProtected = Boolean(passwordFlag);
|
|
3304
|
+
return {
|
|
3305
|
+
origin: urlObj.origin,
|
|
3306
|
+
version: parseInt(versionStr, 10),
|
|
3307
|
+
app,
|
|
3308
|
+
mode,
|
|
3309
|
+
seed,
|
|
3310
|
+
isPasswordProtected,
|
|
3311
|
+
cleanUrl: `${urlObj.origin}/${app}/#/${versionStr}/${app}/${mode}/${seed}${isPasswordProtected ? "/p/" : ""}`
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
/**
|
|
3315
|
+
* Derives the Netflux channel ID (hex string) and symmetric encryption key (32 bytes)
|
|
3316
|
+
* from a CryptPad pad seed and optional password according to CryptPad's createEditCryptor2 / createViewCryptor2 protocol.
|
|
3317
|
+
*/
|
|
3318
|
+
function deriveCryptPadKeys(seedStr, password) {
|
|
3319
|
+
const seed = b64Decode(seedStr);
|
|
3320
|
+
if (!password) {
|
|
3321
|
+
const seed2 = crypto.createHash("sha512").update(seed).digest().subarray(32, 64);
|
|
3322
|
+
const hash2 = crypto.createHash("sha512").update(seed2).digest();
|
|
3323
|
+
return {
|
|
3324
|
+
channelHex: hash2.subarray(0, 16).toString("hex"),
|
|
3325
|
+
cryptKey: new Uint8Array(hash2.subarray(16, 48))
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
const pwBytes = decodeUTF8(password);
|
|
3329
|
+
const superSeed1 = Buffer.concat([Buffer.from(pwBytes), Buffer.from(seed)]);
|
|
3330
|
+
const seed2 = crypto.createHash("sha512").update(superSeed1).digest().subarray(32, 64);
|
|
3331
|
+
const superSeed2 = Buffer.concat([Buffer.from(pwBytes), Buffer.from(seed2)]);
|
|
3332
|
+
const hash2 = crypto.createHash("sha512").update(superSeed2).digest();
|
|
3333
|
+
return {
|
|
3334
|
+
channelHex: hash2.subarray(0, 16).toString("hex"),
|
|
3335
|
+
cryptKey: new Uint8Array(hash2.subarray(16, 48))
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
/**
|
|
3339
|
+
* Decrypts an encrypted CryptPad message payload formatted as `nonce|ciphertext`
|
|
3340
|
+
* (or optionally prefixed with a 64-byte Ed25519 signature from historyKeeper).
|
|
3341
|
+
*/
|
|
3342
|
+
function decryptCryptPadPayload(payload, cryptKey) {
|
|
3343
|
+
if (!payload || typeof payload !== "string") return null;
|
|
3344
|
+
const candidates = [];
|
|
3345
|
+
if (payload.includes("|")) candidates.push(payload);
|
|
3346
|
+
const rawBytes = b64Decode(payload);
|
|
3347
|
+
if (rawBytes.length > 64) {
|
|
3348
|
+
const strWithoutSig = encodeUTF8(rawBytes.subarray(64));
|
|
3349
|
+
if (strWithoutSig.includes("|")) candidates.push(strWithoutSig);
|
|
3350
|
+
}
|
|
3351
|
+
for (const cand of candidates) {
|
|
3352
|
+
const parts = cand.split("|");
|
|
3353
|
+
if (parts.length === 2) try {
|
|
3354
|
+
const nonce = b64Decode(parts[0]);
|
|
3355
|
+
const cipher = b64Decode(parts[1]);
|
|
3356
|
+
if (nonce.length === 24) {
|
|
3357
|
+
const opened = nacl.secretbox.open(cipher, nonce, cryptKey);
|
|
3358
|
+
if (opened) return encodeUTF8(opened);
|
|
3359
|
+
}
|
|
3360
|
+
} catch {}
|
|
3361
|
+
}
|
|
3362
|
+
return null;
|
|
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
|
+
}
|
|
3374
|
+
//#endregion
|
|
3375
|
+
//#region src/providers/cryptpad/netflux.ts
|
|
3376
|
+
/**
|
|
3377
|
+
* Discovers the real WebSocket URL for a CryptPad instance by querying its `/api/config` endpoint.
|
|
3378
|
+
*/
|
|
3379
|
+
async function resolveCryptPadWebsocketUrl(origin, signal) {
|
|
3380
|
+
try {
|
|
3381
|
+
const configUrl = `${origin.replace(/\/$/, "")}/api/config`;
|
|
3382
|
+
const res = await fetch(configUrl, { signal });
|
|
3383
|
+
if (res.ok) {
|
|
3384
|
+
const match = (await res.text()).match(/"websocketPath":\s*"([^"]+)"/);
|
|
3385
|
+
if (match && match[1]) return match[1];
|
|
3386
|
+
}
|
|
3387
|
+
} catch {}
|
|
3388
|
+
const urlObj = new URL(origin);
|
|
3389
|
+
return `${urlObj.protocol === "https:" ? "wss:" : "ws:"}//${urlObj.host}/cryptpad_websocket`;
|
|
3390
|
+
}
|
|
3391
|
+
/**
|
|
3392
|
+
* Connects to the CryptPad Netflux WebSocket server, joins a channel,
|
|
3393
|
+
* requests its complete history from the historyKeeper peer, decrypts all messages,
|
|
3394
|
+
* and returns the decrypted message strings.
|
|
3395
|
+
*/
|
|
3396
|
+
function fetchChannelHistory(wsUrl, channelHex, cryptKey, options = {}) {
|
|
3397
|
+
const { timeoutMs = 8e3, quietPeriodMs = 1200, signal } = options;
|
|
3398
|
+
return new Promise((resolve, reject) => {
|
|
3399
|
+
if (signal?.aborted) return reject(/* @__PURE__ */ new Error("Operation aborted"));
|
|
3400
|
+
const ws = new WebSocket(wsUrl);
|
|
3401
|
+
let seq = 1;
|
|
3402
|
+
const decryptedMessages = [];
|
|
3403
|
+
let quietTimer = null;
|
|
3404
|
+
let overallTimeout = null;
|
|
3405
|
+
let requestedHistory = false;
|
|
3406
|
+
const cleanup = () => {
|
|
3407
|
+
if (quietTimer) clearTimeout(quietTimer);
|
|
3408
|
+
if (overallTimeout) clearTimeout(overallTimeout);
|
|
3409
|
+
try {
|
|
3410
|
+
ws.close();
|
|
3411
|
+
} catch {}
|
|
3412
|
+
};
|
|
3413
|
+
const finish = () => {
|
|
3414
|
+
cleanup();
|
|
3415
|
+
resolve(decryptedMessages);
|
|
3416
|
+
};
|
|
3417
|
+
const resetQuietTimer = () => {
|
|
3418
|
+
if (quietTimer) clearTimeout(quietTimer);
|
|
3419
|
+
quietTimer = setTimeout(finish, quietPeriodMs);
|
|
3420
|
+
};
|
|
3421
|
+
overallTimeout = setTimeout(() => {
|
|
3422
|
+
cleanup();
|
|
3423
|
+
if (decryptedMessages.length > 0) resolve(decryptedMessages);
|
|
3424
|
+
else reject(/* @__PURE__ */ new Error(`Timeout after ${timeoutMs}ms waiting for CryptPad channel "${channelHex}" history.`));
|
|
3425
|
+
}, timeoutMs);
|
|
3426
|
+
if (signal) signal.addEventListener("abort", () => {
|
|
3427
|
+
cleanup();
|
|
3428
|
+
reject(/* @__PURE__ */ new Error("Operation aborted"));
|
|
3429
|
+
});
|
|
3430
|
+
ws.onopen = () => {
|
|
3431
|
+
ws.send(JSON.stringify([
|
|
3432
|
+
seq++,
|
|
3433
|
+
"JOIN",
|
|
3434
|
+
channelHex
|
|
3435
|
+
]));
|
|
3436
|
+
};
|
|
3437
|
+
ws.onmessage = (event) => {
|
|
3438
|
+
try {
|
|
3439
|
+
const raw = typeof event.data === "string" ? event.data : event.data.toString();
|
|
3440
|
+
const msg = JSON.parse(raw);
|
|
3441
|
+
if (!Array.isArray(msg)) return;
|
|
3442
|
+
const [, peerId, cmd, , payload] = msg;
|
|
3443
|
+
if (cmd === "JOIN" && typeof peerId === "string" && peerId.length === 16) {
|
|
3444
|
+
if (!requestedHistory) {
|
|
3445
|
+
requestedHistory = true;
|
|
3446
|
+
ws.send(JSON.stringify([
|
|
3447
|
+
seq++,
|
|
3448
|
+
"MSG",
|
|
3449
|
+
peerId,
|
|
3450
|
+
JSON.stringify([
|
|
3451
|
+
"GET_HISTORY",
|
|
3452
|
+
channelHex,
|
|
3453
|
+
{}
|
|
3454
|
+
])
|
|
3455
|
+
]));
|
|
3456
|
+
resetQuietTimer();
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
if (cmd === "MSG") {
|
|
3460
|
+
let encStr = payload;
|
|
3461
|
+
if (!encStr && typeof msg[3] === "string" && msg[3].length > 50) encStr = msg[3];
|
|
3462
|
+
try {
|
|
3463
|
+
const parsed = JSON.parse(encStr);
|
|
3464
|
+
if (Array.isArray(parsed) && parsed[2] === "MSG" && typeof parsed[4] === "string") encStr = parsed[4];
|
|
3465
|
+
} catch {}
|
|
3466
|
+
if (typeof encStr === "string" && encStr.length > 30) {
|
|
3467
|
+
const decrypted = decryptCryptPadPayload(encStr, cryptKey);
|
|
3468
|
+
if (decrypted) {
|
|
3469
|
+
decryptedMessages.push(decrypted);
|
|
3470
|
+
resetQuietTimer();
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
} catch {}
|
|
3475
|
+
};
|
|
3476
|
+
ws.onerror = (err) => {
|
|
3477
|
+
cleanup();
|
|
3478
|
+
reject(/* @__PURE__ */ new Error(`CryptPad WebSocket error: ${String(err)}`));
|
|
3479
|
+
};
|
|
3480
|
+
});
|
|
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
|
+
}
|
|
3545
|
+
//#endregion
|
|
3546
|
+
//#region src/providers/cryptpad/sheetParser.ts
|
|
3547
|
+
/** Converts column index (0-based) to letter: 0 -> 'A', 25 -> 'Z', 26 -> 'AA'. */
|
|
3548
|
+
function colIndexToLetter(colIndex) {
|
|
3549
|
+
let temp = colIndex;
|
|
3550
|
+
let letter = "";
|
|
3551
|
+
while (temp >= 0) {
|
|
3552
|
+
letter = String.fromCharCode(temp % 26 + 65) + letter;
|
|
3553
|
+
temp = Math.floor(temp / 26) - 1;
|
|
3554
|
+
}
|
|
3555
|
+
return letter;
|
|
3556
|
+
}
|
|
3557
|
+
/**
|
|
3558
|
+
* Parses cell coordinates from a reference like "A1", "Sheet1!B2", or "$C$5".
|
|
3559
|
+
*/
|
|
3560
|
+
function parseCellRef(ref) {
|
|
3561
|
+
const match = ref.match(/^(?:([^!]+)!)?\$?([A-Z]+)\$?(\d+)$/i);
|
|
3562
|
+
if (!match) return null;
|
|
3563
|
+
return {
|
|
3564
|
+
sheet: match[1],
|
|
3565
|
+
col: match[2].toUpperCase(),
|
|
3566
|
+
row: parseInt(match[3], 10)
|
|
3567
|
+
};
|
|
3568
|
+
}
|
|
3569
|
+
/**
|
|
3570
|
+
* Extracts the OnlyOffice real-time collaboration channel ID from decrypted pad metadata messages.
|
|
3571
|
+
*/
|
|
3572
|
+
function extractOnlyOfficeChannelId(metadataMessages) {
|
|
3573
|
+
for (const raw of metadataMessages) try {
|
|
3574
|
+
const parsed = JSON.parse(raw);
|
|
3575
|
+
if (Array.isArray(parsed) && Array.isArray(parsed[1])) {
|
|
3576
|
+
for (const edit of parsed[1]) if (Array.isArray(edit) && typeof edit[2] === "string") try {
|
|
3577
|
+
const inner = JSON.parse(edit[2]);
|
|
3578
|
+
if (inner?.content?.channel && typeof inner.content.channel === "string") return inner.content.channel;
|
|
3579
|
+
} catch {}
|
|
3580
|
+
}
|
|
3581
|
+
if (parsed?.content?.channel && typeof parsed.content.channel === "string") return parsed.content.channel;
|
|
3582
|
+
} catch {}
|
|
3583
|
+
return null;
|
|
3584
|
+
}
|
|
3585
|
+
/**
|
|
3586
|
+
* Parses OnlyOffice incremental binary change records into cell coordinates and text values.
|
|
3587
|
+
*/
|
|
3588
|
+
function parseOnlyOfficeChanges(rtMessages) {
|
|
3589
|
+
const cells = {};
|
|
3590
|
+
for (const raw of rtMessages) try {
|
|
3591
|
+
const data = JSON.parse(raw);
|
|
3592
|
+
if (!Array.isArray(data.changes)) continue;
|
|
3593
|
+
for (const ch of data.changes) {
|
|
3594
|
+
if (!ch?.change) continue;
|
|
3595
|
+
let changeStr = ch.change;
|
|
3596
|
+
if (typeof changeStr !== "string") continue;
|
|
3597
|
+
try {
|
|
3598
|
+
changeStr = JSON.parse(changeStr);
|
|
3599
|
+
} catch {}
|
|
3600
|
+
const parts = changeStr.split(";");
|
|
3601
|
+
if (parts.length < 2) continue;
|
|
3602
|
+
const b64 = parts[1];
|
|
3603
|
+
const buf = Buffer.from(b64, "base64");
|
|
3604
|
+
if (buf.length === 0) continue;
|
|
3605
|
+
for (let i = 0; i < buf.length - 5; i++) if (buf[i] === 8) {
|
|
3606
|
+
const strLen = buf.readUInt32LE(i + 1);
|
|
3607
|
+
if (strLen > 0 && strLen < 4e3 && i + 5 + strLen <= buf.length) {
|
|
3608
|
+
const str = buf.subarray(i + 5, i + 5 + strLen).toString("utf16le");
|
|
3609
|
+
const cellMatch = str.match(/^(?:([^!]+)!)?([A-Z]+)(\d+)$/);
|
|
3610
|
+
if (cellMatch) {
|
|
3611
|
+
const cellRef = `${cellMatch[1] ? `${cellMatch[1]}!` : ""}${cellMatch[2]}${cellMatch[3]}`;
|
|
3612
|
+
const searchStart = i + 5 + strLen;
|
|
3613
|
+
for (let j = searchStart; j < Math.min(searchStart + 30, buf.length - 5); j++) if (buf[j] === 8) {
|
|
3614
|
+
const nextLen = buf.readUInt32LE(j + 1);
|
|
3615
|
+
if (nextLen > 0 && nextLen < 1e4 && j + 5 + nextLen <= buf.length) cells[cellRef] = buf.subarray(j + 5, j + 5 + nextLen).toString("utf16le");
|
|
3616
|
+
break;
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
if (buf.length >= 30 && i >= 40) try {
|
|
3620
|
+
const c1 = buf.readUInt32LE(14);
|
|
3621
|
+
const r1 = buf.readUInt32LE(18);
|
|
3622
|
+
if (r1 < 1e5 && c1 < 200) {
|
|
3623
|
+
const cellRef = `${colIndexToLetter(c1)}${r1 + 1}`;
|
|
3624
|
+
if (!cells[cellRef] && str.trim().length > 0 && !str.includes("!")) cells[cellRef] = str;
|
|
3625
|
+
}
|
|
3626
|
+
} catch {}
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
} catch {}
|
|
3631
|
+
return cells;
|
|
3632
|
+
}
|
|
3633
|
+
/**
|
|
3634
|
+
* Converts a grid of cell references (e.g. { A1: 'key', B1: 'en', A2: 'btn.save', B2: 'Save' })
|
|
3635
|
+
* into structured `SheetRow[]` objects using row 1 as the header column keys.
|
|
3636
|
+
*/
|
|
3637
|
+
function convertCellsToSheetRows(cells) {
|
|
3638
|
+
const rowMap = /* @__PURE__ */ new Map();
|
|
3639
|
+
for (const [cellRef, val] of Object.entries(cells)) {
|
|
3640
|
+
const parsed = parseCellRef(cellRef);
|
|
3641
|
+
if (!parsed) continue;
|
|
3642
|
+
const { col, row } = parsed;
|
|
3643
|
+
if (!rowMap.has(row)) rowMap.set(row, /* @__PURE__ */ new Map());
|
|
3644
|
+
rowMap.get(row).set(col, val);
|
|
3645
|
+
}
|
|
3646
|
+
if (rowMap.size === 0) return [];
|
|
3647
|
+
const sortedRowIndices = Array.from(rowMap.keys()).sort((a, b) => a - b);
|
|
3648
|
+
const headerRowIdx = sortedRowIndices[0];
|
|
3649
|
+
const headerCols = rowMap.get(headerRowIdx);
|
|
3650
|
+
const colToHeaderName = /* @__PURE__ */ new Map();
|
|
3651
|
+
for (const [col, colName] of headerCols.entries()) {
|
|
3652
|
+
const trimmed = colName.trim();
|
|
3653
|
+
if (trimmed.length > 0) colToHeaderName.set(col, trimmed);
|
|
3654
|
+
}
|
|
3655
|
+
const resultRows = [];
|
|
3656
|
+
for (const rowIdx of sortedRowIndices) {
|
|
3657
|
+
if (rowIdx === headerRowIdx) continue;
|
|
3658
|
+
const rowCells = rowMap.get(rowIdx);
|
|
3659
|
+
const sheetRow = {};
|
|
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;
|
|
3662
|
+
if (Object.values(sheetRow).some((v) => typeof v === "string" && v.trim().length > 0)) resultRows.push(sheetRow);
|
|
3663
|
+
}
|
|
3664
|
+
return resultRows;
|
|
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
|
+
}
|
|
3722
|
+
//#endregion
|
|
3723
|
+
//#region src/providers/cryptpad/client.ts
|
|
3724
|
+
/**
|
|
3725
|
+
* Headless programmatic client for interacting with CryptPad documents (spreadsheets)
|
|
3726
|
+
* over end-to-end encrypted Netflux WebSockets without requiring a browser or user interaction.
|
|
3727
|
+
*/
|
|
3728
|
+
var CryptPadClient = class {
|
|
3729
|
+
constructor(options) {
|
|
3730
|
+
if (!options.url || typeof options.url !== "string") throw new Error("CryptPadClient requires a valid \"url\" option.");
|
|
3731
|
+
this.parsedUrl = parsePadUrl(options.url);
|
|
3732
|
+
this.password = options.password ?? process.env.CRYPTPAD_PASSWORD;
|
|
3733
|
+
this.websocketUrl = options.websocketUrl;
|
|
3734
|
+
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
3735
|
+
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
|
+
}
|
|
3737
|
+
/** Gets or derives the symmetric key and primary Netflux channel ID. */
|
|
3738
|
+
getKeys() {
|
|
3739
|
+
if (!this.derivedKeys) this.derivedKeys = deriveCryptPadKeys(this.parsedUrl.seed, this.password);
|
|
3740
|
+
return this.derivedKeys;
|
|
3741
|
+
}
|
|
3742
|
+
/** Resolves the Netflux WebSocket endpoint to connect to. */
|
|
3743
|
+
async getWebsocketUrl(signal) {
|
|
3744
|
+
if (this.websocketUrl) return this.websocketUrl;
|
|
3745
|
+
return resolveCryptPadWebsocketUrl(this.parsedUrl.origin, signal);
|
|
3746
|
+
}
|
|
3747
|
+
/**
|
|
3748
|
+
* Fetches the complete sheet data, including raw cell coordinate mappings,
|
|
3749
|
+
* multi-sheet tabs, and structured rows formatted for translation ingestion.
|
|
3750
|
+
*/
|
|
3751
|
+
async fetchSheetData(signal) {
|
|
3752
|
+
const wsUrl = await this.getWebsocketUrl(signal);
|
|
3753
|
+
const { channelHex, cryptKey } = this.getKeys();
|
|
3754
|
+
const rtChannel = extractOnlyOfficeChannelId(await fetchChannelHistory(wsUrl, channelHex, cryptKey, {
|
|
3755
|
+
timeoutMs: this.timeoutMs,
|
|
3756
|
+
signal
|
|
3757
|
+
}));
|
|
3758
|
+
let cells = {};
|
|
3759
|
+
if (rtChannel) cells = parseOnlyOfficeChanges(await fetchChannelHistory(wsUrl, rtChannel, cryptKey, {
|
|
3760
|
+
timeoutMs: this.timeoutMs,
|
|
3761
|
+
signal
|
|
3762
|
+
}));
|
|
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
|
+
};
|
|
3773
|
+
return {
|
|
3774
|
+
url: this.parsedUrl.cleanUrl,
|
|
3775
|
+
cells,
|
|
3776
|
+
rows: defaultRows,
|
|
3777
|
+
sheets: sheetsResult,
|
|
3778
|
+
sheetNames,
|
|
3779
|
+
metadata: {
|
|
3780
|
+
app: this.parsedUrl.app,
|
|
3781
|
+
mode: this.parsedUrl.mode,
|
|
3782
|
+
channelId: channelHex,
|
|
3783
|
+
rtChannelId: rtChannel ?? void 0
|
|
3784
|
+
}
|
|
3785
|
+
};
|
|
3786
|
+
}
|
|
3787
|
+
/**
|
|
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.
|
|
3812
|
+
*/
|
|
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;
|
|
3866
|
+
}
|
|
3867
|
+
};
|
|
3868
|
+
//#endregion
|
|
3869
|
+
//#region src/providers/cryptpad/sheetProvider.ts
|
|
3870
|
+
const CRYPTPAD_SHEET_INPUT_CAPABILITIES = createCapabilitySet({
|
|
3871
|
+
readTables: true,
|
|
3872
|
+
publicReadNoAuth: false
|
|
3873
|
+
});
|
|
3874
|
+
/**
|
|
3875
|
+
* Creates a read-only {@link TranslationInputProvider} that retrieves translation tables
|
|
3876
|
+
* directly from password-protected or public CryptPad spreadsheets (OnlyOffice sheets)
|
|
3877
|
+
* using end-to-end encrypted Netflux WebSockets without requiring a browser or bot.
|
|
3878
|
+
*/
|
|
3879
|
+
function createCryptPadSheetInputProvider(options) {
|
|
3880
|
+
let sources = options.sources ? [...options.sources] : [];
|
|
3881
|
+
if (sources.length === 0) {
|
|
3882
|
+
const singleUrl = options.url ?? process.env.CRYPTPAD_URL;
|
|
3883
|
+
if (singleUrl) sources.push({
|
|
3884
|
+
tableName: options.tableName ?? "translations",
|
|
3885
|
+
url: singleUrl,
|
|
3886
|
+
password: options.password ?? process.env.CRYPTPAD_PASSWORD
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
if (sources.length === 0) throw new Error("CryptPad Sheet provider requires at least one source (either \"sources\" array or \"url\" option/CRYPTPAD_URL).");
|
|
3890
|
+
return {
|
|
3891
|
+
kind: "input",
|
|
3892
|
+
providerId: options.providerId ?? "cryptpad-sheet",
|
|
3893
|
+
displayName: options.displayName ?? "CryptPad Sheet Input (E2EE)",
|
|
3894
|
+
capabilities: CRYPTPAD_SHEET_INPUT_CAPABILITIES,
|
|
3895
|
+
async readTables(request) {
|
|
3896
|
+
const requested = new Set((request.tableNames ?? []).filter(Boolean));
|
|
3897
|
+
const selectedSources = requested.size === 0 ? sources : sources.filter((source) => requested.has(source.tableName));
|
|
3898
|
+
return {
|
|
3899
|
+
tables: (await Promise.all(selectedSources.map(async (source) => {
|
|
3900
|
+
const url = source.url ?? options.url ?? process.env.CRYPTPAD_URL;
|
|
3901
|
+
if (!url) throw new Error(`CryptPad sheet source "${source.tableName}" does not define a URL.`);
|
|
3902
|
+
const sheetData = await new CryptPadClient({
|
|
3903
|
+
url,
|
|
3904
|
+
password: source.password ?? options.password ?? process.env.CRYPTPAD_PASSWORD,
|
|
3905
|
+
timeoutMs: options.timeoutMs
|
|
3906
|
+
}).fetchSheetData(request.signal);
|
|
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({
|
|
3923
|
+
tableId: source.tableId ?? url,
|
|
3924
|
+
tableName: source.tableName,
|
|
3925
|
+
rows: sheetData.rows,
|
|
3926
|
+
sourcePath: url,
|
|
3927
|
+
metadata: {
|
|
3928
|
+
provider: "cryptpad-sheet",
|
|
3929
|
+
channelId: sheetData.metadata.channelId,
|
|
3930
|
+
rtChannelId: sheetData.metadata.rtChannelId,
|
|
3931
|
+
cellCount: Object.keys(sheetData.cells).length
|
|
3932
|
+
}
|
|
3933
|
+
});
|
|
3934
|
+
return sourceTables;
|
|
3935
|
+
}))).flat(),
|
|
3936
|
+
metadata: {
|
|
3937
|
+
provider: "cryptpad-sheet",
|
|
3938
|
+
sourceCount: selectedSources.length
|
|
3939
|
+
}
|
|
3940
|
+
};
|
|
3941
|
+
}
|
|
3942
|
+
};
|
|
3943
|
+
}
|
|
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
|
|
3263
4011
|
//#region src/providers/syncEngine.ts
|
|
3264
4012
|
function pathKey(locale, sheet, key) {
|
|
3265
4013
|
return `${locale}::${sheet}::${key}`;
|
|
@@ -3406,6 +4154,148 @@ function resolveSyncPlan(input, policy) {
|
|
|
3406
4154
|
};
|
|
3407
4155
|
}
|
|
3408
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
|
|
3409
4299
|
//#region src/providers/cryptpad/fullProvider.ts
|
|
3410
4300
|
const CRYPTPAD_WORKSPACE_OUTPUT_CAPABILITIES = createCapabilitySet({ writeTables: true });
|
|
3411
4301
|
const CRYPTPAD_WORKSPACE_SYNC_CAPABILITIES = createCapabilitySet({
|
|
@@ -3552,6 +4442,9 @@ function createCryptPadWorkspaceSyncProvider(options, depsOverrides = {}) {
|
|
|
3552
4442
|
}
|
|
3553
4443
|
//#endregion
|
|
3554
4444
|
//#region src/providers/cryptpad/assetProvider.ts
|
|
4445
|
+
/**
|
|
4446
|
+
* Options for configuring a CryptPad asset synchronization provider.
|
|
4447
|
+
*/
|
|
3555
4448
|
const CRYPTPAD_ASSET_SYNC_CAPABILITIES = createCapabilitySet({
|
|
3556
4449
|
assetSync: true,
|
|
3557
4450
|
discoverByFolder: true
|
|
@@ -3637,7 +4530,16 @@ function createCryptPadAssetSyncProvider(options, depsOverrides = {}) {
|
|
|
3637
4530
|
displayName: options.displayName ?? "CryptPad Asset Sync",
|
|
3638
4531
|
capabilities: CRYPTPAD_ASSET_SYNC_CAPABILITIES,
|
|
3639
4532
|
async syncAssets(request) {
|
|
3640
|
-
|
|
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
|
+
}));
|
|
3641
4543
|
const downloaded = [];
|
|
3642
4544
|
const updated = [];
|
|
3643
4545
|
const skipped = [];
|
|
@@ -3735,6 +4637,16 @@ function createDefaultProviderCatalog() {
|
|
|
3735
4637
|
name: "Google Drive Folder Discovery",
|
|
3736
4638
|
metadata: { authRequired: true }
|
|
3737
4639
|
},
|
|
4640
|
+
{
|
|
4641
|
+
providerId: "cryptpad-sheet",
|
|
4642
|
+
sourceId: "cryptpad-sheet-url",
|
|
4643
|
+
kind: "table",
|
|
4644
|
+
name: "CryptPad Sheet (E2EE OnlyOffice)",
|
|
4645
|
+
metadata: {
|
|
4646
|
+
authRequired: false,
|
|
4647
|
+
passwordSupported: true
|
|
4648
|
+
}
|
|
4649
|
+
},
|
|
3738
4650
|
{
|
|
3739
4651
|
providerId: "cryptpad-csv",
|
|
3740
4652
|
sourceId: "cryptpad-csv-url",
|
|
@@ -3948,12 +4860,32 @@ function createInputProvider(providerId, options) {
|
|
|
3948
4860
|
displayName: typeof options.displayName === "string" ? options.displayName : void 0
|
|
3949
4861
|
});
|
|
3950
4862
|
}
|
|
4863
|
+
case "cryptpad-sheet":
|
|
4864
|
+
case "cryptpad": return createCryptPadSheetInputProvider({
|
|
4865
|
+
sources: Array.isArray(options.sources) ? options.sources : void 0,
|
|
4866
|
+
url: typeof options.url === "string" ? options.url : void 0,
|
|
4867
|
+
password: typeof options.password === "string" ? options.password : void 0,
|
|
4868
|
+
tableName: typeof options.tableName === "string" ? options.tableName : void 0,
|
|
4869
|
+
providerId: typeof options.providerId === "string" ? options.providerId : void 0,
|
|
4870
|
+
displayName: typeof options.displayName === "string" ? options.displayName : void 0,
|
|
4871
|
+
timeoutMs: typeof options.timeoutMs === "number" ? options.timeoutMs : void 0
|
|
4872
|
+
});
|
|
3951
4873
|
default: throw new Error(`Unsupported input provider: "${providerId}"`);
|
|
3952
4874
|
}
|
|
3953
4875
|
}
|
|
3954
4876
|
function createOutputProvider(providerId, options) {
|
|
3955
4877
|
switch (providerId) {
|
|
3956
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
|
+
});
|
|
3957
4889
|
case "cryptpad-workspace":
|
|
3958
4890
|
if (typeof options.filePath !== "string" || options.filePath.trim().length === 0) throw new Error("cryptpad-workspace output provider requires a non-empty \"filePath\" option.");
|
|
3959
4891
|
return createCryptPadWorkspaceOutputProvider({
|
|
@@ -3969,6 +4901,17 @@ function createOutputProvider(providerId, options) {
|
|
|
3969
4901
|
function createSyncProvider(providerId, options) {
|
|
3970
4902
|
switch (providerId) {
|
|
3971
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
|
+
});
|
|
3972
4915
|
case "cryptpad-workspace":
|
|
3973
4916
|
if (typeof options.filePath !== "string" || options.filePath.trim().length === 0) throw new Error("cryptpad-workspace sync provider requires a non-empty \"filePath\" option.");
|
|
3974
4917
|
return createCryptPadWorkspaceSyncProvider({
|
|
@@ -3984,13 +4927,18 @@ function createSyncProvider(providerId, options) {
|
|
|
3984
4927
|
}
|
|
3985
4928
|
function createAssetSyncProvider(providerId, options) {
|
|
3986
4929
|
switch (providerId) {
|
|
3987
|
-
case "cryptpad-assets":
|
|
3988
|
-
|
|
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.");
|
|
3989
4934
|
return createCryptPadAssetSyncProvider({
|
|
3990
|
-
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,
|
|
3991
4938
|
providerId: typeof options.providerId === "string" ? options.providerId : void 0,
|
|
3992
4939
|
displayName: typeof options.displayName === "string" ? options.displayName : void 0
|
|
3993
4940
|
});
|
|
4941
|
+
}
|
|
3994
4942
|
default: throw new Error(`Unsupported asset sync provider: "${providerId}"`);
|
|
3995
4943
|
}
|
|
3996
4944
|
}
|
|
@@ -4307,4 +5255,4 @@ function migrateProjectToV3(options = {}) {
|
|
|
4307
5255
|
//#region src/index.ts
|
|
4308
5256
|
var src_default = getSpreadSheetData;
|
|
4309
5257
|
//#endregion
|
|
4310
|
-
export { CRYPTPAD_ASSET_SYNC_CAPABILITIES, CRYPTPAD_CSV_INPUT_CAPABILITIES, CRYPTPAD_WORKSPACE_OUTPUT_CAPABILITIES, CRYPTPAD_WORKSPACE_SYNC_CAPABILITIES, DEFAULT_IMAGE_EXTENSIONS, DEFAULT_WAIT_SECONDS, EMPTY_PROVIDER_CAPABILITIES, GOOGLE_SHEETS_PROVIDER_CAPABILITIES, GcpApiError, OPERATION_CAPABILITY_REQUIREMENTS, PROVIDER_CAPABILITIES, assertOperationCapabilities, assertRequiredCapabilities, assertValidProviderRuntimeConfig, buildGoogleAuth, buildManifest, buildSyncPlan, columnIndexToLetter, convertFromDataJsonFormat, convertToDataJsonFormat, createAuthClient, createCapabilitySet, createCryptPadAssetSyncProvider, createCryptPadCsvInputProvider, createCryptPadWorkspaceOutputProvider, createCryptPadWorkspaceSyncProvider, createDefaultProviderCatalog, createGoogleSheetsInputProvider, createGoogleSheetsOutputProvider, createGoogleSheetsSyncProvider, createInMemoryProviderCatalogProvider, createLocaleMapping, createProvidersFromRuntimeConfig, createSpreadsheet, src_default as default, entriesToSeedKeys, entriesToTranslationData, exportDoc, filterValidLocales, findLocalChanges, getGoogleTranslateCode, getLanguagePrefix, getLocaleDisplayName, getMultipleSpreadSheetsData, getNormalizedLocaleForHeader, getOriginalHeaderForLocale, getSpreadSheetData, getTranslationSummary, grantDrivePermissions, handleBidirectionalSync, hasRequiredCapabilities, inferLocaleFromDocName, ingestDoc, isValidLocale, manageDriveTranslations, mapLegacyGoogleOptionsToProviderConfig, mergeMultipleTranslationData, mergeSheets, migrateProjectToV3, missingCapabilities, moveSpreadsheetToFolder, normalizeExtension, normalizeLocaleCode, normalizePrivateKey, parseDocContent, processRawRows, readManifest, readPublicSheet, requiresGoogleAuthForRuntimeConfig, resolveLocaleWithFallback, resolveSyncPlan, runProviderPipeline, sanitizeFolderName, scanDriveFolderForDocs, scanDriveFolderForSpreadsheets, setupWIF, slugifyKey, syncDriveImages, updateSpreadsheetWithLocalChanges, validateCredentials, validateEnv, validateImageDirectory, validateProviderRuntimeConfig, wait, walkDirectory, withRetry, writeLanguageDataFile, writeLocalesFile, writeManifest, writeTranslationFiles };
|
|
5258
|
+
export { CRYPTPAD_ASSET_SYNC_CAPABILITIES, CRYPTPAD_CSV_INPUT_CAPABILITIES, CRYPTPAD_SHEET_INPUT_CAPABILITIES, CRYPTPAD_WORKSPACE_OUTPUT_CAPABILITIES, CRYPTPAD_WORKSPACE_SYNC_CAPABILITIES, CryptPadClient, DEFAULT_IMAGE_EXTENSIONS, DEFAULT_WAIT_SECONDS, EMPTY_PROVIDER_CAPABILITIES, GOOGLE_SHEETS_PROVIDER_CAPABILITIES, GcpApiError, OPERATION_CAPABILITY_REQUIREMENTS, PROVIDER_CAPABILITIES, assertOperationCapabilities, assertRequiredCapabilities, assertValidProviderRuntimeConfig, buildGoogleAuth, buildManifest, buildSyncPlan, columnIndexToLetter, convertCellsToSheetRows, convertFromDataJsonFormat, convertToDataJsonFormat, createAuthClient, createCapabilitySet, createCryptPadAssetSyncProvider, createCryptPadCsvInputProvider, createCryptPadSheetInputProvider, createCryptPadWorkspaceOutputProvider, createCryptPadWorkspaceSyncProvider, createDefaultProviderCatalog, createGoogleSheetsInputProvider, createGoogleSheetsOutputProvider, createGoogleSheetsSyncProvider, createInMemoryProviderCatalogProvider, createLocaleMapping, createProvidersFromRuntimeConfig, createSpreadsheet, decryptCryptPadPayload, src_default as default, deriveCryptPadKeys, entriesToSeedKeys, entriesToTranslationData, exportDoc, extractOnlyOfficeChannelId, fetchChannelHistory, filterValidLocales, findLocalChanges, getGoogleTranslateCode, getLanguagePrefix, getLocaleDisplayName, getMultipleSpreadSheetsData, getNormalizedLocaleForHeader, getOriginalHeaderForLocale, getSpreadSheetData, getTranslationSummary, grantDrivePermissions, handleBidirectionalSync, hasRequiredCapabilities, inferLocaleFromDocName, ingestDoc, isValidLocale, manageDriveTranslations, mapLegacyGoogleOptionsToProviderConfig, mergeMultipleTranslationData, mergeSheets, migrateProjectToV3, missingCapabilities, moveSpreadsheetToFolder, normalizeExtension, normalizeLocaleCode, normalizePrivateKey, parseDocContent, parseOnlyOfficeChanges, parsePadUrl, processRawRows, readManifest, readPublicSheet, requiresGoogleAuthForRuntimeConfig, resolveCryptPadWebsocketUrl, resolveLocaleWithFallback, resolveSyncPlan, runProviderPipeline, sanitizeFolderName, scanDriveFolderForDocs, scanDriveFolderForSpreadsheets, setupWIF, slugifyKey, syncDriveImages, updateSpreadsheetWithLocalChanges, validateCredentials, validateEnv, validateImageDirectory, validateProviderRuntimeConfig, wait, walkDirectory, withRetry, writeLanguageDataFile, writeLocalesFile, writeManifest, writeTranslationFiles };
|