From 9c69f7f8306351206bd83c7d4923776cff216dd1 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Thu, 23 Apr 2026 17:27:28 -0600 Subject: [PATCH] fix(tangle-cloud): format pinned metadata host changes --- .../src/blueprintApps/manifest.ts | 18 +- .../blueprintApps/BlueprintHostCard.tsx | 14 +- .../src/pages/blueprints/create/page.tsx | 4 +- .../src/pages/blueprints/manage/page.tsx | 17 +- .../src/blueprintApps/authoring.ts | 322 +++++++++--------- .../data/graphql/useBlueprintManagement.ts | 20 +- .../src/data/graphql/useBlueprints.ts | 12 +- 7 files changed, 214 insertions(+), 193 deletions(-) diff --git a/apps/tangle-cloud/src/blueprintApps/manifest.ts b/apps/tangle-cloud/src/blueprintApps/manifest.ts index f49839e46..31700bae8 100644 --- a/apps/tangle-cloud/src/blueprintApps/manifest.ts +++ b/apps/tangle-cloud/src/blueprintApps/manifest.ts @@ -198,11 +198,15 @@ function parseExternalApp( const reasons: string[] = []; if (requestedMode === 'iframe') { - reasons.push('Iframe embedding is disabled by policy; verified apps must open in a new tab.'); + reasons.push( + 'Iframe embedding is disabled by policy; verified apps must open in a new tab.', + ); } if (!options.publisherVerified) { - reasons.push('Publisher is not verified for advanced external app handoff.'); + reasons.push( + 'Publisher is not verified for advanced external app handoff.', + ); } if (!options.metadataVerified) { @@ -288,7 +292,8 @@ export function buildBlueprintManifestFromMetadata( ? 'verified' : getPublisherVerificationForNamespace(publisherNamespace), }; - const metadataVerified = blueprint.metadataVerification?.status === 'verified'; + const metadataVerified = + blueprint.metadataVerification?.status === 'verified'; const publisherVerified = publisher.verification === 'verified'; const slugPolicy = canPublisherClaimSlug(normalizedRequestedSlug, publisher) ? 'publisher-scoped' @@ -313,9 +318,12 @@ export function buildBlueprintManifestFromMetadata( }; const allowDeclarativeTier = - manifestRoot !== null && blueprint.metadataVerification?.productionReady === true; + manifestRoot !== null && + blueprint.metadataVerification?.productionReady === true; const trustedExternalApp = - manifest.externalApp?.trust === 'trusted' ? manifest.externalApp : undefined; + manifest.externalApp?.trust === 'trusted' + ? manifest.externalApp + : undefined; const entry: BlueprintAppEntry = { slug: normalizedRequestedSlug, diff --git a/apps/tangle-cloud/src/components/blueprintApps/BlueprintHostCard.tsx b/apps/tangle-cloud/src/components/blueprintApps/BlueprintHostCard.tsx index a80203708..cf9c0d245 100644 --- a/apps/tangle-cloud/src/components/blueprintApps/BlueprintHostCard.tsx +++ b/apps/tangle-cloud/src/components/blueprintApps/BlueprintHostCard.tsx @@ -93,8 +93,8 @@ const BlueprintHostCard = ({ blueprint, serviceId }: Props) => { value={ blueprint.metadataVerification?.status === 'verified' ? 'Verified owner attestation' - : blueprint.metadataVerification?.reason ?? - 'Unverified metadata' + : (blueprint.metadataVerification?.reason ?? + 'Unverified metadata') } /> @@ -158,10 +158,7 @@ const BlueprintHostCard = ({ blueprint, serviceId }: Props) => { )} {blueprintUi.theme.accentColor && ( - + )} {blueprintUi.theme.secondaryColor && ( { {action.fields && action.fields.length > 0 && ( {action.fields - .map((field) => - `${field.label} (${field.input}${field.required ? ', required' : ''})`, + .map( + (field) => + `${field.label} (${field.input}${field.required ? ', required' : ''})`, ) .join(' • ')} diff --git a/apps/tangle-cloud/src/pages/blueprints/create/page.tsx b/apps/tangle-cloud/src/pages/blueprints/create/page.tsx index ac0f0c3ae..25273aaca 100644 --- a/apps/tangle-cloud/src/pages/blueprints/create/page.tsx +++ b/apps/tangle-cloud/src/pages/blueprints/create/page.tsx @@ -877,8 +877,8 @@ const BasicInfoStep: FC = ({ /> Publish the JSON preview below at this URI so cloud.tangle.tools can - resolve your hosted blueprint surfaces and shared runtime metadata. - New SDK blueprints ship the same contract shape in + resolve your hosted blueprint surfaces and shared runtime metadata. New + SDK blueprints ship the same contract shape in `metadata/blueprint-metadata.json`. {requiresIpfsForBlueprintMetadata() ? ' Production hosting only accepts ipfs:// metadata URIs.' diff --git a/apps/tangle-cloud/src/pages/blueprints/manage/page.tsx b/apps/tangle-cloud/src/pages/blueprints/manage/page.tsx index 0abbe3b73..dc3be055e 100644 --- a/apps/tangle-cloud/src/pages/blueprints/manage/page.tsx +++ b/apps/tangle-cloud/src/pages/blueprints/manage/page.tsx @@ -170,17 +170,21 @@ const fetchBlueprintMetadataPreview = async ( controller.abort(); } - const response = await fetch(resolveBlueprintMetadataFetchUrl(metadataUri), { - signal: controller.signal, - cache: 'no-store', - }); + const response = await fetch( + resolveBlueprintMetadataFetchUrl(metadataUri), + { + signal: controller.signal, + cache: 'no-store', + }, + ); if (!response.ok) { throw new Error(`Failed to fetch metadata: ${response.status}`); } const metadataText = await response.text(); - const { parsed, rawMetadata } = parseBlueprintMetadataJsonText(metadataText); + const { parsed, rawMetadata } = + parseBlueprintMetadataJsonText(metadataText); const metadataJson = rawMetadata; if (metadataJson === null) { throw new Error('Metadata payload must be a JSON object'); @@ -195,7 +199,8 @@ const fetchBlueprintMetadataPreview = async ( codeRepository: parsed.codeUrl ?? undefined, docs: metadataJson !== null - ? readString(metadataJson.docs) ?? readString(metadataJson.documentation) + ? (readString(metadataJson.docs) ?? + readString(metadataJson.documentation)) : undefined, metadataHash: computeBlueprintMetadataPayloadHash(metadataJson), }; diff --git a/libs/tangle-shared-ui/src/blueprintApps/authoring.ts b/libs/tangle-shared-ui/src/blueprintApps/authoring.ts index 130482507..e04be360c 100644 --- a/libs/tangle-shared-ui/src/blueprintApps/authoring.ts +++ b/libs/tangle-shared-ui/src/blueprintApps/authoring.ts @@ -142,8 +142,7 @@ const MODULE_SLOT_VALUES = new Set([ 'resources', ]); -const COLOR_PATTERN = - /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i; +const COLOR_PATTERN = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i; const MAX_METADATA_BYTES = 64 * 1024; const MAX_STRING_LENGTH = 240; @@ -190,14 +189,6 @@ const readTrimmedString = ( return trimmed.slice(0, maxLength); }; -const readNullableString = ( - value: unknown, - maxLength = MAX_STRING_LENGTH, -): string | null => { - const trimmed = readTrimmedString(value, maxLength); - return trimmed ?? null; -}; - const readStringArray = ( value: unknown, maxItems = MAX_STRING_LIST_COUNT, @@ -341,124 +332,118 @@ const parseTheme = (value: unknown): BlueprintUiTheme | undefined => { return Object.keys(theme).length > 0 ? theme : undefined; }; -const parseOverviewCards = (value: unknown): BlueprintUiOverviewCard[] | undefined => { +const parseOverviewCards = ( + value: unknown, +): BlueprintUiOverviewCard[] | undefined => { if (!Array.isArray(value)) { return undefined; } - const cards = value - .slice(0, MAX_CARD_COUNT) - .flatMap((entry, index) => { - if (!isRecord(entry)) { - return []; - } - - const title = readTrimmedString(entry.title, 80); - const kind = readTrimmedString(entry.kind, 32)?.toLowerCase(); - if (!title || !kind || !CARD_KIND_VALUES.has(kind as BlueprintUiCardKind)) { - return []; - } - - const tone = readTrimmedString(entry.tone, 24)?.toLowerCase(); - const links = Array.isArray(entry.links) - ? entry.links - .slice(0, MAX_CARD_LINK_COUNT) - .flatMap((link) => { - if (!isRecord(link)) { - return []; - } - - const label = readTrimmedString(link.label, 60); - const href = readTrimmedString(link.href, 2_048); - if (!label || !isValidHttpsUrl(href)) { - return []; - } + const cards = value.slice(0, MAX_CARD_COUNT).flatMap((entry, index) => { + if (!isRecord(entry)) { + return []; + } + + const title = readTrimmedString(entry.title, 80); + const kind = readTrimmedString(entry.kind, 32)?.toLowerCase(); + if (!title || !kind || !CARD_KIND_VALUES.has(kind as BlueprintUiCardKind)) { + return []; + } + + const tone = readTrimmedString(entry.tone, 24)?.toLowerCase(); + const links = Array.isArray(entry.links) + ? entry.links.slice(0, MAX_CARD_LINK_COUNT).flatMap((link) => { + if (!isRecord(link)) { + return []; + } - return [{ label, href }]; - }) - : undefined; - const items = readStringArray(entry.items, MAX_CARD_ITEM_COUNT, 120); + const label = readTrimmedString(link.label, 60); + const href = readTrimmedString(link.href, 2_048); + if (!label || !isValidHttpsUrl(href)) { + return []; + } - return [ - { - id: - sanitizeBlueprintSlug(readTrimmedString(entry.id, 60) ?? '') || - `card-${index + 1}`, - kind: kind as BlueprintUiCardKind, - title, - description: readTrimmedString(entry.description, 240), - value: readTrimmedString(entry.value, 80), - tone: CARD_TONE_VALUES.has(tone as BlueprintUiCardTone) - ? (tone as BlueprintUiCardTone) - : undefined, - ...(links && links.length > 0 ? { links } : {}), - ...(items.length > 0 ? { items } : {}), - } satisfies BlueprintUiOverviewCard, - ]; - }); + return [{ label, href }]; + }) + : undefined; + const items = readStringArray(entry.items, MAX_CARD_ITEM_COUNT, 120); + + return [ + { + id: + sanitizeBlueprintSlug(readTrimmedString(entry.id, 60) ?? '') || + `card-${index + 1}`, + kind: kind as BlueprintUiCardKind, + title, + description: readTrimmedString(entry.description, 240), + value: readTrimmedString(entry.value, 80), + tone: CARD_TONE_VALUES.has(tone as BlueprintUiCardTone) + ? (tone as BlueprintUiCardTone) + : undefined, + ...(links && links.length > 0 ? { links } : {}), + ...(items.length > 0 ? { items } : {}), + } satisfies BlueprintUiOverviewCard, + ]; + }); return cards.length > 0 ? cards : undefined; }; -const parseActionFields = (value: unknown): BlueprintUiActionField[] | undefined => { +const parseActionFields = ( + value: unknown, +): BlueprintUiActionField[] | undefined => { if (!Array.isArray(value)) { return undefined; } - const fields = value - .slice(0, MAX_ACTION_FIELD_COUNT) - .flatMap((entry) => { - if (!isRecord(entry)) { - return []; - } - - const key = - sanitizeBlueprintSlug(readTrimmedString(entry.key, 60) ?? '').replace( - /-/g, - '_', - ); - const label = readTrimmedString(entry.label, 80); - const input = readTrimmedString(entry.input, 24)?.toLowerCase(); - - if ( - !key || - !label || - !input || - !ACTION_FIELD_INPUT_VALUES.has(input as BlueprintUiActionFieldInput) - ) { - return []; - } - - const options = Array.isArray(entry.options) - ? entry.options - .slice(0, MAX_OPTION_COUNT) - .flatMap((option) => { - if (!isRecord(option)) { - return []; - } - - const optionLabel = readTrimmedString(option.label, 60); - const optionValue = readTrimmedString(option.value, 60); - if (!optionLabel || !optionValue) { - return []; - } + const fields = value.slice(0, MAX_ACTION_FIELD_COUNT).flatMap((entry) => { + if (!isRecord(entry)) { + return []; + } + + const key = sanitizeBlueprintSlug( + readTrimmedString(entry.key, 60) ?? '', + ).replace(/-/g, '_'); + const label = readTrimmedString(entry.label, 80); + const input = readTrimmedString(entry.input, 24)?.toLowerCase(); + + if ( + !key || + !label || + !input || + !ACTION_FIELD_INPUT_VALUES.has(input as BlueprintUiActionFieldInput) + ) { + return []; + } + + const options = Array.isArray(entry.options) + ? entry.options.slice(0, MAX_OPTION_COUNT).flatMap((option) => { + if (!isRecord(option)) { + return []; + } - return [{ label: optionLabel, value: optionValue }]; - }) - : undefined; + const optionLabel = readTrimmedString(option.label, 60); + const optionValue = readTrimmedString(option.value, 60); + if (!optionLabel || !optionValue) { + return []; + } - return [ - { - key, - label, - input: input as BlueprintUiActionFieldInput, - required: entry.required === true, - placeholder: readTrimmedString(entry.placeholder, 120), - helpText: readTrimmedString(entry.helpText, 160), - ...(options && options.length > 0 ? { options } : {}), - } satisfies BlueprintUiActionField, - ]; - }); + return [{ label: optionLabel, value: optionValue }]; + }) + : undefined; + + return [ + { + key, + label, + input: input as BlueprintUiActionFieldInput, + required: entry.required === true, + placeholder: readTrimmedString(entry.placeholder, 120), + helpText: readTrimmedString(entry.helpText, 160), + ...(options && options.length > 0 ? { options } : {}), + } satisfies BlueprintUiActionField, + ]; + }); return fields.length > 0 ? fields : undefined; }; @@ -468,44 +453,42 @@ const parseActions = (value: unknown): BlueprintUiAction[] | undefined => { return undefined; } - const actions = value - .slice(0, MAX_ACTION_COUNT) - .flatMap((entry, index) => { - if (!isRecord(entry)) { - return []; - } - - const label = readTrimmedString(entry.label, 80); - const target = readTrimmedString(entry.target, 24)?.toLowerCase(); - if ( - !label || - !target || - !ACTION_TARGET_VALUES.has(target as BlueprintUiActionTarget) - ) { - return []; - } - - const href = readTrimmedString(entry.href, 2_048); - const fields = parseActionFields(entry.fields); - - if (!fields && !isValidHttpsUrl(href)) { - return []; - } - - return [ - { - id: - sanitizeBlueprintSlug(readTrimmedString(entry.id, 60) ?? '') || - `action-${index + 1}`, - label, - description: readTrimmedString(entry.description, 240), - target: target as BlueprintUiActionTarget, - submitLabel: readTrimmedString(entry.submitLabel, 60), - ...(isValidHttpsUrl(href) ? { href } : {}), - ...(fields ? { fields } : {}), - } satisfies BlueprintUiAction, - ]; - }); + const actions = value.slice(0, MAX_ACTION_COUNT).flatMap((entry, index) => { + if (!isRecord(entry)) { + return []; + } + + const label = readTrimmedString(entry.label, 80); + const target = readTrimmedString(entry.target, 24)?.toLowerCase(); + if ( + !label || + !target || + !ACTION_TARGET_VALUES.has(target as BlueprintUiActionTarget) + ) { + return []; + } + + const href = readTrimmedString(entry.href, 2_048); + const fields = parseActionFields(entry.fields); + + if (!fields && !isValidHttpsUrl(href)) { + return []; + } + + return [ + { + id: + sanitizeBlueprintSlug(readTrimmedString(entry.id, 60) ?? '') || + `action-${index + 1}`, + label, + description: readTrimmedString(entry.description, 240), + target: target as BlueprintUiActionTarget, + submitLabel: readTrimmedString(entry.submitLabel, 60), + ...(isValidHttpsUrl(href) ? { href } : {}), + ...(fields ? { fields } : {}), + } satisfies BlueprintUiAction, + ]; + }); return actions.length > 0 ? actions : undefined; }; @@ -547,10 +530,9 @@ const parseResourceViews = ( return []; } - const key = - sanitizeBlueprintSlug( - readTrimmedString(column.key, 60) ?? '', - ).replace(/-/g, '_'); + const key = sanitizeBlueprintSlug( + readTrimmedString(column.key, 60) ?? '', + ).replace(/-/g, '_'); const label = readTrimmedString(column.label, 60); if (!key || !label) { @@ -588,7 +570,9 @@ const parseResourceViews = ( return views.length > 0 ? views : undefined; }; -const parseModules = (value: unknown): BlueprintUiModuleBinding[] | undefined => { +const parseModules = ( + value: unknown, +): BlueprintUiModuleBinding[] | undefined => { if (!Array.isArray(value)) { return undefined; } @@ -616,8 +600,16 @@ const parseModules = (value: unknown): BlueprintUiModuleBinding[] | undefined => module: moduleKey as BlueprintUiModuleKey, slot: slot as BlueprintUiModuleSlot, title: readTrimmedString(entry.title, 80), - metricKeys: readStringArray(entry.metricKeys, MAX_STRING_LIST_COUNT, 48), - eventKinds: readStringArray(entry.eventKinds, MAX_STRING_LIST_COUNT, 48), + metricKeys: readStringArray( + entry.metricKeys, + MAX_STRING_LIST_COUNT, + 48, + ), + eventKinds: readStringArray( + entry.eventKinds, + MAX_STRING_LIST_COUNT, + 48, + ), } satisfies BlueprintUiModuleBinding, ]; }) @@ -699,7 +691,8 @@ const toCanonicalMetadataJson = (value: Record): string => export const computeBlueprintMetadataPayloadHash = ( value: Record, -): Hex => keccak256(toHex(new TextEncoder().encode(toCanonicalMetadataJson(value)))); +): Hex => + keccak256(toHex(new TextEncoder().encode(toCanonicalMetadataJson(value)))); const parseBlueprintMetadataAttestation = ( value: unknown, @@ -825,7 +818,10 @@ export const verifyBlueprintMetadataIntegrity = async ({ stripMetadataAttestation(rawMetadata), ); - if (metadataHash && metadataHash.toLowerCase() !== payloadHash.toLowerCase()) { + if ( + metadataHash && + metadataHash.toLowerCase() !== payloadHash.toLowerCase() + ) { return buildDefaultMetadataVerification({ metadataUri, status: 'invalid', @@ -1073,7 +1069,9 @@ export const parseBlueprintMetadataJsonText = ( }; }; -export const resolveBlueprintMetadataFetchUrl = (metadataUri: string): string => { +export const resolveBlueprintMetadataFetchUrl = ( + metadataUri: string, +): string => { if (!metadataUri.startsWith('ipfs://')) { return metadataUri; } @@ -1140,7 +1138,9 @@ export const buildBlueprintUiMetadataDocument = ({ metrics: draft.surfaces.includes('metrics'), permissions: draft.surfaces.includes('permissions'), }, - externalApp: isValidHttpsUrl(readTrimmedString(draft.externalAppUrl, 2_048)) + externalApp: isValidHttpsUrl( + readTrimmedString(draft.externalAppUrl, 2_048), + ) ? { url: draft.externalAppUrl.trim(), mode: 'link', diff --git a/libs/tangle-shared-ui/src/data/graphql/useBlueprintManagement.ts b/libs/tangle-shared-ui/src/data/graphql/useBlueprintManagement.ts index f05fcdc00..0996e38b5 100644 --- a/libs/tangle-shared-ui/src/data/graphql/useBlueprintManagement.ts +++ b/libs/tangle-shared-ui/src/data/graphql/useBlueprintManagement.ts @@ -217,8 +217,8 @@ const fetchBlueprintsByOwner = async ( const response = await fetch( resolveBlueprintMetadataFetchUrl(bp.metadataUri), { - signal: controller.signal, - cache: 'no-store', + signal: controller.signal, + cache: 'no-store', }, ).finally(() => { clearTimeout(timeoutId); @@ -244,8 +244,8 @@ const fetchBlueprintsByOwner = async ( codeRepository: parsed.codeUrl ?? undefined, docs: metadataJson !== null - ? readString(metadataJson.docs) ?? - readString(metadataJson.documentation) + ? (readString(metadataJson.docs) ?? + readString(metadataJson.documentation)) : undefined, }; } catch (error) { @@ -409,7 +409,11 @@ export const useUpdateBlueprintTx = () => { address: contracts.tangle, abi: TANGLE_ABI, functionName: 'updateBlueprint' as const, - args: [params.blueprintId, params.metadataUri, params.metadataHash] as const, + args: [ + params.blueprintId, + params.metadataUri, + params.metadataHash, + ] as const, }; }, { @@ -424,7 +428,11 @@ export const useUpdateBlueprintTx = () => { metadataUri: string, metadataHash: `0x${string}`, ): Promise => { - const result = await execute?.({ blueprintId, metadataUri, metadataHash }); + const result = await execute?.({ + blueprintId, + metadataUri, + metadataHash, + }); return result?.hash ?? null; }, [execute], diff --git a/libs/tangle-shared-ui/src/data/graphql/useBlueprints.ts b/libs/tangle-shared-ui/src/data/graphql/useBlueprints.ts index 2317a7e42..b22a770d8 100644 --- a/libs/tangle-shared-ui/src/data/graphql/useBlueprints.ts +++ b/libs/tangle-shared-ui/src/data/graphql/useBlueprints.ts @@ -96,8 +96,7 @@ const fetchBlueprintMetadata = async ({ metadataHash?: `0x${string}` | null; blueprintId?: bigint; owner?: Address; -}, -): Promise<{ +}): Promise<{ name: string; description: string; author: string; @@ -130,9 +129,12 @@ const fetchBlueprintMetadata = async ({ } try { - const response = await fetch(resolveBlueprintMetadataFetchUrl(metadataUri), { - signal: AbortSignal.timeout(5000), - }); + const response = await fetch( + resolveBlueprintMetadataFetchUrl(metadataUri), + { + signal: AbortSignal.timeout(5000), + }, + ); if (!response.ok) { throw new Error(`Failed to fetch metadata: ${response.status}`); }