Skip to content

Commit 839e572

Browse files
author
teycir
committed
feat: add cryptographic receipt download feature
- Added `receipt` field to upload result state and API response type - Implemented UI button to download receipt as JSON file with HMAC signature proof - Enhances verifiability of seal creation by providing downloadable proof-of-creation receipt
1 parent c293c9d commit 839e572

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

app/api/verify-receipt/route.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { NextRequest } from 'next/server';
2+
import { jsonResponse } from '@/lib/apiHandler';
3+
import { createAPIRoute } from '@/lib/routeHelper';
4+
5+
export async function POST(request: NextRequest) {
6+
return createAPIRoute(async ({ container }) => {
7+
const body = await request.json() as { receipt: any };
8+
const { receipt } = body;
9+
10+
if (!receipt?.sealId || !receipt?.blobHash || !receipt?.signature) {
11+
return jsonResponse({ valid: false, error: 'Invalid receipt format' }, 400);
12+
}
13+
14+
const data = `${receipt.sealId}:${receipt.blobHash}:${receipt.unlockTime}:${receipt.createdAt}`;
15+
const encoder = new TextEncoder();
16+
17+
const key = await crypto.subtle.importKey(
18+
'raw',
19+
encoder.encode(container.masterKey),
20+
{ name: 'HMAC', hash: 'SHA-256' },
21+
false,
22+
['verify']
23+
);
24+
25+
const sigBytes = new Uint8Array(receipt.signature.match(/.{1,2}/g).map((byte: string) => parseInt(byte, 16)));
26+
const valid = await crypto.subtle.verify('HMAC', key, sigBytes, encoder.encode(data));
27+
28+
return jsonResponse({ valid, sealId: receipt.sealId });
29+
})(request);
30+
}

app/page.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export default function HomePage() {
8080
publicUrl: string;
8181
pulseUrl?: string;
8282
pulseToken?: string;
83+
receipt?: any;
8384
} | null>(null);
8485

8586
// Use a ref for file input fallback (kept even if dropzone exists for accessibility/fallback)
@@ -262,7 +263,7 @@ export default function HomePage() {
262263
body: formData,
263264
});
264265

265-
const data = await response.json() as { success: boolean; publicUrl: string; pulseToken?: string; error?: string };
266+
const data = await response.json() as { success: boolean; publicUrl: string; pulseToken?: string; receipt?: any; error?: string };
266267

267268
if (data.success) {
268269
const origin = globalThis.window ? globalThis.window.location.origin : '';
@@ -273,6 +274,7 @@ export default function HomePage() {
273274
publicUrl,
274275
pulseUrl: data.pulseToken ? `${origin}/pulse` : undefined,
275276
pulseToken: data.pulseToken,
277+
receipt: data.receipt,
276278
});
277279
toast.dismiss(loadingToast);
278280
toast.success('Seal created successfully!');
@@ -355,6 +357,29 @@ export default function HomePage() {
355357
</p>
356358
</div>
357359
)}
360+
361+
{result.receipt && (
362+
<div className="border-t border-neon-green/20 pt-4">
363+
<Button
364+
onClick={() => {
365+
const blob = new Blob([JSON.stringify(result.receipt, null, 2)], { type: 'application/json' });
366+
const url = URL.createObjectURL(blob);
367+
const a = document.createElement('a');
368+
a.href = url;
369+
a.download = `timeseal-receipt-${result.receipt.sealId}.json`;
370+
a.click();
371+
URL.revokeObjectURL(url);
372+
toast.success('Receipt downloaded');
373+
}}
374+
className="w-full bg-neon-green/10"
375+
>
376+
📄 DOWNLOAD CRYPTOGRAPHIC RECEIPT
377+
</Button>
378+
<p className="text-xs text-neon-green/50 mt-2 text-center">
379+
Proof of seal creation with HMAC signature
380+
</p>
381+
</div>
382+
)}
358383
</Card>
359384

360385
<Button

0 commit comments

Comments
 (0)