@eqtylab/explorer 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/.env.api +6 -0
  2. package/.env.iframe +2 -0
  3. package/.env.manifest +2 -0
  4. package/.envrc +1 -0
  5. package/.eslintrc.cjs +17 -0
  6. package/.github/workflows/ci.yml +46 -0
  7. package/.github/workflows/publish.yml +44 -0
  8. package/.husky/pre-push +2 -0
  9. package/.prettierignore +2 -0
  10. package/.prettierrc.json +13 -0
  11. package/LICENSE +201 -0
  12. package/README.md +84 -0
  13. package/flake.lock +61 -0
  14. package/flake.nix +23 -0
  15. package/index.html +13 -0
  16. package/package.json +93 -0
  17. package/postcss.config.js +5 -0
  18. package/public/logos/eci.svg +23 -0
  19. package/public/logos/eqty.svg +3 -0
  20. package/public/manifests/B-200-merged.json +773 -0
  21. package/public/manifests/amd-sev.json +426 -0
  22. package/public/manifests/anchors.json +609 -0
  23. package/public/manifests/association-no-metadata.json +1393 -0
  24. package/public/manifests/association.json +1402 -0
  25. package/public/manifests/eqty-docker.json +562 -0
  26. package/public/manifests/governance.json +1324 -0
  27. package/public/manifests/intel-tdx-with-nvidia.json +441 -0
  28. package/public/manifests/intel-tdx.json +404 -0
  29. package/public/manifests/iroh-collections.json +1285 -0
  30. package/public/manifests/model-signing.json +1536 -0
  31. package/public/manifests/multiple-registrations.json +129 -0
  32. package/public/manifests/no-vcs.json +114 -0
  33. package/public/manifests/storage.json +294 -0
  34. package/public/manifests/sub-graphs.json +1 -0
  35. package/src/animations/variants.ts +52 -0
  36. package/src/app/apiClient/integrityService/fetchAttributes.ts +29 -0
  37. package/src/app/apiClient/integrityService/fetchManifest.ts +28 -0
  38. package/src/app/apiClient/integrityService/types.ts +12 -0
  39. package/src/app/components/integrityService/IntegrityService.tsx +105 -0
  40. package/src/app/components/integrityService/components/AuthButton.tsx +26 -0
  41. package/src/app/components/integrityService/components/FiltersModal.tsx +214 -0
  42. package/src/app/components/integrityService/components/FiltersToggle.tsx +28 -0
  43. package/src/app/components/manifestViewer/ManifestViewer.tsx +90 -0
  44. package/src/app/components/manifestViewer/components/ManifestButtons.tsx +215 -0
  45. package/src/app/config.json +23 -0
  46. package/src/app/config.ts +28 -0
  47. package/src/app/pages/index.tsx +34 -0
  48. package/src/app/stores/manifestStore.ts +19 -0
  49. package/src/app/utils/appMode.ts +37 -0
  50. package/src/assets/fonts/TWKLausanne-300.woff +0 -0
  51. package/src/assets/fonts/TWKLausanne-300.woff2 +0 -0
  52. package/src/assets/fonts/TWKLausanne-400.woff +0 -0
  53. package/src/assets/fonts/TWKLausanne-400.woff2 +0 -0
  54. package/src/assets/fonts/TWKLausanne-500.woff +0 -0
  55. package/src/assets/fonts/TWKLausanne-500.woff2 +0 -0
  56. package/src/assets/fonts/TWKLausanne-600.woff +0 -0
  57. package/src/assets/fonts/TWKLausanne-600.woff2 +0 -0
  58. package/src/assets/vectors/eqtylab.svg +6 -0
  59. package/src/components/app-header.tsx +61 -0
  60. package/src/components/dark-mode-switch.tsx +16 -0
  61. package/src/components/ui/accordion.tsx +47 -0
  62. package/src/components/ui/certificate-chain.tsx +60 -0
  63. package/src/components/ui/certificate-code.tsx +39 -0
  64. package/src/components/ui/details.tsx +20 -0
  65. package/src/components/ui/display-code.tsx +47 -0
  66. package/src/components/ui/panel-label.tsx +12 -0
  67. package/src/components/ui/verify-state-badge.tsx +32 -0
  68. package/src/explorer-component/LineageExplorer.tsx +89 -0
  69. package/src/explorer-component/components/CustomEdge.tsx +28 -0
  70. package/src/explorer-component/components/LineageFlow.tsx +220 -0
  71. package/src/explorer-component/components/NodeIconLoader.tsx +26 -0
  72. package/src/explorer-component/components/nodes/ComputationNode.tsx +56 -0
  73. package/src/explorer-component/components/nodes/CustomNode.tsx +112 -0
  74. package/src/explorer-component/components/nodes/index.ts +4 -0
  75. package/src/explorer-component/components/nodes/nodes.tsx +144 -0
  76. package/src/explorer-component/components/nodes/types.ts +167 -0
  77. package/src/explorer-component/components/sidebar/SectionList.tsx +54 -0
  78. package/src/explorer-component/components/sidebar/SectionTitle.tsx +10 -0
  79. package/src/explorer-component/components/sidebar/Sidebar.tsx +70 -0
  80. package/src/explorer-component/components/sidebar/SidebarWrapper.tsx +76 -0
  81. package/src/explorer-component/components/sidebar/sections/CustomSection.tsx +238 -0
  82. package/src/explorer-component/components/sidebar/sections/FileViewer.tsx +229 -0
  83. package/src/explorer-component/components/sidebar/sections/GovernanceSection.tsx +52 -0
  84. package/src/explorer-component/components/sidebar/sections/RegistrationSection.tsx +254 -0
  85. package/src/explorer-component/components/sidebar/sections/SigstoreSection.tsx +37 -0
  86. package/src/explorer-component/components/sidebar/sections/StorageSection.tsx +73 -0
  87. package/src/explorer-component/components/sidebar/sections/custom/IrohCollectionsView.tsx +139 -0
  88. package/src/explorer-component/components/sidebar/sections/registration/VerificationIcon.tsx +108 -0
  89. package/src/explorer-component/components/sidebar/sections/registration/actor/ActorBadge.tsx +62 -0
  90. package/src/explorer-component/components/sidebar/sections/registration/actor/svg/AMDLogo.tsx +40 -0
  91. package/src/explorer-component/components/sidebar/sections/registration/actor/svg/AzureLogo.tsx +65 -0
  92. package/src/explorer-component/components/sidebar/sections/registration/actor/svg/IntelLogo.tsx +24 -0
  93. package/src/explorer-component/components/sidebar/sections/registration/actor/svg/NvidiaLogo.tsx +27 -0
  94. package/src/explorer-component/components/sidebar/sections/registration/verifierClient.ts +13 -0
  95. package/src/explorer-component/icons/AttributionIcon.svg +6 -0
  96. package/src/explorer-component/icons/BenchmarkIcon.svg +6 -0
  97. package/src/explorer-component/icons/BenchmarkResultIcon.svg +6 -0
  98. package/src/explorer-component/icons/CertificateIcon.svg +6 -0
  99. package/src/explorer-component/icons/CodeIcon.svg +6 -0
  100. package/src/explorer-component/icons/ComputationIcon.svg +6 -0
  101. package/src/explorer-component/icons/DataIcon.svg +6 -0
  102. package/src/explorer-component/icons/DatabaseIcon.svg +6 -0
  103. package/src/explorer-component/icons/DatasetIcon.svg +6 -0
  104. package/src/explorer-component/icons/DeltaLakeIcon.svg +7 -0
  105. package/src/explorer-component/icons/DocumentIcon.svg +6 -0
  106. package/src/explorer-component/icons/MediaIcon.svg +6 -0
  107. package/src/explorer-component/icons/ModelIcon.svg +6 -0
  108. package/src/explorer-component/icons/TeamIcon.svg +3 -0
  109. package/src/explorer-component/icons/TokenIcon.svg +6 -0
  110. package/src/explorer-component/icons/UnknownIcon.svg +6 -0
  111. package/src/explorer-component/icons/index.ts +35 -0
  112. package/src/explorer-component/index.ts +5 -0
  113. package/src/explorer-component/integrityTypes/AnchorTypes.ts +31 -0
  114. package/src/explorer-component/integrityTypes/StatementTypes.ts +333 -0
  115. package/src/explorer-component/integrityTypes/VCTypes.ts +85 -0
  116. package/src/explorer-component/integrityTypes/types.ts +3 -0
  117. package/src/explorer-component/utils/config.ts +163 -0
  118. package/src/explorer-component/utils/formatTimestamp.ts +5 -0
  119. package/src/explorer-component/utils/generateGraph.ts +271 -0
  120. package/src/explorer-component/utils/layoutGraph.ts +148 -0
  121. package/src/explorer-component/utils/resolveIrohBlobs.ts +117 -0
  122. package/src/explorer-component/utils/statementHelpers.ts +252 -0
  123. package/src/explorer-component/utils/stripPrefix.ts +4 -0
  124. package/src/hooks/use-mobile.tsx +19 -0
  125. package/src/index.ts +3 -0
  126. package/src/lib/resource-types.ts +31 -0
  127. package/src/lib/use-escape-key.tsx +16 -0
  128. package/src/lib/utils.ts +19 -0
  129. package/src/main.css +82 -0
  130. package/src/main.tsx +39 -0
  131. package/src/v-comp-viewer/components/container-did-details.tsx +53 -0
  132. package/src/v-comp-viewer/components/system-did-details.tsx +356 -0
  133. package/src/v-comp-viewer/useVComp.ts +196 -0
  134. package/src/v-comp-viewer/utils.tsx +824 -0
  135. package/src/vite-env.d.ts +1 -0
  136. package/tsconfig.json +33 -0
  137. package/tsconfig.node.json +10 -0
  138. package/vercel.json +13 -0
  139. package/vite.app.config.ts +48 -0
  140. package/vite.lib.config.ts +61 -0
