Epsitec.Briefcases.Sdk
2.8.1.2629
Prefix Reserved
dotnet add package Epsitec.Briefcases.Sdk --version 2.8.1.2629
NuGet\Install-Package Epsitec.Briefcases.Sdk -Version 2.8.1.2629
<PackageReference Include="Epsitec.Briefcases.Sdk" Version="2.8.1.2629" />
<PackageVersion Include="Epsitec.Briefcases.Sdk" Version="2.8.1.2629" />
<PackageReference Include="Epsitec.Briefcases.Sdk" />
paket add Epsitec.Briefcases.Sdk --version 2.8.1.2629
#r "nuget: Epsitec.Briefcases.Sdk, 2.8.1.2629"
#:package Epsitec.Briefcases.Sdk@2.8.1.2629
#addin nuget:?package=Epsitec.Briefcases.Sdk&version=2.8.1.2629
#tool nuget:?package=Epsitec.Briefcases.Sdk&version=2.8.1.2629
Epsitec.Briefcases.Sdk
Mobile-facing client SDK for Crésus Briefcases. Online-only, backed by a transient in-memory store: the server is the authoritative store; the client rehydrates the workspace tree from the server ledger at the start of a session and discards everything on exit.
Usage — connect, discover, open, write
var options = new BriefcasesSessionOptions (
ServerUrl: "https://briefcases.example.com",
UserId: userId,
UserKeys: userKeys,
AccessTokenProvider: () => app.GetAccessTokenAsync ());
await using var session = new BriefcasesSession (options);
await session.ConnectAsync ();
// Discover the briefcases you can reach, then resolve the ones you can open.
var known = await session.GetKnownBriefcaseIdsAsync ();
var workspaces = await session.ListWorkspacesAsync (known);
var workspace = await session.OpenWorkspaceAsync (workspaces[0]);
// Push into a chosen folder. The parent is required: use workspace.RootPath
// for the tree root, or a folder path returned by CreateFolderAsync.
var folder = await workspace.CreateFolderAsync (
workspace.RootPath, "Receipts", tags: ["year:2026"]);
var path = await workspace.PushDocumentAsync (
folder,
new DocumentPayload (
"coop-receipt-2026-06-12.jpg",
[
new StreamPayload ("image/jpeg", imageBytes),
new StreamPayload ("x-edm/fulltext", ocrTextUtf8Bytes),
new StreamPayload ("application/json", receiptJsonUtf8Bytes),
],
Tags: ["client:42", "archived"]));
Tree-aware writes and tag queries
PushDocumentAsync and CreateFolderAsync both take a parent NodePath
already present in the workspace view (workspace.RootPath qualifies). The new
node lands at {parent}/n…. All argument validation (parent, file/folder name,
streams, tags, sibling-name collision) is side-effect-free and completes before
any blob is staged or any network call is made.
CreateFolderAsync is idempotent on the folder name: if a child folder of that
name already exists, its path is returned unchanged. The returned path is
immediately usable as a parent for further pushes or folders.
Editing existing nodes — update, rename, retag
Three additive mutations edit a node already in the tree without changing its
path. Each refreshes the view and advances Generation on a real change, but is
a no-op (nothing committed, Generation unchanged) when the requested state
already matches:
// Replace a document's content (full-version replace): streams, name, and tags
// all come from the payload. A different FileName renames the node; different
// Tags replace them; an EMPTY Tags array CLEARS them. Echo the current name and
// tags to edit content only. A fresh per-version node key is minted (this is not
// an access-key rotation).
await workspace.UpdateDocumentAsync (
path,
new DocumentPayload (
"coop-receipt-2026-06-12.jpg",
[new StreamPayload ("image/jpeg", editedImageBytes)],
Tags: ["client:42", "archived"]));
// Rename a file or folder (metadata-only; content untouched).
await workspace.RenameAsync (path, "coop-receipt-final.jpg");
// Replace a node's entire tag set (metadata-only). There is no incremental
// add/remove: read the node's current Tags from the view, compute the full
// intended set, and pass it. An empty set clears all tags.
await workspace.SetTagsAsync (path, ["client:42", "reviewed"]);
Reindex vs refresh for an EDM consumer. All three commit a real Modified
the reader's DiffAsync reports (never None). The etag
(ContentHashes vs PreviousContentHashes) tells the two apart:
UpdateDocumentAsync re-stages content, so the etag changes ⇒ reindex
(OCR/embedding re-extraction); RenameAsync / SetTagsAsync leave the content
untouched, so the etag is equal ⇒ a cheap metadata refresh, no re-extraction.
Find nodes by tag through the FindByTag / FindByTagPrefix extension methods
(one-off lookups); for several queries against one generation, call
BuildTagIndex () once and reuse it.
var sub = await workspace.CreateFolderAsync (folder, "Q4");
await workspace.PushDocumentAsync (sub, document);
ImmutableArray<NodePath> tagged = workspace.FindByTag ("client:42");
ImmutableArray<NodePath> byPrefix = workspace.FindByTagPrefix ("client:");
Discovery — reachable briefcases and their home servers
ListWorkspacesAsync and ListWorkspaceCardsAsync take the candidate
BriefcaseIds to resolve — the server confirms membership per id and never
enumerates a user's briefcases. GetKnownBriefcaseIdsAsync supplies those
candidates:
var known = await session.GetKnownBriefcaseIdsAsync ();
var workspaces = await session.ListWorkspacesAsync (known); // members only
var cards = await session.ListWorkspaceCardsAsync (known); // + decrypted cards
By default the candidate set is what this client already holds locally. Point the
session at a directory and GetKnownBriefcaseIdsAsync additionally returns the
briefcases the directory catalog lists for the user — including ones this device
has never opened — and each briefcase's server work is then routed to that
briefcase's home server:
var options = new BriefcasesSessionOptions (
ServerUrl: "https://home.example.com", // this user's primary home server
UserId: userId,
UserKeys: userKeys,
AccessTokenProvider: () => app.GetAccessTokenAsync ())
{
DirectoryUrl = "https://directory.example.com",
DirectoryAccessTokenProvider = () => app.GetDirectoryTokenAsync (), // optional; falls back to AccessTokenProvider
DirectoryCacheRootPath = localCachePath, // optional; caches home URLs for a fast/offline start
};
With a directory configured, the session opens one connection per distinct home
server and sends each briefcase's reads, writes, blobs, and leases to that
briefcase's home. Discovery is availability-tiered: a warm home-URL cache keeps an
already-started session working while the directory is briefly unreachable, and
GetKnownBriefcaseIdsAsync degrades to the locally-known set rather than throwing.
Without a DirectoryUrl the session is single-server and every operation targets
ServerUrl — the pre-federation behaviour.
Creating a briefcase and inviting a user
A session is not limited to briefcases that already exist. CreateBriefcaseAsync
commits the genesis transaction for a brand-new briefcase and returns it already
opened, so it slots in exactly where OpenWorkspaceAsync would — the whole
write surface above (folders, PushDocumentAsync, updates, RefreshAsync)
applies unchanged:
await using var session = new BriefcasesSession (options);
await session.ConnectAsync ();
// Returns the opened IWorkspace for the new briefcase (no List+Open round-trip).
var workspace = await session.CreateBriefcaseAsync (
new CreateBriefcaseOptions (ProbationMinutes: 2880)); // 48 h successor window
var path = await workspace.PushDocumentAsync (workspace.RootPath, document);
// workspace.Id is the fresh BriefcaseId, usable by Open/Reader/Watch.
InviteAsync is a method on the opened workspace (it reads that workspace's
live ledger state and advances its Generation like any other write). The
invitee's public keys are not derived here — the host supplies the
UserPublicKeys, typically from the Identity package's discovery call
BriefcasesIdentity.GetPublicKeysAsync, which replaces the manual profile.json
exchange:
// inviteeKeys came from the host's discovery step (e.g. Identity.GetPublicKeysAsync).
Invitation invite = await workspace.InviteAsync (inviteeUid, inviteeKeys);
// …or restrict the grant to a subtree already present in workspace.Nodes:
Invitation partial = await workspace.InviteAsync (
inviteeUid, inviteeKeys,
new InviteOptions (FullAccess: false, Path: folder, Message: "Receipts only"));
// invite.Pin is produced ONCE and never persisted by the SDK — share it
// out-of-band (voice, secure messaging). invite.InviteId identifies the invitation.
Full-access invitations require the caller to be the briefcase owner; a
non-owner attempt throws InvalidOperationException.
On the invitee's side (their own session, their own keys), AcceptInvitationAsync
verifies the PIN and returns the BriefcaseId — exactly what OpenWorkspaceAsync
/ OpenReaderAsync / WatchAsync consume. Accept does not auto-open: the
invitee decides when to open (which performs the download sync), mirroring the
List → Open separation:
ImmutableArray<PendingInvitation> pending = await session.ListInvitationsAsync ();
BriefcaseId bid = await session.AcceptInvitationAsync (pending[0].InviteId, pin);
var workspace = await session.OpenWorkspaceAsync (bid);
await workspace.RefreshAsync (); // pull the inviter's committed generations
A wrong PIN throws InvitationPinException, which exposes RemainingAttemptCount
so the caller can re-prompt within the server's rate-limited budget. The inviter
withdraws a pending invitation with CancelInvitationAsync; the invitee turns one
down with DeclineInvitationAsync — both leave it absent from a later
ListInvitationsAsync.
The invitee key authenticity is the caller's trust boundary: the SDK invites whatever
UserPublicKeysit is handed. Binding those keys to the right person is the discovery step's job (Identity.GetPublicKeysAsyncvalidates theml-kem-768profile and the keyset subject).
Succession time capsules (data continuity if the owner disappears)
A briefcase owner can designate a member as successor: the briefcase access
key is sealed for the successor's keys and additionally timelocked to a
public randomness beacon (drand quicknet), so nobody — the server included —
can open the escrow before 2 × ProbationMinutes from the seal. The successor
gains access only after filing a claim, surviving the owner-vetoable probation
window, and waiting out the timelock.
Owner side — designate, inspect, veto:
// Designate (also the manual re-seal: re-designating refreshes the timelock).
var outcome = await workspace.DesignateSuccessorAsync (successorUid);
Console.WriteLine ($"escrow timelocked until {outcome.EscrowUnlockTime:O}");
// Inspect: phase, claim deadlines, and the stored escrow's condition.
var status = await workspace.GetSuccessionStatusAsync ();
foreach (var s in status.Successors)
{
// s.Phase: Designated / Pending / Accepted.
// s.Escrow (owner only): Timelocked / Unusable / Absent (designation-only).
// s.EscrowNeedsRefresh: re-seal recommended (stale or unusable).
Console.WriteLine ($"{s.UserId}: {s.Phase}, escrow {s.Escrow}");
}
// Veto a pending claim (the successor stays designated and may re-request),
// or remove the successor entirely (drops the escrow):
await workspace.CancelSuccessionAsync (successorUid);
await workspace.RevokeSuccessorAsync (successorUid);
The escrow's timelock decays: it protects fully for one probation window past
the owner's last (re-)seal. With AutoRefreshSuccessionEscrows (default true
on BriefcasesSessionOptions), OpenWorkspaceAsync quietly re-seals stale or
unusable escrows whenever the owner opens the workspace; deliberately
escrow-less designations (Absent) are never touched. RefreshSuccessionEscrowsAsync
runs the same pass explicitly.
Successor side — claim, wait out probation, accept and open:
await workspace.RequestSuccessionAsync (); // probation starts (owner can veto)
// … after the probation window …
var accept = await workspace.AcceptSuccessionAsync ();
switch (accept.Result)
{
case SuccessionAcceptResult.Opened:
// Both capsule layers opened; THIS workspace now reads every node
// (owner-equivalent read), e.g. to copy content into a new briefcase.
break;
case SuccessionAcceptResult.Locked:
// Accept committed; the timelock round is not due yet. Retry after:
Console.WriteLine ($"retry after {accept.UnlockTime:O}");
break;
case SuccessionAcceptResult.NoEscrow: // ask the owner to (re-)provision
case SuccessionAcceptResult.EscrowUnusable: // idem (see accept.Detail)
case SuccessionAcceptResult.RelaysUnreachable: // durable; retry later
break;
}
AcceptSuccessionAsync is idempotent: a Locked or RelaysUnreachable outcome
is finished by calling it again — the claim itself is durable ledger state.
Opening the timelock fetches the unlocking drand beacon over the public relays
(verified locally against the pinned quicknet chain); inject an IDrandClient
into the BriefcasesSession constructor to control that transport.
Workspace cards (member-readable listing metadata)
ListWorkspacesAsync returns opaque BriefcaseId values. To show a
human-meaningful listing — workspace name, owner, an optional description, colour,
icon and tags — without downloading each ledger, call ListWorkspaceCardsAsync.
It returns one decrypted WorkspaceCardInfo per accessible briefcase that has a
card and for which the session user holds a capsule; each is decrypted with
the user's own private keys, with no ledger replay.
var known = await session.GetKnownBriefcaseIdsAsync ();
var cards = await session.ListWorkspaceCardsAsync (known);
foreach (var card in cards)
{
Console.WriteLine ($"{card.BriefcaseId}: {card.WorkspaceName}");
// card.OwnerUserId is opaque — resolve the display name via auth.
// card.WorkspaceKind / WorkspaceDescription / WorkspaceColor / Tags are optional.
// card.WorkspaceIcon (+ WorkspaceIconMediaType) is a small icon, or empty.
}
Briefcases without a card, or for which the user has no capsule yet, are omitted
from ListWorkspaceCardsAsync; cross-reference ListWorkspacesAsync to show those
by bid. A card that is present but the user cannot decrypt (a stale capsule, a
corrupt blob) is not dropped — it surfaces as a degraded WorkspaceCardInfo
carrying only its BriefcaseId (empty WorkspaceName, so IsEmpty is true), so a
single unreadable card never fails the whole listing. The card is a low-churn
projection of an in-ledger anchor: a card that lags the ledger head is accepted by
design, and the blind server never sees any card field in cleartext (the blob is
AEAD-bound to its briefcase id under a per-card random key distributed only via
per-member capsules).
Optional signature verification (defense in depth). WorkspaceCardInfo.SignatureVerified
is tri-state: null (not checked), true (the owner's envelope signature verified),
or false (it failed). It stays null unless you inject an
IWorkspaceCardSignerKeyResolver into the session — the resolver returns the signer's
public keys (resolved from the trusted auth directory by signer id + keyset hkid,
which the server serves with each card), and the SDK verifies the envelope itself.
Verification is best-effort and never fails the listing; the SDK takes no auth
dependency, so the consumer that already holds the auth wiring supplies the resolver.
Reading: point-in-time read, generation diff, stream open
IWorkspace is the current tree plus add-only writes. To read a node as it
stood at a past generation, to obtain what changed between two generations,
or to read the decrypted bytes of one stream, open a read-only
IBriefcaseReader:
await using var reader = await session.OpenReaderAsync (bid);
// Read a node (or subtree) at a generation. `at` of `Empty`/`Latest` ⇒ Head.
NodeView? node = await reader.ReadNodeAsync (
NodePath.From ("/n0/n43"), at: GenerationId.From (1200), depth: NodeDepth.All);
// Diff the inclusive window [from, to], ordered by NodePath.
await foreach (var change in reader.DiffAsync (GenerationId.From (1), reader.Head))
{
// change.Kind is Added / Modified / Removed (never None).
// change.ContentHashes vs change.PreviousContentHashes distinguishes a real
// reindex (content changed) from a metadata-only refresh (equal etags).
}
// Open the decrypted plaintext of one stream (default streamId 0 = primary).
await using var stream = await reader.OpenStreamAsync (
NodePath.From ("/n0/n43"), at: GenerationId.Latest, streamId: 1);
Generation arguments (per parameter)
A diff is directional, so resolution differs by parameter — there is no single global rule:
| Parameter | Role | Empty (0) |
Latest (-1) |
|---|---|---|---|
ReadNodeAsync.at |
point "as of" | ⇒ Head |
⇒ Head |
DiffAsync.to |
inclusive upper bound | ⇒ Head |
⇒ Head |
DiffAsync.from |
inclusive lower bound | ⇒ 1 (earliest real generation) |
invalid ⇒ ArgumentException |
A positive value selects that generation; a value past Head (after resolution)
throws ArgumentException. The window is inclusive [from, to]: from == to
yields the changes at that single generation. Past generations are reconstructed
by rebuilding from genesis, so a read or diff returns the tree as it stood —
never a later snapshot's state.
Lifetime and removal semantics
OpenStreamAsyncdownloads lazily as the returned stream is read (no full pre-buffering) and decrypts with the key you already hold. You must dispose the returned stream, and it is valid only while the reader and its session are alive (reading after disposal throwsObjectDisposedException).- Removed covers deletion, crypto-erase, and access loss: a node visible
at
frombut purged from thetosnapshot by an ACL shrink is reportedRemovedso the consumer can drop its local index entry. A relocation surfaces asRemovedat the old path +Addedat the new path (the model has no move).
Change-data-capture loop (shell → local index)
// Catch up from the persisted watermark to Head, one resumable pass.
await foreach (var change in reader.DiffAsync (watermark, reader.Head, scope))
{
switch (change.Kind)
{
case NodeChangeKind.Added:
case NodeChangeKind.Modified:
// reindex when change.ContentHashes != change.PreviousContentHashes,
// else a cheap metadata refresh; fetch bytes via OpenStreamAsync.
break;
case NodeChangeKind.Removed:
// drop the local index entry.
break;
}
}
// Persist the next watermark = to + 1 so the inclusive window never reprocesses
// the boundary generation. The CLI `diff` cursor line carries this `next`.
watermark = reader.Head.Next;
Staying in sync (refresh, watch, continuous CDC)
IWorkspace and IBriefcaseReader open as a snapshot at the current server
head. To observe changes committed by other clients/devices without
re-opening, use the pull (RefreshAsync), push (WatchAsync), and the
high-level fusion (WatchChangesAsync).
Continuous change stream (fire-and-forget)
WatchChangesAsync drains the catch-up from your cursor, then streams every
change as generations arrive — until cancelled. At-least-once and idempotent for
the shell (Added/Modified upsert, Removed drop); it persists no watermark
itself.
await using var reader = await session.OpenReaderAsync (bid);
await foreach (var change in reader.WatchChangesAsync (from: cursor.Next, ct: ct))
{
await ApplyToIndex (reader, change, ct); // index / reindex / refresh / drop
}
Manual composition for a precise, crash-resumable cursor
RefreshAsync returns the inclusive GenerationSpan that arrived (To is
inclusive, like DiffAsync), so DiffAsync (span.From, span.To) reproduces the
delta exactly. Persist span.To and resume at span.To.Next.
await foreach (var update in session.WatchAsync (bid, ct)) // push notification
{
var span = await reader.RefreshAsync (ct); // pull to the new head
if (span.IsEmpty) { continue; }
await foreach (var change in reader.DiffAsync (span.From, span.To, scope, ct))
{
await ApplyToIndex (reader, change, ct);
}
Persist (span.To); // crash-resumable cursor
}
A poll-only variant skips WatchAsync and calls RefreshAsync on a timer.
A failed/offline refresh throws InvalidOperationException (never a silent empty
span); a transient connection drop does not fault WatchAsync — the next
refresh recovers any missed notification.
Optimistic-concurrency writes (lost-update protection)
UpdateDocumentAsync, RenameAsync, SetTagsAsync, and DeleteAsync are
optimistically concurrent: if the targeted node changed under you (another client
committed a new version, or deleted it) between your view being taken and the
commit, the write throws WorkspaceStaleException instead of silently
clobbering. Recover by refreshing, re-resolving the node, and retrying against
the new base.
try
{
await workspace.UpdateDocumentAsync (path, payload, ct);
}
catch (WorkspaceStaleException ex)
{
await workspace.RefreshAsync (ct); // pull the concurrent version
var fresh = workspace.Nodes.First (n => n.Path == ex.Path);
payload = Reconcile (payload, fresh); // re-apply intent on the fresh base
await workspace.UpdateDocumentAsync (path, payload, ct); // retry; ex.ActualBase == Empty ⇒ remote delete
}
PushDocumentAsync / CreateFolderAsync are unaffected — they allocate a fresh
child path, so there is no shared target to race on.
Importing keys from a QR code
A desktop that already holds the user's keys can show a QR pairing code
(briefcases export-keys in the CLI). The mobile app scans it and turns it
into UserKeys: the SDK decrypts the embedded recovery phrase with the user's
password and re-derives the key set locally. The secret never travels in
cleartext, and uid and password are supplied by the app — never by the QR.
// uid is the JWT `sub` from your auth flow; password is entered by the user.
var credentials = BriefcasesKeyImport.FromQrPayload (scannedText, uid, password);
var options = BriefcasesSessionOptions.FromCredentials (
credentials,
() => app.GetAccessTokenAsync ());
await using var session = new BriefcasesSession (options);
await session.ConnectAsync ();
// credentials.Bid is an optional hint: open it directly when present.
IWorkspace workspace;
if (credentials.Bid is { } bid)
{
workspace = await session.OpenWorkspaceAsync (bid);
}
else
{
var known = await session.GetKnownBriefcaseIdsAsync ();
workspace = await session.OpenWorkspaceAsync (
(await session.ListWorkspacesAsync (known))[0]);
}
FromQrPayload throws a typed KeyImportException:
KeyImportFormatException— not a Briefcases pairing code, or malformed.KeyImportIdentityMismatchException— the code was issued for another user (checked before any password work).KeyImportPasswordException— wrong password, or a tampered code.
In a live camera loop, call KeyImportPayload.IsPairingPayload (text) first to
skip foreign QR codes cheaply before attempting an import.
Integration seams the host app owns
- JWT token provider — the SDK never logs in. The app's login flow
acquires the access token and hands the SDK a
Func<Task<string?>>; the provider is consulted for every request (SignalR and blob HTTP), so token rotation is picked up automatically. - User keys — the SDK never derives or persists key material. The app
supplies the decrypted
UserPublicPrivateKeys. The one exception isBriefcasesKeyImport.FromQrPayload(see above), a one-shot provisioning call that re-derives them from a scanned QR pairing code; the SDK still never persists them. - Stream conventions — the SDK treats each stream as opaque bytes plus a
media type;
StreamIdis the position in the payload. Which stream carries an image, full text, or structured JSON — and any wire format inside a stream (like the receipt JSON above) — is the app's contract, not the SDK's.
Lifecycle
BriefcasesSession is IAsyncDisposable. ConnectAsync is single-use;
listing and opening require a connected session. Workspaces refresh after
each successful write (PushDocumentAsync, CreateFolderAsync,
UpdateDocumentAsync, RenameAsync, SetTagsAsync, DeleteAsync); changes
made by other clients are observed by calling RefreshAsync (or watched live —
see Staying in sync above), without re-opening. After disposal, session and
workspace methods throw ObjectDisposedException.
Reclaiming memory over a long session
The default in-memory store keeps every uploaded (encrypted) blob in memory
for the session's lifetime. A push-only client never reads those bytes back,
so for a long-lived session you can reclaim that memory deterministically:
inject your own InMemoryBriefcaseStorage and call ClearBlobs (bid) after a
successful push. The ledger is preserved, so the workspace stays fully usable.
var storage = new InMemoryBriefcaseStorage ();
await using var session = new BriefcasesSession (options, storage);
await session.ConnectAsync ();
var workspace = await session.OpenWorkspaceAsync (bid);
await workspace.PushDocumentAsync (workspace.RootPath, document);
storage.ClearBlobs (bid); // the upload succeeded; drop the local blob copies
| 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. |
-
net10.0
- BouncyCastle.Cryptography (>= 2.7.0-beta.98)
- Microsoft.AspNetCore.Http.Connections.Client (>= 10.0.9)
- Microsoft.AspNetCore.Http.Connections.Common (>= 10.0.9)
- Microsoft.AspNetCore.SignalR.Client (>= 10.0.9)
- Microsoft.AspNetCore.SignalR.Protocols.Json (>= 10.0.9)
- Microsoft.Extensions.Configuration (>= 10.0.9)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.9)
- Microsoft.Extensions.DependencyInjection (>= 10.0.9)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Http (>= 10.0.9)
- Microsoft.Extensions.Http.Resilience (>= 10.7.0)
- Microsoft.Extensions.Logging (>= 10.0.9)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Logging.Configuration (>= 10.0.9)
- Microsoft.Extensions.ObjectPool (>= 10.0.9)
- Microsoft.Extensions.Options (>= 10.0.9)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.9)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Epsitec.Briefcases.Sdk:
| Package | Downloads |
|---|---|
|
Epsitec.Briefcases.Identity
User identity lifecycle for Crésus Briefcases: key provisioning, at-rest key store, login, and keyset registration |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.8.1.2629 | 29 | 7/15/2026 |
| 2.8.0.2629 | 33 | 7/15/2026 |
| 2.7.3.2629 | 36 | 7/14/2026 |
| 2.7.2.2629 | 40 | 7/14/2026 |
| 2.7.1.2629 | 45 | 7/14/2026 |
| 2.7.0.2629 | 39 | 7/14/2026 |
| 2.6.2.2629 | 41 | 7/14/2026 |
| 2.6.1.2629 | 39 | 7/14/2026 |
| 2.6.0.2629 | 40 | 7/14/2026 |
| 2.5.0.2628 | 52 | 7/12/2026 |
| 2.3.0.2628 | 60 | 7/10/2026 |
| 2.2.0.2628 | 59 | 7/8/2026 |
| 2.1.6.2627 | 63 | 7/3/2026 |
| 2.1.5.2627 | 65 | 7/2/2026 |
| 2.1.4.2627 | 59 | 7/2/2026 |
| 2.1.3.2627 | 65 | 6/30/2026 |
| 2.1.2.2627 | 61 | 6/29/2026 |
| 2.1.1.2627 | 66 | 6/29/2026 |
| 2.1.0.2627 | 63 | 6/29/2026 |
| 2.0.0.2626 | 67 | 6/28/2026 |