# space-gib — Implementation Spec

`space-gib` is the ibgib storage and identity provider service, deployed as a Docker
container alongside `blank-gib`. It is a single container that serves both:
1. A **Node.js API server** wrapping `NodeFilesystemSpace_V1` (ibgib storage + keystone ops)
2. A **browser SPA** (four-panel ibgib web app, client-side UI)

Published to: `https://ibgib.space`

---

## Core Design Decisions

### Keystone-as-Scope (not Keystone-as-Identity)
Keystones here are **authorization contexts**, not identity credentials (identity comes
later and is a specialization of this pattern). Any ibgib write requires solving the
current context keystone. The keystone evolution history IS the audit log of what was
written under that scope.

- **Public reads**: all ibgibs are publicly readable — no auth required for GET
- **Keyed writes**: all mutations (put ibgib, evolve keystone) require a valid keystone evolution proof
- **Nested scopes**: keystones can have child keystones. Parent must be evolved to
  create a child. Child has `parentTjpGib` in its `data` (soft-link, not rel8n).
  Operating within child scope does NOT require parent secrets.

### Sync Saga & Identity Delegation (Dynamic Manifest)
Content ibgibs are synchronized via the Sync Saga protocol. The workflow is:
1. **Sync Saga as Dynamic Manifest**: The Sync Saga timeline itself acts as the dynamic manifest of ibgibs transferred (requested/sent). The entire saga graph is traversed to resolve the full set of synced content.
2. **Domain Identity Delegation**: The long-lived Domain Identity (Domain Keystone) is evolved exactly once per sync saga to delegate signing authority to an ephemeral Session Identity (Session Keystone).
3. **Session Identity Binding**: The Session Identity genesis frame is cryptographically bound to specific target domain addresses. It is then used to sign all outgoing context frames and payload transfers during the sync process.

### Address Conventions
- Full ibgib address `ib^gib` is used everywhere (NOT just `tjpGib`)
- `tjpGib` is a reference convenience inside keystones and other ibgibs (stable timeline ID), not a full address
- URL-encode the `^` delimiter in route params: `ib%5Egib`
- Example full addr: `comment abc123%5E789def`
- **Fast TJP Extraction**: Use `getGibInfo` from `@ibgib/ts-gib/src/V1/transforms/transform-helper.mts` to efficiently extract `tjpGib` for routing and path mapping.

### Storage Backend
`NodeFilesystemSpace_V1` from `@ibgib/core-gib`. Known pain point: filesystem
path length limits (255 char max). The space has built-in `mitigateLongPaths` logic.
Data volume mounted at `/data/ibgib-space` inside the container.

---

## Multitenant Architecture: Keystone-as-Domain

`space-gib` implements a horizontal scaling, multitenant architecture where the **Keystone** is the primary mechanism for circumscribing "Digital Private Property."

### Domain Boundaries & Isolation
*   **The Keystone is the Domain**: A "user" (human, org, or agent) is represented by a top-level keystone. This keystone acts as the boundary of a secure domain.
*   **Composite Multitenancy**: Subscoped keystones create nested domains, allowing for independently evolving security contexts within a larger organization or project.
*   **Physical Space Isolation**: Each top-level domain is isolated into its own **self-contained physical space** on the server's filesystem.
*   **Bootstrap-per-Domain**: Every isolated domain has its own "Zero Space" and `bootstrap^gib`. This ensures that a domain is a completely portable, independent unit of the ibGib graph.

### Deterministic Pathing & Scaling
To achieve horizontal scaling and combat OS path limits, physical storage paths are derived deterministically from the keystone's `tjpGib`:
*   **Deterministic Mapping**: The relationship between a Domain (Identity) and its Physical Location is inherent in its `tjpGib`. No central registry or lookup database is required for V1 (Stateless Sharding).
*   **Path Nesting & Balancing**: To prevent filesystem performance degradation, paths use substrings of the `tjpGib` for distribution:
    *   **Git-Style Fan-out**: Substrings are applied at each level to balance OS folders (e.g., `/[tjp_sub1]/[tjp_sub2]/[child_tjp_sub1]/...`).
    *   **Collision Resistance**: Substrings should be long enough (e.g., 8-12 chars) to avoid collisions while staying within path length limits.
