CapNet 0.3.0
dotnet add package CapNet --version 0.3.0
NuGet\Install-Package CapNet -Version 0.3.0
<PackageReference Include="CapNet" Version="0.3.0" />
<PackageVersion Include="CapNet" Version="0.3.0" />
<PackageReference Include="CapNet" />
paket add CapNet --version 0.3.0
#r "nuget: CapNet, 0.3.0"
#:package CapNet@0.3.0
#addin nuget:?package=CapNet&version=0.3.0
#tool nuget:?package=CapNet&version=0.3.0
capjs-dotnet
Thin .NET wrapper around Cap — the privacy-first, self-hosted CAPTCHA built on proof-of-work, RSW time-lock puzzles, and optional browser instrumentation.
The library does not reimplement Cap's challenge algorithm. It calls upstream capjs-core through Jering.Javascript.NodeJS, keeping the .NET surface small (~150 LOC) and delegating cryptography, RSW math, and instrumentation script generation to the library Cap's maintainer ships and tests. The .NET side owns three things: HTTP entry points, redeem-token storage, and JWT-signature replay protection.
Why
If your app is .NET (ASP.NET MVC 5 / Framework 4.8 or newer) and you want Cap's protections without running Cap's standalone Bun server as a sidecar, embed the bridge in-process.
Install
<PackageReference Include="CapNet" Version="0.1.0" />
Ships the .NET library. The Node bridge (bridge.js + node_modules) lives in the repo's CapNet.Bridge.Js/ folder; deploy it next to your application binaries and point CapService at its path. Node.exe must be available on the host (the typical Jering-hosting requirement).
Usage
using CapNet;
using CapNet.Challenges;
using CapNet.Storage;
using Jering.Javascript.NodeJS;
// At startup
var services = new ServiceCollection();
services.AddNodeJS();
var node = services.BuildServiceProvider().GetRequiredService<INodeJSService>();
var cap = new CapService(
secret: Environment.GetEnvironmentVariable("CAP_SECRET"), // ≥16 UTF-8 bytes
node: node,
bridgePath: Path.Combine(AppContext.BaseDirectory, "bridge", "bridge.js"),
state: new MyAppCacheAdapter(), // ICapStateStore — see below
defaults: new ChallengeOptions
{
ChallengeCount = 30,
ChallengeSize = 32,
ChallengeDifficulty = 3,
Instrumentation = new { blockAutomatedBrowsers = true, obfuscationLevel = 3 },
Scope = "my-app",
});
// In your captcha controller
[HttpPost, Route("cap/challenge")]
public async Task<HttpResponseMessage> Challenge()
=> Json(await cap.IssueChallengeJsonAsync());
[HttpPost, Route("cap/redeem")]
public async Task<HttpResponseMessage> Redeem(HttpRequestMessage req)
{
var body = await req.Content.ReadAsStringAsync();
var outcome = await cap.RedeemAsync(body);
return new HttpResponseMessage(outcome.HttpStatus)
{
Content = new StringContent(outcome.ResponseJson, Encoding.UTF8, "application/json"),
};
}
// In your registration / login action filter
bool ok = await cap.VerifyRedeemTokenAsync(submittedToken);
The ICapStateStore contract
CapNet keeps no in-process state. It needs a tiny key/value store with TTL, which you wire to whatever cache your app already runs — IMemoryCache, IDistributedCache, Redis via StackExchange, Azure Cache for Redis, etc. The library prefixes its keys (capnet:nonce:…, capnet:redeem:…), so the namespace is self-contained.
public interface ICapStateStore
{
Task PutAsync(string key, string value, TimeSpan ttl);
Task<string> GetAsync(string key);
Task RemoveAsync(string key);
}
For a single-process dev box, use the bundled MemoryCapStateStore. For multi-server deployments, write a 15-line adapter against your existing distributed cache.
What you get for free, vs what stays your job
| CapNet handles | Your app handles | |
|---|---|---|
| Challenge generation (PoW / RSW / instrumentation) | ✅ via capjs-core | |
| JWT sign/verify | ✅ via capjs-core | |
| Redeem-token TTL | ✅ via ICapStateStore |
|
| Replay protection | ✅ via ICapStateStore |
|
| HTTP plumbing | one controller, three routes | |
| Form integration / hidden field | yours | |
| Action filter wiring | yours | |
| Backing cache (memory or Redis) | you provide via ICapStateStore |
|
| RSW keypair persistence | you persist once, load on startup |
Architecture
Browser (Cap widget)
│
│ POST /cap/challenge ← capjs-core's JSON, verbatim
│ POST /cap/redeem
▼
ASP.NET (your app)
│
│ CapService.IssueChallengeJsonAsync
│ CapService.RedeemAsync
▼
Jering.Javascript.NodeJS ──► bridge.js ──► capjs-core (npm)
│ │
│ ICapStateStore (your impl) │
└─► redis / memory cache │
└──► generateChallenge / validateChallenge
Limitations / known issues
- Widget v0.1.51 has a format-2 speculative-fetch race. When the response is
format: 2, the widget can settle its internal state machine beforesolve()registers its listener, hanging forever. Use format 1 (sha256-pow + optional top-level instrumentation) with the upstream widget. The library exposes format 2 / RSW correctly for clients that drive the protocol directly (seeCapNet.E2E/tests/format2-all-protocols.spec.ts). capjs-coreis pre-1.0 — only versions0.1.xare published. Pin a specific version transitively; treat each bump as a deliberate release of CapNet.- Node.exe required on every host. Jering manages a child-process pool. If "no Node on the auth tier" is a hard policy, this library isn't the right shape — run Cap's standalone Bun server out-of-process instead.
Development
# Build everything
dotnet build CapNet.sln
# Run the demo (http://localhost:5500/)
dotnet run --project CapNet.Demo.Web -- http://localhost:5500/
# Run unit + E2E tests
npm --prefix CapNet.E2E install
npm --prefix CapNet.E2E exec playwright install chromium
npm --prefix CapNet.E2E test
The demo loads the real @cap.js/widget from jsDelivr, so a network connection is required for the manual smoke test.
Building from source
If you've cloned the repo and want to consume your local build from another solution (e.g. to try an unreleased change):
./scripts/pack-local.ps1 # writes ./packages/CapNet.<version>.nupkg
Then in the consumer's nuget.config, add the folder as a source and reference the version you built.
Credits
This is a wrapper. The whole captcha — PoW, RSW, instrumentation, the JS bridge between server and widget — is the work of Tiago Rangel and the Cap contributors. If you find CapNet useful, please star the upstream Cap repo and consider supporting Tiago directly.
License
MIT. Cap and capjs-core are also MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Jering.Javascript.NodeJS (>= 7.0.0)
- Newtonsoft.Json (>= 13.0.3)
- System.Runtime.Caching (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.