Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 38 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,13 @@ const doc = await resolver.resolve('did:ethr:0xF3beAC30C498D9E26865F34fCAa57dBB9

## Caching

Resolving DID Documents can be expensive. It is in most cases best to cache DID documents. Caching has to be
specifically enabled using the `cache` parameter
Resolving DID Documents can be expensive. It is in most cases best to cache DID documents. Caching is controlled via the `cache` option, which accepts a boolean value or a custom `DIDCache` function.

The built-in cache uses a Map, but does not have an automatic TTL, so entries don't expire. This is fine in most web,
mobile and serverless contexts. If you run a long-running process you may want to use an existing configurable caching
system.
### Built-in caching

The built-in Cache can be enabled by passing in a `true` value to the constructor:
The built-in cache uses a `Map` and does not have an automatic TTL, so entries don't expire. This is fine in most web, mobile and serverless contexts. If you run a long-running process, consider using a custom cache with expiration.

Enable the built-in cache by passing `cache: true` to the constructor:

```js
const resolver = new DIDResolver({
Expand All @@ -104,17 +103,44 @@ const resolver = new DIDResolver({
})
```

Here is an example using `js-cache` which has not been tested.
### Disabling cache

To disable caching entirely, pass `cache: false`:

```js
const resolver = new DIDResolver({
ethr,
web
}, {
cache: false
})
```

### Per-call cache control

You can override the global cache setting on a per-call basis using the `cache` option in `DIDResolutionOptions`:

```js
// Global cache enabled, but disable for this specific resolution
const doc = await resolver.resolve('did:ethr:0xabcd...', { cache: false })

// Global cache disabled, but enable for this specific resolution
const doc = await resolver.resolve('did:ethr:0xabcd...', { cache: true })
```

### Custom cache implementation

For advanced use cases, implement a custom `DIDCache` function. It receives the parsed DID, a resolve function, and optional resolution options:

```js
var cache = require('js-cache')
const customCache : DIDCache = (parsed, resolve) => {
// DID spec requires to not cache if no-cache param is set
if (parsed.params && parsed.params['no-cache'] === 'true') return await resolve()
const customCache: DIDCache = async (parsed, resolve, options) => {
// Respect per-call cache control
if (options?.cache === false) return await resolve()

const cached = cache.get(parsed.didUrl)
if (cached !== undefined) return cached
const doc = await resolve()
cache.set(parsed, doc, 60000)
cache.set(parsed.didUrl, doc, 60000)
return doc
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "did-resolver",
"version": "5.0.1",
"version": "6.0.0",
"description": "Resolve DID documents",
"source": "src/resolver.ts",
"main": "./lib/resolver.cjs",
Expand Down
215 changes: 133 additions & 82 deletions src/__tests__/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { vi, describe, it, expect, beforeAll } from 'vitest'
import { Resolver, parse, DIDResolutionResult } from '../resolver'
import { vi, describe, it, expect, beforeAll, Mock } from 'vitest'
import { Resolver, parse, DIDResolver, DIDResolutionResult } from '../resolver'

describe('resolver', () => {
describe('parse()', () => {
Expand Down Expand Up @@ -52,40 +52,6 @@ describe('resolver', () => {
did: 'did:nacl:Md8JiMIwsapml_FtQ2ngnGftNP5UmVCAUuhnLyAsPxI',
didUrl: 'did:nacl:Md8JiMIwsapml_FtQ2ngnGftNP5UmVCAUuhnLyAsPxI',
})
expect(parse('did:example:21tDAKCERh95uGgKbJNHYp;service=agent;foo:bar=high')).toEqual({
method: 'example',
id: '21tDAKCERh95uGgKbJNHYp',
did: 'did:example:21tDAKCERh95uGgKbJNHYp',
didUrl: 'did:example:21tDAKCERh95uGgKbJNHYp;service=agent;foo:bar=high',
params: {
service: 'agent',
'foo:bar': 'high',
},
})
expect(parse('did:example:21tDAKCERh95uGgKbJNHYp;service=agent;foo:bar=high?foo=bar')).toEqual({
method: 'example',
id: '21tDAKCERh95uGgKbJNHYp',
didUrl: 'did:example:21tDAKCERh95uGgKbJNHYp;service=agent;foo:bar=high?foo=bar',
did: 'did:example:21tDAKCERh95uGgKbJNHYp',
query: 'foo=bar',
params: {
service: 'agent',
'foo:bar': 'high',
},
})
expect(parse('did:example:21tDAKCERh95uGgKbJNHYp;service=agent;foo:bar=high/some/path?foo=bar#key1')).toEqual({
method: 'example',
id: '21tDAKCERh95uGgKbJNHYp',
didUrl: 'did:example:21tDAKCERh95uGgKbJNHYp;service=agent;foo:bar=high/some/path?foo=bar#key1',
did: 'did:example:21tDAKCERh95uGgKbJNHYp',
query: 'foo=bar',
path: '/some/path',
fragment: 'key1',
params: {
service: 'agent',
'foo:bar': 'high',
},
})
expect(parse('did:web:example.com%3A8443')).toEqual({
method: 'web',
id: 'example.com%3A8443',
Expand All @@ -98,21 +64,6 @@ describe('resolver', () => {
didUrl: 'did:web:example.com:path:some%2Bsubpath',
did: 'did:web:example.com:path:some%2Bsubpath',
})
expect(
parse('did:example:test:21tDAKCERh95uGgKbJNHYp;service=agent;foo:bar=high/some/path?foo=bar#key1')
).toEqual({
method: 'example',
id: 'test:21tDAKCERh95uGgKbJNHYp',
didUrl: 'did:example:test:21tDAKCERh95uGgKbJNHYp;service=agent;foo:bar=high/some/path?foo=bar#key1',
did: 'did:example:test:21tDAKCERh95uGgKbJNHYp',
query: 'foo=bar',
path: '/some/path',
fragment: 'key1',
params: {
service: 'agent',
'foo:bar': 'high',
},
})
expect(parse('did:123:test::test2')).toEqual({
method: '123',
id: 'test::test2',
Expand All @@ -139,12 +90,96 @@ describe('resolver', () => {
expect(parse('did:CAP:id')).toEqual(null)
expect(parse('did:method:id::anotherid%r9')).toEqual(null)
})

it('handles query and fragment correctly', () => {
expect(parse('did:x:y?key=value')).toEqual({
method: 'x',
id: 'y',
did: 'did:x:y',
didUrl: 'did:x:y?key=value',
query: 'key=value',
})
expect(parse('did:x:y?')).toEqual({
method: 'x',
id: 'y',
did: 'did:x:y',
didUrl: 'did:x:y?',
query: '',
})
expect(parse('did:x:y#')).toEqual({
method: 'x',
id: 'y',
did: 'did:x:y',
didUrl: 'did:x:y#',
fragment: '',
})
expect(parse('did:x:y?query#fragment')).toEqual({
method: 'x',
id: 'y',
did: 'did:x:y',
didUrl: 'did:x:y?query#fragment',
query: 'query',
fragment: 'fragment',
})
})

it('supports RFC 3986 path characters', () => {
expect(parse('did:x:y/path~with-tilde')).toEqual({
method: 'x',
id: 'y',
path: '/path~with-tilde',
did: 'did:x:y',
didUrl: 'did:x:y/path~with-tilde',
})
expect(parse('did:x:y/path!$&()*+,;=')).toEqual({
method: 'x',
id: 'y',
path: '/path!$&()*+,;=',
did: 'did:x:y',
didUrl: 'did:x:y/path!$&()*+,;=',
})
expect(parse('did:x:y//double/slash')).toEqual({
method: 'x',
id: 'y',
path: '//double/slash',
did: 'did:x:y',
didUrl: 'did:x:y//double/slash',
})
})

it('supports lowercase hex in percent-encoding', () => {
expect(parse('did:x:y%3a')).toEqual({
method: 'x',
id: 'y%3a',
did: 'did:x:y%3a',
didUrl: 'did:x:y%3a',
})
expect(parse('did:x:y%2f%3apath')).toEqual({
method: 'x',
id: 'y%2f%3apath',
did: 'did:x:y%2f%3apath',
didUrl: 'did:x:y%2f%3apath',
})
})

it('allows empty idchar segments', () => {
expect(parse('did:example:id::with::empty::segments')).toEqual({
method: 'example',
id: 'id::with::empty::segments',
did: 'did:example:id::with::empty::segments',
didUrl: 'did:example:id::with::empty::segments',
})
})

it('rejects matrix parameters', () => {
expect(parse('did:example:id;param=value')).toEqual(null)
expect(parse('did:example:id;param=value/path')).toEqual(null)
})
})

describe('resolve', () => {
let resolver: Resolver
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let mockmethod: any
let mockmethod: Mock<Parameters<DIDResolver>, ReturnType<DIDResolver>>
const mockReturn = Promise.resolve({
didResolutionMetadata: { contentType: 'application/did+json' },
didDocument: {
Expand Down Expand Up @@ -461,7 +496,7 @@ describe('resolver', () => {
return expect(mockmethod).toBeCalledTimes(1)
})

it('should respect no-cache', async () => {
it('should cache identical resolutions with cache: true', async () => {
mockmethod = vi.fn().mockReturnValue(mockReturn)
resolver = new Resolver(
{
Expand All @@ -470,35 +505,23 @@ describe('resolver', () => {
{ cache: true }
)

await expect(resolver.resolve('did:mock:abcdef')).resolves.toEqual({
didResolutionMetadata: { contentType: 'application/did+json' },
didDocument: {
id: 'did:mock:abcdef',
verificationMethod: [
{
id: 'owner',
controller: '1234',
type: 'xyz',
},
],
},
didDocumentMetadata: {},
})
await expect(resolver.resolve('did:mock:abcdef;no-cache=true')).resolves.toEqual({
didResolutionMetadata: { contentType: 'application/did+json' },
didDocument: {
id: 'did:mock:abcdef',
verificationMethod: [
{
id: 'owner',
controller: '1234',
type: 'xyz',
},
],
await resolver.resolve('did:mock:abcdef')
await resolver.resolve('did:mock:abcdef')
return expect(mockmethod).toBeCalledTimes(1) // Only calls once when cache: true
})

it('should not cache with cache: false', async () => {
mockmethod = vi.fn().mockReturnValue(mockReturn)
resolver = new Resolver(
{
mock: mockmethod,
},
didDocumentMetadata: {},
})
return expect(mockmethod).toBeCalledTimes(2)
{ cache: false }
)

await resolver.resolve('did:mock:abcdef')
await resolver.resolve('did:mock:abcdef')
return expect(mockmethod).toBeCalledTimes(2) // Calls twice when cache: false
})

it('should not cache with different params', async () => {
Expand Down Expand Up @@ -568,6 +591,34 @@ describe('resolver', () => {
})
return expect(mockmethod).toBeCalledTimes(2)
})

it('should bypass cache with cache: false option', async () => {
mockmethod = vi.fn().mockReturnValue(mockReturn)
resolver = new Resolver(
{
mock: mockmethod,
},
{ cache: true }
)

await resolver.resolve('did:mock:abcdef')
await resolver.resolve('did:mock:abcdef', { cache: false })
return expect(mockmethod).toBeCalledTimes(2)
})

it('should safely ignore cache: false when global cache: false', async () => {
mockmethod = vi.fn().mockReturnValue(mockReturn)
resolver = new Resolver(
{
mock: mockmethod,
},
{ cache: false }
)

await resolver.resolve('did:mock:abcdef')
await resolver.resolve('did:mock:abcdef', { cache: false })
return expect(mockmethod).toBeCalledTimes(2)
})
})
})
})
Loading