Pds Did integration ()

* parsing dids to something we understand

* revamping did-sdk with did-resolver

* testing

* fixing some thigns & finishing tests

* fix type error & remove old test

* wip

* create did on server & add email

* check username available & store email

* better test utils

* remove did:test

* allow authstore to sign for controlled dids
This commit is contained in:
Daniel Holmgren 2022-09-28 17:36:53 -05:00 committed by GitHub
parent 6b47ad7622
commit ee68e6977b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 373 additions and 461 deletions

@ -115,42 +115,3 @@ await didWeb.resolve('did:web:alice.com') /* => {
service: [{serviceEndpoint: 'https://alice.com', ...}]
}*/
```
## Testing environments
Registering domain names and DIDs for test environments would be difficult. Instead, ADX uses the `.test` TLD and `did:test` DID to provide some special behaviors:
- Any `*.test` username resolves to the matching `did:test:*` DID.
- Any `did:test:*` DID is resolved using an internal table maintained by the test environment software.
Let's look at an example to understand this better:
- The username: `alice.test`
- The DID: `did:test:alice`
- The hosting service: `http://localhost:5283`
We can resolve the name without an XRPC request:
```typescript
resolveTestName('alice.test') // => {did: 'did:test:alice'}
```
We then configure our did:test API with the alice entry:
```typescript
didTest.set('alice', {
verificationMethod: /*...*/,
service: 'http://localhost:5283',
})
```
And then our resolutions of the test did would work as expected:
```typescript
await didTest.resolve('did:test:alice') /* => {
id: 'did:test:alice',
alsoKnownAs: `https://alice.test`,
verificationMethod: [...],
service: [{serviceEndpoint: 'http://localhost:5283', ...}]
}*/
```

@ -14,12 +14,15 @@ export const methodSchemas: MethodSchema[] = [
encoding: 'application/json',
schema: {
type: 'object',
required: ['username', 'did', 'password'],
required: ['username', 'email', 'password'],
properties: {
email: {
type: 'string',
},
username: {
type: 'string',
},
did: {
inviteCode: {
type: 'string',
},
password: {
@ -32,11 +35,17 @@ export const methodSchemas: MethodSchema[] = [
encoding: 'application/json',
schema: {
type: 'object',
required: ['jwt'],
required: ['jwt', 'username', 'did'],
properties: {
jwt: {
type: 'string',
},
username: {
type: 'string',
},
did: {
type: 'string',
},
},
},
},

@ -11,13 +11,16 @@ export interface CallOptions {
}
export interface InputSchema {
email: string;
username: string;
did: string;
inviteCode?: string;
password: string;
}
export interface OutputSchema {
jwt: string;
username: string;
did: string;
}
export interface Response {

@ -8,19 +8,14 @@ import { vaguerCap, writeCap } from './capabilities'
export class AuthStore implements Signer {
protected keypair: DidableKey
protected ucanStore: ucan.StoreI
protected ucanStore: ucan.StoreI | null = null
protected tokens: string[]
protected controlledDid: string | null
constructor(keypair: DidableKey, ucanStore: ucan.StoreI) {
constructor(keypair: DidableKey, tokens: string[], controlledDid?: string) {
this.keypair = keypair
this.ucanStore = ucanStore
}
static async fromTokens(
keypair: DidableKey,
tokens: string[],
): Promise<AuthStore> {
const ucanStore = await ucan.Store.fromTokens(adxSemantics, tokens)
return new AuthStore(keypair, ucanStore)
this.tokens = tokens
this.controlledDid = controlledDid || null
}
// Update these for sub classes
@ -31,10 +26,14 @@ export class AuthStore implements Signer {
}
async addUcan(token: ucan.Ucan): Promise<void> {
await this.ucanStore.add(token)
const ucanStore = await this.getUcanStore()
await ucanStore.add(token)
}
async getUcanStore(): Promise<ucan.StoreI> {
if (!this.ucanStore) {
this.ucanStore = await ucan.Store.fromTokens(adxSemantics, this.tokens)
}
return this.ucanStore
}
@ -48,19 +47,21 @@ export class AuthStore implements Signer {
// ----------------
async did(): Promise<string> {
async keypairDid(): Promise<string> {
const keypair = await this.getKeypair()
return keypair.did()
}
async did(): Promise<string> {
if (this.controlledDid) {
return this.controlledDid
}
return this.keypairDid()
}
async canSignForDid(did: string): Promise<boolean> {
// for dev purposes
if (did.startsWith('did:test:')) {
return true
}
if (did === (await this.did())) {
return true
}
if (did === this.controlledDid) return true
if (did === (await this.keypairDid())) return true
return false
}

@ -1,16 +1,14 @@
import * as crypto from '@adxp/crypto'
import * as ucan from './ucans'
import AuthStore from './auth-store'
import { adxSemantics } from './semantics'
export class BrowserStore extends AuthStore {
static async load(): Promise<BrowserStore> {
const keypair = await BrowserStore.loadOrCreateKeypair()
const storedUcans = BrowserStore.getStoredUcanStrs()
const ucanStore = await ucan.Store.fromTokens(adxSemantics, storedUcans)
return new BrowserStore(keypair, ucanStore)
return new BrowserStore(keypair, storedUcans)
}
static async loadOrCreateKeypair(): Promise<crypto.EcdsaKeypair> {
@ -35,8 +33,7 @@ export class BrowserStore extends AuthStore {
})
localStorage.setItem('adxKey', JSON.stringify(jwk))
const storedUcans = BrowserStore.getStoredUcanStrs()
const ucanStore = await ucan.Store.fromTokens(adxSemantics, storedUcans)
const authStore = new BrowserStore(keypair, ucanStore)
const authStore = new BrowserStore(keypair, storedUcans)
if (storedUcans.length === 0) {
// since this is the root device, we claim full authority
await authStore.claimFull()
@ -57,11 +54,8 @@ export class BrowserStore extends AuthStore {
async addUcan(token: ucan.Ucan): Promise<void> {
const storedUcans = BrowserStore.getStoredUcanStrs()
BrowserStore.setStoredUcanStrs([...storedUcans, ucan.encode(token)])
await this.ucanStore.add(token)
}
async getUcanStore(): Promise<ucan.StoreI> {
return this.ucanStore
const store = await this.getUcanStore()
await store.add(token)
}
async clear(): Promise<void> {
@ -71,7 +65,8 @@ export class BrowserStore extends AuthStore {
async reset(): Promise<void> {
this.clear()
this.keypair = await BrowserStore.loadOrCreateKeypair()
this.ucanStore = await ucan.Store.fromTokens(adxSemantics, [])
this.tokens = []
this.ucanStore = null
}
}

@ -1,19 +1,17 @@
import * as crypto from '@adxp/crypto'
import * as ucan from './ucans/index'
import AuthStore from './auth-store'
import { adxSemantics } from './semantics'
export class MemoryStore extends AuthStore {
static async load(): Promise<MemoryStore> {
const keypair = await crypto.EcdsaKeypair.create({ exportable: true })
const ucanStore = await ucan.Store.fromTokens(adxSemantics, [])
return new MemoryStore(keypair, ucanStore)
return new MemoryStore(keypair, [])
}
async reset(): Promise<void> {
this.clear()
this.keypair = await crypto.EcdsaKeypair.create({ exportable: true })
this.ucanStore = await ucan.Store.fromTokens(adxSemantics, [])
this.tokens = []
this.ucanStore = null
}
}

@ -12,4 +12,5 @@ const didExamplePlugin: ucans.DidMethodPlugin = {
export const didPlugins = new ucans.Plugins([p256Plugin], {
example: didExamplePlugin,
plc: didExamplePlugin, // @TODO write this plugin
})

@ -28,7 +28,7 @@ export const writeCfg = async (
}
const keypair = await auth.EcdsaKeypair.create({ exportable: true })
const authStore = await auth.AuthStore.fromTokens(keypair, [])
const authStore = new auth.AuthStore(keypair, [])
const fullToken = await authStore.claimFull()
const exportedKey = JSON.stringify(await keypair.export())
@ -68,7 +68,7 @@ export const loadCfg = async (repoPath: string): Promise<Config> => {
const jwk = JSON.parse(jwkStr)
const keypair = await auth.EcdsaKeypair.import(jwk)
const tokenStr = (await readFile(repoPath, 'full.ucan', 'utf-8')) as string
const authStore = await auth.AuthStore.fromTokens(keypair, [tokenStr])
const authStore = new auth.AuthStore(keypair, [tokenStr])
const root = await readRoot(repoPath)
return {
account,

@ -68,6 +68,7 @@ const envApi = {
if (users.has(username)) {
throw new Error(`${username} already exists`)
}
const usernameNoTld = username.slice(0, username.length - '.test'.length)
const servers = devEnv.listOfType(ServerType.PersonalDataServer)
let pds: DevEnvServer
@ -90,8 +91,9 @@ const envApi = {
const pdsRes = await client.todo.adx.createAccount(
{},
{
email: usernameNoTld + '@test.com',
username,
did: `did:test:${username.slice(0, -5)}`,
password: usernameNoTld + '-pass',
},
)
users.set(username, pds)
@ -125,7 +127,7 @@ async function start() {
console.log('Type .help if you get lost')
// create repl
let inst = repl.start() //'adx $ ')
const inst = repl.start() //'adx $ ')
Object.assign(inst.context, envApi)
inst.setupHistory(join(os.homedir(), '.adx-dev-env-history'), () => {})
inst.on('exit', async () => {

@ -44,6 +44,7 @@ export async function generateMockSetup(env: DevEnv) {
carla: env.listOfType(ServerType.PersonalDataServer)[0].getClient(),
}
interface User {
email: string
did: string
username: string
password: string
@ -51,19 +52,22 @@ export async function generateMockSetup(env: DevEnv) {
}
const users: User[] = [
{
did: `did:test:alice`,
email: 'alice@test.com',
did: '',
username: `alice.test`,
password: 'hunter2',
api: clients.alice,
},
{
did: `did:test:bob`,
email: 'bob@test.com',
did: '',
username: `bob.test`,
password: 'hunter2',
api: clients.bob,
},
{
did: `did:test:carla`,
email: 'carla@test.com',
did: '',
username: `carla.test`,
password: 'hunter2',
api: clients.carla,
@ -77,8 +81,9 @@ export async function generateMockSetup(env: DevEnv) {
for (const user of users) {
const res = await clients.loggedout.todo.adx.createAccount(
{},
{ did: user.did, username: user.username, password: user.password },
{ email: user.email, username: user.username, password: user.password },
)
user.did = res.data.did
user.api.setHeader('Authorization', `Bearer ${res.data.jwt}`)
await user.api.todo.social.profile.create(
{ did: user.did },
@ -99,12 +104,12 @@ export async function generateMockSetup(env: DevEnv) {
},
)
}
await follow(alice, 'did:test:bob')
await follow(alice, 'did:test:carla')
await follow(bob, 'did:test:alice')
await follow(bob, 'did:test:carla')
await follow(carla, 'did:test:alice')
await follow(carla, 'did:test:bob')
await follow(alice, bob.did)
await follow(alice, carla.did)
await follow(bob, alice.did)
await follow(bob, carla.did)
await follow(carla, alice.did)
await follow(carla, bob.did)
// a set of posts and reposts
const posts: { uri: string }[] = []
@ -120,7 +125,7 @@ export async function generateMockSetup(env: DevEnv) {
),
)
if (rand(10) === 0) {
let reposter = picka(users)
const reposter = picka(users)
await reposter.api.todo.social.repost.create(
{ did: reposter.did },
{

@ -2,8 +2,8 @@ require('esbuild')
.build({
logLevel: 'info',
entryPoints: [
'src/bin.ts',
'src/server/index.ts',
'src/server/server.ts',
'src/server/db.ts',
'src/client/index.ts',
],

@ -3,7 +3,7 @@
"version": "0.0.1",
"main": "src/index.ts",
"scripts": {
"start": "node dist/server/index.js",
"start": "node dist/bin.js",
"test": "jest",
"prettier": "prettier --check src/",
"prettier:fix": "prettier --write src/",

30
packages/plc/src/bin.ts Normal file

@ -0,0 +1,30 @@
import dotenv from 'dotenv'
import { Database } from './server/db'
import { server } from './server'
const run = async () => {
const env = process.env.ENV
if (env) {
dotenv.config({ path: `./.${env}.env` })
} else {
dotenv.config()
}
let db: Database
const dbLoc = process.env.DATABASE_LOC
if (dbLoc) {
db = await Database.sqlite(dbLoc)
} else {
db = await Database.memory()
}
const envPort = parseInt(process.env.PORT || '')
const port = isNaN(envPort) ? 2582 : envPort
const s = server(db, port)
s.on('listening', () => {
console.log(`👤 PLC server is running at http://localhost:${port}`)
})
}
run()

