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
6 changes: 6 additions & 0 deletions .changeset/fresh-snails-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@workflow/web-shared": patch
"@workflow/web": patch
---

Update readme to call out self-hosting limitations
5 changes: 5 additions & 0 deletions .changeset/world-vercel-encodeuri.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@workflow/world-vercel": patch
---

Encode all user-supplied IDs in URL path segments with `encodeURIComponent()`
4 changes: 4 additions & 0 deletions packages/web-shared/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export default function MyRunDetailView({
Server actions and data fetching are intentionally **not** part of `web-shared`. Implement those in your app
and pass data + callbacks into these components. If you need world run helpers, use `@workflow/core/runtime`.

> **Security notice:** If you implement server-side data fetching using `@workflow/world-vercel` or similar backends,
> ensure that user-supplied IDs (runId, stepId, etc.) are validated before passing them to world functions.
> The server actions in `@workflow/web` do not include authentication — see that package's README for details on securing self-hosted deployments.

## Styling

In order for tailwind classes to be picked up correctly, you might need to configure your NextJS app
Expand Down
5 changes: 5 additions & 0 deletions packages/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ Observability Web UI Package bundled in the [Workflow DevKit](https://useworkflo

## Self-hosting

> **Security notice:** The `@workflow/web` package does not include authentication or authorization.
> All users who can reach the deployment share the same backend credentials configured via environment variables.
> If you self-host this UI, you **must** place it behind your own authentication layer (e.g. VPN, reverse proxy with auth, OAuth).
> Exposing it to untrusted users without authentication is at your own risk and may allow unauthorized access to your workflow data.

While this UI is bundled with the Workflow CLI, you can also self-host it.

There are multiple approaches:
Expand Down
8 changes: 4 additions & 4 deletions packages/world-vercel/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ export async function getEvent(
searchParams.set('remoteRefBehavior', remoteRefBehavior);

const queryString = searchParams.toString();
const endpoint = `/v2/runs/${runId}/events/${eventId}${queryString ? `?${queryString}` : ''}`;
const endpoint = `/v2/runs/${encodeURIComponent(runId)}/events/${encodeURIComponent(eventId)}${queryString ? `?${queryString}` : ''}`;

const event = await makeRequest({
endpoint,
Expand Down Expand Up @@ -308,7 +308,7 @@ export async function getWorkflowRunEvents(
const query = queryString ? `?${queryString}` : '';
const endpoint = correlationId
? `/v2/events${query}`
: `/v2/runs/${runId}/events${query}`;
: `/v2/runs/${encodeURIComponent(runId!)}/events${query}`;

let refResolveConcurrency: number | undefined;
const response = (await makeRequest({
Expand Down Expand Up @@ -384,7 +384,7 @@ export async function createWorkflowRunEvent(
return { run };
}
const wireResult = await makeRequest({
endpoint: `/v1/runs/${id}/events`,
endpoint: `/v1/runs/${encodeURIComponent(id!)}/events`,
options: { method: 'POST' },
data,
config,
Expand All @@ -403,7 +403,7 @@ export async function createWorkflowRunEvent(
}

// For run_created events, runId may be client-provided or null
const runIdPath = id === null ? 'null' : id;
const runIdPath = id === null ? 'null' : encodeURIComponent(id);

const remoteRefBehavior = eventsNeedingResolve.has(data.eventType)
? 'resolve'
Expand Down
4 changes: 2 additions & 2 deletions packages/world-vercel/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export async function getHook(
config?: APIConfig
): Promise<Hook> {
const resolveData = params?.resolveData || 'all';
const endpoint = `/v2/hooks/${hookId}`;
const endpoint = `/v2/hooks/${encodeURIComponent(hookId)}`;

const hook = await makeRequest({
endpoint,
Expand Down Expand Up @@ -124,7 +124,7 @@ export async function disposeHook(
config?: APIConfig
): Promise<Hook> {
return makeRequest({
endpoint: `/v2/hooks/${hookId}`,
endpoint: `/v2/hooks/${encodeURIComponent(hookId)}`,
options: { method: 'DELETE' },
config,
schema: HookSchema,
Expand Down
4 changes: 2 additions & 2 deletions packages/world-vercel/src/runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export async function getWorkflowRun(
searchParams.set('remoteRefBehavior', remoteRefBehavior);

const queryString = searchParams.toString();
const endpoint = `/v2/runs/${id}${queryString ? `?${queryString}` : ''}`;
const endpoint = `/v2/runs/${encodeURIComponent(id)}${queryString ? `?${queryString}` : ''}`;

try {
const run = await makeRequest({
Expand Down Expand Up @@ -231,7 +231,7 @@ export async function cancelWorkflowRunV1(
searchParams.set('remoteRefBehavior', remoteRefBehavior);

const queryString = searchParams.toString();
const endpoint = `/v1/runs/${id}/cancel${queryString ? `?${queryString}` : ''}`;
const endpoint = `/v1/runs/${encodeURIComponent(id)}/cancel${queryString ? `?${queryString}` : ''}`;

try {
const run = await makeRequest({
Expand Down
10 changes: 5 additions & 5 deletions packages/world-vercel/src/steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export async function listWorkflowRunSteps(
searchParams.set('remoteRefBehavior', remoteRefBehavior);

const queryString = searchParams.toString();
const endpoint = `/v2/runs/${runId}/steps${queryString ? `?${queryString}` : ''}`;
const endpoint = `/v2/runs/${encodeURIComponent(runId)}/steps${queryString ? `?${queryString}` : ''}`;

const response = (await makeRequest({
endpoint,
Expand All @@ -185,7 +185,7 @@ export async function createStep(
config?: APIConfig
): Promise<Step> {
const step = await makeRequest({
endpoint: `/v2/runs/${runId}/steps`,
endpoint: `/v2/runs/${encodeURIComponent(runId)}/steps`,
options: { method: 'POST' },
data,
config,
Expand All @@ -202,7 +202,7 @@ export async function updateStep(
): Promise<Step> {
const serialized = serializeError(data);
const step = await makeRequest({
endpoint: `/v2/runs/${runId}/steps/${stepId}`,
endpoint: `/v2/runs/${encodeURIComponent(runId)}/steps/${encodeURIComponent(stepId)}`,
options: { method: 'PUT' },
data: serialized,
config,
Expand Down Expand Up @@ -243,8 +243,8 @@ export async function getStep(

const queryString = searchParams.toString();
const endpoint = runId
? `/v2/runs/${runId}/steps/${stepId}${queryString ? `?${queryString}` : ''}`
: `/v2/steps/${stepId}${queryString ? `?${queryString}` : ''}`;
? `/v2/runs/${encodeURIComponent(runId)}/steps/${encodeURIComponent(stepId)}${queryString ? `?${queryString}` : ''}`
: `/v2/steps/${encodeURIComponent(stepId)}${queryString ? `?${queryString}` : ''}`;

const step = await makeRequest({
endpoint,
Expand Down
6 changes: 4 additions & 2 deletions packages/world-vercel/src/streamer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function getStreamUrl(
) {
if (runId) {
return new URL(
`${httpConfig.baseUrl}/v2/runs/${runId}/stream/${encodeURIComponent(name)}`
`${httpConfig.baseUrl}/v2/runs/${encodeURIComponent(runId)}/stream/${encodeURIComponent(name)}`
);
}
return new URL(`${httpConfig.baseUrl}/v2/stream/${encodeURIComponent(name)}`);
Expand Down Expand Up @@ -141,7 +141,9 @@ export function createStreamer(config?: APIConfig): Streamer {

async listStreamsByRunId(runId: string) {
const httpConfig = await getHttpConfig(config);
const url = new URL(`${httpConfig.baseUrl}/v2/runs/${runId}/streams`);
const url = new URL(
`${httpConfig.baseUrl}/v2/runs/${encodeURIComponent(runId)}/streams`
);
const response = await fetch(url, {
headers: httpConfig.headers,
});
Expand Down
Loading