Skip to content

Commit 2a26dbb

Browse files
mjudeikisclaude
andcommitted
fix(app-studio): make sandbox runner reach ready and preview work in portal
Several bugs prevented the App Studio sandbox from becoming ready and the preview from loading inside the portal. sandbox-runner template (startCommand default): - kro embeds schema defaults into the generated CRD without escaping newlines, so the multi-line block scalar made CRD creation fail with "invalid character '\n' in string literal" and the RGD never activated (no CRD, no instances, no pod). Keep the default single-line and base64-encode the Vite shim. - kro also rewrites inner double quotes to single quotes, so --port "$SANDBOX_PORT" shipped as '$SANDBOX_PORT' and never expanded. Use no double quotes and bare $SANDBOX_PORT / $SHIM. - The Vite config shim must live in the workspace (not /tmp) so Node can resolve the bare `import 'vite'`; it is written as a gitignored dotfile. The import is now best-effort and falls back to a plain { server: { host, allowedHosts } } config when vite cannot be resolved (the no-package.json / npx path), so allowedHosts is applied there too. preview gateway: - The portal embeds the preview in a cross-site iframe, so the auth cookie is a third-party cookie that Chrome blocks by default: direct navigation worked but the portal iframe did not. Mark the cookie Partitioned (CHIPS) so it survives in the partitioned third-party context. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0d050b9 commit 2a26dbb

3 files changed

Lines changed: 69 additions & 3 deletions

File tree

providers/app-studio/previewgateway/previewgateway.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,17 @@ func (h *Handler) redirectWithCookie(w http.ResponseWriter, r *http.Request, pay
155155
HttpOnly: true,
156156
Secure: true,
157157
SameSite: http.SameSiteNoneMode,
158-
Expires: expiresAt,
159-
MaxAge: maxAge,
158+
// The portal embeds the preview in a cross-site iframe (the portal and
159+
// *.preview.localhost are different sites), so this is a third-party
160+
// cookie. Chrome blocks third-party cookies by default, which drops the
161+
// cookie after the token→redirect exchange and breaks the preview inside
162+
// the portal even though direct (first-party) navigation works. Partitioned
163+
// (CHIPS) opts the cookie into partitioned third-party storage keyed by the
164+
// top-level portal site, so it is delivered on the post-redirect request
165+
// inside the iframe. Requires Secure + SameSite=None (both set above).
166+
Partitioned: true,
167+
Expires: expiresAt,
168+
MaxAge: maxAge,
160169
})
161170

162171
query := r.URL.Query()

