super-productivity/electron/shared-with-frontend/preload-api.ts
Johannes Millan 95d3b212bc
fix(electron): gate Jira IPC behind a one-shot capability (#9008)
* docs(plugins): add microsoft 365 calendar provider plan

* fix(jira): gate Electron requests behind one-shot capability

Claim privileged Jira IPC before plugin startup and return responses through invoke instead of a broadcast event. Keep arbitrary HTTP(S) Jira hosts supported while rejecting redirects and bounding request resources.

* fix(jira): enforce Electron request capability

Bind privileged Jira IPC to a main-issued renderer-document token and strip raw Electron events from renderer callbacks. Scope image authentication by origin, base path, and resource type while preserving safe redirects and legacy configurations.

* fix(electron): handle payload-only IPC lifecycle

Clear Jira image authentication before replacement and when a new renderer document claims the capability. Parse before-close IDs from payload-only events so pending sync and finish-day hooks can complete.

* fix(electron): address Jira IPC capability review findings

- electron.effects: read ANY_FILE_DOWNLOADED payload at [0] after the
  payload-only IPC refactor (was [1], now undefined -> TypeError on every
  download); guard against a malformed payload
- jira-capability: rotate the token on re-register so a renderer reload
  that reuses the WebFrameMain object is not permanently locked out of
  Jira; invalidates any stale token
- document that the one-shot consumption order, not the bypassable
  main-frame check, is the real capability boundary
- jira-electron-bridge: skip the no-op clearImgHeaders IPC round-trip
  when image auth was never set up (non-Jira detail-panel open/close)
- jira-api: route a synchronous _toElectronRequestInit throw through
  _handleResponse instead of leaking a dangling request-log entry

* test(electron): cover ANY_FILE_DOWNLOADED payload parsing

Extract parseDownloadedFilePayload from ElectronEffects and add a
regression spec pinning the payload-only shape ([file], not [event,
file]) that caused a TypeError on every download. Hardened against
non-array input surfaced by the new test.

* fix(electron): restore Node ambient globals for frontend build
2026-07-14 21:02:27 +02:00

52 lines
1.8 KiB
TypeScript

import { IPC, IPCEventValue } from './ipc-events.const';
import {
JiraCapabilityEnvelope,
JiraElectronApi,
JiraElectronResponse,
} from './jira-request.model';
import { createOneShotApiConsumer } from './one-shot-api-consumer';
type Invoke = (channel: IPCEventValue, ...args: unknown[]) => Promise<unknown>;
type PayloadListener = (...args: unknown[]) => void;
export const toPayloadOnlyIpcListener =
(listener: PayloadListener) =>
(_event: unknown, ...args: unknown[]): void =>
listener(...args);
export const createJiraPreloadApiConsumer = (
invoke: Invoke,
): (() => JiraElectronApi | null) => {
// Register before renderer code runs. The token remains inside this isolated
// preload closure and is never exposed through the context bridge.
const capabilityTokenPromise = invoke(IPC.JIRA_REGISTER_CAPABILITY).then((token) =>
typeof token === 'string' && token.length > 0 ? token : null,
);
const invokeWithCapability = async <TResponse, TPayload>(
channel: IPCEventValue,
payload: TPayload,
): Promise<TResponse> => {
const capabilityToken = await capabilityTokenPromise;
if (!capabilityToken) {
throw new Error('Jira Electron API is unavailable');
}
const envelope: JiraCapabilityEnvelope<TPayload> = {
capabilityToken,
payload,
};
return invoke(channel, envelope) as Promise<TResponse>;
};
return createOneShotApiConsumer<JiraElectronApi>(() => ({
makeRequest: (request) =>
invokeWithCapability<JiraElectronResponse, typeof request>(
IPC.JIRA_MAKE_REQUEST_EVENT,
request,
),
setupImgHeaders: (config) =>
invokeWithCapability<void, typeof config>(IPC.JIRA_SETUP_IMG_HEADERS, config),
clearImgHeaders: () =>
invokeWithCapability<void, null>(IPC.JIRA_CLEAR_IMG_HEADERS, null),
}));
};