A move writes old path -> new path (per workspace and kind, per owner for a
draft-only move) in its transaction; a draft save or discard addressed to a
path the caller has no draft at resolves through it and keeps the moved
draft's path keys. Creating an item at a path drops the records leaving it.
Every writer (edit routes, sessions, chat, CLI, the tab-close flush) follows
without passing an id, so the id plumbing is gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Also advance the raw-app base on deploy, relocate once per move, drop the
hoisted operator check and the unread base on drafts/list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: add per-route CORS origin allowlist for HTTP triggers
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: fail closed on cold router cache and invalid origin input
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: resolve CORS route from the decoded path like the request handler
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: add instance-wide default allowed origins for HTTP routes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: let non-superadmins read the default allowed origins setting
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: badge the advanced section when a route's origins are restricted
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: state inherited origins on the control and use one hint row
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: trim the origins tooltip and relabel the toggle when a default exists
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: keep the origins format hint visible until an entry is wrong
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: state the at-least-one requirement in the origins hint
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: import the origins validator in the trigger-http tests
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: make an empty allowlist deny rather than fall back to the default
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: address review nits on origin validation and the CORS editor
* fix: derive the origins error from the stored list and tighten host validation
* fix: parse real IPv6 hosts and refuse a newly emptied allowlist
* refactor: make origin validation advisory except for null and non-ascii
* feat: let an empty allowlist be saved as deny every origin
* docs: document the empty allowlist as deny every origin
* fix: bound allowlists, reject commas, and decide cors after the handler
* chore: revert unrelated rustfmt churn in windmill-common tests
* chore: revert unrelated rustfmt churn in windmill-common
* chore: drop the route types the cors restructure replaced
* fix: take the stricter cors decision from before and after the handler
* fix: strip runnable cors headers when the routers are unavailable
* docs: document the allowlist bounds in the openapi schema
* fix: let an unavailable cors read defer to one that resolved
* refactor: carry the resolved cors policy from the handler to the middleware
* docs: describe why an unavailable read fails closed on the paths that reach it
* fix: validate the default origins on the declarative settings path
* test: keep the webhook doc comment with the test it describes
* fix: warn on impossible schemes and ports, and validate the instance setting
* feat: treat an empty allowlist as unset at both levels
* perf: decode the cors path only when the fallback needs it
* docs: document the empty allowlist as unset in the api schema
* docs: describe an empty allowlist as unset in the frontend comments
* docs: say what a null allowlist resolves to, not what it meant before the default existed
* docs: state what the validator refuses and why methods stay broad
* feat: exempt static asset routes from the origin allowlist
* fix: hide the origin control for every static target, not just websites
* fix: exempt only static websites, not single-file static assets
* fix: warn on an unclosed ipv6 host in the origins advisory
* fix: require assets present, not just the static website flag
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* refactor: make the app policy's principal the authority for its identity
* fix: align the app backfill with the sibling migration and audit the uncached address
* chore: refresh the sqlx cache after rebasing onto the merged base
* fix: resolve the app execution address uncached, it decides the job's authorization
* chore: cache the EE queries at the ref this branch pins
* chore: cache the EE queries at the ref this branch pins
* fix: derive the app draft's on-behalf-of address on read
* chore: cache the query the draft derivation test added
* fix: derive the app identity on the draft-table and version reads too
* docs: state the draft resolver's authorization contract
* fix: resolve a draft's principal against workspace membership only
* chore: cache the membership lookup the draft resolver added
* fix: drop an unresolvable draft's address instead of leaving it stale
* perf: evict the address cache on change so app dispatch can read it
* fix: evict on superadmin role changes, not only address changes
* refactor: make the app policy's address optional instead of derived on read
* fix: follow an external superadmin's rename into the apps that name them
* docs: state the removal gate once, and correctly
* refactor: drop the app-policy version constant that gated nothing
* docs: drop the last reference to the removed constant
* perf: read the address cache everywhere now that eviction reaches every replica
* fix: keep persisted addresses off the cache the poller evicts asynchronously
* docs: state where the cached address is accepted and where it is not
* docs: keep the cache rule in one place and drop the stale premise
* docs: sort the two lookups by how long a wrong answer lives
* fix: resolve the schedule address uncached where it is written to the row
* docs: name the release this actually ships in
* perf: evict a superadmin's key per workspace instead of the whole cache
* fix: evict every alias a superadmin principal can be spelled as
* docs: describe the trigger as it is
* docs: cover the round-tripped read in the cache rule
* docs: record why a stale dispatch address cannot escalate
* fix: validate a dispatch address against the principal's live binding
* fix: carry the validated address through to the job row and token
* fix: record the validated address on the job row, not the one handed in
* test: run the substep tag check as the non-superadmin it means to test
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* fix: rewrite a stored app address that disagrees with its principal
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* docs: record the accepted staleness window of the cached dispatch address
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* fix: record the validated address on the job's audit row
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* docs: record the accepted rename race of pre-transaction identity resolution
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* docs: separate the app's stored address from the derived one in the resolver doc
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* docs: describe the job identity fast path the push comments skipped
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* fix: backfill a legacy group-prefixed username as the group it names
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* fix: resolve a schedule edit's identity before opening its transaction
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* fix: never resolve a disabled member to a same-named superadmin
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* docs: state what the email-change notify buys, and rewrap two comment lines
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* fix: keep a group's runnables when offboarding a legacy group-prefixed member
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* fix: read the app author from the stored address, as execution does
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* docs: record the rename race's full consequence as a known, accepted limitation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* docs: record the keep-target group address case as a known, accepted limitation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A rename carries every draft on the item to the new path. An editor left open
across it was still saving by the path it opened on, which the server had to
refuse and answer with where the item went (the "moved" handshake and its
modal). The draft row has an id: the get-by-path overlay now returns it as
draft_id, every later save sends it, and the server writes the row wherever it
is and answers with that path. The editor then follows: it flushes what it holds,
tells the user, and navigates to the item's new path, where the stale prompt
says what changed. The lineage-based move resolvers, the moved status and the
moved modal are gone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A draft occupies its path the way a deployed item does: a never-deployed item,
or a draft left on an archived script. Renaming onto it would either merge two
items or leave the losing row stranded at a path its item has left. The move now
refuses with a BadRequest inside the deploy's transaction, so the rename itself
fails and the source stays deployed. Every draft on the item then moves; there is
no longer a left-behind count to report.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every kind kept its fork base under a different name and type inside the
value: parent_hash (hex) for scripts, version_id for flows, parent_version for
apps. draft.base holds it as one text id, derived on save from the value so
every writer fills it the same way, backfilled by the migration (rows holding a
NUL are skipped, since ->> raises on them). The get-by-path overlay exposes it
as draft_base and the drafts list as base; the editors and the compare page
read that one field and compare it to the head as text.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(frontend): keep crawlers off the login page
Every page on the hub links to /user/login with itself in `rd`, so a crawler
sees one login URL per hub page — 4,311 of them in Search Console, all
rendering this same form and flagged as duplicates without a canonical. Nothing
about a login page belongs in an index, on any instance.
Mark the page noindex, as public_run already is, and ship a robots.txt that
keeps crawlers out of /user/ and /api/. The frontend is embedded as static
assets with an index.html fallback, which is why /robots.txt answered with the
app shell until now; a real file in static/ is served as itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): let crawlers fetch the login page so the noindex is seen
robots.txt disallowed /user/, which stopped a crawler fetching /user/login at
all — and a page that is never fetched never shows its noindex. The two halves
cancelled: the URLs would have moved from "duplicate" to "blocked" rather than
out of the index.
Drop the disallow, keeping /api/. And since the app is client-rendered, the
meta tag only exists after a render pass; send X-Robots-Tag on /user/* from
serve_path as well, which a crawler sees on the first fetch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The stale-draft prompt gains "See what changed", which opens the diff drawer. The
drawer resolves the deployed side by the draft row's own path (not the typed path,
which after a rename still names the archived row), labels which version the left
pane is, and offers a picker over the item's deployed history for scripts, flows
and raw apps. The history endpoints return created_by (and created_at for apps)
so each entry can name its deployer. "Restore to deployed" moves to the header
actions.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A deploy that renames an item is a deploy like any other: every draft on the item
goes stale, and the stale prompt with its diff is the single mechanism to catch up.
move_drafts_for_path now touches only the row's path column, so the value keeps the
base version the draft actually forked from, and the "moved" patch carries no
version restamp. DraftBaseVersion shrinks to the three per-kind lineage fields.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Revoking the sequence grant would strip a privilege a healthy instance had
before this migration ran — the grant it adds is indistinguishable in the
catalog from the one ALTER DEFAULT PRIVILEGES gives at creation time — so the
down is a comment, matching the other grant-only migrations.
The mirror rewrite is spread over three sites that have to agree and fails
silently when they don't, so it gets a test: a draft carrying both path keys
has both moved, and one carrying neither mirror does not gain one. It reads
the value back over HTTP rather than with `sqlx::query!`, which would need an
offline cache entry of its own.
Also drops twelve `.sqlx` entries this branch added and then superseded, and
corrects the doc and openapi text that still described only the typed path
being rewritten.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The upsert now runs as `windmill_user`, so it calls nextval on `draft_id_seq`
as that role. The only thing granting that is the ALTER DEFAULT PRIVILEGES in
20250205131523, whose DO block swallows failures — so an instance where it
errored would fail every autosave with `permission denied for sequence`.
A draft value carries two path keys: the typed one and a mirror the editors
keep in step with it while it differs from the row's path. Rewriting only the
typed one left the mirror naming the old location, and the loaders prefer the
mirror — reopening a moved session script restored the old path and the next
save un-did the move. Both keys now follow, in the move endpoint and in the
passive carry, under the same tri-state rule.
`typed_path_field` answered `draft_path` for every non-script kind, including
resources, variables and triggers, which have no such key. It returns `None`
for them now, and `move_draft` reads its guard off that mapping so the movable
set and the field mapping cannot drift apart.
Also documents that `move_drafts_for_path` mutates every owner's row and
enforces nothing itself, and parses the draft payload once per save instead of
three times.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The RLS envelope was argued at three sites in drafts.rs; it now sits only on
`resolve_moved_to_in`, whose signature is what a caller would break. The
restamp scoping was copy-pasted at all three deploy call sites while already
documented in full on `move_drafts_for_path`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`update_path` is not a symbol in this repo; a script move goes through
`create_script`. The re-assert's comment re-derived the disclosure argument
that already sits on `resolve_moved_to_in`, where a caller would break it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>