@oculisecurity/cli 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 (85) hide show
  1. package/LICENSE.txt +201 -0
  2. package/README.md +67 -0
  3. package/dist/cli.d.ts +18 -0
  4. package/dist/cli.js +565 -0
  5. package/dist/commands/init.d.ts +14 -0
  6. package/dist/commands/init.js +135 -0
  7. package/dist/commands/report.d.ts +33 -0
  8. package/dist/commands/report.js +145 -0
  9. package/dist/commands/serve.d.ts +27 -0
  10. package/dist/commands/serve.js +163 -0
  11. package/dist/commands/tail.d.ts +7 -0
  12. package/dist/commands/tail.js +211 -0
  13. package/dist/commands/uninstall.d.ts +13 -0
  14. package/dist/commands/uninstall.js +111 -0
  15. package/dist/config.d.ts +17 -0
  16. package/dist/config.js +90 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +35 -0
  19. package/dist/init.d.ts +9 -0
  20. package/dist/init.js +50 -0
  21. package/dist/install/claude-code.d.ts +13 -0
  22. package/dist/install/claude-code.js +118 -0
  23. package/dist/install/cursor.d.ts +13 -0
  24. package/dist/install/cursor.js +119 -0
  25. package/dist/install/detect.d.ts +5 -0
  26. package/dist/install/detect.js +64 -0
  27. package/dist/middleware/auth.d.ts +15 -0
  28. package/dist/middleware/auth.js +116 -0
  29. package/dist/routes/adapters/claude-code.d.ts +38 -0
  30. package/dist/routes/adapters/claude-code.js +125 -0
  31. package/dist/routes/adapters/cursor.d.ts +21 -0
  32. package/dist/routes/adapters/cursor.js +139 -0
  33. package/dist/routes/adapters/index.d.ts +16 -0
  34. package/dist/routes/adapters/index.js +56 -0
  35. package/dist/routes/adapters/router.d.ts +31 -0
  36. package/dist/routes/adapters/router.js +97 -0
  37. package/dist/routes/adapters/schema.d.ts +141 -0
  38. package/dist/routes/adapters/schema.js +83 -0
  39. package/dist/routes/adapters/windsurf.d.ts +6 -0
  40. package/dist/routes/adapters/windsurf.js +48 -0
  41. package/dist/routes/admin.d.ts +15 -0
  42. package/dist/routes/admin.js +399 -0
  43. package/dist/routes/call.d.ts +13 -0
  44. package/dist/routes/call.js +68 -0
  45. package/dist/routes/events.d.ts +7 -0
  46. package/dist/routes/events.js +125 -0
  47. package/dist/routes/health.d.ts +2 -0
  48. package/dist/routes/health.js +12 -0
  49. package/dist/routes/hooks.d.ts +11 -0
  50. package/dist/routes/hooks.js +166 -0
  51. package/dist/routes/mcp.d.ts +10 -0
  52. package/dist/routes/mcp.js +170 -0
  53. package/dist/routes/openai-tools.d.ts +9 -0
  54. package/dist/routes/openai-tools.js +121 -0
  55. package/dist/server.d.ts +11 -0
  56. package/dist/server.js +118 -0
  57. package/dist/services/audit.d.ts +92 -0
  58. package/dist/services/audit.js +388 -0
  59. package/dist/services/data-dir.d.ts +7 -0
  60. package/dist/services/data-dir.js +61 -0
  61. package/dist/services/local-policy-templates.d.ts +9 -0
  62. package/dist/services/local-policy-templates.js +47 -0
  63. package/dist/services/local-policy.d.ts +39 -0
  64. package/dist/services/local-policy.js +172 -0
  65. package/dist/services/policy-store.d.ts +82 -0
  66. package/dist/services/policy-store.js +331 -0
  67. package/dist/services/policy.d.ts +8 -0
  68. package/dist/services/policy.js +126 -0
  69. package/dist/services/ratelimit.d.ts +26 -0
  70. package/dist/services/ratelimit.js +60 -0
  71. package/dist/services/sanitizer.d.ts +9 -0
  72. package/dist/services/sanitizer.js +73 -0
  73. package/dist/services/sqlite-loader.d.ts +4 -0
  74. package/dist/services/sqlite-loader.js +16 -0
  75. package/dist/services/telemetry-log.d.ts +76 -0
  76. package/dist/services/telemetry-log.js +260 -0
  77. package/dist/services/tool-executor.d.ts +46 -0
  78. package/dist/services/tool-executor.js +167 -0
  79. package/dist/services/upstream.d.ts +18 -0
  80. package/dist/services/upstream.js +72 -0
  81. package/dist/types.d.ts +112 -0
  82. package/dist/types.js +3 -0
  83. package/package.json +72 -0
  84. package/public/favicon.svg +4 -0
  85. package/public/index.html +3893 -0
