@atproto/identity 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/babel.config.js +3 -0
- package/build.js +23 -0
- package/dist/atp-did.d.ts +14 -0
- package/dist/atproto-data.d.ts +7 -0
- package/dist/base-resolver.d.ts +16 -0
- package/dist/did/atproto-data.d.ts +8 -0
- package/dist/did/base-resolver.d.ts +15 -0
- package/dist/did/did-resolver.d.ts +8 -0
- package/dist/did/index.d.ts +5 -0
- package/dist/did/logger.d.ts +2 -0
- package/dist/did/memory-cache.d.ts +17 -0
- package/dist/did/plc-resolver.d.ts +9 -0
- package/dist/did/web-resolver.d.ts +9 -0
- package/dist/did-cache.d.ts +8 -0
- package/dist/errors.d.ts +21 -0
- package/dist/handle/index.d.ts +8 -0
- package/dist/id-resolver.d.ts +8 -0
- package/dist/identity-resolver.d.ts +8 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +28963 -0
- package/dist/index.js.map +7 -0
- package/dist/logger.d.ts +2 -0
- package/dist/memory-cache.d.ts +18 -0
- package/dist/plc-resolver.d.ts +9 -0
- package/dist/resolver.d.ts +9 -0
- package/dist/src/atp-did.d.ts +14 -0
- package/dist/src/errors.d.ts +5 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/logger.d.ts +2 -0
- package/dist/src/plc/resolver.d.ts +6 -0
- package/dist/src/plc-resolver.d.ts +6 -0
- package/dist/src/resolver.d.ts +14 -0
- package/dist/src/web/db.d.ts +17 -0
- package/dist/src/web/resolver.d.ts +6 -0
- package/dist/src/web/server.d.ts +18 -0
- package/dist/src/web-resolver.d.ts +6 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/types.d.ts +124 -0
- package/dist/web-resolver.d.ts +10 -0
- package/jest.config.js +6 -0
- package/package.json +40 -0
- package/src/did/atproto-data.ts +120 -0
- package/src/did/base-resolver.ts +91 -0
- package/src/did/did-resolver.ts +33 -0
- package/src/did/index.ts +5 -0
- package/src/did/memory-cache.ts +55 -0
- package/src/did/plc-resolver.ts +27 -0
- package/src/did/web-resolver.ts +45 -0
- package/src/errors.ts +29 -0
- package/src/handle/index.ts +60 -0
- package/src/id-resolver.ts +14 -0
- package/src/index.ts +5 -0
- package/src/types.ts +64 -0
- package/test.env +2 -0
- package/test.log +4816 -0
- package/tests/did-cache.test.ts +103 -0
- package/tests/did-resolver.test.ts +116 -0
- package/tests/handle-resolver.test.ts +65 -0
- package/tests/web/db.ts +65 -0
- package/tests/web/server.ts +105 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +13 -0
- package/update-pkg.js +14 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as crypto from '@atproto/crypto'
|
|
2
|
+
import { check } from '@atproto/common-web'
|
|
3
|
+
import { DidCache, AtprotoData, DidDocument, didDocument } from '../types'
|
|
4
|
+
import * as atprotoData from './atproto-data'
|
|
5
|
+
import { DidNotFoundError, PoorlyFormattedDidDocumentError } from '../errors'
|
|
6
|
+
|
|
7
|
+
export abstract class BaseResolver {
|
|
8
|
+
constructor(public cache?: DidCache) {}
|
|
9
|
+
|
|
10
|
+
abstract resolveNoCheck(did: string): Promise<unknown | null>
|
|
11
|
+
|
|
12
|
+
validateDidDoc(did: string, val: unknown): DidDocument {
|
|
13
|
+
if (!check.is(val, didDocument)) {
|
|
14
|
+
throw new PoorlyFormattedDidDocumentError(did, val)
|
|
15
|
+
}
|
|
16
|
+
if (val.id !== did) {
|
|
17
|
+
throw new PoorlyFormattedDidDocumentError(did, val)
|
|
18
|
+
}
|
|
19
|
+
return val
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async resolveNoCache(did: string): Promise<DidDocument | null> {
|
|
23
|
+
const got = await this.resolveNoCheck(did)
|
|
24
|
+
if (got === null) return null
|
|
25
|
+
return this.validateDidDoc(did, got)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async refreshCache(did: string): Promise<void> {
|
|
29
|
+
await this.cache?.refreshCache(did, () => this.resolveNoCache(did))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async resolve(
|
|
33
|
+
did: string,
|
|
34
|
+
forceRefresh = false,
|
|
35
|
+
): Promise<DidDocument | null> {
|
|
36
|
+
if (this.cache && !forceRefresh) {
|
|
37
|
+
const fromCache = await this.cache.checkCache(did)
|
|
38
|
+
if (fromCache?.stale) {
|
|
39
|
+
await this.refreshCache(did)
|
|
40
|
+
}
|
|
41
|
+
if (fromCache) {
|
|
42
|
+
return fromCache.doc
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const got = await this.resolveNoCache(did)
|
|
47
|
+
if (got === null) {
|
|
48
|
+
await this.cache?.clearEntry(did)
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
await this.cache?.cacheDid(did, got)
|
|
52
|
+
return got
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async ensureResolve(did: string, forceRefresh = false): Promise<DidDocument> {
|
|
56
|
+
const result = await this.resolve(did, forceRefresh)
|
|
57
|
+
if (result === null) {
|
|
58
|
+
throw new DidNotFoundError(did)
|
|
59
|
+
}
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async resolveAtprotoData(
|
|
64
|
+
did: string,
|
|
65
|
+
forceRefresh = false,
|
|
66
|
+
): Promise<AtprotoData> {
|
|
67
|
+
const didDocument = await this.ensureResolve(did, forceRefresh)
|
|
68
|
+
return atprotoData.ensureAtpDocument(didDocument)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async resolveAtprotoKey(did: string, forceRefresh = false): Promise<string> {
|
|
72
|
+
if (did.startsWith('did:key:')) {
|
|
73
|
+
return did
|
|
74
|
+
} else {
|
|
75
|
+
const data = await this.resolveAtprotoData(did, forceRefresh)
|
|
76
|
+
return data.signingKey
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async verifySignature(
|
|
81
|
+
did: string,
|
|
82
|
+
data: Uint8Array,
|
|
83
|
+
sig: Uint8Array,
|
|
84
|
+
forceRefresh = false,
|
|
85
|
+
): Promise<boolean> {
|
|
86
|
+
const signingKey = await this.resolveAtprotoKey(did, forceRefresh)
|
|
87
|
+
return crypto.verifySignature(signingKey, data, sig)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default BaseResolver
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DidWebResolver } from './web-resolver'
|
|
2
|
+
import { DidPlcResolver } from './plc-resolver'
|
|
3
|
+
import { DidResolverOpts } from '../types'
|
|
4
|
+
import BaseResolver from './base-resolver'
|
|
5
|
+
import { PoorlyFormattedDidError, UnsupportedDidMethodError } from '../errors'
|
|
6
|
+
|
|
7
|
+
export class DidResolver extends BaseResolver {
|
|
8
|
+
methods: Record<string, BaseResolver>
|
|
9
|
+
|
|
10
|
+
constructor(opts: DidResolverOpts) {
|
|
11
|
+
super(opts.didCache)
|
|
12
|
+
const { timeout = 3000, plcUrl = 'https://plc.directory' } = opts
|
|
13
|
+
// do not pass cache to sub-methods or we will be double caching
|
|
14
|
+
this.methods = {
|
|
15
|
+
plc: new DidPlcResolver(plcUrl, timeout),
|
|
16
|
+
web: new DidWebResolver(timeout),
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async resolveNoCheck(did: string): Promise<unknown> {
|
|
21
|
+
const split = did.split(':')
|
|
22
|
+
if (split[0] !== 'did') {
|
|
23
|
+
throw new PoorlyFormattedDidError(did)
|
|
24
|
+
}
|
|
25
|
+
const method = this.methods[split[1]]
|
|
26
|
+
if (!method) {
|
|
27
|
+
throw new UnsupportedDidMethodError(did)
|
|
28
|
+
}
|
|
29
|
+
return method.resolveNoCheck(did)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default DidResolver
|
package/src/did/index.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { DAY, HOUR } from '@atproto/common-web'
|
|
2
|
+
import { DidCache, CacheResult, DidDocument } from '../types'
|
|
3
|
+
|
|
4
|
+
type CacheVal = {
|
|
5
|
+
doc: DidDocument
|
|
6
|
+
updatedAt: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class MemoryCache implements DidCache {
|
|
10
|
+
public staleTTL: number
|
|
11
|
+
public maxTTL: number
|
|
12
|
+
constructor(staleTTL?: number, maxTTL?: number) {
|
|
13
|
+
this.staleTTL = staleTTL ?? HOUR
|
|
14
|
+
this.maxTTL = maxTTL ?? DAY
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public cache: Map<string, CacheVal> = new Map()
|
|
18
|
+
|
|
19
|
+
async cacheDid(did: string, doc: DidDocument): Promise<void> {
|
|
20
|
+
this.cache.set(did, { doc, updatedAt: Date.now() })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async refreshCache(
|
|
24
|
+
did: string,
|
|
25
|
+
getDoc: () => Promise<DidDocument | null>,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const doc = await getDoc()
|
|
28
|
+
if (doc) {
|
|
29
|
+
await this.cacheDid(did, doc)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async checkCache(did: string): Promise<CacheResult | null> {
|
|
34
|
+
const val = this.cache.get(did)
|
|
35
|
+
if (!val) return null
|
|
36
|
+
const now = Date.now()
|
|
37
|
+
const expired = now > val.updatedAt + this.maxTTL
|
|
38
|
+
if (expired) return null
|
|
39
|
+
|
|
40
|
+
const stale = now > val.updatedAt + this.staleTTL
|
|
41
|
+
return {
|
|
42
|
+
...val,
|
|
43
|
+
did,
|
|
44
|
+
stale,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async clearEntry(did: string): Promise<void> {
|
|
49
|
+
this.cache.delete(did)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async clear(): Promise<void> {
|
|
53
|
+
this.cache.clear()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import axios, { AxiosError } from 'axios'
|
|
2
|
+
import BaseResolver from './base-resolver'
|
|
3
|
+
import { DidCache } from '../types'
|
|
4
|
+
|
|
5
|
+
export class DidPlcResolver extends BaseResolver {
|
|
6
|
+
constructor(
|
|
7
|
+
public plcUrl: string,
|
|
8
|
+
public timeout: number,
|
|
9
|
+
public cache?: DidCache,
|
|
10
|
+
) {
|
|
11
|
+
super(cache)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async resolveNoCheck(did: string): Promise<unknown> {
|
|
15
|
+
try {
|
|
16
|
+
const res = await axios.get(`${this.plcUrl}/${encodeURIComponent(did)}`, {
|
|
17
|
+
timeout: this.timeout,
|
|
18
|
+
})
|
|
19
|
+
return res.data
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err instanceof AxiosError && err.response?.status === 404) {
|
|
22
|
+
return null // Positively not found, versus due to e.g. network error
|
|
23
|
+
}
|
|
24
|
+
throw err
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import axios, { AxiosError } from 'axios'
|
|
2
|
+
import BaseResolver from './base-resolver'
|
|
3
|
+
import { DidCache } from '../types'
|
|
4
|
+
import { PoorlyFormattedDidError, UnsupportedDidWebPathError } from '../errors'
|
|
5
|
+
|
|
6
|
+
export const DOC_PATH = '/.well-known/did.json'
|
|
7
|
+
|
|
8
|
+
export class DidWebResolver extends BaseResolver {
|
|
9
|
+
constructor(public timeout: number, public cache?: DidCache) {
|
|
10
|
+
super(cache)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async resolveNoCheck(did: string): Promise<unknown> {
|
|
14
|
+
const parsedId = did.split(':').slice(2).join(':')
|
|
15
|
+
const parts = parsedId.split(':').map(decodeURIComponent)
|
|
16
|
+
let path: string
|
|
17
|
+
if (parts.length < 1) {
|
|
18
|
+
throw new PoorlyFormattedDidError(did)
|
|
19
|
+
} else if (parts.length === 1) {
|
|
20
|
+
path = parts[0] + DOC_PATH
|
|
21
|
+
} else {
|
|
22
|
+
// how we *would* resolve a did:web with path, if atproto supported it
|
|
23
|
+
//path = parts.join('/') + '/did.json'
|
|
24
|
+
throw new UnsupportedDidWebPathError(did)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const url = new URL(`https://${path}`)
|
|
28
|
+
if (url.hostname === 'localhost') {
|
|
29
|
+
url.protocol = 'http'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const res = await axios.get(url.toString(), {
|
|
34
|
+
responseType: 'json',
|
|
35
|
+
timeout: this.timeout,
|
|
36
|
+
})
|
|
37
|
+
return res.data
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (err instanceof AxiosError && err.response) {
|
|
40
|
+
return null // Positively not found, versus due to e.g. network error
|
|
41
|
+
}
|
|
42
|
+
throw err
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export class DidNotFoundError extends Error {
|
|
2
|
+
constructor(public did: string) {
|
|
3
|
+
super(`Could not resolve DID: ${did}`)
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class PoorlyFormattedDidError extends Error {
|
|
8
|
+
constructor(public did: string) {
|
|
9
|
+
super(`Poorly formatted DID: ${did}`)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class UnsupportedDidMethodError extends Error {
|
|
14
|
+
constructor(public did: string) {
|
|
15
|
+
super(`Unsupported DID method: ${did}`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class PoorlyFormattedDidDocumentError extends Error {
|
|
20
|
+
constructor(public did: string, public doc: unknown) {
|
|
21
|
+
super(`Poorly formatted DID Document: ${doc}`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class UnsupportedDidWebPathError extends Error {
|
|
26
|
+
constructor(public did: string) {
|
|
27
|
+
super(`Unsupported did:web paths: ${did}`)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import dns from 'dns/promises'
|
|
2
|
+
import { HandleResolverOpts } from '../types'
|
|
3
|
+
|
|
4
|
+
const SUBDOMAIN = '_atproto'
|
|
5
|
+
const PREFIX = 'did='
|
|
6
|
+
|
|
7
|
+
export class HandleResolver {
|
|
8
|
+
public timeout: number
|
|
9
|
+
|
|
10
|
+
constructor(opts: HandleResolverOpts = {}) {
|
|
11
|
+
this.timeout = opts.timeout ?? 3000
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async resolve(handle: string): Promise<string | undefined> {
|
|
15
|
+
const dnsPromise = this.resolveDns(handle)
|
|
16
|
+
const httpAbort = new AbortController()
|
|
17
|
+
const httpPromise = this.resolveHttp(handle, httpAbort.signal).catch(
|
|
18
|
+
() => undefined,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const dnsRes = await dnsPromise
|
|
22
|
+
if (dnsRes) {
|
|
23
|
+
httpAbort.abort()
|
|
24
|
+
return dnsRes
|
|
25
|
+
}
|
|
26
|
+
return httpPromise
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async resolveDns(handle: string): Promise<string | undefined> {
|
|
30
|
+
let chunkedResults: string[][]
|
|
31
|
+
try {
|
|
32
|
+
chunkedResults = await dns.resolveTxt(`${SUBDOMAIN}.${handle}`)
|
|
33
|
+
} catch (err) {
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
const results = chunkedResults.map((chunks) => chunks.join(''))
|
|
37
|
+
const found = results.filter((i) => i.startsWith(PREFIX))
|
|
38
|
+
if (found.length !== 1) {
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
return found[0].slice(PREFIX.length)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async resolveHttp(
|
|
45
|
+
handle: string,
|
|
46
|
+
signal?: AbortSignal,
|
|
47
|
+
): Promise<string | undefined> {
|
|
48
|
+
const url = new URL('/.well-known/atproto-did', `https://${handle}`)
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(url, { signal })
|
|
51
|
+
const did = await res.text()
|
|
52
|
+
if (typeof did === 'string' && did.startsWith('did:')) {
|
|
53
|
+
return did
|
|
54
|
+
}
|
|
55
|
+
return undefined
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { HandleResolver } from './handle'
|
|
2
|
+
import DidResolver from './did/did-resolver'
|
|
3
|
+
import { IdentityResolverOpts } from './types'
|
|
4
|
+
|
|
5
|
+
export class IdResolver {
|
|
6
|
+
public handle: HandleResolver
|
|
7
|
+
public did: DidResolver
|
|
8
|
+
|
|
9
|
+
constructor(opts: IdentityResolverOpts = {}) {
|
|
10
|
+
const { timeout = 3000, plcUrl, didCache } = opts
|
|
11
|
+
this.handle = new HandleResolver({ timeout })
|
|
12
|
+
this.did = new DidResolver({ timeout, plcUrl, didCache })
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as z from 'zod'
|
|
2
|
+
|
|
3
|
+
export type IdentityResolverOpts = {
|
|
4
|
+
timeout?: number
|
|
5
|
+
plcUrl?: string
|
|
6
|
+
didCache?: DidCache
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type HandleResolverOpts = {
|
|
10
|
+
timeout?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type DidResolverOpts = {
|
|
14
|
+
timeout?: number
|
|
15
|
+
plcUrl?: string
|
|
16
|
+
didCache?: DidCache
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type AtprotoData = {
|
|
20
|
+
did: string
|
|
21
|
+
signingKey: string
|
|
22
|
+
handle: string
|
|
23
|
+
pds: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type CacheResult = {
|
|
27
|
+
did: string
|
|
28
|
+
doc: DidDocument
|
|
29
|
+
updatedAt: number
|
|
30
|
+
stale: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DidCache {
|
|
34
|
+
cacheDid(did: string, doc: DidDocument): Promise<void>
|
|
35
|
+
checkCache(did: string): Promise<CacheResult | null>
|
|
36
|
+
refreshCache(
|
|
37
|
+
did: string,
|
|
38
|
+
getDoc: () => Promise<DidDocument | null>,
|
|
39
|
+
): Promise<void>
|
|
40
|
+
clearEntry(did: string): Promise<void>
|
|
41
|
+
clear(): Promise<void>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const verificationMethod = z.object({
|
|
45
|
+
id: z.string(),
|
|
46
|
+
type: z.string(),
|
|
47
|
+
controller: z.string(),
|
|
48
|
+
publicKeyMultibase: z.string().optional(),
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
export const service = z.object({
|
|
52
|
+
id: z.string(),
|
|
53
|
+
type: z.string(),
|
|
54
|
+
serviceEndpoint: z.union([z.string(), z.record(z.unknown())]),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
export const didDocument = z.object({
|
|
58
|
+
id: z.string(),
|
|
59
|
+
alsoKnownAs: z.array(z.string()).optional(),
|
|
60
|
+
verificationMethod: z.array(verificationMethod).optional(),
|
|
61
|
+
service: z.array(service).optional(),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
export type DidDocument = z.infer<typeof didDocument>
|
package/test.env
ADDED