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
5 changes: 5 additions & 0 deletions .changeset/proxy-allowed-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@runflow-io/proxy": minor
---

Add `allowedPaths` — an extensible, strictly-matched route allow-list on top of the built-ins (dispatch, run polling, health). Defaults cover what `rf.assets.upload`/`rf.assets.get` need (`POST /v1/asset-uploads`, `POST /v1/asset-uploads/:id/confirmations`, `GET /v1/assets/:id`). Like `allowedModels`, a custom list **replaces** the defaults — spread the exported `DEFAULT_ALLOWED_PATHS` to extend, or pass `[]` to disable the asset routes. Rules support method arrays and `:param` segments, reject traversal (including percent-encoded) and empty segments. 403/415 bodies now carry actionable messages plus machine-readable `code`s (`path_not_allowed`, `model_not_allowed`, `origin_not_allowed`, `json_content_type_required`). The handler also exposes `PUT`/`PATCH`/`DELETE` for framework route exports. `RateLimitResult`'s `void` member is now `undefined` (type-level only).
9 changes: 9 additions & 0 deletions .changeset/sdk-assets-pin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@runflow-io/sdk": minor
---

Add `rf.assets.upload(file)` — the browser-safe presigned upload flow (create session → PUT to storage → confirm) with transient-failure retry and a size-scaled PUT timeout, returning a model-ready signed HTTPS `url` plus the stable `runflow://assets/{id}` `ref`. Fixes the most common external-fork failure: browser file uploads ending up as `data:` URIs that models reject with a 422. Add `rf.assets.get(id)` to re-mint an expired signed url (store the `id`, not the `url`).

Export `composePinPrompt`, `composeRegionPrompt`, `pinRegion`, and `PinPoint` — the pin→region prompt convention (3×3 grid baked into the edit prompt) that previously existed only as private copies inside the studio bundle.

Hardening: proxy mode (`baseUrl`) now never sends `Authorization`, even when `apiKey` is also passed (the documented contract); presigned-URL query strings are redacted from error messages; non-https `upload_url`s are refused. New `RunflowErrorCode` union for autocompletable `catch` handling.
9 changes: 9 additions & 0 deletions .changeset/studio-props-mask.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@runflow-io/studio": minor
---

`<StudioShell>` accepts four optional customization props — `tools` (workflow catalogue), `source` (initial asset URL or sample list, read at mount), `sentinel` (`{ enabled, taskDescription }`), and `copy` (brand/labels) — making vertical forks possible without rebuilding on `./headless`. Zero props renders exactly as before. `mount()` forwards them via the new `props` option.

`./headless` now exports `createMaskController` — the framework-free dual-canvas brush engine (stroke interpolation, coverage, full-resolution thresholded mask blob, guarded against unattached use and bad brush sizes) the shell itself uses, so headless consumers get working mask creation for inpaint workflows without rebuilding it.

The shell's file uploads now default to the SDK's presigned flow through `runflowProxy` (zero-config — no separate `upload` endpoint needed); hosts that explicitly set `urls.upload` keep the legacy multipart path. `unmount()` now also clears theme CSS variables, and blob preview URLs are revoked on unmount.
56 changes: 56 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: CI

on:
push:
branches: [main]
pull_request:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
checks:
name: build · typecheck · test · lint
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10

- name: Install
run: bun install --frozen-lockfile

- name: Build
run: bun run build

- name: Typecheck
run: bun run typecheck

- name: Test
run: bun run test

- name: Lint
run: bun run lint

# Warning ratchet: biome warnings (mostly the studio components'
# downgraded a11y/hooks rules) must not grow past the checked-in
# budget. Lower the budget as warnings get fixed; raising it is a
# conscious review decision.
- name: Lint warning ratchet
run: |
BUDGET=45
count=$(bunx biome check . 2>&1 | grep -oE 'Found [0-9]+ warnings' | grep -oE '[0-9]+' | head -1 || true)
count=${count:-0}
echo "biome warnings: $count (budget: $BUDGET)"
if [ "$count" -gt "$BUDGET" ]; then
echo "::error::Warning count $count exceeds the budget of $BUDGET — fix the new warnings or consciously raise the budget in ci.yml."
exit 1
fi

