diff --git a/.gitignore b/.gitignore index dd173a242..c6c1f9c33 100644 --- a/.gitignore +++ b/.gitignore @@ -76,4 +76,10 @@ alloy.river # Worktrees .worktrees/ +<<<<<<< feat/shared-extension-site-jobs + +# Local Webflow extension dev overrides +webflow-designer-extension-cli/public/dev-runtime-config.js +======= .claude/worktrees/ +>>>>>>> main diff --git a/CHANGELOG.md b/CHANGELOG.md index 72ea098e8..15c26fdbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,49 @@ On merge, CI will: ## [Unreleased] -_Add unreleased changes here._ +### Added + +- shared frontend modules for extension and dashboard reuse: + `web/static/app/lib/site-jobs.js`, `web/static/app/lib/webflow-sites.js`, + `web/static/app/lib/organisation-api.js`, + `web/static/app/lib/scheduler-api.js`, `web/static/app/lib/site-view.js`, + `web/static/app/lib/job-export.js`, and `web/static/app/lib/shell-nav.js` +- shared shell styling in `web/static/app/styles/shell.css`, now loaded by both + `/dashboard` and the Webflow Designer extension +- a native module-based `/extension-auth` flow in + `web/static/app/pages/webflow-login.js`, replacing the old extension popup + dependency on the legacy auth bundle +- the first native in-extension settings section, `Account`, using shared + account settings logic inside the extension shell rather than loading the app + page + +### Changed + +- centralised Webflow extension job fetching, site scoping, and realtime + fallback logic into `web/static/app/lib/site-jobs.js`, reducing duplication + between the app layer and extension bridge runtime +- rewired the Webflow Designer extension to consume shared jobs, Webflow site, + organisation, scheduler, export, shell, and site-view helpers through the + bridge and sync pipeline +- moved `/dashboard` onto the extension-style shell and shared site-focused + module runtime, replacing the older dashboard-specific shell/layout +- aligned extension reuse docs with the current migration state, including the + shared shell layer, module-native popup auth, and remaining native-extension + settings/detail work + +### Fixed + +- extension popup auth return-to-extension handling, including stable popup + callback state restoration and module-native sign-in handoff +- cross-surface organisation sync so dashboard org changes refresh extension + organisation context instead of leaving stale labels behind +- extension account settings loading and profile save support, including CORS + `PATCH` allowance for `/v1/auth/profile` +- avatar rendering regressions across dashboard and extension, including Google + avatar CSP allowance, shared avatar DOM rendering, and extension avatar source + alignment with dashboard identity data +- required shared module/style sync failures now stop the extension build + instead of silently shipping missing bridge dependencies ## Full changelog history diff --git a/dashboard.html b/dashboard.html index e18d24d2b..df7e3e063 100644 --- a/dashboard.html +++ b/dashboard.html @@ -9,10 +9,13 @@ href="/assets/Good-Native_Hover_App_Logo_Webflow.png" /> Dashboard - Hover + + + - - + + diff --git a/docs/architecture/webflow-designer-extension.md b/docs/architecture/webflow-designer-extension.md index 8e23105be..dfcd7d57b 100644 --- a/docs/architecture/webflow-designer-extension.md +++ b/docs/architecture/webflow-designer-extension.md @@ -16,13 +16,27 @@ Designer. It does not contain backend business logic. ## Auth model - Extension initiates auth via popup to GNH-hosted `/extension-auth.html`. -- Popup reuses existing shared auth system in `web/static/js/auth.js`. +- Popup currently uses a hybrid flow: + - server page shell in `web/templates/extension-auth.html` + - module entrypoint in `web/static/app/pages/webflow-login.js` + - legacy shared auth modal and redirect helpers in `web/static/js/auth.js` - First-time users are created via existing `POST /v1/auth/register` path. - Token handoff returns to extension using `postMessage` with origin/state validation. - Extension keeps auth token in session scope (`sessionStorage`) rather than persistent local storage. +## Shared frontend reuse + +- Shared primitives live under `web/static/app/`. +- The extension already reuses shared API helpers and Web Components via + `webflow-designer-extension-cli/scripts/sync-shared.js` and + `webflow-designer-extension-cli/public/lib/bridge.js`. +- Most extension page orchestration still lives in + `webflow-designer-extension-cli/src/index.ts`. +- The extension does not yet share the full jobs page orchestration or shell + styling with the main app. + ## Repository boundaries - Extension code: `/webflow-designer-extension-cli` diff --git a/docs/plans/webflow-extension-reuse-follow-up.md b/docs/plans/webflow-extension-reuse-follow-up.md index 88738de5f..87b824294 100644 --- a/docs/plans/webflow-extension-reuse-follow-up.md +++ b/docs/plans/webflow-extension-reuse-follow-up.md @@ -1,6 +1,6 @@ # Webflow Extension Reuse Follow-up -Date: 2026-04-05 Status: Proposed Scope: Webflow Designer extension +Date: 2026-04-10 Status: In progress Scope: Webflow Designer extension consolidation and shared frontend reuse ## Current state @@ -22,10 +22,13 @@ The Webflow Designer extension has already adopted part of that shared layer: - shared module sync into the extension build - a bridge/import-map pattern so extension code can consume shared modules in a cross-origin runtime +- shared shell styling via `web/static/app/styles/shell.css` -That means the transition is partly complete. Shared primitives exist and are in -use, but the extension has not yet reached the same level of modular -consolidation as the main app. +That means the transition is no longer only a primitives-level migration. Shared +frontend logic now covers jobs, Webflow site configuration, organisation +context, scheduling, shell navigation, job export, and top-level site view +rendering. `/dashboard` has also moved onto the extension-style shell/layout +instead of the older dashboard-specific shell. ## What is already complete @@ -36,7 +39,20 @@ consolidation as the main app. - `pages/` for page orchestration - the extension reuses shared primitives and API helpers through the bridge/sync approach +- shared extension/dashboard logic now includes: + - `web/static/app/lib/site-jobs.js` + - `web/static/app/lib/webflow-sites.js` + - `web/static/app/lib/organisation-api.js` + - `web/static/app/lib/scheduler-api.js` + - `web/static/app/lib/site-view.js` + - `web/static/app/lib/job-export.js` + - `web/static/app/lib/shell-nav.js` - design tokens exist in the app layer and mirror the extension theme +- shared shell styling now lives in `web/static/app/styles/shell.css` +- the extension auth popup is now module-native through + `web/static/app/pages/webflow-login.js`, not the old `/js/auth.js` popup path +- the extension has its first native in-panel settings section (`Account`) + driven by shared settings logic rather than a framed app page - the completed migration is already documented in `CHANGELOG.md` ## Remaining gaps @@ -44,32 +60,38 @@ consolidation as the main app. - The extension still keeps most page orchestration in `webflow-designer-extension-cli/src/index.ts` rather than in shared `/app` modules. -- The current job-list sharing story is incomplete. The repository contains - `web/static/app/pages/webflow-jobs.js`, but the live extension still owns much - of its own job rendering and refresh flow. -- The extension auth popup still depends on legacy `/js/auth.js`. -- Extension shell styling remains separate from app styling. The app tokens - mirror the extension theme, but the extension shell has not been migrated to - the app style layer. -- Some documentation still overstates how far the extension has been - consolidated into the shared module system. +- Native extension settings coverage is incomplete. `Account` is native in the + extension shell, but the rest of the settings sections still need the same + treatment. +- Job details still need a native in-extension implementation rather than + relying on app-page stop-gaps. +- Extension CSS convergence is only partly complete. Shared shell styling now + exists, but the extension still carries a large local stylesheet in + `webflow-designer-extension-cli/public/styles.css`. +- Identity/avatar selection logic is aligned between dashboard and extension, + but still duplicated rather than centralised into one helper. +- Preview Webflow OAuth and run-on-publish flows still depend on callback URL + registration outside this frontend workstream. +- Some documentation and PR metadata still understate how far this branch has + moved beyond the original jobs-only extraction. ## Recommended next phase -This follow-up should be treated as a JavaScript-first reuse pass, not a full UI -unification project. +This follow-up should now be treated as a native extension-surface completion +pass. The branch has already moved beyond a jobs-only or JS-only reuse phase. - Extract surface-agnostic extension logic from `webflow-designer-extension-cli/src/index.ts` into shared `/app` modules. -- Make the job-list sharing story truthful and consistent: - - either move extension job-list behaviour onto shared `pages/` modules - - or narrow the shared module claims so the code and docs say exactly what is - shared +- Continue the native extension settings rollout: + - `Team` next + - then the remaining org-scoped settings sections +- Implement native in-extension job details using shared modules and shared + layout primitives instead of framed app-page fallbacks. - Keep the bridge/import-map approach for cross-origin extension use. -- Continue sharing reusable logic and UI primitives first, before attempting to - merge the full extension shell layout into the main app. -- Replace the remaining legacy auth dependency in `/extension-auth` so the popup - flow no longer relies on `/js/auth.js`. +- Continue moving extension shell/component CSS into shared app styles so the + dashboard and extension stop carrying parallel styling for the same UI. +- Centralise shared identity/avatar selection logic so dashboard and extension + stop duplicating provider-avatar fallback rules. - Update architecture and planning docs so they reflect the current state accurately. @@ -77,9 +99,8 @@ unification project. - Recreating the deleted March 2026 ES modules plan - Re-running the full `/dashboard` modernisation effort -- Forcing full shell or layout convergence between the extension and the main - app in this phase - Replacing working backend Webflow APIs as part of this documentation update +- Solving preview-domain Webflow OAuth registration inside frontend code ## Acceptance criteria @@ -87,10 +108,12 @@ unification project. archived in the changelog. - The new plan clearly states that both ES modules branch checkpoints are already contained in `main`. -- The new plan distinguishes between shared primitives that already exist and - extension page orchestration that is still local. -- The new plan sets a JS-first extension consolidation direction without - implying that the extension already shares all page-level logic with - `/dashboard`. +- The new plan distinguishes between what is already genuinely shared today and + what still remains extension-local. +- The new plan reflects that `/dashboard` has already moved onto the + extension-style shell/layout and that the popup auth flow is already + module-native. +- The new plan makes the remaining work explicit: native settings coverage, + native job details, `index.ts` thinning, and further CSS convergence. - The new plan supersedes the old branch-era planning context without recreating archived migration history. diff --git a/internal/api/middleware.go b/internal/api/middleware.go index 59417a4f8..01e52e3f6 100644 --- a/internal/api/middleware.go +++ b/internal/api/middleware.go @@ -164,7 +164,7 @@ func CORSMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Set CORS headers w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Request-ID") w.Header().Set("Access-Control-Expose-Headers", "X-Request-ID") @@ -251,13 +251,42 @@ func buildConnectSrcValues() string { return strings.Join(values, " ") } +func isWebflowExtensionSurfacePage(r *http.Request) bool { + if r.Method != http.MethodGet { + return false + } + if r.URL.Query().Get("surface") != "webflow-extension" { + return false + } + + path := strings.TrimRight(r.URL.Path, "/") + switch { + case path == "/dashboard": + return true + case path == "/settings": + return true + case strings.HasPrefix(path, "/settings/"): + return true + case strings.HasPrefix(path, "/jobs/"): + return true + default: + return false + } +} + // SecurityHeadersMiddleware adds security-related headers func SecurityHeadersMiddleware(next http.Handler) http.Handler { connectSrcValues := buildConnectSrcValues() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") - w.Header().Set("X-Frame-Options", "DENY") + + frameAncestors := "'none'" + if isWebflowExtensionSurfacePage(r) { + frameAncestors = "'self' https://webflow.com https://*.webflow.com http://localhost:1337 http://127.0.0.1:1337" + } else { + w.Header().Set("X-Frame-Options", "DENY") + } // Content Security Policy csp := fmt.Sprintf(` @@ -266,12 +295,13 @@ func SecurityHeadersMiddleware(next http.Handler) http.Handler { style-src 'self' 'unsafe-inline'; connect-src %s; frame-src https://challenges.cloudflare.com; - img-src 'self' data: https://www.google-analytics.com https://www.googletagmanager.com https://ssl.gstatic.com https://www.gravatar.com; + frame-ancestors %s; + img-src 'self' data: https://www.google-analytics.com https://www.googletagmanager.com https://ssl.gstatic.com https://www.gravatar.com https://lh3.googleusercontent.com https://*.googleusercontent.com; font-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; - `, connectSrcValues) + `, connectSrcValues, frameAncestors) w.Header().Set("Content-Security-Policy", strings.ReplaceAll(csp, "\n", " ")) w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains") diff --git a/internal/api/middleware_test.go b/internal/api/middleware_test.go index 857ff722e..49bed4193 100644 --- a/internal/api/middleware_test.go +++ b/internal/api/middleware_test.go @@ -297,7 +297,7 @@ func TestCORSMiddleware(t *testing.T) { // Check CORS headers are set assert.Equal(t, "*", rec.Header().Get("Access-Control-Allow-Origin")) - assert.Equal(t, "GET, POST, PUT, DELETE, OPTIONS", rec.Header().Get("Access-Control-Allow-Methods")) + assert.Equal(t, "GET, POST, PUT, PATCH, DELETE, OPTIONS", rec.Header().Get("Access-Control-Allow-Methods")) assert.Equal(t, "Content-Type, Authorization, X-Request-ID", rec.Header().Get("Access-Control-Allow-Headers")) assert.Equal(t, "X-Request-ID", rec.Header().Get("Access-Control-Expose-Headers")) @@ -382,11 +382,38 @@ func TestSecurityHeadersMiddleware(t *testing.T) { assert.Equal(t, "DENY", rec.Header().Get("X-Frame-Options")) assert.Contains(t, rec.Header().Get("Content-Security-Policy"), "default-src 'self'") assert.Contains(t, rec.Header().Get("Content-Security-Policy"), "connect-src") + assert.Contains(t, rec.Header().Get("Content-Security-Policy"), "frame-ancestors 'none'") assert.Contains(t, rec.Header().Get("Content-Security-Policy"), "http://127.0.0.1:8765") assert.Contains(t, rec.Header().Get("Content-Security-Policy"), "http://localhost:8765") + assert.Contains(t, rec.Header().Get("Content-Security-Policy"), "https://lh3.googleusercontent.com") assert.Equal(t, "max-age=63072000; includeSubDomains", rec.Header().Get("Strict-Transport-Security")) } +func TestSecurityHeadersMiddlewareAllowsWebflowExtensionSurfacePages(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + middlewareHandler := SecurityHeadersMiddleware(handler) + + req := httptest.NewRequest( + http.MethodGet, + "/settings/account?surface=webflow-extension", + nil, + ) + rec := httptest.NewRecorder() + + middlewareHandler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "", rec.Header().Get("X-Frame-Options")) + assert.Contains( + t, + rec.Header().Get("Content-Security-Policy"), + "frame-ancestors 'self' https://webflow.com https://*.webflow.com http://localhost:1337 http://127.0.0.1:1337", + ) +} + func TestMiddlewareChaining(t *testing.T) { // Test that multiple middlewares work together correctly finalHandlerCalled := false diff --git a/settings.html b/settings.html index ff67edaee..7f5789396 100644 --- a/settings.html +++ b/settings.html @@ -50,6 +50,7 @@ - + +