Something went wrong. Try again.
A decentralized music tracking and discovery platform built on AT Protocol ๐ต rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253import { HeadBucketCommand, S3Client } from "@aws-sdk/client-s3";import { ctx } from "context";import { and, eq } from "drizzle-orm";import { Hono } from "hono";import { env } from "lib/env";import { decryptCredential, encryptCredential } from "lib/storage-crypto";import { verifyToken } from "lib/verifyToken";import tables from "schema";
const app = new Hono();
async function resolveUser(authHeader: string | undefined | null) { const bearer = (authHeader || "").split(" ")[1]?.trim(); if (!bearer || bearer === "null") return null; const { did } = (await verifyToken(bearer)) as { did: string }; return ctx.db .select() .from(tables.users) .where(eq(tables.users.did, did)) .limit(1) .then((rows) => rows[0] ?? null);}
// POST /storage/providers --------------------------------------------------------// Validates S3 connectivity before persisting. Credentials are encrypted at rest.app.post("/providers", async (c) => { const user = await resolveUser(c.req.header("authorization")); if (!user) return c.text("Unauthorized", 401);
let body: { label: string; endpoint: string; region?: string; bucket: string; access_key: string; secret_key: string; public_url?: string; }; try { body = await c.req.json(); } catch { return c.text("Invalid JSON", 400); }
const { label, endpoint, region = "auto", bucket, access_key, secret_key, public_url, } = body;
if (!label || !endpoint || !bucket || !access_key || !secret_key) { return c.text( "Missing required fields: label, endpoint, bucket, access_key, secret_key", 400, ); }
const client = new S3Client({ region, endpoint, credentials: { accessKeyId: access_key, secretAccessKey: secret_key }, });
try { await client.send(new HeadBucketCommand({ Bucket: bucket })); } catch (e: unknown) { // HeadBucket failures mostly surface as bare status codes ("UnknownError"), // which tells the person debugging their bucket nothing. Name the cause. const err = e as { name?: string; message?: string; code?: string; $metadata?: { httpStatusCode?: number }; }; const status = err.$metadata?.httpStatusCode; const message = status === 403 || err.name === "Forbidden" ? "The bucket refused these credentials (403). Check the access key, secret key, and that they have permission on this bucket." : status === 404 || err.name === "NotFound" ? `Bucket "${bucket}" was not found at that endpoint (404). Check the bucket name and endpoint URL.` : status === 301 ? "The bucket exists but in a different region (301). Set the region this bucket actually lives in." : err.code === "ENOTFOUND" || err.code === "ECONNREFUSED" ? "Could not reach that endpoint. Check the endpoint URL." : err.message && err.message !== "UnknownError" ? err.message : "Failed to connect to the S3 bucket with these credentials."; return c.json({ error: "CONNECTIVITY_FAILED", message }, 422); }
const [encryptedAccessKey, encryptedSecretKey] = await Promise.all([ encryptCredential(access_key), encryptCredential(secret_key), ]);
const [provider] = await ctx.db .insert(tables.userStorageProviders) .values({ userId: user.id, label, endpoint, region, bucket, accessKey: encryptedAccessKey, secretKey: encryptedSecretKey, publicUrl: public_url ?? null, verifiedAt: new Date(), }) .returning();
return c.json( { id: provider.id, label: provider.label, endpoint: provider.endpoint, region: provider.region, bucket: provider.bucket, public_url: provider.publicUrl, verified_at: provider.verifiedAt, created_at: provider.createdAt, }, 201, );});
// GET /storage/providers ---------------------------------------------------------app.get("/providers", async (c) => { const user = await resolveUser(c.req.header("authorization")); if (!user) return c.text("Unauthorized", 401);
const providers = await ctx.db .select({ id: tables.userStorageProviders.id, label: tables.userStorageProviders.label, endpoint: tables.userStorageProviders.endpoint, region: tables.userStorageProviders.region, bucket: tables.userStorageProviders.bucket, public_url: tables.userStorageProviders.publicUrl, verified_at: tables.userStorageProviders.verifiedAt, created_at: tables.userStorageProviders.createdAt, }) .from(tables.userStorageProviders) .where(eq(tables.userStorageProviders.userId, user.id));
return c.json(providers);});
// DELETE /storage/providers/:id --------------------------------------------------app.delete("/providers/:id", async (c) => { const user = await resolveUser(c.req.header("authorization")); if (!user) return c.text("Unauthorized", 401);
const providerId = c.req.param("id");
const [provider] = await ctx.db .select() .from(tables.userStorageProviders) .where( and( eq(tables.userStorageProviders.id, providerId), eq(tables.userStorageProviders.userId, user.id), ), ) .limit(1);
if (!provider) return c.text("Not found", 404);
const [referencingUpload] = await ctx.db .select({ id: tables.userUploads.id }) .from(tables.userUploads) .where(eq(tables.userUploads.storageProviderId, providerId)) .limit(1);
if (referencingUpload) { return c.json( { error: "PROVIDER_IN_USE", message: "Cannot delete a storage provider that has uploads referencing it", }, 409, ); }
await ctx.db .delete(tables.userStorageProviders) .where(eq(tables.userStorageProviders.id, providerId));
return new Response(null, { status: 204 });});
// providerId = null โ use Rocksky managed storage// providerId = string โ use the specified BYO provider (must belong to userId)export async function resolveStorageClient( userId: string, providerId: string | null,): Promise<{ client: S3Client; bucket: string; storageProviderId: string | null;}> { if (!providerId) { return { client: new S3Client({ region: env.S3_REGION, endpoint: env.S3_ENDPOINT, credentials: { accessKeyId: env.S3_ACCESS_KEY_ID, secretAccessKey: env.S3_SECRET_ACCESS_KEY, }, }), bucket: env.S3_BUCKET_NAME, storageProviderId: null, }; }
const [provider] = await ctx.db .select() .from(tables.userStorageProviders) .where( and( eq(tables.userStorageProviders.id, providerId), eq(tables.userStorageProviders.userId, userId), ), ) .limit(1);
if (!provider) { throw new Error(`Storage provider ${providerId} not found for user`); }
const [accessKey, secretKey] = await Promise.all([ decryptCredential(provider.accessKey), decryptCredential(provider.secretKey), ]);
return { client: new S3Client({ region: provider.region, endpoint: provider.endpoint, credentials: { accessKeyId: accessKey, secretAccessKey: secretKey }, }), bucket: provider.bucket, storageProviderId: provider.id, };}
export default app;