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.
This commit is contained in:
zarzet
2026-07-26 19:14:29 +07:00
parent 74615a60b5
commit 42c266eafc
2 changed files with 96 additions and 0 deletions
+58
View File
@@ -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()
@@ -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")
}
}