@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.cjs CHANGED
@@ -36,6 +36,19 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
36
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
37
37
 
38
38
  // src/exceptions.ts
39
+ function isReviewCompatibilityError(error) {
40
+ if (typeof error !== "object" && typeof error !== "function" || error === null) {
41
+ return false;
42
+ }
43
+ try {
44
+ return Object.getOwnPropertyDescriptor(
45
+ error,
46
+ REVIEW_COMPATIBILITY_ERROR_BRAND
47
+ )?.value === true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
39
52
  function parseErrorBody(body) {
40
53
  try {
41
54
  const parsed = JSON.parse(body);
@@ -52,7 +65,7 @@ function parseErrorBody(body) {
52
65
  return { details: void 0, error: void 0, apiMessage: void 0 };
53
66
  }
54
67
  }
55
- var MAX_PROMPT_DISPLAY_LEN, FirewallBlockedException, PromptBlockedException, SilmarilApiError;
68
+ var MAX_PROMPT_DISPLAY_LEN, FirewallBlockedException, PromptBlockedException, REVIEW_COMPATIBILITY_ERROR_BRAND, ReviewCompatibilityError, SilmarilApiError;
56
69
  var init_exceptions = __esm({
57
70
  "src/exceptions.ts"() {
58
71
  "use strict";
@@ -88,6 +101,27 @@ var init_exceptions = __esm({
88
101
  }
89
102
  };
90
103
  PromptBlockedException = FirewallBlockedException;
104
+ REVIEW_COMPATIBILITY_ERROR_BRAND = /* @__PURE__ */ Symbol.for(
105
+ "@silmaril-security/sdk/ReviewCompatibilityError"
106
+ );
107
+ ReviewCompatibilityError = class _ReviewCompatibilityError extends Error {
108
+ requestedReview;
109
+ acknowledgedReview;
110
+ constructor(requestedReview, acknowledgedReview) {
111
+ const detail = acknowledgedReview === void 0 ? "the response omitted its acknowledgement" : "the response acknowledgement did not match";
112
+ super(`Silmaril review compatibility error: ${detail} for requested ${requestedReview} review`);
113
+ this.name = "ReviewCompatibilityError";
114
+ this.requestedReview = requestedReview;
115
+ this.acknowledgedReview = acknowledgedReview;
116
+ Object.defineProperty(this, REVIEW_COMPATIBILITY_ERROR_BRAND, {
117
+ value: true,
118
+ enumerable: false,
119
+ configurable: false,
120
+ writable: false
121
+ });
122
+ Object.setPrototypeOf(this, _ReviewCompatibilityError.prototype);
123
+ }
124
+ };
91
125
  SilmarilApiError = class _SilmarilApiError extends Error {
92
126
  status;
93
127
  statusText;
@@ -304,6 +338,9 @@ async function createLangChainHandler(firewall, options = {}) {
304
338
  ...toolName !== void 0 ? { toolName } : {}
305
339
  });
306
340
  } catch (err) {
341
+ if (isReviewCompatibilityError(err)) {
342
+ throw err;
343
+ }
307
344
  if (!failOpen) {
308
345
  throw err;
309
346
  }
@@ -458,6 +495,7 @@ __export(src_exports, {
458
495
  Outcome: () => Outcome,
459
496
  PRIMARY_OUTCOMES: () => PRIMARY_OUTCOMES,
460
497
  PromptBlockedException: () => PromptBlockedException,
498
+ ReviewCompatibilityError: () => ReviewCompatibilityError,
461
499
  SilmarilApiError: () => SilmarilApiError,
462
500
  isHarmfulOutcome: () => isHarmfulOutcome,
463
501
  isPrimaryOutcome: () => isPrimaryOutcome,
@@ -818,12 +856,32 @@ function sanitizeText(text) {
818
856
  }
819
857
 
820
858
  // src/firewall.ts
821
- var SDK_VERSION = "0.7.6";
859
+ var SDK_VERSION = "0.7.8";
822
860
  var DEFAULT_TIMEOUT_MS = 1e4;
823
861
  var DEFAULT_MAX_RETRIES = 5;
824
862
  var MAX_BACKOFF_SECONDS = 30;
825
863
  var MAX_ERROR_BODY_BYTES = 1 << 16;
826
864
  var MAX_TIMEOUT_MS = 2147483647;
865
+ function validateReview(value, name) {
866
+ if (value === void 0) {
867
+ return void 0;
868
+ }
869
+ if (value !== "standard" && value !== "deep") {
870
+ throw new Error(`Firewall: ${name} must be standard or deep`);
871
+ }
872
+ return value;
873
+ }
874
+ function resolveReview(perCallReview, clientReview) {
875
+ return perCallReview === void 0 ? clientReview : validateReview(perCallReview, "review");
876
+ }
877
+ function verifyReviewAcknowledgement(acknowledgedReview, requestedReview) {
878
+ if (requestedReview !== void 0 && acknowledgedReview !== requestedReview) {
879
+ throw new ReviewCompatibilityError(requestedReview, acknowledgedReview);
880
+ }
881
+ }
882
+ function reviewAcknowledgementFromResponse(response) {
883
+ return isRecord2(response) ? response.review : void 0;
884
+ }
827
885
  function resolveMode(value, requestedMode) {
828
886
  let responseMode;
829
887
  if (value === "shadow" || value === "warn" || value === "block") {
@@ -836,7 +894,7 @@ function resolveMode(value, requestedMode) {
836
894
  function legacyMode(shadowMode) {
837
895
  return shadowMode === void 0 ? void 0 : shadowMode ? "shadow" : "block";
838
896
  }
839
- function blockResultFromResponse(data, requestedMode) {
897
+ function blockResultFromResponse(data, requestedMode, requestedReview) {
840
898
  if (data.prediction !== "BENIGN" && data.prediction !== "MALICIOUS") {
841
899
  throw new Error("Firewall: response prediction must be BENIGN or MALICIOUS");
842
900
  }
@@ -845,6 +903,9 @@ function blockResultFromResponse(data, requestedMode) {
845
903
  score: Number(data.score),
846
904
  threshold: Number(data.threshold)
847
905
  };
906
+ if (requestedReview !== void 0) {
907
+ result.review = requestedReview;
908
+ }
848
909
  const mode = resolveMode(data.mode, requestedMode);
849
910
  if (mode !== void 0) {
850
911
  result.mode = mode;
@@ -870,7 +931,7 @@ function blockResultFromResponse(data, requestedMode) {
870
931
  result.detectorCounts = detectorCounts;
871
932
  }
872
933
  }
873
- if (data.governance !== void 0) {
934
+ if (data.governance !== void 0 && data.governance !== null) {
874
935
  result.governance = governanceDecisionFromResponse(data.governance);
875
936
  }
876
937
  return Object.freeze(result);
@@ -894,8 +955,20 @@ function withSdkMetadata(metadata, info) {
894
955
  };
895
956
  return payload;
896
957
  }
958
+ var GOVERNANCE_RESOURCE_KINDS = [
959
+ "agent",
960
+ "tool",
961
+ "mcp_server",
962
+ "mcp_tool",
963
+ "plugin",
964
+ "skill",
965
+ "extension"
966
+ ];
967
+ function isGovernanceResourceKind(value) {
968
+ return typeof value === "string" && GOVERNANCE_RESOURCE_KINDS.includes(value);
969
+ }
897
970
  function governanceResourceToWire(resource) {
898
- if (!["agent", "tool", "mcp_server", "mcp_tool", "plugin", "skill", "extension"].includes(resource.kind)) {
971
+ if (!isGovernanceResourceKind(resource.kind)) {
899
972
  throw new Error("Firewall: invalid governance resource kind");
900
973
  }
901
974
  if (typeof resource.id !== "string" || !resource.id.trim()) {
@@ -914,6 +987,38 @@ function governanceResourceToWire(resource) {
914
987
  ...resource.parentId === void 0 ? {} : { parent_id: resource.parentId }
915
988
  };
916
989
  }
990
+ function governanceResourceFromResponse(value) {
991
+ if (!isRecord2(value)) {
992
+ throw new Error("Firewall: response governance resource must be an object");
993
+ }
994
+ if (!isGovernanceResourceKind(value.kind)) {
995
+ throw new Error("Firewall: response governance resource has an invalid kind");
996
+ }
997
+ if (typeof value.id !== "string" || !value.id.trim()) {
998
+ throw new Error("Firewall: response governance resource id must be a non-empty string");
999
+ }
1000
+ if (value.kind === "mcp_tool") {
1001
+ if (typeof value.parent_id !== "string" || !value.parent_id.trim()) {
1002
+ throw new Error(
1003
+ "Firewall: response governance mcp_tool resource requires a non-empty parent_id"
1004
+ );
1005
+ }
1006
+ return Object.freeze({
1007
+ kind: value.kind,
1008
+ id: value.id,
1009
+ parentId: value.parent_id
1010
+ });
1011
+ }
1012
+ if (value.parent_id !== void 0 && value.parent_id !== null) {
1013
+ throw new Error(
1014
+ "Firewall: response governance parent_id is valid only for mcp_tool resources"
1015
+ );
1016
+ }
1017
+ return Object.freeze({
1018
+ kind: value.kind,
1019
+ id: value.id
1020
+ });
1021
+ }
917
1022
  function governanceContextToWire(context) {
918
1023
  return {
919
1024
  ...context.agent === void 0 ? {} : { agent: context.agent },
@@ -932,13 +1037,25 @@ function governanceDecisionFromResponse(value) {
932
1037
  if (typeof value.policy_version !== "string" || value.policy_version.length === 0) {
933
1038
  throw new Error("Firewall: response governance policy_version must be a non-empty string");
934
1039
  }
935
- if (value.rule_id !== void 0 && typeof value.rule_id !== "string") {
1040
+ if (value.rule_id !== void 0 && value.rule_id !== null && typeof value.rule_id !== "string") {
936
1041
  throw new Error("Firewall: response governance rule_id must be a string when provided");
937
1042
  }
1043
+ if (value.identity_revision !== void 0 && value.identity_revision !== null && typeof value.identity_revision !== "string") {
1044
+ throw new Error(
1045
+ "Firewall: response governance identity_revision must be a string when provided"
1046
+ );
1047
+ }
1048
+ if (value.reason !== void 0 && value.reason !== null && typeof value.reason !== "string") {
1049
+ throw new Error("Firewall: response governance reason must be a string when provided");
1050
+ }
1051
+ const resource = value.resource === void 0 || value.resource === null ? void 0 : governanceResourceFromResponse(value.resource);
938
1052
  return Object.freeze({
939
1053
  action: value.action,
940
- ...value.rule_id === void 0 ? {} : { ruleId: value.rule_id },
941
- policyVersion: value.policy_version
1054
+ ...value.rule_id === void 0 || value.rule_id === null ? {} : { ruleId: value.rule_id },
1055
+ policyVersion: value.policy_version,
1056
+ ...resource === void 0 ? {} : { resource },
1057
+ ...value.identity_revision === void 0 || value.identity_revision === null ? {} : { identityRevision: value.identity_revision },
1058
+ ...value.reason === void 0 || value.reason === null ? {} : { reason: value.reason }
942
1059
  });
943
1060
  }
944
1061
  async function readCappedErrorBody(response) {
@@ -1063,6 +1180,7 @@ var Firewall = class {
1063
1180
  timeoutMs;
1064
1181
  shadowMode;
1065
1182
  mode;
1183
+ review;
1066
1184
  headers;
1067
1185
  constructor(options) {
1068
1186
  if (!options.apiKey) {
@@ -1083,6 +1201,7 @@ var Firewall = class {
1083
1201
  );
1084
1202
  }
1085
1203
  this.mode = options.mode ?? legacyMode(options.shadowMode);
1204
+ this.review = validateReview(options.review, "review");
1086
1205
  this.shadowMode = this.mode === "shadow";
1087
1206
  this.headers = Object.freeze({
1088
1207
  "x-api-key": this.apiKey,
@@ -1122,9 +1241,13 @@ var Firewall = class {
1122
1241
  texts: texts.map((text) => sanitizeText(text))
1123
1242
  };
1124
1243
  const requestedMode = options.mode ?? this.mode;
1244
+ const requestedReview = resolveReview(options.review, this.review);
1125
1245
  if (requestedMode !== void 0) {
1126
1246
  payload.mode = requestedMode;
1127
1247
  }
1248
+ if (requestedReview !== void 0) {
1249
+ payload.review = requestedReview;
1250
+ }
1128
1251
  if (options.hooks && options.hooks.length > 0) {
1129
1252
  payload.hooks = options.hooks.map((h) => String(h));
1130
1253
  }
@@ -1144,6 +1267,7 @@ var Firewall = class {
1144
1267
  );
1145
1268
  }
1146
1269
  const data = await this.postWithRetry(payload, options.signal);
1270
+ verifyReviewAcknowledgement(reviewAcknowledgementFromResponse(data), requestedReview);
1147
1271
  if (!Array.isArray(data.predictions)) {
1148
1272
  throw new Error("Firewall: response predictions must be an array");
1149
1273
  }
@@ -1152,7 +1276,9 @@ var Firewall = class {
1152
1276
  `Firewall: response predictions length ${data.predictions.length} does not match submitted texts length ${payload.texts.length}`
1153
1277
  );
1154
1278
  }
1155
- return data.predictions.map((p) => blockResultFromResponse(p, requestedMode));
1279
+ return data.predictions.map(
1280
+ (p) => blockResultFromResponse(p, requestedMode, requestedReview)
1281
+ );
1156
1282
  }
1157
1283
  asLangChainHandler(options = {}) {
1158
1284
  return Promise.resolve().then(() => (init_langchain(), langchain_exports)).then(
@@ -1202,9 +1328,13 @@ var Firewall = class {
1202
1328
  async classifySingle(text, options, metadataInfo) {
1203
1329
  const payload = { text };
1204
1330
  const requestedMode = options.mode ?? this.mode;
1331
+ const requestedReview = resolveReview(options.review, this.review);
1205
1332
  if (requestedMode !== void 0) {
1206
1333
  payload.mode = requestedMode;
1207
1334
  }
1335
+ if (requestedReview !== void 0) {
1336
+ payload.review = requestedReview;
1337
+ }
1208
1338
  if (options.hook !== void 0) {
1209
1339
  payload.hook = options.hook;
1210
1340
  }
@@ -1219,7 +1349,8 @@ var Firewall = class {
1219
1349
  ...options.governance === void 0 ? {} : { governance: options.governance }
1220
1350
  });
1221
1351
  const data = await this.postWithRetry(payload, options.signal);
1222
- return blockResultFromResponse(data, requestedMode);
1352
+ verifyReviewAcknowledgement(reviewAcknowledgementFromResponse(data), requestedReview);
1353
+ return blockResultFromResponse(data, requestedMode, requestedReview);
1223
1354
  }
1224
1355
  };
1225
1356
 
@@ -1243,6 +1374,7 @@ init_hooks();
1243
1374
  Outcome,
1244
1375
  PRIMARY_OUTCOMES,
1245
1376
  PromptBlockedException,
1377
+ ReviewCompatibilityError,
1246
1378
  SilmarilApiError,
1247
1379
  isHarmfulOutcome,
1248
1380
  isPrimaryOutcome,