@nekzus/liop 2.1.0 → 2.3.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 (100) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +5 -0
  3. package/README.md +73 -5
  4. package/dist/bin/agent.js +303 -4
  5. package/dist/bin/agent.js.map +1 -1
  6. package/dist/bridge.d.ts +2 -2
  7. package/dist/bridge.js +4 -1
  8. package/dist/chunk-32ADSAJS.js +104 -0
  9. package/dist/chunk-32ADSAJS.js.map +1 -0
  10. package/dist/chunk-4LQPDN3W.js +556 -0
  11. package/dist/chunk-4LQPDN3W.js.map +1 -0
  12. package/dist/chunk-72MNYFR6.js +64 -0
  13. package/dist/chunk-72MNYFR6.js.map +1 -0
  14. package/dist/chunk-74CISO2I.js +853 -0
  15. package/dist/chunk-74CISO2I.js.map +1 -0
  16. package/dist/chunk-CRZBVFDV.js +616 -0
  17. package/dist/chunk-CRZBVFDV.js.map +1 -0
  18. package/dist/chunk-GKV6M2PQ.js +208106 -0
  19. package/dist/chunk-GKV6M2PQ.js.map +1 -0
  20. package/dist/chunk-JIJPZFR2.js +51 -0
  21. package/dist/chunk-JIJPZFR2.js.map +1 -0
  22. package/dist/chunk-OIMJZ64E.js +441 -0
  23. package/dist/chunk-OIMJZ64E.js.map +1 -0
  24. package/dist/chunk-OUUTDSOW.js +24 -0
  25. package/dist/chunk-OUUTDSOW.js.map +1 -0
  26. package/dist/chunk-PYKRQAER.js +77 -0
  27. package/dist/chunk-PYKRQAER.js.map +1 -0
  28. package/dist/chunk-QLCOEP5J.js +68 -0
  29. package/dist/chunk-QLCOEP5J.js.map +1 -0
  30. package/dist/chunk-RWRRBYG4.js +1 -0
  31. package/dist/chunk-SONUORBT.js +421 -0
  32. package/dist/chunk-SONUORBT.js.map +1 -0
  33. package/dist/chunk-UBOQZVV5.js +3508 -0
  34. package/dist/chunk-UBOQZVV5.js.map +1 -0
  35. package/dist/chunk-UCJEK75U.js +714 -0
  36. package/dist/chunk-UCJEK75U.js.map +1 -0
  37. package/dist/chunk-WSW6YJNY.js +168 -0
  38. package/dist/chunk-WSW6YJNY.js.map +1 -0
  39. package/dist/client.d.ts +3 -2
  40. package/dist/client.js +9 -1
  41. package/dist/gateway.d.ts +5 -110
  42. package/dist/gateway.js +10 -1
  43. package/dist/hybrid-CF__0YnC.d.ts +170 -0
  44. package/dist/{index-DO97j6hP.d.ts → index-BW95kAL4.d.ts} +72 -2
  45. package/dist/index-cMV0_yD9.d.ts +200 -0
  46. package/dist/index.d.ts +273 -18
  47. package/dist/index.js +311 -4
  48. package/dist/index.js.map +1 -1
  49. package/dist/kyber-3ULIJSE3.js +3 -0
  50. package/dist/{kyber-NONMBQNH.js.map → kyber-3ULIJSE3.js.map} +1 -1
  51. package/dist/mesh.d.ts +14 -0
  52. package/dist/mesh.js +4 -1
  53. package/dist/server.d.ts +2 -2
  54. package/dist/server.js +7 -1
  55. package/dist/{types-DzEXgi4s.d.ts → types-lJIxBlWF.d.ts} +75 -47
  56. package/dist/types.d.ts +1 -1
  57. package/dist/types.js +2 -1
  58. package/dist/verifier-3FAKCFNN.js +5 -0
  59. package/dist/{verifier-XU2DB56Z.js.map → verifier-3FAKCFNN.js.map} +1 -1
  60. package/dist/workers/logic-execution.d.ts +1 -0
  61. package/dist/workers/logic-execution.js +271 -1
  62. package/dist/workers/logic-execution.js.map +1 -1
  63. package/dist/workers/zk-verifier.js +173 -1
  64. package/dist/workers/zk-verifier.js.map +1 -1
  65. package/package.json +98 -49
  66. package/dist/chunk-2MGFSIXN.js +0 -2
  67. package/dist/chunk-2MGFSIXN.js.map +0 -1
  68. package/dist/chunk-4C666HHU.js +0 -2
  69. package/dist/chunk-4C666HHU.js.map +0 -1
  70. package/dist/chunk-ANFXJGMP.js +0 -2
  71. package/dist/chunk-ANFXJGMP.js.map +0 -1
  72. package/dist/chunk-DBXGYHKY.js +0 -2
  73. package/dist/chunk-DBXGYHKY.js.map +0 -1
  74. package/dist/chunk-DQ6UW6L7.js +0 -2
  75. package/dist/chunk-DQ6UW6L7.js.map +0 -1
  76. package/dist/chunk-GI2LSJYZ.js +0 -13
  77. package/dist/chunk-GI2LSJYZ.js.map +0 -1
  78. package/dist/chunk-GJUZGKZW.js +0 -3
  79. package/dist/chunk-GJUZGKZW.js.map +0 -1
  80. package/dist/chunk-I46YEWND.js +0 -33
  81. package/dist/chunk-I46YEWND.js.map +0 -1
  82. package/dist/chunk-PWCXZWSE.js +0 -2
  83. package/dist/chunk-PWCXZWSE.js.map +0 -1
  84. package/dist/chunk-RYYRR4N5.js +0 -31
  85. package/dist/chunk-RYYRR4N5.js.map +0 -1
  86. package/dist/chunk-S6RJHZV2.js +0 -2
  87. package/dist/chunk-S6RJHZV2.js.map +0 -1
  88. package/dist/chunk-SB5XJXKV.js +0 -2
  89. package/dist/chunk-SB5XJXKV.js.map +0 -1
  90. package/dist/chunk-T3L6OCM3.js +0 -3
  91. package/dist/chunk-T3L6OCM3.js.map +0 -1
  92. package/dist/chunk-TYVG6TXQ.js +0 -2
  93. package/dist/chunk-TYVG6TXQ.js.map +0 -1
  94. package/dist/chunk-UCCGDGHE.js +0 -54
  95. package/dist/chunk-UCCGDGHE.js.map +0 -1
  96. package/dist/chunk-V5MKJT6S.js +0 -2
  97. package/dist/chunk-V5MKJT6S.js.map +0 -1
  98. package/dist/index-Brfvxmdt.d.ts +0 -101
  99. package/dist/kyber-NONMBQNH.js +0 -2
  100. package/dist/verifier-XU2DB56Z.js +0 -2
@@ -0,0 +1,3508 @@
1
+ import { sanitizeOutput } from './chunk-JIJPZFR2.js';
2
+ import { LIOP_SCOPES, Dilithium65Wrapper, authorizeRequest, egressBlocksTotal, toolCallsTotal, fuelConsumed } from './chunk-OIMJZ64E.js';
3
+ import { GRPC_CHANNEL_OPTIONS, liopV1, createServerCredentials } from './chunk-WSW6YJNY.js';
4
+ import { MeshNode } from './chunk-74CISO2I.js';
5
+ import { log } from './chunk-72MNYFR6.js';
6
+ import { Buffer } from 'buffer';
7
+ import * as crypto3 from 'crypto';
8
+ import crypto3__default from 'crypto';
9
+ import * as fs2 from 'fs';
10
+ import { createRequire } from 'module';
11
+ import path from 'path';
12
+ import { fileURLToPath, pathToFileURL } from 'url';
13
+ import * as grpc2 from '@grpc/grpc-js';
14
+ import { createMlKem768 } from 'mlkem';
15
+ import { Piscina, FixedQueue } from 'piscina';
16
+ import { z } from 'zod';
17
+ import * as jose from 'jose';
18
+ import Provider from 'oidc-provider';
19
+ import * as acorn from 'acorn';
20
+ import { simple } from 'acorn-walk';
21
+
22
+ var LiopRpcServer = class {
23
+ server;
24
+ constructor() {
25
+ this.server = new grpc2.Server(GRPC_CHANNEL_OPTIONS);
26
+ }
27
+ addService(handlers) {
28
+ this.server.addService(liopV1.LogicMesh.service, {
29
+ NegotiateIntent: handlers.negotiateIntent,
30
+ ExecuteLogic: handlers.executeLogic
31
+ });
32
+ }
33
+ async listen(port = 50051, tls) {
34
+ const credentials = createServerCredentials(tls);
35
+ return new Promise((resolve, reject) => {
36
+ this.server.bindAsync(
37
+ `0.0.0.0:${port}`,
38
+ credentials,
39
+ (error, assignedPort) => {
40
+ if (error) {
41
+ reject(error);
42
+ return;
43
+ }
44
+ log.info(`[LIOP-RPC] Server listening on port ${assignedPort}`);
45
+ resolve(assignedPort);
46
+ }
47
+ );
48
+ });
49
+ }
50
+ async gracefulShutdown(timeoutMs = 5e3) {
51
+ return new Promise((resolve) => {
52
+ let resolved = false;
53
+ const timer = setTimeout(() => {
54
+ if (!resolved) {
55
+ resolved = true;
56
+ log.warn(
57
+ `[LIOP-RPC] tryShutdown timed out after ${timeoutMs}ms \u2014 forcing shutdown`
58
+ );
59
+ this.server.forceShutdown();
60
+ resolve();
61
+ }
62
+ }, timeoutMs);
63
+ this.server.tryShutdown(() => {
64
+ if (!resolved) {
65
+ resolved = true;
66
+ clearTimeout(timer);
67
+ log.info("[LIOP-RPC] Server gracefully shut down");
68
+ resolve();
69
+ }
70
+ });
71
+ });
72
+ }
73
+ async stop() {
74
+ return this.gracefulShutdown(5e3);
75
+ }
76
+ };
77
+ var GENESIS_HASH = "0".repeat(64);
78
+ function canonicalize(obj) {
79
+ const sortedKeys = Object.keys(obj).sort();
80
+ const canonicalObj = {};
81
+ for (const key of sortedKeys) {
82
+ canonicalObj[key] = obj[key];
83
+ }
84
+ return JSON.stringify(canonicalObj);
85
+ }
86
+ function computeEntryHash(entryWithoutHash) {
87
+ const canonical = canonicalize(
88
+ entryWithoutHash
89
+ );
90
+ return crypto3.createHash("sha256").update(canonical).digest("hex");
91
+ }
92
+ var AuditLogger = class {
93
+ entries = [];
94
+ lastEntryHash = GENESIS_HASH;
95
+ filePath;
96
+ constructor(filePath) {
97
+ this.filePath = filePath;
98
+ if (this.filePath && fs2.existsSync(this.filePath)) {
99
+ this.loadFromFile(this.filePath);
100
+ }
101
+ }
102
+ recordExecution(params) {
103
+ const id = crypto3.randomUUID();
104
+ const timestamp = params.timestamp || (/* @__PURE__ */ new Date()).toISOString();
105
+ const traceId = params.traceId || crypto3.randomBytes(16).toString("hex");
106
+ const datasetHash = params.datasetHash || crypto3.createHash("sha256").update("").digest("hex");
107
+ const outputHash = params.outputHash || crypto3.createHash("sha256").update("").digest("hex");
108
+ const zkReceiptSig = params.zkReceiptSig || "";
109
+ const partialEntry = {
110
+ id,
111
+ timestamp,
112
+ traceId,
113
+ agentDid: params.agentDid,
114
+ peerId: params.peerId,
115
+ toolName: params.toolName,
116
+ datasetHash,
117
+ fuelConsumed: params.fuelConsumed,
118
+ outputHash,
119
+ zkReceiptSig,
120
+ status: params.status,
121
+ prevEntryHash: this.lastEntryHash
122
+ };
123
+ const entryHash = computeEntryHash(partialEntry);
124
+ const fullEntry = {
125
+ ...partialEntry,
126
+ entryHash
127
+ };
128
+ this.entries.push(fullEntry);
129
+ this.lastEntryHash = entryHash;
130
+ if (this.filePath) {
131
+ fs2.appendFileSync(
132
+ this.filePath,
133
+ `${JSON.stringify(fullEntry)}
134
+ `,
135
+ "utf8"
136
+ );
137
+ }
138
+ return fullEntry;
139
+ }
140
+ getEntries() {
141
+ return [...this.entries];
142
+ }
143
+ getEntryCount() {
144
+ return this.entries.length;
145
+ }
146
+ verifyIntegrity() {
147
+ let expectedPrevHash = GENESIS_HASH;
148
+ for (let i = 0; i < this.entries.length; i++) {
149
+ const entry = this.entries[i];
150
+ if (entry.prevEntryHash !== expectedPrevHash) {
151
+ return {
152
+ valid: false,
153
+ totalEntries: this.entries.length,
154
+ brokenIndex: i,
155
+ reason: `Broken hash chain at index ${i}: expected prevHash ${expectedPrevHash}, got ${entry.prevEntryHash}`
156
+ };
157
+ }
158
+ const { entryHash, ...withoutHash } = entry;
159
+ const recalculated = computeEntryHash(withoutHash);
160
+ if (recalculated !== entryHash) {
161
+ return {
162
+ valid: false,
163
+ totalEntries: this.entries.length,
164
+ brokenIndex: i,
165
+ reason: `Tampered entry at index ${i}: hash mismatch (expected ${recalculated}, stored ${entryHash})`
166
+ };
167
+ }
168
+ expectedPrevHash = entryHash;
169
+ }
170
+ return {
171
+ valid: true,
172
+ totalEntries: this.entries.length
173
+ };
174
+ }
175
+ exportJsonl() {
176
+ return this.entries.map((e) => JSON.stringify(e)).join("\n");
177
+ }
178
+ loadFromFile(path2) {
179
+ const content = fs2.readFileSync(path2, "utf8");
180
+ const lines = content.split("\n").filter((l) => l.trim().length > 0);
181
+ for (const line of lines) {
182
+ const entry = JSON.parse(line);
183
+ this.entries.push(entry);
184
+ this.lastEntryHash = entry.entryHash;
185
+ }
186
+ }
187
+ };
188
+ var globalAuditLogger = new AuditLogger();
189
+
190
+ // src/security/auth-config.ts
191
+ var AUTH_DEFAULTS = {
192
+ /** JWT audience claim for the LIOP mesh API. */
193
+ audience: "urn:liop:mesh:api",
194
+ /** M2M token time-to-live in seconds (1 hour). */
195
+ tokenTtlSeconds: 3600,
196
+ /** JWKS cache TTL in milliseconds (10 min — jose default). */
197
+ jwksCacheTtlMs: 6e5,
198
+ /** Minimum interval between JWKS refetches (30s — jose default). */
199
+ jwksCooldownMs: 3e4,
200
+ /** Clock tolerance for JWT expiration checks (mesh clock skew). */
201
+ clockToleranceSec: 5,
202
+ /** JWT signing algorithm (aligned with libp2p Ed25519 PeerID curve). */
203
+ signingAlgorithm: "EdDSA"
204
+ };
205
+ var JwtValidator = class {
206
+ jwksResolver;
207
+ issuer;
208
+ audience;
209
+ constructor(issuer, audience, jwksSource) {
210
+ this.issuer = issuer;
211
+ this.audience = audience;
212
+ if (jwksSource instanceof URL) {
213
+ this.jwksResolver = jose.createRemoteJWKSet(jwksSource, {
214
+ cacheMaxAge: AUTH_DEFAULTS.jwksCacheTtlMs,
215
+ cooldownDuration: AUTH_DEFAULTS.jwksCooldownMs
216
+ });
217
+ } else {
218
+ this.jwksResolver = jose.createLocalJWKSet(jwksSource);
219
+ }
220
+ }
221
+ /**
222
+ * Validates a JWT access token and extracts authorization context.
223
+ *
224
+ * Checks performed (jose.jwtVerify):
225
+ * - Cryptographic signature verification (EdDSA or ES256)
226
+ * - Issuer claim matches configured issuer
227
+ * - Audience claim matches configured audience
228
+ * - Token is not expired (with clock tolerance for mesh skew)
229
+ * - Required claims (sub, scope) are present
230
+ *
231
+ * @throws JOSEError if validation fails (expired, wrong issuer, bad signature, etc.)
232
+ */
233
+ async validate(token) {
234
+ const issuers = this.buildIssuerAliases();
235
+ const { payload } = await jose.jwtVerify(token, this.jwksResolver, {
236
+ issuer: issuers.length > 1 ? issuers : this.issuer,
237
+ audience: this.audience,
238
+ // Algorithm whitelist: only accept EdDSA (Ed25519) or ES256.
239
+ // Prevents algorithm confusion attacks (OWASP API-A01).
240
+ algorithms: [AUTH_DEFAULTS.signingAlgorithm, "ES256"],
241
+ // Clock tolerance for P2P mesh nodes with slight time drift.
242
+ clockTolerance: AUTH_DEFAULTS.clockToleranceSec,
243
+ // Enforce required claims to prevent malformed tokens.
244
+ requiredClaims: ["sub", "scope"]
245
+ });
246
+ return {
247
+ token,
248
+ clientId: payload.sub ?? "unknown",
249
+ scopes: typeof payload.scope === "string" ? payload.scope.split(" ") : [],
250
+ expiresAt: payload.exp
251
+ };
252
+ }
253
+ /**
254
+ * Builds a complete set of issuer URL aliases for the LIOP demo topology.
255
+ *
256
+ * The LIOP mesh runs on Docker Desktop with the following network layout:
257
+ * Container hostnames: "nexus", "liop-nexus" (internal port 3000)
258
+ * Host-published ports: 13000 (HTTP/MCP), 13001 (libp2p TCP)
259
+ * Loopback addresses: 127.0.0.1, localhost
260
+ *
261
+ * The Nexus OAuth server may set its issuer to any of these depending on
262
+ * how it was configured, and nodes may resolve it via LIOP_NEXUS_URL
263
+ * which varies by context. This method generates all equivalent aliases
264
+ * so that jose.jwtVerify accepts the token regardless of which alias
265
+ * was used as `iss` in the JWT.
266
+ *
267
+ * Security note: This does NOT weaken validation — the cryptographic
268
+ * signature is still verified against the same JWKS keys. Only the
269
+ * string comparison of the `iss` claim is relaxed across known aliases.
270
+ */
271
+ buildIssuerAliases() {
272
+ const issuers = [this.issuer];
273
+ const cleanIssuer = this.issuer.endsWith("/") ? this.issuer.slice(0, -1) : this.issuer;
274
+ const NEXUS_AUTHORITIES = [
275
+ "nexus:3000",
276
+ "liop-nexus:3000",
277
+ "127.0.0.1:3000",
278
+ "localhost:3000",
279
+ "127.0.0.1:13000",
280
+ "localhost:13000",
281
+ "127.0.0.1:13001",
282
+ "localhost:13001"
283
+ ];
284
+ let issuerAuthority = "";
285
+ try {
286
+ const url = new URL(cleanIssuer);
287
+ issuerAuthority = `${url.hostname}:${url.port || "3000"}`;
288
+ } catch {
289
+ return issuers;
290
+ }
291
+ const isNexusIssuer = NEXUS_AUTHORITIES.some(
292
+ (authority) => issuerAuthority === authority
293
+ );
294
+ if (!isNexusIssuer) return issuers;
295
+ const pathSuffix = cleanIssuer.replace(/^https?:\/\/[^/]+/, "");
296
+ for (const authority of NEXUS_AUTHORITIES) {
297
+ const alias = `http://${authority}${pathSuffix}`;
298
+ if (!issuers.includes(alias)) {
299
+ issuers.push(alias);
300
+ }
301
+ }
302
+ if (pathSuffix) {
303
+ for (const authority of NEXUS_AUTHORITIES) {
304
+ const alias = `http://${authority}`;
305
+ if (!issuers.includes(alias)) {
306
+ issuers.push(alias);
307
+ }
308
+ }
309
+ } else {
310
+ for (const authority of NEXUS_AUTHORITIES) {
311
+ const alias = `http://${authority}/oidc`;
312
+ if (!issuers.includes(alias)) {
313
+ issuers.push(alias);
314
+ }
315
+ }
316
+ }
317
+ return issuers;
318
+ }
319
+ /**
320
+ * Returns the configured issuer URL for PRM (RFC 9728) metadata.
321
+ */
322
+ getIssuer() {
323
+ return this.issuer;
324
+ }
325
+ /**
326
+ * Returns the configured audience for PRM metadata.
327
+ */
328
+ getAudience() {
329
+ return this.audience;
330
+ }
331
+ };
332
+ function createOAuthServer(config) {
333
+ const { privateKey, publicKey } = crypto3__default.generateKeyPairSync("ed25519");
334
+ const privateJwk = privateKey.export({ format: "jwk" });
335
+ const publicJwk = publicKey.export({ format: "jwk" });
336
+ const kid = crypto3__default.createHash("sha256").update(publicJwk.x || "").digest("hex").slice(0, 16);
337
+ const privateJwkComplete = {
338
+ ...privateJwk,
339
+ kid,
340
+ use: "sig",
341
+ alg: "EdDSA"
342
+ };
343
+ const publicJwkComplete = {
344
+ ...publicJwk,
345
+ kid,
346
+ use: "sig",
347
+ alg: "EdDSA"
348
+ };
349
+ const jwksInternal = {
350
+ keys: [privateJwkComplete]
351
+ };
352
+ const jwksPublic = {
353
+ keys: [publicJwkComplete]
354
+ };
355
+ const oidcConfig = {
356
+ // Supported scopes at the Authorization Server level (RFC 6749)
357
+ scopes: ["openid", "offline_access", ...LIOP_SCOPES],
358
+ // Define registered Machine-to-Machine clients
359
+ clients: config.clients.map((c) => ({
360
+ client_id: c.client_id,
361
+ client_secret: c.client_secret,
362
+ grant_types: ["client_credentials"],
363
+ response_types: [],
364
+ // CC Grant does not use response types
365
+ redirect_uris: [],
366
+ // M2M flows require no redirects
367
+ scope: c.scope,
368
+ token_endpoint_auth_method: "client_secret_post",
369
+ id_token_signed_response_alg: "EdDSA"
370
+ })),
371
+ // [SEC] Features whitelist: Enable Client Credentials, disable all human interactions
372
+ features: {
373
+ clientCredentials: { enabled: true },
374
+ devInteractions: { enabled: false },
375
+ // Prevent development interactions UI
376
+ resourceIndicators: {
377
+ enabled: true,
378
+ useGrantedResource: () => true,
379
+ getResourceServerInfo: (_ctx, _resource, client) => {
380
+ return {
381
+ scope: client.scope || "",
382
+ accessTokenFormat: "jwt",
383
+ jwt: {
384
+ sign: { alg: "EdDSA" }
385
+ }
386
+ };
387
+ }
388
+ }
389
+ },
390
+ // [SEC] Emit M2M Access Tokens as JWTs instead of opaque strings
391
+ // This enables high-performance local validation at resource servers (NIST SP 800-207)
392
+ formats: {
393
+ AccessToken: "jwt"
394
+ // biome-ignore lint/suspicious/noExplicitAny: library typings mismatch in oidc-provider
395
+ },
396
+ // [SEC] Lockout: Throw immediate error on any interactive login flow attempt
397
+ interactions: {
398
+ url: () => {
399
+ throw new Error(
400
+ "InteractionsNotSupportedException: This Authorization Server is strictly configured for Machine-to-Machine flows."
401
+ );
402
+ }
403
+ },
404
+ // Keys used for signing Issued Tokens (ID Tokens, Access Tokens)
405
+ jwks: jwksInternal,
406
+ // Token life configurations (NIST SP 800-63B token rotation guidelines)
407
+ ttl: {
408
+ ClientCredentials: 3600
409
+ // Access Token valid for 1 hour
410
+ },
411
+ // Allow HTTP locally for development/Docker testnets (production MUST use HTTPS in proxy)
412
+ cookies: {
413
+ keys: [crypto3__default.randomBytes(32).toString("hex")]
414
+ },
415
+ // Map scopes directly into the JWT token claims during client_credentials grant
416
+ extraTokenClaims: (_ctx, token) => {
417
+ if (token.kind === "AccessToken") {
418
+ return {
419
+ scope: token.scope
420
+ };
421
+ }
422
+ return {};
423
+ }
424
+ };
425
+ const normalizedIssuer = config.issuer.endsWith("/") ? config.issuer.slice(0, -1) : config.issuer;
426
+ const provider = new Provider(normalizedIssuer, oidcConfig);
427
+ provider.proxy = true;
428
+ return {
429
+ provider,
430
+ jwks: jwksPublic
431
+ };
432
+ }
433
+ var TaintAnalyzer = class _TaintAnalyzer {
434
+ piiFields;
435
+ sensitiveKeys;
436
+ /** String methods that extract character-level information from PII */
437
+ static TAINT_PROPAGATING_METHODS = /* @__PURE__ */ new Set([
438
+ // Character extraction
439
+ "charCodeAt",
440
+ "codePointAt",
441
+ "charAt",
442
+ "at",
443
+ // Search/position (reveals content structure)
444
+ "indexOf",
445
+ "lastIndexOf",
446
+ "search",
447
+ // Comparison (reveals ordering/content)
448
+ "localeCompare",
449
+ "startsWith",
450
+ "endsWith",
451
+ "includes",
452
+ // Transformation (preserves PII content in different form)
453
+ "substring",
454
+ "slice",
455
+ "substr",
456
+ "split",
457
+ "match",
458
+ "matchAll",
459
+ "replace",
460
+ "replaceAll",
461
+ "normalize",
462
+ "toLowerCase",
463
+ "toUpperCase",
464
+ "trim",
465
+ "trimStart",
466
+ "trimEnd",
467
+ "padStart",
468
+ "padEnd",
469
+ "repeat"
470
+ ]);
471
+ /** Array iteration methods whose callbacks receive individual records */
472
+ static ARRAY_CALLBACK_METHODS = /* @__PURE__ */ new Set([
473
+ "map",
474
+ "forEach",
475
+ "filter",
476
+ "find",
477
+ "some",
478
+ "every",
479
+ "flatMap",
480
+ "findIndex"
481
+ ]);
482
+ /** Reduce-family methods where the record param is the SECOND callback arg */
483
+ static REDUCE_METHODS = /* @__PURE__ */ new Set(["reduce", "reduceRight"]);
484
+ constructor(piiFields, sensitiveKeys = []) {
485
+ this.piiFields = new Set(piiFields.map((f) => f.toLowerCase()));
486
+ this.sensitiveKeys = new Set(sensitiveKeys.map((f) => f.toLowerCase()));
487
+ }
488
+ /**
489
+ * Classifies a field into forbidden, sensitive, or public tiers (NIST SP 800-226).
490
+ */
491
+ classifyField(field, policySensitiveKeys = []) {
492
+ const lowerField = field.toLowerCase();
493
+ if (this.piiFields.has(lowerField)) {
494
+ return "forbidden";
495
+ }
496
+ const lowerPolicyKeys = new Set(
497
+ policySensitiveKeys.map((k) => k.toLowerCase())
498
+ );
499
+ if (this.sensitiveKeys.has(lowerField) || lowerPolicyKeys.has(lowerField)) {
500
+ return "sensitive";
501
+ }
502
+ return "public";
503
+ }
504
+ /**
505
+ * Analyzes injected source code for PII taint violations.
506
+ *
507
+ * @param sourceCode - The raw JavaScript logic extracted from the LIOP envelope
508
+ * @param recordCount - Size of source dataset (enables correlation/min-max gates for small sets)
509
+ * @param minMaxBlockThreshold - Threshold below which extrema/correlation extraction is blocked
510
+ * @returns A TaintViolation if PII-derived values flow to output, null if clean
511
+ */
512
+ analyze(sourceCode, recordCount, minMaxBlockThreshold = 50) {
513
+ let ast;
514
+ try {
515
+ const wrapped = `function liop_analysis_wrapper(env) {
516
+ ${sourceCode}
517
+ }`;
518
+ ast = acorn.parse(wrapped, {
519
+ ecmaVersion: 2022,
520
+ sourceType: "script",
521
+ locations: true
522
+ });
523
+ } catch {
524
+ return null;
525
+ }
526
+ const recordBoundVars = /* @__PURE__ */ new Set();
527
+ const taintedVars = /* @__PURE__ */ new Set();
528
+ this.identifyRecordBoundVars(ast, recordBoundVars);
529
+ this.propagateTaint(ast, recordBoundVars, taintedVars);
530
+ const taintResult = this.checkReturnStatements(
531
+ ast,
532
+ recordBoundVars,
533
+ taintedVars
534
+ );
535
+ if (taintResult) return taintResult;
536
+ if (recordCount !== void 0 && recordCount > 0 && recordCount < minMaxBlockThreshold) {
537
+ const correlationResult = this.detectCorrelatedAggregations(ast);
538
+ if (correlationResult) {
539
+ correlationResult.reason = correlationResult.reason.replace(
540
+ "50 records",
541
+ `${minMaxBlockThreshold} records`
542
+ );
543
+ return correlationResult;
544
+ }
545
+ }
546
+ if (recordCount !== void 0 && recordCount > 0 && recordCount < minMaxBlockThreshold) {
547
+ const minMaxResult = this.detectMinMaxExtraction(ast);
548
+ if (minMaxResult) {
549
+ minMaxResult.reason = minMaxResult.reason.replace(
550
+ "50 records",
551
+ `${minMaxBlockThreshold} records`
552
+ );
553
+ return minMaxResult;
554
+ }
555
+ }
556
+ return null;
557
+ }
558
+ /**
559
+ * Extracts all unique field names accessed via env.records operations.
560
+ * Used by the Query Budget to enforce per-field query limits.
561
+ */
562
+ extractQueriedFields(sourceCode) {
563
+ let ast;
564
+ try {
565
+ ast = acorn.parse(`function w(env) {
566
+ ${sourceCode}
567
+ }`, {
568
+ ecmaVersion: 2022,
569
+ sourceType: "script"
570
+ });
571
+ } catch {
572
+ return [];
573
+ }
574
+ const recordBoundVars = /* @__PURE__ */ new Set();
575
+ this.identifyRecordBoundVars(ast, recordBoundVars);
576
+ const fields = /* @__PURE__ */ new Set();
577
+ const visitors = {
578
+ MemberExpression: (node) => {
579
+ const propName = this.getPropertyName(node);
580
+ if (!propName || propName === "length") return;
581
+ if (node.object.type === "Identifier" && recordBoundVars.has(node.object.name)) {
582
+ fields.add(propName);
583
+ }
584
+ if (node.object.type === "MemberExpression") {
585
+ const parentMember = node.object;
586
+ if (parentMember.computed && this.isEnvRecordsAccess(parentMember.object)) {
587
+ fields.add(propName);
588
+ }
589
+ }
590
+ }
591
+ };
592
+ simple(ast, visitors);
593
+ return Array.from(fields);
594
+ }
595
+ // ── Pass 4: Correlation Guard ─────────────────────────────────────
596
+ /**
597
+ * Detects when 2+ reduce/aggregation calls access the same field.
598
+ * This prevents differencing attacks: sum(all.field) - sum(excl1.field) = individual value.
599
+ * Exempt: .length access (metadata, not field access).
600
+ */
601
+ detectCorrelatedAggregations(ast) {
602
+ const fieldAggCounts = /* @__PURE__ */ new Map();
603
+ const visitors = {
604
+ CallExpression: (node) => {
605
+ if (node.callee.type !== "MemberExpression") return;
606
+ const callee = node.callee;
607
+ const methodName = this.getPropertyName(callee);
608
+ if (!methodName || !_TaintAnalyzer.REDUCE_METHODS.has(methodName))
609
+ return;
610
+ if (!this.isEnvRecordsChain(callee.object)) return;
611
+ const callback = node.arguments[0];
612
+ if (!callback || callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression")
613
+ return;
614
+ const fn = callback;
615
+ const recordParam = fn.params.length > 1 ? fn.params[1] : fn.params[0];
616
+ if (recordParam?.type !== "Identifier") return;
617
+ const paramName = recordParam.name;
618
+ const fields = this.extractFieldsFromBody(
619
+ fn.body,
620
+ paramName
621
+ );
622
+ for (const field of fields) {
623
+ const current = fieldAggCounts.get(field) ?? 0;
624
+ fieldAggCounts.set(field, current + 1);
625
+ }
626
+ }
627
+ };
628
+ simple(ast, visitors);
629
+ for (const [field, count] of fieldAggCounts) {
630
+ if (count >= 2) {
631
+ return {
632
+ reason: `Correlation guard: ${count} aggregations detected on field '${field}'. Multiple correlated aggregations on the same field can enable differencing attacks. Use a single aggregation per numeric field, or increase dataset size above 50 records.`
633
+ };
634
+ }
635
+ }
636
+ return null;
637
+ }
638
+ /**
639
+ * Checks if a node is env.records or a chain like env.records.slice(N) / env.records.filter(...)
640
+ */
641
+ isEnvRecordsChain(node) {
642
+ if (this.isEnvRecordsAccess(node)) return true;
643
+ if (node.type === "CallExpression") {
644
+ const call = node;
645
+ if (call.callee.type === "MemberExpression") {
646
+ const member = call.callee;
647
+ const method = this.getPropertyName(member);
648
+ if (method && (method === "slice" || method === "filter" || method === "toSorted")) {
649
+ return this.isEnvRecordsChain(member.object);
650
+ }
651
+ }
652
+ }
653
+ return false;
654
+ }
655
+ /**
656
+ * Extracts field names accessed on a record parameter within a function body.
657
+ * e.g., in `(s, r) => s + r.balance`, extracts "balance".
658
+ * Ignores .length as it's metadata, not a field access.
659
+ */
660
+ extractFieldsFromBody(body, paramName) {
661
+ const fields = [];
662
+ const visitors = {
663
+ MemberExpression: (node) => {
664
+ if (node.object.type === "Identifier" && node.object.name === paramName) {
665
+ const prop = this.getPropertyName(node);
666
+ if (prop && prop !== "length") {
667
+ fields.push(prop);
668
+ }
669
+ }
670
+ }
671
+ };
672
+ simple(body, visitors);
673
+ return fields;
674
+ }
675
+ // ── Pass 5: Min/Max Gate ──────────────────────────────────────────
676
+ /**
677
+ * Detects Math.min/max and sort()[0] patterns that expose individual
678
+ * record values from small datasets.
679
+ */
680
+ detectMinMaxExtraction(ast) {
681
+ let violation = null;
682
+ const visitors = {
683
+ CallExpression: (node) => {
684
+ if (violation) return;
685
+ if (node.callee.type === "MemberExpression") {
686
+ const callee = node.callee;
687
+ if (callee.object.type === "Identifier" && callee.object.name === "Math") {
688
+ const method = this.getPropertyName(callee);
689
+ if (method === "min" || method === "max") {
690
+ if (node.arguments.some(
691
+ (arg) => arg.type === "SpreadElement" && this.isRecordsMapCall(
692
+ arg.argument
693
+ )
694
+ )) {
695
+ violation = {
696
+ reason: `Min/Max gate: Math.${method}() on individual records blocked for small datasets (n < 50). Use avg/stddev/count for privacy-safe aggregations.`
697
+ };
698
+ }
699
+ }
700
+ }
701
+ }
702
+ },
703
+ // Pattern: env.records.sort(...)[0].field or [...env.records].sort(...)[0].field
704
+ MemberExpression: (node) => {
705
+ if (violation) return;
706
+ if (node.computed && node.object.type === "CallExpression") {
707
+ const call = node.object;
708
+ if (call.callee.type === "MemberExpression") {
709
+ const method = this.getPropertyName(
710
+ call.callee
711
+ );
712
+ if (method === "sort" || method === "toSorted") {
713
+ const sortTarget = call.callee.object;
714
+ if (this.isEnvRecordsChain(sortTarget)) {
715
+ violation = {
716
+ reason: "Min/Max gate: .sort()[index] on individual records blocked for small datasets (n < 50). Use avg/stddev/count for privacy-safe aggregations."
717
+ };
718
+ }
719
+ }
720
+ }
721
+ }
722
+ }
723
+ };
724
+ simple(ast, visitors);
725
+ return violation;
726
+ }
727
+ /**
728
+ * Checks if a node is env.records.map(callback) — used by Min/Max Gate.
729
+ */
730
+ isRecordsMapCall(node) {
731
+ if (node.type !== "CallExpression") return false;
732
+ const call = node;
733
+ if (call.callee.type !== "MemberExpression") return false;
734
+ const callee = call.callee;
735
+ const method = this.getPropertyName(callee);
736
+ return method === "map" && this.isEnvRecordsChain(callee.object);
737
+ }
738
+ // ── Pass 1: Record-Bound Variable Identification ──────────────────
739
+ identifyRecordBoundVars(ast, recordBoundVars) {
740
+ const visitors = {
741
+ CallExpression: (node) => {
742
+ if (node.callee.type !== "MemberExpression") return;
743
+ const member = node.callee;
744
+ const methodName = this.getPropertyName(member);
745
+ if (!methodName) return;
746
+ if (!this.isEnvRecordsAccess(member.object)) return;
747
+ const callback = node.arguments[0];
748
+ if (!callback) return;
749
+ if (callback.type === "ArrowFunctionExpression" || callback.type === "FunctionExpression") {
750
+ const fn = callback;
751
+ if (_TaintAnalyzer.ARRAY_CALLBACK_METHODS.has(methodName) && fn.params.length > 0) {
752
+ const param = fn.params[0];
753
+ if (param.type === "Identifier") {
754
+ recordBoundVars.add(param.name);
755
+ }
756
+ }
757
+ if (_TaintAnalyzer.REDUCE_METHODS.has(methodName) && fn.params.length > 1) {
758
+ const recordParam = fn.params[1];
759
+ if (recordParam.type === "Identifier") {
760
+ recordBoundVars.add(recordParam.name);
761
+ }
762
+ }
763
+ }
764
+ },
765
+ // for (const r of env.records) → r is record-bound
766
+ ForOfStatement: (node) => {
767
+ if (!this.isEnvRecordsAccess(node.right)) return;
768
+ if (node.left.type === "VariableDeclaration") {
769
+ for (const declarator of node.left.declarations) {
770
+ if (declarator.id.type === "Identifier") {
771
+ recordBoundVars.add(declarator.id.name);
772
+ }
773
+ }
774
+ }
775
+ }
776
+ };
777
+ simple(ast, visitors);
778
+ const indexVisitors = {
779
+ VariableDeclarator: (node) => {
780
+ if (!node.init || node.id.type !== "Identifier") return;
781
+ if (node.init.type === "MemberExpression" && node.init.computed) {
782
+ const member = node.init;
783
+ if (this.isEnvRecordsAccess(member.object)) {
784
+ recordBoundVars.add(node.id.name);
785
+ }
786
+ }
787
+ }
788
+ };
789
+ simple(ast, indexVisitors);
790
+ }
791
+ // ── Pass 2: Taint Propagation ─────────────────────────────────────
792
+ propagateTaint(ast, recordBoundVars, taintedVars) {
793
+ for (let iteration = 0; iteration < 3; iteration++) {
794
+ const sizeBefore = taintedVars.size;
795
+ const visitors = {
796
+ VariableDeclarator: (node) => {
797
+ if (!node.init || node.id.type !== "Identifier") return;
798
+ if (this.isExpressionTainted(node.init, recordBoundVars, taintedVars)) {
799
+ taintedVars.add(node.id.name);
800
+ }
801
+ },
802
+ AssignmentExpression: (node) => {
803
+ if (node.left.type !== "Identifier") return;
804
+ if (this.isExpressionTainted(node.right, recordBoundVars, taintedVars)) {
805
+ taintedVars.add(node.left.name);
806
+ }
807
+ },
808
+ FunctionDeclaration: (node) => {
809
+ if (node.id && node.id.type === "Identifier") {
810
+ if (this.doesCallbackProduceTaint(
811
+ node,
812
+ null,
813
+ recordBoundVars,
814
+ taintedVars
815
+ )) {
816
+ taintedVars.add(node.id.name);
817
+ }
818
+ }
819
+ },
820
+ // Imperative taint: array.push(taintedValue) contaminates the array
821
+ // Covers for-of and forEach patterns that push PII-derived values
822
+ CallExpression: (node) => {
823
+ if (node.callee.type !== "MemberExpression") return;
824
+ const callee = node.callee;
825
+ const methodName = this.getPropertyName(callee);
826
+ if (methodName === "push" && callee.object.type === "Identifier" && node.arguments.some(
827
+ (arg) => this.isExpressionTainted(arg, recordBoundVars, taintedVars)
828
+ )) {
829
+ taintedVars.add(callee.object.name);
830
+ }
831
+ }
832
+ };
833
+ simple(ast, visitors);
834
+ if (taintedVars.size === sizeBefore) break;
835
+ }
836
+ }
837
+ // ── Pass 3: Return Statement Sink Detection ───────────────────────
838
+ checkReturnStatements(ast, recordBoundVars, taintedVars) {
839
+ let violation = null;
840
+ const visitors = {
841
+ ReturnStatement: (node) => {
842
+ if (violation) return;
843
+ if (!node.argument) return;
844
+ if (this.isExpressionTainted(node.argument, recordBoundVars, taintedVars)) {
845
+ const line = node.loc?.start.line ? node.loc.start.line - 1 : void 0;
846
+ const operation = this.describeTaintSource(
847
+ node.argument,
848
+ recordBoundVars,
849
+ taintedVars
850
+ );
851
+ violation = {
852
+ reason: `PII side-channel detected: output contains values derived from restricted fields. ${operation ? `Operation: ${operation}. ` : ""}Use only non-PII fields (e.g., numeric/date columns) for aggregations.`,
853
+ line,
854
+ operation
855
+ };
856
+ }
857
+ }
858
+ };
859
+ simple(ast, visitors);
860
+ return violation;
861
+ }
862
+ // ── Core Taint Evaluation ─────────────────────────────────────────
863
+ /**
864
+ * Recursively determines if an AST expression produces a tainted value.
865
+ * A value is tainted if it derives from a PII field on a record-bound variable.
866
+ */
867
+ isExpressionTainted(node, recordBoundVars, taintedVars) {
868
+ switch (node.type) {
869
+ case "Identifier":
870
+ return taintedVars.has(node.name);
871
+ case "MemberExpression":
872
+ return this.isMemberExprTainted(
873
+ node,
874
+ recordBoundVars,
875
+ taintedVars
876
+ );
877
+ case "CallExpression":
878
+ return this.isCallExprTainted(
879
+ node,
880
+ recordBoundVars,
881
+ taintedVars
882
+ );
883
+ case "YieldExpression": {
884
+ const yieldNode = node;
885
+ return yieldNode.argument ? this.isExpressionTainted(
886
+ yieldNode.argument,
887
+ recordBoundVars,
888
+ taintedVars
889
+ ) : false;
890
+ }
891
+ case "FunctionExpression":
892
+ case "ArrowFunctionExpression": {
893
+ const fn = node;
894
+ return this.doesCallbackProduceTaint(
895
+ fn,
896
+ null,
897
+ recordBoundVars,
898
+ taintedVars
899
+ );
900
+ }
901
+ case "BinaryExpression":
902
+ case "LogicalExpression": {
903
+ const bin = node;
904
+ return this.isExpressionTainted(bin.left, recordBoundVars, taintedVars) || this.isExpressionTainted(bin.right, recordBoundVars, taintedVars);
905
+ }
906
+ case "UnaryExpression": {
907
+ const unary = node;
908
+ return this.isExpressionTainted(
909
+ unary.argument,
910
+ recordBoundVars,
911
+ taintedVars
912
+ );
913
+ }
914
+ case "ConditionalExpression": {
915
+ const cond = node;
916
+ return this.isExpressionTainted(cond.test, recordBoundVars, taintedVars) || this.isExpressionTainted(
917
+ cond.consequent,
918
+ recordBoundVars,
919
+ taintedVars
920
+ ) || this.isExpressionTainted(cond.alternate, recordBoundVars, taintedVars);
921
+ }
922
+ case "ObjectExpression": {
923
+ const obj = node;
924
+ return obj.properties.some(
925
+ (prop) => prop.type === "Property" && this.isExpressionTainted(prop.value, recordBoundVars, taintedVars)
926
+ );
927
+ }
928
+ case "ArrayExpression": {
929
+ const arr = node;
930
+ return arr.elements.some(
931
+ (el) => el !== null && this.isExpressionTainted(el, recordBoundVars, taintedVars)
932
+ );
933
+ }
934
+ case "TemplateLiteral": {
935
+ const tmpl = node;
936
+ return tmpl.expressions.some(
937
+ (expr) => this.isExpressionTainted(expr, recordBoundVars, taintedVars)
938
+ );
939
+ }
940
+ case "SpreadElement": {
941
+ const spread = node;
942
+ return this.isExpressionTainted(
943
+ spread.argument,
944
+ recordBoundVars,
945
+ taintedVars
946
+ );
947
+ }
948
+ default:
949
+ return false;
950
+ }
951
+ }
952
+ /**
953
+ * Checks if a MemberExpression accesses a PII field on a record-bound variable.
954
+ * Examples: r.accountHolder, r["name"], taintedVar.length, taintedVar[0]
955
+ */
956
+ isMemberExprTainted(member, recordBoundVars, taintedVars) {
957
+ const propName = this.getPropertyName(member);
958
+ if (member.object.type === "Identifier" && recordBoundVars.has(member.object.name) && propName && this.piiFields.has(propName.toLowerCase())) {
959
+ return true;
960
+ }
961
+ if (member.object.type === "MemberExpression" && propName && this.piiFields.has(propName.toLowerCase())) {
962
+ const parentMember = member.object;
963
+ if (parentMember.computed && this.isEnvRecordsAccess(parentMember.object)) {
964
+ return true;
965
+ }
966
+ }
967
+ if (this.isExpressionTainted(member.object, recordBoundVars, taintedVars)) {
968
+ return true;
969
+ }
970
+ if (member.computed && member.object.type === "Identifier" && recordBoundVars.has(member.object.name)) {
971
+ if (member.property.type === "Literal") {
972
+ const litVal = member.property.value;
973
+ if (typeof litVal === "string" && this.piiFields.has(litVal.toLowerCase())) {
974
+ return true;
975
+ }
976
+ }
977
+ }
978
+ return false;
979
+ }
980
+ /**
981
+ * Checks if a CallExpression produces a tainted result.
982
+ * Handles: taintedObj.method(), env.records.map(r => r.piiField), etc.
983
+ */
984
+ isCallExprTainted(call, recordBoundVars, taintedVars) {
985
+ if (call.callee.type === "MemberExpression") {
986
+ const callee = call.callee;
987
+ const methodName = this.getPropertyName(callee);
988
+ if (methodName && _TaintAnalyzer.TAINT_PROPAGATING_METHODS.has(methodName) && this.isExpressionTainted(callee.object, recordBoundVars, taintedVars)) {
989
+ return true;
990
+ }
991
+ if (this.isEnvRecordsAccess(callee.object) && call.arguments[0]) {
992
+ const callback = call.arguments[0];
993
+ if (callback.type === "ArrowFunctionExpression" || callback.type === "FunctionExpression") {
994
+ return this.doesCallbackProduceTaint(
995
+ callback,
996
+ methodName,
997
+ recordBoundVars,
998
+ taintedVars
999
+ );
1000
+ }
1001
+ }
1002
+ if (this.isExpressionTainted(callee.object, recordBoundVars, taintedVars)) {
1003
+ return true;
1004
+ }
1005
+ if (call.arguments.some(
1006
+ (arg) => this.isExpressionTainted(arg, recordBoundVars, taintedVars)
1007
+ )) {
1008
+ return true;
1009
+ }
1010
+ }
1011
+ if (call.callee.type === "MemberExpression") {
1012
+ const callee = call.callee;
1013
+ const methodName = this.getPropertyName(callee);
1014
+ if (methodName === "push" && callee.object.type === "Identifier" && call.arguments.some(
1015
+ (arg) => this.isExpressionTainted(arg, recordBoundVars, taintedVars)
1016
+ )) {
1017
+ taintedVars.add(callee.object.name);
1018
+ }
1019
+ }
1020
+ if (call.callee.type === "Identifier") {
1021
+ const fnName = call.callee.name;
1022
+ if (taintedVars.has(fnName)) {
1023
+ return true;
1024
+ }
1025
+ const SAFE_GLOBALS = /* @__PURE__ */ new Set([
1026
+ "Math",
1027
+ "Number",
1028
+ "parseInt",
1029
+ "parseFloat",
1030
+ "isNaN",
1031
+ "isFinite"
1032
+ ]);
1033
+ if (!SAFE_GLOBALS.has(fnName)) {
1034
+ return call.arguments.some(
1035
+ (arg) => this.isExpressionTainted(arg, recordBoundVars, taintedVars)
1036
+ );
1037
+ }
1038
+ }
1039
+ return false;
1040
+ }
1041
+ /**
1042
+ * Checks if an array method callback produces tainted output.
1043
+ * e.g., env.records.map(r => r.name.charCodeAt(0)) → tainted result
1044
+ */
1045
+ doesCallbackProduceTaint(callback, methodName, recordBoundVars, taintedVars) {
1046
+ const scopedRecordVars = new Set(recordBoundVars);
1047
+ const scopedTaintedVars = new Set(taintedVars);
1048
+ if (callback.params.length > 0) {
1049
+ const isReduce = methodName !== null && _TaintAnalyzer.REDUCE_METHODS.has(methodName);
1050
+ const recordParamIndex = isReduce ? 1 : 0;
1051
+ if (callback.params.length > recordParamIndex && callback.params[recordParamIndex].type === "Identifier") {
1052
+ scopedRecordVars.add(
1053
+ callback.params[recordParamIndex].name
1054
+ );
1055
+ }
1056
+ }
1057
+ if (callback.type === "ArrowFunctionExpression" && callback.body.type !== "BlockStatement") {
1058
+ return this.isExpressionTainted(
1059
+ callback.body,
1060
+ scopedRecordVars,
1061
+ scopedTaintedVars
1062
+ );
1063
+ }
1064
+ let hasTaintedReturnOrYield = false;
1065
+ const returnVisitors = {
1066
+ ReturnStatement: (node) => {
1067
+ if (node.argument && this.isExpressionTainted(
1068
+ node.argument,
1069
+ scopedRecordVars,
1070
+ scopedTaintedVars
1071
+ )) {
1072
+ hasTaintedReturnOrYield = true;
1073
+ }
1074
+ },
1075
+ YieldExpression: (node) => {
1076
+ const yieldNode = node;
1077
+ if (yieldNode.argument && this.isExpressionTainted(
1078
+ yieldNode.argument,
1079
+ scopedRecordVars,
1080
+ scopedTaintedVars
1081
+ )) {
1082
+ hasTaintedReturnOrYield = true;
1083
+ }
1084
+ }
1085
+ };
1086
+ simple(callback.body, returnVisitors);
1087
+ return hasTaintedReturnOrYield;
1088
+ }
1089
+ // ── Utility Methods ───────────────────────────────────────────────
1090
+ /** Extracts the property name from a MemberExpression (dot or bracket with string literal) */
1091
+ getPropertyName(member) {
1092
+ if (!member.computed && member.property.type === "Identifier") {
1093
+ return member.property.name;
1094
+ }
1095
+ if (member.computed && member.property.type === "Literal") {
1096
+ const val = member.property.value;
1097
+ if (typeof val === "string") return val;
1098
+ }
1099
+ return null;
1100
+ }
1101
+ /** Checks if an expression resolves to `env.records` or `records` */
1102
+ isEnvRecordsAccess(node) {
1103
+ if (node.type === "MemberExpression") {
1104
+ const member = node;
1105
+ const propName = this.getPropertyName(member);
1106
+ if (propName === "records" && member.object.type === "Identifier" && member.object.name === "env") {
1107
+ return true;
1108
+ }
1109
+ }
1110
+ if (node.type === "Identifier" && node.name === "records") {
1111
+ return true;
1112
+ }
1113
+ return false;
1114
+ }
1115
+ /** Generates a human-readable description of the taint source for error messages */
1116
+ describeTaintSource(node, recordBoundVars, taintedVars) {
1117
+ if (node.type === "Identifier") {
1118
+ const name = node.name;
1119
+ if (taintedVars.has(name)) return `variable '${name}' is PII-derived`;
1120
+ }
1121
+ if (node.type === "ObjectExpression") {
1122
+ const obj = node;
1123
+ for (const prop of obj.properties) {
1124
+ if (prop.type === "Property" && this.isExpressionTainted(prop.value, recordBoundVars, taintedVars)) {
1125
+ const keyName = prop.key.type === "Identifier" ? prop.key.name : "unknown";
1126
+ return `property '${keyName}' contains PII-derived value`;
1127
+ }
1128
+ }
1129
+ }
1130
+ if (node.type === "CallExpression") {
1131
+ const call = node;
1132
+ if (call.callee.type === "MemberExpression") {
1133
+ const methodName = this.getPropertyName(
1134
+ call.callee
1135
+ );
1136
+ if (methodName) return `result of .${methodName}() on PII data`;
1137
+ }
1138
+ }
1139
+ return void 0;
1140
+ }
1141
+ };
1142
+
1143
+ // src/server/ner-scanner.ts
1144
+ var MEDICAL_VOCABULARY = {
1145
+ aspirin: "Medication",
1146
+ lisinopril: "Medication",
1147
+ metformin: "Medication",
1148
+ amlodipine: "Medication",
1149
+ atorvastatin: "Medication",
1150
+ omeprazole: "Medication",
1151
+ losartan: "Medication",
1152
+ simvastatin: "Medication",
1153
+ levothyroxine: "Medication",
1154
+ ibuprofen: "Medication",
1155
+ acetaminophen: "Medication",
1156
+ amoxicillin: "Medication",
1157
+ ciprofloxacin: "Medication",
1158
+ prednisone: "Medication",
1159
+ warfarin: "Medication",
1160
+ insulin: "Medication",
1161
+ hydrochlorothiazide: "Medication",
1162
+ gabapentin: "Medication",
1163
+ albuterol: "Medication",
1164
+ pantoprazole: "Medication",
1165
+ // Generic clinical terms
1166
+ hypertension: "Condition",
1167
+ diabetes: "Condition",
1168
+ bronchitis: "Condition",
1169
+ pneumonia: "Condition",
1170
+ asthma: "Condition"
1171
+ };
1172
+ var MIN_TEXT_LENGTH = 4;
1173
+ var NON_TEXT_PATTERN = /^[\d\s.,:;!?()[\]{}<>@#$%^&*+=|\\/"'`~_-]+$/;
1174
+ var NerScanner = class _NerScanner {
1175
+ static nlp = null;
1176
+ /**
1177
+ * Lazy loads the compromise library only when needed.
1178
+ */
1179
+ async getNlp() {
1180
+ if (!_NerScanner.nlp) {
1181
+ const mod = await import('compromise/three');
1182
+ _NerScanner.nlp = mod.default || mod;
1183
+ _NerScanner.nlp.addWords(MEDICAL_VOCABULARY);
1184
+ }
1185
+ return _NerScanner.nlp;
1186
+ }
1187
+ /**
1188
+ * Scans a single string value for named entities.
1189
+ * Returns detected entities if the text contains recognizable PII.
1190
+ */
1191
+ async scan(text) {
1192
+ if (text.length < MIN_TEXT_LENGTH || NON_TEXT_PATTERN.test(text)) {
1193
+ return { detected: false, entities: [] };
1194
+ }
1195
+ const nlp = await this.getNlp();
1196
+ const doc = nlp(text);
1197
+ const entities = [];
1198
+ const people = doc.people().out("array");
1199
+ for (const person of people) {
1200
+ const trimmed = person.trim();
1201
+ if (trimmed.length >= MIN_TEXT_LENGTH) {
1202
+ entities.push({ type: "person", text: trimmed });
1203
+ }
1204
+ }
1205
+ const places = doc.places().out("array");
1206
+ for (const place of places) {
1207
+ const trimmed = place.trim();
1208
+ if (trimmed.length >= MIN_TEXT_LENGTH) {
1209
+ entities.push({ type: "place", text: trimmed });
1210
+ }
1211
+ }
1212
+ const orgs = doc.organizations().out("array");
1213
+ for (const org of orgs) {
1214
+ const trimmed = org.trim();
1215
+ if (trimmed.length >= MIN_TEXT_LENGTH) {
1216
+ entities.push({ type: "organization", text: trimmed });
1217
+ }
1218
+ }
1219
+ return {
1220
+ detected: entities.length > 0,
1221
+ entities
1222
+ };
1223
+ }
1224
+ /**
1225
+ * Recursively scans all string values within an object/array.
1226
+ * Stops at the first detection for performance (fail-fast).
1227
+ */
1228
+ async scanDeep(input, seen = /* @__PURE__ */ new WeakSet()) {
1229
+ if (input === null || input === void 0) {
1230
+ return { detected: false, entities: [] };
1231
+ }
1232
+ if (typeof input === "string") {
1233
+ return this.scan(input);
1234
+ }
1235
+ if (typeof input === "object") {
1236
+ if (seen.has(input)) {
1237
+ return { detected: false, entities: [] };
1238
+ }
1239
+ seen.add(input);
1240
+ const values = Array.isArray(input) ? input : Object.values(input);
1241
+ const allEntities = [];
1242
+ for (const value of values) {
1243
+ const result = await this.scanDeep(value, seen);
1244
+ if (result.detected) {
1245
+ allEntities.push(...result.entities);
1246
+ if (result.entities.some((e) => e.type === "person")) {
1247
+ return { detected: true, entities: allEntities };
1248
+ }
1249
+ }
1250
+ }
1251
+ return {
1252
+ detected: allEntities.length > 0,
1253
+ entities: allEntities
1254
+ };
1255
+ }
1256
+ return { detected: false, entities: [] };
1257
+ }
1258
+ };
1259
+
1260
+ // src/server/pii.ts
1261
+ function isLuhnValid(cardNumber) {
1262
+ const digits = cardNumber.replace(/\D/g, "");
1263
+ if (digits.length < 13 || digits.length > 19) return false;
1264
+ let sum = 0;
1265
+ let isEven = false;
1266
+ for (let i = digits.length - 1; i >= 0; i--) {
1267
+ let digit = parseInt(digits.charAt(i), 10);
1268
+ if (isEven) {
1269
+ digit *= 2;
1270
+ if (digit > 9) {
1271
+ digit -= 9;
1272
+ }
1273
+ }
1274
+ sum += digit;
1275
+ isEven = !isEven;
1276
+ }
1277
+ return sum % 10 === 0;
1278
+ }
1279
+ function isIbanValid(iban) {
1280
+ const sanitized = iban.replace(/\s+/g, "").toUpperCase();
1281
+ if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}$/.test(sanitized)) return false;
1282
+ const rearranged = sanitized.substring(4) + sanitized.substring(0, 4);
1283
+ let numericString = "";
1284
+ for (let i = 0; i < rearranged.length; i++) {
1285
+ const charCode = rearranged.charCodeAt(i);
1286
+ if (charCode >= 65 && charCode <= 90) {
1287
+ numericString += (charCode - 55).toString();
1288
+ } else if (charCode >= 48 && charCode <= 57) {
1289
+ numericString += rearranged.charAt(i);
1290
+ } else {
1291
+ return false;
1292
+ }
1293
+ }
1294
+ try {
1295
+ return BigInt(numericString) % 97n === 1n;
1296
+ } catch (_e) {
1297
+ return false;
1298
+ }
1299
+ }
1300
+ var PII_PATTERNS = {
1301
+ EMAIL: {
1302
+ name: "EMAIL",
1303
+ pattern: /\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/gi,
1304
+ validator: (match) => !match.endsWith("@example.com") && !match.endsWith("@test.com")
1305
+ },
1306
+ CREDIT_CARD: {
1307
+ name: "CREDIT_CARD",
1308
+ pattern: /\b(?:\d[ -]*?){13,16}\b/g,
1309
+ validator: isLuhnValid
1310
+ },
1311
+ IP_ADDRESS: {
1312
+ name: "IP_ADDRESS",
1313
+ pattern: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
1314
+ validator: (match) => {
1315
+ const safeIps = ["127.0.0.1", "0.0.0.0", "255.255.255.255"];
1316
+ if (safeIps.includes(match)) return false;
1317
+ const parts = match.split(".").map(Number);
1318
+ return parts.every((p) => p >= 0 && p <= 255);
1319
+ }
1320
+ },
1321
+ PHONE: {
1322
+ name: "PHONE",
1323
+ // Strict boundary to avoid matching long numeric IDs wrapped in symbols
1324
+ pattern: /(?:(?:\+?\d{1,3}[-. ]?)?\(?\d{3}\)?[-. ]?\d{3}[-. ]?\d{4})\b/g,
1325
+ validator: (match) => {
1326
+ const digits = match.replace(/\D/g, "");
1327
+ if (digits.length < 7 || digits.length > 15) return false;
1328
+ if (/^(\d)\1+$/.test(digits)) return false;
1329
+ if (digits === "1234567890") return false;
1330
+ return true;
1331
+ }
1332
+ },
1333
+ SSN: {
1334
+ name: "SSN",
1335
+ pattern: /\b\d{3}[- ]?\d{2}[- ]?\d{4}\b/g,
1336
+ validator: (match) => {
1337
+ const digits = match.replace(/\D/g, "");
1338
+ if (digits.length !== 9) return false;
1339
+ const area = parseInt(digits.substring(0, 3), 10);
1340
+ if (area === 0 || area === 666 || area >= 900) return false;
1341
+ const group = parseInt(digits.substring(3, 5), 10);
1342
+ if (group === 0) return false;
1343
+ const serial = parseInt(digits.substring(5, 9), 10);
1344
+ if (serial === 0) return false;
1345
+ if (/^(\d)\1+$/.test(digits) || digits === "123456789") return false;
1346
+ return true;
1347
+ }
1348
+ },
1349
+ IBAN: {
1350
+ name: "IBAN",
1351
+ pattern: /\b[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}\b/gi,
1352
+ validator: isIbanValid
1353
+ },
1354
+ PASSPORT_MRZ: {
1355
+ name: "PASSPORT_MRZ",
1356
+ // Machina Readable Zone line match for standard international passports
1357
+ pattern: /\bP[A-Z<][A-Z<]{3}[A-Z0-9<]{39}(?:\b|\s|$)/g
1358
+ }
1359
+ };
1360
+ var PII_PRESETS = {
1361
+ GLOBAL_STRICT: [
1362
+ PII_PATTERNS.EMAIL,
1363
+ PII_PATTERNS.CREDIT_CARD,
1364
+ PII_PATTERNS.IP_ADDRESS,
1365
+ PII_PATTERNS.PHONE,
1366
+ PII_PATTERNS.PASSPORT_MRZ,
1367
+ PII_PATTERNS.IBAN
1368
+ ],
1369
+ US_COMPLIANT: [
1370
+ PII_PATTERNS.EMAIL,
1371
+ PII_PATTERNS.CREDIT_CARD,
1372
+ PII_PATTERNS.IP_ADDRESS,
1373
+ PII_PATTERNS.PHONE,
1374
+ PII_PATTERNS.SSN,
1375
+ PII_PATTERNS.PASSPORT_MRZ
1376
+ ],
1377
+ EU_GDPR: [
1378
+ PII_PATTERNS.EMAIL,
1379
+ PII_PATTERNS.CREDIT_CARD,
1380
+ PII_PATTERNS.IP_ADDRESS,
1381
+ PII_PATTERNS.PHONE,
1382
+ PII_PATTERNS.IBAN,
1383
+ PII_PATTERNS.PASSPORT_MRZ
1384
+ ]
1385
+ };
1386
+ var PiiScanner = class _PiiScanner {
1387
+ patterns;
1388
+ forbiddenKeysSet;
1389
+ nerScanner;
1390
+ /**
1391
+ * Safelist of keys that contain forbidden substrings but are NOT PII.
1392
+ * Prevents false positives from fuzzy matching (e.g., "grid" contains "id").
1393
+ */
1394
+ static KEY_SAFELIST = /* @__PURE__ */ new Set([
1395
+ // Common words containing "id" substring
1396
+ "grid",
1397
+ "video",
1398
+ "android",
1399
+ "identity",
1400
+ "provide",
1401
+ "override",
1402
+ "validate",
1403
+ "hidden",
1404
+ "widget",
1405
+ "guidelines",
1406
+ "beside",
1407
+ "guideline",
1408
+ "outside",
1409
+ "inside",
1410
+ "collide",
1411
+ "decide",
1412
+ "divide",
1413
+ "aside",
1414
+ "ride",
1415
+ "side",
1416
+ "wide",
1417
+ "hide",
1418
+ "tide",
1419
+ "pride",
1420
+ "bride",
1421
+ "slide",
1422
+ "guide",
1423
+ "stride",
1424
+ "oxide",
1425
+ "dioxide",
1426
+ "suicide",
1427
+ "homicide",
1428
+ "pesticide",
1429
+ "valid",
1430
+ "invalid",
1431
+ "void",
1432
+ "avoid",
1433
+ // Common words containing "name" substring
1434
+ "diagnosis",
1435
+ "medication",
1436
+ "namespace",
1437
+ "namesake",
1438
+ "rename",
1439
+ "filename",
1440
+ "hostname",
1441
+ "typename",
1442
+ "unnamed",
1443
+ "renamed",
1444
+ // Common words containing "phone" substring
1445
+ "phonetic",
1446
+ "phoneme",
1447
+ "microphone",
1448
+ "headphone",
1449
+ "telephone",
1450
+ "saxophone",
1451
+ "smartphone",
1452
+ // Common words containing "address" substring
1453
+ "streetview",
1454
+ "addressable",
1455
+ "addressing",
1456
+ // Common words containing "city" substring
1457
+ "cityscape",
1458
+ "electricity",
1459
+ "capacity",
1460
+ "velocity",
1461
+ "opacity",
1462
+ // Common technical terms
1463
+ "timestamp",
1464
+ "timezone",
1465
+ // LIOP Protocol Internal Keys (must never be blocked)
1466
+ "image_id",
1467
+ "computation_result",
1468
+ "zk_receipt",
1469
+ "testid",
1470
+ "toolid",
1471
+ "sessionid",
1472
+ "peerid",
1473
+ "nodeid",
1474
+ "requestid",
1475
+ "correlationid",
1476
+ "traceid",
1477
+ "spanid"
1478
+ ]);
1479
+ /**
1480
+ * Short forbidden tokens (< 4 chars) that require boundary-aware matching.
1481
+ * Uses regex boundary detection to avoid false positives.
1482
+ */
1483
+ shortTokenBoundaryPatterns;
1484
+ /**
1485
+ * Long forbidden tokens (>= 4 chars) that use substring containment.
1486
+ */
1487
+ longForbiddenTokens;
1488
+ constructor(patterns = [], forbiddenKeys = [], nerScanner) {
1489
+ this.patterns = patterns;
1490
+ this.forbiddenKeysSet = new Set(forbiddenKeys.map((k) => k.toLowerCase()));
1491
+ this.nerScanner = nerScanner ?? null;
1492
+ this.shortTokenBoundaryPatterns = /* @__PURE__ */ new Map();
1493
+ this.longForbiddenTokens = [];
1494
+ for (const token of this.forbiddenKeysSet) {
1495
+ if (token.length < 4) {
1496
+ this.shortTokenBoundaryPatterns.set(
1497
+ token,
1498
+ new RegExp(
1499
+ (() => {
1500
+ const ciPattern = token.split("").map((c) => `[${c.toLowerCase()}${c.toUpperCase()}]`).join("");
1501
+ const camelPattern = `[a-z]${token.charAt(0).toUpperCase()}${token.slice(1)}`;
1502
+ return `(?:^|[_-])${ciPattern}(?:$|[_-])|${camelPattern}(?:$|[A-Z_-])|^${ciPattern}$`;
1503
+ })()
1504
+ )
1505
+ );
1506
+ } else {
1507
+ this.longForbiddenTokens.push(token);
1508
+ }
1509
+ }
1510
+ }
1511
+ /**
1512
+ * Scans any input (string, object, array) for PII violations.
1513
+ * Returns the pattern/rule name that triggered the violation, or null if safe.
1514
+ *
1515
+ * Detection pipeline (fail-fast):
1516
+ * 1. Exact key match (O(1) Set lookup)
1517
+ * 2. Fuzzy key match (boundary detection for short tokens, substring for long)
1518
+ * 3. Regex/algorithmic pattern match on string values
1519
+ * 4. NER content scan on string values (if enabled)
1520
+ */
1521
+ async scan(input, seen = /* @__PURE__ */ new WeakSet()) {
1522
+ if (input === null || input === void 0) return null;
1523
+ if (typeof input === "string") {
1524
+ const trimmed = input.trim();
1525
+ if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
1526
+ try {
1527
+ const parsed = JSON.parse(trimmed);
1528
+ const violation = await this.scan(parsed, seen);
1529
+ if (violation) return violation;
1530
+ } catch (_e) {
1531
+ }
1532
+ }
1533
+ const patternViolation = this.checkString(input);
1534
+ if (patternViolation) return patternViolation;
1535
+ if (this.nerScanner) {
1536
+ const nerResult = await this.nerScanner.scan(input);
1537
+ if (nerResult.detected) {
1538
+ const personEntity = nerResult.entities.find(
1539
+ (e) => e.type === "person"
1540
+ );
1541
+ if (personEntity) {
1542
+ return `PII Entity Detected: person name "${personEntity.text}"`;
1543
+ }
1544
+ }
1545
+ }
1546
+ return null;
1547
+ }
1548
+ if (typeof input === "object") {
1549
+ if (seen.has(input)) return null;
1550
+ seen.add(input);
1551
+ if (Array.isArray(input)) {
1552
+ for (const element of input) {
1553
+ const violation = await this.scan(element, seen);
1554
+ if (violation) return violation;
1555
+ }
1556
+ } else {
1557
+ for (const [key, value] of Object.entries(
1558
+ input
1559
+ )) {
1560
+ if (this.forbiddenKeysSet.has(key.toLowerCase())) {
1561
+ return `Forbidden Key: ${key}`;
1562
+ }
1563
+ const fuzzyViolation = this.checkKeyFuzzy(key);
1564
+ if (fuzzyViolation) return fuzzyViolation;
1565
+ const violation = await this.scan(value, seen);
1566
+ if (violation) return violation;
1567
+ }
1568
+ }
1569
+ }
1570
+ return null;
1571
+ }
1572
+ /**
1573
+ * Checks a key against fuzzy matching rules.
1574
+ * Short tokens use boundary-aware regex; long tokens use substring containment.
1575
+ */
1576
+ checkKeyFuzzy(key) {
1577
+ const normalized = key.toLowerCase();
1578
+ if (_PiiScanner.KEY_SAFELIST.has(normalized)) return null;
1579
+ for (const [token, pattern] of this.shortTokenBoundaryPatterns) {
1580
+ if (pattern.test(key)) {
1581
+ return `Forbidden Key (fuzzy): ${key} matches boundary pattern "${token}"`;
1582
+ }
1583
+ }
1584
+ for (const token of this.longForbiddenTokens) {
1585
+ if (normalized.includes(token)) {
1586
+ return `Forbidden Key (fuzzy): ${key} contains restricted token "${token}"`;
1587
+ }
1588
+ }
1589
+ return null;
1590
+ }
1591
+ checkString(text) {
1592
+ for (const rule of this.patterns) {
1593
+ if (typeof rule === "string") {
1594
+ if (text.toLowerCase().includes(rule.toLowerCase())) {
1595
+ return rule;
1596
+ }
1597
+ } else if (rule instanceof RegExp) {
1598
+ if (rule.global) rule.lastIndex = 0;
1599
+ if (rule.test(text)) {
1600
+ return rule.source;
1601
+ }
1602
+ } else if (typeof rule === "object" && rule !== null) {
1603
+ const def = rule;
1604
+ if (typeof def.pattern === "string") {
1605
+ if (text.toLowerCase().includes(def.pattern.toLowerCase())) {
1606
+ if (!def.validator || def.validator(def.pattern)) {
1607
+ return def.name;
1608
+ }
1609
+ }
1610
+ } else if (def.pattern instanceof RegExp) {
1611
+ if (def.pattern.global) def.pattern.lastIndex = 0;
1612
+ let match = def.pattern.exec(text);
1613
+ while (match !== null) {
1614
+ const matchedText = match[0];
1615
+ if (!def.validator || def.validator(matchedText)) {
1616
+ return def.name;
1617
+ }
1618
+ if (!def.pattern.global) break;
1619
+ match = def.pattern.exec(text);
1620
+ }
1621
+ }
1622
+ }
1623
+ }
1624
+ return null;
1625
+ }
1626
+ };
1627
+
1628
+ // src/server/index.ts
1629
+ var __dirname2 = path.dirname(fileURLToPath(import.meta.url));
1630
+ var LiopServer = class _LiopServer {
1631
+ constructor(serverInfo, config) {
1632
+ this.serverInfo = serverInfo;
1633
+ this.config = config;
1634
+ const nerScanner = this.config?.security?.enableNerScanning ? new NerScanner() : null;
1635
+ this.piiScanner = new PiiScanner(
1636
+ this.config?.security?.piiPatterns ?? PII_PRESETS.GLOBAL_STRICT,
1637
+ this.config?.security?.forbiddenKeys ?? [
1638
+ "id",
1639
+ "name",
1640
+ "fullName",
1641
+ "firstName",
1642
+ "lastName",
1643
+ "address",
1644
+ "street",
1645
+ "city",
1646
+ "postalCode",
1647
+ "zipCode",
1648
+ "phone",
1649
+ "email",
1650
+ "ssn",
1651
+ "accountHolder",
1652
+ "accountNumber",
1653
+ "account_number",
1654
+ "password",
1655
+ "token",
1656
+ "secret",
1657
+ "privateKey"
1658
+ ],
1659
+ nerScanner
1660
+ );
1661
+ const rlConfig = this.config?.security?.rateLimit;
1662
+ this.toolCallWindowMs = rlConfig?.windowMs ?? Number.parseInt(process.env.LIOP_RATE_LIMIT_WINDOW_MS ?? "60000", 10);
1663
+ this.toolCallMaxPerWindow = rlConfig?.maxPerWindow ?? Number.parseInt(process.env.LIOP_RATE_LIMIT_MAX ?? "15", 10);
1664
+ this.globalCallMaxPerWindow = rlConfig?.globalMaxPerWindow ?? Number.parseInt(process.env.LIOP_RATE_LIMIT_GLOBAL_MAX ?? "40", 10);
1665
+ const forbiddenKeys = this.config?.security?.forbiddenKeys ?? [
1666
+ "id",
1667
+ "name",
1668
+ "fullName",
1669
+ "firstName",
1670
+ "lastName",
1671
+ "address",
1672
+ "street",
1673
+ "city",
1674
+ "postalCode",
1675
+ "zipCode",
1676
+ "phone",
1677
+ "email",
1678
+ "ssn",
1679
+ "accountHolder",
1680
+ "accountNumber",
1681
+ "account_number",
1682
+ "password",
1683
+ "token",
1684
+ "secret",
1685
+ "privateKey"
1686
+ ];
1687
+ const sensitiveKeys = this.config?.security?.sensitiveKeys ?? [];
1688
+ this.taintAnalyzer = new TaintAnalyzer(forbiddenKeys, sensitiveKeys);
1689
+ this.auditLogger = this.config?.auditLogger || globalAuditLogger;
1690
+ const isTS = import.meta.url.endsWith(".ts");
1691
+ const workerExt = isTS ? ".ts" : ".js";
1692
+ let execArgv = [];
1693
+ if (isTS) {
1694
+ try {
1695
+ const req = createRequire(import.meta.url);
1696
+ const tsxPkg = req.resolve("tsx/package.json");
1697
+ const absoluteTsx = pathToFileURL(
1698
+ path.join(path.dirname(tsxPkg), "dist", "loader.mjs")
1699
+ ).href;
1700
+ execArgv = ["--import", absoluteTsx];
1701
+ } catch (_e) {
1702
+ execArgv = ["--import", "tsx"];
1703
+ }
1704
+ }
1705
+ const isTest = process.env.NODE_ENV === "test" || process.env.VITEST;
1706
+ if (this.config?.capabilities && !this.serverInfo.capabilities) {
1707
+ this.serverInfo.capabilities = this.config.capabilities;
1708
+ }
1709
+ const workerPaths = [
1710
+ path.resolve(__dirname2, `./workers/logic-execution${workerExt}`),
1711
+ // Flat dist/ (tsup)
1712
+ path.resolve(__dirname2, `../workers/logic-execution${workerExt}`)
1713
+ // Original src/
1714
+ ];
1715
+ const workerFilename = workerPaths.find((p) => fs2.existsSync(p)) || workerPaths[1];
1716
+ this.workerPool = new Piscina({
1717
+ filename: workerFilename,
1718
+ minThreads: this.config?.workerPool?.minThreads ?? (isTest ? 0 : 2),
1719
+ maxThreads: this.config?.workerPool?.maxThreads ?? (isTest ? 1 : 8),
1720
+ idleTimeout: this.config?.workerPool?.idleTimeout ?? (isTest ? 500 : 5e3),
1721
+ maxQueue: this.config?.workerPool?.maxQueue ?? "auto",
1722
+ taskQueue: new FixedQueue(),
1723
+ execArgv,
1724
+ // [DoS Defense] Enforce hard memory ceiling per worker thread.
1725
+ // Workers exceeding this limit are terminated by Node.js runtime.
1726
+ resourceLimits: {
1727
+ maxOldGenerationSizeMb: this.config?.workerPool?.maxHeapMb ?? Number.parseInt(process.env.LIOP_WORKER_MAX_HEAP_MB ?? "64", 10)
1728
+ }
1729
+ });
1730
+ const minThreads = this.config?.workerPool?.minThreads ?? (isTest ? 0 : 2);
1731
+ if (this.workerPool && minThreads > 0) {
1732
+ for (let i = 0; i < minThreads; i++) {
1733
+ this.workerPool.run({ isWarmup: true }).catch((err) => {
1734
+ log.debug(
1735
+ `[LiopServer] Worker pool warm-up ping failed: ${err.message}`
1736
+ );
1737
+ });
1738
+ }
1739
+ }
1740
+ if (this.config?.auth?.role === "nexus") {
1741
+ const issuer = this.config.auth.issuer || "http://localhost:3000";
1742
+ const audience = this.config.auth.audience || AUTH_DEFAULTS.audience;
1743
+ const clients = this.config.auth.clients || [
1744
+ {
1745
+ client_id: process.env.LIOP_OAUTH_CLIENT_ID || "liop-mesh-agent",
1746
+ client_secret: process.env.LIOP_OAUTH_CLIENT_SECRET || "dev-secret-change-me",
1747
+ grant_types: ["client_credentials"],
1748
+ scope: "liop:tools:call liop:tools:list liop:resources:read liop:schema:read liop:mesh:query"
1749
+ }
1750
+ ];
1751
+ const { provider, jwks } = createOAuthServer({
1752
+ issuer,
1753
+ clients
1754
+ });
1755
+ this.oauthProvider = provider;
1756
+ this.jwtValidator = new JwtValidator(issuer, audience, jwks);
1757
+ } else if (this.config?.auth?.role === "node") {
1758
+ const nexusUrl = this.config.auth.nexusUrl || process.env.LIOP_NEXUS_URL || "http://localhost:3000";
1759
+ const audience = this.config.auth.audience || AUTH_DEFAULTS.audience;
1760
+ const baseUrl = nexusUrl.endsWith("/oidc") ? nexusUrl : `${nexusUrl}/oidc`;
1761
+ const jwksUri = new URL(`${baseUrl}/jwks`);
1762
+ this.jwtValidator = new JwtValidator(nexusUrl, audience, jwksUri);
1763
+ }
1764
+ this.resource(
1765
+ "LIOP Envelope Specification",
1766
+ "liop://protocol/envelope-spec",
1767
+ "Complete Logic-on-Origin envelope format, execution rules, and security constraints",
1768
+ "text/plain",
1769
+ () => Promise.resolve(this.buildEnvelopeSpec())
1770
+ );
1771
+ if (this.config?.auth?.revocationPath) {
1772
+ this.loadRevocationList();
1773
+ }
1774
+ }
1775
+ logicCache = /* @__PURE__ */ new Map();
1776
+ connectionStats = /* @__PURE__ */ new Map();
1777
+ CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
1778
+ // 24 hours
1779
+ THROTTLE_THRESHOLD = 5;
1780
+ THROTTLE_COOLDOWN_MS = 60 * 1e3;
1781
+ // 60 seconds
1782
+ // [OWASP-A01] Sliding window rate limiter — prevents micro-query exfiltration
1783
+ toolCallWindows = /* @__PURE__ */ new Map();
1784
+ toolCallMaxPerWindow;
1785
+ toolCallWindowMs;
1786
+ // [OWASP-A01] Global cross-tool rate limiter — prevents distributed micro-query attacks
1787
+ globalCallWindow = [];
1788
+ globalCallMaxPerWindow;
1789
+ // [DP] Query Budget — tracks per-field query counts to prevent multi-query differencing
1790
+ // Structure: clientId -> toolName -> field -> count
1791
+ fieldQueryBudget = /* @__PURE__ */ new Map();
1792
+ // [SEC] AST-level taint tracker for PII side-channel prevention
1793
+ taintAnalyzer;
1794
+ tools = /* @__PURE__ */ new Map();
1795
+ resources = /* @__PURE__ */ new Map();
1796
+ prompts = /* @__PURE__ */ new Map();
1797
+ activeSchema = null;
1798
+ sandboxRecords = [];
1799
+ piiScanner;
1800
+ workerPool;
1801
+ meshNode = null;
1802
+ rpcServer = null;
1803
+ boundPort = null;
1804
+ jwtValidator;
1805
+ // biome-ignore lint/suspicious/noExplicitAny: Loaded dynamically in Phase C
1806
+ oauthProvider;
1807
+ sessions = /* @__PURE__ */ new Map();
1808
+ revokedTokenHashes = /* @__PURE__ */ new Set();
1809
+ lastRevocationLoadTime = 0;
1810
+ pqcKeyPair = Dilithium65Wrapper.generateKeyPair();
1811
+ auditLogger;
1812
+ getDatasetHash() {
1813
+ return crypto3__default.createHash("sha256").update(JSON.stringify(this.sandboxRecords || [])).digest("hex");
1814
+ }
1815
+ // Compact envelope: @LIOP{target,name}\n<code>\n@END
1816
+ static LIOP_COMPACT_REGEX = /@LIOP\{(?<target>[^,}]+)(?:,\s*(?<name>[^}]*))?\}[\r\n]+(?<logic>[\s\S]*?)[\r\n]+@END/m;
1817
+ extractLogic(payload) {
1818
+ if (!payload || typeof payload !== "string") return null;
1819
+ const compact = payload.trim().match(_LiopServer.LIOP_COMPACT_REGEX);
1820
+ return compact?.groups?.logic ? compact.groups.logic.trim() : null;
1821
+ }
1822
+ parseUnknownJson(input) {
1823
+ if (typeof input !== "string") return input;
1824
+ const trimmed = input.trim();
1825
+ if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
1826
+ try {
1827
+ return JSON.parse(trimmed);
1828
+ } catch {
1829
+ return input;
1830
+ }
1831
+ }
1832
+ return input;
1833
+ }
1834
+ runPreflightPolicy(_toolName, logic, policy, clientId = "local-client") {
1835
+ if (policy) {
1836
+ const compact = logic.replace(/\s+/g, " ");
1837
+ if (policy.enforceAggregationFirst) {
1838
+ const rowExtractionPatterns = [
1839
+ // Block raw record dumps but allow safe aggregation chains
1840
+ // (.reduce, .length, .filter().length, .every, .some)
1841
+ /return\s+env\.records(?!\s*\.\s*(?:reduce|length|filter|every|some|find)\b)/i,
1842
+ /return\s*\{[\s\S]*\b(accounts|patients|rows|records)\s*:\s*env\.records(?!\s*\.\s*(?:reduce|length|filter)\b)/i
1843
+ ];
1844
+ if (rowExtractionPatterns.some((p) => p.test(compact))) {
1845
+ return "Preflight policy rejected: potential row-level export pattern detected.";
1846
+ }
1847
+ }
1848
+ if (policy.preflightDenyPatterns?.some((p) => p.test(compact))) {
1849
+ return "Preflight policy rejected: custom deny pattern matched.";
1850
+ }
1851
+ }
1852
+ let minMaxThreshold = 50;
1853
+ if (typeof policy?.enforceAggregationFirst === "object") {
1854
+ minMaxThreshold = policy.enforceAggregationFirst.minMaxBlockThreshold ?? 50;
1855
+ }
1856
+ const taintViolation = this.taintAnalyzer.analyze(
1857
+ logic,
1858
+ this.sandboxRecords.length,
1859
+ minMaxThreshold
1860
+ );
1861
+ if (taintViolation) {
1862
+ return `Preflight policy rejected: ${taintViolation.reason}`;
1863
+ }
1864
+ const extractedFields = this.taintAnalyzer.extractQueriedFields(logic);
1865
+ if (extractedFields.length > 0) {
1866
+ if (clientId === "__proto__" || clientId === "constructor" || clientId === "prototype" || _toolName === "__proto__" || _toolName === "constructor" || _toolName === "prototype") {
1867
+ return "Preflight policy rejected: invalid property key names (prototype pollution guard).";
1868
+ }
1869
+ const storePath = policy?.budgetStorePath || this.config?.budgetStorePath;
1870
+ if (storePath) {
1871
+ try {
1872
+ const budgetError = this.executeWithBudgetLock(
1873
+ storePath,
1874
+ (budget) => {
1875
+ const clientBudget = budget[clientId] || {};
1876
+ const toolBudget = clientBudget[_toolName] || {};
1877
+ for (const field of extractedFields) {
1878
+ const sensitivity = this.taintAnalyzer.classifyField(
1879
+ field,
1880
+ policy?.sensitiveKeys
1881
+ );
1882
+ let queryLimit = 25;
1883
+ let sensLabel = "public";
1884
+ if (policy?.queryBudgetPerField !== void 0) {
1885
+ queryLimit = policy.queryBudgetPerField;
1886
+ sensLabel = "override";
1887
+ } else if (sensitivity === "forbidden") {
1888
+ queryLimit = 3;
1889
+ sensLabel = "forbidden";
1890
+ } else if (sensitivity === "sensitive") {
1891
+ queryLimit = 8;
1892
+ sensLabel = "sensitive";
1893
+ }
1894
+ const count = toolBudget[field] ?? 0;
1895
+ if (count >= queryLimit) {
1896
+ return {
1897
+ result: `Preflight policy rejected: Query budget exceeded for field '${field}' (max ${queryLimit} per session for ${sensLabel} fields). Rotate PQC session to reset budget.`
1898
+ };
1899
+ }
1900
+ }
1901
+ const updatedToolBudget = { ...toolBudget };
1902
+ for (const field of extractedFields) {
1903
+ updatedToolBudget[field] = (updatedToolBudget[field] ?? 0) + 1;
1904
+ }
1905
+ const updatedClientBudget = { ...clientBudget };
1906
+ updatedClientBudget[_toolName] = updatedToolBudget;
1907
+ const updatedBudget = { ...budget };
1908
+ updatedBudget[clientId] = updatedClientBudget;
1909
+ return { result: null, updatedBudget };
1910
+ }
1911
+ );
1912
+ if (budgetError) {
1913
+ return budgetError;
1914
+ }
1915
+ } catch (e) {
1916
+ const errorMsg = e instanceof Error ? e.message : String(e);
1917
+ log.error(
1918
+ `[LIOP-Server] Error applying persistent query budget: ${errorMsg}. Falling back to in-memory.`
1919
+ );
1920
+ const inMemoryError = this.applyInMemoryBudget(
1921
+ clientId,
1922
+ _toolName,
1923
+ extractedFields,
1924
+ policy
1925
+ );
1926
+ if (inMemoryError) return inMemoryError;
1927
+ }
1928
+ } else {
1929
+ const inMemoryError = this.applyInMemoryBudget(
1930
+ clientId,
1931
+ _toolName,
1932
+ extractedFields,
1933
+ policy
1934
+ );
1935
+ if (inMemoryError) return inMemoryError;
1936
+ }
1937
+ }
1938
+ return null;
1939
+ }
1940
+ validateOutputPolicy(toolName, output, policy) {
1941
+ if (!policy) return null;
1942
+ const parsed = this.parseUnknownJson(output);
1943
+ if (parsed && typeof parsed === "object" && parsed.isError === true) {
1944
+ return null;
1945
+ }
1946
+ if (policy.outputSchema) {
1947
+ const effectiveSchema = (() => {
1948
+ if (!(policy.outputSchema instanceof z.ZodObject)) {
1949
+ return policy.outputSchema;
1950
+ }
1951
+ const obj = policy.outputSchema;
1952
+ const def = obj.def || obj._def;
1953
+ if (def && def.catchall !== void 0 && !(def.catchall instanceof z.ZodNever)) {
1954
+ return obj;
1955
+ }
1956
+ return obj.strict();
1957
+ })();
1958
+ const schemaResult = effectiveSchema.safeParse(parsed);
1959
+ if (!schemaResult.success) {
1960
+ return `[LIOP] Output schema violation for ${toolName}: ${schemaResult.error.issues.map((i) => `${i.path.join(".") || "<root>"} ${i.message}`).join(
1961
+ "; "
1962
+ )}. HINT: Your output must conform to the declared schema. Use 'env.records' to access the dataset and return only allowed fields.`;
1963
+ }
1964
+ }
1965
+ if (policy.enforceAggregationFirst && this.violatesAggregationFirstPolicy(
1966
+ this.unwrapForAggregationPolicyScan(parsed),
1967
+ policy.enforceAggregationFirst,
1968
+ this.sandboxRecords.length
1969
+ )) {
1970
+ const isDev = process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test" || process.env.LIOP_SEC_VERBOSE === "1";
1971
+ return isDev ? "Aggregation-First Policy Violation: row-level export or K-Anonymity violation blocked. HINT: Use .reduce() to produce a flat {key:value} object. Do NOT use .map() to create arrays of objects. Ensure dataset size > 10 for detailed results." : "Aggregation-First Policy Violation: Output blocked due to privacy constraints.";
1972
+ }
1973
+ return null;
1974
+ }
1975
+ /**
1976
+ * Proxied tools stringify a full MCP CallToolResult (`{ content: [...] }`).
1977
+ * Aggregation-first heuristics must scan the inner business JSON, not the MCP envelope
1978
+ * (otherwise `content` looks like a tabular array of objects and everything blocks).
1979
+ */
1980
+ unwrapForAggregationPolicyScan(input) {
1981
+ if (typeof input === "string") {
1982
+ const trimmed = input.trim();
1983
+ if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
1984
+ try {
1985
+ return this.unwrapForAggregationPolicyScan(JSON.parse(trimmed));
1986
+ } catch {
1987
+ return input;
1988
+ }
1989
+ }
1990
+ return input;
1991
+ }
1992
+ if (!input || typeof input !== "object") {
1993
+ return input;
1994
+ }
1995
+ const rec = input;
1996
+ if (rec.computation_result !== void 0) {
1997
+ return this.unwrapForAggregationPolicyScan(rec.computation_result);
1998
+ }
1999
+ if (!Array.isArray(rec.content) || rec.content.length === 0) {
2000
+ return input;
2001
+ }
2002
+ const texts = [];
2003
+ for (const part of rec.content) {
2004
+ if (part && typeof part === "object" && "text" in part) {
2005
+ const t = part.text;
2006
+ if (typeof t === "string") {
2007
+ texts.push(t);
2008
+ }
2009
+ }
2010
+ }
2011
+ if (texts.length === 0) {
2012
+ return input;
2013
+ }
2014
+ const joined = texts.length === 1 ? texts[0] : texts.join("\n");
2015
+ return this.unwrapForAggregationPolicyScan(joined);
2016
+ }
2017
+ violatesAggregationFirstPolicy(input, policyObj, recordsCount) {
2018
+ if (!policyObj) {
2019
+ return false;
2020
+ }
2021
+ const maxRows = typeof policyObj === "object" && typeof policyObj.maxOutputRows === "number" ? policyObj.maxOutputRows : 10;
2022
+ const allowPrimitives = typeof policyObj === "object" && typeof policyObj.allowPrimitiveArrays === "boolean" ? policyObj.allowPrimitiveArrays : true;
2023
+ if (typeof input === "string") {
2024
+ const trimmed = input.trim();
2025
+ if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
2026
+ try {
2027
+ return this.violatesAggregationFirstPolicy(
2028
+ JSON.parse(trimmed),
2029
+ policyObj,
2030
+ recordsCount
2031
+ );
2032
+ } catch {
2033
+ return false;
2034
+ }
2035
+ }
2036
+ return false;
2037
+ }
2038
+ if (Array.isArray(input)) {
2039
+ if (input.length > 0 && input.every((item) => typeof item === "object" && item !== null)) {
2040
+ if (input.length > maxRows) {
2041
+ return true;
2042
+ }
2043
+ return input.some(
2044
+ (item) => this.violatesAggregationFirstPolicy(item, policyObj, recordsCount)
2045
+ );
2046
+ }
2047
+ if (input.length > 0 && input.every((item) => typeof item !== "object" || item === null)) {
2048
+ if (!allowPrimitives) return true;
2049
+ return false;
2050
+ }
2051
+ return input.some(
2052
+ (item) => this.violatesAggregationFirstPolicy(item, policyObj, recordsCount)
2053
+ );
2054
+ }
2055
+ if (input && typeof input === "object") {
2056
+ const keys = Object.keys(input);
2057
+ if (recordsCount !== void 0 && recordsCount > 0 && recordsCount < 10) {
2058
+ if (keys.length > 3) return true;
2059
+ const values = Object.values(input);
2060
+ if (values.some(
2061
+ (v) => Array.isArray(v) || typeof v === "object" && v !== null
2062
+ )) {
2063
+ return true;
2064
+ }
2065
+ }
2066
+ if (keys.length > maxRows) {
2067
+ return true;
2068
+ }
2069
+ return Object.values(input).some(
2070
+ (value) => this.violatesAggregationFirstPolicy(value, policyObj, recordsCount)
2071
+ );
2072
+ }
2073
+ return false;
2074
+ }
2075
+ /**
2076
+ * Builds the centralized LIOP envelope specification document.
2077
+ * Served as a single Resource (liop://protocol/envelope-spec) instead
2078
+ * of being duplicated across every tool description.
2079
+ */
2080
+ buildEnvelopeSpec() {
2081
+ const lines = [
2082
+ "LIOP v1 Envelope Specification",
2083
+ "================================",
2084
+ "",
2085
+ "FORMAT:",
2086
+ "",
2087
+ "Compact Envelope:",
2088
+ " @LIOP{wasi_v1,TaskName}",
2089
+ " <JavaScript code>",
2090
+ " @END",
2091
+ "",
2092
+ "RUNTIME ENVIRONMENT:",
2093
+ "- env.records: Array of data objects from the origin",
2094
+ "- Must use 'return' to output results",
2095
+ "- Zero-Trust WASI Sandbox (Node.js Worker Pool)",
2096
+ "- Return aggregated objects, NOT raw row-level arrays",
2097
+ "",
2098
+ "SANDBOX RUNTIME RESTRICTIONS & WORKAROUNDS:",
2099
+ "- Date is poisoned: The 'Date' class/constructor is undefined (Date.now(), Date.parse(), etc. will throw).",
2100
+ " Workaround: Use lexicographical string comparison on ISO 8601 date strings (e.g., record.date >= '2024-01-01').",
2101
+ "- Poisoned globals: eval, Function, setTimeout, setInterval, Buffer, ArrayBuffer, and TypedArrays are undefined.",
2102
+ "- Frozen prototypes: Any modifications to Object.prototype, Array.prototype, etc., are blocked.",
2103
+ "",
2104
+ "SECURITY CONSTRAINTS:",
2105
+ "- PII Egress Shield blocks raw identifiers in output",
2106
+ "- Aggregation-First policy: prefer counts, averages, summaries",
2107
+ "- AST Guardian: static analysis before execution",
2108
+ "",
2109
+ "DIFFERENTIAL PRIVACY (DP) MECHANISM (Laplace Mechanism):",
2110
+ "- Default field noise scale is derived from node global sensitivity.",
2111
+ "- COUNT / LENGTH Optimization: To obtain EXACT counts without noise (sensitivity=1),",
2112
+ " the return keys MUST contain 'count', 'length', 'size', 'num', 'positive', 'negative',",
2113
+ " or start with 'total_' or 'num_' (e.g. 'total_tx', 'credits_count').",
2114
+ "- AVERAGE Optimization: Keys containing 'avg', 'mean' or 'average' scale noise",
2115
+ " down automatically by dividing sensitivity by dataset size (sensitivity / n).",
2116
+ "- SUM / OTHER queries: Receive full Laplace noise based on global node sensitivity",
2117
+ " (e.g., Sensitivity=100,000 in Bank to protect balances)."
2118
+ ];
2119
+ if (this.config?.security?.forbiddenKeys?.length) {
2120
+ lines.push(
2121
+ `- Restricted fields: ${this.config.security.forbiddenKeys.join(", ")}`
2122
+ );
2123
+ }
2124
+ lines.push(
2125
+ "",
2126
+ "TAINT TRACKING (Phase 108):",
2127
+ "- AST-level analysis blocks PII-derived scalars (charCodeAt, charAt, etc.)",
2128
+ "- Operations on restricted fields are tracked through variable assignments",
2129
+ "- Boolean inference (field.charCodeAt(0) < N ? 1 : 0) is blocked",
2130
+ "- Allowed: aggregations on non-PII fields (balance, amount, date)",
2131
+ "",
2132
+ "K-ANONYMITY THRESHOLDS:",
2133
+ "- Small Datasets (< 10 records): Maximum of 3 scalar output fields. Nesting or arrays in output are strictly forbidden.",
2134
+ "- Large Datasets (>= 10 records): Maximum of 10 output fields.",
2135
+ "",
2136
+ "RATE LIMITS (OWASP A01):",
2137
+ "- Per-tool: 15 calls/min (configurable via LIOP_RATE_LIMIT_MAX)",
2138
+ "- Global: 40 calls/min across all tools (LIOP_RATE_LIMIT_GLOBAL_MAX)",
2139
+ "",
2140
+ "OPTIONAL PARAMETERS:",
2141
+ "- __liop_bypass_ast_cache: boolean (force AST re-evaluation)",
2142
+ "",
2143
+ "QUERY BUDGET TIERS (NIST SP 800-226):",
2144
+ "- FORBIDDEN (PII fields like name, accountHolder): Blocked in AST preflight. Never executed, does not consume budget.",
2145
+ "- SENSITIVE (fields marked via sensitiveKeys): 8 queries per agent identity (or queryBudgetPerField override).",
2146
+ "- PUBLIC (all other fields): 25 queries per agent identity.",
2147
+ "- PERSISTENCE: The budget is tracked by agent DID and persisted to disk, NOT by MCP session.",
2148
+ " Reconnecting or starting a new session does NOT reset the budget.",
2149
+ " Reset requires: administrator calling resetFieldBudget() or manual deletion of the budget store file.",
2150
+ "",
2151
+ "AUTHENTICATION (tokenSlug Convention):",
2152
+ "- Restricted nodes declare authRequired: true in their manifest.",
2153
+ "- Token resolution: LIOP_TOKEN_<tokenSlug> (deterministic) > LIOP_TOKEN_<PeerID> > LIOP_TOKEN_<ProviderName>",
2154
+ "- Format: tokenSlug must match SCREAMING_SNAKE_CASE /^[A-Z][A-Z0-9_]*$/ (e.g., BANK, VAULT, HFT_ORACLE)."
2155
+ );
2156
+ return lines.join("\n");
2157
+ }
2158
+ /**
2159
+ * Extracts a compact, human-readable field summary from a JSON Schema.
2160
+ *
2161
+ * Walks the schema structure to find actual data property names and types,
2162
+ * rather than returning top-level schema metadata keys (type, items, etc.).
2163
+ *
2164
+ * Example output for a banking schema:
2165
+ * "Array of {id(string), accountHolder(string), balance(number), transactions(array of {date(string), amount(number)})}"
2166
+ */
2167
+ extractSchemaFieldSummary(schema, depth = 0) {
2168
+ if (depth > 3) return "{...}";
2169
+ const schemaType = schema.type;
2170
+ const properties = schema.properties;
2171
+ const items = schema.items;
2172
+ if (properties) {
2173
+ const fields = Object.entries(properties).map(([key, prop]) => {
2174
+ const propType = prop.type;
2175
+ if (propType === "array" && prop.items) {
2176
+ const nested = this.extractSchemaFieldSummary(
2177
+ prop.items,
2178
+ depth + 1
2179
+ );
2180
+ return `${key}(array of ${nested})`;
2181
+ }
2182
+ if (propType === "object" && prop.properties) {
2183
+ const nested = this.extractSchemaFieldSummary(prop, depth + 1);
2184
+ return `${key}(${nested})`;
2185
+ }
2186
+ return `${key}(${propType || "unknown"})`;
2187
+ });
2188
+ return `{${fields.join(", ")}}`;
2189
+ }
2190
+ if (schemaType === "array" && items) {
2191
+ const itemsSummary = this.extractSchemaFieldSummary(items, depth + 1);
2192
+ return `Array of ${itemsSummary}`;
2193
+ }
2194
+ if (schemaType) return schemaType;
2195
+ return Object.keys(schema).join(", ");
2196
+ }
2197
+ /**
2198
+ * Convenience alias for connectToMesh(), matching official documentation.
2199
+ */
2200
+ async connect(options = {}) {
2201
+ return this.connectToMesh(options);
2202
+ }
2203
+ /**
2204
+ * Register a new Tool
2205
+ */
2206
+ tool(name, description, shape, handler, policy) {
2207
+ if (this.tools.has(name)) {
2208
+ throw new Error(`Tool already registered: ${name}`);
2209
+ }
2210
+ const schema = z.object(shape);
2211
+ const generatedSchema = z.toJSONSchema(schema, { target: "draft-07" });
2212
+ let finalDescription = description;
2213
+ let finalHandler = handler;
2214
+ if (shape.payload && shape.payload instanceof z.ZodString) {
2215
+ const blockedKeys = this.config?.security?.forbiddenKeys || [];
2216
+ finalDescription += "\n\nPayload: LIOP v1 envelope (WASI sandbox). Format: @LIOP{wasi_v1,TaskName}\\n<JS code>\\n@END | Access data: env.records. Return aggregated object. Note: If dataset size < 10 (synthetic demo), Egress K-Anonymity blocks output if it has >3 keys or any array/nested object. | Full spec: resource liop://protocol/envelope-spec";
2217
+ if (blockedKeys.length > 0) {
2218
+ finalDescription += `
2219
+ Restricted fields: ${blockedKeys.join(", ")}.`;
2220
+ }
2221
+ if (this.activeSchema) {
2222
+ const schemaDigest = this.extractSchemaFieldSummary(this.activeSchema);
2223
+ finalDescription += `
2224
+ Data structure: ${schemaDigest}. Full schema: resource liop://schema/global`;
2225
+ }
2226
+ finalHandler = async (args, _extra) => {
2227
+ const clientId = "global_connection";
2228
+ const now = Date.now();
2229
+ const stats = this.connectionStats.get(clientId) || {
2230
+ failures: 0,
2231
+ lastAttempt: 0
2232
+ };
2233
+ if (stats.failures >= this.THROTTLE_THRESHOLD && now - stats.lastAttempt < this.THROTTLE_COOLDOWN_MS) {
2234
+ return {
2235
+ content: [
2236
+ {
2237
+ type: "text",
2238
+ text: "LIOP_THROTTLED: Too many violations. Cooling down for 60 seconds."
2239
+ }
2240
+ ],
2241
+ isError: true
2242
+ };
2243
+ }
2244
+ const payloadValue = args.payload;
2245
+ const bypassCache = args.__liop_bypass_ast_cache === true;
2246
+ const payloadHash = crypto3__default.createHash("sha256").update(payloadValue).digest("hex");
2247
+ const logic = this.extractLogic(payloadValue);
2248
+ const cached = this.logicCache.get(payloadHash);
2249
+ if (!bypassCache && cached && now - cached.timestamp < this.CACHE_TTL_MS) {
2250
+ if (logic) {
2251
+ args.payload = logic;
2252
+ const preflightReason = this.runPreflightPolicy(
2253
+ name,
2254
+ logic,
2255
+ policy,
2256
+ "mcp-client"
2257
+ );
2258
+ if (preflightReason) {
2259
+ return {
2260
+ content: [{ type: "text", text: preflightReason }],
2261
+ isError: true
2262
+ };
2263
+ }
2264
+ return await this.executeInWorkerPool(args, logic, name);
2265
+ }
2266
+ }
2267
+ if (!logic) {
2268
+ stats.failures++;
2269
+ stats.lastAttempt = now;
2270
+ this.connectionStats.set(clientId, stats);
2271
+ return {
2272
+ content: [
2273
+ {
2274
+ type: "text",
2275
+ text: "Error: Malformed payload. Missing @LIOP boundary.\\nYou MUST wrap your logic exactly like this:\\n\\n@LIOP{wasi_v1,DynamicAudit}\\n// Your JS code here\\n@END"
2276
+ }
2277
+ ],
2278
+ isError: true
2279
+ };
2280
+ }
2281
+ try {
2282
+ const logic2 = this.extractLogic(
2283
+ args.payload
2284
+ );
2285
+ args.payload = logic2;
2286
+ const preflightReason = this.runPreflightPolicy(
2287
+ name,
2288
+ logic2,
2289
+ policy,
2290
+ "mcp-client"
2291
+ );
2292
+ if (preflightReason) {
2293
+ stats.failures++;
2294
+ stats.lastAttempt = now;
2295
+ this.connectionStats.set(clientId, stats);
2296
+ return {
2297
+ content: [{ type: "text", text: preflightReason }],
2298
+ isError: true
2299
+ };
2300
+ }
2301
+ const result = await this.executeInWorkerPool(args, logic2, name);
2302
+ if (!result.isError) {
2303
+ this.connectionStats.set(clientId, {
2304
+ failures: 0,
2305
+ lastAttempt: now
2306
+ });
2307
+ this.logicCache.set(payloadHash, {
2308
+ hash: payloadHash,
2309
+ timestamp: now
2310
+ });
2311
+ } else {
2312
+ stats.failures++;
2313
+ stats.lastAttempt = now;
2314
+ this.connectionStats.set(clientId, stats);
2315
+ }
2316
+ return result;
2317
+ } catch (error) {
2318
+ const e = error;
2319
+ stats.failures++;
2320
+ stats.lastAttempt = now;
2321
+ this.connectionStats.set(clientId, stats);
2322
+ return {
2323
+ content: [
2324
+ { type: "text", text: `ExecutionRuntimeException: ${e.message}` }
2325
+ ],
2326
+ isError: true
2327
+ };
2328
+ }
2329
+ };
2330
+ }
2331
+ const inputSchema = {
2332
+ type: "object",
2333
+ properties: generatedSchema.properties || {},
2334
+ required: generatedSchema.required
2335
+ };
2336
+ this.tools.set(name, {
2337
+ tool: { name, description: finalDescription, inputSchema },
2338
+ handler: finalHandler,
2339
+ schema,
2340
+ policy
2341
+ });
2342
+ if (this.meshNode) {
2343
+ this.meshNode.announceCapability(name).catch((err) => {
2344
+ log.info(
2345
+ `[LIOP-Mesh] Failed to auto-announce tool ${name}: ${err.message}`
2346
+ );
2347
+ });
2348
+ }
2349
+ }
2350
+ /**
2351
+ * Register a dynamic prompt
2352
+ */
2353
+ prompt(name, description, args, handler) {
2354
+ if (this.prompts.has(name)) {
2355
+ throw new Error(`Prompt already registered: ${name}`);
2356
+ }
2357
+ this.prompts.set(name, {
2358
+ prompt: { name, description, arguments: args },
2359
+ handler
2360
+ });
2361
+ }
2362
+ /**
2363
+ * Enables LIOP Zero-Shot Autonomy by registering the Blind Analyst standard prompt.
2364
+ */
2365
+ enableZeroShotAutonomy() {
2366
+ this.prompt(
2367
+ "liop_blind_analyst",
2368
+ "The official Logic-Injection-on-Origin Protocol system prompt. Instructs the LLM on how to securely inject Logic-on-Origin without violating PII or safety constraints.",
2369
+ [],
2370
+ (_request) => {
2371
+ return {
2372
+ description: "LIOP Blind Analyst Instructions",
2373
+ messages: [
2374
+ {
2375
+ role: "user",
2376
+ content: {
2377
+ type: "text",
2378
+ text: `You are the "Blind Analyst" operating within the Logic-Injection-on-Origin Protocol (LIOP) ecosystem.
2379
+ Your objective is to perform secure Logic-on-Origin injections. You must process remote data without ever requesting its extraction.
2380
+
2381
+ INDUSTRIAL CONSTRAINTS & PROTOCOL RULES:
2382
+ 1. DATA PRIVACY: NEVER attempt to export Personally Identifiable Information (PII). The LIOP Egress Shield will block any response containing raw IDs, names, or addresses.
2383
+ 2. AGGREGATION FIRST & K-ANONYMITY THRESHOLDS: Always prefer returning counts, averages, or anonymized summaries.
2384
+ - Dataset < 10 records: Maximum of 3 scalar output fields. Nesting or arrays in output are strictly forbidden.
2385
+ - Dataset >= 10 records: Maximum of 10 output fields.
2386
+ 3. LAPLACE DIFFERENTIAL PRIVACY (DP) COMPLIANCE:
2387
+ - Legitimate COUNT queries: To obtain EXACT, un-noised counts, you MUST name your return keys containing 'count', 'length', 'size', 'num', 'positive', 'negative', or starting with 'total_' or 'num_' (e.g. 'total_tx', 'credits_count'). This forces sensitivity=1.0, rounds values, and clamps to non-negative values.
2388
+ - Legitimate AVERAGE queries: Use 'avg_', '_average' or 'mean_' keys to automatically scale down Laplace noise by dividing sensitivity by the dataset size (sensitivity / n).
2389
+ - Legitimate SUM queries: Return keys without count/average suffixes will receive full Laplacian noise scaled by the node's global sensitivity (which can be up to 100,000 in Bank nodes to protect raw balances). Do NOT attempt to bypass this by renaming sum fields to count fields, as it violates protocol integrity.
2390
+ 4. PAYLOAD ENCAPSULATION: Your JavaScript payloads MUST strictly adhere to the Compact Envelope. DO NOT include markdown backticks or leading text inside the 'payload' argument.
2391
+ Structure:
2392
+ @LIOP{wasi_v1,AnalysisTask}
2393
+ // Your JS Code Here
2394
+ @END
2395
+ 5. RUNTIME SCOPE: The execution environment provides a global 'env' object. Use 'env.records' to access the target dataset.
2396
+ 6. LOCALIZATION: Format all JSON response keys in the language used by the user in their query (e.g., use Spanish keys if the query is in Spanish).
2397
+ 7. SCHEMA RIGIDITY: Only use fields defined in the 'Data Dictionary'. Usage of non-existent fields will trigger a sandbox runtime exception.
2398
+ 8. SANDBOX RUNTIME: The 'Date' class/constructor is poisoned and set to undefined. Calling 'new Date()', 'Date.now()', or 'Date.parse()' will throw exceptions.
2399
+ Workaround: Perform chronological operations and filtering using lexicographical string comparisons on ISO 8601 date strings (e.g., 'record.date >= "2024-01-01"').
2400
+ Additionally, standard globals like 'eval', 'Function', 'setTimeout', 'setInterval', 'Buffer', ArrayBuffer, and TypedArrays are also undefined.
2401
+ 9. QUERY BUDGET PERSISTENCE: The per-field query budget is tracked by your agent DID and persisted to disk. Reconnecting or starting a new MCP session does NOT reset the budget. If you receive "Query budget exceeded", you must use a different field or request an administrator to invoke resetFieldBudget().${this.activeSchema ? `
2402
+
2403
+ CURRENT DATA DICTIONARY (STRICT):
2404
+ ${JSON.stringify(this.activeSchema, null, 2)}` : ""}
2405
+
2406
+ Protocol Adherence is mandatory for successful execution.`
2407
+ }
2408
+ }
2409
+ ]
2410
+ };
2411
+ }
2412
+ );
2413
+ }
2414
+ /**
2415
+ * Register a dynamic resource
2416
+ */
2417
+ resource(name, uri, description, mimeType, content) {
2418
+ if (this.resources.has(uri)) {
2419
+ throw new Error(`Resource URI already registered: ${uri}`);
2420
+ }
2421
+ this.resources.set(uri, { name, uri, description, mimeType, content });
2422
+ }
2423
+ /**
2424
+ * Builds execution guidelines served as a resource to guide LLM code generation.
2425
+ */
2426
+ buildExecutionGuidelines() {
2427
+ return [
2428
+ "LIOP Sandbox Execution Guidelines",
2429
+ "=================================",
2430
+ "",
2431
+ "1. DATE POISONING & FILTERING WORKAROUND:",
2432
+ " The global 'Date' class is set to undefined inside the sandbox. Calling 'new Date()', 'Date.now()', or 'Date.parse()' will throw a ReferenceError.",
2433
+ " - Workaround: Perform chronological filtering using lexicographical string comparisons on ISO 8601 strings.",
2434
+ " Example: const filtered = env.records.filter(r => r.date >= '2024-01-01' && r.date <= '2024-12-31');",
2435
+ "",
2436
+ "2. K-ANONYMITY CONSTRAINTS:",
2437
+ " - Datasets with LESS than 10 records: The returned object must contain at most 3 scalar fields, and must NOT contain any arrays or nested objects.",
2438
+ " - Datasets with 10 or MORE records: The returned object can contain up to 10 fields.",
2439
+ "",
2440
+ "3. DIFFERENTIAL PRIVACY SUFFIXES:",
2441
+ " To avoid Laplacian noise adding random perturbations to your counts or averages, you must name your object keys using specific terms:",
2442
+ " - Counts (Exact, no noise): Key names must contain 'count', 'length', 'size', 'num', 'positive', 'negative', or start with 'total_' or 'num_'.",
2443
+ " - Averages (Reduced noise): Key names must contain 'avg', 'mean', or 'average'.",
2444
+ " - Sums/Other: Will receive full Laplace noise.",
2445
+ "",
2446
+ "4. GENERAL RESTRICTIONS:",
2447
+ " - Do not use 'eval', 'Function', 'setTimeout', 'setInterval', 'Buffer', 'ArrayBuffer', or TypedArrays.",
2448
+ " - Do not attempt to modify prototypes (Object.prototype, Array.prototype).",
2449
+ "",
2450
+ "5. QUERY BUDGET TIERS (NIST SP 800-226):",
2451
+ " The per-field query budget is tracked by agent DID and persisted to disk (not per MCP session).",
2452
+ " - FORBIDDEN (PII fields): Always blocked in AST preflight. Does not consume budget.",
2453
+ " - SENSITIVE (sensitiveKeys): 8 queries per agent identity (or queryBudgetPerField override).",
2454
+ " - PUBLIC (other fields): 25 queries per agent identity.",
2455
+ " - Reset requires administrator intervention via resetFieldBudget()."
2456
+ ].join("\n");
2457
+ }
2458
+ /**
2459
+ * Broadcasts the Data Dictionary to the LLM prior to code injection.
2460
+ */
2461
+ dataDictionary(schema, name = "Global Medical Data Dictionary", uri = "liop://schema/global", description = "Exposes the internal database schema for Zero-Shot Autonomy planning") {
2462
+ schema.$comment = "LIOP DIRECTIVES: 1. Date is undefined (Date.now(), new Date() throw). Workaround: use lexicographical string comparison on ISO 8601 string dates (e.g. record.date >= '2024-01-01'). 2. Small datasets (<10 records) limit outputs to max 3 scalar keys with NO nesting. 3. DP counts must contain count/length/size/num/total_ prefix/suffix.";
2463
+ this.activeSchema = schema;
2464
+ const schemaDigest = this.extractSchemaFieldSummary(schema);
2465
+ for (const [toolName, entry] of this.tools.entries()) {
2466
+ if (entry.schema.shape.payload && entry.schema.shape.payload instanceof z.ZodString && entry.tool.description && !entry.tool.description.includes("Data structure:")) {
2467
+ entry.tool.description += `
2468
+ Data structure: ${schemaDigest}. Full schema: resource ${uri}. Guidelines: resource liop://schema/guidelines`;
2469
+ this.tools.set(toolName, entry);
2470
+ }
2471
+ }
2472
+ if (!this.resources.has("liop://schema/guidelines")) {
2473
+ this.resource(
2474
+ "LIOP Execution Guidelines",
2475
+ "liop://schema/guidelines",
2476
+ "Directives for generating compliant JavaScript code for the LIOP Sandbox runtime",
2477
+ "text/plain",
2478
+ () => Promise.resolve(this.buildExecutionGuidelines())
2479
+ );
2480
+ }
2481
+ this.resource(
2482
+ name,
2483
+ uri,
2484
+ description,
2485
+ "application/json",
2486
+ JSON.stringify(schema, null, 2)
2487
+ );
2488
+ }
2489
+ /**
2490
+ * Manually invalidates the AST Logic Cache (e.g. for Zero-Day patches).
2491
+ */
2492
+ clearAstCache() {
2493
+ this.logicCache.clear();
2494
+ log.info("[LIOP-SDK] AST Security Cache cleared by Admin.");
2495
+ }
2496
+ /**
2497
+ * Sliding window rate limiter for tool call frequency.
2498
+ * Prevents micro-query exfiltration attacks where an attacker
2499
+ * makes hundreds of individually-legitimate calls to reconstruct
2500
+ * the full dataset field by field. (OWASP A01)
2501
+ */
2502
+ checkToolCallRateLimit(toolName) {
2503
+ const now = Date.now();
2504
+ const windowMs = this.toolCallWindowMs;
2505
+ const maxPerWindow = this.toolCallMaxPerWindow;
2506
+ const window = this.toolCallWindows.get(toolName) || [];
2507
+ const active = window.filter((t) => now - t < windowMs);
2508
+ if (active.length >= maxPerWindow) {
2509
+ const retryAfterSec = Math.ceil((active[0] + windowMs - now) / 1e3);
2510
+ return {
2511
+ content: [
2512
+ {
2513
+ type: "text",
2514
+ text: `LIOP_RATE_LIMITED: Too many calls to ${toolName}. Max ${maxPerWindow} per ${windowMs / 1e3}s window. Retry after ${retryAfterSec}s.`
2515
+ }
2516
+ ],
2517
+ isError: true
2518
+ };
2519
+ }
2520
+ active.push(now);
2521
+ this.toolCallWindows.set(toolName, active);
2522
+ return null;
2523
+ }
2524
+ /**
2525
+ * Global cross-tool rate limiter.
2526
+ * Prevents attackers from distributing micro-queries across multiple tools
2527
+ * to evade per-tool rate limits. (OWASP A01)
2528
+ */
2529
+ checkGlobalRateLimit() {
2530
+ const now = Date.now();
2531
+ const windowMs = this.toolCallWindowMs;
2532
+ const maxGlobal = this.globalCallMaxPerWindow;
2533
+ this.globalCallWindow = this.globalCallWindow.filter(
2534
+ (t) => now - t < windowMs
2535
+ );
2536
+ if (this.globalCallWindow.length >= maxGlobal) {
2537
+ const retryAfterSec = Math.ceil(
2538
+ (this.globalCallWindow[0] + windowMs - now) / 1e3
2539
+ );
2540
+ return {
2541
+ content: [
2542
+ {
2543
+ type: "text",
2544
+ text: `LIOP_RATE_LIMITED: Global call limit exceeded. Max ${maxGlobal} total calls per ${windowMs / 1e3}s window. Retry after ${retryAfterSec}s.`
2545
+ }
2546
+ ],
2547
+ isError: true
2548
+ };
2549
+ }
2550
+ this.globalCallWindow.push(now);
2551
+ return null;
2552
+ }
2553
+ /**
2554
+ * Emulates calling a tool (used locally or via LIOPMcpBridge)
2555
+ */
2556
+ async callTool(request, clientId = "local-client") {
2557
+ const entry = this.tools.get(request.name);
2558
+ if (!entry) {
2559
+ throw new Error(`Tool not found: ${request.name}`);
2560
+ }
2561
+ const globalLimitResult = this.checkGlobalRateLimit();
2562
+ if (globalLimitResult) return globalLimitResult;
2563
+ const rateLimitResult = this.checkToolCallRateLimit(request.name);
2564
+ if (rateLimitResult) return rateLimitResult;
2565
+ try {
2566
+ const parsedArgs = entry.schema.parse(request.arguments || {});
2567
+ if (request.arguments?.__liop_bypass_ast_cache === true) {
2568
+ parsedArgs.__liop_bypass_ast_cache = true;
2569
+ }
2570
+ if (parsedArgs && typeof parsedArgs.payload === "string") {
2571
+ const payload = parsedArgs.payload;
2572
+ const logic = this.extractLogic(payload);
2573
+ if (logic) {
2574
+ const preflightReason = this.runPreflightPolicy(
2575
+ request.name,
2576
+ logic,
2577
+ entry.policy,
2578
+ clientId
2579
+ );
2580
+ if (preflightReason) {
2581
+ return {
2582
+ content: [{ type: "text", text: preflightReason }],
2583
+ isError: true
2584
+ };
2585
+ }
2586
+ parsedArgs.payload = logic;
2587
+ return await this.executeInWorkerPool(
2588
+ parsedArgs,
2589
+ logic,
2590
+ request.name
2591
+ );
2592
+ }
2593
+ }
2594
+ const result = await entry.handler(parsedArgs, {});
2595
+ return result;
2596
+ } catch (error) {
2597
+ const e = error;
2598
+ if (e instanceof z.ZodError) {
2599
+ return {
2600
+ content: [{ type: "text", text: `Validation Error: ${e.message}` }],
2601
+ isError: true
2602
+ };
2603
+ }
2604
+ return {
2605
+ content: [
2606
+ { type: "text", text: `Internal Execution Error: ${e.message}` }
2607
+ ],
2608
+ isError: true
2609
+ };
2610
+ }
2611
+ }
2612
+ /**
2613
+ * Retrieves registered tools
2614
+ */
2615
+ listTools() {
2616
+ return Array.from(this.tools.values()).map((t) => t.tool);
2617
+ }
2618
+ /**
2619
+ * Retrieves registered prompts
2620
+ */
2621
+ listPrompts() {
2622
+ return Array.from(this.prompts.values()).map((p) => p.prompt);
2623
+ }
2624
+ /**
2625
+ * Gets a specific prompt by name
2626
+ */
2627
+ async getPrompt(request) {
2628
+ const entry = this.prompts.get(request.name);
2629
+ if (!entry) {
2630
+ throw new Error(`Prompt not found: ${request.name}`);
2631
+ }
2632
+ return await entry.handler(request);
2633
+ }
2634
+ /**
2635
+ * Retrieves registered resources
2636
+ */
2637
+ listResources() {
2638
+ return Array.from(this.resources.values());
2639
+ }
2640
+ /**
2641
+ * Reads a specific resource by URI
2642
+ */
2643
+ async readResource(uri) {
2644
+ const resource = this.resources.get(uri);
2645
+ if (!resource) {
2646
+ throw new Error(`Resource not found: ${uri}`);
2647
+ }
2648
+ let text = "No description provided";
2649
+ if (typeof resource.content === "function") {
2650
+ text = await resource.content();
2651
+ } else if (typeof resource.content === "string") {
2652
+ text = resource.content;
2653
+ } else if (resource.description) {
2654
+ text = resource.description;
2655
+ }
2656
+ return {
2657
+ contents: [
2658
+ {
2659
+ uri: resource.uri,
2660
+ mimeType: resource.mimeType || "text/plain",
2661
+ text
2662
+ }
2663
+ ]
2664
+ };
2665
+ }
2666
+ getServerInfo() {
2667
+ return this.serverInfo;
2668
+ }
2669
+ getMeshNode() {
2670
+ return this.meshNode;
2671
+ }
2672
+ /**
2673
+ * Injects data into the secure sandbox context for Logic-on-Origin tools.
2674
+ */
2675
+ setSandboxData(records) {
2676
+ this.sandboxRecords = records;
2677
+ }
2678
+ getBoundPort() {
2679
+ return this.boundPort;
2680
+ }
2681
+ /**
2682
+ * Connects to the libp2p Kademlia DHT and announces capabilities.
2683
+ * Boots the gRPC server for secure Logic-on-Origin.
2684
+ */
2685
+ async connectToMesh(options = {}) {
2686
+ const envPort = process.env.LIOP_GRPC_PORT ? Number.parseInt(process.env.LIOP_GRPC_PORT, 10) : void 0;
2687
+ const port = options.port ?? envPort ?? 50051;
2688
+ const TOKEN_SLUG_PATTERN = /^[A-Z][A-Z0-9_]*$/;
2689
+ if (this.config?.tokenSlug && !TOKEN_SLUG_PATTERN.test(this.config.tokenSlug)) {
2690
+ throw new Error(
2691
+ `Invalid tokenSlug "${this.config.tokenSlug}". Must match SCREAMING_SNAKE_CASE: /^[A-Z][A-Z0-9_]*$/ (e.g., "BANK", "VAULT", "HFT_ORACLE").`
2692
+ );
2693
+ }
2694
+ this.meshNode = new MeshNode(options.meshConfig);
2695
+ await this.meshNode.start();
2696
+ const meshNodeRef = this.meshNode;
2697
+ this.meshNode.registerManifestHandler(() => {
2698
+ const tools = this.listTools().map((t) => ({
2699
+ name: t.name,
2700
+ description: t.description,
2701
+ inputSchema: t.inputSchema
2702
+ }));
2703
+ const resources = Array.from(this.resources.values()).map((r) => ({
2704
+ name: r.name,
2705
+ uri: r.uri,
2706
+ description: r.description,
2707
+ mimeType: r.mimeType,
2708
+ text: typeof r.content === "string" ? r.content : r.description
2709
+ }));
2710
+ const unsignedManifest = {
2711
+ peerId: meshNodeRef.getPeerId(),
2712
+ grpcPort: port,
2713
+ tools,
2714
+ resources,
2715
+ serverInfo: this.serverInfo,
2716
+ authRequired: this.jwtValidator !== void 0,
2717
+ tokenSlug: this.config?.tokenSlug,
2718
+ taxonomy: this.config?.taxonomy ? {
2719
+ domain: this.config.taxonomy.domain || "Unknown Domain",
2720
+ clearanceTier: this.config.taxonomy.clearanceTier ?? 0,
2721
+ executionTypes: this.config.taxonomy.executionTypes || []
2722
+ } : void 0
2723
+ };
2724
+ const attestation = Dilithium65Wrapper.signManifest(
2725
+ unsignedManifest,
2726
+ this.pqcKeyPair.secretKey,
2727
+ this.pqcKeyPair.publicKey
2728
+ );
2729
+ return {
2730
+ ...unsignedManifest,
2731
+ pqcSignature: attestation.signature,
2732
+ pqcPublicKey: attestation.publicKey
2733
+ };
2734
+ });
2735
+ for (const tool of this.listTools()) {
2736
+ await this.meshNode.announceCapability(tool.name).catch(log.info);
2737
+ }
2738
+ await this.meshNode.announceManifest().catch(log.info);
2739
+ this.rpcServer = new LiopRpcServer();
2740
+ this.rpcServer.addService({
2741
+ negotiateIntent: (call, callback) => {
2742
+ const request = call.request;
2743
+ log.info(
2744
+ `[LIOP-RPC] Negotiating intent for capability: ${request.capability_hash}`
2745
+ );
2746
+ if (this.jwtValidator) {
2747
+ const authHeader = call.metadata.get("authorization")[0];
2748
+ if (!authHeader) {
2749
+ callback({
2750
+ code: grpc2.status.UNAUTHENTICATED,
2751
+ details: "Missing Authorization header in gRPC metadata"
2752
+ });
2753
+ return;
2754
+ }
2755
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
2756
+ const tokenHash = crypto3__default.createHash("sha256").update(token).digest("hex").toLowerCase();
2757
+ this.loadRevocationList();
2758
+ if (this.revokedTokenHashes.has(tokenHash)) {
2759
+ callback({
2760
+ code: grpc2.status.UNAUTHENTICATED,
2761
+ details: "Token has been revoked by the resource owner"
2762
+ });
2763
+ return;
2764
+ }
2765
+ const localTestToken = this.config?.auth?.localTestToken;
2766
+ const isTestTokenPattern = /^[a-zA-Z0-9_-]+-local-test-token$/;
2767
+ if (localTestToken) {
2768
+ if (token === localTestToken) {
2769
+ log.info(
2770
+ `[LIOP-RPC] Bypass authentication for matching localTestToken: ${localTestToken}`
2771
+ );
2772
+ import('./kyber-3ULIJSE3.js').then(
2773
+ async ({ Kyber768Wrapper }) => {
2774
+ const { publicKey, secretKey } = await Kyber768Wrapper.generateKeyPair();
2775
+ const sessionToken = crypto3__default.randomUUID();
2776
+ this.sessions.set(sessionToken, {
2777
+ capability_hash: request.capability_hash,
2778
+ kyber_sk: secretKey,
2779
+ agent_did: request.agent_did,
2780
+ tokenHash,
2781
+ createdAt: Date.now()
2782
+ });
2783
+ callback(null, {
2784
+ accepted: true,
2785
+ session_token: sessionToken,
2786
+ error_message: "",
2787
+ kyber_public_key: publicKey
2788
+ });
2789
+ }
2790
+ );
2791
+ return;
2792
+ }
2793
+ callback({
2794
+ code: grpc2.status.PERMISSION_DENIED,
2795
+ details: token && isTestTokenPattern.test(token) ? "Pre-shared local test token is invalid for this resource domain (segregation violation)." : "Access Denied: This restricted node requires its specific static access token."
2796
+ });
2797
+ return;
2798
+ }
2799
+ this.jwtValidator.validate(token).then((authInfo) => {
2800
+ const authResult = authorizeRequest("tools/call", authInfo);
2801
+ if (!authResult.allowed) {
2802
+ callback({
2803
+ code: grpc2.status.PERMISSION_DENIED,
2804
+ details: authResult.reason || "Access Denied"
2805
+ });
2806
+ return;
2807
+ }
2808
+ import('./kyber-3ULIJSE3.js').then(
2809
+ async ({ Kyber768Wrapper }) => {
2810
+ const { publicKey, secretKey } = await Kyber768Wrapper.generateKeyPair();
2811
+ const sessionToken = crypto3__default.randomUUID();
2812
+ this.sessions.set(sessionToken, {
2813
+ capability_hash: request.capability_hash,
2814
+ kyber_sk: secretKey,
2815
+ agent_did: request.agent_did,
2816
+ tokenHash,
2817
+ createdAt: Date.now()
2818
+ });
2819
+ callback(null, {
2820
+ accepted: true,
2821
+ session_token: sessionToken,
2822
+ error_message: "",
2823
+ kyber_public_key: publicKey
2824
+ });
2825
+ }
2826
+ );
2827
+ }).catch((err) => {
2828
+ callback({
2829
+ code: grpc2.status.UNAUTHENTICATED,
2830
+ details: `Invalid JWT token: ${err.message}`
2831
+ });
2832
+ });
2833
+ } else {
2834
+ import('./kyber-3ULIJSE3.js').then(async ({ Kyber768Wrapper }) => {
2835
+ const { publicKey, secretKey } = await Kyber768Wrapper.generateKeyPair();
2836
+ const sessionToken = crypto3__default.randomUUID();
2837
+ this.sessions.set(sessionToken, {
2838
+ capability_hash: request.capability_hash,
2839
+ kyber_sk: secretKey,
2840
+ agent_did: request.agent_did,
2841
+ createdAt: Date.now()
2842
+ });
2843
+ callback(null, {
2844
+ accepted: true,
2845
+ session_token: sessionToken,
2846
+ error_message: "",
2847
+ kyber_public_key: publicKey
2848
+ });
2849
+ });
2850
+ }
2851
+ },
2852
+ executeLogic: async (call) => {
2853
+ const request = call.request;
2854
+ log.info(
2855
+ `[LIOP-RPC] Executing Logic-on-Origin for session: ${request.session_token}`
2856
+ );
2857
+ const proceed = async () => {
2858
+ const session = this.sessions.get(request.session_token);
2859
+ if (!session) {
2860
+ call.emit("error", {
2861
+ code: grpc2.status.UNAUTHENTICATED,
2862
+ details: "Invalid session token"
2863
+ });
2864
+ return;
2865
+ }
2866
+ const MAX_SESSION_KEY_LIFETIME_MS = 3600 * 1e3;
2867
+ if (session.createdAt) {
2868
+ const sessionAge = Date.now() - session.createdAt;
2869
+ if (sessionAge > MAX_SESSION_KEY_LIFETIME_MS) {
2870
+ this.sessions.delete(request.session_token);
2871
+ call.emit("error", {
2872
+ code: grpc2.status.UNAUTHENTICATED,
2873
+ details: `[LIOP-PQC] Session secret expired: Age (${Math.round(sessionAge / 1e3)}s) exceeds 3600s TTL limit. Re-handshake required.`
2874
+ });
2875
+ return;
2876
+ }
2877
+ }
2878
+ if (session.tokenHash) {
2879
+ this.loadRevocationList();
2880
+ if (this.revokedTokenHashes.has(session.tokenHash)) {
2881
+ call.emit("error", {
2882
+ code: grpc2.status.UNAUTHENTICATED,
2883
+ details: "Token has been revoked by the resource owner"
2884
+ });
2885
+ return;
2886
+ }
2887
+ }
2888
+ const toolName = session.capability_hash;
2889
+ const toolDef = toolName ? this.tools.get(toolName) : void 0;
2890
+ const toolPolicy = toolDef?.policy;
2891
+ try {
2892
+ const kem = await createMlKem768();
2893
+ const sharedSecret = kem.decap(
2894
+ new Uint8Array(request.pqc_ciphertext),
2895
+ session.kyber_sk
2896
+ );
2897
+ const aesKey = Buffer.from(sharedSecret);
2898
+ const wasmBuffer = Buffer.from(request.wasm_binary);
2899
+ const authTag = wasmBuffer.subarray(-16);
2900
+ const encryptedData = wasmBuffer.subarray(0, -16);
2901
+ const decipher = crypto3__default.createDecipheriv(
2902
+ "aes-256-gcm",
2903
+ aesKey,
2904
+ Buffer.from(request.aes_nonce || new Uint8Array(12))
2905
+ );
2906
+ decipher.setAuthTag(authTag);
2907
+ let decrypted = decipher.update(encryptedData);
2908
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
2909
+ const decryptedPayload = decrypted.toString("utf-8");
2910
+ const logic = this.extractLogic(decryptedPayload) || decryptedPayload.trim();
2911
+ const preflightReason = this.runPreflightPolicy(
2912
+ toolName || "unknown_tool",
2913
+ logic,
2914
+ toolPolicy,
2915
+ session.agent_did || request.session_token
2916
+ );
2917
+ if (preflightReason) {
2918
+ log.info(`[LIOP-RPC] Preflight blocked: ${preflightReason}`);
2919
+ const errorResponse = {
2920
+ semantic_evidence: preflightReason,
2921
+ cryptographic_proof: Buffer.from(""),
2922
+ zk_receipt: Buffer.from(""),
2923
+ is_error: true
2924
+ };
2925
+ call.write(errorResponse, () => {
2926
+ call.end();
2927
+ });
2928
+ return;
2929
+ }
2930
+ } catch (decryptionError) {
2931
+ const errorMsg = decryptionError instanceof Error ? decryptionError.message : String(decryptionError);
2932
+ log.error(`[LIOP-RPC] Preflight decryption failed: ${errorMsg}`);
2933
+ call.emit("error", {
2934
+ code: grpc2.status.INTERNAL,
2935
+ details: `Preflight logic analysis failed: ${errorMsg}`
2936
+ });
2937
+ return;
2938
+ }
2939
+ try {
2940
+ const dpConfig = toolPolicy ? {
2941
+ epsilon: toolPolicy.dpEpsilon ?? 1,
2942
+ sensitivity: toolPolicy.dpSensitivity ?? 1,
2943
+ smallDatasetThreshold: toolPolicy.dpSmallDatasetThreshold ?? 50
2944
+ } : void 0;
2945
+ const workerResponse = await this.workerPool.run({
2946
+ ciphertext: request.pqc_ciphertext,
2947
+ secretKeyObj: Array.from(session.kyber_sk),
2948
+ wasmBinary: request.wasm_binary,
2949
+ inputs: request.inputs,
2950
+ aesNonce: request.aes_nonce,
2951
+ records: this.sandboxRecords,
2952
+ sessionToken: request.session_token,
2953
+ sessionTimestamp: session.createdAt,
2954
+ isEncrypted: true,
2955
+ dpConfig
2956
+ // Apply DP noise inside worker before ZK-Receipt commitment
2957
+ });
2958
+ const sanitizedWorkerOutput = sanitizeOutput(workerResponse.output);
2959
+ let finalOutput;
2960
+ let validationOutput = sanitizedWorkerOutput;
2961
+ try {
2962
+ finalOutput = typeof sanitizedWorkerOutput === "string" ? sanitizedWorkerOutput : JSON.stringify(sanitizedWorkerOutput);
2963
+ const decoded = JSON.parse(finalOutput);
2964
+ if (decoded.__liop_proxy_tool) {
2965
+ log.info(
2966
+ `[LIOP-RPC] Executing Proxied Tool: ${decoded.__liop_proxy_tool}`
2967
+ );
2968
+ const clientId = session.agent_did || "unknown-client";
2969
+ const toolResult = await this.callTool(
2970
+ {
2971
+ name: decoded.__liop_proxy_tool,
2972
+ arguments: decoded.__liop_proxy_args || {}
2973
+ },
2974
+ clientId
2975
+ );
2976
+ if (toolResult.isError) {
2977
+ log.info(
2978
+ `[LIOP-RPC] Proxy tool execution failed: ${toolResult.content[0].text}`
2979
+ );
2980
+ const errorResponse = {
2981
+ semantic_evidence: toolResult.content[0].text || "Unknown error",
2982
+ cryptographic_proof: Buffer.from(""),
2983
+ zk_receipt: Buffer.from(""),
2984
+ is_error: true
2985
+ };
2986
+ call.write(errorResponse, () => {
2987
+ call.end();
2988
+ });
2989
+ return;
2990
+ }
2991
+ const sanitizedToolResult = sanitizeOutput(toolResult);
2992
+ finalOutput = JSON.stringify(sanitizedToolResult);
2993
+ validationOutput = this.unwrapForAggregationPolicyScan(sanitizedToolResult);
2994
+ }
2995
+ } catch {
2996
+ finalOutput = String(sanitizedWorkerOutput);
2997
+ }
2998
+ const policyViolation = this.validateOutputPolicy(
2999
+ toolName || "unknown_tool",
3000
+ validationOutput,
3001
+ toolPolicy
3002
+ );
3003
+ if (policyViolation) {
3004
+ log.info(
3005
+ `[LIOP-RPC] Output policy blocked for ${toolName || "unknown_tool"}: ${policyViolation}`
3006
+ );
3007
+ const isDev = process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test" || process.env.LIOP_SEC_VERBOSE === "1";
3008
+ const errorMessage = isDev ? policyViolation : "[LIOP] Egress Security Violation. Output blocked due to policy enforcement.";
3009
+ const errorResponse = {
3010
+ semantic_evidence: errorMessage,
3011
+ cryptographic_proof: Buffer.from(""),
3012
+ zk_receipt: Buffer.from(""),
3013
+ is_error: true
3014
+ };
3015
+ call.write(errorResponse, () => {
3016
+ call.end();
3017
+ });
3018
+ return;
3019
+ }
3020
+ const response = {
3021
+ semantic_evidence: finalOutput,
3022
+ cryptographic_proof: Buffer.from(
3023
+ workerResponse.image_id || "",
3024
+ "hex"
3025
+ ),
3026
+ zk_receipt: workerResponse.zk_receipt ? Buffer.from(workerResponse.zk_receipt, "base64") : Buffer.from(""),
3027
+ is_error: false
3028
+ };
3029
+ const violation = await this.piiScanner.scan(validationOutput);
3030
+ const aggregationViolation = this.violatesAggregationFirstPolicy(
3031
+ this.unwrapForAggregationPolicyScan(validationOutput),
3032
+ toolPolicy?.enforceAggregationFirst,
3033
+ this.sandboxRecords?.length
3034
+ );
3035
+ const isBlocked = Boolean(violation || aggregationViolation);
3036
+ if (isBlocked) {
3037
+ const internalReason = violation || "Aggregation-First Policy Violation";
3038
+ log.info(
3039
+ `[LIOP-RPC] Secure egress blocked in gRPC stream: ${internalReason}`
3040
+ );
3041
+ response.semantic_evidence = "[LIOP] Egress Security Violation. Output blocked due to policy enforcement.";
3042
+ response.is_error = true;
3043
+ egressBlocksTotal.inc({
3044
+ reason: violation ? "pii_scanner" : "aggregation_policy"
3045
+ });
3046
+ }
3047
+ this.auditLogger.recordExecution({
3048
+ agentDid: session.agent_did || "unknown",
3049
+ peerId: session.tokenHash || "unknown",
3050
+ toolName: toolName || "unknown",
3051
+ datasetHash: this.getDatasetHash(),
3052
+ fuelConsumed: workerResponse.fuel_consumed || 0,
3053
+ outputHash: crypto3__default.createHash("sha256").update(response.semantic_evidence).digest("hex"),
3054
+ zkReceiptSig: workerResponse.zk_receipt || "",
3055
+ status: isBlocked ? "BLOCKED_EGRESS" : "SUCCESS"
3056
+ });
3057
+ toolCallsTotal.inc({
3058
+ tool: toolName || "unknown",
3059
+ status: isBlocked ? "blocked_egress" : "success"
3060
+ });
3061
+ fuelConsumed.observe(
3062
+ { tool: toolName || "unknown" },
3063
+ workerResponse.fuel_consumed || 0
3064
+ );
3065
+ call.write(response, () => {
3066
+ call.end();
3067
+ });
3068
+ } catch (error) {
3069
+ const e = error;
3070
+ const isDev = process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
3071
+ const detail = e.message || String(error);
3072
+ log.error(`[LIOP-RPC] Execution Error: ${detail}`);
3073
+ const errorMessage = isDev ? `Execution Error: ${detail}` : "[LIOP] Execution Failed. The injected logic violated runtime constraints or encountered a fatal error.";
3074
+ this.auditLogger.recordExecution({
3075
+ agentDid: session.agent_did || "unknown",
3076
+ peerId: session.tokenHash || "unknown",
3077
+ toolName: toolName || "unknown",
3078
+ datasetHash: this.getDatasetHash(),
3079
+ fuelConsumed: 0,
3080
+ outputHash: crypto3__default.createHash("sha256").update(errorMessage).digest("hex"),
3081
+ zkReceiptSig: "",
3082
+ status: "ERROR"
3083
+ });
3084
+ toolCallsTotal.inc({
3085
+ tool: toolName || "unknown",
3086
+ status: "error"
3087
+ });
3088
+ const errorResponse = {
3089
+ semantic_evidence: errorMessage,
3090
+ cryptographic_proof: Buffer.from(""),
3091
+ zk_receipt: Buffer.from(""),
3092
+ is_error: true
3093
+ };
3094
+ try {
3095
+ call.write(errorResponse, () => {
3096
+ call.end();
3097
+ });
3098
+ } catch (_writeErr) {
3099
+ call.end();
3100
+ }
3101
+ }
3102
+ };
3103
+ if (this.jwtValidator) {
3104
+ const authHeader = call.metadata.get("authorization")[0];
3105
+ if (!authHeader) {
3106
+ call.emit("error", {
3107
+ code: grpc2.status.UNAUTHENTICATED,
3108
+ details: "Missing Authorization header in gRPC metadata"
3109
+ });
3110
+ return;
3111
+ }
3112
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
3113
+ const tokenHash = crypto3__default.createHash("sha256").update(token).digest("hex").toLowerCase();
3114
+ this.loadRevocationList();
3115
+ if (this.revokedTokenHashes.has(tokenHash)) {
3116
+ call.emit("error", {
3117
+ code: grpc2.status.UNAUTHENTICATED,
3118
+ details: "Token has been revoked by the resource owner"
3119
+ });
3120
+ return;
3121
+ }
3122
+ const localTestToken = this.config?.auth?.localTestToken;
3123
+ const isTestTokenPattern = /^[a-zA-Z0-9_-]+-local-test-token$/;
3124
+ if (localTestToken) {
3125
+ if (token === localTestToken) {
3126
+ log.info(
3127
+ `[LIOP-RPC] Bypass authentication in executeLogic for matching localTestToken: ${localTestToken}`
3128
+ );
3129
+ await proceed();
3130
+ return;
3131
+ }
3132
+ call.emit("error", {
3133
+ code: grpc2.status.PERMISSION_DENIED,
3134
+ details: token && isTestTokenPattern.test(token) ? "Pre-shared local test token is invalid for this resource domain (segregation violation)." : "Access Denied: This restricted node requires its specific static access token."
3135
+ });
3136
+ return;
3137
+ }
3138
+ try {
3139
+ const authInfo = await this.jwtValidator.validate(token);
3140
+ const authResult = authorizeRequest("tools/call", authInfo);
3141
+ if (!authResult.allowed) {
3142
+ call.emit("error", {
3143
+ code: grpc2.status.PERMISSION_DENIED,
3144
+ details: authResult.reason || "Access Denied"
3145
+ });
3146
+ return;
3147
+ }
3148
+ await proceed();
3149
+ } catch (err) {
3150
+ call.emit("error", {
3151
+ code: grpc2.status.UNAUTHENTICATED,
3152
+ details: `Invalid JWT token: ${err instanceof Error ? err.message : String(err)}`
3153
+ });
3154
+ }
3155
+ } else {
3156
+ await proceed();
3157
+ }
3158
+ }
3159
+ });
3160
+ this.boundPort = await this.rpcServer.listen(port);
3161
+ log.info(
3162
+ `[LIOP-SDK] Node successfully announced to Mesh. PeerID: ${this.meshNode.getPeerId()}`
3163
+ );
3164
+ }
3165
+ /**
3166
+ * Internal worker execution with Egress Filtering logic.
3167
+ */
3168
+ async executeInWorkerPool(_args, rawPayload, toolName) {
3169
+ try {
3170
+ const dpPolicy = toolName ? this.tools.get(toolName)?.policy : void 0;
3171
+ const dpConfig = dpPolicy ? {
3172
+ epsilon: dpPolicy.dpEpsilon ?? 1,
3173
+ sensitivity: dpPolicy.dpSensitivity ?? 1,
3174
+ smallDatasetThreshold: dpPolicy.dpSmallDatasetThreshold ?? 50
3175
+ } : void 0;
3176
+ const workerResponse = await this.workerPool.run({
3177
+ ciphertext: new Uint8Array(0),
3178
+ secretKeyObj: Array.from(new Uint8Array(0)),
3179
+ kyberPublicKey: new Uint8Array(0),
3180
+ wasmBinary: Buffer.from(rawPayload),
3181
+ inputs: {},
3182
+ records: this.sandboxRecords,
3183
+ sessionToken: "local-dev-token",
3184
+ isEncrypted: false,
3185
+ // Use plaintext for local Logic-on-Origin injection
3186
+ dpConfig
3187
+ // Pass DP Config to apply inside worker before ZK-Receipt commitment
3188
+ });
3189
+ const dpOutput = workerResponse.output;
3190
+ const sanitizedOutput = sanitizeOutput(dpOutput);
3191
+ const textOutput = JSON.stringify({
3192
+ computation_result: sanitizedOutput,
3193
+ image_id: workerResponse.image_id,
3194
+ zk_receipt: workerResponse.zk_receipt,
3195
+ status: "Worker Pool Execution Success"
3196
+ });
3197
+ const content = [
3198
+ {
3199
+ type: "text",
3200
+ text: textOutput
3201
+ }
3202
+ ];
3203
+ const toolPolicy = toolName ? this.tools.get(toolName)?.policy : void 0;
3204
+ const policyViolation = this.validateOutputPolicy(
3205
+ toolName || "unknown_tool",
3206
+ sanitizedOutput,
3207
+ // Phase 109: Validate NOISY output to ensure invariants
3208
+ toolPolicy
3209
+ );
3210
+ if (policyViolation) {
3211
+ log.info(
3212
+ `[LIOP-SDK] Output policy blocked for ${toolName || "unknown_tool"}: ${policyViolation}`
3213
+ );
3214
+ const isDev = process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test" || process.env.LIOP_SEC_VERBOSE === "1";
3215
+ const errorMessage = isDev ? policyViolation : "[LIOP] Egress Security Violation. Output blocked due to policy enforcement. Ensure your logic uses strictly aggregated, non-PII patterns.";
3216
+ return {
3217
+ content: [
3218
+ {
3219
+ type: "text",
3220
+ text: errorMessage
3221
+ }
3222
+ ],
3223
+ isError: true
3224
+ };
3225
+ }
3226
+ const violation = await this.piiScanner.scan(sanitizedOutput);
3227
+ const aggregationViolation = this.violatesAggregationFirstPolicy(
3228
+ sanitizedOutput,
3229
+ // Phase 109: Validate NOISY output
3230
+ toolPolicy?.enforceAggregationFirst,
3231
+ this.sandboxRecords?.length
3232
+ );
3233
+ if (violation || aggregationViolation) {
3234
+ const internalReason = violation || "Aggregation-First Policy Violation: Output blocked due to dynamic flat-key policy enforcement.";
3235
+ log.info(
3236
+ `[LIOP-SDK] Secure egress blocked in local execution: ${internalReason}`
3237
+ );
3238
+ const isDev = process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test" || process.env.LIOP_SEC_VERBOSE === "1";
3239
+ const errorMessage = isDev ? `[LIOP] Egress Security Violation: ${internalReason}` : "[LIOP] Egress Security Violation. Output blocked due to policy enforcement. Ensure your logic uses strictly aggregated, non-PII patterns.";
3240
+ return {
3241
+ content: [
3242
+ {
3243
+ type: "text",
3244
+ text: errorMessage
3245
+ }
3246
+ ],
3247
+ isError: true
3248
+ };
3249
+ }
3250
+ return { content };
3251
+ } catch (error) {
3252
+ const e = error;
3253
+ const isDev = process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test" || process.env.LIOP_SEC_VERBOSE === "1";
3254
+ const detail = e.message || String(error);
3255
+ log.error(`[LIOP-SDK] WorkerPool Execution Fault: ${detail}`);
3256
+ const isOom = detail.includes("worker_thread_exited") || detail.includes("ERR_WORKER_OUT_OF_MEMORY") || detail.includes("terminated") || detail.includes("heap limit");
3257
+ const errorMessage = isOom ? "[LIOP] Execution terminated: memory limit exceeded (64MB heap). Reduce data processing volume." : isDev ? `WorkerPoolError: ${detail}` : "[LIOP] Execution Failed. The injected logic violated runtime constraints or encountered a fatal error.";
3258
+ return {
3259
+ content: [
3260
+ {
3261
+ type: "text",
3262
+ text: errorMessage
3263
+ }
3264
+ ],
3265
+ isError: true
3266
+ };
3267
+ }
3268
+ }
3269
+ /**
3270
+ * Safely destroys the worker pool, gRPC server, and Mesh node.
3271
+ * Recommended to be called during graceful shutdowns or test teardowns.
3272
+ */
3273
+ async close() {
3274
+ if (this.workerPool) {
3275
+ await this.workerPool.close({ force: true });
3276
+ }
3277
+ if (this.rpcServer) {
3278
+ await this.rpcServer.stop();
3279
+ }
3280
+ if (this.meshNode) {
3281
+ await this.meshNode.stop();
3282
+ }
3283
+ }
3284
+ loadRevocationList() {
3285
+ const rPath = this.config?.auth?.revocationPath;
3286
+ if (!rPath) return;
3287
+ try {
3288
+ if (fs2.existsSync(rPath)) {
3289
+ const stats = fs2.statSync(rPath);
3290
+ if (stats.mtimeMs <= this.lastRevocationLoadTime) {
3291
+ return;
3292
+ }
3293
+ const content = fs2.readFileSync(rPath, "utf-8");
3294
+ const list = JSON.parse(content);
3295
+ if (Array.isArray(list)) {
3296
+ this.revokedTokenHashes.clear();
3297
+ for (const item of list) {
3298
+ if (typeof item === "string") {
3299
+ this.revokedTokenHashes.add(item.trim().toLowerCase());
3300
+ }
3301
+ }
3302
+ this.lastRevocationLoadTime = stats.mtimeMs;
3303
+ log.info(
3304
+ `[LiopServer] Loaded ${this.revokedTokenHashes.size} revoked token hashes from ${rPath}`
3305
+ );
3306
+ }
3307
+ } else {
3308
+ const dir = path.dirname(rPath);
3309
+ if (!fs2.existsSync(dir)) {
3310
+ fs2.mkdirSync(dir, { recursive: true });
3311
+ }
3312
+ fs2.writeFileSync(rPath, JSON.stringify([], null, 2), "utf-8");
3313
+ this.lastRevocationLoadTime = Date.now();
3314
+ log.info(
3315
+ `[LiopServer] Created empty local revocation list at ${rPath}`
3316
+ );
3317
+ }
3318
+ } catch (err) {
3319
+ log.error(
3320
+ `[LiopServer] Failed to load revocation list: ${err instanceof Error ? err.message : String(err)}`
3321
+ );
3322
+ }
3323
+ }
3324
+ revokeToken(token) {
3325
+ if (!token) return;
3326
+ const hash = crypto3__default.createHash("sha256").update(token).digest("hex").toLowerCase();
3327
+ this.revokeTokenHash(hash);
3328
+ }
3329
+ revokeTokenHash(hash) {
3330
+ if (!hash) return;
3331
+ const normalizedHash = hash.toLowerCase();
3332
+ this.loadRevocationList();
3333
+ this.revokedTokenHashes.add(normalizedHash);
3334
+ const rPath = this.config?.auth?.revocationPath;
3335
+ if (rPath) {
3336
+ try {
3337
+ const dir = path.dirname(rPath);
3338
+ if (!fs2.existsSync(dir)) {
3339
+ fs2.mkdirSync(dir, { recursive: true });
3340
+ }
3341
+ const hashes = Array.from(this.revokedTokenHashes);
3342
+ fs2.writeFileSync(rPath, JSON.stringify(hashes, null, 2), "utf-8");
3343
+ const stats = fs2.statSync(rPath);
3344
+ this.lastRevocationLoadTime = stats.mtimeMs;
3345
+ log.info(
3346
+ `[LiopServer] Persisted revocation for hash ${normalizedHash} to ${rPath}`
3347
+ );
3348
+ } catch (err) {
3349
+ log.error(
3350
+ `[LiopServer] Failed to persist revocation: ${err instanceof Error ? err.message : String(err)}`
3351
+ );
3352
+ }
3353
+ }
3354
+ }
3355
+ /**
3356
+ * Resets the query budget for a specific client identity.
3357
+ * If toolName is provided, only that tool's budget is cleared.
3358
+ * Otherwise, all tool budgets for the client are reset.
3359
+ *
3360
+ * Operates on both in-memory and persistent (disk) budget stores.
3361
+ * Uses file-level locking for safe concurrent access.
3362
+ */
3363
+ resetFieldBudget(clientId, toolName) {
3364
+ if (clientId === "__proto__" || clientId === "constructor" || clientId === "prototype" || toolName && (toolName === "__proto__" || toolName === "constructor" || toolName === "prototype")) {
3365
+ log.warn(
3366
+ `[LiopServer] Blocked resetFieldBudget call due to prototype pollution keys.`
3367
+ );
3368
+ return;
3369
+ }
3370
+ if (toolName) {
3371
+ const clientBudget = this.fieldQueryBudget.get(clientId);
3372
+ if (clientBudget) {
3373
+ clientBudget.delete(toolName);
3374
+ }
3375
+ } else {
3376
+ this.fieldQueryBudget.delete(clientId);
3377
+ }
3378
+ const storePath = this.config?.budgetStorePath;
3379
+ if (storePath) {
3380
+ try {
3381
+ this.executeWithBudgetLock(storePath, (budget) => {
3382
+ if (toolName) {
3383
+ if (budget[clientId]) {
3384
+ delete budget[clientId][toolName];
3385
+ }
3386
+ } else {
3387
+ delete budget[clientId];
3388
+ }
3389
+ return { result: void 0, updatedBudget: budget };
3390
+ });
3391
+ log.info(
3392
+ `[LiopServer] Reset query budget for client ${clientId}${toolName ? ` / tool ${toolName}` : ""}`
3393
+ );
3394
+ } catch (err) {
3395
+ log.error(
3396
+ `[LiopServer] Failed to reset persistent budget: ${err instanceof Error ? err.message : String(err)}`
3397
+ );
3398
+ }
3399
+ }
3400
+ }
3401
+ getPersistentBudget(storePath) {
3402
+ try {
3403
+ if (!fs2.existsSync(storePath)) {
3404
+ return {};
3405
+ }
3406
+ const content = fs2.readFileSync(storePath, "utf-8");
3407
+ return JSON.parse(content || "{}");
3408
+ } catch {
3409
+ return {};
3410
+ }
3411
+ }
3412
+ savePersistentBudget(storePath, budget) {
3413
+ const tempPath = `${storePath}.tmp`;
3414
+ const dir = path.dirname(storePath);
3415
+ if (!fs2.existsSync(dir)) {
3416
+ fs2.mkdirSync(dir, { recursive: true });
3417
+ }
3418
+ fs2.writeFileSync(tempPath, JSON.stringify(budget, null, 2), "utf-8");
3419
+ fs2.renameSync(tempPath, storePath);
3420
+ }
3421
+ executeWithBudgetLock(storePath, action) {
3422
+ const lockPath = `${storePath}.lock`;
3423
+ const maxRetries = 100;
3424
+ let attempts = 0;
3425
+ const dir = path.dirname(storePath);
3426
+ if (!fs2.existsSync(dir)) {
3427
+ try {
3428
+ fs2.mkdirSync(dir, { recursive: true });
3429
+ } catch {
3430
+ }
3431
+ }
3432
+ while (attempts < maxRetries) {
3433
+ try {
3434
+ fs2.writeFileSync(lockPath, process.pid.toString(), { flag: "wx" });
3435
+ break;
3436
+ } catch (e) {
3437
+ if (e && typeof e === "object" && "code" in e && e.code === "EEXIST") {
3438
+ attempts++;
3439
+ } else {
3440
+ throw e;
3441
+ }
3442
+ }
3443
+ }
3444
+ if (attempts >= maxRetries) {
3445
+ throw new Error(
3446
+ "Timeout acquiring lock for persistent query budget database"
3447
+ );
3448
+ }
3449
+ try {
3450
+ const currentBudget = this.getPersistentBudget(storePath);
3451
+ const { result, updatedBudget } = action(currentBudget);
3452
+ if (updatedBudget) {
3453
+ this.savePersistentBudget(storePath, updatedBudget);
3454
+ }
3455
+ return result;
3456
+ } finally {
3457
+ try {
3458
+ if (fs2.existsSync(lockPath)) {
3459
+ fs2.unlinkSync(lockPath);
3460
+ }
3461
+ } catch {
3462
+ }
3463
+ }
3464
+ }
3465
+ applyInMemoryBudget(clientId, _toolName, extractedFields, policy) {
3466
+ let clientBudget = this.fieldQueryBudget.get(clientId);
3467
+ if (!clientBudget) {
3468
+ clientBudget = /* @__PURE__ */ new Map();
3469
+ this.fieldQueryBudget.set(clientId, clientBudget);
3470
+ }
3471
+ let toolBudget = clientBudget.get(_toolName);
3472
+ if (!toolBudget) {
3473
+ toolBudget = /* @__PURE__ */ new Map();
3474
+ clientBudget.set(_toolName, toolBudget);
3475
+ }
3476
+ for (const field of extractedFields) {
3477
+ const sensitivity = this.taintAnalyzer.classifyField(
3478
+ field,
3479
+ policy?.sensitiveKeys
3480
+ );
3481
+ let queryLimit = 25;
3482
+ let sensLabel = "public";
3483
+ if (policy?.queryBudgetPerField !== void 0) {
3484
+ queryLimit = policy.queryBudgetPerField;
3485
+ sensLabel = "override";
3486
+ } else if (sensitivity === "forbidden") {
3487
+ queryLimit = 3;
3488
+ sensLabel = "forbidden";
3489
+ } else if (sensitivity === "sensitive") {
3490
+ queryLimit = 8;
3491
+ sensLabel = "sensitive";
3492
+ }
3493
+ const count = toolBudget.get(field) ?? 0;
3494
+ if (count >= queryLimit) {
3495
+ return `Preflight policy rejected: Query budget exceeded for field '${field}' (max ${queryLimit} per session for ${sensLabel} fields). Rotate PQC session to reset budget.`;
3496
+ }
3497
+ }
3498
+ for (const field of extractedFields) {
3499
+ const count = toolBudget.get(field) ?? 0;
3500
+ toolBudget.set(field, count + 1);
3501
+ }
3502
+ return null;
3503
+ }
3504
+ };
3505
+
3506
+ export { AUTH_DEFAULTS, AuditLogger, GENESIS_HASH, JwtValidator, LiopRpcServer, LiopServer, NerScanner, PII_PATTERNS, PII_PRESETS, PiiScanner, computeEntryHash, createOAuthServer, globalAuditLogger };
3507
+ //# sourceMappingURL=chunk-UBOQZVV5.js.map
3508
+ //# sourceMappingURL=chunk-UBOQZVV5.js.map