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
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,14 @@ it bundles the foundations you reach for again and again behind small, well-defi
mailboxes), job system, timer/cron scheduler, metrics and health checks.
- **Messaging & caching** — in-memory, RabbitMQ and AWS SQS/SNS transports; in-memory and Redis caches.
- **Data & storage** — FreeSql data access, binary persistence (snapshot + WAL), local / S3 / MinIO
object storage, and a virtual filesystem (physical / zip / in-memory + an encrypted vault).
object storage, and a virtual filesystem (physical / zip / in-memory + an encrypted vault, plus
S3-compatible and database-backed backends and composable decorators).
- **Search, mail & workers** — Elasticsearch indexing with a constrained LINQ provider, IMAP/POP3
mail polling and an outbound mail queue, and a worker / manager runtime.
- **Networking, scripting & observability** — TCP/UDP servers with a framing/middleware pipeline,
Lua scripting and Scriban templating, and OpenTelemetry tracing + metrics export.
- **Terminal UI** — MVVM for terminal apps via `SquidStd.Tui`: observable ViewModels, a fluent
binder (+ `AutoBind` and a declarative DSL), ViewModel-first navigation, DryIoc wiring.

Everything is modular: take only the packages you need, each behind a clean abstraction with an
in-memory implementation for tests and an external backend for production.
Expand Down Expand Up @@ -195,13 +198,15 @@ dotnet new squidstd-manager -n Acme.Manager --messaging inmemory
| `SquidStd.Storage` | Local file storage backend (`AddFileStorage`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Storage/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Storage.svg)](https://www.nuget.org/packages/SquidStd.Storage/) |
| `SquidStd.Storage.S3` | S3/MinIO storage backend (`AddS3Storage`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Storage.S3/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Storage.S3.svg)](https://www.nuget.org/packages/SquidStd.Storage.S3/) |
| `SquidStd.Vfs.Abstractions` | Virtual filesystem contracts (`IVirtualFileSystem`, `ILockableFileSystem`, `VfsPath`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Vfs.Abstractions/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Vfs.Abstractions.svg)](https://www.nuget.org/packages/SquidStd.Vfs.Abstractions/) |
| `SquidStd.Vfs` | Virtual filesystem providers — physical, in-memory, and zip — plus `VfsDirectories`, a VFS-backed `DirectoriesConfig`. | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Vfs/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Vfs.svg)](https://www.nuget.org/packages/SquidStd.Vfs/) |
| `SquidStd.Vfs` | Virtual filesystem providers — physical, in-memory, and zip — plus composable decorators (`ReadOnlyFileSystem`, `ScopedFileSystem`, `OverlayFileSystem`, `CachingFileSystem`) and `VfsDirectories`. | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Vfs/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Vfs.svg)](https://www.nuget.org/packages/SquidStd.Vfs/) |
| `SquidStd.Vfs.S3` | S3-compatible VFS backend — AWS S3, MinIO, Cloudflare R2, Backblaze B2 — via the MinIO SDK (`RegisterS3FileSystem`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Vfs.S3/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Vfs.S3.svg)](https://www.nuget.org/packages/SquidStd.Vfs.S3/) |
| `SquidStd.Vfs.Database` | Database-backed VFS storing files as rows via SquidStd.Database / FreeSql (`RegisterDatabaseFileSystem`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Vfs.Database/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Vfs.Database.svg)](https://www.nuget.org/packages/SquidStd.Vfs.Database/) |

### Security — crypto & secrets

| Package | Description | Links |
|---------|-------------|-------|
| `SquidStd.Crypto` | OpenPGP key management/operations over an indexed keyring (`SquidStd.Crypto.Pgp`, `RegisterPgp`) plus the encrypted VFS vault decorator (Argon2id + per-entry AES-GCM). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Crypto/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Crypto.svg)](https://www.nuget.org/packages/SquidStd.Crypto/) |
| `SquidStd.Crypto` | OpenPGP key management/operations over an indexed keyring (`SquidStd.Crypto.Pgp`, `RegisterPgp`), password-based encryption (`PasswordCipher`, Argon2id + AES-256-GCM with a self-describing envelope), and the encrypted VFS vault decorator. | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Crypto/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Crypto.svg)](https://www.nuget.org/packages/SquidStd.Crypto/) |
| `SquidStd.Secrets.Aws` | AWS adapters for the secret seams — KMS envelope `ISecretProtector` and Secrets Manager `ISecretStore`. | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Secrets.Aws/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Secrets.Aws.svg)](https://www.nuget.org/packages/SquidStd.Secrets.Aws/) |

### Search
Expand All @@ -227,6 +232,12 @@ dotnet new squidstd-manager -n Acme.Manager --messaging inmemory
| `SquidStd.Workers` | Worker runtime: consume jobs, dispatch to `IJobHandler`s, publish heartbeats (`AddWorkers`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Workers/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Workers.svg)](https://www.nuget.org/packages/SquidStd.Workers/) |
| `SquidStd.Workers.Manager` | Job enqueue, heartbeat registry, offline sweep, opt-in ASP.NET endpoints (`AddWorkerManager`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Workers.Manager/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Workers.Manager.svg)](https://www.nuget.org/packages/SquidStd.Workers.Manager/) |

### User interface

| Package | Description | Links |
|---------|-------------|-------|
| `SquidStd.Tui` | MVVM for terminal apps on Terminal.Gui v2 — observable ViewModels, a fluent binder (+ `AutoBind` and a declarative DSL), ViewModel-first navigation, DryIoc wiring. | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Tui/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Tui.svg)](https://www.nuget.org/packages/SquidStd.Tui/) |

### Scripting & templating

| Package | Description | Links |
Expand Down
6 changes: 6 additions & 0 deletions docs/articles/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@
href: vfs-abstractions.md
- name: SquidStd.Vfs
href: vfs.md
- name: SquidStd.Vfs.S3
href: vfs-s3.md
- name: SquidStd.Vfs.Database
href: vfs-database.md
- name: SquidStd.Scripting.Lua
href: scripting-lua.md
- name: SquidStd.Templating
Expand Down Expand Up @@ -114,6 +118,8 @@
href: telemetry-opentelemetry.md
- name: SquidStd.Aws.Abstractions
href: aws-abstractions.md
- name: SquidStd.Tui
href: tui.md
- name: SquidStd.Crypto
href: crypto.md
- name: SquidStd.Secrets.Aws
Expand Down
1 change: 1 addition & 0 deletions docs/articles/tui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[!include[](../../src/SquidStd.Tui/README.md)]
1 change: 1 addition & 0 deletions docs/articles/vfs-database.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[!include[](../../src/SquidStd.Vfs.Database/README.md)]
1 change: 1 addition & 0 deletions docs/articles/vfs-s3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[!include[](../../src/SquidStd.Vfs.S3/README.md)]
31 changes: 31 additions & 0 deletions docs/tutorials/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,37 @@ survives a restart.

[!code-csharp[](../../samples/SquidStd.Samples.Crypto/Program.cs#step-4)]

## Password-based encryption

`PasswordCipher` encrypts a payload directly under a password — no key management required. Argon2id
derives the key, AES-256-GCM seals the data, and the result is a self-describing, versioned envelope
(salt, nonce, tag and KDF cost are embedded). Decryption needs only the password and the blob.

```csharp
using SquidStd.Crypto.Password;
using SquidStd.Crypto.Password.Data;

// Bytes round-trip.
byte[] blob = PasswordCipher.Encrypt(payloadBytes, "correct horse battery staple");
byte[] back = PasswordCipher.Decrypt(blob, "correct horse battery staple");

// Text round-trip — the envelope is base64-encoded, safe to store in config or JSON.
string protectedText = PasswordCipher.EncryptString("a secret", "pw");
string clear = PasswordCipher.DecryptString(protectedText, "pw");
```

The cost of the Argon2id key derivation defaults to `PbkdfCost.Moderate`. Raise it when protecting
long-lived secrets:

```csharp
// PbkdfCost.Sensitive — slower derivation, stronger resistance to offline attacks.
byte[] strong = PasswordCipher.Encrypt(payloadBytes, "pw", PbkdfCost.Sensitive);
```

A wrong password or tampered data raises `PasswordDecryptionException`. Use `PasswordCipher` for
user-supplied passwords; for app-key or KMS encryption use `CryptoUtils` / `ISecretProtector`
instead.

## Run it

```bash
Expand Down
2 changes: 2 additions & 0 deletions docs/tutorials/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,7 @@
href: vfs.md
- name: Crypto (PGP)
href: crypto.md
- name: Terminal UI (MVVM)
href: tui.md
- name: Secrets (KMS / Secrets Manager)
href: secrets.md
135 changes: 135 additions & 0 deletions docs/tutorials/tui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Terminal UI (MVVM)

Build a Counter terminal app with an observable ViewModel, then display it two ways — the
imperative `TuiView<T>` and the declarative `TuiComposedView<T>` DSL.

## What you'll build

A small Counter app where a `CounterViewModel` holds a `Value` property and an `IncrementCommand`.
You will see both ways to build the view:

1. **Imperative** — `TuiView<T>`: override `BuildLayout` to add Terminal.Gui widgets, then
override `Bind` to wire them through `ViewBinder`.
2. **Declarative** — `TuiComposedView<T>`: override `Compose` and return a `TuiNode` tree built
with the `Ui.*` DSL; bindings and layout are inferred from the node types.

## Prerequisites

- .NET 10 SDK
- `dotnet add package SquidStd.Tui`

## Steps

### 1. Define the ViewModel

Derive from `TuiViewModel` (which extends `ObservableObject` from CommunityToolkit.Mvvm) and mark
observable properties and relay commands with the standard source-generator attributes.

```csharp
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using SquidStd.Tui.ViewModels;

public sealed partial class CounterViewModel : TuiViewModel
{
[ObservableProperty]
private string _title = "Counter";

[ObservableProperty]
private string _value = "0";

[RelayCommand]
private void Increment() => Value = (int.Parse(Value) + 1).ToString();
}
```

### 2. Imperative view — `TuiView<T>`

Override `BuildLayout` to create and add Terminal.Gui widgets, then override `Bind` to wire them
through `ViewBinder`. The binder tracks property-changed notifications and updates widgets without
polling.

```csharp
using Terminal.Gui;
using SquidStd.Tui.Views;
using SquidStd.Tui.Binders;

public sealed class CounterView : TuiView<CounterViewModel>
{
private Label _value = null!;
private Button _inc = null!;

protected override void BuildLayout()
{
_value = new Label { X = 1, Y = 1 };
_inc = new Button { X = 1, Y = 3, Text = "+1" };
Add(_value, _inc);
}

protected override void Bind(ViewBinder b)
{
b.OneWayTitle(ViewModel, x => x.Title, this);
b.OneWayText(ViewModel, x => x.Value, _value);
b.Command(_inc, ViewModel.IncrementCommand);
}
}
```

### 3. Declarative view — `TuiComposedView<T>`

Instead of `BuildLayout` + `Bind`, derive from `TuiComposedView<T>` and return a node tree from
`Compose`. The framework infers binding direction from the node type (Label → one-way, TextField →
two-way, Button → command) and applies layout automatically.

```csharp
using SquidStd.Tui.Views;
using SquidStd.Tui.Dsl;

public sealed class CounterView : TuiComposedView<CounterViewModel>
{
protected override TuiNode<CounterViewModel> Compose() =>
Ui.VStack(
Ui.Label(x => x.Title),
Ui.Label(x => x.Value),
Ui.Button("+1", x => x.IncrementCommand));
}
```

`Ui.VStack` / `Ui.HStack` stack children vertically or horizontally and auto-assign `Y`/`X`
offsets. `Ui.TextField(x => x.Name)` is two-way by default; pass `BindMode.OneWay` to override.

### 4. Register and run

`RegisterTui()` adds the core TUI services and `TuiApplicationHost` to the DryIoc container.
`RegisterView<TView, TViewModel>()` registers the view for ViewModel-first resolution.
`TuiApplicationHost.RunAsync<TViewModel>()` resolves the root ViewModel, shows its view, and starts
the Terminal.Gui event loop.

```csharp
using DryIoc;
using SquidStd.Tui.Extensions;
using SquidStd.Tui.Hosts;

var container = new Container().RegisterTui();
container.RegisterView<CounterView, CounterViewModel>();

await container.Resolve<TuiApplicationHost>().RunAsync<CounterViewModel>();
```

### 5. ViewModel-first navigation

Push a new ViewModel onto the navigation stack from inside an existing ViewModel via the injected
`ITuiNavigator`:

```csharp
// inside CounterViewModel
await Navigator.NavigateToAsync<DetailViewModel>();
await Navigator.BackAsync();
```

The navigator resolves the view that was registered for `DetailViewModel` and shows it; `BackAsync`
pops the stack and restores the previous view.

## Next steps

- [SquidStd.Tui reference](../articles/tui.md)
94 changes: 94 additions & 0 deletions docs/tutorials/vfs.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,98 @@ zip — as a lockable singleton.

[!code-csharp[](../../samples/SquidStd.Samples.Vfs/Program.cs#step-3)]

## Alternative backends

### S3-compatible storage

Swap the in-memory backend for an S3-compatible store (AWS S3, MinIO, Cloudflare R2, Backblaze B2)
by installing `SquidStd.Vfs.S3` and replacing the `RegisterVfs` call:

```bash
dotnet add package SquidStd.Vfs.S3
```

```csharp
using SquidStd.Vfs.S3.Extensions;

container.RegisterS3FileSystem(o =>
{
o.Bucket = "app-data";
o.Aws.ServiceUrl = "https://s3.amazonaws.com"; // or MinIO/R2/B2 endpoint
o.Aws.AccessKey = "...";
o.Aws.SecretKey = "...";
});

// IVirtualFileSystem is now backed by S3 — the rest of your code is unchanged.
var fs = container.Resolve<IVirtualFileSystem>();
await fs.WriteAllBytesAsync("reports/2026.json", payload);
```

For native AWS with the default credential chain, omit `AccessKey`/`SecretKey` and set only
`o.Aws.Region`.

### Database-backed storage

Store files as rows in a relational database (SQLite, MySQL, PostgreSQL, …) with
`SquidStd.Vfs.Database`. Register `SquidStd.Database` first, then add the VFS backend:

```bash
dotnet add package SquidStd.Vfs.Database
```

```csharp
using SquidStd.Vfs.Database.Extensions;

// SquidStd.Database must already be registered on the container.
container.RegisterDatabaseFileSystem();

var fs = container.Resolve<IVirtualFileSystem>();
await fs.WriteAllBytesAsync("config/settings.json", payload);
```

FreeSql creates the `VfsFileEntity` table automatically on first access. This backend is
well-suited to single-process scenarios or where last-write-wins is acceptable.

## Composable decorators

Decorators wrap any `IVirtualFileSystem` to add behaviour without touching the backend. Construct
them directly and pass the result to `RegisterVfs`:

### Offline-resilient reads with `CachingFileSystem`

Wrap a remote backend (S3, database) with a local cache to keep reads working when the remote is
temporarily unreachable:

```csharp
using SquidStd.Vfs.Services;
using SquidStd.Vfs.S3.Services;

var s3 = new S3FileSystem(s3Options);

container.RegisterVfs(_ => new CachingFileSystem(
remote: s3,
cache: new PhysicalFileSystem("/var/cache/app")));
```

Reads prefer the remote and refresh the cache on success; on a transport failure they fall back to
the stale cached copy. Writes are write-through (remote then cache) and fail when the remote is
unreachable.

### Other decorators

```csharp
// Reject all writes — safe read-only access to a shared backend.
container.RegisterVfs(_ => new ReadOnlyFileSystem(new PhysicalFileSystem("/shared/data")));

// Chroot to a subdirectory — all paths are resolved relative to the prefix.
container.RegisterVfs(_ => new ScopedFileSystem(new PhysicalFileSystem("/var/lib/app"), "tenant-1"));

// Overlay — reads overlay-first then fall back to base; writes go to the overlay only.
container.RegisterVfs(_ => new OverlayFileSystem(
baseFileSystem: new PhysicalFileSystem("/defaults"),
overlay: new InMemoryFileSystem()));
```

## Run it

```bash
Expand All @@ -51,3 +143,5 @@ dotnet run --project samples/SquidStd.Samples.Vfs
## Next steps

- [SquidStd.Vfs reference](../articles/vfs.md)
- [SquidStd.Vfs.S3 reference](../articles/vfs-s3.md)
- [SquidStd.Vfs.Database reference](../articles/vfs-database.md)
Loading