@@ -0,0 +1,399 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerAdminRoutes = registerAdminRoutes;
4
+ const local_policy_1 = require("../services/local-policy");
5
+ function registerAdminRoutes(app, ctx) {
6
+ // GET /admin/search — unified search over audit records (unauthenticated — demo only)
7
+ app.get('/api/admin/search', async (request, reply) => {
8
+ const limit = Math.min(parseInt(request.query.limit ?? '50', 10) || 50, 500);
9
+ const offset = parseInt(request.query.offset ?? '0', 10) || 0;
10
+ const result = ctx.audit.search({
11
+ q: request.query.q,
12
+ decision: request.query.decision,
13
+ from: request.query.from,
14
+ to: request.query.to,
15
+ limit,
16
+ offset,
17
+ });
18
+ reply.send(result);
19
+ });
20
+ // GET /admin/aggregate — server-side GROUP BY aggregation (unauthenticated — demo only)
21
+ app.get('/api/admin/aggregate', async (request, reply) => {
22
+ const groupByRaw = request.query.groupBy;
23
+ if (!groupByRaw || !groupByRaw.trim()) {
24
+ reply.code(400).send({ error: 'groupBy is required' });
25
+ return;
26
+ }
27
+ const groupBy = groupByRaw.split(',').map((s) => s.trim()).filter(Boolean);
28
+ if (groupBy.length === 0) {
29
+ reply.code(400).send({ error: 'groupBy is required' });
30
+ return;
31
+ }
32
+ const limit = Math.min(parseInt(request.query.limit ?? '100', 10) || 100, 500);
33
+ const result = ctx.audit.aggregate({
34
+ groupBy,
35
+ q: request.query.q,
36
+ decision: request.query.decision,
37
+ from: request.query.from,
38
+ to: request.query.to,
39
+ limit,
40
+ });
41
+ reply.send(result);
42
+ });
43
+ // GET /admin/logs — recent audit log entries (requires auth)
44
+ app.get('/api/admin/logs', async (request, reply) => {
45
+ // Auth check — require at least some valid actor (set by auth middleware)
46
+ if (!request.actor?.actor) {
47
+ reply.code(401).send({ error: 'Unauthorized' });
48
+ return;
49
+ }
50
+ const limit = Math.min(parseInt(request.query.limit ?? '50', 10), 500);
51
+ const offset = parseInt(request.query.offset ?? '0', 10);
52
+ const logs = ctx.audit.query(limit, offset);
53
+ const total = ctx.audit.count();
54
+ reply.send({ total, limit, offset, logs });
55
+ });
56
+ // GET /admin/upstreams — list registered upstreams (requires auth)
57
+ app.get('/api/admin/upstreams', async (request, reply) => {
58
+ if (!request.actor?.actor) {
59
+ reply.code(401).send({ error: 'Unauthorized' });
60
+ return;
61
+ }
62
+ const upstreams = ctx.upstream.listUpstreams().map((u) => ({
63
+ id: u.id,
64
+ name: u.name,
65
+ baseUrl: u.baseUrl,
66
+ timeout: u.timeout,
67
+ // never expose authHeader
68
+ }));
69
+ reply.send({ upstreams });
70
+ });
71
+ // -----------------------------------------------------------------------
72
+ // Dashboard stats endpoints (unauthenticated — demo only)
73
+ // -----------------------------------------------------------------------
74
+ app.get('/api/admin/stats/summary', async (request, reply) => {
75
+ const hours = Math.min(parseInt(request.query.hours ?? '24', 10) || 24, 720);
76
+ reply.send(ctx.audit.summary(hours));
77
+ });
78
+ app.get('/api/admin/stats/top-tools', async (request, reply) => {
79
+ const limit = Math.min(parseInt(request.query.limit ?? '5', 10) || 5, 50);
80
+ reply.send(ctx.audit.topTools(limit));
81
+ });
82
+ app.get('/api/admin/stats/top-denied', async (request, reply) => {
83
+ const limit = Math.min(parseInt(request.query.limit ?? '5', 10) || 5, 50);
84
+ reply.send(ctx.audit.topDeniedTools(limit));
85
+ });
86
+ app.get('/api/admin/stats/top-actors', async (request, reply) => {
87
+ const limit = Math.min(parseInt(request.query.limit ?? '5', 10) || 5, 50);
88
+ reply.send(ctx.audit.topActors(limit));
89
+ });
90
+ app.get('/api/admin/stats/decisions', async (_request, reply) => {
91
+ reply.send(ctx.audit.decisionBreakdown());
92
+ });
93
+ app.get('/api/admin/stats/timeseries', async (request, reply) => {
94
+ const hours = Math.min(parseInt(request.query.hours ?? '1', 10) || 1, 168);
95
+ const bucket = Math.min(parseInt(request.query.bucket ?? '5', 10) || 5, 60);
96
+ reply.send(ctx.audit.timeSeries(hours, bucket));
97
+ });
98
+ // -----------------------------------------------------------------------
99
+ // Settings (read-only view of runtime config)
100
+ // -----------------------------------------------------------------------
101
+ app.get('/api/admin/settings', async (_request, reply) => {
102
+ const c = ctx.config;
103
+ reply.send({
104
+ gateway: {
105
+ port: c.port,
106
+ logLevel: c.logLevel,
107
+ opaEnabled: c.opaEnabled,
108
+ opaUrl: c.opaEnabled ? c.opaUrl : null,
109
+ storeFullArgs: c.storeFullArgs,
110
+ dbPath: c.dbPath,
111
+ },
112
+ rateLimit: {
113
+ capacity: c.rateLimitCapacity,
114
+ refillPerSecond: c.rateLimitRefillPerSecond,
115
+ },
116
+ upstreams: c.upstreams.map((u) => ({
117
+ id: u.id,
118
+ name: u.name,
119
+ baseUrl: u.baseUrl,
120
+ timeout: u.timeout,
121
+ hasAuth: !!u.authHeader,
122
+ tools: u.tools ?? [],
123
+ })),
124
+ telemetry: {
125
+ endpointUrl: '/v1/events',
126
+ claudeCodeHook: {
127
+ type: 'http',
128
+ url: `http://<gateway-host>:${c.port}/v1/events`,
129
+ },
130
+ },
131
+ });
132
+ });
133
+ // -----------------------------------------------------------------------
134
+ // Policy management (unauthenticated — demo only)
135
+ // -----------------------------------------------------------------------
136
+ // POST /admin/policies/test — must be registered BEFORE :id param route
137
+ app.post('/api/admin/policies/test', async (request, reply) => {
138
+ const body = request.body;
139
+ if (!body.input || typeof body.input !== 'object') {
140
+ reply.code(400).send({ error: 'input object is required' });
141
+ return;
142
+ }
143
+ try {
144
+ const decision = await ctx.policy.evaluate(body.input);
145
+ reply.send({ decision });
146
+ }
147
+ catch (err) {
148
+ const msg = err instanceof Error ? err.message : String(err);
149
+ reply.code(500).send({ error: 'Policy evaluation failed', details: msg });
150
+ }
151
+ });
152
+ // GET /admin/policies — list all policies
153
+ app.get('/api/admin/policies', async (_request, reply) => {
154
+ const policies = ctx.policyStore.list();
155
+ reply.send({ policies });
156
+ });
157
+ // GET /admin/policies/:id — get single policy
158
+ app.get('/api/admin/policies/:id', async (request, reply) => {
159
+ const id = parseInt(request.params.id, 10);
160
+ if (isNaN(id)) {
161
+ reply.code(400).send({ error: 'Invalid policy id' });
162
+ return;
163
+ }
164
+ const policy = ctx.policyStore.getById(id);
165
+ if (!policy) {
166
+ reply.code(404).send({ error: 'Policy not found' });
167
+ return;
168
+ }
169
+ reply.send(policy);
170
+ });
171
+ // POST /admin/policies — create new policy
172
+ app.post('/api/admin/policies', async (request, reply) => {
173
+ const body = request.body;
174
+ const name = body.name;
175
+ const rego = body.rego;
176
+ if (!name || !rego) {
177
+ reply.code(400).send({ error: 'name and rego are required' });
178
+ return;
179
+ }
180
+ const created = ctx.policyStore.create({
181
+ name,
182
+ rego,
183
+ description: body.description ?? undefined,
184
+ version: body.version ?? undefined,
185
+ updatedBy: body.updatedBy ?? 'admin@oculisecurity.com',
186
+ });
187
+ reply.code(201).send(created);
188
+ });
189
+ // PUT /admin/policies/:id — update policy
190
+ app.put('/api/admin/policies/:id', async (request, reply) => {
191
+ const id = parseInt(request.params.id, 10);
192
+ if (isNaN(id)) {
193
+ reply.code(400).send({ error: 'Invalid policy id' });
194
+ return;
195
+ }
196
+ const body = request.body;
197
+ const updated = ctx.policyStore.update(id, {
198
+ name: body.name,
199
+ description: body.description,
200
+ version: body.version,
201
+ rego: body.rego,
202
+ updatedBy: body.updatedBy ?? 'admin@oculisecurity.com',
203
+ isActive: body.isActive,
204
+ });
205
+ if (!updated) {
206
+ reply.code(404).send({ error: 'Policy not found' });
207
+ return;
208
+ }
209
+ reply.send(updated);
210
+ });
211
+ // POST /admin/policies/:id/duplicate — duplicate a policy
212
+ app.post('/api/admin/policies/:id/duplicate', async (request, reply) => {
213
+ const id = parseInt(request.params.id, 10);
214
+ if (isNaN(id)) {
215
+ reply.code(400).send({ error: 'Invalid policy id' });
216
+ return;
217
+ }
218
+ const duplicated = ctx.policyStore.duplicate(id);
219
+ if (!duplicated) {
220
+ reply.code(404).send({ error: 'Policy not found' });
221
+ return;
222
+ }
223
+ reply.code(201).send(duplicated);
224
+ });
225
+ // DELETE /admin/policies/:id — delete a policy
226
+ app.delete('/api/admin/policies/:id', async (request, reply) => {
227
+ const id = parseInt(request.params.id, 10);
228
+ if (isNaN(id)) {
229
+ reply.code(400).send({ error: 'Invalid policy id' });
230
+ return;
231
+ }
232
+ const deleted = ctx.policyStore.delete(id);
233
+ if (!deleted) {
234
+ reply.code(404).send({ error: 'Policy not found' });
235
+ return;
236
+ }
237
+ reply.send({ success: true });
238
+ });
239
+ // -----------------------------------------------------------------------
240
+ // Visual Rules (unauthenticated — demo only)
241
+ // -----------------------------------------------------------------------
242
+ // POST /admin/rules/test — must be registered BEFORE :id param route
243
+ app.post('/api/admin/rules/test', async (request, reply) => {
244
+ const body = request.body;
245
+ const ruleInput = body.rule;
246
+ const eventInput = body.event;
247
+ if (!ruleInput || !eventInput) {
248
+ reply.code(400).send({ error: 'rule and event objects are required' });
249
+ return;
250
+ }
251
+ // Validate regex patterns
252
+ for (const field of ['command_pattern', 'file_pattern']) {
253
+ const pattern = ruleInput[field];
254
+ if (pattern) {
255
+ try {
256
+ new RegExp(pattern);
257
+ }
258
+ catch {
259
+ reply.code(400).send({ error: `Invalid regex in ${field}: ${pattern}` });
260
+ return;
261
+ }
262
+ }
263
+ }
264
+ const policyRule = {
265
+ id: ruleInput.rule_id ?? 'test',
266
+ match: {
267
+ tool: ruleInput.tool,
268
+ command_pattern: ruleInput.command_pattern,
269
+ file_pattern: ruleInput.file_pattern,
270
+ mcp_server: ruleInput.mcp_server,
271
+ },
272
+ action: (['deny', 'warn', 'allow'].includes(ruleInput.action)
273
+ ? ruleInput.action
274
+ : 'deny'),
275
+ };
276
+ const event = eventInput;
277
+ const matches = (0, local_policy_1.ruleMatches)(policyRule, event);
278
+ reply.send({ matches, action: matches ? policyRule.action : 'no match' });
279
+ });
280
+ // GET /admin/rules — list all visual rules
281
+ app.get('/api/admin/rules', async (_request, reply) => {
282
+ const rules = ctx.policyStore.listVisualRules();
283
+ reply.send({ rules });
284
+ });
285
+ // GET /admin/rules/:id — get single visual rule
286
+ app.get('/api/admin/rules/:id', async (request, reply) => {
287
+ const id = parseInt(request.params.id, 10);
288
+ if (isNaN(id)) {
289
+ reply.code(400).send({ error: 'Invalid rule id' });
290
+ return;
291
+ }
292
+ const rule = ctx.policyStore.getVisualRuleById(id);
293
+ if (!rule) {
294
+ reply.code(404).send({ error: 'Rule not found' });
295
+ return;
296
+ }
297
+ reply.send(rule);
298
+ });
299
+ // POST /admin/rules — create visual rule
300
+ app.post('/api/admin/rules', async (request, reply) => {
301
+ const body = request.body;
302
+ const rule_id = body.rule_id;
303
+ const action = body.action;
304
+ if (!rule_id || !action) {
305
+ reply.code(400).send({ error: 'rule_id and action are required' });
306
+ return;
307
+ }
308
+ if (!['deny', 'warn', 'allow'].includes(action)) {
309
+ reply.code(400).send({ error: 'action must be deny, warn, or allow' });
310
+ return;
311
+ }
312
+ // Validate regex patterns
313
+ for (const field of ['command_pattern', 'file_pattern']) {
314
+ const pattern = body[field];
315
+ if (pattern) {
316
+ try {
317
+ new RegExp(pattern);
318
+ }
319
+ catch {
320
+ reply.code(400).send({ error: `Invalid regex in ${field}: ${pattern}` });
321
+ return;
322
+ }
323
+ }
324
+ }
325
+ // Check at least one match field
326
+ if (!body.tool && !body.command_pattern && !body.file_pattern && !body.mcp_server) {
327
+ reply.code(400).send({ error: 'At least one match field (tool, command_pattern, file_pattern, mcp_server) is required' });
328
+ return;
329
+ }
330
+ try {
331
+ const created = ctx.policyStore.createVisualRule(body);
332
+ reply.code(201).send(created);
333
+ }
334
+ catch (err) {
335
+ const msg = err instanceof Error ? err.message : String(err);
336
+ if (msg.includes('UNIQUE constraint')) {
337
+ reply.code(409).send({ error: `Rule ID "${rule_id}" already exists` });
338
+ return;
339
+ }
340
+ throw err;
341
+ }
342
+ });
343
+ // PUT /admin/rules/:id — update visual rule
344
+ app.put('/api/admin/rules/:id', async (request, reply) => {
345
+ const id = parseInt(request.params.id, 10);
346
+ if (isNaN(id)) {
347
+ reply.code(400).send({ error: 'Invalid rule id' });
348
+ return;
349
+ }
350
+ const body = request.body;
351
+ // Validate regex patterns if provided
352
+ for (const field of ['command_pattern', 'file_pattern']) {
353
+ const pattern = body[field];
354
+ if (pattern) {
355
+ try {
356
+ new RegExp(pattern);
357
+ }
358
+ catch {
359
+ reply.code(400).send({ error: `Invalid regex in ${field}: ${pattern}` });
360
+ return;
361
+ }
362
+ }
363
+ }
364
+ if (body.action && !['deny', 'warn', 'allow'].includes(body.action)) {
365
+ reply.code(400).send({ error: 'action must be deny, warn, or allow' });
366
+ return;
367
+ }
368
+ try {
369
+ const updated = ctx.policyStore.updateVisualRule(id, body);
370
+ if (!updated) {
371
+ reply.code(404).send({ error: 'Rule not found' });
372
+ return;
373
+ }
374
+ reply.send(updated);
375
+ }
376
+ catch (err) {
377
+ const msg = err instanceof Error ? err.message : String(err);
378
+ if (msg.includes('UNIQUE constraint')) {
379
+ reply.code(409).send({ error: `Rule ID "${body.rule_id}" already exists` });
380
+ return;
381
+ }
382
+ throw err;
383
+ }
384
+ });
385
+ // DELETE /admin/rules/:id — delete visual rule
386
+ app.delete('/api/admin/rules/:id', async (request, reply) => {
387
+ const id = parseInt(request.params.id, 10);
388
+ if (isNaN(id)) {
389
+ reply.code(400).send({ error: 'Invalid rule id' });
390
+ return;
391
+ }
392
+ const deleted = ctx.policyStore.deleteVisualRule(id);
393
+ if (!deleted) {
394
+ reply.code(404).send({ error: 'Rule not found' });
395
+ return;
396
+ }
397
+ reply.send({ success: true });
398
+ });
399
+ }
@@ -0,0 +1,13 @@
1
+ import { FastifyInstance } from 'fastify';
2
+ import { PolicyService } from '../services/policy';
3
+ import { AuditService } from '../services/audit';
4
+ import { UpstreamConnector } from '../services/upstream';
5
+ import { RateLimiter } from '../services/ratelimit';
6
+ interface CallRouteContext {
7
+ policy: PolicyService;
8
+ audit: AuditService;
9
+ upstream: UpstreamConnector;
10
+ rateLimiter: RateLimiter;
11
+ }
12
+ export declare function registerCallRoutes(app: FastifyInstance, ctx: CallRouteContext): void;
13
+ export {};
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerCallRoutes = registerCallRoutes;
4
+ const tool_executor_1 = require("../services/tool-executor");
5
+ function registerCallRoutes(app, ctx) {
6
+ const executor = new tool_executor_1.ToolExecutor(ctx);
7
+ app.post('/v1/call', {
8
+ schema: {
9
+ body: {
10
+ type: 'object',
11
+ required: ['upstreamId', 'tool', 'args'],
12
+ properties: {
13
+ upstreamId: { type: 'string' },
14
+ tool: { type: 'string' },
15
+ args: { type: 'object' },
16
+ sessionId: { type: 'string' },
17
+ },
18
+ },
19
+ },
20
+ }, async (request, reply) => {
21
+ const { upstreamId, tool, args, sessionId } = request.body;
22
+ const execResult = await executor.execute({
23
+ actor: request.actor,
24
+ upstreamId,
25
+ tool,
26
+ args,
27
+ sessionId,
28
+ ip: request.ip,
29
+ });
30
+ switch (execResult.kind) {
31
+ case 'allow':
32
+ reply.code(200).send({
33
+ requestId: execResult.requestId,
34
+ decision: { allow: true, reason: execResult.reason },
35
+ upstream: { id: execResult.upstreamId, latencyMs: execResult.latencyMs },
36
+ result: execResult.result,
37
+ });
38
+ return;
39
+ case 'deny':
40
+ if (execResult.httpStatus === 429) {
41
+ reply
42
+ .code(429)
43
+ .header('Retry-After', String(Math.ceil((execResult.retryAfterMs ?? 0) / 1000)))
44
+ .send({
45
+ requestId: execResult.requestId,
46
+ decision: { allow: false, reason: execResult.reason },
47
+ error: `Rate limit exceeded. Retry after ~${execResult.retryAfterMs}ms`,
48
+ });
49
+ }
50
+ else {
51
+ reply.code(execResult.httpStatus).send({
52
+ requestId: execResult.requestId,
53
+ decision: { allow: false, reason: execResult.reason },
54
+ error: execResult.reason,
55
+ });
56
+ }
57
+ return;
58
+ case 'error':
59
+ reply.code(execResult.httpStatus).send({
60
+ requestId: execResult.requestId,
61
+ decision: { allow: true, reason: execResult.reason },
62
+ upstream: { id: execResult.upstreamId, latencyMs: execResult.latencyMs },
63
+ error: execResult.message,
64
+ });
65
+ return;
66
+ }
67
+ });
68
+ }
@@ -0,0 +1,7 @@
1
+ import { FastifyInstance } from 'fastify';
2
+ import { AuditService } from '../services/audit';
3
+ interface EventsRouteContext {
4
+ audit: AuditService;
5
+ }
6
+ export declare function registerEventsRoute(app: FastifyInstance, ctx: EventsRouteContext): void;
7
+ export {};
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.registerEventsRoute = registerEventsRoute;
37
+ const uuid_1 = require("uuid");
38
+ const ClaudeCodeAdapter = __importStar(require("./adapters/claude-code"));
39
+ function normalizePayload(body) {
40
+ if (ClaudeCodeAdapter.detect(body)) {
41
+ const event = ClaudeCodeAdapter.normalize(body);
42
+ return {
43
+ source: 'claude-code',
44
+ events: [
45
+ {
46
+ tool: event.tool ?? '__stop__',
47
+ args: event.tool_args,
48
+ result: event.tool_result,
49
+ sessionId: event.session_id,
50
+ actor: event.actor,
51
+ },
52
+ ],
53
+ };
54
+ }
55
+ // Standard TelemetryPayload — validate manually
56
+ const payload = body;
57
+ if (typeof payload.source !== 'string' ||
58
+ !payload.source ||
59
+ !Array.isArray(payload.events) ||
60
+ payload.events.length < 1 ||
61
+ payload.events.length > 100) {
62
+ return null;
63
+ }
64
+ for (const event of payload.events) {
65
+ if (typeof event.tool !== 'string' || !event.tool) {
66
+ return null;
67
+ }
68
+ }
69
+ return payload;
70
+ }
71
+ function registerEventsRoute(app, ctx) {
72
+ app.post('/v1/events', async (request, reply) => {
73
+ const body = request.body;
74
+ if (!body || typeof body !== 'object') {
75
+ reply.code(400).send({ error: 'Invalid request body' });
76
+ return;
77
+ }
78
+ const payload = normalizePayload(body);
79
+ if (!payload) {
80
+ reply.code(400).send({
81
+ error: 'Invalid payload. Expected { source, events: [{ tool, ... }] } or Claude Code hook format { hook_event_name, tool_name, ... }',
82
+ });
83
+ return;
84
+ }
85
+ const { source, orgId, events } = payload;
86
+ const ip = request.ip;
87
+ const isClaudeCode = source === 'claude-code';
88
+ // For Claude Code hooks, stash the full raw body so nothing is lost
89
+ const rawBodyJson = isClaudeCode ? JSON.stringify(body) : null;
90
+ request.log.info({ source, toolCount: events.length }, 'telemetry received');
91
+ if (isClaudeCode) {
92
+ request.log.debug({ rawBody: body }, 'claude-code hook raw payload');
93
+ }
94
+ for (const event of events) {
95
+ const requestId = (0, uuid_1.v4)();
96
+ // For Claude Code hooks use raw body; for standard events use parsed args
97
+ const argsJson = isClaudeCode
98
+ ? rawBodyJson
99
+ : (event.args ? JSON.stringify(event.args) : null);
100
+ const resultStr = event.result != null ? JSON.stringify(event.result) : null;
101
+ const responseJson = resultStr
102
+ ? resultStr.length <= 8192 ? resultStr : resultStr.slice(0, 8192) + '…'
103
+ : null;
104
+ ctx.audit.log({
105
+ requestId,
106
+ timestamp: event.timestamp ?? new Date().toISOString(),
107
+ actor: event.actor ?? source,
108
+ orgId: orgId ?? 'default',
109
+ upstreamId: `ext:${source}`,
110
+ tool: event.tool,
111
+ argsHash: event.args ? ctx.audit.hash(event.args) : 'none',
112
+ argsJson,
113
+ decision: 'allow',
114
+ reason: 'telemetry',
115
+ latencyMs: event.durationMs ?? 0,
116
+ outcome: event.error ? 'error' : 'success',
117
+ responseHash: event.result != null ? ctx.audit.hash(event.result) : undefined,
118
+ responseJson,
119
+ ip,
120
+ sessionId: event.sessionId ?? requestId,
121
+ });
122
+ }
123
+ reply.code(202).send({ received: events.length });
124
+ });
125
+ }
@@ -0,0 +1,2 @@
1
+ import { FastifyInstance } from 'fastify';
2
+ export declare function registerHealthRoutes(app: FastifyInstance): void;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerHealthRoutes = registerHealthRoutes;
4
+ function registerHealthRoutes(app) {
5
+ app.get('/health', async (_req, reply) => {
6
+ reply.send({
7
+ status: 'ok',
8
+ timestamp: new Date().toISOString(),
9
+ service: 'oculi-security-gateway',
10
+ });
11
+ });
12
+ }
@@ -0,0 +1,11 @@
1
+ import { FastifyInstance } from 'fastify';
2
+ import { PolicyService } from '../services/policy';
3
+ import { AuditService } from '../services/audit';
4
+ import { RateLimiter } from '../services/ratelimit';
5
+ interface HooksRouteContext {
6
+ policy: PolicyService;
7
+ audit: AuditService;
8
+ rateLimiter: RateLimiter;
9
+ }
10
+ export declare function registerHooksRoute(app: FastifyInstance, ctx: HooksRouteContext): void;
11
+ export {};