A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1/**
2 * Upload a single image (the publication logo) to the writer's PDS and return its stored blob
3 * ref (SP10, step C). Reuses the same `agent.uploadBlob` path as the editor's media pipeline
4 * (Decision 0006); the returned `BlobRefJson` is what gets baked into the publication record's
5 * `icon` field. Enforces the lexicon's icon limits (≤1MB, `image/*`) client-side, before upload.
6 */
7import type { Agent } from '@atproto/api';
8import type { BlobRefJson } from './blob';
9
10/** The `site.standard.publication.icon` lexicon limit. */
11export const PUBLICATION_ICON_MAX_BYTES = 1_000_000;
12
13/** Thrown for client-side validation failures (wrong type / too large) — shown to the user. */
14export class ImageValidationError extends Error {
15 constructor( message: string ) {
16 super( message );
17 this.name = 'ImageValidationError';
18 }
19}
20
21export async function uploadImageBlob(
22 agent: Agent,
23 file: File,
24 options: { maxBytes?: number } = {}
25): Promise< BlobRefJson > {
26 const maxBytes = options.maxBytes ?? PUBLICATION_ICON_MAX_BYTES;
27 if ( ! file.type.startsWith( 'image/' ) ) {
28 throw new ImageValidationError( 'Choose an image file (PNG, JPG, GIF, WebP, …).' );
29 }
30 if ( file.size > maxBytes ) {
31 const limitMb = Math.round( ( maxBytes / 1_000_000 ) * 100 ) / 100;
32 throw new ImageValidationError( `Image must be ${ limitMb }MB or smaller.` );
33 }
34 const res = await agent.uploadBlob( file, { encoding: file.type } );
35 const { blob } = res.data;
36 return {
37 $type: 'blob',
38 ref: { $link: blob.ref.toString() },
39 mimeType: blob.mimeType,
40 size: blob.size,
41 };
42}