240 lines
7.2 KiB
TypeScript
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 };
|
|
}
|