Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-03-22 16:42:17 -07:00
commit abf88e0ceb
29 changed files with 695 additions and 234 deletions

View File

@ -30,6 +30,7 @@ services:
- INFISICAL_PROJECT_SLUG=rspace - INFISICAL_PROJECT_SLUG=rspace
- INFISICAL_ENV=prod - INFISICAL_ENV=prod
- INFISICAL_URL=http://infisical:8080 - INFISICAL_URL=http://infisical:8080
- JWT_SECRET=${JWT_SECRET}
- FLOW_SERVICE_URL=http://payment-flow:3010 - FLOW_SERVICE_URL=http://payment-flow:3010
- FLOW_ID=a79144ec-e6a2-4e30-a42a-6d8237a5953d - FLOW_ID=a79144ec-e6a2-4e30-a42a-6d8237a5953d
- FUNNEL_ID=0ff6a9ac-1667-4fc7-9a01-b1620810509f - FUNNEL_ID=0ff6a9ac-1667-4fc7-9a01-b1620810509f

156
lib/canvas-tools.ts Normal file
View File

@ -0,0 +1,156 @@
/**
* Canvas Tool Registry shared by server (Gemini function declarations) and client (shape spawning).
* Pure TypeScript, no DOM or server dependencies.
*/
export interface CanvasToolDefinition {
declaration: {
name: string;
description: string;
parameters: {
type: "object";
properties: Record<string, { type: string; description: string; enum?: string[] }>;
required: string[];
};
};
tagName: string;
buildProps: (args: Record<string, any>) => Record<string, any>;
actionLabel: (args: Record<string, any>) => string;
}
const registry: CanvasToolDefinition[] = [
{
declaration: {
name: "create_map",
description: "Create an interactive map centered on a location. Use when the user wants to see a place, get directions, or explore a geographic area.",
parameters: {
type: "object",
properties: {
latitude: { type: "number", description: "Latitude of the center point" },
longitude: { type: "number", description: "Longitude of the center point" },
zoom: { type: "number", description: "Zoom level (1-18, default 12)" },
location_name: { type: "string", description: "Human-readable name of the location" },
},
required: ["latitude", "longitude", "location_name"],
},
},
tagName: "folk-map",
buildProps: (args) => ({
center: [args.longitude, args.latitude],
zoom: args.zoom || 12,
}),
actionLabel: (args) => `Created map: ${args.location_name}`,
},
{
declaration: {
name: "create_note",
description: "Create a markdown note on the canvas. Use for text content, lists, summaries, instructions, or any written information.",
parameters: {
type: "object",
properties: {
content: { type: "string", description: "Markdown content for the note" },
title: { type: "string", description: "Optional title for the note" },
},
required: ["content"],
},
},
tagName: "folk-markdown",
buildProps: (args) => ({
value: args.title ? `# ${args.title}\n\n${args.content}` : args.content,
}),
actionLabel: (args) => `Created note${args.title ? `: ${args.title}` : ""}`,
},
{
declaration: {
name: "create_embed",
description: "Embed a webpage or web app on the canvas. Use for websites, search results, booking sites, videos, or any URL the user wants to view inline.",
parameters: {
type: "object",
properties: {
url: { type: "string", description: "The URL to embed" },
title: { type: "string", description: "Descriptive title for the embed" },
},
required: ["url"],
},
},
tagName: "folk-embed",
buildProps: (args) => ({
url: args.url,
}),
actionLabel: (args) => `Embedded: ${args.title || args.url}`,
},
{
declaration: {
name: "create_image",
description: "Display an image on the canvas from a URL. Use when showing an existing image, photo, diagram, or any direct image link.",
parameters: {
type: "object",
properties: {
src: { type: "string", description: "Image URL" },
alt: { type: "string", description: "Alt text describing the image" },
},
required: ["src"],
},
},
tagName: "folk-image",
buildProps: (args) => ({
src: args.src,
alt: args.alt || "",
}),
actionLabel: (args) => `Created image${args.alt ? `: ${args.alt}` : ""}`,
},
{
declaration: {
name: "create_bookmark",
description: "Create a bookmark card for a URL. Use when the user wants to save or reference a link without embedding the full page.",
parameters: {
type: "object",
properties: {
url: { type: "string", description: "The URL to bookmark" },
},
required: ["url"],
},
},
tagName: "folk-bookmark",
buildProps: (args) => ({
url: args.url,
}),
actionLabel: (args) => `Bookmarked: ${args.url}`,
},
{
declaration: {
name: "create_image_gen",
description: "Generate an AI image from a text prompt. Use when the user wants to create, generate, or imagine a new image that doesn't exist yet.",
parameters: {
type: "object",
properties: {
prompt: { type: "string", description: "Text prompt describing the image to generate" },
style: {
type: "string",
description: "Visual style for the generated image",
enum: ["photorealistic", "illustration", "painting", "sketch", "punk-zine", "collage", "vintage", "minimalist"],
},
},
required: ["prompt"],
},
},
tagName: "folk-image-gen",
buildProps: (args) => ({
prompt: args.prompt,
style: args.style || "photorealistic",
}),
actionLabel: (args) => `Generating image: ${args.prompt.slice(0, 50)}${args.prompt.length > 50 ? "..." : ""}`,
},
];
export const CANVAS_TOOLS: CanvasToolDefinition[] = [...registry];
export const CANVAS_TOOL_DECLARATIONS = CANVAS_TOOLS.map((t) => t.declaration);
export function findTool(name: string): CanvasToolDefinition | undefined {
return CANVAS_TOOLS.find((t) => t.declaration.name === name);
}
export function registerCanvasTool(def: CanvasToolDefinition): void {
CANVAS_TOOLS.push(def);
}

View File

