@ninelives/agent-governance-sdk 5.0.1

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 (126) hide show
  1. package/README.md +363 -0
  2. package/dist/audit.d.ts +26 -0
  3. package/dist/audit.d.ts.map +1 -0
  4. package/dist/audit.js +107 -0
  5. package/dist/audit.js.map +1 -0
  6. package/dist/cascade-containment.d.ts +84 -0
  7. package/dist/cascade-containment.d.ts.map +1 -0
  8. package/dist/cascade-containment.js +387 -0
  9. package/dist/cascade-containment.js.map +1 -0
  10. package/dist/client.d.ts +34 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +112 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/context-poisoning.d.ts +58 -0
  15. package/dist/context-poisoning.d.ts.map +1 -0
  16. package/dist/context-poisoning.js +410 -0
  17. package/dist/context-poisoning.js.map +1 -0
  18. package/dist/credential-vault.d.ts +213 -0
  19. package/dist/credential-vault.d.ts.map +1 -0
  20. package/dist/credential-vault.js +567 -0
  21. package/dist/credential-vault.js.map +1 -0
  22. package/dist/discovery.d.ts +80 -0
  23. package/dist/discovery.d.ts.map +1 -0
  24. package/dist/discovery.js +404 -0
  25. package/dist/discovery.js.map +1 -0
  26. package/dist/encryption/channel.d.ts +29 -0
  27. package/dist/encryption/channel.d.ts.map +1 -0
  28. package/dist/encryption/channel.js +78 -0
  29. package/dist/encryption/channel.js.map +1 -0
  30. package/dist/encryption/index.d.ts +9 -0
  31. package/dist/encryption/index.d.ts.map +1 -0
  32. package/dist/encryption/index.js +23 -0
  33. package/dist/encryption/index.js.map +1 -0
  34. package/dist/encryption/mesh-client.d.ts +281 -0
  35. package/dist/encryption/mesh-client.d.ts.map +1 -0
  36. package/dist/encryption/mesh-client.js +1110 -0
  37. package/dist/encryption/mesh-client.js.map +1 -0
  38. package/dist/encryption/ratchet.d.ts +38 -0
  39. package/dist/encryption/ratchet.d.ts.map +1 -0
  40. package/dist/encryption/ratchet.js +223 -0
  41. package/dist/encryption/ratchet.js.map +1 -0
  42. package/dist/encryption/registry-client.d.ts +136 -0
  43. package/dist/encryption/registry-client.d.ts.map +1 -0
  44. package/dist/encryption/registry-client.js +369 -0
  45. package/dist/encryption/registry-client.js.map +1 -0
  46. package/dist/encryption/x3dh.d.ts +80 -0
  47. package/dist/encryption/x3dh.d.ts.map +1 -0
  48. package/dist/encryption/x3dh.js +238 -0
  49. package/dist/encryption/x3dh.js.map +1 -0
  50. package/dist/framework-adapter.d.ts +63 -0
  51. package/dist/framework-adapter.d.ts.map +1 -0
  52. package/dist/framework-adapter.js +253 -0
  53. package/dist/framework-adapter.js.map +1 -0
  54. package/dist/identity.d.ts +124 -0
  55. package/dist/identity.d.ts.map +1 -0
  56. package/dist/identity.js +446 -0
  57. package/dist/identity.js.map +1 -0
  58. package/dist/index.d.ts +39 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +95 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/kill-switch.d.ts +22 -0
  63. package/dist/kill-switch.d.ts.map +1 -0
  64. package/dist/kill-switch.js +60 -0
  65. package/dist/kill-switch.js.map +1 -0
  66. package/dist/lifecycle.d.ts +50 -0
  67. package/dist/lifecycle.d.ts.map +1 -0
  68. package/dist/lifecycle.js +113 -0
  69. package/dist/lifecycle.js.map +1 -0
  70. package/dist/mcp.d.ts +41 -0
  71. package/dist/mcp.d.ts.map +1 -0
  72. package/dist/mcp.js +233 -0
  73. package/dist/mcp.js.map +1 -0
  74. package/dist/metrics.d.ts +123 -0
  75. package/dist/metrics.d.ts.map +1 -0
  76. package/dist/metrics.js +289 -0
  77. package/dist/metrics.js.map +1 -0
  78. package/dist/oci-manifest.d.ts +49 -0
  79. package/dist/oci-manifest.d.ts.map +1 -0
  80. package/dist/oci-manifest.js +182 -0
  81. package/dist/oci-manifest.js.map +1 -0
  82. package/dist/policy-backends/cedar.d.ts +15 -0
  83. package/dist/policy-backends/cedar.d.ts.map +1 -0
  84. package/dist/policy-backends/cedar.js +88 -0
  85. package/dist/policy-backends/cedar.js.map +1 -0
  86. package/dist/policy-backends/opa.d.ts +17 -0
  87. package/dist/policy-backends/opa.d.ts.map +1 -0
  88. package/dist/policy-backends/opa.js +74 -0
  89. package/dist/policy-backends/opa.js.map +1 -0
  90. package/dist/policy.d.ts +77 -0
  91. package/dist/policy.d.ts.map +1 -0
  92. package/dist/policy.js +760 -0
  93. package/dist/policy.js.map +1 -0
  94. package/dist/prompt-defense.d.ts +37 -0
  95. package/dist/prompt-defense.d.ts.map +1 -0
  96. package/dist/prompt-defense.js +229 -0
  97. package/dist/prompt-defense.js.map +1 -0
  98. package/dist/protocol-facets.d.ts +87 -0
  99. package/dist/protocol-facets.d.ts.map +1 -0
  100. package/dist/protocol-facets.js +728 -0
  101. package/dist/protocol-facets.js.map +1 -0
  102. package/dist/rings.d.ts +17 -0
  103. package/dist/rings.d.ts.map +1 -0
  104. package/dist/rings.js +61 -0
  105. package/dist/rings.js.map +1 -0
  106. package/dist/sandbox.d.ts +61 -0
  107. package/dist/sandbox.d.ts.map +1 -0
  108. package/dist/sandbox.js +199 -0
  109. package/dist/sandbox.js.map +1 -0
  110. package/dist/surface-parity.d.ts +45 -0
  111. package/dist/surface-parity.d.ts.map +1 -0
  112. package/dist/surface-parity.js +216 -0
  113. package/dist/surface-parity.js.map +1 -0
  114. package/dist/trust.d.ts +28 -0
  115. package/dist/trust.d.ts.map +1 -0
  116. package/dist/trust.js +209 -0
  117. package/dist/trust.js.map +1 -0
  118. package/dist/types.d.ts +422 -0
  119. package/dist/types.d.ts.map +1 -0
  120. package/dist/types.js +29 -0
  121. package/dist/types.js.map +1 -0
  122. package/dist/verify.d.ts +135 -0
  123. package/dist/verify.d.ts.map +1 -0
  124. package/dist/verify.js +356 -0
  125. package/dist/verify.js.map +1 -0
  126. package/package.json +66 -0
