@mondaydotcomorg/atp-providers 0.17.14

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 (58) hide show
  1. package/README.md +430 -0
  2. package/__tests__/oauth-integration.test.ts +457 -0
  3. package/__tests__/scope-checkers.test.ts +560 -0
  4. package/__tests__/token-expiration.test.ts +308 -0
  5. package/dist/audit/jsonl.d.ts +29 -0
  6. package/dist/audit/jsonl.d.ts.map +1 -0
  7. package/dist/audit/jsonl.js +163 -0
  8. package/dist/audit/jsonl.js.map +1 -0
  9. package/dist/audit/opentelemetry.d.ts +23 -0
  10. package/dist/audit/opentelemetry.d.ts.map +1 -0
  11. package/dist/audit/opentelemetry.js +240 -0
  12. package/dist/audit/opentelemetry.js.map +1 -0
  13. package/dist/audit/otel-metrics.d.ts +111 -0
  14. package/dist/audit/otel-metrics.d.ts.map +1 -0
  15. package/dist/audit/otel-metrics.js +115 -0
  16. package/dist/audit/otel-metrics.js.map +1 -0
  17. package/dist/auth/env.d.ts +21 -0
  18. package/dist/auth/env.d.ts.map +1 -0
  19. package/dist/auth/env.js +48 -0
  20. package/dist/auth/env.js.map +1 -0
  21. package/dist/cache/file.d.ts +47 -0
  22. package/dist/cache/file.d.ts.map +1 -0
  23. package/dist/cache/file.js +262 -0
  24. package/dist/cache/file.js.map +1 -0
  25. package/dist/cache/memory.d.ts +30 -0
  26. package/dist/cache/memory.d.ts.map +1 -0
  27. package/dist/cache/memory.js +81 -0
  28. package/dist/cache/memory.js.map +1 -0
  29. package/dist/cache/redis.d.ts +28 -0
  30. package/dist/cache/redis.d.ts.map +1 -0
  31. package/dist/cache/redis.js +124 -0
  32. package/dist/cache/redis.js.map +1 -0
  33. package/dist/index.d.ts +10 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +9 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/oauth/index.d.ts +2 -0
  38. package/dist/oauth/index.d.ts.map +1 -0
  39. package/dist/oauth/index.js +2 -0
  40. package/dist/oauth/index.js.map +1 -0
  41. package/dist/oauth/scope-checkers.d.ts +61 -0
  42. package/dist/oauth/scope-checkers.d.ts.map +1 -0
  43. package/dist/oauth/scope-checkers.js +166 -0
  44. package/dist/oauth/scope-checkers.js.map +1 -0
  45. package/package.json +26 -0
  46. package/project.json +31 -0
  47. package/src/audit/jsonl.ts +189 -0
  48. package/src/audit/opentelemetry.ts +286 -0
  49. package/src/audit/otel-metrics.ts +122 -0
  50. package/src/auth/env.ts +65 -0
  51. package/src/cache/file.ts +330 -0
  52. package/src/cache/memory.ts +105 -0
  53. package/src/cache/redis.ts +160 -0
  54. package/src/index.ts +32 -0
  55. package/src/oauth/index.ts +1 -0
  56. package/src/oauth/scope-checkers.ts +196 -0
  57. package/tsconfig.json +14 -0
  58. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,286 @@
