Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions apps/tangle-cloud/src/blueprintApps/authoring.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,36 @@ describe('blueprint ui authoring', () => {
expect(document).toEqual({
name: 'Trading Agent',
description: 'Automates strategy execution.',
category: 'Trading',
author: 'Tangle',
codeRepository: 'https://github.com/tangle/trading-blueprint',
category: 'Trading',
image: null,
logo: null,
website: 'https://tangle.tools',
codeRepository: 'https://github.com/tangle/trading-blueprint',
blueprintUi: {
slug: 'trading',
displayName: 'Trading Agent',
description: 'Automates strategy execution.',
requestedSlug: 'trading',
publisher: {
name: 'Tangle',
namespace: 'tangle',
verified: false,
},
displayName: 'Trading Agent',
tagline: 'Trading blueprint on Tangle',
description: 'Automates strategy execution.',
surfaces: DEFAULT_BLUEPRINT_UI_DRAFT.surfaces,
resources: {
serviceNoun: 'service',
resourceNoun: 'bot',
resourceRoute: 'bots',
serviceLabel: 'Service',
itemLabel: 'bot',
itemRoute: 'bots',
},
surfaces: {
genericOverview: true,
serviceExplorer: true,
serviceConsole: false,
actionsPanel: true,
resources: true,
chat: false,
vaults: false,
metrics: true,
permissions: false,
},
},
});
Expand All @@ -61,8 +74,8 @@ describe('blueprint ui authoring', () => {
...DEFAULT_BLUEPRINT_UI_DRAFT,
requestedSlug: 'trading',
publisherNamespace: 'tangle',
serviceNoun: 'service',
resourceNoun: 'resource',
serviceNoun: 'Service',
resourceNoun: 'Resource',
surfaces: ['chat', 'resources'],
});
});
Expand Down
155 changes: 22 additions & 133 deletions apps/tangle-cloud/src/blueprintApps/authoring.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
import type {
BlueprintExternalAppMode,
BlueprintResourceRoute,
BlueprintUiManifest,
BlueprintUiSurface,
} from './types';
import { isTrustedExternalAppHost } from './resolver';
import {
buildBlueprintUiMetadataDocument as buildSharedBlueprintUiMetadataDocument,
DEFAULT_BLUEPRINT_UI_DRAFT as SHARED_DEFAULT_BLUEPRINT_UI_DRAFT,
} from '@tangle-network/tangle-shared-ui/blueprintApps/authoring';
import type { BlueprintUiAuthoringDraft } from '@tangle-network/tangle-shared-ui/blueprintApps/types';

export type BlueprintUiAuthoringDraft = {
requestedSlug: string;
publisherNamespace: string;
serviceNoun: string;
resourceNoun: string;
resourceRoute: BlueprintResourceRoute;
surfaces: BlueprintUiSurface[];
externalAppUrl: string;
externalAppMode: BlueprintExternalAppMode;
};
export type { BlueprintUiAuthoringDraft };