*   **Independent Parallelism**: The `lockSpace` mechanism is domain-specific. Updates to `User A` never contend for locks with `User B`, allowing for massive concurrent throughput across different domains.

### Security & Visibility
*   **Public Keystones**: Keystones are considered publicly visible records.
*   **Secret Strength**: Security depends on the strength of the keystone's secret/passphrase (similar to BIP-39 mnemonic phrases in cryptowallets).
*   **Auditability**: The keystone evolution chain within a domain provides a complete, immutable audit log of all authorized content manifest additions for that property boundary.

### Identity: Aggregate & Checkpoint Details
To manage identity metadata without bloating every frame or requiring external ibGibs:
-   **`checkpointDetails`**: Every X frames, a "checkpoint" is written into the keystone's `frameDetails`.
-   **Rehydration**: To resolve the current identity metadata (e.g., username), the system walks back from the tip to the nearest checkpoint or genesis, then replays forward while aggregating the state using a **Last-Write-Wins** strategy.
-   **Aggregate State**: This allows for evolving metadata (email, display name) while maintaining a verifiable, audit-friendly identity timeline.

### Sync Sagas: Temp & Durable Spaces
The synchronization engine (Sync Saga) handles multi-device consistency using a "Commit Phase":
-   **Temp Space**: Content is initially accumulated in an ephemeral `TempSpace`. This allows for long-running transfers without locking the primary domain or risking data corruption on failure.
-   **Durable Space**: The persistent storage space where data is ultimately committed.
-   **Atomic Commit**: Once the content is validated and stored in `TempSpace`, the coordinator executes the commit. Upon successful commit, the verified ibgibs are copied/moved from `TempSpace` to `Durable` space. This transactional boundary is fully integrated with Keystone identity checks, ensuring only authenticated and authorized sync sessions can commit data.
   - _NOTE: Once authenticated per communication leg, the sync saga control (non-domain, meta) ibgibs are immediately stored in durable space for the audit trail as it happens. Only the domain ibgibs are moved in the commit phase._

### Near-Term Goal: Self-Contained Multitenancy
Our immediate priority is establishing the V1 multitenant foundation:
1.  **API Scoping**: Hit an endpoint like `PUT /api/keystone/evolve/:addr`.
2.  **Domain Resolution**: ServeGib_V1 extracts `tjpGib` and calculates `domainRootPath`.
3.  **Factory Closure Pattern**: Inject runtime factory functions (`fnZeroSpaceFactory`, `fnDefaultLocalSpaceFactory`) into the `Metaspace_Nodespace` that capture the `domainRootPath` in their closure.
4.  **Metaspace Orchestration**: Bootstrap the isolated domain and its private user space.
5.  **Keystone Evolution**: Execute authorized evolutions within that isolated boundary.

### Factory Closure Pattern
To isolate domains without modifying core library types:
-   **Runtime Injection**: The `MetaspaceFactory` is created dynamically per request (or domain).
-   **Closure Capture**: The factory functions capture the calculated `domainRootPath` in their scope.
-   **Zero Space Localization**: `fnZeroSpaceFactory` initializes the `NodeFilesystemSpace_V1` with the captured `baseDir`.
-   **New Space Localization**: `fnDefaultLocalSpaceFactory` ensures any new local spaces created (e.g. on first run) are placed within the domain's subfolder.

### Future Evolution
*   **Sharding (V2+)**: The deterministic `tjpGib`-to-path mapping allows load balancers to route requests to specific shards without needing stateful knowledge of the user's location.
*   **Manual Balancing (V3+)**: A future migration/balancing layer can add an optional stateful mapping to override default locations when domains need to move across physical nodes.

---

## Directory Structure

