mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
* 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
100 lines
2.9 KiB
TypeScript
100 lines
2.9 KiB
TypeScript
import { JiraImageAuthConfig } from './shared-with-frontend/jira-request.model';
|
|
|
|
interface JiraImageAuthState {
|
|
origin: string;
|
|
basePath: string;
|
|
authorization: string;
|
|
}
|
|
|
|
let currentAuth: JiraImageAuthState | null = null;
|
|
|
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
const isNullableString = (value: unknown): value is string | null =>
|
|
value === null || typeof value === 'string';
|
|
const isOptionalNullableString = (value: unknown): value is string | null | undefined =>
|
|
value === undefined || isNullableString(value);
|
|
|
|
const parseImageAuthConfig = (config: unknown): JiraImageAuthConfig => {
|
|
if (!isRecord(config)) {
|
|
throw new Error('Invalid Jira image authentication config');
|
|
}
|
|
const { host, userName, password, usePAT } = config;
|
|
|
|
if (
|
|
typeof host !== 'string' ||
|
|
host.trim().length === 0 ||
|
|
!isNullableString(userName) ||
|
|
!isOptionalNullableString(password) ||
|
|
(usePAT !== undefined && typeof usePAT !== 'boolean')
|
|
) {
|
|
throw new Error('Invalid Jira image authentication config');
|
|
}
|
|
|
|
return {
|
|
host,
|
|
userName,
|
|
password,
|
|
usePAT: usePAT === true,
|
|
};
|
|
};
|
|
|
|
// TODO simplify and do encoding in frontend service
|
|
export const setupRequestHeadersForImages = (rawConfig: unknown): void => {
|
|
currentAuth = null;
|
|
const config = parseImageAuthConfig(rawConfig);
|
|
const parsedUrl = new URL(
|
|
/^[a-z][a-z\d+.-]*:\/\//i.test(config.host as string)
|
|
? (config.host as string)
|
|
: `https://${config.host}`,
|
|
);
|
|
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
throw new Error('Jira URL must use HTTP or HTTPS');
|
|
}
|
|
|
|
const password = config.password || '';
|
|
const encoded = Buffer.from(`${config.userName || ''}:${password}`).toString('base64');
|
|
const trimmedPath = parsedUrl.pathname.replace(/\/+$/, '');
|
|
|
|
currentAuth = {
|
|
origin: parsedUrl.origin,
|
|
basePath: trimmedPath || '/',
|
|
authorization: config.usePAT ? `Bearer ${password}` : `Basic ${encoded}`,
|
|
};
|
|
};
|
|
|
|
export const applyJiraImageAuth = (
|
|
rawUrl: string,
|
|
requestHeaders: Record<string, string>,
|
|
resourceType: string,
|
|
): void => {
|
|
if (!currentAuth || resourceType !== 'image') {
|
|
return;
|
|
}
|
|
|
|
let requestUrl: URL;
|
|
try {
|
|
requestUrl = new URL(rawUrl);
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
const isInBasePath =
|
|
currentAuth.basePath === '/' ||
|
|
requestUrl.pathname === currentAuth.basePath ||
|
|
requestUrl.pathname.startsWith(`${currentAuth.basePath}/`);
|
|
if (requestUrl.origin !== currentAuth.origin || !isInBasePath) {
|
|
return;
|
|
}
|
|
|
|
for (const headerName of Object.keys(requestHeaders)) {
|
|
if (headerName.toLowerCase() === 'authorization') {
|
|
delete requestHeaders[headerName];
|
|
}
|
|
}
|
|
requestHeaders.authorization = currentAuth.authorization;
|
|
};
|
|
|
|
export const clearRequestHeadersForImages = (): void => {
|
|
currentAuth = null;
|
|
};
|