1
+ import { trace, context, SpanStatusCode, metrics, Span } from '@opentelemetry/api';
2
+ import type { AuditSink, AuditEvent } from '@mondaydotcomorg/atp-protocol';
3
+ import {
4
+ OTelCounter,
5
+ OTelHistogram,
6
+ OTelAttribute,
7
+ METRIC_CONFIGS,
8
+ OTEL_TRACER_NAME,
9
+ OTEL_METER_NAME,
10
+ ATTRIBUTE_PREFIX_TOOL,
11
+ ATTRIBUTE_PREFIX_METADATA,
12
+ } from './otel-metrics.js';
13
+
14
+ /**
15
+ * OpenTelemetry-based audit sink
16
+ * Provides industry-standard observability with distributed tracing and metrics
17
+ */
18
+ export class OpenTelemetryAuditSink implements AuditSink {
19
+ name = 'opentelemetry';
20
+ private tracer = trace.getTracer(OTEL_TRACER_NAME);
21
+ private meter = metrics.getMeter(OTEL_METER_NAME);
22
+
23
+ private executionCounter = this.meter.createCounter(
24
+ OTelCounter.EXECUTIONS_TOTAL,
25
+ METRIC_CONFIGS[OTelCounter.EXECUTIONS_TOTAL]
26
+ );
27
+
28
+ private toolCallCounter = this.meter.createCounter(
29
+ OTelCounter.TOOLS_CALLS,
30
+ METRIC_CONFIGS[OTelCounter.TOOLS_CALLS]
31
+ );
32
+
33
+ private llmCallCounter = this.meter.createCounter(
34
+ OTelCounter.LLM_CALLS,
35
+ METRIC_CONFIGS[OTelCounter.LLM_CALLS]
36
+ );
37
+
38
+ private approvalCounter = this.meter.createCounter(
39
+ OTelCounter.APPROVALS_TOTAL,
40
+ METRIC_CONFIGS[OTelCounter.APPROVALS_TOTAL]
41
+ );
42
+
43
+ private executionDuration = this.meter.createHistogram(
44
+ OTelHistogram.EXECUTION_DURATION,
45
+ METRIC_CONFIGS[OTelHistogram.EXECUTION_DURATION]
46
+ );
47
+
48
+ private toolDuration = this.meter.createHistogram(
49
+ OTelHistogram.TOOL_DURATION,
50
+ METRIC_CONFIGS[OTelHistogram.TOOL_DURATION]
51
+ );
52
+
53
+ async write(event: AuditEvent): Promise<void> {
54
+ const span = this.tracer.startSpan(`atp.${event.eventType}.${event.action}`, {
55
+ attributes: this.buildAttributes(event),
56
+ });
57
+
58
+ await context.with(trace.setSpan(context.active(), span), async () => {
59
+ try {
60
+ this.handleEvent(span, event);
61
+ this.recordMetrics(event);
62
+ span.setStatus({ code: SpanStatusCode.OK });
63
+ } catch (error: any) {
64
+ span.setStatus({
65
+ code: SpanStatusCode.ERROR,
66
+ message: error.message,
67
+ });
68
+ span.recordException(error);
69
+ } finally {
70
+ span.end();
71
+ }
72
+ });
73
+ }
74
+
75
+ async writeBatch(events: AuditEvent[]): Promise<void> {
76
+ await Promise.all(events.map((event) => this.write(event)));
77
+ }
78
+
79
+ private buildAttributes(event: AuditEvent): Record<string, any> {
80
+ const attrs: Record<string, any> = {
81
+ [OTelAttribute.EVENT_ID]: event.eventId,
82
+ [OTelAttribute.EVENT_TYPE]: event.eventType,
83
+ [OTelAttribute.EVENT_ACTION]: event.action,
84
+ [OTelAttribute.TIMESTAMP]: event.timestamp,
85
+
86
+ [OTelAttribute.CLIENT_ID]: event.clientId,
87
+ [OTelAttribute.STATUS]: event.status,
88
+ };
89
+
90
+ if (event.userId) attrs[OTelAttribute.USER_ID] = event.userId;
91
+ if (event.ipAddress) attrs[OTelAttribute.IP_ADDRESS] = event.ipAddress;
92
+ if (event.userAgent) attrs[OTelAttribute.USER_AGENT] = event.userAgent;
93
+
94
+ if (event.resource) attrs[OTelAttribute.RESOURCE] = event.resource;
95
+ if (event.resourceId) attrs[OTelAttribute.RESOURCE_ID] = event.resourceId;
96
+
97
+ if (event.toolName) attrs[OTelAttribute.TOOL_NAME] = event.toolName;
98
+ if (event.apiGroup) attrs[OTelAttribute.API_GROUP] = event.apiGroup;
99
+
100
+ if (event.duration !== undefined) attrs[OTelAttribute.DURATION_MS] = event.duration;
101
+ if (event.memoryUsed !== undefined) attrs[OTelAttribute.MEMORY_BYTES] = event.memoryUsed;
102
+ if (event.llmCallsCount !== undefined) attrs[OTelAttribute.LLM_CALLS] = event.llmCallsCount;
103
+ if (event.httpCallsCount !== undefined) attrs[OTelAttribute.HTTP_CALLS] = event.httpCallsCount;
104
+
105
+ if (event.riskScore !== undefined) attrs[OTelAttribute.RISK_SCORE] = event.riskScore;
106
+ if (event.securityEvents && event.securityEvents.length > 0) {
107
+ attrs[OTelAttribute.SECURITY_EVENTS] = JSON.stringify(event.securityEvents);
108
+ attrs[OTelAttribute.SECURITY_EVENTS_COUNT] = event.securityEvents.length;
109
+ }
110
+
111
+ if (event.error) {
112
+ attrs[OTelAttribute.ERROR_MESSAGE] = event.error.message;
113
+ if (event.error.code) attrs[OTelAttribute.ERROR_CODE] = event.error.code;
114
+ if (event.error.stack) attrs[OTelAttribute.ERROR_STACK] = event.error.stack;
115
+ }
116
+
117
+ if (event.annotations) {
118
+ Object.assign(attrs, this.flattenObject(event.annotations, ATTRIBUTE_PREFIX_TOOL));
119
+ }
120
+
121
+ if (event.metadata) {
122
+ Object.assign(attrs, this.flattenObject(event.metadata, ATTRIBUTE_PREFIX_METADATA));
123
+ }
124
+
125
+ return attrs;
126
+ }
127
+
128
+ private handleEvent(span: Span, event: AuditEvent): void {
129
+ switch (event.eventType) {
130
+ case 'execution':
131
+ if (event.action === 'start') {
132
+ span.addEvent('Execution started', {
133
+ 'client.id': event.clientId,
134
+ 'resource.id': event.resourceId,
135
+ });
136
+ } else if (event.action === 'complete') {
137
+ span.addEvent('Execution completed', {
138
+ duration_ms: event.duration,
139
+ status: event.status,
140
+ llm_calls: event.llmCallsCount,
141
+ });
142
+ } else if (event.action === 'pause') {
143
+ span.addEvent('Execution paused', {
144
+ status: event.status,
145
+ });
146
+ } else if (event.action === 'resume') {
147
+ span.addEvent('Execution resumed', {
148
+ 'resource.id': event.resourceId,
149
+ });
150
+ }
151
+ break;
152
+
153
+ case 'tool_call':
154
+ span.addEvent(`Tool ${event.action}`, {
155
+ 'tool.name': event.toolName,
156
+ 'api.group': event.apiGroup,
157
+ duration_ms: event.duration,
158
+ });
159
+ if (event.input) {
160
+ span.setAttribute(OTelAttribute.TOOL_INPUT_SIZE, JSON.stringify(event.input).length);
161
+ }
162
+ if (event.output) {
163
+ span.setAttribute(OTelAttribute.TOOL_OUTPUT_SIZE, JSON.stringify(event.output).length);
164
+ }
165
+ break;
166
+
167
+ case 'llm_call':
168
+ span.addEvent('LLM call', {
169
+ duration_ms: event.duration,
170
+ });
171
+ break;
172
+
173
+ case 'approval':
174
+ span.addEvent(`Approval ${event.action}`, {
175
+ 'tool.name': event.toolName,
176
+ });
177
+ break;
178
+
179
+ case 'error':
180
+ if (event.error) {
181
+ span.addEvent('Error occurred', {
182
+ 'error.message': event.error.message,
183
+ 'error.code': event.error.code,
184
+ });
185
+ span.recordException(new Error(event.error.message));
186
+ }
187
+ break;
188
+
189
+ case 'client_init':
190
+ span.addEvent('Client initialized', {
191
+ 'client.id': event.clientId,
192
+ });
193
+ break;
194
+ }
195
+
196
+ if (event.securityEvents && event.securityEvents.length > 0) {
197
+ for (const secEvent of event.securityEvents) {
198
+ span.addEvent('Security event', {
199
+ 'security.event': secEvent,
200
+ 'security.risk_score': event.riskScore,
201
+ });
202
+ }
203
+ }
204
+ }
205
+
206
+ private recordMetrics(event: AuditEvent): void {
207
+ const commonAttrs: Record<string, any> = {
208
+ client_id: event.clientId,
209
+ event_type: event.eventType,
210
+ status: event.status,
211
+ };
212
+
213
+ switch (event.eventType) {
214
+ case 'execution':
215
+ this.executionCounter.add(1, {
216
+ ...commonAttrs,
217
+ action: event.action,
218
+ });
219
+
220
+ if (event.duration !== undefined) {
221
+ this.executionDuration.record(event.duration, {
222
+ ...commonAttrs,
223
+ action: event.action,
224
+ });
225
+ }
226
+ break;
227
+
228
+ case 'tool_call':
229
+ this.toolCallCounter.add(1, {
230
+ ...commonAttrs,
231
+ tool_name: event.toolName,
232
+ api_group: event.apiGroup,
233
+ });
234
+
235
+ if (event.duration !== undefined) {
236
+ this.toolDuration.record(event.duration, {
237
+ ...commonAttrs,
238
+ tool_name: event.toolName,
239
+ });
240
+ }
241
+ break;
242
+
243
+ case 'llm_call':
244
+ this.llmCallCounter.add(1, commonAttrs);
245
+ break;
246
+
247
+ case 'approval':
248
+ this.approvalCounter.add(1, {
249
+ ...commonAttrs,
250
+ action: event.action,
251
+ });
252
+ break;
253
+ }
254
+
255
+ if (event.securityEvents && event.securityEvents.length > 0) {
256
+ const securityEventCounter = this.meter.createCounter(
257
+ OTelCounter.SECURITY_EVENTS,
258
+ METRIC_CONFIGS[OTelCounter.SECURITY_EVENTS]
259
+ );
260
+ securityEventCounter.add(event.securityEvents.length, {
261
+ ...commonAttrs,
262
+ risk_score: event.riskScore,
263
+ });
264
+ }
265
+ }
266
+
267
+ private flattenObject(obj: any, prefix: string): Record<string, any> {
268
+ const result: Record<string, any> = {};
269
+ if (!obj || typeof obj !== 'object') return result;
270
+
271
+ for (const [key, value] of Object.entries(obj)) {
272
+ const fullKey = `${prefix}.${key}`;
273
+ if (value === null || value === undefined) {
274
+ continue;
275
+ }
276
+ if (typeof value === 'object' && !Array.isArray(value)) {
277
+ Object.assign(result, this.flattenObject(value, fullKey));
278
+ } else if (Array.isArray(value)) {
279
+ result[fullKey] = JSON.stringify(value);
280
+ } else {
281
+ result[fullKey] = value;
282
+ }
283
+ }
284
+ return result;
285
+ }
286
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * OpenTelemetry Metrics Definitions
3
+
4
+ /**
5
+ * Counter metric names
6
+ */
7
+ export enum OTelCounter {
8
+ EXECUTIONS_TOTAL = 'atp.executions.total',
9
+ TOOLS_CALLS = 'atp.tools.calls',
10
+ LLM_CALLS = 'atp.llm.calls',
11
+ APPROVALS_TOTAL = 'atp.approvals.total',
12
+ SECURITY_EVENTS = 'atp.security.events',
13
+ }
14
+
15
+ /**
16
+ * Histogram metric names
17
+ */
18
+ export enum OTelHistogram {
19
+ EXECUTION_DURATION = 'atp.execution.duration',
20
+ TOOL_DURATION = 'atp.tool.duration',
21
+ }
22
+
23
+ /**
24
+ * Span/trace names
25
+ */
26
+ export enum OTelSpan {
27
+ EXECUTION_START = 'atp.execution.start',
28
+ EXECUTION_COMPLETE = 'atp.execution.complete',
29
+ EXECUTION_PAUSE = 'atp.execution.pause',
30
+ EXECUTION_RESUME = 'atp.execution.resume',
31
+ EXECUTION_ERROR = 'atp.execution.error',
32
+ TOOL_CALL = 'atp.tool_call',
33
+ LLM_CALL = 'atp.llm_call',
34
+ APPROVAL_REQUEST = 'atp.approval.request',
35
+ APPROVAL_RESPONSE = 'atp.approval.response',
36
+ CLIENT_INIT = 'atp.client_init',
37
+ ERROR = 'atp.error',
38
+ }
39
+
40
+ /**
41
+ * Attribute names (for consistent span/metric attributes)
42
+ */
43
+ export enum OTelAttribute {
44
+ EVENT_ID = 'atp.event.id',
45
+ EVENT_TYPE = 'atp.event.type',
46
+ EVENT_ACTION = 'atp.event.action',
47
+ TIMESTAMP = 'atp.timestamp',
48
+
49
+ CLIENT_ID = 'atp.client.id',
50
+ USER_ID = 'atp.user.id',
51
+ IP_ADDRESS = 'atp.ip_address',
52
+ USER_AGENT = 'atp.user_agent',
53
+ STATUS = 'atp.status',
54
+
55
+ RESOURCE = 'atp.resource',
56
+ RESOURCE_ID = 'atp.resource.id',
57
+
58
+ TOOL_NAME = 'atp.tool.name',
59
+ TOOL_INPUT_SIZE = 'tool.input_size',
60
+ TOOL_OUTPUT_SIZE = 'tool.output_size',
61
+ API_GROUP = 'atp.api.group',
62
+
63
+ DURATION_MS = 'atp.duration_ms',
64
+ MEMORY_BYTES = 'atp.memory_bytes',
65
+ LLM_CALLS = 'atp.llm_calls',
66
+ HTTP_CALLS = 'atp.http_calls',
67
+
68
+ RISK_SCORE = 'atp.security.risk_score',
69
+ SECURITY_EVENTS = 'atp.security.events',
70
+ SECURITY_EVENTS_COUNT = 'atp.security.events_count',
71
+
72
+ ERROR_MESSAGE = 'atp.error.message',
73
+ ERROR_CODE = 'atp.error.code',
74
+ ERROR_STACK = 'atp.error.stack',
75
+ }
76
+
77
+ /**
78
+ * Metric configurations
79
+ */
80
+ export const METRIC_CONFIGS = {
81
+ [OTelCounter.EXECUTIONS_TOTAL]: {
82
+ description: 'Total number of executions',
83
+ unit: '1',
84
+ },
85
+ [OTelCounter.TOOLS_CALLS]: {
86
+ description: 'Tool call count',
87
+ unit: '1',
88
+ },
89
+ [OTelCounter.LLM_CALLS]: {
90
+ description: 'LLM call count',
91
+ unit: '1',
92
+ },
93
+ [OTelCounter.APPROVALS_TOTAL]: {
94
+ description: 'Approval request count',
95
+ unit: '1',
96
+ },
97
+ [OTelCounter.SECURITY_EVENTS]: {
98
+ description: 'Security events count',
99
+ unit: '1',
100
+ },
101
+ [OTelHistogram.EXECUTION_DURATION]: {
102
+ description: 'Execution duration in milliseconds',
103
+ unit: 'ms',
104
+ },
105
+ [OTelHistogram.TOOL_DURATION]: {
106
+ description: 'Tool execution duration',
107
+ unit: 'ms',
108
+ },
109
+ } as const;
110
+
111
+ /**
112
+ * OpenTelemetry tracer and meter names
113
+ */
114
+ export const OTEL_SERVICE_NAME = 'agent-tool-protocol';
115
+ export const OTEL_TRACER_NAME = 'agent-tool-protocol';
116
+ export const OTEL_METER_NAME = 'agent-tool-protocol';
117
+
118
+ /**
119
+ * Attribute prefixes for custom metadata
120
+ */
121
+ export const ATTRIBUTE_PREFIX_TOOL = 'atp.tool';
122
+ export const ATTRIBUTE_PREFIX_METADATA = 'atp.metadata';
@@ -0,0 +1,65 @@
1
+ import type { AuthProvider } from '@mondaydotcomorg/atp-protocol';
2
+
3
+ /**
4
+ * Environment variable based auth provider
5
+ * Simple provider that reads credentials from process.env
6
+ * Good for development and simple deployments
7
+ */
8
+ export class EnvAuthProvider implements AuthProvider {
9
+ name = 'env';
10
+ private prefix: string;
11
+ private credentials: Map<string, string>;
12
+
13
+ constructor(
14
+ options: {
15
+ prefix?: string;
16
+ credentials?: Record<string, string>;
17
+ } = {}
18
+ ) {
19
+ this.prefix = options.prefix || 'ATP_';
20
+ this.credentials = new Map();
21
+
22
+ if (options.credentials) {
23
+ for (const [key, value] of Object.entries(options.credentials)) {
24
+ this.credentials.set(key, value);
25
+ }
26
+ }
27
+ }
28
+
29
+ async getCredential(key: string): Promise<string | null> {
30
+ if (this.credentials.has(key)) {
31
+ return this.credentials.get(key) || null;
32
+ }
33
+
34
+ const envValue = process.env[key] || process.env[`${this.prefix}${key}`];
35
+ return envValue || null;
36
+ }
37
+
38
+ async setCredential(key: string, value: string, _ttl?: number): Promise<void> {
39
+ this.credentials.set(key, value);
40
+ }
41
+
42
+ async deleteCredential(key: string): Promise<void> {
43
+ this.credentials.delete(key);
44
+ }
45
+
46
+ async listCredentials(): Promise<string[]> {
47
+ const keys = new Set<string>();
48
+
49
+ for (const key of this.credentials.keys()) {
50
+ keys.add(key);
51
+ }
52
+
53
+ for (const key of Object.keys(process.env)) {
54
+ if (key.startsWith(this.prefix)) {
55
+ keys.add(key.substring(this.prefix.length));
56
+ }
57
+ }
58
+
59
+ return Array.from(keys);
60
+ }
61
+
62
+ async disconnect(): Promise<void> {
63
+ this.credentials.clear();
64
+ }
65
+ }