```
apps/space-gib/
├── src/
│   ├── client/                   ← Browser SPA (ibgib-create-app skill pattern)
│   │   ├── index.html
│   │   ├── index.mts
│   │   ├── script.mts
│   │   ├── bootstrap.mts
│   │   ├── constants.mts
│   │   ├── types.mts
│   │   ├── helpers.web.mts
│   │   ├── style.css
│   │   ├── app/                  ← App Witness (ibgib-create-app-witness skill)
│   │   │   ├── space-gib-app.mts
│   │   │   ├── space-gib-app-constants.mts
│   │   │   ├── space-gib-app-helpers.mts
│   │   │   └── space-gib-app-types.mts
│   │   ├── shell/                ← UI Shell (ibgib-create-shell skill)
│   │   │   ├── space-gib-shell.mts
│   │   │   ├── space-gib-shell-constants.mts
│   │   │   └── space-gib-shell-types.mts
│   │   └── components/           ← ibgib components (ibgib-create-component skill)
│   │       └── space-main/
│   │           ├── space-main.mts
│   │           ├── space-main.html
│   │           └── space-main.css
│   └── server/                   ← Node.js API server
│       ├── server.mts            ← Express entry point
│       ├── space.mts             ← NodeFilesystemSpace_V1 singleton wrapper
│       ├── middleware/
│       │   └── validate-keystone.mts  ← Middleware: verify evolution proof before write
│       └── routes/
│           ├── ibgib.routes.mts       ← /api/ibgib/* routes
│           └── keystone.routes.mts    ← /api/keystone/* routes
├── dist/
│   ├── client/                   ← Compiled SPA output
│   └── server/                   ← Compiled Node.js output
├── Dockerfile
├── nginx.conf                    ← NOT used; kept for reference only
├── package.json
├── tsconfig.json                 ← Client compilation (browser target)
└── tsconfig.server.json          ← Server compilation (Node.js target, ESM)
```

---

## API Routes

All routes are under `/api/`. SPA is served at all other paths.

### IbGib Routes (`/api/ibgib/`)

```
POST   /api/ibgib
  Body:   { ibGib: IbGib_V1 }           ← single ibgib
  Or:     { ibGibs: IbGib_V1[] }        ← batch (pack)
  Auth:   none (genesis/content ibgibs are self-legitimizing)
  Notes:  server validates internal ibgib structure; stores via NodeFilesystemSpace_V1

GET    /api/ibgib/:domainAddr/:ibGibAddr
  Param:  domainAddr = URL-encoded domain keystone address
  Param:  ibGibAddr = URL-encoded ib^gib address
  Query:  ?getLatest=true|false (default false) — resolves the given addr to its latest timeline tip
  Query:  ?getGraph=true|false (default false) — returns the entire dependency graph instead of a single ibgib
  Query:  ?addrOnly=true|false (default false) — strips the response down to address(es) only, skipping ibgib body transmission.
          - With getGraph=false: returns { addr, clientAddr } — cheap tip-check; if addr !== clientAddr, sync is needed.
          - With getGraph=true: fetches the graph but returns { addr, clientAddr, addrs: string[] } — address manifest
            for delta negotiation (client compares addrs against what it already has to compute the missing set).
  Auth:   none (all reads are public)
  Returns (single): { ibGib: IbGib_V1 } or 404
  Returns (graph): { addr: string, count: number, graph: Record<string, IbGib_V1> } or 404
  Returns (addrOnly, no graph): { addr: string, clientAddr: string }
  Returns (addrOnly + graph): { addr: string, clientAddr: string, addrs: string[] }
  Notes:  If getGraph is true, server walks rel8ns recursively. May be large.

POST   /api/ibgib/pack
  Body:   { knownAddrs: IbGibAddr[] }   ← addresses the client already has
  Auth:   none
  Returns: { ibGibs: IbGib_V1[] }       ← only the missing ones
  Notes:  future expansion for incremental graph sync (requires HTTP/2 or WebSocket)
```

### Keystone Routes (`/api/keystone/`)

```
POST   /api/keystone
  Body:   { ibGib: KeystoneIbGib_V1 }   ← genesis frame (first frame, no prior)
  Auth:   none (genesis is self-legitimizing)
  Returns: { addr: IbGibAddr, tjpGib: Gib }
  Notes:  server validates internal keystone structure; stores genesis frame

PUT    /api/keystone/evolve/:addr
  Param:  addr = URL-encoded CURRENT TIP's full ib^gib address (CAS token)
  Body:   { ibGib: KeystoneIbGib_V1 }   ← the new evolution frame (pre-computed by client)
  Auth:   implicit — valid challenge solutions in the frame ARE the auth
  Returns: { addr: IbGibAddr }           ← address of the accepted new frame
  Error:  409 Conflict if :addr is not the current tip (stale — client must retry)
  Notes:  server validates: (1) chain continuity (hash pre-images match commitments),
          (2) CAS check (:addr === current tip). Rejects if either fails.

GET    /api/keystone/:domainAddr
  Param:  domainAddr = URL-encoded full ib^gib address (any frame OR tjpGib)
  Query:  ?getLatest=true|false (default true) — resolves the keystone to its latest evolution
  Query:  ?getGraph=true|false (default true) — returns the entire keystone timeline chain map
  Auth:   none
  Returns: { domainGraph: Record<string, KeystoneIbGib_V1> }  ← genesis to current tip mapped by address
  Notes:  domainAddr is always the full keystone address, never just the tjpGib. The tjpGib is used as an id sometimes, but an api addr param must be the full address.
```

