From 42c266eafc7d5169f54afcf2131e2e2e7c5e25ed Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 26 Jul 2026 19:14:29 +0700 Subject: [PATCH] fix(extensions): enforce minAppVersion and requiredRuntimeFeatures on load Both manifest gates were parsed but only checked by the Store UI, so manual .sflx installs and directory loads of incompatible extensions failed cryptically at runtime despite SIGNED_SESSION_GUIDE promising a clear refusal. validateExtensionLoad now rejects them on every install path, and ensureRuntimeReady re-checks so a gated package cannot be enabled anyway. Unknown runtime features and too-new feature contract versions fail with explicit messages; an empty app version (tests) skips the version gate. --- go_backend/extension_manager.go | 58 +++++++++++++++++++ .../extension_manager_supplement_test.go | 38 ++++++++++++ 2 files changed, 96 insertions(+) diff --git a/go_backend/extension_manager.go b/go_backend/extension_manager.go index 1030333e..ab49bd55 100644 --- a/go_backend/extension_manager.go +++ b/go_backend/extension_manager.go @@ -256,6 +256,13 @@ func getExtensionInitSettings(extensionID string) map[string]any { } func ensureRuntimeReadyLocked(ext *loadedExtension, applyStoredSettings bool) error { + // Gate enabling too, so a package installed with a failed gate cannot be + // switched on anyway. + if err := validateManifestGates(ext.Manifest); err != nil { + ext.Error = err.Error() + ext.Enabled = false + return err + } if ext.VM == nil || ext.runtime == nil { if err := initializeVMLocked(ext); err != nil { ext.Error = err.Error() @@ -822,7 +829,58 @@ func teardownVMLocked(ext *loadedExtension) { ext.initialized = false } +// supportedRuntimeFeatures maps every feature name the goja runtime provides +// to its current contract version (documented in SIGNED_SESSION_GUIDE.md). +// Bump a version here when a feature's contract changes. +var supportedRuntimeFeatures = map[string]int{ + "signedSession": 1, + "sessionRefresh": 1, + "sessionGrant": 1, + "globalAction": 1, + "webviewAuth": 1, +} + +// validateManifestGates enforces minAppVersion and requiredRuntimeFeatures +// on every load path (.sflx install, upgrade, directory load); the Store UI +// check alone never covered manual installs. An empty app version (tests, +// dev harnesses) skips the version gate. +func validateManifestGates(manifest *ExtensionManifest) error { + if manifest == nil { + return nil + } + minVersion := strings.TrimSpace(manifest.MinAppVersion) + appVersion := strings.TrimSpace(GetAppVersion()) + if minVersion != "" && appVersion != "" && compareVersions(appVersion, minVersion) < 0 { + return fmt.Errorf("requires app %s or later (installed: %s)", minVersion, appVersion) + } + for _, raw := range manifest.RequiredRuntimeFeatures { + name := strings.TrimSpace(raw) + if name == "" { + continue + } + wantVersion := 1 + if at := strings.LastIndex(name, "@"); at > 0 { + if v, err := strconv.Atoi(name[at+1:]); err == nil && v > 0 { + wantVersion = v + } + name = name[:at] + } + have, ok := supportedRuntimeFeatures[name] + if !ok { + return fmt.Errorf("requires runtime feature %q this app build does not provide", name) + } + if have < wantVersion { + return fmt.Errorf("requires runtime feature %s@%d (app provides @%d)", name, wantVersion, have) + } + } + return nil +} + func validateExtensionLoad(ext *loadedExtension) error { + if err := validateManifestGates(ext.Manifest); err != nil { + return err + } + ext.VMMu.Lock() defer ext.VMMu.Unlock() diff --git a/go_backend/extension_manager_supplement_test.go b/go_backend/extension_manager_supplement_test.go index 517cdaa8..e89d394c 100644 --- a/go_backend/extension_manager_supplement_test.go +++ b/go_backend/extension_manager_supplement_test.go @@ -155,3 +155,41 @@ registerExtension({ t.Fatal("expected all extensions unloaded") } } + +func TestValidateManifestGates(t *testing.T) { + originalVersion := GetAppVersion() + defer SetAppVersion(originalVersion) + SetAppVersion("4.5.0") + + if err := validateManifestGates(nil); err != nil { + t.Fatalf("nil manifest = %v", err) + } + if err := validateManifestGates(&ExtensionManifest{MinAppVersion: "4.9.0"}); err == nil { + t.Fatal("expected minAppVersion gate to fail") + } + if err := validateManifestGates(&ExtensionManifest{MinAppVersion: "4.5.0"}); err != nil { + t.Fatalf("equal version should pass: %v", err) + } + SetAppVersion("") + if err := validateManifestGates(&ExtensionManifest{MinAppVersion: "9.9.9"}); err != nil { + t.Fatalf("empty app version must skip the gate: %v", err) + } + SetAppVersion("4.5.0") + + pass := &ExtensionManifest{ + RequiredRuntimeFeatures: []string{"signedSession@1", "sessionGrant"}, + } + if err := validateManifestGates(pass); err != nil { + t.Fatalf("supported features should pass: %v", err) + } + if err := validateManifestGates(&ExtensionManifest{ + RequiredRuntimeFeatures: []string{"quantumDecrypt"}, + }); err == nil { + t.Fatal("unknown feature must fail") + } + if err := validateManifestGates(&ExtensionManifest{ + RequiredRuntimeFeatures: []string{"signedSession@99"}, + }); err == nil { + t.Fatal("future contract version must fail") + } +}