APリファクタとLD-Signatureの検証に対応 (#6300)
* DbResolver * inbox types * 認証順を変更 * User/Keyあたりをまとめる * LD-Signatue * Validate contexts url * LD-Signature DocumentLoaderにProxyとTimeout
This commit is contained in:
		
							parent
							
								
									234294d564
								
							
						
					
					
						commit
						070f1f3c6e
					
				
					 20 changed files with 1052 additions and 233 deletions
				
			
		
							
								
								
									
										4
									
								
								COPYING
									
										
									
									
									
								
							
							
						
						
									
										4
									
								
								COPYING
									
										
									
									
									
								
							|  | @ -13,3 +13,7 @@ https://github.com/twitter/twemoji-parser/blob/master/LICENSE.md | ||||||
| Emoji keywords for Unicode 11 and below by Mu-An Chiou | Emoji keywords for Unicode 11 and below by Mu-An Chiou | ||||||
| License: MIT | License: MIT | ||||||
| https://github.com/muan/emojilib/blob/master/LICENSE | https://github.com/muan/emojilib/blob/master/LICENSE | ||||||
|  | 
 | ||||||
|  | RsaSignature2017 implementation by Transmute Industries Inc | ||||||
|  | License: MIT | ||||||
|  | https://github.com/transmute-industries/RsaSignature2017/blob/master/LICENSE | ||||||
|  |  | ||||||
|  | @ -53,6 +53,7 @@ | ||||||
| 		"@types/cbor": "5.0.0", | 		"@types/cbor": "5.0.0", | ||||||
| 		"@types/dateformat": "3.0.1", | 		"@types/dateformat": "3.0.1", | ||||||
| 		"@types/double-ended-queue": "2.1.1", | 		"@types/double-ended-queue": "2.1.1", | ||||||
|  | 		"@types/escape-regexp": "0.0.0", | ||||||
| 		"@types/glob": "7.1.1", | 		"@types/glob": "7.1.1", | ||||||
| 		"@types/gulp": "4.0.6", | 		"@types/gulp": "4.0.6", | ||||||
| 		"@types/gulp-rename": "0.0.33", | 		"@types/gulp-rename": "0.0.33", | ||||||
|  | @ -60,6 +61,7 @@ | ||||||
| 		"@types/is-url": "1.2.28", | 		"@types/is-url": "1.2.28", | ||||||
| 		"@types/js-yaml": "3.12.3", | 		"@types/js-yaml": "3.12.3", | ||||||
| 		"@types/jsdom": "16.2.1", | 		"@types/jsdom": "16.2.1", | ||||||
|  | 		"@types/jsonld": "1.5.1", | ||||||
| 		"@types/katex": "0.11.0", | 		"@types/katex": "0.11.0", | ||||||
| 		"@types/koa": "2.11.3", | 		"@types/koa": "2.11.3", | ||||||
| 		"@types/koa-bodyparser": "4.3.0", | 		"@types/koa-bodyparser": "4.3.0", | ||||||
|  | @ -126,6 +128,7 @@ | ||||||
| 		"dateformat": "3.0.3", | 		"dateformat": "3.0.3", | ||||||
| 		"diskusage": "1.1.3", | 		"diskusage": "1.1.3", | ||||||
| 		"double-ended-queue": "2.1.0-0", | 		"double-ended-queue": "2.1.0-0", | ||||||
|  | 		"escape-regexp": "0.0.1", | ||||||
| 		"eslint": "6.8.0", | 		"eslint": "6.8.0", | ||||||
| 		"eslint-plugin-vue": "6.2.2", | 		"eslint-plugin-vue": "6.2.2", | ||||||
| 		"eventemitter3": "4.0.0", | 		"eventemitter3": "4.0.0", | ||||||
|  | @ -156,6 +159,7 @@ | ||||||
| 		"jsdom": "16.2.2", | 		"jsdom": "16.2.2", | ||||||
| 		"json5": "2.1.3", | 		"json5": "2.1.3", | ||||||
| 		"json5-loader": "4.0.0", | 		"json5-loader": "4.0.0", | ||||||
|  | 		"jsonld": "3.1.0", | ||||||
| 		"jsrsasign": "8.0.15", | 		"jsrsasign": "8.0.15", | ||||||
| 		"katex": "0.11.1", | 		"katex": "0.11.1", | ||||||
| 		"koa": "2.11.0", | 		"koa": "2.11.0", | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								src/@types/http-signature.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								src/@types/http-signature.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -19,10 +19,12 @@ declare module 'http-signature' { | ||||||
| 		clockSkew?: number; | 		clockSkew?: number; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	interface IParsedSignature  { | 	interface IParsedSignature { | ||||||
| 		scheme: string; | 		scheme: string; | ||||||
| 		params: ISignature; | 		params: ISignature; | ||||||
| 		signingString: string; | 		signingString: string; | ||||||
|  | 		algorithm: string; | ||||||
|  | 		keyId: string; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	type RequestSignerConstructorOptions = | 	type RequestSignerConstructorOptions = | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ import procesObjectStorage from './processors/object-storage'; | ||||||
| import { queueLogger } from './logger'; | import { queueLogger } from './logger'; | ||||||
| import { DriveFile } from '../models/entities/drive-file'; | import { DriveFile } from '../models/entities/drive-file'; | ||||||
| import { getJobInfo } from './get-job-info'; | import { getJobInfo } from './get-job-info'; | ||||||
|  | import { IActivity } from '../remote/activitypub/type'; | ||||||
| 
 | 
 | ||||||
| function initializeQueue(name: string, limitPerSec = -1) { | function initializeQueue(name: string, limitPerSec = -1) { | ||||||
| 	return new Queue(name, { | 	return new Queue(name, { | ||||||
|  | @ -29,6 +30,12 @@ function initializeQueue(name: string, limitPerSec = -1) { | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export type InboxJobData = { | ||||||
|  | 	activity: IActivity, | ||||||
|  | 	/** HTTP-Signature */ | ||||||
|  | 	signature: httpSignature.IParsedSignature | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| function renderError(e: Error): any { | function renderError(e: Error): any { | ||||||
| 	return { | 	return { | ||||||
| 		stack: e?.stack, | 		stack: e?.stack, | ||||||
|  |  | ||||||
|  | @ -1,95 +1,111 @@ | ||||||
| import * as Bull from 'bull'; | import * as Bull from 'bull'; | ||||||
| import * as httpSignature from 'http-signature'; | import * as httpSignature from 'http-signature'; | ||||||
| import { IRemoteUser } from '../../models/entities/user'; |  | ||||||
| import perform from '../../remote/activitypub/perform'; | import perform from '../../remote/activitypub/perform'; | ||||||
| import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person'; |  | ||||||
| import Logger from '../../services/logger'; | import Logger from '../../services/logger'; | ||||||
| import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc'; | import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc'; | ||||||
| import { Instances, Users, UserPublickeys } from '../../models'; | import { Instances } from '../../models'; | ||||||
| import { instanceChart } from '../../services/chart'; | import { instanceChart } from '../../services/chart'; | ||||||
| import { UserPublickey } from '../../models/entities/user-publickey'; |  | ||||||
| import { fetchMeta } from '../../misc/fetch-meta'; | import { fetchMeta } from '../../misc/fetch-meta'; | ||||||
| import { toPuny } from '../../misc/convert-host'; | import { toPuny, extractDbHost } from '../../misc/convert-host'; | ||||||
| import { validActor } from '../../remote/activitypub/type'; | import { getApId } from '../../remote/activitypub/type'; | ||||||
| import { ensure } from '../../prelude/ensure'; |  | ||||||
| import { fetchNodeinfo } from '../../services/fetch-nodeinfo'; | import { fetchNodeinfo } from '../../services/fetch-nodeinfo'; | ||||||
|  | import { InboxJobData } from '..'; | ||||||
|  | import DbResolver from '../../remote/activitypub/db-resolver'; | ||||||
|  | import { resolvePerson } from '../../remote/activitypub/models/person'; | ||||||
|  | import { LdSignature } from '../../remote/activitypub/misc/ld-signature'; | ||||||
| 
 | 
 | ||||||
| const logger = new Logger('inbox'); | const logger = new Logger('inbox'); | ||||||
| 
 | 
 | ||||||
| // ユーザーのinboxにアクティビティが届いた時の処理
 | // ユーザーのinboxにアクティビティが届いた時の処理
 | ||||||
| export default async (job: Bull.Job): Promise<void> => { | export default async (job: Bull.Job<InboxJobData>): Promise<string> => { | ||||||
| 	const signature = job.data.signature; | 	const signature = job.data.signature;	// HTTP-signature
 | ||||||
| 	const activity = job.data.activity; | 	const activity = job.data.activity; | ||||||
| 
 | 
 | ||||||
| 	//#region Log
 | 	//#region Log
 | ||||||
| 	const info = Object.assign({}, activity); | 	const info = Object.assign({}, activity); | ||||||
| 	delete info['@context']; | 	delete info['@context']; | ||||||
| 	delete info['signature']; |  | ||||||
| 	logger.debug(JSON.stringify(info, null, 2)); | 	logger.debug(JSON.stringify(info, null, 2)); | ||||||
| 	//#endregion
 | 	//#endregion
 | ||||||
| 
 | 
 | ||||||
| 	const keyIdLower = signature.keyId.toLowerCase(); |  | ||||||
| 	let user: IRemoteUser; |  | ||||||
| 	let key: UserPublickey; |  | ||||||
| 
 |  | ||||||
| 	if (keyIdLower.startsWith('acct:')) { |  | ||||||
| 		logger.warn(`Old keyId is no longer supported. ${keyIdLower}`); |  | ||||||
| 		return; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// アクティビティ内のホストの検証
 |  | ||||||
| 	const host = toPuny(new URL(signature.keyId).hostname); | 	const host = toPuny(new URL(signature.keyId).hostname); | ||||||
| 	try { |  | ||||||
| 		ValidateActivity(activity, host); |  | ||||||
| 	} catch (e) { |  | ||||||
| 		logger.warn(e.message); |  | ||||||
| 		return; |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	// ブロックしてたら中断
 | 	// ブロックしてたら中断
 | ||||||
| 	const meta = await fetchMeta(); | 	const meta = await fetchMeta(); | ||||||
| 	if (meta.blockedHosts.includes(host)) { | 	if (meta.blockedHosts.includes(host)) { | ||||||
| 		logger.info(`Blocked request: ${host}`); | 		return `Blocked request: ${host}`; | ||||||
| 		return; |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	const _key = await UserPublickeys.findOne({ | 	const keyIdLower = signature.keyId.toLowerCase(); | ||||||
| 		keyId: signature.keyId | 	if (keyIdLower.startsWith('acct:')) { | ||||||
| 	}); | 		return `Old keyId is no longer supported. ${keyIdLower}`; | ||||||
| 
 |  | ||||||
| 	if (_key) { |  | ||||||
| 		// 登録済みユーザー
 |  | ||||||
| 		user = await Users.findOne(_key.userId) as IRemoteUser; |  | ||||||
| 		key = _key; |  | ||||||
| 	} else { |  | ||||||
| 		// 未登録ユーザーの場合はリモート解決
 |  | ||||||
| 		user = await resolvePerson(activity.actor) as IRemoteUser; |  | ||||||
| 		if (user == null) { |  | ||||||
| 			throw new Error('failed to resolve user'); |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		key = await UserPublickeys.findOne(user.id).then(ensure); |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了
 | 	const dbResolver = new DbResolver(); | ||||||
| 	if (activity.type === 'Update') { | 
 | ||||||
| 		if (activity.object && validActor.includes(activity.object.type)) { | 	// HTTP-Signature keyIdを元にDBから取得
 | ||||||
| 			if (!httpSignature.verifySignature(signature, key.keyPem)) { | 	let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId); | ||||||
| 				logger.warn('Update activity received, but signature verification failed.'); | 
 | ||||||
| 			} else { | 	// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
 | ||||||
| 				updatePerson(activity.actor, null, activity.object); | 	if (authUser == null) { | ||||||
|  | 		authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// それでもわからなければ終了
 | ||||||
|  | 	if (authUser == null) { | ||||||
|  | 		return `skip: failed to resolve user`; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// HTTP-Signatureの検証
 | ||||||
|  | 	if (!httpSignature.verifySignature(signature, authUser.key.keyPem)) { | ||||||
|  | 		return 'signature verification failed'; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// signatureのsignerは、activity.actorと一致する必要がある
 | ||||||
|  | 	if (authUser.user.uri !== activity.actor) { | ||||||
|  | 		// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
 | ||||||
|  | 		if (activity.signature) { | ||||||
|  | 			if (activity.signature.type !== 'RsaSignature2017') { | ||||||
|  | 				return `skip: unsupported LD-signature type ${activity.signature.type}`; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// activity.signature.creator: https://example.oom/users/user#main-key
 | ||||||
|  | 			// みたいになっててUserを引っ張れば公開キーも入ることを期待する
 | ||||||
|  | 			if (activity.signature.creator) { | ||||||
|  | 				const candicate = activity.signature.creator.replace(/#.*/, ''); | ||||||
|  | 				await resolvePerson(candicate).catch(() => null); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// keyIdからLD-Signatureのユーザーを取得
 | ||||||
|  | 			authUser = await dbResolver.getAuthUserFromKeyId(activity.signature.creator); | ||||||
|  | 			if (authUser == null) { | ||||||
|  | 				return `skip: LD-Signatureのユーザーが取得できませんでした`; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// LD-Signature検証
 | ||||||
|  | 			const ldSignature = new LdSignature(); | ||||||
|  | 			const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false); | ||||||
|  | 			if (!verified) { | ||||||
|  | 				return `skip: LD-Signatureの検証に失敗しました`; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// もう一度actorチェック
 | ||||||
|  | 			if (authUser.user.uri !== activity.actor) { | ||||||
|  | 				return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`; | ||||||
| 			} | 			} | ||||||
| 			return; |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (!httpSignature.verifySignature(signature, key.keyPem)) { | 	// activity.idがあればホストが署名者のホストであることを確認する
 | ||||||
| 		logger.error('signature verification failed'); | 	if (typeof activity.id === 'string') { | ||||||
| 		return; | 		const signerHost = extractDbHost(authUser.user.uri!); | ||||||
|  | 		const activityIdHost = extractDbHost(activity.id); | ||||||
|  | 		if (signerHost !== activityIdHost) { | ||||||
|  | 			return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`; | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Update stats
 | 	// Update stats
 | ||||||
| 	registerOrFetchInstanceDoc(user.host).then(i => { | 	registerOrFetchInstanceDoc(authUser.user.host).then(i => { | ||||||
| 		Instances.update(i.id, { | 		Instances.update(i.id, { | ||||||
| 			latestRequestReceivedAt: new Date(), | 			latestRequestReceivedAt: new Date(), | ||||||
| 			lastCommunicatedAt: new Date(), | 			lastCommunicatedAt: new Date(), | ||||||
|  | @ -102,42 +118,6 @@ export default async (job: Bull.Job): Promise<void> => { | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	// アクティビティを処理
 | 	// アクティビティを処理
 | ||||||
| 	await perform(user, activity); | 	await perform(authUser.user, activity); | ||||||
|  | 	return `ok`; | ||||||
| }; | }; | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Validate host in activity |  | ||||||
|  * @param activity Activity |  | ||||||
|  * @param host Expect host |  | ||||||
|  */ |  | ||||||
| function ValidateActivity(activity: any, host: string) { |  | ||||||
| 	// id (if exists)
 |  | ||||||
| 	if (typeof activity.id === 'string') { |  | ||||||
| 		const uriHost = toPuny(new URL(activity.id).hostname); |  | ||||||
| 		if (host !== uriHost) { |  | ||||||
| 			const diag = activity.signature ? '. Has LD-Signature. Forwarded?' : ''; |  | ||||||
| 			throw new Error(`activity.id(${activity.id}) has different host(${host})${diag}`); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// actor (if exists)
 |  | ||||||
| 	if (typeof activity.actor === 'string') { |  | ||||||
| 		const uriHost = toPuny(new URL(activity.actor).hostname); |  | ||||||
| 		if (host !== uriHost) throw new Error('activity.actor has different host'); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// For Create activity
 |  | ||||||
| 	if (activity.type === 'Create' && activity.object) { |  | ||||||
| 		// object.id (if exists)
 |  | ||||||
| 		if (typeof activity.object.id === 'string') { |  | ||||||
| 			const uriHost = toPuny(new URL(activity.object.id).hostname); |  | ||||||
| 			if (host !== uriHost) throw new Error('activity.object.id has different host'); |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// object.attributedTo (if exists)
 |  | ||||||
| 		if (typeof activity.object.attributedTo === 'string') { |  | ||||||
| 			const uriHost = toPuny(new URL(activity.object.attributedTo).hostname); |  | ||||||
| 			if (host !== uriHost) throw new Error('activity.object.attributedTo has different host'); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
							
								
								
									
										122
									
								
								src/remote/activitypub/db-resolver.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/remote/activitypub/db-resolver.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,122 @@ | ||||||
|  | import config from '../../config'; | ||||||
|  | import { Note } from '../../models/entities/note'; | ||||||
|  | import { User, IRemoteUser } from '../../models/entities/user'; | ||||||
|  | import { UserPublickey } from '../../models/entities/user-publickey'; | ||||||
|  | import { Notes, Users, UserPublickeys } from '../../models'; | ||||||
|  | import { IObject, getApId } from './type'; | ||||||
|  | import { resolvePerson } from './models/person'; | ||||||
|  | import { ensure } from '../../prelude/ensure'; | ||||||
|  | import escapeRegexp = require('escape-regexp'); | ||||||
|  | 
 | ||||||
|  | export default class DbResolver { | ||||||
|  | 	constructor() { | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * AP Note => Misskey Note in DB | ||||||
|  | 	 */ | ||||||
|  | 	public async getNoteFromApId(value: string | IObject): Promise<Note | null> { | ||||||
|  | 		const parsed = this.parseUri(value); | ||||||
|  | 
 | ||||||
|  | 		if (parsed.id) { | ||||||
|  | 			return (await Notes.findOne({ | ||||||
|  | 				id: parsed.id | ||||||
|  | 			})) || null; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (parsed.uri) { | ||||||
|  | 			return (await Notes.findOne({ | ||||||
|  | 				uri: parsed.uri | ||||||
|  | 			})) || null; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return null; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * AP Person => Misskey User in DB | ||||||
|  | 	 */ | ||||||
|  | 	public async getUserFromApId(value: string | IObject): Promise<User | null> { | ||||||
|  | 		const parsed = this.parseUri(value); | ||||||
|  | 
 | ||||||
|  | 		if (parsed.id) { | ||||||
|  | 			return (await Users.findOne({ | ||||||
|  | 				id: parsed.id | ||||||
|  | 			})) || null; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (parsed.uri) { | ||||||
|  | 			return (await Users.findOne({ | ||||||
|  | 				uri: parsed.uri | ||||||
|  | 			})) || null; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return null; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * AP KeyId => Misskey User and Key | ||||||
|  | 	 */ | ||||||
|  | 	public async getAuthUserFromKeyId(keyId: string): Promise<AuthUser | null> { | ||||||
|  | 		const key = await UserPublickeys.findOne({ | ||||||
|  | 			keyId | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		if (key == null) return null; | ||||||
|  | 
 | ||||||
|  | 		const user = await Users.findOne(key.userId) as IRemoteUser; | ||||||
|  | 
 | ||||||
|  | 		return { | ||||||
|  | 			user, | ||||||
|  | 			key | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * AP Actor id => Misskey User and Key | ||||||
|  | 	 */ | ||||||
|  | 	public async getAuthUserFromApId(uri: string): Promise<AuthUser | null> { | ||||||
|  | 		const user = await resolvePerson(uri) as IRemoteUser; | ||||||
|  | 
 | ||||||
|  | 		if (user == null) return null; | ||||||
|  | 
 | ||||||
|  | 		const key = await UserPublickeys.findOne(user.id).then(ensure); | ||||||
|  | 
 | ||||||
|  | 		return { | ||||||
|  | 			user, | ||||||
|  | 			key | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public parseUri(value: string | IObject): UriParseResult { | ||||||
|  | 		const uri = getApId(value); | ||||||
|  | 
 | ||||||
|  | 		const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/' + '(\\w+)' + '/' + '(\\w+)'); | ||||||
|  | 		const matchLocal = uri.match(localRegex); | ||||||
|  | 
 | ||||||
|  | 		if (matchLocal) { | ||||||
|  | 			return { | ||||||
|  | 				type: matchLocal[1], | ||||||
|  | 				id: matchLocal[2] | ||||||
|  | 			}; | ||||||
|  | 		} else { | ||||||
|  | 			return { | ||||||
|  | 				uri | ||||||
|  | 			}; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export type AuthUser = { | ||||||
|  | 	user: IRemoteUser; | ||||||
|  | 	key: UserPublickey; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | type UriParseResult = { | ||||||
|  | 	/** id in DB (local object only) */ | ||||||
|  | 	id?: string; | ||||||
|  | 	/** uri in DB (remote object only) */ | ||||||
|  | 	uri?: string; | ||||||
|  | 	/** hint of type (local object only, ex: notes, users) */ | ||||||
|  | 	type?: string | ||||||
|  | }; | ||||||
|  | @ -1,28 +1,22 @@ | ||||||
| import { IRemoteUser } from '../../../../models/entities/user'; | import { IRemoteUser } from '../../../../models/entities/user'; | ||||||
| import config from '../../../../config'; |  | ||||||
| import accept from '../../../../services/following/requests/accept'; | import accept from '../../../../services/following/requests/accept'; | ||||||
| import { IFollow } from '../../type'; | import { IFollow } from '../../type'; | ||||||
| import { Users } from '../../../../models'; | import DbResolver from '../../db-resolver'; | ||||||
| 
 | 
 | ||||||
| export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { | export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => { | ||||||
| 	const id = typeof activity.actor === 'string' ? activity.actor : activity.actor.id; | 	// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
 | ||||||
| 	if (id == null) throw new Error('missing id'); |  | ||||||
| 
 | 
 | ||||||
| 	if (!id.startsWith(config.url + '/')) { | 	const dbResolver = new DbResolver(); | ||||||
| 		return; | 	const follower = await dbResolver.getUserFromApId(activity.actor); | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	const follower = await Users.findOne({ |  | ||||||
| 		id: id.split('/').pop() |  | ||||||
| 	}); |  | ||||||
| 
 | 
 | ||||||
| 	if (follower == null) { | 	if (follower == null) { | ||||||
| 		throw new Error('follower not found'); | 		return `skip: follower not found`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (follower.host != null) { | 	if (follower.host != null) { | ||||||
| 		throw new Error('フォローリクエストしたユーザーはローカルユーザーではありません'); | 		return `skip: follower is not a local user`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	await accept(actor, follower); | 	await accept(actor, follower); | ||||||
|  | 	return `ok`; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,32 +1,22 @@ | ||||||
| import config from '../../../../config'; | import { IBlock } from '../../type'; | ||||||
| import { IBlock, getApId } from '../../type'; |  | ||||||
| import block from '../../../../services/blocking/create'; | import block from '../../../../services/blocking/create'; | ||||||
| import { apLogger } from '../../logger'; |  | ||||||
| import { Users } from '../../../../models'; |  | ||||||
| import { IRemoteUser } from '../../../../models/entities/user'; | import { IRemoteUser } from '../../../../models/entities/user'; | ||||||
|  | import DbResolver from '../../db-resolver'; | ||||||
| 
 | 
 | ||||||
| const logger = apLogger; | export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => { | ||||||
|  | 	// ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず
 | ||||||
| 
 | 
 | ||||||
| export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => { | 	const dbResolver = new DbResolver(); | ||||||
| 	const id = getApId(activity.object); | 	const blockee = await dbResolver.getUserFromApId(activity.object); | ||||||
| 
 |  | ||||||
| 	const uri = getApId(activity); |  | ||||||
| 
 |  | ||||||
| 	logger.info(`Block: ${uri}`); |  | ||||||
| 
 |  | ||||||
| 	if (!id.startsWith(config.url + '/')) { |  | ||||||
| 		return; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	const blockee = await Users.findOne(id.split('/').pop()); |  | ||||||
| 
 | 
 | ||||||
| 	if (blockee == null) { | 	if (blockee == null) { | ||||||
| 		throw new Error('blockee not found'); | 		return `skip: blockee not found`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (blockee.host != null) { | 	if (blockee.host != null) { | ||||||
| 		throw new Error('ブロックしようとしているユーザーはローカルユーザーではありません'); | 		return `skip: ブロックしようとしているユーザーはローカルユーザーではありません`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	block(actor, blockee); | 	await block(actor, blockee); | ||||||
|  | 	return `ok`; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -3,19 +3,39 @@ import { IRemoteUser } from '../../../../models/entities/user'; | ||||||
| import { createNote, fetchNote } from '../../models/note'; | import { createNote, fetchNote } from '../../models/note'; | ||||||
| import { getApId, IObject, ICreate } from '../../type'; | import { getApId, IObject, ICreate } from '../../type'; | ||||||
| import { getApLock } from '../../../../misc/app-lock'; | import { getApLock } from '../../../../misc/app-lock'; | ||||||
|  | import { extractDbHost } from '../../../../misc/convert-host'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * 投稿作成アクティビティを捌きます |  * 投稿作成アクティビティを捌きます | ||||||
|  */ |  */ | ||||||
| export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<void> { | export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> { | ||||||
| 	const uri = getApId(note); | 	const uri = getApId(note); | ||||||
| 
 | 
 | ||||||
|  | 	if (typeof note === 'object') { | ||||||
|  | 		if (actor.uri !== note.attributedTo) { | ||||||
|  | 			return `skip: actor.uri !== note.attributedTo`; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (typeof note.id === 'string') { | ||||||
|  | 			if (extractDbHost(actor.uri) !== extractDbHost(note.id)) { | ||||||
|  | 				return `skip: host in actor.uri !== note.id`; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	const unlock = await getApLock(uri); | 	const unlock = await getApLock(uri); | ||||||
| 
 | 
 | ||||||
| 	try { | 	try { | ||||||
| 		const exist = await fetchNote(note); | 		const exist = await fetchNote(note); | ||||||
| 		if (exist == null) { | 		if (exist) return 'skip: note exists'; | ||||||
| 			await createNote(note, resolver, silent); | 
 | ||||||
|  | 		await createNote(note, resolver, silent); | ||||||
|  | 		return 'ok'; | ||||||
|  | 	} catch (e) { | ||||||
|  | 		if (e.statusCode >= 400 && e.statusCode < 500) { | ||||||
|  | 			return `skip ${e.statusCode}`; | ||||||
|  | 		} else { | ||||||
|  | 			throw e; | ||||||
| 		} | 		} | ||||||
| 	} finally { | 	} finally { | ||||||
| 		unlock(); | 		unlock(); | ||||||
|  |  | ||||||
|  | @ -1,22 +1,31 @@ | ||||||
| import { IRemoteUser } from '../../../../models/entities/user'; | import { IRemoteUser } from '../../../../models/entities/user'; | ||||||
| import deleteNode from '../../../../services/note/delete'; | import deleteNode from '../../../../services/note/delete'; | ||||||
| import { apLogger } from '../../logger'; | import { apLogger } from '../../logger'; | ||||||
| import { Notes } from '../../../../models'; | import DbResolver from '../../db-resolver'; | ||||||
|  | import { getApLock } from '../../../../misc/app-lock'; | ||||||
| 
 | 
 | ||||||
| const logger = apLogger; | const logger = apLogger; | ||||||
| 
 | 
 | ||||||
| export default async function(actor: IRemoteUser, uri: string): Promise<void> { | export default async function(actor: IRemoteUser, uri: string): Promise<string> { | ||||||
| 	logger.info(`Deleting the Note: ${uri}`); | 	logger.info(`Deleting the Note: ${uri}`); | ||||||
| 
 | 
 | ||||||
| 	const note = await Notes.findOne({ uri }); | 	const unlock = await getApLock(uri); | ||||||
| 
 | 
 | ||||||
| 	if (note == null) { | 	try { | ||||||
| 		throw new Error('note not found'); | 		const dbResolver = new DbResolver(); | ||||||
|  | 		const note = await dbResolver.getNoteFromApId(uri); | ||||||
|  | 
 | ||||||
|  | 		if (note == null) { | ||||||
|  | 			return 'note not found'; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (note.userId !== actor.id) { | ||||||
|  | 			return '投稿を削除しようとしているユーザーは投稿の作成者ではありません'; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		await deleteNode(actor, note); | ||||||
|  | 		return 'ok: deleted'; | ||||||
|  | 	} finally { | ||||||
|  | 		unlock(); | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	if (note.userId !== actor.id) { |  | ||||||
| 		throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません'); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	await deleteNode(actor, note); |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,26 +1,20 @@ | ||||||
| import { IRemoteUser } from '../../../models/entities/user'; | import { IRemoteUser } from '../../../models/entities/user'; | ||||||
| import config from '../../../config'; |  | ||||||
| import follow from '../../../services/following/create'; | import follow from '../../../services/following/create'; | ||||||
| import { IFollow } from '../type'; | import { IFollow } from '../type'; | ||||||
| import { Users } from '../../../models'; | import DbResolver from '../db-resolver'; | ||||||
| 
 | 
 | ||||||
| export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { | export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => { | ||||||
| 	const id = typeof activity.object === 'string' ? activity.object : activity.object.id; | 	const dbResolver = new DbResolver(); | ||||||
| 	if (id == null) throw new Error('missing id'); | 	const followee = await dbResolver.getUserFromApId(activity.object); | ||||||
| 
 |  | ||||||
| 	if (!id.startsWith(config.url + '/')) { |  | ||||||
| 		return; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	const followee = await Users.findOne(id.split('/').pop()); |  | ||||||
| 
 | 
 | ||||||
| 	if (followee == null) { | 	if (followee == null) { | ||||||
| 		throw new Error('followee not found'); | 		return `skip: followee not found`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (followee.host != null) { | 	if (followee.host != null) { | ||||||
| 		throw new Error('フォローしようとしているユーザーはローカルユーザーではありません'); | 		return `skip: フォローしようとしているユーザーはローカルユーザーではありません`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	await follow(actor, followee, activity.id); | 	await follow(actor, followee, activity.id); | ||||||
|  | 	return `ok`; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,26 +1,22 @@ | ||||||
| import { IRemoteUser } from '../../../../models/entities/user'; | import { IRemoteUser } from '../../../../models/entities/user'; | ||||||
| import config from '../../../../config'; |  | ||||||
| import reject from '../../../../services/following/requests/reject'; | import reject from '../../../../services/following/requests/reject'; | ||||||
| import { IFollow } from '../../type'; | import { IFollow } from '../../type'; | ||||||
| import { Users } from '../../../../models'; | import DbResolver from '../../db-resolver'; | ||||||
| 
 | 
 | ||||||
| export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { | export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => { | ||||||
| 	const id = typeof activity.actor === 'string' ? activity.actor : activity.actor.id; | 	// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
 | ||||||
| 	if (id == null) throw new Error('missing id'); |  | ||||||
| 
 | 
 | ||||||
| 	if (!id.startsWith(config.url + '/')) { | 	const dbResolver = new DbResolver(); | ||||||
| 		return; | 	const follower = await dbResolver.getUserFromApId(activity.actor); | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	const follower = await Users.findOne(id.split('/').pop()); |  | ||||||
| 
 | 
 | ||||||
| 	if (follower == null) { | 	if (follower == null) { | ||||||
| 		throw new Error('follower not found'); | 		return `skip: follower not found`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (follower.host != null) { | 	if (follower.host != null) { | ||||||
| 		throw new Error('フォローリクエストしたユーザーはローカルユーザーではありません'); | 		return `skip: follower is not a local user`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	await reject(actor, follower); | 	await reject(actor, follower); | ||||||
|  | 	return `ok`; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,33 +1,20 @@ | ||||||
| import config from '../../../../config'; |  | ||||||
| import { IBlock } from '../../type'; | import { IBlock } from '../../type'; | ||||||
| import unblock from '../../../../services/blocking/delete'; | import unblock from '../../../../services/blocking/delete'; | ||||||
| import { apLogger } from '../../logger'; |  | ||||||
| import { IRemoteUser } from '../../../../models/entities/user'; | import { IRemoteUser } from '../../../../models/entities/user'; | ||||||
| import { Users } from '../../../../models'; | import DbResolver from '../../db-resolver'; | ||||||
| 
 | 
 | ||||||
| const logger = apLogger; | export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => { | ||||||
| 
 | 	const dbResolver = new DbResolver(); | ||||||
| export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => { | 	const blockee = await dbResolver.getUserFromApId(activity.object); | ||||||
| 	const id = typeof activity.object === 'string' ? activity.object : activity.object.id; |  | ||||||
| 	if (id == null) throw new Error('missing id'); |  | ||||||
| 
 |  | ||||||
| 	const uri = activity.id || activity; |  | ||||||
| 
 |  | ||||||
| 	logger.info(`UnBlock: ${uri}`); |  | ||||||
| 
 |  | ||||||
| 	if (!id.startsWith(config.url + '/')) { |  | ||||||
| 		return; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	const blockee = await Users.findOne(id.split('/').pop()); |  | ||||||
| 
 | 
 | ||||||
| 	if (blockee == null) { | 	if (blockee == null) { | ||||||
| 		throw new Error('blockee not found'); | 		return `skip: blockee not found`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (blockee.host != null) { | 	if (blockee.host != null) { | ||||||
| 		throw new Error('ブロック解除しようとしているユーザーはローカルユーザーではありません'); | 		return `skip: ブロック解除しようとしているユーザーはローカルユーザーではありません`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	unblock(actor, blockee); | 	await unblock(actor, blockee); | ||||||
|  | 	return `ok`; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,26 +1,20 @@ | ||||||
| import config from '../../../../config'; |  | ||||||
| import unfollow from '../../../../services/following/delete'; | import unfollow from '../../../../services/following/delete'; | ||||||
| import cancelRequest from '../../../../services/following/requests/cancel'; | import cancelRequest from '../../../../services/following/requests/cancel'; | ||||||
| import { IFollow } from '../../type'; | import { IFollow } from '../../type'; | ||||||
| import { IRemoteUser } from '../../../../models/entities/user'; | import { IRemoteUser } from '../../../../models/entities/user'; | ||||||
| import { Users, FollowRequests, Followings } from '../../../../models'; | import { FollowRequests, Followings } from '../../../../models'; | ||||||
|  | import DbResolver from '../../db-resolver'; | ||||||
| 
 | 
 | ||||||
| export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { | export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => { | ||||||
| 	const id = typeof activity.object === 'string' ? activity.object : activity.object.id; | 	const dbResolver = new DbResolver(); | ||||||
| 	if (id == null) throw new Error('missing id'); |  | ||||||
| 
 |  | ||||||
| 	if (!id.startsWith(config.url + '/')) { |  | ||||||
| 		return; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	const followee = await Users.findOne(id.split('/').pop()); |  | ||||||
| 
 | 
 | ||||||
|  | 	const followee = await dbResolver.getUserFromApId(activity.object); | ||||||
| 	if (followee == null) { | 	if (followee == null) { | ||||||
| 		throw new Error('followee not found'); | 		return `skip: followee not found`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (followee.host != null) { | 	if (followee.host != null) { | ||||||
| 		throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません'); | 		return `skip: フォロー解除しようとしているユーザーはローカルユーザーではありません`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	const req = await FollowRequests.findOne({ | 	const req = await FollowRequests.findOne({ | ||||||
|  | @ -35,9 +29,13 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { | ||||||
| 
 | 
 | ||||||
| 	if (req) { | 	if (req) { | ||||||
| 		await cancelRequest(followee, actor); | 		await cancelRequest(followee, actor); | ||||||
|  | 		return `ok: follow request canceled`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (following) { | 	if (following) { | ||||||
| 		await unfollow(actor, followee); | 		await unfollow(actor, followee); | ||||||
|  | 		return `ok: unfollowed`; | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	return `skip: リクエストもフォローもされていない`; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,28 +1,34 @@ | ||||||
| import { IRemoteUser } from '../../../../models/entities/user'; | import { IRemoteUser } from '../../../../models/entities/user'; | ||||||
| import { IUpdate, IObject } from '../../type'; | import { IUpdate, validActor } from '../../type'; | ||||||
| import { apLogger } from '../../logger'; | import { apLogger } from '../../logger'; | ||||||
| import { updateQuestion } from '../../models/question'; | import { updateQuestion } from '../../models/question'; | ||||||
|  | import Resolver from '../../resolver'; | ||||||
|  | import { updatePerson } from '../../models/person'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Updateアクティビティを捌きます |  * Updateアクティビティを捌きます | ||||||
|  */ |  */ | ||||||
| export default async (actor: IRemoteUser, activity: IUpdate): Promise<void> => { | export default async (actor: IRemoteUser, activity: IUpdate): Promise<string> => { | ||||||
| 	if ('actor' in activity && actor.uri !== activity.actor) { | 	if ('actor' in activity && actor.uri !== activity.actor) { | ||||||
| 		throw new Error('invalid actor'); | 		return `skip: invalid actor`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	apLogger.debug('Update'); | 	apLogger.debug('Update'); | ||||||
| 
 | 
 | ||||||
| 	const object = activity.object as IObject; | 	const resolver = new Resolver(); | ||||||
| 
 | 
 | ||||||
| 	switch (object.type) { | 	const object = await resolver.resolve(activity.object).catch(e => { | ||||||
| 		case 'Question': | 		apLogger.error(`Resolution failed: ${e}`); | ||||||
| 			apLogger.debug('Question'); | 		throw e; | ||||||
| 			await updateQuestion(object).catch(e => console.log(e)); | 	}); | ||||||
| 			break; |  | ||||||
| 
 | 
 | ||||||
| 		default: | 	if (validActor.includes(object.type)) { | ||||||
| 			apLogger.warn(`Unknown type: ${object.type}`); | 		await updatePerson(actor.uri!, resolver, object); | ||||||
| 			break; | 		return `ok: Person updated`; | ||||||
|  | 	} else if (object.type === 'Question') { | ||||||
|  | 		await updateQuestion(object).catch(e => console.log(e)); | ||||||
|  | 		return `ok: Question updated`; | ||||||
|  | 	} else { | ||||||
|  | 		return `skip: Unknown type: ${object.type}`; | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
|  |  | ||||||
							
								
								
									
										522
									
								
								src/remote/activitypub/misc/contexts.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										522
									
								
								src/remote/activitypub/misc/contexts.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,522 @@ | ||||||
|  | /* tslint:disable:quotemark indent */ | ||||||
|  | const id_v1 = { | ||||||
|  |   "@context": { | ||||||
|  |     "id": "@id", | ||||||
|  |     "type": "@type", | ||||||
|  | 
 | ||||||
|  |     "cred": "https://w3id.org/credentials#", | ||||||
|  |     "dc": "http://purl.org/dc/terms/", | ||||||
|  |     "identity": "https://w3id.org/identity#", | ||||||
|  |     "perm": "https://w3id.org/permissions#", | ||||||
|  |     "ps": "https://w3id.org/payswarm#", | ||||||
|  |     "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", | ||||||
|  |     "rdfs": "http://www.w3.org/2000/01/rdf-schema#", | ||||||
|  |     "sec": "https://w3id.org/security#", | ||||||
|  |     "schema": "http://schema.org/", | ||||||
|  |     "xsd": "http://www.w3.org/2001/XMLSchema#", | ||||||
|  | 
 | ||||||
|  |     "Group": "https://www.w3.org/ns/activitystreams#Group", | ||||||
|  | 
 | ||||||
|  |     "claim": {"@id": "cred:claim", "@type": "@id"}, | ||||||
|  |     "credential": {"@id": "cred:credential", "@type": "@id"}, | ||||||
|  |     "issued": {"@id": "cred:issued", "@type": "xsd:dateTime"}, | ||||||
|  |     "issuer": {"@id": "cred:issuer", "@type": "@id"}, | ||||||
|  |     "recipient": {"@id": "cred:recipient", "@type": "@id"}, | ||||||
|  |     "Credential": "cred:Credential", | ||||||
|  |     "CryptographicKeyCredential": "cred:CryptographicKeyCredential", | ||||||
|  | 
 | ||||||
|  |     "about": {"@id": "schema:about", "@type": "@id"}, | ||||||
|  |     "address": {"@id": "schema:address", "@type": "@id"}, | ||||||
|  |     "addressCountry": "schema:addressCountry", | ||||||
|  |     "addressLocality": "schema:addressLocality", | ||||||
|  |     "addressRegion": "schema:addressRegion", | ||||||
|  |     "comment": "rdfs:comment", | ||||||
|  |     "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, | ||||||
|  |     "creator": {"@id": "dc:creator", "@type": "@id"}, | ||||||
|  |     "description": "schema:description", | ||||||
|  |     "email": "schema:email", | ||||||
|  |     "familyName": "schema:familyName", | ||||||
|  |     "givenName": "schema:givenName", | ||||||
|  |     "image": {"@id": "schema:image", "@type": "@id"}, | ||||||
|  |     "label": "rdfs:label", | ||||||
|  |     "name": "schema:name", | ||||||
|  |     "postalCode": "schema:postalCode", | ||||||
|  |     "streetAddress": "schema:streetAddress", | ||||||
|  |     "title": "dc:title", | ||||||
|  |     "url": {"@id": "schema:url", "@type": "@id"}, | ||||||
|  |     "Person": "schema:Person", | ||||||
|  |     "PostalAddress": "schema:PostalAddress", | ||||||
|  |     "Organization": "schema:Organization", | ||||||
|  | 
 | ||||||
|  |     "identityService": {"@id": "identity:identityService", "@type": "@id"}, | ||||||
|  |     "idp": {"@id": "identity:idp", "@type": "@id"}, | ||||||
|  |     "Identity": "identity:Identity", | ||||||
|  | 
 | ||||||
|  |     "paymentProcessor": "ps:processor", | ||||||
|  |     "preferences": {"@id": "ps:preferences", "@type": "@vocab"}, | ||||||
|  | 
 | ||||||
|  |     "cipherAlgorithm": "sec:cipherAlgorithm", | ||||||
|  |     "cipherData": "sec:cipherData", | ||||||
|  |     "cipherKey": "sec:cipherKey", | ||||||
|  |     "digestAlgorithm": "sec:digestAlgorithm", | ||||||
|  |     "digestValue": "sec:digestValue", | ||||||
|  |     "domain": "sec:domain", | ||||||
|  |     "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, | ||||||
|  |     "initializationVector": "sec:initializationVector", | ||||||
|  |     "member": {"@id": "schema:member", "@type": "@id"}, | ||||||
|  |     "memberOf": {"@id": "schema:memberOf", "@type": "@id"}, | ||||||
|  |     "nonce": "sec:nonce", | ||||||
|  |     "normalizationAlgorithm": "sec:normalizationAlgorithm", | ||||||
|  |     "owner": {"@id": "sec:owner", "@type": "@id"}, | ||||||
|  |     "password": "sec:password", | ||||||
|  |     "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, | ||||||
|  |     "privateKeyPem": "sec:privateKeyPem", | ||||||
|  |     "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, | ||||||
|  |     "publicKeyPem": "sec:publicKeyPem", | ||||||
|  |     "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"}, | ||||||
|  |     "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, | ||||||
|  |     "signature": "sec:signature", | ||||||
|  |     "signatureAlgorithm": "sec:signatureAlgorithm", | ||||||
|  |     "signatureValue": "sec:signatureValue", | ||||||
|  |     "CryptographicKey": "sec:Key", | ||||||
|  |     "EncryptedMessage": "sec:EncryptedMessage", | ||||||
|  |     "GraphSignature2012": "sec:GraphSignature2012", | ||||||
|  |     "LinkedDataSignature2015": "sec:LinkedDataSignature2015", | ||||||
|  | 
 | ||||||
|  |     "accessControl": {"@id": "perm:accessControl", "@type": "@id"}, | ||||||
|  |     "writePermission": {"@id": "perm:writePermission", "@type": "@id"} | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const security_v1 = { | ||||||
|  |   "@context": { | ||||||
|  |     "id": "@id", | ||||||
|  |     "type": "@type", | ||||||
|  | 
 | ||||||
|  |     "dc": "http://purl.org/dc/terms/", | ||||||
|  |     "sec": "https://w3id.org/security#", | ||||||
|  |     "xsd": "http://www.w3.org/2001/XMLSchema#", | ||||||
|  | 
 | ||||||
|  |     "EcdsaKoblitzSignature2016": "sec:EcdsaKoblitzSignature2016", | ||||||
|  |     "Ed25519Signature2018": "sec:Ed25519Signature2018", | ||||||
|  |     "EncryptedMessage": "sec:EncryptedMessage", | ||||||
|  |     "GraphSignature2012": "sec:GraphSignature2012", | ||||||
|  |     "LinkedDataSignature2015": "sec:LinkedDataSignature2015", | ||||||
|  |     "LinkedDataSignature2016": "sec:LinkedDataSignature2016", | ||||||
|  |     "CryptographicKey": "sec:Key", | ||||||
|  | 
 | ||||||
|  |     "authenticationTag": "sec:authenticationTag", | ||||||
|  |     "canonicalizationAlgorithm": "sec:canonicalizationAlgorithm", | ||||||
|  |     "cipherAlgorithm": "sec:cipherAlgorithm", | ||||||
|  |     "cipherData": "sec:cipherData", | ||||||
|  |     "cipherKey": "sec:cipherKey", | ||||||
|  |     "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, | ||||||
|  |     "creator": {"@id": "dc:creator", "@type": "@id"}, | ||||||
|  |     "digestAlgorithm": "sec:digestAlgorithm", | ||||||
|  |     "digestValue": "sec:digestValue", | ||||||
|  |     "domain": "sec:domain", | ||||||
|  |     "encryptionKey": "sec:encryptionKey", | ||||||
|  |     "expiration": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, | ||||||
|  |     "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, | ||||||
|  |     "initializationVector": "sec:initializationVector", | ||||||
|  |     "iterationCount": "sec:iterationCount", | ||||||
|  |     "nonce": "sec:nonce", | ||||||
|  |     "normalizationAlgorithm": "sec:normalizationAlgorithm", | ||||||
|  |     "owner": {"@id": "sec:owner", "@type": "@id"}, | ||||||
|  |     "password": "sec:password", | ||||||
|  |     "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, | ||||||
|  |     "privateKeyPem": "sec:privateKeyPem", | ||||||
|  |     "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, | ||||||
|  |     "publicKeyBase58": "sec:publicKeyBase58", | ||||||
|  |     "publicKeyPem": "sec:publicKeyPem", | ||||||
|  |     "publicKeyWif": "sec:publicKeyWif", | ||||||
|  |     "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"}, | ||||||
|  |     "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, | ||||||
|  |     "salt": "sec:salt", | ||||||
|  |     "signature": "sec:signature", | ||||||
|  |     "signatureAlgorithm": "sec:signingAlgorithm", | ||||||
|  |     "signatureValue": "sec:signatureValue" | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const activitystreams = { | ||||||
|  |   "@context": { | ||||||
|  |     "@vocab": "_:", | ||||||
|  |     "xsd": "http://www.w3.org/2001/XMLSchema#", | ||||||
|  |     "as": "https://www.w3.org/ns/activitystreams#", | ||||||
|  |     "ldp": "http://www.w3.org/ns/ldp#", | ||||||
|  |     "vcard": "http://www.w3.org/2006/vcard/ns#", | ||||||
|  |     "id": "@id", | ||||||
|  |     "type": "@type", | ||||||
|  |     "Accept": "as:Accept", | ||||||
|  |     "Activity": "as:Activity", | ||||||
|  |     "IntransitiveActivity": "as:IntransitiveActivity", | ||||||
|  |     "Add": "as:Add", | ||||||
|  |     "Announce": "as:Announce", | ||||||
|  |     "Application": "as:Application", | ||||||
|  |     "Arrive": "as:Arrive", | ||||||
|  |     "Article": "as:Article", | ||||||
|  |     "Audio": "as:Audio", | ||||||
|  |     "Block": "as:Block", | ||||||
|  |     "Collection": "as:Collection", | ||||||
|  |     "CollectionPage": "as:CollectionPage", | ||||||
|  |     "Relationship": "as:Relationship", | ||||||
|  |     "Create": "as:Create", | ||||||
|  |     "Delete": "as:Delete", | ||||||
|  |     "Dislike": "as:Dislike", | ||||||
|  |     "Document": "as:Document", | ||||||
|  |     "Event": "as:Event", | ||||||
|  |     "Follow": "as:Follow", | ||||||
|  |     "Flag": "as:Flag", | ||||||
|  |     "Group": "as:Group", | ||||||
|  |     "Ignore": "as:Ignore", | ||||||
|  |     "Image": "as:Image", | ||||||
|  |     "Invite": "as:Invite", | ||||||
|  |     "Join": "as:Join", | ||||||
|  |     "Leave": "as:Leave", | ||||||
|  |     "Like": "as:Like", | ||||||
|  |     "Link": "as:Link", | ||||||
|  |     "Mention": "as:Mention", | ||||||
|  |     "Note": "as:Note", | ||||||
|  |     "Object": "as:Object", | ||||||
|  |     "Offer": "as:Offer", | ||||||
|  |     "OrderedCollection": "as:OrderedCollection", | ||||||
|  |     "OrderedCollectionPage": "as:OrderedCollectionPage", | ||||||
|  |     "Organization": "as:Organization", | ||||||
|  |     "Page": "as:Page", | ||||||
|  |     "Person": "as:Person", | ||||||
|  |     "Place": "as:Place", | ||||||
|  |     "Profile": "as:Profile", | ||||||
|  |     "Question": "as:Question", | ||||||
|  |     "Reject": "as:Reject", | ||||||
|  |     "Remove": "as:Remove", | ||||||
|  |     "Service": "as:Service", | ||||||
|  |     "TentativeAccept": "as:TentativeAccept", | ||||||
|  |     "TentativeReject": "as:TentativeReject", | ||||||
|  |     "Tombstone": "as:Tombstone", | ||||||
|  |     "Undo": "as:Undo", | ||||||
|  |     "Update": "as:Update", | ||||||
|  |     "Video": "as:Video", | ||||||
|  |     "View": "as:View", | ||||||
|  |     "Listen": "as:Listen", | ||||||
|  |     "Read": "as:Read", | ||||||
|  |     "Move": "as:Move", | ||||||
|  |     "Travel": "as:Travel", | ||||||
|  |     "IsFollowing": "as:IsFollowing", | ||||||
|  |     "IsFollowedBy": "as:IsFollowedBy", | ||||||
|  |     "IsContact": "as:IsContact", | ||||||
|  |     "IsMember": "as:IsMember", | ||||||
|  |     "subject": { | ||||||
|  |       "@id": "as:subject", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "relationship": { | ||||||
|  |       "@id": "as:relationship", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "actor": { | ||||||
|  |       "@id": "as:actor", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "attributedTo": { | ||||||
|  |       "@id": "as:attributedTo", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "attachment": { | ||||||
|  |       "@id": "as:attachment", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "bcc": { | ||||||
|  |       "@id": "as:bcc", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "bto": { | ||||||
|  |       "@id": "as:bto", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "cc": { | ||||||
|  |       "@id": "as:cc", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "context": { | ||||||
|  |       "@id": "as:context", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "current": { | ||||||
|  |       "@id": "as:current", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "first": { | ||||||
|  |       "@id": "as:first", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "generator": { | ||||||
|  |       "@id": "as:generator", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "icon": { | ||||||
|  |       "@id": "as:icon", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "image": { | ||||||
|  |       "@id": "as:image", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "inReplyTo": { | ||||||
|  |       "@id": "as:inReplyTo", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "items": { | ||||||
|  |       "@id": "as:items", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "instrument": { | ||||||
|  |       "@id": "as:instrument", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "orderedItems": { | ||||||
|  |       "@id": "as:items", | ||||||
|  |       "@type": "@id", | ||||||
|  |       "@container": "@list" | ||||||
|  |     }, | ||||||
|  |     "last": { | ||||||
|  |       "@id": "as:last", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "location": { | ||||||
|  |       "@id": "as:location", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "next": { | ||||||
|  |       "@id": "as:next", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "object": { | ||||||
|  |       "@id": "as:object", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "oneOf": { | ||||||
|  |       "@id": "as:oneOf", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "anyOf": { | ||||||
|  |       "@id": "as:anyOf", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "closed": { | ||||||
|  |       "@id": "as:closed", | ||||||
|  |       "@type": "xsd:dateTime" | ||||||
|  |     }, | ||||||
|  |     "origin": { | ||||||
|  |       "@id": "as:origin", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "accuracy": { | ||||||
|  |       "@id": "as:accuracy", | ||||||
|  |       "@type": "xsd:float" | ||||||
|  |     }, | ||||||
|  |     "prev": { | ||||||
|  |       "@id": "as:prev", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "preview": { | ||||||
|  |       "@id": "as:preview", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "replies": { | ||||||
|  |       "@id": "as:replies", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "result": { | ||||||
|  |       "@id": "as:result", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "audience": { | ||||||
|  |       "@id": "as:audience", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "partOf": { | ||||||
|  |       "@id": "as:partOf", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "tag": { | ||||||
|  |       "@id": "as:tag", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "target": { | ||||||
|  |       "@id": "as:target", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "to": { | ||||||
|  |       "@id": "as:to", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "url": { | ||||||
|  |       "@id": "as:url", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "altitude": { | ||||||
|  |       "@id": "as:altitude", | ||||||
|  |       "@type": "xsd:float" | ||||||
|  |     }, | ||||||
|  |     "content": "as:content", | ||||||
|  |     "contentMap": { | ||||||
|  |       "@id": "as:content", | ||||||
|  |       "@container": "@language" | ||||||
|  |     }, | ||||||
|  |     "name": "as:name", | ||||||
|  |     "nameMap": { | ||||||
|  |       "@id": "as:name", | ||||||
|  |       "@container": "@language" | ||||||
|  |     }, | ||||||
|  |     "duration": { | ||||||
|  |       "@id": "as:duration", | ||||||
|  |       "@type": "xsd:duration" | ||||||
|  |     }, | ||||||
|  |     "endTime": { | ||||||
|  |       "@id": "as:endTime", | ||||||
|  |       "@type": "xsd:dateTime" | ||||||
|  |     }, | ||||||
|  |     "height": { | ||||||
|  |       "@id": "as:height", | ||||||
|  |       "@type": "xsd:nonNegativeInteger" | ||||||
|  |     }, | ||||||
|  |     "href": { | ||||||
|  |       "@id": "as:href", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "hreflang": "as:hreflang", | ||||||
|  |     "latitude": { | ||||||
|  |       "@id": "as:latitude", | ||||||
|  |       "@type": "xsd:float" | ||||||
|  |     }, | ||||||
|  |     "longitude": { | ||||||
|  |       "@id": "as:longitude", | ||||||
|  |       "@type": "xsd:float" | ||||||
|  |     }, | ||||||
|  |     "mediaType": "as:mediaType", | ||||||
|  |     "published": { | ||||||
|  |       "@id": "as:published", | ||||||
|  |       "@type": "xsd:dateTime" | ||||||
|  |     }, | ||||||
|  |     "radius": { | ||||||
|  |       "@id": "as:radius", | ||||||
|  |       "@type": "xsd:float" | ||||||
|  |     }, | ||||||
|  |     "rel": "as:rel", | ||||||
|  |     "startIndex": { | ||||||
|  |       "@id": "as:startIndex", | ||||||
|  |       "@type": "xsd:nonNegativeInteger" | ||||||
|  |     }, | ||||||
|  |     "startTime": { | ||||||
|  |       "@id": "as:startTime", | ||||||
|  |       "@type": "xsd:dateTime" | ||||||
|  |     }, | ||||||
|  |     "summary": "as:summary", | ||||||
|  |     "summaryMap": { | ||||||
|  |       "@id": "as:summary", | ||||||
|  |       "@container": "@language" | ||||||
|  |     }, | ||||||
|  |     "totalItems": { | ||||||
|  |       "@id": "as:totalItems", | ||||||
|  |       "@type": "xsd:nonNegativeInteger" | ||||||
|  |     }, | ||||||
|  |     "units": "as:units", | ||||||
|  |     "updated": { | ||||||
|  |       "@id": "as:updated", | ||||||
|  |       "@type": "xsd:dateTime" | ||||||
|  |     }, | ||||||
|  |     "width": { | ||||||
|  |       "@id": "as:width", | ||||||
|  |       "@type": "xsd:nonNegativeInteger" | ||||||
|  |     }, | ||||||
|  |     "describes": { | ||||||
|  |       "@id": "as:describes", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "formerType": { | ||||||
|  |       "@id": "as:formerType", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "deleted": { | ||||||
|  |       "@id": "as:deleted", | ||||||
|  |       "@type": "xsd:dateTime" | ||||||
|  |     }, | ||||||
|  |     "inbox": { | ||||||
|  |       "@id": "ldp:inbox", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "outbox": { | ||||||
|  |       "@id": "as:outbox", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "following": { | ||||||
|  |       "@id": "as:following", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "followers": { | ||||||
|  |       "@id": "as:followers", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "streams": { | ||||||
|  |       "@id": "as:streams", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "preferredUsername": "as:preferredUsername", | ||||||
|  |     "endpoints": { | ||||||
|  |       "@id": "as:endpoints", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "uploadMedia": { | ||||||
|  |       "@id": "as:uploadMedia", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "proxyUrl": { | ||||||
|  |       "@id": "as:proxyUrl", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "liked": { | ||||||
|  |       "@id": "as:liked", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "oauthAuthorizationEndpoint": { | ||||||
|  |       "@id": "as:oauthAuthorizationEndpoint", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "oauthTokenEndpoint": { | ||||||
|  |       "@id": "as:oauthTokenEndpoint", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "provideClientKey": { | ||||||
|  |       "@id": "as:provideClientKey", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "signClientKey": { | ||||||
|  |       "@id": "as:signClientKey", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "sharedInbox": { | ||||||
|  |       "@id": "as:sharedInbox", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "Public": { | ||||||
|  |       "@id": "as:Public", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "source": "as:source", | ||||||
|  |     "likes": { | ||||||
|  |       "@id": "as:likes", | ||||||
|  |       "@type": "@id" | ||||||
|  |     }, | ||||||
|  |     "shares": { | ||||||
|  |       "@id": "as:shares", | ||||||
|  |       "@type": "@id" | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const CONTEXTS: Record<string, any> = { | ||||||
|  |   "https://w3id.org/identity/v1": id_v1, | ||||||
|  |   "https://w3id.org/security/v1": security_v1, | ||||||
|  |   "https://www.w3.org/ns/activitystreams": activitystreams, | ||||||
|  | }; | ||||||
							
								
								
									
										133
									
								
								src/remote/activitypub/misc/ld-signature.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/remote/activitypub/misc/ld-signature.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,133 @@ | ||||||
|  | import * as crypto from 'crypto'; | ||||||
|  | import * as jsonld from 'jsonld'; | ||||||
|  | import { CONTEXTS } from './contexts'; | ||||||
|  | import fetch from 'node-fetch'; | ||||||
|  | import { httpAgent, httpsAgent } from '../../../misc/fetch'; | ||||||
|  | 
 | ||||||
|  | // RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
 | ||||||
|  | 
 | ||||||
|  | export class LdSignature { | ||||||
|  | 	public debug = false; | ||||||
|  | 	public preLoad = true; | ||||||
|  | 	public loderTimeout = 10 * 1000; | ||||||
|  | 
 | ||||||
|  | 	constructor() { | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise<any> { | ||||||
|  | 		const options = { | ||||||
|  | 			type: 'RsaSignature2017', | ||||||
|  | 			creator, | ||||||
|  | 			domain, | ||||||
|  | 			nonce: crypto.randomBytes(16).toString('hex'), | ||||||
|  | 			created: (created || new Date()).toISOString() | ||||||
|  | 		} as { | ||||||
|  | 			type: string; | ||||||
|  | 			creator: string; | ||||||
|  | 			domain: string; | ||||||
|  | 			nonce: string; | ||||||
|  | 			created: string; | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
|  | 		if (!domain) { | ||||||
|  | 			delete options.domain; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		const toBeSigned = await this.createVerifyData(data, options); | ||||||
|  | 
 | ||||||
|  | 		const signer = crypto.createSign('sha256'); | ||||||
|  | 		signer.update(toBeSigned); | ||||||
|  | 		signer.end(); | ||||||
|  | 
 | ||||||
|  | 		const signature = signer.sign(privateKey); | ||||||
|  | 
 | ||||||
|  | 		return { | ||||||
|  | 			...data, | ||||||
|  | 			signature: { | ||||||
|  | 				...options, | ||||||
|  | 				signatureValue: signature.toString('base64') | ||||||
|  | 			} | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public async verifyRsaSignature2017(data: any, publicKey: string): Promise<boolean> { | ||||||
|  | 		const toBeSigned = await this.createVerifyData(data, data.signature); | ||||||
|  | 		const verifier = crypto.createVerify('sha256'); | ||||||
|  | 		verifier.update(toBeSigned); | ||||||
|  | 		return verifier.verify(publicKey, data.signature.signatureValue, 'base64'); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public async createVerifyData(data: any, options: any) { | ||||||
|  | 		const transformedOptions = { | ||||||
|  | 			...options, | ||||||
|  | 			'@context': 'https://w3id.org/identity/v1' | ||||||
|  | 		}; | ||||||
|  | 		delete transformedOptions['type']; | ||||||
|  | 		delete transformedOptions['id']; | ||||||
|  | 		delete transformedOptions['signatureValue']; | ||||||
|  | 		const canonizedOptions = await this.normalize(transformedOptions); | ||||||
|  | 		const optionsHash = this.sha256(canonizedOptions); | ||||||
|  | 		const transformedData = { ...data }; | ||||||
|  | 		delete transformedData['signature']; | ||||||
|  | 		const cannonidedData = await this.normalize(transformedData); | ||||||
|  | 		const documentHash = this.sha256(cannonidedData); | ||||||
|  | 		const verifyData = `${optionsHash}${documentHash}`; | ||||||
|  | 		return verifyData; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public async normalize(data: any) { | ||||||
|  | 		const customLoader = this.getLoader(); | ||||||
|  | 		return await jsonld.normalize(data, { | ||||||
|  | 			documentLoader: customLoader | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private getLoader() { | ||||||
|  | 		return async (url: string): Promise<any> => { | ||||||
|  | 			if (!url.match('^https?\:\/\/')) throw `Invalid URL ${url}`; | ||||||
|  | 
 | ||||||
|  | 			if (this.preLoad) { | ||||||
|  | 				if (url in CONTEXTS) { | ||||||
|  | 					if (this.debug) console.debug(`HIT: ${url}`); | ||||||
|  | 					return { | ||||||
|  | 						contextUrl: null, | ||||||
|  | 						document: CONTEXTS[url], | ||||||
|  | 						documentUrl: url | ||||||
|  | 					}; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if (this.debug) console.debug(`MISS: ${url}`); | ||||||
|  | 			const document = await this.fetchDocument(url); | ||||||
|  | 			return { | ||||||
|  | 				contextUrl: null, | ||||||
|  | 				document: document, | ||||||
|  | 				documentUrl: url | ||||||
|  | 			}; | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private async fetchDocument(url: string) { | ||||||
|  | 		const json = await fetch(url, { | ||||||
|  | 			headers: { | ||||||
|  | 				Accept: 'application/ld+json, application/json', | ||||||
|  | 			}, | ||||||
|  | 			timeout: this.loderTimeout, | ||||||
|  | 			agent: u => u.protocol == 'http:' ? httpAgent : httpsAgent, | ||||||
|  | 		}).then(res => { | ||||||
|  | 			if (!res.ok) { | ||||||
|  | 				throw `${res.status} ${res.statusText}`; | ||||||
|  | 			} else { | ||||||
|  | 				return res.json(); | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		return json; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public sha256(data: string): string { | ||||||
|  | 		const hash = crypto.createHash('sha256'); | ||||||
|  | 		hash.update(data); | ||||||
|  | 		return hash.digest('hex'); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -15,7 +15,7 @@ import { apLogger } from '../logger'; | ||||||
| import { DriveFile } from '../../../models/entities/drive-file'; | import { DriveFile } from '../../../models/entities/drive-file'; | ||||||
| import { deliverQuestionUpdate } from '../../../services/note/polls/update'; | import { deliverQuestionUpdate } from '../../../services/note/polls/update'; | ||||||
| import { extractDbHost, toPuny } from '../../../misc/convert-host'; | import { extractDbHost, toPuny } from '../../../misc/convert-host'; | ||||||
| import { Notes, Emojis, Polls, MessagingMessages } from '../../../models'; | import { Emojis, Polls, MessagingMessages } from '../../../models'; | ||||||
| import { Note } from '../../../models/entities/note'; | import { Note } from '../../../models/entities/note'; | ||||||
| import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji } from '../type'; | import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji } from '../type'; | ||||||
| import { Emoji } from '../../../models/entities/emoji'; | import { Emoji } from '../../../models/entities/emoji'; | ||||||
|  | @ -26,6 +26,7 @@ import { getApLock } from '../../../misc/app-lock'; | ||||||
| import { createMessage } from '../../../services/messages/create'; | import { createMessage } from '../../../services/messages/create'; | ||||||
| import { parseAudience } from '../audience'; | import { parseAudience } from '../audience'; | ||||||
| import { extractApMentions } from './mention'; | import { extractApMentions } from './mention'; | ||||||
|  | import DbResolver from '../db-resolver'; | ||||||
| 
 | 
 | ||||||
| const logger = apLogger; | const logger = apLogger; | ||||||
| 
 | 
 | ||||||
|  | @ -56,24 +57,9 @@ export function validateNote(object: any, uri: string) { | ||||||
|  * |  * | ||||||
|  * Misskeyに対象のNoteが登録されていればそれを返します。 |  * Misskeyに対象のNoteが登録されていればそれを返します。 | ||||||
|  */ |  */ | ||||||
| export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> { | export async function fetchNote(object: string | IObject): Promise<Note | null> { | ||||||
| 	const uri = getApId(value); | 	const dbResolver = new DbResolver(); | ||||||
| 
 | 	return await dbResolver.getNoteFromApId(object); | ||||||
| 	// URIがこのサーバーを指しているならデータベースからフェッチ
 |  | ||||||
| 	if (uri.startsWith(config.url + '/')) { |  | ||||||
| 		const id = uri.split('/').pop(); |  | ||||||
| 		return await Notes.findOne(id).then(x => x || null); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	//#region このサーバーに既に登録されていたらそれを返す
 |  | ||||||
| 	const exist = await Notes.findOne({ uri }); |  | ||||||
| 
 |  | ||||||
| 	if (exist) { |  | ||||||
| 		return exist; |  | ||||||
| 	} |  | ||||||
| 	//#endregion
 |  | ||||||
| 
 |  | ||||||
| 	return null; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  |  | ||||||
|  | @ -67,6 +67,15 @@ export interface IActivity extends IObject { | ||||||
| 	actor: IObject | string; | 	actor: IObject | string; | ||||||
| 	object: IObject | string; | 	object: IObject | string; | ||||||
| 	target?: IObject | string; | 	target?: IObject | string; | ||||||
|  | 	/** LD-Signature */ | ||||||
|  | 	signature?: { | ||||||
|  | 		type: string; | ||||||
|  | 		created: Date; | ||||||
|  | 		creator: string; | ||||||
|  | 		domain?: string; | ||||||
|  | 		nonce?: string; | ||||||
|  | 		signatureValue: string; | ||||||
|  | 	}; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface ICollection extends IObject { | export interface ICollection extends IObject { | ||||||
|  |  | ||||||
							
								
								
									
										56
									
								
								yarn.lock
									
										
									
									
									
								
							
							
						
						
									
										56
									
								
								yarn.lock
									
										
									
									
									
								
							|  | @ -293,6 +293,11 @@ | ||||||
|   resolved "https://registry.yarnpkg.com/@types/double-ended-queue/-/double-ended-queue-2.1.1.tgz#f077386134f0f736d927812c85c43a04f21ddc27" |   resolved "https://registry.yarnpkg.com/@types/double-ended-queue/-/double-ended-queue-2.1.1.tgz#f077386134f0f736d927812c85c43a04f21ddc27" | ||||||
|   integrity sha512-O2+umEIlHBVyi+ePmucPjpINqTvSnsz+hAok0D4IpvrOsIsDr6c34B0AbNXW2UDVYuxbv51z5dxnrRt23ohgWg== |   integrity sha512-O2+umEIlHBVyi+ePmucPjpINqTvSnsz+hAok0D4IpvrOsIsDr6c34B0AbNXW2UDVYuxbv51z5dxnrRt23ohgWg== | ||||||
| 
 | 
 | ||||||
|  | "@types/escape-regexp@0.0.0": | ||||||
|  |   version "0.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/escape-regexp/-/escape-regexp-0.0.0.tgz#bff0225f9ef30d0dbdbe0e2a24283ee5342990c3" | ||||||
|  |   integrity sha512-HTansGo4tJ7K7W9I9LBdQqnHtPB/Y7tlS+EMrkboaAQLsRPhRpHaqAHe01K1HVXM5e1u1IplRd8EBh+pJrp7Dg== | ||||||
|  | 
 | ||||||
| "@types/eslint-visitor-keys@^1.0.0": | "@types/eslint-visitor-keys@^1.0.0": | ||||||
|   version "1.0.0" |   version "1.0.0" | ||||||
|   resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" |   resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" | ||||||
|  | @ -414,6 +419,11 @@ | ||||||
|   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" |   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" | ||||||
|   integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== |   integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== | ||||||
| 
 | 
 | ||||||
|  | "@types/jsonld@1.5.1": | ||||||
|  |   version "1.5.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/jsonld/-/jsonld-1.5.1.tgz#361e98bdc07814f5c98a42b4063430b243a8fa9b" | ||||||
|  |   integrity sha512-8XI88iiCBVqmNCMBqPOgJhJPPuiIW1Tp2sXqe3NwD137ljhQVkDWY8cuYBBDZQoBYfGzUJvja527bbwqVbRnHQ== | ||||||
|  | 
 | ||||||
| "@types/katex@0.11.0": | "@types/katex@0.11.0": | ||||||
|   version "0.11.0" |   version "0.11.0" | ||||||
|   resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.11.0.tgz#b16c54ee670925ffef0616beae9e90c557e17334" |   resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.11.0.tgz#b16c54ee670925ffef0616beae9e90c557e17334" | ||||||
|  | @ -1867,6 +1877,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001043: | ||||||
|   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001048.tgz#4bb4f1bc2eb304e5e1154da80b93dee3f1cf447e" |   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001048.tgz#4bb4f1bc2eb304e5e1154da80b93dee3f1cf447e" | ||||||
|   integrity sha512-g1iSHKVxornw0K8LG9LLdf+Fxnv7T1Z+mMsf0/YYLclQX4Cd522Ap0Lrw6NFqHgezit78dtyWxzlV2Xfc7vgRg== |   integrity sha512-g1iSHKVxornw0K8LG9LLdf+Fxnv7T1Z+mMsf0/YYLclQX4Cd522Ap0Lrw6NFqHgezit78dtyWxzlV2Xfc7vgRg== | ||||||
| 
 | 
 | ||||||
|  | canonicalize@^1.0.1: | ||||||
|  |   version "1.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.1.tgz#657b4f3fa38a6ecb97a9e5b7b26d7a19cc6e0da9" | ||||||
|  |   integrity sha512-N3cmB3QLhS5TJ5smKFf1w42rJXWe6C1qP01z4dxJiI5v269buii4fLHWETDyf7yEd0azGLNC63VxNMiPd2u0Cg== | ||||||
|  | 
 | ||||||
| caseless@~0.12.0: | caseless@~0.12.0: | ||||||
|   version "0.12.0" |   version "0.12.0" | ||||||
|   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" |   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" | ||||||
|  | @ -5178,6 +5193,19 @@ json5@^1.0.1: | ||||||
|   dependencies: |   dependencies: | ||||||
|     minimist "^1.2.0" |     minimist "^1.2.0" | ||||||
| 
 | 
 | ||||||
|  | jsonld@3.1.0: | ||||||
|  |   version "3.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-3.1.0.tgz#826a7a598942a3969d41301388c51b812a73c6d0" | ||||||
|  |   integrity sha512-9x/AbUsXMMZBPxGy98Y8qMz7CU3WCq1n0KcNfR1P4RZml5oZiEQM+53/VtStOHUTUyC6fX9Sml5olUOZRARTZw== | ||||||
|  |   dependencies: | ||||||
|  |     canonicalize "^1.0.1" | ||||||
|  |     lru-cache "^5.1.1" | ||||||
|  |     object.fromentries "^2.0.2" | ||||||
|  |     rdf-canonize "^1.0.2" | ||||||
|  |     request "^2.88.0" | ||||||
|  |     semver "^6.3.0" | ||||||
|  |     xmldom "0.1.19" | ||||||
|  | 
 | ||||||
| jsprim@^1.2.2: | jsprim@^1.2.2: | ||||||
|   version "1.4.1" |   version "1.4.1" | ||||||
|   resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" |   resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" | ||||||
|  | @ -6258,6 +6286,11 @@ node-fetch@2.6.0: | ||||||
|   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" |   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" | ||||||
|   integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== |   integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== | ||||||
| 
 | 
 | ||||||
|  | node-forge@^0.9.1: | ||||||
|  |   version "0.9.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5" | ||||||
|  |   integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ== | ||||||
|  | 
 | ||||||
| node-object-hash@^1.2.0: | node-object-hash@^1.2.0: | ||||||
|   version "1.4.2" |   version "1.4.2" | ||||||
|   resolved "https://registry.yarnpkg.com/node-object-hash/-/node-object-hash-1.4.2.tgz#385833d85b229902b75826224f6077be969a9e94" |   resolved "https://registry.yarnpkg.com/node-object-hash/-/node-object-hash-1.4.2.tgz#385833d85b229902b75826224f6077be969a9e94" | ||||||
|  | @ -6435,6 +6468,16 @@ object.defaults@^1.0.0, object.defaults@^1.1.0: | ||||||
|     for-own "^1.0.0" |     for-own "^1.0.0" | ||||||
|     isobject "^3.0.0" |     isobject "^3.0.0" | ||||||
| 
 | 
 | ||||||
|  | object.fromentries@^2.0.2: | ||||||
|  |   version "2.0.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9" | ||||||
|  |   integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ== | ||||||
|  |   dependencies: | ||||||
|  |     define-properties "^1.1.3" | ||||||
|  |     es-abstract "^1.17.0-next.1" | ||||||
|  |     function-bind "^1.1.1" | ||||||
|  |     has "^1.0.3" | ||||||
|  | 
 | ||||||
| object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0: | object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0: | ||||||
|   version "2.1.0" |   version "2.1.0" | ||||||
|   resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" |   resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" | ||||||
|  | @ -7690,6 +7733,14 @@ rc@^1.2.7: | ||||||
|     minimist "^1.2.0" |     minimist "^1.2.0" | ||||||
|     strip-json-comments "~2.0.1" |     strip-json-comments "~2.0.1" | ||||||
| 
 | 
 | ||||||
|  | rdf-canonize@^1.0.2: | ||||||
|  |   version "1.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/rdf-canonize/-/rdf-canonize-1.1.0.tgz#61d1609bbdb3234b8f38c9c34ad889bf670e089d" | ||||||
|  |   integrity sha512-DV06OnhVfl2zcZJQCt+YvU+hoZVgpyQpNFLeAmghq8RJybUxD3B4LRzlBquYS5k+LLd8/c3g5Gnhkqjw5qRMvg== | ||||||
|  |   dependencies: | ||||||
|  |     node-forge "^0.9.1" | ||||||
|  |     semver "^6.3.0" | ||||||
|  | 
 | ||||||
| read-pkg-up@^1.0.1: | read-pkg-up@^1.0.1: | ||||||
|   version "1.0.1" |   version "1.0.1" | ||||||
|   resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" |   resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" | ||||||
|  | @ -10225,6 +10276,11 @@ xmlchars@^2.2.0: | ||||||
|   resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" |   resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" | ||||||
|   integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== |   integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== | ||||||
| 
 | 
 | ||||||
|  | xmldom@0.1.19: | ||||||
|  |   version "0.1.19" | ||||||
|  |   resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc" | ||||||
|  |   integrity sha1-Yx/Ad3bv2EEYvyUXGzftTQdaCrw= | ||||||
|  | 
 | ||||||
| xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: | xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: | ||||||
|   version "4.0.2" |   version "4.0.2" | ||||||
|   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" |   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue