diff --git a/scripts/deploy.mjs b/scripts/deploy.mjs index aaf0217..b80f29c 100644 --- a/scripts/deploy.mjs +++ b/scripts/deploy.mjs @@ -228,8 +228,88 @@ async function resolveBlogManifestTxId() { return match?.[1] || ""; } +async function resolveAoConfig() { + const source = await fs.readFile(sourceConfigPath, "utf8"); + const aoUrlMatch = source.match(/AO_URL\s*=\s*"([^"]+)"/); + const aoProcessIdMatch = source.match(/AO_PROCESS_ID\s*=\s*"([^"]+)"/); + return { + aoUrl: aoUrlMatch?.[1] || "", + aoProcessId: aoProcessIdMatch?.[1] || "" + }; +} + +const asObject = (value) => (typeof value === "object" && value !== null ? value : null); +const asString = (value) => (typeof value === "string" && value.length > 0 ? value : null); + +function findManifestIdInTags(tagsValue) { + const tags = Array.isArray(tagsValue) ? tagsValue : []; + for (const tag of tags) { + const tagObject = asObject(tag); + if (!tagObject) continue; + const name = asString(tagObject.name) ?? asString(tagObject.Name); + const value = asString(tagObject.value) ?? asString(tagObject.Value); + if (name === "LatestManifestId" && value) return value; + } + return ""; +} + +function extractManifestId(payload) { + const root = asObject(payload); + const results = asObject(root?.results); + if (!results) return ""; + + const outbox = asObject(results.outbox); + if (outbox) { + for (const message of Object.values(outbox)) { + const messageObject = asObject(message); + if (!messageObject) continue; + const direct = asString(messageObject.LatestManifestId); + if (direct) return direct; + const tagged = findManifestIdInTags(messageObject.Tags); + if (tagged) return tagged; + } + } + + const raw = asObject(results.raw); + const rawMessages = Array.isArray(raw?.Messages) ? raw.Messages : []; + for (const message of rawMessages) { + const messageObject = asObject(message); + if (!messageObject) continue; + const tagged = findManifestIdInTags(messageObject.Tags); + if (tagged) return tagged; + } + + const json = asObject(results.json); + const body = json?.body; + const parsedBody = typeof body === "string" ? asObject(JSON.parse(body)) : asObject(body); + const jsonMessages = Array.isArray(parsedBody?.Messages) ? parsedBody.Messages : []; + for (const message of jsonMessages) { + const messageObject = asObject(message); + if (!messageObject) continue; + const tagged = findManifestIdInTags(messageObject.Tags); + if (tagged) return tagged; + } + + return ""; +} + +async function getLatestManifestFromAo() { + const { aoUrl, aoProcessId } = await resolveAoConfig(); + if (!aoUrl || !aoProcessId) return ""; + + try { + const url = `${aoUrl}/${aoProcessId}~process@1.0/compute?Action=Get&require-codec=application/json&accept-bundle=true`; + const response = await fetch(url, { cache: "no-store" }); + if (!response.ok) return ""; + const payload = await response.json(); + return extractManifestId(payload); + } catch { + return ""; + } +} + async function resolvePostSlugs(gateway) { - const blogManifestTxId = await resolveBlogManifestTxId(); + const blogManifestTxId = (await getLatestManifestFromAo()) || (await resolveBlogManifestTxId()); if (!blogManifestTxId) return []; try { diff --git a/src/lib.ts b/src/lib.ts index c4e0802..836088f 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -1,7 +1,7 @@ import DOMPurify from "dompurify"; import { marked } from "marked"; import { parse as parseYaml } from "yaml"; -import { ARWEAVE_GATEWAY, MANIFEST_TX_ID } from "./config"; +import { AO_PROCESS_ID, AO_URL, ARWEAVE_GATEWAY, MANIFEST_TX_ID } from "./config"; import { parseManifest, type Frontmatter, type ManifestPost } from "./types"; const formatDate = (date: string): string => @@ -25,8 +25,96 @@ export const getReadableDate = (date?: string | null): string | null => { return formatDate(date); }; +const MANIFEST_ID_CACHE_TTL_MS = 60_000; +let manifestIdCache: { expiresAt: number; manifestId: string } | null = null; + +const asObject = (value: unknown): Record | null => + typeof value === "object" && value !== null ? (value as Record) : null; + +const asString = (value: unknown): string | null => + typeof value === "string" && value.length > 0 ? value : null; + +const findManifestIdInTags = (tagsValue: unknown): string | null => { + const tags = Array.isArray(tagsValue) ? tagsValue : []; + for (const tag of tags) { + const tagObject = asObject(tag); + if (!tagObject) continue; + const name = asString(tagObject.name) ?? asString(tagObject.Name); + const value = asString(tagObject.value) ?? asString(tagObject.Value); + if (name === "LatestManifestId" && value) return value; + } + return null; +}; + +const extractManifestId = (payload: unknown): string | null => { + const root = asObject(payload); + const results = asObject(root?.results); + if (!results) return null; + + const outbox = asObject(results.outbox); + if (outbox) { + for (const message of Object.values(outbox)) { + const messageObject = asObject(message); + if (!messageObject) continue; + const direct = asString(messageObject.LatestManifestId); + if (direct) return direct; + const tagged = findManifestIdInTags(messageObject.Tags); + if (tagged) return tagged; + } + } + + const raw = asObject(results.raw); + const rawMessages = Array.isArray(raw?.Messages) ? raw.Messages : []; + for (const message of rawMessages) { + const messageObject = asObject(message); + if (!messageObject) continue; + const tagged = findManifestIdInTags(messageObject.Tags); + if (tagged) return tagged; + } + + const json = asObject(results.json); + const body = json?.body; + const parsedBody = + typeof body === "string" ? asObject(JSON.parse(body)) : asObject(body); + const jsonMessages = Array.isArray(parsedBody?.Messages) ? parsedBody.Messages : []; + for (const message of jsonMessages) { + const messageObject = asObject(message); + if (!messageObject) continue; + const tagged = findManifestIdInTags(messageObject.Tags); + if (tagged) return tagged; + } + + return null; +}; + +const getLatestManifestFromAo = async (): Promise => { + try { + const now = Date.now(); + if (manifestIdCache && manifestIdCache.expiresAt > now) { + return manifestIdCache.manifestId; + } + + const url = `${AO_URL}/${AO_PROCESS_ID}~process@1.0/compute?Action=Get&require-codec=application/json&accept-bundle=true`; + const response = await fetch(url, { cache: "no-store" }); + if (!response.ok) return null; + + const payload: unknown = await response.json(); + const manifestId = extractManifestId(payload); + if (!manifestId) return null; + + manifestIdCache = { + expiresAt: now + MANIFEST_ID_CACHE_TTL_MS, + manifestId + }; + return manifestId; + } catch { + return null; + } +}; + export const loadManifest = async (): Promise => { - const response = await fetch(arweaveUrl(MANIFEST_TX_ID)); + const manifestTxId = (await getLatestManifestFromAo()) ?? MANIFEST_TX_ID; + const response = await fetch(arweaveUrl(manifestTxId)); if (!response.ok) { throw new Error(`Failed to load manifest (${response.status})`); }