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
83 changes: 83 additions & 0 deletions convex/didLogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* DID log storage and retrieval for did:webvh resolution.
*
* Stores DID logs in Convex so they can be served at
* https://trypoo.app/{path}/did.jsonl for DID resolution.
*/

import { v } from "convex/values";
import { mutation, query } from "./_generated/server";

/**
* Store or update a user's DID log.
*/
export const upsertDidLog = mutation({
args: {
userDid: v.string(),
path: v.string(),
log: v.string(),
},
handler: async (ctx, args) => {
const existing = await ctx.db
.query("didLogs")
.withIndex("by_user_did", (q) => q.eq("userDid", args.userDid))
.first();

if (existing) {
await ctx.db.patch(existing._id, {
log: args.log,
path: args.path,
updatedAt: Date.now(),
});
return existing._id;
}

return await ctx.db.insert("didLogs", {
userDid: args.userDid,
path: args.path,
log: args.log,
updatedAt: Date.now(),
});
},
});

/**
* Get a DID log by path (for resolution).
*/
export const getDidLogByPath = query({
args: { path: v.string() },
handler: async (ctx, args) => {
const record = await ctx.db
.query("didLogs")
.withIndex("by_path", (q) => q.eq("path", args.path))
.first();
return record?.log ?? null;
},
});

/**
* Get the full DID log record by path (includes userDid).
*/
export const getDidLogRecordByPath = query({
args: { path: v.string() },
handler: async (ctx, args) => {
return await ctx.db
.query("didLogs")
.withIndex("by_path", (q) => q.eq("path", args.path))
.first();
},
});

/**
* Get a DID log by user DID.
*/
export const getDidLogByUserDid = query({
args: { userDid: v.string() },
handler: async (ctx, args) => {
const record = await ctx.db
.query("didLogs")
.withIndex("by_user_did", (q) => q.eq("userDid", args.userDid))
.first();
return record ?? null;
},
});
103 changes: 103 additions & 0 deletions convex/didLogsHttp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* HTTP actions for DID log storage and retrieval.
*
* POST /api/did/log - Store/update a DID log (requires auth)
* GET /api/did/log?path={path} - Get a DID log by path (public)
*/

import { httpAction } from "./_generated/server";
import { api } from "./_generated/api";

function getCorsHeaders(request: Request): Record<string, string> {
const origin = request.headers.get("Origin") || "*";
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Credentials": "true",
};
}

/**
* Store/update a DID log. Requires JWT auth.
*/
export const storeDidLog = httpAction(async (ctx, request) => {
const corsHeaders = getCorsHeaders(request);

try {
// Verify auth via Authorization header
const authHeader = request.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json", ...corsHeaders },
});
}

const body = await request.json();
const { userDid, path, log } = body as { userDid: string; path: string; log: string };

if (!userDid || !path || !log) {
return new Response(JSON.stringify({ error: "Missing required fields: userDid, path, log" }), {
status: 400,
headers: { "Content-Type": "application/json", ...corsHeaders },
});
}

await ctx.runMutation(api.didLogs.upsertDidLog, { userDid, path, log });

Check failure on line 47 in convex/didLogsHttp.ts

View workflow job for this annotation

GitHub Actions / Build Web App

Property 'didLogs' does not exist on type '{ rateLimits: { checkAndIncrement: FunctionReference<"mutation", "public", { key: string; endpoint: "verify" | "initiate"; }, { allowed: boolean; currentAttempts: number; retryAfterMs?: undefined; } | { ...; }, string | undefined>; checkStatus: FunctionReference<...>; clearAll: FunctionReference<...>; cleanupExpired...'.

return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json", ...corsHeaders },
});
} catch (error) {
console.error("[didLogsHttp] Store error:", error);
return new Response(JSON.stringify({ error: "Failed to store DID log" }), {
status: 500,
headers: { "Content-Type": "application/json", ...corsHeaders },
});
}
});

/**
* Get a DID log by path. Public endpoint (no auth required).
*/
export const getDidLog = httpAction(async (ctx, request) => {
const corsHeaders = getCorsHeaders(request);

try {
const url = new URL(request.url);
const path = url.searchParams.get("path");

if (!path) {
return new Response(JSON.stringify({ error: "Missing 'path' query parameter" }), {
status: 400,
headers: { "Content-Type": "application/json", ...corsHeaders },
});
}

const log = await ctx.runQuery(api.didLogs.getDidLogByPath, { path });

Check failure on line 79 in convex/didLogsHttp.ts

View workflow job for this annotation

GitHub Actions / Build Web App

Property 'didLogs' does not exist on type '{ rateLimits: { checkAndIncrement: FunctionReference<"mutation", "public", { key: string; endpoint: "verify" | "initiate"; }, { allowed: boolean; currentAttempts: number; retryAfterMs?: undefined; } | { ...; }, string | undefined>; checkStatus: FunctionReference<...>; clearAll: FunctionReference<...>; cleanupExpired...'.

if (!log) {
return new Response("Not found", {
status: 404,
headers: corsHeaders,
});
}

// Return as JSONL (text/plain with each line being a JSON object)
return new Response(log, {
status: 200,
headers: {
"Content-Type": "application/jsonl+json",
...corsHeaders,
},
});
} catch (error) {
console.error("[didLogsHttp] Get error:", error);
return new Response("Internal server error", {
status: 500,
headers: corsHeaders,
});
}
});
76 changes: 76 additions & 0 deletions convex/didResources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Queries for serving list resources publicly.
*
* These are used by the HTTP handlers to serve lists as Originals resources
* at /{userPath}/resources/list-{id}.
*/

import { v } from "convex/values";
import { query } from "./_generated/server";
import type { Id } from "./_generated/dataModel";

/**
* Get a list by its Convex ID, verifying ownership by DID.
* Returns null if not found or owner doesn't match.
*/
export const getPublicList = query({
args: {
listId: v.string(),
ownerDid: v.string(),
},
handler: async (ctx, args) => {
// Try to normalize the listId to a Convex ID
let list;
try {
list = await ctx.db.get(args.listId as Id<"lists">);
} catch {
// Invalid ID format
return null;
}

if (!list || list.ownerDid !== args.ownerDid) {
return null;
}

return list;
},
});

/**
* Get all items for a list (public view — no auth required).
* Only returns non-sensitive fields.
*/
export const getPublicListItems = query({
args: {
listId: v.id("lists"),
},
handler: async (ctx, args) => {
const items = await ctx.db
.query("items")
.withIndex("by_list", (q) => q.eq("listId", args.listId))
.collect();

// Sort by order, then createdAt
items.sort((a, b) => {
if (a.order !== undefined && b.order !== undefined) return a.order - b.order;
if (a.order !== undefined) return -1;
if (b.order !== undefined) return 1;
return a.createdAt - b.createdAt;
});

// Only return top-level items (no sub-items) for the resource view
return items
.filter((item) => !item.parentId)
.map((item) => ({
_id: item._id,
name: item.name,
checked: item.checked,
createdAt: item.createdAt,
checkedAt: item.checkedAt,
description: item.description,
priority: item.priority,
dueDate: item.dueDate,
order: item.order,
}));
},
});
Loading
Loading