From 72890d043f5b68b703fe11a76edc2a5e34c34b32 Mon Sep 17 00:00:00 2001 From: jawndiego Date: Thu, 27 Jun 2024 14:59:51 -0400 Subject: [PATCH 01/11] init --- bun.lockb | Bin 50827 -> 51936 bytes package.json | 2 + src/db.ts | 9 - src/index.ts | 46 +++-- src/rvm/index.ts | 440 +++++++++++++++++++++++++++++------------------ src/rvm/sdk.ts | 41 +++-- src/rvm/types.ts | 438 ++++++++++++++++++++++++---------------------- src/schema.ts | 135 +++++++++++++-- 8 files changed, 683 insertions(+), 428 deletions(-) delete mode 100644 src/db.ts diff --git a/bun.lockb b/bun.lockb index d5be3aef2630d9216acc5a8f0b18ca05f35999e5..9effeb4275c74944cc0660c712a650daec09f653 100755 GIT binary patch delta 10251 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 iOfRN|&-#1vw_orRr*~*yI8kCEZ delta 9570 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 diff --git a/package.json b/package.json index 3da276c..a6300ea 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,14 @@ "format": "biome format . --write" }, "dependencies": { + "@ipld/dag-cbor": "^9.2.1", "@noble/curves": "^1.4.0", "drizzle-graphql": "^0.8.3", "drizzle-orm": "^0.31.2", "graphql": "^16.8.1", "graphql-yoga": "^5.3.1", "hono": "^4.4.5", + "multiformats": "^13.1.1", "pg": "^8.12.0" }, "devDependencies": { diff --git a/src/db.ts b/src/db.ts deleted file mode 100644 index 13e049d..0000000 --- a/src/db.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { drizzle } from 'drizzle-orm/node-postgres' -import { Pool } from 'pg' -import * as dbSchema from './schema.js' - -const pool = new Pool({ - connectionString: process.env.DATABASE_URL!, -}) - -export const db = drizzle(pool, { schema: dbSchema }) diff --git a/src/index.ts b/src/index.ts index ef00481..74a6cb7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,42 @@ import { Hono } from 'hono' -import { RiverVmClient } from './rvm/index.js' -import { db } from './db.js' +import type { Message } from '../src/rvm/types.js' +import { River } from './rvm/index.js' const app = new Hono() -const riverVm = new RiverVmClient(db) + +const river = await River.flow() app.post('/message', async (c) => { - // recieve data - const data = await c.req.json() - // verify cryptography of message - const verified = riverVm.verifyMessage(data.message) - if (!verified) return c.json({ message: 'message not verified' }) - // process message - const vmResponse = riverVm.processMessage(data.message) - // return results of message - return c.json({ message: vmResponse }) + try { + // Receive data + const data = await c.req.json() + + if (!data.message || typeof data.message !== 'object') { + return c.json({ error: 'Invalid message format' }, 400) + } + + const message: Message = data.message + + const verified = await river.verifyMessage(message) + + if (!verified) { + return c.json({ error: 'Message not verified' }, 401) + } + const processMessageResponse = await river.processMessage(message) + + return c.json({ result: processMessageResponse }) + } catch (error) { + console.error('Error processing message:', error) + return c.json({ error: 'Internal server error' }, 500) + } +}) + +process.on('SIGINT', async () => { + console.log('Shutting down gracefully') + await river.disconnect() + process.exit(0) }) export default app -console.log('Server ready') +console.log('Server ready') \ No newline at end of file diff --git a/src/rvm/index.ts b/src/rvm/index.ts index a040794..d44c071 100644 --- a/src/rvm/index.ts +++ b/src/rvm/index.ts @@ -1,78 +1,171 @@ +import { Pool } from 'pg' +import { drizzle } from 'drizzle-orm/node-postgres' +import type { NodePgDatabase } from 'drizzle-orm/node-postgres' +import * as dbSchema from '../schema.js' +import { ed25519ph } from '@noble/curves/ed25519' +import type { + Message, + ItemAccRejBody, + ItemSubmitBody, + ItemCreateBody, + MessageData +} from './types.js' import { - Message, - ItemAccRejBody, - ItemSubmitBody, - isChannelCreateBody, - isItemCreateBody, - MessageTypes, -} from "./types.js"; -import { NodePgDatabase } from "drizzle-orm/node-postgres"; -import { ed25519ph } from "@noble/curves/ed25519"; - -export class RiverVmClient { - /* - VM SETUP - */ + isChannelCreateBody, + isItemCreateBody, + isItemSubmitBody, + isItemAccRejBody, + MessageTypes, + CAPTION_MAX_LENGTH +} from './types.js' + +import * as dagCbor from '@ipld/dag-cbor' +import * as Block from 'multiformats/block' +import { sha256 } from 'multiformats/hashes/sha2' + +export async function messageToCid(message: Message) { + return await Block.encode({ value: message, codec: dagCbor, hasher: sha256 }) +} - private vmStorage: NodePgDatabase; - constructor(db: NodePgDatabase) { - this.vmStorage = db; +export class River { + private db: NodePgDatabase + private pool: Pool + + private constructor(db: NodePgDatabase, pool: Pool) { + this.db = db + this.pool = pool + } +// being a little too cute + static async flow(): Promise { + const connectionString = process.env.DATABASE_URL! + + const pool = new Pool({ connectionString }) + + try { + const client = await pool.connect() + client.release() + console.log('Database connection successful') + } catch (err) { + console.error('Failed to connect to the database', err) + throw err + } + + const db = drizzle(pool, { schema: dbSchema }) + return new River(db, pool) + } + + async disconnect() { + await this.pool.end() + } + + // simple getters should not replace graphql + + async getUser(userId: bigint) { + return this.db.query.usersTable.findFirst({ + where: (users, { eq }) => eq(users.id, userId.toString()), + }) + } + + async getChannel(channelId: string) { + return this.db.query.channelTable.findFirst({ + where: (channels, { eq }) => eq(channels.id, channelId), + }) + } + + async getItem(itemId: string) { + return this.db.query.ItemTable.findFirst({ + where: (items, { eq }) => eq(items.id, itemId), + }) + } + + + public async makeCid(messageData: MessageData) { + return await Block.encode({ value: messageData, codec: dagCbor, hasher: sha256 }) } - /* - PUBLIC FUNCTIONS - */ public async verifyMessage(message: Message): Promise { // 1. lookup user id - const userExists = await this.vmStorage.query.usersTable({ - userId: message.messageData.rid, - }); - if (!userExists) return false; + const userExists = await this.db.query.usersTable.findFirst({ + where: (users, { eq }) => eq(users.id, message.messageData.rid.toString()), + }) + if (!userExists) return false + // 2. lookup signing key - const keyExistsForUserAtTimestamp = this.vmStorage.query.keyTable({ - userId: message.messageData.rid, - signer: message.signer, - timestamp: message.messageData.timestamp, - }); - if (!keyExistsForUserAtTimestamp) return false; + const keyExistsForUserAtTimestamp = await this.db.query.keyTable.findFirst({ + where: (keys, { and, eq }) => and( + eq(keys.userid, message.messageData.rid.toString()), + eq(keys.encryptedprivatekey, message.signer) + ) + }) + if (!keyExistsForUserAtTimestamp) return false + // 3. verify hash of message = message.messageHash - const computedHash = _ourHashingFunction(message.messageData); - if (computedHash != message.hash) return false; + // investigate actual hashing function + + const computedHash = (await this.makeCid(message.messageData)) + if (computedHash.toString() !== message.hash.toString()) return false + + // 4. verify signature is valid over hash - const valid = ed25519ph.verify(message.sig, message.hash, message.signer); - if (!valid) return false; + const valid = ed25519ph.verify(message.sig, message.hash, message.signer) + if (!valid) return false + // 5. return true if all checks passed - return true; + return true + } + + public formatItemCreateMessage({ + rid, + fileUri, + }: { + rid: bigint + fileUri: string + }): Message { + const message: Message = { + signer: '0x', + messageData: { + rid: rid, + timestamp: BigInt(Date.now()), + type: MessageTypes.ITEM_CREATE, + body: { uri: fileUri } as ItemCreateBody, + }, + hashType: 1, + hash: new Uint8Array(0), + sigType: 1, + sig: new Uint8Array(0), + } + return message } public async processMessage(message: Message): Promise { // return null if invalid or message type if (!Object.values(MessageTypes).includes(message.messageData.type)) - return null; - // route message to executor function - let vmResponse = null; + return null + + let response = null switch (message.messageData.type) { - case MessageTypes.CHANNEL_CREATE: { - vmResponse = this._msg1_channelCreate(message); - } - case MessageTypes.ITEM_CREATE: { - vmResponse = this._msg5_itemCreate(message); - } - case MessageTypes.ITEM_SUBMIT: { - vmResponse = this._msg8_itemSubmit(message); - } - case MessageTypes.ITEM_ACC_REJ: { - vmResponse = this._msg9_itemAccRej(message); - } + case MessageTypes.CHANNEL_CREATE: + response = await this._msg1_channelCreate(message) + break + case MessageTypes.ITEM_CREATE: + response = await this._msg5_itemCreate(message) + break + case MessageTypes.ITEM_SUBMIT: + response = await this._msg8_itemSubmit(message) + break + case MessageTypes.ITEM_ACC_REJ: + response = await this._msg9_itemAccRej(message) + break + default: + console.warn(`Unexpected message type: ${message.messageData.type}`) } - return vmResponse; + return response } - - /* +/* PRIVATE FUNCTIONS only accessible within vm context/runtime - */ + */ /* NAME: CHANNEL_CREATE @@ -82,117 +175,128 @@ export class RiverVmClient { } */ - private async _msg1_channelCreate(message: Message): Promise { - // make sure message data body is correct type - if (!isChannelCreateBody(message.messageData.body)) return null; - // generate channel id - const channelId = _ourHashingFunction(message.messageData); - // update RVM storage - await this.vmStorage.update.channelTable(channelId, message.messageData); - // return channelId in request - return channelId; - } - - /* - NAME: ITEM_CREATE - TYPE: 5 - BODY: { - uri: string - } - */ - - private async _msg5_itemCreate(message: Message): Promise { - // make sure message data body is correct type - if (!isItemCreateBody(message.messageData.body)) return null; - // generate itemId - const itemId = _ourHashingFunction(message.messageData); - // update RVM storage - await this.vmStorage.update.itemTable(itemId, message.messageData); - // return itemId in request - return itemId; - } - - /* - NAME: ITEM_SUBMIT - TYPE: 8 - BODY: { - itemId: string - channelId: string - caption?: string - } - */ - - private async _msg8_itemSubmit(message: Message): Promise { - // make sure message data body is correct type - if (!isItemSubmitBody(message.messageData.body)) return null; - // destructure body object for vis - const { itemId, channelId, caption } = message.messageData - .body as ItemSubmitBody; - // check if item exists - const itemExists = this.vmStorage.query.itemTable(itemId); - if (!itemExists) return null; - // check if channel exists - const channelExists = this.vmStorage.query.channelTable(channelId); - if (!channelExists) return null; - // check caption for max length - if (caption && caption.length > CAPTION_MAX_LENGTH) return null; - // generate submissionId - const submissionId = _ourHashingFunction(message.messageData); - // check if user is owner of channel - const isOwner = this.vmStorage.query.channelTable( - channelId, - message.messageData.rid - ); - // update RVM storage - if (isOwner) { - await this.vmStorage.update.submissionTable( - submissionId, - message.messageData, - OWNER_FLAG - ); - } else { - await this.vmStorage.update.submissionTable( - submissionId, - message.messageData, - null - ); - } - // return submissionId in request - return submissionId; - } - - /* - NAME: ITEM_ACCREJ - TYPE: 0 - BODY: { - submissionId: string - Response: boolean - caption?: string - } - */ - - private async _msg9_itemAccRej(message: Message): Promise { - // make sure message data body is correct type - if (!isItemAccRejBody(message.messageData.body)) return null; - // destructure body object for vis - const { submissionId, response, caption } = message.messageData - .body as ItemAccRejBody; - // check if submission exists - const submissionExists = this.vmStorage.query.submissionTable(submissionId); - if (!submissionExists) return null; - // check caption for max length - if (caption && caption.length > CAPTION_MAX_LENGTH) return null; - // generate accRejId - const accRejId = _ourHashingFunction(message.messageData); - // check if user is owner/moderator of channel - const isOwnerOrModerator = this.vmStorage.query.channelTable( - submissionExists.channelId, - message.messageData.rid - ); - if (!isOwnerOrModerator) return null; - // update RVM storage - this.vmStorage.update.accRejTable(accRejId, message.messageData); - // return accRejId in request - return accRejId; - } -} + private async _msg1_channelCreate(message: Message): Promise { + // make sure message data body is correct type + if (!isChannelCreateBody(message.messageData.body)) return null + // generate channel id + const channelId = (await this.makeCid(message.messageData)).toString() + // update RVM storage + await this.db.insert(dbSchema.channelTable).values({ + id: channelId, + content: JSON.stringify(message.messageData.body), + timestamp: Number(message.messageData.timestamp), + createdById: message.messageData.rid.toString(), + uri: message.messageData.body.uri, + name: '', + description: '', + }) + return channelId + } + + /* + NAME: ITEM_CREATE + TYPE: 5 + BODY: { + uri: string + } + */ + + private async _msg5_itemCreate(message: Message): Promise { + // make sure message data body is correct type + if (!isItemCreateBody(message.messageData.body)) return null + // generate itemId + const itemId = (await this.makeCid(message.messageData)).toString() + // update RVM storage + await this.db.insert(dbSchema.ItemTable).values({ + id: itemId, + createdById: message.messageData.rid.toString(), + uri: message.messageData.body.uri, + }) + return itemId + } + + /* + NAME: ITEM_SUBMIT + TYPE: 8 + BODY: { + itemId: string + channelId: string + caption?: string + } + */ + + private async _msg8_itemSubmit(message: Message): Promise { + if (!isItemSubmitBody(message.messageData.body)) return null + const { itemId, channelId, caption } = message.messageData.body as ItemSubmitBody + + const itemExists = await this.db.query.ItemTable.findFirst({ + where: (items, { eq }) => eq(items.id, itemId) + }) + if (!itemExists) return null + + const channelExists = await this.db.query.channelTable.findFirst({ + where: (channels, { eq }) => eq(channels.id, channelId) + }) + if (!channelExists) return null + + if (caption && caption.length > CAPTION_MAX_LENGTH) return null + + const submissionId = (await this.makeCid(message.messageData)).toString() + + const isOwner = await this.db.query.channelTable.findFirst({ + where: (channels, { and, eq }) => and( + eq(channels.id, channelId), + eq(channels.createdById, message.messageData.rid.toString()) + ) + }) + + await this.db.insert(dbSchema.submissionsTable).values({ + id: submissionId, + content: JSON.stringify(message.messageData.body), + userId: message.messageData.rid.toString(), + // Add other necessary fields + }) + + return submissionId + } + + /* + NAME: ITEM_ACCREJ + TYPE: 0 + BODY: { + submissionId: string + Response: boolean + caption?: string + } + */ + + private async _msg9_itemAccRej(message: Message): Promise { + if (!isItemAccRejBody(message.messageData.body)) return null + const { submissionId, response, caption } = message.messageData.body as ItemAccRejBody + + const submissionExists = await this.db.query.submissionsTable.findFirst({ + where: (submissions, { eq }) => eq(submissions.id, submissionId) + }) + if (!submissionExists) return null + + if (caption && caption.length > CAPTION_MAX_LENGTH) return null + + const accRejId = (await this.makeCid(message.messageData)).toString() + + const isOwnerOrModerator = await this.db.query.channelTable.findFirst({ + where: (channels, { eq }) => eq(channels.createdById, message.messageData.rid.toString()) + // You might need to adjust this query based on how you determine ownership/moderation + }) + if (!isOwnerOrModerator) return null + + await this.db.insert(dbSchema.acceptedRejectedTable).values({ + messageId: accRejId, + submissionId: submissionId, + response: response.toString(), + caption: caption, + }) + + return accRejId + } + + } diff --git a/src/rvm/sdk.ts b/src/rvm/sdk.ts index bb546ec..f7f700b 100644 --- a/src/rvm/sdk.ts +++ b/src/rvm/sdk.ts @@ -1,20 +1,23 @@ -import { ItemCreateBody, MessageTypes } from "./types.js"; -import { MessageData } from "./types.js"; -import { Message } from "./types.js"; +import { ItemCreateBody, MessageTypes } from './types.js' +import { MessageData } from './types.js' +import { Message } from './types.js' -export function formatItemCreateMessage({rid, fileUri}: {rid: bigint, fileUri: string}): Message { - const message = { - signer: "0x", - messageData: { - rid: rid, - timestamp: BigInt(Date.now()), - type: MessageTypes.ITEM_CREATE, - body: { uri: fileUri } - }, - hashType: 1, - hash: new Uint8Array(0), - sigType: 1, - sig: new Uint8Array(0) - } - return message -} \ No newline at end of file +export function formatItemCreateMessage({ + rid, + fileUri, +}: { rid: bigint; fileUri: string }): Message { + const message = { + signer: '0x', + messageData: { + rid: rid, + timestamp: BigInt(Date.now()), + type: MessageTypes.ITEM_CREATE, + body: { uri: fileUri }, + }, + hashType: 1, + hash: new Uint8Array(0), + sigType: 1, + sig: new Uint8Array(0), + } + return message +} diff --git a/src/rvm/types.ts b/src/rvm/types.ts index 875a4bc..c523863 100644 --- a/src/rvm/types.ts +++ b/src/rvm/types.ts @@ -1,209 +1,237 @@ /* -* -* MESSAGE TYPES -* -*/ + * + * MESSAGE TYPES + * + */ export enum MessageTypes { - NONE = 0, - CHANNEL_CREATE = 1, - CHANNEL_EDIT_MEMBERS = 2, - CHANNEL_EDIT_URI = 3, - CHANNEL_TRANSFER_OWNER = 4, - ITEM_CREATE = 5, - ITEM_EDIT = 6, - ITEM_DELETE = 7, - ITEM_SUBMIT = 8, - ITEM_ACC_REJ = 9, - ITEM_REMOVE = 10, - COMMENT_CREATE = 11, - COMMENT_EDIT = 12, - COMMENT_DELETE = 13, - USER_SET_NAME = 14, - USER_SET_URI = 15, - } - - export enum HashTypes { - NONE = 0, - BLAKE_3 = 1, - } - - export enum SignatureTypes { - NONE = 0, - ED25519 = 1, - EIP712 = 2, + NONE = 0, + CHANNEL_CREATE = 1, + CHANNEL_EDIT_MEMBERS = 2, + CHANNEL_EDIT_URI = 3, + CHANNEL_TRANSFER_OWNER = 4, + ITEM_CREATE = 5, + ITEM_EDIT = 6, + ITEM_DELETE = 7, + ITEM_SUBMIT = 8, + ITEM_ACC_REJ = 9, + ITEM_REMOVE = 10, + COMMENT_CREATE = 11, + COMMENT_EDIT = 12, + COMMENT_DELETE = 13, + USER_SET_NAME = 14, + USER_SET_URI = 15, +} + +export enum HashTypes { + NONE = 0, + BLAKE_3 = 1, +} + +export enum SignatureTypes { + NONE = 0, + ED25519 = 1, + EIP712 = 2, +} + +export type Message = { + signer: string + messageData: MessageData + hashType: HashTypes + hash: Uint8Array + sigType: SignatureTypes + sig: Uint8Array +} + +export type MessageData = { + rid: bigint + timestamp: bigint + type: MessageTypes + body: MessageDataBodyTypes +} + +/* + * + * MESSAGE BODY TYPES + * + */ + +/* + * 1 + */ +export type ChannelCreateBody = { + uri: string +} + + +// type guard function +export function isChannelCreateBody(obj: any): obj is ChannelCreateBody { + return obj && typeof obj === 'object' && typeof obj.uri === 'string' +} + +/* + * 2 + */ +export type ChannelEditMember = { + channelId: string + member: { + rid: bigint + role: 0 | 1 | 2 // 0 = none, 1 = member, 2 = admin } - - export type Message = { - signer: string; - messageData: MessageData; - hashType: HashTypes; - hash: Uint8Array; - sigType: SignatureTypes; - sig: Uint8Array; - }; - - export type MessageData = { - rid: bigint; - timestamp: bigint; - type: MessageTypes; - body: MessageDataBodyTypes; - }; - - - /* - * - * MESSAGE BODY TYPES - * - */ - - /* - * 1 - */ - export type ChannelCreateBody = { - uri: string; - }; - - // type guard function - export function isChannelCreateBody(obj: any): obj is ChannelCreateBody { - return obj && typeof obj === 'object' && typeof obj.uri === 'string'; - } - - /* - * 2 - */ - export type ChannelEditMember = { - channelId: string; - member: { - rid: bigint; - role: 0 | 1 | 2; // 0 = none, 1 = member, 2 = admin - }; - }; - - /* - * 3 - */ - export type ChannelEditUri = { - channelId: string; - uri: string; - }; - - /* - * 4 - */ - export type ChannelTransferOwner = { - channelId: string; - transferToRid: bigint; - }; - - /* - * 5 - */ - export type ItemCreateBody = { - uri: string; - }; - - // type guard function - export function isItemCreateBody(obj: any): obj is ChannelCreateBody { - return obj && typeof obj === 'object' && typeof obj.uri === 'string'; - } - - /* - * 6 - */ - export type ItemEditBody = { - itemId: string; - uri: string; - }; - - /* - * 7 - */ - export type ItemDeleteBody = { - itemId: string; - }; - - /* - * 8 - */ - export type ItemSubmitBody = { - itemId: string; - channelId: string; - caption?: string; // MAX 300 CHAR LIMIT - }; - - /* - * 9 - */ - export type ItemAccRejBody = { - submissionId: string; - response: boolean; // FALSE = rejected, TRUE = accepted - caption?: string; // MAX_CHAR_LIMIT = 300 - }; - - /* - * 10 - */ - export type ItemRemoveBody = { - submissionId: string; - }; - - /* - * 11 - */ - export type CommentCreateBody = { - targetId: string; // Must be SUBMISSION_ID or COMMENT_ID - text: string; // MAX_CHAR_LIMIT = 300 - }; - - /* - * 12 - */ - export type CommentEditBody = { - commentId: string; - text: string; // MAX_CHAR_LIMIT = 300 - }; - - /* - * 13 - */ - export type CommentDeleteBody = { - commentId: string; - }; - - /* - * 14 - */ - export type UserSetNameBody = { - fromId: bigint; - toId: bigint; - username: string; // MAX_CHAR_LIMIT = 15 + regex - }; - - /* - * 15 - */ - export type UserSetUriBody = { - rid: bigint; - uri: string; - }; - - /* - * Message Data Body Union type - */ - export type MessageDataBodyTypes = - | ChannelCreateBody - | ChannelEditMember - | ChannelEditUri - | ChannelTransferOwner - | ItemCreateBody - | ItemEditBody - | ItemDeleteBody - | ItemSubmitBody - | ItemAccRejBody - | ItemRemoveBody - | CommentCreateBody - | CommentEditBody - | CommentDeleteBody - | UserSetNameBody - | UserSetUriBody; \ No newline at end of file +} + +/* + * 3 + */ +export type ChannelEditUri = { + channelId: string + uri: string +} + +/* + * 4 + */ +export type ChannelTransferOwner = { + channelId: string + transferToRid: bigint +} + +/* + * 5 + */ +export type ItemCreateBody = { + uri: string +} + +// type guard function +export function isItemCreateBody(obj: any): obj is ChannelCreateBody { + return obj && typeof obj === 'object' && typeof obj.uri === 'string' +} + + +/* + * 6 + */ +export type ItemEditBody = { + itemId: string + uri: string +} + +/* + * 7 + */ +export type ItemDeleteBody = { + itemId: string +} + +/* + * 8 + */ +export type ItemSubmitBody = { + itemId: string + channelId: string + caption?: string // MAX 300 CHAR LIMIT +} + +export function isItemSubmitBody(obj: any): obj is ItemSubmitBody { + return ( + obj !== null && + typeof obj === 'object' && + typeof obj.itemId === 'string' && + typeof obj.channelId === 'string' && + (obj.caption === undefined || + (typeof obj.caption === 'string' && obj.caption.length <= 300)) + ) +} + + + +/* + * 9 + */ + +export function isItemAccRejBody(obj: any): obj is ItemAccRejBody { + return ( + obj && + typeof obj === 'object' && + typeof obj.submissionId === 'string' && + typeof obj.response === 'boolean' && + (obj.caption === undefined || + (typeof obj.caption === 'string' && obj.caption.length <= 300)) + ) +} + +export type ItemAccRejBody = { + submissionId: string + response: boolean // FALSE = rejected, TRUE = accepted + caption?: string // MAX_CHAR_LIMIT = 300 +} + +/* + * 10 + */ +export type ItemRemoveBody = { + submissionId: string +} + +/* + * 11 + */ +export type CommentCreateBody = { + targetId: string // Must be SUBMISSION_ID or COMMENT_ID + text: string // MAX_CHAR_LIMIT = 300 +} + +/* + * 12 + */ +export type CommentEditBody = { + commentId: string + text: string // MAX_CHAR_LIMIT = 300 +} + +/* + * 13 + */ +export type CommentDeleteBody = { + commentId: string +} + +/* + * 14 + */ +export type UserSetNameBody = { + fromId: bigint + toId: bigint + username: string // MAX_CHAR_LIMIT = 15 + regex +} + +/* + * 15 + */ +export type UserSetUriBody = { + rid: bigint + uri: string +} + +/* + * Message Data Body Union type + */ +export type MessageDataBodyTypes = + | ChannelCreateBody + | ChannelEditMember + | ChannelEditUri + | ChannelTransferOwner + | ItemCreateBody + | ItemEditBody + | ItemDeleteBody + | ItemSubmitBody + | ItemAccRejBody + | ItemRemoveBody + | CommentCreateBody + | CommentEditBody + | CommentDeleteBody + | UserSetNameBody + | UserSetUriBody + + export const CAPTION_MAX_LENGTH = 300 \ No newline at end of file diff --git a/src/schema.ts b/src/schema.ts index ddeca3e..14756a8 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,26 +1,133 @@ -import { integer, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core' +import { integer, pgTable, serial, text, timestamp, numeric } from 'drizzle-orm/pg-core' -export const usersTable = pgTable('users_table', { - id: serial('id').primaryKey(), +export const usersTable = pgTable('users', { + id: numeric('userid').primaryKey(), + to: text('to'), + recovery: text('recovery'), + timestamp: timestamp('timestamp'), + log_addr: text('log_addr'), + block_num: numeric('block_num'), +}) + +export type InsertUser = typeof usersTable.$inferInsert +export type SelectUser = typeof usersTable.$inferSelect + +export const sessionsTable = pgTable('sessions', { + id: text('id').primaryKey(), + userId: numeric('userid') + .notNull() + .references(() => usersTable.id), + deviceId: text('deviceid').notNull(), + created: timestamp('created'), + expiresAt: timestamp('expiresat').notNull(), +}) + +export type InsertSession = typeof sessionsTable.$inferInsert +export type SelectSession = typeof sessionsTable.$inferSelect + +export const keyTable = pgTable( + 'keys', + { + userid: numeric('userid') + .notNull() + .references(() => usersTable.id), + custodyAddress: text('custodyAddress').notNull(), + deviceid: text('deviceid').notNull(), + encryptedpublickey: text('encryptedpublickey').notNull(), + encryptedprivatekey: text('encryptedprivatekey').notNull(), + }, + (table) => ({ + primaryKey: [table.userid, table.custodyAddress, table.deviceid], + }), +) + +export type InsertHash = typeof keyTable.$inferInsert +export type SelectHash = typeof keyTable.$inferSelect + +export const messageTable = pgTable('messages', { + id: text('messageid').primaryKey(), + signer: text('signer').notNull(), + messageType: text('messagetype').notNull(), + messageBody: text('messagebody').notNull(), + hashType: text('hashtype').notNull(), + hash: numeric('hash') + .notNull(), + sigType: text('sigtype').notNull(), + sig: text('sig').notNull(), +}) + +export type InsertPost = typeof messageTable.$inferInsert +export type SelectPost = typeof messageTable.$inferSelect + +export const channelTable = pgTable('channels', { + id: text('messageid') + .notNull() + .references(() => messageTable.id) + .primaryKey(), + content: text('content').notNull(), + timestamp: integer('timestamp').notNull(), + createdById: numeric('createdbyid') + .notNull() + .references(() => usersTable.id), + uri: text('uri').notNull(), name: text('name').notNull(), - age: integer('age').notNull(), - email: text('email').notNull().unique(), + description: text('description').notNull(), }) -export const postsTable = pgTable('posts_table', { - id: serial('id').primaryKey(), - title: text('title').notNull(), +export type InsertChannel = typeof channelTable.$inferInsert +export type SelectChannel = typeof channelTable.$inferSelect + +export const ItemTable = pgTable('items', { + id: text('messageId') + .notNull() + .references(() => messageTable.id) + .primaryKey(), + createdById: numeric('createdbyid') + .notNull() + .references(() => usersTable.id), + uri: text('uri').notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at') + .notNull() + .$onUpdate(() => new Date()), +}) + +export type InsertItem = typeof ItemTable.$inferInsert +export type SelectItem = typeof ItemTable.$inferSelect + +export const submissionsTable = pgTable('submissions', { + id: text('messageId') + .notNull() + .references(() => messageTable.id) + .primaryKey(), content: text('content').notNull(), - userId: integer('user_id') + userId: numeric('userid') .notNull() - .references(() => usersTable.id, { onDelete: 'cascade' }), + .references(() => usersTable.id), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at') .notNull() .$onUpdate(() => new Date()), }) -export type InsertUser = typeof usersTable.$inferInsert -export type SelectUser = typeof usersTable.$inferSelect -export type InsertPost = typeof postsTable.$inferInsert -export type SelectPost = typeof postsTable.$inferSelect +export type InsertSubmission = typeof submissionsTable.$inferInsert +export type SelectSubmission = typeof submissionsTable.$inferSelect + +export const acceptedRejectedTable = pgTable('acceptedrejected', { + messageId: text('messageId') + .notNull() + .references(() => messageTable.id) + .primaryKey(), + submissionId: text('submissionid') + .notNull() + .references(() => submissionsTable.id), + response: text('response').notNull(), + caption: text('caption'), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at') + .notNull() + .$onUpdate(() => new Date()), +}) + +export type InsertacceptedRejected = typeof acceptedRejectedTable.$inferInsert +export type SelectacceptedRejected = typeof acceptedRejectedTable.$inferSelect \ No newline at end of file From 11e544154483c7dbd6eef83904d0d5a9e0c37f3f Mon Sep 17 00:00:00 2001 From: jawndiego Date: Thu, 27 Jun 2024 15:36:48 -0400 Subject: [PATCH 02/11] type guard --- src/index.ts | 3 ++- src/rvm/index.ts | 1 - src/rvm/types.ts | 13 +++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 74a6cb7..d4d884b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { Hono } from 'hono' import type { Message } from '../src/rvm/types.js' import { River } from './rvm/index.js' +import { isMessage } from '../src/rvm/types.js' const app = new Hono() @@ -11,7 +12,7 @@ app.post('/message', async (c) => { // Receive data const data = await c.req.json() - if (!data.message || typeof data.message !== 'object') { + if (!isMessage(data.message)) { return c.json({ error: 'Invalid message format' }, 400) } diff --git a/src/rvm/index.ts b/src/rvm/index.ts index d44c071..676a569 100644 --- a/src/rvm/index.ts +++ b/src/rvm/index.ts @@ -254,7 +254,6 @@ export class River { id: submissionId, content: JSON.stringify(message.messageData.body), userId: message.messageData.rid.toString(), - // Add other necessary fields }) return submissionId diff --git a/src/rvm/types.ts b/src/rvm/types.ts index c523863..27a1ee5 100644 --- a/src/rvm/types.ts +++ b/src/rvm/types.ts @@ -43,6 +43,19 @@ export type Message = { sig: Uint8Array } +export function isMessage(data: any): data is Message { + return ( + typeof data.signer === 'string' && + typeof data.messageData === 'object' && + typeof data.hashType === 'string' && + (data.hashType === 'sha256' || data.hashType === 'sha512') && + data.hash instanceof Uint8Array && + typeof data.sigType === 'string' && + (data.sigType === 'ed25519' || data.sigType === 'secp256k1') && + data.sig instanceof Uint8Array + ) +} + export type MessageData = { rid: bigint timestamp: bigint From 8bdbb231678a453eb61f4aaa463f4a5388450ae7 Mon Sep 17 00:00:00 2001 From: jawndiego Date: Thu, 27 Jun 2024 15:56:17 -0400 Subject: [PATCH 03/11] furthermore --- src/rvm/sdk.ts | 23 ----------------------- src/server.ts | 24 ------------------------ 2 files changed, 47 deletions(-) delete mode 100644 src/rvm/sdk.ts delete mode 100644 src/server.ts diff --git a/src/rvm/sdk.ts b/src/rvm/sdk.ts deleted file mode 100644 index f7f700b..0000000 --- a/src/rvm/sdk.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ItemCreateBody, MessageTypes } from './types.js' -import { MessageData } from './types.js' -import { Message } from './types.js' - -export function formatItemCreateMessage({ - rid, - fileUri, -}: { rid: bigint; fileUri: string }): Message { - const message = { - signer: '0x', - messageData: { - rid: rid, - timestamp: BigInt(Date.now()), - type: MessageTypes.ITEM_CREATE, - body: { uri: fileUri }, - }, - hashType: 1, - hash: new Uint8Array(0), - sigType: 1, - sig: new Uint8Array(0), - } - return message -} diff --git a/src/server.ts b/src/server.ts deleted file mode 100644 index e142355..0000000 --- a/src/server.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { buildSchema } from 'drizzle-graphql' -import { createYoga } from 'graphql-yoga' -import { db } from './db.js' - -const { schema } = buildSchema(db) - -const yoga = createYoga({ - schema, - cors: { - origin: '*', // Allow all origins - credentials: true, // If your client needs to send credentials - } -}) - -const server = Bun.serve({ - fetch: yoga, -}) - -console.info( - `Server is running on ${new URL( - yoga.graphqlEndpoint, - `http://${server.hostname}:${server.port}`, - )}`, -) From 3813fa4322884be210e94b4fc1db6f88280917bb Mon Sep 17 00:00:00 2001 From: jawndiego Date: Thu, 27 Jun 2024 16:04:10 -0400 Subject: [PATCH 04/11] index --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index d4d884b..01251cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,9 +12,9 @@ app.post('/message', async (c) => { // Receive data const data = await c.req.json() - if (!isMessage(data.message)) { - return c.json({ error: 'Invalid message format' }, 400) - } + // if (!isMessage(data.message)) { + // return c.json({ error: 'Invalid message format' }, 400) + // } const message: Message = data.message From 3ed78a7d9cc1407712ccb896054484178161d288 Mon Sep 17 00:00:00 2001 From: jawndiego Date: Thu, 27 Jun 2024 16:04:43 -0400 Subject: [PATCH 05/11] more deletion --- src/queries.ts | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/queries.ts diff --git a/src/queries.ts b/src/queries.ts deleted file mode 100644 index 9c05cf2..0000000 --- a/src/queries.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { db } from './db.js' -import { type InsertUser, usersTable } from './schema.js' - -export async function createUser(data: InsertUser) { - await db.insert(usersTable).values(data) -} From c78175867a6b32bdd4ccf7c8f59a91148e89ddf2 Mon Sep 17 00:00:00 2001 From: jawndiego Date: Thu, 27 Jun 2024 16:05:32 -0400 Subject: [PATCH 06/11] commenting --- src/rvm/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rvm/index.ts b/src/rvm/index.ts index 676a569..67671c8 100644 --- a/src/rvm/index.ts +++ b/src/rvm/index.ts @@ -187,6 +187,7 @@ export class River { timestamp: Number(message.messageData.timestamp), createdById: message.messageData.rid.toString(), uri: message.messageData.body.uri, + // destructure cid to extract name and description? name: '', description: '', }) From a8fb33b6de307b8b3b94d31f66d632edd7d2e5aa Mon Sep 17 00:00:00 2001 From: jawndiego Date: Sat, 29 Jun 2024 18:56:09 -0400 Subject: [PATCH 07/11] two db pools, functioning typeguarding --- src/index.ts | 6 +- src/rvm/index.ts | 351 ++++++++++++++++++++++++--------------------- src/rvm/types.ts | 65 +++++---- src/schema.ts | 14 +- src/utils/index.ts | 8 ++ 5 files changed, 249 insertions(+), 195 deletions(-) create mode 100644 src/utils/index.ts diff --git a/src/index.ts b/src/index.ts index 01251cc..4c01b89 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import { Hono } from 'hono' -import type { Message } from '../src/rvm/types.js' +import type { Message } from '../src/rvm/types.js' import { River } from './rvm/index.js' import { isMessage } from '../src/rvm/types.js' @@ -19,7 +19,7 @@ app.post('/message', async (c) => { const message: Message = data.message const verified = await river.verifyMessage(message) - + if (!verified) { return c.json({ error: 'Message not verified' }, 401) } @@ -40,4 +40,4 @@ process.on('SIGINT', async () => { export default app -console.log('Server ready') \ No newline at end of file +console.log('Server ready') diff --git a/src/rvm/index.ts b/src/rvm/index.ts index 67671c8..f299f57 100644 --- a/src/rvm/index.ts +++ b/src/rvm/index.ts @@ -4,109 +4,138 @@ import type { NodePgDatabase } from 'drizzle-orm/node-postgres' import * as dbSchema from '../schema.js' import { ed25519ph } from '@noble/curves/ed25519' import type { - Message, - ItemAccRejBody, - ItemSubmitBody, - ItemCreateBody, - MessageData + Message, + ItemAccRejBody, + ItemSubmitBody, + ItemCreateBody, + MessageData, } from './types.js' import { - isChannelCreateBody, - isItemCreateBody, - isItemSubmitBody, - isItemAccRejBody, - MessageTypes, - CAPTION_MAX_LENGTH + isChannelCreateBody, + isItemCreateBody, + isItemSubmitBody, + isItemAccRejBody, + MessageTypes, + CAPTION_MAX_LENGTH, } from './types.js' - import * as dagCbor from '@ipld/dag-cbor' import * as Block from 'multiformats/block' import { sha256 } from 'multiformats/hashes/sha2' -export async function messageToCid(message: Message) { - return await Block.encode({ value: message, codec: dagCbor, hasher: sha256 }) -} - export class River { private db: NodePgDatabase + private authDb: NodePgDatabase private pool: Pool + private authPool: Pool - private constructor(db: NodePgDatabase, pool: Pool) { + private constructor( + db: NodePgDatabase, + authDb: NodePgDatabase, + pool: Pool, + authPool: Pool, + ) { this.db = db + this.authDb = authDb this.pool = pool + this.authPool = authPool } -// being a little too cute + static async flow(): Promise { const connectionString = process.env.DATABASE_URL! + const authConnectionString = process.env.AUTH_DATABASE_URL! const pool = new Pool({ connectionString }) + const authPool = new Pool({ connectionString: authConnectionString }) try { const client = await pool.connect() client.release() - console.log('Database connection successful') + const authClient = await authPool.connect() + authClient.release() + console.log('Database connections successful') } catch (err) { - console.error('Failed to connect to the database', err) + console.error('Failed to connect to the databases', err) throw err } const db = drizzle(pool, { schema: dbSchema }) - return new River(db, pool) + const authDb = drizzle(authPool, { schema: dbSchema }) + + return new River(db, authDb, pool, authPool) } async disconnect() { await this.pool.end() + await this.authPool.end() } - // simple getters should not replace graphql + // simple getters should not replace graphql + // auth db async getUser(userId: bigint) { - return this.db.query.usersTable.findFirst({ + return this.authDb.query.usersTable.findFirst({ where: (users, { eq }) => eq(users.id, userId.toString()), }) } - - async getChannel(channelId: string) { + + async getPublicKey(userId: string) { + const result = await this.authDb.query.keyTable.findFirst({ + where: (keys, { eq }) => eq(keys.userid, userId), + columns: { + encryptedpublickey: true, + }, + }) + return result?.encryptedpublickey || null + } + + // main db + + async getChannel(channelId: string) { return this.db.query.channelTable.findFirst({ where: (channels, { eq }) => eq(channels.id, channelId), }) } - + async getItem(itemId: string) { return this.db.query.ItemTable.findFirst({ where: (items, { eq }) => eq(items.id, itemId), }) } - public async makeCid(messageData: MessageData) { - return await Block.encode({ value: messageData, codec: dagCbor, hasher: sha256 }) + return await Block.encode({ + value: messageData, + codec: dagCbor, + hasher: sha256, + }) } - public async verifyMessage(message: Message): Promise { // 1. lookup user id - const userExists = await this.db.query.usersTable.findFirst({ - where: (users, { eq }) => eq(users.id, message.messageData.rid.toString()), + const userExists = await this.authDb.query.usersTable.findFirst({ + where: (users, { eq }) => + eq(users.id, message.messageData.rid.toString()), }) if (!userExists) return false // 2. lookup signing key - const keyExistsForUserAtTimestamp = await this.db.query.keyTable.findFirst({ - where: (keys, { and, eq }) => and( - eq(keys.userid, message.messageData.rid.toString()), - eq(keys.encryptedprivatekey, message.signer) - ) - }) + const keyExistsForUserAtTimestamp = + await this.authDb.query.keyTable.findFirst({ + where: (keys, { and, eq }) => + and( + eq(keys.userid, message.messageData.rid.toString()), + eq(keys.encryptedpublickey, message.signer), + ), + }) + if (!keyExistsForUserAtTimestamp) return false // 3. verify hash of message = message.messageHash - // investigate actual hashing function + // investigate actual hashing function - const computedHash = (await this.makeCid(message.messageData)) + const computedHash = await this.makeCid(message.messageData) if (computedHash.toString() !== message.hash.toString()) return false - // 4. verify signature is valid over hash const valid = ed25519ph.verify(message.sig, message.hash, message.signer) if (!valid) return false @@ -139,30 +168,23 @@ export class River { } public async processMessage(message: Message): Promise { - // return null if invalid or message type if (!Object.values(MessageTypes).includes(message.messageData.type)) return null - - let response = null - switch (message.messageData.type) { - case MessageTypes.CHANNEL_CREATE: - response = await this._msg1_channelCreate(message) - break - case MessageTypes.ITEM_CREATE: - response = await this._msg5_itemCreate(message) - break - case MessageTypes.ITEM_SUBMIT: - response = await this._msg8_itemSubmit(message) - break - case MessageTypes.ITEM_ACC_REJ: - response = await this._msg9_itemAccRej(message) - break - default: - console.warn(`Unexpected message type: ${message.messageData.type}`) + + const handlers: { + [K in MessageTypes]?: (message: Message) => Promise + } = { + [MessageTypes.CHANNEL_CREATE]: this._msg1_channelCreate, + [MessageTypes.ITEM_CREATE]: this._msg5_itemCreate, + [MessageTypes.ITEM_SUBMIT]: this._msg8_itemSubmit, + [MessageTypes.ITEM_ACC_REJ]: this._msg9_itemAccRej, } - return response + + const handler = handlers[message.messageData.type] + return handler ? handler.call(this, message) : null } -/* + + /* PRIVATE FUNCTIONS only accessible within vm context/runtime */ @@ -175,48 +197,48 @@ export class River { } */ - private async _msg1_channelCreate(message: Message): Promise { - // make sure message data body is correct type - if (!isChannelCreateBody(message.messageData.body)) return null - // generate channel id - const channelId = (await this.makeCid(message.messageData)).toString() - // update RVM storage - await this.db.insert(dbSchema.channelTable).values({ - id: channelId, - content: JSON.stringify(message.messageData.body), - timestamp: Number(message.messageData.timestamp), - createdById: message.messageData.rid.toString(), - uri: message.messageData.body.uri, - // destructure cid to extract name and description? - name: '', - description: '', - }) - return channelId - } - - /* + private async _msg1_channelCreate(message: Message): Promise { + // make sure message data body is correct type + if (!isChannelCreateBody(message.messageData.body)) return null + // generate channel id + const channelId = (await this.makeCid(message.messageData)).toString() + // update RVM storage + await this.db.insert(dbSchema.channelTable).values({ + id: channelId, + content: JSON.stringify(message.messageData.body), + timestamp: Number(message.messageData.timestamp), + createdById: message.messageData.rid.toString(), + uri: message.messageData.body.uri, + // destructure cid to extract name and description? + name: '', + description: '', + }) + return channelId + } + + /* NAME: ITEM_CREATE TYPE: 5 BODY: { uri: string } */ - - private async _msg5_itemCreate(message: Message): Promise { - // make sure message data body is correct type - if (!isItemCreateBody(message.messageData.body)) return null - // generate itemId - const itemId = (await this.makeCid(message.messageData)).toString() - // update RVM storage - await this.db.insert(dbSchema.ItemTable).values({ - id: itemId, - createdById: message.messageData.rid.toString(), - uri: message.messageData.body.uri, - }) - return itemId - } - - /* + + private async _msg5_itemCreate(message: Message): Promise { + // make sure message data body is correct type + if (!isItemCreateBody(message.messageData.body)) return null + // generate itemId + const itemId = (await this.makeCid(message.messageData)).toString() + // update RVM storage + await this.db.insert(dbSchema.ItemTable).values({ + id: itemId, + createdById: message.messageData.rid.toString(), + uri: message.messageData.body.uri, + }) + return itemId + } + + /* NAME: ITEM_SUBMIT TYPE: 8 BODY: { @@ -225,42 +247,44 @@ export class River { caption?: string } */ - - private async _msg8_itemSubmit(message: Message): Promise { - if (!isItemSubmitBody(message.messageData.body)) return null - const { itemId, channelId, caption } = message.messageData.body as ItemSubmitBody - - const itemExists = await this.db.query.ItemTable.findFirst({ - where: (items, { eq }) => eq(items.id, itemId) - }) - if (!itemExists) return null - - const channelExists = await this.db.query.channelTable.findFirst({ - where: (channels, { eq }) => eq(channels.id, channelId) - }) - if (!channelExists) return null - - if (caption && caption.length > CAPTION_MAX_LENGTH) return null - - const submissionId = (await this.makeCid(message.messageData)).toString() - - const isOwner = await this.db.query.channelTable.findFirst({ - where: (channels, { and, eq }) => and( - eq(channels.id, channelId), - eq(channels.createdById, message.messageData.rid.toString()) - ) - }) - - await this.db.insert(dbSchema.submissionsTable).values({ - id: submissionId, - content: JSON.stringify(message.messageData.body), - userId: message.messageData.rid.toString(), - }) - - return submissionId - } - - /* + + private async _msg8_itemSubmit(message: Message): Promise { + if (!isItemSubmitBody(message.messageData.body)) return null + const { itemId, channelId, caption } = message.messageData + .body as ItemSubmitBody + + const itemExists = await this.db.query.ItemTable.findFirst({ + where: (items, { eq }) => eq(items.id, itemId), + }) + if (!itemExists) return null + + const channelExists = await this.db.query.channelTable.findFirst({ + where: (channels, { eq }) => eq(channels.id, channelId), + }) + if (!channelExists) return null + + if (caption && caption.length > CAPTION_MAX_LENGTH) return null + + const submissionId = (await this.makeCid(message.messageData)).toString() + + const isOwner = await this.db.query.channelTable.findFirst({ + where: (channels, { and, eq }) => + and( + eq(channels.id, channelId), + eq(channels.createdById, message.messageData.rid.toString()), + ), + }) + + await this.db.insert(dbSchema.submissionsTable).values({ + id: submissionId, + content: JSON.stringify(message.messageData.body), + userId: message.messageData.rid.toString(), + }) + + return submissionId + } + + /* NAME: ITEM_ACCREJ TYPE: 0 BODY: { @@ -269,34 +293,35 @@ export class River { caption?: string } */ - - private async _msg9_itemAccRej(message: Message): Promise { - if (!isItemAccRejBody(message.messageData.body)) return null - const { submissionId, response, caption } = message.messageData.body as ItemAccRejBody - - const submissionExists = await this.db.query.submissionsTable.findFirst({ - where: (submissions, { eq }) => eq(submissions.id, submissionId) - }) - if (!submissionExists) return null - - if (caption && caption.length > CAPTION_MAX_LENGTH) return null - - const accRejId = (await this.makeCid(message.messageData)).toString() - - const isOwnerOrModerator = await this.db.query.channelTable.findFirst({ - where: (channels, { eq }) => eq(channels.createdById, message.messageData.rid.toString()) - // You might need to adjust this query based on how you determine ownership/moderation - }) - if (!isOwnerOrModerator) return null - - await this.db.insert(dbSchema.acceptedRejectedTable).values({ - messageId: accRejId, - submissionId: submissionId, - response: response.toString(), - caption: caption, - }) - - return accRejId - } - - } + + private async _msg9_itemAccRej(message: Message): Promise { + if (!isItemAccRejBody(message.messageData.body)) return null + const { submissionId, response, caption } = message.messageData + .body as ItemAccRejBody + + const submissionExists = await this.db.query.submissionsTable.findFirst({ + where: (submissions, { eq }) => eq(submissions.id, submissionId), + }) + if (!submissionExists) return null + + if (caption && caption.length > CAPTION_MAX_LENGTH) return null + + const accRejId = (await this.makeCid(message.messageData)).toString() + + const isOwnerOrModerator = await this.db.query.channelTable.findFirst({ + where: (channels, { eq }) => + eq(channels.createdById, message.messageData.rid.toString()), + // You might need to adjust this query based on how you determine ownership/moderation + }) + if (!isOwnerOrModerator) return null + + await this.db.insert(dbSchema.acceptedRejectedTable).values({ + messageId: accRejId, + submissionId: submissionId, + response: response.toString(), + caption: caption, + }) + + return accRejId + } +} diff --git a/src/rvm/types.ts b/src/rvm/types.ts index 27a1ee5..c9b06ec 100644 --- a/src/rvm/types.ts +++ b/src/rvm/types.ts @@ -43,7 +43,7 @@ export type Message = { sig: Uint8Array } -export function isMessage(data: any): data is Message { +export function isMessage(data: Message): data is Message { return ( typeof data.signer === 'string' && typeof data.messageData === 'object' && @@ -76,10 +76,14 @@ export type ChannelCreateBody = { uri: string } - // type guard function -export function isChannelCreateBody(obj: any): obj is ChannelCreateBody { - return obj && typeof obj === 'object' && typeof obj.uri === 'string' +export function isChannelCreateBody(obj: unknown): obj is ChannelCreateBody { + return ( + typeof obj === 'object' && + obj !== null && + 'uri' in obj && + typeof (obj as { uri: unknown }).uri === 'string' + ) } /* @@ -117,11 +121,15 @@ export type ItemCreateBody = { } // type guard function -export function isItemCreateBody(obj: any): obj is ChannelCreateBody { - return obj && typeof obj === 'object' && typeof obj.uri === 'string' +export function isItemCreateBody(obj: unknown): obj is ItemCreateBody { + return ( + typeof obj === 'object' && + obj !== null && + 'uri' in obj && + typeof (obj as { uri: unknown }).uri === 'string' + ) } - /* * 6 */ @@ -146,34 +154,41 @@ export type ItemSubmitBody = { caption?: string // MAX 300 CHAR LIMIT } -export function isItemSubmitBody(obj: any): obj is ItemSubmitBody { +export function isItemSubmitBody(obj: unknown): obj is ItemSubmitBody { + if (typeof obj !== 'object' || obj === null) { + return false + } + + const messageBody = obj as Partial + return ( - obj !== null && - typeof obj === 'object' && - typeof obj.itemId === 'string' && - typeof obj.channelId === 'string' && - (obj.caption === undefined || - (typeof obj.caption === 'string' && obj.caption.length <= 300)) + typeof messageBody.itemId === 'string' && + typeof messageBody.channelId === 'string' && + (messageBody.caption === undefined || + (typeof messageBody.caption === 'string' && + messageBody.caption.length <= 300)) ) } - - /* * 9 */ -export function isItemAccRejBody(obj: any): obj is ItemAccRejBody { +export function isItemAccRejBody(obj: unknown): obj is ItemAccRejBody { + if (typeof obj !== 'object' || obj === null) { + return false + } + + const messageBody = obj as Partial + return ( - obj && - typeof obj === 'object' && - typeof obj.submissionId === 'string' && - typeof obj.response === 'boolean' && - (obj.caption === undefined || - (typeof obj.caption === 'string' && obj.caption.length <= 300)) + typeof messageBody.submissionId === 'string' && + typeof messageBody.response === 'boolean' && + (messageBody.caption === undefined || + (typeof messageBody.caption === 'string' && + messageBody.caption.length <= 300)) ) } - export type ItemAccRejBody = { submissionId: string response: boolean // FALSE = rejected, TRUE = accepted @@ -247,4 +262,4 @@ export type MessageDataBodyTypes = | UserSetNameBody | UserSetUriBody - export const CAPTION_MAX_LENGTH = 300 \ No newline at end of file +export const CAPTION_MAX_LENGTH = 300 diff --git a/src/schema.ts b/src/schema.ts index 14756a8..0973a18 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,4 +1,11 @@ -import { integer, pgTable, serial, text, timestamp, numeric } from 'drizzle-orm/pg-core' +import { + integer, + pgTable, + serial, + text, + timestamp, + numeric, +} from 'drizzle-orm/pg-core' export const usersTable = pgTable('users', { id: numeric('userid').primaryKey(), @@ -50,8 +57,7 @@ export const messageTable = pgTable('messages', { messageType: text('messagetype').notNull(), messageBody: text('messagebody').notNull(), hashType: text('hashtype').notNull(), - hash: numeric('hash') - .notNull(), + hash: numeric('hash').notNull(), sigType: text('sigtype').notNull(), sig: text('sig').notNull(), }) @@ -130,4 +136,4 @@ export const acceptedRejectedTable = pgTable('acceptedrejected', { }) export type InsertacceptedRejected = typeof acceptedRejectedTable.$inferInsert -export type SelectacceptedRejected = typeof acceptedRejectedTable.$inferSelect \ No newline at end of file +export type SelectacceptedRejected = typeof acceptedRejectedTable.$inferSelect diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..9efca20 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,8 @@ +import * as dagCbor from '@ipld/dag-cbor' +import * as Block from 'multiformats/block' +import { sha256 } from 'multiformats/hashes/sha2' +import type { Message } from '../rvm/types.js' + +export async function messageToCid(message: Message) { + return await Block.encode({ value: message, codec: dagCbor, hasher: sha256 }) +} From 8e2a08aa78e606d49218f4e00897c68ccfc69a11 Mon Sep 17 00:00:00 2001 From: jawndiego Date: Sat, 29 Jun 2024 22:06:21 -0400 Subject: [PATCH 08/11] publickey --- src/rvm/index.ts | 6 +++--- src/schema.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rvm/index.ts b/src/rvm/index.ts index f299f57..2d055ff 100644 --- a/src/rvm/index.ts +++ b/src/rvm/index.ts @@ -82,10 +82,10 @@ export class River { const result = await this.authDb.query.keyTable.findFirst({ where: (keys, { eq }) => eq(keys.userid, userId), columns: { - encryptedpublickey: true, + publickey: true, }, }) - return result?.encryptedpublickey || null + return result?.publickey || null } // main db @@ -124,7 +124,7 @@ export class River { where: (keys, { and, eq }) => and( eq(keys.userid, message.messageData.rid.toString()), - eq(keys.encryptedpublickey, message.signer), + eq(keys.publickey, message.signer), ), }) diff --git a/src/schema.ts b/src/schema.ts index 0973a18..ea05679 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -40,7 +40,7 @@ export const keyTable = pgTable( .references(() => usersTable.id), custodyAddress: text('custodyAddress').notNull(), deviceid: text('deviceid').notNull(), - encryptedpublickey: text('encryptedpublickey').notNull(), + publickey: text('publickey').notNull(), encryptedprivatekey: text('encryptedprivatekey').notNull(), }, (table) => ({ From dfc9cab2b73b0b20c599cb95184181ff114f0d48 Mon Sep 17 00:00:00 2001 From: 0xTranqui Date: Wed, 3 Jul 2024 11:51:24 -0400 Subject: [PATCH 09/11] types done, still need to add implementation for responding to a submit correctly --- bun.lockb | Bin 51936 -> 51936 bytes src/rvm/index.ts | 54 +++++++++-------- src/rvm/types.ts | 152 +++++++++++++++++++++++++---------------------- 3 files changed, 112 insertions(+), 94 deletions(-) diff --git a/bun.lockb b/bun.lockb index 9effeb4275c74944cc0660c712a650daec09f653..10d6812f93fddcd2583dadd7dfd898d4f2c9cbbe 100755 GIT binary patch delta 843 zcmW;AD@+0b0Eh809csCmh{TJxfqK5r_xV2G=ld+~f`NFENYD-4m_#BGbpWpdZsuJff|23v$hzVwNrIIRHuRIGj)J58s|D8CYZU< zDPRk;zD|T~G%t0E*g@+`r-WU!uXW1UL+3`PihXo%b!s?3?@p(VL-g-;8ki2$0mf)N t=!BTa=GRj+TA7>}ZN5FOnPaJ7-pX6K%Fpx1cq+(ESvf1$e0@1t`2*B`^N#=k delta 841 zcmXZQD@+0b0LF2A47J=$MB+t)kMn)L&-W=l4xeXn7YsyfM1pSU#v~GnsAFEdNF*{7 zBN2&2rr&>l;kR%9&ihE`edP0dYA99;r!s#U(=xya6EiAVY+!O$C5KHkEtNdBFg2%A zz&2X*Dn;y~y`WOY9y*IE73`zCq*BEJddn&`jIZeX7@}dT1Q=mrRV9lJOs=Wqu!-in zN*-I7+E6KA8?8;1B6iWij4-jUlEnrl z4^(p4MDtK3k1b3csT8n{ma9_4F51T`W$dAIqEf*=x@nav4$wPQsbTy~-^UP*bCm!i zOkAjBv4KfXC5KHkGb(v(Vd_$)fNiv{REpR|`&y-pJ#=nVD%eN&R;7vq^zKw@829yk q4AHn(2{5|P)W05|MoW_uqxH9kb#rVu#!5fWALIW&R)2j-ul@r0?C>@K diff --git a/src/rvm/index.ts b/src/rvm/index.ts index 2d055ff..1200690 100644 --- a/src/rvm/index.ts +++ b/src/rvm/index.ts @@ -5,7 +5,7 @@ import * as dbSchema from '../schema.js' import { ed25519ph } from '@noble/curves/ed25519' import type { Message, - ItemAccRejBody, + GenericResponseBody, ItemSubmitBody, ItemCreateBody, MessageData, @@ -14,7 +14,7 @@ import { isChannelCreateBody, isItemCreateBody, isItemSubmitBody, - isItemAccRejBody, + isGenericResponse, MessageTypes, CAPTION_MAX_LENGTH, } from './types.js' @@ -177,7 +177,7 @@ export class River { [MessageTypes.CHANNEL_CREATE]: this._msg1_channelCreate, [MessageTypes.ITEM_CREATE]: this._msg5_itemCreate, [MessageTypes.ITEM_SUBMIT]: this._msg8_itemSubmit, - [MessageTypes.ITEM_ACC_REJ]: this._msg9_itemAccRej, + [MessageTypes.GENERIC_RESPONSE]: this._msg17_genericResponse, } const handler = handlers[message.messageData.type] @@ -294,34 +294,40 @@ export class River { } */ - private async _msg9_itemAccRej(message: Message): Promise { - if (!isItemAccRejBody(message.messageData.body)) return null - const { submissionId, response, caption } = message.messageData - .body as ItemAccRejBody - - const submissionExists = await this.db.query.submissionsTable.findFirst({ - where: (submissions, { eq }) => eq(submissions.id, submissionId), + private async _msg17_genericResponse(message: Message): Promise { + if (!isGenericResponse(message.messageData.body)) return null + const { messageId, response } = message.messageData + .body as GenericResponseBody + + // NOTE: maybe should update messageId format to prepend with messageId + // so that we dont need to keep a global message table and can just + // look up specific message from its corresponding table + // OR perhaps simpler is just some union Message table that joins + // all of the individual message tables for the purpose of this specific query + const messageExists = await this.db.query.messageTable.findFirst({ + where: (messages, { eq }) => eq(messages.id, messageId), }) - if (!submissionExists) return null - if (caption && caption.length > CAPTION_MAX_LENGTH) return null + if (!messageExists) return null - const accRejId = (await this.makeCid(message.messageData)).toString() + const respId = (await this.makeCid(message.messageData)).toString() - const isOwnerOrModerator = await this.db.query.channelTable.findFirst({ - where: (channels, { eq }) => - eq(channels.createdById, message.messageData.rid.toString()), - // You might need to adjust this query based on how you determine ownership/moderation - }) - if (!isOwnerOrModerator) return null - await this.db.insert(dbSchema.acceptedRejectedTable).values({ - messageId: accRejId, - submissionId: submissionId, + /* + TODO: + depending on what message type this is responding to, + process approriate db updaets + ex: if it was response to submit message, update the submission status + if it was response to friend invite, update friend sttatus + */ + + // add this table + await this.db.insert(dbSchema.responseTable).values({ + responseId: respId, + messageId: messageId, response: response.toString(), - caption: caption, }) - return accRejId + return respId } } diff --git a/src/rvm/types.ts b/src/rvm/types.ts index c9b06ec..ab61912 100644 --- a/src/rvm/types.ts +++ b/src/rvm/types.ts @@ -4,25 +4,6 @@ * */ -export enum MessageTypes { - NONE = 0, - CHANNEL_CREATE = 1, - CHANNEL_EDIT_MEMBERS = 2, - CHANNEL_EDIT_URI = 3, - CHANNEL_TRANSFER_OWNER = 4, - ITEM_CREATE = 5, - ITEM_EDIT = 6, - ITEM_DELETE = 7, - ITEM_SUBMIT = 8, - ITEM_ACC_REJ = 9, - ITEM_REMOVE = 10, - COMMENT_CREATE = 11, - COMMENT_EDIT = 12, - COMMENT_DELETE = 13, - USER_SET_NAME = 14, - USER_SET_URI = 15, -} - export enum HashTypes { NONE = 0, BLAKE_3 = 1, @@ -34,6 +15,27 @@ export enum SignatureTypes { EIP712 = 2, } +export enum MessageTypes { + NONE = 0, + CHANNEL_CREATE = 1, + CHANNEL_EDIT = 2, + CHANNEL_DELETE = 3, + CHANNEL_INVITE_MEMBER = 4, + CHANNEL_TRANSFER_OWNER = 5, + ITEM_CREATE = 6, + ITEM_EDIT = 7, + ITEM_DELETE = 8, + ITEM_SUBMIT = 9, + ITEM_REMOVE = 10, + COMMENT_CREATE = 11, + COMMENT_EDIT = 12, + COMMENT_DELETE = 13, + USER_SET_NAME = 14, + USER_SET_DATA = 15, // initially we just support setting a bio capped to specific char count. could eventually support URI schema + USER_INVITE_FRIEND = 16, + GENERIC_RESPONSE = 17, +} + export type Message = { signer: string messageData: MessageData @@ -89,33 +91,38 @@ export function isChannelCreateBody(obj: unknown): obj is ChannelCreateBody { /* * 2 */ -export type ChannelEditMember = { +export type ChannelEditBody = { channelId: string - member: { - rid: bigint - role: 0 | 1 | 2 // 0 = none, 1 = member, 2 = admin - } + uri: string } /* * 3 */ -export type ChannelEditUri = { +export type ChannelDeleteBody = { channelId: string - uri: string } /* * 4 */ -export type ChannelTransferOwner = { +export type ChannelInviteMemberBody = { channelId: string - transferToRid: bigint + memberRid: bigint } + /* * 5 */ +export type ChannelTransferOwnerBody = { + channelId: string + transferToRid: bigint +} + +/* + * 6 + */ export type ItemCreateBody = { uri: string } @@ -131,7 +138,7 @@ export function isItemCreateBody(obj: unknown): obj is ItemCreateBody { } /* - * 6 + * 7 */ export type ItemEditBody = { itemId: string @@ -139,19 +146,19 @@ export type ItemEditBody = { } /* - * 7 + * 8 */ export type ItemDeleteBody = { itemId: string } /* - * 8 + * 9 */ export type ItemSubmitBody = { itemId: string channelId: string - caption?: string // MAX 300 CHAR LIMIT + text?: string // MAX 300 CHAR LIMIT } export function isItemSubmitBody(obj: unknown): obj is ItemSubmitBody { @@ -164,37 +171,12 @@ export function isItemSubmitBody(obj: unknown): obj is ItemSubmitBody { return ( typeof messageBody.itemId === 'string' && typeof messageBody.channelId === 'string' && - (messageBody.caption === undefined || - (typeof messageBody.caption === 'string' && - messageBody.caption.length <= 300)) + (messageBody.text === undefined || + (typeof messageBody.text === 'string' && + messageBody.text.length <= 300)) ) } -/* - * 9 - */ - -export function isItemAccRejBody(obj: unknown): obj is ItemAccRejBody { - if (typeof obj !== 'object' || obj === null) { - return false - } - - const messageBody = obj as Partial - - return ( - typeof messageBody.submissionId === 'string' && - typeof messageBody.response === 'boolean' && - (messageBody.caption === undefined || - (typeof messageBody.caption === 'string' && - messageBody.caption.length <= 300)) - ) -} -export type ItemAccRejBody = { - submissionId: string - response: boolean // FALSE = rejected, TRUE = accepted - caption?: string // MAX_CHAR_LIMIT = 300 -} - /* * 10 */ @@ -228,18 +210,46 @@ export type CommentDeleteBody = { /* * 14 */ +// TODO: set up the protocol logic for handling this correectly +// based off how we are currently doing it in username DB export type UserSetNameBody = { - fromId: bigint - toId: bigint - username: string // MAX_CHAR_LIMIT = 15 + regex + fromRid?: bigint + toRid?: bigint + username?: string // MAX_CHAR_LIMIT = 15 + regex } /* * 15 */ -export type UserSetUriBody = { +export type UserSetDataBody = { rid: bigint - uri: string + data: string // initially just support pure text "bios" of a max length +} + +/* + * 16 + */ +export type UserInviteFriendBody = { + rid: bigint +} + +/* + * 17 + */ +export type GenericResponseBody = { + messageId: string + response: 0 | 1 +} + +export function isGenericResponse(obj: unknown): obj is GenericResponseBody { + return ( + typeof obj === 'object' && + obj !== null && + 'messageId' in obj && + typeof (obj as { messageId: unknown }).messageId === 'string' && + 'response' in obj && + ((obj as { response: unknown }).response === 0 || (obj as { response: unknown }).response === 1) + ) } /* @@ -247,19 +257,21 @@ export type UserSetUriBody = { */ export type MessageDataBodyTypes = | ChannelCreateBody - | ChannelEditMember - | ChannelEditUri - | ChannelTransferOwner + | ChannelEditBody + | ChannelInviteMemberBody + | ChannelTransferOwnerBody | ItemCreateBody | ItemEditBody | ItemDeleteBody | ItemSubmitBody - | ItemAccRejBody | ItemRemoveBody | CommentCreateBody | CommentEditBody | CommentDeleteBody | UserSetNameBody - | UserSetUriBody + | UserSetDataBody + | UserInviteFriendBody + | GenericResponseBody export const CAPTION_MAX_LENGTH = 300 +export const BIO_MAX_LENGTH = 50 \ No newline at end of file From 9b7d81ae35b6d53553d59c81c0b3e945cb2adf9e Mon Sep 17 00:00:00 2001 From: 0xTranqui Date: Wed, 3 Jul 2024 16:37:56 -0400 Subject: [PATCH 10/11] initial implementaton of generic response type working for item submissions. removed unccessary code --- bun.lockb | Bin 51936 -> 52274 bytes package.json | 1 + src/rvm/index.ts | 366 ++++++++++++++++++++++++--------------------- src/rvm/types.ts | 4 +- src/schema.ts | 44 +++--- src/utils/index.ts | 29 +++- 6 files changed, 250 insertions(+), 194 deletions(-) diff --git a/bun.lockb b/bun.lockb index 10d6812f93fddcd2583dadd7dfd898d4f2c9cbbe..f266a0305cfe2a32e3413e6e3ac6d156abe6ae92 100755 GIT binary patch delta 7711 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 delta 7519 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 XKHoQr?%bc(Te;sqwD-`98}$DU9!usN diff --git a/package.json b/package.json index a6300ea..80e47cc 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "dependencies": { "@ipld/dag-cbor": "^9.2.1", "@noble/curves": "^1.4.0", + "@scure/base": "^1.1.7", "drizzle-graphql": "^0.8.3", "drizzle-orm": "^0.31.2", "graphql": "^16.8.1", diff --git a/src/rvm/index.ts b/src/rvm/index.ts index 1200690..45c1ef5 100644 --- a/src/rvm/index.ts +++ b/src/rvm/index.ts @@ -1,15 +1,15 @@ -import { Pool } from 'pg' -import { drizzle } from 'drizzle-orm/node-postgres' -import type { NodePgDatabase } from 'drizzle-orm/node-postgres' -import * as dbSchema from '../schema.js' -import { ed25519ph } from '@noble/curves/ed25519' +import { Pool } from "pg"; +import { drizzle } from "drizzle-orm/node-postgres"; +import type { NodePgDatabase } from "drizzle-orm/node-postgres"; +import * as dbSchema from "../schema.js"; +import { ed25519ph } from "@noble/curves/ed25519"; import type { Message, GenericResponseBody, ItemSubmitBody, ItemCreateBody, MessageData, -} from './types.js' +} from "./types.js"; import { isChannelCreateBody, isItemCreateBody, @@ -17,106 +17,107 @@ import { isGenericResponse, MessageTypes, CAPTION_MAX_LENGTH, -} from './types.js' -import * as dagCbor from '@ipld/dag-cbor' -import * as Block from 'multiformats/block' -import { sha256 } from 'multiformats/hashes/sha2' +} from "./types.js"; +import * as dagCbor from "@ipld/dag-cbor"; +import * as Block from "multiformats/block"; +import { sha256 } from "multiformats/hashes/sha2"; +import { base64url } from "@scure/base"; +import { messageBodyToBase64Url } from "../utils/index.js"; +import { eq } from "drizzle-orm"; + +// HELPERS +// TODO: move into own lib + +export async function makeCid(messageData: MessageData) { + return await Block.encode({ + value: messageData, + codec: dagCbor, + hasher: sha256, + }); +} + +export function formatItemCreateMessage({ + rid, + fileUri, +}: { + rid: bigint; + fileUri: string; +}): Message { + const message: Message = { + signer: "0x", + messageData: { + rid: rid, + timestamp: BigInt(Date.now()), + type: MessageTypes.ITEM_CREATE, + body: { uri: fileUri } as ItemCreateBody, + }, + hashType: 1, + hash: new Uint8Array(0), + sigType: 1, + sig: new Uint8Array(0), + }; + return message; +} + +// OFFICIAL RIVER CLASS export class River { - private db: NodePgDatabase - private authDb: NodePgDatabase - private pool: Pool - private authPool: Pool + private db: NodePgDatabase; + private authDb: NodePgDatabase; + private pool: Pool; + private authPool: Pool; private constructor( db: NodePgDatabase, authDb: NodePgDatabase, pool: Pool, - authPool: Pool, + authPool: Pool ) { - this.db = db - this.authDb = authDb - this.pool = pool - this.authPool = authPool + this.db = db; + this.authDb = authDb; + this.pool = pool; + this.authPool = authPool; } static async flow(): Promise { - const connectionString = process.env.DATABASE_URL! - const authConnectionString = process.env.AUTH_DATABASE_URL! + const connectionString = process.env.DATABASE_URL!; + const authConnectionString = process.env.AUTH_DATABASE_URL!; - const pool = new Pool({ connectionString }) - const authPool = new Pool({ connectionString: authConnectionString }) + const pool = new Pool({ connectionString }); + const authPool = new Pool({ connectionString: authConnectionString }); try { - const client = await pool.connect() - client.release() - const authClient = await authPool.connect() - authClient.release() - console.log('Database connections successful') + const client = await pool.connect(); + client.release(); + const authClient = await authPool.connect(); + authClient.release(); + console.log("Database connections successful"); } catch (err) { - console.error('Failed to connect to the databases', err) - throw err + console.error("Failed to connect to the databases", err); + throw err; } - const db = drizzle(pool, { schema: dbSchema }) - const authDb = drizzle(authPool, { schema: dbSchema }) + const db = drizzle(pool, { schema: dbSchema }); + const authDb = drizzle(authPool, { schema: dbSchema }); - return new River(db, authDb, pool, authPool) + return new River(db, authDb, pool, authPool); } async disconnect() { - await this.pool.end() - await this.authPool.end() - } - - // simple getters should not replace graphql - // auth db - - async getUser(userId: bigint) { - return this.authDb.query.usersTable.findFirst({ - where: (users, { eq }) => eq(users.id, userId.toString()), - }) - } - - async getPublicKey(userId: string) { - const result = await this.authDb.query.keyTable.findFirst({ - where: (keys, { eq }) => eq(keys.userid, userId), - columns: { - publickey: true, - }, - }) - return result?.publickey || null - } - - // main db - - async getChannel(channelId: string) { - return this.db.query.channelTable.findFirst({ - where: (channels, { eq }) => eq(channels.id, channelId), - }) + await this.pool.end(); + await this.authPool.end(); } - async getItem(itemId: string) { - return this.db.query.ItemTable.findFirst({ - where: (items, { eq }) => eq(items.id, itemId), - }) - } - - public async makeCid(messageData: MessageData) { - return await Block.encode({ - value: messageData, - codec: dagCbor, - hasher: sha256, - }) - } + // simple getters should not replace graphql over authdb + // NOTE: i removed all of these because unncessary code at the moment public async verifyMessage(message: Message): Promise { // 1. lookup user id const userExists = await this.authDb.query.usersTable.findFirst({ where: (users, { eq }) => eq(users.id, message.messageData.rid.toString()), - }) - if (!userExists) return false + }); + if (!userExists) return false; // 2. lookup signing key const keyExistsForUserAtTimestamp = @@ -124,64 +125,65 @@ export class River { where: (keys, { and, eq }) => and( eq(keys.userid, message.messageData.rid.toString()), - eq(keys.publickey, message.signer), + eq(keys.publickey, message.signer) ), - }) + }); - if (!keyExistsForUserAtTimestamp) return false + if (!keyExistsForUserAtTimestamp) return false; // 3. verify hash of message = message.messageHash // investigate actual hashing function - const computedHash = await this.makeCid(message.messageData) - if (computedHash.toString() !== message.hash.toString()) return false + const computedHash = await makeCid(message.messageData); + if (computedHash.toString() !== message.hash.toString()) return false; // 4. verify signature is valid over hash - const valid = ed25519ph.verify(message.sig, message.hash, message.signer) - if (!valid) return false + const valid = ed25519ph.verify(message.sig, message.hash, message.signer); + if (!valid) return false; // 5. return true if all checks passed - return true - } - - public formatItemCreateMessage({ - rid, - fileUri, - }: { - rid: bigint - fileUri: string - }): Message { - const message: Message = { - signer: '0x', - messageData: { - rid: rid, - timestamp: BigInt(Date.now()), - type: MessageTypes.ITEM_CREATE, - body: { uri: fileUri } as ItemCreateBody, - }, - hashType: 1, - hash: new Uint8Array(0), - sigType: 1, - sig: new Uint8Array(0), - } - return message + return true; } public async processMessage(message: Message): Promise { if (!Object.values(MessageTypes).includes(message.messageData.type)) - return null + return null; const handlers: { - [K in MessageTypes]?: (message: Message) => Promise + [K in MessageTypes]?: (message: Message) => Promise; } = { [MessageTypes.CHANNEL_CREATE]: this._msg1_channelCreate, - [MessageTypes.ITEM_CREATE]: this._msg5_itemCreate, - [MessageTypes.ITEM_SUBMIT]: this._msg8_itemSubmit, + [MessageTypes.ITEM_CREATE]: this._msg6_itemCreate, + [MessageTypes.ITEM_SUBMIT]: this._msg9_itemSubmit, [MessageTypes.GENERIC_RESPONSE]: this._msg17_genericResponse, - } + }; + + const handler = handlers[message.messageData.type]; + if (!handler) return null; // this check should be unncessary because we're checking for valid types at beginning of funciton? - const handler = handlers[message.messageData.type] - return handler ? handler.call(this, message) : null + const result = await handler.call(this, message); + if (!result) return null; + + this._storeValidMessage(result, message); // if handler returned cid we know its valid, store message in messageTable + return result; + } + + private async _storeValidMessage( + messageId: string, + message: Message + ): Promise { + await this.db.insert(dbSchema.messageTable).values({ + id: messageId, + rid: message.messageData.rid, + timestamp: message.messageData.timestamp, + type: message.messageData.type, + body: messageBodyToBase64Url(message.messageData.body), + signer: message.signer, + hashType: message.hashType, + hash: base64url.encode(message.hash), + sigType: message.sigType, + sig: base64url.encode(message.sig), + }); } /* @@ -199,9 +201,9 @@ export class River { private async _msg1_channelCreate(message: Message): Promise { // make sure message data body is correct type - if (!isChannelCreateBody(message.messageData.body)) return null + if (!isChannelCreateBody(message.messageData.body)) return null; // generate channel id - const channelId = (await this.makeCid(message.messageData)).toString() + const channelId = (await makeCid(message.messageData)).toString(); // update RVM storage await this.db.insert(dbSchema.channelTable).values({ id: channelId, @@ -210,37 +212,37 @@ export class River { createdById: message.messageData.rid.toString(), uri: message.messageData.body.uri, // destructure cid to extract name and description? - name: '', - description: '', - }) - return channelId + name: "", + description: "", + }); + return channelId; } /* NAME: ITEM_CREATE - TYPE: 5 + TYPE: 6 BODY: { uri: string } */ - private async _msg5_itemCreate(message: Message): Promise { + private async _msg6_itemCreate(message: Message): Promise { // make sure message data body is correct type - if (!isItemCreateBody(message.messageData.body)) return null + if (!isItemCreateBody(message.messageData.body)) return null; // generate itemId - const itemId = (await this.makeCid(message.messageData)).toString() + const itemId = (await makeCid(message.messageData)).toString(); // update RVM storage await this.db.insert(dbSchema.ItemTable).values({ id: itemId, createdById: message.messageData.rid.toString(), uri: message.messageData.body.uri, - }) - return itemId + }); + return itemId; } /* NAME: ITEM_SUBMIT - TYPE: 8 + TYPE: 9 BODY: { itemId: string channelId: string @@ -248,56 +250,59 @@ export class River { } */ - private async _msg8_itemSubmit(message: Message): Promise { - if (!isItemSubmitBody(message.messageData.body)) return null - const { itemId, channelId, caption } = message.messageData - .body as ItemSubmitBody + private async _msg9_itemSubmit(message: Message): Promise { + if (!isItemSubmitBody(message.messageData.body)) return null; + const { itemId, channelId, text } = message.messageData + .body as ItemSubmitBody; const itemExists = await this.db.query.ItemTable.findFirst({ where: (items, { eq }) => eq(items.id, itemId), - }) - if (!itemExists) return null + }); + if (!itemExists) return null; const channelExists = await this.db.query.channelTable.findFirst({ where: (channels, { eq }) => eq(channels.id, channelId), - }) - if (!channelExists) return null + }); + if (!channelExists) return null; - if (caption && caption.length > CAPTION_MAX_LENGTH) return null + if (text && text.length > CAPTION_MAX_LENGTH) return null; - const submissionId = (await this.makeCid(message.messageData)).toString() + const submissionId = (await makeCid(message.messageData)).toString(); + // TODO: make this an isOwnerOrMod lookup const isOwner = await this.db.query.channelTable.findFirst({ where: (channels, { and, eq }) => and( eq(channels.id, channelId), - eq(channels.createdById, message.messageData.rid.toString()), + eq(channels.createdById, message.messageData.rid.toString()) ), - }) + }); await this.db.insert(dbSchema.submissionsTable).values({ id: submissionId, content: JSON.stringify(message.messageData.body), userId: message.messageData.rid.toString(), - }) + status: isOwner ? 3 : 0, // channel owenrs/mods get their submissions automatically set to 2 (0 = pending, 1 = declined, 2 = accepted, 3 = owner/mod) + }); - return submissionId + return submissionId; } /* - NAME: ITEM_ACCREJ - TYPE: 0 + NAME: GENERIC_RESPONSE + TYPE: 17 BODY: { - submissionId: string - Response: boolean - caption?: string + messageId: string + response: boolean } */ - private async _msg17_genericResponse(message: Message): Promise { - if (!isGenericResponse(message.messageData.body)) return null + private async _msg17_genericResponse( + message: Message + ): Promise { + if (!isGenericResponse(message.messageData.body)) return null; const { messageId, response } = message.messageData - .body as GenericResponseBody + .body as GenericResponseBody; // NOTE: maybe should update messageId format to prepend with messageId // so that we dont need to keep a global message table and can just @@ -306,28 +311,49 @@ export class River { // all of the individual message tables for the purpose of this specific query const messageExists = await this.db.query.messageTable.findFirst({ where: (messages, { eq }) => eq(messages.id, messageId), - }) - - if (!messageExists) return null - - const respId = (await this.makeCid(message.messageData)).toString() - - - /* - TODO: - depending on what message type this is responding to, - process approriate db updaets - ex: if it was response to submit message, update the submission status - if it was response to friend invite, update friend sttatus - */ + }); + + if (!messageExists) return null; + + const responseId = (await makeCid(message.messageData)).toString(); + + // process things differently depending on type of message + // the genericResponse was targeting + switch (messageExists.type) { + // generic response handler for + case MessageTypes.ITEM_SUBMIT: + // lookup submit message + const submission = await this.db.query.submissionsTable.findFirst({ + where: (submissions, { eq }) => eq(submissions.id, messageId), + }); + // return null if coudnt find submisison + if (!submission) return null; + // return null if submission status not equal to pending + if (submission.status != 0) return null; + // update status field + await this.db + .update(dbSchema.submissionsTable) + .set({ status: response ? 2 : 1 }) // if response == true, set status to accepted (2), if false set to rejected (1) + .where(eq(dbSchema.submissionsTable.id, messageId)); + break; + case MessageTypes.CHANNEL_INVITE_MEMBER: + // TODO: add logic + break; + case MessageTypes.USER_INVITE_FRIEND: + // TODO: add logic + break; + // Default case if no matching case is found + default: + break; + } // add this table - await this.db.insert(dbSchema.responseTable).values({ - responseId: respId, - messageId: messageId, - response: response.toString(), - }) + await this.db.insert(dbSchema.responsesTable).values({ + id: responseId, + targetMessageId: messageId, + response: response, + }); - return respId + return responseId; } } diff --git a/src/rvm/types.ts b/src/rvm/types.ts index ab61912..64843b5 100644 --- a/src/rvm/types.ts +++ b/src/rvm/types.ts @@ -238,7 +238,7 @@ export type UserInviteFriendBody = { */ export type GenericResponseBody = { messageId: string - response: 0 | 1 + response: boolean } export function isGenericResponse(obj: unknown): obj is GenericResponseBody { @@ -270,7 +270,7 @@ export type MessageDataBodyTypes = | CommentDeleteBody | UserSetNameBody | UserSetDataBody - | UserInviteFriendBody + | UserSetDataBody | GenericResponseBody export const CAPTION_MAX_LENGTH = 300 diff --git a/src/schema.ts b/src/schema.ts index ea05679..5987bcb 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -5,6 +5,8 @@ import { text, timestamp, numeric, + bigint, + boolean } from 'drizzle-orm/pg-core' export const usersTable = pgTable('users', { @@ -52,15 +54,17 @@ export type InsertHash = typeof keyTable.$inferInsert export type SelectHash = typeof keyTable.$inferSelect export const messageTable = pgTable('messages', { - id: text('messageid').primaryKey(), + id: text('id').primaryKey(), + rid: bigint('rid', { mode: 'bigint' }), + timestamp: bigint('timestamp', { mode: 'bigint' }), + type: integer('type'), + body: text('body'), // this will be base64url encoded body object signer: text('signer').notNull(), - messageType: text('messagetype').notNull(), - messageBody: text('messagebody').notNull(), - hashType: text('hashtype').notNull(), - hash: numeric('hash').notNull(), - sigType: text('sigtype').notNull(), + hashType: integer('hashtype').notNull(), + hash: text('hash').notNull(), + sigType: integer('sigtype').notNull(), sig: text('sig').notNull(), -}) +}); export type InsertPost = typeof messageTable.$inferInsert export type SelectPost = typeof messageTable.$inferSelect @@ -84,7 +88,7 @@ export type InsertChannel = typeof channelTable.$inferInsert export type SelectChannel = typeof channelTable.$inferSelect export const ItemTable = pgTable('items', { - id: text('messageId') + id: text('itemId') .notNull() .references(() => messageTable.id) .primaryKey(), @@ -102,7 +106,7 @@ export type InsertItem = typeof ItemTable.$inferInsert export type SelectItem = typeof ItemTable.$inferSelect export const submissionsTable = pgTable('submissions', { - id: text('messageId') + id: text('submissionId') .notNull() .references(() => messageTable.id) .primaryKey(), @@ -114,26 +118,24 @@ export const submissionsTable = pgTable('submissions', { updatedAt: timestamp('updated_at') .notNull() .$onUpdate(() => new Date()), + status: integer('status') }) export type InsertSubmission = typeof submissionsTable.$inferInsert export type SelectSubmission = typeof submissionsTable.$inferSelect -export const acceptedRejectedTable = pgTable('acceptedrejected', { - messageId: text('messageId') +export const responsesTable = pgTable('responses', { + id: text('responses') .notNull() .references(() => messageTable.id) .primaryKey(), - submissionId: text('submissionid') - .notNull() - .references(() => submissionsTable.id), - response: text('response').notNull(), - caption: text('caption'), createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at') - .notNull() - .$onUpdate(() => new Date()), + // would be nice if this could reference the specific message + // makes me wonder if we should have specific message response types per message that requires a respojnse + targetMessageId: text('targetMessageId') + .notNull(), + response: boolean('response').notNull() }) -export type InsertacceptedRejected = typeof acceptedRejectedTable.$inferInsert -export type SelectacceptedRejected = typeof acceptedRejectedTable.$inferSelect +export type InsertResponses = typeof responsesTable.$inferInsert +export type SelectResponses = typeof responsesTable.$inferSelect diff --git a/src/utils/index.ts b/src/utils/index.ts index 9efca20..0ead2a7 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,8 +1,35 @@ import * as dagCbor from '@ipld/dag-cbor' import * as Block from 'multiformats/block' import { sha256 } from 'multiformats/hashes/sha2' -import type { Message } from '../rvm/types.js' +import type { Message, MessageDataBodyTypes } from '../rvm/types.js' +import { base64url } from '@scure/base' export async function messageToCid(message: Message) { return await Block.encode({ value: message, codec: dagCbor, hasher: sha256 }) } + +export function messageBodyToBase64Url(messageBody: MessageDataBodyTypes): string { + const jsonString = JSON.stringify(messageBody, (key, value) => + typeof value === "bigint" ? value.toString() : value + ); + const encoder = new TextEncoder() + const uint8Array = encoder.encode(jsonString) + return base64url.encode(uint8Array) +} + +export function base64UrlToMessageBody(encodedMessageBody: string): MessageDataBodyTypes { + const uint8Array = base64url.decode(encodedMessageBody); + const decoder = new TextDecoder(); + const json = decoder.decode(uint8Array); + + return JSON.parse(json, (key, value) => { + // Check if the value is a string and can be parsed as a number + if (typeof value === "string" && !isNaN(Number(value))) { + // Convert back to BigInt if it was originally a bigint + if (BigInt(value).toString() === value) { + return BigInt(value); + } + } + return value; + }); +} \ No newline at end of file From be1735ceeee4376314debe9d51f8817f5f575f16 Mon Sep 17 00:00:00 2001 From: 0xTranqui Date: Thu, 4 Jul 2024 11:02:43 -0400 Subject: [PATCH 11/11] clean up repo + structure --- migrations/0000_large_kylun.sql | 22 ----- migrations/meta/0000_snapshot.json | 112 ----------------------- migrations/meta/_journal.json | 13 --- src/index.ts | 10 +- src/rvm/index.ts | 60 ++---------- src/rvm/{ => lib}/types.ts | 0 src/{utils/index.ts => rvm/lib/utils.ts} | 33 ++++++- src/schema.ts | 1 - 8 files changed, 46 insertions(+), 205 deletions(-) delete mode 100644 migrations/0000_large_kylun.sql delete mode 100644 migrations/meta/0000_snapshot.json delete mode 100644 migrations/meta/_journal.json rename src/rvm/{ => lib}/types.ts (100%) rename src/{utils/index.ts => rvm/lib/utils.ts} (63%) diff --git a/migrations/0000_large_kylun.sql b/migrations/0000_large_kylun.sql deleted file mode 100644 index 6315866..0000000 --- a/migrations/0000_large_kylun.sql +++ /dev/null @@ -1,22 +0,0 @@ -CREATE TABLE IF NOT EXISTS "posts_table" ( - "id" serial PRIMARY KEY NOT NULL, - "title" text NOT NULL, - "content" text NOT NULL, - "user_id" integer NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp NOT NULL -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "users_table" ( - "id" serial PRIMARY KEY NOT NULL, - "name" text NOT NULL, - "age" integer NOT NULL, - "email" text NOT NULL, - CONSTRAINT "users_table_email_unique" UNIQUE("email") -); ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "posts_table" ADD CONSTRAINT "posts_table_user_id_users_table_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users_table"("id") ON DELETE cascade ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; diff --git a/migrations/meta/0000_snapshot.json b/migrations/meta/0000_snapshot.json deleted file mode 100644 index 1b15d6b..0000000 --- a/migrations/meta/0000_snapshot.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "id": "61e17345-4a42-4d3c-ae27-10232ff75045", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.posts_table": { - "name": "posts_table", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "posts_table_user_id_users_table_id_fk": { - "name": "posts_table_user_id_users_table_id_fk", - "tableFrom": "posts_table", - "tableTo": "users_table", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.users_table": { - "name": "users_table", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "age": { - "name": "age", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_table_email_unique": { - "name": "users_table_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - } - } - }, - "enums": {}, - "schemas": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json deleted file mode 100644 index 7e41abc..0000000 --- a/migrations/meta/_journal.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1718115001880, - "tag": "0000_large_kylun", - "breakpoints": true - } - ] -} diff --git a/src/index.ts b/src/index.ts index 4c01b89..bd163e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { Hono } from 'hono' -import type { Message } from '../src/rvm/types.js' +import type { Message } from './rvm/lib/types.js' import { River } from './rvm/index.js' -import { isMessage } from '../src/rvm/types.js' +import { isMessage } from './rvm/lib/types.js' const app = new Hono() @@ -12,9 +12,9 @@ app.post('/message', async (c) => { // Receive data const data = await c.req.json() - // if (!isMessage(data.message)) { - // return c.json({ error: 'Invalid message format' }, 400) - // } + if (!isMessage(data.message)) { + return c.json({ error: 'Invalid message format' }, 400) + } const message: Message = data.message diff --git a/src/rvm/index.ts b/src/rvm/index.ts index 45c1ef5..dc1f396 100644 --- a/src/rvm/index.ts +++ b/src/rvm/index.ts @@ -1,15 +1,9 @@ import { Pool } from "pg"; -import { drizzle } from "drizzle-orm/node-postgres"; -import type { NodePgDatabase } from "drizzle-orm/node-postgres"; -import * as dbSchema from "../schema.js"; import { ed25519ph } from "@noble/curves/ed25519"; -import type { - Message, - GenericResponseBody, - ItemSubmitBody, - ItemCreateBody, - MessageData, -} from "./types.js"; +import { base64url } from "@scure/base"; +import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres"; +import { eq } from "drizzle-orm"; +import * as dbSchema from "../schema.js"; import { isChannelCreateBody, isItemCreateBody, @@ -17,47 +11,11 @@ import { isGenericResponse, MessageTypes, CAPTION_MAX_LENGTH, -} from "./types.js"; -import * as dagCbor from "@ipld/dag-cbor"; -import * as Block from "multiformats/block"; -import { sha256 } from "multiformats/hashes/sha2"; -import { base64url } from "@scure/base"; -import { messageBodyToBase64Url } from "../utils/index.js"; -import { eq } from "drizzle-orm"; - -// HELPERS -// TODO: move into own lib - -export async function makeCid(messageData: MessageData) { - return await Block.encode({ - value: messageData, - codec: dagCbor, - hasher: sha256, - }); -} - -export function formatItemCreateMessage({ - rid, - fileUri, -}: { - rid: bigint; - fileUri: string; -}): Message { - const message: Message = { - signer: "0x", - messageData: { - rid: rid, - timestamp: BigInt(Date.now()), - type: MessageTypes.ITEM_CREATE, - body: { uri: fileUri } as ItemCreateBody, - }, - hashType: 1, - hash: new Uint8Array(0), - sigType: 1, - sig: new Uint8Array(0), - }; - return message; -} + Message, + GenericResponseBody, + ItemSubmitBody, +} from "./lib/types.js"; +import { messageBodyToBase64Url, makeCid } from "./lib/utils.js" // OFFICIAL RIVER CLASS diff --git a/src/rvm/types.ts b/src/rvm/lib/types.ts similarity index 100% rename from src/rvm/types.ts rename to src/rvm/lib/types.ts diff --git a/src/utils/index.ts b/src/rvm/lib/utils.ts similarity index 63% rename from src/utils/index.ts rename to src/rvm/lib/utils.ts index 0ead2a7..b2461ed 100644 --- a/src/utils/index.ts +++ b/src/rvm/lib/utils.ts @@ -1,13 +1,21 @@ import * as dagCbor from '@ipld/dag-cbor' import * as Block from 'multiformats/block' import { sha256 } from 'multiformats/hashes/sha2' -import type { Message, MessageDataBodyTypes } from '../rvm/types.js' +import { Message, MessageData, MessageDataBodyTypes, MessageTypes, ItemCreateBody } from './types.js' import { base64url } from '@scure/base' export async function messageToCid(message: Message) { return await Block.encode({ value: message, codec: dagCbor, hasher: sha256 }) } +export async function makeCid(messageData: MessageData) { + return await Block.encode({ + value: messageData, + codec: dagCbor, + hasher: sha256, + }); +} + export function messageBodyToBase64Url(messageBody: MessageDataBodyTypes): string { const jsonString = JSON.stringify(messageBody, (key, value) => typeof value === "bigint" ? value.toString() : value @@ -32,4 +40,27 @@ export function base64UrlToMessageBody(encodedMessageBody: string): MessageDataB } return value; }); +} + +export function formatItemCreateMessage({ + rid, + fileUri, +}: { + rid: bigint; + fileUri: string; +}): Message { + const message: Message = { + signer: "0x", + messageData: { + rid: rid, + timestamp: BigInt(Date.now()), + type: MessageTypes.ITEM_CREATE, + body: { uri: fileUri } as ItemCreateBody, + }, + hashType: 1, + hash: new Uint8Array(0), + sigType: 1, + sig: new Uint8Array(0), + }; + return message; } \ No newline at end of file diff --git a/src/schema.ts b/src/schema.ts index 5987bcb..2ad1804 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,7 +1,6 @@ import { integer, pgTable, - serial, text, timestamp, numeric,