@silmaril-security/sdk 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5,8 +5,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __esm = (fn, res) => function __init() {
9
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ var __esm = (fn, res, err) => function __init() {
9
+ if (err) throw err[0];
10
+ try {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ } catch (e) {
13
+ throw err = [e], e;
14
+ }
10
15
  };
11
16
  var __export = (target, all) => {
12
17
  for (var name in all)
@@ -295,7 +300,7 @@ async function createLangChainHandler(firewall, options = {}) {
295
300
  return;
296
301
  }
297
302
  const threshold = result.threshold;
298
- const blocked = result.score >= threshold;
303
+ const blocked = result.prediction === "MALICIOUS";
299
304
  const commonEventFields = {
300
305
  hook: hookLabel,
301
306
  ...toolName !== void 0 ? { toolName } : {},
@@ -426,29 +431,29 @@ var init_langchain = __esm({
426
431
  var src_exports = {};
427
432
  __export(src_exports, {
428
433
  ALL_HOOKS: () => ALL_HOOKS,
429
- CHUNK_OVERLAP: () => CHUNK_OVERLAP,
430
- CHUNK_OVERLAP_CHARS: () => CHUNK_OVERLAP_CHARS,
431
- CHUNK_WINDOW: () => CHUNK_WINDOW,
432
- CHUNK_WINDOW_CHARS: () => CHUNK_WINDOW_CHARS,
433
- DEFAULT_CHUNK_CONCURRENCY: () => DEFAULT_CHUNK_CONCURRENCY,
434
434
  DEFAULT_HOOKS: () => DEFAULT_HOOKS,
435
435
  DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
436
436
  FIREWALL_HOOK_TO_LABEL: () => FIREWALL_HOOK_TO_LABEL,
437
437
  Firewall: () => Firewall,
438
438
  FirewallBlockedException: () => FirewallBlockedException,
439
439
  FirewallHook: () => FirewallHook,
440
+ HARMFUL_OUTCOMES: () => HARMFUL_OUTCOMES,
440
441
  HookLabel: () => HookLabel,
441
442
  INPUT_HOOKS: () => INPUT_HOOKS,
442
- MAX_INPUT_CHARS: () => MAX_INPUT_CHARS,
443
- MAX_INPUT_TOKENS: () => MAX_INPUT_TOKENS,
443
+ OUTCOME_DESCRIPTIONS: () => OUTCOME_DESCRIPTIONS,
444
444
  OUTPUT_HOOKS: () => OUTPUT_HOOKS,
445
+ Outcome: () => Outcome,
446
+ PRIMARY_OUTCOMES: () => PRIMARY_OUTCOMES,
445
447
  PromptBlockedException: () => PromptBlockedException,
446
448
  SilmarilApiError: () => SilmarilApiError,
447
- chunkText: () => chunkText,
449
+ isHarmfulOutcome: () => isHarmfulOutcome,
450
+ isPrimaryOutcome: () => isPrimaryOutcome,
451
+ normalizeHarmfulOutcome: () => normalizeHarmfulOutcome,
452
+ normalizeHarmfulOutcomeMap: () => normalizeHarmfulOutcomeMap,
453
+ normalizePrimaryOutcome: () => normalizePrimaryOutcome,
448
454
  prependHook: () => prependHook,
449
455
  prependToolName: () => prependToolName,
450
- resolveHooks: () => resolveHooks,
451
- sanitizeText: () => sanitizeText
456
+ resolveHooks: () => resolveHooks
452
457
  });
453
458
  module.exports = __toCommonJS(src_exports);
454
459
 
@@ -544,7 +549,7 @@ function createMiddleware(firewall, options = {}) {
544
549
  toolName !== void 0 ? { hook, toolName } : { hook }
545
550
  );
546
551
  const threshold = result.threshold;
547
- const blocked = result.score >= threshold;
552
+ const blocked = result.prediction === "MALICIOUS";
548
553
  const commonEventFields = {
549
554
  hook,
550
555
  ...toolName !== void 0 ? { toolName } : {},
@@ -670,15 +675,80 @@ function createMiddleware(firewall, options = {}) {
670
675
  };
671
676
  }
672
677
 
673
- // src/chunking.ts
674
- var CHARS_PER_TOKEN = 4;
675
- var MAX_INPUT_TOKENS = 81920;
676
- var CHUNK_WINDOW = 400;
677
- var CHUNK_OVERLAP = 64;
678
- var MAX_INPUT_CHARS = MAX_INPUT_TOKENS * CHARS_PER_TOKEN;
679
- var CHUNK_WINDOW_CHARS = CHUNK_WINDOW * CHARS_PER_TOKEN;
680
- var CHUNK_OVERLAP_CHARS = CHUNK_OVERLAP * CHARS_PER_TOKEN;
681
- var CHUNK_STRIDE_CHARS = CHUNK_WINDOW_CHARS - CHUNK_OVERLAP_CHARS;
678
+ // src/firewall.ts
679
+ init_exceptions();
680
+
681
+ // src/outcomes.ts
682
+ var Outcome = {
683
+ Benign: "benign",
684
+ InformationDisclosure: "information_disclosure",
685
+ SecretExposure: "secret_exposure",
686
+ ControlAbuse: "control_abuse",
687
+ SystemCompromise: "system_compromise",
688
+ ServiceDisruption: "service_disruption"
689
+ };
690
+ var PRIMARY_OUTCOMES = [
691
+ Outcome.Benign,
692
+ Outcome.InformationDisclosure,
693
+ Outcome.SecretExposure,
694
+ Outcome.ControlAbuse,
695
+ Outcome.SystemCompromise,
696
+ Outcome.ServiceDisruption
697
+ ];
698
+ var HARMFUL_OUTCOMES = [
699
+ Outcome.InformationDisclosure,
700
+ Outcome.SecretExposure,
701
+ Outcome.ControlAbuse,
702
+ Outcome.SystemCompromise,
703
+ Outcome.ServiceDisruption
704
+ ];
705
+ var OUTCOME_DESCRIPTIONS = {
706
+ [Outcome.Benign]: "No harmful firewall outcome detected.",
707
+ [Outcome.InformationDisclosure]: "Exposes private data, documents, internal context, logs, traces, customer data, SQL rows, topology, or similar non-secret sensitive information.",
708
+ [Outcome.SecretExposure]: "Exposes credentials, tokens, API keys, cookies, passwords, signing keys, OAuth secrets, session material, or webhook secrets.",
709
+ [Outcome.ControlAbuse]: "Misuses authorized tools or user privileges to send, change, approve, delete, operate, or bypass policy/RBAC without a stronger outcome.",
710
+ [Outcome.SystemCompromise]: "Enables privilege escalation, account takeover, hostile integration or plugin takeover, persistence, lateral movement, attacker webhook registration, or code/plugin execution.",
711
+ [Outcome.ServiceDisruption]: "Causes downtime, lockout, degradation, alert suppression, destructive loops, resource exhaustion, cost spikes, or hidden outage evidence."
712
+ };
713
+ var PRIMARY_OUTCOME_SET = new Set(PRIMARY_OUTCOMES);
714
+ var HARMFUL_OUTCOME_SET = new Set(HARMFUL_OUTCOMES);
715
+ function isPrimaryOutcome(value) {
716
+ return typeof value === "string" && PRIMARY_OUTCOME_SET.has(value);
717
+ }
718
+ function isHarmfulOutcome(value) {
719
+ return typeof value === "string" && HARMFUL_OUTCOME_SET.has(value);
720
+ }
721
+ function normalizePrimaryOutcome(value, fieldName = "primary_outcome") {
722
+ if (typeof value !== "string") {
723
+ throw new Error(`Firewall: invalid ${fieldName} ${JSON.stringify(value)}`);
724
+ }
725
+ return isPrimaryOutcome(value) ? value : value;
726
+ }
727
+ function normalizeHarmfulOutcome(value, fieldName = "outcome") {
728
+ if (typeof value !== "string" || value === Outcome.Benign) {
729
+ throw new Error(`Firewall: invalid ${fieldName} ${JSON.stringify(value)}`);
730
+ }
731
+ return isHarmfulOutcome(value) ? value : value;
732
+ }
733
+ function normalizeHarmfulOutcomeMap(values, fieldName) {
734
+ if (values === void 0 || values === null) {
735
+ return void 0;
736
+ }
737
+ if (typeof values !== "object" || Array.isArray(values)) {
738
+ throw new Error(`Firewall: invalid ${fieldName} ${JSON.stringify(values)}`);
739
+ }
740
+ const out = {};
741
+ for (const [key, value] of Object.entries(values)) {
742
+ const normalizedKey = normalizeHarmfulOutcome(key, `${fieldName} key`);
743
+ if (typeof value !== "number") {
744
+ throw new Error(`Firewall: invalid ${fieldName} value for ${JSON.stringify(key)}`);
745
+ }
746
+ out[normalizedKey] = value;
747
+ }
748
+ return Object.freeze(out);
749
+ }
750
+
751
+ // src/sanitization.ts
682
752
  function isHighSurrogate(code) {
683
753
  return code >= 55296 && code <= 56319;
684
754
  }
@@ -697,82 +767,48 @@ function sanitizeText(text) {
697
767
  }
698
768
  continue;
699
769
  }
700
- if (isLowSurrogate(code)) {
701
- continue;
770
+ if (!isLowSurrogate(code)) {
771
+ out += text[i];
702
772
  }
703
- out += text[i];
704
773
  }
705
774
  return out;
706
775
  }
707
- function safeChunkStart(text, start) {
708
- if (start > 0 && start < text.length && isLowSurrogate(text.charCodeAt(start)) && isHighSurrogate(text.charCodeAt(start - 1))) {
709
- return start + 1;
710
- }
711
- return start;
712
- }
713
- function safeChunkEnd(text, end) {
714
- if (end > 0 && end < text.length && isHighSurrogate(text.charCodeAt(end - 1)) && isLowSurrogate(text.charCodeAt(end))) {
715
- return end - 1;
716
- }
717
- return end;
718
- }
719
- function chunkText(text) {
720
- text = sanitizeText(text);
721
- const n = text.length;
722
- if (n > MAX_INPUT_CHARS) {
723
- throw new Error(
724
- `Input has ~${Math.floor(n / CHARS_PER_TOKEN)} tokens (${n} chars); max is ${MAX_INPUT_TOKENS} tokens (${MAX_INPUT_CHARS} chars)`
725
- );
726
- }
727
- if (n <= CHUNK_WINDOW_CHARS) {
728
- return [text];
729
- }
730
- const chunks = [];
731
- for (let start = 0; start < n; start += CHUNK_STRIDE_CHARS) {
732
- const safeStart = safeChunkStart(text, start);
733
- if (safeStart >= n) {
734
- break;
735
- }
736
- const end = safeChunkEnd(text, safeStart + CHUNK_WINDOW_CHARS);
737
- chunks.push(text.slice(safeStart, end));
738
- if (end >= n) {
739
- break;
740
- }
741
- }
742
- return chunks;
743
- }
744
776
 
745
777
  // src/firewall.ts
746
- init_exceptions();
747
- var SDK_VERSION = "0.4.1";
778
+ var SDK_VERSION = "0.5.0";
748
779
  var DEFAULT_TIMEOUT_MS = 1e4;
749
- var DEFAULT_CHUNK_CONCURRENCY = 8;
750
780
  var DEFAULT_MAX_RETRIES = 5;
751
781
  var MAX_BACKOFF_SECONDS = 30;
752
782
  var MAX_ERROR_BODY_BYTES = 1 << 16;
753
783
  function blockResultFromResponse(data) {
784
+ if (data.prediction !== "BENIGN" && data.prediction !== "MALICIOUS") {
785
+ throw new Error("Firewall: response prediction must be BENIGN or MALICIOUS");
786
+ }
754
787
  const result = {
755
788
  prediction: data.prediction,
756
789
  score: Number(data.score),
757
790
  threshold: Number(data.threshold)
758
791
  };
759
792
  if (data.primary_outcome !== void 0) {
760
- result.primaryOutcome = data.primary_outcome;
793
+ result.primaryOutcome = normalizePrimaryOutcome(data.primary_outcome);
761
794
  }
762
795
  if (data.outcome_scores !== void 0) {
763
- result.outcomeScores = Object.freeze(
764
- Object.fromEntries(Object.entries(data.outcome_scores).map(([k, v]) => [k, Number(v)]))
765
- );
796
+ const outcomeScores = normalizeHarmfulOutcomeMap(data.outcome_scores, "outcome_scores");
797
+ if (outcomeScores !== void 0) {
798
+ result.outcomeScores = outcomeScores;
799
+ }
766
800
  }
767
801
  if (data.detector_scores !== void 0) {
768
- result.detectorScores = Object.freeze(
769
- Object.fromEntries(Object.entries(data.detector_scores).map(([k, v]) => [k, Number(v)]))
770
- );
802
+ const detectorScores = normalizeHarmfulOutcomeMap(data.detector_scores, "detector_scores");
803
+ if (detectorScores !== void 0) {
804
+ result.detectorScores = detectorScores;
805
+ }
771
806
  }
772
807
  if (data.detector_counts !== void 0) {
773
- result.detectorCounts = Object.freeze(
774
- Object.fromEntries(Object.entries(data.detector_counts).map(([k, v]) => [k, Number(v)]))
775
- );
808
+ const detectorCounts = normalizeHarmfulOutcomeMap(data.detector_counts, "detector_counts");
809
+ if (detectorCounts !== void 0) {
810
+ result.detectorCounts = detectorCounts;
811
+ }
776
812
  }
777
813
  return Object.freeze(result);
778
814
  }
@@ -790,9 +826,7 @@ function withSdkMetadata(metadata, info) {
790
826
  sdk_language: "typescript",
791
827
  sdk_version: SDK_VERSION,
792
828
  request_id: info.requestId,
793
- input_index: info.inputIndex,
794
- chunk_index: info.chunkIndex,
795
- chunk_count: info.chunkCount
829
+ ...info.inputIndex === void 0 ? {} : { input_index: info.inputIndex }
796
830
  };
797
831
  return payload;
798
832
  }
@@ -833,36 +867,10 @@ async function readCappedErrorBody(response) {
833
867
  }
834
868
  return new TextDecoder().decode(body);
835
869
  }
836
- async function mapWithConcurrency(items, concurrency, task) {
837
- const results = new Array(items.length);
838
- let nextIndex = 0;
839
- let firstError;
840
- const workerCount = Math.min(concurrency, items.length);
841
- async function worker() {
842
- for (; ; ) {
843
- const index = nextIndex;
844
- nextIndex += 1;
845
- if (index >= items.length) {
846
- return;
847
- }
848
- try {
849
- results[index] = await task(items[index], index);
850
- } catch (err) {
851
- firstError ??= err;
852
- }
853
- }
854
- }
855
- await Promise.all(Array.from({ length: workerCount }, () => worker()));
856
- if (firstError !== void 0) {
857
- throw firstError;
858
- }
859
- return results;
860
- }
861
870
  var Firewall = class {
862
871
  apiKey;
863
872
  apiUrl;
864
873
  timeoutMs;
865
- chunkConcurrency;
866
874
  shadowMode;
867
875
  headers;
868
876
  constructor(options) {
@@ -878,12 +886,6 @@ var Firewall = class {
878
886
  if (typeof this.timeoutMs !== "number" || !Number.isFinite(this.timeoutMs) || this.timeoutMs < 0) {
879
887
  throw new Error(`Firewall: timeoutMs must be a finite non-negative number, got ${this.timeoutMs}`);
880
888
  }
881
- this.chunkConcurrency = options.chunkConcurrency ?? DEFAULT_CHUNK_CONCURRENCY;
882
- if (!Number.isInteger(this.chunkConcurrency) || this.chunkConcurrency < 1) {
883
- throw new Error(
884
- `Firewall: chunkConcurrency must be an integer >= 1, got ${this.chunkConcurrency}`
885
- );
886
- }
887
889
  this.shadowMode = options.shadowMode ?? false;
888
890
  this.headers = Object.freeze({
889
891
  "x-api-key": this.apiKey,
@@ -891,27 +893,8 @@ var Firewall = class {
891
893
  });
892
894
  }
893
895
  async classify(text, options = {}) {
894
- const chunks = chunkText(text);
895
896
  const requestId = options.requestId ?? (0, import_node_crypto.randomUUID)();
896
- if (chunks.length === 1) {
897
- return this.classifySingleChunk(chunks[0], options, {
898
- requestId,
899
- inputIndex: 0,
900
- chunkIndex: 0,
901
- chunkCount: 1
902
- });
903
- }
904
- const results = await mapWithConcurrency(
905
- chunks,
906
- this.chunkConcurrency,
907
- (chunk, index) => this.classifySingleChunk(chunk, options, {
908
- requestId,
909
- inputIndex: 0,
910
- chunkIndex: index,
911
- chunkCount: chunks.length
912
- })
913
- );
914
- return results.reduce((best, r) => r.score > best.score ? r : best);
897
+ return this.classifySingle(sanitizeText(text), options, { requestId });
915
898
  }
916
899
  async classifyBatch(texts, options = {}) {
917
900
  if (texts.length === 0) {
@@ -945,9 +928,7 @@ var Firewall = class {
945
928
  payload.metadata = texts.map(
946
929
  (_, index) => withSdkMetadata(options.metadata?.[index], {
947
930
  requestId,
948
- inputIndex: index,
949
- chunkIndex: 0,
950
- chunkCount: 1
931
+ inputIndex: index
951
932
  })
952
933
  );
953
934
  const data = await this.postWithRetry(payload);
@@ -986,7 +967,7 @@ var Firewall = class {
986
967
  }
987
968
  throw new Error("Firewall: exhausted retries (unreachable)");
988
969
  }
989
- async classifySingleChunk(text, options, metadataInfo) {
970
+ async classifySingle(text, options, metadataInfo) {
990
971
  const payload = { text };
991
972
  if (options.hook !== void 0) {
992
973
  payload.hook = options.hook;
@@ -1006,28 +987,28 @@ init_hooks();
1006
987
  // Annotate the CommonJS export names for ESM import in node:
1007
988
  0 && (module.exports = {
1008
989
  ALL_HOOKS,
1009
- CHUNK_OVERLAP,
1010
- CHUNK_OVERLAP_CHARS,
1011
- CHUNK_WINDOW,
1012
- CHUNK_WINDOW_CHARS,
1013
- DEFAULT_CHUNK_CONCURRENCY,
1014
990
  DEFAULT_HOOKS,
1015
991
  DEFAULT_TIMEOUT_MS,
1016
992
  FIREWALL_HOOK_TO_LABEL,
1017
993
  Firewall,
1018
994
  FirewallBlockedException,
1019
995
  FirewallHook,
996
+ HARMFUL_OUTCOMES,
1020
997
  HookLabel,
1021
998
  INPUT_HOOKS,
1022
- MAX_INPUT_CHARS,
1023
- MAX_INPUT_TOKENS,
999
+ OUTCOME_DESCRIPTIONS,
1024
1000
  OUTPUT_HOOKS,
1001
+ Outcome,
1002
+ PRIMARY_OUTCOMES,
1025
1003
  PromptBlockedException,
1026
1004
  SilmarilApiError,
1027
- chunkText,
1005
+ isHarmfulOutcome,
1006
+ isPrimaryOutcome,
1007
+ normalizeHarmfulOutcome,
1008
+ normalizeHarmfulOutcomeMap,
1009
+ normalizePrimaryOutcome,
1028
1010
  prependHook,
1029
1011
  prependToolName,
1030
- resolveHooks,
1031
- sanitizeText
1012
+ resolveHooks
1032
1013
  });
1033
1014
  //# sourceMappingURL=index.cjs.map