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 <noreply@anthropic.com>
This commit is contained in:
parent
8bd6e61ffc
commit
73ad020812
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -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<AppEnv>();
|
||||
|
||||
// Detect subdomain routing and set context flag
|
||||
app.use("*", async (c, next) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -82,10 +82,10 @@ export async function getUserByUsername(username: string) {
|
|||
// CREDENTIAL OPERATIONS
|
||||
// ============================================================================
|
||||
|
||||
export async function storeCredential(cred: StoredCredential): Promise<void> {
|
||||
export async function storeCredential(cred: StoredCredential, did?: string): Promise<void> {
|
||||
// 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<void> {
|
||||
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<number> {
|
||||
const result = await sql`UPDATE space_members SET user_did = ${newDid} WHERE user_did = ${oldDid}`;
|
||||
return result.count;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RECOVERY TOKEN OPERATIONS
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -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 <username>.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<s
|
|||
const hasWallet = !!profile?.walletAddress;
|
||||
const upInfo = await getUserUPAddress(userId);
|
||||
|
||||
// Read stored DID from database instead of deriving from userId
|
||||
const user = await getUserById(userId);
|
||||
const did = user?.did || `did:key:${userId.slice(0, 32)}`;
|
||||
|
||||
const payload = {
|
||||
iss: 'https://auth.rspace.online',
|
||||
sub: userId,
|
||||
|
|
@ -1588,7 +1606,7 @@ async function generateSessionToken(userId: string, username: string): Promise<s
|
|||
exp: now + CONFIG.sessionDuration,
|
||||
jti: Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString('base64url'),
|
||||
username,
|
||||
did: `did:key:${userId.slice(0, 32)}`,
|
||||
did,
|
||||
eid: {
|
||||
authLevel: 3, // ELEVATED (fresh WebAuthn)
|
||||
authTime: now,
|
||||
|
|
|
|||
|
|
@ -687,7 +687,7 @@
|
|||
<td class="owner-cell" title="${escapeHtml(s.ownerDID)}">${truncateDID(s.ownerDID)}</td>
|
||||
<td>
|
||||
<div class="actions-cell">
|
||||
<a href="/${encodeURIComponent(s.slug)}/canvas" class="action-link">Open</a>
|
||||
<a href="/${encodeURIComponent(s.slug)}/rspace" class="action-link">Open</a>
|
||||
<button class="action-link danger" onclick="window.__deleteSpace('${escapeHtml(s.slug)}', '${escapeHtml(s.name)}')">Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in New Issue