chunkable-events #3
9 changed files with 273 additions and 195 deletions
44
biome.json
Normal file
44
biome.json
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||||
|
"files": {
|
||||||
|
"include": ["index.ts", "**/*.ts"]
|
||||||
|
},
|
||||||
|
"vcs": {
|
||||||
|
"enabled": true,
|
||||||
|
"clientKind": "git",
|
||||||
|
"useIgnoreFile": true
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"style": {
|
||||||
|
"noNonNullAssertion": "off",
|
||||||
|
"useNodejsImportProtocol": "warn"
|
||||||
|
},
|
||||||
|
"complexity": {
|
||||||
|
"useLiteralKeys": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"formatWithErrors": true,
|
||||||
|
"ignore": [],
|
||||||
|
"attributePosition": "auto",
|
||||||
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 2,
|
||||||
|
"lineWidth": 80,
|
||||||
|
"lineEnding": "lf"
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"arrowParentheses": "always",
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"quoteStyle": "single",
|
||||||
|
"quoteProperties": "asNeeded",
|
||||||
|
"semicolons": "always",
|
||||||
|
"trailingCommas": "all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
deno.json
13
deno.json
|
@ -1,8 +1,11 @@
|
||||||
{
|
{
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"dev": "deno run --allow-read --allow-write --allow-net --allow-ffi --allow-env --env-file --watch index.ts"
|
"dev": "deno run --allow-read --allow-write --allow-net --allow-ffi --allow-env --env-file --watch index.ts",
|
||||||
|
"lint": "biome check",
|
||||||
|
"lint:fix": "biome check --write --unsafe"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
|
"@biomejs/biome": "npm:@biomejs/biome@^1.9.4",
|
||||||
"@db/sqlite": "jsr:@db/sqlite@^0.12.0",
|
"@db/sqlite": "jsr:@db/sqlite@^0.12.0",
|
||||||
"@noble/ciphers": "jsr:@noble/ciphers@^1.2.1",
|
"@noble/ciphers": "jsr:@noble/ciphers@^1.2.1",
|
||||||
"@nostr/tools": "jsr:@nostr/tools@^2.10.4",
|
"@nostr/tools": "jsr:@nostr/tools@^2.10.4",
|
||||||
|
@ -12,13 +15,5 @@
|
||||||
"@std/fmt": "jsr:@std/fmt@^1.0.4",
|
"@std/fmt": "jsr:@std/fmt@^1.0.4",
|
||||||
"@std/log": "jsr:@std/log@^0.224.13",
|
"@std/log": "jsr:@std/log@^0.224.13",
|
||||||
"@types/deno": "npm:@types/deno@^2.0.0"
|
"@types/deno": "npm:@types/deno@^2.0.0"
|
||||||
},
|
|
||||||
"fmt": {
|
|
||||||
"indentWidth": 2,
|
|
||||||
"useTabs": false,
|
|
||||||
"lineWidth": 80,
|
|
||||||
"proseWrap": "always",
|
|
||||||
"semiColons": true,
|
|
||||||
"singleQuote": false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
40
deno.lock
generated
40
deno.lock
generated
|
@ -30,6 +30,8 @@
|
||||||
"jsr:@std/path@0.217": "0.217.0",
|
"jsr:@std/path@0.217": "0.217.0",
|
||||||
"jsr:@std/path@0.221": "0.221.0",
|
"jsr:@std/path@0.221": "0.221.0",
|
||||||
"jsr:@std/path@^1.0.8": "1.0.8",
|
"jsr:@std/path@^1.0.8": "1.0.8",
|
||||||
|
"npm:@biomejs/biome@1.9.4": "1.9.4",
|
||||||
|
"npm:@biomejs/biome@^1.9.4": "1.9.4",
|
||||||
"npm:@noble/ciphers@~0.5.1": "0.5.3",
|
"npm:@noble/ciphers@~0.5.1": "0.5.3",
|
||||||
"npm:@noble/curves@1.2.0": "1.2.0",
|
"npm:@noble/curves@1.2.0": "1.2.0",
|
||||||
"npm:@noble/hashes@1.3.1": "1.3.1",
|
"npm:@noble/hashes@1.3.1": "1.3.1",
|
||||||
|
@ -168,6 +170,43 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
|
"@biomejs/biome@1.9.4": {
|
||||||
|
"integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==",
|
||||||
|
"dependencies": [
|
||||||
|
"@biomejs/cli-darwin-arm64",
|
||||||
|
"@biomejs/cli-darwin-x64",
|
||||||
|
"@biomejs/cli-linux-arm64",
|
||||||
|
"@biomejs/cli-linux-arm64-musl",
|
||||||
|
"@biomejs/cli-linux-x64",
|
||||||
|
"@biomejs/cli-linux-x64-musl",
|
||||||
|
"@biomejs/cli-win32-arm64",
|
||||||
|
"@biomejs/cli-win32-x64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@biomejs/cli-darwin-arm64@1.9.4": {
|
||||||
|
"integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="
|
||||||
|
},
|
||||||
|
"@biomejs/cli-darwin-x64@1.9.4": {
|
||||||
|
"integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="
|
||||||
|
},
|
||||||
|
"@biomejs/cli-linux-arm64-musl@1.9.4": {
|
||||||
|
"integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="
|
||||||
|
},
|
||||||
|
"@biomejs/cli-linux-arm64@1.9.4": {
|
||||||
|
"integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="
|
||||||
|
},
|
||||||
|
"@biomejs/cli-linux-x64-musl@1.9.4": {
|
||||||
|
"integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="
|
||||||
|
},
|
||||||
|
"@biomejs/cli-linux-x64@1.9.4": {
|
||||||
|
"integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="
|
||||||
|
},
|
||||||
|
"@biomejs/cli-win32-arm64@1.9.4": {
|
||||||
|
"integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="
|
||||||
|
},
|
||||||
|
"@biomejs/cli-win32-x64@1.9.4": {
|
||||||
|
"integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="
|
||||||
|
},
|
||||||
"@noble/ciphers@0.5.3": {
|
"@noble/ciphers@0.5.3": {
|
||||||
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="
|
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="
|
||||||
},
|
},
|
||||||
|
@ -272,6 +311,7 @@
|
||||||
"jsr:@std/encoding@^1.0.6",
|
"jsr:@std/encoding@^1.0.6",
|
||||||
"jsr:@std/fmt@^1.0.4",
|
"jsr:@std/fmt@^1.0.4",
|
||||||
"jsr:@std/log@~0.224.13",
|
"jsr:@std/log@~0.224.13",
|
||||||
|
"npm:@biomejs/biome@^1.9.4",
|
||||||
"npm:@types/deno@2"
|
"npm:@types/deno@2"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
240
index.ts
240
index.ts
|
@ -1,9 +1,15 @@
|
||||||
import { NSchema as n } from "jsr:@nostrify/nostrify";
|
import { Database } from 'jsr:@db/sqlite';
|
||||||
|
import { NSchema as n } from 'jsr:@nostrify/nostrify';
|
||||||
import type {
|
import type {
|
||||||
NostrClientREQ,
|
NostrClientREQ,
|
||||||
NostrEvent,
|
NostrEvent,
|
||||||
NostrFilter,
|
NostrFilter,
|
||||||
} from "jsr:@nostrify/types";
|
} from 'jsr:@nostrify/types';
|
||||||
|
import { encodeBase64 } from 'jsr:@std/encoding@0.224/base64';
|
||||||
|
import { randomBytes } from '@noble/ciphers/webcrypto';
|
||||||
|
import * as nostrTools from '@nostr/tools';
|
||||||
|
import { nip44 } from '@nostr/tools';
|
||||||
|
import { MIN_POW, POW_TO_MINE } from './consts.ts';
|
||||||
import {
|
import {
|
||||||
getCCNPrivateKey,
|
getCCNPrivateKey,
|
||||||
getCCNPubkey,
|
getCCNPubkey,
|
||||||
|
@ -15,56 +21,48 @@ import {
|
||||||
isValidJSON,
|
isValidJSON,
|
||||||
parseATagQuery,
|
parseATagQuery,
|
||||||
randomTimeUpTo2DaysInThePast,
|
randomTimeUpTo2DaysInThePast,
|
||||||
} from "./utils.ts";
|
} from './utils.ts';
|
||||||
import * as nostrTools from "@nostr/tools";
|
import { getEveFilePath } from './utils/files.ts';
|
||||||
import { nip44 } from "@nostr/tools";
|
import { log, setupLogger } from './utils/logs.ts';
|
||||||
import { randomBytes } from "@noble/ciphers/webcrypto";
|
import { mixQuery, sql, sqlPartial } from './utils/queries.ts';
|
||||||
import { encodeBase64 } from "jsr:@std/encoding@0.224/base64";
|
|
||||||
import { Database } from "jsr:@db/sqlite";
|
|
||||||
import { mixQuery, sql, sqlPartial } from "./utils/queries.ts";
|
|
||||||
import { log, setupLogger } from "./utils/logs.ts";
|
|
||||||
import { getEveFilePath } from "./utils/files.ts";
|
|
||||||
import { MIN_POW, POW_TO_MINE } from "./consts.ts";
|
|
||||||
|
|
||||||
await setupLogger();
|
await setupLogger();
|
||||||
|
|
||||||
if (!Deno.env.has("ENCRYPTION_KEY")) {
|
if (!Deno.env.has('ENCRYPTION_KEY')) {
|
||||||
log.error(
|
log.error(
|
||||||
`Missing ENCRYPTION_KEY. Please set it in your env.\nA new one has been generated for you: ENCRYPTION_KEY="${
|
`Missing ENCRYPTION_KEY. Please set it in your env.\nA new one has been generated for you: ENCRYPTION_KEY="${encodeBase64(
|
||||||
encodeBase64(
|
randomBytes(32),
|
||||||
randomBytes(32),
|
)}"`,
|
||||||
)
|
|
||||||
}"`,
|
|
||||||
);
|
);
|
||||||
Deno.exit(1);
|
Deno.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = new Database(await getEveFilePath("db"));
|
const db = new Database(await getEveFilePath('db'));
|
||||||
const pool = new nostrTools.SimplePool();
|
const pool = new nostrTools.SimplePool();
|
||||||
const relays = [
|
const relays = [
|
||||||
"wss://relay.arx-ccn.com/",
|
'wss://relay.arx-ccn.com/',
|
||||||
"wss://relay.dannymorabito.com/",
|
'wss://relay.dannymorabito.com/',
|
||||||
"wss://nos.lol/",
|
'wss://nos.lol/',
|
||||||
"wss://nostr.einundzwanzig.space/",
|
'wss://nostr.einundzwanzig.space/',
|
||||||
"wss://nostr.massmux.com/",
|
'wss://nostr.massmux.com/',
|
||||||
"wss://nostr.mom/",
|
'wss://nostr.mom/',
|
||||||
"wss://nostr.wine/",
|
'wss://nostr.wine/',
|
||||||
"wss://purplerelay.com/",
|
'wss://purplerelay.com/',
|
||||||
"wss://relay.damus.io/",
|
'wss://relay.damus.io/',
|
||||||
"wss://relay.goodmorningbitcoin.com/",
|
'wss://relay.goodmorningbitcoin.com/',
|
||||||
"wss://relay.lexingtonbitcoin.org/",
|
'wss://relay.lexingtonbitcoin.org/',
|
||||||
"wss://relay.nostr.band/",
|
'wss://relay.nostr.band/',
|
||||||
"wss://relay.primal.net/",
|
'wss://relay.primal.net/',
|
||||||
"wss://relay.snort.social/",
|
'wss://relay.snort.social/',
|
||||||
"wss://strfry.iris.to/",
|
'wss://strfry.iris.to/',
|
||||||
"wss://cache2.primal.net/v1",
|
'wss://cache2.primal.net/v1',
|
||||||
];
|
];
|
||||||
|
|
||||||
export function runMigrations(db: Database, latestVersion: number) {
|
export function runMigrations(db: Database, latestVersion: number) {
|
||||||
const migrations = Deno.readDirSync(`${import.meta.dirname}/migrations`);
|
const migrations = Deno.readDirSync(`${import.meta.dirname}/migrations`);
|
||||||
for (const migrationFile of migrations) {
|
for (const migrationFile of migrations) {
|
||||||
const migrationVersion = Number.parseInt(
|
const migrationVersion = Number.parseInt(
|
||||||
migrationFile.name.split("-")[0],
|
migrationFile.name.split('-')[0],
|
||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -76,34 +74,31 @@ export function runMigrations(db: Database, latestVersion: number) {
|
||||||
const migrationSql = Deno.readTextFileSync(
|
const migrationSql = Deno.readTextFileSync(
|
||||||
`${import.meta.dirname}/migrations/${migrationFile.name}`,
|
`${import.meta.dirname}/migrations/${migrationFile.name}`,
|
||||||
);
|
);
|
||||||
db.run("BEGIN TRANSACTION");
|
db.run('BEGIN TRANSACTION');
|
||||||
try {
|
try {
|
||||||
db.run(migrationSql);
|
db.run(migrationSql);
|
||||||
const end = Date.now();
|
const end = Date.now();
|
||||||
const durationMs = end - start;
|
const durationMs = end - start;
|
||||||
sql`
|
sql`
|
||||||
INSERT INTO migration_history (migration_version, migration_name, executed_at, duration_ms, status) VALUES (${migrationVersion}, ${migrationFile.name}, ${
|
INSERT INTO migration_history (migration_version, migration_name, executed_at, duration_ms, status) VALUES (${migrationVersion}, ${migrationFile.name}, ${new Date().toISOString()}, ${durationMs}, 'success');
|
||||||
new Date().toISOString()
|
|
||||||
}, ${durationMs}, 'success');
|
|
||||||
db.run("COMMIT TRANSACTION");
|
db.run("COMMIT TRANSACTION");
|
||||||
`(db);
|
`(db);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
db.run("ROLLBACK TRANSACTION");
|
db.run('ROLLBACK TRANSACTION');
|
||||||
const error = e instanceof Error
|
const error =
|
||||||
? e
|
e instanceof Error
|
||||||
: typeof e === "string"
|
? e
|
||||||
? new Error(e)
|
: typeof e === 'string'
|
||||||
: new Error(JSON.stringify(e));
|
? new Error(e)
|
||||||
|
: new Error(JSON.stringify(e));
|
||||||
const end = Date.now();
|
const end = Date.now();
|
||||||
const durationMs = end - start;
|
const durationMs = end - start;
|
||||||
sql`
|
sql`
|
||||||
INSERT INTO migration_history (migration_version, migration_name, executed_at, duration_ms, status, error_message) VALUES (${migrationVersion}, ${migrationFile.name}, ${
|
INSERT INTO migration_history (migration_version, migration_name, executed_at, duration_ms, status, error_message) VALUES (${migrationVersion}, ${migrationFile.name}, ${new Date().toISOString()}, ${durationMs}, 'failed', ${error.message});
|
||||||
new Date().toISOString()
|
|
||||||
}, ${durationMs}, 'failed', ${error.message});
|
|
||||||
`(db);
|
`(db);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
db.run("END TRANSACTION");
|
db.run('END TRANSACTION');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,8 +106,8 @@ export function runMigrations(db: Database, latestVersion: number) {
|
||||||
async function createEncryptedEvent(
|
async function createEncryptedEvent(
|
||||||
event: nostrTools.VerifiedEvent,
|
event: nostrTools.VerifiedEvent,
|
||||||
): Promise<nostrTools.VerifiedEvent> {
|
): Promise<nostrTools.VerifiedEvent> {
|
||||||
if (!event.id) throw new Error("Event must have an ID");
|
if (!event.id) throw new Error('Event must have an ID');
|
||||||
if (!event.sig) throw new Error("Event must be signed");
|
if (!event.sig) throw new Error('Event must be signed');
|
||||||
const ccnPubKey = await getCCNPubkey();
|
const ccnPubKey = await getCCNPubkey();
|
||||||
const ccnPrivateKey = await getCCNPrivateKey();
|
const ccnPrivateKey = await getCCNPrivateKey();
|
||||||
const randomPrivateKey = nostrTools.generateSecretKey();
|
const randomPrivateKey = nostrTools.generateSecretKey();
|
||||||
|
@ -129,7 +124,7 @@ async function createEncryptedEvent(
|
||||||
kind: 1059,
|
kind: 1059,
|
||||||
created_at: randomTimeUpTo2DaysInThePast(),
|
created_at: randomTimeUpTo2DaysInThePast(),
|
||||||
content: nip44.encrypt(JSON.stringify(seal), conversationKey),
|
content: nip44.encrypt(JSON.stringify(seal), conversationKey),
|
||||||
tags: [["p", ccnPubKey]],
|
tags: [['p', ccnPubKey]],
|
||||||
pubkey: randomPrivateKeyPubKey,
|
pubkey: randomPrivateKeyPubKey,
|
||||||
};
|
};
|
||||||
const minedGiftWrap = nostrTools.nip13.minePow(giftWrapTemplate, POW_TO_MINE);
|
const minedGiftWrap = nostrTools.nip13.minePow(giftWrapTemplate, POW_TO_MINE);
|
||||||
|
@ -143,20 +138,20 @@ async function decryptEvent(
|
||||||
const ccnPrivkey = await getCCNPrivateKey();
|
const ccnPrivkey = await getCCNPrivateKey();
|
||||||
|
|
||||||
if (event.kind !== 1059) {
|
if (event.kind !== 1059) {
|
||||||
throw new Error("Cannot decrypt event -- not a gift wrap");
|
throw new Error('Cannot decrypt event -- not a gift wrap');
|
||||||
}
|
}
|
||||||
|
|
||||||
const pow = nostrTools.nip13.getPow(event.id);
|
const pow = nostrTools.nip13.getPow(event.id);
|
||||||
|
|
||||||
if (pow < MIN_POW) {
|
if (pow < MIN_POW) {
|
||||||
throw new Error("Cannot decrypt event -- PoW too low");
|
throw new Error('Cannot decrypt event -- PoW too low');
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversationKey = nip44.getConversationKey(ccnPrivkey, event.pubkey);
|
const conversationKey = nip44.getConversationKey(ccnPrivkey, event.pubkey);
|
||||||
const seal = JSON.parse(nip44.decrypt(event.content, conversationKey));
|
const seal = JSON.parse(nip44.decrypt(event.content, conversationKey));
|
||||||
if (!seal) throw new Error("Cannot decrypt event -- no seal");
|
if (!seal) throw new Error('Cannot decrypt event -- no seal');
|
||||||
if (seal.kind !== 13) {
|
if (seal.kind !== 13) {
|
||||||
throw new Error("Cannot decrypt event subevent -- not a seal");
|
throw new Error('Cannot decrypt event subevent -- not a seal');
|
||||||
}
|
}
|
||||||
const content = JSON.parse(nip44.decrypt(seal.content, conversationKey));
|
const content = JSON.parse(nip44.decrypt(seal.content, conversationKey));
|
||||||
return content as nostrTools.VerifiedEvent;
|
return content as nostrTools.VerifiedEvent;
|
||||||
|
@ -174,7 +169,7 @@ function addEventToDb(
|
||||||
|
|
||||||
if (existingEvent) throw new EventAlreadyExistsException();
|
if (existingEvent) throw new EventAlreadyExistsException();
|
||||||
try {
|
try {
|
||||||
db.run("BEGIN TRANSACTION");
|
db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
if (isReplaceableEvent(decryptedEvent.kind)) {
|
if (isReplaceableEvent(decryptedEvent.kind)) {
|
||||||
sql`
|
sql`
|
||||||
|
@ -187,7 +182,7 @@ function addEventToDb(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAddressableEvent(decryptedEvent.kind)) {
|
if (isAddressableEvent(decryptedEvent.kind)) {
|
||||||
const dTag = decryptedEvent.tags.find((tag) => tag[0] === "d")?.[1];
|
const dTag = decryptedEvent.tags.find((tag) => tag[0] === 'd')?.[1];
|
||||||
if (dTag) {
|
if (dTag) {
|
||||||
sql`
|
sql`
|
||||||
UPDATE events
|
UPDATE events
|
||||||
|
@ -209,7 +204,7 @@ function addEventToDb(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCCNReplaceableEvent(decryptedEvent.kind)) {
|
if (isCCNReplaceableEvent(decryptedEvent.kind)) {
|
||||||
const dTag = decryptedEvent.tags.find((tag) => tag[0] === "d")?.[1];
|
const dTag = decryptedEvent.tags.find((tag) => tag[0] === 'd')?.[1];
|
||||||
sql`
|
sql`
|
||||||
UPDATE events
|
UPDATE events
|
||||||
SET replaced = 1
|
SET replaced = 1
|
||||||
|
@ -259,9 +254,9 @@ function addEventToDb(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
db.run("COMMIT TRANSACTION");
|
db.run('COMMIT TRANSACTION');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
db.run("ROLLBACK TRANSACTION");
|
db.run('ROLLBACK TRANSACTION');
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -281,7 +276,8 @@ async function setupAndSubscribeToExternalEvents() {
|
||||||
|
|
||||||
if (!isInitialized) runMigrations(db, -1);
|
if (!isInitialized) runMigrations(db, -1);
|
||||||
|
|
||||||
const latestVersion = sql`
|
const latestVersion =
|
||||||
|
sql`
|
||||||
SELECT migration_version FROM migration_history WHERE status = 'success' ORDER BY migration_version DESC LIMIT 1
|
SELECT migration_version FROM migration_history WHERE status = 'success' ORDER BY migration_version DESC LIMIT 1
|
||||||
`(db)[0]?.migration_version ?? -1;
|
`(db)[0]?.migration_version ?? -1;
|
||||||
|
|
||||||
|
@ -291,7 +287,7 @@ async function setupAndSubscribeToExternalEvents() {
|
||||||
relays,
|
relays,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"#p": [ccnPubkey],
|
'#p': [ccnPubkey],
|
||||||
kinds: [1059],
|
kinds: [1059],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -303,7 +299,7 @@ async function setupAndSubscribeToExternalEvents() {
|
||||||
}
|
}
|
||||||
if (knownOriginalEvents.indexOf(event.id) >= 0) return;
|
if (knownOriginalEvents.indexOf(event.id) >= 0) return;
|
||||||
if (!nostrTools.verifyEvent(event)) {
|
if (!nostrTools.verifyEvent(event)) {
|
||||||
log.warn("Invalid event received");
|
log.warn('Invalid event received');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (encryptedEventIsInDb(event)) return;
|
if (encryptedEventIsInDb(event)) return;
|
||||||
|
@ -328,20 +324,19 @@ async function setupAndSubscribeToExternalEvents() {
|
||||||
const ccnCreationEventTemplate = {
|
const ccnCreationEventTemplate = {
|
||||||
kind: 0,
|
kind: 0,
|
||||||
content: JSON.stringify({
|
content: JSON.stringify({
|
||||||
display_name: "New CCN",
|
display_name: 'New CCN',
|
||||||
name: "New CCN",
|
name: 'New CCN',
|
||||||
bot: true,
|
bot: true,
|
||||||
}),
|
}),
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
tags: [["p", ccnPubkey]],
|
tags: [['p', ccnPubkey]],
|
||||||
};
|
};
|
||||||
const ccnCreationEvent = nostrTools.finalizeEvent(
|
const ccnCreationEvent = nostrTools.finalizeEvent(
|
||||||
ccnCreationEventTemplate,
|
ccnCreationEventTemplate,
|
||||||
await getCCNPrivateKey(),
|
await getCCNPrivateKey(),
|
||||||
);
|
);
|
||||||
const encryptedCCNCreationEvent = await createEncryptedEvent(
|
const encryptedCCNCreationEvent =
|
||||||
ccnCreationEvent,
|
await createEncryptedEvent(ccnCreationEvent);
|
||||||
);
|
|
||||||
if (timerCleaned) return; // in case we get an event before the timer is cleaned
|
if (timerCleaned) return; // in case we get an event before the timer is cleaned
|
||||||
await Promise.any(pool.publish(relays, encryptedCCNCreationEvent));
|
await Promise.any(pool.publish(relays, encryptedCCNCreationEvent));
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
@ -375,20 +370,20 @@ function filtersMatchingEvent(
|
||||||
if (!filters) continue;
|
if (!filters) continue;
|
||||||
const isMatching = filters.every((filter) =>
|
const isMatching = filters.every((filter) =>
|
||||||
Object.entries(filter).every(([type, value]) => {
|
Object.entries(filter).every(([type, value]) => {
|
||||||
if (type === "ids") return value.includes(event.id);
|
if (type === 'ids') return value.includes(event.id);
|
||||||
if (type === "kinds") return value.includes(event.kind);
|
if (type === 'kinds') return value.includes(event.kind);
|
||||||
if (type === "authors") return value.includes(event.pubkey);
|
if (type === 'authors') return value.includes(event.pubkey);
|
||||||
if (type === "since") return event.created_at >= value;
|
if (type === 'since') return event.created_at >= value;
|
||||||
if (type === "until") return event.created_at <= value;
|
if (type === 'until') return event.created_at <= value;
|
||||||
if (type === "limit") return event.created_at <= value;
|
if (type === 'limit') return event.created_at <= value;
|
||||||
if (type.startsWith("#")) {
|
if (type.startsWith('#')) {
|
||||||
const tagName = type.slice(1);
|
const tagName = type.slice(1);
|
||||||
return event.tags.some(
|
return event.tags.some(
|
||||||
(tag: string[]) => tag[0] === tagName && value.includes(tag[1]),
|
(tag: string[]) => tag[0] === tagName && value.includes(tag[1]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
if (isMatching) matching.push(subscription);
|
if (isMatching) matching.push(subscription);
|
||||||
}
|
}
|
||||||
|
@ -398,15 +393,13 @@ function filtersMatchingEvent(
|
||||||
function handleRequest(connection: UserConnection, request: NostrClientREQ) {
|
function handleRequest(connection: UserConnection, request: NostrClientREQ) {
|
||||||
const [, subscriptionId, ...filters] = request;
|
const [, subscriptionId, ...filters] = request;
|
||||||
if (connection.subscriptions.has(subscriptionId)) {
|
if (connection.subscriptions.has(subscriptionId)) {
|
||||||
return log.warn("Duplicate subscription ID");
|
return log.warn('Duplicate subscription ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`New subscription: ${subscriptionId} with filters: ${
|
`New subscription: ${subscriptionId} with filters: ${JSON.stringify(
|
||||||
JSON.stringify(
|
filters,
|
||||||
filters,
|
)}`,
|
||||||
)
|
|
||||||
}`,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let query = sqlPartial`SELECT * FROM events WHERE replaced = 0`;
|
let query = sqlPartial`SELECT * FROM events WHERE replaced = 0`;
|
||||||
|
@ -425,19 +418,19 @@ function handleRequest(connection: UserConnection, request: NostrClientREQ) {
|
||||||
query = mixQuery(query, sqlPartial`(`);
|
query = mixQuery(query, sqlPartial`(`);
|
||||||
|
|
||||||
const filter = Object.entries(filters[i]).filter(([type, value]) => {
|
const filter = Object.entries(filters[i]).filter(([type, value]) => {
|
||||||
if (type === "ids") return value.length > 0;
|
if (type === 'ids') return value.length > 0;
|
||||||
if (type === "authors") return value.length > 0;
|
if (type === 'authors') return value.length > 0;
|
||||||
if (type === "kinds") return value.length > 0;
|
if (type === 'kinds') return value.length > 0;
|
||||||
if (type.startsWith("#")) return value.length > 0;
|
if (type.startsWith('#')) return value.length > 0;
|
||||||
if (type === "since") return value > 0;
|
if (type === 'since') return value > 0;
|
||||||
if (type === "until") return value > 0;
|
if (type === 'until') return value > 0;
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let j = 0; j < filter.length; j++) {
|
for (let j = 0; j < filter.length; j++) {
|
||||||
const [type, value] = filter[j];
|
const [type, value] = filter[j];
|
||||||
|
|
||||||
if (type === "ids") {
|
if (type === 'ids') {
|
||||||
const uniqueIds = [...new Set(value)];
|
const uniqueIds = [...new Set(value)];
|
||||||
query = mixQuery(query, sqlPartial`id IN (`);
|
query = mixQuery(query, sqlPartial`id IN (`);
|
||||||
for (let k = 0; k < uniqueIds.length; k++) {
|
for (let k = 0; k < uniqueIds.length; k++) {
|
||||||
|
@ -452,7 +445,7 @@ function handleRequest(connection: UserConnection, request: NostrClientREQ) {
|
||||||
query = mixQuery(query, sqlPartial`)`);
|
query = mixQuery(query, sqlPartial`)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "authors") {
|
if (type === 'authors') {
|
||||||
const uniqueAuthors = [...new Set(value)];
|
const uniqueAuthors = [...new Set(value)];
|
||||||
query = mixQuery(query, sqlPartial`pubkey IN (`);
|
query = mixQuery(query, sqlPartial`pubkey IN (`);
|
||||||
for (let k = 0; k < uniqueAuthors.length; k++) {
|
for (let k = 0; k < uniqueAuthors.length; k++) {
|
||||||
|
@ -467,7 +460,7 @@ function handleRequest(connection: UserConnection, request: NostrClientREQ) {
|
||||||
query = mixQuery(query, sqlPartial`)`);
|
query = mixQuery(query, sqlPartial`)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "kinds") {
|
if (type === 'kinds') {
|
||||||
const uniqueKinds = [...new Set(value)];
|
const uniqueKinds = [...new Set(value)];
|
||||||
query = mixQuery(query, sqlPartial`kind IN (`);
|
query = mixQuery(query, sqlPartial`kind IN (`);
|
||||||
for (let k = 0; k < uniqueKinds.length; k++) {
|
for (let k = 0; k < uniqueKinds.length; k++) {
|
||||||
|
@ -482,16 +475,16 @@ function handleRequest(connection: UserConnection, request: NostrClientREQ) {
|
||||||
query = mixQuery(query, sqlPartial`)`);
|
query = mixQuery(query, sqlPartial`)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.startsWith("#")) {
|
if (type.startsWith('#')) {
|
||||||
const tag = type.slice(1);
|
const tag = type.slice(1);
|
||||||
const uniqueValues = [...new Set(value)];
|
const uniqueValues = [...new Set(value)];
|
||||||
query = mixQuery(query, sqlPartial`(`);
|
query = mixQuery(query, sqlPartial`(`);
|
||||||
for (let k = 0; k < uniqueValues.length; k++) {
|
for (let k = 0; k < uniqueValues.length; k++) {
|
||||||
const tagValue = uniqueValues[k] as string;
|
const tagValue = uniqueValues[k] as string;
|
||||||
if (tag === "a") {
|
if (tag === 'a') {
|
||||||
const aTagInfo = parseATagQuery(tagValue);
|
const aTagInfo = parseATagQuery(tagValue);
|
||||||
|
|
||||||
if (aTagInfo.dTag && aTagInfo.dTag !== "") {
|
if (aTagInfo.dTag && aTagInfo.dTag !== '') {
|
||||||
if (isCCNReplaceableEvent(aTagInfo.kind)) {
|
if (isCCNReplaceableEvent(aTagInfo.kind)) {
|
||||||
// CCN replaceable event reference
|
// CCN replaceable event reference
|
||||||
query = mixQuery(
|
query = mixQuery(
|
||||||
|
@ -561,11 +554,11 @@ function handleRequest(connection: UserConnection, request: NostrClientREQ) {
|
||||||
query = mixQuery(query, sqlPartial`)`);
|
query = mixQuery(query, sqlPartial`)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "since") {
|
if (type === 'since') {
|
||||||
query = mixQuery(query, sqlPartial`created_at >= ${value}`);
|
query = mixQuery(query, sqlPartial`created_at >= ${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "until") {
|
if (type === 'until') {
|
||||||
query = mixQuery(query, sqlPartial`created_at <= ${value}`);
|
query = mixQuery(query, sqlPartial`created_at <= ${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -588,10 +581,13 @@ function handleRequest(connection: UserConnection, request: NostrClientREQ) {
|
||||||
const rawTags = sql`SELECT * FROM event_tags_view WHERE event_id = ${
|
const rawTags = sql`SELECT * FROM event_tags_view WHERE event_id = ${
|
||||||
events[i].id
|
events[i].id
|
||||||
}`(connection.db);
|
}`(connection.db);
|
||||||
const tagsByIndex = new Map<number, {
|
const tagsByIndex = new Map<
|
||||||
name: string;
|
number,
|
||||||
values: Map<number, string>;
|
{
|
||||||
}>();
|
name: string;
|
||||||
|
values: Map<number, string>;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
for (const tag of rawTags) {
|
for (const tag of rawTags) {
|
||||||
let tagData = tagsByIndex.get(tag.tag_index);
|
let tagData = tagsByIndex.get(tag.tag_index);
|
||||||
|
@ -629,9 +625,9 @@ function handleRequest(connection: UserConnection, request: NostrClientREQ) {
|
||||||
sig: events[i].sig,
|
sig: events[i].sig,
|
||||||
};
|
};
|
||||||
|
|
||||||
connection.socket.send(JSON.stringify(["EVENT", subscriptionId, event]));
|
connection.socket.send(JSON.stringify(['EVENT', subscriptionId, event]));
|
||||||
}
|
}
|
||||||
connection.socket.send(JSON.stringify(["EOSE", subscriptionId]));
|
connection.socket.send(JSON.stringify(['EOSE', subscriptionId]));
|
||||||
|
|
||||||
connection.subscriptions.set(subscriptionId, filters);
|
connection.subscriptions.set(subscriptionId, filters);
|
||||||
}
|
}
|
||||||
|
@ -642,8 +638,8 @@ async function handleEvent(
|
||||||
) {
|
) {
|
||||||
const valid = nostrTools.verifyEvent(event);
|
const valid = nostrTools.verifyEvent(event);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
connection.socket.send(JSON.stringify(["NOTICE", "Invalid event"]));
|
connection.socket.send(JSON.stringify(['NOTICE', 'Invalid event']));
|
||||||
return log.warn("Invalid event");
|
return log.warn('Invalid event');
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptedEvent = await createEncryptedEvent(event);
|
const encryptedEvent = await createEncryptedEvent(event);
|
||||||
|
@ -651,19 +647,19 @@ async function handleEvent(
|
||||||
addEventToDb(event, encryptedEvent);
|
addEventToDb(event, encryptedEvent);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof EventAlreadyExistsException) {
|
if (e instanceof EventAlreadyExistsException) {
|
||||||
log.warn("Event already exists");
|
log.warn('Event already exists');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Promise.any(pool.publish(relays, encryptedEvent));
|
await Promise.any(pool.publish(relays, encryptedEvent));
|
||||||
|
|
||||||
connection.socket.send(JSON.stringify(["OK", event.id, true, "Event added"]));
|
connection.socket.send(JSON.stringify(['OK', event.id, true, 'Event added']));
|
||||||
|
|
||||||
const filtersThatMatchEvent = filtersMatchingEvent(event, connection);
|
const filtersThatMatchEvent = filtersMatchingEvent(event, connection);
|
||||||
|
|
||||||
for (let i = 0; i < filtersThatMatchEvent.length; i++) {
|
for (let i = 0; i < filtersThatMatchEvent.length; i++) {
|
||||||
const filter = filtersThatMatchEvent[i];
|
const filter = filtersThatMatchEvent[i];
|
||||||
connection.socket.send(JSON.stringify(["EVENT", filter, event]));
|
connection.socket.send(JSON.stringify(['EVENT', filter, event]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -680,10 +676,10 @@ function handleClose(connection: UserConnection, subscriptionId: string) {
|
||||||
Deno.serve({
|
Deno.serve({
|
||||||
port: 6942,
|
port: 6942,
|
||||||
handler: (request) => {
|
handler: (request) => {
|
||||||
if (request.headers.get("upgrade") === "websocket") {
|
if (request.headers.get('upgrade') === 'websocket') {
|
||||||
if (!isLocalhost(request)) {
|
if (!isLocalhost(request)) {
|
||||||
return new Response(
|
return new Response(
|
||||||
"Forbidden. Please read the Arx-CCN documentation for more information on how to interact with the relay.",
|
'Forbidden. Please read the Arx-CCN documentation for more information on how to interact with the relay.',
|
||||||
{ status: 403 },
|
{ status: 403 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -692,31 +688,31 @@ Deno.serve({
|
||||||
|
|
||||||
const connection = new UserConnection(socket, new Map(), db);
|
const connection = new UserConnection(socket, new Map(), db);
|
||||||
|
|
||||||
socket.onopen = () => log.info("User connected");
|
socket.onopen = () => log.info('User connected');
|
||||||
socket.onmessage = (event) => {
|
socket.onmessage = (event) => {
|
||||||
log.debug(`Received: ${event.data}`);
|
log.debug(`Received: ${event.data}`);
|
||||||
if (typeof event.data !== "string" || !isValidJSON(event.data)) {
|
if (typeof event.data !== 'string' || !isValidJSON(event.data)) {
|
||||||
return log.warn("Invalid request");
|
return log.warn('Invalid request');
|
||||||
}
|
}
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
if (!isArray(data)) return log.warn("Invalid request");
|
if (!isArray(data)) return log.warn('Invalid request');
|
||||||
|
|
||||||
const msg = n.clientMsg().parse(data);
|
const msg = n.clientMsg().parse(data);
|
||||||
switch (msg[0]) {
|
switch (msg[0]) {
|
||||||
case "REQ":
|
case 'REQ':
|
||||||
return handleRequest(connection, n.clientREQ().parse(data));
|
return handleRequest(connection, n.clientREQ().parse(data));
|
||||||
case "EVENT":
|
case 'EVENT':
|
||||||
return handleEvent(connection, n.clientEVENT().parse(data)[1]);
|
return handleEvent(connection, n.clientEVENT().parse(data)[1]);
|
||||||
case "CLOSE":
|
case 'CLOSE':
|
||||||
return handleClose(connection, n.clientCLOSE().parse(data)[1]);
|
return handleClose(connection, n.clientCLOSE().parse(data)[1]);
|
||||||
default:
|
default:
|
||||||
return log.warn("Invalid request");
|
return log.warn('Invalid request');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
socket.onclose = () => log.info("User disconnected");
|
socket.onclose = () => log.info('User disconnected');
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
return new Response("Eve Relay");
|
return new Response('Eve Relay');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
47
utils.ts
47
utils.ts
|
@ -1,19 +1,19 @@
|
||||||
import { exists } from "jsr:@std/fs";
|
import { decodeBase64, encodeBase64 } from 'jsr:@std/encoding@0.224/base64';
|
||||||
import * as nostrTools from "@nostr/tools";
|
import { exists } from 'jsr:@std/fs';
|
||||||
import * as nip06 from "@nostr/tools/nip06";
|
import * as nostrTools from '@nostr/tools';
|
||||||
import { decodeBase64, encodeBase64 } from "jsr:@std/encoding@0.224/base64";
|
import * as nip06 from '@nostr/tools/nip06';
|
||||||
import { getEveFilePath } from "./utils/files.ts";
|
|
||||||
import {
|
import {
|
||||||
decryptUint8Array,
|
decryptUint8Array,
|
||||||
encryptionKey,
|
|
||||||
encryptUint8Array,
|
encryptUint8Array,
|
||||||
} from "./utils/encryption.ts";
|
encryptionKey,
|
||||||
|
} from './utils/encryption.ts';
|
||||||
|
import { getEveFilePath } from './utils/files.ts';
|
||||||
|
|
||||||
export function isLocalhost(req: Request): boolean {
|
export function isLocalhost(req: Request): boolean {
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
const hostname = url.hostname;
|
const hostname = url.hostname;
|
||||||
return (
|
return (
|
||||||
hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost"
|
hostname === '127.0.0.1' || hostname === '::1' || hostname === 'localhost'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,11 +39,12 @@ export function randomTimeUpTo2DaysInThePast() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCCNPubkey(): Promise<string> {
|
export async function getCCNPubkey(): Promise<string> {
|
||||||
const ccnPubPath = await getEveFilePath("ccn.pub");
|
const ccnPubPath = await getEveFilePath('ccn.pub');
|
||||||
const seedPath = await getEveFilePath("ccn.seed");
|
const seedPath = await getEveFilePath('ccn.seed');
|
||||||
const doWeHaveKey = await exists(ccnPubPath);
|
const doWeHaveKey = await exists(ccnPubPath);
|
||||||
if (doWeHaveKey) return Deno.readTextFileSync(ccnPubPath);
|
if (doWeHaveKey) return Deno.readTextFileSync(ccnPubPath);
|
||||||
const ccnSeed = Deno.env.get("CCN_SEED") ||
|
const ccnSeed =
|
||||||
|
Deno.env.get('CCN_SEED') ||
|
||||||
((await exists(seedPath))
|
((await exists(seedPath))
|
||||||
? Deno.readTextFileSync(seedPath)
|
? Deno.readTextFileSync(seedPath)
|
||||||
: nip06.generateSeedWords());
|
: nip06.generateSeedWords());
|
||||||
|
@ -53,7 +54,7 @@ export async function getCCNPubkey(): Promise<string> {
|
||||||
|
|
||||||
Deno.writeTextFileSync(ccnPubPath, ccnPublicKey);
|
Deno.writeTextFileSync(ccnPubPath, ccnPublicKey);
|
||||||
Deno.writeTextFileSync(
|
Deno.writeTextFileSync(
|
||||||
await getEveFilePath("ccn.priv"),
|
await getEveFilePath('ccn.priv'),
|
||||||
encodeBase64(encryptedPrivateKey),
|
encodeBase64(encryptedPrivateKey),
|
||||||
);
|
);
|
||||||
Deno.writeTextFileSync(seedPath, ccnSeed);
|
Deno.writeTextFileSync(seedPath, ccnSeed);
|
||||||
|
@ -63,7 +64,7 @@ export async function getCCNPubkey(): Promise<string> {
|
||||||
|
|
||||||
export async function getCCNPrivateKey(): Promise<Uint8Array> {
|
export async function getCCNPrivateKey(): Promise<Uint8Array> {
|
||||||
const encryptedPrivateKey = Deno.readTextFileSync(
|
const encryptedPrivateKey = Deno.readTextFileSync(
|
||||||
await getEveFilePath("ccn.priv"),
|
await getEveFilePath('ccn.priv'),
|
||||||
);
|
);
|
||||||
return decryptUint8Array(decodeBase64(encryptedPrivateKey), encryptionKey);
|
return decryptUint8Array(decodeBase64(encryptedPrivateKey), encryptionKey);
|
||||||
}
|
}
|
||||||
|
@ -77,21 +78,25 @@ export function isAddressableEvent(kind: number): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRegularEvent(kind: number): boolean {
|
export function isRegularEvent(kind: number): boolean {
|
||||||
return (kind >= 1000 && kind < 10000) ||
|
return (
|
||||||
|
(kind >= 1000 && kind < 10000) ||
|
||||||
(kind >= 4 && kind < 45) ||
|
(kind >= 4 && kind < 45) ||
|
||||||
kind === 1 ||
|
kind === 1 ||
|
||||||
kind === 2;
|
kind === 2
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCCNReplaceableEvent(kind: number): boolean {
|
export function isCCNReplaceableEvent(kind: number): boolean {
|
||||||
return (kind >= 60000 && kind < 65536);
|
return kind >= 60000 && kind < 65536;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseATagQuery(
|
export function parseATagQuery(aTagValue: string): {
|
||||||
aTagValue: string,
|
kind: number;
|
||||||
): { kind: number; pubkey: string; dTag?: string } {
|
pubkey: string;
|
||||||
const parts = aTagValue.split(":");
|
dTag?: string;
|
||||||
if (parts.length < 2) return { kind: 0, pubkey: "" };
|
} {
|
||||||
|
const parts = aTagValue.split(':');
|
||||||
|
if (parts.length < 2) return { kind: 0, pubkey: '' };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: Number.parseInt(parts[0], 10),
|
kind: Number.parseInt(parts[0], 10),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
import { decodeBase64 } from 'jsr:@std/encoding/base64';
|
||||||
import { managedNonce } from "@noble/ciphers/webcrypto";
|
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
|
||||||
import { decodeBase64 } from "jsr:@std/encoding/base64";
|
import { managedNonce } from '@noble/ciphers/webcrypto';
|
||||||
export const encryptionKey = decodeBase64(Deno.env.get("ENCRYPTION_KEY") || "");
|
export const encryptionKey = decodeBase64(Deno.env.get('ENCRYPTION_KEY') || '');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypts a given Uint8Array using the XChaCha20-Poly1305 algorithm.
|
* Encrypts a given Uint8Array using the XChaCha20-Poly1305 algorithm.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { exists } from "jsr:@std/fs";
|
import { exists } from 'jsr:@std/fs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the path to Eve's configuration directory and ensures its existence.
|
* Return the path to Eve's configuration directory and ensures its existence.
|
||||||
|
@ -14,13 +14,11 @@ import { exists } from "jsr:@std/fs";
|
||||||
|
|
||||||
export async function getEveConfigHome(): Promise<string> {
|
export async function getEveConfigHome(): Promise<string> {
|
||||||
let storagePath: string;
|
let storagePath: string;
|
||||||
if (Deno.build.os === "darwin") {
|
if (Deno.build.os === 'darwin') {
|
||||||
storagePath = `${
|
storagePath = `${Deno.env.get('HOME')}/Library/Application Support/eve/arx/Eve`;
|
||||||
Deno.env.get("HOME")
|
|
||||||
}/Library/Application Support/eve/arx/Eve`;
|
|
||||||
} else {
|
} else {
|
||||||
const xdgConfigHome = Deno.env.get("XDG_CONFIG_HOME") ??
|
const xdgConfigHome =
|
||||||
`${Deno.env.get("HOME")}/.config`;
|
Deno.env.get('XDG_CONFIG_HOME') ?? `${Deno.env.get('HOME')}/.config`;
|
||||||
storagePath = `${xdgConfigHome}/arx/Eve`;
|
storagePath = `${xdgConfigHome}/arx/Eve`;
|
||||||
}
|
}
|
||||||
if (!(await exists(storagePath))) {
|
if (!(await exists(storagePath))) {
|
||||||
|
|
|
@ -1,59 +1,59 @@
|
||||||
import * as colors from "jsr:@std/fmt@^1.0.4/colors";
|
import * as colors from 'jsr:@std/fmt@^1.0.4/colors';
|
||||||
import * as log from "jsr:@std/log";
|
import * as log from 'jsr:@std/log';
|
||||||
import { getEveFilePath } from "./files.ts";
|
import { getEveFilePath } from './files.ts';
|
||||||
export * as log from "jsr:@std/log";
|
export * as log from 'jsr:@std/log';
|
||||||
|
|
||||||
export async function setupLogger() {
|
export async function setupLogger() {
|
||||||
const formatLevel = (level: number): string => {
|
const formatLevel = (level: number): string => {
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
10: colors.gray("[DEBUG]"),
|
10: colors.gray('[DEBUG]'),
|
||||||
20: colors.green("[INFO] "),
|
20: colors.green('[INFO] '),
|
||||||
30: colors.yellow("[WARN] "),
|
30: colors.yellow('[WARN] '),
|
||||||
40: colors.red("[ERROR]"),
|
40: colors.red('[ERROR]'),
|
||||||
50: colors.bgRed("[FATAL]"),
|
50: colors.bgRed('[FATAL]'),
|
||||||
}[level] || `[LVL${level}]`
|
}[level] || `[LVL${level}]`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const levelName = (level: number): string => {
|
const levelName = (level: number): string => {
|
||||||
return {
|
return (
|
||||||
10: "DEBUG",
|
{
|
||||||
20: "INFO",
|
10: 'DEBUG',
|
||||||
30: "WARN",
|
20: 'INFO',
|
||||||
40: "ERROR",
|
30: 'WARN',
|
||||||
50: "FATAL",
|
40: 'ERROR',
|
||||||
}[level] || `LVL${level}`;
|
50: 'FATAL',
|
||||||
|
}[level] || `LVL${level}`
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatArg = (arg: unknown): string => {
|
const formatArg = (arg: unknown): string => {
|
||||||
if (typeof arg === "object") return JSON.stringify(arg);
|
if (typeof arg === 'object') return JSON.stringify(arg);
|
||||||
return String(arg);
|
return String(arg);
|
||||||
};
|
};
|
||||||
|
|
||||||
await log.setup({
|
await log.setup({
|
||||||
handlers: {
|
handlers: {
|
||||||
console: new log.ConsoleHandler("DEBUG", {
|
console: new log.ConsoleHandler('DEBUG', {
|
||||||
useColors: true,
|
useColors: true,
|
||||||
formatter: (record) => {
|
formatter: (record) => {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
let msg = `${colors.dim(`[${timestamp}]`)} ${
|
let msg = `${colors.dim(`[${timestamp}]`)} ${formatLevel(record.level)} ${record.msg}`;
|
||||||
formatLevel(record.level)
|
|
||||||
} ${record.msg}`;
|
|
||||||
|
|
||||||
if (record.args.length > 0) {
|
if (record.args.length > 0) {
|
||||||
const args = record.args
|
const args = record.args
|
||||||
.map((arg, i) => `${colors.dim(`arg${i}:`)} ${formatArg(arg)}`)
|
.map((arg, i) => `${colors.dim(`arg${i}:`)} ${formatArg(arg)}`)
|
||||||
.join(" ");
|
.join(' ');
|
||||||
msg += ` ${colors.dim("|")} ${args}`;
|
msg += ` ${colors.dim('|')} ${args}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg;
|
return msg;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
file: new log.FileHandler("DEBUG", {
|
file: new log.FileHandler('DEBUG', {
|
||||||
filename: Deno.env.get("LOG_FILE") ||
|
filename:
|
||||||
await getEveFilePath("eve-logs.jsonl"),
|
Deno.env.get('LOG_FILE') || (await getEveFilePath('eve-logs.jsonl')),
|
||||||
formatter: (record) => {
|
formatter: (record) => {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
|
@ -67,8 +67,8 @@ export async function setupLogger() {
|
||||||
},
|
},
|
||||||
loggers: {
|
loggers: {
|
||||||
default: {
|
default: {
|
||||||
level: "DEBUG",
|
level: 'DEBUG',
|
||||||
handlers: ["console", "file"],
|
handlers: ['console', 'file'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { BindValue, Database } from "@db/sqlite";
|
import type { BindValue, Database } from '@db/sqlite';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a SQL query with placeholders for values.
|
* Construct a SQL query with placeholders for values.
|
||||||
|
@ -23,8 +23,8 @@ export function sqlPartial(
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
query: segments.reduce(
|
query: segments.reduce(
|
||||||
(acc, str, i) => acc + str + (i < values.length ? "?" : ""),
|
(acc, str, i) => acc + str + (i < values.length ? '?' : ''),
|
||||||
"",
|
'',
|
||||||
),
|
),
|
||||||
values: values,
|
values: values,
|
||||||
};
|
};
|
||||||
|
@ -72,7 +72,7 @@ export function mixQuery(...queries: { query: string; values: BindValue[] }[]) {
|
||||||
query: `${acc.query} ${query}`,
|
query: `${acc.query} ${query}`,
|
||||||
values: [...acc.values, ...values],
|
values: [...acc.values, ...values],
|
||||||
}),
|
}),
|
||||||
{ query: "", values: [] },
|
{ query: '', values: [] },
|
||||||
);
|
);
|
||||||
return { query, values };
|
return { query, values };
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue