@nekzus/liop 2.1.0-alpha.1 → 2.1.0-alpha.11

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