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) => {
|
enterBtn.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (this.#sourceSlug) {
|
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";
|
statusBar.style.display = "none";
|
||||||
const enterBtn = content.querySelector(".enter-btn");
|
const enterBtn = content.querySelector(".enter-btn");
|
||||||
enterBtn?.addEventListener("click", () => {
|
enterBtn?.addEventListener("click", () => {
|
||||||
if (this.#sourceSlug) window.open(`/${this.#sourceSlug}/canvas`, "_blank");
|
if (this.#sourceSlug) window.open(`/${this.#sourceSlug}/rspace`, "_blank");
|
||||||
});
|
});
|
||||||
// Disconnect when collapsed
|
// Disconnect when collapsed
|
||||||
this.#disconnect();
|
this.#disconnect();
|
||||||
|
|
|
||||||
|
|
@ -681,7 +681,7 @@ export class FolkRApp extends FolkShape {
|
||||||
const attrMode = this.getAttribute("mode");
|
const attrMode = this.getAttribute("mode");
|
||||||
if (attrMode === "iframe" || attrMode === "widget") this.#mode = attrMode;
|
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) {
|
if (!this.#spaceSlug) {
|
||||||
const pathParts = window.location.pathname.split("/").filter(Boolean);
|
const pathParts = window.location.pathname.split("/").filter(Boolean);
|
||||||
if (pathParts.length >= 1) this.#spaceSlug = pathParts[0];
|
if (pathParts.length >= 1) this.#spaceSlug = pathParts[0];
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* Canvas module — the collaborative infinite canvas.
|
* Canvas module — the collaborative infinite canvas.
|
||||||
*
|
*
|
||||||
* This is the original rSpace canvas restructured as an rSpace module.
|
* 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).
|
* / in standalone mode).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,8 @@ const DIST_DIR = resolve(import.meta.dir, "../dist");
|
||||||
|
|
||||||
|
|
||||||
// ── Hono app ──
|
// ── Hono app ──
|
||||||
const app = new Hono();
|
type AppEnv = { Variables: { isSubdomain: boolean } };
|
||||||
|
const app = new Hono<AppEnv>();
|
||||||
|
|
||||||
// Detect subdomain routing and set context flag
|
// Detect subdomain routing and set context flag
|
||||||
app.use("*", async (c, next) => {
|
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":"update-shape","shapeId":"shape-123","fields":{"content":"Updated text"}}]
|
||||||
[MI_ACTION:{"type":"delete-shape","shapeId":"shape-123"}]
|
[MI_ACTION:{"type":"delete-shape","shapeId":"shape-123"}]
|
||||||
[MI_ACTION:{"type":"move-shape","shapeId":"shape-123","x":400,"y":200}]
|
[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.
|
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,
|
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);
|
seenSlugs.add(slug);
|
||||||
|
|
||||||
let vis = data.meta.visibility || "public";
|
let vis = data.meta.visibility || "public";
|
||||||
const isOwner = !!(claims && data.meta.ownerDID === claims.sub);
|
// Check both claims.sub (raw userId) and did:key: format for
|
||||||
const memberEntry = claims ? data.members?.[claims.sub] : undefined;
|
// 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;
|
const isMember = !!memberEntry;
|
||||||
|
|
||||||
// Owner's personal space (slug matches username) is always private
|
// Owner's personal space (slug matches username) is always private
|
||||||
|
|
@ -354,7 +357,7 @@ spaces.post("/", async (c) => {
|
||||||
name: result.name,
|
name: result.name,
|
||||||
visibility: result.visibility,
|
visibility: result.visibility,
|
||||||
ownerDID: result.ownerDID,
|
ownerDID: result.ownerDID,
|
||||||
url: `/${result.slug}/canvas`,
|
url: `/${result.slug}/rspace`,
|
||||||
}, 201);
|
}, 201);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -444,7 +444,7 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok && data.slug) {
|
if (res.ok && data.slug) {
|
||||||
window.location.href = rspaceNavUrl(data.slug, "canvas");
|
window.location.href = rspaceNavUrl(data.slug, "rspace");
|
||||||
} else {
|
} else {
|
||||||
status.textContent = data.error || "Failed to create space";
|
status.textContent = data.error || "Failed to create space";
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
|
|
|
||||||
|
|
@ -82,10 +82,10 @@ export async function getUserByUsername(username: string) {
|
||||||
// CREDENTIAL OPERATIONS
|
// 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)
|
// Ensure user exists first (with display name + DID so they're never NULL)
|
||||||
const did = `did:key:${cred.userId.slice(0, 32)}`;
|
// 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);
|
await createUser(cred.userId, cred.username, cred.username, did || undefined);
|
||||||
|
|
||||||
await sql`
|
await sql`
|
||||||
INSERT INTO credentials (credential_id, user_id, public_key, counter, transports, created_at, rp_id)
|
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;
|
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
|
// RECOVERY TOKEN OPERATIONS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -562,7 +562,7 @@ app.post('/api/register/start', async (c) => {
|
||||||
* Complete registration - verify and store credential
|
* Complete registration - verify and store credential
|
||||||
*/
|
*/
|
||||||
app.post('/api/register/complete', async (c) => {
|
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
|
// Verify challenge
|
||||||
const challengeRecord = await getChallenge(challenge);
|
const challengeRecord = await getChallenge(challenge);
|
||||||
|
|
@ -578,8 +578,11 @@ app.post('/api/register/complete', async (c) => {
|
||||||
// In production, verify the attestation properly
|
// In production, verify the attestation properly
|
||||||
// For now, we trust the client-side verification
|
// For now, we trust the client-side verification
|
||||||
|
|
||||||
// Create user and store credential in database
|
// Use client-derived DID if available (proper did:key:z6Mk... from PRF),
|
||||||
const did = `did:key:${userId.slice(0, 32)}`;
|
// 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);
|
await createUser(userId, username, username, did);
|
||||||
|
|
||||||
// Set recovery email if provided during registration
|
// Set recovery email if provided during registration
|
||||||
|
|
@ -599,11 +602,18 @@ app.post('/api/register/complete', async (c) => {
|
||||||
transports: credential.transports,
|
transports: credential.transports,
|
||||||
rpId,
|
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', {
|
console.log('EncryptID: Credential registered', {
|
||||||
credentialId: credential.credentialId.slice(0, 20) + '...',
|
credentialId: credential.credentialId.slice(0, 20) + '...',
|
||||||
userId: userId.slice(0, 20) + '...',
|
userId: userId.slice(0, 20) + '...',
|
||||||
|
did: did.slice(0, 30) + '...',
|
||||||
|
hasWallet: !!eoaAddress,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-provision user space at <username>.rspace.online
|
// Auto-provision user space at <username>.rspace.online
|
||||||
|
|
@ -736,12 +746,16 @@ app.post('/api/auth/complete', async (c) => {
|
||||||
storedCredential.username
|
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({
|
return c.json({
|
||||||
success: true,
|
success: true,
|
||||||
userId: storedCredential.userId,
|
userId: storedCredential.userId,
|
||||||
username: storedCredential.username,
|
username: storedCredential.username,
|
||||||
token,
|
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 hasWallet = !!profile?.walletAddress;
|
||||||
const upInfo = await getUserUPAddress(userId);
|
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 = {
|
const payload = {
|
||||||
iss: 'https://auth.rspace.online',
|
iss: 'https://auth.rspace.online',
|
||||||
sub: userId,
|
sub: userId,
|
||||||
|
|
@ -1588,7 +1606,7 @@ async function generateSessionToken(userId: string, username: string): Promise<s
|
||||||
exp: now + CONFIG.sessionDuration,
|
exp: now + CONFIG.sessionDuration,
|
||||||
jti: Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString('base64url'),
|
jti: Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString('base64url'),
|
||||||
username,
|
username,
|
||||||
did: `did:key:${userId.slice(0, 32)}`,
|
did,
|
||||||
eid: {
|
eid: {
|
||||||
authLevel: 3, // ELEVATED (fresh WebAuthn)
|
authLevel: 3, // ELEVATED (fresh WebAuthn)
|
||||||
authTime: now,
|
authTime: now,
|
||||||
|
|
|
||||||
|
|
@ -687,7 +687,7 @@
|
||||||
<td class="owner-cell" title="${escapeHtml(s.ownerDID)}">${truncateDID(s.ownerDID)}</td>
|
<td class="owner-cell" title="${escapeHtml(s.ownerDID)}">${truncateDID(s.ownerDID)}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="actions-cell">
|
<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>
|
<button class="action-link danger" onclick="window.__deleteSpace('${escapeHtml(s.slug)}', '${escapeHtml(s.name)}')">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
|
|
@ -371,7 +371,7 @@
|
||||||
submitBtn.textContent = "Creating...";
|
submitBtn.textContent = "Creating...";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/communities", {
|
const response = await fetch("/api/spaces", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -383,10 +383,10 @@
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
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) {
|
} catch (err) {
|
||||||
errorMessage.textContent = err.message;
|
errorMessage.textContent = err.message;
|
||||||
errorMessage.style.display = "block";
|
errorMessage.style.display = "block";
|
||||||
|
|
|
||||||
|
|
@ -555,7 +555,7 @@
|
||||||
const demo = document.getElementById('cta-demo');
|
const demo = document.getElementById('cta-demo');
|
||||||
if (primary) {
|
if (primary) {
|
||||||
primary.textContent = 'Go to My Space';
|
primary.textContent = 'Go to My Space';
|
||||||
primary.href = '/' + username + '/canvas';
|
primary.href = '/' + username + '/rspace';
|
||||||
}
|
}
|
||||||
if (demo) {
|
if (demo) {
|
||||||
demo.textContent = 'View Demo Space';
|
demo.textContent = 'View Demo Space';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue