@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.
Files changed (65) hide show
  1. package/README.md +3 -0
  2. package/babel.config.js +3 -0
  3. package/build.js +23 -0
  4. package/dist/atp-did.d.ts +14 -0
  5. package/dist/atproto-data.d.ts +7 -0
  6. package/dist/base-resolver.d.ts +16 -0
  7. package/dist/did/atproto-data.d.ts +8 -0
  8. package/dist/did/base-resolver.d.ts +15 -0
  9. package/dist/did/did-resolver.d.ts +8 -0
  10. package/dist/did/index.d.ts +5 -0
  11. package/dist/did/logger.d.ts +2 -0
  12. package/dist/did/memory-cache.d.ts +17 -0
  13. package/dist/did/plc-resolver.d.ts +9 -0
  14. package/dist/did/web-resolver.d.ts +9 -0
  15. package/dist/did-cache.d.ts +8 -0
  16. package/dist/errors.d.ts +21 -0
  17. package/dist/handle/index.d.ts +8 -0
  18. package/dist/id-resolver.d.ts +8 -0
  19. package/dist/identity-resolver.d.ts +8 -0
  20. package/dist/index.d.ts +5 -0
  21. package/dist/index.js +28963 -0
  22. package/dist/index.js.map +7 -0
  23. package/dist/logger.d.ts +2 -0
  24. package/dist/memory-cache.d.ts +18 -0
  25. package/dist/plc-resolver.d.ts +9 -0
  26. package/dist/resolver.d.ts +9 -0
  27. package/dist/src/atp-did.d.ts +14 -0
  28. package/dist/src/errors.d.ts +5 -0
  29. package/dist/src/index.d.ts +4 -0
  30. package/dist/src/logger.d.ts +2 -0
  31. package/dist/src/plc/resolver.d.ts +6 -0
  32. package/dist/src/plc-resolver.d.ts +6 -0
  33. package/dist/src/resolver.d.ts +14 -0
  34. package/dist/src/web/db.d.ts +17 -0
  35. package/dist/src/web/resolver.d.ts +6 -0
  36. package/dist/src/web/server.d.ts +18 -0
  37. package/dist/src/web-resolver.d.ts +6 -0
  38. package/dist/tsconfig.build.tsbuildinfo +1 -0
  39. package/dist/types.d.ts +124 -0
  40. package/dist/web-resolver.d.ts +10 -0
  41. package/jest.config.js +6 -0
  42. package/package.json +40 -0
  43. package/src/did/atproto-data.ts +120 -0
  44. package/src/did/base-resolver.ts +91 -0
  45. package/src/did/did-resolver.ts +33 -0
  46. package/src/did/index.ts +5 -0
  47. package/src/did/memory-cache.ts +55 -0
  48. package/src/did/plc-resolver.ts +27 -0
  49. package/src/did/web-resolver.ts +45 -0
  50. package/src/errors.ts +29 -0
  51. package/src/handle/index.ts +60 -0
  52. package/src/id-resolver.ts +14 -0
  53. package/src/index.ts +5 -0
  54. package/src/types.ts +64 -0
  55. package/test.env +2 -0
  56. package/test.log +4816 -0
  57. package/tests/did-cache.test.ts +103 -0
  58. package/tests/did-resolver.test.ts +116 -0
  59. package/tests/handle-resolver.test.ts +65 -0
  60. package/tests/web/db.ts +65 -0
  61. package/tests/web/server.ts +105 -0
  62. package/tsconfig.build.json +4 -0
  63. package/tsconfig.build.tsbuildinfo +1 -0
  64. package/tsconfig.json +13 -0
  65. 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
@@ -0,0 +1,5 @@
1
+ export * from './web-resolver'
2
+ export * from './plc-resolver'
3
+ export * from './did-resolver'
4
+ export * from './atproto-data'
5
+ export * from './memory-cache'
@@ -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
@@ -0,0 +1,5 @@
1
+ export * from './did'
2
+ export * from './handle'
3
+ export * from './id-resolver'
4
+ export * from './errors'
5
+ export * from './types'
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
@@ -0,0 +1,2 @@
1
+ LOG_ENABLED=true
2
+ LOG_DESTINATION=test.log