Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,8 @@ Process-sandbox database drivers launch inside OS-level isolation when available

Querya refuses **silent** unsandboxed launch. `SandboxProcessRunner` throws `SandboxOsIsolationUnavailableException` until the user approves via the consent dialog registered from the main window.

**Linux:** install `bubblewrap` and ensure unprivileged user namespaces are enabled if you want OS sandbox without manual confirmation.
**Linux:** install `bubblewrap` and ensure unprivileged user namespaces are enabled if you want OS sandbox without manual confirmation.

## Local extension sideload (`.zip` / `.qext`)

Installing from a local file does **not** verify integrity unless you paste an optional **SHA-256 checksum** in the install dialog. Marketplace installs always require a manifest checksum (#396). Sideload is intended for trusted local packages and development builds.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:querya_desktop/core/extensions/models/extension_manifest.dart';
import 'package:querya_desktop/core/layout/window_layout.dart';
import 'package:querya_desktop/core/market/marketplace_repository.dart';
import 'package:querya_desktop/features/extensions/presentation/widgets/extension_card.dart';
import 'package:querya_desktop/features/extensions/presentation/widgets/extension_sideload_dialog.dart';
import 'package:querya_desktop/shared/widgets/widgets.dart';

void showExtensionManagerDialog(material.BuildContext context) {
Expand Down Expand Up @@ -124,7 +125,18 @@ class _ExtensionManagerContentState
],
);
if (file == null) return;
await LocalExtensionInstaller().installFromPath(file.path);

if (!mounted) return;
final request = await showExtensionSideloadDialog(
context,
archivePath: file.path,
);
if (request == null) return;

await LocalExtensionInstaller().installFromPath(
request.archivePath,
expectedSha256: request.sha256Checksum,
);
await LocalExtensionRegistry.instance.reload();
if (!mounted) return;
setState(() {
Expand Down Expand Up @@ -269,7 +281,8 @@ class _ExtensionManagerContentState
const material.SizedBox(width: 12),
material.Expanded(
child: const Text(
'Install a local .zip or .qext package without the Marketplace.',
'Install a local .zip or .qext package. Integrity verification '
'is optional — paste SHA-256 if the publisher provides one.',
).muted().small(),
),
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import 'package:flutter/material.dart' as material;
import 'package:path/path.dart' as p;
import 'package:querya_desktop/shared/widgets/widgets.dart';

class ExtensionSideloadRequest {
const ExtensionSideloadRequest({
required this.archivePath,
this.sha256Checksum,
});

final String archivePath;
final String? sha256Checksum;
}

Future<ExtensionSideloadRequest?> showExtensionSideloadDialog(
material.BuildContext context, {
required String archivePath,
}) async {
return showAppDialog<ExtensionSideloadRequest>(
context: context,
builder: (dialogContext) => _ExtensionSideloadDialog(
archivePath: archivePath,
),
);
}

class _ExtensionSideloadDialog extends material.StatefulWidget {
const _ExtensionSideloadDialog({required this.archivePath});

final String archivePath;

@override
material.State<_ExtensionSideloadDialog> createState() =>
_ExtensionSideloadDialogState();
}

class _ExtensionSideloadDialogState
extends material.State<_ExtensionSideloadDialog> {
final _checksumController = material.TextEditingController();

@override
void dispose() {
_checksumController.dispose();
super.dispose();
}

void _submit() {
final checksum = _checksumController.text.trim();
material.Navigator.pop(
context,
ExtensionSideloadRequest(
archivePath: widget.archivePath,
sha256Checksum: checksum.isEmpty ? null : checksum,
),
);
}

@override
material.Widget build(material.BuildContext context) {
final cs = Theme.of(context).colorScheme;
final fileName = p.basename(widget.archivePath);

return material.AlertDialog(
title: const material.Text('Install local extension'),
content: material.SizedBox(
width: 440,
child: material.Column(
mainAxisSize: material.MainAxisSize.min,
crossAxisAlignment: material.CrossAxisAlignment.start,
children: [
material.Text('File: $fileName'),
const material.SizedBox(height: 12),
material.Container(
width: double.infinity,
padding: const material.EdgeInsets.all(12),
decoration: material.BoxDecoration(
color: cs.muted.withValues(alpha: 0.35),
borderRadius: material.BorderRadius.circular(8),
border: material.Border.all(color: cs.border),
),
child: material.Row(
crossAxisAlignment: material.CrossAxisAlignment.start,
children: [
material.Icon(
material.Icons.warning_amber_rounded,
size: 18,
color: cs.mutedForeground,
),
const material.SizedBox(width: 10),
material.Expanded(
child: material.Text(
'Local packages are not verified unless you provide a '
'SHA-256 checksum. Only install archives from sources you '
'trust.',
style: material.TextStyle(
fontSize: 13,
color: cs.mutedForeground,
),
),
),
],
),
),
const material.SizedBox(height: 12),
material.TextField(
controller: _checksumController,
decoration: const material.InputDecoration(
labelText: 'SHA-256 checksum (optional)',
hintText: '64-character hex digest from publisher',
),
autocorrect: false,
),
],
),
),
actions: [
OutlineButton(
onPressed: () => material.Navigator.pop(context),
child: const material.Text('Cancel'),
),
PrimaryButton(
onPressed: _submit,
child: const material.Text('Install'),
),
],
);
}
}
Loading