Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/database-migrations-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ on:
branches:
- main
workflow_dispatch: # Allows manual triggering

permissions:
contents: read # only needs to check out the repo to run migrations

jobs:
migrate:
name: Run Database Migrations
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/database-migrations-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ on:
branches:
- release
workflow_dispatch: # Allows manual triggering

permissions:
contents: read # only needs to check out the repo to run migrations

jobs:
migrate:
name: Run Database Migrations
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/trigger-tasks-deploy-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ on:
push:
branches:
- main

permissions:
contents: read # only needs to check out the repo to build & deploy

jobs:
deploy:
runs-on: warp-ubuntu-latest-arm64-4x
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/trigger-tasks-deploy-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
branches:
- release

permissions:
contents: read # only needs to check out the repo to build & deploy

jobs:
deploy:
runs-on: warp-ubuntu-latest-arm64-4x
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ interface Props {
url?: string;
}

/**
* Only allow http(s) URLs to reach an iframe `src` or anchor `href`. User-typed
* input flows into the preview iframe, so an unvalidated value like
* `javascript:...` or `data:text/html,...` would execute in the preview context
* (XSS). Returns the normalized href, or undefined if the value isn't http(s).
*/
function toSafeUrl(value: string | undefined | null): string | undefined {
if (!value) return undefined;
try {
const parsed = new URL(value);
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
? parsed.href
: undefined;
} catch {
return undefined;
}
}

export function Preview({ className, disabled, url }: Props) {
const [currentUrl, setCurrentUrl] = useState(url);
const [error, setError] = useState<string | null>(null);
Expand All @@ -21,36 +39,46 @@ export function Preview({ className, disabled, url }: Props) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const loadStartTime = useRef<number | null>(null);

// Sanitized view of currentUrl used for every DOM sink (iframe src, anchor href).
const safeUrl = toSafeUrl(currentUrl);

useEffect(() => {
setCurrentUrl(url);
setInputValue(url || '');
}, [url]);

const refreshIframe = () => {
if (iframeRef.current && currentUrl) {
if (iframeRef.current && safeUrl) {
setIsLoading(true);
setError(null);
loadStartTime.current = Date.now();
iframeRef.current.src = '';
setTimeout(() => {
if (iframeRef.current) {
iframeRef.current.src = currentUrl;
iframeRef.current.src = safeUrl;
}
}, 10);
}
};

const loadNewUrl = () => {
if (iframeRef.current && inputValue) {
if (inputValue !== currentUrl) {
setIsLoading(true);
setError(null);
loadStartTime.current = Date.now();
iframeRef.current.src = inputValue;
} else {
refreshIframe();
}
if (!inputValue) return;
const safeInput = toSafeUrl(inputValue);
if (!safeInput) {
setError('Enter a valid http(s) URL');
return;
}
if (safeInput === safeUrl) {
refreshIframe();
return;
}
// Drive the iframe through state (not iframeRef.current.src) so the src
// prop, the external-link href, and refresh/try-again all stay in sync
// with the URL actually shown.
setIsLoading(true);
setError(null);
loadStartTime.current = Date.now();
setCurrentUrl(safeInput);
};

const handleIframeLoad = () => {
Expand All @@ -67,7 +95,12 @@ export function Preview({ className, disabled, url }: Props) {
<Panel className={className}>
<PanelHeader>
<div className="absolute flex items-center space-x-1">
<a href={currentUrl} target="_blank" className="cursor-pointer px-1">
<a
href={safeUrl}
target="_blank"
rel="noopener noreferrer"
className="cursor-pointer px-1"
>
<CompassIcon className="w-4" />
</a>
<button
Expand Down Expand Up @@ -101,12 +134,12 @@ export function Preview({ className, disabled, url }: Props) {
</PanelHeader>

<div className="flex h-[calc(100%-2rem-1px)] relative">
{currentUrl && !disabled && (
{safeUrl && !disabled && (
<>
<ScrollArea className="w-full">
<iframe
ref={iframeRef}
src={currentUrl}
src={safeUrl}
className="w-full h-full"
onLoad={handleIframeLoad}
onError={handleIframeError}
Expand All @@ -128,10 +161,10 @@ export function Preview({ className, disabled, url }: Props) {
className="text-primary hover:underline text-sm"
type="button"
onClick={() => {
if (currentUrl) {
if (safeUrl) {
setIsLoading(true);
setError(null);
const newUrl = new URL(currentUrl);
const newUrl = new URL(safeUrl);
newUrl.searchParams.set('t', Date.now().toString());
setCurrentUrl(newUrl.toString());
}
Expand Down
15 changes: 10 additions & 5 deletions packages/integration-platform/scripts/generate-task-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,24 @@ function generateTaskTypes() {
lines.push(' { name: string; description: string; department: string; frequency: string }');
lines.push('> = {');

// Escape a value for embedding inside a single-quoted TS string literal.
// Backslash MUST be escaped first, otherwise the quote-escaping is incomplete
// (e.g. a trailing "\" or "\'" would break out of the literal).
const sq = (value: string) => `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;

for (const task of tasks) {
// Escape description for use in template literal
// Escape description for use in a template literal (backslash first).
const escapedDesc = task.description
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\$/g, '\\$')
.substring(0, 100); // Truncate for readability

lines.push(` '${task.id}': {`);
lines.push(` name: '${task.name.replace(/'/g, "\\'")}',`);
lines.push(` ${sq(task.id)}: {`);
lines.push(` name: ${sq(task.name)},`);
lines.push(` description: \`${escapedDesc}...\`,`);
lines.push(` department: '${task.department}',`);
lines.push(` frequency: '${task.frequency}',`);
lines.push(` department: ${sq(task.department)},`);
lines.push(` frequency: ${sq(task.frequency)},`);
lines.push(' },');
}

Expand Down
Loading