export type BuildBlueprintUiMetadataDocumentParams = {
name: string;
Expand All @@ -28,24 +17,8 @@ export type BuildBlueprintUiMetadataDocumentParams = {
draft: BlueprintUiAuthoringDraft;
};

export const DEFAULT_BLUEPRINT_UI_SURFACES: BlueprintUiSurface[] = [
'generic-overview',
'service-explorer',
'service-console',
'actions-panel',
'resources',
'permissions',
];

export const DEFAULT_BLUEPRINT_UI_DRAFT: BlueprintUiAuthoringDraft = {
requestedSlug: '',
publisherNamespace: '',
serviceNoun: 'service',
resourceNoun: 'resource',
resourceRoute: 'custom',
surfaces: DEFAULT_BLUEPRINT_UI_SURFACES,
externalAppUrl: '',
externalAppMode: 'link',
export const DEFAULT_BLUEPRINT_UI_DRAFT = {
...SHARED_DEFAULT_BLUEPRINT_UI_DRAFT,
};

const trim = (value: string): string => value.trim();
Expand All @@ -55,79 +28,17 @@ export const sanitizeBlueprintUiDraft = (
): BlueprintUiAuthoringDraft => ({
requestedSlug: trim(draft.requestedSlug),
publisherNamespace: trim(draft.publisherNamespace),
serviceNoun: trim(draft.serviceNoun) || 'service',
resourceNoun: trim(draft.resourceNoun) || 'resource',
serviceNoun: trim(draft.serviceNoun) || 'Service',
resourceNoun: trim(draft.resourceNoun) || 'Resource',
resourceRoute: draft.resourceRoute,
surfaces:
draft.surfaces.length > 0
? Array.from(new Set(draft.surfaces))
: DEFAULT_BLUEPRINT_UI_SURFACES,
: DEFAULT_BLUEPRINT_UI_DRAFT.surfaces,
externalAppUrl: trim(draft.externalAppUrl),
externalAppMode: draft.externalAppMode,
});

const buildTagline = (name: string, category?: string): string => {
const trimmedCategory = category?.trim();
if (trimmedCategory) {
return `${trimmedCategory} blueprint on Tangle`;
}

return `${name.trim() || 'Blueprint'} on Tangle`;
};

const buildManifest = ({
name,
description,
category,
draft,
}: {
name: string;
description: string;
category?: string;
draft: BlueprintUiAuthoringDraft;
}): BlueprintUiManifest => {
const sanitizedDraft = sanitizeBlueprintUiDraft(draft);
let externalApp: BlueprintUiManifest['externalApp'];

if (sanitizedDraft.externalAppUrl) {
try {
const host = new URL(sanitizedDraft.externalAppUrl).hostname;
const trust = isTrustedExternalAppHost(host) ? 'trusted' : 'restricted';

externalApp = {
url: sanitizedDraft.externalAppUrl,
mode: sanitizedDraft.externalAppMode,
label: 'Open blueprint app',
host,
trust,
...(trust === 'restricted'
? {
reason:
'External app host is not on the trusted Tangle allowlist yet.',
}
: {}),
};
} catch {
externalApp = undefined;
}
}

return {
displayName: trim(name) || 'Untitled blueprint',
tagline: buildTagline(name, category),
description:
trim(description) ||
'Blueprint metadata published for the shared Tangle Cloud host surface.',
surfaces: sanitizedDraft.surfaces,
resources: {
serviceNoun: sanitizedDraft.serviceNoun,
resourceNoun: sanitizedDraft.resourceNoun,
resourceRoute: sanitizedDraft.resourceRoute,
},
...(externalApp ? { externalApp } : {}),
};
};

export const buildBlueprintUiMetadataDocument = ({
name,
description,
Expand All @@ -137,36 +48,14 @@ export const buildBlueprintUiMetadataDocument = ({
website,
author,
draft,
}: BuildBlueprintUiMetadataDocumentParams): Record<string, unknown> => {
const sanitizedDraft = sanitizeBlueprintUiDraft(draft);

return {
...(trim(name) ? { name: trim(name) } : {}),
...(trim(description) ? { description: trim(description) } : {}),
...(trim(category ?? '') ? { category: trim(category ?? '') } : {}),
...(trim(author ?? '') ? { author: trim(author ?? '') } : {}),
...(trim(codeRepository ?? '')
? { codeRepository: trim(codeRepository ?? '') }
: {}),
...(trim(logo ?? '') ? { logo: trim(logo ?? '') } : {}),
...(trim(website ?? '') ? { website: trim(website ?? '') } : {}),
blueprintUi: {
...(sanitizedDraft.requestedSlug
? { slug: sanitizedDraft.requestedSlug }
: {}),
...(sanitizedDraft.publisherNamespace
? {
publisher: {
namespace: sanitizedDraft.publisherNamespace,
},
}
: {}),
...buildManifest({
name,
description,
category,
draft: sanitizedDraft,
}),
},
};
};
}: BuildBlueprintUiMetadataDocumentParams): Record<string, unknown> =>
buildSharedBlueprintUiMetadataDocument({
name,
description,
category: category ?? '',
codeRepository: codeRepository ?? '',
logo: logo ?? '',
website: website ?? '',
author: author ?? '',
draft: sanitizeBlueprintUiDraft(draft),
});
99 changes: 99 additions & 0 deletions apps/tangle-cloud/src/blueprintApps/hostContract.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
parseBlueprintMetadataDocument,
parseBlueprintMetadataJsonText,
} from '@tangle-network/tangle-shared-ui/blueprintApps/authoring';

describe('shared blueprint host contract', () => {
it('parses rich declarative tier 2 metadata', () => {
const parsed = parseBlueprintMetadataDocument({
name: 'Atlas',
description: 'Operator coordination workspace',
author: 'Northstar',
blueprintUi: {
displayName: 'Atlas Workspace',
description: 'Shared hosted app for Atlas operators.',
requestedSlug: 'atlas',
publisher: {
name: 'Northstar',
namespace: 'northstar',
verified: true,
},
resources: {
serviceLabel: 'Workspace',
itemLabel: 'Run',
itemRoute: 'runs',
},
surfaces: ['generic-overview', 'resources', 'metrics'],
theme: {
accentColor: '#0F766E',
badgeLabel: 'Curated',
icon: 'bot',
},
overviewCards: [
{
id: 'uptime',
kind: 'stat',
title: 'Operator Uptime',
value: '99.9%',
},
],
actions: [
{
id: 'provision',
label: 'Provision workspace',
target: 'service',
fields: [
{
key: 'workspace_name',
label: 'Workspace name',
input: 'text',
required: true,
},
],
},
],
resourceViews: [
{
id: 'runs',
title: 'Run ledger',
kind: 'table',
target: 'resource',
columns: [
{ key: 'status', label: 'Status', emphasis: true },
{ key: 'updated_at', label: 'Updated' },
],
},
],
modules: [
{
module: 'metrics-overview',
slot: 'overview',
metricKeys: ['success_rate', 'latency_p95'],
},
],
},
});

expect(parsed.blueprintUi?.tier).toBe('declarative');
expect(parsed.blueprintUi?.theme?.accentColor).toBe('#0F766E');
expect(parsed.blueprintUi?.overviewCards?.[0]?.title).toBe(
'Operator Uptime',
);
expect(parsed.blueprintUi?.actions?.[0]?.fields?.[0]?.key).toBe(
'workspace_name',
);
expect(parsed.blueprintUi?.resourceViews?.[0]?.columns).toHaveLength(2);
expect(parsed.blueprintUi?.modules?.[0]?.module).toBe('metrics-overview');
});

it('rejects oversized metadata payloads', () => {
const oversized = JSON.stringify({
name: 'Large Blueprint',
description: 'x'.repeat(70_000),
});

expect(() => parseBlueprintMetadataJsonText(oversized)).toThrow(
'Blueprint metadata exceeded',
);
});
});
Loading
Loading