/** * SpaceConnection — bilateral typed relationships between spaces. * * Distinct from nesting (SpaceRef: unilateral visual embed) and LayerFlow * (intra-space module flows). SpaceConnections are stored in BOTH spaces' * Automerge docs with the same ID, enabling economic streams, delegative * authority, and information sharing across space boundaries. */ import type { FlowKind } from "./layer-types"; // ── Connection state machine ── export type ConnectionState = "proposed" | "active" | "paused" | "revoked"; export const CONNECTION_STATE_COLORS: Record = { proposed: "#f59e0b", // amber active: "#22c55e", // green paused: "#64748b", // slate revoked: "#ef4444", // red }; // ── Directionality ── export type ConnectionDirection = "outbound" | "inbound" | "bidirectional"; /** Invert direction from the other space's perspective */ export function invertDirection(dir: ConnectionDirection): ConnectionDirection { if (dir === "outbound") return "inbound"; if (dir === "inbound") return "outbound"; return "bidirectional"; } // ── Economic stream spec ── export interface EconomicFlowSpec { tokenId: string; // mint shape ID, ERC-20 address, or slug:mintId tokenName?: string; rateLimit: number; // max tokens per window (0 = unlimited) rateWindowMs: number; // window duration in ms requireApproval: boolean; // per-transfer approval gate minTransfer?: number; maxTransfer?: number; } // ── Delegation spec (liquid democracy) ── export type DelegableAuthority = | "voting" | "moderation" | "curation" | "treasury" | "membership" | "custom"; export interface DelegationSpec { authority: DelegableAuthority; scope?: string; // human-readable scope description maxDepth: number; // re-delegation depth (0 = no chain) weight: number; // 0-1 fractional delegation retainAuthority: boolean; // delegator keeps authority alongside delegate expiry?: number; // unix timestamp } // ── Information stream spec ── export interface InformationFlowSpec { allowedShapeTypes?: string[]; // shape types that can flow (empty = all) allowedTags?: string[]; allowedModules?: string[]; feedIds?: string[]; // subscribe to specific feeds maxItemsPerSync?: number; historicalSync: boolean; // include history or only new items } // ── Per-connection permissions ── export interface ConnectionPermissions { read: boolean; write: boolean; configure: boolean; // can modify connection params pause: boolean; revoke: boolean; expiry?: number; // auto-expire unix timestamp } // ── Per-space connection policy ── export interface ConnectionPolicy { inboundConsent: "open" | "members" | "approval" | "closed"; outboundConsent: "open" | "members" | "approval" | "closed"; defaultPermissions: ConnectionPermissions; acceptedFlowKinds: FlowKind[]; allowlist?: string[]; // slugs bypassing consent blocklist?: string[]; // slugs always denied } // ── The SpaceConnection record (stored in BOTH spaces' docs) ── export interface SpaceConnection { id: string; // shared ID (same in both docs) localSlug: string; // this space remoteSlug: string; // other space remoteDID?: string; // remote owner DID at creation time direction: ConnectionDirection; // from this space's perspective flowKinds: FlowKind[]; state: ConnectionState; economicFlows?: EconomicFlowSpec[]; delegations?: DelegationSpec[]; informationFlow?: InformationFlowSpec; permissions: ConnectionPermissions; strength: number; // 0-1, visual membrane permeability label?: string; meta?: Record; proposedBy: string; // DID proposedAt: number; acceptedBy?: string; acceptedAt?: number; lastModifiedBy?: string; lastModifiedAt?: number; stateReason?: string; // reason for pause/revoke } // ── Pending connection request (in-memory, like PendingNestRequest) ── export type ConnectionRequestStatus = "pending" | "accepted" | "rejected"; export interface PendingConnectionRequest { id: string; fromSlug: string; toSlug: string; proposedBy: string; // DID direction: ConnectionDirection; flowKinds: FlowKind[]; permissions: ConnectionPermissions; strength: number; label?: string; economicFlows?: EconomicFlowSpec[]; delegations?: DelegationSpec[]; informationFlow?: InformationFlowSpec; meta?: Record; message?: string; status: ConnectionRequestStatus; createdAt: number; resolvedAt?: number; resolvedBy?: string; } // ── Derived membrane permeability (for visualization) ── export interface MembranePermeability { inbound: { kind: FlowKind; strength: number; connectionId: string }[]; outbound: { kind: FlowKind; strength: number; connectionId: string }[]; overallPermeability: number; // 0 = sealed, 1 = fully open } // ── Default policies ── export const DEFAULT_PERSONAL_CONNECTION_POLICY: ConnectionPolicy = { inboundConsent: "approval", outboundConsent: "approval", defaultPermissions: { read: true, write: false, configure: false, pause: true, revoke: true, }, acceptedFlowKinds: ["data", "trust"], }; export const DEFAULT_COMMUNITY_CONNECTION_POLICY: ConnectionPolicy = { inboundConsent: "members", outboundConsent: "members", defaultPermissions: { read: true, write: true, configure: false, pause: true, revoke: true, }, acceptedFlowKinds: ["economic", "trust", "data", "governance", "resource"], }; // ── Helpers ── /** * Cascade connection permissions across a nesting chain (intersection). * When space A nests B which has connections, the effective permissions * are the intersection of the nest permissions and connection permissions. */ export function cascadeConnectionPermissions( chain: ConnectionPermissions[], ): ConnectionPermissions { return { read: chain.every(p => p.read), write: chain.every(p => p.write), configure: chain.every(p => p.configure), pause: chain.every(p => p.pause), revoke: chain.every(p => p.revoke), expiry: chain.some(p => p.expiry) ? Math.min(...chain.filter(p => p.expiry).map(p => p.expiry!)) : undefined, }; } /** * Compute membrane permeability from a set of active connections. */ export function computeMembranePermeability( connections: SpaceConnection[], localSlug: string, ): MembranePermeability { const inbound: MembranePermeability["inbound"] = []; const outbound: MembranePermeability["outbound"] = []; for (const conn of connections) { if (conn.state !== "active") continue; for (const kind of conn.flowKinds) { const entry = { kind, strength: conn.strength, connectionId: conn.id }; if (conn.direction === "inbound" || conn.direction === "bidirectional") { inbound.push(entry); } if (conn.direction === "outbound" || conn.direction === "bidirectional") { outbound.push(entry); } } } // Overall permeability: average of all active connection strengths, or 0 if none const activeConns = connections.filter(c => c.state === "active"); const overallPermeability = activeConns.length > 0 ? activeConns.reduce((sum, c) => sum + c.strength, 0) / activeConns.length : 0; return { inbound, outbound, overallPermeability }; }