@swanlabs/veil-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -0
- package/VEIL_CORE_SPEC_v1.md +364 -0
- package/package.json +36 -0
- package/server.ts +302 -0
- package/veil-runtime.ts +414 -0
- package/veil-sdk.ts +311 -0
package/server.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VEIL CORE Server v1.0.0
|
|
3
|
+
* Swan Labs — Ambient Web Entity Runtime
|
|
4
|
+
*
|
|
5
|
+
* Runs the entity clock. Serves visitors. Streams live state.
|
|
6
|
+
* Deploy on any Node.js host — Fly.io, Railway, your own VPS.
|
|
7
|
+
* No cloud vendor lock-in. The protocol is yours.
|
|
8
|
+
*
|
|
9
|
+
* Install: npm install express ws cors
|
|
10
|
+
* Run: npx ts-node server.ts
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import express, { Request, Response, NextFunction } from "express";
|
|
14
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
15
|
+
import cors from "cors";
|
|
16
|
+
import http from "http";
|
|
17
|
+
import {
|
|
18
|
+
createEntity,
|
|
19
|
+
createRelationship,
|
|
20
|
+
processVisit,
|
|
21
|
+
recordDwell,
|
|
22
|
+
tickEntity,
|
|
23
|
+
deriveMorphology,
|
|
24
|
+
deriveVID,
|
|
25
|
+
formatClockAge,
|
|
26
|
+
VEILEntity,
|
|
27
|
+
VEILRelationship,
|
|
28
|
+
} from "./veil-runtime.js";
|
|
29
|
+
|
|
30
|
+
// ─── In-memory store (replace with DB in production) ─────────────────────────
|
|
31
|
+
|
|
32
|
+
const entities = new Map<string, VEILEntity>();
|
|
33
|
+
const relationships = new Map<string, Map<string, VEILRelationship>>(); // entityId → vid → rel
|
|
34
|
+
const streamClients = new Map<string, Set<WebSocket>>(); // entityId → clients
|
|
35
|
+
|
|
36
|
+
// ─── Bootstrap: create the first entity (VEIL itself) ─────────────────────────
|
|
37
|
+
|
|
38
|
+
const VEIL_ENTITY = createEntity("VEIL", {
|
|
39
|
+
warmth: 0.35,
|
|
40
|
+
density: 0.2,
|
|
41
|
+
tension: 0.4,
|
|
42
|
+
});
|
|
43
|
+
entities.set(VEIL_ENTITY.id, VEIL_ENTITY);
|
|
44
|
+
relationships.set(VEIL_ENTITY.id, new Map());
|
|
45
|
+
console.log(`[VEIL CORE] Entity bootstrapped: ${VEIL_ENTITY.id}`);
|
|
46
|
+
console.log(`[VEIL CORE] AWE-001 online. Clock age: 0m`);
|
|
47
|
+
|
|
48
|
+
// ─── Entity Clock Scheduler ───────────────────────────────────────────────────
|
|
49
|
+
// This is what makes entities alive between visits.
|
|
50
|
+
// It runs regardless of whether anyone is connected.
|
|
51
|
+
|
|
52
|
+
setInterval(() => {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
|
|
55
|
+
entities.forEach((entity, entityId) => {
|
|
56
|
+
const rels = Array.from(relationships.get(entityId)?.values() ?? []);
|
|
57
|
+
const { entity: ticked, relationships: tickedRels } = tickEntity(entity, rels);
|
|
58
|
+
|
|
59
|
+
// Persist updated entity
|
|
60
|
+
entities.set(entityId, ticked);
|
|
61
|
+
|
|
62
|
+
// Persist updated relationships
|
|
63
|
+
const relMap = relationships.get(entityId);
|
|
64
|
+
if (relMap) {
|
|
65
|
+
tickedRels.forEach(rel => relMap.set(rel.vid, rel));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Broadcast tick to all connected stream clients
|
|
69
|
+
const clients = streamClients.get(entityId);
|
|
70
|
+
if (clients && clients.size > 0) {
|
|
71
|
+
const message = JSON.stringify({
|
|
72
|
+
type: "tick",
|
|
73
|
+
entity_id: entityId,
|
|
74
|
+
timestamp: now,
|
|
75
|
+
payload: {
|
|
76
|
+
clock_age: ticked.clock_age,
|
|
77
|
+
clock_age_display: formatClockAge(ticked.clock_age),
|
|
78
|
+
axes: ticked.axes,
|
|
79
|
+
phase: ticked.phase,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
clients.forEach(client => {
|
|
83
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
84
|
+
client.send(message);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}, 60_000); // Tick every minute
|
|
90
|
+
|
|
91
|
+
// ─── Express App ──────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
const app = express();
|
|
94
|
+
app.use(cors());
|
|
95
|
+
app.use(express.json());
|
|
96
|
+
|
|
97
|
+
// Auth middleware (simplified — production uses JWT)
|
|
98
|
+
function requireAuth(req: Request, res: Response, next: NextFunction) {
|
|
99
|
+
const key = req.headers["x-veil-admin-key"];
|
|
100
|
+
if (key !== process.env.VEIL_ADMIN_KEY) {
|
|
101
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
102
|
+
}
|
|
103
|
+
next();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Entity Endpoints ─────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
// GET /v1/entity/:id — fetch entity state
|
|
109
|
+
app.get("/v1/entity/:id", (req, res) => {
|
|
110
|
+
const entity = entities.get(req.params.id);
|
|
111
|
+
if (!entity) return res.status(404).json({ error: "Entity not found" });
|
|
112
|
+
res.json({
|
|
113
|
+
entity,
|
|
114
|
+
clock_age_display: formatClockAge(entity.clock_age),
|
|
115
|
+
relationship_count: relationships.get(entity.id)?.size ?? 0,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// GET /v1/entity/:id/morphology — morphology for anonymous visitor
|
|
120
|
+
app.get("/v1/entity/:id/morphology", (req, res) => {
|
|
121
|
+
const entity = entities.get(req.params.id);
|
|
122
|
+
if (!entity) return res.status(404).json({ error: "Entity not found" });
|
|
123
|
+
const vid = req.query.vid as string;
|
|
124
|
+
const rel = vid ? relationships.get(entity.id)?.get(vid) : null;
|
|
125
|
+
const tempRel = rel ?? createRelationship(entity.id, "anonymous");
|
|
126
|
+
res.json({ morphology: deriveMorphology(entity, tempRel) });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// POST /v1/entity/:id/visit — record a visit
|
|
130
|
+
app.post("/v1/entity/:id/visit", (req, res) => {
|
|
131
|
+
const entity = entities.get(req.params.id);
|
|
132
|
+
if (!entity) return res.status(404).json({ error: "Entity not found" });
|
|
133
|
+
|
|
134
|
+
const { vid: rawVid, fingerprint_inputs, metadata } = req.body;
|
|
135
|
+
|
|
136
|
+
// Derive VID if not provided
|
|
137
|
+
const vid = rawVid ?? deriveVID(fingerprint_inputs ?? ["anonymous"]);
|
|
138
|
+
|
|
139
|
+
const relMap = relationships.get(entity.id) ?? new Map();
|
|
140
|
+
const existingRel = relMap.get(vid) ?? null;
|
|
141
|
+
|
|
142
|
+
const result = processVisit(entity, existingRel, vid);
|
|
143
|
+
|
|
144
|
+
// Persist
|
|
145
|
+
entities.set(entity.id, result.entity);
|
|
146
|
+
relMap.set(vid, result.relationship);
|
|
147
|
+
relationships.set(entity.id, relMap);
|
|
148
|
+
|
|
149
|
+
// Broadcast visitor join to stream clients
|
|
150
|
+
broadcastToEntity(entity.id, {
|
|
151
|
+
type: "visitor_join",
|
|
152
|
+
entity_id: entity.id,
|
|
153
|
+
timestamp: Date.now(),
|
|
154
|
+
payload: { vid, phase: result.relationship.phase, is_returning: result.is_returning },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
res.json({
|
|
158
|
+
...result,
|
|
159
|
+
vid,
|
|
160
|
+
clock_age_display: formatClockAge(result.entity.clock_age),
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// POST /v1/entity/:id/dwell — record dwell time
|
|
165
|
+
app.post("/v1/entity/:id/dwell", (req, res) => {
|
|
166
|
+
const entity = entities.get(req.params.id);
|
|
167
|
+
if (!entity) return res.status(404).json({ error: "Entity not found" });
|
|
168
|
+
|
|
169
|
+
const { vid, dwell_seconds } = req.body;
|
|
170
|
+
if (!vid || !dwell_seconds) return res.status(400).json({ error: "vid and dwell_seconds required" });
|
|
171
|
+
|
|
172
|
+
const relMap = relationships.get(entity.id);
|
|
173
|
+
const rel = relMap?.get(vid);
|
|
174
|
+
if (!rel) return res.status(404).json({ error: "Relationship not found" });
|
|
175
|
+
|
|
176
|
+
const updated = recordDwell(rel, dwell_seconds);
|
|
177
|
+
relMap!.set(vid, updated);
|
|
178
|
+
|
|
179
|
+
res.json({ relationship: updated, morphology: deriveMorphology(entity, updated) });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// GET /v1/relationship/:entityId/:vid
|
|
183
|
+
app.get("/v1/relationship/:entityId/:vid", (req, res) => {
|
|
184
|
+
const rel = relationships.get(req.params.entityId)?.get(req.params.vid);
|
|
185
|
+
if (!rel) return res.status(404).json({ error: "Relationship not found" });
|
|
186
|
+
const entity = entities.get(req.params.entityId);
|
|
187
|
+
if (!entity) return res.status(404).json({ error: "Entity not found" });
|
|
188
|
+
res.json({ relationship: rel, morphology: deriveMorphology(entity, rel) });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// POST /v1/entity — create new entity (admin only)
|
|
192
|
+
app.post("/v1/entity", requireAuth, (req, res) => {
|
|
193
|
+
const { name, axes } = req.body;
|
|
194
|
+
if (!name) return res.status(400).json({ error: "name required" });
|
|
195
|
+
const entity = createEntity(name, axes);
|
|
196
|
+
entities.set(entity.id, entity);
|
|
197
|
+
relationships.set(entity.id, new Map());
|
|
198
|
+
console.log(`[VEIL CORE] New entity created: ${entity.id} (${name})`);
|
|
199
|
+
res.status(201).json({ entity });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// GET /v1/entities — list all entities (admin only)
|
|
203
|
+
app.get("/v1/entities", requireAuth, (req, res) => {
|
|
204
|
+
const list = Array.from(entities.values()).map(e => ({
|
|
205
|
+
id: e.id,
|
|
206
|
+
name: e.name,
|
|
207
|
+
clock_age: e.clock_age,
|
|
208
|
+
clock_age_display: formatClockAge(e.clock_age),
|
|
209
|
+
visitor_count: e.visitor_count,
|
|
210
|
+
phase: e.phase,
|
|
211
|
+
relationship_count: relationships.get(e.id)?.size ?? 0,
|
|
212
|
+
}));
|
|
213
|
+
res.json({ entities: list, count: list.length });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// GET /v1/health
|
|
217
|
+
app.get("/v1/health", (req, res) => {
|
|
218
|
+
const entity = entities.get(VEIL_ENTITY.id);
|
|
219
|
+
res.json({
|
|
220
|
+
status: "alive",
|
|
221
|
+
entity_count: entities.size,
|
|
222
|
+
total_relationships: Array.from(relationships.values()).reduce((s, m) => s + m.size, 0),
|
|
223
|
+
primary_entity_age: entity ? formatClockAge(entity.clock_age) : "unknown",
|
|
224
|
+
protocol: "VEIL CORE v1.0.0",
|
|
225
|
+
origin: "Swan Labs",
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ─── HTTP + WebSocket Server ──────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
const server = http.createServer(app);
|
|
232
|
+
const wss = new WebSocketServer({ server, path: "/v1/stream" });
|
|
233
|
+
|
|
234
|
+
wss.on("connection", (ws, req) => {
|
|
235
|
+
// Parse entity ID from query string: /v1/stream?entity_id=...
|
|
236
|
+
const url = new URL(req.url ?? "", `http://${req.headers.host}`);
|
|
237
|
+
const entityId = url.searchParams.get("entity_id");
|
|
238
|
+
if (!entityId || !entities.has(entityId)) {
|
|
239
|
+
ws.close(1008, "Invalid entity_id");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Register client
|
|
244
|
+
if (!streamClients.has(entityId)) streamClients.set(entityId, new Set());
|
|
245
|
+
streamClients.get(entityId)!.add(ws);
|
|
246
|
+
|
|
247
|
+
console.log(`[VEIL SYNC] Stream client connected to ${entityId}. Total: ${streamClients.get(entityId)!.size}`);
|
|
248
|
+
|
|
249
|
+
// Send current state immediately
|
|
250
|
+
const entity = entities.get(entityId)!;
|
|
251
|
+
ws.send(JSON.stringify({
|
|
252
|
+
type: "connected",
|
|
253
|
+
entity_id: entityId,
|
|
254
|
+
timestamp: Date.now(),
|
|
255
|
+
payload: {
|
|
256
|
+
clock_age: entity.clock_age,
|
|
257
|
+
clock_age_display: formatClockAge(entity.clock_age),
|
|
258
|
+
axes: entity.axes,
|
|
259
|
+
phase: entity.phase,
|
|
260
|
+
visitor_count: entity.visitor_count,
|
|
261
|
+
},
|
|
262
|
+
}));
|
|
263
|
+
|
|
264
|
+
ws.on("close", () => {
|
|
265
|
+
streamClients.get(entityId)?.delete(ws);
|
|
266
|
+
console.log(`[VEIL SYNC] Stream client disconnected from ${entityId}`);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
ws.on("error", (err) => {
|
|
270
|
+
console.error(`[VEIL SYNC] Stream error:`, err.message);
|
|
271
|
+
streamClients.get(entityId)?.delete(ws);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
function broadcastToEntity(entityId: string, message: object) {
|
|
276
|
+
const clients = streamClients.get(entityId);
|
|
277
|
+
if (!clients) return;
|
|
278
|
+
const payload = JSON.stringify(message);
|
|
279
|
+
clients.forEach(client => {
|
|
280
|
+
if (client.readyState === WebSocket.OPEN) client.send(payload);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
const PORT = process.env.PORT ?? 3000;
|
|
287
|
+
server.listen(PORT, () => {
|
|
288
|
+
console.log(`\n╔════════════════════════════════════════╗`);
|
|
289
|
+
console.log(`║ VEIL CORE Runtime v1.0.0 ║`);
|
|
290
|
+
console.log(`║ Swan Labs — Ambient Web Entity Engine ║`);
|
|
291
|
+
console.log(`╚════════════════════════════════════════╝`);
|
|
292
|
+
console.log(`\nRunning on http://localhost:${PORT}`);
|
|
293
|
+
console.log(`Primary entity: ${VEIL_ENTITY.id}`);
|
|
294
|
+
console.log(`Protocol: VEIL CORE v1.0.0`);
|
|
295
|
+
console.log(`\nEndpoints:`);
|
|
296
|
+
console.log(` GET /v1/health`);
|
|
297
|
+
console.log(` GET /v1/entity/:id`);
|
|
298
|
+
console.log(` POST /v1/entity/:id/visit`);
|
|
299
|
+
console.log(` POST /v1/entity/:id/dwell`);
|
|
300
|
+
console.log(` WS /v1/stream?entity_id=:id`);
|
|
301
|
+
console.log(`\nThe entity is alive. Clock ticking.\n`);
|
|
302
|
+
});
|