**On `/evolve` vs bare `PUT /api/keystone/:addr`:**
We use `PUT /api/keystone/evolve/:addr` (not `PUT /api/keystone/:addr`) because:
- Evolution has distinct server-side semantics: chain continuity check + CAS
- Avoids router ambiguity with the `GET /api/keystone/:domainAddr` route
- The `:domainAddr` being BEFORE the route specific term makes native Node routing more complicated
- Explicit `/evolve` makes intent clear in logs, proxies, and future middleware

---

## Server Entry Point Sketch

```typescript
// src/server/server.mts
import * as http from 'http';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { readFileSync, statSync } from 'fs';

const PORT = process.env.PORT ?? 3000;
const CLIENT_DIST = join(dirname(fileURLToPath(import.meta.url)), '../client');

const server = http.createServer((req, res) => {
    try {
        const url = new URL(req.url || '/', `http://${req.headers.host}`);

        // Basic CORS
        res.setHeader('Access-Control-Allow-Origin', '*');
        res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
        res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

        if (req.method === 'OPTIONS') {
            res.writeHead(204);
            res.end();
            return;
        }

        // API routes
        if (url.pathname.startsWith('/api/ibgib/')) {
            // ... handle ibgib routes natively
            return;
        }
        if (url.pathname.startsWith('/api/keystone/')) {
            // ... handle keystone routes natively
            return;
        }

        // SPA fallback — serve static files
        let filePath = join(CLIENT_DIST, url.pathname === '/' ? 'index.html' : url.pathname);
        try {
            if (!statSync(filePath).isFile()) filePath = join(CLIENT_DIST, 'index.html');
        } catch {
            filePath = join(CLIENT_DIST, 'index.html');
        }

        const content = readFileSync(filePath);
        res.writeHead(200);
        res.end(content);

    } catch (err) {
        res.writeHead(500);
        res.end('Internal Server Error');
    }
});

server.listen(PORT, () => console.log(`space-gib listening on :${PORT}`));
```

---

## Space Singleton Sketch

```typescript
// src/server/space.mts
import { NodeFilesystemSpace_V1 } from '@ibgib/core-gib/dist/witness/space/filesystem-space/node-filesystem-space/node-filesystem-space-v1.mjs';

const DATA_DIR = process.env.DATA_DIR ?? '/data/ibgib-space';

let _space: NodeFilesystemSpace_V1 | null = null;

export async function getSpace(): Promise<NodeFilesystemSpace_V1> {
    if (!_space) {
        _space = new NodeFilesystemSpace_V1({ baseDir: DATA_DIR });
        await _space.initialized;
    }
    return _space;
}
```

---

## IbGib Routes Sketch

```typescript
// src/server/routes/ibgib.routes.mts
// ... implemented using native node http Request/Response parsing ...
```

---

## Keystone Routes Sketch

```typescript
// src/server/routes/keystone.routes.mts
// ... implemented using native node http Request/Response parsing ...
```

---

## Dockerfile

```dockerfile
FROM node:22-alpine
WORKDIR /app

# Server runtime
COPY dist/server ./server

# Client SPA (served as static files)
COPY dist/client ./client

# Only production deps
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

ENV PORT=3000
ENV DATA_DIR=/data/ibgib-space

EXPOSE 3000
CMD ["node", "server/server.mjs"]
```

---

## docker-compose Additions

**`docker-compose.yml`** (base / local):
```yaml
  space-gib:
    build: ./apps/space-gib
    volumes:
      - ibgib_space_data:/data/ibgib-space
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.space-gib.rule=Host(`space.ibgib.localhost`)"
      - "traefik.http.routers.space-gib.entrypoints=websecure"
      - "traefik.http.routers.space-gib.tls=true"
      - "traefik.http.services.space-gib.loadbalancer.server.port=3000"