@ -1,6 +1,7 @@
import { FolkShape } from "./folk-shape"; import { FolkShape } from "./folk-shape";
import { css, html } from "./tags"; import { css, html } from "./tags";
import { SpeechDictation } from "./speech-dictation"; import { SpeechDictation } from "./speech-dictation";
import { findTool } from "./canvas-tools";
const styles = css` const styles = css`
:host { :host {
@ -304,11 +305,50 @@ const styles = css`
code { code {
font-family: "Monaco", "Consolas", monospace; font-family: "Monaco", "Consolas", monospace;
} }
.input-controls {
display: flex;
gap: 6px;
align-items: center;
}
.tools-btn {
padding: 5px 10px;
border: 2px solid var(--rs-input-border, #e2e8f0);
border-radius: 6px;
font-size: 12px;
background: var(--rs-input-bg, #fff);
color: var(--rs-text-muted, #94a3b8);
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.tools-btn:hover {
border-color: #6366f1;
}
.tools-btn.active {
background: #6366f1;
border-color: #6366f1;
color: white;
}
.message.tool-action {
align-self: center;
background: #ecfdf5;
color: #065f46;
border-radius: 20px;
padding: 6px 14px;
font-size: 12px;
font-weight: 500;
max-width: 90%;
}
`; `;
export interface ChatMessage { export interface ChatMessage {
id: string; id: string;
role: "user" | "assistant"; role: "user" | "assistant" | "tool-action";
content: string; content: string;
images?: string[]; images?: string[];
timestamp: Date; timestamp: Date;
@ -345,6 +385,7 @@ export class FolkPrompt extends FolkShape {
#error: string | null = null; #error: string | null = null;
#model = "gemini-flash"; #model = "gemini-flash";
#pendingImages: string[] = []; #pendingImages: string[] = [];
#toolsEnabled = false;
#messagesEl: HTMLElement | null = null; #messagesEl: HTMLElement | null = null;
#promptInput: HTMLTextAreaElement | null = null; #promptInput: HTMLTextAreaElement | null = null;
@ -352,6 +393,7 @@ export class FolkPrompt extends FolkShape {
#sendBtn: HTMLButtonElement | null = null; #sendBtn: HTMLButtonElement | null = null;
#attachInput: HTMLInputElement | null = null; #attachInput: HTMLInputElement | null = null;
#pendingImagesEl: HTMLElement | null = null; #pendingImagesEl: HTMLElement | null = null;
#toolsBtn: HTMLButtonElement | null = null;
get messages() { get messages() {
return this.#messages; return this.#messages;
@ -381,18 +423,21 @@ export class FolkPrompt extends FolkShape {
</div> </div>
</div> </div>
<div class="input-area"> <div class="input-area">
<select class="model-select"> <div class="input-controls">
<optgroup label="Gemini"> <select class="model-select">
<option value="gemini-flash">Gemini 2.5 Flash</option> <optgroup label="Gemini">
<option value="gemini-pro">Gemini 2.5 Pro</option> <option value="gemini-flash">Gemini 2.5 Flash</option>
</optgroup> <option value="gemini-pro">Gemini 2.5 Pro</option>
<optgroup label="Local (Ollama)"> </optgroup>
<option value="llama3.2">Llama 3.2 (3B)</option> <optgroup label="Local (Ollama)">
<option value="llama3.1">Llama 3.1 (8B)</option> <option value="llama3.2">Llama 3.2 (3B)</option>
<option value="qwen2.5-coder">Qwen Coder (7B)</option> <option value="llama3.1">Llama 3.1 (8B)</option>
<option value="mistral-small">Mistral Small (24B)</option> <option value="qwen2.5-coder">Qwen Coder (7B)</option>
</optgroup> <option value="mistral-small">Mistral Small (24B)</option>
</select> </optgroup>
</select>
<button class="tools-btn" title="Enable canvas tools — AI can create maps, notes, embeds, and more">Tools</button>
</div>
<div class="pending-images"></div> <div class="pending-images"></div>
<div class="prompt-row"> <div class="prompt-row">
<textarea class="prompt-input" placeholder="Type your message..." rows="2"></textarea> <textarea class="prompt-input" placeholder="Type your message..." rows="2"></textarea>
@ -418,10 +463,23 @@ export class FolkPrompt extends FolkShape {
this.#sendBtn = wrapper.querySelector(".send-btn"); this.#sendBtn = wrapper.querySelector(".send-btn");
this.#attachInput = wrapper.querySelector(".attach-input"); this.#attachInput = wrapper.querySelector(".attach-input");
this.#pendingImagesEl = wrapper.querySelector(".pending-images"); this.#pendingImagesEl = wrapper.querySelector(".pending-images");
this.#toolsBtn = wrapper.querySelector(".tools-btn") as HTMLButtonElement;
const clearBtn = wrapper.querySelector(".clear-btn") as HTMLButtonElement; const clearBtn = wrapper.querySelector(".clear-btn") as HTMLButtonElement;
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement; const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
const attachBtn = wrapper.querySelector(".attach-btn") as HTMLButtonElement | null; const attachBtn = wrapper.querySelector(".attach-btn") as HTMLButtonElement | null;
// Tools toggle
this.#toolsBtn?.addEventListener("click", (e) => {
e.stopPropagation();
this.#toolsEnabled = !this.#toolsEnabled;
this.#toolsBtn!.classList.toggle("active", this.#toolsEnabled);
if (this.#promptInput) {
this.#promptInput.placeholder = this.#toolsEnabled
? "Ask me to create maps, notes, images, or embeds on the canvas..."
: "Type your message...";
}
});
// Attach button // Attach button
attachBtn?.addEventListener("click", (e) => { attachBtn?.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
@ -566,16 +624,18 @@ export class FolkPrompt extends FolkShape {
this.#renderMessages(true); this.#renderMessages(true);
try { try {
const useTools = this.#toolsEnabled && this.#model.startsWith("gemini");
const response = await fetch("/api/prompt", { const response = await fetch("/api/prompt", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
messages: this.#messages.map((m) => ({ messages: this.#messages.filter((m) => m.role !== "tool-action").map((m) => ({
role: m.role, role: m.role,
content: m.content, content: m.content,
...(m.images?.length ? { images: m.images } : {}), ...(m.images?.length ? { images: m.images } : {}),
})), })),
model: this.#model, model: this.#model,
...(useTools ? { useTools: true } : {}),
}), }),
}); });
@ -585,6 +645,11 @@ export class FolkPrompt extends FolkShape {
const result = await response.json(); const result = await response.json();
// Execute tool calls if any
if (result.toolCalls?.length) {
this.#executeToolCalls(result.toolCalls);
}
const assistantMessage: ChatMessage = { const assistantMessage: ChatMessage = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
role: "assistant", role: "assistant",
@ -607,6 +672,42 @@ export class FolkPrompt extends FolkShape {
} }
} }
#executeToolCalls(toolCalls: { name: string; args: Record<string, any>; label: string }[]) {
const api = (window as any).__canvasApi;
if (!api) {
console.warn("[folk-prompt] Canvas API not available — cannot spawn shapes");
return;
}
let offsetY = 0;
for (const tc of toolCalls) {
const tool = findTool(tc.name);
if (!tool) continue;
const props = tool.buildProps(tc.args);
const defaults = api.SHAPE_DEFAULTS?.[tool.tagName] || { width: 300, height: 200 };
const preferX = this.x + this.width + 40 + defaults.width / 2;
const preferY = this.y + offsetY + defaults.height / 2;
const pos = api.findFreePosition(defaults.width, defaults.height, preferX, preferY, this);
try {
api.newShape(tool.tagName, props, { x: pos.x + defaults.width / 2, y: pos.y + defaults.height / 2 });
} catch (e) {
console.error(`[folk-prompt] Failed to create ${tool.tagName}:`, e);
}
// Tool action card in chat
this.#messages.push({
id: crypto.randomUUID(),
role: "tool-action",
content: tc.label,
timestamp: new Date(),
});
offsetY += defaults.height + 20;
}
}
#clearChat() { #clearChat() {
this.#messages = []; this.#messages = [];
this.#error = null; this.#error = null;
@ -629,6 +730,9 @@ export class FolkPrompt extends FolkShape {
let messagesHtml = this.#messages let messagesHtml = this.#messages
.map((msg) => { .map((msg) => {
if (msg.role === "tool-action") {
return `<div class="message tool-action">${this.#escapeHtml(msg.content)}</div>`;
}
let imgHtml = ""; let imgHtml = "";
if (msg.images?.length) { if (msg.images?.length) {
imgHtml = `<div class="msg-images">${msg.images.map((src) => `<img src="${this.#escapeHtml(src)}" />`).join("")}</div>`; imgHtml = `<div class="msg-images">${msg.images.map((src) => `<img src="${this.#escapeHtml(src)}" />`).join("")}</div>`;
@ -684,7 +788,7 @@ export class FolkPrompt extends FolkShape {
...super.toJSON(), ...super.toJSON(),
type: "folk-prompt", type: "folk-prompt",
model: this.#model, model: this.#model,
messages: this.messages.map((msg) => ({ messages: this.messages.filter((m) => m.role !== "tool-action").map((msg) => ({
role: msg.role, role: msg.role,
content: msg.content, content: msg.content,
...(msg.images?.length ? { images: msg.images } : {}), ...(msg.images?.length ? { images: msg.images } : {}),

View File

@ -14,7 +14,7 @@ import * as Automerge from "@automerge/automerge";
import { renderShell } from "../../server/shell"; import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server'; import type { SyncServer } from '../../server/local-first/sync-server';
import { bnbSchema, bnbDocId } from './schemas'; import { bnbSchema, bnbDocId } from './schemas';
@ -573,7 +573,7 @@ routes.get("/api/listings", async (c) => {
routes.post("/api/listings", async (c) => { routes.post("/api/listings", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -637,7 +637,7 @@ routes.get("/api/listings/:id", async (c) => {
routes.patch("/api/listings/:id", async (c) => { routes.patch("/api/listings/:id", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -721,7 +721,7 @@ routes.get("/api/listings/:id/availability", async (c) => {
routes.post("/api/listings/:id/availability", async (c) => { routes.post("/api/listings/:id/availability", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -755,7 +755,7 @@ routes.post("/api/listings/:id/availability", async (c) => {
routes.patch("/api/availability/:id", async (c) => { routes.patch("/api/availability/:id", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -815,7 +815,7 @@ routes.get("/api/stays", async (c) => {
routes.post("/api/stays", async (c) => { routes.post("/api/stays", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -888,7 +888,7 @@ function stayTransition(statusTarget: StayStatus, timestampField: 'respondedAt'
return async (c: any) => { return async (c: any) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -918,7 +918,7 @@ routes.post("/api/stays/:id/complete", stayTransition('completed', 'completedAt'
routes.post("/api/stays/:id/messages", async (c) => { routes.post("/api/stays/:id/messages", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -968,7 +968,7 @@ routes.get("/api/endorsements", async (c) => {
routes.post("/api/endorsements", async (c) => { routes.post("/api/endorsements", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -1167,7 +1167,7 @@ routes.get("/api/config", async (c) => {
routes.patch("/api/config", async (c) => { routes.patch("/api/config", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;

View File

@ -17,10 +17,7 @@ import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule, SpaceLifecycleContext } from "../../shared/module"; import type { RSpaceModule, SpaceLifecycleContext } from "../../shared/module";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import { import { verifyToken, extractToken } from "../../server/auth";
verifyEncryptIDToken,
extractToken,
} from "@encryptid/sdk/server";
import type { SyncServer } from '../../server/local-first/sync-server'; import type { SyncServer } from '../../server/local-first/sync-server';
import { import {
booksCatalogSchema, booksCatalogSchema,
@ -163,7 +160,7 @@ routes.post("/api/books", async (c) => {
let claims; let claims;
try { try {
claims = await verifyEncryptIDToken(token); claims = await verifyToken(token);
} catch { } catch {
return c.json({ error: "Invalid token" }, 401); return c.json({ error: "Invalid token" }, 401);
} }

View File

@ -13,7 +13,7 @@ import * as Automerge from "@automerge/automerge";
import { renderShell } from "../../server/shell"; import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server'; import type { SyncServer } from '../../server/local-first/sync-server';
import { calendarSchema, calendarDocId } from './schemas'; import { calendarSchema, calendarDocId } from './schemas';
@ -315,7 +315,7 @@ routes.post("/api/events", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -404,7 +404,7 @@ routes.post("/api/events", async (c) => {
routes.post("/api/import-ics", async (c) => { routes.post("/api/import-ics", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -573,7 +573,7 @@ routes.patch("/api/events/:id", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -672,7 +672,7 @@ routes.get("/api/sources", async (c) => {
routes.post("/api/sources", async (c) => { routes.post("/api/sources", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;

View File

@ -14,7 +14,7 @@ import { renderShell, buildSpaceUrl } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import { depositOrderRevenue } from "./flow"; import { depositOrderRevenue } from "./flow";
import type { RSpaceModule } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server'; import type { SyncServer } from '../../server/local-first/sync-server';
import { import {
@ -484,7 +484,7 @@ routes.post("/api/orders", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
let buyerDid: string | null = null; let buyerDid: string | null = null;
if (token) { if (token) {
try { const claims = await verifyEncryptIDToken(token); buyerDid = claims.sub; } catch {} try { const claims = await verifyToken(token); buyerDid = claims.sub; } catch {}
} }
const body = await c.req.json(); const body = await c.req.json();
@ -567,7 +567,7 @@ routes.get("/api/orders", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
let authedBuyer: string | null = null; let authedBuyer: string | null = null;
if (token) { if (token) {
try { const claims = await verifyEncryptIDToken(token); authedBuyer = claims.sub; } catch {} try { const claims = await verifyToken(token); authedBuyer = claims.sub; } catch {}
} }
const { status, provider_id, buyer_id, limit = "50", offset = "0" } = c.req.query(); const { status, provider_id, buyer_id, limit = "50", offset = "0" } = c.req.query();
@ -1426,7 +1426,7 @@ routes.post("/api/payments", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { const {
@ -1512,7 +1512,7 @@ routes.get("/api/payments", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const paymentDocs = getSpacePaymentDocs(space); const paymentDocs = getSpacePaymentDocs(space);
const payments = paymentDocs const payments = paymentDocs
@ -2251,7 +2251,7 @@ routes.post("/api/group-buys", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { catalogEntryId, tiers, description, closesInDays } = body; const { catalogEntryId, tiers, description, closesInDays } = body;
@ -2371,7 +2371,7 @@ routes.post("/api/group-buys/:id/pledge", async (c) => {
let buyerId: string | null = null; let buyerId: string | null = null;
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (token) { if (token) {
try { const claims = await verifyEncryptIDToken(token); buyerId = claims.sub || null; } catch { /* public pledge */ } try { const claims = await verifyToken(token); buyerId = claims.sub || null; } catch { /* public pledge */ }
} }
const pledgeId = crypto.randomUUID(); const pledgeId = crypto.randomUUID();

View File

@ -14,7 +14,7 @@ import * as Automerge from "@automerge/automerge";
import { renderShell, renderExternalAppShell } from "../../server/shell"; import { renderShell, renderExternalAppShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server'; import type { SyncServer } from '../../server/local-first/sync-server';
import { filesSchema, filesDocId } from './schemas'; import { filesSchema, filesDocId } from './schemas';
@ -166,7 +166,7 @@ routes.post("/api/files", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const formData = await c.req.formData(); const formData = await c.req.formData();
const file = formData.get("file") as File | null; const file = formData.get("file") as File | null;
@ -300,7 +300,7 @@ routes.post("/api/files/:id/share", async (c) => {
const authToken = extractToken(c.req.raw.headers); const authToken = extractToken(c.req.raw.headers);
if (!authToken) return c.json({ error: "Authentication required" }, 401); if (!authToken) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }
const fileId = c.req.param("id"); const fileId = c.req.param("id");
const space = c.req.param("space") || c.req.query("space") || "default"; const space = c.req.param("space") || c.req.query("space") || "default";
@ -376,7 +376,7 @@ routes.post("/api/shares/:shareId/revoke", async (c) => {
const authToken = extractToken(c.req.raw.headers); const authToken = extractToken(c.req.raw.headers);
if (!authToken) return c.json({ error: "Authentication required" }, 401); if (!authToken) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }
const shareId = c.req.param("shareId"); const shareId = c.req.param("shareId");
const space = c.req.param("space") || c.req.query("space") || "default"; const space = c.req.param("space") || c.req.query("space") || "default";
@ -506,7 +506,7 @@ routes.post("/api/cards", async (c) => {
const authToken = extractToken(c.req.raw.headers); const authToken = extractToken(c.req.raw.headers);
if (!authToken) return c.json({ error: "Authentication required" }, 401); if (!authToken) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json<{ title: string; body?: string; card_type?: string; tags?: string[]; shared_space?: string }>(); const body = await c.req.json<{ title: string; body?: string; card_type?: string; tags?: string[]; shared_space?: string }>();
const space = c.req.param("space") || body.shared_space || "default"; const space = c.req.param("space") || body.shared_space || "default";

View File

@ -9,7 +9,7 @@ import * as Automerge from "@automerge/automerge";
import { renderShell } from "../../server/shell"; import { renderShell } from "../../server/shell";
import type { RSpaceModule } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import { getTransakEnv, getTransakWebhookSecret } from "../../shared/transak"; import { getTransakEnv, getTransakWebhookSecret } from "../../shared/transak";
import type { SyncServer } from '../../server/local-first/sync-server'; import type { SyncServer } from '../../server/local-first/sync-server';
@ -151,7 +151,7 @@ routes.post("/api/flows", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.text(); const body = await c.req.text();
const res = await fetch(`${FLOW_SERVICE_URL}/api/flows`, { const res = await fetch(`${FLOW_SERVICE_URL}/api/flows`, {
@ -386,7 +386,7 @@ const ENTRY_POINT = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789'; // v0.6
routes.post("/api/flows/submit-userop", async (c) => { routes.post("/api/flows/submit-userop", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
if (!_pimlico) return c.json({ error: "Pimlico bundler not configured" }, 503); if (!_pimlico) return c.json({ error: "Pimlico bundler not configured" }, 503);
@ -404,7 +404,7 @@ routes.post("/api/flows/submit-userop", async (c) => {
routes.post("/api/flows/send-userop", async (c) => { routes.post("/api/flows/send-userop", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
if (!_pimlico) return c.json({ error: "Pimlico bundler not configured" }, 503); if (!_pimlico) return c.json({ error: "Pimlico bundler not configured" }, 503);
@ -424,7 +424,7 @@ routes.post("/api/flows/send-userop", async (c) => {
routes.get("/api/flows/userop/:hash", async (c) => { routes.get("/api/flows/userop/:hash", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
if (!_pimlico) return c.json({ error: "Pimlico bundler not configured" }, 503); if (!_pimlico) return c.json({ error: "Pimlico bundler not configured" }, 503);
@ -540,7 +540,7 @@ routes.post("/api/space-flows", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const { space, flowId } = await c.req.json(); const { space, flowId } = await c.req.json();
if (!space || !flowId) return c.json({ error: "space and flowId required" }, 400); if (!space || !flowId) return c.json({ error: "space and flowId required" }, 400);
@ -560,7 +560,7 @@ routes.delete("/api/space-flows/:flowId", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const flowId = c.req.param("flowId"); const flowId = c.req.param("flowId");
const space = c.req.query("space") || ""; const space = c.req.query("space") || "";
@ -636,7 +636,7 @@ routes.post("/api/mortgage/positions", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json() as Partial<MortgagePosition>; const body = await c.req.json() as Partial<MortgagePosition>;
if (!body.principal || !body.termMonths || !body.interestRate) { if (!body.principal || !body.termMonths || !body.interestRate) {
@ -710,7 +710,7 @@ routes.post("/api/budgets/allocate", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const { space, allocations } = await c.req.json() as { space?: string; allocations?: Record<string, number> }; const { space, allocations } = await c.req.json() as { space?: string; allocations?: Record<string, number> };
if (!allocations) return c.json({ error: "allocations required" }, 400); if (!allocations) return c.json({ error: "allocations required" }, 400);
@ -745,7 +745,7 @@ routes.post("/api/budgets/segments", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const { space, action, segmentId, name, color } = await c.req.json() as { const { space, action, segmentId, name, color } = await c.req.json() as {
space?: string; action: 'add' | 'remove'; segmentId?: string; name?: string; color?: string; space?: string; action: 'add' | 'remove'; segmentId?: string; name?: string; color?: string;

View File

@ -9,7 +9,7 @@ import { renderShell, renderExternalAppShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import { provisionInstance, destroyInstance } from "./lib/provisioner"; import { provisionInstance, destroyInstance } from "./lib/provisioner";
import type { RSpaceModule } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server'; import type { SyncServer } from '../../server/local-first/sync-server';
import { import {
@ -44,7 +44,7 @@ routes.get("/api/instances", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const doc = ensureDoc(); const doc = ensureDoc();
const instances = Object.values(doc.instances).filter( const instances = Object.values(doc.instances).filter(
@ -60,7 +60,7 @@ routes.post("/api/instances", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json<{ const body = await c.req.json<{
name: string; name: string;
@ -126,7 +126,7 @@ routes.get("/api/instances/:id", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const doc = ensureDoc(); const doc = ensureDoc();
const instance = doc.instances[c.req.param("id")]; const instance = doc.instances[c.req.param("id")];
@ -142,7 +142,7 @@ routes.delete("/api/instances/:id", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const doc = ensureDoc(); const doc = ensureDoc();
const instance = doc.instances[c.req.param("id")]; const instance = doc.instances[c.req.param("id")];

View File

@ -13,7 +13,7 @@ import * as Automerge from "@automerge/automerge";
import { renderShell } from "../../server/shell"; import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server'; import type { SyncServer } from '../../server/local-first/sync-server';
import { import {
@ -548,7 +548,7 @@ routes.post("/api/mailboxes", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { slug, name, email, description, visibility = "private", imap_user, imap_password } = body; const { slug, name, email, description, visibility = "private", imap_user, imap_password } = body;
@ -710,7 +710,7 @@ routes.post("/api/threads/:id/comments", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const threadId = c.req.param("id"); const threadId = c.req.param("id");
const body = await c.req.json(); const body = await c.req.json();
@ -756,7 +756,7 @@ routes.post("/api/threads/:id/reply", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const threadId = c.req.param("id"); const threadId = c.req.param("id");
const found = findThreadById(threadId); const found = findThreadById(threadId);
@ -805,7 +805,7 @@ routes.post("/api/threads/:id/reply-all", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const threadId = c.req.param("id"); const threadId = c.req.param("id");
const found = findThreadById(threadId); const found = findThreadById(threadId);
@ -866,7 +866,7 @@ routes.post("/api/threads/:id/forward", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const threadId = c.req.param("id"); const threadId = c.req.param("id");
const found = findThreadById(threadId); const found = findThreadById(threadId);
@ -963,7 +963,7 @@ routes.post("/api/approvals", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { mailbox_slug, thread_id, subject, body_text, to_addresses, cc_addresses, in_reply_to, references: refs, reply_type } = body; const { mailbox_slug, thread_id, subject, body_text, to_addresses, cc_addresses, in_reply_to, references: refs, reply_type } = body;
@ -1009,7 +1009,7 @@ routes.post("/api/approvals/:id/sign", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const id = c.req.param("id"); const id = c.req.param("id");
const body = await c.req.json(); const body = await c.req.json();
@ -1083,7 +1083,7 @@ routes.post("/api/personal-inboxes", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { label, email, imap_host, imap_port, imap_user, imap_pass, smtp_host, smtp_port, smtp_user, smtp_pass } = body; const { label, email, imap_host, imap_port, imap_user, imap_pass, smtp_host, smtp_port, smtp_user, smtp_pass } = body;
@ -1158,7 +1158,7 @@ routes.get("/api/personal-inboxes", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const inboxes: any[] = []; const inboxes: any[] = [];
for (const { doc } of getAllMailboxDocs()) { for (const { doc } of getAllMailboxDocs()) {
@ -1188,7 +1188,7 @@ routes.delete("/api/personal-inboxes/:id", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const inboxId = c.req.param("id"); const inboxId = c.req.param("id");
_personalCredentials.delete(inboxId); _personalCredentials.delete(inboxId);
@ -1214,7 +1214,7 @@ routes.post("/api/agent-inboxes", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { name, email, personality, auto_reply = false, auto_classify = false, rules = [] } = body; const { name, email, personality, auto_reply = false, auto_classify = false, rules = [] } = body;
@ -1287,7 +1287,7 @@ routes.get("/api/agent-inboxes", async (c) => {
routes.delete("/api/agent-inboxes/:id", async (c) => { routes.delete("/api/agent-inboxes/:id", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const agentId = c.req.param("id"); const agentId = c.req.param("id");
@ -1328,7 +1328,7 @@ routes.post("/api/workspaces", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { slug, name, description } = body; const { slug, name, description } = body;

View File

@ -13,7 +13,7 @@ import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule, SpaceLifecycleContext } from "../../shared/module"; import type { RSpaceModule, SpaceLifecycleContext } from "../../shared/module";
import { resolveDataSpace } from "../../shared/scope-resolver"; import { resolveDataSpace } from "../../shared/scope-resolver";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import { notebookSchema, notebookDocId, connectionsDocId, createNoteItem } from "./schemas"; import { notebookSchema, notebookDocId, connectionsDocId, createNoteItem } from "./schemas";
import type { NotebookDoc, NoteItem, ConnectionsDoc } from "./schemas"; import type { NotebookDoc, NoteItem, ConnectionsDoc } from "./schemas";
@ -285,7 +285,7 @@ routes.post("/api/notebooks", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { title, description, cover_color } = body; const { title, description, cover_color } = body;
@ -339,7 +339,7 @@ routes.put("/api/notebooks/:id", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const id = c.req.param("id"); const id = c.req.param("id");
const body = await c.req.json(); const body = await c.req.json();
@ -437,7 +437,7 @@ routes.post("/api/notes", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { notebook_id, title, content, content_format, type, url, language, file_url, mime_type, file_size, duration, tags } = body; const { notebook_id, title, content, content_format, type, url, language, file_url, mime_type, file_size, duration, tags } = body;
@ -641,7 +641,7 @@ routes.post("/api/import/upload", async (c) => {
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const formData = await c.req.formData(); const formData = await c.req.formData();
const file = formData.get("file") as File | null; const file = formData.get("file") as File | null;
@ -693,7 +693,7 @@ routes.post("/api/import/files", async (c) => {
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const formData = await c.req.formData(); const formData = await c.req.formData();
const notebookId = formData.get("notebookId") as string | null; const notebookId = formData.get("notebookId") as string | null;
@ -745,7 +745,7 @@ routes.post("/api/import/notion", async (c) => {
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { pageIds, notebookId, recursive } = body; const { pageIds, notebookId, recursive } = body;
@ -791,7 +791,7 @@ routes.post("/api/import/google-docs", async (c) => {
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { docIds, notebookId } = body; const { docIds, notebookId } = body;
@ -996,7 +996,7 @@ routes.post("/api/export/notion", async (c) => {
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { notebookId, noteIds, parentId } = body; const { notebookId, noteIds, parentId } = body;
@ -1036,7 +1036,7 @@ routes.post("/api/export/google-docs", async (c) => {
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { notebookId, noteIds, parentId } = body; const { notebookId, noteIds, parentId } = body;
@ -1078,7 +1078,7 @@ routes.post("/api/sync/note/:noteId", async (c) => {
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const noteId = c.req.param("noteId"); const noteId = c.req.param("noteId");
const found = findNote(dataSpace, noteId); const found = findNote(dataSpace, noteId);
@ -1131,7 +1131,7 @@ routes.post("/api/sync/notebook/:id", async (c) => {
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const notebookId = c.req.param("id"); const notebookId = c.req.param("id");
const docId = notebookDocId(dataSpace, notebookId); const docId = notebookDocId(dataSpace, notebookId);
@ -1191,7 +1191,7 @@ routes.post("/api/sync/upload", async (c) => {
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const formData = await c.req.formData(); const formData = await c.req.formData();
const file = formData.get("file") as File | null; const file = formData.get("file") as File | null;
@ -1306,7 +1306,7 @@ const UPLOAD_DIR = "/data/files/generated";
routes.post("/api/uploads", async (c) => { routes.post("/api/uploads", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const formData = await c.req.formData(); const formData = await c.req.formData();
const file = formData.get("file") as File | null; const file = formData.get("file") as File | null;
@ -1351,7 +1351,7 @@ routes.get("/api/uploads/:filename", async (c) => {
routes.post("/api/voice/transcribe", async (c) => { routes.post("/api/voice/transcribe", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
try { try {
const formData = await c.req.formData(); const formData = await c.req.formData();
@ -1372,7 +1372,7 @@ routes.post("/api/voice/transcribe", async (c) => {
routes.post("/api/voice/diarize", async (c) => { routes.post("/api/voice/diarize", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
try { try {
const formData = await c.req.formData(); const formData = await c.req.formData();
@ -1397,7 +1397,7 @@ const NOTEBOOK_API_URL = process.env.NOTEBOOK_API_URL || "http://open-notebook:5
routes.post("/api/notes/summarize", async (c) => { routes.post("/api/notes/summarize", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const { content, model = "gemini-flash", length = "medium" } = await c.req.json<{ const { content, model = "gemini-flash", length = "medium" } = await c.req.json<{
content: string; model?: string; length?: "short" | "medium" | "long"; content: string; model?: string; length?: "short" | "medium" | "long";
@ -1443,7 +1443,7 @@ routes.post("/api/notes/summarize", async (c) => {
routes.post("/api/notes/deep-summarize", async (c) => { routes.post("/api/notes/deep-summarize", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const { noteIds, query } = await c.req.json<{ noteIds: string[]; query?: string }>(); const { noteIds, query } = await c.req.json<{ noteIds: string[]; query?: string }>();
if (!noteIds?.length) return c.json({ error: "noteIds required" }, 400); if (!noteIds?.length) return c.json({ error: "noteIds required" }, 400);
@ -1498,7 +1498,7 @@ routes.post("/api/notes/deep-summarize", async (c) => {
routes.post("/api/notes/send-to-notebook", async (c) => { routes.post("/api/notes/send-to-notebook", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const { noteId, title, content } = await c.req.json<{ const { noteId, title, content } = await c.req.json<{
noteId: string; title: string; content: string; noteId: string; title: string; content: string;
@ -1535,7 +1535,7 @@ routes.post("/api/notes/send-to-notebook", async (c) => {
routes.post("/api/articles/unlock", async (c) => { routes.post("/api/articles/unlock", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Unauthorized" }, 401); if (!token) return c.json({ error: "Unauthorized" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const { url } = await c.req.json<{ url: string }>(); const { url } = await c.req.json<{ url: string }>();
if (!url) return c.json({ error: "Missing url" }, 400); if (!url) return c.json({ error: "Missing url" }, 400);

View File

@ -32,8 +32,8 @@ import {
import { DEMO_FEED } from "./lib/types"; import { DEMO_FEED } from "./lib/types";
import { getListmonkConfig, listmonkFetch } from "./lib/listmonk-proxy"; import { getListmonkConfig, listmonkFetch } from "./lib/listmonk-proxy";
import { getPostizConfig, getIntegrations, createPost, createThread } from "./lib/postiz-client"; import { getPostizConfig, getIntegrations, createPost, createThread } from "./lib/postiz-client";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "../../server/auth";
import type { EncryptIDClaims } from "@encryptid/sdk/server"; import type { EncryptIDClaims } from "../../server/auth";
import { resolveCallerRole, roleAtLeast } from "../../server/spaces"; import { resolveCallerRole, roleAtLeast } from "../../server/spaces";
import type { SpaceRoleString } from "../../server/spaces"; import type { SpaceRoleString } from "../../server/spaces";
@ -432,7 +432,7 @@ async function requireNewsletterRole(c: any, minRole: SpaceRoleString): Promise<
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const result = await resolveCallerRole(space, claims); const result = await resolveCallerRole(space, claims);
@ -1951,7 +1951,7 @@ routes.get("/newsletter-list", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (token) { if (token) {
try { try {
const claims = await verifyEncryptIDToken(token); const claims = await verifyToken(token);
const result = await resolveCallerRole(space, claims); const result = await resolveCallerRole(space, claims);
if (result) role = result.role; if (result) role = result.role;
} catch { /* keep viewer default */ } } catch { /* keep viewer default */ }

View File

@ -17,10 +17,7 @@ import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule, SpaceLifecycleContext } from "../../shared/module"; import type { RSpaceModule, SpaceLifecycleContext } from "../../shared/module";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import { import { verifyToken, extractToken } from "../../server/auth";
verifyEncryptIDToken,
extractToken,
} from "@encryptid/sdk/server";
import { setupX402FromEnv } from "../../shared/x402/hono-middleware"; import { setupX402FromEnv } from "../../shared/x402/hono-middleware";
import type { SyncServer } from '../../server/local-first/sync-server'; import type { SyncServer } from '../../server/local-first/sync-server';
import { import {
@ -300,7 +297,7 @@ routes.post("/api/splats", async (c) => {
let claims; let claims;
try { try {
claims = await verifyEncryptIDToken(token); claims = await verifyToken(token);
} catch { } catch {
return c.json({ error: "Invalid token" }, 401); return c.json({ error: "Invalid token" }, 401);
} }
@ -413,7 +410,7 @@ routes.post("/api/splats/from-media", async (c) => {
let claims; let claims;
try { try {
claims = await verifyEncryptIDToken(token); claims = await verifyToken(token);
} catch { } catch {
return c.json({ error: "Invalid token" }, 401); return c.json({ error: "Invalid token" }, 401);
} }
@ -557,7 +554,7 @@ routes.post("/api/splats/save-generated", async (c) => {
let claims; let claims;
try { try {
claims = await verifyEncryptIDToken(token); claims = await verifyToken(token);
} catch { } catch {
return c.json({ error: "Invalid token" }, 401); return c.json({ error: "Invalid token" }, 401);
} }
@ -646,7 +643,7 @@ routes.get("/api/splats/my-history", async (c) => {
let claims; let claims;
try { try {
claims = await verifyEncryptIDToken(token); claims = await verifyToken(token);
} catch { } catch {
return c.json({ error: "Invalid token" }, 401); return c.json({ error: "Invalid token" }, 401);
} }
@ -670,7 +667,7 @@ routes.delete("/api/splats/:id", async (c) => {
let claims; let claims;
try { try {
claims = await verifyEncryptIDToken(token); claims = await verifyToken(token);
} catch { } catch {
return c.json({ error: "Invalid token" }, 401); return c.json({ error: "Invalid token" }, 401);
} }

View File

@ -13,7 +13,7 @@ import * as Automerge from '@automerge/automerge';
import { renderShell } from "../../server/shell"; import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule, SpaceLifecycleContext } from "../../shared/module"; import type { RSpaceModule, SpaceLifecycleContext } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server'; import type { SyncServer } from '../../server/local-first/sync-server';
import { boardSchema, boardDocId, createTaskItem } from './schemas'; import { boardSchema, boardDocId, createTaskItem } from './schemas';
@ -214,7 +214,7 @@ routes.post("/api/spaces", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { name, description, icon } = body; const { name, description, icon } = body;
@ -322,7 +322,7 @@ routes.post("/api/spaces/:slug/tasks", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const slug = c.req.param("slug"); const slug = c.req.param("slug");
const body = await c.req.json(); const body = await c.req.json();
@ -367,7 +367,7 @@ routes.patch("/api/tasks/:id", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
let updatedBy: string | null = null; let updatedBy: string | null = null;
if (token) { if (token) {
try { const claims = await verifyEncryptIDToken(token); updatedBy = claims.sub; } catch {} try { const claims = await verifyToken(token); updatedBy = claims.sub; } catch {}
} }
const id = c.req.param("id"); const id = c.req.param("id");

View File

@ -13,7 +13,7 @@ import * as Automerge from "@automerge/automerge";
import { renderShell } from "../../server/shell"; import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server'; import type { SyncServer } from '../../server/local-first/sync-server';
import { import {
@ -96,7 +96,7 @@ routes.post("/api/trips", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { title, description, start_date, end_date, budget_total, budget_currency } = body; const { title, description, start_date, end_date, budget_total, budget_currency } = body;
@ -199,7 +199,7 @@ routes.put("/api/trips/:id", async (c) => {
routes.post("/api/trips/:id/destinations", async (c) => { routes.post("/api/trips/:id/destinations", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -236,7 +236,7 @@ routes.post("/api/trips/:id/destinations", async (c) => {
routes.post("/api/trips/:id/itinerary", async (c) => { routes.post("/api/trips/:id/itinerary", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -273,7 +273,7 @@ routes.post("/api/trips/:id/itinerary", async (c) => {
routes.post("/api/trips/:id/bookings", async (c) => { routes.post("/api/trips/:id/bookings", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -311,7 +311,7 @@ routes.post("/api/trips/:id/bookings", async (c) => {
routes.post("/api/trips/:id/expenses", async (c) => { routes.post("/api/trips/:id/expenses", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -362,7 +362,7 @@ routes.get("/api/trips/:id/packing", async (c) => {
routes.post("/api/trips/:id/packing", async (c) => { routes.post("/api/trips/:id/packing", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;

View File

@ -9,7 +9,7 @@ import { Hono } from "hono";
import { renderShell } from "../../server/shell"; import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import { S3Client, ListObjectsV2Command, GetObjectCommand, HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; import { S3Client, ListObjectsV2Command, GetObjectCommand, HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
@ -152,7 +152,7 @@ routes.get("/api/v/*", async (c) => {
routes.post("/api/videos", async (c) => { routes.post("/api/videos", async (c) => {
const authToken = extractToken(c.req.raw.headers); const authToken = extractToken(c.req.raw.headers);
if (!authToken) return c.json({ error: "Authentication required" }, 401); if (!authToken) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }
const client = getS3(); const client = getS3();
if (!client) return c.json({ error: "R2 not configured" }, 503); if (!client) return c.json({ error: "R2 not configured" }, 503);
@ -198,7 +198,7 @@ routes.get("/api/health", (c) => c.json({ ok: true }));
routes.post("/api/360split", async (c) => { routes.post("/api/360split", async (c) => {
const authToken = extractToken(c.req.raw.headers); const authToken = extractToken(c.req.raw.headers);
if (!authToken) return c.json({ error: "Authentication required" }, 401); if (!authToken) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }
if (!SPLIT_360_URL) return c.json({ error: "360split service not configured" }, 503); if (!SPLIT_360_URL) return c.json({ error: "360split service not configured" }, 503);
@ -255,7 +255,7 @@ routes.get("/api/360split/status/:jobId", async (c) => {
routes.post("/api/360split/import/:jobId", async (c) => { routes.post("/api/360split/import/:jobId", async (c) => {
const authToken = extractToken(c.req.raw.headers); const authToken = extractToken(c.req.raw.headers);
if (!authToken) return c.json({ error: "Authentication required" }, 401); if (!authToken) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }
if (!SPLIT_360_URL) return c.json({ error: "360split service not configured" }, 503); if (!SPLIT_360_URL) return c.json({ error: "360split service not configured" }, 503);
const client = getS3(); const client = getS3();
@ -312,7 +312,7 @@ routes.post("/api/360split/import/:jobId", async (c) => {
routes.post("/api/live-split", async (c) => { routes.post("/api/live-split", async (c) => {
const authToken = extractToken(c.req.raw.headers); const authToken = extractToken(c.req.raw.headers);
if (!authToken) return c.json({ error: "Authentication required" }, 401); if (!authToken) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }
if (!SPLIT_360_URL) return c.json({ error: "360split service not configured" }, 503); if (!SPLIT_360_URL) return c.json({ error: "360split service not configured" }, 503);
@ -363,7 +363,7 @@ routes.get("/api/live-split/status/:sessionId", async (c) => {
routes.post("/api/live-split/stop/:sessionId", async (c) => { routes.post("/api/live-split/stop/:sessionId", async (c) => {
const authToken = extractToken(c.req.raw.headers); const authToken = extractToken(c.req.raw.headers);
if (!authToken) return c.json({ error: "Authentication required" }, 401); if (!authToken) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }
if (!SPLIT_360_URL) return c.json({ error: "360split service not configured" }, 503); if (!SPLIT_360_URL) return c.json({ error: "360split service not configured" }, 503);
const sessionId = c.req.param("sessionId"); const sessionId = c.req.param("sessionId");

View File

@ -14,7 +14,7 @@ import * as Automerge from "@automerge/automerge";
import { renderShell } from "../../server/shell"; import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server'; import type { SyncServer } from '../../server/local-first/sync-server';
import { vnbSchema, vnbDocId } from './schemas'; import { vnbSchema, vnbDocId } from './schemas';
@ -612,7 +612,7 @@ routes.get("/api/vehicles", async (c) => {
routes.post("/api/vehicles", async (c) => { routes.post("/api/vehicles", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -691,7 +691,7 @@ routes.get("/api/vehicles/:id", async (c) => {
routes.patch("/api/vehicles/:id", async (c) => { routes.patch("/api/vehicles/:id", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -782,7 +782,7 @@ routes.get("/api/vehicles/:id/availability", async (c) => {
routes.post("/api/vehicles/:id/availability", async (c) => { routes.post("/api/vehicles/:id/availability", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -822,7 +822,7 @@ routes.post("/api/vehicles/:id/availability", async (c) => {
routes.patch("/api/availability/:id", async (c) => { routes.patch("/api/availability/:id", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -888,7 +888,7 @@ routes.get("/api/rentals", async (c) => {
routes.post("/api/rentals", async (c) => { routes.post("/api/rentals", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -967,7 +967,7 @@ function rentalTransition(statusTarget: RentalStatus, timestampField: 'responded
return async (c: any) => { return async (c: any) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -997,7 +997,7 @@ routes.post("/api/rentals/:id/complete", rentalTransition('completed', 'complete
routes.post("/api/rentals/:id/messages", async (c) => { routes.post("/api/rentals/:id/messages", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -1047,7 +1047,7 @@ routes.get("/api/endorsements", async (c) => {
routes.post("/api/endorsements", async (c) => { routes.post("/api/endorsements", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;
@ -1250,7 +1250,7 @@ routes.get("/api/config", async (c) => {
routes.patch("/api/config", async (c) => { routes.patch("/api/config", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space; const dataSpace = c.get("effectiveSpace") || space;

View File

@ -15,7 +15,7 @@ import * as Automerge from '@automerge/automerge';
import { renderShell } from "../../server/shell"; import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server'; import type { SyncServer } from '../../server/local-first/sync-server';
import { proposalSchema, proposalDocId, computeElo, ELO_DEFAULT } from './schemas'; import { proposalSchema, proposalDocId, computeElo, ELO_DEFAULT } from './schemas';
@ -286,7 +286,7 @@ routes.post("/api/spaces", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { name, slug, description, visibility = "public" } = body; const { name, slug, description, visibility = "public" } = body;
@ -359,7 +359,7 @@ routes.post("/api/proposals", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { space_slug, title, description } = body; const { space_slug, title, description } = body;
@ -407,7 +407,7 @@ routes.post("/api/proposals/:id/vote", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const id = c.req.param("id"); const id = c.req.param("id");
const body = await c.req.json(); const body = await c.req.json();
@ -470,7 +470,7 @@ routes.post("/api/proposals/:id/final-vote", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims; let claims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const id = c.req.param("id"); const id = c.req.param("id");
const body = await c.req.json(); const body = await c.req.json();
@ -564,7 +564,7 @@ routes.get("/api/proposals/pair", (c) => {
routes.post("/api/proposals/compare", async (c) => { routes.post("/api/proposals/compare", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json(); const body = await c.req.json();
const { winnerId, loserId } = body; const { winnerId, loserId } = body;

View File

@ -10,7 +10,7 @@ import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "../../server/auth";
const routes = new Hono(); const routes = new Hono();
@ -271,7 +271,7 @@ async function verifyWalletAuth(c: any): Promise<{ sub: string; did?: string; us
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return null; if (!token) return null;
try { try {
const claims = await verifyEncryptIDToken(token); const claims = await verifyToken(token);
return claims as any; return claims as any;
} catch { } catch {
return null; return null;

24
server/auth.ts Normal file
View File

@ -0,0 +1,24 @@
/**
* Auth wrapper local JWT verification for rSpace server.
*
* When JWT_SECRET is available, verifies tokens locally via HMAC-SHA256 (<1ms).
* Otherwise falls back to internal HTTP call to EncryptID service.
* Re-exports extractToken and types for convenience.
*/
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import type { EncryptIDClaims, VerifyOptions } from "@encryptid/sdk/server";
export { extractToken };
export type { EncryptIDClaims };
const JWT_SECRET = process.env.JWT_SECRET;
const ENCRYPTID_INTERNAL = process.env.ENCRYPTID_INTERNAL_URL || "http://encryptid:3000";
const verifyOpts: VerifyOptions = JWT_SECRET
? { secret: JWT_SECRET }
: { serverUrl: ENCRYPTID_INTERNAL };
export function verifyToken(token: string): Promise<EncryptIDClaims> {
return verifyEncryptIDToken(token, verifyOpts);
}

View File

@ -1,5 +1,16 @@
declare module '@encryptid/sdk/server' { declare module '@encryptid/sdk/server' {
export function verifyEncryptIDToken(token: string): Promise<EncryptIDClaims>; export interface VerifyOptions {
/** JWT secret for local HMAC-SHA256 verification */
secret?: string;
/** EncryptID server URL for remote verification */
serverUrl?: string;
/** Expected audience */
audience?: string;
/** Clock tolerance in seconds */
clockTolerance?: number;
}
export function verifyEncryptIDToken(token: string, options?: VerifyOptions): Promise<EncryptIDClaims>;
export function evaluateSpaceAccess( export function evaluateSpaceAccess(
slug: string, slug: string,
token: string | null, token: string | null,

View File

@ -35,12 +35,12 @@ import { seedTemplateShapes } from "./seed-template";
// Campaign demo moved to rsocials module — see modules/rsocials/campaign-data.ts // Campaign demo moved to rsocials module — see modules/rsocials/campaign-data.ts
import type { SpaceVisibility } from "./community-store"; import type { SpaceVisibility } from "./community-store";
import { import {
verifyEncryptIDToken,
evaluateSpaceAccess, evaluateSpaceAccess,
extractToken,
authenticateWSUpgrade, authenticateWSUpgrade,
} from "@encryptid/sdk/server"; } from "@encryptid/sdk/server";
import type { EncryptIDClaims, SpaceAuthConfig } from "@encryptid/sdk/server"; import type { SpaceAuthConfig } from "@encryptid/sdk/server";
import { verifyToken, extractToken } from "./auth";
import type { EncryptIDClaims } from "./auth";
// ── Module system ── // ── Module system ──
import { registerModule, getAllModules, getModuleInfoList, getModule } from "../shared/module"; import { registerModule, getAllModules, getModuleInfoList, getModule } from "../shared/module";
@ -518,7 +518,7 @@ app.post("/api/communities", async (c) => {
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { try {
claims = await verifyEncryptIDToken(token); claims = await verifyToken(token);
} catch { } catch {
return c.json({ error: "Invalid or expired authentication token" }, 401); return c.json({ error: "Invalid or expired authentication token" }, 401);
} }
@ -653,7 +653,7 @@ app.get("/api/space-access/:slug", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ access: false, reason: "not-authenticated" }); if (!token) return c.json({ access: false, reason: "not-authenticated" });
let claims: EncryptIDClaims | null = null; let claims: EncryptIDClaims | null = null;
try { claims = await verifyEncryptIDToken(token); } catch {} try { claims = await verifyToken(token); } catch {}
if (!claims) return c.json({ access: false, reason: "not-authenticated" }); if (!claims) return c.json({ access: false, reason: "not-authenticated" });
const config = await getSpaceConfig(slug); const config = await getSpaceConfig(slug);
@ -711,7 +711,7 @@ import { getBalance, getTokenDoc, transferTokens, mintFromOnChain } from "./toke
// Wire EncryptID JWT verifier into CRDT scheme // Wire EncryptID JWT verifier into CRDT scheme
setTokenVerifier(async (token: string) => { setTokenVerifier(async (token: string) => {
const claims = await verifyEncryptIDToken(token); const claims = await verifyToken(token);
return { sub: claims.sub, did: claims.did as string | undefined, username: claims.username }; return { sub: claims.sub, did: claims.did as string | undefined, username: claims.username };
}); });
@ -735,7 +735,7 @@ const x402Test = setupX402FromEnv({
console.warn("[x402 bridge] No JWT — skipping cUSDC mint (on-chain payment still valid)"); console.warn("[x402 bridge] No JWT — skipping cUSDC mint (on-chain payment still valid)");
return; return;
} }
const claims = await verifyEncryptIDToken(token); const claims = await verifyToken(token);
const did = (claims.did as string) || `did:key:${(claims.sub as string).slice(0, 32)}`; const did = (claims.did as string) || `did:key:${(claims.sub as string).slice(0, 32)}`;
const label = claims.username || did; const label = claims.username || did;
const amount = process.env.X402_UPLOAD_PRICE || "0.01"; const amount = process.env.X402_UPLOAD_PRICE || "0.01";
@ -1619,8 +1619,13 @@ const OLLAMA_MODELS: Record<string, string> = {
"mistral-small": "mistral-small:24b", "mistral-small": "mistral-small:24b",
}; };
const CANVAS_TOOLS_SYSTEM_PROMPT = `You are a helpful AI assistant in rSpace, a collaborative canvas workspace.
When the user asks to create, show, display, or visualize something, use the available tools to spawn shapes on the canvas.
After creating shapes, give a brief summary of what you placed. Only create shapes directly relevant to the request.
For text-only questions (explanations, coding help, math), respond with text don't create shapes unless asked.`;
app.post("/api/prompt", async (c) => { app.post("/api/prompt", async (c) => {
const { messages, model = "gemini-flash" } = await c.req.json(); const { messages, model = "gemini-flash", useTools = false, systemPrompt } = await c.req.json();
if (!messages?.length) return c.json({ error: "messages required" }, 400); if (!messages?.length) return c.json({ error: "messages required" }, 400);
// Determine provider // Determine provider
@ -1629,7 +1634,15 @@ app.post("/api/prompt", async (c) => {
const { GoogleGenerativeAI } = await import("@google/generative-ai"); const { GoogleGenerativeAI } = await import("@google/generative-ai");
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
const geminiModel = genAI.getGenerativeModel({ model: GEMINI_MODELS[model] });
// Build model config with optional tools
const modelConfig: any = { model: GEMINI_MODELS[model] };
if (useTools) {
const { CANVAS_TOOL_DECLARATIONS } = await import("../lib/canvas-tools");
modelConfig.tools = [{ functionDeclarations: CANVAS_TOOL_DECLARATIONS }];
modelConfig.systemInstruction = systemPrompt || CANVAS_TOOLS_SYSTEM_PROMPT;
}
const geminiModel = genAI.getGenerativeModel(modelConfig);
// Convert chat messages to Gemini contents format (with optional images) // Convert chat messages to Gemini contents format (with optional images)
const contents = messages.map((m: { role: string; content: string; images?: string[] }) => { const contents = messages.map((m: { role: string; content: string; images?: string[] }) => {
@ -1646,9 +1659,54 @@ app.post("/api/prompt", async (c) => {
}); });
try { try {
const result = await geminiModel.generateContent({ contents }); if (!useTools) {
const text = result.response.text(); const result = await geminiModel.generateContent({ contents });
return c.json({ content: text }); const text = result.response.text();
return c.json({ content: text });
}
// Multi-turn tool loop (max 5 rounds)
const toolCalls: { name: string; args: Record<string, any>; label: string }[] = [];
const { findTool } = await import("../lib/canvas-tools");
let loopContents = [...contents];
for (let turn = 0; turn < 5; turn++) {
const result = await geminiModel.generateContent({ contents: loopContents });
const response = result.response;
const candidate = response.candidates?.[0];
if (!candidate) break;
const parts = candidate.content?.parts || [];
const fnCalls = parts.filter((p: any) => p.functionCall);
if (fnCalls.length === 0) {
// No more tool calls — extract final text
const text = response.text();
return c.json({ content: text, toolCalls });
}
// Record tool calls and build function responses
const fnResponseParts: any[] = [];
for (const part of fnCalls) {
const fc = part.functionCall;
const tool = findTool(fc.name);
const label = tool?.actionLabel(fc.args) || fc.name;
toolCalls.push({ name: fc.name, args: fc.args, label });
fnResponseParts.push({
functionResponse: {
name: fc.name,
response: { success: true, message: `${label} — shape will be created on the canvas.` },
},
});
}
// Append model turn + function responses for next iteration
loopContents.push({ role: "model", parts });
loopContents.push({ role: "user", parts: fnResponseParts });
}
// Exhausted loop — return what we have
return c.json({ content: "I've set up the requested items on your canvas.", toolCalls });
} catch (e: any) { } catch (e: any) {
console.error("[prompt] Gemini error:", e.message); console.error("[prompt] Gemini error:", e.message);
return c.json({ error: "Gemini request failed" }, 502); return c.json({ error: "Gemini request failed" }, 502);
@ -1985,7 +2043,7 @@ app.post("/api/spaces/auto-provision", async (c) => {
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { try {
claims = await verifyEncryptIDToken(token); claims = await verifyToken(token);
} catch { } catch {
return c.json({ error: "Invalid or expired token" }, 401); return c.json({ error: "Invalid or expired token" }, 401);
} }
@ -2111,7 +2169,7 @@ for (const mod of getAllModules()) {
return c.json({ error: "Authentication required" }, 401); return c.json({ error: "Authentication required" }, 401);
} }
let claims: EncryptIDClaims | null = null; let claims: EncryptIDClaims | null = null;
try { claims = await verifyEncryptIDToken(token); } catch {} try { claims = await verifyToken(token); } catch {}
if (!claims) { if (!claims) {
return c.json({ error: "Authentication required" }, 401); return c.json({ error: "Authentication required" }, 401);
} }
@ -2135,7 +2193,7 @@ for (const mod of getAllModules()) {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
let claims: EncryptIDClaims | null = null; let claims: EncryptIDClaims | null = null;
if (token) { if (token) {
try { claims = await verifyEncryptIDToken(token); } catch {} try { claims = await verifyToken(token); } catch {}
} }
const resolved = await resolveCallerRole(space, claims); const resolved = await resolveCallerRole(space, claims);
if (resolved) { if (resolved) {
@ -2242,7 +2300,7 @@ app.get("/admin-data", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
if (!isAdminDID(claims.sub)) return c.json({ error: "Admin access required" }, 403); if (!isAdminDID(claims.sub)) return c.json({ error: "Admin access required" }, 403);
const STORAGE_DIR = process.env.STORAGE_DIR || "./data/communities"; const STORAGE_DIR = process.env.STORAGE_DIR || "./data/communities";
@ -2309,7 +2367,7 @@ app.post("/admin-action", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
if (!isAdminDID(claims.sub)) return c.json({ error: "Admin access required" }, 403); if (!isAdminDID(claims.sub)) return c.json({ error: "Admin access required" }, 403);
const body = await c.req.json(); const body = await c.req.json();
@ -2721,7 +2779,7 @@ const server = Bun.serve<WSData>({
if (jwtUsername === subdomain && !provisioningInProgress.has(subdomain)) { if (jwtUsername === subdomain && !provisioningInProgress.has(subdomain)) {
provisioningInProgress.add(subdomain); provisioningInProgress.add(subdomain);
try { try {
const claims = await verifyEncryptIDToken(token); const claims = await verifyToken(token);
const username = claims.username?.toLowerCase(); const username = claims.username?.toLowerCase();
if (username === subdomain && !(await communityExists(subdomain))) { if (username === subdomain && !(await communityExists(subdomain))) {
await createSpace({ await createSpace({

View File

@ -7,11 +7,8 @@
import { Hono } from "hono"; import { Hono } from "hono";
import type { Context, Next } from "hono"; import type { Context, Next } from "hono";
import { import { verifyToken, extractToken } from "../auth";
verifyEncryptIDToken, import type { EncryptIDClaims } from "../auth";
extractToken,
} from "@encryptid/sdk/server";
import type { EncryptIDClaims } from "@encryptid/sdk/server";
import { import {
putBackup, putBackup,
getBackup, getBackup,
@ -40,7 +37,7 @@ backupRouter.use("*", async (c: Context<BackupEnv>, next: Next) => {
} }
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { try {
claims = await verifyEncryptIDToken(token); claims = await verifyToken(token);
} catch { } catch {
return c.json({ error: "Invalid or expired token" }, 401); return c.json({ error: "Invalid or expired token" }, 401);
} }

View File

@ -13,8 +13,8 @@ import type { MiMessage } from "./mi-provider";
import { getModuleInfoList, getAllModules } from "../shared/module"; import { getModuleInfoList, getAllModules } from "../shared/module";
import { resolveCallerRole, roleAtLeast } from "./spaces"; import { resolveCallerRole, roleAtLeast } from "./spaces";
import type { SpaceRoleString } from "./spaces"; import type { SpaceRoleString } from "./spaces";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyToken, extractToken } from "./auth";
import type { EncryptIDClaims } from "@encryptid/sdk/server"; import type { EncryptIDClaims } from "./auth";
import { buildModuleCapabilities, MODULE_ROUTES } from "../lib/mi-module-routes"; import { buildModuleCapabilities, MODULE_ROUTES } from "../lib/mi-module-routes";
import type { MiAction } from "../lib/mi-actions"; import type { MiAction } from "../lib/mi-actions";
@ -40,7 +40,7 @@ mi.post("/ask", async (c) => {
try { try {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (token) { if (token) {
claims = await verifyEncryptIDToken(token); claims = await verifyToken(token);
} }
} catch { /* unauthenticated → viewer */ } } catch { /* unauthenticated → viewer */ }
@ -369,7 +369,7 @@ mi.post("/validate-actions", async (c) => {
try { try {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (token) { if (token) {
const claims = await verifyEncryptIDToken(token); const claims = await verifyToken(token);
if (space && claims) { if (space && claims) {
const resolved = await resolveCallerRole(space, claims); const resolved = await resolveCallerRole(space, claims);
if (resolved) callerRole = resolved.role; if (resolved) callerRole = resolved.role;

View File

@ -5,10 +5,7 @@
*/ */
import { Hono } from "hono"; import { Hono } from "hono";
import { import { verifyToken, extractToken } from "./auth";
verifyEncryptIDToken,
extractToken,
} from "@encryptid/sdk/server";
import { import {
getUserNotifications, getUserNotifications,
getUnreadCount, getUnreadCount,
@ -28,7 +25,7 @@ async function requireAuth(req: Request) {
const token = extractToken(req.headers); const token = extractToken(req.headers);
if (!token) return null; if (!token) return null;
try { try {
return await verifyEncryptIDToken(token); return await verifyToken(token);
} catch { } catch {
return null; return null;
} }

View File

@ -59,11 +59,8 @@ import {
invertDirection, invertDirection,
computeMembranePermeability, computeMembranePermeability,
} from "../lib/connection-types"; } from "../lib/connection-types";
import { import { verifyToken, extractToken } from "./auth";
verifyEncryptIDToken, import type { EncryptIDClaims } from "./auth";
extractToken,
} from "@encryptid/sdk/server";
import type { EncryptIDClaims } from "@encryptid/sdk/server";
import { getAllModules, getModule } from "../shared/module"; import { getAllModules, getModule } from "../shared/module";
import type { SpaceLifecycleContext } from "../shared/module"; import type { SpaceLifecycleContext } from "../shared/module";
import { syncServer } from "./sync-instance"; import { syncServer } from "./sync-instance";
@ -222,7 +219,7 @@ spaces.get("/", async (c) => {
let claims: EncryptIDClaims | null = null; let claims: EncryptIDClaims | null = null;
if (token) { if (token) {
try { try {
claims = await verifyEncryptIDToken(token); claims = await verifyToken(token);
} catch { } catch {
// Invalid token — treat as unauthenticated // Invalid token — treat as unauthenticated
} }
@ -320,7 +317,7 @@ spaces.post("/", async (c) => {
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { try {
claims = await verifyEncryptIDToken(token); claims = await verifyToken(token);
} catch { } catch {
return c.json({ error: "Invalid or expired token" }, 401); return c.json({ error: "Invalid or expired token" }, 401);
} }
@ -397,7 +394,7 @@ spaces.patch("/:slug/modules", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const doc = getDocumentData(slug); const doc = getDocumentData(slug);
@ -484,7 +481,7 @@ spaces.get("/admin", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
if (!isAdmin(claims.sub)) return c.json({ error: "Admin access required" }, 403); if (!isAdmin(claims.sub)) return c.json({ error: "Admin access required" }, 403);
const STORAGE_DIR = process.env.STORAGE_DIR || "./data/communities"; const STORAGE_DIR = process.env.STORAGE_DIR || "./data/communities";
@ -550,7 +547,7 @@ spaces.delete("/admin/:slug", async (c) => {
const token = extractToken(c.req.raw.headers); const token = extractToken(c.req.raw.headers);
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
if (!isAdmin(claims.sub)) return c.json({ error: "Admin access required" }, 403); if (!isAdmin(claims.sub)) return c.json({ error: "Admin access required" }, 403);
await loadCommunity(slug); await loadCommunity(slug);
@ -598,7 +595,7 @@ spaces.get("/notifications", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const slugs = await listCommunities(); const slugs = await listCommunities();
const ownedSlugs = new Set<string>(); const ownedSlugs = new Set<string>();
@ -647,7 +644,7 @@ spaces.delete("/:slug", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
// Protect core spaces // Protect core spaces
if (slug === "demo" || slug === "commonshub") { if (slug === "demo" || slug === "commonshub") {
@ -696,7 +693,7 @@ spaces.patch("/:slug", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -736,7 +733,7 @@ spaces.get("/:slug/members", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -765,7 +762,7 @@ spaces.patch("/:slug/members/:did", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -815,7 +812,7 @@ spaces.delete("/:slug/members/:did", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -856,7 +853,7 @@ spaces.get("/:slug/access-requests", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -896,7 +893,7 @@ spaces.patch("/:slug/nest-policy", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -935,7 +932,7 @@ spaces.post("/:slug/nest", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json<{ const body = await c.req.json<{
sourceSlug: string; sourceSlug: string;
@ -1072,7 +1069,7 @@ spaces.patch("/:slug/nest/:refId", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -1104,7 +1101,7 @@ spaces.delete("/:slug/nest/:refId", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -1146,7 +1143,7 @@ spaces.get("/:slug/nested-in", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
// Must be admin/owner of the space to see where it's nested // Must be admin/owner of the space to see where it's nested
await loadCommunity(slug); await loadCommunity(slug);
@ -1175,7 +1172,7 @@ spaces.get("/:slug/nest-requests", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -1203,7 +1200,7 @@ spaces.patch("/:slug/nest-requests/:reqId", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -1290,7 +1287,7 @@ spaces.patch("/:slug/connection-policy", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -1316,7 +1313,7 @@ spaces.get("/:slug/connections", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -1341,7 +1338,7 @@ spaces.post("/:slug/connections", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json<{ const body = await c.req.json<{
toSlug: string; toSlug: string;
@ -1496,7 +1493,7 @@ spaces.get("/:slug/connections/:connId", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -1523,7 +1520,7 @@ spaces.patch("/:slug/connections/:connId", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -1592,7 +1589,7 @@ spaces.delete("/:slug/connections/:connId", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -1641,7 +1638,7 @@ spaces.get("/:slug/connection-requests", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -1669,7 +1666,7 @@ spaces.patch("/:slug/connection-requests/:reqId", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -1759,7 +1756,7 @@ spaces.get("/:slug/membrane", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -1803,7 +1800,7 @@ spaces.patch("/:slug/encryption", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -1843,7 +1840,7 @@ spaces.post("/:slug/access-requests", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -1904,7 +1901,7 @@ spaces.patch("/:slug/access-requests/:reqId", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -1985,7 +1982,7 @@ spaces.post("/:slug/copy-shapes", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
// Verify target space exists and user has write access // Verify target space exists and user has write access
await loadCommunity(targetSlug); await loadCommunity(targetSlug);
@ -2098,7 +2095,7 @@ spaces.post("/:slug/invite", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -2218,7 +2215,7 @@ spaces.post("/:slug/members/add", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);
@ -2325,7 +2322,7 @@ spaces.post("/:slug/invite/accept", async (c) => {
if (!token) return c.json({ error: "Authentication required — sign in first" }, 401); if (!token) return c.json({ error: "Authentication required — sign in first" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json<{ inviteToken: string }>(); const body = await c.req.json<{ inviteToken: string }>();
if (!body.inviteToken) return c.json({ error: "inviteToken is required" }, 400); if (!body.inviteToken) return c.json({ error: "inviteToken is required" }, 400);
@ -2359,7 +2356,7 @@ spaces.get("/:slug/invites", async (c) => {
if (!token) return c.json({ error: "Authentication required" }, 401); if (!token) return c.json({ error: "Authentication required" }, 401);
let claims: EncryptIDClaims; let claims: EncryptIDClaims;
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
await loadCommunity(slug); await loadCommunity(slug);
const data = getDocumentData(slug); const data = getDocumentData(slug);

View File

@ -54,6 +54,7 @@ export class RStackSpaceSettings extends HTMLElement {
private _addMode: "username" | "email" = "username"; private _addMode: "username" | "email" = "username";
private _lookupResult: { did: string; username: string; displayName: string } | null = null; private _lookupResult: { did: string; username: string; displayName: string } | null = null;
private _lookupError = ""; private _lookupError = "";
private _searchResults: { id: string; did: string; username: string; displayName: string }[] = [];
private _moduleId = ""; private _moduleId = "";
private _moduleConfig: ModuleConfig | null = null; private _moduleConfig: ModuleConfig | null = null;
private _moduleSettingsValues: Record<string, string | boolean> = {}; private _moduleSettingsValues: Record<string, string | boolean> = {};
@ -167,20 +168,25 @@ export class RStackSpaceSettings extends HTMLElement {
} catch {} } catch {}
} }
// Resolve missing displayNames from EncryptID // Resolve displayNames from EncryptID for all members + owner
const unresolvedDids = this._members.filter(m => !m.displayName).map(m => m.did); const allDids = [...new Set([
if (unresolvedDids.length) { ...this._members.map(m => m.did),
...(this._ownerDID ? [this._ownerDID] : []),
])];
if (allDids.length) {
try { try {
const res = await fetch("/api/users/resolve-dids", { const res = await fetch("/api/users/resolve-dids", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ dids: unresolvedDids }), body: JSON.stringify({ dids: allDids }),
}); });
if (res.ok) { if (res.ok) {
const resolved = await res.json() as Record<string, { username: string; displayName: string }>; const resolved = await res.json() as Record<string, { username: string; displayName: string }>;
for (const m of this._members) { for (const m of this._members) {
if (!m.displayName && resolved[m.did]) { // Match by did or by id (members may be stored as userId)
m.displayName = resolved[m.did].displayName || resolved[m.did].username; const info = resolved[m.did];
if (info) {
m.displayName = info.displayName || info.username;
} }
} }
} }
@ -291,7 +297,7 @@ export class RStackSpaceSettings extends HTMLElement {
const membersHTML = this._members.map(m => { const membersHTML = this._members.map(m => {
const isOwnerRow = m.did === this._ownerDID; const isOwnerRow = m.did === this._ownerDID;
const displayName = m.displayName || m.did.slice(0, 16) + "…"; const displayName = m.displayName || "Unknown user";
const initial = displayName.charAt(0).toUpperCase(); const initial = displayName.charAt(0).toUpperCase();
const roleBadge = isOwnerRow const roleBadge = isOwnerRow
? `<span class="role-badge role-owner">Owner</span>` ? `<span class="role-badge role-owner">Owner</span>`
@ -404,8 +410,20 @@ export class RStackSpaceSettings extends HTMLElement {
${this._addMode === "username" ? ` ${this._addMode === "username" ? `
<div class="add-form"> <div class="add-form">
<input type="text" class="input" id="add-username" placeholder="Username…" /> <div class="search-wrapper">
${this._lookupResult ? `<div class="lookup-result">Found: <strong>${this._esc(this._lookupResult.displayName)}</strong> (@${this._esc(this._lookupResult.username)})</div>` : ""} <input type="text" class="input" id="add-username" placeholder="Search username…" autocomplete="off" />
${this._searchResults.length > 0 ? `
<div class="search-dropdown">
${this._searchResults.map((u, i) => `
<div class="search-item" data-idx="${i}">
<span class="search-item-name">${this._esc(u.displayName)}</span>
<span class="search-item-username">@${this._esc(u.username)}</span>
</div>
`).join("")}
</div>
` : ""}
</div>
${this._lookupResult ? `<div class="lookup-result">Selected: <strong>${this._esc(this._lookupResult.displayName)}</strong> (@${this._esc(this._lookupResult.username)})</div>` : ""}
${this._lookupError ? `<div class="error-msg">${this._esc(this._lookupError)}</div>` : ""} ${this._lookupError ? `<div class="error-msg">${this._esc(this._lookupError)}</div>` : ""}
<div class="add-row"> <div class="add-row">
<select class="input role-input" id="add-role"> <select class="input role-input" id="add-role">
@ -414,7 +432,7 @@ export class RStackSpaceSettings extends HTMLElement {
<option value="moderator">moderator</option> <option value="moderator">moderator</option>
<option value="admin">admin</option> <option value="admin">admin</option>
</select> </select>
<button class="add-btn" id="add-by-username">Add</button> <button class="add-btn" id="add-by-username" ${!this._lookupResult ? "disabled" : ""}>Add</button>
</div> </div>
</div> </div>
` : ` ` : `
@ -473,6 +491,14 @@ export class RStackSpaceSettings extends HTMLElement {
}); });
} }
// Search result selection
sr.querySelectorAll(".search-item").forEach(item => {
item.addEventListener("click", () => {
const idx = parseInt((item as HTMLElement).dataset.idx || "0");
if (this._searchResults[idx]) this._selectSearchResult(this._searchResults[idx]);
});
});
// Add by username // Add by username
sr.getElementById("add-by-username")?.addEventListener("click", () => this._addByUsername()); sr.getElementById("add-by-username")?.addEventListener("click", () => this._addByUsername());
@ -522,27 +548,48 @@ export class RStackSpaceSettings extends HTMLElement {
} }
private async _lookupUser(username: string) { private async _lookupUser(username: string) {
if (!username || username.length < 2) return; if (!username || username.length < 2) {
this._searchResults = [];
this._lookupResult = null;
this._lookupError = "";
this._render();
return;
}
const token = getToken(); const token = getToken();
if (!token) return; if (!token) return;
try { try {
const res = await fetch(`https://auth.rspace.online/api/users/lookup?username=${encodeURIComponent(username)}`, { const res = await fetch(`/api/users/search?q=${encodeURIComponent(username)}&limit=5`, {
headers: { "Authorization": `Bearer ${token}` }, headers: { "Authorization": `Bearer ${token}` },
}); });
if (res.ok) { if (res.ok) {
this._lookupResult = await res.json(); this._searchResults = await res.json();
this._lookupError = ""; this._lookupError = this._searchResults.length === 0 && username.length > 2 ? "No users found" : "";
// Auto-select if exact match
const exact = this._searchResults.find(u => u.username.toLowerCase() === username.toLowerCase());
this._lookupResult = exact ? { did: exact.id, username: exact.username, displayName: exact.displayName } : null;
} else { } else {
this._searchResults = [];
this._lookupResult = null; this._lookupResult = null;
this._lookupError = username.length > 2 ? "User not found" : ""; this._lookupError = username.length > 2 ? "No users found" : "";
} }
} catch { } catch {
this._lookupError = "Lookup failed"; this._lookupError = "Lookup failed";
this._searchResults = [];
} }
this._render(); this._render();
} }
private _selectSearchResult(user: { id: string; did: string; username: string; displayName: string }) {
this._lookupResult = { did: user.id, username: user.username, displayName: user.displayName };
this._searchResults = [];
this._lookupError = "";
this._render();
// Set the input value to the selected username
const input = this.shadowRoot?.getElementById("add-username") as HTMLInputElement;
if (input) input.value = user.username;
}
private async _addByUsername() { private async _addByUsername() {
const sr = this.shadowRoot!; const sr = this.shadowRoot!;
const input = sr.getElementById("add-username") as HTMLInputElement; const input = sr.getElementById("add-username") as HTMLInputElement;
@ -914,12 +961,58 @@ const PANEL_CSS = `
} }
.add-btn:hover { opacity: 0.9; } .add-btn:hover { opacity: 0.9; }
.search-wrapper {
position: relative;
}
.search-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--rs-bg-surface);
border: 1px solid var(--rs-border);
border-radius: 8px;
margin-top: 4px;
overflow: hidden;
z-index: 10;
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
}
.search-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
transition: background 0.1s;
}
.search-item:hover {
background: var(--rs-bg-hover);
}
.search-item-name {
font-size: 0.82rem;
font-weight: 500;
color: var(--rs-text-primary);
}
.search-item-username {
font-size: 0.72rem;
color: var(--rs-text-muted);
}
.lookup-result { .lookup-result {
font-size: 0.78rem; font-size: 0.78rem;
color: #14b8a6; color: #14b8a6;
padding: 4px 0; padding: 4px 0;
} }
.add-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error-msg { .error-msg {
font-size: 0.78rem; font-size: 0.78rem;
color: #f87171; color: #f87171;

View File

@ -3819,6 +3819,35 @@ app.get('/api/users/lookup', async (c) => {
}); });
}); });
// GET /api/users/search?q=<partial>&limit=5 — fuzzy username/display_name search
app.get('/api/users/search', async (c) => {
const claims = await verifyTokenFromRequest(c.req.header('Authorization'));
if (!claims) return c.json({ error: 'Authentication required' }, 401);
const q = (c.req.query('q') || '').trim();
if (q.length < 2) return c.json({ error: 'Query must be at least 2 characters' }, 400);
const limit = Math.min(Math.max(parseInt(c.req.query('limit') || '5'), 1), 10);
const pattern = `${q}%`;
const rows = await sql`
SELECT id, did, username, display_name
FROM users
WHERE username ILIKE ${pattern} OR display_name ILIKE ${pattern}
ORDER BY
CASE WHEN username ILIKE ${q} THEN 0 ELSE 1 END,
username
LIMIT ${limit}
`;
return c.json(rows.map((r: any) => ({
id: r.id,
did: r.did,
username: r.username,
displayName: r.display_name || r.username,
})));
});
// POST /api/users/resolve-dids — batch-resolve DIDs/userIds to usernames (public profile data) // POST /api/users/resolve-dids — batch-resolve DIDs/userIds to usernames (public profile data)
app.post('/api/users/resolve-dids', async (c) => { app.post('/api/users/resolve-dids', async (c) => {
const body = await c.req.json(); const body = await c.req.json();