Skip to content

Commit

Permalink
fix: profile package circular dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
yknl committed Aug 26, 2020
1 parent 9ce79f6 commit c749612
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 328 deletions.
16 changes: 16 additions & 0 deletions packages/profile/jest.config.js
@@ -0,0 +1,16 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
coverageDirectory: './coverage/',
collectCoverage: true,
globals: {
'ts-jest': {
diagnostics: {
ignoreCodes: ['TS151001'],
},
},
},
moduleFileExtensions: ['js', 'ts', 'd.ts'],
setupFiles: ['./tests/global-setup.ts'],
setupFilesAfterEnv: ['./tests/setup.ts'],
};
3 changes: 2 additions & 1 deletion packages/profile/package.json
Expand Up @@ -31,7 +31,8 @@
"build:esm": "tsc --outDir ./lib-esm -m es6 -t es2017",
"build:cjs:watch": "tsc --outDir ./lib -m commonjs -t es2017 --watch",
"build:esm:watch": "tsc --outDir ./lib-esm -m es6 -t es2017 --watch",
"test": "echo \"Error: run tests from root\" && exit 1"
"test": "jest",
"test:watch": "jest --watch --coverage=false"
},
"bugs": {
"url": "https://github.com/blockstack/blockstack.js/issues"
Expand Down
13 changes: 5 additions & 8 deletions packages/profile/src/index.ts
@@ -1,9 +1,12 @@
export {
Profile
Profile,
Person,
makeProfileZoneFile,
getTokenFileUrl,
resolveZoneFileToProfile
} from './profile'

export {
Person,
Organization,
CreativeWork,
resolveZoneFileToPerson
Expand All @@ -15,9 +18,3 @@ export {
verifyProfileToken,
extractProfile
} from './profileTokens'

export {
makeProfileZoneFile,
getTokenFileUrl,
resolveZoneFileToProfile
} from './profileZoneFiles'
310 changes: 309 additions & 1 deletion packages/profile/src/profile.ts
Expand Up @@ -2,7 +2,16 @@
import * as inspector from 'schema-inspector'

import { signProfileToken, extractProfile } from './profileTokens'
import { makeProfileZoneFile } from './profileZoneFiles'

import { getPersonFromLegacyFormat } from './profileSchemas/personLegacy'
import {
getName, getFamilyName, getGivenName, getAvatarUrl, getDescription,
getVerifiedAccounts, getAddress, getBirthDate,
getConnections, getOrganizations
} from './profileSchemas/personUtils'

import { makeZoneFile, parseZoneFile } from 'zone-file'
import { Logger, fetchPrivate } from '@stacks/common'

const schemaDefinition: {[key: string]: any} = {
type: 'object',
Expand Down Expand Up @@ -47,3 +56,302 @@ export class Profile {
return makeProfileZoneFile(domainName, tokenFileURL)
}
}

const personSchemaDefinition = {
type: 'object',
strict: false,
properties: {
'@context': { type: 'string', optional: true },
'@type': { type: 'string' },
'@id': { type: 'string', optional: true },
name: { type: 'string', optional: true },
givenName: { type: 'string', optional: true },
familyName: { type: 'string', optional: true },
description: { type: 'string', optional: true },
image: {
type: 'array',
optional: true,
items: {
type: 'object',
properties: {
'@type': { type: 'string' },
name: { type: 'string', optional: true },
contentUrl: { type: 'string', optional: true }
}
}
},
website: {
type: 'array',
optional: true,
items: {
type: 'object',
properties: {
'@type': { type: 'string' },
url: { type: 'string', optional: true }
}
}
},
account: {
type: 'array',
optional: true,
items: {
type: 'object',
properties: {
'@type': { type: 'string' },
service: { type: 'string', optional: true },
identifier: { type: 'string', optional: true },
proofType: { type: 'string', optional: true },
proofUrl: { type: 'string', optional: true },
proofMessage: { type: 'string', optional: true },
proofSignature: { type: 'string', optional: true }
}
}
},
worksFor: {
type: 'array',
optional: true,
items: {
type: 'object',
properties: {
'@type': { type: 'string' },
'@id': { type: 'string', optional: true }
}
}
},
knows: {
type: 'array',
optional: true,
items: {
type: 'object',
properties: {
'@type': { type: 'string' },
'@id': { type: 'string', optional: true }
}
}
},
address: {
type: 'object',
optional: true,
properties: {
'@type': { type: 'string' },
streetAddress: { type: 'string', optional: true },
addressLocality: { type: 'string', optional: true },
postalCode: { type: 'string', optional: true },
addressCountry: { type: 'string', optional: true }
}
},
birthDate: { type: 'string', optional: true },
taxID: { type: 'string', optional: true }
}
}

/**
* @ignore
*/
export class Person extends Profile {
constructor(profile = {}) {
super(profile)
this._profile = Object.assign({}, {
'@type': 'Person'
}, this._profile)
}

static validateSchema(profile: any, strict = false) {
personSchemaDefinition.strict = strict
return inspector.validate(schemaDefinition, profile)
}

static fromToken(token: string, publicKeyOrAddress: string | null = null): Person {
const profile = extractProfile(token, publicKeyOrAddress)
return new Person(profile)
}

static fromLegacyFormat(legacyProfile: any) {
const profile = getPersonFromLegacyFormat(legacyProfile)
return new Person(profile)
}

toJSON() {
return {
profile: this.profile(),
name: this.name(),
givenName: this.givenName(),
familyName: this.familyName(),
description: this.description(),
avatarUrl: this.avatarUrl(),
verifiedAccounts: this.verifiedAccounts(),
address: this.address(),
birthDate: this.birthDate(),
connections: this.connections(),
organizations: this.organizations()
}
}

profile() {
return Object.assign({}, this._profile)
}

name() {
return getName(this.profile())
}

givenName() {
return getGivenName(this.profile())
}

familyName() {
return getFamilyName(this.profile())
}

description() {
return getDescription(this.profile())
}

avatarUrl() {
return getAvatarUrl(this.profile())
}

verifiedAccounts(verifications?: any[]) {
return getVerifiedAccounts(this.profile(), verifications)
}

address() {
return getAddress(this.profile())
}

birthDate() {
return getBirthDate(this.profile())
}

connections() {
return getConnections(this.profile())
}

organizations() {
return getOrganizations(this.profile())
}
}

/**
*
* @param origin
* @param tokenFileUrl
*
* @ignore
*/
export function makeProfileZoneFile(origin: string, tokenFileUrl: string): string {
if (tokenFileUrl.indexOf('://') < 0) {
throw new Error('Invalid token file url')
}

const urlScheme = tokenFileUrl.split('://')[0]
const urlParts = tokenFileUrl.split('://')[1].split('/')
const domain = urlParts[0]
const pathname = `/${urlParts.slice(1).join('/')}`

const zoneFile = {
$origin: origin,
$ttl: 3600,
uri: [
{
name: '_http._tcp',
priority: 10,
weight: 1,
target: `${urlScheme}://${domain}${pathname}`
}
]
}

const zoneFileTemplate = '{$origin}\n{$ttl}\n{uri}\n'


return makeZoneFile(zoneFile, zoneFileTemplate)
}

/**
*
* @param zoneFileJson
*
* @ignore
*/
export function getTokenFileUrl(zoneFileJson: any): string | null {
if (!zoneFileJson.hasOwnProperty('uri')) {
return null
}
if (!Array.isArray(zoneFileJson.uri)) {
return null
}
if (zoneFileJson.uri.length < 1) {
return null
}
const firstUriRecord = zoneFileJson.uri[0]

if (!firstUriRecord.hasOwnProperty('target')) {
return null
}
let tokenFileUrl = firstUriRecord.target

if (tokenFileUrl.startsWith('https')) {
// pass
} else if (tokenFileUrl.startsWith('http')) {
// pass
} else {
tokenFileUrl = `https://${tokenFileUrl}`
}

return tokenFileUrl
}

/**
*
* @param zoneFile
* @param publicKeyOrAddress
*
* @ignore
*/
export function resolveZoneFileToProfile(zoneFile: any, publicKeyOrAddress: string) {
return new Promise((resolve, reject) => {
let zoneFileJson = null
try {
zoneFileJson = parseZoneFile(zoneFile)
if (!zoneFileJson.hasOwnProperty('$origin')) {
zoneFileJson = null
}
} catch (e) {
reject(e)
}

let tokenFileUrl: string | null = null
if (zoneFileJson && Object.keys(zoneFileJson).length > 0) {
tokenFileUrl = getTokenFileUrl(zoneFileJson)
} else {
let profile = null
try {
profile = JSON.parse(zoneFile)
profile = Person.fromLegacyFormat(profile).profile()
} catch (error) {
reject(error)
}
resolve(profile)
return
}

if (tokenFileUrl) {
fetchPrivate(tokenFileUrl)
.then(response => response.text())
.then(responseText => JSON.parse(responseText))
.then((responseJson) => {
const tokenRecords = responseJson
const profile = extractProfile(tokenRecords[0].token, publicKeyOrAddress)
resolve(profile)
})
.catch((error) => {
Logger.error(`resolveZoneFileToProfile: error fetching token file ${tokenFileUrl}: ${error}`)
reject(error)
})
} else {
Logger.debug('Token file url not found. Resolving to blank profile.')
resolve({})
}
})
}
1 change: 0 additions & 1 deletion packages/profile/src/profileSchemas/index.ts
@@ -1,4 +1,3 @@
export { Person } from './person'
export { Organization } from './organization'
export { CreativeWork } from './creativework'
export { getPersonFromLegacyFormat } from './personLegacy'
Expand Down

0 comments on commit c749612

Please sign in to comment.