volumes:
  ibgib_space_data:
```

**`docker-compose.prod.yml`**:
```yaml
  space-gib:
    labels:
      - "traefik.http.routers.space-gib.rule=${SPACE_GIB_HOST_RULE:-Host(`ibgib.space`)}"
      - "traefik.http.routers.space-gib.tls.certresolver=myresolver"
    restart: always
```

---

## package.json Shape

```json
{
  "name": "@ibgib/space-gib",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "description": "ibgib storage and provider service — SaaS at ibgib.space",
  "scripts": {
    "build:client": "node ../../build/dist/concrete-build/build-space-gib.mjs",
    "build:server": "tsc -p tsconfig.server.json",
    "build": "npm run build:client && npm run build:server",
    "start": "node dist/server/server.mjs",
    "dev:server": "node --watch dist/server/server.mjs"
  },
  "dependencies": {
    "@ibgib/core-gib": "*",
    "@ibgib/web-gib": "*"
  },
  "devDependencies": {
  },
  "engines": { "node": ">=22.0.0" }
}
```

---

## tsconfig.server.json Shape

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist/server",
    "rootDir": "./src/server",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/server/**/*.mts"],
  "exclude": ["node_modules", "dist"]
}
```

---

## Implementation Phases

### Phase 1: Scaffold Client SPA
- Use `ibgib-create-app` skill to generate the 4-panel SPA in `src/client/`
- Tokens: `APP_NAME=space-gib`, `APP_CLASSNAME_PREFIX=SpaceGib`
- Initial component: `space-main` — displays a placeholder four-panel layout
- No real data yet; just confirms the SPA builds and the Docker image boots

### Phase 2: Server Skeleton + Storage
- Create `src/server/server.mts`, `space.mts`, route files
- Wire up `NodeFilesystemSpace_V1` with Docker volume
- Implement and manually test:
  - `POST /api/ibgib` (store any ibgib)
  - `GET /api/ibgib/:addr` (retrieve by full addr)
  - `GET /api/ibgib/graph/:addr` (recursive graph walk)
- Write `tsconfig.server.json` and `Dockerfile`
- Add to `docker-compose.yml`

### Phase 3: Keystone Operations
- Implement `POST /api/keystone` (genesis)
- Implement `PUT /api/keystone/evolve/:addr` with CAS + chain continuity check
  - Uses `KeystoneService_V1.validate()` from `@ibgib/core-gib`
  - Implement `getKeystoneTip()` via space metastone lookup
- Implement `GET /api/keystone/:addr` (full chain retrieval)

### Phase 4: Manifest + Scoped Writes
- Server-side manifest ibgib creation (given a list of content addrs → create manifest ibgib)
- Enforce: any content write must be accompanied by a valid keystone evolution
  whose `claim.target` = the manifest's addr
- Validate soft-link parent reference when creating child keystones
- New route: `POST /api/manifest` → creates manifest ibgib from dependency list

### Phase 5: Client Wires Up
- `space-main` component navigates keystone hierarchy
- Create keystone UI (calls `POST /api/keystone`)
- Evolve keystone UI (client computes evolution, calls `PUT /api/keystone/evolve/:addr`)
- Browse/view ibgib content in space

### Phase 6: Comment Ibgib Use Case
- Client creates comment ibgib using existing ibgib transform machinery
- Server creates manifest from resulting dependency graph
- Full end-to-end: create comment → manifest → evolve context keystone → store content

---

## Key Known Constraints

- **Long path mitigation**: `NodeFilesystemSpace_V1` has `mitigateLongPaths=true` by default.
  Ibgib addresses can exceed 255 chars; the space hashes long paths into a `long/` subdir.
  This is a known pain point — be aware when debugging missing ibgibs.
- **No auth on reads**: by design; all ibgibs are public. Write auth via keystone proofs only.
- **CAS on keystone evolve**: clients must retry on 409. The retry loop is the client's
  responsibility. No server-side locking.
- **Single space instance**: the server is a singleton wrapper; one `NodeFilesystemSpace_V1`
  per process. Horizontal scaling requires a shared storage backend (Postgres — future).
