Skip to content

Commit 3685ded

Browse files
committed
Nuget Server
1 parent cadeaeb commit 3685ded

16 files changed

Lines changed: 2642 additions & 2 deletions

‎Directory.Packages.props‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
released independently. -->
4141
<PackageVersion Include="Shiny.Mediator" Version="6.10.0"/>
4242
<PackageVersion Include="ModelContextProtocol" Version="2.1.0"/>
43+
44+
<!-- The NuGet feed's version rules. Deliberately only this one of NuGet's client packages:
45+
NuGet.Packaging would bring Newtonsoft.Json and the signing stack with it, which is the
46+
end of being trim- and AOT-clean; the nuspec is read by hand instead. -->
47+
<PackageVersion Include="NuGet.Versioning" Version="7.9.0"/>
4348

4449
<PackageVersion Include="Shiny.Core" Version="5.8.0"/>
4550
<PackageVersion Include="Shiny.Net.Discovery" Version="5.8.0"/>
@@ -50,6 +55,10 @@
5055
hand-rolled one, because "our framing agrees with itself" is not the claim being
5156
tested. Nothing that ships depends on it. -->
5257
<PackageVersion Include="Grpc.Net.Client" Version="2.83.0"/>
58+
59+
<!-- Test-only, for the same reason: the NuGet feed tests push, restore-download, search and
60+
read metadata through NuGet's own client stack. -->
61+
<PackageVersion Include="NuGet.Protocol" Version="7.9.0"/>
5362
</ItemGroup>
5463
<ItemGroup>
5564
<PackageVersion Include="xunit.v3" Version="3.2.2"/>

‎Shiny.Net.HttpServer.slnx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<Project Path="src/Shiny.Net.HttpServer.Mediator/Shiny.Net.HttpServer.Mediator.csproj" />
3838
<Project Path="src/Shiny.Net.HttpServer.Mediator.SourceGenerators/Shiny.Net.HttpServer.Mediator.SourceGenerators.csproj" />
3939
<Project Path="src/Shiny.Net.HttpServer.Mobile/Shiny.Net.HttpServer.Mobile.csproj" />
40+
<Project Path="src/Shiny.Net.HttpServer.NuGet/Shiny.Net.HttpServer.NuGet.csproj" />
4041
<Project Path="src/Shiny.Net.HttpServer.OAuthLoopback/Shiny.Net.HttpServer.OAuthLoopback.csproj" />
4142
<Project Path="src/Shiny.Net.HttpServer.Proxy/Shiny.Net.HttpServer.Proxy.csproj" />
4243
<Project Path="src/Shiny.Net.HttpServer.SourceGenerators/Shiny.Net.HttpServer.SourceGenerators.csproj" />

‎build.slnf‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"src\\Shiny.Net.HttpServer.Tunnels\\Shiny.Net.HttpServer.Tunnels.csproj",
1515
"src\\Shiny.Net.HttpServer.WebDav\\Shiny.Net.HttpServer.WebDav.csproj",
1616
"src\\Shiny.Net.HttpServer.Tus\\Shiny.Net.HttpServer.Tus.csproj",
17+
"src\\Shiny.Net.HttpServer.NuGet\\Shiny.Net.HttpServer.NuGet.csproj",
1718
"src\\Shiny.Net.HttpServer.CalDav\\Shiny.Net.HttpServer.CalDav.csproj",
1819
"src\\Shiny.Net.HttpServer.DocumentDb\\Shiny.Net.HttpServer.DocumentDb.csproj",
1920
"src\\Shiny.Net.HttpServer.Mediator\\Shiny.Net.HttpServer.Mediator.csproj",

