SVRN7/
├── src/
│ ├── Svrn7.Core/ Models, interfaces, enums (DidDocument, Svrn7Role, DidStatus, etc.)
│ ├── Svrn7.Crypto/ secp256k1 key generation, Base58, signing (CryptoService)
│ ├── Svrn7.DIDComm/ DIDComm V2 pack/unpack (all modes: plaintext, JWS, JWE, SignThenEncrypt)
│ ├── Svrn7.Store/ LiteDB registries: LiteDidDocumentRegistry, LiteInboxStore, etc.
│ ├── Svrn7.Federation/ ISvrn7Driver, Svrn7Driver — federation + DID management
│ ├── Svrn7.Society/ ISvrn7SocietyDriver — adds citizen/VC/transfer layer
│ ├── Svrn7.Identity/ DIDDocumentService, DID resolve pipeline
│ ├── Svrn7.Ledger/ Epoch, supply, Merkle ledger
│ ├── Svrn7.TDA/ Trusted Digital Assistant — Kestrel host + LOBE runtime
│ └── Web7.SVRN7.Apps.PandoMail/ WinForms email client (.NET 8, Windows)
├── tests/
│ ├── Svrn7.Tests/
│ ├── Svrn7.TDA.Tests/
│ └── Svrn7.Society.Tests/
├── tools/ Initialize-Testnet.ps1, Build-LOBEPackages.ps1, Pando.Packaging.psm1
└── docs/ DEBUG.md, DIDDEBUG.md, WANDERERDEBUG.md, LOBEDEBUG.md, etc.
POST /didcomm (HTTP/2, Kestrel)
└── DrawbridgeService
└── IDIDCommService.UnpackAsync ← JWE decrypt + JWS verify at inbound boundary
└── LiteInboxStore.EnqueueAsync
└── DIDCommMessageSwitchboard (drain loop)
└── LobeManager.TryResolveProtocol(@type)
└── LobeManager.EnsureLoadedAsync(modulePath)
└── Import-Module [-Force] in IsolatedRunspaceFactory
└── Cmdlet invocation (PowerShell.Invoke())
└── OutboundMessage (plaintext envelope)
└── PackOutboundAsync ← SignThenEncrypt at outbound boundary (HTTP only)
└── DeliverAsync (HTTP/2)
Key components:
| Class | Role |
|---|---|
DrawbridgeService |
Single inbound gate: POST /didcomm only |
DIDCommMessageSwitchboard |
Inbox drain loop; routes by @type; outbound delivery |
LobeManager |
Protocol registry; eager/JIT import; EnsureLoadedAsync |
IsolatedRunspaceFactory |
Runspace pool (min 2, max ProcessorCount×2) |
TdaHost / $SVRN7 |
Context object injected into every runspace; exposes Driver, GetMessageAsync, CurrentEpoch, etc. |
Eager LOBEs are loaded once at startup into the InitialSessionState.
JIT LOBEs run Import-Module -Force on every dispatch — hot-update without TDA restart (overhead ~30 ms, tracked as TDA-001a).
The TDA's public inbound surface is HTTP/2-only (no HTTP/1.1 fallback). Reasons:
-
mTLS mutual peer authentication — TDA-to-TDA communication requires both sides to authenticate with certificates. HTTP/2 + TLS 1.3 via ALPN is the modern standard for this on Kestrel; it eliminates downgrade attacks that are possible when HTTP/1.1 is allowed as a fallback.
-
Multiplexed concurrent streams without HOL blocking — A TDA may receive many inbound DIDComm messages simultaneously from multiple peers. HTTP/2 multiplexes them over a single connection; HTTP/1.1 head-of-line blocking would serialize them.
-
Minimal, single-endpoint attack surface — One verb (
POST), one path (/didcomm), one protocol, one port. Disabling HTTP/1.1 removes an entire class of HTTP/1.1-specific parsing vulnerabilities (request smuggling, header injection). -
Mature on Kestrel / same transport as gRPC — .NET's HTTP/2 stack is battle-tested via gRPC. Same deployment, same operational patterns, no additional runtime dependency.
-
HPACK header compression — DIDComm headers (
Content-Type: application/didcomm-encrypted+json, etc.) are identical on every message. HTTP/2 compresses them after the first exchange.
Why not gRPC directly? gRPC framing (Protobuf + length-prefixed binary) adds schema complexity for third-party LOBE authors and clients that only need to POST a JSON envelope. Raw HTTP/2 POST keeps the wire format simple.
Local UI clients (PandoMail): PandoMail is on .NET 8. TdaMailClient.Send() uses HttpClient with HTTP/2 + mTLS directly for POST /didcomm. The /localcomm-ws push channel uses RFC 8441 (WebSocket over HTTP/2 extended CONNECT) — a deliberate design choice for server-push, not a .NET limitation.
DIDCommMessageSwitchboard.PackOutboundAsync applies SignThenEncrypt (secp256k1 ES256K JWS inside X25519 JWE) to every outbound message before HTTP delivery. LOBEs always return plaintext OutboundMessage objects — they have no access to key material and no knowledge of which pack mode to use. The Switchboard is the sole place where packing decisions are made.
Transport rule (P-008):
| Transport | Direction | Protocol | Pack mode | Enforcement |
|---|---|---|---|---|
HTTP (POST /didcomm) — TDA-to-TDA |
outbound | General | SignThenEncrypt (secp256k1 ES256K JWS inside X25519 JWE) | Switchboard PackOutboundAsync |
HTTP (POST /didcomm) — TDA-to-TDA |
outbound | DID discovery | Plaintext (application/didcomm-plain+json) |
Switchboard IsPlaintextDiscoveryMessage → skip PackOutboundAsync |
HTTP (POST /didcomm) — TDA-to-TDA |
inbound | General | SignThenEncrypt required | DrawbridgeService rejects non-encrypted with 415 |
HTTP (POST /didcomm) — TDA-to-TDA |
inbound | DID discovery | Plaintext accepted | DrawbridgeService admits application/didcomm-plain+json only for PlaintextDiscoveryProtocols; rejects other plaintext with 403 |
WebSocket (/localcomm-ws) — TDA-to-local-UI |
outbound | All | Plaintext (application/didcomm-plain+json) |
Switchboard (PeerEndpoint starts with ws://) |
WebSocket (/localcomm-ws) — local-UI/tool-to-TDA |
inbound | All | Plaintext accepted | localhost-only; no content-type gate |
DID discovery plaintext whitelist (Svrn7Constants.PlaintextDiscoveryProtocols):
did:drn:svrn7.net/protocols/Svrn7.Identity.0.8.0/did-resolve-requestdid:drn:svrn7.net/protocols/Svrn7.Identity.0.8.0/did-resolve-response
Why plaintext for DID resolution? DID resolution is an open discovery service — any TDA may query any other TDA's DID Documents (own, Citizens, Societies) freely, without a prior relationship. Encryption is structurally impossible on this path: you cannot encrypt to a peer whose DID Document you have not yet resolved, which is exactly what the query is for. The plaintext permission is scoped strictly to PlaintextDiscoveryProtocols; any other plaintext on POST /didcomm is rejected 403.
Why plaintext for WebSocket? The /localcomm-ws channel is localhost-only. PandoMail holds no key material and shares the Citizen TDA's DID. Encryption would require giving PandoMail long-lived private keys — that contradicts the shared-identity design and would expand the attack surface unnecessarily.
Symmetric design: This is the outbound counterpart of the decrypt-at-boundary pattern on the inbound side. DrawbridgeService unpacks every inbound message at the HTTP boundary before anything enters the inbox; DIDCommMessageSwitchboard.PackOutboundAsync packs every outbound HTTP message at the delivery boundary before anything leaves the process (DID discovery protocols excepted).
Fallback behaviour: If the recipient's DID Document does not contain an X25519KeyAgreementKey2020 entry — for example, a TDA bootstrapped before X25519 keys were added — PackOutboundAsync logs a warning and sends plaintext. This is a degraded mode. All TDAs bootstrapped with the current codebase include an X25519 key by default.
Key material in TdaOptions:
| Field | Key type | Use |
|---|---|---|
AgentKeyAgreementPrivateKey |
X25519 (32 bytes) | Inbound JWE decryption (DrawbridgeService.UnpackAsync) |
AgentSigningPrivateKey |
secp256k1 (32 bytes) | Outbound JWS signing (DIDCommMessageSwitchboard.PackOutboundAsync) |
Both are decrypted from agent-identity.wallet at startup (pre-host) and held for the process lifetime. Neither is passed into LOBE runspaces.
DrawbridgeService fully unpacks every inbound DIDComm message — JWE decryption and JWS signature verification — before anything is written to the inbox. The decrypted plaintext body is what gets stored in InboxMessage.PackedPayload. The original wire envelope is preserved separately in InboxMessage.JweEnvelope for audit. This is a deliberate architectural constraint, not an implementation convenience.
Storing the raw JWE and decrypting later (in the Switchboard or in a LOBE) would break the system in five distinct ways:
-
Routing breaks entirely —
DIDCommMessageSwitchboardroutes byInboxMessage.MessageType, which is extracted from the unpacked inner payload at enqueue time. A stored JWE has no accessible@type; the Switchboard would have to assign a synthetic type (application/didcomm-encrypted+json) that no LOBE handles, dead-lettering every encrypted message. -
Every LOBE breaks — LOBE handlers access message content as:
$body = $msg.PackedPayload | ConvertFrom-Json
If
PackedPayloadis a JWE envelope,ConvertFrom-Jsonreturns the JWE structure, not the application payload. All existing LOBEs would require a decrypt-first step, making LOBE authoring significantly more complex and error-prone. -
Key material must enter the LOBE runspace — Decrypting in a LOBE requires the X25519 private key (
AgentKeyAgreementPrivateKey) to be available there. Currently it is held only byDrawbridgeServiceand never enters a runspace. Injecting it into$SVRN7expands the attack surface: a buggy or malicious LOBE would have access to the TDA's decryption key. -
FromDidis unavailable for reply routing — The sender DID is extracted from the JWS inner layer duringUnpackJwsAsync. Without unpacking at the boundary,InboxMessage.FromDidis null. LOBE handlers that need to send a reply have no sender identity without repeating the full unpack. -
Stuck-message recovery corrupts the retry cycle —
LiteInboxStore.ResetStuckMessagesAsyncrequeuesProcessingmessages toPendingon startup after a crash. If stored payloads are JWE envelopes the Switchboard cannot route, each recovery attempt fails, re-enqueues, and fails again untilmaxAttemptsis exhausted and the message is permanently dead-lettered. Every unclean shutdown would silently destroy messages.
The JweEnvelope field satisfies the audit/forensics use case without any of the above costs: the original wire format is always available for inspection, replay, or legal hold, while the pipeline operates entirely on verified plaintext.
lobes/
└── {Name}.{version}/
├── {Name}.{version}.psm1 ← entry-point module (exported cmdlets only)
├── {Name}.{version}.lobe.json ← descriptor
└── {Name}.Impl.{version}.psm1 ← optional implementation module
{
"lobe": { "name": "Pando.Diagnostics", "version": "0.1.0", "module": "Pando.Diagnostics.0.1.0.psm1" },
"protocols": [
{
"uri": "did:drn:svrn7.net/protocols/Pando.Diagnostics.0.1.0/Query-TOD",
"direction": "inbound",
"match": "exact",
"entrypoint": "Invoke-PandoDiagnosticsDateQuery"
}
]
}match is "exact" or "prefix". direction is always "inbound" for handler protocols.
#Requires -Version 7.2andSet-StrictMode -Version Latestat the top of every LOBE psm1.$ErrorActionPreference = 'Stop'- Use
Assert-BodyFields/Get-BodyField(fromSvrn7.Common) for all body field access —Set-StrictModethrows on absentPSCustomObjectproperties before guards can fire. - Every handler returns either
[Svrn7.TDA.OutboundMessage](to trigger outbound delivery) or$null(no reply). - LOBEs always construct plaintext envelopes (
typ = 'application/didcomm-plain+json'). The Switchboard applies SignThenEncrypt for HTTP delivery and plaintext for WebSocket delivery — LOBEs do not callDIDCommPackingServicedirectly. Export-ModuleMember -Function @(...)must be explicit.
function Invoke-MyLobeVerb {
[CmdletBinding()]
[OutputType([Svrn7.TDA.OutboundMessage])]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string] $MessageDid
)
process {
$msg = $SVRN7.GetMessageAsync($MessageDid).GetAwaiter().GetResult()
$body = $msg.PackedPayload | ConvertFrom-Json -ErrorAction Stop
# ... handle ...
$endpoint = Resolve-SocietySenderEndpoint -Did $msg.FromDid
if (-not $endpoint) { Write-Warning "...: cannot resolve endpoint — result not delivered."; return }
[Svrn7.TDA.OutboundMessage]::new($endpoint, $envelope)
}
}did:drn:svrn7.net/protocols/{LOBE}.{version}/{Verb-Noun}
Examples:
did:drn:svrn7.net/protocols/Pando.Diagnostics.0.1.0/Query-TODdid:drn:svrn7.net/protocols/Svrn7.Federation.0.8.0/initialize-federationdid:drn:svrn7.net/protocols/PandoMail.0.8.0/message
svrn7.net always (never svrn7.io). Verb-Noun uses PascalCase or kebab-case consistently within a LOBE.
Outbound PeerEndpoint is the full URL including path — used verbatim, no suffix appended:
http://localhost:8443/didcomm
See docs/AGENTWALLET.md for the full design (per-identity storage, encrypted
wallet, encrypted databases, per-instance LOBEs, publish workflow) and
SECURITY.md for the at-rest/local security model (password, pin, wallet,
LiteDB encryption, instance naming/port design, threat model).
$env:PANDO_WALLET_PASSWORD = '...' # or you are prompted (double-entry on first run)
dotnet .\Svrn7.TDA.dll --name MyTDA [--port 8443] [--url http://localhost] [--reset]| Parameter | Default | Notes |
|---|---|---|
--name |
(required) | Selects the runtime directory; stored as Svrn7Name in the Wanderer DID Document on first run |
--port |
auto | First run: used verbatim if given, else auto-selected from --port-base (8440). Later runs: rebinds the published port; a conflicting --port is rejected unless --republish-endpoint |
--port-base / --port-span |
8440 / 64 |
First-run auto-select range |
--url |
http://localhost |
Base URL for the DID Document service endpoint; full endpoint = {url}:{port}/didcomm |
--data-root / $PANDO_HOME |
~/.web7-pando |
Root for all per-identity data |
--recovery-phrase "<12 words>" |
— | First run only: restore from an existing BIP39 phrase |
--republish-endpoint |
off | Move the published endpoint to this run's --port/--url (rewrites the DID Document in the encrypted svrn7-dids.db, Version + 1) |
--reset |
off | Deletes the whole <name>-<genesisHash8>/ directory and re-bootstraps (confirm prompt on a terminal) |
--lobe-feed / Tda:LobeRemoteFeed |
— | Fallback source (dir/UNC or HTTP base URL) for a LOBE package not in lobe-library/ |
db-shell (subcommand) |
— | Svrn7.TDA.dll db-shell --name X [--db dids|msg|vcs|schemas|main] [--collection C] [--sql "…"] — read the encrypted databases after unlocking the wallet |
Data layout (~/.web7-pando/ by default):
~/.web7-pando/
├── bin/Debug/net8.0/ published TDA binaries (web7-pando publish profile)
├── lobe-library/ {id}.{version}.nupkg — LOBE package source (filled by Publish or --lobe-feed)
└── <name>-<genesisHash8>/ one directory per identity (slug = Blake3(secp256k1 pub)[..8])
├── agent-identity.wallet encrypted: secp256k1 + X25519 keys, did, role, recovery phrase, DB master key
├── agent-identity.wallet.bak / .lockout
├── identity.meta.json cleartext locator — did, name, role, createdUtc, parentTdaDid (pointer only).
│ NO endpoint URLs: every endpoint comes from the encrypted svrn7-dids.db
│ (own port via DidRegistryPeek pre-host; parent via its DID Document).
│ Rewritten unconditionally each startup (scrubs stale keys from old builds).
├── lobes/ per-instance; lobes.config.json materialized from the embedded default,
│ LOBE modules installed on first reference from lobe-library/
└── mem/ svrn7.db, svrn7-dids.db, svrn7-msg.db, svrn7-vcs.db, svrn7-schemas.db
(+ LiteDB *-log.db) — ALL AES-encrypted; key derived from the wallet
Wallet: Svrn7.Trust.AgentWallet — Argon2id (64 MiB/3/4) + AES-256-GCM. Password
from $PANDO_WALLET_PASSWORD else an interactive prompt (fails fast if neither).
The DB master key is a stable random 32 bytes inside the wallet payload, so a
password change never re-keys the databases.
Testnet script: tools/Initialize-Testnet.ps1 builds, copies dist/*.nupkg into
~/.web7-pando/lobe-library/, sets $env:PANDO_WALLET_PASSWORD, and launches
Wanderer1–4 on 8441–8444 in separate titled console windows.
| Role | Svrn7Role enum |
DID format |
|---|---|---|
| Wanderer | Wanderer |
did:drn:wanderer.svrn7.net/agent/1.0/<genesis-hash> |
| Citizen | Citizen |
did:drn:<societyname>.svrn7.net/citizen/1.0/<genesis-hash> |
| Society | Society |
did:drn:federation.svrn7.net/<societyname>/1.0/<genesis-hash> |
| Federation | Federation |
did:drn:federation.svrn7.net/federation/1.0/<genesis-hash> |
All DIDs use the did:drn: method. <genesis-hash> = Blake3(genesis_secp256k1_compressed_pubkey_bytes) hex-encoded (64 chars). Derived once from the initial key pair and never changes — key rotation updates the DID Document's verificationMethod entries without altering the DID itself. New-Svrn7Did derives the DID deterministically from the key pair and role.
Active | Suspended | Deactivated
Wanderer | Citizen | Society | Federation | LOBEPackageManager | LOBEMarketplace
On a first run (no <name>-<hash8>/ directory for this --name) the TDA, before
the host is built:
- generates a 12-word BIP39 phrase (or takes
--recovery-phrase), derives the secp256k1 identity key (BIP32m/7'/0'/0'/0/0) and the X25519 key-agreement key (HKDF from the same seed); genesisHash = Blake3(secp256k1 compressed pubkey)→ instance dir~/.web7-pando/<name>-<genesisHash8>/;- generates a random 32-byte database master key;
- writes
agent-identity.wallet— one AES-256-GCM blob (Argon2id password key) holding both private keys,did,role, the recovery phrase, and the DB master key. No plaintext key file.
After the host builds it creates the DID Document (secp256k1 + X25519 public keys,
X25519KeyAgreementKey2020, service endpoint on the bound port) inside the
encrypted svrn7-dids.db — the single source for every endpoint URL — and writes
the cleartext identity.meta.json locator (did / name / role / createdUtc /
parentTdaDid only; no endpoint). Later runs read their own bound port from that
DB via DidRegistryPeek in the pre-host block. Bootstrap telemetry:
Svrn7.TDA.Bootstrap ActivitySource + Meter (AddSource/AddMeter in
Program.cs), counters tda.bootstrap.{endpoint_peek,port_resolved,meta_write, parent_endpoint_resolve}.
Key roles (unchanged): the secp256k1 key does genesis-hash derivation, DIDComm
JWS signing (AgentSigningPrivateKey), and transaction signing; the X25519 key
does JWE encrypt/decrypt (AgentKeyAgreementPrivateKey). Neither ever enters a
LOBE runspace. agent-identity.json (plaintext) is superseded — migrate with
--reset.
- Never add
Co-Authored-By,Generated by, or any AI attribution to commits or source files. - Commit messages: imperative mood, concise subject line, blank line before body if needed.
- PowerShell 7.2+ required everywhere — LOBE psm1 files, debug guides, tooling scripts.
Set-StrictMode -Version Latestis active in all LOBEs — never accessPSCustomObjectproperties without guards (Assert-BodyFields/Get-BodyField).- Dot-sourcing
.psm1files in PS 7 applies module-context scoping. Use[scriptblock]::Create([System.IO.File]::ReadAllText($path)).Invoke()for dynamic loading outside the LOBE runtime. Initialize-Svrn7Assemblies -ModuleRoot $PSScriptRootmust be called before accessing anySvrn7.*.NET types in a standalone PS session (outside a TDA runspace). It is called automatically byNew-Svrn7KeyPair,New-Svrn7Did, etc. if the driver is not already initialised.Send-LocalDIDCommMessage(inSvrn7.Common) connects to a local TDA's/localcomm-wsWebSocket and sends a plaintext DIDComm message.POST /didcommenforcesapplication/didcomm-encrypted+json(SignThenEncrypt) and rejects plaintext. A running TDA is required to receive DIDComm replies.
| Limitation | Detail | Backlog ref |
|---|---|---|
| JIT LOBE reimport on every dispatch | Import-Module -Force runs each time for hot-update support; ~30 ms overhead per message. Deferred to Epoch 1. |
TDA-001a |
| PS cannot receive DIDComm replies | A standalone PowerShell session has no HTTP/2 listener — replies require a running TDA as the from DID. |
— |
- Protocol URIs:
svrn7.net(neversvrn7.io) - Cmdlets:
Verb-NounPascalCase — e.g.Send-EmailNotify,Invoke-PandoDiagnosticsDateQuery - "DID Document Resolver" / "DID Document Resolution" (not "DID Resolver")
- Interfaces:
IDidDocumentResolver,ISvrn7Driver,ISvrn7SocietyDriver - Implementations:
LocalDidDocumentResolver,FederationDidDocumentResolver - "Shared Reserve Currency (SRC)" (not "Reserve Currency")
DidDocument(one word, notDIDDocument) in C# types;DIDDocumentin prose- LiteDB registry classes:
Lite{Entity}Registry/Lite{Entity}Store
Web7.SVRN7.Apps.PandoMail is a .NET 8 WinForms Outlook 2003-style email client.
It integrates with the Web 7.0 Pando Citizen TDA in a 1:1 relationship
(one PandoMail instance per Citizen TDA instance, same machine).
The TDA pushes async notifications to PandoMail. There is no polling.
The notification channel is a localhost-only WebSocket endpoint on the TDA,
on the same port as POST /didcomm:
ws://localhost:{port}/localcomm-ws
Single port serves both surfaces:
| Path | Protocol | Direction |
|---|---|---|
/didcomm |
HTTP/2 (POST) |
Inbound DIDComm from remote TDAs |
/localcomm-ws |
WebSocket (RFC 8441 — HTTP/2 extended CONNECT) | Outbound push to local UI clients |
Kestrel uses HttpProtocols.Http2 only. RFC 8441 enables WebSocket over HTTP/2
on /localcomm-ws without enabling HTTP/1.1 on the listener. PandoMail is on .NET 8
and HttpClient supports RFC 8441, so no HttpProtocols.Http1AndHttp2 workaround
is needed and no HTTP/1.1 attack surface is introduced.
This endpoint is not published in the Citizen TDA's DID Document — it is a private local UI attachment point, not a peer-to-peer TDA interface.
Messages on this channel are DIDComm V2 plaintext envelopes
(application/didcomm-plain+json). The channel is localhost-only; PandoMail
holds no key material and shares the Citizen TDA's DID. The @type field
determines dispatch on the PandoMail side. See Principles.md P-008.
Sender TDA (remote)
└── DIDComm V2 SignThenEncrypt (secp256k1 JWS + X25519 JWE)
└── POST /didcomm → Local Citizen TDA (Kestrel, HTTP/2, mTLS)
└── DrawbridgeService.UnpackAsync ← decrypt + verify at inbound boundary
└── Switchboard (routes by @type)
└── Svrn7.Email LOBE (.psm1)
├── Persist to LiteDB (Long-Term Message Memory)
├── Decode SMTP-over-DIDComm payload
└── OutboundMessage (plaintext) → ws://localhost:{port}/localcomm-ws
└── TdaMailClient (PandoMail background thread)
├── DIDComm unpack (plaintext — no decrypt/verify)
├── Dispatch on @type
└── BeginInvoke → MessageStore.Append()
└── SortableBindingList<MailMessage>
└── DataGridView refresh
PandoMail Compose UI
└── TdaMailClient.Send(MailMessage)
└── POST /didcomm (localhost, mTLS)
└── Svrn7.Email LOBE
└── Wrap as SMTP-over-DIDComm
└── DIDComm V2 SignThenEncrypt → Recipient TDA
Inbound email notification @type:
did:drn:svrn7.net/protocols/Email-Notify.0.1.0/new-message
Follows the Locator DID URL convention (draft-herman-drn-resource-addressing-00).
Protocol URIs use svrn7.net (not svrn7.io).
| Component | Project | Purpose |
|---|---|---|
TdaMailClient |
Web7.SVRN7.Apps.PandoMail |
Send() → POST /didcomm; WebSocket receive loop |
MessageStore |
Web7.SVRN7.Apps.PandoMail |
Replace Inbox.xml source with TdaMailClient initial load + live append |
MainForm |
Web7.SVRN7.Apps.PandoMail |
BeginInvoke marshal on WebSocket notification receipt |
Svrn7.Email LOBE |
Svrn7.Email.psm1 |
After LiteDB persist, send DIDComm Email-Notify envelope over WebSocket |
- Handles SMTP messages over DIDComm — this LOBE already exists
- On inbound message: persist to LiteDB first, then push notification to PandoMail via WebSocket
- The LOBE does not write directly to any PandoMail data structure — pass-by-reference semantics apply (LiteDB ObjectId passed, not payload copy)
-
PandoMail DID identity: shared DID (decided). PandoMail shares the Citizen TDA's DID — same identity, no key fragment or sub-DID.
TdaMailClientposts to the local TDA using the shared DID; the TDA signs and forwards using its own key material. -
Initial inbox load: How PandoMail populates
MessageStoreon startup (before any WebSocket push arrives) — options are a DIDComm query message to theSvrn7.EmailLOBE, or a localhost-only REST endpoint on the TDA.
-
PandoMail targets .NET 8 (Windows only).
TdaMailClient.Send()usesHttpClientwith HTTP/2 + mTLS directly forPOST /didcomm. The/localcomm-wspush channel uses RFC 8441 (WebSocket over HTTP/2 extended CONNECT) — both Kestrel andHttpClientsupport it at .NET 8. -
Both
/didcommand/localcomm-wsare on the same port for a given TDA. Kestrel usesHttpProtocols.Http2only — RFC 8441 carries the WebSocket upgrade over HTTP/2 streams, so no HTTP/1.1 is needed and no second port is opened. -
The TDA's public inbound surface remains
POST /didcommonly (Kestrel, HTTP/2, mTLS). The WebSocket endpoint is a separate, localhost-scoped path on the same port. -
The WebSocket Local UI Attachment Point pattern is reusable — any local UI app (calendar, contacts, tasks) on any OS where the TDA runs can connect to the same WebSocket endpoint and dispatch on
@type. Future notification types:did:drn:svrn7.net/protocols/Calendar-Notify.0.1.0/new-event did:drn:svrn7.net/protocols/Presence-Notify.0.1.0/status-change