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 => { try { const event = await parseJson(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 => { try { const createSignatureEventContent = await parseJson(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(json) return meta } throw new MetaStorageError(MetaStorageErrorType.UNHANDLED_FETCH_ERROR) }