Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374import { validateManifest } from './manifest-validator'import type { PluginManifest } from '../types'import type { CatalogBundle } from './catalog-storage'
export interface CatalogPlugin { waitForMetadata?: boolean status?: 'available' | 'blocked' manifest: PluginManifest repository: string notes: string testedAt: string bundlePath: string files: { 'main.js': string; 'styles.css'?: string }}
export const catalogUrl = new URL( import.meta.env.VITE_PLUGIN_CATALOG_URL || '/plugin-catalog/catalog.json', location.href,).hrefconst cacheKey = `reams:plugin-catalog:${catalogUrl}`
export function parseCatalog(value: unknown): CatalogPlugin[] { const catalog = value as { schemaVersion?: number; plugins?: unknown[] } if (catalog?.schemaVersion !== 1 || !Array.isArray(catalog.plugins)) throw new Error('Unsupported plugin catalog') const ids = new Set<string>() return catalog.plugins.map(value => { const entry = value as CatalogPlugin validateManifest(entry.manifest) if (entry.waitForMetadata !== undefined && typeof entry.waitForMetadata !== 'boolean') throw new Error('Invalid metadata prerequisite') if (entry.status !== undefined && entry.status !== 'available' && entry.status !== 'blocked') throw new Error('Invalid compatibility status') if (ids.has(entry.manifest.id)) throw new Error('Duplicate plugin in catalog') ids.add(entry.manifest.id) for (const key of ['notes', 'testedAt', 'bundlePath', 'repository'] as const) { if (typeof entry[key] !== 'string' || !entry[key]) throw new Error(`Invalid catalog ${key}`) } if (!/^https:\/\//.test(entry.repository)) throw new Error('Invalid plugin repository') if (!/^bundles\/[a-z0-9-]+\/[a-zA-Z0-9._-]+\/$/.test(entry.bundlePath)) throw new Error('Invalid bundle path') if (!entry.files || !/^[a-f0-9]{64}$/.test(entry.files['main.js'])) throw new Error('Missing bundle digest') if (entry.files['styles.css'] && !/^[a-f0-9]{64}$/.test(entry.files['styles.css'])) throw new Error('Invalid stylesheet digest') return entry })}
export function cachedCatalog(): CatalogPlugin[] { try { return parseCatalog(JSON.parse(localStorage.getItem(cacheKey) || 'null')) } catch { return [] }}
export async function fetchCatalog(signal?: AbortSignal): Promise<CatalogPlugin[]> { const response = await fetch(catalogUrl, { cache: 'no-cache', signal }) if (!response.ok) throw new Error(`Catalog unavailable (HTTP ${response.status})`) const value: unknown = await response.json() const entries = parseCatalog(value) try { localStorage.setItem(cacheKey, JSON.stringify(value)) } catch { /* Installed bundles live in IndexedDB. */ } return entries}
async function downloadFile(entry: CatalogPlugin, file: 'main.js' | 'styles.css'): Promise<string> { const response = await fetch(new URL(`${entry.bundlePath}${file}`, catalogUrl)) if (!response.ok) throw new Error(`Could not download ${file} (HTTP ${response.status})`) const bytes = await response.arrayBuffer() const digest = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), b => b.toString(16).padStart(2, '0')).join('') if (digest !== entry.files[file]) throw new Error(`The downloaded ${file} does not match the tested release`) return new TextDecoder().decode(bytes)}
export async function downloadCatalogBundle(entry: CatalogPlugin): Promise<CatalogBundle> { if (entry.status === 'blocked') throw new Error('This plugin needs compatibility fixes before it can be installed') const [mainJs, stylesCss] = await Promise.all([ downloadFile(entry, 'main.js'), entry.files['styles.css'] ? downloadFile(entry, 'styles.css') : undefined, ]) return { manifest: entry.manifest, mainJs, stylesCss, sourceUrl: entry.repository, installedAt: Date.now(), waitForMetadata: entry.waitForMetadata }}