CCN invitation and write permissions to CCN
This commit is contained in:
parent
a8ffce918e
commit
36c7401fa8
5 changed files with 395 additions and 107 deletions
|
@ -14,6 +14,71 @@ import { sql } from './utils/queries.ts';
|
|||
export class EventAlreadyExistsException extends Error {}
|
||||
export class ChunkedEventReceived extends Error {}
|
||||
|
||||
export function createEncryptedEventForPubkey(
|
||||
pubkey: string,
|
||||
event: nostrTools.VerifiedEvent,
|
||||
) {
|
||||
const randomPrivateKey = nostrTools.generateSecretKey();
|
||||
const randomPrivateKeyPubKey = nostrTools.getPublicKey(randomPrivateKey);
|
||||
const conversationKey = nip44.getConversationKey(randomPrivateKey, pubkey);
|
||||
|
||||
const eventJson = JSON.stringify(event);
|
||||
const encryptedEvent = nip44.encrypt(eventJson, conversationKey);
|
||||
|
||||
const sealTemplate = {
|
||||
kind: 13,
|
||||
created_at: randomTimeUpTo2DaysInThePast(),
|
||||
content: encryptedEvent,
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const seal = nostrTools.finalizeEvent(sealTemplate, randomPrivateKey);
|
||||
const giftWrapTemplate = {
|
||||
kind: 1059,
|
||||
created_at: randomTimeUpTo2DaysInThePast(),
|
||||
content: nip44.encrypt(JSON.stringify(seal), conversationKey),
|
||||
tags: [['p', pubkey]],
|
||||
pubkey: randomPrivateKeyPubKey,
|
||||
};
|
||||
const minedGiftWrap = nostrTools.nip13.minePow(giftWrapTemplate, POW_TO_MINE);
|
||||
|
||||
const giftWrap = nostrTools.finalizeEvent(minedGiftWrap, randomPrivateKey);
|
||||
return giftWrap;
|
||||
}
|
||||
|
||||
export function createEncryptedChunkForPubkey(
|
||||
pubkey: string,
|
||||
chunk: string,
|
||||
chunkIndex: number,
|
||||
totalChunks: number,
|
||||
messageId: string,
|
||||
privateKey: Uint8Array,
|
||||
) {
|
||||
const randomPrivateKey = nostrTools.generateSecretKey();
|
||||
const randomPrivateKeyPubKey = nostrTools.getPublicKey(randomPrivateKey);
|
||||
const conversationKey = nip44.getConversationKey(randomPrivateKey, pubkey);
|
||||
|
||||
const sealTemplate = {
|
||||
kind: 13,
|
||||
created_at: randomTimeUpTo2DaysInThePast(),
|
||||
content: nip44.encrypt(chunk, conversationKey),
|
||||
tags: [['chunk', String(chunkIndex), String(totalChunks), messageId]],
|
||||
};
|
||||
|
||||
const seal = nostrTools.finalizeEvent(sealTemplate, privateKey);
|
||||
const giftWrapTemplate = {
|
||||
kind: 1059,
|
||||
created_at: randomTimeUpTo2DaysInThePast(),
|
||||
content: nip44.encrypt(JSON.stringify(seal), conversationKey),
|
||||
tags: [['p', pubkey]],
|
||||
pubkey: randomPrivateKeyPubKey,
|
||||
};
|
||||
|
||||
const minedGiftWrap = nostrTools.nip13.minePow(giftWrapTemplate, POW_TO_MINE);
|
||||
const giftWrap = nostrTools.finalizeEvent(minedGiftWrap, randomPrivateKey);
|
||||
return giftWrap;
|
||||
}
|
||||
|
||||
export async function createEncryptedEvent(
|
||||
event: nostrTools.VerifiedEvent,
|
||||
db: Database,
|
||||
|
@ -29,32 +94,7 @@ export async function createEncryptedEvent(
|
|||
|
||||
const eventJson = JSON.stringify(event);
|
||||
if (eventJson.length <= MAX_CHUNK_SIZE) {
|
||||
const randomPrivateKey = nostrTools.generateSecretKey();
|
||||
const randomPrivateKeyPubKey = nostrTools.getPublicKey(randomPrivateKey);
|
||||
const conversationKey = nip44.getConversationKey(
|
||||
randomPrivateKey,
|
||||
ccnPubKey,
|
||||
);
|
||||
const sealTemplate = {
|
||||
kind: 13,
|
||||
created_at: randomTimeUpTo2DaysInThePast(),
|
||||
content: nip44.encrypt(eventJson, conversationKey),
|
||||
tags: [],
|
||||
};
|
||||
const seal = nostrTools.finalizeEvent(sealTemplate, ccnPrivateKey);
|
||||
const giftWrapTemplate = {
|
||||
kind: 1059,
|
||||
created_at: randomTimeUpTo2DaysInThePast(),
|
||||
content: nip44.encrypt(JSON.stringify(seal), conversationKey),
|
||||
tags: [['p', ccnPubKey]],
|
||||
pubkey: randomPrivateKeyPubKey,
|
||||
};
|
||||
const minedGiftWrap = nostrTools.nip13.minePow(
|
||||
giftWrapTemplate,
|
||||
POW_TO_MINE,
|
||||
);
|
||||
const giftWrap = nostrTools.finalizeEvent(minedGiftWrap, randomPrivateKey);
|
||||
return giftWrap;
|
||||
return createEncryptedEventForPubkey(ccnPubKey, event);
|
||||
}
|
||||
|
||||
const chunks: string[] = [];
|
||||
|
@ -67,35 +107,15 @@ export async function createEncryptedEvent(
|
|||
const encryptedChunks = [];
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const chunk = chunks[i];
|
||||
const randomPrivateKey = nostrTools.generateSecretKey();
|
||||
const randomPrivateKeyPubKey = nostrTools.getPublicKey(randomPrivateKey);
|
||||
const conversationKey = nip44.getConversationKey(
|
||||
randomPrivateKey,
|
||||
ccnPubKey,
|
||||
);
|
||||
|
||||
const sealTemplate = {
|
||||
kind: 13,
|
||||
created_at: randomTimeUpTo2DaysInThePast(),
|
||||
content: nip44.encrypt(chunk, conversationKey),
|
||||
tags: [['chunk', String(i), String(totalChunks), messageId]],
|
||||
};
|
||||
|
||||
const seal = nostrTools.finalizeEvent(sealTemplate, ccnPrivateKey);
|
||||
const giftWrapTemplate = {
|
||||
kind: 1059,
|
||||
created_at: randomTimeUpTo2DaysInThePast(),
|
||||
content: nip44.encrypt(JSON.stringify(seal), conversationKey),
|
||||
tags: [['p', ccnPubKey]],
|
||||
pubkey: randomPrivateKeyPubKey,
|
||||
};
|
||||
|
||||
const minedGiftWrap = nostrTools.nip13.minePow(
|
||||
giftWrapTemplate,
|
||||
POW_TO_MINE,
|
||||
);
|
||||
encryptedChunks.push(
|
||||
nostrTools.finalizeEvent(minedGiftWrap, randomPrivateKey),
|
||||
createEncryptedChunkForPubkey(
|
||||
ccnPubKey,
|
||||
chunk,
|
||||
i,
|
||||
totalChunks,
|
||||
messageId,
|
||||
ccnPrivateKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -111,26 +131,30 @@ function attemptDecryptWithKey(
|
|||
ccnPrivkey: Uint8Array,
|
||||
ccnPubkey: string,
|
||||
): Option<nostrTools.VerifiedEvent> {
|
||||
const conversationKey = nip44.getConversationKey(ccnPrivkey, event.pubkey);
|
||||
const sealResult = map(
|
||||
Some(nip44.decrypt(event.content, conversationKey)),
|
||||
JSON.parse,
|
||||
);
|
||||
try {
|
||||
const conversationKey = nip44.getConversationKey(ccnPrivkey, event.pubkey);
|
||||
const sealResult = map(
|
||||
Some(nip44.decrypt(event.content, conversationKey)),
|
||||
JSON.parse,
|
||||
);
|
||||
|
||||
return flatMap(sealResult, (seal) => {
|
||||
if (!seal || seal.kind !== 13) return None();
|
||||
return flatMap(sealResult, (seal) => {
|
||||
if (!seal || seal.kind !== 13) return None();
|
||||
|
||||
const chunkTag = seal.tags.find((tag: string[]) => tag[0] === 'chunk');
|
||||
if (!chunkTag) {
|
||||
const contentResult = map(
|
||||
Some(nip44.decrypt(seal.content, conversationKey)),
|
||||
JSON.parse,
|
||||
);
|
||||
return map(contentResult, (content) => ({ ...content, ccnPubkey }));
|
||||
}
|
||||
const chunkTag = seal.tags.find((tag: string[]) => tag[0] === 'chunk');
|
||||
if (!chunkTag) {
|
||||
const contentResult = map(
|
||||
Some(nip44.decrypt(seal.content, conversationKey)),
|
||||
JSON.parse,
|
||||
);
|
||||
return map(contentResult, (content) => ({ ...content, ccnPubkey }));
|
||||
}
|
||||
|
||||
return None();
|
||||
});
|
||||
} catch {
|
||||
return None();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -163,8 +187,8 @@ function handleChunkedMessage(
|
|||
const chunk = nip44.decrypt(seal.content, conversationKey);
|
||||
|
||||
const storedChunks = sql`
|
||||
SELECT COUNT(*) as count
|
||||
FROM event_chunks
|
||||
SELECT COUNT(*) as count
|
||||
FROM event_chunks
|
||||
WHERE message_id = ${messageId}
|
||||
`(db)[0].count;
|
||||
|
||||
|
@ -175,8 +199,8 @@ function handleChunkedMessage(
|
|||
|
||||
if (storedChunks + 1 === Number(totalChunks)) {
|
||||
const allChunks = sql`
|
||||
SELECT * FROM event_chunks
|
||||
WHERE message_id = ${messageId}
|
||||
SELECT * FROM event_chunks
|
||||
WHERE message_id = ${messageId}
|
||||
ORDER BY chunk_index
|
||||
`(db);
|
||||
|
||||
|
@ -200,34 +224,67 @@ export async function decryptEvent(
|
|||
throw new Error('Cannot decrypt event -- not a gift wrap');
|
||||
}
|
||||
|
||||
const pow = nostrTools.nip13.getPow(event.id);
|
||||
|
||||
if (pow < MIN_POW) {
|
||||
throw new Error('Cannot decrypt event -- PoW too low');
|
||||
}
|
||||
|
||||
const allCCNs = getAllCCNs(db);
|
||||
if (allCCNs.length === 0) {
|
||||
throw new Error('No CCNs found');
|
||||
}
|
||||
|
||||
for (const ccn of allCCNs) {
|
||||
const ccnPrivkey = await getCCNPrivateKeyByPubkey(ccn.pubkey);
|
||||
const decryptedEvent = attemptDecryptWithKey(event, ccnPrivkey, ccn.pubkey);
|
||||
if (decryptedEvent.isSome) {
|
||||
return { ...decryptedEvent.value, ccnPubkey: ccn.pubkey };
|
||||
}
|
||||
const pow = nostrTools.nip13.getPow(event.id);
|
||||
const isInvite =
|
||||
event.tags.findIndex(
|
||||
(tag: string[]) => tag[0] === 'type' && tag[1] === 'invite',
|
||||
) !== -1;
|
||||
const eventDestination = event.tags.find(
|
||||
(tag: string[]) => tag[0] === 'p',
|
||||
)?.[1];
|
||||
|
||||
if (!eventDestination) {
|
||||
throw new Error('Cannot decrypt event -- no destination');
|
||||
}
|
||||
|
||||
for (const ccn of allCCNs) {
|
||||
const ccnPrivkey = await getCCNPrivateKeyByPubkey(ccn.pubkey);
|
||||
try {
|
||||
const chuncked = handleChunkedMessage(db, event, ccnPrivkey, ccn.pubkey);
|
||||
return { ...chuncked, ccnPubkey: ccn.pubkey };
|
||||
} catch (e) {
|
||||
if (e instanceof ChunkedEventReceived) {
|
||||
throw e;
|
||||
}
|
||||
if (isInvite) {
|
||||
const ccnPrivkey = await getCCNPrivateKeyByPubkey(eventDestination);
|
||||
const decryptedEvent = attemptDecryptWithKey(
|
||||
event,
|
||||
ccnPrivkey,
|
||||
eventDestination,
|
||||
);
|
||||
if (decryptedEvent.isSome) {
|
||||
const recipient = decryptedEvent.value.tags.find(
|
||||
(tag: string[]) => tag[0] === 'p',
|
||||
)?.[1];
|
||||
if (recipient !== eventDestination)
|
||||
throw new Error('Cannot decrypt invite');
|
||||
return { ...decryptedEvent.value, ccnPubkey: eventDestination };
|
||||
}
|
||||
throw new Error('Cannot decrypt invite');
|
||||
}
|
||||
|
||||
if (pow < MIN_POW) {
|
||||
throw new Error('Cannot decrypt event -- PoW too low');
|
||||
}
|
||||
|
||||
const ccnPrivkey = await getCCNPrivateKeyByPubkey(eventDestination);
|
||||
const decryptedEvent = attemptDecryptWithKey(
|
||||
event,
|
||||
ccnPrivkey,
|
||||
eventDestination,
|
||||
);
|
||||
if (decryptedEvent.isSome) {
|
||||
return { ...decryptedEvent.value, ccnPubkey: eventDestination };
|
||||
}
|
||||
|
||||
try {
|
||||
const chuncked = handleChunkedMessage(
|
||||
db,
|
||||
event,
|
||||
ccnPrivkey,
|
||||
eventDestination,
|
||||
);
|
||||
return { ...chuncked, ccnPubkey: eventDestination };
|
||||
} catch (e) {
|
||||
if (e instanceof ChunkedEventReceived) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
211
index.ts
211
index.ts
|
@ -1,5 +1,8 @@
|
|||
import { bytesToHex, hexToBytes } from '@noble/ciphers/utils';
|
||||
import { randomBytes } from '@noble/ciphers/webcrypto';
|
||||
import { sha512 } from '@noble/hashes/sha2';
|
||||
import * as nostrTools from '@nostr/tools';
|
||||
import { base64 } from '@scure/base';
|
||||
import { Database } from 'jsr:@db/sqlite';
|
||||
import { NSchema as n } from 'jsr:@nostrify/nostrify';
|
||||
import type {
|
||||
|
@ -8,17 +11,23 @@ import type {
|
|||
NostrFilter,
|
||||
} from 'jsr:@nostrify/types';
|
||||
import { encodeBase64 } from 'jsr:@std/encoding@0.224/base64';
|
||||
import { CHUNK_CLEANUP_INTERVAL, CHUNK_MAX_AGE } from './consts.ts';
|
||||
import {
|
||||
CHUNK_CLEANUP_INTERVAL,
|
||||
CHUNK_MAX_AGE,
|
||||
POW_TO_MINE,
|
||||
} from './consts.ts';
|
||||
import {
|
||||
ChunkedEventReceived,
|
||||
EventAlreadyExistsException,
|
||||
createEncryptedEvent,
|
||||
createEncryptedEventForPubkey,
|
||||
decryptEvent,
|
||||
} from './eventEncryptionDecryption.ts';
|
||||
import {
|
||||
createNewCCN,
|
||||
getActiveCCN,
|
||||
getAllCCNs,
|
||||
getCCNPrivateKeyByPubkey,
|
||||
isAddressableEvent,
|
||||
isArray,
|
||||
isCCNReplaceableEvent,
|
||||
|
@ -42,6 +51,8 @@ if (!Deno.env.has('ENCRYPTION_KEY')) {
|
|||
Deno.exit(1);
|
||||
}
|
||||
|
||||
await Deno.mkdir(await getEveFilePath('ccn_keys'), { recursive: true });
|
||||
|
||||
const db = new Database(await getEveFilePath('db'));
|
||||
const pool = new nostrTools.SimplePool();
|
||||
const relays = [
|
||||
|
@ -123,12 +134,106 @@ function addEventToDb(
|
|||
`(db)[0];
|
||||
|
||||
if (existingEvent) throw new EventAlreadyExistsException();
|
||||
|
||||
const isInvite =
|
||||
decryptedEvent.tags.findIndex(
|
||||
(tag: string[]) => tag[0] === 'type' && tag[1] === 'invite',
|
||||
) !== -1;
|
||||
|
||||
if (isInvite) {
|
||||
const shadContent = bytesToHex(
|
||||
sha512.create().update(decryptedEvent.content).digest(),
|
||||
);
|
||||
|
||||
const inviteUsed = sql`
|
||||
SELECT COUNT(*) as count FROM inviter_invitee WHERE invite_hash = ${shadContent}
|
||||
`(db)[0].count;
|
||||
|
||||
if (inviteUsed > 0) {
|
||||
throw new Error('Invite already used');
|
||||
}
|
||||
|
||||
const inviteEvent = sql`
|
||||
SELECT * FROM events WHERE kind = 9999 AND id IN (
|
||||
SELECT event_id FROM event_tags WHERE tag_name = 'i' AND tag_id IN (
|
||||
SELECT tag_id FROM event_tags_values WHERE value_position = 1 AND value = ${shadContent}
|
||||
)
|
||||
)
|
||||
`(db)[0];
|
||||
|
||||
if (!inviteEvent) {
|
||||
throw new Error('Invite event not found');
|
||||
}
|
||||
|
||||
const inviterPubkey = inviteEvent.pubkey;
|
||||
const inviteePubkey = decryptedEvent.pubkey;
|
||||
|
||||
db.run('BEGIN TRANSACTION');
|
||||
|
||||
sql`
|
||||
INSERT INTO inviter_invitee (ccn_pubkey, inviter_pubkey, invitee_pubkey, invite_hash) VALUES (${ccnPubkey}, ${inviterPubkey}, ${inviteePubkey}, ${shadContent})
|
||||
`(db);
|
||||
|
||||
sql`
|
||||
INSERT INTO allowed_writes (ccn_pubkey, pubkey) VALUES (${ccnPubkey}, ${inviteePubkey})
|
||||
`(db);
|
||||
|
||||
db.run('COMMIT TRANSACTION');
|
||||
|
||||
const allowedPubkeys = sql`
|
||||
SELECT pubkey FROM allowed_writes WHERE ccn_pubkey = ${ccnPubkey}
|
||||
`(db).flatMap((row) => row.pubkey);
|
||||
const ccnName = sql`
|
||||
SELECT name FROM ccns WHERE pubkey = ${ccnPubkey}
|
||||
`(db)[0].name;
|
||||
|
||||
getCCNPrivateKeyByPubkey(ccnPubkey).then((ccnPrivateKey) => {
|
||||
if (!ccnPrivateKey) {
|
||||
throw new Error('CCN private key not found');
|
||||
}
|
||||
|
||||
const tags = allowedPubkeys.map((pubkey) => ['p', pubkey]);
|
||||
tags.push(['t', 'invite']);
|
||||
tags.push(['name', ccnName]);
|
||||
|
||||
const privateKeyEvent = nostrTools.finalizeEvent(
|
||||
nostrTools.nip13.minePow(
|
||||
{
|
||||
kind: 9998,
|
||||
created_at: Date.now(),
|
||||
content: base64.encode(ccnPrivateKey),
|
||||
tags,
|
||||
pubkey: ccnPubkey,
|
||||
},
|
||||
POW_TO_MINE,
|
||||
),
|
||||
ccnPrivateKey,
|
||||
);
|
||||
|
||||
const encryptedKeyEvent = createEncryptedEventForPubkey(
|
||||
inviteePubkey,
|
||||
privateKeyEvent,
|
||||
);
|
||||
publishToRelays(encryptedKeyEvent);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isAllowedWrite = sql`
|
||||
SELECT COUNT(*) as count FROM allowed_writes WHERE ccn_pubkey = ${ccnPubkey} AND pubkey = ${decryptedEvent.pubkey}
|
||||
`(db)[0].count;
|
||||
|
||||
if (isAllowedWrite === 0) {
|
||||
throw new Error('Not allowed to write to this CCN');
|
||||
}
|
||||
|
||||
try {
|
||||
db.run('BEGIN TRANSACTION');
|
||||
|
||||
if (isReplaceableEvent(decryptedEvent.kind)) {
|
||||
sql`
|
||||
UPDATE events
|
||||
UPDATE events
|
||||
SET replaced = 1
|
||||
WHERE kind = ${decryptedEvent.kind}
|
||||
AND pubkey = ${decryptedEvent.pubkey}
|
||||
|
@ -141,15 +246,15 @@ function addEventToDb(
|
|||
const dTag = decryptedEvent.tags.find((tag) => tag[0] === 'd')?.[1];
|
||||
if (dTag) {
|
||||
sql`
|
||||
UPDATE events
|
||||
UPDATE events
|
||||
SET replaced = 1
|
||||
WHERE kind = ${decryptedEvent.kind}
|
||||
AND pubkey = ${decryptedEvent.pubkey}
|
||||
AND created_at < ${decryptedEvent.created_at}
|
||||
AND ccn_pubkey = ${ccnPubkey}
|
||||
AND id IN (
|
||||
SELECT event_id FROM event_tags
|
||||
WHERE tag_name = 'd'
|
||||
SELECT event_id FROM event_tags
|
||||
WHERE tag_name = 'd'
|
||||
AND tag_id IN (
|
||||
SELECT tag_id FROM event_tags_values
|
||||
WHERE value_position = 1
|
||||
|
@ -163,14 +268,14 @@ function addEventToDb(
|
|||
if (isCCNReplaceableEvent(decryptedEvent.kind)) {
|
||||
const dTag = decryptedEvent.tags.find((tag) => tag[0] === 'd')?.[1];
|
||||
sql`
|
||||
UPDATE events
|
||||
UPDATE events
|
||||
SET replaced = 1
|
||||
WHERE kind = ${decryptedEvent.kind}
|
||||
AND created_at < ${decryptedEvent.created_at}
|
||||
AND ccn_pubkey = ${ccnPubkey}
|
||||
AND id IN (
|
||||
SELECT event_id FROM event_tags
|
||||
WHERE tag_name = 'd'
|
||||
SELECT event_id FROM event_tags
|
||||
WHERE tag_name = 'd'
|
||||
AND tag_id IN (
|
||||
SELECT tag_id FROM event_tags_values
|
||||
WHERE value_position = 1
|
||||
|
@ -561,7 +666,7 @@ function handleRequest(connection: UserConnection, request: NostrClientREQ) {
|
|||
FROM event_tags t
|
||||
WHERE t.tag_name = ${tag}
|
||||
AND t.tag_id IN (
|
||||
SELECT v.tag_id
|
||||
SELECT v.tag_id
|
||||
FROM event_tags_values v
|
||||
WHERE v.value_position = 1
|
||||
AND v.value = ${tagValue}
|
||||
|
@ -735,7 +840,7 @@ function handleSocketError(
|
|||
|
||||
async function handleCreateCCN(
|
||||
connection: UserConnection,
|
||||
data: { name: string; seed?: string },
|
||||
data: { name: string; seed?: string; creator: string },
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (!data.name || typeof data.name !== 'string') {
|
||||
|
@ -743,7 +848,17 @@ async function handleCreateCCN(
|
|||
return;
|
||||
}
|
||||
|
||||
const newCcn = await createNewCCN(connection.db, data.name, data.seed);
|
||||
if (!data.creator || typeof data.creator !== 'string') {
|
||||
connection.sendNotice('Creator is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const newCcn = await createNewCCN(
|
||||
connection.db,
|
||||
data.name,
|
||||
data.creator,
|
||||
data.seed,
|
||||
);
|
||||
|
||||
activateCCN(connection.db, newCcn.pubkey);
|
||||
|
||||
|
@ -834,6 +949,73 @@ function handleActivateCCN(
|
|||
}
|
||||
}
|
||||
|
||||
async function handleAddCCN(
|
||||
connection: UserConnection,
|
||||
data: { name: string; allowedPubkeys: string[]; privateKey: string },
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (!data.privateKey || typeof data.privateKey !== 'string') {
|
||||
connection.sendNotice('CCN private key is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const privateKeyBytes = hexToBytes(data.privateKey);
|
||||
const pubkey = nostrTools.getPublicKey(privateKeyBytes);
|
||||
|
||||
const ccnExists = sql`
|
||||
SELECT COUNT(*) as count FROM ccns WHERE pubkey = ${pubkey}
|
||||
`(connection.db)[0].count;
|
||||
|
||||
if (ccnExists > 0) {
|
||||
connection.sendNotice('CCN already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
const ccnPublicKey = nostrTools.getPublicKey(privateKeyBytes);
|
||||
const ccnPrivPath = await getEveFilePath(`ccn_keys/${ccnPublicKey}`);
|
||||
Deno.writeTextFileSync(ccnPrivPath, encodeBase64(privateKeyBytes));
|
||||
|
||||
db.run('BEGIN TRANSACTION');
|
||||
|
||||
sql`INSERT INTO ccns (pubkey, name) VALUES (${ccnPublicKey}, ${data.name})`(
|
||||
db,
|
||||
);
|
||||
for (const allowedPubkey of data.allowedPubkeys)
|
||||
sql`INSERT INTO allowed_writes (ccn_pubkey, pubkey) VALUES (${ccnPublicKey}, ${allowedPubkey})`(
|
||||
db,
|
||||
);
|
||||
|
||||
db.run('COMMIT TRANSACTION');
|
||||
|
||||
activateCCN(connection.db, ccnPublicKey);
|
||||
|
||||
pool.subscribeMany(
|
||||
relays,
|
||||
[
|
||||
{
|
||||
'#p': [ccnPublicKey],
|
||||
kinds: [1059],
|
||||
},
|
||||
],
|
||||
{
|
||||
onevent: createSubscriptionEventHandler(connection.db),
|
||||
},
|
||||
);
|
||||
|
||||
connection.sendResponse([
|
||||
'OK',
|
||||
'CCN ADDED',
|
||||
true,
|
||||
JSON.stringify({
|
||||
pubkey: ccnPublicKey,
|
||||
name: 'New CCN',
|
||||
}),
|
||||
]);
|
||||
} catch (error: unknown) {
|
||||
handleSocketError(connection, 'ADD CCN', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCCNCommands(
|
||||
connection: UserConnection,
|
||||
command: string,
|
||||
|
@ -843,7 +1025,12 @@ function handleCCNCommands(
|
|||
case 'CREATE':
|
||||
return handleCreateCCN(
|
||||
connection,
|
||||
data as { name: string; seed?: string },
|
||||
data as { name: string; seed?: string; creator: string },
|
||||
);
|
||||
case 'ADD':
|
||||
return handleAddCCN(
|
||||
connection,
|
||||
data as { name: string; allowedPubkeys: string[]; privateKey: string },
|
||||
);
|
||||
case 'LIST':
|
||||
return handleGetCCNs(connection);
|
||||
|
|
24
migrations/6-invitations.sql
Normal file
24
migrations/6-invitations.sql
Normal file
|
@ -0,0 +1,24 @@
|
|||
CREATE TABLE inviter_invitee(
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(16)))),
|
||||
ccn_pubkey TEXT NOT NULL,
|
||||
inviter_pubkey TEXT NOT NULL,
|
||||
invitee_pubkey TEXT NOT NULL,
|
||||
invite_hash TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
FOREIGN KEY (ccn_pubkey) REFERENCES ccns(pubkey) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_inviter_invitee_ccn_pubkey ON inviter_invitee(ccn_pubkey);
|
||||
CREATE INDEX idx_inviter_invitee_inviter_pubkey ON inviter_invitee(inviter_pubkey);
|
||||
CREATE INDEX idx_inviter_invitee_invitee_pubkey ON inviter_invitee(invitee_pubkey);
|
||||
|
||||
CREATE TABLE allowed_writes (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(16)))),
|
||||
ccn_pubkey TEXT NOT NULL,
|
||||
pubkey TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
FOREIGN KEY (ccn_pubkey) REFERENCES ccns(pubkey) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_allowed_writes_ccn_pubkey ON allowed_writes(ccn_pubkey);
|
||||
CREATE INDEX idx_allowed_writes_pubkey ON allowed_writes(pubkey);
|
8
utils.ts
8
utils.ts
|
@ -61,6 +61,7 @@ export function getAllCCNs(db: Database): { pubkey: string; name: string }[] {
|
|||
export async function createNewCCN(
|
||||
db: Database,
|
||||
name: string,
|
||||
creator: string,
|
||||
seed?: string,
|
||||
): Promise<{ pubkey: string; privkey: Uint8Array }> {
|
||||
const ccnSeed = seed || nip06.generateSeedWords();
|
||||
|
@ -78,7 +79,14 @@ export async function createNewCCN(
|
|||
Deno.writeTextFileSync(ccnSeedPath, ccnSeed);
|
||||
Deno.writeTextFileSync(ccnPrivPath, encodeBase64(encryptedPrivateKey));
|
||||
|
||||
db.run('BEGIN TRANSACTION');
|
||||
|
||||
sql`INSERT INTO ccns (pubkey, name) VALUES (${ccnPublicKey}, ${name})`(db);
|
||||
sql`INSERT INTO allowed_writes (ccn_pubkey, pubkey) VALUES (${ccnPublicKey}, ${creator})`(
|
||||
db,
|
||||
);
|
||||
|
||||
db.run('COMMIT TRANSACTION');
|
||||
|
||||
return {
|
||||
pubkey: ccnPublicKey,
|
||||
|
|
12
utils/invites.ts
Normal file
12
utils/invites.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { bytesToHex } from '@noble/ciphers/utils';
|
||||
import { nip19 } from '@nostr/tools';
|
||||
import { bech32m } from '@scure/base';
|
||||
|
||||
export function readInvite(invite: `${string}1${string}`) {
|
||||
const decoded = bech32m.decode(invite, false);
|
||||
if (decoded.prefix !== 'eveinvite') return false;
|
||||
const hexBytes = bech32m.fromWords(decoded.words);
|
||||
const npub = nip19.npubEncode(bytesToHex(hexBytes.slice(0, 32)));
|
||||
const inviteCode = bytesToHex(hexBytes.slice(32));
|
||||
return { npub, invite: inviteCode };
|
||||
}
|
Loading…
Add table
Reference in a new issue