Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

identity: example of tsdoc rendered in to README #1598

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ codegen: ## Re-generate packages from lexicon/ files
# clean up codegen output
pnpm format

.PHONY: docs
docs: ## Re-generate API documentation (eg, READMEs)
pnpm run --filter @atproto/identity docs
pnpm prettier --write packages/identity/API.md

.PHONY: lint
lint: ## Run style checks and verify syntax
pnpm verify
Expand Down
157 changes: 157 additions & 0 deletions packages/identity/API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# @atproto/identity API Documentation

<!-- TSDOC_START -->

## :toolbox: Functions

- [getDid](#gear-getdid)
- [getKey](#gear-getkey)
- [getHandle](#gear-gethandle)
- [getPds](#gear-getpds)
- [getFeedGen](#gear-getfeedgen)
- [getNotif](#gear-getnotif)
- [parseToAtprotoDocument](#gear-parsetoatprotodocument)
- [ensureAtpDocument](#gear-ensureatpdocument)

### :gear: getDid

| Function | Type |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getDid` | `(doc: { id?: string; alsoKnownAs?: string[]; verificationMethod?: { id?: string; type?: string; controller?: string; publicKeyMultibase?: string; }[]; service?: { id?: string; type?: string; serviceEndpoint?: string or Record<...>; }[]; }) => string` |

### :gear: getKey

| Function | Type |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getKey` | `(doc: { id?: string; alsoKnownAs?: string[]; verificationMethod?: { id?: string; type?: string; controller?: string; publicKeyMultibase?: string; }[]; service?: { id?: string; type?: string; serviceEndpoint?: string or Record<...>; }[]; }) => string` |

### :gear: getHandle

| Function | Type |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getHandle` | `(doc: { id?: string; alsoKnownAs?: string[]; verificationMethod?: { id?: string; type?: string; controller?: string; publicKeyMultibase?: string; }[]; service?: { id?: string; type?: string; serviceEndpoint?: string or Record<...>; }[]; }) => string` |

### :gear: getPds

| Function | Type |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getPds` | `(doc: { id?: string; alsoKnownAs?: string[]; verificationMethod?: { id?: string; type?: string; controller?: string; publicKeyMultibase?: string; }[]; service?: { id?: string; type?: string; serviceEndpoint?: string or Record<...>; }[]; }) => string` |

### :gear: getFeedGen

| Function | Type |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getFeedGen` | `(doc: { id?: string; alsoKnownAs?: string[]; verificationMethod?: { id?: string; type?: string; controller?: string; publicKeyMultibase?: string; }[]; service?: { id?: string; type?: string; serviceEndpoint?: string or Record<...>; }[]; }) => string` |

### :gear: getNotif

| Function | Type |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getNotif` | `(doc: { id?: string; alsoKnownAs?: string[]; verificationMethod?: { id?: string; type?: string; controller?: string; publicKeyMultibase?: string; }[]; service?: { id?: string; type?: string; serviceEndpoint?: string or Record<...>; }[]; }) => string` |

### :gear: parseToAtprotoDocument

| Function | Type |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `parseToAtprotoDocument` | `(doc: { id?: string; alsoKnownAs?: string[]; verificationMethod?: { id?: string; type?: string; controller?: string; publicKeyMultibase?: string; }[]; service?: { id?: string; type?: string; serviceEndpoint?: string or Record<...>; }[]; }) => Partial<...>` |

### :gear: ensureAtpDocument

| Function | Type |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ensureAtpDocument` | `(doc: { id?: string; alsoKnownAs?: string[]; verificationMethod?: { id?: string; type?: string; controller?: string; publicKeyMultibase?: string; }[]; service?: { id?: string; type?: string; serviceEndpoint?: string or Record<...>; }[]; }) => AtprotoData` |

## :wrench: Constants

- [verificationMethod](#gear-verificationmethod)
- [service](#gear-service)
- [didDocument](#gear-diddocument)

### :gear: verificationMethod

| Constant | Type |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `verificationMethod` | `ZodObject<{ id: ZodString; type: ZodString; controller: ZodString; publicKeyMultibase: ZodOptional<ZodString>; }, "strip", ZodTypeAny, { ...; }, { ...; }>` |

### :gear: service

| Constant | Type |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `service` | `ZodObject<{ id: ZodString; type: ZodString; serviceEndpoint: ZodUnion<[ZodString, ZodRecord<ZodString, ZodUnknown>]>; }, "strip", ZodTypeAny, { ...; }, { ...; }>` |

### :gear: didDocument

| Constant | Type |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `didDocument` | `ZodObject<{ id: ZodString; alsoKnownAs: ZodOptional<ZodArray<ZodString, "many">>; verificationMethod: ZodOptional<ZodArray<ZodObject<{ id: ZodString; type: ZodString; controller: ZodString; publicKeyMultibase: ZodOptional<...>; }, "strip", ZodTypeAny, { ...; }, { ...; }>, "many">>; service: ZodOptional<...>; }, "str...` |

## :factory: DidNotFoundError

## :factory: PoorlyFormattedDidError

## :factory: UnsupportedDidMethodError

## :factory: PoorlyFormattedDidDocumentError

## :factory: UnsupportedDidWebPathError

## :factory: HandleResolver

Resolves a handle (domain name) to a DID.

Calling code must validate handle/DID pariing against the DID document itself.

### Methods

- [parseDnsResult](#gear-parsednsresult)

#### :gear: parseDnsResult

| Method | Type |
| ---------------- | ---------------------------------------- |
| `parseDnsResult` | `(chunkedResults: string[][]) => string` |

## :factory: BaseResolver

### Methods

- [validateDidDoc](#gear-validatediddoc)

#### :gear: validateDidDoc

Throws if argument is not a valid DidDocument.

Only checks type structure, does not parse internal fields.

| Method | Type |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `validateDidDoc` | `(did: string, val: unknown) => { id?: string; alsoKnownAs?: string[]; verificationMethod?: { id?: string; type?: string; controller?: string; publicKeyMultibase?: string; }[]; service?: { id?: string; type?: string; serviceEndpoint?: string or Record<...>; }[]; }` |

Parameters:

- `did`: - DID to verify against field in the document itself
- `val`: - object to verify structure as a DidDocument

## :factory: DidWebResolver

Resolves did:web DIDs.

Supports only top-level (domain) DIDs, not paths (with additional ":" segments in the DID). UnsupportedDidWebPathError will be thrown if path segments are detected.

## :factory: DidPlcResolver

## :factory: DidResolver

## :factory: IdResolver

Wrapper of a DID resolver and handle resolver.

Calling code is responsible for cross-validate handle/DID pairing.

## :factory: MemoryCache

Tiered in-memory cache.

Entries older than maxTTL are considered invalid and not returned. Entries older than staleTTL are returned, marked as stale.

<!-- TSDOC_END -->
4 changes: 3 additions & 1 deletion packages/identity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"test": "jest",
"test:log": "cat test.log | pino-pretty",
"build": "node ./build.js",
"docs": "tsdoc --src=src/*,src/handle/*,src/did/* --dest=API.md",
"postbuild": "tsc --build tsconfig.build.json",
"update-main-to-dist": "node ../../update-main-to-dist.js packages/identity"
},
Expand All @@ -37,6 +38,7 @@
"@did-plc/server": "^0.0.1",
"cors": "^2.8.5",
"express": "^4.18.2",
"get-port": "^6.1.2"
"get-port": "^6.1.2",
"tsdoc-markdown": "^0.0.4"
}
}
16 changes: 16 additions & 0 deletions packages/identity/src/did/base-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ export abstract class BaseResolver {

abstract resolveNoCheck(did: string): Promise<unknown | null>

/** Throws if argument is not a valid DidDocument.
*
* Only checks type structure, does not parse internal fields.
*
* @param did - DID to verify against field in the document itself
* @param val - object to verify structure as a DidDocument
*/
validateDidDoc(did: string, val: unknown): DidDocument {
if (!check.is(val, didDocument)) {
throw new PoorlyFormattedDidDocumentError(did, val)
Expand All @@ -29,6 +36,12 @@ export abstract class BaseResolver {
await this.cache?.refreshCache(did, () => this.resolveNoCache(did))
}

/**
* Resolves DID, possibly from cached. Stale entries are refreshed; never returns a stale entry.
*
* @param forceRefresh - force update of cache
* @returns DID document if successful, null if successful not found (eg, HTTP 404), throws on resolution error
*/
async resolve(
did: string,
forceRefresh = false,
Expand All @@ -52,6 +65,7 @@ export abstract class BaseResolver {
return got
}

/** Variant of resolve() which throws if DID does not exist, instead of returning null */
async ensureResolve(did: string, forceRefresh = false): Promise<DidDocument> {
const result = await this.resolve(did, forceRefresh)
if (result === null) {
Expand All @@ -68,6 +82,7 @@ export abstract class BaseResolver {
return atprotoData.ensureAtpDocument(didDocument)
}

/** Helper to do DID resolution and extract repo public key */
async resolveAtprotoKey(did: string, forceRefresh = false): Promise<string> {
if (did.startsWith('did:key:')) {
return did
Expand All @@ -77,6 +92,7 @@ export abstract class BaseResolver {
}
}

/** Helper to do DID resolution, extract repo public key, and verify signature */
async verifySignature(
did: string,
data: Uint8Array,
Expand Down
5 changes: 5 additions & 0 deletions packages/identity/src/did/memory-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ type CacheVal = {
updatedAt: number
}

/**
* Tiered in-memory cache.
*
* Entries older than maxTTL are considered invalid and not returned. Entries older than staleTTL are returned, marked as stale.
*/
export class MemoryCache implements DidCache {
public staleTTL: number
public maxTTL: number
Expand Down
9 changes: 7 additions & 2 deletions packages/identity/src/did/web-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import BaseResolver from './base-resolver'
import { DidCache } from '../types'
import { PoorlyFormattedDidError, UnsupportedDidWebPathError } from '../errors'

export const DOC_PATH = '/.well-known/did.json'
const DIDWEB_WELLKNOWN_PATH = '/.well-known/did.json'

/**
* Resolves did:web DIDs.
*
* Supports only top-level (domain) DIDs, not paths (with additional ":" segments in the DID). UnsupportedDidWebPathError will be thrown if path segments are detected.
*/
export class DidWebResolver extends BaseResolver {
constructor(public timeout: number, public cache?: DidCache) {
super(cache)
Expand All @@ -17,7 +22,7 @@ export class DidWebResolver extends BaseResolver {
if (parts.length < 1) {
throw new PoorlyFormattedDidError(did)
} else if (parts.length === 1) {
path = parts[0] + DOC_PATH
path = parts[0] + DIDWEB_WELLKNOWN_PATH
} else {
// how we *would* resolve a did:web with path, if atproto supported it
//path = parts.join('/') + '/did.json'
Expand Down
17 changes: 17 additions & 0 deletions packages/identity/src/handle/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@ import { HandleResolverOpts } from '../types'
const SUBDOMAIN = '_atproto'
const PREFIX = 'did='

/**
* Resolves a handle (domain name) to a DID.
*
* Calling code must validate handle/DID pariing against the DID document itself.
*
* @link https://atproto.com/specs/handle#handle-resolution
*/
export class HandleResolver {
/**
* Resolution process timeout in miliseconds.
*
* TODO: not actually implemented for either resolution method?
*/
public timeout: number
private backupNameservers: string[] | undefined
private backupNameserverIps: string[] | undefined
Expand All @@ -14,6 +26,11 @@ export class HandleResolver {
this.backupNameservers = opts.backupNameservers
}

/**
* Attempts DNS and HTTP resolution in parallel.
*
* Returns the DNS result first, HTTP if it fails or is not found, and falls back to DNS backup (if configured).
*/
async resolve(handle: string): Promise<string | undefined> {
const dnsPromise = this.resolveDns(handle)
const httpAbort = new AbortController()
Expand Down
5 changes: 5 additions & 0 deletions packages/identity/src/id-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { HandleResolver } from './handle'
import DidResolver from './did/did-resolver'
import { IdentityResolverOpts } from './types'

/**
* Wrapper of a DID resolver and handle resolver.
*
* Calling code is responsible for cross-validate handle/DID pairing.
*/
export class IdResolver {
public handle: HandleResolver
public did: DidResolver
Expand Down
12 changes: 12 additions & 0 deletions packages/identity/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import * as z from 'zod'

export type IdentityResolverOpts = {
/** Resolution timeout in miliseconds */
timeout?: number
plcUrl?: string
didCache?: DidCache
backupNameservers?: string[]
}

export type HandleResolverOpts = {
/** Resolution timeout in miliseconds */
timeout?: number
backupNameservers?: string[]
}

export type DidResolverOpts = {
/** Resolution timeout in miliseconds */
timeout?: number
plcUrl?: string
didCache?: DidCache
}

export type AtprotoData = {
did: string
/** Public key of repo signing key, as multibase (as included in the DID document) */
signingKey: string
handle: string
pds: string
Expand All @@ -33,13 +37,16 @@ export type CacheResult = {
}

export interface DidCache {
/** Inserts a single entry to the cache */
cacheDid(did: string, doc: DidDocument): Promise<void>
checkCache(did: string): Promise<CacheResult | null>
refreshCache(
did: string,
getDoc: () => Promise<DidDocument | null>,
): Promise<void>
/** Removes a single entry from the cache */
clearEntry(did: string): Promise<void>
/** Removes all entries from the cache */
clear(): Promise<void>
}

Expand All @@ -63,4 +70,9 @@ export const didDocument = z.object({
service: z.array(service).optional(),
})

/**
* Represents the subset of DID Document format used by atproto
*
* @link https://www.w3.org/TR/did-core/
*/
export type DidDocument = z.infer<typeof didDocument>
Loading
Loading