@ -1,4 +1,3 @@
export * from './client'
export * from './lib'
export * from './server/db'
export * from './server/server'
export * from './server'

@ -1,30 +1,34 @@
import dotenv from 'dotenv'
// catch errors that get thrown in async route handlers
// this is a relatively non-invasive change to express
// they get handled in the error.handler middleware
// leave at top of file before importing Routes
import 'express-async-errors'
import express from 'express'
import cors from 'cors'
import http from 'http'
import { Database } from './db'
import { server } from './server'
import * as error from './error'
import router from './routes'
import { Locals } from './locals'
const run = async () => {
const env = process.env.ENV
if (env) {
dotenv.config({ path: `./.${env}.env` })
} else {
dotenv.config()
}
export * from './db'
let db: Database
const dbLoc = process.env.DATABASE_LOC
if (dbLoc) {
db = await Database.sqlite(dbLoc)
} else {
db = await Database.memory()
}
export const server = (db: Database, port: number): http.Server => {
const app = express()
app.use(express.json())
app.use(cors())
const envPort = parseInt(process.env.PORT || '')
const port = isNaN(envPort) ? 2582 : envPort
const s = server(db, port)
s.on('listening', () => {
console.log(`🌞 PLC server is running at http://localhost:${port}`)
app.use((_req, res, next) => {
const locals: Locals = { db }
Object.assign(res.locals, locals)
next()
})
app.use('/', router)
app.use(error.handler)
return app.listen(port)
}
run()
export default server

@ -1,32 +0,0 @@
// catch errors that get thrown in async route handlers
// this is a relatively non-invasive change to express
// they get handled in the error.handler middleware
// leave at top of file before importing Routes
import 'express-async-errors'
import express from 'express'
import cors from 'cors'
import http from 'http'
import { Database } from './db'
import * as error from './error'
import router from './routes'
import { Locals } from './locals'
export const server = (db: Database, port: number): http.Server => {
const app = express()
app.use(express.json())
app.use(cors())
app.use((_req, res, next) => {
const locals: Locals = { db }
Object.assign(res.locals, locals)
next()
})
app.use('/', router)
app.use(error.handler)
return app.listen(port)
}
export default server

@ -21,6 +21,7 @@
"@adxp/common": "*",
"@adxp/crypto": "*",
"@adxp/lexicon": "*",
"@adxp/plc": "*",
"@adxp/repo": "*",
"@adxp/uri": "*",
"cors": "^2.8.5",

@ -1,16 +1,16 @@
import { Server } from '../../../lexicon'
import * as CreateAccount from '../../../lexicon/types/todo/adx/createAccount'
import { InvalidRequestError } from '@adxp/xrpc-server'
import * as util from '../../../util'
import { Repo } from '@adxp/repo'
import { AuthStore } from '@adxp/auth'
import { PlcClient } from '@adxp/plc'
export default function (server: Server) {
server.todo.adx.getAccountsConfig((_params, _input, _req, res) => {
const cfg = util.getConfig(res)
let availableUserDomains: string[]
if (cfg.debugMode && cfg.didTestRegistry) {
if (cfg.debugMode && !!cfg.testNameRegistry) {
availableUserDomains = ['test']
} else {
throw new Error('TODO')
@ -30,8 +30,8 @@ export default function (server: Server) {
})
server.todo.adx.createAccount(async (_params, input, _req, res) => {
const { did, username, password } = input.body
const cfg = util.getConfig(res)
const { email, username, password } = input.body
const { db, blockstore, auth, config, keypair } = util.getLocals(res)
if (username.startsWith('did:')) {
return {
@ -40,45 +40,44 @@ export default function (server: Server) {
message: 'Cannot register a username that starts with `did:`',
}
}
if (!did.startsWith('did:')) {
return {
status: 400,
message: 'Cannot register a did that does not start with `did:`',
}
}
let isTestUser = false
if (username.endsWith('.test') || did.startsWith('did:test:')) {
if (!cfg.debugMode || !cfg.didTestRegistry) {
if (username.endsWith('.test')) {
if (!config.debugMode || !config.testNameRegistry) {
throw new InvalidRequestError(
'Cannot register a test user if debug mode is not enabled',
)
}
if (!username.endsWith('.test')) {
throw new Error(`Cannot use did:test with non-*.test username`)
}
if (!did.startsWith('did:test:')) {
throw new Error(`Cannot use *.test with a non did:test:* DID`)
}
isTestUser = true
}
const { db, blockstore, keypair, auth } = util.getLocals(res)
await db.registerUser(username, did, password)
// verify username is available
const found = await db.getUser(username)
if (found !== null) {
throw new InvalidRequestError(`Username already taken: ${username}`)
}
const authStore = await AuthStore.fromTokens(keypair, [])
const plcClient = new PlcClient(config.didPlcUrl)
// @TODO real service name
const did = await plcClient.createDid(
keypair,
keypair.did(),
username,
config.origin,
)
await db.registerUser(email, username, did, password)
const authStore = new AuthStore(keypair, [], did)
const repo = await Repo.create(blockstore, did, authStore)
await db.setRepoRoot(did, repo.cid)
if (isTestUser) {
cfg.didTestRegistry?.set(username.slice(0, -5), {
name: username,
service: cfg.origin,
})
if (isTestUser && config.testNameRegistry) {
config.testNameRegistry['username'] = did
}
const jwt = auth.createToken(did)
return { encoding: 'application/json', body: { jwt } }
return { encoding: 'application/json', body: { jwt, username, did } }
})
server.todo.adx.deleteAccount(() => {

@ -6,16 +6,15 @@ export default function (server: Server) {
server.todo.adx.resolveName((params, _in, _req, res) => {
const cfg = util.getConfig(res)
let did: string = ''
let did = ''
if (!params.name || params.name === cfg.hostname) {
// self
const keypair = util.getKeypair(res)
did = keypair.did()
} else if (params.name.endsWith('.test')) {
// .test
did = `did:test:${params.name.slice(0, -5)}`
} else if (params.name.endsWith('.test') && cfg.testNameRegistry) {
did = cfg.testNameRegistry[params.name]
} else {
// TODO
// @TODO
}
if (!did) {
throw new InvalidRequestError(`Unable to resolve name`)

@ -21,7 +21,7 @@ export default function (server: Server) {
let name: string
let did: string
// let didDoc: didSdk.DIDDocument
let nameIsCorrect: boolean | undefined
// let nameIsCorrect: boolean | undefined
// @TODO add back once we have a did network
// if (nameOrDid.startsWith('did:')) {
@ -48,7 +48,7 @@ export default function (server: Server) {
throw new InvalidRequestError(`Could not find user: ${nameOrDid}`)
}
const didDoc = {} as any
nameIsCorrect = true
const nameIsCorrect = true
const collections = await db.listCollectionsForDid(user.did)
@ -86,7 +86,9 @@ export default function (server: Server) {
return {
encoding: 'application/json',
body: { records: records as { uri: string; value: {} }[] },
body: {
records: records as { uri: string; value: Record<string, unknown> }[],
},
}
})
@ -128,7 +130,7 @@ export default function (server: Server) {
}
}
}
const authStore = await util.getAuthstore(res)
const authStore = await util.getAuthstore(res, did)
const repo = await util.maybeLoadRepo(res, did, authStore)
if (!repo) {
throw new InvalidRequestError(
@ -168,7 +170,7 @@ export default function (server: Server) {
)
}
}
const authStore = await util.getAuthstore(res)
const authStore = await util.getAuthstore(res, did)
const repo = await util.maybeLoadRepo(res, did, authStore)
if (!repo) {
throw new InvalidRequestError(
@ -207,7 +209,7 @@ export default function (server: Server) {
)
}
}
const authStore = await util.getAuthstore(res)
const authStore = await util.getAuthstore(res, did)
const repo = await util.maybeLoadRepo(res, did, authStore)
if (!repo) {
throw new InvalidRequestError(
@ -237,7 +239,7 @@ export default function (server: Server) {
if (!auth.verifyUser(req, did)) {
throw new AuthRequiredError()
}
const authStore = await util.getAuthstore(res)
const authStore = await util.getAuthstore(res, did)
const repo = await util.maybeLoadRepo(res, did, authStore)
if (!repo) {
throw new InvalidRequestError(

@ -4,7 +4,7 @@ import * as GetFeed from '../../../lexicon/types/todo/social/getFeed'
import { FollowIndex } from '../../../db/records/follow'
import { PostIndex } from '../../../db/records/post'
import { ProfileIndex } from '../../../db/records/profile'
import { UserDid } from '../../../db/user-dids'
import { User } from '../../../db/user'
import * as util from '../../../db/util'
import { LikeIndex } from '../../../db/records/like'
import { RepostIndex } from '../../../db/records/repost'
@ -26,7 +26,7 @@ export default function (server: Server) {
if (author === undefined) {
builder
.from(UserDid, 'user')
.from(User, 'user')
.innerJoin(FollowIndex, 'follow', 'follow.creator = user.did')
.leftJoin(RepostIndex, 'repost', 'repost.creator = follow.subject')
.innerJoin(
@ -34,7 +34,7 @@ export default function (server: Server) {
'post',
'post.creator = follow.subject OR post.uri = repost.subject',
)
.leftJoin(UserDid, 'author', 'author.did = post.creator')
.leftJoin(User, 'author', 'author.did = post.creator')
.where('user.did = :did', { did: requester })
} else {
const authorWhere = author.startsWith('did:')
@ -44,7 +44,7 @@ export default function (server: Server) {
.from(PostIndex, 'post')
.leftJoin(RepostIndex, 'repost', 'repost.subject = post.uri')
.leftJoin(
UserDid,
User,
'author',
'author.did = post.creator OR author.did = repost.creator',
)
@ -73,7 +73,7 @@ export default function (server: Server) {
'author_profile',
'author_profile.creator = author.did',
)
.leftJoin(UserDid, 'reposted_by', 'reposted_by.did = repost.creator')
.leftJoin(User, 'reposted_by', 'reposted_by.did = repost.creator')
.leftJoin(
ProfileIndex,
'reposted_by_profile',

@ -3,7 +3,7 @@ import * as GetLikedBy from '../../../lexicon/types/todo/social/getLikedBy'
import { AdxRecord } from '../../../db/record'
import { LikeIndex } from '../../../db/records/like'
import { ProfileIndex } from '../../../db/records/profile'
import { UserDid } from '../../../db/user-dids'
import { User } from '../../../db/user'
import { getLocals } from '../../../util'
export default function (server: Server) {
@ -23,7 +23,7 @@ export default function (server: Server) {
])
.from(LikeIndex, 'like')
.leftJoin(AdxRecord, 'record', 'like.uri = record.uri')
.leftJoin(UserDid, 'user', 'like.creator = user.did')
.leftJoin(User, 'user', 'like.creator = user.did')
.leftJoin(ProfileIndex, 'profile', 'profile.creator = user.did')
.where('like.subject = :uri', { uri })
.orderBy('like.createdAt')

@ -4,7 +4,7 @@ import { DataSource } from 'typeorm'
import * as GetPostThread from '../../../lexicon/types/todo/social/getPostThread'
import { PostIndex } from '../../../db/records/post'
import { ProfileIndex } from '../../../db/records/profile'
import { UserDid } from '../../../db/user-dids'
import { User } from '../../../db/user'
import * as util from '../../../db/util'
import { LikeIndex } from '../../../db/records/like'
import { RepostIndex } from '../../../db/records/repost'
@ -92,7 +92,7 @@ const postInfoBuilder = (db: DataSource, requester: string) => {
])
.from(PostIndex, 'post')
.innerJoin(AdxRecord, 'record', 'record.uri = post.uri')
.innerJoin(UserDid, 'author', 'author.did = post.creator')
.innerJoin(User, 'author', 'author.did = post.creator')
.leftJoin(
ProfileIndex,
'author_profile',

@ -4,7 +4,7 @@ import * as GetProfile from '../../../lexicon/types/todo/social/getProfile'
import { FollowIndex } from '../../../db/records/follow'
import { PostIndex } from '../../../db/records/post'
import { ProfileBadgeIndex, ProfileIndex } from '../../../db/records/profile'
import { UserDid } from '../../../db/user-dids'
import { User } from '../../../db/user'
import * as util from '../../../db/util'
import { BadgeIndex } from '../../../db/records/badge'
import { getLocals } from '../../../util'
@ -32,7 +32,7 @@ export default function (server: Server) {
'posts_count.count AS postsCount',
'requester_follow.uri AS requesterFollow',
])
.from(UserDid, 'user')
.from(User, 'user')
.leftJoin(ProfileIndex, 'profile', 'profile.creator = user.did')
.leftJoin(
util.countSubquery(FollowIndex, 'creator'),
@ -80,7 +80,7 @@ export default function (server: Server) {
'profile_badge.profile = profile.uri',
)
.innerJoin(BadgeIndex, 'badge', 'badge.uri = profile_badge.badge')
.leftJoin(UserDid, 'issuer', 'issuer.did = badge.creator')
.leftJoin(User, 'issuer', 'issuer.did = badge.creator')
.leftJoin(
ProfileIndex,
'issuer_profile',

@ -2,7 +2,7 @@ import { Server } from '../../../lexicon'
import * as GetRepostedBy from '../../../lexicon/types/todo/social/getRepostedBy'
import { AdxRecord } from '../../../db/record'
import { ProfileIndex } from '../../../db/records/profile'
import { UserDid } from '../../../db/user-dids'
import { User } from '../../../db/user'
import { RepostIndex } from '../../../db/records/repost'
import { getLocals } from '../../../util'
@ -23,7 +23,7 @@ export default function (server: Server) {
])
.from(RepostIndex, 'repost')
.leftJoin(AdxRecord, 'record', 'repost.uri = record.uri')
.leftJoin(UserDid, 'user', 'repost.creator = user.did')
.leftJoin(User, 'user', 'repost.creator = user.did')
.leftJoin(ProfileIndex, 'profile', 'profile.creator = user.did')
.where('repost.subject = :uri', { uri })
.orderBy('repost.createdAt')

@ -4,7 +4,7 @@ import * as GetUserFollowers from '../../../lexicon/types/todo/social/getUserFol
import { AdxRecord } from '../../../db/record'
import { FollowIndex } from '../../../db/records/follow'
import { ProfileIndex } from '../../../db/records/profile'
import { UserDid } from '../../../db/user-dids'
import { User } from '../../../db/user'
import * as util from './util'
import { getLocals } from '../../../util'
@ -29,7 +29,7 @@ export default function (server: Server) {
])
.from(FollowIndex, 'follow')
.innerJoin(AdxRecord, 'record', 'record.uri = follow.uri')
.innerJoin(UserDid, 'creator', 'creator.did = record.did')
.innerJoin(User, 'creator', 'creator.did = record.did')
.leftJoin(ProfileIndex, 'profile', 'profile.creator = record.did')
.where('follow.subject = :subject', { subject: subject.did })
.orderBy('follow.createdAt')

@ -4,7 +4,7 @@ import * as GetUserFollows from '../../../lexicon/types/todo/social/getUserFollo
import { AdxRecord } from '../../../db/record'
import { FollowIndex } from '../../../db/records/follow'
import { ProfileIndex } from '../../../db/records/profile'
import { UserDid } from '../../../db/user-dids'
import { User } from '../../../db/user'
import * as util from './util'
import { getLocals } from '../../../util'
@ -29,7 +29,7 @@ export default function (server: Server) {
])
.from(FollowIndex, 'follow')
.innerJoin(AdxRecord, 'record', 'follow.uri = record.uri')
.innerJoin(UserDid, 'subject', 'follow.subject = subject.did')
.innerJoin(User, 'subject', 'follow.subject = subject.did')
.leftJoin(ProfileIndex, 'profile', 'profile.creator = follow.subject')
.where('follow.creator = :creator', { creator: creator.did })
.orderBy('follow.createdAt')

@ -1,6 +1,6 @@
import { DataSource } from 'typeorm'
import { ProfileIndex } from '../../../db/records/profile'
import { UserDid } from '../../../db/user-dids'
import { User } from '../../../db/user'
import * as util from '../../../db/util'
type UserInfo = {
@ -20,7 +20,7 @@ export const getUserInfo = async (
'user.username AS name',
'profile.displayName AS displayName',
])
.from(UserDid, 'user')
.from(User, 'user')
.leftJoin(ProfileIndex, 'profile', 'profile.creator = user.did')
.where(util.userWhereClause(user), { user })
.getRawOne()

@ -1,5 +1,3 @@
import { DidTestRegistry } from './lib/did/did-test'
export interface ServerConfigValues {
debugMode?: boolean
@ -9,10 +7,13 @@ export interface ServerConfigValues {
jwtSecret: string
didPlcUrl: string
serverDid: string
blockstoreLocation?: string
databaseLocation?: string
didTestRegistry?: DidTestRegistry
testNameRegistry?: Record<string, string>
}
export class ServerConfig {
@ -33,10 +34,13 @@ export class ServerConfig {
const jwtSecret = process.env.JWT_SECRET || 'jwt_secret'
const didPlcUrl = process.env.DID_PLC_URL || 'http://localhost:2582'
const serverDid = process.env.SERVER_DID || ''
const blockstoreLocation = process.env.BLOCKSTORE_LOC
const databaseLocation = process.env.DATABASE_LOC
const didTestRegistry = debugMode ? new DidTestRegistry() : undefined
const testNameRegistry = debugMode ? {} : undefined
return new ServerConfig({
debugMode,
@ -44,9 +48,11 @@ export class ServerConfig {
hostname,
port,
jwtSecret,
serverDid,
didPlcUrl,
blockstoreLocation,
databaseLocation,
didTestRegistry,
testNameRegistry,
})
}
@ -76,6 +82,14 @@ export class ServerConfig {
return this.cfg.jwtSecret
}
get didPlcUrl() {
return this.cfg.didPlcUrl
}
get serverDid() {
return this.cfg.serverDid
}
get blockstoreLocation() {
return this.cfg.blockstoreLocation
}
@ -92,7 +106,7 @@ export class ServerConfig {
return !this.databaseLocation
}
get didTestRegistry() {
return this.cfg.didTestRegistry
get testNameRegistry() {
return this.cfg.testNameRegistry
}
}

@ -20,7 +20,7 @@ import { AdxUri } from '@adxp/uri'
import { CID } from 'multiformats/cid'
import { RepoRoot } from './repo-root'
import { AdxRecord } from './record'
import { UserDid } from './user-dids'
import { User } from './user'
import * as util from './util'
export class Database {
@ -61,7 +61,7 @@ export class Database {
ProfileIndex,
ProfileBadgeIndex,
RepostIndex,
UserDid,
User,
],
synchronize: true,
})
@ -96,19 +96,25 @@ export class Database {
}
async getUser(
user: string,
usernameOrDid: string,
): Promise<{ username: string; did: string } | null> {
const table = this.db.getRepository(UserDid)
const found = user.startsWith('did:')
? await table.findOneBy({ did: user })
: await table.findOneBy({ username: user })
const table = this.db.getRepository(User)
const found = usernameOrDid.startsWith('did:')
? await table.findOneBy({ did: usernameOrDid })
: await table.findOneBy({ username: usernameOrDid })
return found ? { username: found.username, did: found.did } : null
}
async registerUser(username: string, did: string, password: string) {
const table = this.db.getRepository(UserDid)
const user = new UserDid()
async registerUser(
email: string,
username: string,
did: string,
password: string,
) {
const table = this.db.getRepository(User)
const user = new User()
user.email = email
user.username = username
user.did = did
user.password = await util.scryptHash(password)
@ -119,7 +125,7 @@ export class Database {
username: string,
password: string,
): Promise<string | null> {
const found = await this.db.getRepository(UserDid).findOneBy({ username })
const found = await this.db.getRepository(User).findOneBy({ username })
if (!found) return null
const validPass = await util.scryptVerify(password, found.password)
if (!validPass) return null

@ -9,7 +9,7 @@ import {
ManyToOne,
} from 'typeorm'
import { DbRecordPlugin } from '../types'
import { UserDid } from '../user-dids'
import { User } from '../user'
import schemas from '../schemas'
import { collectionToTableName } from '../util'
@ -22,7 +22,7 @@ export class BadgeIndex {
uri: string
@Column('varchar')
@ManyToOne(() => UserDid, (user) => user.did)
@ManyToOne(() => User, (user) => user.did)
creator: string
@Column('varchar')

@ -9,7 +9,7 @@ import {
ManyToOne,
} from 'typeorm'
import { DbRecordPlugin } from '../types'
import { UserDid } from '../user-dids'
import { User } from '../user'
import schemas from '../schemas'
import { collectionToTableName } from '../util'
@ -22,7 +22,7 @@ export class FollowIndex {
uri: string
@Column('varchar')
@ManyToOne(() => UserDid, (user) => user.did)
@ManyToOne(() => User, (user) => user.did)
creator: string
@Column('varchar')

@ -9,7 +9,7 @@ import {
ManyToOne,
} from 'typeorm'
import { DbRecordPlugin } from '../types'
import { UserDid } from '../user-dids'
import { User } from '../user'
import schemas from '../schemas'
import { collectionToTableName } from '../util'
@ -22,7 +22,7 @@ export class LikeIndex {
uri: string
@Column('varchar')
@ManyToOne(() => UserDid, (user) => user.did)
@ManyToOne(() => User, (user) => user.did)
creator: string
@Column('varchar')

@ -2,7 +2,7 @@ import { AdxUri } from '@adxp/uri'
import * as Post from '../../lexicon/types/todo/social/post'
import { DataSource, Entity, Column, PrimaryColumn, ManyToOne } from 'typeorm'
import { DbRecordPlugin } from '../types'
import { UserDid } from '../user-dids'
import { User } from '../user'
import schemas from '../schemas'
import { collectionToTableName } from '../util'
@ -15,7 +15,7 @@ export class PostIndex {
uri: string
@Column('varchar')
@ManyToOne(() => UserDid, (user) => user.did)
@ManyToOne(() => User, (user) => user.did)
creator: string
@Column('text')

@ -9,7 +9,7 @@ import {
ManyToOne,
} from 'typeorm'
import { DbRecordPlugin } from '../types'
import { UserDid } from '../user-dids'
import { User } from '../user'
import schemas from '../schemas'
import { collectionToTableName } from '../util'
import { PostIndex } from './post'
@ -23,7 +23,7 @@ export class RepostIndex {
uri: string
@Column('varchar')
@ManyToOne(() => UserDid, (user) => user.did)
@ManyToOne(() => User, (user) => user.did)
creator: string
@Column('varchar')

@ -1,7 +1,7 @@
import { Entity, Column, PrimaryColumn, Index } from 'typeorm'
@Entity({ name: 'user_dids' })
export class UserDid {
@Entity({ name: 'users' })
export class User {
@PrimaryColumn('varchar')
did: string
@ -9,6 +9,9 @@ export class UserDid {
@Index()
username: string
@Column('varchar')
email: string
@Column('varchar')
password: string
}

@ -15,7 +15,6 @@ import ServerAuth from './auth'
import * as error from './error'
import { ServerConfig, ServerConfigValues } from './config'
export { DidTestRegistry } from './lib/did/did-test'
export type { ServerConfigValues } from './config'
export { ServerConfig } from './config'
export { Database } from './db'

@ -14,12 +14,15 @@ export const methodSchemas: MethodSchema[] = [
encoding: 'application/json',
schema: {
type: 'object',
required: ['username', 'did', 'password'],
required: ['username', 'email', 'password'],
properties: {
email: {
type: 'string',
},
username: {
type: 'string',
},
did: {
inviteCode: {
type: 'string',
},
password: {
@ -32,11 +35,17 @@ export const methodSchemas: MethodSchema[] = [
encoding: 'application/json',
schema: {
type: 'object',
required: ['jwt'],
required: ['jwt', 'username', 'did'],
properties: {
jwt: {
type: 'string',
},
username: {
type: 'string',
},
did: {
type: 'string',
},
},
},
},

@ -11,8 +11,9 @@ export interface HandlerInput {
}
export interface InputSchema {
email: string;
username: string;
did: string;
inviteCode?: string;
password: string;
}
@ -31,6 +32,8 @@ export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
jwt: string;
username: string;
did: string;
}
export type Handler = (

@ -1,58 +0,0 @@
import z from 'zod'
import { DidDocument } from './types'
const testEntry = z.object({
name: z.string(),
service: z.string(),
})
export type TestEntry = z.infer<typeof testEntry>
// globals
// =
export class DidTestRegistry {
entries: Map<string, TestEntry> = new Map()
resolve(did: string) {
const name = did.split(':')[2] || ''
const entry = this.entries.get(name)
if (!entry) {
throw new Error(`Entry not found: ${did}`)
}
return toDidDocument(did, entry)
}
set(name: string, entry: TestEntry) {
this.entries.set(name, entry)
}
}
// helpers
// =
function toDidDocument(did: string, entry: TestEntry): DidDocument {
return {
'@context': ['https://www.w3.org/ns/did/v1'],
id: did,
alsoKnownAs: [`https://${entry.name}`],
verificationMethod: [
/*TODO*/
],
assertionMethod: [
/* TODO `${did}#signingKey`*/
],
capabilityInvocation: [
/* TODO `${did}#signingKey`*/
],
capabilityDelegation: [
/* TODO `${did}#signingKey`*/
],
service: [
{
id: `${did}#atpPds`,
type: 'AtpPersonalDataServer',
serviceEndpoint: entry.service,
},
],
}
}

@ -1,68 +0,0 @@
import {
DidDocument,
DidDocVerificationMethod,
DidDocService,
KeyCapabilitySection,
} from './types'
export class DidDocAPI {
constructor(public didDoc: DidDocument) {}
gidDID(): string {
return this.didDoc.id
}
getController(): string {
if (this.didDoc.controller) {
if (Array.isArray(this.didDoc.controller)) {
return this.didDoc.controller[0]
}
return this.didDoc.controller
}
throw new Error('Controller is not defined')
}
listPublicKeys(purpose: KeyCapabilitySection): DidDocVerificationMethod[] {
const keys = this.didDoc[purpose]
if (!keys || keys.length === 0) return []
return keys.map((key: string | DidDocVerificationMethod) => {
if (typeof key === 'string') return findKey(this.didDoc, key)
return key
})
}
getPublicKey(
purpose: KeyCapabilitySection,
offset = 0,
): DidDocVerificationMethod {
const keys = this.didDoc[purpose]
if (!keys || keys.length === 0) throw new Error('Key not found')
const key = keys[offset]
if (!key) throw new Error('Key not found')
if (typeof key === 'string') return findKey(this.didDoc, key)
return key
}
listServices(): DidDocService[] {
return this.didDoc.service || []
}
getService(type: string): DidDocService {
if (!this.didDoc.service || this.didDoc.service.length === 0)
throw new Error('Service not found')
const service = this.didDoc.service.find((s) => s.type === type)
if (!service) throw new Error('Service not found')
return service
}
}
function findKey(didDoc: DidDocument, id: string) {
if (!didDoc.verificationMethod)
throw new Error('Malformed DID document: no verification methods set')
const key = didDoc.verificationMethod.find((m) => m.id === id)
if (!key)
throw new Error(
`Malformed DID document: no verification method of ID ${id} found`,
)
return key
}

@ -1,8 +0,0 @@
import { ServerConfig } from '../../config'
export function resolve(did: string, cfg: ServerConfig) {
if (did.startsWith('did:test:') && cfg.didTestRegistry) {
return cfg.didTestRegistry.resolve(did)
}
throw new Error(`Unsupported did method: ${did}`)
}

@ -1,35 +0,0 @@
import z from 'zod'
export const didDocVerificationMethod = z.object({
id: z.string(),
type: z.string(),
controller: z.string(),
publicKeyMultibase: z.string(),
})
export type DidDocVerificationMethod = z.infer<typeof didDocVerificationMethod>
export const didDocService = z.object({
id: z.string(),
type: z.string(),
serviceEndpoint: z.string(),
})
export type DidDocService = z.infer<typeof didDocService>
export const didDocument = z.object({
'@context': z.array(z.string()),
id: z.string(),
alsoKnownAs: z.array(z.string()),
controller: z.union([z.string(), z.array(z.string())]).optional(),
verificationMethod: z.array(didDocVerificationMethod),
assertionMethod: z.array(z.string()),
capabilityInvocation: z.array(z.string()),
capabilityDelegation: z.array(z.string()),
service: z.array(didDocService),
})
export type DidDocument = z.infer<typeof didDocument>
export type KeyCapabilitySection =
| 'verificationMethod'
| 'assertionMethod'
| 'capabilityInvocation'
| 'capabilityDelegation'

@ -1,6 +1,6 @@
import express from 'express'
import * as util from '../util'
import { UserDid } from '../db/user-dids'
import { User } from '../db/user'
const router = express.Router()
@ -9,19 +9,19 @@ router.get('/did.json', async (req, res) => {
const { db, config } = util.getLocals(res)
const hostname = req.hostname
let userDidRecord: UserDid | null = null
let userRecord: { username: string; did: string } | null = null
try {
userDidRecord = await db.getUser(hostname)
userRecord = await db.getUser(hostname)
} catch (e) {
console.error(`Error looking up user in did:web route`)
console.error(e)
return res.status(500).end()
}
if (!userDidRecord) {
if (!userRecord) {
return res.status(404).end()
}
if (userDidRecord.did !== `did:web:${hostname}`) {
if (userRecord.did !== `did:web:${hostname}`) {
return res.status(404).end()
}
// TODO
@ -31,14 +31,14 @@ router.get('/did.json', async (req, res) => {
return res.json({
'@context': ['https://www.w3.org/ns/did/v1'],
id: userDidRecord.did,
alsoKnownAs: `https://${userDidRecord.username}`,
id: userRecord.did,
alsoKnownAs: `https://${userRecord.username}`,
verificationMethod: [
// TODO
],
service: [
{
id: `${userDidRecord.did}#atpPds`,
id: `${userRecord.did}#atpPds`,
type: 'AtpPersonalDataServer',
serviceEndpoint: config.origin,
},

@ -2,6 +2,7 @@ import { Request, Response } from 'express'
import { check } from '@adxp/common'
import { IpldStore, Repo } from '@adxp/repo'
import * as auth from '@adxp/auth'
import * as plc from '@adxp/plc'
import { Database } from './db'
import { ServerError } from './error'
import { ServerConfig } from './config'
@ -100,9 +101,15 @@ export const getLocals = (res: Response): Locals => {
}
}
export const getAuthstore = async (res: Response): Promise<auth.AuthStore> => {
export const getAuthstore = (res: Response, did: string): auth.AuthStore => {
const keypair = getKeypair(res)
return auth.AuthStore.fromTokens(keypair, [])
// @TODO check that we can sign on behalf of this DID
return new auth.AuthStore(keypair, [], did)
}
export const getPlcClient = (res: Response): plc.PlcClient => {
const cfg = getConfig(res)
return new plc.PlcClient(cfg.didPlcUrl)
}
export const maybeLoadRepo = async (

@ -1,8 +1,9 @@
import { MemoryBlockstore } from '@adxp/repo'
import * as crypto from '@adxp/crypto'
import * as plc from '@adxp/plc'
import getPort from 'get-port'
import server, { DidTestRegistry, ServerConfig, Database } from '../src/index'
import server, { ServerConfig, Database } from '../src/index'
const USE_TEST_SERVER = true
@ -18,10 +19,26 @@ export const runTestServer = async (): Promise<{
close: async () => {},
}
}
const port = await getPort()
const pdsPort = await getPort()
const keypair = await crypto.EcdsaKeypair.create()
// run plc server
const plcPort = await getPort()
const plcUrl = `http://localhost:${plcPort}`
const plcDb = await plc.Database.memory()
const plcServer = plc.server(plcDb, plcPort)
// setup server did
const plcClient = new plc.PlcClient(plcUrl)
const serverDid = await plcClient.createDid(
keypair,
keypair.did(),
'pds.test',
`http://localhost:${pdsPort}`,
)
const db = await Database.memory()
const serverBlockstore = new MemoryBlockstore()
const keypair = await crypto.EcdsaKeypair.create()
const s = server(
serverBlockstore,
db,
@ -30,16 +47,23 @@ export const runTestServer = async (): Promise<{
debugMode: true,
scheme: 'http',
hostname: 'localhost',
port,
port: pdsPort,
serverDid,
didPlcUrl: plcUrl,
jwtSecret: 'jwt-secret',
didTestRegistry: new DidTestRegistry(),
testNameRegistry: {},
}),
)
return {
url: `http://localhost:${port}`,
url: `http://localhost:${pdsPort}`,
close: async () => {
await db.close()
s.close()
await Promise.all([
db.close(),
s.close(),
plcServer?.close(),
plcDb?.close(),
])
},
}
}

@ -5,10 +5,9 @@ import AdxApi, {
import * as util from './_util'
const username = 'alice.test'
const did = 'did:test:alice'
const password = 'test123'
describe('auth', () => {
describe('account', () => {
let client: AdxServiceClient
let close: util.CloseFn
@ -22,18 +21,26 @@ describe('auth', () => {
await close()
})
it('servers the accounts system config', async () => {
it('serves the accounts system config', async () => {
const res = await client.todo.adx.getAccountsConfig({})
expect(res.data.availableUserDomains[0]).toBe('test')
expect(typeof res.data.inviteCodeRequired).toBe('boolean')
})
let did: string
let jwt: string
it('creates an account', async () => {
const res = await client.todo.adx.createAccount(
{},
{ username, did, password },
{ email: 'alice@test.com', username, password },
)
expect(typeof res.data.jwt).toBe('string')
did = res.data.did
jwt = res.data.jwt
expect(typeof jwt).toBe('string')
expect(did.startsWith('did:plc:')).toBeTruthy()
expect(res.data.username).toEqual(username)
})
it('fails on invalid usernames', async () => {
@ -58,14 +65,12 @@ describe('auth', () => {
await expect(client.todo.adx.getSession({})).rejects.toThrow()
})
let jwt: string
it('logs in', async () => {
const res = await client.todo.adx.createSession({}, { username, password })
jwt = res.data.jwt
expect(typeof jwt).toBe('string')
expect(res.data.name).toBe('alice.test')
expect(res.data.did).toBe('did:test:alice')
expect(res.data.did).toBe(did)
})
it('can perform authenticated requests', async () => {

@ -4,11 +4,17 @@ import { AdxUri } from '@adxp/uri'
import * as util from './_util'
const alice = {
email: 'alice@test.com',
username: 'alice.test',
did: 'did:test:alice',
did: '',
password: 'alice-pass',
}
const bob = { username: 'bob.test', did: 'did:test:bob', password: 'bob-pass' }
const bob = {
email: 'alice@test.com',
username: 'bob.test',
did: '',
password: 'bob-pass',
}
describe('crud operations', () => {
let client: AdxServiceClient
@ -27,9 +33,25 @@ describe('crud operations', () => {
})
it('registers users', async () => {
const res = await client.todo.adx.createAccount({}, alice)
const res = await client.todo.adx.createAccount(
{},
{
email: alice.email,
username: alice.username,
password: alice.password,
},
)
aliceClient.setHeader('authorization', `Bearer ${res.data.jwt}`)
await client.todo.adx.createAccount({}, bob)
alice.did = res.data.did
const res2 = await client.todo.adx.createAccount(
{},
{
email: bob.email,
username: bob.username,
password: bob.password,
},
)
bob.did = res2.data.did
})
it('describes repo', async () => {

@ -1,28 +1,32 @@
export const users = {
alice: {
email: 'alice@test.com',
name: 'alice.test',
did: 'did:test:alice',
did: '',
password: 'alice-pass',
displayName: 'ali',
description: 'its me!',
},
bob: {
email: 'bob@test.com',
name: 'bob.test',
did: 'did:test:bob',
did: '',
password: 'bob-pass',
displayName: 'bobby',
description: 'hi im bob',
},
carol: {
email: 'carol@test.com',
name: 'carol.test',
did: 'did:test:carol',
did: '',
password: 'carol-pass',
displayName: undefined,
description: undefined,
},
dan: {
email: 'dan@test.com',
name: 'dan.test',
did: 'did:test:dan',
did: '',
password: 'dan-pass',
displayName: undefined,
description: undefined,

@ -7,9 +7,9 @@ let alicePosts: AdxUri[] = []
let bobPosts: AdxUri[] = []
let carolPosts: AdxUri[] = []
let danPosts: AdxUri[] = []
let bobFollows: Record<string, AdxUri> = {}
let bobLikes: Record<string, AdxUri> = {}
let badges: AdxUri[] = []
const bobFollows: Record<string, AdxUri> = {}
const bobLikes: Record<string, AdxUri> = {}
const badges: AdxUri[] = []
let authTokens: Record<string, string> = {}
const getHeaders = (did: string) => {
@ -35,34 +35,38 @@ describe('pds views', () => {
{},
{
username: users.alice.name,
did: users.alice.did,
email: users.alice.email,
password: users.alice.password,
},
)
users.alice.did = res1.data.did
const res2 = await client.todo.adx.createAccount(
{},
{
username: users.bob.name,
did: users.bob.did,
email: users.bob.email,
password: users.bob.password,
},
)
users.bob.did = res2.data.did
const res3 = await client.todo.adx.createAccount(
{},
{
username: users.carol.name,
did: users.carol.did,
email: users.carol.email,
password: users.carol.password,
},
)
users.carol.did = res3.data.did
const res4 = await client.todo.adx.createAccount(
{},
{
username: users.dan.name,
did: users.dan.did,
email: users.dan.email,
password: users.dan.password,
},
)
users.dan.did = res4.data.did
authTokens = {
[users.alice.did]: res1.data.jwt,
[users.bob.did]: res2.data.jwt,

@ -13,6 +13,7 @@
{ "path": "../crypto/tsconfig.build.json" },
{ "path": "../lexicon/tsconfig.build.json" },
{ "path": "../lex-cli/tsconfig.build.json" },
{ "path": "../plc/tsconfig.build.json" },
{ "path": "../repo/tsconfig.build.json" },
{ "path": "../uri/tsconfig.build.json" },
{ "path": "../xrpc-server/tsconfig.build.json" }

@ -8,10 +8,11 @@
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["username", "did", "password"],
"required": ["username", "email", "password"],
"properties": {
"email": {"type": "string"},
"username": {"type": "string"},
"did": {"type": "string"},
"inviteCode": {"type": "string"},
"password": {"type": "string"}
}
}
@ -20,9 +21,11 @@
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["jwt"],
"required": ["jwt", "username", "did"],
"properties": {
"jwt": { "type": "string" }
"jwt": { "type": "string" },
"username": { "type": "string" },
"did": { "type": "string" }
}
}
},