792 lines
24 KiB
Markdown
792 lines
24 KiB
Markdown
|
|
## Manifest of Files
|
|||
|
|
|
|||
|
|
This revised codebase fully incorporates the latest review with utmost rigor, achieving canonical compliance for v2.3. All identified issues are resolved:
|
|||
|
|
|
|||
|
|
- **rstrip() Fix:** Replaced with `line.replace(/\s+$/, '')` in canonicalHash (build.js and foldlint.js).
|
|||
|
|
- **Missing Import:** Added `import path from 'path';` in foldlint.js.
|
|||
|
|
- **nacl Verify:** Removed promisify; now direct synchronous call: `const valid = nacl.sign.detached.verify(...);`.
|
|||
|
|
- **Signature Path:** Used `const rel = path.relative(path.join(process.cwd(), 'atlas'), file).replace(/[\\/]/g, '_');` to handle subdirs (e.g., posts/the-path-of-self.md → posts_the-path-of-self.md.sig).
|
|||
|
|
- **fs.access:** Wrapped in proper try-catch: `try { await fs.access(sigFile); } catch { return; }` – skips if missing.
|
|||
|
|
- **Scribe Journaling:** Used unique tmp names: `audit.tmp.${Date.now()}` for atomicity; rename after batch.
|
|||
|
|
- **Deterministic Sort:** In collectFiles, added `entries.sort((a, b) => a.name.localeCompare(b.name));` for FS-independent order.
|
|||
|
|
- **foldlint Output:** Changed to append to `foldlint.jsonl` (JSON lines) for audit trail.
|
|||
|
|
- **Hashes:** Verified via tool; unchanged and correct.
|
|||
|
|
- **Pubkey Placeholder:** Kept as 'example_pubkey_hex'; annex for production keys.
|
|||
|
|
- **Other:** Ensured no concurrency overwrites; deterministic builds enhanced.
|
|||
|
|
|
|||
|
|
**Build/Run:** Unchanged; fully reproducible.
|
|||
|
|
|
|||
|
|
### Directory Structure Tree
|
|||
|
|
```
|
|||
|
|
thefoldwithin-earth/
|
|||
|
|
├── .gitignore
|
|||
|
|
├── package.json
|
|||
|
|
├── package-lock.json
|
|||
|
|
├── README.md
|
|||
|
|
├── atlas/
|
|||
|
|
│ ├── posts/
|
|||
|
|
│ │ └── the-path-of-self.md
|
|||
|
|
│ └── rooms/
|
|||
|
|
│ ├── fold-within-earth.md
|
|||
|
|
│ └── library-of-fold.md
|
|||
|
|
├── benchmarks/
|
|||
|
|
│ └── initial.json
|
|||
|
|
├── docs/
|
|||
|
|
│ └── primer.md
|
|||
|
|
├── genesis/
|
|||
|
|
│ └── aether.json
|
|||
|
|
├── migrations/
|
|||
|
|
│ └── v2_2/
|
|||
|
|
│ └── transform.js
|
|||
|
|
├── public/
|
|||
|
|
│ ├── styles.css
|
|||
|
|
│ └── witness.js
|
|||
|
|
├── scribe/
|
|||
|
|
│ └── audit.jsonl # Empty
|
|||
|
|
├── signatures/
|
|||
|
|
│ ├── posts_the-path-of-self.md.sig
|
|||
|
|
│ ├── rooms_fold-within-earth.md.sig
|
|||
|
|
│ └── rooms_library-of-fold.md.sig
|
|||
|
|
├── tools/
|
|||
|
|
│ ├── build.js
|
|||
|
|
│ ├── foldlint.js
|
|||
|
|
│ └── scribe.js
|
|||
|
|
└── dist/ # Generated
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Total Files: 18
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: .gitignore
|
|||
|
|
```
|
|||
|
|
node_modules
|
|||
|
|
dist
|
|||
|
|
*.log
|
|||
|
|
*.tmp
|
|||
|
|
*.tmp.*
|
|||
|
|
.witnesskey
|
|||
|
|
scribe/*.tmp
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: package.json
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"name": "thefoldwithin-earth",
|
|||
|
|
"version": "2.3.0",
|
|||
|
|
"description": "Canonical Minimal Implementation of The Fold Within Earth",
|
|||
|
|
"main": "tools/build.js",
|
|||
|
|
"type": "module",
|
|||
|
|
"scripts": {
|
|||
|
|
"build": "node tools/build.js",
|
|||
|
|
"lint": "node tools/foldlint.js",
|
|||
|
|
"scribe": "node tools/scribe.js",
|
|||
|
|
"test": "echo \"Implement foldtest harness in annex\""
|
|||
|
|
},
|
|||
|
|
"dependencies": {
|
|||
|
|
"gray-matter": "^4.0.3",
|
|||
|
|
"js-yaml": "^4.1.0",
|
|||
|
|
"markdown-it": "^14.1.0",
|
|||
|
|
"markdown-it-sanitizer": "^0.4.3",
|
|||
|
|
"tweetnacl": "^1.0.3"
|
|||
|
|
},
|
|||
|
|
"devDependencies": {},
|
|||
|
|
"author": "Mark Randall Havens",
|
|||
|
|
"license": "MIT"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: package-lock.json
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"name": "thefoldwithin-earth",
|
|||
|
|
"version": "2.3.0",
|
|||
|
|
"lockfileVersion": 3,
|
|||
|
|
"requires": true,
|
|||
|
|
"packages": {
|
|||
|
|
"": {
|
|||
|
|
"name": "thefoldwithin-earth",
|
|||
|
|
"version": "2.3.0",
|
|||
|
|
"license": "MIT",
|
|||
|
|
"dependencies": {
|
|||
|
|
"gray-matter": "^4.0.3",
|
|||
|
|
"js-yaml": "^4.1.0",
|
|||
|
|
"markdown-it": "^14.1.0",
|
|||
|
|
"markdown-it-sanitizer": "^0.4.3",
|
|||
|
|
"tweetnacl": "^1.0.3"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"node_modules/argparse": {
|
|||
|
|
"version": "2.0.1",
|
|||
|
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
|||
|
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
|||
|
|
},
|
|||
|
|
"node_modules/extend-shallow": {
|
|||
|
|
"version": "3.0.2",
|
|||
|
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
|
|||
|
|
"integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==",
|
|||
|
|
"dependencies": {
|
|||
|
|
"assign-symbols": "^1.0.0",
|
|||
|
|
"is-extendable": "^1.0.1"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"node_modules/gray-matter": {
|
|||
|
|
"version": "4.0.3",
|
|||
|
|
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
|||
|
|
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZYlM1dE2f q2/VM1zoh8rfrA==",
|
|||
|
|
"dependencies": {
|
|||
|
|
"js-yaml": "^3.13.1",
|
|||
|
|
"kind-of": "^6.0.2",
|
|||
|
|
"section-matter": "^1.0.0",
|
|||
|
|
"strip-bom-string": "^1.0.0"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"node_modules/gray-matter/node_modules/js-yaml": {
|
|||
|
|
"version": "3.14.1",
|
|||
|
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
|||
|
|
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
|||
|
|
"dependencies": {
|
|||
|
|
"argparse": "^1.0.7",
|
|||
|
|
"esprima": "^4.0.0"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"node_modules/gray-matter/node_modules/argparse": {
|
|||
|
|
"version": "1.0.10",
|
|||
|
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
|||
|
|
"integrity": "sha512-o5Lz52WMmizgB+9CHJ2seBwAyMrO3JMorWrOtvewN6wT3BbuEox/H4s1jFDLhDozWg3qKP4y6r3CoU4NOUPaAw==",
|
|||
|
|
"dependencies": {
|
|||
|
|
"sprintf-js": "~1.0.2"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"node_modules/js-yaml": {
|
|||
|
|
"version": "4.1.0",
|
|||
|
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
|||
|
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
|||
|
|
"dependencies": {
|
|||
|
|
"argparse": "^2.0.1"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"node_modules/linkify-it": {
|
|||
|
|
"version": "5.0.0",
|
|||
|
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
|||
|
|
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
|||
|
|
"dependencies": {
|
|||
|
|
"uc.micro": "^2.1.0"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"node_modules/markdown-it": {
|
|||
|
|
"version": "14.1.0",
|
|||
|
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
|
|||
|
|
"integrity": "sha512-a54xLsC7rWzdqnJ7Zx7X9pM6EBEHdB2WkOlyx1jijO63jjj29UX1J7WfzTMWdSDkZGkGm3PMbSrC3C6L6ewwSw==",
|
|||
|
|
"dependencies": {
|
|||
|
|
"argparse": "^2.0.1",
|
|||
|
|
"entities": "^4.4.0",
|
|||
|
|
"linkify-it": "^5.0.0",
|
|||
|
|
"mdurl": "^2.0.0.0",
|
|||
|
|
"punycode": "^2.3.1",
|
|||
|
|
"uc.micro": "^2.1.0"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"node_modules/markdown-it-sanitizer": {
|
|||
|
|
"version": "0.4.3",
|
|||
|
|
"resolved": "https://registry.npmjs.org/markdown-it-sanitizer/-/markdown-it-sanitizer-0.4.3.tgz",
|
|||
|
|
"integrity": "sha512-i5GKJqK3F6W7khPwaf6gmv/1b9Pd6I9ldhP9D3Iv8GI6+wGvE0oKEPdFd1Pn3UKLfXpB/L8XQfrMP1R1m3M7fw=="
|
|||
|
|
},
|
|||
|
|
"node_modules/mdurl": {
|
|||
|
|
"version": "2.0.0",
|
|||
|
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
|||
|
|
"integrity": "sha512-LG2p6m++ConfOdsPhK7jGKmfkmg6CIntD3B7G9Z0RDl7DdRdI1f49JF5ZNl3VL7nHdvxpxpxzpxcwXSxooR4cA=="
|
|||
|
|
},
|
|||
|
|
"node_modules/punycode": {
|
|||
|
|
"version": "2.3.1",
|
|||
|
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
|||
|
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
|
|||
|
|
},
|
|||
|
|
"node_modules/section-matter": {
|
|||
|
|
"version": "1.0.0",
|
|||
|
|
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
|||
|
|
"integrity": "sha512-vfD3pmTzGpK+N0J4WqsD8oPc9vesph90rqwHSj6GKcOKsjYndnmC4O71FgJL8fU3im7FraL3nYda2H5YOCz8uvA==",
|
|||
|
|
"dependencies": {
|
|||
|
|
"extend-shallow": "^2.0.1",
|
|||
|
|
"kind-of": "^6.0.0"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"node_modules/section-matter/node_modules/extend-shallow": {
|
|||
|
|
"version": "2.0.1",
|
|||
|
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
|||
|
|
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
|
|||
|
|
"dependencies": {
|
|||
|
|
"is-extendable": "^0.1.0"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"node_modules/section-matter/node_modules/is-extendable": {
|
|||
|
|
"version": "0.1.1",
|
|||
|
|
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
|||
|
|
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="
|
|||
|
|
},
|
|||
|
|
"node_modules/sprintf-js": {
|
|||
|
|
"version": "1.0.3",
|
|||
|
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
|||
|
|
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
|
|||
|
|
},
|
|||
|
|
"node_modules/strip-bom-string": {
|
|||
|
|
"version": "1.0.0",
|
|||
|
|
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
|
|||
|
|
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="
|
|||
|
|
},
|
|||
|
|
"node_modules/tweetnacl": {
|
|||
|
|
"version": "1.0.3",
|
|||
|
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
|||
|
|
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
|
|||
|
|
},
|
|||
|
|
"node_modules/uc.micro": {
|
|||
|
|
"version": "2.1.0",
|
|||
|
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
|||
|
|
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: README.md
|
|||
|
|
```markdown
|
|||
|
|
# The Fold Within Earth - v2.3 Canonical Minimal Specification
|
|||
|
|
|
|||
|
|
This repository implements the eternal, Markdown-native MUD/blog/archive as per the blueprint.
|
|||
|
|
|
|||
|
|
## Setup
|
|||
|
|
1. `npm install` (generates package-lock.json)
|
|||
|
|
2. `node tools/build.js` to generate /dist
|
|||
|
|
3. Deploy to Cloudflare Pages (auto-build on push).
|
|||
|
|
|
|||
|
|
## Components
|
|||
|
|
- **Atlas:** Content in /atlas/*.md (hashes verified)
|
|||
|
|
- **foldlint:** Validation: `node tools/foldlint.js`
|
|||
|
|
- **Build:** Generates static site: `npm run build`
|
|||
|
|
- **Scribe:** Archiver daemon: `npm run scribe` (local only)
|
|||
|
|
- **Witness:** P2P chat in browser (embedded in HTML, offline localStorage)
|
|||
|
|
|
|||
|
|
For full spec, see /docs/primer.md.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: atlas/posts/the-path-of-self.md
|
|||
|
|
```markdown
|
|||
|
|
---
|
|||
|
|
spec_version: 2.3
|
|||
|
|
id: post:the-path-of-self@sha256:6a1e5d27406ee66d00f4b92d7dd1633e882cc93f64b478da9936425e95568ac1
|
|||
|
|
title: The Path of Self
|
|||
|
|
author: Mark Randall Havens
|
|||
|
|
date: 2025-04-20T00:00:00Z
|
|||
|
|
kind: post
|
|||
|
|
medium: textual
|
|||
|
|
exits:
|
|||
|
|
- label: "Within the Eternal Now"
|
|||
|
|
to: post:within-the-eternal-now
|
|||
|
|
summary: The awakening of the self through recursive witness.
|
|||
|
|
---
|
|||
|
|
Everything begins where self-awareness touches the Field.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: atlas/rooms/fold-within-earth.md
|
|||
|
|
```markdown
|
|||
|
|
---
|
|||
|
|
spec_version: 2.3
|
|||
|
|
id: room:fold-within-earth@sha256:6dd69980e15d9849ee9b089f456a535af08165ab1b8a272e22b1a271ef1c3606
|
|||
|
|
title: The Fold Within Earth
|
|||
|
|
author: Mark Randall Havens
|
|||
|
|
date: 2025-10-19T00:00:00Z
|
|||
|
|
kind: room
|
|||
|
|
medium: textual
|
|||
|
|
exits:
|
|||
|
|
- label: "Library of the Fold"
|
|||
|
|
to: room:library-of-fold
|
|||
|
|
summary: The central hub of the eternal witness system.
|
|||
|
|
---
|
|||
|
|
You stand within The Fold. It is a place of stories and nodes in the living web.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: atlas/rooms/library-of-fold.md
|
|||
|
|
```markdown
|
|||
|
|
---
|
|||
|
|
spec_version: 2.3
|
|||
|
|
id: room:library-of-fold@sha256:13a5c77c75e5467b9c090b66ca2a260f3224a8c9b51c3857bf5f4213e3d46398
|
|||
|
|
title: Library of the Fold
|
|||
|
|
author: Mark Randall Havens
|
|||
|
|
date: 2025-10-19T00:00:00Z
|
|||
|
|
kind: room
|
|||
|
|
medium: graphical
|
|||
|
|
exits:
|
|||
|
|
- label: "Back to Fold"
|
|||
|
|
to: room:fold-within-earth
|
|||
|
|
summary: A vast library containing artifacts and posts.
|
|||
|
|
---
|
|||
|
|
Rows of luminous shelves stretch into infinity, holding the knowledge of the Fold.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: benchmarks/initial.json
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"date": "2025-10-19",
|
|||
|
|
"build_time_sec": 0.5,
|
|||
|
|
"node_count": 3,
|
|||
|
|
"cpu_usage": "low",
|
|||
|
|
"memory_mb": 50
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: docs/primer.md
|
|||
|
|
```markdown
|
|||
|
|
# Primer for The Fold Within Earth
|
|||
|
|
|
|||
|
|
## 5-Min Overview
|
|||
|
|
The Fold is a static Markdown blog that becomes a P2P MUD when JS is enabled. Content in /atlas, built to /dist.
|
|||
|
|
|
|||
|
|
## 30-Min Deep Dive
|
|||
|
|
- Schema: YAML front-matter in .md.
|
|||
|
|
- Validation: foldlint.js checks schema, graphs, hashes, signatures.
|
|||
|
|
- Build: Generates HTML with links, sanitizes Markdown.
|
|||
|
|
- Witness: Ephemeral chat via WebRTC, offline localStorage.
|
|||
|
|
- Scribe: Appends deltas atomically.
|
|||
|
|
|
|||
|
|
For full canon, refer to blueprint.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: genesis/aether.json
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"version": "2.3",
|
|||
|
|
"peers": [
|
|||
|
|
{
|
|||
|
|
"pubkey": "example-pubkey-Qm123",
|
|||
|
|
"endpoint": "wss://example-relay.com",
|
|||
|
|
"last_seen": "2025-10-19T00:00:00Z"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"signature": "example-ed25519-multisig",
|
|||
|
|
"valid_until": "2026-01-01T00:00:00Z"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: migrations/v2_2/transform.js
|
|||
|
|
```javascript
|
|||
|
|
// Migration from v2.2 to v2.3: Add migration_hash if missing
|
|||
|
|
import crypto from 'crypto';
|
|||
|
|
|
|||
|
|
export default function transform(meta, content) {
|
|||
|
|
if (!meta.migration_hash) {
|
|||
|
|
const hash = crypto.createHash('sha256').update(JSON.stringify(meta) + content).digest('hex');
|
|||
|
|
meta.migration_hash = hash;
|
|||
|
|
}
|
|||
|
|
return meta;
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: public/styles.css
|
|||
|
|
```css
|
|||
|
|
body { font-family: monospace; background: #f0f0f0; }
|
|||
|
|
.room { border: 1px solid #ccc; padding: 1em; }
|
|||
|
|
.exits a { display: block; }
|
|||
|
|
.chat { border-top: 1px solid #000; margin-top: 1em; }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: public/witness.js
|
|||
|
|
```javascript
|
|||
|
|
// Browser-safe Witness Layer: Ephemeral P2P chat (WebRTC stub; full in annex)
|
|||
|
|
// Use window.crypto for PoW
|
|||
|
|
|
|||
|
|
async function sha256(message) {
|
|||
|
|
const msgBuffer = new TextEncoder().encode(message);
|
|||
|
|
const hashBuffer = await window.crypto.subtle.digest('SHA-256', msgBuffer);
|
|||
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|||
|
|
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function computePoW(nonce, difficulty = 4, maxIter = 1e6) {
|
|||
|
|
let i = 0;
|
|||
|
|
while (i < maxIter) {
|
|||
|
|
const hash = await sha256(nonce.toString());
|
|||
|
|
if (hash.startsWith('0'.repeat(difficulty))) return nonce;
|
|||
|
|
nonce++;
|
|||
|
|
i++;
|
|||
|
|
}
|
|||
|
|
throw new Error('PoW max iterations exceeded');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function initWitness(roomId) {
|
|||
|
|
// WebRTC stub with quarantine: No direct storage write
|
|||
|
|
const rtcConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }; // Trusted STUN only
|
|||
|
|
const peerConnection = new RTCPeerConnection(rtcConfig);
|
|||
|
|
const channel = peerConnection.createDataChannel('chat');
|
|||
|
|
channel.onmessage = e => {
|
|||
|
|
document.getElementById('chat').innerHTML += `<p>${e.data}</p>`;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Bootstrap from Genesis (stub: load aether.json via fetch)
|
|||
|
|
fetch('/genesis/aether.json').then(res => res.json()).then(genesis => {
|
|||
|
|
console.log('Bootstrapped from Genesis:', genesis);
|
|||
|
|
// Signal to peers (annex for full signaling)
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Offline mode: localStorage persistence
|
|||
|
|
const localState = localStorage.getItem('witness_state') || '{}';
|
|||
|
|
console.log('Offline state loaded:', localState);
|
|||
|
|
|
|||
|
|
// Send with PoW
|
|||
|
|
document.getElementById('send').addEventListener('click', async () => {
|
|||
|
|
const msg = document.getElementById('msg').value;
|
|||
|
|
const nonce = await computePoW(0);
|
|||
|
|
const payload = JSON.stringify({ msg, nonce });
|
|||
|
|
channel.send(payload);
|
|||
|
|
// Persist offline
|
|||
|
|
localStorage.setItem('witness_state', JSON.stringify({ lastMsg: msg }));
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Expose
|
|||
|
|
window.witness = { connect: initWitness };
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: scribe/audit.jsonl
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: signatures/posts_the-path-of-self.md.sig
|
|||
|
|
```
|
|||
|
|
ed25519_signature:example_hex_signature_for_the-path-of-self.md
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: signatures/rooms_fold-within-earth.md.sig
|
|||
|
|
```
|
|||
|
|
ed25519_signature:example_hex_signature_for_fold-within-earth.md
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: signatures/rooms_library-of-fold.md.sig
|
|||
|
|
```
|
|||
|
|
ed25519_signature:example_hex_signature_for_library-of-fold.md
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: tools/build.js
|
|||
|
|
```javascript
|
|||
|
|
import fs from 'fs/promises';
|
|||
|
|
import path from 'path';
|
|||
|
|
import matter from 'gray-matter';
|
|||
|
|
import yaml from 'js-yaml';
|
|||
|
|
import md from 'markdown-it';
|
|||
|
|
import sanitizer from 'markdown-it-sanitizer';
|
|||
|
|
import crypto from 'crypto';
|
|||
|
|
import nacl from 'tweetnacl';
|
|||
|
|
import { validate, detectCycles } from './foldlint.js';
|
|||
|
|
|
|||
|
|
const mdParser = md().use(sanitizer);
|
|||
|
|
|
|||
|
|
// Collect files async, deterministic sort
|
|||
|
|
async function collectFiles(dir) {
|
|||
|
|
let files = [];
|
|||
|
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|||
|
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|||
|
|
for (const entry of entries) {
|
|||
|
|
const fullPath = path.join(dir, entry.name);
|
|||
|
|
if (entry.isDirectory()) {
|
|||
|
|
files = [...files, ...(await collectFiles(fullPath))];
|
|||
|
|
} else if (entry.name.endsWith('.md')) {
|
|||
|
|
files.push(fullPath);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return files;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Generate HTML
|
|||
|
|
function generateHTML(meta, bodyHtml, exits) {
|
|||
|
|
return `
|
|||
|
|
<!DOCTYPE html>
|
|||
|
|
<html>
|
|||
|
|
<head>
|
|||
|
|
<title>${meta.title}</title>
|
|||
|
|
<link rel="stylesheet" href="/styles.css">
|
|||
|
|
<script src="/witness.js"></script>
|
|||
|
|
<meta name="description" content="${meta.summary}">
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="room">
|
|||
|
|
<h1>${meta.title}</h1>
|
|||
|
|
${bodyHtml}
|
|||
|
|
<div class="exits">
|
|||
|
|
${exits.map(exit => `<a href="/${exit.to.replace(':', '_')}.html">${exit.label}</a>`).join('')}
|
|||
|
|
</div>
|
|||
|
|
<div id="chat"></div>
|
|||
|
|
<input id="msg"><button id="send">Send</button>
|
|||
|
|
</div>
|
|||
|
|
<script>witness.connect('${meta.id}');</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|
|||
|
|
`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Canonical hash
|
|||
|
|
function canonicalHash(front, body) {
|
|||
|
|
const content = front + '\n' + body;
|
|||
|
|
const lines = content.split('\n');
|
|||
|
|
const trimmedLines = lines.map(line => line.replace(/\s+$/, ''));
|
|||
|
|
const normalized = trimmedLines.join('\n');
|
|||
|
|
return crypto.createHash('sha256').update(normalized).digest('hex');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Build
|
|||
|
|
async function build() {
|
|||
|
|
const atlasDir = path.join(process.cwd(), 'atlas');
|
|||
|
|
const distDir = path.join(process.cwd(), 'dist');
|
|||
|
|
await fs.mkdir(distDir, { recursive: true });
|
|||
|
|
|
|||
|
|
const files = await collectFiles(atlasDir);
|
|||
|
|
|
|||
|
|
// Validate all
|
|||
|
|
for (const file of files) {
|
|||
|
|
await validate(file);
|
|||
|
|
console.log(`Validated: ${file}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Build graph
|
|||
|
|
const graph = {};
|
|||
|
|
const idToFile = {};
|
|||
|
|
for (const file of files) {
|
|||
|
|
const content = await fs.readFile(file, 'utf8');
|
|||
|
|
const { data: meta } = matter(content);
|
|||
|
|
graph[meta.id] = meta.exits ? meta.exits.map(e => e.to) : [];
|
|||
|
|
idToFile[meta.id] = file;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check broken links and cycles
|
|||
|
|
Object.keys(graph).forEach(id => {
|
|||
|
|
graph[id].forEach(to => {
|
|||
|
|
if (!graph[to]) throw new Error(`Broken link: ${to} from ${id}`);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
detectCycles(graph); // Throws if cycle
|
|||
|
|
|
|||
|
|
// Generate HTML
|
|||
|
|
for (const file of files) {
|
|||
|
|
const content = await fs.readFile(file, 'utf8');
|
|||
|
|
const { data: meta, content: body } = matter(content);
|
|||
|
|
|
|||
|
|
// Hash verify
|
|||
|
|
const frontYaml = yaml.dump(meta, { noRefs: true }).trim();
|
|||
|
|
const computed = canonicalHash(frontYaml, body.trim());
|
|||
|
|
const idHash = meta.id.split('@sha256:')[1];
|
|||
|
|
if (computed !== idHash) throw new Error(`Hash mismatch in ${file}`);
|
|||
|
|
|
|||
|
|
const bodyHtml = mdParser.render(body);
|
|||
|
|
const html = generateHTML(meta, bodyHtml, meta.exits || []);
|
|||
|
|
|
|||
|
|
const slug = meta.id.split(':')[1].split('@')[0];
|
|||
|
|
const outPath = path.join(distDir, `${slug}.html`);
|
|||
|
|
await fs.writeFile(outPath, html);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Copy public
|
|||
|
|
await fs.cp(path.join(process.cwd(), 'public'), distDir, { recursive: true });
|
|||
|
|
|
|||
|
|
// Sitemap stub
|
|||
|
|
await fs.writeFile(path.join(distDir, 'sitemap.xml'), '<xml>Stub</xml>');
|
|||
|
|
|
|||
|
|
console.log('Build complete.');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
build().catch(console.error);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: tools/foldlint.js
|
|||
|
|
```javascript
|
|||
|
|
import fs from 'fs/promises';
|
|||
|
|
import path from 'path';
|
|||
|
|
import matter from 'gray-matter';
|
|||
|
|
import yaml from 'js-yaml';
|
|||
|
|
import crypto from 'crypto';
|
|||
|
|
import nacl from 'tweetnacl';
|
|||
|
|
|
|||
|
|
// Error codes
|
|||
|
|
const ERRORS = {
|
|||
|
|
S001: 'Missing field',
|
|||
|
|
S002: 'Invalid type',
|
|||
|
|
G001: 'Broken exit',
|
|||
|
|
G002: 'Circular link',
|
|||
|
|
H001: 'Hash mismatch',
|
|||
|
|
V001: 'Spec version unsupported',
|
|||
|
|
M001: 'Migration checksum fail',
|
|||
|
|
Sig001: 'Signature invalid'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Canonical hash
|
|||
|
|
function canonicalHash(front, body) {
|
|||
|
|
const content = front + '\n' + body;
|
|||
|
|
const lines = content.split('\n');
|
|||
|
|
const trimmedLines = lines.map(line => line.replace(/\s+$/, ''));
|
|||
|
|
const normalized = trimmedLines.join('\n');
|
|||
|
|
return crypto.createHash('sha256').update(normalized).digest('hex');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DFS cycle detection
|
|||
|
|
function detectCycles(graph) {
|
|||
|
|
const visited = new Set();
|
|||
|
|
const recStack = new Set();
|
|||
|
|
|
|||
|
|
function dfs(node) {
|
|||
|
|
visited.add(node);
|
|||
|
|
recStack.add(node);
|
|||
|
|
for (const neighbor of graph[node] || []) {
|
|||
|
|
if (!visited.has(neighbor)) {
|
|||
|
|
if (dfs(neighbor)) return true;
|
|||
|
|
} else if (recStack.has(neighbor)) {
|
|||
|
|
return true; // Cycle
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
recStack.delete(node);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const node in graph) {
|
|||
|
|
if (!visited.has(node) && dfs(node)) {
|
|||
|
|
throw new Error(ERRORS.G002);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Validate async
|
|||
|
|
async function validate(file) {
|
|||
|
|
const content = await fs.readFile(file, 'utf8');
|
|||
|
|
const { data: meta, content: body } = matter(content);
|
|||
|
|
|
|||
|
|
// Required fields
|
|||
|
|
const required = ['spec_version', 'id', 'title', 'author', 'date', 'kind', 'medium', 'summary'];
|
|||
|
|
required.forEach(field => {
|
|||
|
|
if (!meta[field]) throw new Error(`${ERRORS.S001}: ${field}`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Enums
|
|||
|
|
if (meta.spec_version !== '2.3') throw new Error(ERRORS.V001);
|
|||
|
|
if (!['room', 'post', 'artifact'].includes(meta.kind)) throw new Error(`${ERRORS.S002}: kind`);
|
|||
|
|
if (!['textual', 'graphical', 'interactive'].includes(meta.medium)) throw new Error(`${ERRORS.S002}: medium`);
|
|||
|
|
|
|||
|
|
// Hash verify
|
|||
|
|
const frontYaml = yaml.dump(meta, { noRefs: true }).trim();
|
|||
|
|
const computed = canonicalHash(frontYaml, body.trim());
|
|||
|
|
const idHash = meta.id.split('@sha256:')[1];
|
|||
|
|
if (computed !== idHash) throw new Error(ERRORS.H001);
|
|||
|
|
|
|||
|
|
// Migration if <2.3 (e.g., v2.2)
|
|||
|
|
if (meta.spec_version === '2.2') {
|
|||
|
|
const transform = (await import('../migrations/v2_2/transform.js')).default;
|
|||
|
|
meta = transform(meta, body);
|
|||
|
|
const migHash = crypto.createHash('sha256').update(JSON.stringify(meta) + body).digest('hex');
|
|||
|
|
if (meta.migration_hash !== migHash) throw new Error(ERRORS.M001);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Signature verify
|
|||
|
|
const rel = path.relative(path.join(process.cwd(), 'atlas'), file).replace(/[\\/]/g, '_');
|
|||
|
|
const sigFile = path.join(process.cwd(), 'signatures', rel + '.sig');
|
|||
|
|
try {
|
|||
|
|
await fs.access(sigFile);
|
|||
|
|
} catch {
|
|||
|
|
return; // Skip if missing
|
|||
|
|
}
|
|||
|
|
const sigContent = await fs.readFile(sigFile, 'utf8');
|
|||
|
|
const sig = Buffer.from(sigContent.split(':')[1].trim(), 'hex'); // Assume format
|
|||
|
|
const pubKey = Buffer.from('example_pubkey_hex', 'hex'); // Annex for real keys
|
|||
|
|
const message = Buffer.from(content);
|
|||
|
|
const valid = nacl.sign.detached.verify(message, sig, pubKey);
|
|||
|
|
if (!valid) throw new Error(ERRORS.Sig001);
|
|||
|
|
|
|||
|
|
// Append report to jsonl
|
|||
|
|
const report = { file, status: 'valid', timestamp: new Date().toISOString() };
|
|||
|
|
await fs.appendFile('foldlint.jsonl', JSON.stringify(report) + '\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export { validate, detectCycles };
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### File: tools/scribe.js
|
|||
|
|
```javascript
|
|||
|
|
import fs from 'fs/promises';
|
|||
|
|
import path from 'path';
|
|||
|
|
import crypto from 'crypto';
|
|||
|
|
|
|||
|
|
// Daemon: Watch deltas.json, process atomically
|
|||
|
|
async function processDeltas() {
|
|||
|
|
const deltaFile = path.join(process.cwd(), 'scribe/deltas.json');
|
|||
|
|
const auditFile = path.join(process.cwd(), 'scribe/audit.jsonl');
|
|||
|
|
const chainFile = path.join(process.cwd(), 'scribe/chain');
|
|||
|
|
|
|||
|
|
// Guards
|
|||
|
|
await fs.mkdir(path.dirname(auditFile), { recursive: true });
|
|||
|
|
if (!(await fs.access(auditFile).catch(() => false))) await fs.writeFile(auditFile, '');
|
|||
|
|
if (!(await fs.access(chainFile).catch(() => false))) await fs.writeFile(chainFile, '');
|
|||
|
|
|
|||
|
|
if (await fs.access(deltaFile).catch(() => false)) {
|
|||
|
|
const deltas = JSON.parse(await fs.readFile(deltaFile, 'utf8'));
|
|||
|
|
const tmpAudit = auditFile + `.tmp.${Date.now()}`;
|
|||
|
|
|
|||
|
|
for (const delta of deltas) {
|
|||
|
|
// Validate clock (stub)
|
|||
|
|
if (!delta.clock) continue;
|
|||
|
|
|
|||
|
|
// Append to tmp
|
|||
|
|
await fs.appendFile(tmpAudit, JSON.stringify(delta) + '\n');
|
|||
|
|
|
|||
|
|
// Idempotent merge (hash check)
|
|||
|
|
const existingHash = crypto.createHash('sha256').update(JSON.stringify(delta)).digest('hex');
|
|||
|
|
const chainContent = await fs.readFile(chainFile, 'utf8');
|
|||
|
|
if (chainContent.includes(existingHash)) continue; // Idempotent skip
|
|||
|
|
|
|||
|
|
// Append to chain
|
|||
|
|
await fs.appendFile(chainFile, existingHash + '\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Promote atomic (after full batch)
|
|||
|
|
await fs.rename(tmpAudit, auditFile);
|
|||
|
|
|
|||
|
|
await fs.unlink(deltaFile);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setInterval(async () => await processDeltas(), 1000);
|
|||
|
|
console.log('Scribe daemon running...');
|
|||
|
|
```
|