import { mkdir, readdir } from "node:fs/promises"; import * as Automerge from "@automerge/automerge"; const STORAGE_DIR = process.env.STORAGE_DIR || "./data/communities"; export type SpaceVisibility = 'public' | 'public_read' | 'authenticated' | 'members_only'; // ── Nest Permissions & Policy ── export interface NestPermissions { read: boolean; write: boolean; addShapes: boolean; deleteShapes: boolean; reshare: boolean; expiry?: number; // unix timestamp — auto-revoke after this time } export interface NestNotifications { onNestRequest: boolean; onNestCreated: boolean; onNestRevoked: boolean; onReshare: boolean; channel: 'inbox' | 'email' | 'both'; } export interface NestPolicy { consent: 'open' | 'members' | 'approval' | 'closed'; notifications: NestNotifications; defaultPermissions: NestPermissions; allowlist?: string[]; // slugs that bypass consent blocklist?: string[]; // slugs that are always denied } // ── SpaceRef: Nesting Primitive ── export interface SpaceRefFilter { shapeTypes?: string[]; shapeIds?: string[]; tags?: string[]; moduleIds?: string[]; } export interface SpaceRef { id: string; sourceSlug: string; sourceDID?: string; filter?: SpaceRefFilter; x: number; y: number; width: number; height: number; rotation: number; permissions: NestPermissions; collapsed?: boolean; label?: string; createdAt: number; createdBy: string; // DID of who created this nesting } export type NestRequestStatus = 'pending' | 'approved' | 'denied'; export interface PendingNestRequest { id: string; sourceSlug: string; targetSlug: string; requestedBy: string; // DID requestedPermissions: NestPermissions; message?: string; status: NestRequestStatus; createdAt: number; resolvedAt?: number; resolvedBy?: string; modifiedPermissions?: NestPermissions; } // ── Default Nest Policies ── export const DEFAULT_USER_NEST_POLICY: NestPolicy = { consent: 'approval', notifications: { onNestRequest: true, onNestCreated: true, onNestRevoked: false, onReshare: true, channel: 'inbox', }, defaultPermissions: { read: true, write: false, addShapes: false, deleteShapes: false, reshare: false, }, }; export const DEFAULT_COMMUNITY_NEST_POLICY: NestPolicy = { consent: 'members', notifications: { onNestRequest: false, onNestCreated: true, onNestRevoked: true, onReshare: false, channel: 'inbox', }, defaultPermissions: { read: true, write: true, addShapes: true, deleteShapes: false, reshare: true, }, }; export const DEFAULT_USER_MODULES = ['canvas', 'notes', 'files', 'wallet']; // ── Core Types ── export interface CommunityMeta { name: string; slug: string; createdAt: string; visibility: SpaceVisibility; ownerDID: string | null; enabledModules?: string[]; description?: string; avatar?: string; nestPolicy?: NestPolicy; encrypted?: boolean; encryptionKeyId?: string; } export interface ShapeData { type: string; id: string; x: number; y: number; width: number; height: number; rotation: number; content?: string; sourceId?: string; targetId?: string; [key: string]: unknown; // Allow additional shape-specific properties } export interface SpaceMember { did: string; role: 'viewer' | 'participant' | 'moderator' | 'admin'; joinedAt: number; displayName?: string; } export interface CommunityDoc { meta: CommunityMeta; shapes: { [id: string]: ShapeData; }; members: { [did: string]: SpaceMember; }; nestedSpaces: { [refId: string]: SpaceRef; }; } // Per-peer sync state for Automerge interface PeerState { syncState: Automerge.SyncState; lastActivity: number; } // In-memory cache of Automerge documents const communities = new Map>(); // Track sync state per peer (WebSocket connection) const peerSyncStates = new Map>(); // Debounce save timers const saveTimers = new Map(); // Ensure storage directory exists await mkdir(STORAGE_DIR, { recursive: true }); /** * Load community document from disk */ export async function loadCommunity(slug: string): Promise | null> { // Check cache first if (communities.has(slug)) { return communities.get(slug)!; } // Try to load Automerge binary first const binaryPath = `${STORAGE_DIR}/${slug}.automerge`; const binaryFile = Bun.file(binaryPath); if (await binaryFile.exists()) { try { const buffer = await binaryFile.arrayBuffer(); let bytes = new Uint8Array(buffer); // Check for encrypted magic bytes if (bytes.length >= ENCRYPTED_MAGIC.length && bytes[0] === ENCRYPTED_MAGIC[0] && bytes[1] === ENCRYPTED_MAGIC[1] && bytes[2] === ENCRYPTED_MAGIC[2] && bytes[3] === ENCRYPTED_MAGIC[3]) { // Encrypted file: extract keyId length (4 bytes), keyId, then ciphertext const keyIdLen = new DataView(bytes.buffer, bytes.byteOffset + 4, 4).getUint32(0); const keyId = new TextDecoder().decode(bytes.slice(8, 8 + keyIdLen)); const ciphertext = bytes.slice(8 + keyIdLen); const key = await deriveSpaceKey(keyId); bytes = new Uint8Array(await decryptBinary(ciphertext, key)); console.log(`[Store] Decrypted ${slug} (keyId: ${keyId})`); } const doc = Automerge.load(bytes); communities.set(slug, doc); return doc; } catch (e) { console.error(`Failed to load Automerge doc for ${slug}:`, e); } } // Fallback: try JSON format and migrate const jsonPath = `${STORAGE_DIR}/${slug}.json`; const jsonFile = Bun.file(jsonPath); if (await jsonFile.exists()) { try { const data = (await jsonFile.json()) as CommunityDoc; // Migrate JSON to Automerge const doc = jsonToAutomerge(data); communities.set(slug, doc); // Save as Automerge binary await saveCommunity(slug); return doc; } catch (e) { console.error(`Failed to migrate JSON for ${slug}:`, e); return null; } } return null; } /** * Convert JSON document to Automerge document */ function jsonToAutomerge(data: CommunityDoc): Automerge.Doc { let doc = Automerge.init(); doc = Automerge.change(doc, "Import from JSON", (d) => { d.meta = { ...data.meta }; d.shapes = {}; for (const [id, shape] of Object.entries(data.shapes || {})) { d.shapes[id] = { ...shape }; } d.members = {}; for (const [did, member] of Object.entries(data.members || {})) { d.members[did] = { ...member }; } d.nestedSpaces = {}; for (const [refId, ref] of Object.entries(data.nestedSpaces || {})) { d.nestedSpaces[refId] = { ...ref }; } }); return doc; } /** * Save community document to disk (debounced) */ export async function saveCommunity(slug: string): Promise { const doc = communities.get(slug); if (!doc) return; // Clear existing timer const existingTimer = saveTimers.get(slug); if (existingTimer) { clearTimeout(existingTimer); } // Debounce saves to avoid excessive disk writes const timer = setTimeout(async () => { const currentDoc = communities.get(slug); if (!currentDoc) return; const binary = Automerge.save(currentDoc); const path = `${STORAGE_DIR}/${slug}.automerge`; // Encrypt at rest if the space has encryption enabled if (currentDoc.meta.encrypted && currentDoc.meta.encryptionKeyId) { try { const keyId = currentDoc.meta.encryptionKeyId; const key = await deriveSpaceKey(keyId); const ciphertext = await encryptBinary(binary, key); const keyIdBytes = new TextEncoder().encode(keyId); // Format: magic (4) + keyIdLen (4) + keyId + ciphertext const header = new Uint8Array(8 + keyIdBytes.length + ciphertext.length); header.set(ENCRYPTED_MAGIC, 0); new DataView(header.buffer).setUint32(4, keyIdBytes.length); header.set(keyIdBytes, 8); header.set(ciphertext, 8 + keyIdBytes.length); await Bun.write(path, header); console.log(`[Store] Saved ${slug} encrypted (${header.length} bytes, keyId: ${keyId})`); } catch (e) { // Fallback to unencrypted if encryption fails console.error(`[Store] Encryption failed for ${slug}, saving unencrypted:`, e); await Bun.write(path, binary); } } else { await Bun.write(path, binary); console.log(`[Store] Saved ${slug} (${binary.length} bytes)`); } }, 2000); saveTimers.set(slug, timer); } /** * Create a new community/space */ export async function createCommunity( name: string, slug: string, ownerDID: string | null = null, visibility: SpaceVisibility = 'public_read', options?: { enabledModules?: string[]; nestPolicy?: NestPolicy; description?: string; }, ): Promise> { let doc = Automerge.init(); doc = Automerge.change(doc, "Create community", (d) => { d.meta = { name, slug, createdAt: new Date().toISOString(), visibility, ownerDID, }; if (options?.enabledModules) { d.meta.enabledModules = options.enabledModules; } if (options?.nestPolicy) { d.meta.nestPolicy = options.nestPolicy; } if (options?.description) { d.meta.description = options.description; } d.shapes = {}; d.members = {}; d.nestedSpaces = {}; // If owner is known, add them as admin member if (ownerDID) { d.members[ownerDID] = { did: ownerDID, role: 'admin', joinedAt: Date.now(), }; } }); communities.set(slug, doc); await saveCommunity(slug); return doc; } /** * Check if community exists */ export async function communityExists(slug: string): Promise { if (communities.has(slug)) return true; const binaryPath = `${STORAGE_DIR}/${slug}.automerge`; const jsonPath = `${STORAGE_DIR}/${slug}.json`; const binaryFile = Bun.file(binaryPath); const jsonFile = Bun.file(jsonPath); return (await binaryFile.exists()) || (await jsonFile.exists()); } /** * List all communities */ export async function listCommunities(): Promise { try { const files = await readdir(STORAGE_DIR); const slugs = new Set(); for (const f of files) { if (f.endsWith(".automerge")) { slugs.add(f.replace(".automerge", "")); } else if (f.endsWith(".json")) { slugs.add(f.replace(".json", "")); } } return Array.from(slugs); } catch { return []; } } /** * Get or create sync state for a peer */ export function getPeerSyncState(slug: string, peerId: string): PeerState { if (!peerSyncStates.has(slug)) { peerSyncStates.set(slug, new Map()); } const communityPeers = peerSyncStates.get(slug)!; if (!communityPeers.has(peerId)) { communityPeers.set(peerId, { syncState: Automerge.initSyncState(), lastActivity: Date.now(), }); } const peerState = communityPeers.get(peerId)!; peerState.lastActivity = Date.now(); return peerState; } /** * Remove peer sync state (on disconnect) */ export function removePeerSyncState(slug: string, peerId: string): void { const communityPeers = peerSyncStates.get(slug); if (communityPeers) { communityPeers.delete(peerId); if (communityPeers.size === 0) { peerSyncStates.delete(slug); } } } /** * Get all peer IDs for a community */ export function getCommunityPeers(slug: string): string[] { const communityPeers = peerSyncStates.get(slug); return communityPeers ? Array.from(communityPeers.keys()) : []; } /** * Process incoming sync message from a peer * Returns response message and messages for other peers */ export function receiveSyncMessage( slug: string, peerId: string, message: Uint8Array, ): { response: Uint8Array | null; broadcastToPeers: Map; } { const doc = communities.get(slug); if (!doc) { return { response: null, broadcastToPeers: new Map() }; } const peerState = getPeerSyncState(slug, peerId); // Apply incoming sync message const result = Automerge.receiveSyncMessage( doc, peerState.syncState, message ); const newDoc = result[0]; const newSyncState = result[1]; communities.set(slug, newDoc); peerState.syncState = newSyncState; // Save if the document actually changed (Automerge 2.x receiveSyncMessage // returns null for patches, so detect changes via object identity instead) if (newDoc !== doc) { saveCommunity(slug); } // Generate response for this peer const [nextSyncState, responseMessage] = Automerge.generateSyncMessage( newDoc, peerState.syncState ); peerState.syncState = nextSyncState; // Generate messages for other peers const broadcastToPeers = new Map(); const communityPeers = peerSyncStates.get(slug); if (communityPeers && newDoc !== doc) { for (const [otherPeerId, otherPeerState] of communityPeers) { if (otherPeerId !== peerId) { const [newOtherSyncState, otherMessage] = Automerge.generateSyncMessage( newDoc, otherPeerState.syncState ); otherPeerState.syncState = newOtherSyncState; if (otherMessage) { broadcastToPeers.set(otherPeerId, otherMessage); } } } } return { response: responseMessage || null, broadcastToPeers, }; } /** * Generate initial sync message for a new peer */ export function generateSyncMessageForPeer( slug: string, peerId: string, ): Uint8Array | null { const doc = communities.get(slug); if (!doc) return null; const peerState = getPeerSyncState(slug, peerId); const [newSyncState, message] = Automerge.generateSyncMessage( doc, peerState.syncState ); peerState.syncState = newSyncState; return message || null; } /** * Get document as plain object (for API responses) */ export function getDocumentData(slug: string): CommunityDoc | null { const doc = communities.get(slug); if (!doc) return null; // Convert Automerge doc to plain object return JSON.parse(JSON.stringify(doc)); } /** * Add multiple shapes in a single Automerge change (for external API calls) */ export function addShapes( slug: string, shapes: Record[], ): string[] { const doc = communities.get(slug); if (!doc) return []; const ids: string[] = []; const newDoc = Automerge.change(doc, `Add ${shapes.length} shapes`, (d) => { if (!d.shapes) d.shapes = {}; for (const shape of shapes) { const id = (shape.id as string) || `shape-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; ids.push(id); d.shapes[id] = { type: (shape.type as string) || 'geo', x: (shape.x as number) ?? 100, y: (shape.y as number) ?? 100, width: (shape.width as number) ?? 300, height: (shape.height as number) ?? 200, rotation: (shape.rotation as number) ?? 0, ...shape, id, } as ShapeData; } }); communities.set(slug, newDoc); saveCommunity(slug); return ids; } // Legacy functions for backward compatibility export function updateShape( slug: string, shapeId: string, data: ShapeData, ): void { const doc = communities.get(slug); if (doc) { const newDoc = Automerge.change(doc, `Update shape ${shapeId}`, (d) => { if (!d.shapes) d.shapes = {}; d.shapes[shapeId] = data; }); communities.set(slug, newDoc); saveCommunity(slug); } } export function deleteShape(slug: string, shapeId: string): void { const doc = communities.get(slug); if (doc) { const newDoc = Automerge.change(doc, `Delete shape ${shapeId}`, (d) => { if (d.shapes && d.shapes[shapeId]) { delete d.shapes[shapeId]; } }); communities.set(slug, newDoc); saveCommunity(slug); } } /** * Forget a shape — soft-delete. Shape stays in the doc but is hidden from view. * Can be restored with rememberShape(). */ export function forgetShape(slug: string, shapeId: string, forgottenBy?: string): void { const doc = communities.get(slug); if (!doc || !doc.shapes?.[shapeId]) return; const newDoc = Automerge.change(doc, `Forget shape ${shapeId}`, (d) => { if (d.shapes[shapeId]) { (d.shapes[shapeId] as Record).forgotten = true; (d.shapes[shapeId] as Record).forgottenAt = Date.now(); if (forgottenBy) { (d.shapes[shapeId] as Record).forgottenBy = forgottenBy; } } }); communities.set(slug, newDoc); saveCommunity(slug); } /** * Remember a forgotten shape — restore it to the canvas. */ export function rememberShape(slug: string, shapeId: string): void { const doc = communities.get(slug); if (!doc || !doc.shapes?.[shapeId]) return; const newDoc = Automerge.change(doc, `Remember shape ${shapeId}`, (d) => { if (d.shapes[shapeId]) { (d.shapes[shapeId] as Record).forgotten = false; (d.shapes[shapeId] as Record).forgottenAt = 0; (d.shapes[shapeId] as Record).forgottenBy = ''; } }); communities.set(slug, newDoc); saveCommunity(slug); } /** * Update specific fields of a shape (for bidirectional module sync callbacks). * Only updates the fields provided, preserving all other shape data. */ export function updateShapeFields( slug: string, shapeId: string, fields: Record, ): boolean { const doc = communities.get(slug); if (!doc || !doc.shapes?.[shapeId]) return false; const newDoc = Automerge.change(doc, `Update shape ${shapeId} fields`, (d) => { if (d.shapes[shapeId]) { for (const [key, value] of Object.entries(fields)) { (d.shapes[shapeId] as Record)[key] = value; } } }); communities.set(slug, newDoc); saveCommunity(slug); return true; } /** * Set a member in the community's Automerge doc */ export function setMember( slug: string, did: string, role: SpaceMember['role'], displayName?: string, ): void { const doc = communities.get(slug); if (!doc) return; const newDoc = Automerge.change(doc, `Set member ${did}`, (d) => { if (!d.members) d.members = {}; d.members[did] = { did, role, joinedAt: d.members[did]?.joinedAt || Date.now(), ...(displayName ? { displayName } : {}), }; }); communities.set(slug, newDoc); saveCommunity(slug); } /** * Remove a member from the community's Automerge doc */ export function removeMember(slug: string, did: string): void { const doc = communities.get(slug); if (!doc) return; const newDoc = Automerge.change(doc, `Remove member ${did}`, (d) => { if (d.members && d.members[did]) { delete d.members[did]; } }); communities.set(slug, newDoc); saveCommunity(slug); } /** * Clear all shapes from a community (for demo reset) */ export function clearShapes(slug: string): void { const doc = communities.get(slug); if (doc) { const newDoc = Automerge.change(doc, "Clear all shapes", (d) => { d.shapes = {}; }); communities.set(slug, newDoc); saveCommunity(slug); } } // ── Nested Spaces CRUD ── /** * Add a SpaceRef (nest a space into another) */ export function addNestedSpace(slug: string, ref: SpaceRef): void { const doc = communities.get(slug); if (!doc) return; const newDoc = Automerge.change(doc, `Nest space ${ref.sourceSlug}`, (d) => { if (!d.nestedSpaces) d.nestedSpaces = {}; d.nestedSpaces[ref.id] = { ...ref }; }); communities.set(slug, newDoc); saveCommunity(slug); } /** * Update a SpaceRef (permissions, filter, position) */ export function updateNestedSpace( slug: string, refId: string, fields: Partial, ): boolean { const doc = communities.get(slug); if (!doc || !doc.nestedSpaces?.[refId]) return false; const newDoc = Automerge.change(doc, `Update nest ${refId}`, (d) => { if (d.nestedSpaces[refId]) { for (const [key, value] of Object.entries(fields)) { if (key === 'id') continue; // never change ID (d.nestedSpaces[refId] as unknown as Record)[key] = value; } } }); communities.set(slug, newDoc); saveCommunity(slug); return true; } /** * Remove a SpaceRef (un-nest) */ export function removeNestedSpace(slug: string, refId: string): boolean { const doc = communities.get(slug); if (!doc || !doc.nestedSpaces?.[refId]) return false; const newDoc = Automerge.change(doc, `Remove nest ${refId}`, (d) => { if (d.nestedSpaces && d.nestedSpaces[refId]) { delete d.nestedSpaces[refId]; } }); communities.set(slug, newDoc); saveCommunity(slug); return true; } /** * Get the NestPolicy for a space, with fallback to default */ export function getNestPolicy(slug: string): NestPolicy | null { const doc = communities.get(slug); if (!doc) return null; return doc.meta.nestPolicy || DEFAULT_COMMUNITY_NEST_POLICY; } /** * Update the NestPolicy for a space */ export function updateNestPolicy(slug: string, policy: Partial): boolean { const doc = communities.get(slug); if (!doc) return false; const newDoc = Automerge.change(doc, `Update nest policy`, (d) => { const current = d.meta.nestPolicy || { ...DEFAULT_COMMUNITY_NEST_POLICY }; d.meta.nestPolicy = { ...current, ...policy, notifications: { ...current.notifications, ...(policy.notifications || {}) }, defaultPermissions: { ...current.defaultPermissions, ...(policy.defaultPermissions || {}) }, }; }); communities.set(slug, newDoc); saveCommunity(slug); return true; } /** * Update enabled modules for a space */ export function setEnabledModules(slug: string, modules: string[]): boolean { const doc = communities.get(slug); if (!doc) return false; const newDoc = Automerge.change(doc, `Set enabled modules`, (d) => { d.meta.enabledModules = modules; }); communities.set(slug, newDoc); saveCommunity(slug); return true; } /** * Cap requested permissions against a policy's defaultPermissions ceiling */ export function capPermissions( requested: NestPermissions, ceiling: NestPermissions, ): NestPermissions { return { read: requested.read && ceiling.read, write: requested.write && ceiling.write, addShapes: requested.addShapes && ceiling.addShapes, deleteShapes: requested.deleteShapes && ceiling.deleteShapes, reshare: requested.reshare && ceiling.reshare, expiry: requested.expiry ? (ceiling.expiry ? Math.min(requested.expiry, ceiling.expiry) : requested.expiry) : ceiling.expiry, }; } /** * Cascade permissions across a nesting chain (intersection — most restrictive wins) */ export function cascadePermissions(chain: NestPermissions[]): NestPermissions { return { read: chain.every(p => p.read), write: chain.every(p => p.write), addShapes: chain.every(p => p.addShapes), deleteShapes: chain.every(p => p.deleteShapes), reshare: chain.every(p => p.reshare), expiry: chain.some(p => p.expiry) ? Math.min(...chain.filter(p => p.expiry).map(p => p.expiry!)) : undefined, }; } // ── Encryption ── /** * Toggle encryption on a space and set the key identifier. * When encrypted: true, the Automerge binary is AES-256 encrypted at rest. * The server decrypts on load and serves plaintext to authorized clients * (this is "Approach B: plaintext projection" from the architecture spec). * * Full E2E encryption (Approach C) will be added when EncryptID Layer 2 * client-side key delegation is implemented. */ export function setEncryption( slug: string, encrypted: boolean, encryptionKeyId?: string, ): boolean { const doc = communities.get(slug); if (!doc) return false; const newDoc = Automerge.change(doc, `Set encryption ${encrypted ? 'on' : 'off'}`, (d) => { d.meta.encrypted = encrypted; d.meta.encryptionKeyId = encrypted ? (encryptionKeyId || `key-${slug}-${Date.now()}`) : undefined; }); communities.set(slug, newDoc); saveCommunity(slug); return true; } /** * Derive an AES-256-GCM key from a space's encryption key identifier. * In production this will use EncryptID Layer 2 key derivation. * For now, uses a deterministic HMAC-based key from a server secret. */ async function deriveSpaceKey(keyId: string): Promise { const serverSecret = process.env.ENCRYPTION_SECRET || 'REDACTED_ENCRYPTION_FALLBACK'; const encoder = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey( 'raw', encoder.encode(serverSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'], ); const derived = await crypto.subtle.sign('HMAC', keyMaterial, encoder.encode(keyId)); return crypto.subtle.importKey( 'raw', derived, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'], ); } /** * Encrypt binary data using AES-256-GCM. * Returns: 12-byte IV + ciphertext + 16-byte auth tag (all concatenated). */ async function encryptBinary(data: Uint8Array, key: CryptoKey): Promise { const iv = crypto.getRandomValues(new Uint8Array(12)); // Copy into a fresh ArrayBuffer to satisfy strict BufferSource typing const plainBuf = new ArrayBuffer(data.byteLength); new Uint8Array(plainBuf).set(data); const ciphertext = await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, key, plainBuf, ); const result = new Uint8Array(12 + ciphertext.byteLength); result.set(iv, 0); result.set(new Uint8Array(ciphertext), 12); return result; } /** * Decrypt binary data encrypted with AES-256-GCM. * Expects: 12-byte IV + ciphertext + 16-byte auth tag. */ async function decryptBinary(data: Uint8Array, key: CryptoKey): Promise { const iv = data.slice(0, 12); const ciphertext = data.slice(12); const plaintext = await crypto.subtle.decrypt( { name: 'AES-GCM', iv }, key, ciphertext, ); return new Uint8Array(plaintext); } // Magic bytes to identify encrypted Automerge files const ENCRYPTED_MAGIC = new Uint8Array([0x72, 0x53, 0x45, 0x4E]); // "rSEN" (rSpace ENcrypted) /** * Find all spaces that a given space is nested into (reverse lookup) */ export async function findNestedIn(sourceSlug: string): Promise> { const results: Array<{ slug: string; refId: string; ref: SpaceRef }> = []; const allSlugs = await listCommunities(); for (const slug of allSlugs) { await loadCommunity(slug); const doc = communities.get(slug); if (!doc?.nestedSpaces) continue; for (const [refId, ref] of Object.entries(doc.nestedSpaces)) { if (ref.sourceSlug === sourceSlug) { results.push({ slug, refId, ref: JSON.parse(JSON.stringify(ref)) }); } } } return results; }