From 73ad020812954400688625317159ebd134a6aabb Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 20 Mar 2026 11:07:01 -0700 Subject: [PATCH] fix(spaces): fix space creation routing and use /rspace URLs Space creation was broken because the canvas module has id "rspace" but all navigation URLs used "/canvas". On production subdomain routing this resulted in 404s after creating a space. - Switch create-space form from deprecated /api/communities to /api/spaces - Replace all /canvas navigation URLs with /rspace to match module ID - Fix DID matching in space listing to check both sub and did:key formats - Add proper client DID support in EncryptID registration flow Co-Authored-By: Claude Opus 4.6 --- lib/folk-canvas.ts | 4 +-- lib/folk-rapp.ts | 2 +- modules/rspace/mod.ts | 2 +- server/index.ts | 3 ++- server/mi-routes.ts | 2 +- server/spaces.ts | 9 ++++--- shared/components/rstack-space-switcher.ts | 2 +- src/encryptid/db.ts | 17 +++++++++--- src/encryptid/server.ts | 30 +++++++++++++++++----- website/admin.html | 2 +- website/create-space.html | 6 ++--- website/index.html | 2 +- 12 files changed, 57 insertions(+), 24 deletions(-) diff --git a/lib/folk-canvas.ts b/lib/folk-canvas.ts index b3877bf..c76332d 100644 --- a/lib/folk-canvas.ts +++ b/lib/folk-canvas.ts @@ -339,7 +339,7 @@ export class FolkCanvas extends FolkShape { enterBtn.addEventListener("click", (e) => { e.stopPropagation(); if (this.#sourceSlug) { - window.open(`/${this.#sourceSlug}/canvas`, "_blank"); + window.open(`/${this.#sourceSlug}/rspace`, "_blank"); } }); @@ -462,7 +462,7 @@ export class FolkCanvas extends FolkShape { statusBar.style.display = "none"; const enterBtn = content.querySelector(".enter-btn"); enterBtn?.addEventListener("click", () => { - if (this.#sourceSlug) window.open(`/${this.#sourceSlug}/canvas`, "_blank"); + if (this.#sourceSlug) window.open(`/${this.#sourceSlug}/rspace`, "_blank"); }); // Disconnect when collapsed this.#disconnect(); diff --git a/lib/folk-rapp.ts b/lib/folk-rapp.ts index 266943c..077e9f7 100644 --- a/lib/folk-rapp.ts +++ b/lib/folk-rapp.ts @@ -681,7 +681,7 @@ export class FolkRApp extends FolkShape { const attrMode = this.getAttribute("mode"); if (attrMode === "iframe" || attrMode === "widget") this.#mode = attrMode; - // Auto-derive spaceSlug from current URL if not explicitly provided (/{space}/canvas → space) + // Auto-derive spaceSlug from current URL if not explicitly provided (/{space}/rspace → space) if (!this.#spaceSlug) { const pathParts = window.location.pathname.split("/").filter(Boolean); if (pathParts.length >= 1) this.#spaceSlug = pathParts[0]; diff --git a/modules/rspace/mod.ts b/modules/rspace/mod.ts index 36c30c6..045d354 100644 --- a/modules/rspace/mod.ts +++ b/modules/rspace/mod.ts @@ -2,7 +2,7 @@ * Canvas module — the collaborative infinite canvas. * * This is the original rSpace canvas restructured as an rSpace module. - * Routes are relative to the mount point (/:space/canvas in unified mode, + * Routes are relative to the mount point (/:space/rspace in unified mode, * / in standalone mode). */ diff --git a/server/index.ts b/server/index.ts index daec693..2060485 100644 --- a/server/index.ts +++ b/server/index.ts @@ -133,7 +133,8 @@ const DIST_DIR = resolve(import.meta.dir, "../dist"); // ── Hono app ── -const app = new Hono(); +type AppEnv = { Variables: { isSubdomain: boolean } }; +const app = new Hono(); // Detect subdomain routing and set context flag app.use("*", async (c, next) => { diff --git a/server/mi-routes.ts b/server/mi-routes.ts index 24d0a50..bd32085 100644 --- a/server/mi-routes.ts +++ b/server/mi-routes.ts @@ -165,7 +165,7 @@ include action markers in your response. Each marker is on its own line: [MI_ACTION:{"type":"update-shape","shapeId":"shape-123","fields":{"content":"Updated text"}}] [MI_ACTION:{"type":"delete-shape","shapeId":"shape-123"}] [MI_ACTION:{"type":"move-shape","shapeId":"shape-123","x":400,"y":200}] -[MI_ACTION:{"type":"navigate","path":"/myspace/canvas"}] +[MI_ACTION:{"type":"navigate","path":"/myspace/rspace"}] Use "$1", "$2", etc. as ref values when creating shapes, then reference them in subsequent connect actions. Available shape types: folk-markdown, folk-wrapper, folk-image-gen, folk-video-gen, folk-prompt, diff --git a/server/spaces.ts b/server/spaces.ts index f8b5a7d..d2e324f 100644 --- a/server/spaces.ts +++ b/server/spaces.ts @@ -244,8 +244,11 @@ spaces.get("/", async (c) => { seenSlugs.add(slug); let vis = data.meta.visibility || "public"; - const isOwner = !!(claims && data.meta.ownerDID === claims.sub); - const memberEntry = claims ? data.members?.[claims.sub] : undefined; + // Check both claims.sub (raw userId) and did:key: format for + // compatibility — auto-provisioned spaces store ownerDID as did:key: + const callerDid = claims ? ((claims.did as string) || `did:key:${(claims.sub as string).slice(0, 32)}`) : ""; + const isOwner = !!(claims && (data.meta.ownerDID === claims.sub || data.meta.ownerDID === callerDid)); + const memberEntry = claims ? (data.members?.[claims.sub] || data.members?.[callerDid]) : undefined; const isMember = !!memberEntry; // Owner's personal space (slug matches username) is always private @@ -354,7 +357,7 @@ spaces.post("/", async (c) => { name: result.name, visibility: result.visibility, ownerDID: result.ownerDID, - url: `/${result.slug}/canvas`, + url: `/${result.slug}/rspace`, }, 201); }); diff --git a/shared/components/rstack-space-switcher.ts b/shared/components/rstack-space-switcher.ts index 6e62ec7..d9413dc 100644 --- a/shared/components/rstack-space-switcher.ts +++ b/shared/components/rstack-space-switcher.ts @@ -444,7 +444,7 @@ export class RStackSpaceSwitcher extends HTMLElement { }); const data = await res.json(); if (res.ok && data.slug) { - window.location.href = rspaceNavUrl(data.slug, "canvas"); + window.location.href = rspaceNavUrl(data.slug, "rspace"); } else { status.textContent = data.error || "Failed to create space"; submitBtn.disabled = false; diff --git a/src/encryptid/db.ts b/src/encryptid/db.ts index 8c87413..73a55a3 100644 --- a/src/encryptid/db.ts +++ b/src/encryptid/db.ts @@ -82,10 +82,10 @@ export async function getUserByUsername(username: string) { // CREDENTIAL OPERATIONS // ============================================================================ -export async function storeCredential(cred: StoredCredential): Promise { +export async function storeCredential(cred: StoredCredential, did?: string): Promise { // Ensure user exists first (with display name + DID so they're never NULL) - const did = `did:key:${cred.userId.slice(0, 32)}`; - await createUser(cred.userId, cred.username, cred.username, did); + // If a proper DID is provided (e.g. from PRF key derivation), use it; otherwise omit + await createUser(cred.userId, cred.username, cred.username, did || undefined); await sql` INSERT INTO credentials (credential_id, user_id, public_key, counter, transports, created_at, rp_id) @@ -237,6 +237,17 @@ export async function getUserById(userId: string) { return user || null; } +/** Update a user's DID (e.g. upgrading from truncated to proper did:key:z6Mk...) */ +export async function updateUserDid(userId: string, newDid: string): Promise { + await sql`UPDATE users SET did = ${newDid}, updated_at = NOW() WHERE id = ${userId}`; +} + +/** Update all space memberships from one DID to another */ +export async function migrateSpaceMemberDid(oldDid: string, newDid: string): Promise { + const result = await sql`UPDATE space_members SET user_did = ${newDid} WHERE user_did = ${oldDid}`; + return result.count; +} + // ============================================================================ // RECOVERY TOKEN OPERATIONS // ============================================================================ diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index 6bc0574..1a40f3d 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -562,7 +562,7 @@ app.post('/api/register/start', async (c) => { * Complete registration - verify and store credential */ app.post('/api/register/complete', async (c) => { - const { challenge, credential, userId, username, email } = await c.req.json(); + const { challenge, credential, userId, username, email, clientDid, eoaAddress } = await c.req.json(); // Verify challenge const challengeRecord = await getChallenge(challenge); @@ -578,8 +578,11 @@ app.post('/api/register/complete', async (c) => { // In production, verify the attestation properly // For now, we trust the client-side verification - // Create user and store credential in database - const did = `did:key:${userId.slice(0, 32)}`; + // Use client-derived DID if available (proper did:key:z6Mk... from PRF), + // otherwise generate a provisional one from userId + const did = (clientDid && typeof clientDid === 'string' && clientDid.startsWith('did:key:z')) + ? clientDid + : `did:key:${userId.slice(0, 32)}`; await createUser(userId, username, username, did); // Set recovery email if provided during registration @@ -599,11 +602,18 @@ app.post('/api/register/complete', async (c) => { transports: credential.transports, rpId, }; - await storeCredential(storedCredential); + await storeCredential(storedCredential, did); + + // Store wallet address if provided (from PRF-derived EOA) + if (eoaAddress && typeof eoaAddress === 'string' && eoaAddress.startsWith('0x')) { + await updateUserProfile(userId, { walletAddress: eoaAddress }); + } console.log('EncryptID: Credential registered', { credentialId: credential.credentialId.slice(0, 20) + '...', userId: userId.slice(0, 20) + '...', + did: did.slice(0, 30) + '...', + hasWallet: !!eoaAddress, }); // Auto-provision user space at .rspace.online @@ -736,12 +746,16 @@ app.post('/api/auth/complete', async (c) => { storedCredential.username ); + // Read stored DID from database + const authUser = await getUserById(storedCredential.userId); + const authDid = authUser?.did || `did:key:${storedCredential.userId.slice(0, 32)}`; + return c.json({ success: true, userId: storedCredential.userId, username: storedCredential.username, token, - did: `did:key:${storedCredential.userId.slice(0, 32)}`, + did: authDid, }); }); @@ -1580,6 +1594,10 @@ async function generateSessionToken(userId: string, username: string): Promise${truncateDID(s.ownerDID)}
- Open + Open
diff --git a/website/create-space.html b/website/create-space.html index fb2e741..19c299e 100644 --- a/website/create-space.html +++ b/website/create-space.html @@ -371,7 +371,7 @@ submitBtn.textContent = "Creating..."; try { - const response = await fetch("/api/communities", { + const response = await fetch("/api/spaces", { method: "POST", headers: { "Content-Type": "application/json", @@ -383,10 +383,10 @@ const data = await response.json(); if (!response.ok) { - throw new Error(data.error || "Failed to create community"); + throw new Error(data.error || "Failed to create space"); } - window.location.href = `/${slug}/canvas`; + window.location.href = `/${slug}/rspace`; } catch (err) { errorMessage.textContent = err.message; errorMessage.style.display = "block"; diff --git a/website/index.html b/website/index.html index 7d2ab84..e678eab 100644 --- a/website/index.html +++ b/website/index.html @@ -555,7 +555,7 @@ const demo = document.getElementById('cta-demo'); if (primary) { primary.textContent = 'Go to My Space'; - primary.href = '/' + username + '/canvas'; + primary.href = '/' + username + '/rspace'; } if (demo) { demo.textContent = 'View Demo Space';