FreshIdentity.Core
2.0.0-preview.0.5
dotnet add package FreshIdentity.Core --version 2.0.0-preview.0.5
NuGet\Install-Package FreshIdentity.Core -Version 2.0.0-preview.0.5
<PackageReference Include="FreshIdentity.Core" Version="2.0.0-preview.0.5" />
<PackageVersion Include="FreshIdentity.Core" Version="2.0.0-preview.0.5" />
<PackageReference Include="FreshIdentity.Core" />
paket add FreshIdentity.Core --version 2.0.0-preview.0.5
#r "nuget: FreshIdentity.Core, 2.0.0-preview.0.5"
#:package FreshIdentity.Core@2.0.0-preview.0.5
#addin nuget:?package=FreshIdentity.Core&version=2.0.0-preview.0.5&prerelease
#tool nuget:?package=FreshIdentity.Core&version=2.0.0-preview.0.5&prerelease
FreshIdentity
Identity and Auth service for mere mortals. Batteries included. 😊
FreshIdentity takes care of
- Identity – People or services.
- Authentication – Who are you? Passwords, GitHub, and phishing-resistant passkeys.
- Authorization – What are you allowed to do?
out of the box.
The main usage scenario is to provide
- single-sign-on (SSO), and
- role-based authorization
for other apps and services.
How-to
Force a password change on next login
fresh property set <user> <email> mustChangePassword true
Configuration
The service listens on plain HTTP at http://localhost:5555. Terminate public TLS at a reverse proxy and forward traffic to port 5555. Keep machine-specific database paths and credentials in the ignored appsettings.Development.json or in IdentityConfig__... environment variables.
Browser authentication is organized into persisted authentication sites. A site contains one or more exact public origins, a login title, its enabled authentication methods, an optional cookie domain, its passkey relying-party ID, and GitHub bindings. Accounts, credentials, roles, and properties remain global; a site is a browser session boundary, not a tenant. Runtime site changes apply without restarting the service and survive restarts in db.json. IdentityConfig:Sites:Bootstrap is seed-only: once a store contains sites, changing bootstrap settings does not overwrite them.
Cookies are host-only by default. Configure a site's parent cookie domain only when its related origins intentionally share sessions and every sibling host is equally trusted: protected cookies are bearer values that a compromised sibling can replay even though it cannot decrypt them. Cookies and continuations are cryptographically bound to the site and cannot be replayed at an unrelated site.
Every browser request must resolve to one enabled exact site origin. Production origins require HTTPS; loopback HTTP is accepted only for development. DNS, TLS, and request routing remain operator responsibilities. FreshIdentity does not discover or configure proxies, containers, certificates, or routes. If a Production store has no site, the administrative API remains available for recovery while browser authentication returns 404.
Redirect roles use boundary-aware scopes. For example, https://app.example:redirect permits that origin and paths below it, but not https://app.example.evil.test. OAuth2 client callback URIs are matched exactly, including their query string.
Persistence and bootstrap access
Set IdentityConfig__DataBaseDir to a writable, persistent directory for every administrable deployment. Without it, FreshIdentity uses an ephemeral in-memory store and deliberately does not reveal its bootstrap API key.
When a new persistent store is created, FreshIdentity writes db.json, Data Protection keys under keys/, and a one-time bootstrap-apikey.txt into that directory. The key belongs to the initial global-admin account (*:*). Load it into a secret manager or protected environment variable, then delete the bootstrap file. Restarts reuse db.json and do not recreate the file; deleting the file does not revoke the credential hash stored in the database. Restrict the entire directory to the service account because it contains password and machine-credential hashes, identity data, authorization state, and cookie-protection keys. FreshIdentity applies owner-only modes on Unix; on Windows, configure a service-account-only directory ACL.
The database has an explicit schema version. Opening an older unversioned store automatically migrates API keys and OAuth client secrets to one-way SHA-256 hashes while preserving the existing credentials. Schema version 1 stores are upgraded when passkey collections are initialized, and schema version 2 stores are upgraded to version 3 when authentication sites and site-bound passkey metadata are added. Raw machine and enrollment secrets are never written to the versioned db.json; they are returned only at creation. Passkey public keys, credential IDs, attestation objects, and client data are not authentication secrets, but they are security- and privacy-sensitive account metadata and remain protected by the data-directory controls. Existing passkeys can be migrated only when exactly one passkey-enabled site can be resolved safely; startup fails rather than guessing between relying parties. The site-bound cookie protection purpose signs out existing browser sessions during this upgrade. The migration is forward-only, so back up the protected data directory before upgrading if binary rollback is required. Backups of a pre-migration database still contain live secrets and require the original strict protection.
New Data Protection keys are encrypted with current-user DPAPI on Windows, so the service must retain the same Windows identity. Existing plaintext key rings are not rewritten automatically: rotate them during a planned maintenance window by stopping the service and replacing keys/, which signs out every browser session. Browser sessions are bound to both the authentication-site version and account version, so site configuration changes and credential or account changes—including passkey enrollment, rename, or revocation—revoke previously issued sessions. The browser performing a successful self-service passkey change receives a replacement session. Routine WebAuthn signature-counter and backup-state updates deliberately preserve the account version, so signing in with a passkey does not sign out other browsers. Normal logout clears only the current browser; mutate the account (for example, change its password) when every issued session must be revoked.
FreshIdentity currently targets a single active service instance. OAuth codes, access tokens, GitHub correlation state, and short-lived single-use passkey ceremony state are process-local, and db.json is not a distributed store. Horizontal deployment requires a shared account/state implementation; sharing only the Data Protection key ring is insufficient. Until then, do not run multiple writers (and keep requests sticky if using a warm standby). A future shared key ring must use an explicit certificate or external key-encryption provider.
POST /api authenticates from Authorization: apikey <key>; any authorization value in the JSON body is ignored. The key must be enabled and unexpired, its account must be enabled, and the account must have the global-admin role. Keep API keys out of URLs, committed settings, and logs.
Administrative reads return explicit safe account, authentication-site, GitHub-binding, password, GitHub-identity, API-key, OAuth-client, passkey, and pending-enrollment summaries. They never include password hashes, machine-credential or enrollment hashes and secrets, passkey public-key/attestation blobs, or raw GitHub provider data. API keys have non-secret credential IDs so they can be listed, updated, enabled, disabled, and revoked without supplying the secret. Newly created API keys, OAuth client secrets, and passkey enrollment secrets are returned only by their creation call; capture them immediately. API responses are marked no-store.
Administrative mutations accept optional account or site ETags. A stale precondition is rejected with HTTP 409 and etag_mismatch. Operations that would remove the final active global-admin API access path are rejected with last_administrator; disabling or deleting the final usable browser site is rejected with last_authentication_site. The caller must explicitly request the corresponding lockout override. Use that override only during a planned decommission. Successful administrative mutations emit structured audit logs containing the actor account, non-secret API-key ID, operation, target, result, timestamp, and trace ID; secrets and property values are never audit fields.
New and changed passwords must contain 12–256 characters and cannot be blank or whitespace-only. They are protected with versioned PBKDF2-HMAC-SHA512 hashes. Existing legacy SHA-512 credentials remain usable, including passwords up to 8,192 characters (subject to the 16 KiB encoded form limit), and are upgraded automatically after a successful password login. New credentials cannot use those grandfathered lengths.
Password sign-in and password-change submissions are limited to 10 attempts per minute per client IP. Passkey option, registration, assertion, rename, and revocation requests share a 30-request-per-minute per-client-IP limit. Browser forms use antiforgery tokens, and accounts marked mustChangePassword receive only a short-lived restricted session until the change succeeds. OAuth authorization and token requests are each limited to 60 requests per minute per client IP; state is limited to 1,024 characters, and the in-memory authorization-code store allows at most 256 codes per client and 4,096 total. The access-token store allows 256 tokens per identity and 4,096 total. OAuth authorization-code failures return JSON OAuth errors with protocol-appropriate status codes and no-store caching. GitHub login endpoints allow 20 requests per minute per client IP, and upstream calls time out after 10 seconds. When deploying behind a proxy, list its IP address in the semicolon-separated IdentityConfig__KnownProxies setting (for example, 10.0.0.10). FreshIdentity accepts forwarded client/protocol headers only from loopback and those explicit proxies.
Forward-auth clients pass their API key in the X-FreshIdentity-ApiKey header; never place it in the query string. Browser authentication and continuation cookies are encrypted, authenticated, Secure, HttpOnly, and SameSite=Lax, using the persisted Data Protection keys.
Passkeys
For the shortest local passkey test, build the CLI and run fresh local as described in Local playground. It starts the real service, provisions a passwordless test account, and opens the browser enrollment flow without requiring configuration or manual API-key handling.
Passkeys are a standard site authentication method. They use the .NET 10 WebAuthn verifier, discoverable credentials, and required local user verification (device PIN or biometrics). FreshIdentity accepts synced consumer passkeys, platform authenticators, password managers, and user-verifying security keys. It requests no authenticator attestation and does not claim AAL3 hardware-key assurance. Passkey private keys never reach FreshIdentity.
Development automatically creates a localhost site with password and passkey authentication enabled, so no passkey environment flag is required. A Production bootstrap site can be seeded with exact configuration:
IdentityConfig__Sites__Bootstrap__primary__Origins=https://login.example.org
IdentityConfig__Sites__Bootstrap__primary__Name=Primary
IdentityConfig__Sites__Bootstrap__primary__LoginTitle=Example login
IdentityConfig__Sites__Bootstrap__primary__PasskeyRelyingPartyId=login.example.org
The relying-party ID is a DNS domain without a scheme, port, or path. An origin includes the public scheme, host, and any non-default port, but no path. Use the most specific stable login hostname: a parent RP ID such as example.org intentionally makes the credential available to eligible subdomains and expands the trust boundary. An RP ID becomes immutable while passkeys or a pending enrollment reference its site. Multiple related origins may share a site and RP ID; unrelated RP IDs require separate passkeys even when the account is shared.
The legacy IdentityConfig__Passkeys__ServerDomain and IdentityConfig__Passkeys__AllowedOrigins pair remains a bootstrap adapter for a store without sites. IdentityConfig__Passkeys__Enabled=false is an emergency global kill switch; Enabled=true is unnecessary. A trusted TLS-terminating proxy must preserve the public host and Origin and forward the trusted protocol as described above.
An already signed-in person can add, rename, and revoke passkeys on the account page. To provision a passwordless account, an administrator creates the account and a short-lived, single-use enrollment:
dotnet run --project src/fresh/fresh.csproj -- account create --label "Example person"
dotnet run --project src/fresh/fresh.csproj -- passkey enrollment create <account-id> --site <site-id> --minutes 60
The second command prints the enrollment secret exactly once. Send it through an appropriately protected channel; it is a bearer credential until used or expired. The database stores only its SHA-256 hash. The user opens /auth/passkey/enroll, pastes the code, names the passkey, and completes the browser prompt. Successful registration consumes the enrollment atomically and signs in the new account. Creating a replacement enrollment invalidates the previous one. Public anonymous account signup is deliberately not provided.
Operators can inspect safe metadata and recover an account whose authenticators were lost:
fresh passkey list <account-id>
fresh passkey rename <passkey-id> "Work laptop"
fresh passkey revoke <passkey-id>
fresh passkey enrollment create <account-id> --site <site-id> --minutes 60
fresh passkey enrollment revoke <enrollment-id>
A passkey-only user should register multiple authenticators or use a synced passkey provider. Administrative revocation plus a new enrollment is the recovery path. Enrollment is capped at 24 hours; an account can hold at most 10 passkeys; names are limited to 200 characters. Ceremony state is server-side, single-use, and retained for five minutes. Passkey authentication is independent of a password credential's mustChangePassword flag.
GitHub OAuth
GitHub client registrations remain operator/startup configuration so client secrets can come from environment or secret providers rather than db.json. Sites bind to configured registration IDs and exact callback URIs at runtime. One GitHub OAuth App may serve up to ten exact callbacks, or sites may use separate registrations for independent ownership, branding, rotation, and blast-radius reduction.
IdentityConfig__Github__Registrations__main__DisplayName=GitHub
IdentityConfig__Github__Registrations__main__ClientId=<github-client-id>
IdentityConfig__Github__Registrations__main__ClientSecret=<github-client-secret>
IdentityConfig__Github__Registrations__main__CallbackUris=https://login.example.org/auth/github/callback
CallbackUris seeds bindings only while the database has no persisted sites. After bootstrap, add or remove bindings with fresh site github; update the corresponding callback list at GitHub separately. Callbacks must be exact site URLs ending in /auth/github/callback; wildcards are rejected. FreshIdentity uses direct site callbacks, short-lived single-use server-side state, a protected site correlation cookie, and PKCE S256. OAuth codes, state, PKCE verifiers, access tokens, and client secrets are never logged or persisted.
The legacy singular IdentityConfig__Github__ClientId, ClientSecret, and CallbackUri settings create a registration named default during migration.
appsettings.json
{
"IdentityConfig": {
"DataBaseDir": "./db",
"Sites": {
"Bootstrap": {
"primary": {
"Name": "Primary",
"LoginTitle": "Example login",
"Origins": "https://login.example.org",
"CookieDomain": ".example.org",
"PasswordEnabled": true,
"PasskeysEnabled": true,
"PasskeyRelyingPartyId": "login.example.org"
}
}
},
"Github": {
"Registrations": {
"main": {
"DisplayName": "GitHub",
"ClientId": "<github-client-id>",
"ClientSecret": "<github-client-secret>",
"CallbackUris": "https://login.example.org/auth/github/callback"
}
}
},
"SignalCliRestApi": {
"Url": "http://x.x.x.x:port",
"Number": "+12345xxxx"
}
}
}
docker-compose.yml
services:
login:
# ...
environment:
- IdentityConfig__DataBaseDir=/data
- IdentityConfig__Sites__Bootstrap__primary__Name=Primary
- IdentityConfig__Sites__Bootstrap__primary__Origins=https://login.example.org
- IdentityConfig__Sites__Bootstrap__primary__CookieDomain=.example.org
- IdentityConfig__Sites__Bootstrap__primary__PasskeyRelyingPartyId=login.example.org
- IdentityConfig__Github__Registrations__main__DisplayName=GitHub
- IdentityConfig__Github__Registrations__main__ClientId=<github-client-id>
- IdentityConfig__Github__Registrations__main__ClientSecret=<github-client-secret>
- IdentityConfig__Github__Registrations__main__CallbackUris=https://login.example.org/auth/github/callback
volumes:
- ./data:/data
# ...
Quick start
Local playground
The CLI executable produced by the repository build contains the real FreshIdentity login host and its browser assets. Build it once, then start the persistent localhost playground:
.\build.cmd
.\publish\fresh\fresh.exe local
./build.sh
./publish/fresh/fresh local
The first run creates an isolated passwordless test account and opens the passkey enrollment page. Click Create passkey and complete the browser prompt. Stop the foreground server with Ctrl+C. A later fresh local reuses the private playground data and opens passkey sign-in, making restart testing a single command.
The playground binds only to localhost, defaults to http://localhost:5555, ignores production identity configuration, and stores its database and private administrative state beneath the current user's local application-data directory. It consumes and deletes the service's bootstrap file automatically. Use localhost, not 127.0.0.1, because the passkey relying-party ID is localhost.
fresh local Start or resume the playground and open the browser
fresh local --enroll Create another passkey enrollment
fresh local --port 5556 Use or add another exact localhost origin
fresh local --no-open Print the page and one-time code instead
fresh local reset Confirm and delete the playground
fresh local reset --yes Delete it without an interactive prompt
Resetting the playground does not remove its now-orphaned credentials from Windows Hello, a password manager, or a security key; remove those entries from the authenticator when desired. If the browser cannot be launched, the CLI prints the enrollment page and one-time code separately. --diagnostics enables the otherwise quiet host logs.
Manual local service
For service development or manual administration rather than the guided playground:
dotnet tool restore
dotnet paket restore
$env:IdentityConfig__DataBaseDir = Join-Path $PWD "data"
dotnet run --project src/FreshIdentity.Login/FreshIdentity.Login.csproj
Open http://localhost:5555 after the service starts. On first start, load the bootstrap key without placing its value in shell history, then delete the one-time file:
$env:FRESHIDENTITY_API_KEY = (Get-Content .\data\bootstrap-apikey.txt -Raw).Trim()
Remove-Item .\data\bootstrap-apikey.txt
fresh CLI
Run the CLI from source with dotnet run --project src/fresh/fresh.csproj --, or use the executable produced under publish/fresh/ by the build scripts. fresh local is available from the built/published executable and from source; only a separately installed global-tool package is not provided. An explicit administration connection is always an endpoint/key pair; the CLI never combines a cached key with an overridden endpoint or an environment key with a cached endpoint.
For the local service, pair the bootstrap key above with its endpoint:
$env:FRESHIDENTITY_CONNECTION_URI = "http://localhost:5555/api"
dotnet run --project src/fresh/fresh.csproj -- info
Runtime sites are managed without restarting the service:
fresh site list
fresh site create --name "Customer A" --origin https://login.customer-a.example --rp-id login.customer-a.example
fresh site origin add <site-id> https://alternate.customer-a.example
fresh site github bind <site-id> main https://login.customer-a.example/auth/github/callback
fresh site disable <site-id>
Site mutations use ETags automatically through the CLI. Disabling or deleting the final usable browser site requires --allow-lockout; sites with passkeys or pending enrollment cannot be deleted. Removing an origin first requires removing any GitHub binding that uses it. GitHub registration IDs, labels, and enabled status available to runtime bindings are reported by fresh info.
Only loopback development endpoints may use HTTP. Every non-loopback endpoint must use HTTPS. --connectionuri <uri> is an alternative to FRESHIDENTITY_CONNECTION_URI, but it must still be paired with FRESHIDENTITY_API_KEY. If both endpoint sources are present, they must identify the same URI. Supplying only one member of an explicit pair or conflicting endpoints is a configuration error and no request is sent.
To cache the explicit pair locally and make it the default:
dotnet run --project src/fresh/fresh.csproj -- credentials add local
dotnet run --project src/fresh/fresh.csproj -- credentials default local
$env:FRESHIDENTITY_CONNECTION_URI = $null
$env:FRESHIDENTITY_API_KEY = $null
dotnet run --project src/fresh/fresh.csproj -- info
Credential display commands report only whether a key exists; they never print cached key values. Newly created API keys, OAuth client secrets, and passkey enrollment secrets are necessarily printed once by their creation commands and should be captured securely. Passwords and API keys are read from their documented environment variables or secure input rather than command-line arguments.
Commands are completely validated before prompts or API calls. Account and OAuth listings are bounded to 50 items by default and 200 items per server request; use --all to retrieve every page. Account search supports labels, identifiers, usernames, GitHub metadata, OAuth client IDs, passkey names and IDs, status, and credential-type filters. Administrator-created and reset passwords require a change on first login by default; pass --must-change false only when that is intentional.
Exit code 0 means success, 1 means an operational, HTTP, or logical API failure, 2 means invalid arguments or connection configuration, and 130 means cancellation. Logical failures retain their JSON response on stdout while diagnostics go to stderr. Use --diagnostics only when a stack trace is needed for an unexpected failure.
fresh property set -f <id> <scope> <key> <file> parses the file as one JSON value; without -f, the supplied value is stored as a string. Run fresh --help for the complete command reference.
Docker
docker build -t freshidentity .
docker volume create freshidentity-data
docker run --rm --name freshidentity -p 127.0.0.1:5555:5555 `
-e IdentityConfig__DataBaseDir=/data `
-v freshidentity-data:/data freshidentity
The image runs in Production by default. Every Production request must be HTTPS after trusted forwarded headers are processed. The TLS-terminating proxy described above must forward X-Forwarded-Proto: https from an address listed in IdentityConfig__KnownProxies; direct HTTP requests and requests from misconfigured proxies are rejected with HTTP 400 before authentication or antiforgery processing. For a direct, loopback-only UI smoke test, add -e ASPNETCORE_ENVIRONMENT=Development to the command. Never use that override for a deployed container.
The container exposes HTTP on port 5555 for the trusted proxy. In a second PowerShell session, capture and remove the bootstrap file after the first start:
$env:FRESHIDENTITY_API_KEY = (docker exec freshidentity cat /data/bootstrap-apikey.txt).Trim()
docker exec freshidentity rm /data/bootstrap-apikey.txt
The named volume inherits the image's service-user ownership. If you use a host bind mount instead, make that directory writable only by the container's APP_UID.
.NET
Install the current prerelease of FreshIdentity.Core:
dotnet add package FreshIdentity.Core --prerelease
var apiKey = Environment.GetEnvironmentVariable("FRESHIDENTITY_API_KEY")
?? throw new InvalidOperationException("Missing FreshIdentity API key.");
using var client =
new IdentityServiceClient(
endpoint: "https://login.example.org/api",
apiKey : apiKey
);
Create Account
Creates a labelled, enabled account. It has no sign-in or machine credentials until they are added.
var x = await client.CreateAccountAsync(label: "Example service");
Response
{
"account": {
"id": "1e9b90d7a4ffc80886d5f8528f2ef72a",
"label": "Example service",
"displayLabel": "Example service",
"isEnabled": true,
"created": "2026-08-20T13:09:58.5219337+00:00",
"updated": "2026-08-20T13:09:58.5219337+00:00",
"etag": "ab030dfa56f147da7ad8608b9573892410426c48",
"roles": [],
"properties": {},
"passwordCredentials": [],
"githubIdentities": [],
"apiKeys": [],
"oAuth2Clients": [],
"passkeys": []
}
}
Manage Accounts
// get an account by id
var x = await client.GetAccountAsync(id: "1e9b90d7a4ffc80886d5f8528f2ef72a");
// find the latest 3 enabled API-key accounts
var xs = await client.ListAccountsAsync(
take: 3,
orderBy: "created",
reverse: true,
query: "service",
isEnabled: true,
credential: AccountCredentialKind.ApiKey
);
// disable with optimistic concurrency
var account = x.Account;
var disabled = await client.UpdateAccountAsync(new(
account.Id,
IsEnabled: false,
ExpectedEtag: account.Etag
));
// delete using the latest ETag
var reply = await client.DeleteAccountAsync(
account.Id,
expectedEtag: disabled.Account.Etag
);
Console.WriteLine(
reply.Deleted
? "account has been deleted"
: "account does not exist"
);
Manage machine credentials
Creation responses are the only place raw secrets appear. Persist the secret before discarding the response; subsequent reads return only safe metadata and the non-secret credential ID.
var account = (await client.GetAccountAsync("example-service")).Account
?? throw new InvalidOperationException("Account not found.");
var createdKey = await client.CreateApiKeyAsync(
account.Id,
description: "deployment automation",
validUntil: DateTimeOffset.UtcNow.AddDays(90),
expectedEtag: account.Etag
);
StoreInSecretManager(createdKey.Secret);
await client.DeleteApiKeyAsync(
createdKey.ApiKey.Id,
expectedEtag: createdKey.AccountEtag
);
Provision and manage passkeys
Passkeys themselves are created only through a browser WebAuthn ceremony. The administrative client creates the one-time enrollment credential and can list, rename, or revoke the resulting safe credential identifiers:
var account = (await client.GetAccountAsync("example-person")).Account
?? throw new InvalidOperationException("Account not found.");
var sites = await client.ListAuthenticationSitesAsync();
var site = sites.Items.Single(x => x.Name == "Primary");
var enrollment = await client.CreatePasskeyEnrollmentAsync(
account.Id,
DateTimeOffset.UtcNow.AddHours(1),
account.Etag,
site.Id
);
DeliverEnrollmentCodeSecurely(enrollment.Secret);
var passkeys = await client.ListPasskeysAsync(account.Id);
await client.DeletePasskeyAsync(
passkeys.Items.Single().Id,
expectedEtag: passkeys.AccountEtag
);
Run fresh --help for the complete account, API-key, OAuth2, password, passkey, role, property, concurrency, and lockout-override command reference.
Contributing
See CONTRIBUTING.md for build, test, coverage, CI, coding, and Aardvark.Build release-note guidance.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
- Aardvark.Build (>= 2.0.7 && < 2.1.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.0-preview.0.5 | 49 | 8/22/2026 |
| 2.0.0-preview.0.4 | 34 | 8/22/2026 |
| 2.0.0-preview.0.3 | 37 | 8/20/2026 |
| 2.0.0-preview.0.2 | 62 | 7/11/2026 |
| 2.0.0-preview.0.1 | 203 | 8/11/2025 |
| 1.0.19 | 475 | 4/4/2023 |
| 1.0.18 | 394 | 4/4/2023 |
| 1.0.17 | 365 | 4/4/2023 |
| 1.0.16 | 405 | 4/3/2023 |
| 1.0.12 | 404 | 3/17/2023 |
| 1.0.11 | 578 | 11/4/2022 |
| 1.0.10 | 561 | 11/3/2022 |
| 1.0.9 | 648 | 10/14/2022 |
| 1.0.8 | 601 | 8/31/2022 |
| 1.0.7 | 612 | 8/24/2022 |
| 1.0.6 | 603 | 8/24/2022 |
| 1.0.5 | 600 | 8/23/2022 |
| 1.0.4 | 606 | 8/22/2022 |
| 1.0.3 | 597 | 8/22/2022 |
| 1.0.2 | 633 | 8/22/2022 |
- Add `fresh local`, a foreground localhost playground in the repository-built CLI that starts the real login service, securely consumes bootstrap access, provisions a passwordless test account, and opens guided passkey enrollment on first use.
- Persist private playground state beneath the current user's local application-data directory so later runs immediately exercise passkey sign-in and restart durability; add explicit enrollment recovery, exact alternate-port origins, browser-launch fallback, concurrent-run locking, confirmed reset, and actionable port-conflict handling.
- Keep the playground loopback-only and isolated from production configuration, cached CLI connections, Docker, and source-tree data; transfer its single-use enrollment credential in a client-only URL fragment that is prefilled and removed before the WebAuthn user gesture.
- Extract the existing service composition into a reusable login-host boundary while retaining the standalone service entry point, and bundle the login assembly and browser assets into the published CLI directory.
- Expand CLI, host, storage, lifecycle, redaction, recovery, and package-layout coverage to 491 tests; dogfood guided enrollment, explicit passkey sign-in, mobile/desktop layout, and sign-in after a server restart with headless Playwright and a virtual authenticator.