-
${this.esc(entry.title || "Untitled")}
-
`;
@@ -821,7 +826,18 @@ class FolkCartShop extends HTMLElement {
.status-cancelled, .status-closed { background: rgba(239,68,68,0.15); color: #f87171; }
.status-shipped { background: rgba(56,189,248,0.15); color: #38bdf8; }
- .price { color: var(--rs-text-primary); font-weight: 600; font-size: 1rem; margin-top: 0.5rem; }
+ .price { color: var(--rs-text-primary); font-weight: 600; font-size: 1rem; }
+
+ /* Catalog product cards */
+ .catalog-grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
+ .catalog-card { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
+ .catalog-img { width: 100%; aspect-ratio: 1; overflow: hidden; background: var(--rs-bg-surface-raised); }
+ .catalog-img img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s; }
+ .catalog-card:hover .catalog-img img { transform: scale(1.04); }
+ .catalog-body { padding: 1rem; display: flex; flex-direction: column; flex: 1; }
+ .catalog-body .card-title { margin-bottom: 0.25rem; }
+ .card-desc { color: var(--rs-text-secondary); font-size: 0.8125rem; margin-bottom: 0.75rem; line-height: 1.4; flex: 1; }
+ .catalog-footer { display: flex; align-items: center; justify-content: space-between; margin-top: auto; }
.text-green { color: #4ade80; }
.progress-bar { background: var(--rs-bg-surface-raised); border-radius: 999px; height: 8px; overflow: hidden; }
@@ -878,8 +894,9 @@ class FolkCartShop extends HTMLElement {
.empty { text-align: center; padding: 3rem; color: var(--rs-text-muted); font-size: 0.875rem; }
.loading { text-align: center; padding: 3rem; color: var(--rs-text-secondary); }
- @media (max-width: 480px) {
+ @media (max-width: 600px) {
.grid { grid-template-columns: 1fr; }
+ .catalog-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
.url-input-row { flex-direction: column; }
}
`;
diff --git a/modules/rnetwork/components/folk-graph-viewer.ts b/modules/rnetwork/components/folk-graph-viewer.ts
index ffa5f7b..5a53626 100644
--- a/modules/rnetwork/components/folk-graph-viewer.ts
+++ b/modules/rnetwork/components/folk-graph-viewer.ts
@@ -62,72 +62,10 @@ class FolkGraphViewer extends HTMLElement {
connectedCallback() {
this.space = this.getAttribute("space") || "demo";
- if (this.space === "demo") { this.loadDemoData(); return; }
this.render(); // Show loading state
this.loadData(); // Async — will re-render when data arrives
}
- private loadDemoData() {
- this.info = { name: "rSpace Community", member_count: 10, company_count: 3, opportunity_count: 0 };
-
- this.workspaces = [
- { name: "Commons DAO", slug: "commons-dao", nodeCount: 5, edgeCount: 4 },
- { name: "Mycelial Lab", slug: "mycelial-lab", nodeCount: 5, edgeCount: 4 },
- { name: "Regenerative Fund", slug: "regenerative-fund", nodeCount: 5, edgeCount: 4 },
- ];
-
- // Organizations
- this.nodes = [
- { id: "org-1", name: "Commons DAO", type: "company", workspace: "Commons DAO", description: "Decentralized governance cooperative" },
- { id: "org-2", name: "Mycelial Lab", type: "company", workspace: "Mycelial Lab", description: "Protocol research collective" },
- { id: "org-3", name: "Regenerative Fund", type: "company", workspace: "Regenerative Fund", description: "Impact funding vehicle" },
-
- // People — Commons DAO
- { id: "p-1", name: "Alice Chen", type: "person", workspace: "Commons DAO", role: "Lead Engineer", location: "Vancouver" },
- { id: "p-2", name: "Bob Nakamura", type: "person", workspace: "Commons DAO", role: "Community Lead", location: "Tokyo" },
- { id: "p-3", name: "Carol Santos", type: "person", workspace: "Commons DAO", role: "Treasury Steward", location: "São Paulo" },
- { id: "p-4", name: "Dave Okafor", type: "person", workspace: "Commons DAO", role: "Governance Facilitator", location: "Lagos" },
-
- // People — Mycelial Lab
- { id: "p-5", name: "Eva Larsson", type: "person", workspace: "Mycelial Lab", role: "Ops Coordinator", location: "Stockholm" },
- { id: "p-6", name: "Frank Müller", type: "person", workspace: "Mycelial Lab", role: "Protocol Designer", location: "Berlin" },
- { id: "p-7", name: "Grace Kim", type: "person", workspace: "Mycelial Lab", role: "Strategy Lead", location: "Seoul" },
-
- // People — Regenerative Fund
- { id: "p-8", name: "Hiro Tanaka", type: "person", workspace: "Regenerative Fund", role: "Research Lead", location: "Osaka" },
- { id: "p-9", name: "Iris Patel", type: "person", workspace: "Regenerative Fund", role: "Developer Relations", location: "Mumbai" },
- { id: "p-10", name: "James Wright", type: "person", workspace: "Regenerative Fund", role: "Security Auditor", location: "London" },
- ];
-
- // Edges: work_at links + cross-org point_of_contact
- this.edges = [
- // Work_at — Commons DAO
- { source: "p-1", target: "org-1", type: "work_at" },
- { source: "p-2", target: "org-1", type: "work_at" },
- { source: "p-3", target: "org-1", type: "work_at" },
- { source: "p-4", target: "org-1", type: "work_at" },
-
- // Work_at — Mycelial Lab
- { source: "p-5", target: "org-2", type: "work_at" },
- { source: "p-6", target: "org-2", type: "work_at" },
- { source: "p-7", target: "org-2", type: "work_at" },
-
- // Work_at — Regenerative Fund
- { source: "p-8", target: "org-3", type: "work_at" },
- { source: "p-9", target: "org-3", type: "work_at" },
- { source: "p-10", target: "org-3", type: "work_at" },
-
- // Cross-org point_of_contact edges
- { source: "p-1", target: "p-6", type: "point_of_contact", label: "Alice ↔ Frank" },
- { source: "p-2", target: "p-3", type: "point_of_contact", label: "Bob ↔ Carol" },
- { source: "p-4", target: "p-7", type: "point_of_contact", label: "Dave ↔ Grace" },
- ];
-
- this.layoutDirty = true;
- this.render();
- requestAnimationFrame(() => this.fitView());
- }
-
private getApiBase(): string {
const path = window.location.pathname;
const match = path.match(/^(\/[^/]+)?\/rnetwork/);
diff --git a/modules/rnetwork/mod.ts b/modules/rnetwork/mod.ts
index 888f409..dc73dbb 100644
--- a/modules/rnetwork/mod.ts
+++ b/modules/rnetwork/mod.ts
@@ -7,7 +7,7 @@
*/
import { Hono } from "hono";
-import { renderShell, renderExternalAppShell } from "../../server/shell";
+import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
@@ -275,35 +275,27 @@ routes.get("/api/opportunities", async (c) => {
return c.json({ opportunities });
});
-// ── CRM sub-route — embed Twenty CRM via iframe ──
+// ── CRM sub-route — API-driven CRM view ──
routes.get("/crm", (c) => {
const space = c.req.param("space") || "demo";
- const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
- return c.html(renderExternalAppShell({
+ return c.html(renderShell({
title: `${space} — CRM | rSpace`,
moduleId: "rnetwork",
spaceSlug: space,
modules: getModuleInfoList(),
- appUrl: "https://crm.rspace.online",
- appName: "Twenty CRM",
+ body: `
`,
+ scripts: ``,
+ styles: `
`,
}));
});
// ── Page route ──
routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
- const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
const view = c.req.query("view");
if (view === "app") {
- return c.html(renderExternalAppShell({
- title: `${space} — CRM | rSpace`,
- moduleId: "rnetwork",
- spaceSlug: space,
- modules: getModuleInfoList(),
- appUrl: "https://crm.rspace.online",
- appName: "Twenty CRM",
- }));
+ return c.redirect(`/${space}/rnetwork/crm`, 301);
}
return c.html(renderShell({
@@ -311,7 +303,7 @@ routes.get("/", (c) => {
moduleId: "rnetwork",
spaceSlug: space,
modules: getModuleInfoList(),
- body: `
+ body: `
`,
scripts: ``,
styles: `
`,
diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts
index 7a3775b..eaee22d 100644
--- a/src/encryptid/server.ts
+++ b/src/encryptid/server.ts
@@ -1528,16 +1528,19 @@ async function generateSessionToken(userId: string, username: string): Promise
{
const now = Math.floor(Date.now() / 1000);
- // Build claims
- const claims: EncryptIDClaims = {
- iss: 'https://auth.ridentity.online',
- sub: did,
- aud: [
- 'rspace.online',
- 'rwallet.online',
- 'rvote.online',
- 'rfiles.online',
- 'rmaps.online',
- ],
- iat: now,
- exp: now + 15 * 60, // 15 minutes
- jti: bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer),
+ // Get a server-signed JWT by exchanging the credential via EncryptID
+ let accessToken: string;
+ try {
+ // Step 1: Get a server challenge
+ const startRes = await fetch(`${ENCRYPTID_SERVER}/api/auth/start`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ credentialId: authResult.credentialId }),
+ });
+ if (!startRes.ok) throw new Error('Failed to start server auth');
+ const { options } = await startRes.json();
- eid: {
- credentialId: authResult.credentialId,
- authLevel: AuthLevel.ELEVATED, // Fresh WebAuthn
- authTime: now,
- capabilities,
- recoveryConfigured: false, // TODO: Check actual status
- },
- };
+ // Step 2: Complete auth with credentialId to get signed token
+ const completeRes = await fetch(`${ENCRYPTID_SERVER}/api/auth/complete`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ challenge: options.challenge,
+ credential: { credentialId: authResult.credentialId },
+ }),
+ });
+ if (!completeRes.ok) throw new Error('Server auth failed');
+ const data = await completeRes.json();
+ if (!data.token) throw new Error('No token in response');
+ accessToken = data.token;
+ } catch (err) {
+ console.warn('EncryptID: Server token exchange failed, using local token', err);
+ // Fallback to unsigned token if server is unreachable
+ accessToken = this.createUnsignedToken({
+ iss: 'https://auth.ridentity.online',
+ sub: did,
+ aud: ['rspace.online'],
+ iat: now,
+ exp: now + 15 * 60,
+ jti: bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer),
+ eid: {
+ credentialId: authResult.credentialId,
+ authLevel: AuthLevel.ELEVATED,
+ authTime: now,
+ capabilities,
+ recoveryConfigured: false,
+ },
+ });
+ }
- // In production, tokens would be signed by server
- // For now, we create unsigned tokens for the prototype
- const accessToken = this.createUnsignedToken(claims);
- const refreshToken = this.createRefreshToken(did);
+ // Decode claims from the token (works for both signed and unsigned JWTs)
+ let claims: EncryptIDClaims;
+ try {
+ const payloadB64 = accessToken.split('.')[1];
+ claims = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')));
+ } catch {
+ // Fallback claims
+ claims = {
+ iss: 'https://auth.ridentity.online',
+ sub: did,
+ aud: ['rspace.online'],
+ iat: now,
+ exp: now + 15 * 60,
+ jti: bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer),
+ eid: {
+ credentialId: authResult.credentialId,
+ authLevel: AuthLevel.ELEVATED,
+ authTime: now,
+ capabilities,
+ recoveryConfigured: false,
+ },
+ };
+ }
this.session = {
accessToken,
- refreshToken,
+ refreshToken: accessToken, // Server token serves as both
claims,
lastAuthTime: Date.now(),
};
@@ -206,8 +249,9 @@ export class SessionManager {
this.scheduleRefresh();
console.log('EncryptID: Session created', {
- did: did.slice(0, 30) + '...',
- authLevel: AuthLevel[claims.eid.authLevel],
+ did: (claims.sub || did).slice(0, 30) + '...',
+ authLevel: AuthLevel[claims.eid?.authLevel ?? AuthLevel.ELEVATED],
+ signed: !accessToken.endsWith('.'),
});
return this.session;
@@ -436,30 +480,38 @@ export class SessionManager {
}
private async refreshTokens(): Promise {
- // In production, this would call the server to refresh tokens
- // For the prototype, we just extend the expiration
if (!this.session) return;
- const now = Math.floor(Date.now() / 1000);
+ try {
+ // Call EncryptID server to refresh the token
+ const res = await fetch(`${ENCRYPTID_SERVER}/api/session/refresh`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this.session.accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ });
- // Downgrade auth level on refresh (user hasn't re-authenticated)
- this.session.claims.eid.authLevel = Math.min(
- this.session.claims.eid.authLevel,
- AuthLevel.STANDARD
- );
+ if (!res.ok) throw new Error(`Refresh failed: ${res.status}`);
+ const data = await res.json();
+ if (!data.token) throw new Error('No token in refresh response');
- this.session.claims.iat = now;
- this.session.claims.exp = now + 15 * 60;
- this.session.claims.jti = bufferToBase64url(
- crypto.getRandomValues(new Uint8Array(16)).buffer
- );
+ // Decode new claims
+ const payloadB64 = data.token.split('.')[1];
+ const claims = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')));
- this.session.accessToken = this.createUnsignedToken(this.session.claims);
+ this.session.accessToken = data.token;
+ this.session.refreshToken = data.token;
+ this.session.claims = claims;
- this.persistSession();
- this.scheduleRefresh();
+ this.persistSession();
+ this.scheduleRefresh();
- console.log('EncryptID: Tokens refreshed');
+ console.log('EncryptID: Tokens refreshed (server-signed)');
+ } catch (err) {
+ console.warn('EncryptID: Token refresh failed', err);
+ // Session will expire naturally; user will need to re-authenticate
+ }
}
}