@nekzus/liop 2.1.0-alpha.4 → 2.1.0-alpha.5

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