@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 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.2",
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/crypto": "^0.5.2",
22
- "@atproto/common-web": "^0.5.2"
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
- }
@@ -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
- }
@@ -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
- }
@@ -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
@@ -1,5 +0,0 @@
1
- export * from './web-resolver.js'
2
- export * from './plc-resolver.js'
3
- export * from './did-resolver.js'
4
- export * from './atproto-data.js'
5
- export * from './memory-cache.js'
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
@@ -1,5 +0,0 @@
1
- export * from './did/index.js'
2
- export * from './handle/index.js'
3
- export * from './id-resolver.js'
4
- export * from './errors.js'
5
- export * from './types.js'
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
@@ -1,2 +0,0 @@
1
- LOG_ENABLED=true
2
- LOG_DESTINATION=test.log
@@ -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
- }
@@ -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
- }
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig/isomorphic.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "outDir": "./dist",
6
- },
7
- "include": ["./src"],
8
- }
@@ -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
@@ -1,7 +0,0 @@
1
- {
2
- "include": [],
3
- "references": [
4
- { "path": "./tsconfig.build.json" },
5
- { "path": "./tsconfig.tests.json" },
6
- ],
7
- }
@@ -1,7 +0,0 @@
1
- {
2
- "extends": "../../tsconfig/tests.json",
3
- "compilerOptions": {
4
- "rootDir": ".",
5
- },
6
- "include": ["./tests"],
7
- }