Pds Did integration (#190)
* 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:
parent
6b47ad7622
commit
ee68e6977b
docs/specs/adx
packages
api/src
auth/src
cli/src/lib
dev-env/src
plc
server
schemas/todo.adx
@ -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
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" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user