‎readme.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ JWT, OpenAPI, HPACK, QPACK — is built on what is in the box.
3535
| [Shiny.Net.HttpServer.Tunnels](https://www.nuget.org/packages/Shiny.Net.HttpServer.Tunnels) | Agent-backed tunnels — supervises `cloudflared`, `ngrok` or `tailscale` and reports the public URL. Desktop, server and CLI. iOS and tvOS forbid process creation, so the agents throw there with a message pointing at the SSH provider or the relay |
3636
| [Shiny.Net.HttpServer.OAuthLoopback](https://www.nuget.org/packages/Shiny.Net.HttpServer.OAuthLoopback) | The OAuth 2.0 / OpenID Connect loopback redirect receiver (RFC 8252). A desktop app or CLI signs in through the system browser, the way VS Code and `gh auth login` do, on an ephemeral `127.0.0.1` port. It validates `state`, accepts form_post, times out and shows a completion page, and it replaces `HttpListener`. PKCE and the token exchange stay with your OAuth client. Desktop only |
3737
| [Shiny.Net.HttpServer.Tus](https://www.nuget.org/packages/Shiny.Net.HttpServer.Tus) | Resumable uploads over the tus 1.0.0 protocol — a phone upload that drops half way resumes from the last byte instead of restarting; works with tus-js-client, Uppy and TusDotNetClient |
38+
| [Shiny.Net.HttpServer.NuGet](https://www.nuget.org/packages/Shiny.Net.HttpServer.NuGet) | A private NuGet feed — the V3 protocol with push, unlist, relist, search and autocomplete, so `dotnet nuget push`, `dotnet restore`, Visual Studio and Rider work against it unchanged. Packages are stored in NuGet's own folder-feed layout; versions use the official NuGet.Versioning |
3839
| [Shiny.Net.HttpServer.CommandLine](https://www.nuget.org/packages/Shiny.Net.HttpServer.CommandLine) | A .NET tool — `shinyhttpserver` — that serves a directory over WebDAV, so one address is both a browser file manager (browse, upload, rename, delete) and a drive Finder or Explorer can mount, with basic auth, per-operation permissions, and a QR code in the banner so a phone can scan its way in — `--tunnel` swaps the LAN address for a public pinggy.io tunnel so the phone need not be on the same network. In a terminal it opens a dashboard: live requests (clearable), uploads and downloads in progress, and a settings form that changes the tunnel, HTTPS, port, users and permissions on the running server (`--no-tui` for the plain banner) |
3940

4041
## Getting Started
@@ -156,6 +157,7 @@ is built on the one below and they compose in the same app.
156157
| **WebDAV** | RFC 4918 classes 1 and 2 over a directory — or over anything an `IWebDavFileSystem` describes — `PROPFIND`, `PROPPATCH`, `MKCOL`, `COPY`, `MOVE`, `LOCK`/`UNLOCK`, the `If` header and dead properties — so an app's storage mounts as a drive with no client to write |
157158
| **CalDAV & CardDAV** | Calendars and contacts over RFC 4791 / 6352: `/.well-known` discovery, principals per signed-in user, `MKCALENDAR`, validated `PUT` with `no-uid-conflict` and ETag preconditions, `calendar-query` / `addressbook-query` / multiget / free-busy / `sync-collection` — so the phone's own Calendar and Contacts apps sync with your app |
158159
| **Resumable uploads** | tus 1.0.0 core plus creation, creation-with-upload, defer-length, termination, expiration, checksum and concatenation — streamed to a pluggable store, bytes from an interrupted request kept, offsets durable across restarts |
160+
| **NuGet feed** | The NuGet V3 server API — service index, package content, registrations, search and autocomplete — plus push, delete-as-unlist and relist behind API keys, so a team's packages need a route, not another service |
159161
| **Lifecycle** | Start, stop and restart at runtime, serialized and idempotent, with an observable state — an embedded server gets toggled, not just booted. It also follows the device: rebinding when the addresses change, and following the app between foreground and background |
160162
| **Resilience** | Every state change says *why* it happened and carries the exception behind it, and a server that stops serving never does it quietly. A listener that dies underneath a running server is logged, reported and rebound; a transient accept failure is retried with backoff; a restart or a rebind whose bind is refused keeps trying and, if it never succeeds, says so unmistakably rather than leaving a server that claims to be running with nothing behind it. On by default — an app that has to opt in to not-silently-dying will not have opted in |
161163
| **Sign-in** | The loopback half of an OAuth / OpenID Connect sign-in from a desktop app or CLI. It opens the system browser, receives the redirect on an ephemeral `127.0.0.1` port and checks `state`. The code is handed to whichever OAuth client you already use, and the user is told to return to the app |

‎skills/shiny-httpserver/SKILL.md‎

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: shiny-httpserver
3-
description: Generate code using Shiny.Net.HttpServer — a dependency-light, AOT/trim-clean HTTP/1.1, HTTP/2 & HTTP/3 server that runs anywhere .NET runs, including .NET MAUI and native tvOS, where ASP.NET Core cannot. Covers routing, middleware, source-generated typed endpoints, results and JSON, content negotiation with XML/MessagePack/protobuf formatters in both directions, static files and Blazor WASM, uploads/downloads, WebSockets, SSE, SignalR-style switchboards (real-time two-way calls over SSE with groups, an operator, client results, resumable lines, and a generated typed .NET client), sessions, OpenAPI, authentication (Basic/API key/cookie/JWT), authorization, CORS, rate limiting, IP filtering, TLS and self-signed certificates, tunnelling (relay, SSH, quick tunnels, Azure Relay, and supervised cloudflared/ngrok/tailscale agents), serving a directory over WebDAV, serving gRPC and gRPC-Web, hosting an MCP server with RFC 9728 OAuth discovery, receiving the OAuth/OIDC loopback redirect so a desktop app or CLI can sign in through the system browser (RFC 8252), health checks, OpenTelemetry-shaped metrics and tracing, W3C access logs, request timeouts, output caching and conditional requests, request decompression, antiforgery and browser security headers, a reverse proxy with destination clusters, load balancing, health checks, session affinity, transforms, WebSocket forwarding and IConfiguration-driven routes, mDNS/Bonjour advertising and discovery, MAUI lifecycle (background/foreground, Android foreground service, network rebinding), host filtering against DNS rebinding, request localization, webhook signature verification (GitHub/Stripe/Slack/Standard Webhooks), idempotency keys, RFC 9530 content digests, 103 Early Hints, the PROXY protocol, RFC 6902 JSON Patch, API versioning with per-version OpenAPI, a Scalar API reference page, tus resumable uploads, CalDAV/CardDAV, automatic HTTPS via ACME (Let's Encrypt/ZeroSSL), and an in-memory test harness.
3+
description: Generate code using Shiny.Net.HttpServer — a dependency-light, AOT/trim-clean HTTP/1.1, HTTP/2 & HTTP/3 server that runs anywhere .NET runs, including .NET MAUI and native tvOS, where ASP.NET Core cannot. Covers routing, middleware, source-generated typed endpoints, results and JSON, content negotiation with XML/MessagePack/protobuf formatters in both directions, static files and Blazor WASM, uploads/downloads, WebSockets, SSE, SignalR-style switchboards (real-time two-way calls over SSE with groups, an operator, client results, resumable lines, and a generated typed .NET client), sessions, OpenAPI, authentication (Basic/API key/cookie/JWT), authorization, CORS, rate limiting, IP filtering, TLS and self-signed certificates, tunnelling (relay, SSH, quick tunnels, Azure Relay, and supervised cloudflared/ngrok/tailscale agents), serving a directory over WebDAV, serving gRPC and gRPC-Web, hosting an MCP server with RFC 9728 OAuth discovery, receiving the OAuth/OIDC loopback redirect so a desktop app or CLI can sign in through the system browser (RFC 8252), health checks, OpenTelemetry-shaped metrics and tracing, W3C access logs, request timeouts, output caching and conditional requests, request decompression, antiforgery and browser security headers, a reverse proxy with destination clusters, load balancing, health checks, session affinity, transforms, WebSocket forwarding and IConfiguration-driven routes, mDNS/Bonjour advertising and discovery, MAUI lifecycle (background/foreground, Android foreground service, network rebinding), host filtering against DNS rebinding, request localization, webhook signature verification (GitHub/Stripe/Slack/Standard Webhooks), idempotency keys, RFC 9530 content digests, 103 Early Hints, the PROXY protocol, RFC 6902 JSON Patch, API versioning with per-version OpenAPI, a Scalar API reference page, tus resumable uploads, CalDAV/CardDAV, a private NuGet feed (V3 protocol with push), automatic HTTPS via ACME (Let's Encrypt/ZeroSSL), and an in-memory test harness.
44
auto_invoke: true
55
triggers:
66
- Shiny.Net.HttpServer
@@ -374,6 +374,23 @@ triggers:
374374
- tus-js-client
375375
- Uppy
376376
- TusDotNetClient
377+
- Shiny.Net.HttpServer.NuGet
378+
- MapNuGetFeed
379+
- NuGetFeedOptions
380+
- NuGetFeedMountBuilder
381+
- INuGetPackageStore
382+
- DiskNuGetPackageStore
383+
- NuGetPackage
384+
- NuGetDeleteBehavior
385+
- NuGetPushContext
386+
- NuGetApiKeyContext
387+
- nuget feed
388+
- nuget server
389+
- private nuget feed
390+
- package feed
391+
- host nuget packages
392+
- dotnet nuget push
393+
- X-NuGet-ApiKey
377394
- Idempotency-Key
378395
- AddIdempotency
379396
- UseIdempotency
@@ -488,6 +505,7 @@ Invoke this skill when the user wants to:
488505
- Cache responses, honour conditional requests, bound how long a handler may take, or forward a
489506
route to another server
490507
- Test endpoints without binding a port
508+
- Host a private NuGet feed that `dotnet nuget push` and `dotnet restore` talk to
491509
- Sign a desktop app or CLI in through the system browser (the OAuth/OIDC loopback redirect,
492510
what VS Code and `gh auth login` do) without `HttpListener`
493511

@@ -525,6 +543,7 @@ dotnet add package Shiny.Net.HttpServer.Testing # in-memory HttpClient
525543
dotnet add package Shiny.Net.HttpServer.Tunnels # cloudflared / ngrok / tailscale agents (desktop + CLI only)
526544
dotnet add package Shiny.Net.HttpServer.OAuthLoopback # OAuth/OIDC loopback redirect receiver (desktop + CLI only)
527545
dotnet add package Shiny.Net.HttpServer.Tus # tus 1.0.0 resumable uploads
546+
dotnet add package Shiny.Net.HttpServer.NuGet # a private NuGet V3 feed (push, restore, search)
528547
dotnet add package Shiny.Net.HttpServer.CalDav # calendars & contacts over CalDAV/CardDAV
529548
dotnet add package Shiny.Net.HttpServer.Acme # automatic HTTPS via Let's Encrypt/ZeroSSL (public hosts, not phones)
530549

@@ -1367,6 +1386,47 @@ app.UseCors(p => p.WithOrigins("https://app.example.com").WithTusHeaders()); //
13671386
- `RemoveExpiredUploadsAsync()` on the returned builder runs cleanup from a platform background job.
13681387
- The routes are excluded from OpenAPI. Do not describe them.
13691388

1389+
### A private NuGet feed
1390+
1391+
`MapNuGetFeed` serves the NuGet V3 server API, so `dotnet restore`, `dotnet nuget push`,
1392+
`dotnet package search`, Visual Studio and Rider work against it as a plain package source. Reach for it
1393+
when the user says *NuGet server*, *private feed*, *package feed*, or *host our packages*. Tier 1:
1394+
nine raw routes mapped in one call (package `Shiny.Net.HttpServer.NuGet`; versions via the official
1395+
`NuGet.Versioning`).
1396+
1397+
```csharp
1398+
app.MapNuGetFeed("/nuget", o =>
1399+
{
1400+
o.Store = new DiskNuGetPackageStore(Path.Combine(dataDir, "packages")); // required
1401+
o.ApiKeys.Add(config["NuGet:ApiKey"]!); // X-NuGet-ApiKey, compared in constant time
1402+
o.DeleteBehavior = NuGetDeleteBehavior.Unlist; // default; .Delete removes files
1403+
o.OnBeforePushAsync = ctx =>
1404+
{
1405+
if (!ctx.Package.Id.StartsWith("Contoso.")) ctx.Reject(403, "Only Contoso.* packages.");
1406+
return ValueTask.CompletedTask;
1407+
};
1408+
})
1409+
.ForReads(r => r.RequireAuthorization()); // optional: a private feed (Basic creds in nuget.config)
1410+
1411+
// client: dotnet nuget add source http://host:5000/nuget/v3/index.json -n team --allow-insecure-connections
1412+
// dotnet nuget push My.Pkg.1.0.0.nupkg -s team -k <key>
1413+
```
1414+
1415+
- The package source URL is always `{prefix}/v3/index.json`. Give users that URL, not the prefix.
1416+
- A writable feed with no `ApiKeys` and no `ValidateApiKeyAsync` throws when mapped. Use
1417+
`ReadOnly = true` for a feed nobody pushes to, or `RequireApiKey = false` only when `ForWrites(...)`
1418+
adds authorization.
1419+
- NuGet refuses plain-HTTP sources unless `allowInsecureConnections="true"` is set on the source. On a
1420+
public host, serve it with TLS or ACME.
1421+
- Push size: `MaxPackageSize` (250 MB) **and** `Limits.MaxRequestBodySize` (30 MB) both apply. Raise
1422+
the server limit for big packages.
1423+
- Behind a proxy that rewrites the host, set `PublicBaseUrl`. Every link in the protocol is absolute.
1424+
- `DiskNuGetPackageStore` uses NuGet's own folder-feed layout (`{id}/{version}/{id}.{version}.nupkg`),
1425+
so the directory is also a local source and can be seeded by copying files in (call `Reload()`).
1426+
- Not supported: `.snupkg` symbol packages, the catalog, download counts, the V2/OData API. Do not
1427+
promise them.
1428+
- The routes are excluded from OpenAPI. Do not describe them.
1429+
13701430
## Realtime
13711431

13721432
```csharp

0 commit comments

Comments
 (0)