feat(option): add sw offline

fix(openresty): scope sw injection per cert partition

fix(lint): satisfy revive and biome format for sw offline feature

docs: sw offline scope changelog

fix(frontend): use scoped query key for sw scope zones

fix(frontend): hide preview link in sw contact page editor

feat(frontend): add sw scope domain picker and contact page fields

refactor(frontend): generalize html editor workspace for reuse

feat(openresty): scope sw offline injection by route domains

feat(openresty): add sw offline domains snapshot field

feat(option): add sw offline domains scope option

docs: fill html editor workspace generalization detail

docs: sw offline scope implementation plan

docs: sw offline scope design

test(openresty): assert single merged access block in sw enabled servers

fix(openresty): restrict sw intercept to https server blocks

fix(openresty): version sw offline cache by html content

fix(agent): escape redir in sw challenge page to prevent xss

fix(agent): return sw.runtime module table and add lua spec

docs: sw offline fallback changelog

fix(frontend): memoize option map to preserve unsaved contact page edits

feat(frontend): add response pages module with contact page tab

feat(agent): ship sw offline lua assets and placeholder substitution

feat(config): wire sw offline options into config snapshot

feat(openresty): render sw offline assets and challenge intercept

feat(openresty): add sw offline ConfigSnapshot fields and placeholder

feat(db): seed sw offline options

feat(option): add sw offline config keys and validation

docs: add service worker offline fallback implementation plan

docs: adopt global-option pattern for SW offline fallback (matches origin error page)

docs: unify offline contact page with error pages as response pages

docs: service worker offline fallback design (issue #23)
This commit is contained in:
ryan
2026-08-08 12:22:21 +08:00
parent 734fe45baa
commit ca21ff3a5b
33 changed files with 4021 additions and 40 deletions
+79
View File
@@ -337,6 +337,85 @@ func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) {
}
}
func TestManagerApplyShipsSWAssetsAndReplacesSWDirPlaceholder(t *testing.T) {
tempDir := t.TempDir()
manager := &Manager{
MainConfigPath: filepath.Join(tempDir, "nginx.conf"),
RouteConfigPath: filepath.Join(tempDir, "routes.conf"),
CertDir: filepath.Join(tempDir, "certs"),
NginxCertDir: "/etc/nginx/openflare-certs",
LuaDir: filepath.Join(tempDir, "lua"),
NginxLuaDir: "/etc/nginx/openflare-lua",
Executor: &fakeExecutor{},
}
outcome := manager.Apply(
context.Background(),
"include __OPENFLARE_ROUTE_CONFIG__;",
"alias __OPENFLARE_SW_DIR__/sw.js;\nalias __OPENFLARE_SW_DIR__/offline.html;\ncontent_by_lua_file __OPENFLARE_SW_DIR__/challenge.lua;\nrequire(\"__OPENFLARE_LUA_DIR__\")",
[]protocol.SupportFile{
{Path: "sw/sw.js", Content: "js"},
{Path: "sw/offline.html", Content: "html"},
},
)
if outcome.Status != ApplyStatusSuccess {
t.Fatalf("Apply failed: %#v", outcome)
}
routeData, err := os.ReadFile(manager.RouteConfigPath)
if err != nil {
t.Fatalf("failed to read route config: %v", err)
}
rendered := string(routeData)
for _, want := range []string{
"/etc/nginx/openflare-certs/sw/sw.js",
"/etc/nginx/openflare-certs/sw/offline.html",
"/etc/nginx/openflare-certs/sw/challenge.lua",
} {
if !strings.Contains(rendered, want) {
t.Fatalf("route config missing %q, got %s", want, rendered)
}
}
if strings.Contains(rendered, openrestyrender.SWDirPlaceholder) {
t.Fatalf("route config still contains SW dir placeholder: %s", rendered)
}
for _, path := range []string{
filepath.Join(manager.CertDir, "sw", "sw.js"),
filepath.Join(manager.CertDir, "sw", "offline.html"),
filepath.Join(manager.CertDir, "sw", "challenge.lua"),
filepath.Join(manager.CertDir, "sw", "runtime.lua"),
} {
if _, err := os.Stat(path); err != nil {
t.Fatalf("expected SW asset %s to exist: %v", path, err)
}
}
for _, path := range []string{
filepath.Join(manager.LuaDir, "sw", "challenge.lua"),
filepath.Join(manager.LuaDir, "sw", "runtime.lua"),
} {
if _, err := os.Stat(path); err != nil {
t.Fatalf("expected SW lua asset %s to exist: %v", path, err)
}
}
checksum, err := manager.CurrentChecksum()
if err != nil {
t.Fatalf("CurrentChecksum failed: %v", err)
}
expected := bundleChecksum(
"include __OPENFLARE_ROUTE_CONFIG__;",
"alias __OPENFLARE_SW_DIR__/sw.js;\nalias __OPENFLARE_SW_DIR__/offline.html;\ncontent_by_lua_file __OPENFLARE_SW_DIR__/challenge.lua;\nrequire(\"__OPENFLARE_LUA_DIR__\")",
[]protocol.SupportFile{
{Path: "sw/sw.js", Content: "js"},
{Path: "sw/offline.html", Content: "html"},
},
)
if checksum != expected {
t.Fatalf("unexpected checksum: got %s want %s", checksum, expected)
}
}
func TestManagerRenderMainConfigInitializesWAFRuntimeInWorker(t *testing.T) {
manager := &Manager{NginxLuaDir: "/etc/nginx/openflare-lua"}
rendered := manager.renderMainConfig("events {}\nhttp {\n lua_shared_dict openflare_waf_config 1m;\n server {}\n}\n")