sigit.io/src/utils/meta.ts

214 lines
5.6 KiB
TypeScript
Raw Normal View History

import { CreateSignatureEventContent, Meta } from '../types'
import {
decryptArrayBuffer,
encryptArrayBuffer,
fromUnixTimestamp,
getHash,
parseJson,
uploadToFileStorage
} from '.'
import { Event, verifyEvent } from 'nostr-tools'
import { toast } from 'react-toastify'
import { extractFileExtensions } from './file'
import { handleError } from '../types/errors'
import {
MetaParseError,
MetaParseErrorType
} from '../types/errors/MetaParseError'
import axios from 'axios'
import {
MetaStorageError,
MetaStorageErrorType
} from '../types/errors/MetaStorageError'
export enum SignStatus {
Signed = 'Signed',
Awaiting = 'Awaiting',
Pending = 'Pending',
Invalid = 'Invalid',
Viewer = 'Viewer'
}
export enum SigitStatus {
Partial = 'In-Progress',
Complete = 'Completed'
}
export interface SigitCardDisplayInfo {
createdAt?: number
title?: string
submittedBy?: `npub1${string}`
signers: `npub1${string}`[]
fileExtensions: string[]
signedStatus: SigitStatus
isValid: boolean
}
/**
* Wrapper for event parser that throws custom SigitMetaParseError with cause and context
* @param raw Raw string for parsing
* @returns parsed Event
*/
export const parseNostrEvent = async (raw: string): Promise<Event> => {
try {
const event = await parseJson<Event>(raw)
return event
} catch (error) {
throw new MetaParseError(MetaParseErrorType.PARSE_ERROR_EVENT, {
cause: handleError(error),
context: raw
})
}
}
/**
* Wrapper for event content parser that throws custom SigitMetaParseError with cause and context
* @param raw Raw string for parsing
* @returns parsed CreateSignatureEventContent
*/
export const parseCreateSignatureEventContent = async (
raw: string
): Promise<CreateSignatureEventContent> => {
try {
const createSignatureEventContent =
await parseJson<CreateSignatureEventContent>(raw)
return createSignatureEventContent
} catch (error) {
throw new MetaParseError(
MetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT_CONTENT,
{
cause: handleError(error),
context: raw
}
)
}
}
/**
* Extracts only necessary metadata for the card display
* @param meta Sigit metadata
* @returns SigitCardDisplayInfo
*/
export const extractSigitCardDisplayInfo = async (meta: Meta) => {
if (!meta?.createSignature) return
const sigitInfo: SigitCardDisplayInfo = {
signers: [],
fileExtensions: [],
signedStatus: SigitStatus.Partial,
isValid: false
}
try {
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
sigitInfo.isValid = verifyEvent(createSignatureEvent)
// created_at in nostr events are stored in seconds
sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at)
const createSignatureContent = await parseCreateSignatureEventContent(
createSignatureEvent.content
)
const files = Object.keys(createSignatureContent.fileHashes)
const { extensions } = extractFileExtensions(files)
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
const isCompletelySigned = createSignatureContent.signers.every((signer) =>
signedBy.includes(signer)
)
sigitInfo.title = createSignatureContent.title
sigitInfo.submittedBy = createSignatureEvent.pubkey as `npub1${string}`
sigitInfo.signers = createSignatureContent.signers
sigitInfo.fileExtensions = extensions
if (isCompletelySigned) {
sigitInfo.signedStatus = SigitStatus.Complete
}
return sigitInfo
} catch (error) {
if (error instanceof MetaParseError) {
toast.error(error.message)
console.error(error.name, error.message, error.cause, error.context)
} else {
console.error('Unexpected error', error)
}
}
}
export const uploadMetaToFileStorage = async (
meta: Meta,
encryptionKey: string | undefined
) => {
// Value is the stringified meta object
const value = JSON.stringify(meta)
const encoder = new TextEncoder()
// Encode it to the arrayBuffer
const uint8Array = encoder.encode(value)
if (!encryptionKey) {
throw new MetaStorageError(MetaStorageErrorType.ENCRYPTION_KEY_REQUIRED)
}
// Encrypt the file contents with the same encryption key from the create signature
const encryptedArrayBuffer = await encryptArrayBuffer(
uint8Array,
encryptionKey
)
const hash = await getHash(encryptedArrayBuffer)
if (!hash) {
throw new MetaStorageError(MetaStorageErrorType.HASHING_FAILED)
}
// Create the encrypted json file from array buffer and hash
const file = new File([encryptedArrayBuffer], `${hash}.json`)
const url = await uploadToFileStorage(file)
return url
}
export const fetchMetaFromFileStorage = async (
url: string,
encryptionKey: string | undefined
) => {
if (!encryptionKey) {
throw new MetaStorageError(MetaStorageErrorType.ENCRYPTION_KEY_REQUIRED)
}
const encryptedArrayBuffer = await axios.get(url, {
responseType: 'arraybuffer'
})
// Verify hash
const parts = url.split('/')
const urlHash = parts[parts.length - 1]
const hash = await getHash(encryptedArrayBuffer.data)
if (hash !== urlHash) {
throw new MetaStorageError(MetaStorageErrorType.HASH_VERIFICATION_FAILED)
}
const arrayBuffer = await decryptArrayBuffer(
encryptedArrayBuffer.data,
encryptionKey
).catch((err) => {
throw new MetaStorageError(MetaStorageErrorType.DECRYPTION_FAILED, {
cause: err
})
})
if (arrayBuffer) {
// Decode meta.json and parse
const decoder = new TextDecoder()
const json = decoder.decode(arrayBuffer)
const meta = await parseJson<Meta>(json)
return meta
}
throw new MetaStorageError(MetaStorageErrorType.UNHANDLED_FETCH_ERROR)
}