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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108import { createHash, randomUUID, timingSafeEqual } from "node:crypto";import type { Track } from "types/track";
export class SubmissionUnavailable extends Error {}
export function validSubmissionSecret( provided: string | undefined, expected: string,): boolean { if (!provided || expected.length < 32) return false; const a = Buffer.from(provided); const b = Buffer.from(expected); return a.length === b.length && timingSafeEqual(a, b);}
// Generated once by Rust and saved with the job; respect the lexicon's TID key.export function submissionRecordKey(key: unknown): string { if ( typeof key !== "string" || !/^[234567ab][234567abcdefghijklmnopqrstuvwxyz]{12}$/.test(key) ) throw new Error("Invalid submission record key"); return key;}
export function submissionInput( did: string, track: Track, explicitKey?: string,) { // The transport schema accepts YYYY-MM-DD, whereas trackSchema produces Date. const payload = { ...track, releaseDate: track.releaseDate?.toISOString().slice(0, 10) ?? track.releaseDate, }; const key = explicitKey || (track.timestamp ? createHash("sha256") .update( JSON.stringify([ did, track.title.toLowerCase(), track.artist.toLowerCase(), track.album.toLowerCase(), track.timestamp, ]), ) .digest("hex") : randomUUID()); return { did, track: payload, idempotencyKey: key };}
export async function enqueueSubmission( config: { url: string; secret: string }, did: string, track: Track, explicitKey?: string, skipTeal = false,): Promise<string> { return enqueueDurable(config, "/v1/submissions", { ...submissionInput(did, track, explicitKey), ...(skipTeal ? { skipTeal: true } : {}), });}
export async function enqueueTealSubmission( config: { url: string; secret: string }, did: string, record: unknown, idempotencyKey: string,): Promise<string> { return enqueueDurable(config, "/v1/tealfm", { did, record, idempotencyKey });}
async function enqueueDurable( config: { url: string; secret: string }, path: string, input: unknown,): Promise<string> { if (!config.url || config.secret.length < 32) throw new SubmissionUnavailable("Durable submissions are not configured"); try { const response = await fetch(`${config.url.replace(/\/$/, "")}${path}`, { method: "POST", headers: { "content-type": "application/json", "x-rocksky-submissions-secret": config.secret, }, body: JSON.stringify(input), signal: AbortSignal.timeout(15_000), redirect: "error", }); if (response.status !== 202) throw new Error(`Queue returned ${response.status}`); const job = (await response.json()) as { id?: string }; if (!job.id || !/^[a-f0-9]{64}$/.test(job.id)) throw new Error("Invalid queue acknowledgement"); return job.id; } catch { // Never fall back to an in-memory pipeline, and never expose the secret. throw new SubmissionUnavailable( "Submission could not be durably accepted; retry with the same timestamp or Idempotency-Key", ); }}