# The live e2e proof (examples/e2e-proof) needs RUNFLOW_API_KEY and
# spends real credits, so it stays a local/manual gate — see
# `bun run proof`.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@ Add a changeset for every user-visible change:
bun changeset
```

## End-to-end proof

`bun run proof` exercises the real customer chain — browser SDK →
`@runflow-io/proxy` → api.runflow.io — across every modality, including
file upload via `rf.assets.upload` and the proxy allow-list. It needs
`RUNFLOW_API_KEY` and spends real credits, so it's a local/manual gate
(not CI). Results land in `.proof/`.

For a worked example of a vertical fork (customize `<StudioShell>` via
its `tools` / `source` / `sentinel` / `copy` props, or build a custom UI
on `./headless`), see the
[real-estate-studio-sdk](https://github.com/runflow-io/real-estate-studio-sdk)
reference repo.

## License

MIT
20 changes: 20 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@
}
}
},
"overrides": [
{
"include": ["packages/studio/src/components/**"],
"linter": {
"rules": {
"a11y": {
"noLabelWithoutControl": "warn",
"noAutofocus": "warn",
"useSemanticElements": "warn"
},
"suspicious": {
"noArrayIndexKey": "warn"
},
"correctness": {
"useExhaustiveDependencies": "warn"
}
}
}
}
],
"javascript": {
"formatter": {
"quoteStyle": "double",
Expand Down
10 changes: 5 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 75 additions & 0 deletions docs/plans/run-384-sdk-gaps/backend-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Backend contract: resolve `runflow://` asset refs at dispatch (RUN-384 gaps 2 + 6)

**Target repo:** `runflow-monorepo` (api.runflow.io). **Not** implemented in `runflow-js` —
this document is the companion spec for the backend ticket.

## Current behavior (verified in code)

- **Writes** (`POST /v1/models/{owner}/{slug}/runs`, `POST /v1/comfyui-workflows/.../runs`,
batches, admin retries): inline `data:` URIs in `body.input`/`body.metadata` are
auto-materialized to R2 and stored as `runflow://assets/{uuid}` refs. This is correct
and should stay.
- **Reads** (`GET /v1/runs`, `GET /v1/runs/{id}`, batch/canonical listings): asset-backed
URLs are re-signed and `runflow://assets/{uuid}` refs are resolved to short-TTL signed
HTTPS URLs. Also correct.
- **The gap:** at **model dispatch**, the materialized `runflow://` ref is forwarded to the
model worker **as-is**. Worker media validators accept only HTTP(S)/`data:` URIs —
`google/nano-banana-pro/edit` (and most non-ComfyUI models) reject with
`422: media URL must use HTTP(S) or data URI, got 'runflow'`. ComfyUI workflow file
inputs already accept the refs; singleton model dispatch does not.

So the `data:`-materialization *convenience* currently breaks the exact requests it
rewrites. (The original ticket framed this as "make materialization opt-in"; the better
fix below removes the need for any flag.)

## Requested change

At the dispatch layer — after materialization, before the input reaches the model
worker / provider transport — apply the **same resolution the read path already does**:

1. Walk `body.input` (and `metadata` where it feeds workers) for strings of scheme
`runflow://assets/{uuid}`.
2. Resolve each ref org-scoped (existing read-side resolver semantics: load asset, check
`access_expires_at`, sign `r2_key`) into a signed HTTPS URL whose TTL comfortably
covers worker pull + retries (suggest ≥ the worker's max queue+run window).
3. Forward the signed HTTPS URL to the worker. Persist the **ref** (not the signed URL)
on the run record, as today.
4. Unknown/foreign-org/expired refs → 422 with a precise message
(`asset not found or expired: runflow://assets/{uuid}`) — fail at dispatch, not in
the worker.

### Acceptance criteria

- `POST /v1/models/google/nano-banana-pro/edit/runs` with `input.image_urls:
["runflow://assets/{uuid}"]` succeeds end to end (no 422), for both an explicit ref
and one produced by `data:` auto-materialization.
- Read-side responses are unchanged (refs still resolve on read).
- ComfyUI dispatch behavior unchanged.
- A run whose ref points at a foreign org's asset 404s/422s without leaking existence
details beyond the standard non-leaky pattern.

### Why dispatch-side resolution (not validator changes, not an opt-in flag)

- One implementation point instead of N model-validator changes across providers.
- Asset refs become first-class on the write path, matching the read path — the SDK can
then hand `UploadedAsset.ref` (stable, no TTL) to any model instead of the signed `url`.
- The `data:` materialization default stays a pure convenience with no footgun, so no
config flag is needed.

## Interim state (already shipped in runflow-js)

`rf.assets.upload(file)` returns the **signed HTTPS** url from the confirmation response
(`routers/asset_uploads.py` signs it server-side), so external forks are unblocked today
without this change. Once dispatch-side resolution lands, the SDK will document `ref` as
the preferred long-lived input.

## Suggested ticket

> **Title:** Resolve `runflow://assets/{uuid}` refs to signed HTTPS at model dispatch
> **Parent:** RUN-384
> **Why:** Auto-materialized `data:` inputs currently 422 on most singleton models
> (worker media validators only accept HTTP(S)/data:). Read path already resolves refs;
> dispatch must do the same so asset refs are first-class across the stack.
> **Scope:** dispatch layer for `POST /v1/models/.../runs` (+ batches, retries);
> resolver reuse from the read path; 422 on unknown/expired refs; tests per acceptance
> criteria above. ComfyUI unchanged.
Loading
Loading