@@ -0,0 +1,271 @@
1
+ import {
2
+ LineageEdge,
3
+ LineageNode,
4
+ ComputeNodeSidebarData,
5
+ DataNodeSidebarData,
6
+ createEdge,
7
+ createDefaultNode,
8
+ IrohData,
9
+ DataNodeRegistration,
10
+ MetadataLineage,
11
+ StorageLineage,
12
+ Subgraph,
13
+ SubgraphInfo,
14
+ GraphManifest,
15
+ } from "@/explorer-component/components/nodes/types";
16
+ import { resolveIrohCollections } from "./resolveIrohBlobs";
17
+ import { stripCidPrefix } from "./stripPrefix.ts";
18
+
19
+ import { layoutNodes as layoutNodes } from "./layoutGraph.ts";
20
+ import { sortStatements, indexStatements } from "./statementHelpers.ts";
21
+ import {
22
+ Anchor,
23
+ ComputationStatement,
24
+ ComputationStatementFromJSON,
25
+ DataStatement,
26
+ DataStatementFromJSON,
27
+ VcStatement,
28
+ RegistrationTypes,
29
+ GovernanceStatement,
30
+ SigstoreBundle,
31
+ } from "@/explorer-component/integrityTypes/types.ts";
32
+ import { ExplorerConfig } from "./config.ts";
33
+
34
+ /// Iterate over graphs and their statements to create the ReactFlow nodes and edges
35
+ /// Handles the new manifest format with graphs containing statements
36
+ export function generateGraphFromGraphs(
37
+ config: ExplorerConfig,
38
+ manifest: GraphManifest,
39
+ customIcons?: Record<string, string>,
40
+ ): [LineageNode[], LineageEdge[]] {
41
+ const allStatements: Record<string, object> = {};
42
+
43
+ for (const graph of manifest.graphs) {
44
+ for (const statement of graph.statements) {
45
+ const statementId = (statement as any)["@id"];
46
+ if (statementId) {
47
+ const graphInfo: SubgraphInfo = {
48
+ name: graph.name,
49
+ id: graph.id,
50
+ parent: graph.parent,
51
+ };
52
+ (statement as any)["graph"] = graphInfo;
53
+ allStatements[statementId] = statement;
54
+ }
55
+ }
56
+ }
57
+
58
+ // Use the existing function with the flattened statements
59
+ return generateGraphFromStatements(config, allStatements, manifest.blobs, manifest.anchors, customIcons);
60
+ }
61
+
62
+ /// Iterate over all the statements to create the ReactFlow nodes and edges
63
+ /// related info (VCs, metadata, etc) is attached to the ReactFlow node
64
+ export function generateGraphFromStatements(
65
+ config: ExplorerConfig,
66
+ statements: Record<string, object>,
67
+ blobs: Record<string, string>,
68
+ anchors?: Anchor[],
69
+ customIcons?: Record<string, string>,
70
+ ): [LineageNode[], LineageEdge[]] {
71
+ // The Nodes to diplay in the graph
72
+ const nodesMap: Map<string, LineageNode> = new Map();
73
+ // The edges in the graph
74
+ let edgesMap: LineageEdge[] = [];
75
+
76
+ const irohCollections = resolveIrohCollections(blobs);
77
+
78
+ const sortedStatements = sortStatements(statements);
79
+
80
+ // Map of metadata that has already been resolved. Key is metadataSubjectID.
81
+ const { metadataLineage, vcs, actors, storage, governance, sigstoreBundles } = indexStatements(
82
+ sortedStatements,
83
+ blobs,
84
+ );
85
+
86
+ for (const [cid, statement] of sortedStatements) {
87
+ try {
88
+ // @ts-ignore
89
+ switch (statement["@type"]) {
90
+ case RegistrationTypes.Computation: {
91
+ const computeStatement: ComputationStatement = ComputationStatementFromJSON(statement);
92
+
93
+ const [computeNodes, computeEdges] = createComputeNode(
94
+ config,
95
+ computeStatement,
96
+ metadataLineage,
97
+ vcs,
98
+ actors,
99
+ irohCollections,
100
+ anchors,
101
+ customIcons,
102
+ governance,
103
+ (statement as any)["graph"],
104
+ );
105
+ edgesMap = [...edgesMap, ...computeEdges];
106
+ for (const node of computeNodes) {
107
+ if (!nodesMap.has(node.id)) {
108
+ // fail safe to create the data node for inputs or outputs that don't have a data registration statement
109
+ nodesMap.set(node.id, node);
110
+ }
111
+ }
112
+ break;
113
+ }
114
+ case RegistrationTypes.Data: {
115
+ const dataStatement: DataStatement = DataStatementFromJSON(statement);
116
+
117
+ if (nodesMap.has(dataStatement.data)) {
118
+ const registeredByActor = actors.get(dataStatement.registeredBy);
119
+ const vcStatement = vcs.get(dataStatement.id);
120
+ const anchor = anchors?.find((anchor) => anchor.statements.includes(dataStatement.id));
121
+
122
+ const registration: DataNodeRegistration = {
123
+ statement: dataStatement,
124
+ registeredByActor,
125
+ anchors: anchor,
126
+ vcs: vcStatement,
127
+ };
128
+
129
+ const existingNode = nodesMap.get(dataStatement.data);
130
+ const sidebarData = existingNode?.data.sidebarData as DataNodeSidebarData;
131
+ if (sidebarData && "registrations" in sidebarData) {
132
+ // Node already has a DataRegistration statement. Append this one to that list.
133
+ const existingRegistrations = sidebarData.registrations ?? [];
134
+ sidebarData.registrations = [...existingRegistrations, registration];
135
+ }
136
+ } else {
137
+ const irohCollection = irohCollections.get(stripCidPrefix(dataStatement.data)) || null;
138
+ const sourceData = irohCollection ? irohCollection : blobs[stripCidPrefix(dataStatement.data)];
139
+
140
+ const dataNode = createDataNode(
141
+ config,
142
+ dataStatement,
143
+ metadataLineage,
144
+ vcs,
145
+ actors,
146
+ storage,
147
+ sourceData,
148
+ anchors,
149
+ customIcons,
150
+ governance,
151
+ sigstoreBundles,
152
+ (statement as any)["graph"],
153
+ );
154
+ nodesMap.set(dataNode.id, dataNode);
155
+ }
156
+ break;
157
+ }
158
+ default:
159
+ break;
160
+ }
161
+ } catch (error) {
162
+ console.error(`Failed to parse statement for ID ${cid}:`, error);
163
+ }
164
+ }
165
+
166
+ const nodes: LineageNode[] = Array.from(nodesMap.values());
167
+ return layoutNodes(nodes, edgesMap);
168
+ }
169
+
170
+ /// Creates a single data node. If the nodeID already exists, it is updated with this DataStatement data.
171
+ function createDataNode(
172
+ config: ExplorerConfig,
173
+ statement: DataStatement,
174
+ // metadataJsonMap: Map<string, any[]>,
175
+ metadataLineage: Map<string, MetadataLineage[]>,
176
+ vcs: Map<string, VcStatement[]>,
177
+ actors: Map<string, any>,
178
+ storage: Map<string, StorageLineage>,
179
+ sourceData?: IrohData[] | string,
180
+ anchors?: Anchor[],
181
+ customIcons?: Record<string, string>,
182
+ governance?: Map<string, GovernanceStatement>,
183
+ sigstoreBundles?: Map<string, SigstoreBundle[]>,
184
+ graph?: Subgraph,
185
+ ): LineageNode {
186
+ // Use the first metadata info for the node display
187
+ const metadata = metadataLineage.get(statement.data)?.[0].metadata as { assetType: string; name: string };
188
+ const nodeType = metadata["assetType"];
189
+ const nodeLabel = metadata["name"] ? metadata["name"] : statement.data.slice(-6);
190
+
191
+ const nodeConfig = config.nodes.get(nodeType) || config.fallbackConfig;
192
+ if (nodeConfig == config.fallbackConfig && nodeType) {
193
+ nodeConfig.displayType = nodeType;
194
+ }
195
+ const node = createDefaultNode(statement.data, nodeConfig, nodeLabel, customIcons, graph);
196
+
197
+ const registeredByActor = actors.get(statement.registeredBy);
198
+ const vcStatement = vcs.get(statement.id);
199
+ const anchor = anchors?.find((anchor) => anchor.statements.includes(statement.id));
200
+
201
+ const registration: DataNodeRegistration = { statement, registeredByActor, anchors: anchor, vcs: vcStatement };
202
+ const sidebarData: DataNodeSidebarData = {
203
+ registrations: [registration],
204
+ metadataLineage: metadataLineage.get(statement.data),
205
+ sourceData: sourceData,
206
+ storage: storage.get(statement.data),
207
+ governance: governance?.get(statement.data),
208
+ sigstoreBundles: sigstoreBundles?.get(statement.data),
209
+ };
210
+
211
+ node.data.sidebarData = sidebarData;
212
+ return node;
213
+ }
214
+
215
+ /// Creates a Compute node (and all edges) and generic Data nodes for all the inputs and outputs.
216
+ /// Returns the created Nodes and Edges
217
+ function createComputeNode(
218
+ config: ExplorerConfig,
219
+ statement: ComputationStatement,
220
+ metadataLineage: Map<string, MetadataLineage[]>,
221
+ vcs: Map<string, VcStatement[]>,
222
+ actors: Map<string, any>,
223
+ irohCollections: Map<string, IrohData[]>,
224
+ anchors?: Anchor[],
225
+ customIcons?: Record<string, string>,
226
+ governance?: Map<string, GovernanceStatement>,
227
+ graph?: Subgraph,
228
+ ): [LineageNode[], LineageEdge[]] {
229
+ const nodeConfig = config.nodes.get("Computation") || config.fallbackConfig;
230
+ const nodes: LineageNode[] = [];
231
+ const computeNode = createDefaultNode(statement.id, nodeConfig, "compute", customIcons, graph);
232
+ const sourceData = statement.computation
233
+ ? irohCollections.get(stripCidPrefix(statement.computation)) || undefined
234
+ : undefined;
235
+
236
+ const sideBarData: ComputeNodeSidebarData = {
237
+ statement: statement,
238
+ metadataLineage: metadataLineage.get(statement.id), // can only be one metadata for a compute
239
+ vcStatement: vcs.get(statement.id),
240
+ registeredByActor: actors.get(statement.registeredBy),
241
+ executedOnActor: statement.executedOn && actors.get(statement.executedOn),
242
+ operatedByActor: statement.operatedBy && actors.get(statement.operatedBy),
243
+ sourceData: sourceData,
244
+ anchor: anchors?.find((anchor) => anchor.statements.includes(statement.id)),
245
+ governance: governance?.get(statement.id),
246
+ };
247
+ computeNode.data.sidebarData = sideBarData;
248
+
249
+ nodes.push(computeNode);
250
+
251
+ const inputs = Array.isArray(statement.input) ? statement.input : [statement.input];
252
+ const outputs = Array.isArray(statement.output) ? statement.output : [statement.output];
253
+
254
+ const defaultDataNodeConfig = config.nodes.get("Data") || config.fallbackConfig;
255
+ const inputEdges: LineageEdge[] = inputs.map((input: string) => {
256
+ // Create default input nodes in case they don't have a DataRegistration
257
+ const inputNode = createDefaultNode(input, defaultDataNodeConfig, "data");
258
+ nodes.push(inputNode);
259
+ return createEdge(input, statement.id);
260
+ });
261
+
262
+ const outputEdges = outputs.map((output: string) => {
263
+ const outputNode = createDefaultNode(output, defaultDataNodeConfig, "data");
264
+ nodes.push(outputNode);
265
+ return createEdge(statement.id, output);
266
+ });
267
+
268
+ const edges = [...inputEdges, ...outputEdges];
269
+
270
+ return [nodes, edges];
271
+ }
@@ -0,0 +1,148 @@
1
+ import { LineageEdge, LineageNode } from "../components/nodes/types";
2
+ import dagre from "@dagrejs/dagre";
3
+
4
+ /// Positions all the nodes and edges
5
+ export const layoutNodes = (nodes: LineageNode[], edges: LineageEdge[]): [LineageNode[], LineageEdge[]] => {
6
+ const verticalGapBetweenNodes = 30;
7
+ const horizontalGapBetweenColumns = 180;
8
+
9
+ const dagreGraph = initializeGraph(nodes, edges);
10
+
11
+ // Map of column number and Nodes in that column
12
+ const columnMap = getNodesByColumn(dagreGraph, nodes);
13
+
14
+ const { columnHeights, columnWidths, maxHeight } = sizeGraphColumns(dagreGraph, columnMap, verticalGapBetweenNodes);
15
+
16
+ const positionedNodes = setNodePositions(
17
+ dagreGraph,
18
+ columnMap,
19
+ columnHeights,
20
+ columnWidths,
21
+ verticalGapBetweenNodes,
22
+ horizontalGapBetweenColumns,
23
+ maxHeight,
24
+ );
25
+ return [positionedNodes, edges];
26
+ };
27
+
28
+ // Initializes the dagre Graph with all the nodes and edges
29
+ function initializeGraph(nodes: LineageNode[], edges: LineageEdge[]): dagre.graphlib.Graph {
30
+ const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
31
+
32
+ const direction = "LR";
33
+ dagreGraph.setGraph({
34
+ rankdir: direction,
35
+ nodesep: 30,
36
+ ranksep: 300,
37
+ align: "UL",
38
+ });
39
+
40
+ dagreGraph.setGraph({ rankdir: direction });
41
+
42
+ for (const node of nodes) {
43
+ dagreGraph.setNode(node.id, {
44
+ ...node.data.size,
45
+ });
46
+ }
47
+
48
+ edges.forEach((edge) => {
49
+ dagreGraph.setEdge(edge.source, edge.target);
50
+ });
51
+
52
+ dagre.layout(dagreGraph);
53
+
54
+ return dagreGraph;
55
+ }
56
+
57
+ // Returns a MAP of each "column" (aka Rank) and the nodes in the column
58
+ function getNodesByColumn(graph: dagre.graphlib.Graph, nodes: LineageNode[]): Map<number, LineageNode[]> {
59
+ const columnMap = new Map<number, LineageNode[]>();
60
+ for (const node of nodes) {
61
+ const dNode = graph.node(node.id) as { rank: number } | undefined;
62
+
63
+ if (dNode && typeof dNode.rank === "number") {
64
+ const rankNodes = columnMap.get(dNode.rank) ?? [];
65
+ rankNodes.push(node);
66
+ columnMap.set(dNode.rank, rankNodes);
67
+ }
68
+ }
69
+
70
+ return columnMap;
71
+ }
72
+
73
+ // Calculates the column heights and width for each column in the graph based off the number and size of the nodes in each column
74
+ // Also returns the height of the tallest column
75
+ function sizeGraphColumns(
76
+ graph: dagre.graphlib.Graph,
77
+ columnMap: Map<number, LineageNode[]>,
78
+ verticalGapBetweenNodes: number,
79
+ ): { columnHeights: Map<number, number>; columnWidths: Map<number, number>; maxHeight: number } {
80
+ const columnHeights = new Map<number, number>();
81
+ const columnWidths = new Map<number, number>();
82
+ let maxHeight = 0;
83
+ for (const [rank, nodes] of columnMap) {
84
+ nodes.sort((a, b) => {
85
+ const da = graph.node(a.id);
86
+ const db = graph.node(b.id);
87
+ return da.y - db.y;
88
+ });
89
+ let rankHeight = 0;
90
+ let maxWidth = 0;
91
+ for (const node of nodes) {
92
+ const graphNode = graph.node(node.id);
93
+ rankHeight += graphNode.height;
94
+ maxWidth = Math.max(maxWidth, graphNode.width);
95
+ }
96
+ rankHeight += (nodes.length - 1) * verticalGapBetweenNodes;
97
+
98
+ maxHeight = Math.max(maxHeight, rankHeight);
99
+ columnHeights.set(rank, rankHeight);
100
+ columnWidths.set(rank, maxWidth);
101
+ }
102
+ return { columnHeights: columnHeights, columnWidths: columnWidths, maxHeight };
103
+ }
104
+
105
+ // Sets the position of each node in the graph, and returns the nodes with updated positions
106
+ function setNodePositions(
107
+ graph: dagre.graphlib.Graph,
108
+ columnMap: Map<number, LineageNode[]>,
109
+ columnHeights: Map<number, number>,
110
+ columnWidths: Map<number, number>,
111
+ verticalGapBetweenNodes: number,
112
+ horizontalGapBetweenNodes: number,
113
+ maxColumnHeight: number,
114
+ ): LineageNode[] {
115
+ const positionedNodes: LineageNode[] = [];
116
+ let xPosition = 0;
117
+ const rankEntries = Array.from(columnMap.entries());
118
+ rankEntries.sort((a, b) => {
119
+ return a[0] - b[0];
120
+ });
121
+
122
+ for (const [rank, nodes] of rankEntries) {
123
+ const rankHeight = columnHeights.get(rank) || 0;
124
+ const rankWidth = columnWidths.get(rank) || 0;
125
+ const rankOffset = (maxColumnHeight - rankHeight) / 2;
126
+ let yPosition = 0;
127
+
128
+ for (const [, node] of nodes.entries()) {
129
+ const graphNode = graph.node(node.id);
130
+ node.position = {
131
+ x: xPosition + rankWidth / 2 - graphNode.width / 2,
132
+ y: yPosition + rankOffset,
133
+ };
134
+
135
+ const predecessors = graph.predecessors(node.id);
136
+ node.data.hasPrecedingNode = (predecessors && predecessors.length > 0) || false;
137
+
138
+ const successors = graph.successors(node.id);
139
+ node.data.hasSuccessorNode = (successors && successors.length > 0) || false;
140
+
141
+ positionedNodes.push(node);
142
+ yPosition += graphNode.height + verticalGapBetweenNodes;
143
+ }
144
+ xPosition += rankWidth + horizontalGapBetweenNodes;
145
+ }
146
+
147
+ return positionedNodes;
148
+ }
@@ -0,0 +1,117 @@
1
+ import { CID } from "multiformats/cid";
2
+ import { create as createMultihash } from "multiformats/hashes/digest";
3
+ import { IrohData } from "../components/nodes/types";
4
+
5
+ const RAW_BINARY = 0x55;
6
+ const BLAKE3_HASHSEQ = 0x80;
7
+ const BLAKE3_256 = 0x1e;
8
+
9
+ export function getIrohMetaCid(codec: number, collectionBlob: Uint8Array): string {
10
+ const first32 = collectionBlob.slice(0, 32);
11
+ const multihash = createMultihash(BLAKE3_256, first32); // 0x1e == BLAKE3-256 multihash code
12
+ const cid = CID.createV1(codec, multihash);
13
+ return cid.toString();
14
+ }
15
+
16
+ export function resolveIrohCollections(blobs: Record<string, string>): Map<string, IrohData[]> {
17
+ // Map<cid, bytes> of possible iroh collections from the Blob
18
+ const irohCandidateBlobs: Record<string, Uint8Array> = {};
19
+ for (const [cid, blob] of Object.entries(blobs)) {
20
+ try {
21
+ const decoded = Uint8Array.from(atob(blob), (c) => c.charCodeAt(0));
22
+
23
+ if (decoded.length >= 64) {
24
+ irohCandidateBlobs[cid] = decoded;
25
+ } else {
26
+ //console.log(`Skipping blob '${cid}' — too short (${decoded.length} bytes)`);
27
+ }
28
+ } catch (e) {
29
+ console.error(`Failed to decode blob for CID '${cid}':`, e);
30
+ throw e;
31
+ }
32
+ }
33
+
34
+ const irohCollections = new Map();
35
+
36
+ for (const [cid, blob] of Object.entries(irohCandidateBlobs)) {
37
+ const metaCidLegacy = getIrohMetaCid(RAW_BINARY, blob);
38
+ const meta_cid = getIrohMetaCid(BLAKE3_HASHSEQ, blob);
39
+ if (metaCidLegacy in blobs) {
40
+ const metaBlob = blobs[metaCidLegacy];
41
+ const collection_blob = blobs[cid];
42
+ const nameCidMap = extractCollectionNames(blobs, metaBlob, collection_blob);
43
+ irohCollections.set(cid, nameCidMap);
44
+ } else if (meta_cid in blobs) {
45
+ const metaBlob = blobs[metaCidLegacy];
46
+ const collection_blob = blobs[cid];
47
+ const nameCidMap = extractCollectionNames(blobs, metaBlob, collection_blob);
48
+ irohCollections.set(cid, nameCidMap);
49
+ }
50
+ }
51
+
52
+ return irohCollections;
53
+ }
54
+
55
+ /**
56
+ * Decodes a base64 collection blob and extracts file names after CollectionV0. or CollectionV1.
57
+ * Handles both versions and multiple names.
58
+ * @param metaBlob - The base64-encoded meta blob that has the names of the items in the collection.
59
+ * @param collectionBlob - The base64-encoded collection blob of the iroh collection.
60
+ * @returns Map of FileName -> CID in the iroh collection.
61
+ */
62
+ function extractCollectionNames(blobs: Record<string, string>, metaBlob: string, collectionBlob: string): IrohData[] {
63
+ // collection blob contains the hash sequence.
64
+ const collectionBlobStr = atob(collectionBlob);
65
+ const hashSeq = Uint8Array.from(collectionBlobStr, (c) => c.charCodeAt(0));
66
+
67
+ const hashes = parseHashSeq(hashSeq).slice(1); // drop meta hash
68
+
69
+ // meta blob is encoded with `postcard` encoding, which is a length-prefixed format.
70
+ const names = [];
71
+
72
+ const decodedMetaBlob = atob(metaBlob);
73
+ const metaBytes = Uint8Array.from(decodedMetaBlob, (c) => c.charCodeAt(0));
74
+
75
+ const headerLen = 13; // fixed length u8 array, no length prefix
76
+ const namesBytes = metaBytes.slice(headerLen); // drop header
77
+
78
+ let unprocessedNamesBytes = namesBytes.slice(1); // drop array length byte
79
+ while (unprocessedNamesBytes.length > 0) {
80
+ const nameLen = unprocessedNamesBytes[0];
81
+ unprocessedNamesBytes = unprocessedNamesBytes.slice(1); // drop length byte
82
+
83
+ const nameBytes = unprocessedNamesBytes.slice(0, nameLen);
84
+ unprocessedNamesBytes = unprocessedNamesBytes.slice(nameLen); // drop name bytes
85
+
86
+ const name = new TextDecoder().decode(nameBytes);
87
+ names.push(name);
88
+ }
89
+
90
+ if (names.length !== hashes.length) {
91
+ console.error(`Names and hashes length mismatch: ${names.length} != ${hashes.length}. Names: '${names}'`);
92
+ return [];
93
+ }
94
+
95
+ const irohDataList: IrohData[] = [];
96
+ for (let i = 0; i < names.length; i++) {
97
+ const blob = blobs[hashes[i]];
98
+ const data = blob ? atob(blob) : null;
99
+ irohDataList.push({
100
+ path: names[i],
101
+ cid: hashes[i],
102
+ data: data,
103
+ });
104
+ }
105
+ return irohDataList;
106
+ }
107
+
108
+ function parseHashSeq(bytes: Uint8Array): string[] {
109
+ if (bytes.length % 32 !== 0) throw new Error("Invalid hash sequence length");
110
+ const hashes: string[] = [];
111
+ for (let i = 0; i < bytes.length; i += 32) {
112
+ const chunk = bytes.slice(i, i + 32);
113
+ const hashCid = getIrohMetaCid(RAW_BINARY, chunk);
114
+ hashes.push(hashCid);
115
+ }
116
+ return hashes;
117
+ }