@dexterai/vault
The off-chain mirror of the dexter-vault Anchor program. Byte-precise instruction builders, message encoders, account decoders, and signer abstractions for the passkey-rooted vault.
Building an app? Reach for
@dexterai/x402instead: it gives your agent a spending limit in a few lines, both the buyer and seller sides, and depends on this package transitively. Come to@dexterai/vaultwhen you assemble your own vault transactions: a facilitator, a custom settlement path, or a second Open Tabs Standard implementation. This is the engine, not the front door.
Why this package exists
Three places used to hand-roll the same protocol: dexter-api/src/vault/, dexter-facilitator/src/vault/, and dexter-vault/tests/. One of them registered role 3 (ProgramExec for settle_tab_voucher); two did not. The end-to-end tab-settle smoke kept failing with Role not found for ID: 3, and it ate hours of debugging on 2026-06-02 before anyone caught the drift.
This package is the structural fix. The canonical 4-role Swig provisioner, every instruction builder, every byte-precise message encoder, the vault account decoder, and the precompile helpers each live in exactly one file. Every consumer (dexter-api, dexter-facilitator, dexter-vault tests, @dexterai/x402/tab) imports from here. The drift bug class is gone.
If you are about to hand-roll a vault instruction builder, a precompile message, a Swig role list, or a vault account decoder, import from here instead.
Install
npm install @dexterai/vault
Targets the dexter-vault V7 program: 70 generated-IDL instructions, per-counterparty SessionAccount PDAs, and a monotonic per-vault PasskeyAuthorization replay guard. Pre-V7 vaults must complete the program's migration path before using V7 passkey-gated builders; V5 vaults migrate through V6 first.
The byte contract
Every byte the on-chain program checks lives here, in exactly one file each: instruction discriminators, the 188-byte V2 session-registration message, the 252-byte V3 revocation message, the 44-byte voucher payload, the canonical 200-byte passkey-authorization envelope, and the Vault + SessionAccount + PasskeyAuthorization account layouts. The dexter-vault program is the source of truth; this package is the TypeScript that talks to it, with byte-parity locked by snapshot tests (see below). A programmatic Swig role makes the non-custodial spend path possible, and the entire spend path goes through the vault program, with no master key, no escrow, and no trust.
Provision a vault (server-side enrollment)
import {
buildSetSwigAtomicFromIdentity,
SWIG_BIRTH_MODE_OWNER_ONLY,
} from '@dexterai/vault/instructions';
import { Transaction } from '@solana/web3.js';
// `clientDataJSON` and `authenticatorData` come from the owner passkey ceremony
// over buildSetSwigOperationMessage(expectedSwigAddress).
const atomicBirth = buildSetSwigAtomicFromIdentity({
vaultPda,
feePayer: feePayerKeypair.publicKey,
dexterMasterPubkey: sessionMaster.publicKey,
identitySeed: userHandleBytes, // per-user, stable, ≤32 bytes
hmacKey: serverSecret.subarray(0, 32), // 32-byte HMAC key
clientDataJSON,
authenticatorData,
spendLimitAtomic: 0n,
sessionTtlSeconds: 0n,
spendMode: SWIG_BIRTH_MODE_OWNER_ONLY,
});
const tx = new Transaction().add(p256VerifyInstruction, atomicBirth);
Fresh enrollment is one Vault instruction so Swig creation, the program-defined
authority spine, role-0 neutering, and the reverse SwigVaultBinding commit or
roll back together. The legacy buildSwigCreationBundle multi-instruction API
is retained only as a fail-closed compatibility symbol and always throws.
Settle a tab voucher (facilitator-side)
import { buildSettleTabVoucherInstruction } from '@dexterai/vault/instructions';
import { buildEd25519VerifyInstruction } from '@dexterai/vault/precompile';
import { buildVoucherMessage } from '@dexterai/vault/messages';
import { readVaultFull } from '@dexterai/vault/reader';
const vaultState = await readVaultFull(connection, vaultPda);
const voucherMessage = buildVoucherMessage(channelId, cumulativeAmount, sequenceNumber);
const tx = new Transaction().add(
// Ed25519 precompile verifies the session key signed the voucher bytes.
buildEd25519VerifyInstruction(sessionPubkey, sessionSignature, voucherMessage),
// settle_tab_voucher consumes the verified voucher; Swig role 3 drives the SPL transfer.
// allowedCounterparty names the per-counterparty session PDA being settled against.
buildSettleTabVoucherInstruction({
vaultPda,
swigAddress: new PublicKey(vaultState.swigAddress!),
dexterAuthority: sessionMaster.publicKey,
allowedCounterparty,
channelId,
cumulativeAmount,
sequenceNumber,
}),
);
Read vault state
import { readVaultOnchain, readVaultFull } from '@dexterai/vault/reader';
import { fetchSessionAccount, isSessionLive } from '@dexterai/vault/session';
// Slim shape: { exists, pendingVoucherCount, pendingWithdrawal }.
const slim = await readVaultOnchain(connection, vaultPda);
// Full shape adds: { version, swigAddress, dexterAuthority, liveSessionCount }.
const full = await readVaultFull(connection, vaultPda);
// V7: per-counterparty session state remains in its own PDA, not in the vault.
const s = await fetchSessionAccount(connection, vaultPda, allowedCounterparty);
if (s && isSessionLive(s)) {
console.log(`tab open: ${s.session.spent} / ${s.session.maxAmount}`);
}
Read crystallized claims (the reservation tier)
A voucher can be crystallized into a LockedClaim: an irreversible, buyer-unwithdrawable reservation of the spent amount. The vault tracks the running sum in outstanding_locked_amount, surfaced by readVaultFull as outstandingLockedAmount. This is the reservation that backs lock-mode tabs — the standard tab protection as shipped, surgically reserving only the accrued amount rather than freezing the whole wallet.
import { readVaultFull, fetchVaultLockedClaims, decodeLockedClaim } from '@dexterai/vault/reader';
// Vault-level total: the sum the withdrawal gate reserves out of the balance.
const { outstandingLockedAmount } = await readVaultFull(connection, vaultPda);
// Per-claim detail. Each claim is a terminal state machine: Pending → Settled
// or Pending → Abandoned. Filter to the live (unsettled) reservations.
const pending = await fetchVaultLockedClaims(connection, vaultPda, { status: 'Pending' });
// reconciliation invariant: sum(pending.amount) === outstandingLockedAmount
for (const c of pending) {
console.log(`${c.voucherHash}: ${c.amount} held by ${c.currentHolder} (${c.status})`);
}
// decodeLockedClaim(address, accountData) decodes a single account you already
// hold — the same moving-cursor decoder fetchVaultLockedClaims uses internally.
Derive the counterfactual Swig address
import { deriveCounterfactualAddresses } from '@dexterai/vault/counterfactual';
// Returns both the state PDA (program-owned) and the wallet-address PDA
// (system-owned, the asset holder). Useful for showing a deposit address
// before the Swig is on chain.
const { swigStateAddress, swigWalletAddress } = await deriveCounterfactualAddresses({
identitySeed: userHandleBytes,
hmacKey: serverSecret.subarray(0, 32),
});
Sessions: one tab per counterparty
Each session lives in its own SessionAccount PDA, [b"session", vault, allowed_counterparty]: one tab per (vault, counterparty), many counterparties per vault. Registering against a counterparty that already has a session replaces it in place (same seed) and resets the meters (spent, currentOutstanding); anything building UX on top should warn before replacing.
Registering requires the sibling contract: the program checks that the transaction names every other version≠0 session of the vault, so it can sweep expired ones and prove the new cap does not overcommit the vault's balance.
import { buildRegisterSessionKeyInstruction, buildRevokeSessionKeyInstruction } from '@dexterai/vault/instructions';
import { fetchVaultSessionAccounts, sessionPdasOf, waitForSession, resolveVaultUsdcAta } from '@dexterai/vault/session';
// fetch siblings FRESH immediately before building (the gate sweeps expired
// siblings; a stale list fails the on-chain completeness check)
const siblings = sessionPdasOf(await fetchVaultSessionAccounts(connection, vaultPda));
// resolve the vault's USDC ATA — returns its address if it exists, or `null`
// for a credit-only vault that never received a deposit (see note below)
const vaultUsdcAta = await resolveVaultUsdcAta(connection, swigAddress);
const ix = buildRegisterSessionKeyInstruction({ ...args, payer, vaultUsdcAta, siblingSessionPdas: siblings });
// ... send [secp256r1Precompile, ix] ...
await waitForSession(connection, vaultPda, allowedCounterparty, { expectedSessionPubkey });
The builder handles the fiddly parts of the sibling list (excludes the target, dedups, sorts strict-ascending by raw bytes, marks all writable); your only job is fetching it fresh. waitForSession is content-aware confirm-visibility: it waits for the new session_pubkey, because on a replace the old registration also passes existence and version checks under read-your-writes lag.
vaultUsdcAta is optional. The register gate computes backing as own_USDC + available_standby_credit. A self-funded vault passes its USDC ATA so its balance counts; a credit-only vault never receives a deposit, so it has no ATA — pass vaultUsdcAta: null and own-USDC is counted as 0 (backing is the standby line alone). resolveVaultUsdcAta makes this decision for you (derive-and-probe): use its PublicKey | null result directly. Omitting an ATA that exists only ever understates backing, so it is safe; a self-funded vault that wants its balance counted must pass it.
Credit primitives
The vault program supports a tab that spends past the user's balance, backed by a financier's standby capital, structured so the buyer cannot rug the financier and the financier cannot seize more than the agreed bound. The builders for that path live here:
import {
drawCredit,
originateLineCredit,
repayCredit,
seizeCollateral,
} from '@dexterai/vault/tab';
Credit is not a separate product. It is a tab that can spend past its balance, so it composes the same primitives: open_standby arms the backing, draw_credit / repay_credit move the borrowed balance, seize_collateral runs the default path, and the LockedClaim crystallized tier (@dexterai/vault/instructions) plus factoring / instant payout (@dexterai/vault/factoring) build on top. Demonstrated on Solana mainnet: a draw, a repayment, and a default-and-seize. This is the newest surface in the package; treat it accordingly.
originateLineCredit is the evidence-bearing draw path. It derives the
versioned evidence and terms commitments, then returns (but does not send) one
atomic record_line_origination -> draw_credit -> Swig SignV2 envelope. The
per-draw receipt is immutable evidence for the existing PrincipalNode facility;
it is not a transferable claim and is not a second balance ledger. Read it with
readLineOrigination or decode the compact event and call
fetchLineOriginationForEvent for the required full receipt.
New first draws should commit LineTermsV2. V2 binds the agreed due date,
requested recovery window, rate, principal, asset, and policy/evidence hashes,
but deliberately does not contain defaultAt or curePeriodSeconds: those
values are finalized from the actual landing clock. Existing V1 exports remain
unchanged.
import { originateLineCredit } from '@dexterai/vault/tab';
import { LINE_TERMS_V2_SCHEDULE_POLICY } from '@dexterai/vault/credit';
const result = await originateLineCredit({
...draw,
terms: {
schedulePolicy: LINE_TERMS_V2_SCHEDULE_POLICY,
assetDecimals: 6,
dueAt,
legalAgreementHash,
underwritingHash,
policyHash,
pricingHash,
},
});
// originateLineCredit binds the exact live mint, principal, node rate, and
// requested recovery window before hashing the V2 envelope.
Spend grants (@dexterai/vault/grant)
An app proposes a bounded spend-tab; the user's passkey endorses the exact 188-byte registration scope. Two halves:
// App side: produce a self-contained request blob (no keys, signs nothing):
import { requestSpendGrant, encodeSpendGrantRequest } from '@dexterai/vault/grant';
const blob = requestSpendGrant({
app: { name: 'Acme Research', domain: 'acme.example' },
counterparty: SELLER_ADDRESS, // the on-chain binding (session PDA seed)
capAtomic: '5000000', // $5; the user may only SHORTEN
expiresAtUnix: Math.floor(Date.now() / 1000) + 7 * 86400,
sessionPubkey: AGENT_SESSION_PUBKEY, // optional; see custody note
});
const consentUrl = `https://dexter.cash/grant?req=${encodeSpendGrantRequest(blob)}`;
// Consent side: parse untrusted input, apply shorten-only edits, run the
// passkey ceremony over the exact bytes, end at the SIGNED GRANT:
import { parseSpendGrantRequest, approveSpendGrant } from '@dexterai/vault/grant';
const request = parseSpendGrantRequest(rawBlob);
const approved = await approveSpendGrant({
request,
vaultPda, // the USER's vault, never from the blob
edits: { capAtomic: '2000000' }, // shorten-only; raises throw
sign: (message) => myWebAuthnPipeline(message),
});
An optional seller field carries { calledUrl, offer?: { format: 'jws', signature }, expectedHost? }.
calledUrl must be HTTPS with no credentials or explicit port and at most 2048 characters.
The compact JWS has three base64url segments and at most 4096 characters;
expectedHost is a lowercase ASCII hostname of at most 253 characters.
Both request entry points reject malformed seller input with bad_seller and drop unknown keys.
The request blob stays unsigned. The quote inside carries the store's signature;
callers verify that signature and the seller binding separately.
If the blob carries sessionPubkey, the requesting app's agent holds the session secret and can drive spend to the consented cap on its own pacing. Omit it and approveSpendGrant generates the keypair caller-side, so the requester never sees the secret. Either way exposure is bounded by cap × counterparty × expiry, enforced on-chain at settle.
Approval and refusal evidence (@dexterai/vault/mandate)
verifyMandateApproval returns ceremonyNonce as lowercase hex only after the
complete 200-byte approval verifies. It returns null for a legacy 32-byte
challenge or an invalid or unavailable approval.
verifyCaseFailedTransaction accepts three CaseRefusalAttempt variants:
settle_voucher, settle_tab_voucher and settle_locked_voucher. The existing
variants retain their counterparty fields. A locked payout uses:
const attempt = {
instruction: 'settle_locked_voucher' as const,
vault, claim, destinationOwner,
amount: 1_000_000n, // Expected amount from the claim evidence.
holder, // Optional expected holder signer.
authority, // Optional expected Dexter authority signer.
};
verifyCaseFailedTransaction(transaction, signature, {
program: DEXTER_VAULT_PROGRAM_ID.toBase58(), code: 6070, attempt,
});
The locked payout check decodes the following Swig SignV2 TransferChecked and requires the canonical source USDC ATA, USDC mint, expected destination ATA, claim amount and six decimals. Bound accounts must be signed static keys. The caller supplies the expected claim amount and establishes the claim state and vault's Swig identity from separate evidence. Transaction signatures cover the attempted instructions; the RPC node reports the failure code and logs.
Byte-parity guarantee
tests/byte-parity.test.ts, tests/precompile.test.ts, tests/swigBundle.test.ts, tests/counterfactual.test.ts, and tests/reader.test.ts together snapshot:
- Every hand-pinned instruction discriminator in
DISCRIMINATORS, derived fromsha256("global:<name>")and checked against the generated IDL. - The core session/voucher message layouts, byte-by-byte: 188-byte V2 session registration, 252-byte V3 exact-state revocation, and the 44-byte voucher payload, plus operation-specific authorization-message suites.
- Both precompile builders, secp256r1 (SIMD-0075) and Ed25519, including the 14-byte offsets table.
- The vault account decoder for the V7 layout, the 162-byte SessionAccount decoder, and the per-vault replay-guard decoder.
- The atomic Swig-birth contract: deterministic identity derivation, canonical reverse binding, owner-only mode encoding, exact 211-byte consent, semantic ProgramExec marker catalog, and fail-closed legacy enrollment.
- The counterfactual derivation for a known seed.
If a future change drifts any of these by a single byte, the snapshot tests fail. The on-chain dexter-vault program is the ultimate referee; these tests catch the drift before it ships.
npm test
Pre-audit, and we say so. The dexter-vault program this package mirrors is not yet externally audited; funding is in flight. The report and any findings publish in the program repo. Responsible disclosure: [email protected].
Architecture
┌─────────────────────────────────┐
│ dexter-vault (Anchor program) │ ← source of truth
│ V7: 70 IDL instructions + guard│
└────────────────┬────────────────┘
│ defines bytes
▼
┌─────────────────────────────────┐
│ @dexterai/vault (this package) │ ← off-chain mirror
│ byte-parity locked by tests │
└────────┬────┬──────────┬────────┘
│ │ │
┌─────────────────┘ │ └──────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────────┐ ┌───────────────────┐
│ dexter-api │ │ dexter-facilitator│ │ dexter-vault tests│
│ (DB + glue) │ │ (tab settle path) │ │ (anchor smokes) │
└──────────────┘ └──────────────────┘ └───────────────────┘
▲ ▲
│ │
└────────── @dexterai/x402/tab imports message ───────┘
helpers from @dexterai/vault; keeps
the HTTP wrapping
Subpath exports
Each subpath is a tree-shakeable entry point. Pull only what you need.
| Subpath | Contents |
|---|---|
@dexterai/vault | Re-exports types + counterfactual + session for convenience |
@dexterai/vault/types | VaultState, VaultStateFull, SessionAccountState, SignedVoucher, VoucherPayload, AtomicAmount, HumanAmount, TabNetworkId, and the rest |
@dexterai/vault/constants | Program IDs, mint IDs, the complete pinned DISCRIMINATORS map, PDA seeds, and OTS domain tags |
@dexterai/vault/instructions | Every builder, including replay-guard initialization, atomic Swig birth, governed Send/swap and trade-grant flows, exact cash-backed Native Tab open, session register/revoke and one-ceremony live replacement, Tab settlement, withdrawals, passkey rotation/proof, and migrations. Legacy multi-instruction Swig enrollment and unsigned Tab close are fail-closed. |
@dexterai/vault/messages | sessionRegisterMessage (188 bytes), sessionRevokeMessage (252 bytes), sessionReplaceV1Message (348 bytes), buildLegacyV1TabBindingOperationMessage (501 bytes), buildVoucherMessage (44 bytes), and the operation-specific consent builders |
@dexterai/vault/reader | readVaultOnchain, readVaultFull, passkey-safe Vault decoding, LockedClaim readers, and Line receipt/event fetchers (decodeLineOrigination, readLineOrigination, fetchLineOriginationForEvent) |
@dexterai/vault/session | V7 per-counterparty sessions: deriveSessionPda, fetchSessionAccount, fetchVaultSessionAccounts, sessionPdasOf, waitForSession, registerSessionWithRetry, resolveVaultUsdcAta (derive-and-probe the vault USDC ATA, null for credit-only), and the rest |
@dexterai/vault/grant | Spend-grant consent flow: requestSpendGrant, parseSpendGrantRequest, approveSpendGrant, encode/decode |
@dexterai/vault/precompile | buildSecp256r1VerifyInstruction, buildPrecompileMessage, buildEd25519VerifyInstruction |
@dexterai/vault/counterfactual | deriveCounterfactualAddresses |
@dexterai/vault/signers · /node · /browser | Ed25519Signer / PasskeySigner interfaces; NodeEd25519Signer; WebAuthnAssertion (browser P-256 ceremony) |
@dexterai/vault/tab | Composed verbs: openTab, settleTab, readTabMeter, drawCredit, originateLineCredit, repayCredit, seizeCollateral |
@dexterai/vault/factoring | computeFactoringSplit, buildInstantPayoutInstructions |
@dexterai/vault/kit | kitInstructionsToWeb3, getRpc (Swig-kit↔web3 bridge) |
The legacy direct @dexterai/vault/connect ceremony was retired with the V7
authorization protocol. It cannot satisfy the canonical replay-guard envelope
from a third-party WebAuthn origin. Website integrations must use
@dexterai/connect, whose hosted Dexter consent window pins the signing origin
while leaving the opener website unrestricted.
Existing V7 vaults must run the permissionless, idempotent
buildInitializePasskeyAuthorizationInstruction once before their first guarded
operation. A vault-version migration alone does not create this replay-guard PDA.
One-ceremony live session rotation (unpublished 0.42.1 repair)
V7 rejects direct overwrite of an unexpired SessionAccount. The previously safe
rotation shape requires two guard-bound WebAuthn assertions in one transaction:
revoke_session_key(N) followed by register_session_key(N+1). With canonical
200-byte authorization challenges, that exact transaction is 1,621 bytes at
zero sibling sessions even after every eligible account is compressed through
an address lookup table; Solana's packet cap is 1,232 bytes.
replace_session_key_v1 repairs that transport dead-end with one assertion. Its
fixed 348-byte operation binds the current guard nonce, canonical target PDA,
all ten retired registration fields/meters, and all six replacement fields.
The instruction preserves the meters, requires the replacement limits to cover
them, and changes only the passkey-endorsed session scope. No retired fields are
duplicated in instruction data: the program rebuilds them from the live target,
so any post-ceremony meter drift makes the operation hash stale.
composeRevokeThenRegister now emits [secp(replace), replace_session_key_v1] for a live target and retains [secp(register), register_session_key] for absent, cleared, or expired targets. Before every
replace attempt it re-fetches the target, replay guard, and complete sibling
set; target-meter or guard-nonce drift fails closed before retry. The deprecated
revokeCeremony input is ignored and never enters a transaction.
A full address lookup table is required for realistic browser payloads. Pinned wire sizes for zero/one/four sibling sessions are respectively 991/993/999 bytes with the full ALT. The same legacy shapes are 1234/1267/1366 bytes and v0 without the ALT is 1236/1269/1368 bytes, so neither uncompressed form is a safe fallback under Solana's 1,232-byte cap. These are PREP artifact measurements; they are not evidence of package publication or program deployment.
Signer abstraction
interface Ed25519Signer {
readonly publicKey: Uint8Array; // 32 bytes
sign(message: Uint8Array): Promise<Uint8Array>; // 64-byte signature
}
interface PasskeySigner {
readonly credentialId: Uint8Array;
sign(challenge: Uint8Array): Promise<{
signature: Uint8Array;
clientDataJSON: Uint8Array;
authenticatorData: Uint8Array;
}>;
}
NodeEd25519Signer ships at @dexterai/vault/signers/node. The browser passkey signer is WebAuthnAssertion at @dexterai/vault/signers/browser: a pure-browser P-256 ceremony that runs navigator.credentials.get() and returns the three on-chain-ready buffers (64-byte compact lowS signature, raw clientDataJSON, raw authenticatorData), with zero fetch calls. It implements PasskeySigner; consumers compose it with their own server-policy adapter.
Versioning
The current SDK targets the dexter-vault V7 program (70 generated-IDL instructions; per-counterparty SessionAccount PDAs; a monotonic per-vault PasskeyAuthorization replay guard; role 3 ProgramExec for settle_tab_voucher on every new Swig). Pre-V7 vaults must complete the program's migration path before using V7 passkey-gated builders; V5 vaults migrate through V6 first. The crystallized-claim reader (outstandingLockedAmount, fetchVaultLockedClaims, decodeLockedClaim) and the settle_locked_voucher Swig marker arrived in 0.10.0. Future program versions bump the SDK major or document the delta in the CHANGELOG. The byte-parity tests are the structural lock: any layout change requires an explicit snapshot update.
Publishing
The former local release* npm scripts have been removed. Use the verified
archive and maintainer approval procedure below for publication.
Releases use Publish Vault SDK. The build
job runs npm run verify:release:package, which tests the committed source,
packs it, and checks fresh ESM, CommonJS, and TypeScript consumers. Its tarball
and release-proof.json are saved as a GitHub Actions artifact. A second job
uploads the same tarball to npm staging for a maintainer to approve with 2FA.
Configure the vault-npm-production GitHub environment with a deployment rule
allowing release tags. In the npm settings for
@dexterai/vault, add a GitHub Actions trusted publisher with these exact values:
| Setting | Value |
|---|---|
| Organization | Dexter-DAO |
| Repository | dexter-vault-sdk |
| Workflow filename | publish-vault.yml |
| Environment | vault-npm-production |
| Allowed action | Enable npm stage publish only |
These settings require configuration in GitHub and npm. Adding the workflow to the repository does not configure either service. The workflow uses Node 24 and npm 11.19.1, which meet the requirements for npm trusted publishing. Vault's source repository is private, so the publish job disables provenance generation as required by npm's current support for private repositories.
To release a stable version, merge the version and lockfile changes into main,
then push a tag matching package.json, such as v0.43.4. The workflow checks
that the tagged commit belongs to main. Pull requests run package verification
without requesting publication approval.
The staging job checks the saved hashes and submits the tested tarball using
OIDC. Its run summary shows the stage ID and an npm stage approve command;
the vault-stage artifact contains the npm response and package checksums.
Review that package, then approve it in npm with 2FA. Until that approval, the
release remains pending and cannot be
installed from the public registry. The approval happens in npm because the
GitHub plan for this private repository does not support required reviewers.
After approval, rerun the workflow's staging job to verify npm's package
integrity and the latest tag. A published version
is accepted only when its bytes match the tested package. If a staging attempt
was interrupted, inspect npm's Staged Packages tab before rerunning: a pending
stage already reserves that package version.
For a manual run, choose the release tag in GitHub Actions or use:
gh workflow run publish-vault.yml --repo Dexter-DAO/dexter-vault-sdk --ref v0.43.4
License
MIT. © 2026 Dexter.
Part of the Dexter-DAO open source family.
Local staging when GitHub runners are unavailable
Use Node 22.14 or newer and npm 11.19.1. Log in with npm login --auth-type=web,
then run npm run verify:release:package from a clean, committed checkout.
The selected commit must be merged into main before publication.
Stage the exact archive recorded in the verifier's release-proof.json:
npm stage publish /path/from/release-proof/packed/dexterai-vault-VERSION.tgz \
--ignore-scripts --access public --tag latest --provenance=false --json
npm stage view STAGE_ID --json
Compare the staged integrity and shasum with the package proof. Wait until
npm stage view reports staged before running npm stage approve STAGE_ID;
this avoids requesting approval while npm's automated review is still running.
Complete the browser authentication, then verify that npm's version, integrity
and latest tag match the tested archive. Retain the proof and stage receipt.