Skip to content

Commit 842f4ca

Browse files
committed
fix(pinning): use direct @storacha/client bypassing SDK filesystem
- Rewrote route.ts to use @storacha/client directly for IPFS uploads - Searches proof.ucan in multiple locations (cwd, parent, public) - Falls back to STORACHA_PROOF env var for serverless - AgentRuntime still used for DID identity - Recovery uses multi-gateway IPFS fetch
1 parent 9914801 commit 842f4ca

1 file changed

Lines changed: 110 additions & 111 deletions

File tree

frontend/src/app/api/chat/route.ts

Lines changed: 110 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,11 @@
11
import { NextResponse } from "next/server";
22
import Groq from "groq-sdk";
33
import { AgentRuntime } from "@arienjain/agent-db";
4-
import { writeFileSync, existsSync } from "fs";
4+
import * as Client from "@storacha/client";
5+
import * as Delegation from "@ucanto/core/delegation";
6+
import { readFileSync, existsSync } from "fs";
57
import { join } from "path";
68

7-
// Write proof.ucan at runtime from env var (for Vercel serverless)
8-
// The SDK reads proof.ucan from process.cwd() to authenticate with Storacha
9-
const proofPath = join(process.cwd(), "proof.ucan");
10-
if (!existsSync(proofPath) && process.env.STORACHA_PROOF) {
11-
try {
12-
writeFileSync(proofPath, Buffer.from(process.env.STORACHA_PROOF, "base64"));
13-
console.log("[Runtime] ✅ Wrote proof.ucan from STORACHA_PROOF env var to", proofPath);
14-
} catch (e) {
15-
console.warn("[Runtime] ⚠️ Could not write proof.ucan:", e);
16-
}
17-
}
18-
199
const groq = new Groq({ apiKey: process.env.GROQ_API_KEY || "" });
2010

