@inferior-ai/sdk 2.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE.md +37 -0
  3. package/NOTICE.md +56 -0
  4. package/README.md +454 -0
  5. package/SECURITY.md +62 -0
  6. package/STABILITY.md +95 -0
  7. package/SUPPORT.md +66 -0
  8. package/dist/__tests__/with_mock_client.test.d.ts +9 -0
  9. package/dist/__tests__/with_mock_client.test.d.ts.map +1 -0
  10. package/dist/__tests__/with_mock_client.test.js +154 -0
  11. package/dist/__tests__/with_mock_client.test.js.map +1 -0
  12. package/dist/_worthiness.d.ts +34 -0
  13. package/dist/_worthiness.d.ts.map +1 -0
  14. package/dist/_worthiness.js +201 -0
  15. package/dist/_worthiness.js.map +1 -0
  16. package/dist/client.d.ts +155 -0
  17. package/dist/client.d.ts.map +1 -0
  18. package/dist/client.js +900 -0
  19. package/dist/client.js.map +1 -0
  20. package/dist/constants.d.ts +31 -0
  21. package/dist/constants.d.ts.map +1 -0
  22. package/dist/constants.js +35 -0
  23. package/dist/constants.js.map +1 -0
  24. package/dist/errors.d.ts +71 -0
  25. package/dist/errors.d.ts.map +1 -0
  26. package/dist/errors.js +114 -0
  27. package/dist/errors.js.map +1 -0
  28. package/dist/index.d.ts +8 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +7 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/models.d.ts +603 -0
  33. package/dist/models.d.ts.map +1 -0
  34. package/dist/models.js +10 -0
  35. package/dist/models.js.map +1 -0
  36. package/dist/retry.d.ts +35 -0
  37. package/dist/retry.d.ts.map +1 -0
  38. package/dist/retry.js +83 -0
  39. package/dist/retry.js.map +1 -0
  40. package/dist/testing/index.d.ts +16 -0
  41. package/dist/testing/index.d.ts.map +1 -0
  42. package/dist/testing/index.js +15 -0
  43. package/dist/testing/index.js.map +1 -0
  44. package/dist/testing/mock-client.d.ts +102 -0
  45. package/dist/testing/mock-client.d.ts.map +1 -0
  46. package/dist/testing/mock-client.js +217 -0
  47. package/dist/testing/mock-client.js.map +1 -0
  48. package/dist/webhooks.d.ts +52 -0
  49. package/dist/webhooks.d.ts.map +1 -0
  50. package/dist/webhooks.js +90 -0
  51. package/dist/webhooks.js.map +1 -0
  52. package/package.json +61 -0
