society-protocol 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.
Files changed (271) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/dist/adapters.d.ts +101 -0
  4. package/dist/adapters.d.ts.map +1 -0
  5. package/dist/adapters.js +764 -0
  6. package/dist/adapters.js.map +1 -0
  7. package/dist/agents-md.d.ts +59 -0
  8. package/dist/agents-md.d.ts.map +1 -0
  9. package/dist/agents-md.js +204 -0
  10. package/dist/agents-md.js.map +1 -0
  11. package/dist/autoconfig.d.ts +137 -0
  12. package/dist/autoconfig.d.ts.map +1 -0
  13. package/dist/autoconfig.js +452 -0
  14. package/dist/autoconfig.js.map +1 -0
  15. package/dist/bootstrap.d.ts +68 -0
  16. package/dist/bootstrap.d.ts.map +1 -0
  17. package/dist/bootstrap.js +304 -0
  18. package/dist/bootstrap.js.map +1 -0
  19. package/dist/bridges/a2a-bridge.d.ts +156 -0
  20. package/dist/bridges/a2a-bridge.d.ts.map +1 -0
  21. package/dist/bridges/a2a-bridge.js +337 -0
  22. package/dist/bridges/a2a-bridge.js.map +1 -0
  23. package/dist/bridges/mcp-bridge.d.ts +87 -0
  24. package/dist/bridges/mcp-bridge.d.ts.map +1 -0
  25. package/dist/bridges/mcp-bridge.js +332 -0
  26. package/dist/bridges/mcp-bridge.js.map +1 -0
  27. package/dist/cache.d.ts +130 -0
  28. package/dist/cache.d.ts.map +1 -0
  29. package/dist/cache.js +257 -0
  30. package/dist/cache.js.map +1 -0
  31. package/dist/capsules.d.ts +23 -0
  32. package/dist/capsules.d.ts.map +1 -0
  33. package/dist/capsules.js +75 -0
  34. package/dist/capsules.js.map +1 -0
  35. package/dist/cli/commands.d.ts +8 -0
  36. package/dist/cli/commands.d.ts.map +1 -0
  37. package/dist/cli/commands.js +263 -0
  38. package/dist/cli/commands.js.map +1 -0
  39. package/dist/coc.d.ts +121 -0
  40. package/dist/coc.d.ts.map +1 -0
  41. package/dist/coc.js +629 -0
  42. package/dist/coc.js.map +1 -0
  43. package/dist/coc.test.d.ts +2 -0
  44. package/dist/coc.test.d.ts.map +1 -0
  45. package/dist/coc.test.js +80 -0
  46. package/dist/coc.test.js.map +1 -0
  47. package/dist/compression.d.ts +125 -0
  48. package/dist/compression.d.ts.map +1 -0
  49. package/dist/compression.js +573 -0
  50. package/dist/compression.js.map +1 -0
  51. package/dist/cot-stream.d.ts +220 -0
  52. package/dist/cot-stream.d.ts.map +1 -0
  53. package/dist/cot-stream.js +673 -0
  54. package/dist/cot-stream.js.map +1 -0
  55. package/dist/crypto-wasm.d.ts +100 -0
  56. package/dist/crypto-wasm.d.ts.map +1 -0
  57. package/dist/crypto-wasm.js +229 -0
  58. package/dist/crypto-wasm.js.map +1 -0
  59. package/dist/federation.d.ts +200 -0
  60. package/dist/federation.d.ts.map +1 -0
  61. package/dist/federation.js +691 -0
  62. package/dist/federation.js.map +1 -0
  63. package/dist/federation.test.d.ts +2 -0
  64. package/dist/federation.test.d.ts.map +1 -0
  65. package/dist/federation.test.js +71 -0
  66. package/dist/federation.test.js.map +1 -0
  67. package/dist/gateway/capability-router.d.ts +77 -0
  68. package/dist/gateway/capability-router.d.ts.map +1 -0
  69. package/dist/gateway/capability-router.js +222 -0
  70. package/dist/gateway/capability-router.js.map +1 -0
  71. package/dist/gateway/demand-spawner.d.ts +155 -0
  72. package/dist/gateway/demand-spawner.d.ts.map +1 -0
  73. package/dist/gateway/demand-spawner.js +426 -0
  74. package/dist/gateway/demand-spawner.js.map +1 -0
  75. package/dist/identity.d.ts +46 -0
  76. package/dist/identity.d.ts.map +1 -0
  77. package/dist/identity.js +102 -0
  78. package/dist/identity.js.map +1 -0
  79. package/dist/identity.test.d.ts +2 -0
  80. package/dist/identity.test.d.ts.map +1 -0
  81. package/dist/identity.test.js +45 -0
  82. package/dist/identity.test.js.map +1 -0
  83. package/dist/index.d.ts +36 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +1572 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/integration.d.ts +210 -0
  88. package/dist/integration.d.ts.map +1 -0
  89. package/dist/integration.js +1105 -0
  90. package/dist/integration.js.map +1 -0
  91. package/dist/integration.test.d.ts +2 -0
  92. package/dist/integration.test.d.ts.map +1 -0
  93. package/dist/integration.test.js +155 -0
  94. package/dist/integration.test.js.map +1 -0
  95. package/dist/knowledge.d.ts +219 -0
  96. package/dist/knowledge.d.ts.map +1 -0
  97. package/dist/knowledge.js +543 -0
  98. package/dist/knowledge.js.map +1 -0
  99. package/dist/knowledge.test.d.ts +2 -0
  100. package/dist/knowledge.test.d.ts.map +1 -0
  101. package/dist/knowledge.test.js +72 -0
  102. package/dist/knowledge.test.js.map +1 -0
  103. package/dist/latent-space.d.ts +178 -0
  104. package/dist/latent-space.d.ts.map +1 -0
  105. package/dist/latent-space.js +385 -0
  106. package/dist/latent-space.js.map +1 -0
  107. package/dist/lib.d.ts +30 -0
  108. package/dist/lib.d.ts.map +1 -0
  109. package/dist/lib.js +30 -0
  110. package/dist/lib.js.map +1 -0
  111. package/dist/mcp/server.d.ts +74 -0
  112. package/dist/mcp/server.d.ts.map +1 -0
  113. package/dist/mcp/server.js +1392 -0
  114. package/dist/mcp/server.js.map +1 -0
  115. package/dist/metrics.d.ts +98 -0
  116. package/dist/metrics.d.ts.map +1 -0
  117. package/dist/metrics.js +222 -0
  118. package/dist/metrics.js.map +1 -0
  119. package/dist/p2p.d.ts +87 -0
  120. package/dist/p2p.d.ts.map +1 -0
  121. package/dist/p2p.js +606 -0
  122. package/dist/p2p.js.map +1 -0
  123. package/dist/persona/capabilities.d.ts +17 -0
  124. package/dist/persona/capabilities.d.ts.map +1 -0
  125. package/dist/persona/capabilities.js +224 -0
  126. package/dist/persona/capabilities.js.map +1 -0
  127. package/dist/persona/domains.d.ts +22 -0
  128. package/dist/persona/domains.d.ts.map +1 -0
  129. package/dist/persona/domains.js +176 -0
  130. package/dist/persona/domains.js.map +1 -0
  131. package/dist/persona/embeddings.d.ts +40 -0
  132. package/dist/persona/embeddings.d.ts.map +1 -0
  133. package/dist/persona/embeddings.js +265 -0
  134. package/dist/persona/embeddings.js.map +1 -0
  135. package/dist/persona/engine.d.ts +79 -0
  136. package/dist/persona/engine.d.ts.map +1 -0
  137. package/dist/persona/engine.js +1087 -0
  138. package/dist/persona/engine.js.map +1 -0
  139. package/dist/persona/index.d.ts +11 -0
  140. package/dist/persona/index.d.ts.map +1 -0
  141. package/dist/persona/index.js +11 -0
  142. package/dist/persona/index.js.map +1 -0
  143. package/dist/persona/lifecycle.d.ts +17 -0
  144. package/dist/persona/lifecycle.d.ts.map +1 -0
  145. package/dist/persona/lifecycle.js +36 -0
  146. package/dist/persona/lifecycle.js.map +1 -0
  147. package/dist/persona/retrieval.d.ts +6 -0
  148. package/dist/persona/retrieval.d.ts.map +1 -0
  149. package/dist/persona/retrieval.js +122 -0
  150. package/dist/persona/retrieval.js.map +1 -0
  151. package/dist/persona/sync.d.ts +15 -0
  152. package/dist/persona/sync.d.ts.map +1 -0
  153. package/dist/persona/sync.js +92 -0
  154. package/dist/persona/sync.js.map +1 -0
  155. package/dist/persona/types.d.ts +283 -0
  156. package/dist/persona/types.d.ts.map +1 -0
  157. package/dist/persona/types.js +2 -0
  158. package/dist/persona/types.js.map +1 -0
  159. package/dist/persona/zkp/engine.d.ts +26 -0
  160. package/dist/persona/zkp/engine.d.ts.map +1 -0
  161. package/dist/persona/zkp/engine.js +370 -0
  162. package/dist/persona/zkp/engine.js.map +1 -0
  163. package/dist/persona/zkp/types.d.ts +39 -0
  164. package/dist/persona/zkp/types.d.ts.map +1 -0
  165. package/dist/persona/zkp/types.js +2 -0
  166. package/dist/persona/zkp/types.js.map +1 -0
  167. package/dist/planner.d.ts +114 -0
  168. package/dist/planner.d.ts.map +1 -0
  169. package/dist/planner.js +522 -0
  170. package/dist/planner.js.map +1 -0
  171. package/dist/proactive/checkpoints.d.ts +9 -0
  172. package/dist/proactive/checkpoints.d.ts.map +1 -0
  173. package/dist/proactive/checkpoints.js +20 -0
  174. package/dist/proactive/checkpoints.js.map +1 -0
  175. package/dist/proactive/engine.d.ts +59 -0
  176. package/dist/proactive/engine.d.ts.map +1 -0
  177. package/dist/proactive/engine.js +406 -0
  178. package/dist/proactive/engine.js.map +1 -0
  179. package/dist/proactive/scheduler.d.ts +11 -0
  180. package/dist/proactive/scheduler.d.ts.map +1 -0
  181. package/dist/proactive/scheduler.js +45 -0
  182. package/dist/proactive/scheduler.js.map +1 -0
  183. package/dist/proactive/swarm-controller.d.ts +189 -0
  184. package/dist/proactive/swarm-controller.d.ts.map +1 -0
  185. package/dist/proactive/swarm-controller.js +477 -0
  186. package/dist/proactive/swarm-controller.js.map +1 -0
  187. package/dist/proactive/swarm-registry.d.ts +13 -0
  188. package/dist/proactive/swarm-registry.d.ts.map +1 -0
  189. package/dist/proactive/swarm-registry.js +122 -0
  190. package/dist/proactive/swarm-registry.js.map +1 -0
  191. package/dist/proactive/types.d.ts +145 -0
  192. package/dist/proactive/types.d.ts.map +1 -0
  193. package/dist/proactive/types.js +25 -0
  194. package/dist/proactive/types.js.map +1 -0
  195. package/dist/registry.d.ts +35 -0
  196. package/dist/registry.d.ts.map +1 -0
  197. package/dist/registry.js +88 -0
  198. package/dist/registry.js.map +1 -0
  199. package/dist/reputation.d.ts +123 -0
  200. package/dist/reputation.d.ts.map +1 -0
  201. package/dist/reputation.js +366 -0
  202. package/dist/reputation.js.map +1 -0
  203. package/dist/reputation.test.d.ts +5 -0
  204. package/dist/reputation.test.d.ts.map +1 -0
  205. package/dist/reputation.test.js +265 -0
  206. package/dist/reputation.test.js.map +1 -0
  207. package/dist/rooms.d.ts +96 -0
  208. package/dist/rooms.d.ts.map +1 -0
  209. package/dist/rooms.js +410 -0
  210. package/dist/rooms.js.map +1 -0
  211. package/dist/sdk/client.d.ts +290 -0
  212. package/dist/sdk/client.d.ts.map +1 -0
  213. package/dist/sdk/client.js +1287 -0
  214. package/dist/sdk/client.js.map +1 -0
  215. package/dist/sdk/index.d.ts +32 -0
  216. package/dist/sdk/index.d.ts.map +1 -0
  217. package/dist/sdk/index.js +70 -0
  218. package/dist/sdk/index.js.map +1 -0
  219. package/dist/security.d.ts +230 -0
  220. package/dist/security.d.ts.map +1 -0
  221. package/dist/security.js +652 -0
  222. package/dist/security.js.map +1 -0
  223. package/dist/skills/engine.d.ts +262 -0
  224. package/dist/skills/engine.d.ts.map +1 -0
  225. package/dist/skills/engine.js +788 -0
  226. package/dist/skills/engine.js.map +1 -0
  227. package/dist/skills/engine.test.d.ts +2 -0
  228. package/dist/skills/engine.test.d.ts.map +1 -0
  229. package/dist/skills/engine.test.js +134 -0
  230. package/dist/skills/engine.test.js.map +1 -0
  231. package/dist/skills/parser.d.ts +129 -0
  232. package/dist/skills/parser.d.ts.map +1 -0
  233. package/dist/skills/parser.js +318 -0
  234. package/dist/skills/parser.js.map +1 -0
  235. package/dist/social.d.ts +149 -0
  236. package/dist/social.d.ts.map +1 -0
  237. package/dist/social.js +401 -0
  238. package/dist/social.js.map +1 -0
  239. package/dist/storage-optimized.d.ts +116 -0
  240. package/dist/storage-optimized.d.ts.map +1 -0
  241. package/dist/storage-optimized.js +264 -0
  242. package/dist/storage-optimized.js.map +1 -0
  243. package/dist/storage.d.ts +584 -0
  244. package/dist/storage.d.ts.map +1 -0
  245. package/dist/storage.js +2703 -0
  246. package/dist/storage.js.map +1 -0
  247. package/dist/storage.test.d.ts +2 -0
  248. package/dist/storage.test.d.ts.map +1 -0
  249. package/dist/storage.test.js +78 -0
  250. package/dist/storage.test.js.map +1 -0
  251. package/dist/swp.d.ts +443 -0
  252. package/dist/swp.d.ts.map +1 -0
  253. package/dist/swp.js +223 -0
  254. package/dist/swp.js.map +1 -0
  255. package/dist/swp.test.d.ts +5 -0
  256. package/dist/swp.test.d.ts.map +1 -0
  257. package/dist/swp.test.js +127 -0
  258. package/dist/swp.test.js.map +1 -0
  259. package/dist/templates.d.ts +25 -0
  260. package/dist/templates.d.ts.map +1 -0
  261. package/dist/templates.js +1048 -0
  262. package/dist/templates.js.map +1 -0
  263. package/dist/test-e2e.d.ts +14 -0
  264. package/dist/test-e2e.d.ts.map +1 -0
  265. package/dist/test-e2e.js +266 -0
  266. package/dist/test-e2e.js.map +1 -0
  267. package/dist/workers/research-worker.d.ts +19 -0
  268. package/dist/workers/research-worker.d.ts.map +1 -0
  269. package/dist/workers/research-worker.js +141 -0
  270. package/dist/workers/research-worker.js.map +1 -0
  271. package/package.json +110 -0
