mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 08:02:40 +00:00
* feat(apps): use the windmill-client SDK from raw app frontend code
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): bound the raw app SDK token to deployed runnables
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): deny dependency jobs and survive a failed SDK mint
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): confine the SDK token's users scope to the viewer's identity
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* docs: describe the full raw-app SDK sentinel narrowing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): deny workflow-as-code replay for raw app SDK tokens
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): deny preview-flow restart replay for raw app SDK tokens
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): re-prompt when an app widens its SDK scopes mid-consent
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* feat(apps): support the frontend SDK in sandboxed raw apps
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): hand the sandboxed SDK token over only once per loaded document
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): bind the sandboxed SDK handoff to the document we loaded
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): use an unguessable nonce for the sandboxed SDK handoff
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): reply to the sandboxed SDK handshake over its own port
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): answer the raw app handshake only over a transferred port
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): set frontend_sdk_scopes in the S3-gated policy literals
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* docs: describe the sandboxed wrapper's credential as it now works
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* ui nit
* feat(apps): make the frontend SDK work in the raw app editor preview
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): guard the preview token mint and drop superseded responses
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): refuse job tokens on every raw app SDK mint path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* docs: correct the mint caller list and the preview retry rationale
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): apply the consent response's render mode before rendering
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): restart the viewer when a redeploy changes the render mode
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): restart on every render-mode change, not just the first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): clear the preview's SDK credential when scopes go away
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): remove window.process in the preview instead of blanking it
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* docs: cut the raw app SDK comments down to the invariant
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* refactor(apps): use randomUUID for the raw app handshake nonce
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): re-read the render mode before rendering without a token
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* fix(apps): make the raw app handshake nonce unguessable again
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* chore: pin the EE ref to a commit that builds against this OSS tree
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
* refactor(apps): authenticate the raw app preview by session instead of a token
The editor preview is same-origin and unsandboxed, so app code there already
holds the editing user's session cookie. Minting a scoped bearer for it added
an endpoint and a portable 12h credential without containing anything.
Inject only BASE_URL and WM_WORKSPACE: `windmill-client` falls back to
credentialed same-origin requests when it finds no token, so the SDK runs as
the editing user. Drops POST /apps/preview_sdk_token and the mint/race
handling in the editor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA
* fix(sdk): send credentials only outside the browser
The API answers `Access-Control-Allow-Origin: *` and never sets
`allow_credentials`, so a credentialed cross-origin request fails before the
bearer is read — which is what a sandboxed raw app issues. Keying this on the
browser rather than on `WM_TOKEN` leaves non-browser callers byte-identical,
and browsers keep sending cookies same-origin through fetch's own default.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA
* remove windmill-client from templates
* fix(sdk): drop credentials only for raw app bundles
A sandboxed raw app calls the API from an opaque origin, and the API answers
`Access-Control-Allow-Origin: *`, which a credentialed request can never pair
with. Gate on WM_RAW_APP, set by the two places that build a raw app's
`window.process.env`, so every other windmill-client consumer is untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA
* feat(apps): make frontend SDK access sandbox-only
An unsandboxed bundle runs same-origin with the viewer's full session, so a
consent prompt there implies a boundary that does not exist and the token adds
nothing it could not already do. Advertise scopes and mint only when isolation
is on; turning the toggle off clears the declared scopes with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA
* Revert "refactor(apps): authenticate the raw app preview by session instead of a token"
This reverts commit 81905e455b, restoring POST /apps/preview_sdk_token.
Session auth gave the preview the editing user's full permissions and worked
regardless of policy, so an app that would 403 for a viewer — or that declares
no scopes at all — ran fine in the preview and broke only once deployed. The
preview now takes the same credential as a deployed app, gated the same way:
sandbox off or no scopes means no env at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA
* docs(apps): state the sandbox-only SDK contract in the public schema
The Policy and EmbedTokenResponse descriptions still promised a token to any
raw app with non-empty scopes, and said raw apps skip tokens entirely. Point
authors at adding windmill-client themselves too, since the starter templates
no longer carry it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA
* fix(apps): drop the preview token before minting its replacement
A mint is asynchronous, so clearing the env only on the empty-scope path left
the running preview — and any build fed meanwhile — holding scopes the policy
had just removed, or a token for the workspace just left, for as long as the
request took.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA
* fix(apps): restart the preview realm when its credential changes
Re-feeding the build resets the preview's DOM but keeps its JavaScript realm,
so the previous bundle's timers, listeners and pending callbacks went on using
the client they imported — and the token it captured at module load — after the
policy dropped it. Reload both shells instead; each replays the build on its
way back, so only the new realm survives.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA
* fix(apps): start the preview once per credential change
Restarting the realm made its shell replay the build immediately, so a delayed
mint ran the app once tokenless and again tokenful — mount-time side effects
twice per scope or workspace change. Hold the build back until the mint
settles: the shell comes back blank and whichever finishes last starts the app.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA
* fix(apps): wait for the detached preview shell before replaying
Its reload was only initiated, never awaited — unlike the inline iframe it had
no readiness flag — so a mint settling first posted the build to the retiring
document, which then ran alongside the replacement shell's own replay. Track
readiness from both paths that announce it: `load` for a freshly opened window,
`appPreviewReady` for a reload.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA
* chore: update ee-repo-ref to 99e143fa1e2e6c33b3525366a5afe48f7a4f020e
This commit updates the EE repository reference after PR #688 was merged in windmill-ee-private.
Previous ee-repo-ref: 609e197fbc08f1ce83dd86f816748cc19d213f77
New ee-repo-ref: 99e143fa1e2e6c33b3525366a5afe48f7a4f020e
Automated by sync-ee-ref workflow.
* nit better description
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
148 lines
5.0 KiB
Svelte
148 lines
5.0 KiB
Svelte
<script lang="ts">
|
|
/*
|
|
* WIN-2006: shared in-workspace app viewer (low-code AND raw). Renders the app
|
|
* through the same PublicAppFrame -> PublicApp machinery the public viewer uses,
|
|
* so the sandbox behavior is identical on every page. PublicAppFrame picks the
|
|
* rendering: an opaque /app_embed iframe for sandboxed low-code, or inline (with
|
|
* the bundle isolated in RawAppPreview's own opaque iframe) for raw and for
|
|
* unsandboxed apps. When the publisher opts an app into sandbox isolation, its
|
|
* untrusted markup/JS must not run with the member's full session — hence the
|
|
* scoped embed token / opaque isolation.
|
|
*/
|
|
import { base } from '$lib/base'
|
|
import PublicApp from '$lib/components/apps/editor/PublicApp.svelte'
|
|
import PublicAppFrame from '$lib/components/apps/editor/PublicAppFrame.svelte'
|
|
import { Button } from '$lib/components/common'
|
|
import { AppService, OpenAPI } from '$lib/gen'
|
|
import { userStore } from '$lib/stores'
|
|
import { canWrite } from '$lib/utils'
|
|
import { getUserExt } from '$lib/user'
|
|
import { Pen } from 'lucide-svelte'
|
|
import { page } from '$app/state'
|
|
|
|
let {
|
|
workspace,
|
|
path,
|
|
editHref
|
|
}: {
|
|
workspace: string
|
|
path: string
|
|
/** Where the Edit button points (low-code vs raw editor). */
|
|
editHref: string
|
|
} = $props()
|
|
|
|
let app: any = $state(undefined)
|
|
let notExists = $state(false)
|
|
let noPermission = $state(false)
|
|
let canWriteApp = $state(false)
|
|
let refresh: (() => void) | undefined
|
|
|
|
// The opaque iframe loads the dedicated cookieless, chrome-less viewer route.
|
|
// The page's query/hash are forwarded so the app sees the same `ctx.query` /
|
|
// `ctx.hash` as the pre-sandbox viewer did. Captured ONCE (not reactively):
|
|
// the embedder later mirrors the app's own hash/query changes back onto this
|
|
// page's URL (wm_embed_hash relay), and re-deriving the iframe src from them
|
|
// would reload the app on its every navigation.
|
|
const initialSearchHash = page.url.search + page.url.hash
|
|
let viewerUrl = $derived(`${base}/app_embed/${workspace}/${path}${initialSearchHash}`)
|
|
|
|
const hideEditBtn = page.url.searchParams.get('hideEditBtn') === 'true'
|
|
const hideRefreshBar = page.url.searchParams.get('hideRefreshBar') === 'true'
|
|
|
|
// Embedder side: mint a scoped embed token (by path) from the member's session.
|
|
async function fetchEmbedToken(opts?: { sdkConsent?: boolean }): Promise<{ token?: string }> {
|
|
const headers: Record<string, string> = {}
|
|
if (typeof OpenAPI.TOKEN === 'string' && OpenAPI.TOKEN) {
|
|
headers['Authorization'] = `Bearer ${OpenAPI.TOKEN}`
|
|
}
|
|
const consent = opts?.sdkConsent ? '?sdk_consent=true' : ''
|
|
const res = await fetch(`${OpenAPI.BASE}/w/${workspace}/apps/embed_token/p/${path}${consent}`, {
|
|
headers
|
|
})
|
|
if (!res.ok) {
|
|
const err: any = new Error('Failed to fetch embed token')
|
|
err.status = res.status
|
|
throw err
|
|
}
|
|
return await res.json()
|
|
}
|
|
|
|
// Viewer side — used for the inline renderings (raw, and unsandboxed low-code);
|
|
// the sandboxed low-code case loads inside the opaque /app_embed iframe instead.
|
|
// getAppByPath returns bundle_secret + runnables for raw apps, which
|
|
// PublicApp -> RawAppPreview needs.
|
|
async function loadApp() {
|
|
try {
|
|
userStore.set(await getUserExt(workspace))
|
|
} catch (e) {
|
|
console.warn('Anonymous user')
|
|
}
|
|
try {
|
|
const loaded: any = await AppService.getAppByPath({ workspace, path })
|
|
// Raw apps need the bundle secret to load their bundle. getAppByPath
|
|
// doesn't compute it (unlike the public handlers), so fetch it here — the
|
|
// same call the previous raw viewer used — and hand it to PublicApp ->
|
|
// RawAppPreview via bundle_secret.
|
|
if (loaded?.raw_app && !loaded.bundle_secret) {
|
|
try {
|
|
loaded.bundle_secret = await AppService.getPublicSecretOfLatestVersionOfApp({
|
|
workspace,
|
|
path
|
|
})
|
|
} catch (e) {
|
|
console.error('Failed to load raw app bundle secret', e)
|
|
}
|
|
}
|
|
app = loaded
|
|
noPermission = false
|
|
notExists = false
|
|
} catch (e: any) {
|
|
if (e.status == 401) refresh?.()
|
|
else if (e.status == 403) noPermission = true
|
|
else notExists = true
|
|
}
|
|
}
|
|
|
|
// Edit button: determine write access on this real-origin page (cookie).
|
|
async function loadPerms() {
|
|
try {
|
|
const lite: any = await AppService.getAppLiteByPath({ workspace, path })
|
|
canWriteApp = canWrite(lite?.path, lite?.extra_perms ?? {}, $userStore)
|
|
} catch (_) {
|
|
canWriteApp = false
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
if (workspace && path) loadPerms()
|
|
})
|
|
</script>
|
|
|
|
<PublicAppFrame
|
|
{fetchEmbedToken}
|
|
{viewerUrl}
|
|
onViewerReady={(_token, requestTokenRefresh) => {
|
|
refresh = requestTokenRefresh
|
|
loadApp()
|
|
}}
|
|
>
|
|
{#snippet viewer()}
|
|
<PublicApp
|
|
{app}
|
|
{workspace}
|
|
{notExists}
|
|
{noPermission}
|
|
jwtError={false}
|
|
inWorkspace
|
|
{hideRefreshBar}
|
|
onLoginSuccess={() => loadApp()}
|
|
></PublicApp>
|
|
{/snippet}
|
|
</PublicAppFrame>
|
|
|
|
{#if canWriteApp && !hideEditBtn}
|
|
<div id="app-edit-btn" class="absolute bottom-4 z-50 right-4">
|
|
<Button size="sm" startIcon={{ icon: Pen }} variant="subtle" href={editHref}>Edit</Button>
|
|
</div>
|
|
{/if}
|