@intelmesh/sdk 0.1.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/.github/scripts/compute-disttag.sh +47 -0
- package/.github/workflows/release.yml +206 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +2 -0
- package/.prettierrc +8 -0
- package/CLAUDE.md +37 -0
- package/LICENSE +21 -0
- package/commitlint.config.cjs +3 -0
- package/dist/index.d.ts +1293 -0
- package/dist/index.js +1651 -0
- package/docs/superpowers/plans/2026-04-10-release-pipeline.md +798 -0
- package/docs/superpowers/specs/2026-04-10-release-pipeline-design.md +309 -0
- package/eslint.config.mjs +38 -0
- package/package.json +72 -0
- package/src/builders/event.ts +72 -0
- package/src/builders/rule.ts +143 -0
- package/src/client/errors.ts +171 -0
- package/src/client/http.ts +209 -0
- package/src/client/intelmesh.ts +57 -0
- package/src/client/pagination.ts +50 -0
- package/src/generated/types.ts +11 -0
- package/src/index.ts +106 -0
- package/src/provision/index.ts +6 -0
- package/src/provision/provisioner.ts +326 -0
- package/src/provision/rule-builder.ts +193 -0
- package/src/resources/apikeys.ts +63 -0
- package/src/resources/audit.ts +29 -0
- package/src/resources/evaluations.ts +38 -0
- package/src/resources/events.ts +61 -0
- package/src/resources/lists.ts +91 -0
- package/src/resources/phases.ts +71 -0
- package/src/resources/rules.ts +98 -0
- package/src/resources/scopes.ts +71 -0
- package/src/resources/scores.ts +63 -0
- package/src/testkit/assertion.ts +76 -0
- package/src/testkit/harness.ts +252 -0
- package/src/testkit/index.ts +7 -0
- package/src/types.ts +330 -0
- package/tests/client/errors.test.ts +159 -0
- package/tests/provision/provisioner.test.ts +311 -0
- package/tests/scripts/compute-disttag.test.ts +178 -0
- package/tests/testkit/harness.test.ts +291 -0
- package/tsconfig.eslint.json +8 -0
- package/tsconfig.json +29 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Harness unit tests — mock fetch, no network calls
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { Harness, withHarness } from '../../src/testkit/harness.js';
|
|
7
|
+
import { Provisioner } from '../../src/provision/provisioner.js';
|
|
8
|
+
|
|
9
|
+
/** Tracks API key lifecycle counts. */
|
|
10
|
+
interface MockState {
|
|
11
|
+
keysCreated: number;
|
|
12
|
+
keysDeleted: number;
|
|
13
|
+
eventsIngested: number;
|
|
14
|
+
idSeq: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolves the URL string from a fetch input.
|
|
19
|
+
* @param input
|
|
20
|
+
*/
|
|
21
|
+
function resolveUrl(input: string | URL | Request): string {
|
|
22
|
+
if (typeof input === 'string') return input;
|
|
23
|
+
if (input instanceof URL) return input.toString();
|
|
24
|
+
return input.url;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Handles POST requests in the mock fetch.
|
|
29
|
+
* @param url
|
|
30
|
+
* @param state
|
|
31
|
+
*/
|
|
32
|
+
// eslint-disable-next-line max-lines-per-function -- each branch is a simple JSON fixture; splitting further would obscure test data
|
|
33
|
+
function handlePost(url: string, state: MockState): Response | null {
|
|
34
|
+
if (url.includes('/api/v1/api-keys')) {
|
|
35
|
+
state.keysCreated++;
|
|
36
|
+
state.idSeq++;
|
|
37
|
+
return jsonResponse({
|
|
38
|
+
data: {
|
|
39
|
+
id: `key-${String(state.idSeq)}`,
|
|
40
|
+
name: 'testkit',
|
|
41
|
+
key: 'sk-test-ephemeral',
|
|
42
|
+
enabled: true,
|
|
43
|
+
permissions: [
|
|
44
|
+
'events:write',
|
|
45
|
+
'events:simulate',
|
|
46
|
+
'rules:read',
|
|
47
|
+
'rules:write',
|
|
48
|
+
'scopes:read',
|
|
49
|
+
'scopes:write',
|
|
50
|
+
'lists:read',
|
|
51
|
+
'lists:write',
|
|
52
|
+
'scores:read',
|
|
53
|
+
'scores:write',
|
|
54
|
+
'api_keys:manage',
|
|
55
|
+
'evaluations:read',
|
|
56
|
+
'audit:read',
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (url.includes('/api/v1/events/ingest')) {
|
|
62
|
+
state.eventsIngested++;
|
|
63
|
+
return jsonResponse({
|
|
64
|
+
data: {
|
|
65
|
+
event_id: `evt-${String(state.eventsIngested)}`,
|
|
66
|
+
decision: { action: 'block', severity: 'critical' },
|
|
67
|
+
transient_score: 42,
|
|
68
|
+
duration_ms: 5,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (url.includes('/api/v1/events/simulate')) {
|
|
73
|
+
return jsonResponse({
|
|
74
|
+
data: {
|
|
75
|
+
event_id: 'evt-sim-1',
|
|
76
|
+
decision: { action: '', severity: '' },
|
|
77
|
+
transient_score: 0,
|
|
78
|
+
duration_ms: 3,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (url.includes('/api/v1/phases')) {
|
|
83
|
+
state.idSeq++;
|
|
84
|
+
return jsonResponse({
|
|
85
|
+
data: {
|
|
86
|
+
id: `phase-${String(state.idSeq)}`,
|
|
87
|
+
name: 'p',
|
|
88
|
+
position: 1,
|
|
89
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (url.includes('/api/v1/rules')) {
|
|
94
|
+
state.idSeq++;
|
|
95
|
+
return jsonResponse({
|
|
96
|
+
data: {
|
|
97
|
+
id: `rule-${String(state.idSeq)}`,
|
|
98
|
+
name: 'r',
|
|
99
|
+
phase_id: 'p1',
|
|
100
|
+
priority: 1,
|
|
101
|
+
expression: 'true',
|
|
102
|
+
applicable_when: '',
|
|
103
|
+
actions: {},
|
|
104
|
+
enabled: true,
|
|
105
|
+
dry_run: false,
|
|
106
|
+
current_version_id: 'v1',
|
|
107
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
108
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creates a mock fetch for harness tests.
|
|
117
|
+
* @param state
|
|
118
|
+
*/
|
|
119
|
+
function createMockFetch(state: MockState): typeof globalThis.fetch {
|
|
120
|
+
return vi.fn((input: string | URL | Request, init?: RequestInit) => {
|
|
121
|
+
const url = resolveUrl(input);
|
|
122
|
+
const method = init?.method ?? 'GET';
|
|
123
|
+
|
|
124
|
+
if (method === 'POST') {
|
|
125
|
+
const res = handlePost(url, state);
|
|
126
|
+
if (res) return Promise.resolve(res);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (method === 'DELETE') {
|
|
130
|
+
state.keysDeleted += url.includes('/api/v1/api-keys/') ? 1 : 0;
|
|
131
|
+
return Promise.resolve(jsonResponse({ data: null }));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (method === 'GET' && url.includes('/api/v1/lists/')) {
|
|
135
|
+
return Promise.resolve(
|
|
136
|
+
jsonResponse({
|
|
137
|
+
data: {
|
|
138
|
+
id: 'list-1',
|
|
139
|
+
name: 'test-list',
|
|
140
|
+
description: '',
|
|
141
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
142
|
+
updated_at: '2025-01-01T00:00:00Z',
|
|
143
|
+
},
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return Promise.resolve(new Response('Not Found', { status: 404 }));
|
|
149
|
+
}) as typeof globalThis.fetch;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Creates a JSON Response.
|
|
154
|
+
* @param body
|
|
155
|
+
*/
|
|
156
|
+
function jsonResponse(body: unknown): Response {
|
|
157
|
+
return new Response(JSON.stringify(body), {
|
|
158
|
+
status: 200,
|
|
159
|
+
headers: { 'Content-Type': 'application/json' },
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// eslint-disable-next-line max-lines-per-function -- describe block spans multiple related test cases; splitting would fragment cohesive coverage
|
|
164
|
+
describe('Harness', () => {
|
|
165
|
+
it('creates and deletes ephemeral API key', async () => {
|
|
166
|
+
const state: MockState = { keysCreated: 0, keysDeleted: 0, eventsIngested: 0, idSeq: 0 };
|
|
167
|
+
const h = createHarnessWithMockFetch(state);
|
|
168
|
+
await h.setup();
|
|
169
|
+
|
|
170
|
+
expect(state.keysCreated).toBe(1);
|
|
171
|
+
expect(h.client()).toBeTruthy();
|
|
172
|
+
|
|
173
|
+
await h.cleanup();
|
|
174
|
+
expect(state.keysDeleted).toBe(1);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('throws when client() called before setup()', () => {
|
|
178
|
+
const state: MockState = { keysCreated: 0, keysDeleted: 0, eventsIngested: 0, idSeq: 0 };
|
|
179
|
+
const h = createHarnessWithMockFetch(state);
|
|
180
|
+
expect(() => h.client()).toThrow('call setup() first');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('sends events and returns assertions', async () => {
|
|
184
|
+
const state: MockState = { keysCreated: 0, keysDeleted: 0, eventsIngested: 0, idSeq: 0 };
|
|
185
|
+
const h = createHarnessWithMockFetch(state);
|
|
186
|
+
await h.setup();
|
|
187
|
+
|
|
188
|
+
const assertion = await h.send('transaction.pix', { amount: 5000 });
|
|
189
|
+
assertion.expectDecision('block', 'critical');
|
|
190
|
+
assertion.expectScore(42);
|
|
191
|
+
|
|
192
|
+
expect(state.eventsIngested).toBe(1);
|
|
193
|
+
await h.cleanup();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('expectNoDecision works for empty decisions', async () => {
|
|
197
|
+
const state: MockState = { keysCreated: 0, keysDeleted: 0, eventsIngested: 0, idSeq: 0 };
|
|
198
|
+
const h = createHarnessWithMockFetch(state);
|
|
199
|
+
await h.setup();
|
|
200
|
+
|
|
201
|
+
const assertion = await h.sendSimulate('login.success', { user: 'test' });
|
|
202
|
+
assertion.expectNoDecision();
|
|
203
|
+
assertion.expectScore(0);
|
|
204
|
+
|
|
205
|
+
await h.cleanup();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('expectDecision throws on mismatch', async () => {
|
|
209
|
+
const state: MockState = { keysCreated: 0, keysDeleted: 0, eventsIngested: 0, idSeq: 0 };
|
|
210
|
+
const h = createHarnessWithMockFetch(state);
|
|
211
|
+
await h.setup();
|
|
212
|
+
|
|
213
|
+
const assertion = await h.send('transaction.pix', { amount: 5000 });
|
|
214
|
+
expect(() => assertion.expectDecision('allow', 'low')).toThrow('decision action');
|
|
215
|
+
|
|
216
|
+
await h.cleanup();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('expectScore throws on mismatch', async () => {
|
|
220
|
+
const state: MockState = { keysCreated: 0, keysDeleted: 0, eventsIngested: 0, idSeq: 0 };
|
|
221
|
+
const h = createHarnessWithMockFetch(state);
|
|
222
|
+
await h.setup();
|
|
223
|
+
|
|
224
|
+
const assertion = await h.send('transaction.pix', { amount: 5000 });
|
|
225
|
+
expect(() => assertion.expectScore(99)).toThrow('score');
|
|
226
|
+
|
|
227
|
+
await h.cleanup();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('withHarness', () => {
|
|
232
|
+
it('runs setup and cleanup automatically', async () => {
|
|
233
|
+
await withHarness(
|
|
234
|
+
{
|
|
235
|
+
baseURL: 'http://localhost:8080',
|
|
236
|
+
adminKey: 'admin-key',
|
|
237
|
+
},
|
|
238
|
+
async (_harness) => {
|
|
239
|
+
// The withHarness function uses the real IntelMesh constructor,
|
|
240
|
+
// so we cannot inject mock fetch here without refactoring.
|
|
241
|
+
// This test validates the wrapper pattern compiles correctly.
|
|
242
|
+
},
|
|
243
|
+
).catch(() => {
|
|
244
|
+
// Expected: real fetch will fail in test environment.
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('Harness with Provisioner', () => {
|
|
250
|
+
it('provisions and tears down resources', async () => {
|
|
251
|
+
const state: MockState = { keysCreated: 0, keysDeleted: 0, eventsIngested: 0, idSeq: 0 };
|
|
252
|
+
const h = createHarnessWithMockFetch(state);
|
|
253
|
+
await h.setup();
|
|
254
|
+
|
|
255
|
+
const client = h.client();
|
|
256
|
+
const p = new Provisioner(client)
|
|
257
|
+
.phase('screening', 1)
|
|
258
|
+
.rule('block-rule')
|
|
259
|
+
.inPhase('screening')
|
|
260
|
+
.priority(1)
|
|
261
|
+
.when('true')
|
|
262
|
+
.decide('block', 'critical')
|
|
263
|
+
.halt()
|
|
264
|
+
.done();
|
|
265
|
+
|
|
266
|
+
await h.provision(p);
|
|
267
|
+
|
|
268
|
+
expect(p.phaseId('screening')).toBeTruthy();
|
|
269
|
+
expect(p.ruleId('block-rule')).toBeTruthy();
|
|
270
|
+
|
|
271
|
+
await h.cleanup();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Helper to create a Harness backed by mock fetch.
|
|
277
|
+
* We use the IntelMesh constructor's custom fetch option.
|
|
278
|
+
* @param state
|
|
279
|
+
*/
|
|
280
|
+
function createHarnessWithMockFetch(state: MockState): Harness {
|
|
281
|
+
const mockFetch = createMockFetch(state);
|
|
282
|
+
|
|
283
|
+
// Patch globalThis.fetch so the Harness (which creates IntelMesh clients
|
|
284
|
+
// internally without a custom fetch option) uses the mock.
|
|
285
|
+
globalThis.fetch = mockFetch;
|
|
286
|
+
|
|
287
|
+
return new Harness({
|
|
288
|
+
baseURL: 'http://localhost:8080',
|
|
289
|
+
adminKey: 'admin-key',
|
|
290
|
+
});
|
|
291
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"ignoreDeprecations": "6.0",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"module": "ES2022",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"lib": ["ES2022", "DOM"],
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"rootDir": "src",
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"declarationMap": true,
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"noUncheckedIndexedAccess": true,
|
|
15
|
+
"noImplicitOverride": true,
|
|
16
|
+
"noImplicitReturns": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"exactOptionalPropertyTypes": false,
|
|
21
|
+
"forceConsistentCasingInFileNames": true,
|
|
22
|
+
"esModuleInterop": true,
|
|
23
|
+
"skipLibCheck": true,
|
|
24
|
+
"isolatedModules": true,
|
|
25
|
+
"verbatimModuleSyntax": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src"],
|
|
28
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
29
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: false,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
include: ['tests/**/*.test.ts'],
|
|
8
|
+
coverage: {
|
|
9
|
+
provider: 'v8',
|
|
10
|
+
include: ['src/**/*.ts'],
|
|
11
|
+
exclude: ['src/generated/**'],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
});
|