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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 103 additions & 63 deletions app/api/snippets/[id]/verify/route.ts
Original file line number Diff line number Diff line change
@@ -1,142 +1,182 @@
import { NextRequest, NextResponse } from 'next/server';
import { getSnippetWithHash, verifySnippetIntegrity, storeSnippetHash } from '@/lib/db';
import { generateSnippetHash } from '@/lib/hash';
import { submitHashToStellar } from '@/lib/stellar';
import { NextRequest, NextResponse } from "next/server";
import {
getSnippetWithHash,
verifySnippetIntegrity,
storeSnippetHash,
} from "@/lib/db";
import { generateSnippetHash } from "@/lib/hash";
import { submitHashToStellar } from "@/lib/stellar";

/**
* POST /api/snippets/[id]/verify
* Store the snippet hash on the Stellar blockchain
*
* Body: { secretKey?: string } - Optional secret key for signing (for future SDK integration)
*
* Anchors the snippet's creation timestamp + content hash on the Stellar
* blockchain, providing immutable proof-of-existence.
*
* - The snippet's `created_at` timestamp is embedded in the Stellar memo,
* permanently linking the snippet ID, its content hash, and its creation
* time to a specific on-chain transaction.
* - Once stored, the record cannot be overwritten (immutability enforced in DB).
*
* Body: { secretKey?: string }
*/
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params;

// Get the snippet

const snippet = await getSnippetWithHash(id);

if (!snippet) {
return NextResponse.json({ error: "Snippet not found" }, { status: 404 });
}

// Reject if already verified — timestamps are immutable
if (snippet.on_chain_hash) {
return NextResponse.json(
{ error: 'Snippet not found' },
{ status: 404 }
{
error:
"Snippet is already verified on-chain. Timestamps are immutable.",
data: {
snippetId: id,
onChainHash: snippet.on_chain_hash,
transactionHash: snippet.transaction_hash,
verifiedAt: snippet.verified_at,
createdAt: snippet.created_at,
},
},
{ status: 409 },
);
}
// Generate SHA-256 hash of current snippet content

// Generate SHA-256 hash of snippet content
const onChainHash = generateSnippetHash(
snippet.title,
snippet.description || '',
snippet.description || "",
snippet.code,
snippet.language,
snippet.tags || []
snippet.tags || [],
);

console.log('[v0] Generated hash for snippet:', { id, onChainHash });

// Submit hash to Stellar blockchain
// In production, you would use the secret key from the request
// For now, we use a mock transaction
const secretKey = process.env.STELLAR_SECRET_KEY || '';


// Use the snippet's original creation timestamp as the proof-of-existence anchor
const createdAt: string =
snippet.created_at instanceof Date
? snippet.created_at.toISOString()
: String(snippet.created_at);

console.log("[verify] Anchoring snippet on Stellar:", {
id,
onChainHash,
createdAt,
});

// Submit to Stellar — memo encodes snippetId + contentHash + createdAt
const secretKey = process.env.STELLAR_SECRET_KEY || "";
const stellarResult = await submitHashToStellar(
secretKey,
onChainHash,
id
id,
createdAt,
);

if (!stellarResult.success) {
return NextResponse.json(
{ error: 'Failed to submit hash to Stellar blockchain', details: stellarResult.error },
{ status: 500 }
{
error: "Failed to submit to Stellar blockchain",
details: stellarResult.error,
},
{ status: 502 },
);
}

// Store the hash and transaction hash in the database
// This ensures immutability - once stored, hashes cannot be altered

// Persist hash + tx hash — immutability enforced inside storeSnippetHash
const updatedSnippet = await storeSnippetHash(
id,
onChainHash,
stellarResult.transactionHash!
stellarResult.transactionHash!,
);

return NextResponse.json({
success: true,
message: 'Snippet hash stored on Stellar blockchain',
message: "Snippet creation timestamp anchored on Stellar blockchain",
data: {
snippetId: id,
onChainHash,
transactionHash: stellarResult.transactionHash,
verifiedAt: updatedSnippet.verified_at
}
stellarMemo: stellarResult.memo,
createdAt,
verifiedAt: updatedSnippet.verified_at,
},
});
} catch (error) {
console.error('[v0] Error storing hash on blockchain:', error);
} catch (error: any) {
console.error("[verify] Error anchoring snippet:", error);

// Surface immutability violations as 409
if (error?.message?.includes("immutable")) {
return NextResponse.json({ error: error.message }, { status: 409 });
}

return NextResponse.json(
{ error: 'Failed to store hash on blockchain' },
{ status: 500 }
{ error: "Failed to anchor snippet on blockchain" },
{ status: 500 },
);
}
}

/**
* GET /api/snippets/[id]/verify
* Verify snippet integrity by comparing stored hash with current content
*
* Verifies snippet integrity by comparing the current content hash against
* the hash stored on the Stellar blockchain at creation time.
*/
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params;

// Get the snippet with its on-chain hash

const snippet = await getSnippetWithHash(id);

