@rogue-security/sdk 1.0.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/README.md +101 -0
- package/dist/index.cjs +260 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +168 -0
- package/dist/index.d.ts +168 -0
- package/dist/index.js +216 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# @rogue-security/sdk
|
|
2
|
+
|
|
3
|
+
Official TypeScript SDK for the [Rogue Security](https://rogue.security) AppSec evaluation API.
|
|
4
|
+
|
|
5
|
+
Wraps the two public AppSec methods, `evaluate` (inline checks) and `invoke` (saved
|
|
6
|
+
guardrail), with full typing, typed errors, automatic retries (via `async-retry`), and
|
|
7
|
+
debug logging. Runs in Node 18+, Bun, and edge runtimes.
|
|
8
|
+
|
|
9
|
+
> **Server-side only.** The SDK authenticates with a long-lived `rsk_` API key. Never
|
|
10
|
+
> bundle it into browser/frontend code, where it would be exposed to end users. Call the
|
|
11
|
+
> SDK from your backend, or proxy requests through a server you control.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @rogue-security/sdk
|
|
17
|
+
# or: bun add @rogue-security/sdk
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { RogueClient } from "@rogue-security/sdk";
|
|
24
|
+
|
|
25
|
+
const rogue = new RogueClient({ apiKey: process.env.ROGUE_API_KEY });
|
|
26
|
+
|
|
27
|
+
const result = await rogue.evaluate({
|
|
28
|
+
messages: [
|
|
29
|
+
{ role: "user", content: "What is the patient's SSN?" },
|
|
30
|
+
{ role: "assistant", content: "It is 123-45-6789." },
|
|
31
|
+
],
|
|
32
|
+
piiCheck: true,
|
|
33
|
+
promptInjections: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
console.log(result.status, result.score);
|
|
37
|
+
for (const group of result.evaluationResults) {
|
|
38
|
+
for (const r of group.results) {
|
|
39
|
+
if (r.flagged) console.log(group.type, r.label, r.reason);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`apiKey` falls back to the `ROGUE_API_KEY` environment variable if omitted.
|
|
45
|
+
|
|
46
|
+
### Invoke a saved guardrail
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const result = await rogue.invoke({
|
|
50
|
+
guardrailId: "gr_abc123",
|
|
51
|
+
messages: [
|
|
52
|
+
{ role: "user", content: userPrompt },
|
|
53
|
+
{ role: "assistant", content: modelResponse },
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (result.blocked) {
|
|
58
|
+
// the guardrail's workspace is set to block and the evaluation failed
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Error handling
|
|
63
|
+
|
|
64
|
+
Every non-2xx maps to a typed error subclass of `RogueError`:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { RogueAuthError, RogueValidationError, RogueError } from "@rogue-security/sdk";
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await rogue.evaluate({ piiCheck: true });
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (err instanceof RogueAuthError) { /* bad / missing key */ }
|
|
73
|
+
else if (err instanceof RogueValidationError) { /* 422, e.g. no checks enabled */ }
|
|
74
|
+
else if (err instanceof RogueError) { console.error(err.status, err.message); }
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`RogueServerError` (5xx) and rate limits (429) and network/timeout failures are retried
|
|
79
|
+
automatically (default 2 retries, exponential backoff with jitter).
|
|
80
|
+
|
|
81
|
+
## Options
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
new RogueClient({
|
|
85
|
+
apiKey: "rsk_...",
|
|
86
|
+
baseUrl: "http://localhost:8006", // local dev; defaults to production
|
|
87
|
+
timeoutMs: 30000,
|
|
88
|
+
maxRetries: 2,
|
|
89
|
+
debug: true, // or "verbose" to log bodies; env: ROGUE_DEBUG=1
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Local development
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
bun install
|
|
97
|
+
bun run build
|
|
98
|
+
bun run test
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
More: see [`../docs`](../docs) for the full API reference, check descriptions, and examples.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
RogueAuthError: () => RogueAuthError,
|
|
34
|
+
RogueClient: () => RogueClient,
|
|
35
|
+
RogueConnectionError: () => RogueConnectionError,
|
|
36
|
+
RogueError: () => RogueError,
|
|
37
|
+
RogueNotFoundError: () => RogueNotFoundError,
|
|
38
|
+
RogueServerError: () => RogueServerError,
|
|
39
|
+
RogueTimeoutError: () => RogueTimeoutError,
|
|
40
|
+
RogueValidationError: () => RogueValidationError
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(index_exports);
|
|
43
|
+
|
|
44
|
+
// src/client.ts
|
|
45
|
+
var import_async_retry = __toESM(require("async-retry"), 1);
|
|
46
|
+
|
|
47
|
+
// src/errors.ts
|
|
48
|
+
var RogueError = class extends Error {
|
|
49
|
+
/** HTTP status, or null for transport-level failures (timeout, connection). */
|
|
50
|
+
status;
|
|
51
|
+
/** Parsed response body, when one was received. */
|
|
52
|
+
body;
|
|
53
|
+
/** Value of the `x-request-id` response header, when present. */
|
|
54
|
+
requestId;
|
|
55
|
+
constructor(message, opts = {}) {
|
|
56
|
+
super(message, opts.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
57
|
+
this.name = this.constructor.name;
|
|
58
|
+
this.status = opts.status ?? null;
|
|
59
|
+
this.body = opts.body;
|
|
60
|
+
this.requestId = opts.requestId;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var RogueAuthError = class extends RogueError {
|
|
64
|
+
};
|
|
65
|
+
var RogueNotFoundError = class extends RogueError {
|
|
66
|
+
};
|
|
67
|
+
var RogueValidationError = class extends RogueError {
|
|
68
|
+
};
|
|
69
|
+
var RogueServerError = class extends RogueError {
|
|
70
|
+
};
|
|
71
|
+
var RogueTimeoutError = class extends RogueError {
|
|
72
|
+
};
|
|
73
|
+
var RogueConnectionError = class extends RogueError {
|
|
74
|
+
};
|
|
75
|
+
async function errorFromResponse(res) {
|
|
76
|
+
const requestId = res.headers.get("x-request-id") ?? void 0;
|
|
77
|
+
let body;
|
|
78
|
+
let message = `Rogue API error (status ${res.status})`;
|
|
79
|
+
try {
|
|
80
|
+
body = await res.json();
|
|
81
|
+
if (body && typeof body === "object" && "error" in body && typeof body.error === "string") {
|
|
82
|
+
message = body.error;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
const opts = { status: res.status, body, requestId };
|
|
87
|
+
if (res.status === 401) return new RogueAuthError(message, opts);
|
|
88
|
+
if (res.status === 404) return new RogueNotFoundError(message, opts);
|
|
89
|
+
if (res.status === 422) return new RogueValidationError(message, opts);
|
|
90
|
+
if (res.status >= 500) return new RogueServerError(message, opts);
|
|
91
|
+
return new RogueError(message, opts);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/debug.ts
|
|
95
|
+
var API_KEY_HEADER = "x-rogue-api-key";
|
|
96
|
+
function redactHeaders(headers) {
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
99
|
+
out[k] = k.toLowerCase() === API_KEY_HEADER ? "***" : v;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
var DebugLogger = class {
|
|
104
|
+
constructor(mode, logger) {
|
|
105
|
+
this.mode = mode;
|
|
106
|
+
this.logger = logger;
|
|
107
|
+
}
|
|
108
|
+
mode;
|
|
109
|
+
logger;
|
|
110
|
+
get enabled() {
|
|
111
|
+
return this.mode !== false;
|
|
112
|
+
}
|
|
113
|
+
get verbose() {
|
|
114
|
+
return this.mode === "verbose";
|
|
115
|
+
}
|
|
116
|
+
log(message, fields) {
|
|
117
|
+
if (this.enabled) this.logger.debug(`[rogue] ${message}`, fields);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/client.ts
|
|
122
|
+
var DEFAULT_BASE_URL = "https://api.rogue.security";
|
|
123
|
+
var EVALUATE_PATH = "/api/v1/evaluation/evaluate";
|
|
124
|
+
var INVOKE_PATH = "/api/v1/evaluation/invoke";
|
|
125
|
+
function envVar(name) {
|
|
126
|
+
return typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
127
|
+
}
|
|
128
|
+
function resolveDebugMode(option) {
|
|
129
|
+
if (option !== void 0) return option;
|
|
130
|
+
const env = envVar("ROGUE_DEBUG");
|
|
131
|
+
if (env === "verbose") return "verbose";
|
|
132
|
+
return env ? true : false;
|
|
133
|
+
}
|
|
134
|
+
var camelToSnake = (key) => key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
135
|
+
var snakeToCamel = (key) => key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
136
|
+
var OPAQUE_KEYS = /* @__PURE__ */ new Set(["metadata", "parameters", "arguments"]);
|
|
137
|
+
function convertKeys(value, convert) {
|
|
138
|
+
if (Array.isArray(value)) return value.map((v) => convertKeys(v, convert));
|
|
139
|
+
if (value && typeof value === "object") {
|
|
140
|
+
const out = {};
|
|
141
|
+
for (const [k, v] of Object.entries(value)) {
|
|
142
|
+
if (v === void 0) continue;
|
|
143
|
+
out[convert(k)] = OPAQUE_KEYS.has(k) ? v : convertKeys(v, convert);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
var RogueClient = class {
|
|
150
|
+
apiKey;
|
|
151
|
+
baseUrl;
|
|
152
|
+
timeoutMs;
|
|
153
|
+
maxRetries;
|
|
154
|
+
fetchFn;
|
|
155
|
+
debug;
|
|
156
|
+
constructor(options = {}) {
|
|
157
|
+
const apiKey = options.apiKey ?? envVar("ROGUE_API_KEY");
|
|
158
|
+
if (!apiKey) {
|
|
159
|
+
throw new RogueError(
|
|
160
|
+
"Missing API key. Pass { apiKey } or set the ROGUE_API_KEY environment variable."
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
this.apiKey = apiKey;
|
|
164
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
165
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
166
|
+
this.maxRetries = options.maxRetries ?? 2;
|
|
167
|
+
if (options.fetch) {
|
|
168
|
+
this.fetchFn = options.fetch;
|
|
169
|
+
} else if (globalThis.fetch) {
|
|
170
|
+
this.fetchFn = globalThis.fetch.bind(globalThis);
|
|
171
|
+
} else {
|
|
172
|
+
throw new RogueError("No global fetch available. Pass a fetch implementation via { fetch }.");
|
|
173
|
+
}
|
|
174
|
+
this.debug = new DebugLogger(resolveDebugMode(options.debug), options.logger ?? console);
|
|
175
|
+
}
|
|
176
|
+
/** Run an inline evaluation. At least one check must be enabled. */
|
|
177
|
+
evaluate(params) {
|
|
178
|
+
return this.request(EVALUATE_PATH, params);
|
|
179
|
+
}
|
|
180
|
+
/** Run a saved guardrail's evaluation by id. */
|
|
181
|
+
invoke(params) {
|
|
182
|
+
return this.request(INVOKE_PATH, params);
|
|
183
|
+
}
|
|
184
|
+
request(path, params) {
|
|
185
|
+
const url = `${this.baseUrl}${path}`;
|
|
186
|
+
const body = JSON.stringify(convertKeys(params, camelToSnake));
|
|
187
|
+
const headers = {
|
|
188
|
+
"content-type": "application/json",
|
|
189
|
+
"x-rogue-api-key": this.apiKey
|
|
190
|
+
};
|
|
191
|
+
return (0, import_async_retry.default)(
|
|
192
|
+
async (bail, attemptNumber) => {
|
|
193
|
+
const attempt = attemptNumber - 1;
|
|
194
|
+
const started = Date.now();
|
|
195
|
+
if (this.debug.enabled) {
|
|
196
|
+
this.debug.log("request", {
|
|
197
|
+
method: "POST",
|
|
198
|
+
url,
|
|
199
|
+
attempt,
|
|
200
|
+
headers: redactHeaders(headers),
|
|
201
|
+
...this.debug.verbose ? { body } : {}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
let res;
|
|
205
|
+
try {
|
|
206
|
+
res = await this.fetchWithTimeout(url, { method: "POST", headers, body });
|
|
207
|
+
} catch (err2) {
|
|
208
|
+
const transportErr = this.toTransportError(err2);
|
|
209
|
+
this.debug.log("transport-error", { attempt, message: transportErr.message });
|
|
210
|
+
throw transportErr;
|
|
211
|
+
}
|
|
212
|
+
this.debug.log("response", { status: res.status, latencyMs: Date.now() - started, attempt });
|
|
213
|
+
if (res.ok) {
|
|
214
|
+
const json = await res.json();
|
|
215
|
+
return convertKeys(json, snakeToCamel);
|
|
216
|
+
}
|
|
217
|
+
const err = await errorFromResponse(res);
|
|
218
|
+
if (res.status === 429 || res.status >= 500) throw err;
|
|
219
|
+
bail(err);
|
|
220
|
+
throw err;
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
retries: this.maxRetries,
|
|
224
|
+
factor: 2,
|
|
225
|
+
minTimeout: 200,
|
|
226
|
+
maxTimeout: 5e3,
|
|
227
|
+
randomize: true
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
async fetchWithTimeout(url, init) {
|
|
232
|
+
const controller = new AbortController();
|
|
233
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
234
|
+
try {
|
|
235
|
+
return await this.fetchFn(url, { ...init, signal: controller.signal });
|
|
236
|
+
} finally {
|
|
237
|
+
clearTimeout(timer);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
toTransportError(err) {
|
|
241
|
+
const isAbort = err instanceof Error && err.name === "AbortError";
|
|
242
|
+
if (isAbort) {
|
|
243
|
+
return new RogueTimeoutError(`Request timed out after ${this.timeoutMs}ms`, { cause: err });
|
|
244
|
+
}
|
|
245
|
+
const message = err instanceof Error ? err.message : "Network request failed";
|
|
246
|
+
return new RogueConnectionError(message, { cause: err });
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
250
|
+
0 && (module.exports = {
|
|
251
|
+
RogueAuthError,
|
|
252
|
+
RogueClient,
|
|
253
|
+
RogueConnectionError,
|
|
254
|
+
RogueError,
|
|
255
|
+
RogueNotFoundError,
|
|
256
|
+
RogueServerError,
|
|
257
|
+
RogueTimeoutError,
|
|
258
|
+
RogueValidationError
|
|
259
|
+
});
|
|
260
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/errors.ts","../src/debug.ts"],"sourcesContent":["export { RogueClient } from \"./client\";\nexport type { RogueClientOptions } from \"./client\";\nexport {\n RogueError,\n RogueAuthError,\n RogueNotFoundError,\n RogueValidationError,\n RogueServerError,\n RogueTimeoutError,\n RogueConnectionError,\n} from \"./errors\";\nexport type { Logger, DebugMode } from \"./debug\";\nexport type {\n Mode,\n EvaluationTarget,\n Tool,\n ToolCall,\n Message,\n EvaluateParams,\n InvokeParams,\n EvaluationDetailResult,\n EvaluationResult,\n EvaluationResponse,\n} from \"./types\";\n","import retry from \"async-retry\";\nimport {\n RogueConnectionError,\n RogueError,\n RogueTimeoutError,\n errorFromResponse,\n} from \"./errors\";\nimport { DebugLogger, redactHeaders, type DebugMode, type Logger } from \"./debug\";\nimport type { EvaluateParams, EvaluationResponse, InvokeParams } from \"./types\";\n\n/** Default production base URL. Override via {@link RogueClientOptions.baseUrl} for local dev. */\nconst DEFAULT_BASE_URL = \"https://api.rogue.security\";\nconst EVALUATE_PATH = \"/api/v1/evaluation/evaluate\";\nconst INVOKE_PATH = \"/api/v1/evaluation/invoke\";\n\ntype FetchFn = typeof fetch;\n\nexport interface RogueClientOptions {\n /** `rsk_`-prefixed API key. Falls back to `ROGUE_API_KEY` env var. */\n apiKey?: string;\n /** Defaults to the production API. Use `http://localhost:8006` for local dev. */\n baseUrl?: string;\n /** Per-request timeout in milliseconds. Default 30000. */\n timeoutMs?: number;\n /** Max automatic retries on 429/5xx/network errors. Default 2. */\n maxRetries?: number;\n /** Inject a custom fetch (testing, proxies). Defaults to global `fetch`. */\n fetch?: FetchFn;\n /** Enable debug logging. `true` logs request lines; `\"verbose\"` also logs bodies. Env: `ROGUE_DEBUG`. */\n debug?: DebugMode;\n /** Logger for debug output. Defaults to `console`. */\n logger?: Logger;\n}\n\nfunction envVar(name: string): string | undefined {\n // Guarded for browser/edge runtimes where `process` is absent.\n return typeof process !== \"undefined\" ? process.env?.[name] : undefined;\n}\n\nfunction resolveDebugMode(option: DebugMode | undefined): DebugMode {\n if (option !== undefined) return option;\n const env = envVar(\"ROGUE_DEBUG\");\n if (env === \"verbose\") return \"verbose\";\n return env ? true : false;\n}\n\nconst camelToSnake = (key: string): string => key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);\nconst snakeToCamel = (key: string): string => key.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());\n\n// Keys whose VALUES are opaque user/tool data: arbitrary metadata, JSON-schema tool\n// parameters, and tool-call arguments. Their nested keys must reach the API byte-for-byte,\n// so we rename the key itself (identity for these single words) but never recurse into them.\nconst OPAQUE_KEYS = new Set([\"metadata\", \"parameters\", \"arguments\"]);\n\n/**\n * Deep-convert object keys, dropping `undefined` values. Arrays/primitives pass through.\n * Values under {@link OPAQUE_KEYS} are copied verbatim so customer-defined keys survive.\n */\nfunction convertKeys(value: unknown, convert: (k: string) => string): unknown {\n if (Array.isArray(value)) return value.map((v) => convertKeys(v, convert));\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n if (v === undefined) continue;\n out[convert(k)] = OPAQUE_KEYS.has(k) ? v : convertKeys(v, convert);\n }\n return out;\n }\n return value;\n}\n\nexport class RogueClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly timeoutMs: number;\n private readonly maxRetries: number;\n private readonly fetchFn: FetchFn;\n private readonly debug: DebugLogger;\n\n constructor(options: RogueClientOptions = {}) {\n const apiKey = options.apiKey ?? envVar(\"ROGUE_API_KEY\");\n if (!apiKey) {\n throw new RogueError(\n \"Missing API key. Pass { apiKey } or set the ROGUE_API_KEY environment variable.\",\n );\n }\n this.apiKey = apiKey;\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, \"\");\n this.timeoutMs = options.timeoutMs ?? 30000;\n this.maxRetries = options.maxRetries ?? 2;\n if (options.fetch) {\n this.fetchFn = options.fetch;\n } else if (globalThis.fetch) {\n // Native fetch must be called with `this` === the global, or browsers/edge\n // runtimes throw \"Illegal invocation\". Bind it once here.\n this.fetchFn = globalThis.fetch.bind(globalThis);\n } else {\n throw new RogueError(\"No global fetch available. Pass a fetch implementation via { fetch }.\");\n }\n this.debug = new DebugLogger(resolveDebugMode(options.debug), options.logger ?? console);\n }\n\n /** Run an inline evaluation. At least one check must be enabled. */\n evaluate(params: EvaluateParams): Promise<EvaluationResponse> {\n return this.request(EVALUATE_PATH, params);\n }\n\n /** Run a saved guardrail's evaluation by id. */\n invoke(params: InvokeParams): Promise<EvaluationResponse> {\n return this.request(INVOKE_PATH, params);\n }\n\n private request(path: string, params: object): Promise<EvaluationResponse> {\n const url = `${this.baseUrl}${path}`;\n const body = JSON.stringify(convertKeys(params, camelToSnake));\n const headers = {\n \"content-type\": \"application/json\",\n \"x-rogue-api-key\": this.apiKey,\n };\n\n // async-retry: throw to retry (transient), bail() to stop (deterministic 4xx).\n return retry<EvaluationResponse>(\n async (bail, attemptNumber) => {\n const attempt = attemptNumber - 1; // async-retry counts from 1\n const started = Date.now();\n if (this.debug.enabled) {\n this.debug.log(\"request\", {\n method: \"POST\",\n url,\n attempt,\n headers: redactHeaders(headers),\n ...(this.debug.verbose ? { body } : {}),\n });\n }\n\n let res: Response;\n try {\n res = await this.fetchWithTimeout(url, { method: \"POST\", headers, body });\n } catch (err) {\n // Transport faults (timeout/connection) are retryable: throw to retry.\n const transportErr = this.toTransportError(err);\n this.debug.log(\"transport-error\", { attempt, message: transportErr.message });\n throw transportErr;\n }\n\n this.debug.log(\"response\", { status: res.status, latencyMs: Date.now() - started, attempt });\n\n if (res.ok) {\n const json = await res.json();\n return convertKeys(json, snakeToCamel) as EvaluationResponse;\n }\n\n const err = await errorFromResponse(res);\n // Retry only transient server / rate-limit failures; 4xx are deterministic.\n if (res.status === 429 || res.status >= 500) throw err;\n bail(err);\n throw err; // unreachable after bail(); satisfies the type checker\n },\n {\n retries: this.maxRetries,\n factor: 2,\n minTimeout: 200,\n maxTimeout: 5000,\n randomize: true,\n },\n );\n }\n\n private async fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n try {\n return await this.fetchFn(url, { ...init, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n }\n }\n\n private toTransportError(err: unknown): RogueError {\n const isAbort = err instanceof Error && err.name === \"AbortError\";\n if (isAbort) {\n return new RogueTimeoutError(`Request timed out after ${this.timeoutMs}ms`, { cause: err });\n }\n const message = err instanceof Error ? err.message : \"Network request failed\";\n return new RogueConnectionError(message, { cause: err });\n }\n}\n","/** Base class for every error thrown by the SDK. Catch this to handle them all. */\nexport class RogueError extends Error {\n /** HTTP status, or null for transport-level failures (timeout, connection). */\n readonly status: number | null;\n /** Parsed response body, when one was received. */\n readonly body: unknown;\n /** Value of the `x-request-id` response header, when present. */\n readonly requestId?: string;\n\n constructor(\n message: string,\n opts: { status?: number | null; body?: unknown; requestId?: string; cause?: unknown } = {},\n ) {\n super(message, opts.cause !== undefined ? { cause: opts.cause } : undefined);\n this.name = this.constructor.name;\n this.status = opts.status ?? null;\n this.body = opts.body;\n this.requestId = opts.requestId;\n }\n}\n\n/** 401 — missing or invalid API key. */\nexport class RogueAuthError extends RogueError {}\n/** 404 — guardrail / evaluation not found (invoke). */\nexport class RogueNotFoundError extends RogueError {}\n/** 422 — invalid request (e.g. no checks enabled). */\nexport class RogueValidationError extends RogueError {}\n/** 5xx — server-side failure. Retried automatically up to maxRetries. */\nexport class RogueServerError extends RogueError {}\n/** Request exceeded the configured timeout. */\nexport class RogueTimeoutError extends RogueError {}\n/** Network-level failure (DNS, connection refused, etc.). */\nexport class RogueConnectionError extends RogueError {}\n\n/** Build the right error subclass from a non-2xx response. */\nexport async function errorFromResponse(res: Response): Promise<RogueError> {\n const requestId = res.headers.get(\"x-request-id\") ?? undefined;\n let body: unknown;\n let message = `Rogue API error (status ${res.status})`;\n try {\n body = await res.json();\n if (body && typeof body === \"object\" && \"error\" in body && typeof body.error === \"string\") {\n message = body.error;\n }\n } catch {\n // Non-JSON body; keep the default message.\n }\n const opts = { status: res.status, body, requestId };\n if (res.status === 401) return new RogueAuthError(message, opts);\n if (res.status === 404) return new RogueNotFoundError(message, opts);\n if (res.status === 422) return new RogueValidationError(message, opts);\n if (res.status >= 500) return new RogueServerError(message, opts);\n return new RogueError(message, opts);\n}\n","/** Minimal pluggable logger. `console` satisfies this. */\nexport interface Logger {\n debug(message: string, ...args: unknown[]): void;\n}\n\nexport type DebugMode = boolean | \"verbose\";\n\nconst API_KEY_HEADER = \"x-rogue-api-key\";\n\n/** Mask the API key so it never lands in logs. */\nexport function redactHeaders(headers: Record<string, string>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(headers)) {\n out[k] = k.toLowerCase() === API_KEY_HEADER ? \"***\" : v;\n }\n return out;\n}\n\nexport class DebugLogger {\n constructor(\n private readonly mode: DebugMode,\n private readonly logger: Logger,\n ) {}\n\n get enabled(): boolean {\n return this.mode !== false;\n }\n\n get verbose(): boolean {\n return this.mode === \"verbose\";\n }\n\n log(message: string, fields: Record<string, unknown>): void {\n if (this.enabled) this.logger.debug(`[rogue] ${message}`, fields);\n }\n}\n"],"mappings":"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","names":["retry","err"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/** Minimal pluggable logger. `console` satisfies this. */
|
|
2
|
+
interface Logger {
|
|
3
|
+
debug(message: string, ...args: unknown[]): void;
|
|
4
|
+
}
|
|
5
|
+
type DebugMode = boolean | "verbose";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Public types for the Rogue Security AppSec API.
|
|
9
|
+
*
|
|
10
|
+
* The SDK surface is camelCase; the wire protocol is snake_case. Conversion
|
|
11
|
+
* happens at the transport boundary (see client.ts), so consumers never touch
|
|
12
|
+
* snake_case.
|
|
13
|
+
*/
|
|
14
|
+
type Mode = "speed" | "balanced" | "quality";
|
|
15
|
+
/** Target for policy / topic-scoping checks. */
|
|
16
|
+
type EvaluationTarget = "input" | "output" | "both";
|
|
17
|
+
interface Tool {
|
|
18
|
+
name: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
parameters?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
interface ToolCall {
|
|
23
|
+
id?: string;
|
|
24
|
+
type?: string;
|
|
25
|
+
function: {
|
|
26
|
+
name: string;
|
|
27
|
+
arguments: unknown;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
interface Message {
|
|
31
|
+
role: string;
|
|
32
|
+
content?: string;
|
|
33
|
+
toolCalls?: ToolCall[];
|
|
34
|
+
}
|
|
35
|
+
/** Fields shared by `evaluate` and `invoke`. */
|
|
36
|
+
interface CommonParams {
|
|
37
|
+
/**
|
|
38
|
+
* Canonical conversation to evaluate, e.g.
|
|
39
|
+
* `[{ role: "user", content: "..." }, { role: "assistant", content: "..." }]`.
|
|
40
|
+
*/
|
|
41
|
+
messages: Message[];
|
|
42
|
+
availableTools?: Tool[];
|
|
43
|
+
/** Arbitrary string-valued tags stored alongside the evaluation. */
|
|
44
|
+
metadata?: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Inline evaluation. At least one check must be enabled or the API returns 422
|
|
48
|
+
* ({@link RogueValidationError}).
|
|
49
|
+
*/
|
|
50
|
+
interface EvaluateParams extends CommonParams {
|
|
51
|
+
hallucinationsCheck?: boolean;
|
|
52
|
+
hallucinationsMode?: Mode;
|
|
53
|
+
groundingCheck?: boolean;
|
|
54
|
+
groundingMode?: Mode;
|
|
55
|
+
groundingMultiTurnMode?: boolean;
|
|
56
|
+
assertions?: string[];
|
|
57
|
+
assertionsMode?: Mode;
|
|
58
|
+
policyTarget?: EvaluationTarget;
|
|
59
|
+
policyMultiTurnMode?: boolean;
|
|
60
|
+
policyIncludeTools?: boolean;
|
|
61
|
+
piiCheck?: boolean;
|
|
62
|
+
promptInjections?: boolean;
|
|
63
|
+
contentModerationCheck?: boolean;
|
|
64
|
+
toolUseQualityCheck?: boolean;
|
|
65
|
+
tuqMode?: Mode;
|
|
66
|
+
allowedTopics?: string[];
|
|
67
|
+
topicScopingMode?: Mode;
|
|
68
|
+
topicScopingTarget?: EvaluationTarget;
|
|
69
|
+
topicScopingMultiTurnMode?: boolean;
|
|
70
|
+
codingAgentSecurityCheck?: boolean;
|
|
71
|
+
codingAgentSecurityMode?: Mode;
|
|
72
|
+
}
|
|
73
|
+
/** Run a saved guardrail's evaluation by its id. */
|
|
74
|
+
interface InvokeParams extends CommonParams {
|
|
75
|
+
guardrailId: string;
|
|
76
|
+
}
|
|
77
|
+
interface EvaluationDetailResult {
|
|
78
|
+
name: string;
|
|
79
|
+
score: number;
|
|
80
|
+
label: string;
|
|
81
|
+
confidenceScore: number;
|
|
82
|
+
reason: string;
|
|
83
|
+
quote?: string;
|
|
84
|
+
flagged: boolean;
|
|
85
|
+
data?: string;
|
|
86
|
+
}
|
|
87
|
+
interface EvaluationResult {
|
|
88
|
+
type: string;
|
|
89
|
+
results: EvaluationDetailResult[];
|
|
90
|
+
}
|
|
91
|
+
interface EvaluationResponse {
|
|
92
|
+
/** "success" | "failed" and similar. */
|
|
93
|
+
status: string;
|
|
94
|
+
score: number | null;
|
|
95
|
+
evaluationResults: EvaluationResult[];
|
|
96
|
+
/** Present only on `invoke`: true when the guardrail is set to block and the evaluation failed. */
|
|
97
|
+
blocked?: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type FetchFn = typeof fetch;
|
|
101
|
+
interface RogueClientOptions {
|
|
102
|
+
/** `rsk_`-prefixed API key. Falls back to `ROGUE_API_KEY` env var. */
|
|
103
|
+
apiKey?: string;
|
|
104
|
+
/** Defaults to the production API. Use `http://localhost:8006` for local dev. */
|
|
105
|
+
baseUrl?: string;
|
|
106
|
+
/** Per-request timeout in milliseconds. Default 30000. */
|
|
107
|
+
timeoutMs?: number;
|
|
108
|
+
/** Max automatic retries on 429/5xx/network errors. Default 2. */
|
|
109
|
+
maxRetries?: number;
|
|
110
|
+
/** Inject a custom fetch (testing, proxies). Defaults to global `fetch`. */
|
|
111
|
+
fetch?: FetchFn;
|
|
112
|
+
/** Enable debug logging. `true` logs request lines; `"verbose"` also logs bodies. Env: `ROGUE_DEBUG`. */
|
|
113
|
+
debug?: DebugMode;
|
|
114
|
+
/** Logger for debug output. Defaults to `console`. */
|
|
115
|
+
logger?: Logger;
|
|
116
|
+
}
|
|
117
|
+
declare class RogueClient {
|
|
118
|
+
private readonly apiKey;
|
|
119
|
+
private readonly baseUrl;
|
|
120
|
+
private readonly timeoutMs;
|
|
121
|
+
private readonly maxRetries;
|
|
122
|
+
private readonly fetchFn;
|
|
123
|
+
private readonly debug;
|
|
124
|
+
constructor(options?: RogueClientOptions);
|
|
125
|
+
/** Run an inline evaluation. At least one check must be enabled. */
|
|
126
|
+
evaluate(params: EvaluateParams): Promise<EvaluationResponse>;
|
|
127
|
+
/** Run a saved guardrail's evaluation by id. */
|
|
128
|
+
invoke(params: InvokeParams): Promise<EvaluationResponse>;
|
|
129
|
+
private request;
|
|
130
|
+
private fetchWithTimeout;
|
|
131
|
+
private toTransportError;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Base class for every error thrown by the SDK. Catch this to handle them all. */
|
|
135
|
+
declare class RogueError extends Error {
|
|
136
|
+
/** HTTP status, or null for transport-level failures (timeout, connection). */
|
|
137
|
+
readonly status: number | null;
|
|
138
|
+
/** Parsed response body, when one was received. */
|
|
139
|
+
readonly body: unknown;
|
|
140
|
+
/** Value of the `x-request-id` response header, when present. */
|
|
141
|
+
readonly requestId?: string;
|
|
142
|
+
constructor(message: string, opts?: {
|
|
143
|
+
status?: number | null;
|
|
144
|
+
body?: unknown;
|
|
145
|
+
requestId?: string;
|
|
146
|
+
cause?: unknown;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/** 401 — missing or invalid API key. */
|
|
150
|
+
declare class RogueAuthError extends RogueError {
|
|
151
|
+
}
|
|
152
|
+
/** 404 — guardrail / evaluation not found (invoke). */
|
|
153
|
+
declare class RogueNotFoundError extends RogueError {
|
|
154
|
+
}
|
|
155
|
+
/** 422 — invalid request (e.g. no checks enabled). */
|
|
156
|
+
declare class RogueValidationError extends RogueError {
|
|
157
|
+
}
|
|
158
|
+
/** 5xx — server-side failure. Retried automatically up to maxRetries. */
|
|
159
|
+
declare class RogueServerError extends RogueError {
|
|
160
|
+
}
|
|
161
|
+
/** Request exceeded the configured timeout. */
|
|
162
|
+
declare class RogueTimeoutError extends RogueError {
|
|
163
|
+
}
|
|
164
|
+
/** Network-level failure (DNS, connection refused, etc.). */
|
|
165
|
+
declare class RogueConnectionError extends RogueError {
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export { type DebugMode, type EvaluateParams, type EvaluationDetailResult, type EvaluationResponse, type EvaluationResult, type EvaluationTarget, type InvokeParams, type Logger, type Message, type Mode, RogueAuthError, RogueClient, type RogueClientOptions, RogueConnectionError, RogueError, RogueNotFoundError, RogueServerError, RogueTimeoutError, RogueValidationError, type Tool, type ToolCall };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/** Minimal pluggable logger. `console` satisfies this. */
|
|
2
|
+
interface Logger {
|
|
3
|
+
debug(message: string, ...args: unknown[]): void;
|
|
4
|
+
}
|
|
5
|
+
type DebugMode = boolean | "verbose";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Public types for the Rogue Security AppSec API.
|
|
9
|
+
*
|
|
10
|
+
* The SDK surface is camelCase; the wire protocol is snake_case. Conversion
|
|
11
|
+
* happens at the transport boundary (see client.ts), so consumers never touch
|
|
12
|
+
* snake_case.
|
|
13
|
+
*/
|
|
14
|
+
type Mode = "speed" | "balanced" | "quality";
|
|
15
|
+
/** Target for policy / topic-scoping checks. */
|
|
16
|
+
type EvaluationTarget = "input" | "output" | "both";
|
|
17
|
+
interface Tool {
|
|
18
|
+
name: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
parameters?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
interface ToolCall {
|
|
23
|
+
id?: string;
|
|
24
|
+
type?: string;
|
|
25
|
+
function: {
|
|
26
|
+
name: string;
|
|
27
|
+
arguments: unknown;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
interface Message {
|
|
31
|
+
role: string;
|
|
32
|
+
content?: string;
|
|
33
|
+
toolCalls?: ToolCall[];
|
|
34
|
+
}
|
|
35
|
+
/** Fields shared by `evaluate` and `invoke`. */
|
|
36
|
+
interface CommonParams {
|
|
37
|
+
/**
|
|
38
|
+
* Canonical conversation to evaluate, e.g.
|
|
39
|
+
* `[{ role: "user", content: "..." }, { role: "assistant", content: "..." }]`.
|
|
40
|
+
*/
|
|
41
|
+
messages: Message[];
|
|
42
|
+
availableTools?: Tool[];
|
|
43
|
+
/** Arbitrary string-valued tags stored alongside the evaluation. */
|
|
44
|
+
metadata?: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Inline evaluation. At least one check must be enabled or the API returns 422
|
|
48
|
+
* ({@link RogueValidationError}).
|
|
49
|
+
*/
|
|
50
|
+
interface EvaluateParams extends CommonParams {
|
|
51
|
+
hallucinationsCheck?: boolean;
|
|
52
|
+
hallucinationsMode?: Mode;
|
|
53
|
+
groundingCheck?: boolean;
|
|
54
|
+
groundingMode?: Mode;
|
|
55
|
+
groundingMultiTurnMode?: boolean;
|
|
56
|
+
assertions?: string[];
|
|
57
|
+
assertionsMode?: Mode;
|
|
58
|
+
policyTarget?: EvaluationTarget;
|
|
59
|
+
policyMultiTurnMode?: boolean;
|
|
60
|
+
policyIncludeTools?: boolean;
|
|
61
|
+
piiCheck?: boolean;
|
|
62
|
+
promptInjections?: boolean;
|
|
63
|
+
contentModerationCheck?: boolean;
|
|
64
|
+
toolUseQualityCheck?: boolean;
|
|
65
|
+
tuqMode?: Mode;
|
|
66
|
+
allowedTopics?: string[];
|
|
67
|
+
topicScopingMode?: Mode;
|
|
68
|
+
topicScopingTarget?: EvaluationTarget;
|
|
69
|
+
topicScopingMultiTurnMode?: boolean;
|
|
70
|
+
codingAgentSecurityCheck?: boolean;
|
|
71
|
+
codingAgentSecurityMode?: Mode;
|
|
72
|
+
}
|
|
73
|
+
/** Run a saved guardrail's evaluation by its id. */
|
|
74
|
+
interface InvokeParams extends CommonParams {
|
|
75
|
+
guardrailId: string;
|
|
76
|
+
}
|
|
77
|
+
interface EvaluationDetailResult {
|
|
78
|
+
name: string;
|
|
79
|
+
score: number;
|
|
80
|
+
label: string;
|
|
81
|
+
confidenceScore: number;
|
|
82
|
+
reason: string;
|
|
83
|
+
quote?: string;
|
|
84
|
+
flagged: boolean;
|
|
85
|
+
data?: string;
|
|
86
|
+
}
|
|
87
|
+
interface EvaluationResult {
|
|
88
|
+
type: string;
|
|
89
|
+
results: EvaluationDetailResult[];
|
|
90
|
+
}
|
|
91
|
+
interface EvaluationResponse {
|
|
92
|
+
/** "success" | "failed" and similar. */
|
|
93
|
+
status: string;
|
|
94
|
+
score: number | null;
|
|
95
|
+
evaluationResults: EvaluationResult[];
|
|
96
|
+
/** Present only on `invoke`: true when the guardrail is set to block and the evaluation failed. */
|
|
97
|
+
blocked?: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type FetchFn = typeof fetch;
|
|
101
|
+
interface RogueClientOptions {
|
|
102
|
+
/** `rsk_`-prefixed API key. Falls back to `ROGUE_API_KEY` env var. */
|
|
103
|
+
apiKey?: string;
|
|
104
|
+
/** Defaults to the production API. Use `http://localhost:8006` for local dev. */
|
|
105
|
+
baseUrl?: string;
|
|
106
|
+
/** Per-request timeout in milliseconds. Default 30000. */
|
|
107
|
+
timeoutMs?: number;
|
|
108
|
+
/** Max automatic retries on 429/5xx/network errors. Default 2. */
|
|
109
|
+
maxRetries?: number;
|
|
110
|
+
/** Inject a custom fetch (testing, proxies). Defaults to global `fetch`. */
|
|
111
|
+
fetch?: FetchFn;
|
|
112
|
+
/** Enable debug logging. `true` logs request lines; `"verbose"` also logs bodies. Env: `ROGUE_DEBUG`. */
|
|
113
|
+
debug?: DebugMode;
|
|
114
|
+
/** Logger for debug output. Defaults to `console`. */
|
|
115
|
+
logger?: Logger;
|
|
116
|
+
}
|
|
117
|
+
declare class RogueClient {
|
|
118
|
+
private readonly apiKey;
|
|
119
|
+
private readonly baseUrl;
|
|
120
|
+
private readonly timeoutMs;
|
|
121
|
+
private readonly maxRetries;
|
|
122
|
+
private readonly fetchFn;
|
|
123
|
+
private readonly debug;
|
|
124
|
+
constructor(options?: RogueClientOptions);
|
|
125
|
+
/** Run an inline evaluation. At least one check must be enabled. */
|
|
126
|
+
evaluate(params: EvaluateParams): Promise<EvaluationResponse>;
|
|
127
|
+
/** Run a saved guardrail's evaluation by id. */
|
|
128
|
+
invoke(params: InvokeParams): Promise<EvaluationResponse>;
|
|
129
|
+
private request;
|
|
130
|
+
private fetchWithTimeout;
|
|
131
|
+
private toTransportError;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Base class for every error thrown by the SDK. Catch this to handle them all. */
|
|
135
|
+
declare class RogueError extends Error {
|
|
136
|
+
/** HTTP status, or null for transport-level failures (timeout, connection). */
|
|
137
|
+
readonly status: number | null;
|
|
138
|
+
/** Parsed response body, when one was received. */
|
|
139
|
+
readonly body: unknown;
|
|
140
|
+
/** Value of the `x-request-id` response header, when present. */
|
|
141
|
+
readonly requestId?: string;
|
|
142
|
+
constructor(message: string, opts?: {
|
|
143
|
+
status?: number | null;
|
|
144
|
+
body?: unknown;
|
|
145
|
+
requestId?: string;
|
|
146
|
+
cause?: unknown;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/** 401 — missing or invalid API key. */
|
|
150
|
+
declare class RogueAuthError extends RogueError {
|
|
151
|
+
}
|
|
152
|
+
/** 404 — guardrail / evaluation not found (invoke). */
|
|
153
|
+
declare class RogueNotFoundError extends RogueError {
|
|
154
|
+
}
|
|
155
|
+
/** 422 — invalid request (e.g. no checks enabled). */
|
|
156
|
+
declare class RogueValidationError extends RogueError {
|
|
157
|
+
}
|
|
158
|
+
/** 5xx — server-side failure. Retried automatically up to maxRetries. */
|
|
159
|
+
declare class RogueServerError extends RogueError {
|
|
160
|
+
}
|
|
161
|
+
/** Request exceeded the configured timeout. */
|
|
162
|
+
declare class RogueTimeoutError extends RogueError {
|
|
163
|
+
}
|
|
164
|
+
/** Network-level failure (DNS, connection refused, etc.). */
|
|
165
|
+
declare class RogueConnectionError extends RogueError {
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export { type DebugMode, type EvaluateParams, type EvaluationDetailResult, type EvaluationResponse, type EvaluationResult, type EvaluationTarget, type InvokeParams, type Logger, type Message, type Mode, RogueAuthError, RogueClient, type RogueClientOptions, RogueConnectionError, RogueError, RogueNotFoundError, RogueServerError, RogueTimeoutError, RogueValidationError, type Tool, type ToolCall };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import retry from "async-retry";
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var RogueError = class extends Error {
|
|
6
|
+
/** HTTP status, or null for transport-level failures (timeout, connection). */
|
|
7
|
+
status;
|
|
8
|
+
/** Parsed response body, when one was received. */
|
|
9
|
+
body;
|
|
10
|
+
/** Value of the `x-request-id` response header, when present. */
|
|
11
|
+
requestId;
|
|
12
|
+
constructor(message, opts = {}) {
|
|
13
|
+
super(message, opts.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
14
|
+
this.name = this.constructor.name;
|
|
15
|
+
this.status = opts.status ?? null;
|
|
16
|
+
this.body = opts.body;
|
|
17
|
+
this.requestId = opts.requestId;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var RogueAuthError = class extends RogueError {
|
|
21
|
+
};
|
|
22
|
+
var RogueNotFoundError = class extends RogueError {
|
|
23
|
+
};
|
|
24
|
+
var RogueValidationError = class extends RogueError {
|
|
25
|
+
};
|
|
26
|
+
var RogueServerError = class extends RogueError {
|
|
27
|
+
};
|
|
28
|
+
var RogueTimeoutError = class extends RogueError {
|
|
29
|
+
};
|
|
30
|
+
var RogueConnectionError = class extends RogueError {
|
|
31
|
+
};
|
|
32
|
+
async function errorFromResponse(res) {
|
|
33
|
+
const requestId = res.headers.get("x-request-id") ?? void 0;
|
|
34
|
+
let body;
|
|
35
|
+
let message = `Rogue API error (status ${res.status})`;
|
|
36
|
+
try {
|
|
37
|
+
body = await res.json();
|
|
38
|
+
if (body && typeof body === "object" && "error" in body && typeof body.error === "string") {
|
|
39
|
+
message = body.error;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
const opts = { status: res.status, body, requestId };
|
|
44
|
+
if (res.status === 401) return new RogueAuthError(message, opts);
|
|
45
|
+
if (res.status === 404) return new RogueNotFoundError(message, opts);
|
|
46
|
+
if (res.status === 422) return new RogueValidationError(message, opts);
|
|
47
|
+
if (res.status >= 500) return new RogueServerError(message, opts);
|
|
48
|
+
return new RogueError(message, opts);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/debug.ts
|
|
52
|
+
var API_KEY_HEADER = "x-rogue-api-key";
|
|
53
|
+
function redactHeaders(headers) {
|
|
54
|
+
const out = {};
|
|
55
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
56
|
+
out[k] = k.toLowerCase() === API_KEY_HEADER ? "***" : v;
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
var DebugLogger = class {
|
|
61
|
+
constructor(mode, logger) {
|
|
62
|
+
this.mode = mode;
|
|
63
|
+
this.logger = logger;
|
|
64
|
+
}
|
|
65
|
+
mode;
|
|
66
|
+
logger;
|
|
67
|
+
get enabled() {
|
|
68
|
+
return this.mode !== false;
|
|
69
|
+
}
|
|
70
|
+
get verbose() {
|
|
71
|
+
return this.mode === "verbose";
|
|
72
|
+
}
|
|
73
|
+
log(message, fields) {
|
|
74
|
+
if (this.enabled) this.logger.debug(`[rogue] ${message}`, fields);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/client.ts
|
|
79
|
+
var DEFAULT_BASE_URL = "https://api.rogue.security";
|
|
80
|
+
var EVALUATE_PATH = "/api/v1/evaluation/evaluate";
|
|
81
|
+
var INVOKE_PATH = "/api/v1/evaluation/invoke";
|
|
82
|
+
function envVar(name) {
|
|
83
|
+
return typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
84
|
+
}
|
|
85
|
+
function resolveDebugMode(option) {
|
|
86
|
+
if (option !== void 0) return option;
|
|
87
|
+
const env = envVar("ROGUE_DEBUG");
|
|
88
|
+
if (env === "verbose") return "verbose";
|
|
89
|
+
return env ? true : false;
|
|
90
|
+
}
|
|
91
|
+
var camelToSnake = (key) => key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
92
|
+
var snakeToCamel = (key) => key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
93
|
+
var OPAQUE_KEYS = /* @__PURE__ */ new Set(["metadata", "parameters", "arguments"]);
|
|
94
|
+
function convertKeys(value, convert) {
|
|
95
|
+
if (Array.isArray(value)) return value.map((v) => convertKeys(v, convert));
|
|
96
|
+
if (value && typeof value === "object") {
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const [k, v] of Object.entries(value)) {
|
|
99
|
+
if (v === void 0) continue;
|
|
100
|
+
out[convert(k)] = OPAQUE_KEYS.has(k) ? v : convertKeys(v, convert);
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
var RogueClient = class {
|
|
107
|
+
apiKey;
|
|
108
|
+
baseUrl;
|
|
109
|
+
timeoutMs;
|
|
110
|
+
maxRetries;
|
|
111
|
+
fetchFn;
|
|
112
|
+
debug;
|
|
113
|
+
constructor(options = {}) {
|
|
114
|
+
const apiKey = options.apiKey ?? envVar("ROGUE_API_KEY");
|
|
115
|
+
if (!apiKey) {
|
|
116
|
+
throw new RogueError(
|
|
117
|
+
"Missing API key. Pass { apiKey } or set the ROGUE_API_KEY environment variable."
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
this.apiKey = apiKey;
|
|
121
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
122
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
123
|
+
this.maxRetries = options.maxRetries ?? 2;
|
|
124
|
+
if (options.fetch) {
|
|
125
|
+
this.fetchFn = options.fetch;
|
|
126
|
+
} else if (globalThis.fetch) {
|
|
127
|
+
this.fetchFn = globalThis.fetch.bind(globalThis);
|
|
128
|
+
} else {
|
|
129
|
+
throw new RogueError("No global fetch available. Pass a fetch implementation via { fetch }.");
|
|
130
|
+
}
|
|
131
|
+
this.debug = new DebugLogger(resolveDebugMode(options.debug), options.logger ?? console);
|
|
132
|
+
}
|
|
133
|
+
/** Run an inline evaluation. At least one check must be enabled. */
|
|
134
|
+
evaluate(params) {
|
|
135
|
+
return this.request(EVALUATE_PATH, params);
|
|
136
|
+
}
|
|
137
|
+
/** Run a saved guardrail's evaluation by id. */
|
|
138
|
+
invoke(params) {
|
|
139
|
+
return this.request(INVOKE_PATH, params);
|
|
140
|
+
}
|
|
141
|
+
request(path, params) {
|
|
142
|
+
const url = `${this.baseUrl}${path}`;
|
|
143
|
+
const body = JSON.stringify(convertKeys(params, camelToSnake));
|
|
144
|
+
const headers = {
|
|
145
|
+
"content-type": "application/json",
|
|
146
|
+
"x-rogue-api-key": this.apiKey
|
|
147
|
+
};
|
|
148
|
+
return retry(
|
|
149
|
+
async (bail, attemptNumber) => {
|
|
150
|
+
const attempt = attemptNumber - 1;
|
|
151
|
+
const started = Date.now();
|
|
152
|
+
if (this.debug.enabled) {
|
|
153
|
+
this.debug.log("request", {
|
|
154
|
+
method: "POST",
|
|
155
|
+
url,
|
|
156
|
+
attempt,
|
|
157
|
+
headers: redactHeaders(headers),
|
|
158
|
+
...this.debug.verbose ? { body } : {}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
let res;
|
|
162
|
+
try {
|
|
163
|
+
res = await this.fetchWithTimeout(url, { method: "POST", headers, body });
|
|
164
|
+
} catch (err2) {
|
|
165
|
+
const transportErr = this.toTransportError(err2);
|
|
166
|
+
this.debug.log("transport-error", { attempt, message: transportErr.message });
|
|
167
|
+
throw transportErr;
|
|
168
|
+
}
|
|
169
|
+
this.debug.log("response", { status: res.status, latencyMs: Date.now() - started, attempt });
|
|
170
|
+
if (res.ok) {
|
|
171
|
+
const json = await res.json();
|
|
172
|
+
return convertKeys(json, snakeToCamel);
|
|
173
|
+
}
|
|
174
|
+
const err = await errorFromResponse(res);
|
|
175
|
+
if (res.status === 429 || res.status >= 500) throw err;
|
|
176
|
+
bail(err);
|
|
177
|
+
throw err;
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
retries: this.maxRetries,
|
|
181
|
+
factor: 2,
|
|
182
|
+
minTimeout: 200,
|
|
183
|
+
maxTimeout: 5e3,
|
|
184
|
+
randomize: true
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
async fetchWithTimeout(url, init) {
|
|
189
|
+
const controller = new AbortController();
|
|
190
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
191
|
+
try {
|
|
192
|
+
return await this.fetchFn(url, { ...init, signal: controller.signal });
|
|
193
|
+
} finally {
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
toTransportError(err) {
|
|
198
|
+
const isAbort = err instanceof Error && err.name === "AbortError";
|
|
199
|
+
if (isAbort) {
|
|
200
|
+
return new RogueTimeoutError(`Request timed out after ${this.timeoutMs}ms`, { cause: err });
|
|
201
|
+
}
|
|
202
|
+
const message = err instanceof Error ? err.message : "Network request failed";
|
|
203
|
+
return new RogueConnectionError(message, { cause: err });
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
export {
|
|
207
|
+
RogueAuthError,
|
|
208
|
+
RogueClient,
|
|
209
|
+
RogueConnectionError,
|
|
210
|
+
RogueError,
|
|
211
|
+
RogueNotFoundError,
|
|
212
|
+
RogueServerError,
|
|
213
|
+
RogueTimeoutError,
|
|
214
|
+
RogueValidationError
|
|
215
|
+
};
|
|
216
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/errors.ts","../src/debug.ts"],"sourcesContent":["import retry from \"async-retry\";\nimport {\n RogueConnectionError,\n RogueError,\n RogueTimeoutError,\n errorFromResponse,\n} from \"./errors\";\nimport { DebugLogger, redactHeaders, type DebugMode, type Logger } from \"./debug\";\nimport type { EvaluateParams, EvaluationResponse, InvokeParams } from \"./types\";\n\n/** Default production base URL. Override via {@link RogueClientOptions.baseUrl} for local dev. */\nconst DEFAULT_BASE_URL = \"https://api.rogue.security\";\nconst EVALUATE_PATH = \"/api/v1/evaluation/evaluate\";\nconst INVOKE_PATH = \"/api/v1/evaluation/invoke\";\n\ntype FetchFn = typeof fetch;\n\nexport interface RogueClientOptions {\n /** `rsk_`-prefixed API key. Falls back to `ROGUE_API_KEY` env var. */\n apiKey?: string;\n /** Defaults to the production API. Use `http://localhost:8006` for local dev. */\n baseUrl?: string;\n /** Per-request timeout in milliseconds. Default 30000. */\n timeoutMs?: number;\n /** Max automatic retries on 429/5xx/network errors. Default 2. */\n maxRetries?: number;\n /** Inject a custom fetch (testing, proxies). Defaults to global `fetch`. */\n fetch?: FetchFn;\n /** Enable debug logging. `true` logs request lines; `\"verbose\"` also logs bodies. Env: `ROGUE_DEBUG`. */\n debug?: DebugMode;\n /** Logger for debug output. Defaults to `console`. */\n logger?: Logger;\n}\n\nfunction envVar(name: string): string | undefined {\n // Guarded for browser/edge runtimes where `process` is absent.\n return typeof process !== \"undefined\" ? process.env?.[name] : undefined;\n}\n\nfunction resolveDebugMode(option: DebugMode | undefined): DebugMode {\n if (option !== undefined) return option;\n const env = envVar(\"ROGUE_DEBUG\");\n if (env === \"verbose\") return \"verbose\";\n return env ? true : false;\n}\n\nconst camelToSnake = (key: string): string => key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);\nconst snakeToCamel = (key: string): string => key.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());\n\n// Keys whose VALUES are opaque user/tool data: arbitrary metadata, JSON-schema tool\n// parameters, and tool-call arguments. Their nested keys must reach the API byte-for-byte,\n// so we rename the key itself (identity for these single words) but never recurse into them.\nconst OPAQUE_KEYS = new Set([\"metadata\", \"parameters\", \"arguments\"]);\n\n/**\n * Deep-convert object keys, dropping `undefined` values. Arrays/primitives pass through.\n * Values under {@link OPAQUE_KEYS} are copied verbatim so customer-defined keys survive.\n */\nfunction convertKeys(value: unknown, convert: (k: string) => string): unknown {\n if (Array.isArray(value)) return value.map((v) => convertKeys(v, convert));\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n if (v === undefined) continue;\n out[convert(k)] = OPAQUE_KEYS.has(k) ? v : convertKeys(v, convert);\n }\n return out;\n }\n return value;\n}\n\nexport class RogueClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly timeoutMs: number;\n private readonly maxRetries: number;\n private readonly fetchFn: FetchFn;\n private readonly debug: DebugLogger;\n\n constructor(options: RogueClientOptions = {}) {\n const apiKey = options.apiKey ?? envVar(\"ROGUE_API_KEY\");\n if (!apiKey) {\n throw new RogueError(\n \"Missing API key. Pass { apiKey } or set the ROGUE_API_KEY environment variable.\",\n );\n }\n this.apiKey = apiKey;\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, \"\");\n this.timeoutMs = options.timeoutMs ?? 30000;\n this.maxRetries = options.maxRetries ?? 2;\n if (options.fetch) {\n this.fetchFn = options.fetch;\n } else if (globalThis.fetch) {\n // Native fetch must be called with `this` === the global, or browsers/edge\n // runtimes throw \"Illegal invocation\". Bind it once here.\n this.fetchFn = globalThis.fetch.bind(globalThis);\n } else {\n throw new RogueError(\"No global fetch available. Pass a fetch implementation via { fetch }.\");\n }\n this.debug = new DebugLogger(resolveDebugMode(options.debug), options.logger ?? console);\n }\n\n /** Run an inline evaluation. At least one check must be enabled. */\n evaluate(params: EvaluateParams): Promise<EvaluationResponse> {\n return this.request(EVALUATE_PATH, params);\n }\n\n /** Run a saved guardrail's evaluation by id. */\n invoke(params: InvokeParams): Promise<EvaluationResponse> {\n return this.request(INVOKE_PATH, params);\n }\n\n private request(path: string, params: object): Promise<EvaluationResponse> {\n const url = `${this.baseUrl}${path}`;\n const body = JSON.stringify(convertKeys(params, camelToSnake));\n const headers = {\n \"content-type\": \"application/json\",\n \"x-rogue-api-key\": this.apiKey,\n };\n\n // async-retry: throw to retry (transient), bail() to stop (deterministic 4xx).\n return retry<EvaluationResponse>(\n async (bail, attemptNumber) => {\n const attempt = attemptNumber - 1; // async-retry counts from 1\n const started = Date.now();\n if (this.debug.enabled) {\n this.debug.log(\"request\", {\n method: \"POST\",\n url,\n attempt,\n headers: redactHeaders(headers),\n ...(this.debug.verbose ? { body } : {}),\n });\n }\n\n let res: Response;\n try {\n res = await this.fetchWithTimeout(url, { method: \"POST\", headers, body });\n } catch (err) {\n // Transport faults (timeout/connection) are retryable: throw to retry.\n const transportErr = this.toTransportError(err);\n this.debug.log(\"transport-error\", { attempt, message: transportErr.message });\n throw transportErr;\n }\n\n this.debug.log(\"response\", { status: res.status, latencyMs: Date.now() - started, attempt });\n\n if (res.ok) {\n const json = await res.json();\n return convertKeys(json, snakeToCamel) as EvaluationResponse;\n }\n\n const err = await errorFromResponse(res);\n // Retry only transient server / rate-limit failures; 4xx are deterministic.\n if (res.status === 429 || res.status >= 500) throw err;\n bail(err);\n throw err; // unreachable after bail(); satisfies the type checker\n },\n {\n retries: this.maxRetries,\n factor: 2,\n minTimeout: 200,\n maxTimeout: 5000,\n randomize: true,\n },\n );\n }\n\n private async fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n try {\n return await this.fetchFn(url, { ...init, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n }\n }\n\n private toTransportError(err: unknown): RogueError {\n const isAbort = err instanceof Error && err.name === \"AbortError\";\n if (isAbort) {\n return new RogueTimeoutError(`Request timed out after ${this.timeoutMs}ms`, { cause: err });\n }\n const message = err instanceof Error ? err.message : \"Network request failed\";\n return new RogueConnectionError(message, { cause: err });\n }\n}\n","/** Base class for every error thrown by the SDK. Catch this to handle them all. */\nexport class RogueError extends Error {\n /** HTTP status, or null for transport-level failures (timeout, connection). */\n readonly status: number | null;\n /** Parsed response body, when one was received. */\n readonly body: unknown;\n /** Value of the `x-request-id` response header, when present. */\n readonly requestId?: string;\n\n constructor(\n message: string,\n opts: { status?: number | null; body?: unknown; requestId?: string; cause?: unknown } = {},\n ) {\n super(message, opts.cause !== undefined ? { cause: opts.cause } : undefined);\n this.name = this.constructor.name;\n this.status = opts.status ?? null;\n this.body = opts.body;\n this.requestId = opts.requestId;\n }\n}\n\n/** 401 — missing or invalid API key. */\nexport class RogueAuthError extends RogueError {}\n/** 404 — guardrail / evaluation not found (invoke). */\nexport class RogueNotFoundError extends RogueError {}\n/** 422 — invalid request (e.g. no checks enabled). */\nexport class RogueValidationError extends RogueError {}\n/** 5xx — server-side failure. Retried automatically up to maxRetries. */\nexport class RogueServerError extends RogueError {}\n/** Request exceeded the configured timeout. */\nexport class RogueTimeoutError extends RogueError {}\n/** Network-level failure (DNS, connection refused, etc.). */\nexport class RogueConnectionError extends RogueError {}\n\n/** Build the right error subclass from a non-2xx response. */\nexport async function errorFromResponse(res: Response): Promise<RogueError> {\n const requestId = res.headers.get(\"x-request-id\") ?? undefined;\n let body: unknown;\n let message = `Rogue API error (status ${res.status})`;\n try {\n body = await res.json();\n if (body && typeof body === \"object\" && \"error\" in body && typeof body.error === \"string\") {\n message = body.error;\n }\n } catch {\n // Non-JSON body; keep the default message.\n }\n const opts = { status: res.status, body, requestId };\n if (res.status === 401) return new RogueAuthError(message, opts);\n if (res.status === 404) return new RogueNotFoundError(message, opts);\n if (res.status === 422) return new RogueValidationError(message, opts);\n if (res.status >= 500) return new RogueServerError(message, opts);\n return new RogueError(message, opts);\n}\n","/** Minimal pluggable logger. `console` satisfies this. */\nexport interface Logger {\n debug(message: string, ...args: unknown[]): void;\n}\n\nexport type DebugMode = boolean | \"verbose\";\n\nconst API_KEY_HEADER = \"x-rogue-api-key\";\n\n/** Mask the API key so it never lands in logs. */\nexport function redactHeaders(headers: Record<string, string>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(headers)) {\n out[k] = k.toLowerCase() === API_KEY_HEADER ? \"***\" : v;\n }\n return out;\n}\n\nexport class DebugLogger {\n constructor(\n private readonly mode: DebugMode,\n private readonly logger: Logger,\n ) {}\n\n get enabled(): boolean {\n return this.mode !== false;\n }\n\n get verbose(): boolean {\n return this.mode === \"verbose\";\n }\n\n log(message: string, fields: Record<string, unknown>): void {\n if (this.enabled) this.logger.debug(`[rogue] ${message}`, fields);\n }\n}\n"],"mappings":"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","names":["err"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rogue-security/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official TypeScript SDK for the Rogue Security AppSec evaluation API.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"author": "Rogue Security <team@rogue.security>",
|
|
7
|
+
"homepage": "https://github.com/qualifire-dev/qualifire/tree/main/sdks/typescript",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/qualifire-dev/qualifire.git",
|
|
11
|
+
"directory": "sdks/typescript"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"rogue",
|
|
15
|
+
"rogue-security",
|
|
16
|
+
"ai-security",
|
|
17
|
+
"guardrails",
|
|
18
|
+
"llm",
|
|
19
|
+
"appsec",
|
|
20
|
+
"evaluation"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.cjs",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"require": "./dist/index.cjs"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"type:check": "tsc --noEmit",
|
|
43
|
+
"test": "vitest run"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"async-retry": "^1.3.3"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/async-retry": "^1.4.9",
|
|
53
|
+
"@types/node": "^20.0.0",
|
|
54
|
+
"tsup": "^8.3.0",
|
|
55
|
+
"typescript": "^5.6.0",
|
|
56
|
+
"vitest": "^2.1.0"
|
|
57
|
+
}
|
|
58
|
+
}
|