@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.
Files changed (45) hide show
  1. package/.github/scripts/compute-disttag.sh +47 -0
  2. package/.github/workflows/release.yml +206 -0
  3. package/.husky/commit-msg +1 -0
  4. package/.husky/pre-commit +2 -0
  5. package/.prettierrc +8 -0
  6. package/CLAUDE.md +37 -0
  7. package/LICENSE +21 -0
  8. package/commitlint.config.cjs +3 -0
  9. package/dist/index.d.ts +1293 -0
  10. package/dist/index.js +1651 -0
  11. package/docs/superpowers/plans/2026-04-10-release-pipeline.md +798 -0
  12. package/docs/superpowers/specs/2026-04-10-release-pipeline-design.md +309 -0
  13. package/eslint.config.mjs +38 -0
  14. package/package.json +72 -0
  15. package/src/builders/event.ts +72 -0
  16. package/src/builders/rule.ts +143 -0
  17. package/src/client/errors.ts +171 -0
  18. package/src/client/http.ts +209 -0
  19. package/src/client/intelmesh.ts +57 -0
  20. package/src/client/pagination.ts +50 -0
  21. package/src/generated/types.ts +11 -0
  22. package/src/index.ts +106 -0
  23. package/src/provision/index.ts +6 -0
  24. package/src/provision/provisioner.ts +326 -0
  25. package/src/provision/rule-builder.ts +193 -0
  26. package/src/resources/apikeys.ts +63 -0
  27. package/src/resources/audit.ts +29 -0
  28. package/src/resources/evaluations.ts +38 -0
  29. package/src/resources/events.ts +61 -0
  30. package/src/resources/lists.ts +91 -0
  31. package/src/resources/phases.ts +71 -0
  32. package/src/resources/rules.ts +98 -0
  33. package/src/resources/scopes.ts +71 -0
  34. package/src/resources/scores.ts +63 -0
  35. package/src/testkit/assertion.ts +76 -0
  36. package/src/testkit/harness.ts +252 -0
  37. package/src/testkit/index.ts +7 -0
  38. package/src/types.ts +330 -0
  39. package/tests/client/errors.test.ts +159 -0
  40. package/tests/provision/provisioner.test.ts +311 -0
  41. package/tests/scripts/compute-disttag.test.ts +178 -0
  42. package/tests/testkit/harness.test.ts +291 -0
  43. package/tsconfig.eslint.json +8 -0
  44. package/tsconfig.json +29 -0
  45. 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
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": ["node"]
5
+ },
6
+ "include": ["src", "tests"],
7
+ "exclude": ["node_modules", "dist"]
8
+ }
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
+ }
@@ -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
+ });