if (!snippet) {
return NextResponse.json(
{ error: 'Snippet not found' },
{ status: 404 }
);
return NextResponse.json({ error: "Snippet not found" }, { status: 404 });
}

if (!snippet.on_chain_hash) {
return NextResponse.json({
verified: false,
snippetId: id,
message: 'This snippet has not been verified on the blockchain yet',
message: "This snippet has not been anchored on the blockchain yet.",
onChainHash: null,
transactionHash: null,
verifiedAt: null
verifiedAt: null,
createdAt: snippet.created_at,
});
}

// Verify integrity by comparing current content with stored hash

const result = await verifySnippetIntegrity(
id,
snippet.title,
snippet.description || '',
snippet.description || "",
snippet.code,
snippet.language,
snippet.tags || []
snippet.tags || [],
);

return NextResponse.json({
verified: result.isValid,
snippetId: id,
message: result.message,
onChainHash: snippet.on_chain_hash,
transactionHash: snippet.transaction_hash,
verifiedAt: snippet.verified_at
verifiedAt: snippet.verified_at,
createdAt: snippet.created_at,
});
} catch (error) {
console.error('[v0] Error verifying snippet:', error);
console.error("[verify] Error verifying snippet:", error);
return NextResponse.json(
{ error: 'Failed to verify snippet integrity' },
{ status: 500 }
{ error: "Failed to verify snippet integrity" },
{ status: 500 },
);
}
}
147 changes: 147 additions & 0 deletions lib/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,150 @@ export async function restoreVersion(
throw error;
}
}

// ============ NFT Functions ============

export async function updateSnippetNft(
id: string,
txHash: string,
metadata: object,
) {
try {
const result = await sql`
UPDATE snippets
SET nft_transaction_hash = ${txHash},
nft_metadata = ${JSON.stringify(metadata)},
updated_at = ${new Date()}
WHERE id = ${id}
RETURNING *
`;
return result[0] as any;
} catch (error) {
console.error("Error updating snippet NFT:", error);
throw error;
}
}

// ============ On-Chain Timestamp Verification Functions ============

/**
* Fetch a snippet including its on-chain verification fields.
*/
export async function getSnippetWithHash(id: string) {
try {
const result = await sql`
SELECT id, title, description, code, language, tags,
owner_wallet_address, created_at, updated_at,
on_chain_hash, transaction_hash, verified_at
FROM snippets
WHERE id = ${id}
`;
return (result[0] as any) || null;
} catch (error) {
console.error("Error fetching snippet with hash:", error);
throw error;
}
}

/**
* Persist the on-chain hash and Stellar transaction hash for a snippet.
* Immutability: if on_chain_hash is already set, this call is rejected
* to prevent overwriting an existing proof-of-existence record.
*/
export async function storeSnippetHash(
id: string,
onChainHash: string,
transactionHash: string,
) {
try {
// Guard: do not overwrite an existing verified record
const existing = await sql`
SELECT on_chain_hash FROM snippets WHERE id = ${id}
`;
if (existing[0]?.on_chain_hash) {
throw new Error(
"Snippet is already verified on-chain. Timestamps are immutable.",
);
}

const verifiedAt = new Date();
const result = await sql`
UPDATE snippets
SET on_chain_hash = ${onChainHash},
transaction_hash = ${transactionHash},
verified_at = ${verifiedAt}
WHERE id = ${id}
RETURNING id, on_chain_hash, transaction_hash, verified_at
`;
return result[0] as any;
} catch (error) {
console.error("Error storing snippet hash:", error);
throw error;
}
}

/**
* Compare the snippet's current content hash against the stored on-chain hash.
*/
export async function verifySnippetIntegrity(
id: string,
title: string,
description: string,
code: string,
language: string,
tags: string[],
): Promise<{ isValid: boolean; message: string }> {
try {
const { generateSnippetHash } = await import("@/lib/hash");

const snippet = await sql`
SELECT on_chain_hash FROM snippets WHERE id = ${id}
`;

if (!snippet[0]?.on_chain_hash) {
return {
isValid: false,
message: "Snippet has not been verified on-chain yet.",
};
}

const currentHash = generateSnippetHash(
title,
description,
code,
language,
tags,
);

const isValid = currentHash === snippet[0].on_chain_hash;

return {
isValid,
message: isValid
? "Snippet integrity verified — content matches the on-chain record."
: "Integrity check failed — snippet content has been modified since on-chain verification.",
};
} catch (error) {
console.error("Error verifying snippet integrity:", error);
throw error;
}
}

/**
* Return all snippets that have been verified on the Stellar blockchain.
*/
export async function getVerifiedSnippets() {
try {
const result = await sql`
SELECT id, title, language, on_chain_hash, transaction_hash, verified_at, created_at
FROM snippets
WHERE on_chain_hash IS NOT NULL
AND is_deleted = false
ORDER BY verified_at DESC
`;
return result as any[];
} catch (error) {
console.error("Error fetching verified snippets:", error);
throw error;
}
}
Loading