rspace-online/lib/connection-types.ts

240 lines
7.2 KiB
TypeScript

/**
* 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<ConnectionState, string> = {
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<string, unknown>;
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<string, unknown>;
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 };
}