providers/app-studio/previewgateway/previewgateway_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ func TestPreviewGatewayQueryTokenWritesCookieAndRedirects(t *testing.T) {
117117
if got, want := cookie.SameSite, http.SameSiteNoneMode; got != want {
118118
t.Fatalf("cookie SameSite = %v, want %v", got, want)
119119
}
120+
if !cookie.Partitioned {
121+
t.Fatalf("cookie must be Partitioned (CHIPS) so it survives in the cross-site portal iframe, got %+v", cookie)
122+
}
120123
if got, want := int64(cookie.Expires.Unix()), payload.ExpiresAt; got != want {
121124
t.Fatalf("cookie expiry = %d, want %d", got, want)
122125
}

providers/infrastructure/install/templates/sandbox-runner.yaml

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,61 @@ spec:
2828
default: "/workspace"
2929
startCommand:
3030
type: string
31-
default: "PORT_VALUE=\"$SANDBOX_PORT\"; if [ -z \"$PORT_VALUE\" ]; then PORT_VALUE=3000; fi; if [ -f package.json ]; then npm install --no-audit --no-fund && (npm run dev -- --host 0.0.0.0 --port \"$PORT_VALUE\" || npm start); else npx --yes vite --host 0.0.0.0 --port \"$PORT_VALUE\"; fi"
31+
# A Vite config shim is written into the workspace and passed via --config
32+
# so the dev server accepts the proxied preview Host header. Without it
33+
# Vite 5+'s default server.allowedHosts rejects any non-localhost Host (the
34+
# request arrives through the data-plane proxy / preview gateway) with
35+
# "Blocked request. This host is not allowed", so the preview never becomes
36+
# reachable in production. (Locally the preview host ends in .localhost,
37+
# which Vite always allows, so the shim is a no-op there.)
38+
#
39+
# The shim tries to import 'vite' to merge the project's own config (keeping
40+
# its plugins) and only force server.host + server.allowedHosts. Crucially
41+
# the import is best-effort: when 'vite' cannot be resolved (the no-package
42+
# .json / `npx vite` path, where Vite lives under .npm/_npx and not in the
43+
# workspace node_modules), it falls back to a plain
44+
# { server: { host, allowedHosts } } config -- itself a valid Vite config --
45+
# so allowedHosts is applied in that path too. The file lives in the
46+
# workspace (not /tmp) because Node resolves the bare `import 'vite'`
47+
# relative to the config file's own directory; it is a dotfile added to
48+
# .gitignore so it is never committed to the user's repo.
49+
#
50+
# NOTE 1: this MUST stay a single-line string with NO literal newlines AND
51+
# no backslash-n escapes. kro embeds schema defaults into the generated
52+
# CRD's OpenAPI schema without JSON-escaping newlines, so a multi-line
53+
# block scalar (or a "\n" inside a double-quoted YAML value, which YAML
54+
# would unescape to a real newline) makes CRD creation fail with
55+
# "invalid character '\n' in string literal" and the RGD never activates.
56+
# The shim source is therefore base64-encoded and decoded at runtime, and
57+
# the .gitignore append uses `echo` rather than `printf '%s\n'`.
58+
#
59+
# NOTE 2: this MUST NOT contain any double quotes. kro renders schema
60+
# defaults through a `string | default="..."` mini-format that swaps every
61+
# inner double quote to a single quote. A shell single-quoted '$VAR' does
62+
# NOT expand, so double-quoting a variable here (e.g. --port "$SANDBOX_PORT")
63+
# silently ships a literal "$SANDBOX_PORT" to the runner. Use bare $VARs;
64+
# $SANDBOX_PORT is always set by the deployment env and $SHIM is a fixed
65+
# filename, so neither needs quoting for word-splitting.
66+
#
67+
# Decoded shim source:
68+
# export default async (env) => {
69+
# const forced = { server: { host: true, allowedHosts: true } }
70+
# let loadConfigFromFile, mergeConfig
71+
# try {
72+
# ({ loadConfigFromFile, mergeConfig } = await import('vite'))
73+
# } catch (e) {
74+
# return forced
75+
# }
76+
# let base = {}
77+
# try {
78+
# const loaded = await loadConfigFromFile(env, undefined, process.cwd())
79+
# if (loaded && loaded.config) base = loaded.config
80+
# } catch (e) {
81+
# console.error('[kedge] could not load project vite config:', e)
82+
# }
83+
# return mergeConfig(base, forced)
84+
# }
85+
default: "SHIM=.kedge-vite.config.mjs; printf '%s' 'ZXhwb3J0IGRlZmF1bHQgYXN5bmMgKGVudikgPT4gewogIGNvbnN0IGZvcmNlZCA9IHsgc2VydmVyOiB7IGhvc3Q6IHRydWUsIGFsbG93ZWRIb3N0czogdHJ1ZSB9IH0KICBsZXQgbG9hZENvbmZpZ0Zyb21GaWxlLCBtZXJnZUNvbmZpZwogIHRyeSB7CiAgICAoeyBsb2FkQ29uZmlnRnJvbUZpbGUsIG1lcmdlQ29uZmlnIH0gPSBhd2FpdCBpbXBvcnQoJ3ZpdGUnKSkKICB9IGNhdGNoIChlKSB7CiAgICByZXR1cm4gZm9yY2VkCiAgfQogIGxldCBiYXNlID0ge30KICB0cnkgewogICAgY29uc3QgbG9hZGVkID0gYXdhaXQgbG9hZENvbmZpZ0Zyb21GaWxlKGVudiwgdW5kZWZpbmVkLCBwcm9jZXNzLmN3ZCgpKQogICAgaWYgKGxvYWRlZCAmJiBsb2FkZWQuY29uZmlnKSBiYXNlID0gbG9hZGVkLmNvbmZpZwogIH0gY2F0Y2ggKGUpIHsKICAgIGNvbnNvbGUuZXJyb3IoJ1trZWRnZV0gY291bGQgbm90IGxvYWQgcHJvamVjdCB2aXRlIGNvbmZpZzonLCBlKQogIH0KICByZXR1cm4gbWVyZ2VDb25maWcoYmFzZSwgZm9yY2VkKQp9Cg==' | base64 -d > $SHIM; grep -qxF $SHIM .gitignore 2>/dev/null || echo $SHIM >> .gitignore; if [ -f package.json ]; then npm install --no-audit --no-fund && (npm run dev -- --host 0.0.0.0 --port $SANDBOX_PORT --config $SHIM || npm run dev -- --host 0.0.0.0 --port $SANDBOX_PORT || npm start); else npx --yes vite --host 0.0.0.0 --port $SANDBOX_PORT --config $SHIM || npx --yes vite --host 0.0.0.0 --port $SANDBOX_PORT; fi"
3286
runnerImage:
3387
type: string
3488
description: "Container image for the live development runner. Defaults to the standard kedge runner (published with app-studio releases); override per instance to pin a digest or use a custom runtime."

0 commit comments

Comments
 (0)