@@ -0,0 +1,728 @@
1
+ "use strict";
2
+ // Copyright (c) Microsoft Corporation.
3
+ // Licensed under the MIT License.
4
+ /**
5
+ * Protocol-aware facet extraction for policy evaluation.
6
+ *
7
+ * Populates `sql.*` and `k8s.*` fields in the evaluation context so policy
8
+ * rules can reference wire-level semantics (e.g. `sql.verb`, `k8s.namespace`)
9
+ * instead of only HTTP metadata.
10
+ *
11
+ * This is the TypeScript port of `protocol_facets.py` and mirrors its public
12
+ * contract: `FacetRegistry`, `defaultRegistry`, and `extractProtocolFacets`.
13
+ *
14
+ * To add support for a new protocol, register an extractor on
15
+ * `defaultRegistry`:
16
+ *
17
+ * ```ts
18
+ * import { defaultRegistry } from '@microsoft/agent-governance-sdk';
19
+ *
20
+ * defaultRegistry.register('redis', (redisCtx) => ({
21
+ * verb: String(redisCtx.command ?? '').toUpperCase(),
22
+ * key: String(redisCtx.key ?? ''),
23
+ * }));
24
+ * ```
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.defaultRegistry = exports.FacetRegistry = void 0;
28
+ exports.extractSqlFacets = extractSqlFacets;
29
+ exports.extractK8sFacets = extractK8sFacets;
30
+ exports.extractProtocolFacets = extractProtocolFacets;
31
+ /**
32
+ * Holds protocol facet extractors keyed by context field name.
33
+ *
34
+ * Each extractor receives the sub-object stored at its registered key and
35
+ * returns fields to merge back into that sub-object. Errors thrown inside an
36
+ * extractor are caught and logged so a broken parser can never block policy
37
+ * evaluation (fail-open for extraction, fail-closed for policy).
38
+ */
39
+ class FacetRegistry {
40
+ extractors = [];
41
+ /** Register an extractor for sub-objects stored at `contextKey`. */
42
+ register(contextKey, extractor) {
43
+ this.extractors.push({ key: contextKey, extractor });
44
+ }
45
+ /**
46
+ * Run all registered extractors against `context`. Mutates the top-level
47
+ * `context` map (replacing the slot for each registered key with a fresh
48
+ * merged copy) but does NOT mutate the caller's nested sub-objects, so
49
+ * passing the same sub-object alias into multiple evaluations is safe.
50
+ */
51
+ extract(context) {
52
+ for (const { key, extractor } of this.extractors) {
53
+ const sub = context[key];
54
+ if (!isPlainObject(sub))
55
+ continue;
56
+ try {
57
+ // Hand the extractor a defensive copy so a buggy implementation
58
+ // cannot mutate the caller's input through the argument.
59
+ const facets = extractor({ ...sub });
60
+ if (facets && typeof facets === 'object') {
61
+ // Replace the slot with a merged copy rather than mutating
62
+ // `sub` in place, so the caller's original sub-object (which
63
+ // may be aliased elsewhere) is left untouched.
64
+ context[key] = { ...sub, ...facets };
65
+ }
66
+ }
67
+ catch (err) {
68
+ // Never block evaluation if an extractor throws.
69
+ // eslint-disable-next-line no-console
70
+ console.warn(`[protocol-facets] extractor for '${key}' threw: ${err instanceof Error ? err.message : String(err)}`);
71
+ }
72
+ }
73
+ return context;
74
+ }
75
+ /** Number of registered extractors (useful for tests). */
76
+ get size() {
77
+ return this.extractors.length;
78
+ }
79
+ /** Remove all extractors. Primarily for tests. */
80
+ clear() {
81
+ this.extractors.length = 0;
82
+ }
83
+ }
84
+ exports.FacetRegistry = FacetRegistry;
85
+ function isPlainObject(value) {
86
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
87
+ }
88
+ // ── SQL facets ──────────────────────────────────────────────────────────────
89
+ /**
90
+ * Canonical set of SQL verbs we surface to policies. Anything not in this set
91
+ * is reported as `UNKNOWN` (fail-closed).
92
+ */
93
+ const SQL_KNOWN_VERBS = new Set([
94
+ 'SELECT',
95
+ 'INSERT',
96
+ 'UPDATE',
97
+ 'DELETE',
98
+ 'DROP',
99
+ 'TRUNCATE',
100
+ 'ALTER',
101
+ 'CREATE',
102
+ 'GRANT',
103
+ 'REVOKE',
104
+ 'MERGE',
105
+ 'CALL',
106
+ 'EXECUTE',
107
+ 'EXPLAIN',
108
+ 'WITH',
109
+ 'REPLACE',
110
+ 'RENAME',
111
+ 'COMMENT',
112
+ ]);
113
+ const EMPTY_SQL_FACETS = { verb: '', target: '', tables: '', functions: '' };
114
+ const UNKNOWN_SQL_FACETS = {
115
+ verb: 'UNKNOWN',
116
+ target: '',
117
+ tables: '',
118
+ functions: '',
119
+ };
120
+ /**
121
+ * Best-effort regex tokenizer used when no AST parser is available.
122
+ *
123
+ * This intentionally avoids pulling in a heavy SQL grammar dependency. It is
124
+ * accurate enough for the wire-protocol policy use case (verb + first table)
125
+ * and matches the Python implementation's exposed fields. For complex SQL
126
+ * (CTEs, subqueries, dialect-specific syntax) callers should register their
127
+ * own extractor backed by a full parser such as `node-sql-parser`.
128
+ */
129
+ function regexExtractSql(rawQuery) {
130
+ const stripped = stripSqlComments(rawQuery).trim();
131
+ if (!stripped)
132
+ return { ...EMPTY_SQL_FACETS };
133
+ // Multi-statement guard: fail closed if the input contains more than one
134
+ // meaningful statement (ignoring trailing semicolons and empties). A single
135
+ // attacker-supplied query like `SELECT 1; DROP TABLE x` must not be
136
+ // classified as `SELECT`.
137
+ const statements = splitSqlStatements(stripped);
138
+ if (statements.length > 1) {
139
+ return { ...UNKNOWN_SQL_FACETS };
140
+ }
141
+ const query = statements[0] ?? stripped;
142
+ // First non-whitespace word is the surface verb.
143
+ const verbMatch = query.match(/^\s*([A-Za-z]+)/);
144
+ if (!verbMatch)
145
+ return { ...UNKNOWN_SQL_FACETS };
146
+ const surfaceVerb = verbMatch[1].toUpperCase();
147
+ // CTE handling: `WITH ... <verb> ...` should be classified by the underlying
148
+ // DML verb, not by the `WITH` keyword. Matches Python sqlglot which exposes
149
+ // the inner statement type for `WITH` queries.
150
+ let verb;
151
+ if (surfaceVerb === 'WITH') {
152
+ verb = detectCteInnerVerb(query);
153
+ }
154
+ else {
155
+ verb = SQL_KNOWN_VERBS.has(surfaceVerb) ? surfaceVerb : 'UNKNOWN';
156
+ }
157
+ const tables = extractTables(query, verb);
158
+ const functions = extractFunctions(query);
159
+ const target = pickTarget(query, verb, tables);
160
+ return {
161
+ verb,
162
+ target,
163
+ tables: tables.join(','),
164
+ functions: functions.join(','),
165
+ };
166
+ }
167
+ /**
168
+ * Split SQL on top-level semicolons (semicolons outside of quoted strings).
169
+ * Trailing empty/whitespace segments are dropped.
170
+ */
171
+ function splitSqlStatements(sql) {
172
+ const out = [];
173
+ let buf = '';
174
+ let quote = '';
175
+ for (let i = 0; i < sql.length; i++) {
176
+ const ch = sql[i];
177
+ if (quote) {
178
+ buf += ch;
179
+ if (ch === quote) {
180
+ // Handle SQL doubled-quote escape ('' or "")
181
+ if (sql[i + 1] === quote) {
182
+ buf += sql[++i];
183
+ }
184
+ else {
185
+ quote = '';
186
+ }
187
+ }
188
+ continue;
189
+ }
190
+ if (ch === "'" || ch === '"' || ch === '`') {
191
+ quote = ch;
192
+ buf += ch;
193
+ continue;
194
+ }
195
+ if (ch === ';') {
196
+ const t = buf.trim();
197
+ if (t)
198
+ out.push(t);
199
+ buf = '';
200
+ continue;
201
+ }
202
+ buf += ch;
203
+ }
204
+ const tail = buf.trim();
205
+ if (tail)
206
+ out.push(tail);
207
+ return out;
208
+ }
209
+ /**
210
+ * For a CTE-prefixed query (`WITH ... AS (...) <verb> ...`), find the first
211
+ * DML keyword that appears at top-level paren depth (i.e. not inside a CTE
212
+ * body or subquery). Falls back to SELECT because every CTE must terminate
213
+ * in one of SELECT/INSERT/UPDATE/DELETE/MERGE.
214
+ */
215
+ function detectCteInnerVerb(query) {
216
+ const candidates = new Set(['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'MERGE']);
217
+ let depth = 0;
218
+ let quote = '';
219
+ let token = '';
220
+ let sawFirstWord = false;
221
+ for (let i = 0; i <= query.length; i++) {
222
+ const ch = i < query.length ? query[i] : ' ';
223
+ if (quote) {
224
+ if (ch === quote) {
225
+ if (query[i + 1] === quote) {
226
+ i++;
227
+ }
228
+ else {
229
+ quote = '';
230
+ }
231
+ }
232
+ continue;
233
+ }
234
+ if (ch === "'" || ch === '"' || ch === '`') {
235
+ quote = ch;
236
+ continue;
237
+ }
238
+ if (ch === '(') {
239
+ depth++;
240
+ token = '';
241
+ continue;
242
+ }
243
+ if (ch === ')') {
244
+ depth = Math.max(0, depth - 1);
245
+ token = '';
246
+ continue;
247
+ }
248
+ if (/[A-Za-z_]/.test(ch)) {
249
+ token += ch;
250
+ continue;
251
+ }
252
+ if (token) {
253
+ if (depth === 0) {
254
+ const up = token.toUpperCase();
255
+ // Skip the leading WITH itself.
256
+ if (!sawFirstWord && up === 'WITH') {
257
+ sawFirstWord = true;
258
+ }
259
+ else if (candidates.has(up)) {
260
+ return up;
261
+ }
262
+ }
263
+ token = '';
264
+ }
265
+ }
266
+ return 'SELECT';
267
+ }
268
+ /**
269
+ * Verb-aware target picker so `INSERT INTO target SELECT ... FROM src` and
270
+ * `UPDATE target SET ... FROM src` resolve `sql.target` to the written object,
271
+ * not the read-from one.
272
+ */
273
+ function pickTarget(query, verb, tables) {
274
+ let m = null;
275
+ switch (verb) {
276
+ case 'INSERT':
277
+ case 'MERGE':
278
+ m = query.match(new RegExp(`\\bINTO\\s+${IDENT}`, 'i'));
279
+ break;
280
+ case 'UPDATE':
281
+ m = query.match(new RegExp(`\\bUPDATE\\s+${IDENT}`, 'i'));
282
+ break;
283
+ case 'DELETE':
284
+ m = query.match(new RegExp(`\\bDELETE\\s+FROM\\s+${IDENT}`, 'i'));
285
+ if (!m)
286
+ m = query.match(new RegExp(`\\bFROM\\s+${IDENT}`, 'i'));
287
+ break;
288
+ case 'DROP':
289
+ case 'TRUNCATE':
290
+ case 'ALTER':
291
+ case 'CREATE':
292
+ case 'RENAME':
293
+ m = query.match(new RegExp(`\\b${verb}\\s+(?:TABLE|VIEW|INDEX|SEQUENCE|SCHEMA|DATABASE|TRIGGER|FUNCTION|PROCEDURE)?\\s*(?:IF\\s+(?:NOT\\s+)?EXISTS\\s+)?${IDENT}`, 'i'));
294
+ break;
295
+ case 'GRANT':
296
+ case 'REVOKE':
297
+ m = query.match(new RegExp(`\\bON\\s+(?:TABLE\\s+)?${IDENT}`, 'i'));
298
+ break;
299
+ default:
300
+ // SELECT, WITH-as-SELECT, etc.: first FROM target.
301
+ m = query.match(new RegExp(`\\bFROM\\s+${IDENT}`, 'i'));
302
+ break;
303
+ }
304
+ if (m && m[1]) {
305
+ return normalizeIdent(m[1]);
306
+ }
307
+ return tables[0] ?? '';
308
+ }
309
+ /**
310
+ * Strip `/* ... *\/` and `-- ...` SQL comments in a quote-aware way.
311
+ *
312
+ * A naive regex strip would corrupt inputs like `SELECT '--'; DROP TABLE x`
313
+ * (the `--` inside the string literal would be treated as a line comment
314
+ * and the trailing `DROP` would be silently consumed) and, in the case of
315
+ * the block-comment pattern, is susceptible to polynomial backtracking on
316
+ * adversarial input (a CodeQL warning). This single-pass walker respects
317
+ * single, double and backtick quoting (with SQL doubled-quote escapes)
318
+ * before recognising comment markers.
319
+ */
320
+ function stripSqlComments(sql) {
321
+ let out = '';
322
+ let quote = '';
323
+ let i = 0;
324
+ while (i < sql.length) {
325
+ const c = sql[i];
326
+ if (quote) {
327
+ out += c;
328
+ if (c === quote) {
329
+ if (sql[i + 1] === quote) {
330
+ // Escaped doubled quote
331
+ out += sql[i + 1];
332
+ i += 2;
333
+ continue;
334
+ }
335
+ quote = '';
336
+ }
337
+ i++;
338
+ continue;
339
+ }
340
+ if (c === "'" || c === '"' || c === '`') {
341
+ quote = c;
342
+ out += c;
343
+ i++;
344
+ continue;
345
+ }
346
+ if (c === '-' && sql[i + 1] === '-') {
347
+ i += 2;
348
+ while (i < sql.length && sql[i] !== '\n' && sql[i] !== '\r')
349
+ i++;
350
+ out += ' ';
351
+ continue;
352
+ }
353
+ if (c === '/' && sql[i + 1] === '*') {
354
+ i += 2;
355
+ while (i + 1 < sql.length && !(sql[i] === '*' && sql[i + 1] === '/'))
356
+ i++;
357
+ if (i + 1 < sql.length)
358
+ i += 2;
359
+ out += ' ';
360
+ continue;
361
+ }
362
+ out += c;
363
+ i++;
364
+ }
365
+ return out;
366
+ }
367
+ const IDENT_PART = '(?:[A-Za-z_][A-Za-z0-9_]*|"[^"]+"|`[^`]+`|\\[[^\\]]+\\])';
368
+ const IDENT = `(${IDENT_PART}(?:\\.${IDENT_PART})*)`;
369
+ function extractTables(query, verb) {
370
+ const found = [];
371
+ const patterns = [];
372
+ // Generic clauses across most verbs.
373
+ patterns.push(new RegExp(`\\bFROM\\s+${IDENT}`, 'gi'));
374
+ patterns.push(new RegExp(`\\bJOIN\\s+${IDENT}`, 'gi'));
375
+ patterns.push(new RegExp(`\\bINTO\\s+${IDENT}`, 'gi'));
376
+ patterns.push(new RegExp(`\\bUPDATE\\s+${IDENT}`, 'gi'));
377
+ // Verb-specific patterns for DDL/DCL where the target follows the verb directly.
378
+ if (verb === 'DROP' ||
379
+ verb === 'TRUNCATE' ||
380
+ verb === 'ALTER' ||
381
+ verb === 'CREATE' ||
382
+ verb === 'RENAME') {
383
+ // e.g. DROP TABLE foo, ALTER TABLE foo, CREATE INDEX ... ON foo
384
+ patterns.push(new RegExp(`\\b${verb}\\s+(?:TABLE|VIEW|INDEX|SEQUENCE|SCHEMA|DATABASE|TRIGGER|FUNCTION|PROCEDURE)?\\s*(?:IF\\s+(?:NOT\\s+)?EXISTS\\s+)?${IDENT}`, 'gi'));
385
+ patterns.push(new RegExp(`\\bON\\s+${IDENT}`, 'gi'));
386
+ }
387
+ if (verb === 'GRANT' || verb === 'REVOKE') {
388
+ // GRANT/REVOKE ... ON <object> TO/FROM <principal>
389
+ patterns.push(new RegExp(`\\bON\\s+(?:TABLE\\s+)?${IDENT}`, 'gi'));
390
+ }
391
+ const seen = new Set();
392
+ for (const re of patterns) {
393
+ let m;
394
+ while ((m = re.exec(query)) !== null) {
395
+ const ident = normalizeIdent(m[1]);
396
+ if (ident && !seen.has(ident)) {
397
+ seen.add(ident);
398
+ found.push(ident);
399
+ }
400
+ }
401
+ }
402
+ return found;
403
+ }
404
+ // Reserved words that look like function calls syntactically but aren't.
405
+ const FUNC_DENYLIST = new Set([
406
+ 'VALUES',
407
+ 'IN',
408
+ 'EXISTS',
409
+ 'ANY',
410
+ 'ALL',
411
+ 'SOME',
412
+ 'CAST',
413
+ 'CASE',
414
+ 'IF',
415
+ 'DISTINCT',
416
+ 'ON',
417
+ 'USING',
418
+ 'WHEN',
419
+ 'THEN',
420
+ 'ELSE',
421
+ 'AND',
422
+ 'OR',
423
+ 'NOT',
424
+ ]);
425
+ function extractFunctions(query) {
426
+ // Use matchAll on a fresh per-call regex so the module-scoped /g state
427
+ // can never leak between calls (this is otherwise a common foot-gun).
428
+ const re = /\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
429
+ const found = [];
430
+ const seen = new Set();
431
+ for (const m of query.matchAll(re)) {
432
+ const name = m[1].toUpperCase();
433
+ if (FUNC_DENYLIST.has(name))
434
+ continue;
435
+ if (!seen.has(name)) {
436
+ seen.add(name);
437
+ found.push(name);
438
+ }
439
+ }
440
+ return found;
441
+ }
442
+ function normalizeIdent(ident) {
443
+ // Split into parts on dots that are outside any quote/bracket pair, then
444
+ // take the last segment and strip surrounding quotes/brackets. Matches
445
+ // Python sqlglot's `Table.name` (unqualified name).
446
+ const s = ident.trim();
447
+ const parts = [];
448
+ let depth = '';
449
+ let buf = '';
450
+ for (let i = 0; i < s.length; i++) {
451
+ const ch = s[i];
452
+ if (depth === '') {
453
+ if (ch === '"') {
454
+ depth = '"';
455
+ buf += ch;
456
+ }
457
+ else if (ch === '`') {
458
+ depth = '`';
459
+ buf += ch;
460
+ }
461
+ else if (ch === '[') {
462
+ depth = ']';
463
+ buf += ch;
464
+ }
465
+ else if (ch === '.') {
466
+ parts.push(buf);
467
+ buf = '';
468
+ }
469
+ else {
470
+ buf += ch;
471
+ }
472
+ }
473
+ else {
474
+ buf += ch;
475
+ if ((depth === '"' && ch === '"') ||
476
+ (depth === '`' && ch === '`') ||
477
+ (depth === ']' && ch === ']')) {
478
+ depth = '';
479
+ }
480
+ }
481
+ }
482
+ parts.push(buf);
483
+ let last = parts[parts.length - 1].trim();
484
+ if ((last.startsWith('"') && last.endsWith('"')) ||
485
+ (last.startsWith('`') && last.endsWith('`'))) {
486
+ last = last.slice(1, -1);
487
+ }
488
+ else if (last.startsWith('[') && last.endsWith(']')) {
489
+ last = last.slice(1, -1);
490
+ }
491
+ return last;
492
+ }
493
+ /**
494
+ * Public SQL facet extractor. Reads `sqlCtx.query` and returns
495
+ * `{ verb, target, tables, functions }`.
496
+ *
497
+ * Empty/missing query → all-empty fields.
498
+ * Unrecognized verb → `verb: 'UNKNOWN'`, other fields empty.
499
+ */
500
+ function extractSqlFacets(sqlCtx) {
501
+ const raw = sqlCtx.query;
502
+ if (typeof raw !== 'string' || raw.trim() === '') {
503
+ return { ...EMPTY_SQL_FACETS };
504
+ }
505
+ try {
506
+ return regexExtractSql(raw);
507
+ }
508
+ catch {
509
+ return { ...UNKNOWN_SQL_FACETS };
510
+ }
511
+ }
512
+ // ── Kubernetes facets ───────────────────────────────────────────────────────
513
+ const METHOD_TO_VERB_NAMED = {
514
+ GET: 'get',
515
+ DELETE: 'delete',
516
+ PUT: 'update',
517
+ PATCH: 'patch',
518
+ POST: 'create',
519
+ HEAD: 'get',
520
+ };
521
+ const METHOD_TO_VERB_COLLECTION = {
522
+ GET: 'list',
523
+ POST: 'create',
524
+ DELETE: 'deletecollection',
525
+ PUT: 'update',
526
+ PATCH: 'patch',
527
+ HEAD: 'list',
528
+ };
529
+ // Ordered most specific first. Matches Python's _K8S_PATH_PATTERNS, plus
530
+ // explicit `watch` and proxy-tail handling (namespaced and cluster-scoped).
531
+ const K8S_PATH_PATTERNS = [
532
+ // Watch — namespaced collection
533
+ {
534
+ re: /^\/api\/[^/]+\/watch\/namespaces\/([^/]+)\/([^/]+)\/?$/,
535
+ groups: ['namespace', 'resource'],
536
+ isWatch: true,
537
+ },
538
+ {
539
+ re: /^\/apis\/[^/]+\/[^/]+\/watch\/namespaces\/([^/]+)\/([^/]+)\/?$/,
540
+ groups: ['namespace', 'resource'],
541
+ isWatch: true,
542
+ },
543
+ // Watch — namespaced named object
544
+ {
545
+ re: /^\/api\/[^/]+\/watch\/namespaces\/([^/]+)\/([^/]+)\/([^/]+)\/?$/,
546
+ groups: ['namespace', 'resource', 'name'],
547
+ isWatch: true,
548
+ },
549
+ {
550
+ re: /^\/apis\/[^/]+\/[^/]+\/watch\/namespaces\/([^/]+)\/([^/]+)\/([^/]+)\/?$/,
551
+ groups: ['namespace', 'resource', 'name'],
552
+ isWatch: true,
553
+ },
554
+ // Watch — cluster-scoped
555
+ { re: /^\/api\/[^/]+\/watch\/([^/]+)\/?$/, groups: ['resource'], isWatch: true },
556
+ { re: /^\/apis\/[^/]+\/[^/]+\/watch\/([^/]+)\/?$/, groups: ['resource'], isWatch: true },
557
+ // Proxy tail (namespaced): pods/<name>/proxy/<...> → subresource = proxy
558
+ {
559
+ re: /^\/api\/[^/]+\/namespaces\/([^/]+)\/([^/]+)\/([^/]+)\/(proxy)(?:\/.*)?$/,
560
+ groups: ['namespace', 'resource', 'name', 'subresource'],
561
+ isWatch: false,
562
+ },
563
+ {
564
+ re: /^\/apis\/[^/]+\/[^/]+\/namespaces\/([^/]+)\/([^/]+)\/([^/]+)\/(proxy)(?:\/.*)?$/,
565
+ groups: ['namespace', 'resource', 'name', 'subresource'],
566
+ isWatch: false,
567
+ },
568
+ // Generic namespaced — most specific first
569
+ {
570
+ re: /^\/api\/[^/]+\/namespaces\/([^/]+)\/([^/]+)\/([^/]+)\/([^/]+)\/?$/,
571
+ groups: ['namespace', 'resource', 'name', 'subresource'],
572
+ isWatch: false,
573
+ },
574
+ {
575
+ re: /^\/apis\/[^/]+\/[^/]+\/namespaces\/([^/]+)\/([^/]+)\/([^/]+)\/([^/]+)\/?$/,
576
+ groups: ['namespace', 'resource', 'name', 'subresource'],
577
+ isWatch: false,
578
+ },
579
+ {
580
+ re: /^\/api\/[^/]+\/namespaces\/([^/]+)\/([^/]+)\/([^/]+)\/?$/,
581
+ groups: ['namespace', 'resource', 'name'],
582
+ isWatch: false,
583
+ },
584
+ {
585
+ re: /^\/apis\/[^/]+\/[^/]+\/namespaces\/([^/]+)\/([^/]+)\/([^/]+)\/?$/,
586
+ groups: ['namespace', 'resource', 'name'],
587
+ isWatch: false,
588
+ },
589
+ {
590
+ re: /^\/api\/[^/]+\/namespaces\/([^/]+)\/([^/]+)\/?$/,
591
+ groups: ['namespace', 'resource'],
592
+ isWatch: false,
593
+ },
594
+ {
595
+ re: /^\/apis\/[^/]+\/[^/]+\/namespaces\/([^/]+)\/([^/]+)\/?$/,
596
+ groups: ['namespace', 'resource'],
597
+ isWatch: false,
598
+ },
599
+ // Cluster-scoped (resource, name, subresource): e.g. /api/v1/nodes/n1/status
600
+ {
601
+ re: /^\/api\/[^/]+\/([^/]+)\/([^/]+)\/([^/]+)\/?$/,
602
+ groups: ['resource', 'name', 'subresource'],
603
+ isWatch: false,
604
+ },
605
+ {
606
+ re: /^\/apis\/[^/]+\/[^/]+\/([^/]+)\/([^/]+)\/([^/]+)\/?$/,
607
+ groups: ['resource', 'name', 'subresource'],
608
+ isWatch: false,
609
+ },
610
+ // Cluster-scoped proxy tail
611
+ {
612
+ re: /^\/api\/[^/]+\/([^/]+)\/([^/]+)\/(proxy)(?:\/.*)?$/,
613
+ groups: ['resource', 'name', 'subresource'],
614
+ isWatch: false,
615
+ },
616
+ {
617
+ re: /^\/apis\/[^/]+\/[^/]+\/([^/]+)\/([^/]+)\/(proxy)(?:\/.*)?$/,
618
+ groups: ['resource', 'name', 'subresource'],
619
+ isWatch: false,
620
+ },
621
+ { re: /^\/api\/[^/]+\/([^/]+)\/([^/]+)\/?$/, groups: ['resource', 'name'], isWatch: false },
622
+ {
623
+ re: /^\/apis\/[^/]+\/[^/]+\/([^/]+)\/([^/]+)\/?$/,
624
+ groups: ['resource', 'name'],
625
+ isWatch: false,
626
+ },
627
+ { re: /^\/api\/[^/]+\/([^/]+)\/?$/, groups: ['resource'], isWatch: false },
628
+ { re: /^\/apis\/[^/]+\/[^/]+\/([^/]+)\/?$/, groups: ['resource'], isWatch: false },
629
+ ];
630
+ const EMPTY_K8S_FACETS = {
631
+ verb: '',
632
+ resource: '',
633
+ namespace: '',
634
+ name: '',
635
+ subresource: '',
636
+ };
637
+ /**
638
+ * Public Kubernetes facet extractor. Reads `k8sCtx.method` and `k8sCtx.path`
639
+ * and returns `{ verb, resource, namespace, name, subresource }`.
640
+ */
641
+ function extractK8sFacets(k8sCtx) {
642
+ const methodRaw = k8sCtx.method;
643
+ const pathRaw = k8sCtx.path;
644
+ const method = typeof methodRaw === 'string' ? methodRaw.toUpperCase() : '';
645
+ const rawPath = typeof pathRaw === 'string' ? pathRaw : '';
646
+ const result = { ...EMPTY_K8S_FACETS };
647
+ if (!rawPath)
648
+ return result;
649
+ // Strip query/fragment before pattern matching so resources or namespaces
650
+ // whose names contain "watch" cannot spoof the verb signal, and so
651
+ // ?watch=true can be detected independently of path.
652
+ let pathOnly = rawPath;
653
+ let query = '';
654
+ const qIdx = pathOnly.indexOf('?');
655
+ if (qIdx >= 0) {
656
+ query = pathOnly.slice(qIdx + 1);
657
+ pathOnly = pathOnly.slice(0, qIdx);
658
+ }
659
+ const hIdx = pathOnly.indexOf('#');
660
+ if (hIdx >= 0)
661
+ pathOnly = pathOnly.slice(0, hIdx);
662
+ let matched = {};
663
+ let matchedIsWatch = false;
664
+ for (const { re, groups, isWatch } of K8S_PATH_PATTERNS) {
665
+ const m = pathOnly.match(re);
666
+ if (m) {
667
+ const captured = {};
668
+ groups.forEach((g, i) => {
669
+ captured[g] = m[i + 1];
670
+ });
671
+ matched = captured;
672
+ matchedIsWatch = isWatch;
673
+ break;
674
+ }
675
+ }
676
+ result.resource = matched.resource ?? '';
677
+ result.namespace = matched.namespace ?? '';
678
+ result.name = matched.name ?? '';
679
+ result.subresource = matched.subresource ?? '';
680
+ // Honor ?watch=true as an alternate watch signal.
681
+ let queryIsWatch = false;
682
+ if (query) {
683
+ for (const kv of query.split('&')) {
684
+ const eq = kv.indexOf('=');
685
+ if (eq < 0)
686
+ continue;
687
+ if (kv.slice(0, eq) === 'watch') {
688
+ const v = kv.slice(eq + 1);
689
+ if (v === 'true' || v === '1' || v === 'True') {
690
+ queryIsWatch = true;
691
+ break;
692
+ }
693
+ }
694
+ }
695
+ }
696
+ // `watch` is a read-only verb. Only emit it for GET/HEAD or empty
697
+ // method, so a write method with ?watch=true cannot be silently
698
+ // classified as a benign watch.
699
+ const methodAllowsWatch = method === '' || method === 'GET' || method === 'HEAD';
700
+ const isWatch = (matchedIsWatch || queryIsWatch) && methodAllowsWatch;
701
+ const hasName = result.name !== '';
702
+ if (isWatch) {
703
+ result.verb = 'watch';
704
+ }
705
+ else if (method) {
706
+ const table = hasName ? METHOD_TO_VERB_NAMED : METHOD_TO_VERB_COLLECTION;
707
+ result.verb = table[method] ?? method.toLowerCase();
708
+ }
709
+ return result;
710
+ }
711
+ // ── Default registry & top-level API ───────────────────────────────────────
712
+ /**
713
+ * Module-level registry pre-loaded with SQL and Kubernetes extractors.
714
+ * Import it to add support for new protocols via `register()`.
715
+ */
716
+ exports.defaultRegistry = new FacetRegistry();
717
+ exports.defaultRegistry.register('sql', extractSqlFacets);
718
+ exports.defaultRegistry.register('k8s', extractK8sFacets);
719
+ /**
720
+ * Enrich a policy evaluation context with wire-protocol facets.
721
+ *
722
+ * Uses the supplied registry, or `defaultRegistry` if none is given.
723
+ * Mutates `context` in place and returns it for chaining.
724
+ */
725
+ function extractProtocolFacets(context, registry = exports.defaultRegistry) {
726
+ return registry.extract(context);
727
+ }
728
+ //# sourceMappingURL=protocol-facets.js.map