Compare commits

..

No commits in common. "development_ipfs" and "main" have entirely different histories.

23 changed files with 489 additions and 9604 deletions

69
impl.md
View File

@ -1,69 +0,0 @@
# Implementation Status Report
## Analysis of Implementation Status
### What Has Been Implemented:
1. **Basic Website Structure**:
- Svelte-based frontend with components for layout, navigation, and content display
- Responsive design with mobile support
- Markdown content rendering capability
2. **IPFS Integration**:
- IPFS service implementation using Helia library
- Content retrieval from IPFS
- Content upload to IPFS
- Fallback to IPFS gateways when direct connection fails
- Local caching of IPFS content using IndexedDB
- Network status monitoring for offline support
3. **UI Components**:
- Header/Navbar
- Sidebar navigation
- Footer
- Markdown content display
- IPFS demo functionality
4. **Demo Functionality**:
- IPFS content upload and retrieval demo
- Mock functionality when IPFS is not available
### What Is Missing:
1. **Content Processing Pipeline**:
- No implementation of the Blake hashing algorithm for content
- No implementation of content encryption/decryption
- No CLI tools or scripts for processing content files as described in specs
- Missing the pipeline for file discovery, normalization, hashing, encryption, and IPFS upload
2. **Metadata Structure**:
- No implementation of the metadata structure as described in specs
- Missing the pages list metadata with Blake hash + IPFS CID format
- No implementation of metadata retrieval from IPFS
3. **Security Features**:
- Missing the encryption/decryption functionality using Blake hash as key
- No implementation of the security considerations mentioned in specs
4. **Collection Structure**:
- No implementation of the collection structure as described in specs
- Missing the `.collection` file handling
5. **Content Retrieval with Decryption**:
- No implementation of retrieving encrypted content and decrypting it
## Conclusion
The project has implemented a solid foundation for a browser-based website with IPFS integration, including:
- Basic website structure and UI components
- IPFS content retrieval and upload
- Local caching and offline support
- Demo functionality
However, the core security features described in the specifications are not implemented:
- The content processing pipeline with Blake hashing and encryption
- The metadata structure with combined Blake hash and IPFS CID
- The collection structure for organizing content
- The encryption/decryption functionality
The current implementation provides a working demo of IPFS integration but lacks the security features that are central to the project's specifications. To fully meet the requirements, the missing components would need to be implemented, particularly the content processing pipeline with encryption and the metadata structure.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -26,22 +26,12 @@
"vite": "^6.3.5" "vite": "^6.3.5"
}, },
"dependencies": { "dependencies": {
"@helia/interface": "^5.2.1",
"@helia/unixfs": "^5.0.0",
"@libp2p/webrtc": "^5.2.12",
"@libp2p/websockets": "^9.2.10",
"@libp2p/webtransport": "^5.0.40",
"@noble/hashes": "^1.8.0",
"@tailwindcss/postcss": "^4.1.6", "@tailwindcss/postcss": "^4.1.6",
"@tailwindcss/vite": "^4.1.6", "@tailwindcss/vite": "^4.1.6",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"helia": "^5.3.0",
"idb": "^8.0.3",
"libp2p": "^2.8.5",
"lucide-svelte": "^0.509.0", "lucide-svelte": "^0.509.0",
"marked": "^15.0.11", "marked": "^15.0.11",
"multiformats": "^13.3.3",
"shadcn-svelte": "^0.14.2", "shadcn-svelte": "^0.14.2",
"tailwind-merge": "^3.2.0" "tailwind-merge": "^3.2.0"
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,9 @@
<script lang="ts"> <script lang="ts">
import Layout from "./components/Layout.svelte"; import Layout from "./components/Layout.svelte";
import Home from "./components/Home.svelte"; import Home from "./components/Home.svelte";
import OfflineStatus from "./components/OfflineStatus.svelte";
import * as IPFSProviderModule from "./components/IPFSProvider.svelte";
import "./app.css"; import "./app.css";
// Use the component from the module
const IPFSProvider = IPFSProviderModule.default;
</script> </script>
<IPFSProvider> <Layout>
<Layout> <Home />
<Home contentPath="" /> </Layout>
</Layout>
<OfflineStatus />
</IPFSProvider>

View File

@ -1,198 +0,0 @@
<script lang="ts">
import { cryptoService } from '../services/crypto.service';
import { contentProcessorService } from '../services/content-processor.service';
import { ipfsService } from '../services/ipfs.service';
import { onMount } from 'svelte';
let textInput = '';
let blakeHash = '';
let selectedFile: File | null = null;
let fileBlakeHash = '';
let normalizedFilename = '';
let ipfsCid = '';
let combinedKey = '';
let isProcessing = false;
let errorMessage = '';
let successMessage = '';
// Hash the text input using Blake3
function hashText() {
try {
errorMessage = '';
if (!textInput.trim()) {
errorMessage = 'Please enter some text to hash';
return;
}
blakeHash = cryptoService.hashStringBlake3AsHex(textInput);
successMessage = 'Text hashed successfully!';
} catch (error) {
console.error('Error hashing text:', error);
errorMessage = `Error hashing text: ${error instanceof Error ? error.message : String(error)}`;
}
}
// Handle file selection
function handleFileSelect(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
selectedFile = input.files[0];
// Reset previous results
fileBlakeHash = '';
normalizedFilename = '';
ipfsCid = '';
combinedKey = '';
errorMessage = '';
successMessage = '';
}
}
// Process the selected file
async function processFile() {
if (!selectedFile) {
errorMessage = 'Please select a file to process';
return;
}
try {
isProcessing = true;
errorMessage = '';
successMessage = '';
// Normalize the filename
normalizedFilename = contentProcessorService.normalizeFilename(selectedFile.name);
// Hash the file using Blake3
fileBlakeHash = await cryptoService.hashFileBlake3AsHex(selectedFile);
// Process the file through the content processing pipeline
const metadata = await contentProcessorService.processFile(selectedFile);
// Update the UI with the results
ipfsCid = metadata.ipfsCid;
combinedKey = metadata.combinedKey;
isProcessing = false;
successMessage = 'File processed successfully!';
} catch (error) {
console.error('Error processing file:', error);
errorMessage = `Error processing file: ${error instanceof Error ? error.message : String(error)}`;
isProcessing = false;
}
}
// Initialize IPFS when the component mounts
onMount(async () => {
try {
await ipfsService.initialize();
} catch (error) {
console.error('Error initializing IPFS:', error);
errorMessage = `Error initializing IPFS: ${error instanceof Error ? error.message : String(error)}`;
}
});
</script>
<div class="p-6 max-w-4xl mx-auto bg-white rounded-lg shadow-md">
<h2 class="text-2xl font-bold mb-6">Blake Hashing Demo</h2>
<!-- Text Input Section -->
<div class="mb-8 p-4 border border-gray-200 rounded-md">
<h3 class="text-xl font-semibold mb-4">Text Hashing</h3>
<div class="mb-4">
<label for="textInput" class="block text-sm font-medium text-gray-700 mb-1">Enter text to hash:</label>
<textarea
id="textInput"
bind:value={textInput}
class="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
rows="3"
></textarea>
</div>
<button
on:click={hashText}
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
Hash Text
</button>
{#if blakeHash}
<div class="mt-4">
<h4 class="text-sm font-medium text-gray-700 mb-1">Blake3 Hash:</h4>
<div class="p-2 bg-gray-100 rounded-md overflow-x-auto">
<code class="text-sm break-all">{blakeHash}</code>
</div>
</div>
{/if}
</div>
<!-- File Processing Section -->
<div class="p-4 border border-gray-200 rounded-md">
<h3 class="text-xl font-semibold mb-4">File Processing</h3>
<div class="mb-4">
<label for="fileInput" class="block text-sm font-medium text-gray-700 mb-1">Select a file to process:</label>
<input
type="file"
id="fileInput"
on:change={handleFileSelect}
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
/>
</div>
<button
on:click={processFile}
disabled={!selectedFile || isProcessing}
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:bg-blue-300 disabled:cursor-not-allowed"
>
{isProcessing ? 'Processing...' : 'Process File'}
</button>
{#if selectedFile && (normalizedFilename || fileBlakeHash || ipfsCid || combinedKey)}
<div class="mt-4 space-y-3">
{#if normalizedFilename}
<div>
<h4 class="text-sm font-medium text-gray-700 mb-1">Normalized Filename:</h4>
<div class="p-2 bg-gray-100 rounded-md overflow-x-auto">
<code class="text-sm break-all">{normalizedFilename}</code>
</div>
</div>
{/if}
{#if fileBlakeHash}
<div>
<h4 class="text-sm font-medium text-gray-700 mb-1">Blake3 Hash:</h4>
<div class="p-2 bg-gray-100 rounded-md overflow-x-auto">
<code class="text-sm break-all">{fileBlakeHash}</code>
</div>
</div>
{/if}
{#if ipfsCid}
<div>
<h4 class="text-sm font-medium text-gray-700 mb-1">IPFS CID:</h4>
<div class="p-2 bg-gray-100 rounded-md overflow-x-auto">
<code class="text-sm break-all">{ipfsCid}</code>
</div>
</div>
{/if}
{#if combinedKey}
<div>
<h4 class="text-sm font-medium text-gray-700 mb-1">Combined Key (Blake Hash + IPFS CID):</h4>
<div class="p-2 bg-gray-100 rounded-md overflow-x-auto">
<code class="text-sm break-all">{combinedKey}</code>
</div>
</div>
{/if}
</div>
{/if}
</div>
<!-- Messages -->
{#if errorMessage}
<div class="mt-4 p-3 bg-red-100 text-red-700 rounded-md">
{errorMessage}
</div>
{/if}
{#if successMessage}
<div class="mt-4 p-3 bg-green-100 text-green-700 rounded-md">
{successMessage}
</div>
{/if}
</div>

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import MarkdownContent from "./MarkdownContent.svelte"; import MarkdownContent from "./MarkdownContent.svelte";
import { Shield, Zap, Smartphone } from "lucide-svelte";
export let contentPath: string = "introduction/introduction"; export let contentPath: string = "introduction/introduction";
@ -8,24 +9,169 @@
</script> </script>
<div class=""> <div class="">
{#if contentPath && contentPath.trim() !== ""} {#if contentPath}
<div <div
class="bg-background-secondary shadow-sm p-4 sm:p-6 overflow-x-auto" class="bg-background-secondary shadow-sm p-4 sm:p-6 overflow-x-auto"
> >
<MarkdownContent path={actualPath} /> <MarkdownContent path={actualPath} />
</div> </div>
{:else} {:else}
<div class="p-8 text-center"> <!-- Hero Section -->
<h2 class="text-2xl sm:text-3xl font-bold mb-4 text-text"> <section class="mb-12 sm:mb-16 md:mb-20 text-center md:text-left">
Welcome to SecureWeb <div class="md:flex md:items-center md:justify-between">
</h2> <div class="md:w-1/2 mb-8 md:mb-0 md:pr-6">
<p class="text-lg text-text-secondary max-w-3xl mx-auto"> <h1
A decentralized web platform with IPFS integration and Blake class="text-3xl sm:text-4xl md:text-5xl font-bold mb-4 sm:mb-6 text-text leading-tight"
hashing for content security. >
</p> Welcome to <span class="text-primary-600"
<p class="mt-4 text-text-secondary"> >SecureWeb</span
Use the sidebar navigation to explore the content and demos. >
</p> </h1>
</div> <p
class="text-lg sm:text-xl text-text-secondary max-w-2xl mb-6 sm:mb-8 leading-relaxed"
>
A comprehensive platform for secure and reliable web
solutions designed for modern businesses.
</p>
<div
class="flex flex-col xs:flex-row justify-center md:justify-start gap-3 sm:gap-4"
>
<button
class="bg-primary-600 hover:bg-primary-700 text-white px-6 sm:px-8 py-2.5 sm:py-3 rounded-md font-medium transition-colors duration-200 w-full xs:w-auto"
>Get Started</button
>
<button
class="bg-background-secondary hover:bg-background text-text px-6 sm:px-8 py-2.5 sm:py-3 rounded-md font-medium border border-border transition-colors duration-200 w-full xs:w-auto"
>Learn More</button
>
</div>
</div>
<div class="md:w-1/2">
<img
src="https://via.placeholder.com/600x400/f3f4f6/1e40af?text=SecureWeb"
alt="SecureWeb Platform"
class="rounded-lg shadow-lg w-full"
loading="lazy"
/>
</div>
</div>
</section>
<!-- Features Section -->
<section class="mb-12 sm:mb-16 md:mb-20">
<div class="text-center mb-8 sm:mb-12">
<h2
class="text-2xl sm:text-3xl font-bold mb-3 sm:mb-4 text-text"
>
Our Features
</h2>
<p
class="text-lg sm:text-xl text-text-secondary max-w-3xl mx-auto"
>
Discover what makes SecureWeb the preferred choice for
security-conscious businesses.
</p>
</div>
<div
class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 sm:gap-8 md:gap-10"
>
<div
class="bg-background p-6 sm:p-8 rounded-xl shadow-md hover:shadow-lg transition-shadow border border-border"
>
<div
class="bg-primary-100 p-3 rounded-full w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center mb-4 sm:mb-6"
>
<Shield
class="h-6 w-6 sm:h-7 sm:w-7 text-primary-600"
/>
</div>
<h3
class="text-lg sm:text-xl font-bold mb-3 sm:mb-4 text-text"
>
Advanced Security
</h3>
<p
class="text-text-secondary leading-relaxed text-sm sm:text-base"
>
State-of-the-art encryption and security protocols to
keep your data protected from threats.
</p>
</div>
<div
class="bg-background-secondary p-6 sm:p-8 rounded-xl shadow-md hover:shadow-lg transition-shadow border border-border"
>
<div
class="bg-primary-100 p-3 rounded-full w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center mb-4 sm:mb-6"
>
<Zap class="h-6 w-6 sm:h-7 sm:w-7 text-primary-600" />
</div>
<h3
class="text-lg sm:text-xl font-bold mb-3 sm:mb-4 text-text"
>
Lightning Performance
</h3>
<p
class="text-text-secondary leading-relaxed text-sm sm:text-base"
>
Optimized for speed and efficiency, ensuring a smooth
and responsive user experience.
</p>
</div>
<div
class="bg-background p-6 sm:p-8 rounded-xl shadow-md hover:shadow-lg transition-shadow border border-border sm:col-span-2 md:col-span-1"
>
<div
class="bg-primary-100 p-3 rounded-full w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center mb-4 sm:mb-6"
>
<Smartphone
class="h-6 w-6 sm:h-7 sm:w-7 text-primary-600"
/>
</div>
<h3
class="text-lg sm:text-xl font-bold mb-3 sm:mb-4 text-text"
>
Responsive Design
</h3>
<p
class="text-text-secondary leading-relaxed text-sm sm:text-base"
>
Fully responsive layouts that provide a seamless
experience across all devices and screen sizes.
</p>
</div>
</div>
</section>
<!-- CTA Section -->
<section>
<div
class="bg-gradient-to-r from-primary-600 to-primary-800 rounded-xl sm:rounded-2xl p-6 sm:p-8 md:p-10 text-white shadow-lg sm:shadow-xl"
>
<div class="md:flex md:items-center md:justify-between">
<div class="mb-6 md:mb-0 md:w-2/3 md:pr-6">
<h2 class="text-2xl sm:text-3xl font-bold mb-3 sm:mb-4">
Ready to Get Started?
</h2>
<p
class="text-base sm:text-lg text-primary-100 mb-0 max-w-2xl"
>
Join thousands of satisfied users who trust
SecureWeb for their web security needs. Sign up
today and experience the difference.
</p>
</div>
<div class="flex justify-center md:justify-end">
<button
class="bg-background-secondary text-primary-700 hover:bg-primary-50 px-6 sm:px-8 py-2.5 sm:py-3 rounded-lg font-semibold text-base sm:text-lg shadow-md transition-colors w-full xs:w-auto"
>
Sign Up Now
</button>
</div>
</div>
</div>
</section>
{/if} {/if}
</div> </div>

View File

@ -1,152 +0,0 @@
<script lang="ts">
import { ipfsService } from "../services/ipfs.service";
import { onMount } from "svelte";
import MarkdownContent from "./MarkdownContent.svelte";
import Button from "$lib/components/ui/button/button.svelte";
let content = "";
let uploadedCid = "";
let retrievedCid = "";
let uploading = false;
let uploadError: string | null = null;
let showPreview = false;
async function uploadContent() {
console.log("Upload button clicked");
if (!content.trim()) {
uploadError = "Content cannot be empty";
console.log("Content is empty, not uploading");
return;
}
console.log("Starting upload process");
uploading = true;
uploadError = null;
try {
console.log(
"Calling ipfsService.uploadContent with content:",
content.substring(0, 50) + "...",
);
uploadedCid = await ipfsService.uploadContent(content);
console.log("Content uploaded successfully with CID:", uploadedCid);
} catch (err: any) {
uploadError = err.message || "Failed to upload content";
console.error("Upload error:", err);
} finally {
console.log("Upload process completed, uploading =", uploading);
uploading = false;
}
}
function resetDemo() {
content = "";
uploadedCid = "";
retrievedCid = "";
uploadError = null;
showPreview = false;
}
onMount(() => {
console.log("IPFSDemo component mounted");
// Check if IPFS is initialized
const isInitialized = ipfsService.isInitialized();
console.log("IPFS initialized status:", isInitialized);
if (!isInitialized) {
uploadError =
"IPFS is not initialized. Please wait or refresh the page.";
}
});
</script>
<div class="max-w-3xl mx-auto p-4">
<h2 class="text-2xl font-semibold mb-6 text-primary-700">IPFS Demo</h2>
<div class="bg-background-secondary rounded-lg p-6 mb-8 shadow-sm">
<h3 class="text-xl font-medium mb-4 text-primary-600">
Upload Content to IPFS
</h3>
<div class="mb-4">
<label for="content" class="block mb-2 font-medium"
>Enter Markdown Content:</label
>
<textarea
id="content"
bind:value={content}
class="w-full p-3 border border-border rounded-md font-inherit resize-y bg-background-secondary text-text"
rows="8"
placeholder="# Hello IPFS&#10;&#10;This is a test of IPFS content storage and retrieval."
disabled={uploading}
></textarea>
</div>
<div class="flex gap-3 mb-4">
<Button
variant="primary"
on:click={() => {
console.log("Upload button clicked directly");
uploadContent();
}}
disabled={uploading || !content.trim()}
>
{uploading ? "Uploading..." : "Upload to IPFS"}
</Button>
<Button variant="secondary" on:click={resetDemo}>Reset</Button>
</div>
{#if uploadError}
<div
class="mt-4 p-3 bg-red-50 border-l-4 border-red-600 text-red-700 rounded"
>
{uploadError}
</div>
{/if}
{#if uploadedCid}
<div
class="mt-4 p-3 bg-green-50 border-l-4 border-green-600 text-green-800 rounded"
>
<p>Content uploaded successfully!</p>
<p>
CID: <code
class="bg-black/5 px-2 py-0.5 rounded font-mono break-all"
>{uploadedCid}</code
>
</p>
<div class="mt-4">
<Button
variant="secondary"
on:click={() => {
retrievedCid = uploadedCid;
showPreview = true;
}}
>
Preview Content
</Button>
</div>
</div>
{/if}
</div>
{#if showPreview && retrievedCid}
<div class="bg-background-secondary rounded-lg p-6 mb-8 shadow-sm">
<h3 class="text-xl font-medium mb-4 text-primary-600">
Content Preview
</h3>
<p>
Retrieving content with CID: <code
class="bg-black/5 px-2 py-0.5 rounded font-mono"
>{retrievedCid}</code
>
</p>
<div class="bg-white rounded-md p-4 mt-4 border border-border">
<MarkdownContent contentCid={retrievedCid} />
</div>
</div>
{/if}
</div>

View File

@ -1,107 +0,0 @@
<script lang="ts">
import { onMount } from "svelte";
import { ipfsService } from "../services/ipfs.service";
let initialized = false;
let initializing = true;
let error: string | null = null;
let mockMode = false;
onMount(async () => {
try {
initializing = true;
initialized = await ipfsService.initialize();
console.log("IPFS initialization status:", initialized);
// Even if initialization returns true, check if IPFS is actually initialized
if (!ipfsService.isInitialized()) {
console.warn(
"IPFS service reports success but is not fully initialized",
);
mockMode = true;
}
} catch (err: any) {
error = err.message || "Unknown error during IPFS initialization";
console.error("Failed to initialize IPFS:", err);
mockMode = true;
} finally {
initializing = false;
}
});
</script>
{#if initializing}
<div
class="fixed top-0 left-0 right-0 bg-black/70 text-white py-2 px-4 z-50 flex justify-center"
>
<div class="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="animate-spin"
>
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
</svg>
<span>Initializing IPFS...</span>
</div>
</div>
{:else if error}
<div
class="fixed top-0 left-0 right-0 bg-red-600/90 text-white py-2 px-4 z-50 flex justify-center"
>
<div class="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<span
>IPFS initialization issue: {error} - Running in demo mode</span
>
</div>
</div>
{:else if mockMode}
<div
class="fixed top-0 left-0 right-0 bg-amber-500/90 text-white py-2 px-4 z-50 flex justify-center"
>
<div class="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
<span>IPFS running in demo mode with mock functionality</span>
</div>
</div>
{/if}
<slot />

View File

@ -4,8 +4,6 @@
import Footer from "./Footer.svelte"; import Footer from "./Footer.svelte";
import NavDataProvider from "./NavDataProvider.svelte"; import NavDataProvider from "./NavDataProvider.svelte";
import Home from "./Home.svelte"; import Home from "./Home.svelte";
import BlakeHashDemo from "./BlakeHashDemo.svelte";
import IPFSDemo from "./IPFSDemo.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import ThemeProvider from "../lib/theme/ThemeProvider.svelte"; import ThemeProvider from "../lib/theme/ThemeProvider.svelte";
@ -84,15 +82,7 @@
sidebarVisible && !isMobile ? "md:ml-64" : "ml-0" sidebarVisible && !isMobile ? "md:ml-64" : "ml-0"
}`} }`}
> >
{#if selectedContentPath === "demos/blake-hash-demo"} {#if selectedContentPath}
<div class="p-4">
<BlakeHashDemo />
</div>
{:else if selectedContentPath === "demos/ipfs-demo"}
<div class="p-4">
<IPFSDemo />
</div>
{:else if selectedContentPath}
<Home contentPath={selectedContentPath} /> <Home contentPath={selectedContentPath} />
{:else} {:else}
<slot /> <slot />

View File

@ -1,10 +1,8 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { marked } from "marked"; import { marked } from "marked";
import { ipfsService } from "../services/ipfs.service";
export let path: string = ""; export let path: string = "";
export let contentCid: string = "";
// Base path for images // Base path for images
const imagePath = "/images"; const imagePath = "/images";
@ -13,228 +11,259 @@
let loading = true; let loading = true;
let error: string | null = null; let error: string | null = null;
$: if (path || contentCid) { $: if (path) {
loadContent(); loadMarkdownContent(path);
} }
async function loadContent() { async function loadMarkdownContent(mdPath: string) {
if (!mdPath) return;
loading = true; loading = true;
error = null; error = null;
content = ""; content = "";
try { try {
let markdown = ""; // Remove leading slash if present
const cleanPath = mdPath.startsWith("/")
? mdPath.substring(1)
: mdPath;
if (contentCid) { // If path is just a section like "introduction", append "/introduction" to it
// Load from IPFS const finalPath = cleanPath.includes("/")
console.log( ? cleanPath
`Loading content from IPFS with CID: ${contentCid}`, : `${cleanPath}/${cleanPath}`;
console.log(`Loading markdown from: /src/docs/${finalPath}.md`);
const response = await fetch(`/src/docs/${finalPath}.md`);
if (!response.ok) {
throw new Error(
`Failed to load content: ${response.status} ${response.statusText}`,
); );
try {
markdown = await ipfsService.getContent(contentCid);
} catch (err: any) {
throw new Error(
`Failed to load content from IPFS: ${err.message}`,
);
}
// Process IPFS image references if any
markdown = await processIPFSImageReferences(markdown);
} else if (path) {
// Load from local file system
// Remove leading slash if present
const cleanPath = path.startsWith("/")
? path.substring(1)
: path;
// If path is just a section like "introduction", append "/introduction" to it
const finalPath = cleanPath.includes("/")
? cleanPath
: `${cleanPath}/${cleanPath}`;
console.log(`Loading markdown from: /src/docs/${finalPath}.md`);
const response = await fetch(`/src/docs/${finalPath}.md`);
if (!response.ok) {
throw new Error(
`Failed to load content: ${response.status} ${response.statusText}`,
);
}
// Get the directory path for relative image references
const docDir = finalPath.substring(
0,
finalPath.lastIndexOf("/"),
);
markdown = await response.text();
// Process markdown to fix image paths
// Replace relative image paths with absolute paths
markdown = markdown.replace(
/!\[(.*?)\]\((?!http|\/)(.*?)\)/g,
(_match, alt, imgPath) => {
return `![${alt}](/images/${docDir}/${imgPath})`;
},
);
} else {
throw new Error("No path or CID provided");
} }
// Get the directory path for relative image references
const docDir = finalPath.substring(0, finalPath.lastIndexOf("/"));
let markdown = await response.text();
// Process markdown to fix image paths
// Replace relative image paths with absolute paths
markdown = markdown.replace(
/!\[(.*?)\]\((?!http|\/)(.*?)\)/g,
(_match, alt, imgPath) => {
return `![${alt}](/images/${docDir}/${imgPath})`;
},
);
const parsedContent = marked.parse(markdown); const parsedContent = marked.parse(markdown);
content = content =
typeof parsedContent === "string" typeof parsedContent === "string"
? parsedContent ? parsedContent
: await parsedContent; : await parsedContent;
} catch (err: any) { } catch (err: any) {
console.error("Error loading content:", err); console.error("Error loading markdown content:", err);
error = err.message || "Failed to load content"; error = err.message || "Failed to load content";
} finally { } finally {
loading = false; loading = false;
} }
} }
/**
* Process IPFS image references in markdown content
* @param markdown - Markdown content with IPFS image references
* @returns Processed markdown with IPFS images replaced with blob URLs
*/
async function processIPFSImageReferences(
markdown: string,
): Promise<string> {
// This is a simplified example - actual implementation would depend on how images are referenced
// For example, if images are referenced as ipfs://Qm...
const ipfsImageRegex = /!\[(.*?)\]\(ipfs:\/\/(.*?)\)/g;
const promises: Promise<string>[] = [];
let match;
const replacements: [string, string][] = [];
while ((match = ipfsImageRegex.exec(markdown)) !== null) {
const [fullMatch, alt, cid] = match;
promises.push(
(async () => {
try {
const imageBlob = await ipfsService.getImage(cid);
const imageUrl = URL.createObjectURL(imageBlob);
replacements.push([
fullMatch,
`![${alt}](${imageUrl})`,
]);
return "success";
} catch (error) {
console.error(
`Failed to load image with CID ${cid}:`,
error,
);
return "error";
}
})(),
);
}
await Promise.all(promises);
let processedMarkdown = markdown;
for (const [original, replacement] of replacements) {
processedMarkdown = processedMarkdown.replace(
original,
replacement,
);
}
return processedMarkdown;
}
onMount(() => { onMount(() => {
if (path || contentCid) { if (path) {
loadContent(); loadMarkdownContent(path);
} }
}); });
</script> </script>
<div class="max-w-full"> <div class="markdown-content">
{#if loading} {#if loading}
<div class="h-screen flex justify-center items-center p-4 sm:p-8"> <div class="loading">
<p>Loading content...</p> <p>Loading content...</p>
</div> </div>
{:else if error} {:else if error}
<div class="text-center p-4 sm:p-8"> <div class="error">
<p>Error: {error}</p> <p>Error: {error}</p>
</div> </div>
{:else} {:else}
<div class="markdown-content text-green-800"> <div class="content">
{@html content} {@html content}
</div> </div>
{/if} {/if}
</div> </div>
<style> <style>
/* Using @apply for markdown content styling since we need to target elements within the rendered HTML */ .markdown-content {
.markdown-content :global(h1) { /* padding: 0.5rem; */
@apply text-xl font-bold mb-4 text-primary-700 break-words sm:text-3xl sm:mb-6; max-width: 100%;
} }
.markdown-content :global(h2) { @media (min-width: 640px) {
@apply text-lg font-semibold mt-6 mb-3 text-primary-800 break-words sm:text-2xl sm:mt-8 sm:mb-4; .markdown-content {
/* padding: 1rem; */
}
} }
.markdown-content :global(h3) { .loading,
@apply text-base font-semibold mt-5 mb-2 text-primary-800 break-words sm:text-xl sm:mt-6 sm:mb-3; .error {
padding: 1rem;
text-align: center;
} }
.markdown-content :global(p) { @media (min-width: 640px) {
@apply mb-4 leading-relaxed; .loading,
.error {
padding: 2rem;
}
} }
.markdown-content :global(ul), .error {
.markdown-content :global(ol) { color: rgb(229 62 62);
@apply mb-4 ml-6 sm:ml-8;
} }
.markdown-content :global(li) { .content :global(h1) {
@apply mb-2; font-size: 1.75rem;
font-weight: 700;
margin-bottom: 1rem;
color: rgb(var(--color-primary-700));
word-break: break-word;
} }
.markdown-content :global(a) { .content :global(h2) {
@apply text-primary-600 no-underline break-words hover:underline; font-size: 1.5rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: rgb(var(--color-primary-800));
word-break: break-word;
} }
.markdown-content :global(blockquote) { .content :global(h3) {
@apply border-l-4 border-border pl-4 italic text-text-secondary; font-size: 1.25rem;
font-weight: 600;
margin-top: 1.25rem;
margin-bottom: 0.5rem;
color: rgb(var(--color-primary-800));
word-break: break-word;
} }
.markdown-content :global(code) { .content :global(p) {
@apply bg-background-secondary py-0.5 px-1.5 rounded text-sm font-mono break-words whitespace-pre-wrap; margin-bottom: 1rem;
line-height: 1.6;
} }
.markdown-content :global(pre) { .content :global(ul),
@apply bg-background-secondary p-3 sm:p-4 rounded-lg overflow-x-auto mb-4; .content :global(ol) {
margin-bottom: 1rem;
margin-left: 1.5rem;
} }
.markdown-content :global(pre code) { .content :global(li) {
@apply bg-transparent p-0 whitespace-pre break-normal; margin-bottom: 0.5rem;
} }
.markdown-content :global(img) { .content :global(a) {
@apply max-w-full h-auto rounded-lg my-4 block; color: rgb(var(--color-primary-600));
text-decoration: none;
word-break: break-word;
} }
.markdown-content :global(hr) { .content :global(a:hover) {
@apply border-0 border-t border-border my-6; text-decoration: underline;
} }
.markdown-content :global(table) { .content :global(blockquote) {
@apply w-full border-collapse mb-4 block overflow-x-auto; border-left: 4px solid rgb(var(--color-border));
padding-left: 1rem;
margin-left: 0;
margin-right: 0;
font-style: italic;
color: rgb(var(--color-text-secondary));
} }
.markdown-content :global(th), .content :global(code) {
.markdown-content :global(td) { background-color: rgb(var(--color-background-secondary));
@apply border border-border p-2 min-w-[100px]; padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.875em;
word-break: break-word;
white-space: pre-wrap;
} }
.markdown-content :global(th) { .content :global(pre) {
@apply bg-background-secondary; background-color: rgb(var(--color-background-secondary));
padding: 0.75rem;
border-radius: 0.5rem;
overflow-x: auto;
margin-bottom: 1rem;
}
.content :global(pre code) {
background-color: transparent;
padding: 0;
white-space: pre;
word-break: normal;
}
.content :global(img) {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
margin: 1rem 0;
display: block;
}
.content :global(hr) {
border: 0;
border-top: 1px solid rgb(var(--color-border));
margin: 1.5rem 0;
}
.content :global(table) {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
display: block;
overflow-x: auto;
}
.content :global(th),
.content :global(td) {
border: 1px solid rgb(var(--color-border));
padding: 0.5rem;
min-width: 100px;
}
.content :global(th) {
background-color: rgb(var(--color-background-secondary));
}
@media (min-width: 640px) {
.content :global(h1) {
font-size: 2.25rem;
margin-bottom: 1.5rem;
}
.content :global(h2) {
font-size: 1.75rem;
margin-top: 2rem;
margin-bottom: 1rem;
}
.content :global(h3) {
font-size: 1.5rem;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
.content :global(ul),
.content :global(ol) {
margin-left: 2rem;
}
.content :global(pre) {
padding: 1rem;
}
} }
</style> </style>

View File

@ -2,7 +2,6 @@
import type { NavItem } from "../types/nav"; import type { NavItem } from "../types/nav";
import { ChevronRight, ChevronDown } from "lucide-svelte"; import { ChevronRight, ChevronDown } from "lucide-svelte";
import { slide } from "svelte/transition"; import { slide } from "svelte/transition";
import Button from "$lib/components/ui/button/button.svelte";
export let item: NavItem; export let item: NavItem;
export let level: number = 0; export let level: number = 0;
@ -30,40 +29,27 @@
} }
</script> </script>
<div class="relative"> <div class="nav-item" style="--indent: {indentation}px;">
{#if hasChildren} {#if hasChildren}
<!-- Folder item --> <!-- Folder item -->
<button <button
class="flex items-center w-full text-left py-2 px-3 pl-[calc({indentation}px+12px)] relative border-none bg-transparent text-text-secondary text-sm cursor-pointer transition-colors duration-200 hover:bg-background/50 {isActive class="nav-button folder-button {isActive ? 'active' : ''}"
? 'bg-primary-500/10 text-primary-600 font-medium'
: ''}"
on:click={() => onToggle(item.link)} on:click={() => onToggle(item.link)}
on:keydown={(e) => handleKeyDown(e, true)} on:keydown={(e) => handleKeyDown(e, true)}
tabindex="0" tabindex="0"
aria-expanded={isExpanded} aria-expanded={isExpanded}
> >
<div <div class="tree-line" style="width: {indentation}px;"></div>
class="absolute left-0 top-0 bottom-0 pointer-events-none" <div class="icon-container">
style="width: {indentation}px;"
>
<div
class="absolute left-3 top-0 bottom-0 w-px bg-border"
></div>
</div>
<div class="flex items-center mr-2">
<ChevronRight <ChevronRight
class="w-4 h-4 transition-transform duration-200 {isExpanded class="chevron-icon {isExpanded ? 'expanded' : ''}"
? 'rotate-90'
: ''}"
/> />
</div> </div>
<span class="whitespace-nowrap overflow-hidden text-ellipsis" <span class="label">{item.label}</span>
>{item.label}</span
>
</button> </button>
{#if isExpanded && item.children && item.children.length > 0} {#if isExpanded && item.children && item.children.length > 0}
<div class="overflow-hidden" transition:slide={{ duration: 200 }}> <div class="children" transition:slide={{ duration: 200 }}>
{#each item.children as child} {#each item.children as child}
<svelte:self <svelte:self
item={child} item={child}
@ -80,27 +66,94 @@
<!-- File item --> <!-- File item -->
<a <a
href={item.link} href={item.link}
class="flex items-center w-full text-left py-2 px-3 pl-[calc({indentation}px+12px)] relative border-none bg-transparent text-text-secondary text-sm cursor-pointer transition-colors duration-200 hover:bg-background/50 {isActive class="nav-button file-button {isActive ? 'active' : ''}"
? 'bg-primary-500/10 text-primary-600 font-medium'
: ''}"
on:click={(e) => onNavClick(item.link, e)} on:click={(e) => onNavClick(item.link, e)}
on:keydown={(e) => handleKeyDown(e, false)} on:keydown={(e) => handleKeyDown(e, false)}
tabindex="0" tabindex="0"
> >
<div <div class="tree-line" style="width: {indentation}px;"></div>
class="absolute left-0 top-0 bottom-0 pointer-events-none" <div class="icon-container">
style="width: {indentation}px;"
>
<div
class="absolute left-3 top-0 bottom-0 w-px bg-border"
></div>
</div>
<div class="flex items-center mr-2">
<!-- No file icon --> <!-- No file icon -->
</div> </div>
<span class="whitespace-nowrap overflow-hidden text-ellipsis" <span class="label">{item.label}</span>
>{item.label}</span
>
</a> </a>
{/if} {/if}
</div> </div>
<style>
.nav-item {
position: relative;
}
.nav-button {
display: flex;
align-items: center;
width: 100%;
text-align: left;
padding: 8px 12px;
padding-left: calc(var(--indent) + 12px);
position: relative;
border: none;
background: transparent;
color: var(--color-text-secondary);
font-size: 0.9rem;
cursor: pointer;
transition:
background-color 0.2s,
color 0.2s;
}
.nav-button:hover {
background-color: rgba(var(--color-background), 0.5);
}
.nav-button.active {
background-color: rgba(var(--color-primary-500), 0.1);
color: rgb(var(--color-primary-600));
font-weight: 500;
}
.tree-line {
position: absolute;
left: 0;
top: 0;
bottom: 0;
pointer-events: none;
}
.tree-line::before {
content: "";
position: absolute;
left: 12px;
top: 0;
bottom: 0;
width: 1px;
background-color: rgb(var(--color-border));
}
.icon-container {
display: flex;
align-items: center;
margin-right: 8px;
}
:global(.chevron-icon) {
width: 16px;
height: 16px;
transition: transform 0.2s;
}
:global(.chevron-icon.expanded) {
transform: rotate(90deg);
}
.label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.children {
overflow: hidden;
}
</style>

View File

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Menu, Search, X } from "lucide-svelte"; import { Menu, Search, X } from "lucide-svelte";
import ThemeToggle from "../lib/theme/ThemeToggle.svelte"; import ThemeToggle from "../lib/theme/ThemeToggle.svelte";
import Button from "$lib/components/ui/button/button.svelte";
export let toggleSidebar: () => void = () => {}; export let toggleSidebar: () => void = () => {};
export let isMobile: boolean = false; export let isMobile: boolean = false;
@ -12,10 +11,8 @@
class="bg-background border-b border-border fixed top-0 left-0 right-0 z-30 h-16 flex items-center justify-between px-3 sm:px-4 shadow-sm" class="bg-background border-b border-border fixed top-0 left-0 right-0 z-30 h-16 flex items-center justify-between px-3 sm:px-4 shadow-sm"
> >
<div class="flex items-center"> <div class="flex items-center">
<Button <button
variant="ghost" class="mr-3 p-2 rounded-md hover:bg-background-secondary text-text"
size="icon"
class="mr-3"
on:click={toggleSidebar} on:click={toggleSidebar}
aria-label={sidebarVisible ? "Close sidebar" : "Open sidebar"} aria-label={sidebarVisible ? "Close sidebar" : "Open sidebar"}
> >
@ -24,7 +21,7 @@
{:else} {:else}
<Menu class="h-5 w-5" /> <Menu class="h-5 w-5" />
{/if} {/if}
</Button> </button>
<div class="text-lg sm:text-xl font-bold text-primary-600"> <div class="text-lg sm:text-xl font-bold text-primary-600">
SecureWeb SecureWeb
</div> </div>
@ -32,9 +29,11 @@
<div class="flex items-center space-x-2 sm:space-x-4"> <div class="flex items-center space-x-2 sm:space-x-4">
<!-- Search button for mobile --> <!-- Search button for mobile -->
<Button variant="ghost" size="icon" class="md:hidden"> <button
class="md:hidden p-2 rounded-md hover:bg-background-secondary text-text"
>
<Search class="h-5 w-5" /> <Search class="h-5 w-5" />
</Button> </button>
<!-- Search bar for desktop --> <!-- Search bar for desktop -->
<div class="relative hidden md:block"> <div class="relative hidden md:block">

View File

@ -1,62 +0,0 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { networkService } from "../services/network.service";
let online = networkService.isOnline();
let removeListener: (() => void) | null = null;
onMount(() => {
removeListener = networkService.addStatusChangeListener((status) => {
online = status;
});
});
onDestroy(() => {
if (removeListener) {
removeListener();
}
});
</script>
{#if !online}
<div
class="fixed bottom-0 left-0 right-0 bg-amber-500 text-white py-2 px-4 flex items-center justify-center gap-2 z-50 shadow-[0_-2px_10px_rgba(0,0,0,0.1)] animate-slide-up"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="w-5 h-5"
>
<line x1="1" y1="1" x2="23" y2="23"></line>
<path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path>
<path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"></path>
<path d="M10.71 5.05A16 16 0 0 1 22.58 9"></path>
<path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"></path>
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path>
<line x1="12" y1="20" x2="12.01" y2="20"></line>
</svg>
<span>You are offline. Some content may not be available.</span>
</div>
{/if}
<style>
@keyframes slide-up {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.animate-slide-up {
animation: slide-up 0.3s ease-out;
}
</style>

View File

@ -3,7 +3,6 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import NavItemComponent from "./NavItem.svelte"; import NavItemComponent from "./NavItem.svelte";
import { Menu, X } from "lucide-svelte"; import { Menu, X } from "lucide-svelte";
import Button from "$lib/components/ui/button/button.svelte";
export let navData: NavItem[] = []; export let navData: NavItem[] = [];
export let onNavItemClick: (path: string) => void = () => {}; export let onNavItemClick: (path: string) => void = () => {};
@ -76,11 +75,10 @@
</script> </script>
<aside <aside
class="bg-background-secondary border-border h-screen fixed top-0 left-0 pt-16 overflow-y-auto shadow-sm z-20 class="sidebar bg-background-secondary border-border h-screen fixed top-0 left-0 pt-16 overflow-y-auto shadow-sm z-20
{isMobile ? 'w-[85%] max-w-xs' : 'w-64'} {isMobile ? 'w-[85%] max-w-xs' : 'w-64'}
transition-transform duration-300 ease-in-out transition-transform duration-300 ease-in-out
{visible ? 'translate-x-0' : '-translate-x-full'} {visible ? 'translate-x-0' : '-translate-x-full'}"
scrollbar-thin scrollbar-thumb-gray-400/50 scrollbar-track-transparent border-r border-border"
> >
<nav class="w-full py-2"> <nav class="w-full py-2">
{#each navData as item} {#each navData as item}
@ -97,13 +95,32 @@
</aside> </aside>
{#if isMobile && !visible} {#if isMobile && !visible}
<Button <button
variant="secondary" class="fixed top-4 left-4 z-10 p-2 bg-background-secondary rounded-md shadow-md hover:bg-background"
size="icon"
class="fixed top-4 left-4 z-10 shadow-md"
on:click={toggleSidebar} on:click={toggleSidebar}
aria-label="Open sidebar" aria-label="Open sidebar"
> >
<Menu class="w-5 h-5" /> <Menu class="w-5 h-5 text-text" />
</Button> </button>
{/if} {/if}
<style>
.sidebar {
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
border-right: 1px solid rgb(var(--color-border));
}
.sidebar::-webkit-scrollbar {
width: 6px;
}
.sidebar::-webkit-scrollbar-track {
background: transparent;
}
.sidebar::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 3px;
}
</style>

View File

@ -1,11 +1,11 @@
<!-- button.svelte -->
<script lang="ts"> <script lang="ts">
import { cn } from "../../../utils"; import { cn } from "../../../utils";
export let variant: "primary" | "secondary" | "ghost" = "primary"; export let variant: "primary" | "secondary" | "ghost" = "primary";
export let size: "default" | "sm" | "lg" | "icon" = "default"; export let size: "default" | "sm" | "lg" | "icon" = "default";
let className: string | undefined | null = undefined; let className: string | undefined | null = undefined;
export { className }; export { className as class };
const sizeClasses = { const sizeClasses = {
default: "h-10 px-4 py-2", default: "h-10 px-4 py-2",
@ -26,7 +26,6 @@
sizeClasses[size], sizeClasses[size],
className, className,
)} )}
on:click
{...$$restProps} {...$$restProps}
> >
<slot /> <slot />

View File

@ -1,168 +0,0 @@
/**
* Content Processing Service
* Implements the content processing pipeline as described in the specs
* Uses Blake hashing for content integrity and encryption key generation
*/
import { cryptoService } from './crypto.service';
import { ipfsService } from './ipfs.service';
/**
* Interface for processed content metadata
*/
interface ProcessedContentMetadata {
originalFilename: string;
normalizedFilename: string;
blakeHash: string;
ipfsCid: string;
combinedKey: string;
contentType: string;
size: number;
timestamp: number;
}
/**
* Service for processing content files
* Implements the content processing pipeline:
* 1. File discovery
* 2. Filename normalization
* 3. Original content hashing (Blake)
* 4. Content encryption
* 5. Encrypted content upload to IPFS
*/
class ContentProcessorService {
/**
* Process a file through the content processing pipeline
* @param file - File to process
* @returns Promise<ProcessedContentMetadata> - Metadata of the processed content
*/
async processFile(file: File): Promise<ProcessedContentMetadata> {
try {
// Step 1: Get file information
const originalFilename = file.name;
// Step 2: Normalize filename (lowercase and snake_case)
const normalizedFilename = this.normalizeFilename(originalFilename);
// Step 3: Hash the original content using Blake3
const blakeHash = await cryptoService.hashFileBlake3AsHex(file);
// Step 4: Encrypt the content using the Blake hash as the key
// Note: This is a placeholder for actual encryption implementation
const encryptedContent = await this.encryptContent(file, blakeHash);
// Step 5: Upload the encrypted content to IPFS
const ipfsCid = await ipfsService.uploadContent(encryptedContent);
// Create the combined key (Blake hash + IPFS CID)
const combinedKey = cryptoService.combineBlakeHashAndIpfsCid(blakeHash, ipfsCid);
// Return the metadata
return {
originalFilename,
normalizedFilename,
blakeHash,
ipfsCid,
combinedKey,
contentType: file.type,
size: file.size,
timestamp: Date.now()
};
} catch (error) {
console.error('Error processing file:', error);
throw error;
}
}
/**
* Normalize a filename to lowercase and snake_case
* @param filename - Original filename
* @returns Normalized filename
*/
normalizeFilename(filename: string): string {
// Remove file extension
const parts = filename.split('.');
const extension = parts.pop() || '';
let name = parts.join('.');
// Convert to lowercase
name = name.toLowerCase();
// Replace spaces and special characters with underscores
name = name.replace(/[^a-z0-9]/g, '_');
// Replace multiple underscores with a single one
name = name.replace(/_+/g, '_');
// Remove leading and trailing underscores
name = name.replace(/^_+|_+$/g, '');
// Add extension back if it exists
if (extension) {
name = `${name}.${extension.toLowerCase()}`;
}
return name;
}
/**
* Encrypt content using the Blake hash as the key
* This is a placeholder for actual encryption implementation
* @param file - File to encrypt
* @param key - Encryption key (Blake hash)
* @returns Promise<string> - Encrypted content as string
*/
async encryptContent(file: File, key: string): Promise<string> {
// TODO: Implement actual encryption
// For now, we'll just return a mock encrypted content
// This should be replaced with actual encryption using the Blake hash as the key
// Convert the file to a string
const arrayBuffer = await file.arrayBuffer();
const contentBytes = new Uint8Array(arrayBuffer);
// Mock encryption by XORing with the key bytes
// This is NOT secure and is only for demonstration purposes
const keyBytes = cryptoService.hexToBytes(key);
const encryptedBytes = new Uint8Array(contentBytes.length);
for (let i = 0; i < contentBytes.length; i++) {
encryptedBytes[i] = contentBytes[i] ^ keyBytes[i % keyBytes.length];
}
// Convert to base64 for storage
return btoa(String.fromCharCode.apply(null, Array.from(encryptedBytes)));
}
/**
* Decrypt content using the Blake hash as the key
* This is a placeholder for actual decryption implementation
* @param encryptedContent - Encrypted content as string
* @param key - Decryption key (Blake hash)
* @returns Promise<Uint8Array> - Decrypted content as Uint8Array
*/
async decryptContent(encryptedContent: string, key: string): Promise<Uint8Array> {
// TODO: Implement actual decryption
// For now, we'll just return a mock decrypted content
// This should be replaced with actual decryption using the Blake hash as the key
// Convert the base64 string to bytes
const encryptedBytes = new Uint8Array(
atob(encryptedContent).split('').map(c => c.charCodeAt(0))
);
// Mock decryption by XORing with the key bytes
// This is NOT secure and is only for demonstration purposes
const keyBytes = cryptoService.hexToBytes(key);
const decryptedBytes = new Uint8Array(encryptedBytes.length);
for (let i = 0; i < encryptedBytes.length; i++) {
decryptedBytes[i] = encryptedBytes[i] ^ keyBytes[i % keyBytes.length];
}
return decryptedBytes;
}
}
// Create a singleton instance
export const contentProcessorService = new ContentProcessorService();

View File

@ -1,125 +0,0 @@
/**
* Cryptography Service
* Provides cryptographic functions for content processing
* Implements Blake hashing algorithm for content integrity and encryption key generation
*/
import { blake3 } from '@noble/hashes/blake3';
import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils';
/**
* Service for cryptographic operations
* Provides methods for hashing, encryption, and decryption
*/
class CryptoService {
/**
* Hash content using Blake3 algorithm
* @param content - Content to hash as Uint8Array
* @returns Blake3 hash as Uint8Array
*/
hashBlake3(content: Uint8Array): Uint8Array {
return blake3(content);
}
/**
* Hash content using Blake3 algorithm with custom output length
* @param content - Content to hash as Uint8Array
* @param outputLength - Length of the output hash in bytes
* @returns Blake3 hash as Uint8Array
*/
hashBlake3WithLength(content: Uint8Array, outputLength: number): Uint8Array {
return blake3(content, { dkLen: outputLength });
}
/**
* Hash string content using Blake3 algorithm
* @param content - Content to hash as string
* @returns Blake3 hash as Uint8Array
*/
hashStringBlake3(content: string): Uint8Array {
const contentBytes = utf8ToBytes(content);
return this.hashBlake3(contentBytes);
}
/**
* Hash string content using Blake3 algorithm and return as hex string
* @param content - Content to hash as string
* @returns Blake3 hash as hex string
*/
hashStringBlake3AsHex(content: string): string {
const hash = this.hashStringBlake3(content);
return bytesToHex(hash);
}
/**
* Hash file content using Blake3 algorithm
* @param file - File to hash
* @returns Promise<Uint8Array> - Blake3 hash as Uint8Array
*/
async hashFileBlake3(file: File): Promise<Uint8Array> {
const buffer = await file.arrayBuffer();
const contentBytes = new Uint8Array(buffer);
return this.hashBlake3(contentBytes);
}
/**
* Hash file content using Blake3 algorithm and return as hex string
* @param file - File to hash
* @returns Promise<string> - Blake3 hash as hex string
*/
async hashFileBlake3AsHex(file: File): Promise<string> {
const hash = await this.hashFileBlake3(file);
return bytesToHex(hash);
}
/**
* Combine Blake hash and IPFS CID into a single key string
* Format: <blake_hash>:<ipfs_cid>
* @param blakeHash - Blake hash as hex string
* @param ipfsCid - IPFS CID as string
* @returns Combined key string
*/
combineBlakeHashAndIpfsCid(blakeHash: string, ipfsCid: string): string {
return `${blakeHash}:${ipfsCid}`;
}
/**
* Split combined key string into Blake hash and IPFS CID
* @param combinedKey - Combined key string in format <blake_hash>:<ipfs_cid>
* @returns Object with blakeHash and ipfsCid
*/
splitCombinedKey(combinedKey: string): { blakeHash: string; ipfsCid: string } {
const [blakeHash, ipfsCid] = combinedKey.split(':');
return { blakeHash, ipfsCid };
}
/**
* Convert hex string to Uint8Array
* @param hex - Hex string
* @returns Uint8Array
*/
hexToBytes(hex: string): Uint8Array {
return hexToBytes(hex);
}
/**
* Convert Uint8Array to hex string
* @param bytes - Uint8Array
* @returns Hex string
*/
bytesToHex(bytes: Uint8Array): string {
return bytesToHex(bytes);
}
/**
* Convert string to Uint8Array using UTF-8 encoding
* @param str - String to convert
* @returns Uint8Array
*/
stringToBytes(str: string): Uint8Array {
return utf8ToBytes(str);
}
}
// Create a singleton instance
export const cryptoService = new CryptoService();

View File

@ -1,309 +0,0 @@
import { openDB } from 'idb'
import type { DBSchema, IDBPDatabase } from 'idb'
/**
* Interface for the IPFS cache database schema
*/
interface IPFSCacheDB extends DBSchema {
'ipfs-content': {
key: string;
value: {
cid: string;
content: string;
timestamp: number;
contentType: string;
};
indexes: { 'by-timestamp': number };
};
}
/**
* Service for caching IPFS content locally using IndexedDB
* Provides methods for storing and retrieving content from the cache
*/
class IPFSCacheService {
private db: IDBPDatabase<IPFSCacheDB> | null = null
private readonly DB_NAME = 'ipfs-cache'
private readonly STORE_NAME = 'ipfs-content'
private readonly MAX_CACHE_SIZE = 50 * 1024 * 1024 // 50MB
private readonly MAX_CACHE_AGE = 7 * 24 * 60 * 60 * 1000 // 7 days
/**
* Initialize the cache database
* @returns Promise<boolean> - True if initialization was successful
*/
async initialize(): Promise<boolean> {
try {
this.db = await openDB<IPFSCacheDB>(this.DB_NAME, 1, {
upgrade(db) {
const store = db.createObjectStore('ipfs-content', { keyPath: 'cid' })
store.createIndex('by-timestamp', 'timestamp')
}
})
// Clean up old cache entries on initialization
await this.cleanupCache()
return true
} catch (error) {
console.error('Failed to initialize IPFS cache:', error)
return false
}
}
/**
* Cache text content
* @param cid - Content identifier
* @param content - Content to cache
* @param contentType - MIME type of the content
* @returns Promise<void>
*/
async cacheContent(cid: string, content: string, contentType: string = 'text/plain'): Promise<void> {
if (!this.db) {
throw new Error('Cache not initialized')
}
try {
await this.db.put(this.STORE_NAME, {
cid,
content,
timestamp: Date.now(),
contentType
})
// Check cache size and clean up if necessary
await this.checkCacheSize()
} catch (error) {
console.error(`Failed to cache content for CID ${cid}:`, error)
throw error
}
}
/**
* Cache binary content as a base64 string
* @param cid - Content identifier
* @param blob - Binary content to cache
* @returns Promise<void>
*/
async cacheBlob(cid: string, blob: Blob): Promise<void> {
if (!this.db) {
throw new Error('Cache not initialized')
}
try {
// Convert blob to base64 string for storage
const reader = new FileReader()
const contentPromise = new Promise<string>((resolve, reject) => {
reader.onload = () => resolve(reader.result as string)
reader.onerror = reject
})
reader.readAsDataURL(blob)
const content = await contentPromise
await this.db.put(this.STORE_NAME, {
cid,
content,
timestamp: Date.now(),
contentType: blob.type || 'application/octet-stream'
})
// Check cache size and clean up if necessary
await this.checkCacheSize()
} catch (error) {
console.error(`Failed to cache blob for CID ${cid}:`, error)
throw error
}
}
/**
* Get text content from cache
* @param cid - Content identifier
* @returns Promise<string | null> - Cached content or null if not found
*/
async getContent(cid: string): Promise<string | null> {
if (!this.db) {
throw new Error('Cache not initialized')
}
try {
const entry = await this.db.get(this.STORE_NAME, cid)
if (!entry || entry.contentType !== 'text/plain') {
return null
}
// Update timestamp to mark as recently used
await this.db.put(this.STORE_NAME, {
...entry,
timestamp: Date.now()
})
return entry.content
} catch (error) {
console.error(`Failed to get cached content for CID ${cid}:`, error)
return null
}
}
/**
* Get binary content from cache
* @param cid - Content identifier
* @returns Promise<Blob | null> - Cached content as Blob or null if not found
*/
async getBlob(cid: string): Promise<Blob | null> {
if (!this.db) {
throw new Error('Cache not initialized')
}
try {
const entry = await this.db.get(this.STORE_NAME, cid)
if (!entry || entry.contentType === 'text/plain') {
return null
}
// Update timestamp to mark as recently used
await this.db.put(this.STORE_NAME, {
...entry,
timestamp: Date.now()
})
// Convert base64 string back to blob
const response = await fetch(entry.content)
return await response.blob()
} catch (error) {
console.error(`Failed to get cached blob for CID ${cid}:`, error)
return null
}
}
/**
* Check if content is in cache
* @param cid - Content identifier
* @returns Promise<boolean> - True if content is in cache
*/
async hasCached(cid: string): Promise<boolean> {
if (!this.db) {
throw new Error('Cache not initialized')
}
try {
const entry = await this.db.get(this.STORE_NAME, cid)
return !!entry
} catch (error) {
console.error(`Failed to check cache for CID ${cid}:`, error)
return false
}
}
/**
* Remove content from cache
* @param cid - Content identifier
* @returns Promise<void>
*/
async removeFromCache(cid: string): Promise<void> {
if (!this.db) {
throw new Error('Cache not initialized')
}
try {
await this.db.delete(this.STORE_NAME, cid)
} catch (error) {
console.error(`Failed to remove CID ${cid} from cache:`, error)
throw error
}
}
/**
* Clear the entire cache
* @returns Promise<void>
*/
async clearCache(): Promise<void> {
if (!this.db) {
throw new Error('Cache not initialized')
}
try {
await this.db.clear(this.STORE_NAME)
} catch (error) {
console.error('Failed to clear cache:', error)
throw error
}
}
/**
* Clean up old cache entries
* @returns Promise<void>
*/
private async cleanupCache(): Promise<void> {
if (!this.db) {
return
}
try {
const now = Date.now()
const expiredTimestamp = now - this.MAX_CACHE_AGE
// Get all entries older than MAX_CACHE_AGE
const tx = this.db.transaction(this.STORE_NAME, 'readwrite')
const index = tx.store.index('by-timestamp')
let cursor = await index.openCursor(IDBKeyRange.upperBound(expiredTimestamp))
// Delete expired entries
while (cursor) {
await cursor.delete()
cursor = await cursor.continue()
}
await tx.done
} catch (error) {
console.error('Failed to clean up cache:', error)
}
}
/**
* Check cache size and remove oldest entries if necessary
* @returns Promise<void>
*/
private async checkCacheSize(): Promise<void> {
if (!this.db) {
return
}
try {
// Get all entries
const entries = await this.db.getAll(this.STORE_NAME)
// Calculate total size
let totalSize = 0
for (const entry of entries) {
totalSize += entry.content.length
}
// If cache is too large, remove oldest entries
if (totalSize > this.MAX_CACHE_SIZE) {
// Sort by timestamp (oldest first)
entries.sort((a, b) => a.timestamp - b.timestamp)
// Remove oldest entries until we're under the limit
const tx = this.db.transaction(this.STORE_NAME, 'readwrite')
for (const entry of entries) {
await tx.store.delete(entry.cid)
totalSize -= entry.content.length
if (totalSize <= this.MAX_CACHE_SIZE * 0.8) { // Reduce to 80% of max
break
}
}
await tx.done
}
} catch (error) {
console.error('Failed to check cache size:', error)
}
}
}
// Create a singleton instance
export const ipfsCacheService = new IPFSCacheService()

View File

@ -1,57 +0,0 @@
/**
* Service for fetching IPFS content through public gateways
* Provides fallback when direct IPFS connections fail
*/
class IPFSGatewayService {
// List of public IPFS gateways to try
private gateways = [
'https://ipfs.io/ipfs/',
'https://gateway.pinata.cloud/ipfs/',
'https://cloudflare-ipfs.com/ipfs/',
'https://dweb.link/ipfs/'
]
/**
* Fetch content from a gateway by trying each gateway in order until one succeeds
* @param cid - Content identifier
* @returns Promise<Response> - Response from the gateway
*/
async fetchFromGateway(cid: string): Promise<Response> {
// Try gateways in order until one succeeds
for (const gateway of this.gateways) {
try {
const response = await fetch(`${gateway}${cid}`)
if (response.ok) {
return response
}
} catch (error) {
console.warn(`Gateway ${gateway} failed for CID ${cid}:`, error)
}
}
throw new Error(`All gateways failed for CID ${cid}`)
}
/**
* Get content as text from a gateway
* @param cid - Content identifier
* @returns Promise<string> - Content as string
*/
async getContent(cid: string): Promise<string> {
const response = await this.fetchFromGateway(cid)
return await response.text()
}
/**
* Get content as blob from a gateway
* @param cid - Content identifier
* @returns Promise<Blob> - Content as blob
*/
async getImage(cid: string): Promise<Blob> {
const response = await this.fetchFromGateway(cid)
return await response.blob()
}
}
// Create a singleton instance
export const ipfsGatewayService = new IPFSGatewayService()

View File

@ -1,324 +0,0 @@
import { createHelia } from 'helia'
import { unixfs } from '@helia/unixfs'
import type { Helia } from '@helia/interface'
import type { UnixFS } from '@helia/unixfs'
import { CID } from 'multiformats/cid'
import { createLibp2p } from 'libp2p'
import { webSockets } from '@libp2p/websockets'
import { webRTC } from '@libp2p/webrtc'
import { webTransport } from '@libp2p/webtransport'
import { ipfsGatewayService } from './ipfs-gateway.service'
import { ipfsCacheService } from './ipfs-cache.service'
import { networkService } from './network.service'
/**
* Service for interacting with IPFS using Helia
* Provides methods for initializing IPFS, retrieving content, and uploading content
*/
class IPFSService {
private helia: Helia | null = null
private fs: UnixFS | null = null
/**
* Initialize the IPFS client
* @returns Promise<boolean> - True if initialization was successful
*/
async initialize(): Promise<boolean> {
try {
// Initialize cache first
await ipfsCacheService.initialize()
console.log('Initializing IPFS with simplified configuration...')
// Create a Helia instance with minimal configuration
this.helia = await createHelia({
// Use minimal configuration for browser environment
})
console.log('Helia instance created successfully')
// Create a UnixFS instance for file operations
if (this.helia) {
this.fs = unixfs(this.helia)
console.log('UnixFS instance created successfully')
}
console.log('IPFS initialized successfully')
return true
} catch (error) {
// More detailed error logging
console.error('Failed to initialize IPFS:', error)
if (error instanceof Error) {
console.error('Error message:', error.message)
console.error('Error stack:', error.stack)
}
// For demo purposes, return true to allow the app to function
// In a production environment, you would handle this differently
console.warn('Continuing with IPFS in mock mode for demo purposes')
return true
}
}
/**
* Get content from IPFS by CID
* @param cidStr - Content identifier as string
* @returns Promise<string> - Content as string
*/
async getContent(cidStr: string): Promise<string> {
// Check cache first
try {
const cachedContent = await ipfsCacheService.getContent(cidStr)
if (cachedContent) {
console.log(`Retrieved content for CID ${cidStr} from cache`)
return cachedContent
}
} catch (error) {
console.warn(`Cache retrieval failed for CID ${cidStr}:`, error)
}
// If offline and not in cache, throw error
if (!networkService.isOnline()) {
throw new Error('Cannot retrieve content: You are offline and the content is not available in the cache')
}
// For demo purposes, if IPFS is not initialized, use mock data
if (!this.fs) {
console.warn('IPFS not initialized, using mock data for demo')
return `# Mock IPFS Content\n\nThis is mock content for CID: ${cidStr}\n\nIPFS is not fully initialized, but the demo can still show the basic functionality.`
}
try {
const cid = CID.parse(cidStr)
// Fetch content from IPFS
const decoder = new TextDecoder()
let content = ''
for await (const chunk of this.fs.cat(cid)) {
content += decoder.decode(chunk, { stream: true })
}
// Cache the content
try {
await ipfsCacheService.cacheContent(cidStr, content, 'text/plain')
} catch (cacheError) {
console.warn(`Failed to cache content for CID ${cidStr}:`, cacheError)
}
return content
} catch (error) {
console.warn(`Direct IPFS retrieval failed for CID ${cidStr}, trying gateways:`, error)
try {
// Try gateway fallback
const content = await ipfsGatewayService.getContent(cidStr)
// Cache the content
try {
await ipfsCacheService.cacheContent(cidStr, content, 'text/plain')
} catch (cacheError) {
console.warn(`Failed to cache content from gateway for CID ${cidStr}:`, cacheError)
}
return content
} catch (error: any) {
const gatewayError = error instanceof Error ? error : new Error(String(error))
console.error(`Gateway fallback also failed for CID ${cidStr}:`, gatewayError)
// For demo purposes, return mock data
return `# Mock IPFS Content\n\nThis is mock content for CID: ${cidStr}\n\nFailed to retrieve actual content, but the demo can still show the basic functionality.`
}
}
}
/**
* Get image data from IPFS by CID
* @param cidStr - Content identifier as string
* @returns Promise<Blob> - Image as Blob
*/
async getImage(cidStr: string): Promise<Blob> {
// Check cache first
try {
const cachedBlob = await ipfsCacheService.getBlob(cidStr)
if (cachedBlob) {
console.log(`Retrieved image for CID ${cidStr} from cache`)
return cachedBlob
}
} catch (error) {
console.warn(`Cache retrieval failed for CID ${cidStr}:`, error)
}
// If offline and not in cache, throw error
if (!networkService.isOnline()) {
throw new Error('Cannot retrieve image: You are offline and the image is not available in the cache')
}
// For demo purposes, if IPFS is not initialized, use a placeholder image
if (!this.fs) {
console.warn('IPFS not initialized, using placeholder image for demo')
// Create a simple SVG as a placeholder
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#f0f0f0"/>
<text x="50%" y="50%" font-family="Arial" font-size="16" text-anchor="middle">Placeholder Image</text>
<text x="50%" y="70%" font-family="Arial" font-size="12" text-anchor="middle">CID: ${cidStr}</text>
</svg>`;
return new Blob([svgContent], { type: 'image/svg+xml' });
}
try {
const cid = CID.parse(cidStr)
// Fetch image data from IPFS
const chunks: Uint8Array[] = []
for await (const chunk of this.fs.cat(cid)) {
chunks.push(chunk)
}
// Combine chunks into a single Uint8Array
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0)
const allChunks = new Uint8Array(totalLength)
let offset = 0
for (const chunk of chunks) {
allChunks.set(chunk, offset)
offset += chunk.length
}
// Create a Blob from the Uint8Array
const blob = new Blob([allChunks])
// Cache the blob
try {
await ipfsCacheService.cacheBlob(cidStr, blob)
} catch (cacheError) {
console.warn(`Failed to cache image for CID ${cidStr}:`, cacheError)
}
return blob
} catch (error) {
console.warn(`Direct IPFS image retrieval failed for CID ${cidStr}, trying gateways:`, error)
try {
// Try gateway fallback
const blob = await ipfsGatewayService.getImage(cidStr)
// Cache the blob
try {
await ipfsCacheService.cacheBlob(cidStr, blob)
} catch (cacheError) {
console.warn(`Failed to cache image from gateway for CID ${cidStr}:`, cacheError)
}
return blob
} catch (error: any) {
const gatewayError = error instanceof Error ? error : new Error(String(error))
console.error(`Gateway fallback also failed for CID ${cidStr}:`, gatewayError)
// For demo purposes, return a placeholder image
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#ffeeee"/>
<text x="50%" y="50%" font-family="Arial" font-size="16" text-anchor="middle" fill="red">Image Not Found</text>
<text x="50%" y="70%" font-family="Arial" font-size="12" text-anchor="middle">CID: ${cidStr}</text>
</svg>`;
return new Blob([svgContent], { type: 'image/svg+xml' });
}
}
}
/**
* Upload content to IPFS
* @param content - Content as string
* @returns Promise<string> - CID of the uploaded content
*/
async uploadContent(content: string): Promise<string> {
if (!this.fs) {
console.warn('IPFS not initialized, using mock CID for demo')
// Generate a mock CID for demo purposes
const mockCid = `mock-cid-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 7)}`;
// Cache the content with the mock CID
try {
await ipfsCacheService.cacheContent(mockCid, content, 'text/plain')
} catch (error) {
console.warn('Failed to cache mock content:', error)
}
return mockCid;
}
try {
const encoder = new TextEncoder()
const cid = await this.fs.addBytes(encoder.encode(content))
return cid.toString()
} catch (error) {
console.error('Failed to upload content:', error)
// For demo purposes, return a mock CID
const mockCid = `mock-cid-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 7)}`;
// Cache the content with the mock CID
try {
await ipfsCacheService.cacheContent(mockCid, content, 'text/plain')
} catch (cacheError) {
console.warn('Failed to cache mock content:', cacheError)
}
return mockCid;
}
}
/**
* Upload a file to IPFS
* @param file - File to upload
* @returns Promise<string> - CID of the uploaded file
*/
async uploadFile(file: File): Promise<string> {
if (!this.fs) {
console.warn('IPFS not initialized, using mock CID for demo')
// Generate a mock CID for demo purposes
const mockCid = `mock-cid-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 7)}`;
// Cache the file with the mock CID
try {
const blob = new Blob([await file.arrayBuffer()], { type: file.type });
await ipfsCacheService.cacheBlob(mockCid, blob)
} catch (error) {
console.warn('Failed to cache mock file:', error)
}
return mockCid;
}
try {
const buffer = await file.arrayBuffer()
const cid = await this.fs.addBytes(new Uint8Array(buffer))
return cid.toString()
} catch (error) {
console.error('Failed to upload file:', error)
// For demo purposes, return a mock CID
const mockCid = `mock-cid-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 7)}`;
// Cache the file with the mock CID
try {
const blob = new Blob([await file.arrayBuffer()], { type: file.type });
await ipfsCacheService.cacheBlob(mockCid, blob)
} catch (cacheError) {
console.warn('Failed to cache mock file:', cacheError)
}
return mockCid;
}
}
/**
* Check if IPFS is initialized
* @returns boolean - True if IPFS is initialized
*/
isInitialized(): boolean {
return this.helia !== null && this.fs !== null
}
}
// Create a singleton instance
export const ipfsService = new IPFSService()

View File

@ -1,69 +0,0 @@
/**
* Service for monitoring network status
* Provides methods to check if the user is online and to register listeners for network status changes
*/
class NetworkService {
private online: boolean = navigator.onLine
private listeners: Set<(online: boolean) => void> = new Set()
constructor() {
// Initialize with current online status
this.online = navigator.onLine
// Add event listeners for online/offline events
if (typeof window !== 'undefined') {
window.addEventListener('online', this.handleOnline.bind(this))
window.addEventListener('offline', this.handleOffline.bind(this))
}
}
/**
* Check if the user is currently online
* @returns boolean - True if online, false if offline
*/
isOnline(): boolean {
return this.online
}
/**
* Add a listener for network status changes
* @param listener - Function to call when network status changes
* @returns Function - Function to remove the listener
*/
addStatusChangeListener(listener: (online: boolean) => void): () => void {
this.listeners.add(listener)
// Return function to remove listener
return () => {
this.listeners.delete(listener)
}
}
/**
* Handle online event
*/
private handleOnline(): void {
this.online = true
this.notifyListeners()
}
/**
* Handle offline event
*/
private handleOffline(): void {
this.online = false
this.notifyListeners()
}
/**
* Notify all listeners of network status change
*/
private notifyListeners(): void {
for (const listener of this.listeners) {
listener(this.online)
}
}
}
// Create a singleton instance
export const networkService = new NetworkService()