@atproto/identity 0.5.2 → 0.5.3
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/CHANGELOG.md +16 -0
- package/package.json +17 -10
- package/jest.config.cjs +0 -21
- package/src/did/atproto-data.ts +0 -78
- package/src/did/base-resolver.ts +0 -100
- package/src/did/did-resolver.ts +0 -38
- package/src/did/index.ts +0 -5
- package/src/did/memory-cache.ts +0 -54
- package/src/did/plc-resolver.ts +0 -33
- package/src/did/util.ts +0 -15
- package/src/did/web-resolver.ts +0 -51
- package/src/errors.ts +0 -32
- package/src/handle/index.ts +0 -102
- package/src/id-resolver.ts +0 -17
- package/src/index.ts +0 -5
- package/src/types.ts +0 -53
- package/test.env +0 -2
- package/tests/did-cache.test.ts +0 -103
- package/tests/did-document.test.ts +0 -103
- package/tests/did-resolver.test.ts +0 -116
- package/tests/handle-resolver.test.ts +0 -67
- package/tests/web/db.ts +0 -63
- package/tests/web/server.ts +0 -105
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @atproto/identity
|
|
2
2
|
|
|
3
|
+
## 0.5.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add missing `@types` dependencies
|
|
8
|
+
|
|
9
|
+
- [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update TypeScript build to rely on references to composite internal projects
|
|
10
|
+
|
|
11
|
+
- [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Bundle only necessary files in the NPM tarball, including the `CHANGELOG.md` and `README.md` files (if present).
|
|
12
|
+
|
|
13
|
+
- [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build with `noImplicitAny` enabled
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [[`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07)]:
|
|
16
|
+
- @atproto/common-web@0.5.3
|
|
17
|
+
- @atproto/crypto@0.5.3
|
|
18
|
+
|
|
3
19
|
## 0.5.2
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/identity",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Library for decentralized identities in atproto using DIDs and handles",
|
|
6
6
|
"keywords": [
|
|
@@ -14,29 +14,36 @@
|
|
|
14
14
|
"url": "https://github.com/bluesky-social/atproto",
|
|
15
15
|
"directory": "packages/identity"
|
|
16
16
|
},
|
|
17
|
+
"files": [
|
|
18
|
+
"./dist",
|
|
19
|
+
"./README.md",
|
|
20
|
+
"./CHANGELOG.md"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
17
29
|
"engines": {
|
|
18
30
|
"node": ">=22"
|
|
19
31
|
},
|
|
20
32
|
"dependencies": {
|
|
21
|
-
"@atproto/
|
|
22
|
-
"@atproto/
|
|
33
|
+
"@atproto/common-web": "^0.5.3",
|
|
34
|
+
"@atproto/crypto": "^0.5.3"
|
|
23
35
|
},
|
|
24
36
|
"devDependencies": {
|
|
25
37
|
"@did-plc/lib": "^0.0.1",
|
|
26
38
|
"@did-plc/server": "^0.0.1",
|
|
27
39
|
"@jest/globals": "^30.0.0",
|
|
40
|
+
"@types/express": "^4.17.21",
|
|
41
|
+
"@types/cors": "^2.8.19",
|
|
28
42
|
"cors": "^2.8.5",
|
|
29
43
|
"express": "^4.18.2",
|
|
30
44
|
"get-port": "^6.1.2",
|
|
31
45
|
"jest": "^30.0.0"
|
|
32
46
|
},
|
|
33
|
-
"type": "module",
|
|
34
|
-
"exports": {
|
|
35
|
-
".": {
|
|
36
|
-
"types": "./dist/index.d.ts",
|
|
37
|
-
"default": "./dist/index.js"
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
47
|
"scripts": {
|
|
41
48
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
42
49
|
"test:log": "cat test.log | pino-pretty",
|
package/jest.config.cjs
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/** @type {import('jest').Config} */
|
|
2
|
-
module.exports = {
|
|
3
|
-
displayName: 'Identity',
|
|
4
|
-
transform: {
|
|
5
|
-
'^.+\\.(t|j)s$': [
|
|
6
|
-
'@swc/jest',
|
|
7
|
-
{
|
|
8
|
-
jsc: {
|
|
9
|
-
parser: { syntax: 'typescript', importAttributes: true },
|
|
10
|
-
experimental: { keepImportAttributes: true },
|
|
11
|
-
transform: {},
|
|
12
|
-
},
|
|
13
|
-
module: { type: 'es6' },
|
|
14
|
-
},
|
|
15
|
-
],
|
|
16
|
-
},
|
|
17
|
-
extensionsToTreatAsEsm: ['.ts'],
|
|
18
|
-
transformIgnorePatterns: [],
|
|
19
|
-
setupFiles: ['<rootDir>/../../test.setup.ts'],
|
|
20
|
-
moduleNameMapper: { '^(\\.\\.?\\/.+)\\.js$': ['$1.ts', '$1.js'] },
|
|
21
|
-
}
|
package/src/did/atproto-data.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getDid,
|
|
3
|
-
getFeedGenEndpoint,
|
|
4
|
-
getHandle,
|
|
5
|
-
getNotifEndpoint,
|
|
6
|
-
getPdsEndpoint,
|
|
7
|
-
getSigningKey,
|
|
8
|
-
} from '@atproto/common-web'
|
|
9
|
-
import * as crypto from '@atproto/crypto'
|
|
10
|
-
import { AtprotoData, DidDocument } from '../types.js'
|
|
11
|
-
|
|
12
|
-
export {
|
|
13
|
-
getDid,
|
|
14
|
-
getFeedGenEndpoint as getFeedGen,
|
|
15
|
-
getHandle,
|
|
16
|
-
getNotifEndpoint as getNotif,
|
|
17
|
-
getPdsEndpoint as getPds,
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const getKey = (doc: DidDocument): string | undefined => {
|
|
21
|
-
const key = getSigningKey(doc)
|
|
22
|
-
if (!key) return undefined
|
|
23
|
-
return getDidKeyFromMultibase(key)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const getDidKeyFromMultibase = (key: {
|
|
27
|
-
type: string
|
|
28
|
-
publicKeyMultibase: string
|
|
29
|
-
}): string | undefined => {
|
|
30
|
-
const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)
|
|
31
|
-
let didKey: string | undefined = undefined
|
|
32
|
-
if (key.type === 'EcdsaSecp256r1VerificationKey2019') {
|
|
33
|
-
didKey = crypto.formatDidKey(crypto.P256_JWT_ALG, keyBytes)
|
|
34
|
-
} else if (key.type === 'EcdsaSecp256k1VerificationKey2019') {
|
|
35
|
-
didKey = crypto.formatDidKey(crypto.SECP256K1_JWT_ALG, keyBytes)
|
|
36
|
-
} else if (key.type === 'Multikey') {
|
|
37
|
-
const parsed = crypto.parseMultikey(key.publicKeyMultibase)
|
|
38
|
-
didKey = crypto.formatDidKey(parsed.jwtAlg, parsed.keyBytes)
|
|
39
|
-
}
|
|
40
|
-
return didKey
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export const parseToAtprotoDocument = (
|
|
44
|
-
doc: DidDocument,
|
|
45
|
-
): Partial<AtprotoData> => {
|
|
46
|
-
const did = getDid(doc)
|
|
47
|
-
return {
|
|
48
|
-
did,
|
|
49
|
-
signingKey: getKey(doc),
|
|
50
|
-
handle: getHandle(doc),
|
|
51
|
-
pds: getPdsEndpoint(doc),
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export const ensureAtpDocument = (doc: DidDocument): AtprotoData => {
|
|
56
|
-
const { did, signingKey, handle, pds } = parseToAtprotoDocument(doc)
|
|
57
|
-
if (!did) {
|
|
58
|
-
throw new Error(`Could not parse id from doc: ${doc}`)
|
|
59
|
-
}
|
|
60
|
-
if (!signingKey) {
|
|
61
|
-
throw new Error(`Could not parse signingKey from doc: ${doc}`)
|
|
62
|
-
}
|
|
63
|
-
if (!handle) {
|
|
64
|
-
throw new Error(`Could not parse handle from doc: ${doc}`)
|
|
65
|
-
}
|
|
66
|
-
if (!pds) {
|
|
67
|
-
throw new Error(`Could not parse pds from doc: ${doc}`)
|
|
68
|
-
}
|
|
69
|
-
return { did, signingKey, handle, pds }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export const ensureAtprotoKey = (doc: DidDocument): string => {
|
|
73
|
-
const { signingKey } = parseToAtprotoDocument(doc)
|
|
74
|
-
if (!signingKey) {
|
|
75
|
-
throw new Error(`Could not parse signingKey from doc: ${doc}`)
|
|
76
|
-
}
|
|
77
|
-
return signingKey
|
|
78
|
-
}
|
package/src/did/base-resolver.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { check } from '@atproto/common-web'
|
|
2
|
-
import * as crypto from '@atproto/crypto'
|
|
3
|
-
import { DidNotFoundError, PoorlyFormattedDidDocumentError } from '../errors.js'
|
|
4
|
-
import {
|
|
5
|
-
AtprotoData,
|
|
6
|
-
CacheResult,
|
|
7
|
-
DidCache,
|
|
8
|
-
DidDocument,
|
|
9
|
-
didDocument,
|
|
10
|
-
} from '../types.js'
|
|
11
|
-
import * as atprotoData from './atproto-data.js'
|
|
12
|
-
|
|
13
|
-
export abstract class BaseResolver {
|
|
14
|
-
constructor(public cache?: DidCache) {}
|
|
15
|
-
|
|
16
|
-
abstract resolveNoCheck(did: string): Promise<unknown | null>
|
|
17
|
-
|
|
18
|
-
validateDidDoc(did: string, val: unknown): DidDocument {
|
|
19
|
-
if (!check.is(val, didDocument)) {
|
|
20
|
-
throw new PoorlyFormattedDidDocumentError(did, val)
|
|
21
|
-
}
|
|
22
|
-
if (val.id !== did) {
|
|
23
|
-
throw new PoorlyFormattedDidDocumentError(did, val)
|
|
24
|
-
}
|
|
25
|
-
return val
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async resolveNoCache(did: string): Promise<DidDocument | null> {
|
|
29
|
-
const got = await this.resolveNoCheck(did)
|
|
30
|
-
if (got === null) return null
|
|
31
|
-
return this.validateDidDoc(did, got)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async refreshCache(did: string, prevResult?: CacheResult): Promise<void> {
|
|
35
|
-
await this.cache?.refreshCache(
|
|
36
|
-
did,
|
|
37
|
-
() => this.resolveNoCache(did),
|
|
38
|
-
prevResult,
|
|
39
|
-
)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async resolve(
|
|
43
|
-
did: string,
|
|
44
|
-
forceRefresh = false,
|
|
45
|
-
): Promise<DidDocument | null> {
|
|
46
|
-
let fromCache: CacheResult | null = null
|
|
47
|
-
if (this.cache && !forceRefresh) {
|
|
48
|
-
fromCache = await this.cache.checkCache(did)
|
|
49
|
-
if (fromCache && !fromCache.expired) {
|
|
50
|
-
if (fromCache?.stale) {
|
|
51
|
-
await this.refreshCache(did, fromCache)
|
|
52
|
-
}
|
|
53
|
-
return fromCache.doc
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const got = await this.resolveNoCache(did)
|
|
58
|
-
if (got === null) {
|
|
59
|
-
await this.cache?.clearEntry(did)
|
|
60
|
-
return null
|
|
61
|
-
}
|
|
62
|
-
await this.cache?.cacheDid(did, got, fromCache ?? undefined)
|
|
63
|
-
return got
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async ensureResolve(did: string, forceRefresh = false): Promise<DidDocument> {
|
|
67
|
-
const result = await this.resolve(did, forceRefresh)
|
|
68
|
-
if (result === null) {
|
|
69
|
-
throw new DidNotFoundError(did)
|
|
70
|
-
}
|
|
71
|
-
return result
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async resolveAtprotoData(
|
|
75
|
-
did: string,
|
|
76
|
-
forceRefresh = false,
|
|
77
|
-
): Promise<AtprotoData> {
|
|
78
|
-
const didDocument = await this.ensureResolve(did, forceRefresh)
|
|
79
|
-
return atprotoData.ensureAtpDocument(didDocument)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async resolveAtprotoKey(did: string, forceRefresh = false): Promise<string> {
|
|
83
|
-
if (did.startsWith('did:key:')) {
|
|
84
|
-
return did
|
|
85
|
-
} else {
|
|
86
|
-
const didDocument = await this.ensureResolve(did, forceRefresh)
|
|
87
|
-
return atprotoData.ensureAtprotoKey(didDocument)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async verifySignature(
|
|
92
|
-
did: string,
|
|
93
|
-
data: Uint8Array,
|
|
94
|
-
sig: Uint8Array,
|
|
95
|
-
forceRefresh = false,
|
|
96
|
-
): Promise<boolean> {
|
|
97
|
-
const signingKey = await this.resolveAtprotoKey(did, forceRefresh)
|
|
98
|
-
return crypto.verifySignature(signingKey, data, sig)
|
|
99
|
-
}
|
|
100
|
-
}
|
package/src/did/did-resolver.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PoorlyFormattedDidError,
|
|
3
|
-
UnsupportedDidMethodError,
|
|
4
|
-
} from '../errors.js'
|
|
5
|
-
import { DidResolverOpts } from '../types.js'
|
|
6
|
-
import { BaseResolver } from './base-resolver.js'
|
|
7
|
-
import { DidPlcResolver } from './plc-resolver.js'
|
|
8
|
-
import { DidWebResolver } from './web-resolver.js'
|
|
9
|
-
|
|
10
|
-
export class DidResolver extends BaseResolver {
|
|
11
|
-
methods: Map<string, BaseResolver>
|
|
12
|
-
|
|
13
|
-
constructor(opts: DidResolverOpts) {
|
|
14
|
-
super(opts.didCache)
|
|
15
|
-
const { timeout = 3000, plcUrl = 'https://plc.directory' } = opts
|
|
16
|
-
// do not pass cache to sub-methods or we will be double caching
|
|
17
|
-
this.methods = new Map([
|
|
18
|
-
['plc', new DidPlcResolver(plcUrl, timeout)],
|
|
19
|
-
['web', new DidWebResolver(timeout)],
|
|
20
|
-
])
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async resolveNoCheck(did: string): Promise<unknown> {
|
|
24
|
-
if (!did.startsWith('did:')) {
|
|
25
|
-
throw new PoorlyFormattedDidError(did)
|
|
26
|
-
}
|
|
27
|
-
const methodSepIdx = did.indexOf(':', 4)
|
|
28
|
-
if (methodSepIdx === -1) {
|
|
29
|
-
throw new PoorlyFormattedDidError(did)
|
|
30
|
-
}
|
|
31
|
-
const methodName = did.slice(4, methodSepIdx)
|
|
32
|
-
const method = this.methods.get(methodName)
|
|
33
|
-
if (!method) {
|
|
34
|
-
throw new UnsupportedDidMethodError(did)
|
|
35
|
-
}
|
|
36
|
-
return method.resolveNoCheck(did)
|
|
37
|
-
}
|
|
38
|
-
}
|
package/src/did/index.ts
DELETED
package/src/did/memory-cache.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { DAY, HOUR } from '@atproto/common-web'
|
|
2
|
-
import { CacheResult, DidCache, DidDocument } from '../types.js'
|
|
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
|
-
const stale = now > val.updatedAt + this.staleTTL
|
|
39
|
-
return {
|
|
40
|
-
...val,
|
|
41
|
-
did,
|
|
42
|
-
stale,
|
|
43
|
-
expired,
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async clearEntry(did: string): Promise<void> {
|
|
48
|
-
this.cache.delete(did)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async clear(): Promise<void> {
|
|
52
|
-
this.cache.clear()
|
|
53
|
-
}
|
|
54
|
-
}
|
package/src/did/plc-resolver.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { DidCache } from '../types.js'
|
|
2
|
-
import { BaseResolver } from './base-resolver.js'
|
|
3
|
-
import { timed } from './util.js'
|
|
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
|
-
return timed(this.timeout, async (signal) => {
|
|
16
|
-
const url = new URL(`/${encodeURIComponent(did)}`, this.plcUrl)
|
|
17
|
-
const res = await fetch(url, {
|
|
18
|
-
redirect: 'error',
|
|
19
|
-
headers: { accept: 'application/did+ld+json,application/json' },
|
|
20
|
-
signal,
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
// Positively not found, versus due to e.g. network error
|
|
24
|
-
if (res.status === 404) return null
|
|
25
|
-
|
|
26
|
-
if (!res.ok) {
|
|
27
|
-
throw Object.assign(new Error(res.statusText), { status: res.status })
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return res.json()
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
}
|
package/src/did/util.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export async function timed<F extends (signal: AbortSignal) => unknown>(
|
|
2
|
-
ms: number,
|
|
3
|
-
fn: F,
|
|
4
|
-
): Promise<Awaited<ReturnType<F>>> {
|
|
5
|
-
const abortController = new AbortController()
|
|
6
|
-
const timer = setTimeout(() => abortController.abort(), ms)
|
|
7
|
-
const signal = abortController.signal
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
return (await fn(signal)) as Awaited<ReturnType<F>>
|
|
11
|
-
} finally {
|
|
12
|
-
clearTimeout(timer)
|
|
13
|
-
abortController.abort()
|
|
14
|
-
}
|
|
15
|
-
}
|
package/src/did/web-resolver.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PoorlyFormattedDidError,
|
|
3
|
-
UnsupportedDidWebPathError,
|
|
4
|
-
} from '../errors.js'
|
|
5
|
-
import { DidCache } from '../types.js'
|
|
6
|
-
import { BaseResolver } from './base-resolver.js'
|
|
7
|
-
import { timed } from './util.js'
|
|
8
|
-
|
|
9
|
-
export const DOC_PATH = '/.well-known/did.json'
|
|
10
|
-
|
|
11
|
-
export class DidWebResolver extends BaseResolver {
|
|
12
|
-
constructor(
|
|
13
|
-
public timeout: number,
|
|
14
|
-
public cache?: DidCache,
|
|
15
|
-
) {
|
|
16
|
-
super(cache)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async resolveNoCheck(did: string): Promise<unknown> {
|
|
20
|
-
const parsedId = did.split(':').slice(2).join(':')
|
|
21
|
-
const parts = parsedId.split(':').map(decodeURIComponent)
|
|
22
|
-
let path: string
|
|
23
|
-
if (parts.length < 1) {
|
|
24
|
-
throw new PoorlyFormattedDidError(did)
|
|
25
|
-
} else if (parts.length === 1) {
|
|
26
|
-
path = parts[0] + DOC_PATH
|
|
27
|
-
} else {
|
|
28
|
-
// how we *would* resolve a did:web with path, if atproto supported it
|
|
29
|
-
//path = parts.join('/') + '/did.json'
|
|
30
|
-
throw new UnsupportedDidWebPathError(did)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const url = new URL(`https://${path}`)
|
|
34
|
-
if (url.hostname === 'localhost') {
|
|
35
|
-
url.protocol = 'http'
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return timed(this.timeout, async (signal) => {
|
|
39
|
-
const res = await fetch(url, {
|
|
40
|
-
signal,
|
|
41
|
-
redirect: 'error',
|
|
42
|
-
headers: { accept: 'application/did+ld+json,application/json' },
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
// Positively not found, versus due to e.g. network error
|
|
46
|
-
if (!res.ok) return null
|
|
47
|
-
|
|
48
|
-
return res.json()
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
}
|
package/src/errors.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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(
|
|
21
|
-
public did: string,
|
|
22
|
-
public doc: unknown,
|
|
23
|
-
) {
|
|
24
|
-
super(`Poorly formatted DID Document: ${doc}`)
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class UnsupportedDidWebPathError extends Error {
|
|
29
|
-
constructor(public did: string) {
|
|
30
|
-
super(`Unsupported did:web paths: ${did}`)
|
|
31
|
-
}
|
|
32
|
-
}
|
package/src/handle/index.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import * as dns from 'node:dns/promises'
|
|
2
|
-
import { HandleResolverOpts } from '../types.js'
|
|
3
|
-
|
|
4
|
-
const SUBDOMAIN = '_atproto'
|
|
5
|
-
const PREFIX = 'did='
|
|
6
|
-
|
|
7
|
-
export class HandleResolver {
|
|
8
|
-
public timeout: number
|
|
9
|
-
private backupNameservers: string[] | undefined
|
|
10
|
-
private backupNameserverIps: string[] | undefined
|
|
11
|
-
|
|
12
|
-
constructor(opts: HandleResolverOpts = {}) {
|
|
13
|
-
this.timeout = opts.timeout ?? 3000
|
|
14
|
-
this.backupNameservers = opts.backupNameservers
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async resolve(handle: string): Promise<string | undefined> {
|
|
18
|
-
const dnsPromise = this.resolveDns(handle)
|
|
19
|
-
const httpAbort = new AbortController()
|
|
20
|
-
const httpPromise = this.resolveHttp(handle, httpAbort.signal).catch(
|
|
21
|
-
() => undefined,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
const dnsRes = await dnsPromise
|
|
25
|
-
if (dnsRes) {
|
|
26
|
-
httpAbort.abort()
|
|
27
|
-
return dnsRes
|
|
28
|
-
}
|
|
29
|
-
const res = await httpPromise
|
|
30
|
-
if (res) {
|
|
31
|
-
return res
|
|
32
|
-
}
|
|
33
|
-
return this.resolveDnsBackup(handle)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async resolveDns(handle: string): Promise<string | undefined> {
|
|
37
|
-
let chunkedResults: string[][]
|
|
38
|
-
try {
|
|
39
|
-
chunkedResults = await dns.resolveTxt(`${SUBDOMAIN}.${handle}`)
|
|
40
|
-
} catch (err) {
|
|
41
|
-
return undefined
|
|
42
|
-
}
|
|
43
|
-
return this.parseDnsResult(chunkedResults)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async resolveHttp(
|
|
47
|
-
handle: string,
|
|
48
|
-
signal?: AbortSignal,
|
|
49
|
-
): Promise<string | undefined> {
|
|
50
|
-
const url = new URL('/.well-known/atproto-did', `https://${handle}`)
|
|
51
|
-
try {
|
|
52
|
-
const res = await fetch(url, { signal })
|
|
53
|
-
const did = (await res.text()).split('\n')[0].trim()
|
|
54
|
-
if (typeof did === 'string' && did.startsWith('did:')) {
|
|
55
|
-
return did
|
|
56
|
-
}
|
|
57
|
-
return undefined
|
|
58
|
-
} catch (err) {
|
|
59
|
-
return undefined
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async resolveDnsBackup(handle: string): Promise<string | undefined> {
|
|
64
|
-
let chunkedResults: string[][]
|
|
65
|
-
try {
|
|
66
|
-
const backupIps = await this.getBackupNameserverIps()
|
|
67
|
-
if (!backupIps || backupIps.length < 1) return undefined
|
|
68
|
-
const resolver = new dns.Resolver()
|
|
69
|
-
resolver.setServers(backupIps)
|
|
70
|
-
chunkedResults = await resolver.resolveTxt(`${SUBDOMAIN}.${handle}`)
|
|
71
|
-
} catch (err) {
|
|
72
|
-
return undefined
|
|
73
|
-
}
|
|
74
|
-
return this.parseDnsResult(chunkedResults)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
parseDnsResult(chunkedResults: string[][]): string | undefined {
|
|
78
|
-
const results = chunkedResults.map((chunks) => chunks.join(''))
|
|
79
|
-
const found = results.filter((i) => i.startsWith(PREFIX))
|
|
80
|
-
if (found.length !== 1) {
|
|
81
|
-
return undefined
|
|
82
|
-
}
|
|
83
|
-
return found[0].slice(PREFIX.length)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
private async getBackupNameserverIps(): Promise<string[] | undefined> {
|
|
87
|
-
if (!this.backupNameservers) {
|
|
88
|
-
return undefined
|
|
89
|
-
} else if (!this.backupNameserverIps) {
|
|
90
|
-
const responses = await Promise.allSettled(
|
|
91
|
-
this.backupNameservers.map((h) => dns.lookup(h)),
|
|
92
|
-
)
|
|
93
|
-
for (const res of responses) {
|
|
94
|
-
if (res.status === 'fulfilled') {
|
|
95
|
-
this.backupNameserverIps ??= []
|
|
96
|
-
this.backupNameserverIps.push(res.value.address)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return this.backupNameserverIps
|
|
101
|
-
}
|
|
102
|
-
}
|
package/src/id-resolver.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { DidResolver } from './did/did-resolver.js'
|
|
2
|
-
import { HandleResolver } from './handle/index.js'
|
|
3
|
-
import { IdentityResolverOpts } from './types.js'
|
|
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({
|
|
12
|
-
timeout,
|
|
13
|
-
backupNameservers: opts.backupNameservers,
|
|
14
|
-
})
|
|
15
|
-
this.did = new DidResolver({ timeout, plcUrl, didCache })
|
|
16
|
-
}
|
|
17
|
-
}
|
package/src/index.ts
DELETED
package/src/types.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { DidDocument } from '@atproto/common-web'
|
|
2
|
-
|
|
3
|
-
export { didDocument } from '@atproto/common-web'
|
|
4
|
-
export type { DidDocument } from '@atproto/common-web'
|
|
5
|
-
|
|
6
|
-
export type IdentityResolverOpts = {
|
|
7
|
-
timeout?: number
|
|
8
|
-
plcUrl?: string
|
|
9
|
-
didCache?: DidCache
|
|
10
|
-
backupNameservers?: string[]
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type HandleResolverOpts = {
|
|
14
|
-
timeout?: number
|
|
15
|
-
backupNameservers?: string[]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type DidResolverOpts = {
|
|
19
|
-
timeout?: number
|
|
20
|
-
plcUrl?: string
|
|
21
|
-
didCache?: DidCache
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type AtprotoData = {
|
|
25
|
-
did: string
|
|
26
|
-
signingKey: string
|
|
27
|
-
handle: string
|
|
28
|
-
pds: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export type CacheResult = {
|
|
32
|
-
did: string
|
|
33
|
-
doc: DidDocument
|
|
34
|
-
updatedAt: number
|
|
35
|
-
stale: boolean
|
|
36
|
-
expired: boolean
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface DidCache {
|
|
40
|
-
cacheDid(
|
|
41
|
-
did: string,
|
|
42
|
-
doc: DidDocument,
|
|
43
|
-
prevResult?: CacheResult,
|
|
44
|
-
): Promise<void>
|
|
45
|
-
checkCache(did: string): Promise<CacheResult | null>
|
|
46
|
-
refreshCache(
|
|
47
|
-
did: string,
|
|
48
|
-
getDoc: () => Promise<DidDocument | null>,
|
|
49
|
-
prevResult?: CacheResult,
|
|
50
|
-
): Promise<void>
|
|
51
|
-
clearEntry(did: string): Promise<void>
|
|
52
|
-
clear(): Promise<void>
|
|
53
|
-
}
|
package/test.env
DELETED
package/tests/did-cache.test.ts
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import * as plc from '@did-plc/lib'
|
|
2
|
-
import { Database as DidPlcDb, PlcServer } from '@did-plc/server'
|
|
3
|
-
import getPort from 'get-port'
|
|
4
|
-
import { wait } from '@atproto/common-web'
|
|
5
|
-
import { Secp256k1Keypair } from '@atproto/crypto'
|
|
6
|
-
import { MemoryCache } from '../src/did/memory-cache.js'
|
|
7
|
-
import { DidResolver } from '../src/index.js'
|
|
8
|
-
|
|
9
|
-
describe('did cache', () => {
|
|
10
|
-
let close: () => Promise<void>
|
|
11
|
-
let plcUrl: string
|
|
12
|
-
let did: string
|
|
13
|
-
|
|
14
|
-
let didCache: MemoryCache
|
|
15
|
-
let didResolver: DidResolver
|
|
16
|
-
|
|
17
|
-
beforeAll(async () => {
|
|
18
|
-
const plcDB = DidPlcDb.mock()
|
|
19
|
-
const plcPort = await getPort()
|
|
20
|
-
const plcServer = PlcServer.create({ db: plcDB, port: plcPort })
|
|
21
|
-
await plcServer.start()
|
|
22
|
-
|
|
23
|
-
plcUrl = 'http://localhost:' + plcPort
|
|
24
|
-
|
|
25
|
-
const signingKey = await Secp256k1Keypair.create()
|
|
26
|
-
const rotationKey = await Secp256k1Keypair.create()
|
|
27
|
-
const plcClient = new plc.Client(plcUrl)
|
|
28
|
-
did = await plcClient.createDid({
|
|
29
|
-
signingKey: signingKey.did(),
|
|
30
|
-
handle: 'alice.test',
|
|
31
|
-
pds: 'https://bsky.social',
|
|
32
|
-
rotationKeys: [rotationKey.did()],
|
|
33
|
-
signer: rotationKey,
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
didCache = new MemoryCache()
|
|
37
|
-
didResolver = new DidResolver({ plcUrl, didCache })
|
|
38
|
-
|
|
39
|
-
close = async () => {
|
|
40
|
-
await plcServer.destroy()
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
afterAll(async () => {
|
|
45
|
-
await close()
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('caches dids on lookup', async () => {
|
|
49
|
-
const resolved = await didResolver.resolve(did)
|
|
50
|
-
expect(resolved?.id).toBe(did)
|
|
51
|
-
|
|
52
|
-
const cached = await didResolver.cache?.checkCache(did)
|
|
53
|
-
expect(cached?.did).toBe(did)
|
|
54
|
-
expect(cached?.doc).toEqual(resolved)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('clears cache and repopulates', async () => {
|
|
58
|
-
await didResolver.cache?.clear()
|
|
59
|
-
await didResolver.resolve(did)
|
|
60
|
-
|
|
61
|
-
const cached = await didResolver.cache?.checkCache(did)
|
|
62
|
-
expect(cached?.did).toBe(did)
|
|
63
|
-
expect(cached?.doc.id).toEqual(did)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('accurately reports stale dids & refreshes the cache', async () => {
|
|
67
|
-
const didCache = new MemoryCache(1)
|
|
68
|
-
const shortCacheResolver = new DidResolver({ plcUrl, didCache })
|
|
69
|
-
const doc = await shortCacheResolver.resolve(did)
|
|
70
|
-
|
|
71
|
-
// let's mess with the cached doc so we get something different
|
|
72
|
-
await didCache.cacheDid(did, { ...doc, id: 'did:example:alice' })
|
|
73
|
-
await wait(5)
|
|
74
|
-
|
|
75
|
-
// first check the cache & see that we have the stale value
|
|
76
|
-
const cached = await shortCacheResolver.cache?.checkCache(did)
|
|
77
|
-
expect(cached?.stale).toBe(true)
|
|
78
|
-
expect(cached?.doc.id).toEqual('did:example:alice')
|
|
79
|
-
// see that the resolver gives us the stale value while it revalidates
|
|
80
|
-
const staleGet = await shortCacheResolver.resolve(did)
|
|
81
|
-
expect(staleGet?.id).toEqual('did:example:alice')
|
|
82
|
-
|
|
83
|
-
// since it revalidated, ensure we have the new value
|
|
84
|
-
const updatedCache = await shortCacheResolver.cache?.checkCache(did)
|
|
85
|
-
expect(updatedCache?.doc.id).toEqual(did)
|
|
86
|
-
const updatedGet = await shortCacheResolver.resolve(did)
|
|
87
|
-
expect(updatedGet?.id).toEqual(did)
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('does not return expired dids & refreshes the cache', async () => {
|
|
91
|
-
const didCache = new MemoryCache(0, 1)
|
|
92
|
-
const shortExpireResolver = new DidResolver({ plcUrl, didCache })
|
|
93
|
-
const doc = await shortExpireResolver.resolve(did)
|
|
94
|
-
|
|
95
|
-
// again, we mess with the cached doc so we get something different
|
|
96
|
-
await didCache.cacheDid(did, { ...doc, id: 'did:example:alice' })
|
|
97
|
-
await wait(5)
|
|
98
|
-
|
|
99
|
-
// see that the resolver does not return expired value & instead force refreshes
|
|
100
|
-
const staleGet = await shortExpireResolver.resolve(did)
|
|
101
|
-
expect(staleGet?.id).toEqual(did)
|
|
102
|
-
})
|
|
103
|
-
})
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { DidResolver, ensureAtpDocument } from '../src/index.js'
|
|
2
|
-
|
|
3
|
-
describe('did parsing', () => {
|
|
4
|
-
it('throws on bad DID document', async () => {
|
|
5
|
-
const did = 'did:plc:yk4dd2qkboz2yv6tpubpc6co'
|
|
6
|
-
const docJson = `{
|
|
7
|
-
"ideep": "did:plc:yk4dd2qkboz2yv6tpubpc6co",
|
|
8
|
-
"blah": [
|
|
9
|
-
"https://dholms.xyz"
|
|
10
|
-
],
|
|
11
|
-
"zoot": [
|
|
12
|
-
{
|
|
13
|
-
"id": "#elsewhere",
|
|
14
|
-
"type": "EcdsaSecp256k1VerificationKey2019",
|
|
15
|
-
"controller": "did:plc:yk4dd2qkboz2yv6tpubpc6co",
|
|
16
|
-
"publicKeyMultibase": "zQYEBzXeuTM9UR3rfvNag6L3RNAs5pQZyYPsomTsgQhsxLdEgCrPTLgFna8yqCnxPpNT7DBk6Ym3dgPKNu86vt9GR"
|
|
17
|
-
}
|
|
18
|
-
],
|
|
19
|
-
"yarg": [ ]
|
|
20
|
-
}`
|
|
21
|
-
const resolver = new DidResolver({})
|
|
22
|
-
expect(() => {
|
|
23
|
-
resolver.validateDidDoc(did, JSON.parse(docJson))
|
|
24
|
-
}).toThrow()
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('parses legacy DID format, extracts atpData', async () => {
|
|
28
|
-
const did = 'did:plc:yk4dd2qkboz2yv6tpubpc6co'
|
|
29
|
-
const docJson = `{
|
|
30
|
-
"@context": [
|
|
31
|
-
"https://www.w3.org/ns/did/v1",
|
|
32
|
-
"https://w3id.org/security/suites/secp256k1-2019/v1"
|
|
33
|
-
],
|
|
34
|
-
"id": "did:plc:yk4dd2qkboz2yv6tpubpc6co",
|
|
35
|
-
"alsoKnownAs": [
|
|
36
|
-
"at://dholms.xyz"
|
|
37
|
-
],
|
|
38
|
-
"verificationMethod": [
|
|
39
|
-
{
|
|
40
|
-
"id": "#atproto",
|
|
41
|
-
"type": "EcdsaSecp256k1VerificationKey2019",
|
|
42
|
-
"controller": "did:plc:yk4dd2qkboz2yv6tpubpc6co",
|
|
43
|
-
"publicKeyMultibase": "zQYEBzXeuTM9UR3rfvNag6L3RNAs5pQZyYPsomTsgQhsxLdEgCrPTLgFna8yqCnxPpNT7DBk6Ym3dgPKNu86vt9GR"
|
|
44
|
-
}
|
|
45
|
-
],
|
|
46
|
-
"service": [
|
|
47
|
-
{
|
|
48
|
-
"id": "#atproto_pds",
|
|
49
|
-
"type": "AtprotoPersonalDataServer",
|
|
50
|
-
"serviceEndpoint": "https://bsky.social"
|
|
51
|
-
}
|
|
52
|
-
]
|
|
53
|
-
}`
|
|
54
|
-
const resolver = new DidResolver({})
|
|
55
|
-
const doc = resolver.validateDidDoc(did, JSON.parse(docJson))
|
|
56
|
-
const atpData = ensureAtpDocument(doc)
|
|
57
|
-
expect(atpData.did).toEqual(did)
|
|
58
|
-
expect(atpData.handle).toEqual('dholms.xyz')
|
|
59
|
-
expect(atpData.pds).toEqual('https://bsky.social')
|
|
60
|
-
expect(atpData.signingKey).toEqual(
|
|
61
|
-
'did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF',
|
|
62
|
-
)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('parses newer Multikey DID format, extracts atpData', async () => {
|
|
66
|
-
const did = 'did:plc:yk4dd2qkboz2yv6tpubpc6co'
|
|
67
|
-
const docJson = `{
|
|
68
|
-
"@context": [
|
|
69
|
-
"https://www.w3.org/ns/did/v1",
|
|
70
|
-
"https://w3id.org/security/multikey/v1",
|
|
71
|
-
"https://w3id.org/security/suites/secp256k1-2019/v1"
|
|
72
|
-
],
|
|
73
|
-
"id": "did:plc:yk4dd2qkboz2yv6tpubpc6co",
|
|
74
|
-
"alsoKnownAs": [
|
|
75
|
-
"at://dholms.xyz"
|
|
76
|
-
],
|
|
77
|
-
"verificationMethod": [
|
|
78
|
-
{
|
|
79
|
-
"id": "did:plc:yk4dd2qkboz2yv6tpubpc6co#atproto",
|
|
80
|
-
"type": "Multikey",
|
|
81
|
-
"controller": "did:plc:yk4dd2qkboz2yv6tpubpc6co",
|
|
82
|
-
"publicKeyMultibase": "zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF"
|
|
83
|
-
}
|
|
84
|
-
],
|
|
85
|
-
"service": [
|
|
86
|
-
{
|
|
87
|
-
"id": "#atproto_pds",
|
|
88
|
-
"type": "AtprotoPersonalDataServer",
|
|
89
|
-
"serviceEndpoint": "https://bsky.social"
|
|
90
|
-
}
|
|
91
|
-
]
|
|
92
|
-
}`
|
|
93
|
-
const resolver = new DidResolver({})
|
|
94
|
-
const doc = resolver.validateDidDoc(did, JSON.parse(docJson))
|
|
95
|
-
const atpData = ensureAtpDocument(doc)
|
|
96
|
-
expect(atpData.did).toEqual(did)
|
|
97
|
-
expect(atpData.handle).toEqual('dholms.xyz')
|
|
98
|
-
expect(atpData.pds).toEqual('https://bsky.social')
|
|
99
|
-
expect(atpData.signingKey).toEqual(
|
|
100
|
-
'did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF',
|
|
101
|
-
)
|
|
102
|
-
})
|
|
103
|
-
})
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import * as plc from '@did-plc/lib'
|
|
2
|
-
import { Database as DidPlcDb, PlcServer } from '@did-plc/server'
|
|
3
|
-
import getPort from 'get-port'
|
|
4
|
-
import { Secp256k1Keypair } from '@atproto/crypto'
|
|
5
|
-
import { DidDocument, DidResolver } from '../src/index.js'
|
|
6
|
-
import { DidWebDb } from './web/db.js'
|
|
7
|
-
import { DidWebServer } from './web/server.js'
|
|
8
|
-
|
|
9
|
-
describe('did resolver', () => {
|
|
10
|
-
let close: () => Promise<void>
|
|
11
|
-
let webServer: DidWebServer
|
|
12
|
-
let plcUrl: string
|
|
13
|
-
let resolver: DidResolver
|
|
14
|
-
|
|
15
|
-
beforeAll(async () => {
|
|
16
|
-
const webDb = DidWebDb.memory()
|
|
17
|
-
webServer = DidWebServer.create(webDb, await getPort())
|
|
18
|
-
await new Promise((resolve, reject) => {
|
|
19
|
-
webServer._httpServer?.on('listening', resolve)
|
|
20
|
-
webServer._httpServer?.on('error', reject)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
const plcDB = DidPlcDb.mock()
|
|
24
|
-
const plcPort = await getPort()
|
|
25
|
-
const plcServer = PlcServer.create({ db: plcDB, port: plcPort })
|
|
26
|
-
await plcServer.start()
|
|
27
|
-
|
|
28
|
-
plcUrl = 'http://localhost:' + plcPort
|
|
29
|
-
resolver = new DidResolver({ plcUrl })
|
|
30
|
-
|
|
31
|
-
close = async () => {
|
|
32
|
-
await webServer.close()
|
|
33
|
-
await plcServer.destroy()
|
|
34
|
-
}
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
afterAll(async () => {
|
|
38
|
-
await close()
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
const handle = 'alice.test'
|
|
42
|
-
const pds = 'https://service.test'
|
|
43
|
-
let signingKey: Secp256k1Keypair
|
|
44
|
-
let rotationKey: Secp256k1Keypair
|
|
45
|
-
let webDid: string
|
|
46
|
-
let plcDid: string
|
|
47
|
-
let didWebDoc: DidDocument
|
|
48
|
-
let didPlcDoc: DidDocument
|
|
49
|
-
|
|
50
|
-
it('creates the did on did:web & did:plc', async () => {
|
|
51
|
-
signingKey = await Secp256k1Keypair.create()
|
|
52
|
-
rotationKey = await Secp256k1Keypair.create()
|
|
53
|
-
const client = new plc.Client(plcUrl)
|
|
54
|
-
plcDid = await client.createDid({
|
|
55
|
-
signingKey: signingKey.did(),
|
|
56
|
-
handle,
|
|
57
|
-
pds,
|
|
58
|
-
rotationKeys: [rotationKey.did()],
|
|
59
|
-
signer: rotationKey,
|
|
60
|
-
})
|
|
61
|
-
didPlcDoc = await client.getDocument(plcDid)
|
|
62
|
-
const domain = encodeURIComponent(`localhost:${webServer.port}`)
|
|
63
|
-
webDid = `did:web:${domain}`
|
|
64
|
-
didWebDoc = {
|
|
65
|
-
...didPlcDoc,
|
|
66
|
-
id: webDid,
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
await webServer.put(didWebDoc)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('resolve valid did:web', async () => {
|
|
73
|
-
const didRes = await resolver.ensureResolve(webDid)
|
|
74
|
-
expect(didRes).toEqual(didWebDoc)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('resolve valid atpData from did:web', async () => {
|
|
78
|
-
const atpData = await resolver.resolveAtprotoData(webDid)
|
|
79
|
-
expect(atpData.did).toEqual(webDid)
|
|
80
|
-
expect(atpData.handle).toEqual(handle)
|
|
81
|
-
expect(atpData.pds).toEqual(pds)
|
|
82
|
-
expect(atpData.signingKey).toEqual(signingKey.did())
|
|
83
|
-
expect(atpData.handle).toEqual(handle)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('throws on malformed did:webs', async () => {
|
|
87
|
-
await expect(resolver.ensureResolve(`did:web:asdf`)).rejects.toThrow()
|
|
88
|
-
await expect(resolver.ensureResolve(`did:web:`)).rejects.toThrow()
|
|
89
|
-
await expect(resolver.ensureResolve(``)).rejects.toThrow()
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('throws on did:web with path components', async () => {
|
|
93
|
-
await expect(
|
|
94
|
-
resolver.ensureResolve(`did:web:example.com:u:bob`),
|
|
95
|
-
).rejects.toThrow()
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('resolve valid did:plc', async () => {
|
|
99
|
-
const didRes = await resolver.ensureResolve(plcDid)
|
|
100
|
-
expect(didRes).toEqual(didPlcDoc)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('resolve valid atpData from did:plc', async () => {
|
|
104
|
-
const atpData = await resolver.resolveAtprotoData(plcDid)
|
|
105
|
-
expect(atpData.did).toEqual(plcDid)
|
|
106
|
-
expect(atpData.handle).toEqual(handle)
|
|
107
|
-
expect(atpData.pds).toEqual(pds)
|
|
108
|
-
expect(atpData.signingKey).toEqual(signingKey.did())
|
|
109
|
-
expect(atpData.handle).toEqual(handle)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('throws on malformed did:plc', async () => {
|
|
113
|
-
await expect(resolver.ensureResolve(`did:plc:asdf`)).rejects.toThrow()
|
|
114
|
-
await expect(resolver.ensureResolve(`did:plc`)).rejects.toThrow()
|
|
115
|
-
})
|
|
116
|
-
})
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { jest } from '@jest/globals'
|
|
2
|
-
|
|
3
|
-
jest.unstable_mockModule('node:dns/promises', () => {
|
|
4
|
-
return {
|
|
5
|
-
resolveTxt: (handle: string) => {
|
|
6
|
-
if (handle === '_atproto.simple.test') {
|
|
7
|
-
return [['did=did:example:simpleDid']]
|
|
8
|
-
}
|
|
9
|
-
if (handle === '_atproto.noisy.test') {
|
|
10
|
-
return [
|
|
11
|
-
['blah blah blah'],
|
|
12
|
-
['did:example:fakeDid'],
|
|
13
|
-
['atproto=did:example:fakeDid'],
|
|
14
|
-
['did=did:example:noisyDid'],
|
|
15
|
-
[
|
|
16
|
-
'chunk long domain aspdfoiuwerpoaisdfupasodfiuaspdfoiuasdpfoiausdfpaosidfuaspodifuaspdfoiuasdpfoiasudfpasodifuaspdofiuaspdfoiuasd',
|
|
17
|
-
'apsodfiuweproiasudfpoasidfu',
|
|
18
|
-
],
|
|
19
|
-
]
|
|
20
|
-
}
|
|
21
|
-
if (handle === '_atproto.bad.test') {
|
|
22
|
-
return [
|
|
23
|
-
['blah blah blah'],
|
|
24
|
-
['did:example:fakeDid'],
|
|
25
|
-
['atproto=did:example:fakeDid'],
|
|
26
|
-
[
|
|
27
|
-
'chunk long domain aspdfoiuwerpoaisdfupasodfiuaspdfoiuasdpfoiausdfpaosidfuaspodifuaspdfoiuasdpfoiasudfpasodifuaspdofiuaspdfoiuasd',
|
|
28
|
-
'apsodfiuweproiasudfpoasidfu',
|
|
29
|
-
],
|
|
30
|
-
]
|
|
31
|
-
}
|
|
32
|
-
if (handle === '_atproto.multi.test') {
|
|
33
|
-
return [['did=did:example:firstDid'], ['did=did:example:secondDid']]
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
const { HandleResolver } = await import('../src/index.js')
|
|
40
|
-
|
|
41
|
-
describe('handle resolver', () => {
|
|
42
|
-
let resolver: InstanceType<typeof HandleResolver>
|
|
43
|
-
|
|
44
|
-
beforeAll(async () => {
|
|
45
|
-
resolver = new HandleResolver()
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('handles a simple DNS resolution', async () => {
|
|
49
|
-
const did = await resolver.resolveDns('simple.test')
|
|
50
|
-
expect(did).toBe('did:example:simpleDid')
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('handles a noisy DNS resolution', async () => {
|
|
54
|
-
const did = await resolver.resolveDns('noisy.test')
|
|
55
|
-
expect(did).toBe('did:example:noisyDid')
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('handles a bad DNS resolution', async () => {
|
|
59
|
-
const did = await resolver.resolveDns('bad.test')
|
|
60
|
-
expect(did).toBeUndefined()
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('throws on multiple dids under same domain', async () => {
|
|
64
|
-
const did = await resolver.resolveDns('multi.test')
|
|
65
|
-
expect(did).toBeUndefined()
|
|
66
|
-
})
|
|
67
|
-
})
|
package/tests/web/db.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { DidDocument } from '../../src/types.js'
|
|
2
|
-
|
|
3
|
-
interface DidStore {
|
|
4
|
-
put(key: string, val: string): Promise<void>
|
|
5
|
-
del(key: string): Promise<void>
|
|
6
|
-
get(key: string): Promise<string>
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
class MemoryStore implements DidStore {
|
|
10
|
-
private store: Record<string, string> = {}
|
|
11
|
-
|
|
12
|
-
async put(key: string, val: string): Promise<void> {
|
|
13
|
-
this.store[key] = val
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async del(key: string): Promise<void> {
|
|
17
|
-
this.assertHas(key)
|
|
18
|
-
delete this.store[key]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async get(key: string): Promise<string> {
|
|
22
|
-
this.assertHas(key)
|
|
23
|
-
return this.store[key]
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
assertHas(key: string): void {
|
|
27
|
-
if (!this.store[key]) {
|
|
28
|
-
throw new Error(`No object with key: ${key}`)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export class DidWebDb {
|
|
34
|
-
constructor(private store: DidStore) {}
|
|
35
|
-
|
|
36
|
-
static memory(): DidWebDb {
|
|
37
|
-
const store = new MemoryStore()
|
|
38
|
-
return new DidWebDb(store)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async put(didPath: string, didDoc: DidDocument): Promise<void> {
|
|
42
|
-
await this.store.put(didPath, JSON.stringify(didDoc))
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async get(didPath: string): Promise<DidDocument | null> {
|
|
46
|
-
try {
|
|
47
|
-
const got = await this.store.get(didPath)
|
|
48
|
-
return JSON.parse(got)
|
|
49
|
-
} catch (err) {
|
|
50
|
-
console.log(`Could not get did with path ${didPath}: ${err}`)
|
|
51
|
-
return null
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async has(didPath: string): Promise<boolean> {
|
|
56
|
-
const got = await this.get(didPath)
|
|
57
|
-
return got !== null
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async del(didPath: string): Promise<void> {
|
|
61
|
-
await this.store.del(didPath)
|
|
62
|
-
}
|
|
63
|
-
}
|
package/tests/web/server.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import http from 'node:http'
|
|
2
|
-
import cors from 'cors'
|
|
3
|
-
import express, { Router, json } from 'express'
|
|
4
|
-
import { DidDocument } from '../../src/index.js'
|
|
5
|
-
import { DidWebDb } from './db.js'
|
|
6
|
-
|
|
7
|
-
const DOC_PATH = '/.well-known/did.json'
|
|
8
|
-
|
|
9
|
-
const routes = Router()
|
|
10
|
-
|
|
11
|
-
// Get DID Doc
|
|
12
|
-
routes.get('/*', async (req, res) => {
|
|
13
|
-
const db = res.locals.db
|
|
14
|
-
const got = await db.get(req.url)
|
|
15
|
-
if (got === null) {
|
|
16
|
-
return res.status(404).send('Not found')
|
|
17
|
-
}
|
|
18
|
-
res.type('application/did+ld+json')
|
|
19
|
-
res.send(JSON.stringify(got))
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
// Write DID
|
|
23
|
-
routes.post('/', async (req, res) => {
|
|
24
|
-
const { didDoc } = req.body
|
|
25
|
-
if (!didDoc) {
|
|
26
|
-
return res.status(400)
|
|
27
|
-
}
|
|
28
|
-
// @TODO add in some proof
|
|
29
|
-
// @TODO validate didDoc body
|
|
30
|
-
const db = res.locals.db
|
|
31
|
-
const path = idToPath(didDoc.id)
|
|
32
|
-
await db.put(path, didDoc)
|
|
33
|
-
res.status(200).send()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
const idToPath = (id: string): string => {
|
|
37
|
-
const idp = id.split(':').slice(3)
|
|
38
|
-
let path =
|
|
39
|
-
idp.length > 0
|
|
40
|
-
? idp.map(decodeURIComponent).join('/') + '/did.json'
|
|
41
|
-
: DOC_PATH
|
|
42
|
-
|
|
43
|
-
if (!path.startsWith('/')) path = `/${path}`
|
|
44
|
-
return path
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export class DidWebServer {
|
|
48
|
-
port: number
|
|
49
|
-
private _db: DidWebDb
|
|
50
|
-
_app: express.Application
|
|
51
|
-
_httpServer: http.Server | null = null
|
|
52
|
-
|
|
53
|
-
constructor(_app: express.Application, _db: DidWebDb, port: number) {
|
|
54
|
-
this._app = _app
|
|
55
|
-
this._db = _db
|
|
56
|
-
this.port = port
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
static create(db: DidWebDb, port: number): DidWebServer {
|
|
60
|
-
const app = express()
|
|
61
|
-
|
|
62
|
-
app.use(cors())
|
|
63
|
-
app.use(json())
|
|
64
|
-
app.use((_req, res, next) => {
|
|
65
|
-
res.locals.db = db
|
|
66
|
-
next()
|
|
67
|
-
})
|
|
68
|
-
app.use('/', routes)
|
|
69
|
-
|
|
70
|
-
const server = new DidWebServer(app, db, port)
|
|
71
|
-
server._httpServer = app.listen(port)
|
|
72
|
-
return server
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async getByPath(didPath?: string): Promise<DidDocument | null> {
|
|
76
|
-
if (!didPath) return null
|
|
77
|
-
return this._db.get(didPath)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async getById(did?: string): Promise<DidDocument | null> {
|
|
81
|
-
if (!did) return null
|
|
82
|
-
const path = idToPath(did)
|
|
83
|
-
return this.getByPath(path)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async put(didDoc: DidDocument) {
|
|
87
|
-
await this._db.put(idToPath(didDoc.id), didDoc)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async delete(didOrDoc: string | DidDocument) {
|
|
91
|
-
const did = typeof didOrDoc === 'string' ? didOrDoc : didOrDoc.id
|
|
92
|
-
const path = idToPath(did)
|
|
93
|
-
await this._db.del(path)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
close(): Promise<void> {
|
|
97
|
-
return new Promise((resolve) => {
|
|
98
|
-
if (this._httpServer) {
|
|
99
|
-
this._httpServer.close(() => resolve())
|
|
100
|
-
} else {
|
|
101
|
-
resolve()
|
|
102
|
-
}
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
}
|
package/tsconfig.build.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":"7.0.0-dev.20260614.1","root":["./src/errors.ts","./src/id-resolver.ts","./src/index.ts","./src/types.ts","./src/did/atproto-data.ts","./src/did/base-resolver.ts","./src/did/did-resolver.ts","./src/did/index.ts","./src/did/memory-cache.ts","./src/did/plc-resolver.ts","./src/did/util.ts","./src/did/web-resolver.ts","./src/handle/index.ts"]}
|
package/tsconfig.json
DELETED