mcard-js 2.1.49 → 2.1.51
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/dist/CardCollection-EMSBVZP3.js +10 -0
- package/dist/CardCollection-KQWR4PCV.js +10 -0
- package/dist/CardCollection-ORGE2XBG.js +10 -0
- package/dist/EngineRegistry-ABZXHZWO.js +17 -0
- package/dist/EngineRegistry-EIOT4MUZ.js +17 -0
- package/dist/EngineRegistry-IQ6EVO72.js +17 -0
- package/dist/EngineRegistry-PHRFXEOE.js +17 -0
- package/dist/IndexedDBEngine-EWA3SLAO.js +12 -0
- package/dist/IndexedDBEngine-FXAD42F3.js +12 -0
- package/dist/IndexedDBEngine-RD4447IS.js +12 -0
- package/dist/LLMRuntime-ARUWOX52.js +17 -0
- package/dist/LLMRuntime-C3XCO7WF.js +17 -0
- package/dist/LLMRuntime-CQ7X43QR.js +17 -0
- package/dist/LLMRuntime-PD45COKE.js +17 -0
- package/dist/LLMRuntime-QOUMLT33.js +17 -0
- package/dist/LLMRuntime-SZNLTHD7.js +17 -0
- package/dist/LLMRuntime-TVJGK2BG.js +17 -0
- package/dist/LambdaRuntime-25GMEJCU.js +19 -0
- package/dist/LambdaRuntime-7KQUMHPI.js +19 -0
- package/dist/LambdaRuntime-DRT7ODPC.js +19 -0
- package/dist/LambdaRuntime-HSREEYQG.js +19 -0
- package/dist/LambdaRuntime-IH7NVG6Z.js +19 -0
- package/dist/LambdaRuntime-MPG27FM2.js +19 -0
- package/dist/LambdaRuntime-ODSWIMNM.js +19 -0
- package/dist/LambdaRuntime-PHGRZYAW.js +19 -0
- package/dist/LambdaRuntime-QOEYR37L.js +19 -0
- package/dist/LambdaRuntime-RT33TFN2.js +19 -0
- package/dist/LambdaRuntime-W6TQBP5O.js +19 -0
- package/dist/Loader-35WSUC53.js +14 -0
- package/dist/Loader-STS3G4OQ.js +16 -0
- package/dist/Loader-W22AEM6F.js +12 -0
- package/dist/Loader-YBPWP43S.js +12 -0
- package/dist/Loader-ZYSS7B4D.js +12 -0
- package/dist/NetworkRuntime-KR2QITXV.js +987 -0
- package/dist/NetworkRuntime-S6V2CMZV.js +1575 -0
- package/dist/OllamaProvider-2ANW6EB2.js +9 -0
- package/dist/OllamaProvider-5QFJKYAC.js +9 -0
- package/dist/OllamaProvider-6QXJGR7V.js +9 -0
- package/dist/OllamaProvider-ABEEFX7M.js +9 -0
- package/dist/OllamaProvider-Z2CGY5LY.js +9 -0
- package/dist/VCard-225X42W7.js +25 -0
- package/dist/chunk-2APJYBH4.js +368 -0
- package/dist/chunk-4DFTWDRB.js +497 -0
- package/dist/chunk-4PBRTFSY.js +112 -0
- package/dist/chunk-4T3H25AP.js +299 -0
- package/dist/chunk-5DFXPIRL.js +42 -0
- package/dist/chunk-5HRZV4R3.js +217 -0
- package/dist/chunk-6ZRJXVJ3.js +529 -0
- package/dist/chunk-7N7JYGN2.js +364 -0
- package/dist/chunk-7QTJUGYQ.js +74 -0
- package/dist/chunk-7TXIPJI2.js +2360 -0
- package/dist/chunk-BFJUD527.js +2369 -0
- package/dist/chunk-CHXIVTQV.js +364 -0
- package/dist/chunk-DM2ABCA4.js +497 -0
- package/dist/chunk-DTPHGTBQ.js +275 -0
- package/dist/chunk-EDAJ5FO6.js +405 -0
- package/dist/chunk-ETJWXHKZ.js +246 -0
- package/dist/chunk-FLYGNPUC.js +2369 -0
- package/dist/chunk-FSDRDWOP.js +34 -0
- package/dist/chunk-GIKMCG4D.js +497 -0
- package/dist/chunk-IJKS3LGK.js +428 -0
- package/dist/chunk-JUQ2VQZA.js +428 -0
- package/dist/chunk-JVW4J7BY.js +2369 -0
- package/dist/chunk-JWTRVEC3.js +2369 -0
- package/dist/chunk-KJM4C65U.js +299 -0
- package/dist/chunk-KMC566CN.js +591 -0
- package/dist/chunk-KMNP6DBL.js +455 -0
- package/dist/chunk-LVU7O5IY.js +597 -0
- package/dist/chunk-M4C6RWLA.js +373 -0
- package/dist/chunk-NAAAKSEO.js +541 -0
- package/dist/chunk-NKIXLPHL.js +373 -0
- package/dist/chunk-NOEDMK7I.js +428 -0
- package/dist/chunk-NOPYSBOQ.js +2360 -0
- package/dist/chunk-P4G42QCY.js +2369 -0
- package/dist/chunk-PKLONZCF.js +253 -0
- package/dist/chunk-PNGECWPN.js +597 -0
- package/dist/chunk-PYP6T64W.js +217 -0
- package/dist/chunk-QFT3COE2.js +217 -0
- package/dist/chunk-QFZFXMNX.js +275 -0
- package/dist/chunk-QZGRQRJP.js +2369 -0
- package/dist/chunk-R3XRBAM7.js +253 -0
- package/dist/chunk-RYP66UMH.js +74 -0
- package/dist/chunk-RZIZYRLF.js +112 -0
- package/dist/chunk-T43V44RS.js +2369 -0
- package/dist/chunk-UCNVX5BZ.js +74 -0
- package/dist/chunk-UDF7HS4V.js +368 -0
- package/dist/chunk-VJPXJVEH.js +299 -0
- package/dist/chunk-VW3KBDK5.js +74 -0
- package/dist/chunk-X72XIYSN.js +364 -0
- package/dist/chunk-XETU7TV4.js +112 -0
- package/dist/chunk-Y4BT6LHA.js +368 -0
- package/dist/chunk-YQGB6BIA.js +2369 -0
- package/dist/chunk-ZEQPO3XV.js +217 -0
- package/dist/chunk-ZKRKWXEQ.js +2369 -0
- package/dist/chunk-ZMK2HTZ5.js +275 -0
- package/dist/constants-CLB7B6MN.js +101 -0
- package/dist/constants-O343SMHL.js +103 -0
- package/dist/constants-YPGDEX5X.js +103 -0
- package/dist/index.browser.cjs +11 -5
- package/dist/index.browser.js +12 -12
- package/dist/index.cjs +2358 -1896
- package/dist/index.d.cts +934 -776
- package/dist/index.d.ts +934 -776
- package/dist/index.js +1353 -1271
- package/dist/storage/SqliteNodeEngine.cjs +12 -6
- package/dist/storage/SqliteNodeEngine.js +4 -4
- package/dist/storage/SqliteWasmEngine.cjs +11 -5
- package/dist/storage/SqliteWasmEngine.js +4 -4
- package/package.json +5 -3
|
@@ -0,0 +1,987 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HTTP_DEFAULT_BACKOFF,
|
|
3
|
+
HTTP_DEFAULT_BASE_DELAY_MS,
|
|
4
|
+
HTTP_DEFAULT_CACHE_TTL_SECONDS,
|
|
5
|
+
HTTP_DEFAULT_MAX_ATTEMPTS,
|
|
6
|
+
HTTP_DEFAULT_MAX_DELAY_MS,
|
|
7
|
+
HTTP_DEFAULT_TIMEOUT_MS,
|
|
8
|
+
NETWORK_DEFAULT_LISTEN_PORT,
|
|
9
|
+
NETWORK_DEFAULT_ORCHESTRATOR_SLEEP_MS,
|
|
10
|
+
RATE_LIMIT_DEFAULT_MAX_BURST,
|
|
11
|
+
RATE_LIMIT_DEFAULT_TOKENS_PER_SECOND,
|
|
12
|
+
RATE_LIMIT_WAIT_INTERVAL_MS
|
|
13
|
+
} from "./chunk-FSDRDWOP.js";
|
|
14
|
+
import {
|
|
15
|
+
MCard
|
|
16
|
+
} from "./chunk-GGQCF7ZK.js";
|
|
17
|
+
import "./chunk-ASW6AOA7.js";
|
|
18
|
+
import "./chunk-PNKVD2UK.js";
|
|
19
|
+
|
|
20
|
+
// src/ptr/node/NetworkRuntime.ts
|
|
21
|
+
import * as http from "http";
|
|
22
|
+
|
|
23
|
+
// src/ptr/node/network/NetworkSecurity.ts
|
|
24
|
+
var NetworkSecurity = class {
|
|
25
|
+
config;
|
|
26
|
+
constructor(config) {
|
|
27
|
+
this.config = config || this.loadSecurityConfigFromEnv();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Load security configuration from environment variables
|
|
31
|
+
*/
|
|
32
|
+
loadSecurityConfigFromEnv() {
|
|
33
|
+
const parseList = (value) => {
|
|
34
|
+
if (!value) return void 0;
|
|
35
|
+
return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
allowed_domains: parseList(process.env.CLM_ALLOWED_DOMAINS),
|
|
39
|
+
blocked_domains: parseList(process.env.CLM_BLOCKED_DOMAINS),
|
|
40
|
+
allowed_protocols: parseList(process.env.CLM_ALLOWED_PROTOCOLS),
|
|
41
|
+
block_private_ips: process.env.CLM_BLOCK_PRIVATE_IPS === "true",
|
|
42
|
+
block_localhost: process.env.CLM_BLOCK_LOCALHOST === "true"
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate URL against security policy
|
|
47
|
+
* Throws SecurityViolationError if URL is not allowed
|
|
48
|
+
*/
|
|
49
|
+
validateUrl(urlString) {
|
|
50
|
+
let url;
|
|
51
|
+
try {
|
|
52
|
+
url = new URL(urlString);
|
|
53
|
+
} catch {
|
|
54
|
+
throw this.createSecurityError("DOMAIN_BLOCKED", `Invalid URL: ${urlString}`, urlString);
|
|
55
|
+
}
|
|
56
|
+
const hostname = url.hostname.toLowerCase();
|
|
57
|
+
const protocol = url.protocol.replace(":", "");
|
|
58
|
+
if (this.config.blocked_domains) {
|
|
59
|
+
for (const pattern of this.config.blocked_domains) {
|
|
60
|
+
if (this.matchDomainPattern(hostname, pattern)) {
|
|
61
|
+
throw this.createSecurityError(
|
|
62
|
+
"DOMAIN_BLOCKED",
|
|
63
|
+
`Domain '${hostname}' is blocked by security policy`,
|
|
64
|
+
urlString
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (this.config.allowed_domains && this.config.allowed_domains.length > 0) {
|
|
70
|
+
const isAllowed = this.config.allowed_domains.some(
|
|
71
|
+
(pattern) => this.matchDomainPattern(hostname, pattern)
|
|
72
|
+
);
|
|
73
|
+
if (!isAllowed) {
|
|
74
|
+
throw this.createSecurityError(
|
|
75
|
+
"DOMAIN_NOT_ALLOWED",
|
|
76
|
+
`Domain '${hostname}' is not in the allowed list`,
|
|
77
|
+
urlString
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (this.config.allowed_protocols && this.config.allowed_protocols.length > 0) {
|
|
82
|
+
if (!this.config.allowed_protocols.includes(protocol)) {
|
|
83
|
+
throw this.createSecurityError(
|
|
84
|
+
"PROTOCOL_NOT_ALLOWED",
|
|
85
|
+
`Protocol '${protocol}' is not allowed. Allowed: ${this.config.allowed_protocols.join(", ")}`,
|
|
86
|
+
urlString
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (this.config.block_localhost) {
|
|
91
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") {
|
|
92
|
+
throw this.createSecurityError(
|
|
93
|
+
"LOCALHOST_BLOCKED",
|
|
94
|
+
"Localhost access is blocked by security policy",
|
|
95
|
+
urlString
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (this.config.block_private_ips) {
|
|
100
|
+
if (this.isPrivateIP(hostname)) {
|
|
101
|
+
throw this.createSecurityError(
|
|
102
|
+
"PRIVATE_IP_BLOCKED",
|
|
103
|
+
`Private IP '${hostname}' is blocked by security policy`,
|
|
104
|
+
urlString
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Match hostname against domain pattern (supports wildcards like *.example.com)
|
|
111
|
+
*/
|
|
112
|
+
matchDomainPattern(hostname, pattern) {
|
|
113
|
+
const patternLower = pattern.toLowerCase();
|
|
114
|
+
if (patternLower.startsWith("*.")) {
|
|
115
|
+
const suffix = patternLower.slice(1);
|
|
116
|
+
return hostname.endsWith(suffix) || hostname === patternLower.slice(2);
|
|
117
|
+
}
|
|
118
|
+
return hostname === patternLower;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Check if hostname is a private IP address
|
|
122
|
+
*/
|
|
123
|
+
isPrivateIP(hostname) {
|
|
124
|
+
const privatePatterns = [
|
|
125
|
+
/^10\.\d+\.\d+\.\d+$/,
|
|
126
|
+
// 10.x.x.x
|
|
127
|
+
/^192\.168\.\d+\.\d+$/,
|
|
128
|
+
// 192.168.x.x
|
|
129
|
+
/^172\.(1[6-9]|2\d|3[01])\.\d+\.\d+$/,
|
|
130
|
+
// 172.16-31.x.x
|
|
131
|
+
/^169\.254\.\d+\.\d+$/,
|
|
132
|
+
// Link-local
|
|
133
|
+
/^fc00:/i,
|
|
134
|
+
// IPv6 private
|
|
135
|
+
/^fd00:/i
|
|
136
|
+
// IPv6 private
|
|
137
|
+
];
|
|
138
|
+
return privatePatterns.some((pattern) => pattern.test(hostname));
|
|
139
|
+
}
|
|
140
|
+
createSecurityError(code, message, url) {
|
|
141
|
+
const error = new Error(message);
|
|
142
|
+
error.securityViolation = { code, message, url };
|
|
143
|
+
return error;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// src/ptr/node/network/MCardSerialization.ts
|
|
148
|
+
var MCardSerialization = class {
|
|
149
|
+
/**
|
|
150
|
+
* Serialize an MCard to a JSON-safe payload for network transfer
|
|
151
|
+
*/
|
|
152
|
+
static serialize(card) {
|
|
153
|
+
return {
|
|
154
|
+
hash: card.hash,
|
|
155
|
+
content: Buffer.from(card.content).toString("base64"),
|
|
156
|
+
g_time: card.g_time,
|
|
157
|
+
contentType: card.contentType,
|
|
158
|
+
hashFunction: card.hashFunction
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Deserialize a JSON payload back to an MCard
|
|
163
|
+
* Uses fromData if hash/g_time provided (preserves identity)
|
|
164
|
+
* Otherwise creates new MCard (generates new hash/g_time)
|
|
165
|
+
*/
|
|
166
|
+
static async deserialize(json) {
|
|
167
|
+
if (!json.content) {
|
|
168
|
+
throw new Error("Missing content in MCard payload");
|
|
169
|
+
}
|
|
170
|
+
const content = Buffer.from(json.content, "base64");
|
|
171
|
+
if (json.hash && json.g_time) {
|
|
172
|
+
return MCard.fromData(content, json.hash, json.g_time);
|
|
173
|
+
}
|
|
174
|
+
return MCard.create(content);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Verify hash matches content (optional strict mode)
|
|
178
|
+
*/
|
|
179
|
+
static verifyHash(card, expectedHash) {
|
|
180
|
+
if (card.hash !== expectedHash) {
|
|
181
|
+
console.warn(`[Network] Hash mismatch. Expected: ${expectedHash}, Got: ${card.hash}`);
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// src/ptr/node/network/NetworkInfrastructure.ts
|
|
189
|
+
var RateLimiter = class {
|
|
190
|
+
limits;
|
|
191
|
+
defaultLimit;
|
|
192
|
+
constructor(tokensPerSecond = RATE_LIMIT_DEFAULT_TOKENS_PER_SECOND, maxBurst = RATE_LIMIT_DEFAULT_MAX_BURST) {
|
|
193
|
+
this.limits = /* @__PURE__ */ new Map();
|
|
194
|
+
this.defaultLimit = { tokensPerSecond, maxBurst };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Check if request allowed. Consumes a token if allowed.
|
|
198
|
+
*/
|
|
199
|
+
check(domain) {
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
const bucket = this.limits.get(domain) || {
|
|
202
|
+
tokens: this.defaultLimit.maxBurst,
|
|
203
|
+
lastRefill: now
|
|
204
|
+
};
|
|
205
|
+
const elapsed = (now - bucket.lastRefill) / 1e3;
|
|
206
|
+
const refill = elapsed * this.defaultLimit.tokensPerSecond;
|
|
207
|
+
bucket.tokens = Math.min(this.defaultLimit.maxBurst, bucket.tokens + refill);
|
|
208
|
+
bucket.lastRefill = now;
|
|
209
|
+
if (bucket.tokens >= 1) {
|
|
210
|
+
bucket.tokens -= 1;
|
|
211
|
+
this.limits.set(domain, bucket);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
this.limits.set(domain, bucket);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Wait until rate limit allows request
|
|
219
|
+
*/
|
|
220
|
+
async waitFor(domain) {
|
|
221
|
+
while (!this.check(domain)) {
|
|
222
|
+
await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_WAIT_INTERVAL_MS));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
var NetworkCache = class {
|
|
227
|
+
memoryCache;
|
|
228
|
+
collection;
|
|
229
|
+
constructor(collection) {
|
|
230
|
+
this.memoryCache = /* @__PURE__ */ new Map();
|
|
231
|
+
this.collection = collection;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Generate cache key from request config
|
|
235
|
+
*/
|
|
236
|
+
static generateKey(method, url, body) {
|
|
237
|
+
const keyData = `${method}:${url}:${body || ""}`;
|
|
238
|
+
let hash = 0;
|
|
239
|
+
for (let i = 0; i < keyData.length; i++) {
|
|
240
|
+
const char = keyData.charCodeAt(i);
|
|
241
|
+
hash = (hash << 5) - hash + char;
|
|
242
|
+
hash = hash & hash;
|
|
243
|
+
}
|
|
244
|
+
return `cache_${Math.abs(hash).toString(36)}`;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Get cached response if valid
|
|
248
|
+
*/
|
|
249
|
+
get(cacheKey) {
|
|
250
|
+
const cached = this.memoryCache.get(cacheKey);
|
|
251
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
252
|
+
return { ...cached.response, timing: { ...cached.response.timing, total: 0 } };
|
|
253
|
+
}
|
|
254
|
+
if (cached) {
|
|
255
|
+
this.memoryCache.delete(cacheKey);
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Cache a response with TTL
|
|
261
|
+
*/
|
|
262
|
+
async set(cacheKey, response, ttlSeconds, persist = false) {
|
|
263
|
+
this.memoryCache.set(cacheKey, {
|
|
264
|
+
response,
|
|
265
|
+
expiresAt: Date.now() + ttlSeconds * 1e3
|
|
266
|
+
});
|
|
267
|
+
if (persist && this.collection) {
|
|
268
|
+
const cacheEntry = {
|
|
269
|
+
key: cacheKey,
|
|
270
|
+
response,
|
|
271
|
+
expiresAt: Date.now() + ttlSeconds * 1e3,
|
|
272
|
+
cachedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
273
|
+
};
|
|
274
|
+
const card = await MCard.create(JSON.stringify(cacheEntry));
|
|
275
|
+
await this.collection.add(card);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
var RetryUtils = class {
|
|
280
|
+
static calculateBackoffDelay(attempt, strategy, baseDelay, maxDelay) {
|
|
281
|
+
let delay;
|
|
282
|
+
switch (strategy) {
|
|
283
|
+
case "exponential":
|
|
284
|
+
delay = baseDelay * Math.pow(2, attempt - 1);
|
|
285
|
+
break;
|
|
286
|
+
case "linear":
|
|
287
|
+
delay = baseDelay * attempt;
|
|
288
|
+
break;
|
|
289
|
+
case "constant":
|
|
290
|
+
default:
|
|
291
|
+
delay = baseDelay;
|
|
292
|
+
}
|
|
293
|
+
const jitter = delay * 0.1 * (Math.random() * 2 - 1);
|
|
294
|
+
delay = Math.round(delay + jitter);
|
|
295
|
+
return maxDelay ? Math.min(delay, maxDelay) : delay;
|
|
296
|
+
}
|
|
297
|
+
static shouldRetryStatus(status, retryOn) {
|
|
298
|
+
const defaultRetryStatuses = [408, 429, 500, 502, 503, 504];
|
|
299
|
+
const retryStatuses = retryOn || defaultRetryStatuses;
|
|
300
|
+
return retryStatuses.includes(status);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// src/ptr/node/network/HttpClient.ts
|
|
305
|
+
var HttpClient = class {
|
|
306
|
+
rateLimiter;
|
|
307
|
+
cache;
|
|
308
|
+
constructor(rateLimiter, cache) {
|
|
309
|
+
this.rateLimiter = rateLimiter;
|
|
310
|
+
this.cache = cache;
|
|
311
|
+
}
|
|
312
|
+
async request(url, method, headers, body, config) {
|
|
313
|
+
const startTime = Date.now();
|
|
314
|
+
const fetchUrl = new URL(url);
|
|
315
|
+
const cacheConfig = config.cache;
|
|
316
|
+
const cacheKey = NetworkCache.generateKey(method, fetchUrl.toString(), typeof body === "string" ? body : void 0);
|
|
317
|
+
if (cacheConfig?.enabled && method === "GET") {
|
|
318
|
+
const cachedResponse = this.cache.get(cacheKey);
|
|
319
|
+
if (cachedResponse) {
|
|
320
|
+
console.log(`[Network] Cache hit for ${url}`);
|
|
321
|
+
return { ...cachedResponse, cached: true };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const domain = fetchUrl.hostname;
|
|
325
|
+
await this.rateLimiter.waitFor(domain);
|
|
326
|
+
const retryConfig = config.retry || {
|
|
327
|
+
max_attempts: HTTP_DEFAULT_MAX_ATTEMPTS,
|
|
328
|
+
backoff: HTTP_DEFAULT_BACKOFF,
|
|
329
|
+
base_delay: HTTP_DEFAULT_BASE_DELAY_MS,
|
|
330
|
+
max_delay: HTTP_DEFAULT_MAX_DELAY_MS
|
|
331
|
+
};
|
|
332
|
+
let lastError = null;
|
|
333
|
+
let lastStatus = null;
|
|
334
|
+
let retriesAttempted = 0;
|
|
335
|
+
for (let attempt = 1; attempt <= retryConfig.max_attempts; attempt++) {
|
|
336
|
+
const timeout = config.timeout || HTTP_DEFAULT_TIMEOUT_MS;
|
|
337
|
+
const controller = new AbortController();
|
|
338
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
339
|
+
try {
|
|
340
|
+
const ttfbStart = Date.now();
|
|
341
|
+
const response = await fetch(fetchUrl.toString(), {
|
|
342
|
+
method,
|
|
343
|
+
headers,
|
|
344
|
+
body,
|
|
345
|
+
signal: controller.signal
|
|
346
|
+
});
|
|
347
|
+
clearTimeout(timeoutId);
|
|
348
|
+
if (!response.ok && RetryUtils.shouldRetryStatus(response.status, retryConfig.retry_on)) {
|
|
349
|
+
lastStatus = response.status;
|
|
350
|
+
if (attempt < retryConfig.max_attempts) {
|
|
351
|
+
retriesAttempted++;
|
|
352
|
+
const delay = RetryUtils.calculateBackoffDelay(
|
|
353
|
+
attempt,
|
|
354
|
+
retryConfig.backoff,
|
|
355
|
+
retryConfig.base_delay,
|
|
356
|
+
retryConfig.max_delay
|
|
357
|
+
);
|
|
358
|
+
console.log(`[Network] Retry ${attempt}/${retryConfig.max_attempts} for ${url} (status: ${response.status}, delay: ${delay}ms)`);
|
|
359
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const ttfbTime = Date.now() - ttfbStart;
|
|
364
|
+
let responseBody;
|
|
365
|
+
const responseType = config.responseType || "json";
|
|
366
|
+
if (responseType === "json") {
|
|
367
|
+
try {
|
|
368
|
+
responseBody = await response.json();
|
|
369
|
+
} catch {
|
|
370
|
+
responseBody = await response.text();
|
|
371
|
+
}
|
|
372
|
+
} else if (responseType === "text") {
|
|
373
|
+
responseBody = await response.text();
|
|
374
|
+
} else if (responseType === "binary") {
|
|
375
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
376
|
+
responseBody = Buffer.from(arrayBuffer).toString("base64");
|
|
377
|
+
} else {
|
|
378
|
+
responseBody = await response.text();
|
|
379
|
+
}
|
|
380
|
+
const totalTime = Date.now() - startTime;
|
|
381
|
+
let mcard_hash;
|
|
382
|
+
try {
|
|
383
|
+
const bodyStr = typeof responseBody === "string" ? responseBody : JSON.stringify(responseBody);
|
|
384
|
+
const responseCard = await MCard.create(bodyStr);
|
|
385
|
+
mcard_hash = responseCard.hash;
|
|
386
|
+
} catch {
|
|
387
|
+
}
|
|
388
|
+
const timing = {
|
|
389
|
+
dns: 0,
|
|
390
|
+
connect: 0,
|
|
391
|
+
ttfb: ttfbTime,
|
|
392
|
+
total: totalTime
|
|
393
|
+
};
|
|
394
|
+
const result = {
|
|
395
|
+
success: true,
|
|
396
|
+
status: response.status,
|
|
397
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
398
|
+
body: responseBody,
|
|
399
|
+
timing,
|
|
400
|
+
mcard_hash
|
|
401
|
+
};
|
|
402
|
+
if (cacheConfig?.enabled && method === "GET" && response.ok) {
|
|
403
|
+
await this.cache.set(
|
|
404
|
+
cacheKey,
|
|
405
|
+
result,
|
|
406
|
+
cacheConfig.ttl ?? HTTP_DEFAULT_CACHE_TTL_SECONDS,
|
|
407
|
+
cacheConfig.storage === "mcard"
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return result;
|
|
411
|
+
} catch (error) {
|
|
412
|
+
clearTimeout(timeoutId);
|
|
413
|
+
lastError = error;
|
|
414
|
+
if (attempt < retryConfig.max_attempts) {
|
|
415
|
+
retriesAttempted++;
|
|
416
|
+
const delay = RetryUtils.calculateBackoffDelay(
|
|
417
|
+
attempt,
|
|
418
|
+
retryConfig.backoff,
|
|
419
|
+
retryConfig.base_delay,
|
|
420
|
+
retryConfig.max_delay
|
|
421
|
+
);
|
|
422
|
+
console.log(`[Network] Retry ${attempt}/${retryConfig.max_attempts} for ${url} (error: ${lastError.message}, delay: ${delay}ms)`);
|
|
423
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const err = lastError;
|
|
429
|
+
return {
|
|
430
|
+
success: false,
|
|
431
|
+
error: {
|
|
432
|
+
code: err?.name === "AbortError" ? "TIMEOUT" : "HTTP_ERROR",
|
|
433
|
+
message: err?.message || "Request failed after retries",
|
|
434
|
+
status: lastStatus,
|
|
435
|
+
retries_attempted: retriesAttempted
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// src/ptr/node/NetworkRuntime.ts
|
|
442
|
+
var NetworkRuntime = class {
|
|
443
|
+
collection;
|
|
444
|
+
security;
|
|
445
|
+
cache;
|
|
446
|
+
rateLimiter;
|
|
447
|
+
httpClient;
|
|
448
|
+
constructor(collection) {
|
|
449
|
+
this.collection = collection;
|
|
450
|
+
this.security = new NetworkSecurity();
|
|
451
|
+
this.cache = new NetworkCache(collection);
|
|
452
|
+
this.rateLimiter = new RateLimiter();
|
|
453
|
+
this.httpClient = new HttpClient(this.rateLimiter, this.cache);
|
|
454
|
+
}
|
|
455
|
+
async execute(_code, context, config, _chapterDir) {
|
|
456
|
+
const builtin = config.builtin;
|
|
457
|
+
const builtinConfig = config.config ?? {};
|
|
458
|
+
if (!builtin) {
|
|
459
|
+
throw new Error('NetworkRuntime requires "builtin" to be defined in config.');
|
|
460
|
+
}
|
|
461
|
+
switch (builtin) {
|
|
462
|
+
case "http_request":
|
|
463
|
+
return this.handleHttpRequest(builtinConfig, context);
|
|
464
|
+
case "http_get":
|
|
465
|
+
return this.handleHttpGet(builtinConfig, context);
|
|
466
|
+
case "http_post":
|
|
467
|
+
return this.handleHttpPost(builtinConfig, context);
|
|
468
|
+
case "load_url":
|
|
469
|
+
return this.handleLoadUrl(builtinConfig, context);
|
|
470
|
+
case "mcard_send":
|
|
471
|
+
return this.handleMCardSend(builtinConfig, context);
|
|
472
|
+
case "listen_http":
|
|
473
|
+
return this.handleListenHttp(builtinConfig, context);
|
|
474
|
+
case "mcard_sync":
|
|
475
|
+
return this.handleMCardSync(builtinConfig, context);
|
|
476
|
+
case "listen_sync":
|
|
477
|
+
return this.handleListenSync(builtinConfig, context);
|
|
478
|
+
case "webrtc_connect":
|
|
479
|
+
case "webrtc_listen":
|
|
480
|
+
console.warn(`[Network] WebRTC builtin ${builtin} mocked in TS runtime.`);
|
|
481
|
+
return { success: true, connection_id: "mock_conn_" + Date.now(), mock: true };
|
|
482
|
+
case "mcard_read":
|
|
483
|
+
return this.handleMCardRead(builtinConfig, context);
|
|
484
|
+
case "run_command":
|
|
485
|
+
return this.handleRunCommand(builtinConfig, context);
|
|
486
|
+
case "clm_orchestrator":
|
|
487
|
+
return this.handleOrchestrator(builtinConfig, context);
|
|
488
|
+
case "session_record":
|
|
489
|
+
case "signaling_server":
|
|
490
|
+
throw new Error(`Builtin ${builtin} removed (Kenotic Principle).`);
|
|
491
|
+
default:
|
|
492
|
+
throw new Error(`Unknown network builtin: ${builtin}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async handleHttpGet(config, context) {
|
|
496
|
+
return this.handleHttpRequest({ ...config, method: "GET" }, context);
|
|
497
|
+
}
|
|
498
|
+
async handleHttpPost(config, context) {
|
|
499
|
+
const params = { ...config, method: "POST" };
|
|
500
|
+
if (config.json) {
|
|
501
|
+
params.headers = { ...params.headers, "Content-Type": "application/json" };
|
|
502
|
+
params.body = JSON.stringify(config.json);
|
|
503
|
+
}
|
|
504
|
+
return this.handleHttpRequest(params, context);
|
|
505
|
+
}
|
|
506
|
+
async handleHttpRequest(config, context) {
|
|
507
|
+
const url = this.interpolate(config.url ?? "", context);
|
|
508
|
+
this.security.validateUrl(url);
|
|
509
|
+
const method = config.method || "GET";
|
|
510
|
+
const headers = this.interpolateHeaders(config.headers || {}, context);
|
|
511
|
+
let body = config.body;
|
|
512
|
+
if (typeof body === "string") {
|
|
513
|
+
body = this.interpolate(body, context);
|
|
514
|
+
} else if (typeof body === "object" && body !== null) {
|
|
515
|
+
body = JSON.stringify(body);
|
|
516
|
+
}
|
|
517
|
+
const fetchUrl = new URL(url);
|
|
518
|
+
if (config.query_params) {
|
|
519
|
+
for (const [key, value] of Object.entries(config.query_params)) {
|
|
520
|
+
fetchUrl.searchParams.append(key, this.interpolate(String(value), context));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return this.httpClient.request(
|
|
524
|
+
fetchUrl.toString(),
|
|
525
|
+
method,
|
|
526
|
+
headers,
|
|
527
|
+
body,
|
|
528
|
+
{
|
|
529
|
+
retry: config.retry,
|
|
530
|
+
cache: config.cache,
|
|
531
|
+
timeout: typeof config.timeout === "number" ? config.timeout : config.timeout?.total,
|
|
532
|
+
responseType: config.response_type
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
async handleLoadUrl(config, context) {
|
|
537
|
+
const url = this.interpolate(config.url, context);
|
|
538
|
+
this.security.validateUrl(url);
|
|
539
|
+
try {
|
|
540
|
+
const res = await fetch(url);
|
|
541
|
+
const text = await res.text();
|
|
542
|
+
return {
|
|
543
|
+
url,
|
|
544
|
+
content: text,
|
|
545
|
+
status: res.status,
|
|
546
|
+
headers: Object.fromEntries(res.headers.entries())
|
|
547
|
+
};
|
|
548
|
+
} catch (e) {
|
|
549
|
+
return { success: false, error: String(e) };
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
async handleMCardSend(config, context) {
|
|
553
|
+
if (!this.collection) {
|
|
554
|
+
throw new Error("MCard Send requires a CardCollection.");
|
|
555
|
+
}
|
|
556
|
+
const hash = this.interpolate(config.hash, context);
|
|
557
|
+
const url = this.interpolate(config.url, context);
|
|
558
|
+
const card = await this.collection.get(hash);
|
|
559
|
+
if (!card) {
|
|
560
|
+
return { success: false, error: `MCard not found: ${hash}` };
|
|
561
|
+
}
|
|
562
|
+
const payload = MCardSerialization.serialize(card);
|
|
563
|
+
return this.handleHttpPost({
|
|
564
|
+
url,
|
|
565
|
+
json: payload,
|
|
566
|
+
headers: config.headers
|
|
567
|
+
}, context);
|
|
568
|
+
}
|
|
569
|
+
async handleListenHttp(config, context) {
|
|
570
|
+
const port = Number(this.interpolate(String(config.port || NETWORK_DEFAULT_LISTEN_PORT), context));
|
|
571
|
+
const path = this.interpolate(config.path || "/mcard", context);
|
|
572
|
+
return new Promise((resolve, reject) => {
|
|
573
|
+
const server = http.createServer(async (req, res) => {
|
|
574
|
+
if (req.method === "POST" && req.url === path) {
|
|
575
|
+
const bodyChunks = [];
|
|
576
|
+
req.on("data", (chunk) => bodyChunks.push(chunk));
|
|
577
|
+
req.on("end", async () => {
|
|
578
|
+
try {
|
|
579
|
+
const body = Buffer.concat(bodyChunks).toString();
|
|
580
|
+
const json = JSON.parse(body);
|
|
581
|
+
const card = await MCardSerialization.deserialize(json);
|
|
582
|
+
if (json.hash) {
|
|
583
|
+
MCardSerialization.verifyHash(card, json.hash);
|
|
584
|
+
}
|
|
585
|
+
if (this.collection) {
|
|
586
|
+
await this.collection.add(card);
|
|
587
|
+
}
|
|
588
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
589
|
+
res.end(JSON.stringify({ success: true, hash: card.hash }));
|
|
590
|
+
} catch (e) {
|
|
591
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
592
|
+
res.end(JSON.stringify({ success: false, error: String(e) }));
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
} else {
|
|
596
|
+
res.writeHead(404);
|
|
597
|
+
res.end();
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
server.listen(port, () => {
|
|
601
|
+
console.info(`[Network] Listening on port ${port} at ${path}`);
|
|
602
|
+
resolve({
|
|
603
|
+
success: true,
|
|
604
|
+
message: `Server started on port ${port}`
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
server.on("error", (err) => {
|
|
608
|
+
reject(err);
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
async handleMCardSync(config, context) {
|
|
613
|
+
if (!this.collection) {
|
|
614
|
+
throw new Error("MCard Sync requires a CardCollection.");
|
|
615
|
+
}
|
|
616
|
+
const mode = this.interpolate(config.mode || "pull", context);
|
|
617
|
+
const urlParams = this.interpolate(config.url ?? "", context);
|
|
618
|
+
const url = urlParams.endsWith("/") ? urlParams.slice(0, -1) : urlParams;
|
|
619
|
+
const localCards = await this.collection.getAllMCardsRaw();
|
|
620
|
+
const localHashes = new Set(localCards.map((c) => c.hash));
|
|
621
|
+
const manifestRes = await this.handleHttpRequest({
|
|
622
|
+
url: `${url}/manifest`,
|
|
623
|
+
method: "GET"
|
|
624
|
+
}, context);
|
|
625
|
+
const manifestResult = manifestRes;
|
|
626
|
+
if (!manifestResult.success) {
|
|
627
|
+
const errorMessage = typeof manifestResult.error === "string" ? manifestResult.error : manifestResult.error?.message;
|
|
628
|
+
throw new Error(`Failed to fetch remote manifest: ${errorMessage}`);
|
|
629
|
+
}
|
|
630
|
+
const remoteHashes = new Set(manifestResult.body ?? []);
|
|
631
|
+
const stats = {
|
|
632
|
+
mode,
|
|
633
|
+
local_total: localHashes.size,
|
|
634
|
+
remote_total: remoteHashes.size,
|
|
635
|
+
synced: 0,
|
|
636
|
+
pushed: 0,
|
|
637
|
+
pulled: 0
|
|
638
|
+
};
|
|
639
|
+
const pushCards = async () => {
|
|
640
|
+
const toSend = [];
|
|
641
|
+
for (const card of localCards) {
|
|
642
|
+
if (!remoteHashes.has(card.hash)) {
|
|
643
|
+
toSend.push(card);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (toSend.length > 0) {
|
|
647
|
+
const payload = {
|
|
648
|
+
cards: toSend.map((card) => MCardSerialization.serialize(card))
|
|
649
|
+
};
|
|
650
|
+
const pushRes = await this.handleHttpPost({
|
|
651
|
+
url: `${url}/batch`,
|
|
652
|
+
json: payload,
|
|
653
|
+
headers: config.headers
|
|
654
|
+
}, context);
|
|
655
|
+
const pushResult = pushRes;
|
|
656
|
+
if (!pushResult.success) {
|
|
657
|
+
const errorMessage = typeof pushResult.error === "string" ? pushResult.error : pushResult.error?.message;
|
|
658
|
+
throw new Error(`Failed to push batch: ${errorMessage}`);
|
|
659
|
+
}
|
|
660
|
+
return toSend.length;
|
|
661
|
+
}
|
|
662
|
+
return 0;
|
|
663
|
+
};
|
|
664
|
+
const pullCards = async () => {
|
|
665
|
+
const neededHashes = [];
|
|
666
|
+
for (const h of remoteHashes) {
|
|
667
|
+
if (!localHashes.has(h)) {
|
|
668
|
+
neededHashes.push(h);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (neededHashes.length > 0) {
|
|
672
|
+
const fetchRes = await this.handleHttpPost({
|
|
673
|
+
url: `${url}/get`,
|
|
674
|
+
json: { hashes: neededHashes },
|
|
675
|
+
headers: config.headers
|
|
676
|
+
}, context);
|
|
677
|
+
const fetchResult = fetchRes;
|
|
678
|
+
if (!fetchResult.success) {
|
|
679
|
+
const errorMessage = typeof fetchResult.error === "string" ? fetchResult.error : fetchResult.error?.message;
|
|
680
|
+
throw new Error(`Failed to pull batch: ${errorMessage}`);
|
|
681
|
+
}
|
|
682
|
+
const receivedCards = fetchResult.body?.cards ?? [];
|
|
683
|
+
for (const json of receivedCards) {
|
|
684
|
+
const card = await MCardSerialization.deserialize(json);
|
|
685
|
+
await this.collection.add(card);
|
|
686
|
+
}
|
|
687
|
+
return receivedCards.length;
|
|
688
|
+
}
|
|
689
|
+
return 0;
|
|
690
|
+
};
|
|
691
|
+
if (mode === "push") {
|
|
692
|
+
stats.pushed = await pushCards();
|
|
693
|
+
stats.synced = stats.pushed;
|
|
694
|
+
} else if (mode === "pull") {
|
|
695
|
+
stats.pulled = await pullCards();
|
|
696
|
+
stats.synced = stats.pulled;
|
|
697
|
+
} else if (mode === "both" || mode === "bidirectional") {
|
|
698
|
+
const pushed = await pushCards();
|
|
699
|
+
const pulled = await pullCards();
|
|
700
|
+
stats.synced = pushed + pulled;
|
|
701
|
+
stats.pushed = pushed;
|
|
702
|
+
stats.pulled = pulled;
|
|
703
|
+
}
|
|
704
|
+
return { success: true, stats };
|
|
705
|
+
}
|
|
706
|
+
// ============ WebRTC Implementation ============
|
|
707
|
+
async handleListenSync(config, context) {
|
|
708
|
+
if (!this.collection) {
|
|
709
|
+
throw new Error("Listen Sync requires a CardCollection.");
|
|
710
|
+
}
|
|
711
|
+
const port = Number(this.interpolate(String(config.port || NETWORK_DEFAULT_LISTEN_PORT), context));
|
|
712
|
+
const basePath = this.interpolate(config.base_path || "/sync", context);
|
|
713
|
+
return new Promise((resolve, reject) => {
|
|
714
|
+
const server = http.createServer(async (req, res) => {
|
|
715
|
+
const url = req.url || "";
|
|
716
|
+
const readBody = async () => {
|
|
717
|
+
return new Promise((res2, rej) => {
|
|
718
|
+
const chunks = [];
|
|
719
|
+
req.on("data", (c) => chunks.push(c));
|
|
720
|
+
req.on("end", () => {
|
|
721
|
+
try {
|
|
722
|
+
const str = Buffer.concat(chunks).toString();
|
|
723
|
+
res2(JSON.parse(str || "{}"));
|
|
724
|
+
} catch (e) {
|
|
725
|
+
rej(e);
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
req.on("error", rej);
|
|
729
|
+
});
|
|
730
|
+
};
|
|
731
|
+
try {
|
|
732
|
+
if (req.method === "GET" && url === `${basePath}/manifest`) {
|
|
733
|
+
const all = await this.collection.getAllMCardsRaw();
|
|
734
|
+
const hashes = all.map((c) => c.hash);
|
|
735
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
736
|
+
res.end(JSON.stringify(hashes));
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (req.method === "POST" && url === `${basePath}/batch`) {
|
|
740
|
+
const json = await readBody();
|
|
741
|
+
const cards = Array.isArray(json.cards) ? json.cards : [];
|
|
742
|
+
let added = 0;
|
|
743
|
+
for (const cJson of cards) {
|
|
744
|
+
const card = await MCardSerialization.deserialize(cJson);
|
|
745
|
+
await this.collection.add(card);
|
|
746
|
+
added++;
|
|
747
|
+
}
|
|
748
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
749
|
+
res.end(JSON.stringify({ success: true, added }));
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (req.method === "POST" && url === `${basePath}/get`) {
|
|
753
|
+
const json = await readBody();
|
|
754
|
+
const requestedHashes = Array.isArray(json.hashes) ? json.hashes : [];
|
|
755
|
+
const foundCards = [];
|
|
756
|
+
for (const h of requestedHashes) {
|
|
757
|
+
const card = await this.collection.get(h);
|
|
758
|
+
if (card) {
|
|
759
|
+
foundCards.push(MCardSerialization.serialize(card));
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
763
|
+
res.end(JSON.stringify({ success: true, cards: foundCards }));
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
res.writeHead(404);
|
|
767
|
+
res.end();
|
|
768
|
+
} catch (e) {
|
|
769
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
770
|
+
res.end(JSON.stringify({ success: false, error: String(e) }));
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
server.listen(port, () => {
|
|
774
|
+
console.log(`[Network] Sync listening on port ${port} at ${basePath}`);
|
|
775
|
+
resolve({
|
|
776
|
+
success: true,
|
|
777
|
+
message: `Sync Server started on port ${port}`,
|
|
778
|
+
port,
|
|
779
|
+
basePath
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
server.on("error", (err) => {
|
|
783
|
+
reject(err);
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
interpolate(text, context) {
|
|
788
|
+
if (!text || typeof text !== "string") return text;
|
|
789
|
+
return text.replace(/\$\{([^}]+)\}/g, (_, path) => {
|
|
790
|
+
const keys = path.split(".");
|
|
791
|
+
let val = context;
|
|
792
|
+
for (const key of keys) {
|
|
793
|
+
if (val && typeof val === "object" && key in val) {
|
|
794
|
+
val = val[key];
|
|
795
|
+
} else {
|
|
796
|
+
return "";
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return String(val);
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
interpolateHeaders(headers, context) {
|
|
803
|
+
const result = {};
|
|
804
|
+
for (const [key, val] of Object.entries(headers)) {
|
|
805
|
+
result[key] = this.interpolate(val, context);
|
|
806
|
+
}
|
|
807
|
+
return result;
|
|
808
|
+
}
|
|
809
|
+
async handleMCardRead(config, context) {
|
|
810
|
+
if (!this.collection) {
|
|
811
|
+
throw new Error("MCard Read requires a CardCollection.");
|
|
812
|
+
}
|
|
813
|
+
const hash = this.interpolate(config.hash, context);
|
|
814
|
+
if (!hash) throw new Error("Hash is required for mcard_read");
|
|
815
|
+
const card = await this.collection.get(hash);
|
|
816
|
+
if (!card) return { success: false, error: "MCard not found", hash };
|
|
817
|
+
let content = card.getContentAsText();
|
|
818
|
+
if (config.parse_json !== false) {
|
|
819
|
+
try {
|
|
820
|
+
if (typeof content === "string") {
|
|
821
|
+
content = JSON.parse(content);
|
|
822
|
+
}
|
|
823
|
+
} catch (e) {
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return {
|
|
827
|
+
success: true,
|
|
828
|
+
hash,
|
|
829
|
+
content,
|
|
830
|
+
g_time: card.g_time
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
async handleOrchestrator(config, context) {
|
|
834
|
+
const steps = config.steps || [];
|
|
835
|
+
const state = {};
|
|
836
|
+
let allSuccess = true;
|
|
837
|
+
console.log(`[NetworkRuntime] Starting Orchestration with ${steps.length} steps.`);
|
|
838
|
+
for (const step of steps) {
|
|
839
|
+
const stepName = step.name || step.action;
|
|
840
|
+
console.log(`[Orchestrator] Step: ${stepName}`);
|
|
841
|
+
try {
|
|
842
|
+
if (step.action === "start_process") {
|
|
843
|
+
const cmd = this.interpolate(step.command ?? "", context);
|
|
844
|
+
const { spawn } = await import("child_process");
|
|
845
|
+
const parts = cmd.split(" ");
|
|
846
|
+
const env = { ...process.env, ...step.env || {} };
|
|
847
|
+
const proc = spawn(parts[0], parts.slice(1), {
|
|
848
|
+
detached: true,
|
|
849
|
+
stdio: "inherit",
|
|
850
|
+
cwd: process.cwd(),
|
|
851
|
+
env
|
|
852
|
+
});
|
|
853
|
+
proc.unref();
|
|
854
|
+
if (step.id_key && proc.pid !== void 0) {
|
|
855
|
+
state[step.id_key] = proc.pid;
|
|
856
|
+
console.log(`[Orchestrator] Process started (PID: ${proc.pid}) stored in '${step.id_key}'`);
|
|
857
|
+
} else {
|
|
858
|
+
console.log(`[Orchestrator] Process started (PID: ${proc.pid})`);
|
|
859
|
+
}
|
|
860
|
+
if (step.wait_after) {
|
|
861
|
+
await new Promise((r) => setTimeout(r, step.wait_after));
|
|
862
|
+
}
|
|
863
|
+
} else if (step.action === "run_clm") {
|
|
864
|
+
if (!context.runCLM) throw new Error("runCLM capability not available in context");
|
|
865
|
+
const file = step.file;
|
|
866
|
+
if (!file) {
|
|
867
|
+
throw new Error("run_clm step requires file");
|
|
868
|
+
}
|
|
869
|
+
const input = step.input || {};
|
|
870
|
+
console.log(`[Orchestrator] Running CLM: ${file}`);
|
|
871
|
+
const res = await context.runCLM(file, input);
|
|
872
|
+
if (!res.success) {
|
|
873
|
+
console.error(`[Orchestrator] CLM Failed: ${file}`, res.error);
|
|
874
|
+
if (!step.continue_on_error) {
|
|
875
|
+
allSuccess = false;
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
} else {
|
|
879
|
+
console.log(`[Orchestrator] CLM Passed: ${file}`);
|
|
880
|
+
}
|
|
881
|
+
} else if (step.action === "run_clm_background") {
|
|
882
|
+
const file = step.file;
|
|
883
|
+
if (!file) {
|
|
884
|
+
throw new Error("run_clm_background step requires file");
|
|
885
|
+
}
|
|
886
|
+
const filter = file.replace(/\.(yaml|yml|clm)$/i, "");
|
|
887
|
+
const cmd = `npx tsx examples/run-all-clms.ts ${filter}`;
|
|
888
|
+
const { spawn } = await import("child_process");
|
|
889
|
+
const parts = cmd.split(" ");
|
|
890
|
+
const env = { ...process.env, ...step.env || {} };
|
|
891
|
+
const proc = spawn(parts[0], parts.slice(1), {
|
|
892
|
+
detached: true,
|
|
893
|
+
stdio: "inherit",
|
|
894
|
+
cwd: process.cwd(),
|
|
895
|
+
env
|
|
896
|
+
});
|
|
897
|
+
proc.unref();
|
|
898
|
+
if (step.id_key && proc.pid !== void 0) {
|
|
899
|
+
state[step.id_key] = proc.pid;
|
|
900
|
+
console.log(`[Orchestrator] Background CLM started (PID: ${proc.pid}) stored in '${step.id_key}'`);
|
|
901
|
+
}
|
|
902
|
+
if (step.wait_after) {
|
|
903
|
+
await new Promise((r) => setTimeout(r, step.wait_after));
|
|
904
|
+
}
|
|
905
|
+
} else if (step.action === "stop_process") {
|
|
906
|
+
const key = step.pid_key;
|
|
907
|
+
if (!key) {
|
|
908
|
+
console.warn("[Orchestrator] stop_process missing pid_key");
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
const pid = state[key];
|
|
912
|
+
if (typeof pid === "number") {
|
|
913
|
+
try {
|
|
914
|
+
context.process?.kill(pid);
|
|
915
|
+
console.log(`[Orchestrator] Stopped process ${pid} (${key})`);
|
|
916
|
+
} catch (e) {
|
|
917
|
+
console.warn(`[Orchestrator] Failed to stop process ${pid}: ${e}`);
|
|
918
|
+
}
|
|
919
|
+
} else {
|
|
920
|
+
console.warn(`[Orchestrator] No PID found for key '${key}'`);
|
|
921
|
+
}
|
|
922
|
+
} else if (step.action === "sleep") {
|
|
923
|
+
const ms = step.ms || NETWORK_DEFAULT_ORCHESTRATOR_SLEEP_MS;
|
|
924
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
925
|
+
} else if (step.action === "start_signaling_server") {
|
|
926
|
+
throw new Error(`Builtin signaling server removed (Kenotic Principle).`);
|
|
927
|
+
} else if (step.action === "stop_signaling_server") {
|
|
928
|
+
throw new Error(`Builtin signaling server removed (Kenotic Principle).`);
|
|
929
|
+
}
|
|
930
|
+
} catch (e) {
|
|
931
|
+
console.error(`[Orchestrator] Step '${stepName}' caused error:`, e);
|
|
932
|
+
allSuccess = false;
|
|
933
|
+
if (!step.continue_on_error) break;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
success: allSuccess,
|
|
938
|
+
state
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
async handleRunCommand(config, context) {
|
|
942
|
+
const command = this.interpolate(config.command, context);
|
|
943
|
+
console.log(`[NetworkRuntime] Executing command: ${command}`);
|
|
944
|
+
const { exec, spawn } = await import("child_process");
|
|
945
|
+
if (config.background) {
|
|
946
|
+
const parts = command.split(" ");
|
|
947
|
+
const cmd = parts[0];
|
|
948
|
+
const args = parts.slice(1);
|
|
949
|
+
if (!cmd) {
|
|
950
|
+
throw new Error("Command is required");
|
|
951
|
+
}
|
|
952
|
+
const subprocess = spawn(cmd, args, {
|
|
953
|
+
detached: true,
|
|
954
|
+
stdio: "ignore"
|
|
955
|
+
});
|
|
956
|
+
subprocess.unref();
|
|
957
|
+
console.log(`[NetworkRuntime] Started background process with PID: ${subprocess.pid}`);
|
|
958
|
+
return {
|
|
959
|
+
success: true,
|
|
960
|
+
pid: subprocess.pid,
|
|
961
|
+
message: "Background process started"
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
return new Promise((resolve, reject) => {
|
|
965
|
+
exec(command, (error, stdout, stderr) => {
|
|
966
|
+
if (error) {
|
|
967
|
+
console.error(`[NetworkRuntime] Command failed: ${error.message}`);
|
|
968
|
+
return resolve({
|
|
969
|
+
success: false,
|
|
970
|
+
error: error.message,
|
|
971
|
+
stderr
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
console.log(`[NetworkRuntime] Command output:
|
|
975
|
+
${stdout}`);
|
|
976
|
+
resolve({
|
|
977
|
+
success: true,
|
|
978
|
+
stdout,
|
|
979
|
+
stderr
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
export {
|
|
986
|
+
NetworkRuntime
|
|
987
|
+
};
|