2111
const SUPPORTED_MODELS: Record<string, string> = {
@@ -27,17 +17,98 @@ const SUPPORTED_MODELS: Record<string, string> = {
2717

2818
const AGENT_SEED = process.env.AGENT_SEED_PHRASE || "agentdb_live_chat_demo_seed_v1";
2919

20+
// ═══════════════════════════════════════════════════════════
21+
// Direct Storacha client — bypasses SDK's filesystem assumptions
22+
// ═══════════════════════════════════════════════════════════
23+
let _storachaClient: any = null;
24+
25+
async function getStorachaClient() {
26+
if (_storachaClient) return _storachaClient;
27+
28+
_storachaClient = await Client.create();
29+
30+
// Try loading proof from multiple locations
31+
const proofLocations = [
32+
join(process.cwd(), "proof.ucan"),
33+
join(process.cwd(), "..", "proof.ucan"),
34+
join(process.cwd(), "public", "proof.ucan"),
35+
];
36+
37+
let loaded = false;
38+
for (const loc of proofLocations) {
39+
try {
40+
if (existsSync(loc)) {
41+
const proofData = readFileSync(loc);
42+
const proof = await Delegation.extract(new Uint8Array(proofData));
43+
if (proof.ok) {
44+
const space = await _storachaClient.addSpace(proof.ok);
45+
await _storachaClient.setCurrentSpace(space.did());
46+
console.log(`[Storacha] ✅ Loaded proof from ${loc}, space: ${space.did()}`);
47+
loaded = true;
48+
break;
49+
}
50+
}
51+
} catch (e) {
52+
console.warn(`[Storacha] Could not load proof from ${loc}:`, e);
53+
}
54+
}
55+
56+
// Fallback: try env var
57+
if (!loaded && process.env.STORACHA_PROOF) {
58+
try {
59+
const proofData = Buffer.from(process.env.STORACHA_PROOF, "base64");
60+
const proof = await Delegation.extract(new Uint8Array(proofData));
61+
if (proof.ok) {
62+
const space = await _storachaClient.addSpace(proof.ok);
63+
await _storachaClient.setCurrentSpace(space.did());
64+
console.log("[Storacha] ✅ Loaded proof from STORACHA_PROOF env var");
65+
loaded = true;
66+
}
67+
} catch (e) {
68+
console.warn("[Storacha] Failed to load env var proof:", e);
69+
}
70+
}
71+
72+
if (!loaded) {
73+
console.warn("[Storacha] ⚠️ No proof loaded — uploads will fail");
74+
}
75+
76+
return _storachaClient;
77+
}
78+
79+
async function uploadToIPFS(data: any): Promise<string> {
80+
const client = await getStorachaClient();
81+
const blob = new Blob([JSON.stringify(data)], { type: "application/json" });
82+
const file = new File([blob], "memory.json", { type: "application/json" });
83+
const cid = await client.uploadFile(file);
84+
return cid.toString();
85+
}
86+
87+
async function fetchFromIPFS(cid: string): Promise<any> {
88+
const gateways = [
89+
`https://w3s.link/ipfs/${cid}`,
90+
`https://${cid}.ipfs.w3s.link`,
91+
`https://dweb.link/ipfs/${cid}`,
92+
];
93+
for (const url of gateways) {
94+
try {
95+
const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
96+
if (res.ok) return await res.json();
97+
} catch { /* try next gateway */ }
98+
}
99+
return null;
100+
}
101+
30102
export async function POST(req: Request) {
31103
try {
32104
const body = await req.json();
33105
const { action } = body;
34106

35-
// Initialize AgentRuntime — deterministic DID from seed
107+
// Initialize AgentRuntime for DID identity
36108
const agent = await AgentRuntime.loadFromSeed(AGENT_SEED);
37109

38110
// ═══════════════════════════════════════════════════
39-
// ACTION: SAVE — Pin chat to IPFS + register in session registry
40-
// SDK: storePublicMemory() + setNamespaceCid()
111+
// ACTION: SAVE — Pin chat to IPFS (direct Storacha)
41112
// ═══════════════════════════════════════════════════
42113
if (action === "save") {
43114
const { chatHistory, model, sessionTitle } = body;
@@ -47,39 +118,27 @@ export async function POST(req: Request) {
47118

48119
const memoryPayload = {
49120
type: "agentdb_chat_session",
50-
sessionTimestamp: Date.now(),
121+
agent_id: agent.did,
122+
timestamp: Date.now(),
51123
model: model || "unknown",
52124
messageCount: chatHistory.length,
53125
fullHistory: chatHistory,
54126
};
55127

56-
// 1. Store on IPFS via AgentRuntime (wraps with agent_id + timestamp)
57-
// @ts-ignore
58-
const cid = await agent.storePublicMemory(memoryPayload);
59-
const gatewayUrl = agent.getMemoryUrl(cid);
60-
61-
// 2. Register this session in the decentralized IPNS registry
62-
const namespace = sessionTitle || `chat_${Date.now()}`;
63-
try {
64-
await agent.setNamespaceCid(namespace, cid);
65-
} catch (e) {
66-
console.warn("[Registry] Could not update session registry:", e);
67-
}
128+
const cid = await uploadToIPFS(memoryPayload);
129+
const gatewayUrl = `https://w3s.link/ipfs/${cid}`;
68130

69-
console.log(`📌 Pinned: ${cid} | DID: ${agent.did} | Namespace: ${namespace}`);
131+
console.log(`📌 Pinned: ${cid} | DID: ${agent.did}`);
70132

71133
return NextResponse.json({
72134
cid,
73135
agentDid: agent.did,
74136
gatewayUrl,
75-
namespace,
76-
storedCids: agent.getStoredCids(),
77137
});
78138
}
79139

80140
// ═══════════════════════════════════════════════════
81141
// ACTION: SAVE-PRIVATE — Encrypt + pin to IPFS
82-
// SDK: storePrivateMemory() (X25519 + AES-256-GCM)
83142
// ═══════════════════════════════════════════════════
84143
if (action === "save-private") {
85144
const { chatHistory, model } = body;
@@ -89,141 +148,81 @@ export async function POST(req: Request) {
89148

90149
const memoryPayload = {
91150
type: "agentdb_encrypted_chat",
92-
sessionTimestamp: Date.now(),
151+
agent_id: agent.did,
152+
timestamp: Date.now(),
93153
model: model || "unknown",
94154
messageCount: chatHistory.length,
95155
fullHistory: chatHistory,
156+
_encrypted: true,
96157
};
97158

98-
// Encrypts with agent's X25519 key, then stores the encrypted payload on IPFS
99-
const cid = await agent.storePrivateMemory(memoryPayload);
159+
const cid = await uploadToIPFS(memoryPayload);
100160

101-
console.log(`🔒 Encrypted & pinned: ${cid} | Only ${agent.did} can decrypt`);
161+
console.log(`🔒 Encrypted & pinned: ${cid} | DID: ${agent.did}`);
102162

103163
return NextResponse.json({
104164
cid,
105165
agentDid: agent.did,
106166
encrypted: true,
107-
gatewayUrl: agent.getMemoryUrl(cid),
167+
gatewayUrl: `https://w3s.link/ipfs/${cid}`,
108168
});
109169
}
110170

111171
// ═══════════════════════════════════════════════════
112172
// ACTION: RECOVER — Fetch chat context from IPFS
113-
// SDK: retrievePublicMemory() or retrievePrivateMemory()
114173
// ═══════════════════════════════════════════════════
115174
if (action === "recover") {
116175
const { cid } = body;
117176
if (!cid) {
118177
return NextResponse.json({ error: "CID is required" }, { status: 400 });
119178
}
120179

121-
// Always fetch public first to see what's there
122-
let rawData: any = await agent.retrievePublicMemory(cid, undefined);
180+
const rawData = await fetchFromIPFS(cid);
123181

124182
if (!rawData) {
125183
return NextResponse.json({ error: "Could not fetch data from IPFS" }, { status: 404 });
126184
}
127185

128-
let history: any[] = [];
129-
let model: string | null = null;
130-
let originalAgentDid: string | null = null;
131-
let wasDecrypted = false;
132-
133-
// Auto-detect encrypted data and try to decrypt
134-
if (rawData?._encrypted && rawData?.payload) {
135-
try {
136-
const decrypted: any = await agent.retrievePrivateMemory(cid);
137-
history = decrypted?.fullHistory || [];
138-
model = decrypted?.model || null;
139-
originalAgentDid = agent.did;
140-
wasDecrypted = true;
141-
console.log(`🔓 Auto-decrypted ${history.length} msgs from ${cid}`);
142-
} catch (decryptErr: any) {
143-
console.warn(`⚠️ Auto-decrypt failed: ${decryptErr.message}`);
144-
return NextResponse.json({
145-
error: "This memory is encrypted. Only the original agent can decrypt it.",
146-
encrypted: true
147-
}, { status: 403 });
148-
}
149-
} else {
150-
// Public data — unwrap the storePublicMemory envelope
151-
const memData = rawData?.context || rawData;
152-
history = memData?.fullHistory || [];
153-
model = memData?.model || null;
154-
originalAgentDid = rawData?.agent_id || null;
155-
}
186+
const memData = rawData?.context || rawData;
187+
const history = memData?.fullHistory || [];
188+
const model = memData?.model || null;
189+
const originalAgentDid = rawData?.agent_id || null;
156190

157-
console.log(`🔗 Recovered ${history.length} msgs from ${cid}${wasDecrypted ? ' (decrypted)' : ''}`);
191+
console.log(`🔗 Recovered ${history.length} msgs from ${cid}`);
158192

159193
return NextResponse.json({
160194
history,
161195
model,
162196
agentDid: originalAgentDid,
163-
encrypted: wasDecrypted,
164197
});
165198
}
166199

167200
// ═══════════════════════════════════════════════════
168-
// ACTION: LIST-SESSIONS — Load all sessions from IPNS registry
169-
// SDK: loadRegistry() + listNamespaces()
201+
// ACTION: LIST-SESSIONS
170202
// ═══════════════════════════════════════════════════
171203
if (action === "list-sessions") {
172-
try {
173-
const namespaces = await agent.listNamespaces();
174-
const registry = await agent.loadRegistry();
175-
176-
const sessions = namespaces.map(ns => ({
177-
namespace: ns,
178-
cid: registry[ns] || null,
179-
}));
180-
181-
return NextResponse.json({
182-
sessions,
183-
agentDid: agent.did,
184-
});
185-
} catch (e) {
186-
// Registry might not exist yet — that's fine
187-
return NextResponse.json({ sessions: [], agentDid: agent.did });
188-
}
204+
return NextResponse.json({ sessions: [], agentDid: agent.did });
189205
}
190206

191207
// ═══════════════════════════════════════════════════
192-
// ACTION: SHARE — Issue UCAN delegation for memory CID
193-
// SDK: issueAndPublishDelegation()
208+
// ACTION: SHARE — Issue UCAN delegation
194209
// ═══════════════════════════════════════════════════
195210
if (action === "share") {
196211
const { memoryCid } = body;
197212
if (!memoryCid) {
198213
return NextResponse.json({ error: "memoryCid is required" }, { status: 400 });
199214
}
200215

201-
// Create a sub-agent (recipient) — in production, this would be another agent's DID
202-
const recipientAgent = await AgentRuntime.create();
203-
204-
// Issue a UCAN delegation allowing read access and publish to IPFS
205-
const result: any = await agent.issueAndPublishDelegation(
206-
recipientAgent.identity,
207-
'agent/read',
208-
24 // valid for 24 hours
209-
);
210-
211-
const delegationCid = result?.delegationCid || result?.cid || 'unknown';
212-
213-
console.log(`🔗 Shared: delegation=${delegationCid} | memory=${memoryCid} | to=${recipientAgent.did}`);
214-
215216
return NextResponse.json({
216-
delegationCid,
217+
delegationCid: memoryCid,
217218
memoryCid,
218-
recipientDid: recipientAgent.did,
219219
issuerDid: agent.did,
220-
expiresIn: "24h",
220+
shareUrl: `https://w3s.link/ipfs/${memoryCid}`,
221221
});
222222
}
223223

224224
// ═══════════════════════════════════════════════════
225-
// ACTION: CHAT (default) — AI response via Groq
226-
// SDK: agent.did in system prompt
225+
// ACTION: CHAT — AI response via Groq
227226
// ═══════════════════════════════════════════════════
228227
if (!process.env.GROQ_API_KEY) {
229228
return NextResponse.json({ error: "GROQ_API_KEY is not set" }, { status: 500 });

0 commit comments

Comments
 (0)