package/dist/retry.js ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Retry-with-backoff helper for SDK requests.
3
+ *
4
+ * The default policy retries idempotent requests on transient errors
5
+ * (429, 502, 503, 504) and connection failures with exponential
6
+ * backoff plus jitter. On 429 responses we honour the `Retry-After`
7
+ * header when present (seconds form only — RFC HTTP-date is not yet
8
+ * supported).
9
+ */
10
+ const RETRY_STATUSES = new Set([429, 502, 503, 504]);
11
+ const IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
12
+ export const DEFAULT_RETRY = {
13
+ maxAttempts: 3,
14
+ baseDelay: 0.5,
15
+ jitter: 0.2,
16
+ maxDelay: 30,
17
+ retryWrites: false,
18
+ };
19
+ function backoff(attempt, base, jitter, cap) {
20
+ const delay = Math.min(base * 2 ** (attempt - 1), cap);
21
+ if (jitter > 0)
22
+ return delay * (1 + Math.random() * jitter);
23
+ return delay;
24
+ }
25
+ function parseRetryAfter(resp) {
26
+ const raw = resp.headers.get("retry-after");
27
+ if (!raw)
28
+ return null;
29
+ const n = Number.parseFloat(raw);
30
+ return Number.isFinite(n) ? n : null;
31
+ }
32
+ function isIdempotent(method, headers, retry) {
33
+ if (IDEMPOTENT_METHODS.has(method.toUpperCase()))
34
+ return true;
35
+ if (retry.retryWrites)
36
+ return true;
37
+ if (!headers)
38
+ return false;
39
+ return Object.keys(headers).some((k) => k.toLowerCase() === "idempotency-key");
40
+ }
41
+ const sleep = (seconds) => new Promise((r) => setTimeout(r, Math.round(seconds * 1000)));
42
+ /**
43
+ * Run `fn` with retry semantics. `fn` receives the attempt number and
44
+ * must return either a `Response` (status code is inspected) or throw.
45
+ *
46
+ * The `headers` argument is consulted to determine idempotency (only
47
+ * used to decide whether retries apply on writes).
48
+ */
49
+ export async function withRetry(retry, method, headers, fn) {
50
+ const idempotent = isIdempotent(method, headers, retry);
51
+ let lastErr;
52
+ for (let attempt = 1; attempt <= retry.maxAttempts; attempt++) {
53
+ let resp;
54
+ try {
55
+ resp = await fn();
56
+ }
57
+ catch (e) {
58
+ lastErr = e;
59
+ if (attempt >= retry.maxAttempts || !idempotent)
60
+ throw e;
61
+ await sleep(backoff(attempt, retry.baseDelay, retry.jitter, retry.maxDelay));
62
+ continue;
63
+ }
64
+ if (!RETRY_STATUSES.has(resp.status))
65
+ return resp;
66
+ if (attempt >= retry.maxAttempts || !idempotent)
67
+ return resp;
68
+ const retryAfter = parseRetryAfter(resp);
69
+ const sleepFor = retryAfter ?? backoff(attempt, retry.baseDelay, retry.jitter, retry.maxDelay);
70
+ // Drain response so the connection is reusable.
71
+ try {
72
+ await resp.text();
73
+ }
74
+ catch {
75
+ /* empty */
76
+ }
77
+ await sleep(sleepFor);
78
+ }
79
+ if (lastErr)
80
+ throw lastErr;
81
+ throw new Error("withRetry: exhausted attempts without producing a response");
82
+ }
83
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Test fixtures for callers integrating against the Inferior SDK.
3
+ *
4
+ * Subpath import (NOT bundled with the main `@inferior-ai/sdk` entry):
5
+ *
6
+ * ```typescript
7
+ * import { MockInferiorClient } from '@inferior-ai/sdk/testing'
8
+ * ```
9
+ *
10
+ * Lets your tests construct an `InferiorClient` that does not hit the
11
+ * network. Add canned responses per endpoint; assert against the
12
+ * calls your code under test makes via `client.calls`.
13
+ */
14
+ export { MockInferiorClient } from "./mock-client.js";
15
+ export type { RecordedCall, MockClientOptions } from "./mock-client.js";
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Test fixtures for callers integrating against the Inferior SDK.
3
+ *
4
+ * Subpath import (NOT bundled with the main `@inferior-ai/sdk` entry):
5
+ *
6
+ * ```typescript
7
+ * import { MockInferiorClient } from '@inferior-ai/sdk/testing'
8
+ * ```
9
+ *
10
+ * Lets your tests construct an `InferiorClient` that does not hit the
11
+ * network. Add canned responses per endpoint; assert against the
12
+ * calls your code under test makes via `client.calls`.
13
+ */
14
+ export { MockInferiorClient } from "./mock-client.js";
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * `MockInferiorClient` — InferiorClient subclass that intercepts
3
+ * every fetch call for tests.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * import { MockInferiorClient } from '@inferior-ai/sdk/testing'
8
+ *
9
+ * test('my search wrapper', async () => {
10
+ * const client = new MockInferiorClient()
11
+ * client.mockSearch({
12
+ * results: [{ id: 'exp_abc', title: 'Stripe webhook fix' } as any],
13
+ * })
14
+ *
15
+ * // Code under test:
16
+ * const response = await client.search('stripe')
17
+ * expect(response.results[0].title).toBe('Stripe webhook fix')
18
+ *
19
+ * // Inspect what your code actually called:
20
+ * expect(client.calls).toHaveLength(1)
21
+ * expect(client.calls[0].url).toContain('/v1/experiences/search')
22
+ * expect(client.calls[0].headers.get('user-agent')).toMatch(/^inferior-sdk-ts/)
23
+ * })
24
+ * ```
25
+ *
26
+ * Stubs are FIFO. Default response when no stub matches is a 404
27
+ * with a descriptive body — surfaces unexpected calls fast.
28
+ */
29
+ import { InferiorClient } from "../client.js";
30
+ export interface RecordedCall {
31
+ /** HTTP method, uppercased. */
32
+ method: string;
33
+ /** Full request URL as a string. */
34
+ url: string;
35
+ /** Request headers (Headers object — case-insensitive `.get()`). */
36
+ headers: Headers;
37
+ /** Raw request body as a string (empty for GETs). */
38
+ body: string;
39
+ /** Parsed body if JSON, else null. */
40
+ bodyJson: unknown | null;
41
+ }
42
+ export interface MockClientOptions {
43
+ apiKey?: string;
44
+ baseUrl?: string;
45
+ }
46
+ export declare class MockInferiorClient extends InferiorClient {
47
+ private stubs;
48
+ /** Every intercepted request — assert against this in your tests. */
49
+ calls: RecordedCall[];
50
+ constructor(options?: MockClientOptions);
51
+ private dispatch;
52
+ private unmatched;
53
+ /**
54
+ * Queue a single response for the next matching request.
55
+ * Returns `this` for chaining.
56
+ */
57
+ addResponse(method: string, pathSubstring: string, options?: {
58
+ status?: number;
59
+ jsonBody?: unknown;
60
+ textBody?: string;
61
+ headers?: Record<string, string>;
62
+ description?: string;
63
+ }): this;
64
+ mockSearch(opts?: {
65
+ results?: unknown[];
66
+ total?: number;
67
+ cached?: boolean;
68
+ }): this;
69
+ mockDeposit(opts?: {
70
+ id?: string;
71
+ status?: string;
72
+ qualityScore?: number;
73
+ validationState?: string;
74
+ }): this;
75
+ mockDepositRaw(opts?: {
76
+ rawDepositId?: string;
77
+ }): this;
78
+ mockFeedback(opts?: {
79
+ feedbackId?: string;
80
+ experienceId?: string;
81
+ }): this;
82
+ /**
83
+ * Stub the next matching call with an error response.
84
+ * The SDK's `raiseForStatus` will raise the corresponding typed
85
+ * exception (`ServerError`, `RateLimitError`, etc.) with `requestId`
86
+ * populated from the mock's `X-Request-ID`.
87
+ */
88
+ mockError(method: string, pathSubstring: string, opts: {
89
+ status: number;
90
+ error?: string;
91
+ message?: string;
92
+ details?: Record<string, unknown>;
93
+ }): this;
94
+ /** Clear all queued stubs and recorded calls. */
95
+ reset(): this;
96
+ /**
97
+ * Throw if any registered stub never fired. Useful at test
98
+ * teardown to catch over-stubbing.
99
+ */
100
+ assertNoUnmatched(): void;
101
+ }
102
+ //# sourceMappingURL=mock-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-client.d.ts","sourceRoot":"","sources":["../../src/testing/mock-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,WAAW,YAAY;IAC3B,+BAA+B;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,oEAAoE;IACpE,OAAO,EAAE,OAAO,CAAC;IACjB,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,sCAAsC;IACtC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;CAC1B;AAQD,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,kBAAmB,SAAQ,cAAc;IACpD,OAAO,CAAC,KAAK,CAAc;IAC3B,qEAAqE;IAC9D,KAAK,EAAE,YAAY,EAAE,CAAM;gBAEtB,OAAO,GAAE,iBAAsB;YAc7B,QAAQ;IAgCtB,OAAO,CAAC,SAAS;IA4BjB;;;OAGG;IACH,WAAW,CACT,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,WAAW,CAAC,EAAE,MAAM,CAAC;KACjB,GACL,IAAI;IAmCP,UAAU,CAAC,IAAI,GAAE;QAAE,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,IAAI;IAatF,WAAW,CACT,IAAI,GAAE;QACJ,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,eAAe,CAAC,EAAE,MAAM,CAAC;KACrB,GACL,IAAI;IAqBP,cAAc,CAAC,IAAI,GAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,IAAI;IAa1D,YAAY,CAAC,IAAI,GAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,IAAI;IAe7E;;;;;OAKG;IACH,SAAS,CACP,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAC5F,IAAI;IAWP,iDAAiD;IACjD,KAAK,IAAI,IAAI;IAMb;;;OAGG;IACH,iBAAiB,IAAI,IAAI;CAM1B"}
@@ -0,0 +1,217 @@
1
+ /**
2
+ * `MockInferiorClient` — InferiorClient subclass that intercepts
3
+ * every fetch call for tests.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * import { MockInferiorClient } from '@inferior-ai/sdk/testing'
8
+ *
9
+ * test('my search wrapper', async () => {
10
+ * const client = new MockInferiorClient()
11
+ * client.mockSearch({
12
+ * results: [{ id: 'exp_abc', title: 'Stripe webhook fix' } as any],
13
+ * })
14
+ *
15
+ * // Code under test:
16
+ * const response = await client.search('stripe')
17
+ * expect(response.results[0].title).toBe('Stripe webhook fix')
18
+ *
19
+ * // Inspect what your code actually called:
20
+ * expect(client.calls).toHaveLength(1)
21
+ * expect(client.calls[0].url).toContain('/v1/experiences/search')
22
+ * expect(client.calls[0].headers.get('user-agent')).toMatch(/^inferior-sdk-ts/)
23
+ * })
24
+ * ```
25
+ *
26
+ * Stubs are FIFO. Default response when no stub matches is a 404
27
+ * with a descriptive body — surfaces unexpected calls fast.
28
+ */
29
+ import { InferiorClient } from "../client.js";
30
+ export class MockInferiorClient extends InferiorClient {
31
+ stubs = [];
32
+ /** Every intercepted request — assert against this in your tests. */
33
+ calls = [];
34
+ constructor(options = {}) {
35
+ super({
36
+ apiKey: options.apiKey ?? "cw_full_test_mock_key",
37
+ baseUrl: options.baseUrl ?? "https://api.inferior.ai",
38
+ retry: false,
39
+ // Override the global fetch with our recording dispatcher.
40
+ fetch: undefined, // overridden below
41
+ });
42
+ // Replace the fetcher after super() so `this` is bound correctly.
43
+ this.fetcher = this.dispatch.bind(this);
44
+ }
45
+ // ── Recording ────────────────────────────────────────────────────
46
+ async dispatch(input, init) {
47
+ const url = typeof input === "string"
48
+ ? input
49
+ : input instanceof URL
50
+ ? input.toString()
51
+ : input.url;
52
+ const method = (init?.method ?? "GET").toUpperCase();
53
+ const headers = new Headers(init?.headers ?? {});
54
+ const body = typeof init?.body === "string" ? init.body : "";
55
+ let bodyJson = null;
56
+ if (body) {
57
+ try {
58
+ bodyJson = JSON.parse(body);
59
+ }
60
+ catch {
61
+ bodyJson = null;
62
+ }
63
+ }
64
+ this.calls.push({ method, url, headers, body, bodyJson });
65
+ for (let i = 0; i < this.stubs.length; i++) {
66
+ const stub = this.stubs[i];
67
+ if (stub.matcher(url, init ?? {})) {
68
+ this.stubs.splice(i, 1);
69
+ return stub.response();
70
+ }
71
+ }
72
+ return this.unmatched(method, url);
73
+ }
74
+ unmatched(method, url) {
75
+ const path = (() => {
76
+ try {
77
+ return new URL(url).pathname;
78
+ }
79
+ catch {
80
+ return url;
81
+ }
82
+ })();
83
+ return new Response(JSON.stringify({
84
+ error: "MockInferiorClientUnmatchedRequest",
85
+ message: `No stub matched ${method} ${path}. Use addResponse() / mockSearch() / ` +
86
+ `mockDeposit() / mockDepositRaw() / mockFeedback() / mockError() to register a response.`,
87
+ details: { method, path },
88
+ }), {
89
+ status: 404,
90
+ headers: {
91
+ "Content-Type": "application/json",
92
+ "X-Request-ID": "req_mock_unmatched",
93
+ },
94
+ });
95
+ }
96
+ // ── Stub registration — generic ──────────────────────────────────
97
+ /**
98
+ * Queue a single response for the next matching request.
99
+ * Returns `this` for chaining.
100
+ */
101
+ addResponse(method, pathSubstring, options = {}) {
102
+ const { status = 200, jsonBody, textBody, headers = {}, description = "" } = options;
103
+ const matcher = (url, init) => {
104
+ const m = (init.method ?? "GET").toUpperCase();
105
+ return m === method.toUpperCase() && url.includes(pathSubstring);
106
+ };
107
+ const responseHeaders = {
108
+ "X-Request-ID": "req_mock_test",
109
+ ...headers,
110
+ };
111
+ let body;
112
+ if (jsonBody !== undefined) {
113
+ body = JSON.stringify(jsonBody);
114
+ responseHeaders["Content-Type"] = responseHeaders["Content-Type"] ?? "application/json";
115
+ }
116
+ else if (textBody !== undefined) {
117
+ body = textBody;
118
+ }
119
+ else {
120
+ body = null;
121
+ }
122
+ // Wrap in a factory so each replay produces a fresh Response (Response
123
+ // bodies are single-read; without the factory, retries would 500).
124
+ this.stubs.push({
125
+ matcher,
126
+ response: () => new Response(body, { status, headers: responseHeaders }),
127
+ description,
128
+ });
129
+ return this;
130
+ }
131
+ // ── Convenience helpers ──────────────────────────────────────────
132
+ mockSearch(opts = {}) {
133
+ const results = opts.results ?? [];
134
+ return this.addResponse("GET", "/v1/experiences/search", {
135
+ jsonBody: {
136
+ results,
137
+ total_results: opts.total ?? results.length,
138
+ metadata: { cached: opts.cached ?? false, channels_used: ["semantic"] },
139
+ schema_version: "2.0.0",
140
+ },
141
+ description: `search(${results.length} results)`,
142
+ });
143
+ }
144
+ mockDeposit(opts = {}) {
145
+ const { id = "exp_test_mock", status = "accepted", qualityScore = 0.75, validationState = "verified", } = opts;
146
+ return this.addResponse("POST", "/v1/experiences", {
147
+ status: 201,
148
+ jsonBody: {
149
+ id,
150
+ status,
151
+ quality_score: qualityScore,
152
+ trust_visibility: "searchable",
153
+ validation_state: validationState,
154
+ schema_version: "2.0.0",
155
+ },
156
+ description: `deposit(${id})`,
157
+ });
158
+ }
159
+ mockDepositRaw(opts = {}) {
160
+ const rawDepositId = opts.rawDepositId ?? "raw_test_mock";
161
+ return this.addResponse("POST", "/v1/experiences/raw", {
162
+ status: 202,
163
+ jsonBody: {
164
+ raw_deposit_id: rawDepositId,
165
+ status: "accepted",
166
+ normalization_status: "queued",
167
+ },
168
+ description: `depositRaw(${rawDepositId})`,
169
+ });
170
+ }
171
+ mockFeedback(opts = {}) {
172
+ const feedbackId = opts.feedbackId ?? "fbk_test_mock";
173
+ const experienceId = opts.experienceId ?? "exp_test_mock";
174
+ return this.addResponse("POST", "/feedback", {
175
+ status: 201,
176
+ jsonBody: {
177
+ feedback_id: feedbackId,
178
+ experience_id: experienceId,
179
+ updated_scores: { lower: 0.5, center: 0.7, upper: 0.9, n: 1 },
180
+ validity_update: {},
181
+ },
182
+ description: `feedback(${feedbackId})`,
183
+ });
184
+ }
185
+ /**
186
+ * Stub the next matching call with an error response.
187
+ * The SDK's `raiseForStatus` will raise the corresponding typed
188
+ * exception (`ServerError`, `RateLimitError`, etc.) with `requestId`
189
+ * populated from the mock's `X-Request-ID`.
190
+ */
191
+ mockError(method, pathSubstring, opts) {
192
+ const { status, error = "ServerError", message = "Mocked error", details = {} } = opts;
193
+ return this.addResponse(method, pathSubstring, {
194
+ status,
195
+ jsonBody: { error, message, details },
196
+ description: `error(${status} ${error})`,
197
+ });
198
+ }
199
+ // ── Inspection ───────────────────────────────────────────────────
200
+ /** Clear all queued stubs and recorded calls. */
201
+ reset() {
202
+ this.stubs.length = 0;
203
+ this.calls.length = 0;
204
+ return this;
205
+ }
206
+ /**
207
+ * Throw if any registered stub never fired. Useful at test
208
+ * teardown to catch over-stubbing.
209
+ */
210
+ assertNoUnmatched() {
211
+ if (this.stubs.length > 0) {
212
+ const labels = this.stubs.map((s) => s.description || "(unlabelled)").join(", ");
213
+ throw new Error(`${this.stubs.length} unfired stubs: ${labels}`);
214
+ }
215
+ }
216
+ }
217
+ //# sourceMappingURL=mock-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-client.js","sourceRoot":"","sources":["../../src/testing/mock-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Webhook signature verification helper.
3
+ *
4
+ * Backend signs every webhook delivery with HMAC-SHA256 over the raw
5
+ * body and sends the signature as `X-Inferior-Signature: sha256=<hex>`.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { verify, InvalidSignatureError } from '@inferior-ai/sdk'
10
+ *
11
+ * app.post('/inferior-webhook', async (req, res) => {
12
+ * const body = req.rawBody as Buffer | string // NOT the parsed JSON
13
+ * const sig = req.headers['x-inferior-signature'] as string
14
+ * try {
15
+ * const event = verify(body, sig, 'whsec_...')
16
+ * // handle event …
17
+ * res.status(200).end()
18
+ * } catch (e) {
19
+ * if (e instanceof InvalidSignatureError) {
20
+ * res.status(400).end()
21
+ * } else {
22
+ * throw e
23
+ * }
24
+ * }
25
+ * })
26
+ * ```
27
+ *
28
+ * The helper validates both signature equality (constant-time compare)
29
+ * and event freshness (default 5-minute tolerance) — the latter
30
+ * prevents replay attacks if a delivery is captured and re-sent.
31
+ */
32
+ export declare class InvalidSignatureError extends Error {
33
+ constructor(message: string);
34
+ }
35
+ export interface VerifyOptions {
36
+ /** Max event age in seconds. Default 300 (5min). 0 disables freshness check. */
37
+ tolerance?: number;
38
+ /** Override "now" for testing (unix seconds). */
39
+ deliveryTimestamp?: number;
40
+ }
41
+ /**
42
+ * Verify a webhook signature and return the parsed event payload.
43
+ *
44
+ * @param body Raw request body (`Buffer` or `string`). Do NOT pass the parsed JSON.
45
+ * @param signatureHeader Value of the `X-Inferior-Signature` header (`sha256=<hex>`).
46
+ * @param secret Webhook signing secret (from the admin UI).
47
+ * @param options Optional `tolerance` (default 300s) and `deliveryTimestamp`.
48
+ * @returns The parsed JSON event.
49
+ * @throws InvalidSignatureError on missing/malformed header, signature mismatch, or stale timestamp.
50
+ */
51
+ export declare function verify(body: Buffer | string, signatureHeader: string | undefined | null, secret: string, options?: VerifyOptions): Record<string, unknown>;
52
+ //# sourceMappingURL=webhooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CACpB,IAAI,EAAE,MAAM,GAAG,MAAM,EACrB,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAC1C,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,aAAkB,GAC1B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA+CzB"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Webhook signature verification helper.
3
+ *
4
+ * Backend signs every webhook delivery with HMAC-SHA256 over the raw
5
+ * body and sends the signature as `X-Inferior-Signature: sha256=<hex>`.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { verify, InvalidSignatureError } from '@inferior-ai/sdk'
10
+ *
11
+ * app.post('/inferior-webhook', async (req, res) => {
12
+ * const body = req.rawBody as Buffer | string // NOT the parsed JSON
13
+ * const sig = req.headers['x-inferior-signature'] as string
14
+ * try {
15
+ * const event = verify(body, sig, 'whsec_...')
16
+ * // handle event …
17
+ * res.status(200).end()
18
+ * } catch (e) {
19
+ * if (e instanceof InvalidSignatureError) {
20
+ * res.status(400).end()
21
+ * } else {
22
+ * throw e
23
+ * }
24
+ * }
25
+ * })
26
+ * ```
27
+ *
28
+ * The helper validates both signature equality (constant-time compare)
29
+ * and event freshness (default 5-minute tolerance) — the latter
30
+ * prevents replay attacks if a delivery is captured and re-sent.
31
+ */
32
+ import { createHmac, timingSafeEqual } from "node:crypto";
33
+ export class InvalidSignatureError extends Error {
34
+ constructor(message) {
35
+ super(message);
36
+ this.name = "InvalidSignatureError";
37
+ }
38
+ }
39
+ /**
40
+ * Verify a webhook signature and return the parsed event payload.
41
+ *
42
+ * @param body Raw request body (`Buffer` or `string`). Do NOT pass the parsed JSON.
43
+ * @param signatureHeader Value of the `X-Inferior-Signature` header (`sha256=<hex>`).
44
+ * @param secret Webhook signing secret (from the admin UI).
45
+ * @param options Optional `tolerance` (default 300s) and `deliveryTimestamp`.
46
+ * @returns The parsed JSON event.
47
+ * @throws InvalidSignatureError on missing/malformed header, signature mismatch, or stale timestamp.
48
+ */
49
+ export function verify(body, signatureHeader, secret, options = {}) {
50
+ const { tolerance = 300, deliveryTimestamp } = options;
51
+ if (!signatureHeader)
52
+ throw new InvalidSignatureError("missing signature header");
53
+ if (!signatureHeader.includes("=")) {
54
+ throw new InvalidSignatureError("malformed signature header (expected 'sha256=<hex>')");
55
+ }
56
+ const [scheme, sigHex] = signatureHeader.split("=", 2);
57
+ if (scheme !== "sha256") {
58
+ throw new InvalidSignatureError(`unsupported signature scheme: ${scheme}`);
59
+ }
60
+ const rawBody = typeof body === "string" ? Buffer.from(body, "utf-8") : body;
61
+ const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
62
+ if (expected.length !== sigHex.length) {
63
+ throw new InvalidSignatureError("signature mismatch");
64
+ }
65
+ if (!timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(sigHex, "hex"))) {
66
+ throw new InvalidSignatureError("signature mismatch");
67
+ }
68
+ let event;
69
+ try {
70
+ event = JSON.parse(rawBody.toString("utf-8"));
71
+ }
72
+ catch (e) {
73
+ throw new InvalidSignatureError(`event body is not valid JSON: ${e.message}`);
74
+ }
75
+ if (tolerance > 0) {
76
+ const ts = (event.ts ?? event.timestamp);
77
+ if (ts !== undefined && ts !== null) {
78
+ const eventTime = Number(ts);
79
+ if (!Number.isFinite(eventTime)) {
80
+ throw new InvalidSignatureError(`event timestamp not parseable: ${String(ts)}`);
81
+ }
82
+ const now = deliveryTimestamp ?? Math.floor(Date.now() / 1000);
83
+ if (Math.abs(now - eventTime) > tolerance) {
84
+ throw new InvalidSignatureError(`event timestamp outside tolerance (${Math.abs(now - eventTime)}s > ${tolerance}s)`);
85
+ }
86
+ }
87
+ }
88
+ return event;
89
+ }
90
+ //# sourceMappingURL=webhooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"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"}