Fork of github.com/did-method-plc/did-method-plc
1import { check, cidForCbor } from '@atproto/common'
2import { Keypair } from '@atproto/crypto'
3import axios, { AxiosError } from 'axios'
4import {
5 atprotoOp,
6 createUpdateOp,
7 didForCreateOp,
8 tombstoneOp,
9 updateAtprotoKeyOp,
10 updateHandleOp,
11 updatePdsOp,
12 updateRotationKeysOp,
13} from './operations'
14import * as t from './types'
15
16export class Client {
17 constructor(public url: string) {}
18
19 private async makeGetReq(url: string) {
20 try {
21 const res = await axios.get(url)
22 return res.data
23 } catch (err) {
24 if (!axios.isAxiosError(err)) {
25 throw err
26 }
27 throw PlcClientError.fromAxiosError(err)
28 }
29 }
30
31 async getDocument(did: string): Promise<t.DidDocument> {
32 return await this.makeGetReq(`${this.url}/${encodeURIComponent(did)}`)
33 }
34
35 async getDocumentData(did: string): Promise<t.DocumentData> {
36 return await this.makeGetReq(`${this.url}/${encodeURIComponent(did)}/data`)
37 }
38
39 async getOperationLog(did: string): Promise<t.CompatibleOpOrTombstone[]> {
40 return await this.makeGetReq(`${this.url}/${encodeURIComponent(did)}/log`)
41 }
42
43 async getAuditableLog(did: string): Promise<t.ExportedOp[]> {
44 return await this.makeGetReq(
45 `${this.url}/${encodeURIComponent(did)}/log/audit`,
46 )
47 }
48
49 postOpUrl(did: string): string {
50 return `${this.url}/${encodeURIComponent(did)}`
51 }
52
53 async getLastOp(did: string): Promise<t.CompatibleOpOrTombstone> {
54 return await this.makeGetReq(
55 `${this.url}/${encodeURIComponent(did)}/log/last`,
56 )
57 }
58
59 async sendOperation(did: string, op: t.OpOrTombstone) {
60 try {
61 await axios.post(this.postOpUrl(did), op)
62 } catch (err) {
63 if (!axios.isAxiosError(err)) {
64 throw err
65 }
66 throw PlcClientError.fromAxiosError(err)
67 }
68 }
69
70 async export(after?: string, count?: number): Promise<t.ExportedOp[]> {
71 const url = new URL(`${this.url}/export`)
72 if (after) {
73 url.searchParams.append('after', after)
74 }
75 if (count !== undefined) {
76 url.searchParams.append('count', count.toString(10))
77 }
78 const res = await axios.get(url.toString())
79 const lines = res.data.split('\n')
80 return lines.map((l) => JSON.parse(l))
81 }
82
83 async createDid(opts: {
84 signingKey: string
85 handle: string
86 pds: string
87 rotationKeys: string[]
88 signer: Keypair
89 }): Promise<string> {
90 const op = await atprotoOp({ ...opts, prev: null })
91 const did = await didForCreateOp(op)
92 await this.sendOperation(did, op)
93 return did
94 }
95
96 async ensureLastOp(did) {
97 const lastOp = await this.getLastOp(did)
98 if (check.is(lastOp, t.def.tombstone)) {
99 throw new Error('Cannot apply op to tombstone')
100 }
101 return lastOp
102 }
103
104 async updateData(
105 did: string,
106 signer: Keypair,
107 fn: (lastOp: t.UnsignedOperation) => Omit<t.UnsignedOperation, 'prev'>,
108 ) {
109 const lastOp = await this.ensureLastOp(did)
110 const op = await createUpdateOp(lastOp, signer, fn)
111 await this.sendOperation(did, op)
112 }
113
114 async updateAtprotoKey(did: string, signer: Keypair, atprotoKey: string) {
115 const lastOp = await this.ensureLastOp(did)
116 const op = await updateAtprotoKeyOp(lastOp, signer, atprotoKey)
117 await this.sendOperation(did, op)
118 }
119
120 async updateHandle(did: string, signer: Keypair, handle: string) {
121 const lastOp = await this.ensureLastOp(did)
122 const op = await updateHandleOp(lastOp, signer, handle)
123 await this.sendOperation(did, op)
124 }
125
126 async updatePds(did: string, signer: Keypair, endpoint: string) {
127 const lastOp = await this.ensureLastOp(did)
128 const op = await updatePdsOp(lastOp, signer, endpoint)
129 await this.sendOperation(did, op)
130 }
131
132 async updateRotationKeys(did: string, signer: Keypair, keys: string[]) {
133 const lastOp = await this.ensureLastOp(did)
134 const op = await updateRotationKeysOp(lastOp, signer, keys)
135 await this.sendOperation(did, op)
136 }
137
138 async tombstone(did: string, signer: Keypair) {
139 const lastOp = await this.ensureLastOp(did)
140 const prev = await cidForCbor(lastOp)
141 const op = await tombstoneOp(prev, signer)
142 await this.sendOperation(did, op)
143 }
144
145 async health() {
146 return await this.makeGetReq(`${this.url}/_health`)
147 }
148}
149
150export class PlcClientError extends Error {
151 constructor(
152 public status: number,
153 public data: unknown,
154 public message: string,
155 ) {
156 super(message)
157 }
158
159 static fromAxiosError(err: AxiosError) {
160 return new PlcClientError(
161 err.response?.status || 500,
162 err.response?.data,
163 err.message,
164 )
165 }
166}
167
168export default Client