@silmaril-security/sdk 0.7.6 → 0.7.8

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.js CHANGED
@@ -14,6 +14,19 @@ var __export = (target, all) => {
14
14
  };
15
15
 
16
16
  // src/exceptions.ts
17
+ function isReviewCompatibilityError(error) {
18
+ if (typeof error !== "object" && typeof error !== "function" || error === null) {
19
+ return false;
20
+ }
21
+ try {
22
+ return Object.getOwnPropertyDescriptor(
23
+ error,
24
+ REVIEW_COMPATIBILITY_ERROR_BRAND
25
+ )?.value === true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
17
30
  function parseErrorBody(body) {
18
31
  try {
19
32
  const parsed = JSON.parse(body);
@@ -30,7 +43,7 @@ function parseErrorBody(body) {
30
43
  return { details: void 0, error: void 0, apiMessage: void 0 };
31
44
  }
32
45
  }
33
- var MAX_PROMPT_DISPLAY_LEN, FirewallBlockedException, PromptBlockedException, SilmarilApiError;
46
+ var MAX_PROMPT_DISPLAY_LEN, FirewallBlockedException, PromptBlockedException, REVIEW_COMPATIBILITY_ERROR_BRAND, ReviewCompatibilityError, SilmarilApiError;
34
47
  var init_exceptions = __esm({
35
48
  "src/exceptions.ts"() {
36
49
  "use strict";
@@ -66,6 +79,27 @@ var init_exceptions = __esm({
66
79
  }
67
80
  };
68
81
  PromptBlockedException = FirewallBlockedException;
82
+ REVIEW_COMPATIBILITY_ERROR_BRAND = /* @__PURE__ */ Symbol.for(
83
+ "@silmaril-security/sdk/ReviewCompatibilityError"
84
+ );
85
+ ReviewCompatibilityError = class _ReviewCompatibilityError extends Error {
86
+ requestedReview;
87
+ acknowledgedReview;
88
+ constructor(requestedReview, acknowledgedReview) {
89
+ const detail = acknowledgedReview === void 0 ? "the response omitted its acknowledgement" : "the response acknowledgement did not match";
90
+ super(`Silmaril review compatibility error: ${detail} for requested ${requestedReview} review`);
91
+ this.name = "ReviewCompatibilityError";
92
+ this.requestedReview = requestedReview;
93
+ this.acknowledgedReview = acknowledgedReview;
94
+ Object.defineProperty(this, REVIEW_COMPATIBILITY_ERROR_BRAND, {
95
+ value: true,
96
+ enumerable: false,
97
+ configurable: false,
98
+ writable: false
99
+ });
100
+ Object.setPrototypeOf(this, _ReviewCompatibilityError.prototype);
101
+ }
102
+ };
69
103
  SilmarilApiError = class _SilmarilApiError extends Error {
70
104
  status;
71
105
  statusText;
@@ -282,6 +316,9 @@ async function createLangChainHandler(firewall, options = {}) {
282
316
  ...toolName !== void 0 ? { toolName } : {}
283
317
  });
284
318
  } catch (err) {
319
+ if (isReviewCompatibilityError(err)) {
320
+ throw err;
321
+ }
285
322
  if (!failOpen) {
286
323
  throw err;
287
324
  }
@@ -766,12 +803,32 @@ function sanitizeText(text) {
766
803
  }
767
804
 
768
805
  // src/firewall.ts
769
- var SDK_VERSION = "0.7.6";
806
+ var SDK_VERSION = "0.7.8";
770
807
  var DEFAULT_TIMEOUT_MS = 1e4;
771
808
  var DEFAULT_MAX_RETRIES = 5;
772
809
  var MAX_BACKOFF_SECONDS = 30;
773
810
  var MAX_ERROR_BODY_BYTES = 1 << 16;
774
811
  var MAX_TIMEOUT_MS = 2147483647;
812
+ function validateReview(value, name) {
813
+ if (value === void 0) {
814
+ return void 0;
815
+ }
816
+ if (value !== "standard" && value !== "deep") {
817
+ throw new Error(`Firewall: ${name} must be standard or deep`);
818
+ }
819
+ return value;
820
+ }
821
+ function resolveReview(perCallReview, clientReview) {
822
+ return perCallReview === void 0 ? clientReview : validateReview(perCallReview, "review");
823
+ }
824
+ function verifyReviewAcknowledgement(acknowledgedReview, requestedReview) {
825
+ if (requestedReview !== void 0 && acknowledgedReview !== requestedReview) {
826
+ throw new ReviewCompatibilityError(requestedReview, acknowledgedReview);
827
+ }
828
+ }
829
+ function reviewAcknowledgementFromResponse(response) {
830
+ return isRecord2(response) ? response.review : void 0;
831
+ }
775
832
  function resolveMode(value, requestedMode) {
776
833
  let responseMode;
777
834
  if (value === "shadow" || value === "warn" || value === "block") {
@@ -784,7 +841,7 @@ function resolveMode(value, requestedMode) {
784
841
  function legacyMode(shadowMode) {
785
842
  return shadowMode === void 0 ? void 0 : shadowMode ? "shadow" : "block";
786
843
  }
787
- function blockResultFromResponse(data, requestedMode) {
844
+ function blockResultFromResponse(data, requestedMode, requestedReview) {
788
845
  if (data.prediction !== "BENIGN" && data.prediction !== "MALICIOUS") {
789
846
  throw new Error("Firewall: response prediction must be BENIGN or MALICIOUS");
790
847
  }
@@ -793,6 +850,9 @@ function blockResultFromResponse(data, requestedMode) {
793
850
  score: Number(data.score),
794
851
  threshold: Number(data.threshold)
795
852
  };
853
+ if (requestedReview !== void 0) {
854
+ result.review = requestedReview;
855
+ }
796
856
  const mode = resolveMode(data.mode, requestedMode);
797
857
  if (mode !== void 0) {
798
858
  result.mode = mode;
@@ -818,7 +878,7 @@ function blockResultFromResponse(data, requestedMode) {
818
878
  result.detectorCounts = detectorCounts;
819
879
  }
820
880
  }
821
- if (data.governance !== void 0) {
881
+ if (data.governance !== void 0 && data.governance !== null) {
822
882
  result.governance = governanceDecisionFromResponse(data.governance);
823
883
  }
824
884
  return Object.freeze(result);
@@ -842,8 +902,20 @@ function withSdkMetadata(metadata, info) {
842
902
  };
843
903
  return payload;
844
904
  }
905
+ var GOVERNANCE_RESOURCE_KINDS = [
906
+ "agent",
907
+ "tool",
908
+ "mcp_server",
909
+ "mcp_tool",
910
+ "plugin",
911
+ "skill",
912
+ "extension"
913
+ ];
914
+ function isGovernanceResourceKind(value) {
915
+ return typeof value === "string" && GOVERNANCE_RESOURCE_KINDS.includes(value);
916
+ }
845
917
  function governanceResourceToWire(resource) {
846
- if (!["agent", "tool", "mcp_server", "mcp_tool", "plugin", "skill", "extension"].includes(resource.kind)) {
918
+ if (!isGovernanceResourceKind(resource.kind)) {
847
919
  throw new Error("Firewall: invalid governance resource kind");
848
920
  }
849
921
  if (typeof resource.id !== "string" || !resource.id.trim()) {
@@ -862,6 +934,38 @@ function governanceResourceToWire(resource) {
862
934
  ...resource.parentId === void 0 ? {} : { parent_id: resource.parentId }
863
935
  };
864
936
  }
937
+ function governanceResourceFromResponse(value) {
938
+ if (!isRecord2(value)) {
939
+ throw new Error("Firewall: response governance resource must be an object");
940
+ }
941
+ if (!isGovernanceResourceKind(value.kind)) {
942
+ throw new Error("Firewall: response governance resource has an invalid kind");
943
+ }
944
+ if (typeof value.id !== "string" || !value.id.trim()) {
945
+ throw new Error("Firewall: response governance resource id must be a non-empty string");
946
+ }
947
+ if (value.kind === "mcp_tool") {
948
+ if (typeof value.parent_id !== "string" || !value.parent_id.trim()) {
949
+ throw new Error(
950
+ "Firewall: response governance mcp_tool resource requires a non-empty parent_id"
951
+ );
952
+ }
953
+ return Object.freeze({
954
+ kind: value.kind,
955
+ id: value.id,
956
+ parentId: value.parent_id
957
+ });
958
+ }
959
+ if (value.parent_id !== void 0 && value.parent_id !== null) {
960
+ throw new Error(
961
+ "Firewall: response governance parent_id is valid only for mcp_tool resources"
962
+ );
963
+ }
964
+ return Object.freeze({
965
+ kind: value.kind,
966
+ id: value.id
967
+ });
968
+ }
865
969
  function governanceContextToWire(context) {
866
970
  return {
867
971
  ...context.agent === void 0 ? {} : { agent: context.agent },
@@ -880,13 +984,25 @@ function governanceDecisionFromResponse(value) {
880
984
  if (typeof value.policy_version !== "string" || value.policy_version.length === 0) {
881
985
  throw new Error("Firewall: response governance policy_version must be a non-empty string");
882
986
  }
883
- if (value.rule_id !== void 0 && typeof value.rule_id !== "string") {
987
+ if (value.rule_id !== void 0 && value.rule_id !== null && typeof value.rule_id !== "string") {
884
988
  throw new Error("Firewall: response governance rule_id must be a string when provided");
885
989
  }
990
+ if (value.identity_revision !== void 0 && value.identity_revision !== null && typeof value.identity_revision !== "string") {
991
+ throw new Error(
992
+ "Firewall: response governance identity_revision must be a string when provided"
993
+ );
994
+ }
995
+ if (value.reason !== void 0 && value.reason !== null && typeof value.reason !== "string") {
996
+ throw new Error("Firewall: response governance reason must be a string when provided");
997
+ }
998
+ const resource = value.resource === void 0 || value.resource === null ? void 0 : governanceResourceFromResponse(value.resource);
886
999
  return Object.freeze({
887
1000
  action: value.action,
888
- ...value.rule_id === void 0 ? {} : { ruleId: value.rule_id },
889
- policyVersion: value.policy_version
1001
+ ...value.rule_id === void 0 || value.rule_id === null ? {} : { ruleId: value.rule_id },
1002
+ policyVersion: value.policy_version,
1003
+ ...resource === void 0 ? {} : { resource },
1004
+ ...value.identity_revision === void 0 || value.identity_revision === null ? {} : { identityRevision: value.identity_revision },
1005
+ ...value.reason === void 0 || value.reason === null ? {} : { reason: value.reason }
890
1006
  });
891
1007
  }
892
1008
  async function readCappedErrorBody(response) {
@@ -1011,6 +1127,7 @@ var Firewall = class {
1011
1127
  timeoutMs;
1012
1128
  shadowMode;
1013
1129
  mode;
1130
+ review;
1014
1131
  headers;
1015
1132
  constructor(options) {
1016
1133
  if (!options.apiKey) {
@@ -1031,6 +1148,7 @@ var Firewall = class {
1031
1148
  );
1032
1149
  }
1033
1150
  this.mode = options.mode ?? legacyMode(options.shadowMode);
1151
+ this.review = validateReview(options.review, "review");
1034
1152
  this.shadowMode = this.mode === "shadow";
1035
1153
  this.headers = Object.freeze({
1036
1154
  "x-api-key": this.apiKey,
@@ -1070,9 +1188,13 @@ var Firewall = class {
1070
1188
  texts: texts.map((text) => sanitizeText(text))
1071
1189
  };
1072
1190
  const requestedMode = options.mode ?? this.mode;
1191
+ const requestedReview = resolveReview(options.review, this.review);
1073
1192
  if (requestedMode !== void 0) {
1074
1193
  payload.mode = requestedMode;
1075
1194
  }
1195
+ if (requestedReview !== void 0) {
1196
+ payload.review = requestedReview;
1197
+ }
1076
1198
  if (options.hooks && options.hooks.length > 0) {
1077
1199
  payload.hooks = options.hooks.map((h) => String(h));
1078
1200
  }
@@ -1092,6 +1214,7 @@ var Firewall = class {
1092
1214
  );
1093
1215
  }
1094
1216
  const data = await this.postWithRetry(payload, options.signal);
1217
+ verifyReviewAcknowledgement(reviewAcknowledgementFromResponse(data), requestedReview);
1095
1218
  if (!Array.isArray(data.predictions)) {
1096
1219
  throw new Error("Firewall: response predictions must be an array");
1097
1220
  }
@@ -1100,7 +1223,9 @@ var Firewall = class {
1100
1223
  `Firewall: response predictions length ${data.predictions.length} does not match submitted texts length ${payload.texts.length}`
1101
1224
  );
1102
1225
  }
1103
- return data.predictions.map((p) => blockResultFromResponse(p, requestedMode));
1226
+ return data.predictions.map(
1227
+ (p) => blockResultFromResponse(p, requestedMode, requestedReview)
1228
+ );
1104
1229
  }
1105
1230
  asLangChainHandler(options = {}) {
1106
1231
  return Promise.resolve().then(() => (init_langchain(), langchain_exports)).then(
@@ -1150,9 +1275,13 @@ var Firewall = class {
1150
1275
  async classifySingle(text, options, metadataInfo) {
1151
1276
  const payload = { text };
1152
1277
  const requestedMode = options.mode ?? this.mode;
1278
+ const requestedReview = resolveReview(options.review, this.review);
1153
1279
  if (requestedMode !== void 0) {
1154
1280
  payload.mode = requestedMode;
1155
1281
  }
1282
+ if (requestedReview !== void 0) {
1283
+ payload.review = requestedReview;
1284
+ }
1156
1285
  if (options.hook !== void 0) {
1157
1286
  payload.hook = options.hook;
1158
1287
  }
@@ -1167,7 +1296,8 @@ var Firewall = class {
1167
1296
  ...options.governance === void 0 ? {} : { governance: options.governance }
1168
1297
  });
1169
1298
  const data = await this.postWithRetry(payload, options.signal);
1170
- return blockResultFromResponse(data, requestedMode);
1299
+ verifyReviewAcknowledgement(reviewAcknowledgementFromResponse(data), requestedReview);
1300
+ return blockResultFromResponse(data, requestedMode, requestedReview);
1171
1301
  }
1172
1302
  };
1173
1303
 
@@ -1190,6 +1320,7 @@ export {
1190
1320
  Outcome,
1191
1321
  PRIMARY_OUTCOMES,
1192
1322
  PromptBlockedException,
1323
+ ReviewCompatibilityError,
1193
1324
  SilmarilApiError,
1194
1325
  isHarmfulOutcome,
1195
1326
  isPrimaryOutcome,