Skip to content
LogoLogo

Credential-gated assets

Some assets require you to prove something about yourself before you can access them — that you hold a particular verifiable credential, issued by someone the publisher trusts. This guide covers both sides: consuming a gated asset, and publishing one.

This is new in nautilus v2. It did not exist in v1, because the stack it depends on did not.

How the pieces fit

Four components are involved, and they form a deliberate loop so that the node observes every credential exchange:

Loading diagram...

The loop is deliberate: every credential exchange passes through the node, so it observes the result rather than taking the client's word for it.

nautilus only ever talks to two of them directly: ocean-node, through its policy-server passthrough endpoint, and the walt.id wallet, to actually present credentials. Everything else happens between the services.

What nautilus needs out of the exchange is one thing: a verifier session id. That is the only value ocean-node wants when you later download or run compute.

Consuming a gated asset

Configure a credential provider once. Nothing changes at the call site — access and compute handle the exchange themselves.

import { ,  } from 'ethers'
import { ,  } from '@deltadao/nautilus'
 
const  = new ('0x...', new ('https://rpc.dev.pontus-x.eu'))
const  = { : 'https://node.example.org' }
 
// The provider needs a node client, so build the instance in two steps.
const  = await .(, {  })
 
const  = await .(, {
  ,
  : new (.(), { 
    : 'https://wallet.example.org'
  }) 
})
 
// Unchanged — the credential exchange happens inside.
const {  } = await .({ : 'did:ope:1234abcd...' })

What happens, in order

Loading diagram...
  1. nautilus asks the node to start a verification for this asset, service and consumer.
  2. If the node says verification already succeeded — or the asset carries no credential policy at all — it stops there.
  3. Otherwise it fetches the presentation definition, authenticates to your walt.id wallet with your Ethereum key, finds credentials matching the definition, and presents them.
  4. The resulting session id is carried into the download or compute call.

Choosing credentials and DIDs

By default the provider is headless: it presents every matching credential and uses the wallet's first DID. That is what makes it usable from a script or CI with no interaction.

For an interactive application, supply the selection callbacks instead:

const  = new (.(), {
  : 'https://wallet.example.org',
 
  // Ask the user which of the matching credentials to present.
  : async () => { 
    return .(() => . === 'a-chosen-credential') 
  }, 
 
  // Ask the user which holder DID to present as.
  : async () => [0]. 
})

You can also pin a wallet and DID outright with walletId and did.

Diagnosing a refusal

When a presentation is rejected, the reason is usually a specific policy — not a general "access denied". explainFailure digs it out of the verifier's report:

const  = await .('a-session-id') 
'revoked-status-list: credential is revoked'

If the wallet holds nothing that matches, the thrown error carries the credentials you are missing, rather than just reporting no match.

Reusing a session you already hold

If a session was established elsewhere — in a browser, say — replay it instead of repeating the exchange:

const  = new ('an-existing-session-id') 

Publishing a gated asset

Declare which credentials you require, and the checks they must pass:


  .(., [ 
    { : 'VerifiableId', : 'jwt_vc_json' } 
  ]) 
  .(., [ 
    'signature', 
    'not-before', 
    'revoked-status-list'
  ]) 
  .(., [ 
    'holder-binding', 
    { : 'minimum-credentials', : '1' } 
  ]) 

VC policies check each presented credential; VP policies check the presentation as a whole. The policy server's defaults are signature, not-before and revoked-status-list. Valid names can be listed from the verifier's /openid4vc/policy-list endpoint.

Gating also works per service, via ServiceBuilder.addRequestCredentials — useful when an asset offers both an open preview and a restricted full dataset.

Issuing from a real DID

By default nautilus signs the DDO with your Ethereum key, using a non-standard alg: 'ETH-EIP191' header, and sets the issuer to your address. That works without any SSI setup, but the credential is not verifiable by a standard JOSE verifier.

To issue from a proper DID, sign with a walt.id wallet key:

import { ,  } from 'ethers'
import {
  ,
  ,
  ,
  
} from '@deltadao/nautilus'
 
const  = new ('0x...', new ('https://rpc.dev.pontus-x.eu'))
const  = { : 'https://node.example.org' }
const  = await .(, {  })
 
const  = new ({ : 'https://wallet.example.org' })
const  = await .()
const [] = await .(.)
const [] = await .(., .)
const [] = await .(., .)
 
const  = await .(, {
  ,
  : new (.()),
  : new ({ 
    , 
    : ., 
    : .., 
    : ., 
    : . 
  }) 
})

Two things to know about deployments

A node without a policy server allows everything. ocean-node fails open when its POLICY_SERVER_URL is unset. nautilus detects this — initializePSVerification returns null — and continues without SSI rather than failing. Do not assume a gated asset is enforced without checking how the node is configured.

Publish-time enforcement does not exist yet. The policy server's newDDO, updateDDO, validateDDO, encrypt and decrypt actions are stubs that always allow. Only download and startCompute are really checked.

Swapping out walt.id

The walt.id endpoints nautilus uses (/wallet-api/*, /openid4vc/*) are the v1 generation, which walt.id has marked for deprecation in favour of *-api2 (OID4VCI/VP 1.0).

nautilus keeps them behind a narrow eleven-method WaltIdWallet interface for exactly this reason: a v2 backend is a new implementation of that interface, not a rewrite. Pass your own with the wallet option.

Try it

The examples cover both halves of the exchange. Set SSI_WALLET_API in .env first:

npm start -- ssi:connect                 # authenticate, list your wallets, keys and DIDs
npm start -- ssi:round-trip              # publish a gated dataset and consume it, end to end
npm start -- ssi:consume did:ope:…       # consume someone else's gated asset