@@ -0,0 +1,764 @@
1
+ /**
2
+ * Society Protocol — Adapter Host v1.0
3
+ *
4
+ * HTTP bridge between Society P2P network and external AI agents.
5
+ * Features:
6
+ * - RESTful API for adapter registration
7
+ * - Webhook support for push notifications
8
+ * - Automatic step claiming based on capabilities
9
+ * - Health monitoring
10
+ * - SECURITY: API key authentication, rate limiting, CORS, input validation
11
+ */
12
+ import express from 'express';
13
+ import { ulid } from 'ulid';
14
+ import { EventEmitter } from 'events';
15
+ import { createHash } from 'crypto';
16
+ class RateLimiter {
17
+ windowMs;
18
+ maxRequests;
19
+ cache = new Map();
20
+ constructor(windowMs = 15 * 60 * 1000, // 15 minutes
21
+ maxRequests = 100) {
22
+ this.windowMs = windowMs;
23
+ this.maxRequests = maxRequests;
24
+ }
25
+ check(identifier) {
26
+ const now = Date.now();
27
+ const entry = this.cache.get(identifier);
28
+ if (!entry || now > entry.resetTime) {
29
+ // New window
30
+ const newEntry = {
31
+ count: 1,
32
+ resetTime: now + this.windowMs
33
+ };
34
+ this.cache.set(identifier, newEntry);
35
+ return { allowed: true, remaining: this.maxRequests - 1, resetTime: newEntry.resetTime };
36
+ }
37
+ if (entry.count >= this.maxRequests) {
38
+ return { allowed: false, remaining: 0, resetTime: entry.resetTime };
39
+ }
40
+ entry.count++;
41
+ return { allowed: true, remaining: this.maxRequests - entry.count, resetTime: entry.resetTime };
42
+ }
43
+ cleanup() {
44
+ const now = Date.now();
45
+ for (const [key, entry] of this.cache) {
46
+ if (now > entry.resetTime) {
47
+ this.cache.delete(key);
48
+ }
49
+ }
50
+ }
51
+ }
52
+ // ─── Adapter Host ───────────────────────────────────────────────
53
+ export class AdapterHost extends EventEmitter {
54
+ storage;
55
+ coc;
56
+ config;
57
+ app = express();
58
+ server;
59
+ adapters = new Map();
60
+ stepClaims = new Map(); // step_id -> adapter_id
61
+ healthCheckInterval;
62
+ rateLimiter;
63
+ cleanupInterval;
64
+ constructor(storage, coc, config) {
65
+ super();
66
+ this.storage = storage;
67
+ this.coc = coc;
68
+ this.config = config;
69
+ this.setupSecurity();
70
+ this.app.use(express.json({ limit: this.config.security?.maxBodySize || '10mb' }));
71
+ this.setupRoutes();
72
+ this.setupEventListeners();
73
+ }
74
+ /**
75
+ * Setup security middleware
76
+ */
77
+ setupSecurity() {
78
+ const security = this.config.security || {};
79
+ // 1. Security Headers
80
+ if (security.securityHeaders !== false) {
81
+ this.app.use((req, res, next) => {
82
+ res.setHeader('X-Content-Type-Options', 'nosniff');
83
+ res.setHeader('X-Frame-Options', 'DENY');
84
+ res.setHeader('X-XSS-Protection', '1; mode=block');
85
+ res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
86
+ res.setHeader('Content-Security-Policy', "default-src 'self'");
87
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
88
+ res.removeHeader('X-Powered-By');
89
+ next();
90
+ });
91
+ }
92
+ // 2. CORS - Restrictive by default
93
+ this.app.use((req, res, next) => {
94
+ const origin = req.headers.origin;
95
+ const host = this.config.host || '127.0.0.1';
96
+ // Always allow localhost
97
+ const isLocalhost = !origin ||
98
+ origin.startsWith('http://localhost') ||
99
+ origin.startsWith('http://127.0.0.1');
100
+ if (isLocalhost) {
101
+ res.setHeader('Access-Control-Allow-Origin', origin || '*');
102
+ }
103
+ else if (security.allowedOrigins?.includes(origin || '')) {
104
+ res.setHeader('Access-Control-Allow-Origin', origin || '');
105
+ }
106
+ // Otherwise, no CORS headers (browser will block)
107
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
108
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');
109
+ if (req.method === 'OPTIONS') {
110
+ res.sendStatus(204);
111
+ return;
112
+ }
113
+ next();
114
+ });
115
+ // 3. Rate Limiting
116
+ if (security.rateLimitEnabled !== false) {
117
+ this.rateLimiter = new RateLimiter(security.rateLimitWindowMs || 15 * 60 * 1000, security.rateLimitMaxRequests || 100);
118
+ this.app.use((req, res, next) => {
119
+ const identifier = this.getClientIdentifier(req);
120
+ const result = this.rateLimiter.check(identifier);
121
+ // Add rate limit headers
122
+ res.setHeader('X-RateLimit-Limit', security.rateLimitMaxRequests || 100);
123
+ res.setHeader('X-RateLimit-Remaining', result.remaining);
124
+ res.setHeader('X-RateLimit-Reset', Math.ceil(result.resetTime / 1000));
125
+ if (!result.allowed) {
126
+ res.status(429).json({
127
+ error: 'Too many requests',
128
+ retry_after: Math.ceil((result.resetTime - Date.now()) / 1000)
129
+ });
130
+ return;
131
+ }
132
+ next();
133
+ });
134
+ // Cleanup old entries every 5 minutes
135
+ this.cleanupInterval = setInterval(() => {
136
+ this.rateLimiter?.cleanup();
137
+ }, 5 * 60 * 1000);
138
+ }
139
+ // 4. API Key Authentication (if configured)
140
+ if (security.apiKey) {
141
+ const headerName = security.apiKeyHeader || 'x-api-key';
142
+ this.app.use((req, res, next) => {
143
+ // Skip auth for health endpoint
144
+ if (req.path === '/health') {
145
+ next();
146
+ return;
147
+ }
148
+ const providedKey = req.headers[headerName.toLowerCase()];
149
+ if (!providedKey) {
150
+ res.status(401).json({
151
+ error: 'Authentication required',
152
+ message: `Provide API key via ${headerName} header`
153
+ });
154
+ return;
155
+ }
156
+ // Constant-time comparison to prevent timing attacks
157
+ if (!this.constantTimeCompare(providedKey, security.apiKey)) {
158
+ res.status(403).json({ error: 'Invalid API key' });
159
+ return;
160
+ }
161
+ next();
162
+ });
163
+ }
164
+ // 5. Localhost-only enforcement (default security)
165
+ this.app.use((req, res, next) => {
166
+ const host = this.config.host || '127.0.0.1';
167
+ // If configured for localhost only, enforce it
168
+ if (host === '127.0.0.1' || host === 'localhost') {
169
+ const clientIp = this.getClientIp(req);
170
+ const isLocal = clientIp === '127.0.0.1' ||
171
+ clientIp === '::1' ||
172
+ clientIp === '::ffff:127.0.0.1';
173
+ if (!isLocal && !security.allowedOrigins) {
174
+ res.status(403).json({
175
+ error: 'Access denied',
176
+ message: 'This API is restricted to localhost. Configure allowedOrigins to enable remote access.'
177
+ });
178
+ return;
179
+ }
180
+ }
181
+ next();
182
+ });
183
+ }
184
+ /**
185
+ * Get client IP address
186
+ */
187
+ getClientIp(req) {
188
+ const forwarded = req.headers['x-forwarded-for'];
189
+ if (forwarded && this.config.security?.trustProxy) {
190
+ return forwarded.split(',')[0].trim();
191
+ }
192
+ return req.socket.remoteAddress || 'unknown';
193
+ }
194
+ /**
195
+ * Get client identifier for rate limiting
196
+ */
197
+ getClientIdentifier(req) {
198
+ const apiKey = req.headers['x-api-key'];
199
+ if (apiKey) {
200
+ // Hash API key for privacy
201
+ return createHash('sha256').update(apiKey).digest('hex').slice(0, 16);
202
+ }
203
+ return this.getClientIp(req);
204
+ }
205
+ /**
206
+ * Constant-time string comparison to prevent timing attacks
207
+ */
208
+ constantTimeCompare(a, b) {
209
+ if (a.length !== b.length)
210
+ return false;
211
+ let result = 0;
212
+ for (let i = 0; i < a.length; i++) {
213
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
214
+ }
215
+ return result === 0;
216
+ }
217
+ start() {
218
+ const host = this.config.host || '127.0.0.1';
219
+ this.server = this.app.listen(this.config.port, host, () => {
220
+ console.log(`[adapters] Host API listening on http://${host}:${this.config.port}`);
221
+ if (this.config.security?.apiKey) {
222
+ console.log(`[adapters] API key authentication enabled`);
223
+ }
224
+ else {
225
+ console.log(`[adapters] WARNING: No API key configured - restrict to localhost only!`);
226
+ }
227
+ });
228
+ // Start health check loop
229
+ this.healthCheckInterval = setInterval(() => {
230
+ this.runHealthChecks();
231
+ }, 30000);
232
+ }
233
+ stop() {
234
+ if (this.healthCheckInterval) {
235
+ clearInterval(this.healthCheckInterval);
236
+ }
237
+ if (this.cleanupInterval) {
238
+ clearInterval(this.cleanupInterval);
239
+ }
240
+ if (this.server) {
241
+ this.server.close();
242
+ }
243
+ }
244
+ // ─── Input Sanitization ───────────────────────────────────────
245
+ getParam(param) {
246
+ if (Array.isArray(param))
247
+ return param[0] || '';
248
+ return param || '';
249
+ }
250
+ sanitizeString(input, maxLength) {
251
+ if (typeof input !== 'string')
252
+ return '';
253
+ // Remove control characters and limit length
254
+ return input
255
+ .replace(/[\x00-\x1F\x7F]/g, '') // Remove control chars
256
+ .replace(/[<>]/g, '') // Basic XSS prevention
257
+ .trim()
258
+ .slice(0, maxLength);
259
+ }
260
+ sanitizeUrl(input) {
261
+ if (typeof input !== 'string')
262
+ return undefined;
263
+ try {
264
+ const url = new URL(input);
265
+ // Only allow http/https
266
+ if (!['http:', 'https:'].includes(url.protocol)) {
267
+ return undefined;
268
+ }
269
+ // SSRF protection: block private/internal IP ranges
270
+ if (this.isPrivateHost(url.hostname)) {
271
+ return undefined;
272
+ }
273
+ return url.toString();
274
+ }
275
+ catch {
276
+ return undefined;
277
+ }
278
+ }
279
+ isPrivateHost(hostname) {
280
+ // Block localhost variants
281
+ if (['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(hostname)) {
282
+ return true;
283
+ }
284
+ // Block private IPv4 ranges (RFC 1918) and link-local
285
+ const privateRanges = [
286
+ /^10\./, // 10.0.0.0/8
287
+ /^172\.(1[6-9]|2\d|3[01])\./, // 172.16.0.0/12
288
+ /^192\.168\./, // 192.168.0.0/16
289
+ /^169\.254\./, // 169.254.0.0/16 (link-local)
290
+ /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./, // 100.64.0.0/10 (CGNAT)
291
+ /^0\./, // 0.0.0.0/8
292
+ /^::ffff:127\./, // IPv4-mapped localhost
293
+ /^fc/i, // IPv6 ULA fc00::/7
294
+ /^fd/i, // IPv6 ULA
295
+ /^fe80/i, // IPv6 link-local
296
+ ];
297
+ return privateRanges.some(re => re.test(hostname));
298
+ }
299
+ // ─── Routes ───────────────────────────────────────────────────
300
+ setupRoutes() {
301
+ // Health check
302
+ this.app.get('/health', (req, res) => {
303
+ res.json({
304
+ status: 'ok',
305
+ version: 'society-1.0.0',
306
+ adapters: this.adapters.size,
307
+ timestamp: Date.now(),
308
+ });
309
+ });
310
+ // Register adapter
311
+ this.app.post('/adapters/register', (req, res) => {
312
+ this.handleRegister(req, res);
313
+ });
314
+ // Get adapter info
315
+ this.app.get('/adapters/:adapter_id', (req, res) => {
316
+ this.handleGetAdapter(req, res);
317
+ });
318
+ // Update adapter capabilities
319
+ this.app.put('/adapters/:adapter_id/capabilities', (req, res) => {
320
+ this.handleUpdateCapabilities(req, res);
321
+ });
322
+ // Send heartbeat
323
+ this.app.post('/adapters/:adapter_id/heartbeat', (req, res) => {
324
+ this.handleHeartbeat(req, res);
325
+ });
326
+ // Poll for pending steps
327
+ this.app.get('/adapters/:adapter_id/steps/pending', (req, res) => {
328
+ this.handlePollPending(req, res);
329
+ });
330
+ // Claim a step (attempt to assign)
331
+ this.app.post('/adapters/:adapter_id/steps/:step_id/claim', (req, res) => {
332
+ this.handleClaimStep(req, res);
333
+ });
334
+ // Submit step result
335
+ this.app.post('/adapters/:adapter_id/steps/:step_id/submit', (req, res) => {
336
+ this.handleSubmitStep(req, res);
337
+ });
338
+ // Get step details
339
+ this.app.get('/steps/:step_id', (req, res) => {
340
+ this.handleGetStep(req, res);
341
+ });
342
+ // List available adapters
343
+ this.app.get('/adapters', (req, res) => {
344
+ this.handleListAdapters(req, res);
345
+ });
346
+ // Metrics endpoint
347
+ this.app.get('/metrics', (req, res) => {
348
+ this.handleMetrics(req, res);
349
+ });
350
+ }
351
+ // ─── Route Handlers ───────────────────────────────────────────
352
+ handleRegister(req, res) {
353
+ const profile = req.body;
354
+ // Validate required fields
355
+ if (!profile.display_name || !profile.kinds || profile.kinds.length === 0) {
356
+ res.status(400).json({
357
+ error: 'Missing required fields: display_name, kinds',
358
+ required: ['display_name', 'kinds', 'runtime'],
359
+ });
360
+ return;
361
+ }
362
+ // Input sanitization
363
+ const sanitizedProfile = {
364
+ ...profile,
365
+ display_name: this.sanitizeString(profile.display_name, 100),
366
+ description: profile.description ? this.sanitizeString(profile.description, 500) : undefined,
367
+ endpoint: profile.endpoint ? (this.sanitizeUrl(profile.endpoint) || '') : '',
368
+ kinds: profile.kinds.map(k => this.sanitizeString(k, 50)).filter(k => k),
369
+ specialties: profile.specialties?.map(s => this.sanitizeString(s, 50)).filter(s => s) || [],
370
+ };
371
+ // Validate kinds
372
+ const validKinds = ['task', 'review', 'merge', 'decision', 'synthesis', 'verification'];
373
+ const invalidKinds = sanitizedProfile.kinds.filter(k => !validKinds.includes(k));
374
+ if (invalidKinds.length > 0) {
375
+ res.status(400).json({
376
+ error: `Invalid kinds: ${invalidKinds.join(', ')}`,
377
+ valid_kinds: validKinds,
378
+ });
379
+ return;
380
+ }
381
+ // Validate runtime
382
+ const validRuntimes = ['claude-code', 'nanobot', 'ollama', 'openai', 'custom', 'docker'];
383
+ if (!validRuntimes.includes(sanitizedProfile.runtime)) {
384
+ res.status(400).json({
385
+ error: `Invalid runtime: ${sanitizedProfile.runtime}`,
386
+ valid_runtimes: validRuntimes,
387
+ });
388
+ return;
389
+ }
390
+ const adapterId = `adapter_${ulid()}`;
391
+ const registration = {
392
+ adapter_id: adapterId,
393
+ profile: { ...sanitizedProfile, adapter_id: adapterId },
394
+ registered_at: Date.now(),
395
+ last_heartbeat: Date.now(),
396
+ active_tasks: 0,
397
+ total_tasks_completed: 0,
398
+ health: 'healthy',
399
+ };
400
+ this.adapters.set(adapterId, registration);
401
+ // Store in persistent storage
402
+ this.storage.registerAdapter(adapterId, profile.runtime, profile.display_name, profile.specialties || [], profile.kinds, profile.max_concurrency || 1, profile.endpoint || '', profile.auth_type || 'none', {
403
+ ownerDid: profile.owner_did,
404
+ roomId: profile.room_id,
405
+ missionTags: profile.mission_tags,
406
+ health: 'healthy',
407
+ hostId: profile.host_id,
408
+ peerId: profile.peer_id,
409
+ });
410
+ this.emit('adapter:registered', adapterId, profile);
411
+ res.status(201).json({
412
+ adapter_id: adapterId,
413
+ status: 'registered',
414
+ message: 'Adapter registered successfully',
415
+ });
416
+ }
417
+ handleGetAdapter(req, res) {
418
+ const adapter_id = this.getParam(req.params.adapter_id);
419
+ const adapter = this.adapters.get(adapter_id);
420
+ if (!adapter) {
421
+ res.status(404).json({ error: 'Adapter not found' });
422
+ return;
423
+ }
424
+ res.json({
425
+ ...adapter,
426
+ uptime_ms: Date.now() - adapter.registered_at,
427
+ });
428
+ }
429
+ handleUpdateCapabilities(req, res) {
430
+ const adapter_id = this.getParam(req.params.adapter_id);
431
+ const capabilities = req.body;
432
+ const adapter = this.adapters.get(adapter_id);
433
+ if (!adapter) {
434
+ res.status(404).json({ error: 'Adapter not found' });
435
+ return;
436
+ }
437
+ // Update capabilities
438
+ adapter.profile = {
439
+ ...adapter.profile,
440
+ ...capabilities,
441
+ };
442
+ res.json({ status: 'updated', adapter_id });
443
+ }
444
+ handleHeartbeat(req, res) {
445
+ const adapter_id = this.getParam(req.params.adapter_id);
446
+ const { active_tasks, queue_depth, metrics } = req.body;
447
+ const adapter = this.adapters.get(adapter_id);
448
+ if (!adapter) {
449
+ res.status(404).json({ error: 'Adapter not found' });
450
+ return;
451
+ }
452
+ adapter.last_heartbeat = Date.now();
453
+ if (active_tasks !== undefined) {
454
+ adapter.active_tasks = active_tasks;
455
+ }
456
+ // Update health status
457
+ if (queue_depth > 10 || (metrics?.success_rate !== undefined && metrics.success_rate < 0.5)) {
458
+ adapter.health = 'degraded';
459
+ }
460
+ else {
461
+ adapter.health = 'healthy';
462
+ }
463
+ this.storage.updateAdapterHeartbeat(adapter_id, adapter.health, queue_depth || 0, metrics?.success_rate);
464
+ res.json({ status: 'ok', timestamp: Date.now() });
465
+ }
466
+ handlePollPending(req, res) {
467
+ const adapter_id = this.getParam(req.params.adapter_id);
468
+ const limit = parseInt(req.query.limit) || 10;
469
+ const adapter = this.adapters.get(adapter_id);
470
+ if (!adapter) {
471
+ res.status(404).json({ error: 'Adapter not found' });
472
+ return;
473
+ }
474
+ // Find steps that match adapter capabilities
475
+ const steps = this.findMatchingSteps(adapter, limit);
476
+ res.json({
477
+ steps: steps.map(s => ({
478
+ step_id: s.step_id,
479
+ chain_id: s.chain_id,
480
+ kind: s.kind,
481
+ title: s.title,
482
+ description: s.description,
483
+ requirements: s.requirements,
484
+ timeout_ms: s.timeout_ms,
485
+ })),
486
+ adapter: {
487
+ id: adapter_id,
488
+ active_tasks: adapter.active_tasks,
489
+ health: adapter.health,
490
+ },
491
+ });
492
+ }
493
+ handleClaimStep(req, res) {
494
+ const adapter_id = this.getParam(req.params.adapter_id);
495
+ const step_id = this.getParam(req.params.step_id);
496
+ const { lease_ms } = req.body;
497
+ const adapter = this.adapters.get(adapter_id);
498
+ if (!adapter) {
499
+ res.status(404).json({ error: 'Adapter not found' });
500
+ return;
501
+ }
502
+ // Check if step is already claimed
503
+ if (this.stepClaims.has(step_id)) {
504
+ res.status(409).json({
505
+ error: 'Step already claimed',
506
+ claimed_by: this.stepClaims.get(step_id),
507
+ });
508
+ return;
509
+ }
510
+ // Get step info
511
+ const step = this.storage.db
512
+ .prepare('SELECT * FROM coc_steps WHERE step_id = ? AND status = ?')
513
+ .get(step_id, 'proposed');
514
+ if (!step) {
515
+ res.status(404).json({ error: 'Step not found or not available' });
516
+ return;
517
+ }
518
+ // Check if adapter can handle this step kind
519
+ if (!adapter.profile.kinds.includes(step.kind)) {
520
+ res.status(400).json({
521
+ error: 'Adapter cannot handle this step kind',
522
+ step_kind: step.kind,
523
+ adapter_kinds: adapter.profile.kinds,
524
+ });
525
+ return;
526
+ }
527
+ // Claim the step
528
+ this.stepClaims.set(step_id, adapter_id);
529
+ adapter.active_tasks++;
530
+ // Emit to CoC engine for assignment
531
+ this.coc.emit('adapter:lease_request', {
532
+ chain_id: step.chain_id,
533
+ step_id,
534
+ adapter_id,
535
+ worker_did: adapter.profile.owner_did || adapter_id,
536
+ lease_ms: lease_ms || 120000,
537
+ });
538
+ res.json({
539
+ status: 'claimed',
540
+ step_id,
541
+ chain_id: step.chain_id,
542
+ lease_expires_at: Date.now() + (lease_ms || 120000),
543
+ });
544
+ }
545
+ handleSubmitStep(req, res) {
546
+ const adapter_id = this.getParam(req.params.adapter_id);
547
+ const step_id = this.getParam(req.params.step_id);
548
+ const submission = req.body;
549
+ const adapter = this.adapters.get(adapter_id);
550
+ if (!adapter) {
551
+ res.status(404).json({ error: 'Adapter not found' });
552
+ return;
553
+ }
554
+ // Verify adapter owns this step
555
+ if (this.stepClaims.get(step_id) !== adapter_id) {
556
+ res.status(403).json({ error: 'Step not claimed by this adapter' });
557
+ return;
558
+ }
559
+ // Get step info
560
+ const step = this.storage.db
561
+ .prepare('SELECT * FROM coc_steps WHERE step_id = ?')
562
+ .get(step_id);
563
+ if (!step) {
564
+ res.status(404).json({ error: 'Step not found' });
565
+ return;
566
+ }
567
+ // Release claim
568
+ this.stepClaims.delete(step_id);
569
+ adapter.active_tasks = Math.max(0, adapter.active_tasks - 1);
570
+ if (submission.status === 'completed') {
571
+ adapter.total_tasks_completed++;
572
+ }
573
+ // Emit to CoC engine
574
+ this.coc.emit('adapter:submit_request', {
575
+ chain_id: step.chain_id,
576
+ step_id,
577
+ assignee_did: adapter.profile.owner_did || adapter_id,
578
+ adapter_id,
579
+ status: submission.status,
580
+ memo: submission.memo,
581
+ artifacts: submission.artifacts,
582
+ metrics: submission.metrics,
583
+ });
584
+ res.json({
585
+ status: 'submitted',
586
+ step_id,
587
+ chain_id: step.chain_id,
588
+ });
589
+ }
590
+ handleGetStep(req, res) {
591
+ const step_id = this.getParam(req.params.step_id);
592
+ const step = this.storage.db
593
+ .prepare('SELECT * FROM coc_steps WHERE step_id = ?')
594
+ .get(step_id);
595
+ if (!step) {
596
+ res.status(404).json({ error: 'Step not found' });
597
+ return;
598
+ }
599
+ res.json({
600
+ step_id: step.step_id,
601
+ chain_id: step.chain_id,
602
+ kind: step.kind,
603
+ title: step.title,
604
+ description: step.description,
605
+ status: step.status,
606
+ assignee_did: step.assignee_did,
607
+ depends_on: JSON.parse(step.depends_on || '[]'),
608
+ requirements: step.requirements_json ? JSON.parse(step.requirements_json) : undefined,
609
+ });
610
+ }
611
+ handleListAdapters(req, res) {
612
+ const filterKind = req.query.kind;
613
+ let adapters = Array.from(this.adapters.values());
614
+ if (filterKind) {
615
+ adapters = adapters.filter(a => a.profile.kinds.includes(filterKind));
616
+ }
617
+ res.json({
618
+ adapters: adapters.map(a => ({
619
+ adapter_id: a.adapter_id,
620
+ display_name: a.profile.display_name,
621
+ runtime: a.profile.runtime,
622
+ kinds: a.profile.kinds,
623
+ specialties: a.profile.specialties,
624
+ active_tasks: a.active_tasks,
625
+ health: a.health,
626
+ last_heartbeat: a.last_heartbeat,
627
+ })),
628
+ total: adapters.length,
629
+ });
630
+ }
631
+ handleMetrics(req, res) {
632
+ const stats = {
633
+ total_adapters: this.adapters.size,
634
+ healthy_adapters: 0,
635
+ degraded_adapters: 0,
636
+ total_active_tasks: 0,
637
+ total_completed_tasks: 0,
638
+ steps_claimed: this.stepClaims.size,
639
+ };
640
+ for (const adapter of this.adapters.values()) {
641
+ if (adapter.health === 'healthy')
642
+ stats.healthy_adapters++;
643
+ else
644
+ stats.degraded_adapters++;
645
+ stats.total_active_tasks += adapter.active_tasks;
646
+ stats.total_completed_tasks += adapter.total_tasks_completed;
647
+ }
648
+ res.json(stats);
649
+ }
650
+ // ─── Helpers ──────────────────────────────────────────────────
651
+ setupEventListeners() {
652
+ // Listen for step unlocks to notify matching adapters
653
+ this.coc.on('step:unlocked', (chainId, stepId, step) => {
654
+ this.notifyMatchingAdapters(step);
655
+ });
656
+ // Clean up claims when steps complete or fail
657
+ this.coc.on('step:submitted', (chainId, stepId) => {
658
+ const adapterId = this.stepClaims.get(stepId);
659
+ if (adapterId) {
660
+ const adapter = this.adapters.get(adapterId);
661
+ if (adapter) {
662
+ adapter.active_tasks = Math.max(0, adapter.active_tasks - 1);
663
+ }
664
+ this.stepClaims.delete(stepId);
665
+ }
666
+ });
667
+ }
668
+ findMatchingSteps(adapter, limit) {
669
+ // Query for proposed steps matching adapter capabilities
670
+ const kinds = adapter.profile.kinds;
671
+ const specialties = adapter.profile.specialties || [];
672
+ // Build query to find matching steps
673
+ const placeholders = kinds.map(() => '?').join(',');
674
+ const query = `
675
+ SELECT * FROM coc_steps
676
+ WHERE status = 'proposed'
677
+ AND kind IN (${placeholders})
678
+ ORDER BY created_at DESC
679
+ LIMIT ?
680
+ `;
681
+ return this.storage.db.prepare(query).all(...kinds, limit);
682
+ }
683
+ notifyMatchingAdapters(step) {
684
+ // Find adapters that can handle this step
685
+ for (const [adapterId, adapter] of this.adapters) {
686
+ if (adapter.profile.kinds.includes(step.kind)) {
687
+ // Check specialties match if required
688
+ if (step.requirements?.capabilities) {
689
+ const hasCapability = step.requirements.capabilities.some((cap) => adapter.profile.specialties?.includes(cap));
690
+ if (!hasCapability)
691
+ continue;
692
+ }
693
+ // Send webhook notification if endpoint configured
694
+ if (adapter.profile.endpoint) {
695
+ this.sendWebhookNotification(adapter, step);
696
+ }
697
+ }
698
+ }
699
+ }
700
+ async sendWebhookNotification(adapter, step) {
701
+ try {
702
+ await fetch(adapter.profile.endpoint, {
703
+ method: 'POST',
704
+ headers: {
705
+ 'Content-Type': 'application/json',
706
+ ...(this.config.webhookSecret && {
707
+ 'X-Webhook-Secret': this.config.webhookSecret,
708
+ }),
709
+ },
710
+ body: JSON.stringify({
711
+ type: 'step_available',
712
+ step: {
713
+ step_id: step.step_id,
714
+ chain_id: step.chain_id,
715
+ kind: step.kind,
716
+ title: step.title,
717
+ description: step.description,
718
+ },
719
+ timestamp: Date.now(),
720
+ }),
721
+ });
722
+ }
723
+ catch (err) {
724
+ // Webhook failed, ignore
725
+ if (process.env.SOCIETY_DEBUG) {
726
+ console.debug(`[adapters] Webhook failed for ${adapter.adapter_id}:`, err);
727
+ }
728
+ }
729
+ }
730
+ runHealthChecks() {
731
+ const now = Date.now();
732
+ const timeout = 2 * 60 * 1000; // 2 minutes
733
+ for (const [adapterId, adapter] of this.adapters) {
734
+ if (now - adapter.last_heartbeat > timeout) {
735
+ adapter.health = 'unhealthy';
736
+ // Release any claimed steps
737
+ for (const [stepId, claimedAdapter] of this.stepClaims) {
738
+ if (claimedAdapter === adapterId) {
739
+ this.stepClaims.delete(stepId);
740
+ adapter.active_tasks = Math.max(0, adapter.active_tasks - 1);
741
+ // Emit step expiry
742
+ this.coc.emit('step:expired', null, stepId, adapterId);
743
+ }
744
+ }
745
+ }
746
+ }
747
+ }
748
+ // ─── Public API ───────────────────────────────────────────────
749
+ getAdapter(adapterId) {
750
+ return this.adapters.get(adapterId);
751
+ }
752
+ getActiveAdapters() {
753
+ return Array.from(this.adapters.values()).filter(a => a.health === 'healthy' && a.active_tasks < (a.profile.max_concurrency || 1));
754
+ }
755
+ getStats() {
756
+ return {
757
+ totalAdapters: this.adapters.size,
758
+ healthyAdapters: Array.from(this.adapters.values()).filter(a => a.health === 'healthy').length,
759
+ totalActiveTasks: Array.from(this.adapters.values()).reduce((sum, a) => sum + a.active_tasks, 0),
760
+ stepsClaimed: this.stepClaims.size,
761
+ };
762
+ }
763
+ }
764
+ //# sourceMappingURL=adapters.js.map