From 27897fca3acbf2f5775ed3f94f428961c4551a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Rame=CC=81?= Date: Thu, 30 Jul 2026 19:13:55 +0200 Subject: [PATCH] wip TO REVIEW --- src/frontend/apps/drive/eslint.config.mjs | 8 ++ .../encryption/fetchRegisteredKeys.ts | 28 +++++ .../encryption/oo-bridge/OOEditor.tsx | 102 +++++++++++------- .../features/encryption/oo-bridge/types.ts | 54 ++++++++++ 4 files changed, 152 insertions(+), 40 deletions(-) create mode 100644 src/frontend/apps/drive/src/features/encryption/fetchRegisteredKeys.ts diff --git a/src/frontend/apps/drive/eslint.config.mjs b/src/frontend/apps/drive/eslint.config.mjs index 93ffbd0c..8286b8c6 100644 --- a/src/frontend/apps/drive/eslint.config.mjs +++ b/src/frontend/apps/drive/eslint.config.mjs @@ -15,6 +15,14 @@ const eslintConfig = [ rules: { "react-hooks/exhaustive-deps": "off", "@next/next/no-img-element": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], }, }, ]; diff --git a/src/frontend/apps/drive/src/features/encryption/fetchRegisteredKeys.ts b/src/frontend/apps/drive/src/features/encryption/fetchRegisteredKeys.ts new file mode 100644 index 00000000..693261b2 --- /dev/null +++ b/src/frontend/apps/drive/src/features/encryption/fetchRegisteredKeys.ts @@ -0,0 +1,28 @@ +/** + * Adapter over `vaultClient.fetchPublicKeys`, which returns the full + * registry directory keyed by userId. Drive only ever needs the + * encryption public key and its version, and only for users that + * actually hold a published key — this collapses the directory back to + * the two parallel maps Drive's call sites consume, preserving the + * original semantics (users without an encryption key simply do not + * appear in either map). + */ +export async function fetchRegisteredKeys(userIds: string[]): Promise<{ + publicKeys: Record; + versions: Record; +}> { + const vaultClient = window.__driveVaultClient; + if (!vaultClient) { + throw new Error('Vault client not available'); + } + const users = await vaultClient.fetchPublicKeys(userIds); + const publicKeys: Record = {}; + const versions: Record = {}; + for (const [id, u] of Object.entries(users)) { + if (u.encryptionPublicKey) { + publicKeys[id] = u.encryptionPublicKey; + versions[id] = u.version; + } + } + return { publicKeys, versions }; +} diff --git a/src/frontend/apps/drive/src/features/encryption/oo-bridge/OOEditor.tsx b/src/frontend/apps/drive/src/features/encryption/oo-bridge/OOEditor.tsx index 5070ac78..3e6cd6fd 100644 --- a/src/frontend/apps/drive/src/features/encryption/oo-bridge/OOEditor.tsx +++ b/src/frontend/apps/drive/src/features/encryption/oo-bridge/OOEditor.tsx @@ -25,7 +25,6 @@ import { convertFromInternal, convertFromInternalToPdf, } from './x2tConverter'; -import { extractMedia } from './odtBundle'; import { EXTENSION_TO_X2T_TYPE } from './types'; import { getEffectiveMimetype } from '@/features/explorer/utils/mimeTypes'; import { KeyMismatchPanel } from '@/features/encryption/KeyMismatchPanel'; @@ -60,9 +59,6 @@ import { acquireCellLock, releaseCellLock, releaseAllUserLocks, - acquireSaveLock, - releaseSaveLock, - isSaveLocked, resetAllLocks, } from './locks'; import { @@ -97,7 +93,9 @@ declare global { } interface OOEditorInstance { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary connectMockServer: (callbacks: any) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary sendMessageToOO: (msg: any) => void; asc_nativeGetFile: () => Uint8Array; destroyEditor: () => void; @@ -171,6 +169,7 @@ function sendToEditorGuarded(msg: { type?: string } & Record) { ); return; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary sendToEditor(msg as any); } @@ -195,7 +194,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { detail: string; } | null>(null); const [saveRetrying, setSaveRetrying] = useState(false); - const [isConnected, setIsConnected] = useState(false); + const [, setIsConnected] = useState(false); const [relayFailed, setRelayFailed] = useState(false); /** * Bumped when the relay tells us our history cursor is stale (close @@ -235,23 +234,6 @@ export const OOEditor = ({ item }: OOEditorProps) => { // excluded from leader election: a crashed editor can't run // `forceSave`. Cleared when the peer disconnects or we rejoin. const crashedPeersRef = useRef>(new Set()); - /** - * Set true when a `peer:needs-save` arrives from a peer recovering - * from a crash. The next `save:committed` broadcast we observe is - * then the save that the crashed peer asked for, and we must reinit - * ourselves from the fresh S3 bytes to keep our OO load-phase IDs - * aligned with the recovering peer's fresh load. Without this - * realignment, saveChanges we broadcast after the peer's reload - * reference internal ids they no longer recognise (and vice-versa) - * — the OT engine then silently drops those patches or crashes. - * - * Cleared after triggering reinit, or by a timeout if the save - * never lands (leader dead, network wedged, etc). - */ - const pendingRecoveryReinitRef = useRef(false); - const pendingRecoveryReinitTimerRef = useRef | null>(null); /** * Peer state transfer (CryptPad-style). We keep the .bin OO loaded * from AND every saveChanges frame observed since that load, all @@ -324,6 +306,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { const awaitingPeerStateRef = useRef(false); // Reference to the inner OO iframe's window — used to register media in // g_oDocumentUrls when inbound saveChanges envelopes carry inline images. + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary const innerWindowRef = useRef(null); /** * Remote saveChanges envelopes that arrived before OO finished loading @@ -526,6 +509,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { const ooIframe = document.querySelector( 'iframe[name="frameEditor"]' ) as HTMLIFrameElement | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary const innerWindow = ooIframe?.contentWindow as any; const innerEditor = innerWindow?.editor || innerWindow?.editorCell; // OO exposes a few aliases; walk them defensively. @@ -540,6 +524,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { ); return; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary const preview = content.slice(0, 5).map((el: any, i: number) => { let text = ''; try { @@ -572,7 +557,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { // is a quick way to eyeball whether the IDs referenced in the live // saveChanges resolve on both peers. if (typeof window !== 'undefined') { - (window as any).__driveLogIds = (label?: string) => + window.__driveLogIds = (label?: string) => logParagraphIds(label ?? 'manual'); } @@ -1079,11 +1064,17 @@ export const OOEditor = ({ item }: OOEditorProps) => { return bytes.buffer; }); + // Version of the user's encryption key this wrap was produced against, + // stored on the access row. Default to 1 for the current single-version + // reality when the field is absent. + const keyVersion = item.encryption_public_key_version_for_user ?? 1; + // optimizeMemory: hot path — decrypted document bins are routinely // several MB and `encryptedBuffer` is not reused after this call. const { data: decryptedBuffer } = await vaultClient.decryptWithKey( encryptedBuffer, entryKeyBytes.buffer, + keyVersion, encryptedKeyChain.length > 0 ? encryptedKeyChain : undefined, { optimizeMemory: true } ); @@ -1099,12 +1090,14 @@ export const OOEditor = ({ item }: OOEditorProps) => { setState('converting'); const converted = await convertToInternal(decryptedBuffer, filename); const bin: Uint8Array = converted.bin; - let extractedImages: Array<{ name: string; data: Uint8Array }> = + const extractedImages: Array<{ name: string; data: Uint8Array }> = converted.images; if (cancelled) return; // Step 4: Create blob URL and load editor - const blob = new Blob([bin], { + // Copy into a fresh ArrayBuffer-backed view so the value is a + // valid `BlobPart` (a Uint8Array over a `SharedArrayBuffer` is not). + const blob = new Blob([new Uint8Array(bin)], { type: 'application/octet-stream', }); // `let` so we can swap in a peer-provided blob after the @@ -1113,10 +1106,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { // Initialize participant tracking // Use user.sub (OIDC subject) — same ID the relay server uses - const localUser = initLocalUser( - user.sub!, - user.full_name || user.email - ); + initLocalUser(user.sub!, user.full_name || user.email); const uniqueOOId = getUniqueOOId(); resetPatchIndex(0); @@ -1201,6 +1191,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { // encryptedRelay.ts), so they broadcast end-to-end // encrypted between editor peers. chat: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary } as any, // Single source of truth for permissions — there used to be // TWO `permissions` blocks on this object, the second one @@ -1222,6 +1213,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { editCommentAuthorOnly: false, macros: 'none', protect: false, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary } as any, }, events: { @@ -1235,6 +1227,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { const ooIframe = document.querySelector( 'iframe[name="frameEditor"]' ) as HTMLIFrameElement | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary const innerWindow = ooIframe?.contentWindow as any; const docUrls = innerWindow?.AscCommon?.g_oDocumentUrls; if (docUrls && extractedImages.length > 0) { @@ -1385,6 +1378,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { const ooIframe = document.querySelector( 'iframe[name="frameEditor"]' ) as HTMLIFrameElement | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary const innerWindow = ooIframe?.contentWindow as any; const editor = innerWindow?.editor; // Resolve the prototype that holds `asc_PasteData` @@ -1395,9 +1389,12 @@ export const OOEditor = ({ item }: OOEditorProps) => { const proto = editor && Object.getPrototypeOf(editor); if (proto && typeof proto.asc_PasteData === 'function') { const original = proto.asc_PasteData; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary if (!(original as any).__driveSlidePasteWrapped) { proto.asc_PasteData = function ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary this: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary ...args: any[] ) { // args[0] is the format kind: 4 = OO internal @@ -1408,6 +1405,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { formatKind === 8 && typeof this.Paste === 'function' ) { + // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; setTimeout(() => { try { @@ -1423,6 +1421,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { } return original.apply(this, args); }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary (proto.asc_PasteData as any).__driveSlidePasteWrapped = true; } @@ -1485,6 +1484,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { const ooIframe = document.querySelector( 'iframe[name="frameEditor"]' ) as HTMLIFrameElement | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary const innerWindow = ooIframe?.contentWindow as any; const innerEditor = innerWindow?.editor || innerWindow?.editorCell; @@ -1521,6 +1521,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { const ooIframe = document.querySelector( 'iframe[name="frameEditor"]' ) as HTMLIFrameElement | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary const innerWindow = ooIframe?.contentWindow as any; innerWindowRef.current = innerWindow; @@ -1879,7 +1880,9 @@ export const OOEditor = ({ item }: OOEditorProps) => { ) { let imgCounter = 0; innerWindow.AscCommon.sendImgUrls = function ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary api: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary images: any[], callback: ( data: Array<{ url: string; path: string }> @@ -2026,16 +2029,17 @@ export const OOEditor = ({ item }: OOEditorProps) => { proto._downloadAsUsingServer = function ( _actionType: unknown, _options: unknown, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary oAdditionalData: any, _dataContainer: unknown, downloadType: unknown ) { - (window as any).__driveDownloadCtx = { + window.__driveDownloadCtx = { title: oAdditionalData?.title, outputformat: oAdditionalData?.outputformat, downloadType, }; - // eslint-disable-next-line prefer-rest-params + // eslint-disable-next-line prefer-rest-params, @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary return original.apply(this, arguments as any); }; proto.__driveDownloadPatched = true; @@ -2051,6 +2055,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { setErrorCode(null); setState('error'); }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary } as any, }; @@ -2065,7 +2070,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { // passes null for userName, panel passes the username) and the same // user ends up with two different colors. Returning a deterministic // color per id keeps both call sites in sync. - (window as any).APP = (window as any).APP || {}; + const app = (window.APP ??= {}); // Theme hooks: OO's ChangeTheme calls window.parent.APP.changeTheme // when the LOCAL user picks a slide theme. The patched SDK in this // bundle also calls APP.remoteTheme() when applying a remote peer's @@ -2073,10 +2078,10 @@ export const OOEditor = ({ item }: OOEditorProps) => { // actual theme data already propagates through the normal OT // saveChanges pipeline (ChangeTheme creates a history point), so // these callbacks are purely notification hooks — no-ops are safe. - (window as any).APP.changeTheme = (_indexTheme: number) => { + app.changeTheme = (_indexTheme: number) => { /* no-op — theme change propagates via OT saveChanges */ }; - (window as any).APP.remoteTheme = () => { + app.remoteTheme = () => { /* no-op — remote theme applied by OO internally via ChangeTheme(id, null, true) */ }; // Image insertion: OO calls window.parent.APP.UploadImageFiles when @@ -2122,7 +2127,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { }; // Drag-and-drop / paste path. - (window as any).APP.UploadImageFiles = ( + app.UploadImageFiles = ( files: FileList | File[], _documentId: string, _documentUserId: string, @@ -2163,7 +2168,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { odp: 'application/vnd.oasis.opendocument.presentation', }; - (window as any).APP.printPdf = ( + app.printPdf = ( dataContainer: { data: Uint8Array | ArrayBuffer | null }, callback: (result: unknown) => void ) => { @@ -2172,6 +2177,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { const ooIframe = document.querySelector( 'iframe[name="frameEditor"]' ) as HTMLIFrameElement | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary const innerWindow = ooIframe?.contentWindow as any; const innerEditor = innerWindow?.editor || innerWindow?.editorCell; @@ -2192,6 +2198,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { binBuffer = rawBin.buffer; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary const ctx = (window as any).__driveDownloadCtx ?? {}; const title: string = ctx.title || `document.${x2tExtension}`; const ext = ( @@ -2337,7 +2344,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { // Modal "Insert image / Replace image / From file" path: opens a // file picker and forwards the result through the same converter. - (window as any).APP.AddImage = ( + app.AddImage = ( successCb: (res: { url: string; name: string }) => void, errorCb?: (err?: unknown) => void ) => { @@ -2360,7 +2367,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { input.click(); }; - (window as any).APP.getUserColor = (userId: string) => { + app.getUserColor = (userId: string) => { if (!userId) return null; // 16 colors evenly distributed on the HSL wheel — same id → same color. let hash = 0; @@ -2523,6 +2530,10 @@ export const OOEditor = ({ item }: OOEditorProps) => { encryptedSymmetricKey: entryKeyBytes.buffer, encryptedKeyChain: encryptedKeyChain.length > 0 ? encryptedKeyChain : [], + // Version of the user's encryption key the entry-point wrap was + // produced against, stored on the access row. Default to 1 for the + // current single-version reality when the field is absent. + keyVersion: item.encryption_public_key_version_for_user ?? 1, callbacks: { onSaveChanges: (_userId, message, media) => { // Media is accumulated the same way in both phases so it @@ -2579,6 +2590,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { message as Record ); withIncomingOTGate(() => + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary sendToEditorGuarded(message as any) ); }; @@ -2613,6 +2625,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { } } addRemoteUser(userId, userName, userId); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary sendToEditor(buildConnectStateMessage() as any); }, onPeerLeave: userId => { @@ -2664,6 +2677,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { })), }); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary sendToEditor(buildConnectStateMessage() as any); }, onRoomState: peers => { @@ -2694,6 +2708,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { addRemoteUser(peer.userId, peer.userName, peer.userId); } } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary sendToEditor(buildConnectStateMessage() as any); // If other editable peers are already in the room, ask @@ -2759,6 +2774,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { user: remoteOOId || userId, time: Date.now(), }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary const guid = (block as any)?.guid; const key = guid || @@ -2771,6 +2787,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { sendToEditor({ type: 'getLock', locks, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary } as any); if (!documentReadyRef.current) { pendingRemoteChangesRef.current.push(apply); @@ -2796,6 +2813,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { time: Date.now(), }, ], + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary } as any); if (!documentReadyRef.current) { pendingRemoteChangesRef.current.push(apply); @@ -2805,6 +2823,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { }, onMessageBroadcast: (_userId, messages) => { const apply = () => + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary sendToEditorGuarded({ type: 'message', messages } as any); if (!documentReadyRef.current) { pendingRemoteChangesRef.current.push(apply); @@ -2814,6 +2833,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { }, onMetaBroadcast: (_userId, messages) => { const apply = () => + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary sendToEditorGuarded({ type: 'meta', messages } as any); if (!documentReadyRef.current) { pendingRemoteChangesRef.current.push(apply); @@ -3022,6 +3042,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { sendToEditorGuarded({ type: 'getLock', locks, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary } as any); } }, @@ -3228,6 +3249,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { for (const chainMsg of peerDump.chain) { const apply = () => { observeIncomingSaveChanges(chainMsg as Record); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary withIncomingOTGate(() => sendToEditorGuarded(chainMsg as any)); }; handleIncomingSaveChanges( @@ -3377,6 +3399,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { document.querySelector( 'iframe[name="frameEditor"]' ) as HTMLIFrameElement | null + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor bundle boundary )?.contentWindow as any; const innerEditor = innerWindow?.editor || innerWindow?.editorCell; @@ -3536,7 +3559,6 @@ export const OOEditor = ({ item }: OOEditorProps) => { // destroyed instance. setEditorInstance(null); }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [item.id, reinitKey]); if (state === 'error') { @@ -3551,7 +3573,7 @@ export const OOEditor = ({ item }: OOEditorProps) => { if (isKeyMismatch) { return ( ); } diff --git a/src/frontend/apps/drive/src/features/encryption/oo-bridge/types.ts b/src/frontend/apps/drive/src/features/encryption/oo-bridge/types.ts index 34d7400f..120d5cd7 100644 --- a/src/frontend/apps/drive/src/features/encryption/oo-bridge/types.ts +++ b/src/frontend/apps/drive/src/features/encryption/oo-bridge/types.ts @@ -91,6 +91,10 @@ export interface OOConfig { name: string; }; lang: string; + coEditing?: { + mode: 'fast' | 'strict'; + change: boolean; + }; customization?: { chat?: boolean; compactToolbar: boolean; @@ -267,3 +271,53 @@ export const EXTENSION_TO_X2T_TYPE: Record = { ppt: 'presentation', odp: 'presentation', }; + +/** + * Context captured when OnlyOffice routes a download/print through our + * patched `_downloadAsUsingServer`, read back in the `printPdf` hook to + * pick the target format. + */ +export interface DriveDownloadContext { + title?: string; + outputformat?: string; + downloadType?: unknown; +} + +/** + * The `window.APP` bridge object OnlyOffice's `connectMockServer` reads + * from. Only the hooks the drive bridge installs are typed here; the + * signatures are derived from how OnlyOffice invokes each one. + */ +export interface OOAppBridge { + changeTheme?: (indexTheme: number) => void; + remoteTheme?: () => void; + UploadImageFiles?: ( + files: FileList | File[], + documentId: string, + documentUserId: string, + jwt: string, + callback: (err: number | null, urls: string[]) => void + ) => void; + printPdf?: ( + dataContainer: { data: Uint8Array | ArrayBuffer | null }, + callback: (result: unknown) => void + ) => void; + AddImage?: ( + successCb: (res: { url: string; name: string }) => void, + errorCb?: (err?: unknown) => void + ) => void; + getUserColor?: ( + userId: string + ) => { r: number; g: number; b: number; a: number } | null; +} + +// Globals the drive OO bridge itself sets on `window` (as opposed to the +// OnlyOffice-owned `DocsAPI`, declared in OOEditor.tsx). Typed so the +// assignment/read sites need no `any`. +declare global { + interface Window { + __driveLogIds?: (label?: string) => void; + __driveDownloadCtx?: DriveDownloadContext; + APP?: OOAppBridge; + } +}