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
30 changes: 19 additions & 11 deletions examples/chat/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { connectDB, ConversationModel, MessageModel, UserModel } from "@/lib/db";
import { getOpenAIClient } from "@/lib/openai-client";
import { bufferToArrayBuffer, putGeneratedImage } from "@/lib/generated-image-cache";
import { recordChatProvenance, recordImageProvenance, type SupportedProvider } from "@/lib/provenance";
import { getPKClientAsync } from "@/lib/pk-client";
import { cacheBundle } from "@/lib/bundle-cache";
Expand Down Expand Up @@ -113,18 +114,20 @@ function buildImageRequest({
return imageRequest;
}

async function blobFromImageResult(imageUrl: string, b64Json?: string): Promise<Blob | undefined> {
async function bufferFromImageResult(imageUrl: string, b64Json?: string): Promise<{ buffer: Buffer; contentType: string } | undefined> {
if (b64Json) {
const buffer = Buffer.from(b64Json, "base64");
return new Blob([buffer], { type: "image/png" });
return { buffer, contentType: "image/png" };
}

if (!imageUrl) return undefined;
const imgResp = await fetch(imageUrl);
if (!imgResp.ok) {
throw new Error(`Image download failed with HTTP ${imgResp.status}`);
}
return imgResp.blob();
const contentType = imgResp.headers.get("content-type") ?? "image/png";
const buffer = Buffer.from(await imgResp.arrayBuffer());
return { buffer, contentType };
}

// ─── Background provenance recorder ──────────────────────────────────────────
Expand Down Expand Up @@ -441,22 +444,27 @@ export async function POST(req: Request) {

const firstImage = response.data?.[0] as { url?: string; b64_json?: string; revised_prompt?: string } | undefined;
const b64Json = firstImage?.b64_json;
const imageUrl = firstImage?.url ?? (b64Json ? `data:image/png;base64,${b64Json}` : "");
const revisedPrompt = firstImage?.revised_prompt ?? prompt;

if (!imageUrl) {
if (!firstImage?.url && !b64Json) {
throw new Error("Image API returned no image data.");
}

// Download the image binary while the generated payload is fresh.
// Storing it here avoids re-fetching later when the URL may have expired,
// and lets the provenance recorder upload the real image to IPFS for embeddings.
// GPT Image models return base64 by default and do not support URL output.
// Store bytes in a short-lived server cache so MongoDB only stores a small URL;
// provenance later replaces this with a durable IPFS gateway URL.
let imageBlob: Blob | undefined;
let imageUrl = firstImage.url ?? "";
try {
imageBlob = await blobFromImageResult(imageUrl, b64Json);
const imageBytes = await bufferFromImageResult(firstImage.url ?? "", b64Json);
if (!imageBytes) throw new Error("Image API returned no downloadable image data.");
const cached = await putGeneratedImage(imageBytes.buffer, imageBytes.contentType);
imageUrl = cached.url;
imageBlob = new Blob([bufferToArrayBuffer(imageBytes.buffer)], { type: imageBytes.contentType });
} catch (err) {
console.warn("[chat] generated image download failed:", err);
// Non-fatal: provenance recording will fall back to metadata JSON
console.warn("[chat] generated image byte capture failed:", err);
if (!imageUrl) throw err;
// Non-fatal for legacy URL responses: provenance recording will fall back to metadata JSON.
}

toolResults.push({ name: "generate_image", result: { imageUrl, revisedPrompt, prompt, model: imageModel }, blob: imageBlob });
Expand Down
21 changes: 21 additions & 0 deletions examples/chat/app/api/media/generated/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { NextResponse } from "next/server";
import { bufferToArrayBuffer, getGeneratedImage } from "@/lib/generated-image-cache";

type Params = { params: Promise<{ id: string }> };

export async function GET(_req: Request, { params }: Params): Promise<NextResponse> {
const { id } = await params;
const image = await getGeneratedImage(id);

if (!image) {
return NextResponse.json({ error: "Generated image expired or not found" }, { status: 404 });
}

return new NextResponse(bufferToArrayBuffer(image.buffer), {
headers: {
"Content-Type": image.contentType,
"Cache-Control": "private, max-age=1800",
"Content-Length": String(image.buffer.length),
},
});
}
40 changes: 14 additions & 26 deletions examples/chat/app/api/pk-proxy/claim/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,48 +87,36 @@ export async function POST(req: NextRequest) {

try {
const entityId = await pk.entity({ role: "human", name: userId });
const governanceExtensions = buildGovernanceExtensions({
governance,
userId: entityId,
fileName: file.name,
roleInWorkflow: owned ? "source" : "reference",
});

const result = await pk.file(file, {
entity: { id: entityId, role: "human", name: userId },
action: {
type: owned ? "create" : "reference",
extensions: {
[AUTHORIZATION_NAMESPACE]: buildGovernanceExtensions({
governance,
userId: entityId,
fileName: file.name,
roleInWorkflow: owned ? "source" : "reference",
}).authorization,
[GOVERNANCE_NAMESPACE]: buildGovernanceExtensions({
governance,
userId: entityId,
fileName: file.name,
roleInWorkflow: owned ? "source" : "reference",
}).governance,
[AUTHORIZATION_NAMESPACE]: governanceExtensions.authorization,
[GOVERNANCE_NAMESPACE]: governanceExtensions.governance,
},
},
resourceType,
resourceExtensions: {
[AUTHORIZATION_NAMESPACE]: buildGovernanceExtensions({
governance,
userId: entityId,
fileName: file.name,
roleInWorkflow: owned ? "source" : "reference",
}).authorization,
[GOVERNANCE_NAMESPACE]: buildGovernanceExtensions({
governance,
userId: entityId,
fileName: file.name,
roleInWorkflow: owned ? "source" : "reference",
}).governance,
[AUTHORIZATION_NAMESPACE]: governanceExtensions.authorization,
[GOVERNANCE_NAMESPACE]: governanceExtensions.governance,
},
attribution: {
contrib: {
weight: governance.contributionWeight,
basis: "points",
source: owned ? "self-declared" : "default",
source: "self-declared",
category: governance.contributorRole,
note: governance.note,
note:
governance.note ??
`${file.name}: ${owned ? "creator claim" : "external source reference"} with ${governance.licenseType}; authorization ${governance.authorizationStatus}.`,
},
license: {
type: governance.licenseType,
Expand Down
Loading
Loading