chore: reformat, logo

This commit is contained in:
florian 2024-03-21 21:01:02 +01:00
parent 8649e6dbdf
commit b5f737ca37
2 changed files with 138 additions and 136 deletions

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"printWidth": 120,
"useTabs": false,
"tabWidth": 2,
"singleQuote": true
}

View File

@ -1,16 +1,14 @@
import type {
KVNamespace,
R2Bucket,
ReadableStream,
} from "@cloudflare/workers-types";
import dayjs from "dayjs";
import { Hono } from "hono";
import { cache } from "hono/cache";
import { cors } from "hono/cors";
import { HTTPException } from "hono/http-exception";
import { stream } from "hono/streaming";
import { validateEvent, verifyEvent } from "nostr-tools";
import type { Event } from "nostr-tools";
import { cache } from 'hono/cache';
import { cors } from 'hono/cors';
import { Hono } from 'hono';
import { html } from 'hono/html';
import { HTTPException } from 'hono/http-exception';
import { logger } from 'hono/logger';
import { stream } from 'hono/streaming';
import { validateEvent, verifyEvent } from 'nostr-tools';
import dayjs from 'dayjs';
import type { KVNamespace, R2Bucket, ReadableStream } from '@cloudflare/workers-types';
import type { Event } from 'nostr-tools';
type Bindings = {
BLOSSOM_BUCKET: R2Bucket;
@ -29,128 +27,43 @@ type BlobDescriptor = BlobData & {
url: string;
};
type AuthType = "upload" | "delete";
const app = new Hono<{ Bindings: Bindings }>();
const parseHashFromPath = (path: string) => {
const match = path.toLowerCase().match(/\/([0-9a-f]{64})(\.[a-z]+)?/);
const hash = match && match[1];
const ext = match && match[2];
return { hash, ext };
};
app.use("*", cors());
app.get("/list/:pubkey", async (c) => {
const pubkey = c.req.param("pubkey");
const keyList = await c.env.KV_BLOSSOM.list({ prefix: pubkey + ":" });
// TODO paging is needed for >1000 blobs
const finalList: any[] = [];
await Promise.all(
keyList.keys.map(async (key) => {
const descriptor = await c.env.KV_BLOSSOM.get(key.name);
const [_, blobKey] = key.name.split(":");
if (descriptor) {
const blobDescriptor: BlobDescriptor = {
...(JSON.parse(descriptor) as BlobData),
sha256: blobKey,
url: c.env.PUBLIC_URL + "/" + blobKey,
};
blobKey;
finalList.push(blobDescriptor);
}
})
);
return await c.json(finalList);
});
app.get(
"*",
cache({
cacheName: "my-app",
cacheControl: "max-age=3600",
})
);
app.get("*", async (c) => {
const { hash } = parseHashFromPath(c.req.path);
if (!hash) {
throw new HTTPException(400, { message: "Invalid path, hash missing" });
}
if (c.req.method == "HEAD") {
const blob = await c.env.BLOSSOM_BUCKET.head(hash);
if (!blob) {
throw new HTTPException(404, { message: "Blob hash not found." });
}
c.status(200);
return c.body("");
} else {
const blob = await c.env.BLOSSOM_BUCKET.get(hash);
if (!blob) {
throw new HTTPException(404, { message: "Blob hash not found." });
}
return stream(c, async (stream) => {
// Write a process to be executed when aborted.
stream.onAbort(() => {
console.warn("Aborted!");
});
c.header(
"Content-Type",
blob.httpMetadata?.contentType || "application/octet-stream"
);
// TODO maybe redirect if file > xx MB
await stream.pipe(blob.body as ReadableStream<any>);
});
}
});
type AuthType = 'upload' | 'delete';
const getAuth = (authHeader?: string) => {
if (!authHeader?.startsWith("Nostr "))
throw new HTTPException(400, { message: "Missing Nostr: Auth Scheme." });
if (!authHeader?.startsWith('Nostr ')) throw new HTTPException(400, { message: 'Missing Nostr: Auth Scheme.' });
const authEvent = authHeader
? (JSON.parse(atob(authHeader.replace(/^Nostr\s/i, ""))) as Event)
: undefined;
const authEvent = authHeader ? (JSON.parse(atob(authHeader.replace(/^Nostr\s/i, ''))) as Event) : undefined;
if (!authEvent) {
throw new HTTPException(400, { message: "Nostr-Auth Token is invalid." });
throw new HTTPException(400, { message: 'Nostr-Auth Token is invalid.' });
}
if (!validateEvent(authEvent)) {
throw new HTTPException(400, { message: "Auth event is not valid." });
throw new HTTPException(400, { message: 'Auth event is not valid.' });
}
if (!verifyEvent(authEvent)) {
throw new HTTPException(400, {
message: "Auth event signature is not valid.",
message: 'Auth event signature is not valid.',
});
}
if (authEvent.kind !== 24242) {
throw new HTTPException(400, { message: "Unexpected auth kind" });
throw new HTTPException(400, { message: 'Unexpected auth kind' });
}
const type = authEvent.tags.find((t) => t[0] === "t")?.[1] as AuthType;
const type = authEvent.tags.find((t) => t[0] === 't')?.[1] as AuthType;
if (!type) {
throw new HTTPException(400, { message: "Auth missing type" });
throw new HTTPException(400, { message: 'Auth missing type' });
}
const now = dayjs().unix();
const expiration = authEvent.tags.find((t) => t[0] === "expiration")?.[1];
const expiration = authEvent.tags.find((t) => t[0] === 'expiration')?.[1];
if (!expiration) {
throw new HTTPException(400, { message: "Auth missing expiration" });
throw new HTTPException(400, { message: 'Auth missing expiration' });
}
if (parseInt(expiration) < now) {
throw new HTTPException(400, { message: "Auth expired" });
throw new HTTPException(400, { message: 'Auth expired' });
}
return {
@ -160,38 +73,121 @@ const getAuth = (authHeader?: string) => {
};
};
const authTokenWasUsedBefore = async (
auth: { event: Event; type: AuthType },
kv: KVNamespace
) => {
const authTokenWasUsedBefore = async (auth: { event: Event; type: AuthType }, kv: KVNamespace) => {
const key = `auth:${auth.type}:${auth.event.id}`;
const usedBefore = (await kv.get(key)) != null;
await kv.put(key, "used", { expirationTtl: 60 * 60 * 24 * 10 }); // 10 days
await kv.put(key, 'used', { expirationTtl: 60 * 60 * 24 * 10 }); // 10 days
return usedBefore;
};
app.put("/upload", async (c) => {
const auth = getAuth(c.req.header("authorization") as string);
const parseHashFromPath = (path: string) => {
const match = path.toLowerCase().match(/\/([0-9a-f]{64})(\.[a-z]+)?/);
const hash = match && match[1];
const ext = match && match[2];
return { hash, ext };
};
if (auth.type !== "upload") {
throw new HTTPException(400, { message: "Auth type not allowed." });
const app = new Hono<{ Bindings: Bindings }>();
app.use(cors());
app.use(logger());
app.get('/', (c) =>
c.html(
html`<!DOCTYPE html>
<body style="font-size: 5vw;background-color: #111;position: fixed;top: 50%;left: 50%;translate: -50% -50%;">
🌸
</body> `
)
);
app.get(
cache({
cacheName: 'blossom',
cacheControl: 'max-age=604800',
})
);
app.get('/list/:pubkey', async (c) => {
const pubkey = c.req.param('pubkey');
const keyList = await c.env.KV_BLOSSOM.list({ prefix: pubkey + ':' });
// TODO paging is needed for >1000 blobs
const finalList: any[] = [];
await Promise.all(
keyList.keys.map(async (key) => {
const descriptor = await c.env.KV_BLOSSOM.get(key.name);
const [_, blobKey] = key.name.split(':');
if (descriptor) {
const blobDescriptor: BlobDescriptor = {
...(JSON.parse(descriptor) as BlobData),
sha256: blobKey,
url: c.env.PUBLIC_URL + '/' + blobKey,
};
blobKey;
finalList.push(blobDescriptor);
}
})
);
return await c.json(finalList);
});
app.get('*', async (c) => {
const { hash } = parseHashFromPath(c.req.path);
if (!hash) {
throw new HTTPException(400, { message: 'Invalid path, hash missing' });
}
if (c.req.method == 'HEAD') {
const blob = await c.env.BLOSSOM_BUCKET.head(hash);
if (!blob) {
throw new HTTPException(404, { message: 'Blob hash not found.' });
}
return c.body(null, 204);
} else {
const blob = await c.env.BLOSSOM_BUCKET.get(hash);
if (!blob) {
throw new HTTPException(404, { message: 'Blob hash not found.' });
}
return stream(c, async (stream) => {
// Write a process to be executed when aborted.
stream.onAbort(() => {
console.warn('Aborted!');
});
c.header('Content-Type', blob.httpMetadata?.contentType || 'application/octet-stream');
// TODO maybe redirect if file > xx MB
await stream.pipe(blob.body as ReadableStream<any>);
});
}
});
app.put('/upload', async (c) => {
const auth = getAuth(c.req.header('authorization') as string);
if (auth.type !== 'upload') {
throw new HTTPException(400, { message: 'Auth type not allowed.' });
}
if (await authTokenWasUsedBefore(auth, c.env.KV_BLOSSOM)) {
throw new HTTPException(400, { message: "Token was used before." });
throw new HTTPException(400, { message: 'Token was used before.' });
}
const uploadBuffer = await c.req.raw.arrayBuffer();
if (uploadBuffer) {
const keyBuffer = await crypto.subtle.digest("SHA-256", uploadBuffer);
const keyBuffer = await crypto.subtle.digest('SHA-256', uploadBuffer);
const key = Array.from(new Uint8Array(keyBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
const contentType = c.req.header("content-type");
const contentType = c.req.header('content-type');
const pubkey = auth.event.pubkey;
const storedObject = await c.env.BLOSSOM_BUCKET.put(key, uploadBuffer, {
@ -208,42 +204,42 @@ app.put("/upload", async (c) => {
created: new Date(storedObject.uploaded).getTime(),
};
console.log(`writing to ${pubkey + ":" + key}`, JSON.stringify(blobData));
await c.env.KV_BLOSSOM.put(pubkey + ":" + key, JSON.stringify(blobData));
console.log(`writing to ${pubkey + ':' + key}`, JSON.stringify(blobData));
await c.env.KV_BLOSSOM.put(pubkey + ':' + key, JSON.stringify(blobData));
c.status(200);
return c.json({
...blobData,
url: c.env.PUBLIC_URL + "/" + key,
url: c.env.PUBLIC_URL + '/' + key,
sha256: key,
});
}
});
app.delete("*", async (c) => {
const auth = getAuth(c.req.header("authorization") as string);
app.delete('*', async (c) => {
const auth = getAuth(c.req.header('authorization') as string);
if (auth.type !== "delete") {
throw new HTTPException(400, { message: "Auth type not allowed." });
if (auth.type !== 'delete') {
throw new HTTPException(400, { message: 'Auth type not allowed.' });
}
if (await authTokenWasUsedBefore(auth, c.env.KV_BLOSSOM)) {
throw new HTTPException(400, { message: "Token was used before." });
throw new HTTPException(400, { message: 'Token was used before.' });
}
const pubkey = auth.event.pubkey;
const { hash } = parseHashFromPath(c.req.path);
if (!hash) {
throw new HTTPException(400, { message: "Invalid path, hash missing" });
throw new HTTPException(400, { message: 'Invalid path, hash missing' });
}
try {
await c.env.BLOSSOM_BUCKET.delete(hash);
await c.env.KV_BLOSSOM.delete(pubkey + ":" + hash);
return c.body("");
await c.env.KV_BLOSSOM.delete(pubkey + ':' + hash);
c.body(null, 204);
} catch (e) {
throw new HTTPException(404, { message: "Blob not found." });
throw new HTTPException(404, { message: 'Blob not found.' });
}
});