2024-08-12 14:26:03 +02:00
|
|
|
import { CreateSignatureEventContent, Meta } from '../types'
|
2024-12-06 20:00:38 +01:00
|
|
|
import {
|
|
|
|
decryptArrayBuffer,
|
|
|
|
encryptArrayBuffer,
|
|
|
|
fromUnixTimestamp,
|
|
|
|
getHash,
|
|
|
|
parseJson,
|
|
|
|
uploadToFileStorage
|
|
|
|
} from '.'
|
2024-08-15 17:48:05 +02:00
|
|
|
import { Event, verifyEvent } from 'nostr-tools'
|
2024-08-12 14:26:03 +02:00
|
|
|
import { toast } from 'react-toastify'
|
2024-08-22 18:20:54 +02:00
|
|
|
import { extractFileExtensions } from './file'
|
2024-08-28 09:29:05 +02:00
|
|
|
import { handleError } from '../types/errors'
|
2024-08-28 09:32:23 +02:00
|
|
|
import {
|
|
|
|
MetaParseError,
|
|
|
|
MetaParseErrorType
|
|
|
|
} from '../types/errors/MetaParseError'
|
2024-12-06 20:00:38 +01:00
|
|
|
import axios from 'axios'
|
|
|
|
import {
|
|
|
|
MetaStorageError,
|
|
|
|
MetaStorageErrorType
|
|
|
|
} from '../types/errors/MetaStorageError'
|
2024-08-12 14:26:03 +02:00
|
|
|
|
|
|
|
export enum SignStatus {
|
|
|
|
Signed = 'Signed',
|
2024-08-14 14:27:49 +02:00
|
|
|
Awaiting = 'Awaiting',
|
2024-08-12 14:26:03 +02:00
|
|
|
Pending = 'Pending',
|
2024-08-14 14:27:49 +02:00
|
|
|
Invalid = 'Invalid',
|
|
|
|
Viewer = 'Viewer'
|
2024-08-12 14:26:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export enum SigitStatus {
|
|
|
|
Partial = 'In-Progress',
|
|
|
|
Complete = 'Completed'
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface SigitCardDisplayInfo {
|
|
|
|
createdAt?: number
|
|
|
|
title?: string
|
2024-08-13 13:32:32 +02:00
|
|
|
submittedBy?: `npub1${string}`
|
2024-08-12 14:26:03 +02:00
|
|
|
signers: `npub1${string}`[]
|
|
|
|
fileExtensions: string[]
|
|
|
|
signedStatus: SigitStatus
|
2024-08-15 17:48:05 +02:00
|
|
|
isValid: boolean
|
2024-08-12 14:26:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-08-13 17:28:14 +02:00
|
|
|
* Wrapper for event parser that throws custom SigitMetaParseError with cause and context
|
2024-08-12 14:26:03 +02:00
|
|
|
* @param raw Raw string for parsing
|
|
|
|
* @returns parsed Event
|
|
|
|
*/
|
2024-08-13 17:28:14 +02:00
|
|
|
export const parseNostrEvent = async (raw: string): Promise<Event> => {
|
2024-08-12 14:26:03 +02:00
|
|
|
try {
|
2024-08-13 17:28:14 +02:00
|
|
|
const event = await parseJson<Event>(raw)
|
|
|
|
return event
|
2024-08-12 14:26:03 +02:00
|
|
|
} catch (error) {
|
2024-08-28 09:32:23 +02:00
|
|
|
throw new MetaParseError(MetaParseErrorType.PARSE_ERROR_EVENT, {
|
2024-08-13 17:28:14 +02:00
|
|
|
cause: handleError(error),
|
|
|
|
context: raw
|
|
|
|
})
|
2024-08-12 14:26:03 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) {
|
2024-08-28 09:29:05 +02:00
|
|
|
throw new MetaParseError(
|
2024-08-28 09:32:23 +02:00
|
|
|
MetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT_CONTENT,
|
2024-08-12 14:26:03 +02:00
|
|
|
{
|
|
|
|
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: [],
|
2024-08-15 17:48:05 +02:00
|
|
|
signedStatus: SigitStatus.Partial,
|
|
|
|
isValid: false
|
2024-08-12 14:26:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2024-08-13 17:28:14 +02:00
|
|
|
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
|
2024-08-12 14:26:03 +02:00
|
|
|
|
2024-08-15 17:48:05 +02:00
|
|
|
sigitInfo.isValid = verifyEvent(createSignatureEvent)
|
|
|
|
|
2024-08-12 14:26:03 +02:00
|
|
|
// created_at in nostr events are stored in seconds
|
2024-08-13 11:52:05 +02:00
|
|
|
sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at)
|
2024-08-12 14:26:03 +02:00
|
|
|
|
|
|
|
const createSignatureContent = await parseCreateSignatureEventContent(
|
|
|
|
createSignatureEvent.content
|
|
|
|
)
|
|
|
|
|
|
|
|
const files = Object.keys(createSignatureContent.fileHashes)
|
2024-08-14 18:59:00 +02:00
|
|
|
const { extensions } = extractFileExtensions(files)
|
2024-08-12 14:26:03 +02:00
|
|
|
|
|
|
|
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
|
|
|
const isCompletelySigned = createSignatureContent.signers.every((signer) =>
|
|
|
|
signedBy.includes(signer)
|
|
|
|
)
|
|
|
|
|
|
|
|
sigitInfo.title = createSignatureContent.title
|
2024-08-13 13:32:32 +02:00
|
|
|
sigitInfo.submittedBy = createSignatureEvent.pubkey as `npub1${string}`
|
2024-08-12 14:26:03 +02:00
|
|
|
sigitInfo.signers = createSignatureContent.signers
|
|
|
|
sigitInfo.fileExtensions = extensions
|
|
|
|
|
|
|
|
if (isCompletelySigned) {
|
|
|
|
sigitInfo.signedStatus = SigitStatus.Complete
|
|
|
|
}
|
|
|
|
|
|
|
|
return sigitInfo
|
|
|
|
} catch (error) {
|
2024-08-28 09:29:05 +02:00
|
|
|
if (error instanceof MetaParseError) {
|
2024-08-12 14:26:03 +02:00
|
|
|
toast.error(error.message)
|
|
|
|
console.error(error.name, error.message, error.cause, error.context)
|
|
|
|
} else {
|
|
|
|
console.error('Unexpected error', error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-12-06 20:00:38 +01:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|