@midnight-ntwrk/midnight-did-api
v0.5.0
Published
Programmatic API for creating, updating, deactivating, and resolving Midnight DIDs.
Downloads
5,374
Readme
@midnight-ntwrk/midnight-did-api
Programmatic API for creating, updating, deactivating, and resolving Midnight DIDs.
Responsibilities
- Build/connect providers (node, indexer, proof server)
- Submit contract circuits for DID operations
- Map inputs/outputs between app/domain and ledger/runtime
- Generate and persist DID controller private state for create/rotation flows
- Provide integration test topology and helpers
- Return DID resolution data (
didDocument,didDocumentMetadata) for API callers
Use It When
- you need programmatic DID deployment or mutation flows
- you need provider bootstrap for standalone, preprod, or env-driven mainnet
- you are building a higher-level application and do not want to manage raw contract/runtime wiring
Architecture
graph TD
App[Manager / Tests / Integrator]
API[API facade]
Domain[Domain validation]
DidPkg[DID mapper]
Contract[Contract bindings]
Providers[Providers]
Chain[(Midnight chain)]
Indexer[(Indexer)]
App --> API
API --> Domain
API --> DidPkg
API --> Contract
API --> Providers
Providers --> Chain
Providers --> IndexerUpdate Sequence
sequenceDiagram
participant Caller
participant API
participant Contract
participant Indexer
Caller->>API: addService / addVerificationMethod / ...
API->>API: validate + normalize
API->>API: sign controller authorization digest for current contract version
API->>Contract: submit circuit tx + controller signature
Contract-->>API: accepted tx
API->>Indexer: fetch current state
API-->>Caller: updated DID state or DID Resolution ResultState Model
API enforces lifecycle rules around:
- active DID: allows updates
- deactivated DID: mutating operations rejected
- controller authorization: signs a domain-separated digest containing contract id, current version, operation name, and operation arguments before each controller-gated mutation
- controller rotation: generates a new wallet-local secret, derives the next controller public key locally, submits the rotation circuit with a current-version controller signature, and stores the new secret after the transaction succeeds
- controller recovery: a dedicated
recoveryAuthorityPublicKeycan authorizerecoverControllerKeyto rotate the active controller key; ordinary controller-gated operations require only the active controller secret, while recovery requires the matching recovery secret
(Exact schema/canonicalization rules live in domain.)
Controller Secret Recovery Posture
The API package can initialize, persist, rotate, recover, and restore private
state that authorizes DID updates. It cannot recover secrets from ledger state,
but it can submit recoverControllerKey when private state contains, or the
caller explicitly supplies, the recoverySecretKey matching the on-ledger
recoveryAuthorityPublicKey. Explicitly supplied recovery secrets are used for
that recovery call and are not newly persisted into active private state, though
an already stored recovery secret that matches the on-ledger recovery authority
is preserved when the new controller secret is promoted.
Applications should back up controller and recovery private state alongside
their wallet backup material, protect it with custody controls appropriate for
production signing keys, and test restore/recovery flows before relying on a DID.
Private state created before the recovery-authority contract surface contains no
recovery secret; it can still authorize ordinary controller-gated operations when
paired with a compatible contract, but it cannot submit recoverControllerKey
unless the recovery secret is imported or supplied explicitly. If both the active
controller secret and recovery secret are lost, the DID remains publicly
resolvable but cannot be updated, rotated, recovered, or deactivated by this
method version. Organizational operators that need multi-person approval or
social recovery should implement that policy before the API call that signs a
controller or recovery authorization; the contract still receives one signature
for the selected operation.
Resolution Responses
The API package exposes both convenience and DID Core envelope helpers.
resolve returns the ledger-derived DID Document and DID Document metadata, or
null when the contract state is missing. resolveDIDResolutionResult returns
the full DID Core Resolution Result envelope with didResolutionMetadata.
Successful abstract resolve responses must not set
didResolutionMetadata.contentType; that field is reserved for
resolveRepresentation responses where the body is a DID Document byte stream.
The API package also exports resolveRepresentation(providers, didContract,
options). It delegates to the shared MidnightDIDResolver and returns
didDocumentStream as a Uint8Array | null (null on resolution errors),
didDocumentMetadata, and didResolutionMetadata. This is the package boundary intended for
midnight-did-resolver: the downstream service owns HTTP routing and status
codes, while this package owns ledger access, representation selection, and DID
resolution errors.
Build & Test
- Build:
pnpm --filter ./packages/api build - Typecheck examples:
pnpm --filter ./packages/api typecheck:examples - API import discipline:
pnpm run check:api-source-imports - DID package import discipline:
pnpm run check:source-imports - Unit tests:
pnpm --filter ./packages/api test - Integration tests:
pnpm --filter ./packages/api test-api
API TypeScript source and tests use explicit .js or .json extensions for
relative imports, including Vitest mocks. This keeps the package aligned with
the emitted ESM graph and avoids resolver-only test behavior.
The wider check:source-imports guard applies the same rule to all DID-owned
TypeScript package sources outside generated src/managed artifacts.
Runtime Profiles
StandaloneConfigTestnetLocalConfigTestnetRemoteConfigPreprodConfigMainnetConfigProfileConfig
Defaults:
- all profile defaults live in
src/config-profiles.ts PreprodConfigandMainnetConfiguse public indexer v4 endpoints (/api/v4/graphql+/ws).MainnetConfigdefaults to local proof server (http://127.0.0.1:6300) so it can be used with local proving while targeting mainnet indexer/node.- constructing any profile config calls
setNetworkId()throughapplyMidnightNetworkProfile(), so wallet and contract operations see the correct Midnight network before they start.
The docs site publishes the generated endpoint matrix at
https://midnightntwrk.github.io/midnight-did/guide/network-endpoints; it is
generated from src/config-profiles.ts during docs preparation and validation.
You can still override MainnetConfig endpoints explicitly when needed. New
tooling should use ProfileConfig when the profile name is data-driven rather
than hard-coded in a class constructor. Every ProfileConfig instance exposes
the resolved profileName so logs and operator tooling can report the active
profile without inferring it from URLs.
Network Mapping Helpers
Use the typed mapping helpers when converting between Midnight runtime network ids and DID-domain network names:
RuntimeToDomain.NetworkMap: maps runtimeNetworkIdvalues to DID-domainMidnightNetworkvalues.DomainToRuntime.NetworkMap: maps DID-domainMidnightNetworkvalues back to runtimeNetworkIdvalues.RuntimeToDomainNetworkMapandDomainToRuntimeNetworkMap: readonly public type aliases exported from the package barrel for downstream configuration and test helpers.
The older NetworkMapping export is a compatibility alias for
RuntimeToDomain.NetworkMap. New code should prefer the direction-specific
helpers so map intent is visible at the call site.
Provider adapters for proof, indexer, and ZK configuration are loaded lazily by
configureProviders(). Importing the API package barrel for mapping helpers,
types, or examples does not load those runtime adapters.
Release Artifact Metadata
The package embeds ZK artifact locations for its own published version:
import {
MIDNIGHT_DID_API_VERSION,
createMidnightDidZkArtifactLocations,
} from "@midnight-ntwrk/midnight-did-api";
const locations = createMidnightDidZkArtifactLocations(MIDNIGHT_DID_API_VERSION);Use locations.ghcr.reference to pull the matching GHCR OCI artifact in Node or
CI tooling. RC and final release versions also include
locations.githubRelease.archiveUrl; snapshot versions publish workflow
artifacts and GHCR artifacts only, so locations.githubRelease is null.
Node consumers can download, verify, and unpack GitHub Release assets directly:
import {
downloadMidnightDidGithubReleaseZkArtifacts,
MIDNIGHT_DID_API_VERSION,
} from "@midnight-ntwrk/midnight-did-api";
const bundle = await downloadMidnightDidGithubReleaseZkArtifacts({
version: MIDNIGHT_DID_API_VERSION,
outputDir: ".midnight-did-zk",
});
process.env.MIDNIGHT_DID_ZK_CONFIG_PATH = bundle.zkConfigPath;bundle.zkConfigPath is the directory to pass to NodeZkConfigProvider or to
expose from an HTTP server for FetchZkConfigProvider. The helper verifies the
release .sha256 file, checks that the downloaded manifest matches the embedded
archive manifest, and validates every circuit file checksum before returning.
When outputDir, tempDir, or pullDir are omitted, helper-created
directories are retained because bundle.archivePath and bundle.zkConfigPath
point into them. Callers that need deterministic cleanup should pass explicit
directories and remove them after the ZK provider no longer needs the files.
For GHCR OCI artifacts, use pullMidnightDidGhcrZkArtifacts() in an environment
with the oras CLI available.
When the ZK bundle is unpacked outside the installed package, set
MIDNIGHT_DID_ZK_CONFIG_PATH to the directory containing manifest.json,
keys/, and zkir/ before importing @midnight-ntwrk/midnight-did-api.
Without this override, the API uses the managed artifacts bundled with the
installed contract package when available.
setLogger() is optional for embedders. Until it is called, API helpers use a
no-op logger so wallet/provider setup can run in minimal scripts without
preconfiguring logging.
Main Source Files
src/index.tssrc/lib.tspublic compatibility facadesrc/config-profiles.tsnetwork profile catalog and network-id applicationsrc/deploy.tscontract deployment, join, and private-state initializationsrc/providers.tsprovider composition for DID runtime dependenciessrc/private-state-storage.tsprivate-state storage account/password wiringsrc/transaction-intents.tsmanual unshielded intent signing workaroundsrc/wallet-context.tsSDK wallet construction and restore context assemblysrc/wallet-dust.tsdust-registration workflow helpersrc/wallet-provider.tswallet facade to Midnight wallet/provider adaptersrc/wallet-state.tswallet snapshot, sync, balance, and funding wait helperssrc/wallet.tswallet construction and restore facadesrc/wallet-keys.tsseed parsing, HD key derivation, and unshielded address helperssrc/wallet-sdk-config.tsshared wallet SDK configuration builderssrc/lightweight.tsstateless crypto helpers only; wallet wait behavior lives insrc/wallet-state.tssrc/did-subject.tsDID subject and bound fragment normalizationsrc/ledger-mappers.tsDID document domain-to-ledger DTO mapping helperssrc/update.tsDID document update, deactivate, and resolve orchestrationsrc/types.tssrc/test/
Type-safety policy:
- production API source must not use
as anycasts as unknown ascasts must be explicitly allowlisted inpnpm run check:did-surface-discipline- keep SDK type mismatches localized behind narrow adapter helpers and update the surface-discipline guard when an intentional compatibility escape hatch is unavoidable
Legacy deep source files src/contract-lifecycle.ts and src/did-operations.ts
remain as short-lived deprecation shims for external deep source-path imports.
Internal code should use the split modules above, and package consumers should
import from @midnight-ntwrk/midnight-did-api.
Deploy And Update Example
See examples/README.md, examples/deploy-did.ts, and
examples/update-did.ts for package-local deploy/update flows that use only API
package exports. Resolver services, DID manager UI, and reusable secret storage
stay in midnight-did-resolver.
Integration Teardown
packages/api/src/test/commons.ts now uses:
- unique compose project names per run
env.down({ removeVolumes: true })- fallback
docker compose down --volumes --remove-orphans
This reduces container/volume leaks when tests fail mid-run.
