From cdfba02b0359dec69925562e1a134612cf4ee31e Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 23 Mar 2026 14:30:55 -0700 Subject: [PATCH 1/3] fix(rcart): exempt payment endpoints from private space access gate Payment creation, QR codes, and pay pages should be accessible to any authenticated user regardless of space visibility, since the payment goes to the creator's wallet. The route handlers enforce their own auth. Co-Authored-By: Claude Opus 4.6 --- server/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/index.ts b/server/index.ts index 31b2ff3..f39250c 100644 --- a/server/index.ts +++ b/server/index.ts @@ -2225,7 +2225,9 @@ for (const mod of getAllModules()) { || pathname.endsWith("/api/transak/config") || pathname.endsWith("/api/transak/webhook") || pathname.endsWith("/api/coinbase/webhook") - || pathname.endsWith("/api/ramp/webhook"); + || pathname.endsWith("/api/ramp/webhook") + || pathname.includes("/rcart/api/payments") + || pathname.includes("/rcart/pay/"); if (!isHtmlRequest && !isPublicEndpoint && (vis === "private" || vis === "permissioned")) { const token = extractToken(c.req.raw.headers); From cb92a7f6d87e76ed9c1b5f9114209b27861e7225 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 23 Mar 2026 14:32:28 -0700 Subject: [PATCH 2/3] =?UTF-8?q?feat(rtrips):=20AI=20trip=20planner=20?= =?UTF-8?q?=E2=80=94=20canvas=20tools=20+=20"Plan=20with=20AI"=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register 5 trip-specific tools (destination, itinerary, booking, budget, packing list) in canvas-tools.ts so Gemini can create trip shapes. Add public toolsEnabled/systemPrompt properties to folk-prompt with persistence. Add #trip-planner hash handler to canvas.html that auto- creates a pre-configured AI prompt. Add "Plan with AI" gradient button to rTrips list and detail views. Co-Authored-By: Claude Opus 4.6 --- lib/canvas-tools.ts | 138 ++++++++++++++++++ lib/folk-prompt.ts | 37 ++++- .../rtrips/components/folk-trips-planner.ts | 7 + website/canvas.html | 27 ++++ 4 files changed, 201 insertions(+), 8 deletions(-) diff --git a/lib/canvas-tools.ts b/lib/canvas-tools.ts index 449776e..0678f45 100644 --- a/lib/canvas-tools.ts +++ b/lib/canvas-tools.ts @@ -141,6 +141,144 @@ const registry: CanvasToolDefinition[] = [ }), actionLabel: (args) => `Generating image: ${args.prompt.slice(0, 50)}${args.prompt.length > 50 ? "..." : ""}`, }, + // ── Trip Planning Tools ── + { + declaration: { + name: "create_destination", + description: "Create a destination card for a trip location. Use when the user mentions a city, place, or stop on their trip.", + parameters: { + type: "object", + properties: { + destName: { type: "string", description: "Name of the destination (city or place)" }, + country: { type: "string", description: "Country name" }, + lat: { type: "number", description: "Latitude coordinate" }, + lng: { type: "number", description: "Longitude coordinate" }, + arrivalDate: { type: "string", description: "Arrival date in YYYY-MM-DD format" }, + departureDate: { type: "string", description: "Departure date in YYYY-MM-DD format" }, + notes: { type: "string", description: "Additional notes about this destination" }, + }, + required: ["destName"], + }, + }, + tagName: "folk-destination", + buildProps: (args) => ({ + destName: args.destName, + ...(args.country ? { country: args.country } : {}), + ...(args.lat != null ? { lat: args.lat } : {}), + ...(args.lng != null ? { lng: args.lng } : {}), + ...(args.arrivalDate ? { arrivalDate: args.arrivalDate } : {}), + ...(args.departureDate ? { departureDate: args.departureDate } : {}), + ...(args.notes ? { notes: args.notes } : {}), + }), + actionLabel: (args) => `Created destination: ${args.destName}${args.country ? `, ${args.country}` : ""}`, + }, + { + declaration: { + name: "create_itinerary", + description: "Create an itinerary card with a list of activities/events organized by date. Use when planning a schedule or day-by-day plan.", + parameters: { + type: "object", + properties: { + tripTitle: { type: "string", description: "Title for the itinerary" }, + itemsJson: { type: "string", description: 'JSON array of items. Each: {"id":"","title":"...","date":"YYYY-MM-DD","startTime":"HH:MM","category":"ACTIVITY|TRANSPORT|MEAL|FREE_TIME|FLIGHT"}' }, + }, + required: ["tripTitle", "itemsJson"], + }, + }, + tagName: "folk-itinerary", + buildProps: (args) => { + let items: any[] = []; + try { items = JSON.parse(args.itemsJson); } catch { items = []; } + return { tripTitle: args.tripTitle, items }; + }, + actionLabel: (args) => `Created itinerary: ${args.tripTitle}`, + }, + { + declaration: { + name: "create_booking", + description: "Create a booking card for a flight, hotel, transport, activity, or restaurant reservation.", + parameters: { + type: "object", + properties: { + bookingType: { + type: "string", + description: "Type of booking", + enum: ["FLIGHT", "HOTEL", "CAR_RENTAL", "TRAIN", "BUS", "FERRY", "ACTIVITY", "RESTAURANT", "OTHER"], + }, + provider: { type: "string", description: "Provider/company name (e.g. airline, hotel name)" }, + cost: { type: "number", description: "Cost amount" }, + currency: { type: "string", description: "ISO currency code (e.g. USD, EUR)" }, + startDate: { type: "string", description: "Start/check-in date in YYYY-MM-DD format" }, + endDate: { type: "string", description: "End/check-out date in YYYY-MM-DD format" }, + bookingStatus: { type: "string", description: "Booking status", enum: ["PENDING", "CONFIRMED", "CANCELLED"] }, + details: { type: "string", description: "Additional booking details or notes" }, + }, + required: ["bookingType", "provider"], + }, + }, + tagName: "folk-booking", + buildProps: (args) => ({ + bookingType: args.bookingType, + provider: args.provider, + ...(args.cost != null ? { cost: args.cost } : {}), + ...(args.currency ? { currency: args.currency } : {}), + ...(args.startDate ? { startDate: args.startDate } : {}), + ...(args.endDate ? { endDate: args.endDate } : {}), + ...(args.bookingStatus ? { bookingStatus: args.bookingStatus } : {}), + ...(args.details ? { details: args.details } : {}), + }), + actionLabel: (args) => `Created booking: ${args.bookingType} — ${args.provider}`, + }, + { + declaration: { + name: "create_budget", + description: "Create a budget tracker card with total budget and expense line items. Use when the user wants to track trip costs.", + parameters: { + type: "object", + properties: { + budgetTotal: { type: "number", description: "Total budget amount" }, + currency: { type: "string", description: "ISO currency code (e.g. USD, EUR)" }, + expensesJson: { type: "string", description: 'JSON array of expenses. Each: {"id":"","category":"TRANSPORT|ACCOMMODATION|FOOD|ACTIVITY|SHOPPING|OTHER","description":"...","amount":123,"date":"YYYY-MM-DD"}' }, + }, + required: ["budgetTotal"], + }, + }, + tagName: "folk-budget", + buildProps: (args) => { + let expenses: any[] = []; + try { expenses = JSON.parse(args.expensesJson); } catch { expenses = []; } + return { + budgetTotal: args.budgetTotal, + ...(args.currency ? { currency: args.currency } : {}), + expenses, + }; + }, + actionLabel: (args) => `Created budget: ${args.currency || "USD"} ${args.budgetTotal}`, + }, + { + declaration: { + name: "create_packing_list", + description: "Create a packing list card with checkable items organized by category. Use when the user needs help with what to pack.", + parameters: { + type: "object", + properties: { + itemsJson: { type: "string", description: 'JSON array of packing items. Each: {"id":"","name":"...","category":"CLOTHING|FOOTWEAR|ELECTRONICS|GEAR|PERSONAL|DOCUMENTS|SAFETY|SUPPLIES","quantity":1,"packed":false}' }, + }, + required: ["itemsJson"], + }, + }, + tagName: "folk-packing-list", + buildProps: (args) => { + let items: any[] = []; + try { items = JSON.parse(args.itemsJson); } catch { items = []; } + return { items }; + }, + actionLabel: (args) => { + let count = 0; + try { count = JSON.parse(args.itemsJson).length; } catch {} + return `Created packing list (${count} items)`; + }, + }, ]; export const CANVAS_TOOLS: CanvasToolDefinition[] = [...registry]; diff --git a/lib/folk-prompt.ts b/lib/folk-prompt.ts index bdb5d31..8f636f5 100644 --- a/lib/folk-prompt.ts +++ b/lib/folk-prompt.ts @@ -386,6 +386,7 @@ export class FolkPrompt extends FolkShape { #model = "gemini-flash"; #pendingImages: string[] = []; #toolsEnabled = false; + #systemPrompt = ""; #messagesEl: HTMLElement | null = null; #promptInput: HTMLTextAreaElement | null = null; @@ -399,6 +400,20 @@ export class FolkPrompt extends FolkShape { return this.#messages; } + get toolsEnabled() { return this.#toolsEnabled; } + set toolsEnabled(v: boolean) { + this.#toolsEnabled = v; + this.#toolsBtn?.classList.toggle("active", v); + if (this.#promptInput) { + this.#promptInput.placeholder = v + ? "Ask me to create maps, notes, images, or embeds on the canvas..." + : "Type your message..."; + } + } + + get systemPrompt() { return this.#systemPrompt; } + set systemPrompt(v: string) { this.#systemPrompt = v; } + override createRenderRoot() { const root = super.createRenderRoot(); @@ -471,15 +486,15 @@ export class FolkPrompt extends FolkShape { // 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..."; - } + this.toolsEnabled = !this.#toolsEnabled; }); + // Sync initial state after DOM ready + this.#toolsBtn?.classList.toggle("active", this.#toolsEnabled); + if (this.#toolsEnabled && this.#promptInput) { + this.#promptInput.placeholder = "Ask me to create maps, notes, images, or embeds on the canvas..."; + } + // Attach button attachBtn?.addEventListener("click", (e) => { e.stopPropagation(); @@ -635,7 +650,7 @@ export class FolkPrompt extends FolkShape { ...(m.images?.length ? { images: m.images } : {}), })), model: this.#model, - ...(useTools ? { useTools: true } : {}), + ...(useTools ? { useTools: true, systemPrompt: this.#systemPrompt || undefined } : {}), }), }); @@ -780,6 +795,8 @@ export class FolkPrompt extends FolkShape { static override fromData(data: Record): FolkPrompt { const shape = FolkShape.fromData(data) as FolkPrompt; + if (data.toolsEnabled) shape.toolsEnabled = true; + if (data.systemPrompt) shape.systemPrompt = data.systemPrompt; return shape; } @@ -788,6 +805,8 @@ export class FolkPrompt extends FolkShape { ...super.toJSON(), type: "folk-prompt", model: this.#model, + toolsEnabled: this.#toolsEnabled || undefined, + systemPrompt: this.#systemPrompt || undefined, messages: this.messages.filter((m) => m.role !== "tool-action").map((msg) => ({ role: msg.role, content: msg.content, @@ -799,5 +818,7 @@ export class FolkPrompt extends FolkShape { override applyData(data: Record): void { super.applyData(data); + if (data.toolsEnabled !== undefined) this.toolsEnabled = !!data.toolsEnabled; + if (data.systemPrompt !== undefined) this.systemPrompt = data.systemPrompt; } } diff --git a/modules/rtrips/components/folk-trips-planner.ts b/modules/rtrips/components/folk-trips-planner.ts index 6d92d1a..644c216 100644 --- a/modules/rtrips/components/folk-trips-planner.ts +++ b/modules/rtrips/components/folk-trips-planner.ts @@ -421,6 +421,8 @@ class FolkTripsPlanner extends HTMLElement { .rapp-nav__title { font-size: 15px; font-weight: 600; flex: 1; color: var(--rs-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rapp-nav__btn { padding: 6px 14px; border-radius: 6px; border: none; background: #14b8a6; color: #fff; font-weight: 600; cursor: pointer; font-size: 13px; } .rapp-nav__btn:hover { background: #0d9488; } + .rapp-nav__btn--ai { background: linear-gradient(135deg, #0ea5e9, #6366f1); } + .rapp-nav__btn--ai:hover { opacity: 0.9; background: linear-gradient(135deg, #0ea5e9, #6366f1); } .trip-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; } .trip-card { @@ -504,6 +506,7 @@ class FolkTripsPlanner extends HTMLElement {
My Trips +
${this.trips.length > 0 ? `
@@ -546,6 +549,7 @@ class FolkTripsPlanner extends HTMLElement {
${this._history.canGoBack ? `` : ""} ${this.esc(t.title)} + ${st.label}
@@ -678,6 +682,9 @@ class FolkTripsPlanner extends HTMLElement { private attachListeners() { this.shadow.querySelector("#btn-tour")?.addEventListener("click", () => this.startTour()); this.shadow.getElementById("create-trip")?.addEventListener("click", () => this.createTrip()); + this.shadow.getElementById("btn-plan-ai")?.addEventListener("click", () => { + window.location.href = `/${this.space}/rspace#trip-planner`; + }); this.shadow.querySelectorAll("[data-trip]").forEach(el => { el.addEventListener("click", () => { diff --git a/website/canvas.html b/website/canvas.html index a703ec8..009087b 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -3642,6 +3642,33 @@ if (hash.startsWith("#pin-")) { const pinId = hash.slice(5); setTimeout(() => pinManager.openPinById(pinId), 500); + } else if (hash === "#trip-planner") { + history.replaceState(null, "", location.pathname + location.search); + setTimeout(() => { + const TRIP_SYSTEM_PROMPT = `You are a travel planning AI in rSpace. Help plan trips by creating visual shapes on the canvas. + +When the user describes a trip, proactively create: +- Destination cards for each city/place (with coordinates and dates) +- A map showing the destinations +- Flight/accommodation search embeds (Google Flights, Airbnb, Booking.com) +- An itinerary with activities by date +- A budget tracker with estimated costs +- A packing list tailored to the destination + +Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying questions if needed before creating shapes.`; + + const prompt = newShape("folk-prompt", { + toolsEnabled: true, + systemPrompt: TRIP_SYSTEM_PROMPT, + }); + if (prompt) { + setTimeout(() => { + const root = prompt.shadowRoot || prompt; + const ta = root.querySelector(".prompt-input"); + if (ta) ta.focus(); + }, 200); + } + }, 300); } }); From a9ff1cf94b1de13a9cb317dd7295b0d38aa27808 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 23 Mar 2026 14:32:38 -0700 Subject: [PATCH 3/3] feat(encryptid): complete social recovery end-to-end flow Add /recover/social page for users to finalize account recovery after guardian approvals, fix status filter so approved requests remain findable, return requestId from initiation API with tracking link on login page, and add actionUrl to recovery notifications. Co-Authored-By: Claude Opus 4.6 --- src/encryptid/db.ts | 2 +- src/encryptid/server.ts | 225 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 223 insertions(+), 4 deletions(-) diff --git a/src/encryptid/db.ts b/src/encryptid/db.ts index 0d8daa5..a940f28 100644 --- a/src/encryptid/db.ts +++ b/src/encryptid/db.ts @@ -540,7 +540,7 @@ export async function getRecoveryRequest(requestId: string): Promise { const rows = await sql` SELECT * FROM recovery_requests - WHERE user_id = ${userId} AND status = 'pending' AND expires_at > NOW() + WHERE user_id = ${userId} AND status IN ('pending', 'approved') AND expires_at > NOW() ORDER BY initiated_at DESC LIMIT 1 `; if (rows.length === 0) return null; diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index 7719fc0..a608363 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -2257,6 +2257,220 @@ app.get('/recover', (c) => { `); }); +// ============================================================================ +// SOCIAL RECOVERY COMPLETION PAGE +// ============================================================================ + +/** + * GET /recover/social — page where user finalizes social recovery + * After guardians approve, user opens this to register a new passkey + */ +app.get('/recover/social', (c) => { + return c.html(` + + + + + + Social Recovery — rStack Identity + + + +
+
👥
+

Social Recovery

+

Your guardians are helping you recover your account

+ +
Checking recovery status...
+ + + + + + + +
+ + + + + `); +}); + // ============================================================================ // GUARDIAN MANAGEMENT ROUTES // ============================================================================ @@ -2757,7 +2971,7 @@ app.post('/api/recovery/social/initiate', async (c) => { // Check for existing active request const existing = await getActiveRecoveryRequest(user.id); if (existing) { - return c.json({ success: true, message: 'A recovery request is already active. Recovery emails have been re-sent.' }); + return c.json({ success: true, requestId: existing.id, message: 'A recovery request is already active. Recovery emails have been re-sent.' }); } // Create recovery request (7 day expiry, 2-of-3 threshold) @@ -2773,6 +2987,7 @@ app.post('/api/recovery/social/initiate', async (c) => { title: 'Account recovery initiated', body: `A recovery request was initiated for your account. ${accepted.length} guardians have been contacted.`, metadata: { recoveryRequestId: requestId, threshold: 2, totalGuardians: accepted.length }, + actionUrl: `/recover/social?id=${requestId}`, }).catch(() => {}); // Create approval tokens and notify guardians @@ -2854,7 +3069,7 @@ app.post('/api/recovery/social/initiate', async (c) => { } } - return c.json({ success: true, message: 'If the account exists and has guardians, recovery emails have been sent.' }); + return c.json({ success: true, requestId, message: 'If the account exists and has guardians, recovery emails have been sent.' }); }); /** @@ -2973,6 +3188,7 @@ app.post('/api/recovery/social/approve', async (c) => { title: 'Account recovery approved', body: `${request.approvalCount}/${request.threshold} guardians approved. You can now recover your account.`, metadata: { recoveryRequestId: request.id }, + actionUrl: `https://auth.rspace.online/recover/social?id=${request.id}`, }).catch(() => {}); } @@ -7394,7 +7610,10 @@ app.get('/', (c) => { body: JSON.stringify(body), }); const data = await res.json(); - msgEl.textContent = data.message || 'Recovery request sent. Check with your guardians.'; + msgEl.innerHTML = data.message || 'Recovery request sent. Check with your guardians.'; + if (data.requestId) { + msgEl.innerHTML += '

Track your recovery progress →'; + } msgEl.style.color = '#86efac'; msgEl.style.display = 'block'; // Also try email recovery