@peac/telemetry 0.9.31
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/.turbo/turbo-build.log +4 -0
- package/LICENSE +190 -0
- package/README.md +116 -0
- package/dist/attributes.d.ts +58 -0
- package/dist/attributes.d.ts.map +1 -0
- package/dist/attributes.js +67 -0
- package/dist/attributes.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/noop.d.ts +15 -0
- package/dist/noop.d.ts.map +1 -0
- package/dist/noop.js +21 -0
- package/dist/noop.js.map +1 -0
- package/dist/provider.d.ts +54 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +65 -0
- package/dist/provider.js.map +1 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +37 -0
- package/src/attributes.ts +71 -0
- package/src/index.ts +63 -0
- package/src/noop.ts +20 -0
- package/src/provider.ts +64 -0
- package/src/types.ts +163 -0
- package/tests/attributes.test.ts +126 -0
- package/tests/noop.test.ts +109 -0
- package/tests/provider.test.ts +223 -0
- package/tests/types.test.ts +218 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @peac/telemetry - Attribute constants tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
PEAC_ATTRS,
|
|
8
|
+
PEAC_EVENTS,
|
|
9
|
+
PEAC_METRICS,
|
|
10
|
+
TRACE_CONTEXT_EXTENSIONS,
|
|
11
|
+
} from '../src/attributes.js';
|
|
12
|
+
|
|
13
|
+
describe('PEAC_ATTRS', () => {
|
|
14
|
+
it('should define core attributes', () => {
|
|
15
|
+
expect(PEAC_ATTRS.VERSION).toBe('peac.version');
|
|
16
|
+
expect(PEAC_ATTRS.EVENT).toBe('peac.event');
|
|
17
|
+
expect(PEAC_ATTRS.RECEIPT_HASH).toBe('peac.receipt.hash');
|
|
18
|
+
expect(PEAC_ATTRS.POLICY_HASH).toBe('peac.policy.hash');
|
|
19
|
+
expect(PEAC_ATTRS.DECISION).toBe('peac.decision');
|
|
20
|
+
expect(PEAC_ATTRS.REASON_CODE).toBe('peac.reason_code');
|
|
21
|
+
expect(PEAC_ATTRS.ISSUER).toBe('peac.issuer');
|
|
22
|
+
expect(PEAC_ATTRS.ISSUER_HASH).toBe('peac.issuer_hash');
|
|
23
|
+
expect(PEAC_ATTRS.KID).toBe('peac.kid');
|
|
24
|
+
expect(PEAC_ATTRS.VALID).toBe('peac.valid');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should use stable OTel semconv for HTTP', () => {
|
|
28
|
+
// These are stable OTel semantic conventions
|
|
29
|
+
expect(PEAC_ATTRS.HTTP_METHOD).toBe('http.request.method');
|
|
30
|
+
expect(PEAC_ATTRS.HTTP_PATH).toBe('url.path');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should define HTTP hash attributes', () => {
|
|
34
|
+
expect(PEAC_ATTRS.HTTP_HOST_HASH).toBe('peac.http.host_hash');
|
|
35
|
+
expect(PEAC_ATTRS.HTTP_CLIENT_HASH).toBe('peac.http.client_hash');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should define payment attributes', () => {
|
|
39
|
+
expect(PEAC_ATTRS.PAYMENT_RAIL).toBe('peac.payment.rail');
|
|
40
|
+
expect(PEAC_ATTRS.PAYMENT_AMOUNT).toBe('peac.payment.amount');
|
|
41
|
+
expect(PEAC_ATTRS.PAYMENT_CURRENCY).toBe('peac.payment.currency');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should define duration attribute', () => {
|
|
45
|
+
expect(PEAC_ATTRS.DURATION_MS).toBe('peac.duration_ms');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should be readonly (const assertion)', () => {
|
|
49
|
+
// TypeScript ensures this at compile time
|
|
50
|
+
// Runtime test: object should be frozen-like
|
|
51
|
+
const keys = Object.keys(PEAC_ATTRS);
|
|
52
|
+
expect(keys.length).toBeGreaterThan(0);
|
|
53
|
+
|
|
54
|
+
// All values should be strings
|
|
55
|
+
for (const key of keys) {
|
|
56
|
+
expect(typeof PEAC_ATTRS[key as keyof typeof PEAC_ATTRS]).toBe('string');
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should use peac. prefix consistently', () => {
|
|
61
|
+
const peacPrefixed = Object.values(PEAC_ATTRS).filter((v) => v.startsWith('peac.'));
|
|
62
|
+
const otherPrefixed = Object.values(PEAC_ATTRS).filter((v) => !v.startsWith('peac.'));
|
|
63
|
+
|
|
64
|
+
// Most should be peac. prefixed
|
|
65
|
+
expect(peacPrefixed.length).toBeGreaterThan(otherPrefixed.length);
|
|
66
|
+
|
|
67
|
+
// HTTP should use standard OTel conventions
|
|
68
|
+
expect(otherPrefixed).toContain('http.request.method');
|
|
69
|
+
expect(otherPrefixed).toContain('url.path');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('PEAC_EVENTS', () => {
|
|
74
|
+
it('should define all event names', () => {
|
|
75
|
+
expect(PEAC_EVENTS.RECEIPT_ISSUED).toBe('peac.receipt.issued');
|
|
76
|
+
expect(PEAC_EVENTS.RECEIPT_VERIFIED).toBe('peac.receipt.verified');
|
|
77
|
+
expect(PEAC_EVENTS.ACCESS_DECISION).toBe('peac.access.decision');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should use peac. prefix', () => {
|
|
81
|
+
for (const event of Object.values(PEAC_EVENTS)) {
|
|
82
|
+
expect(event.startsWith('peac.')).toBe(true);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('PEAC_METRICS', () => {
|
|
88
|
+
it('should define counter metrics', () => {
|
|
89
|
+
expect(PEAC_METRICS.RECEIPTS_ISSUED).toBe('peac.receipts.issued');
|
|
90
|
+
expect(PEAC_METRICS.RECEIPTS_VERIFIED).toBe('peac.receipts.verified');
|
|
91
|
+
expect(PEAC_METRICS.ACCESS_DECISIONS).toBe('peac.access.decisions');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should define histogram metrics', () => {
|
|
95
|
+
expect(PEAC_METRICS.ISSUE_DURATION).toBe('peac.issue.duration');
|
|
96
|
+
expect(PEAC_METRICS.VERIFY_DURATION).toBe('peac.verify.duration');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should use peac. prefix', () => {
|
|
100
|
+
for (const metric of Object.values(PEAC_METRICS)) {
|
|
101
|
+
expect(metric.startsWith('peac.')).toBe(true);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('TRACE_CONTEXT_EXTENSIONS', () => {
|
|
107
|
+
it('should use w3c/ namespace (vendor-neutral)', () => {
|
|
108
|
+
expect(TRACE_CONTEXT_EXTENSIONS.TRACEPARENT).toBe('w3c/traceparent');
|
|
109
|
+
expect(TRACE_CONTEXT_EXTENSIONS.TRACESTATE).toBe('w3c/tracestate');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should match extension key pattern', () => {
|
|
113
|
+
// Pattern: ^[a-z0-9_.-]+/[a-z0-9_.-]+$
|
|
114
|
+
const pattern = /^[a-z0-9_.-]+\/[a-z0-9_.-]+$/;
|
|
115
|
+
|
|
116
|
+
expect(pattern.test(TRACE_CONTEXT_EXTENSIONS.TRACEPARENT)).toBe(true);
|
|
117
|
+
expect(pattern.test(TRACE_CONTEXT_EXTENSIONS.TRACESTATE)).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should NOT use io.opentelemetry namespace', () => {
|
|
121
|
+
// Vendor-neutral: w3c/ not io.opentelemetry/
|
|
122
|
+
for (const key of Object.values(TRACE_CONTEXT_EXTENSIONS)) {
|
|
123
|
+
expect(key.startsWith('io.opentelemetry')).toBe(false);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @peac/telemetry - No-op provider tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
6
|
+
import { noopProvider } from '../src/noop.js';
|
|
7
|
+
import type { TelemetryProvider } from '../src/types.js';
|
|
8
|
+
|
|
9
|
+
describe('noopProvider', () => {
|
|
10
|
+
it('should implement TelemetryProvider interface', () => {
|
|
11
|
+
const provider: TelemetryProvider = noopProvider;
|
|
12
|
+
|
|
13
|
+
expect(typeof provider.onReceiptIssued).toBe('function');
|
|
14
|
+
expect(typeof provider.onReceiptVerified).toBe('function');
|
|
15
|
+
expect(typeof provider.onAccessDecision).toBe('function');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should not throw on onReceiptIssued', () => {
|
|
19
|
+
expect(() =>
|
|
20
|
+
noopProvider.onReceiptIssued({
|
|
21
|
+
receiptHash: 'sha256:test',
|
|
22
|
+
})
|
|
23
|
+
).not.toThrow();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should not throw on onReceiptVerified', () => {
|
|
27
|
+
expect(() =>
|
|
28
|
+
noopProvider.onReceiptVerified({
|
|
29
|
+
receiptHash: 'sha256:test',
|
|
30
|
+
valid: true,
|
|
31
|
+
})
|
|
32
|
+
).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should not throw on onAccessDecision', () => {
|
|
36
|
+
expect(() =>
|
|
37
|
+
noopProvider.onAccessDecision({
|
|
38
|
+
decision: 'allow',
|
|
39
|
+
})
|
|
40
|
+
).not.toThrow();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return undefined from all methods', () => {
|
|
44
|
+
const issued = noopProvider.onReceiptIssued({
|
|
45
|
+
receiptHash: 'sha256:test',
|
|
46
|
+
});
|
|
47
|
+
const verified = noopProvider.onReceiptVerified({
|
|
48
|
+
receiptHash: 'sha256:test',
|
|
49
|
+
valid: true,
|
|
50
|
+
});
|
|
51
|
+
const decision = noopProvider.onAccessDecision({
|
|
52
|
+
decision: 'allow',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(issued).toBeUndefined();
|
|
56
|
+
expect(verified).toBeUndefined();
|
|
57
|
+
expect(decision).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should handle complex input without errors', () => {
|
|
61
|
+
expect(() =>
|
|
62
|
+
noopProvider.onReceiptIssued({
|
|
63
|
+
receiptHash: 'sha256:complex',
|
|
64
|
+
policyHash: 'sha256:policy',
|
|
65
|
+
issuer: 'https://api.example.com',
|
|
66
|
+
kid: '2025-01-01T00:00:00Z',
|
|
67
|
+
http: { method: 'POST', path: '/api/v1/resource' },
|
|
68
|
+
durationMs: 150,
|
|
69
|
+
})
|
|
70
|
+
).not.toThrow();
|
|
71
|
+
|
|
72
|
+
expect(() =>
|
|
73
|
+
noopProvider.onReceiptVerified({
|
|
74
|
+
receiptHash: 'sha256:complex',
|
|
75
|
+
issuer: 'https://api.example.com',
|
|
76
|
+
kid: '2025-01-01',
|
|
77
|
+
valid: false,
|
|
78
|
+
reasonCode: 'SIGNATURE_INVALID',
|
|
79
|
+
http: { method: 'GET', path: '/verify' },
|
|
80
|
+
durationMs: 25,
|
|
81
|
+
})
|
|
82
|
+
).not.toThrow();
|
|
83
|
+
|
|
84
|
+
expect(() =>
|
|
85
|
+
noopProvider.onAccessDecision({
|
|
86
|
+
receiptHash: 'sha256:complex',
|
|
87
|
+
policyHash: 'sha256:policy',
|
|
88
|
+
decision: 'deny',
|
|
89
|
+
reasonCode: 'INSUFFICIENT_PAYMENT',
|
|
90
|
+
payment: { rail: 'stripe', amount: 500, currency: 'USD' },
|
|
91
|
+
http: { method: 'POST', path: '/protected' },
|
|
92
|
+
})
|
|
93
|
+
).not.toThrow();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should be usable as default provider', () => {
|
|
97
|
+
// Simulate setting as default
|
|
98
|
+
let currentProvider: TelemetryProvider | undefined = noopProvider;
|
|
99
|
+
|
|
100
|
+
// Should work without errors
|
|
101
|
+
currentProvider.onReceiptIssued({ receiptHash: 'sha256:test' });
|
|
102
|
+
currentProvider.onReceiptVerified({ receiptHash: 'sha256:test', valid: true });
|
|
103
|
+
currentProvider.onAccessDecision({ decision: 'allow' });
|
|
104
|
+
|
|
105
|
+
// Should be replaceable
|
|
106
|
+
currentProvider = undefined;
|
|
107
|
+
expect(currentProvider).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @peac/telemetry - Provider registry tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
providerRef,
|
|
8
|
+
setTelemetryProvider,
|
|
9
|
+
getTelemetryProvider,
|
|
10
|
+
isTelemetryEnabled,
|
|
11
|
+
} from '../src/provider.js';
|
|
12
|
+
import { noopProvider } from '../src/noop.js';
|
|
13
|
+
import type { TelemetryProvider } from '../src/types.js';
|
|
14
|
+
|
|
15
|
+
describe('providerRef', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Reset provider before each test
|
|
18
|
+
providerRef.current = undefined;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should start with undefined (disabled)', () => {
|
|
22
|
+
expect(providerRef.current).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should allow direct assignment', () => {
|
|
26
|
+
providerRef.current = noopProvider;
|
|
27
|
+
expect(providerRef.current).toBe(noopProvider);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should allow setting to undefined', () => {
|
|
31
|
+
providerRef.current = noopProvider;
|
|
32
|
+
providerRef.current = undefined;
|
|
33
|
+
expect(providerRef.current).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('setTelemetryProvider', () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
providerRef.current = undefined;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should set the provider', () => {
|
|
43
|
+
setTelemetryProvider(noopProvider);
|
|
44
|
+
expect(providerRef.current).toBe(noopProvider);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should allow undefined to disable', () => {
|
|
48
|
+
setTelemetryProvider(noopProvider);
|
|
49
|
+
setTelemetryProvider(undefined);
|
|
50
|
+
expect(providerRef.current).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should be idempotent', () => {
|
|
54
|
+
setTelemetryProvider(noopProvider);
|
|
55
|
+
setTelemetryProvider(noopProvider);
|
|
56
|
+
setTelemetryProvider(noopProvider);
|
|
57
|
+
expect(providerRef.current).toBe(noopProvider);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should allow replacing provider', () => {
|
|
61
|
+
const provider1: TelemetryProvider = {
|
|
62
|
+
onReceiptIssued: vi.fn(),
|
|
63
|
+
onReceiptVerified: vi.fn(),
|
|
64
|
+
onAccessDecision: vi.fn(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const provider2: TelemetryProvider = {
|
|
68
|
+
onReceiptIssued: vi.fn(),
|
|
69
|
+
onReceiptVerified: vi.fn(),
|
|
70
|
+
onAccessDecision: vi.fn(),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
setTelemetryProvider(provider1);
|
|
74
|
+
expect(providerRef.current).toBe(provider1);
|
|
75
|
+
|
|
76
|
+
setTelemetryProvider(provider2);
|
|
77
|
+
expect(providerRef.current).toBe(provider2);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should not throw', () => {
|
|
81
|
+
expect(() => setTelemetryProvider(noopProvider)).not.toThrow();
|
|
82
|
+
expect(() => setTelemetryProvider(undefined)).not.toThrow();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('getTelemetryProvider', () => {
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
providerRef.current = undefined;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should return undefined when no provider set', () => {
|
|
92
|
+
expect(getTelemetryProvider()).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should return the current provider', () => {
|
|
96
|
+
setTelemetryProvider(noopProvider);
|
|
97
|
+
expect(getTelemetryProvider()).toBe(noopProvider);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return same value as providerRef.current', () => {
|
|
101
|
+
setTelemetryProvider(noopProvider);
|
|
102
|
+
expect(getTelemetryProvider()).toBe(providerRef.current);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('isTelemetryEnabled', () => {
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
providerRef.current = undefined;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should return false when disabled', () => {
|
|
112
|
+
expect(isTelemetryEnabled()).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should return true when provider set', () => {
|
|
116
|
+
setTelemetryProvider(noopProvider);
|
|
117
|
+
expect(isTelemetryEnabled()).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should return false after disabling', () => {
|
|
121
|
+
setTelemetryProvider(noopProvider);
|
|
122
|
+
setTelemetryProvider(undefined);
|
|
123
|
+
expect(isTelemetryEnabled()).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('hot path pattern', () => {
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
providerRef.current = undefined;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should skip telemetry when disabled', () => {
|
|
133
|
+
const mockFn = vi.fn();
|
|
134
|
+
|
|
135
|
+
// Hot path pattern
|
|
136
|
+
const p = providerRef.current;
|
|
137
|
+
if (p) {
|
|
138
|
+
mockFn();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
expect(mockFn).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should call telemetry when enabled', () => {
|
|
145
|
+
const mockProvider: TelemetryProvider = {
|
|
146
|
+
onReceiptIssued: vi.fn(),
|
|
147
|
+
onReceiptVerified: vi.fn(),
|
|
148
|
+
onAccessDecision: vi.fn(),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
setTelemetryProvider(mockProvider);
|
|
152
|
+
|
|
153
|
+
// Hot path pattern
|
|
154
|
+
const p = providerRef.current;
|
|
155
|
+
if (p) {
|
|
156
|
+
p.onReceiptIssued({ receiptHash: 'sha256:test' });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
expect(mockProvider.onReceiptIssued).toHaveBeenCalledWith({
|
|
160
|
+
receiptHash: 'sha256:test',
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should guard against throwing providers', () => {
|
|
165
|
+
const throwingProvider: TelemetryProvider = {
|
|
166
|
+
onReceiptIssued: () => {
|
|
167
|
+
throw new Error('Provider error');
|
|
168
|
+
},
|
|
169
|
+
onReceiptVerified: vi.fn(),
|
|
170
|
+
onAccessDecision: vi.fn(),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
setTelemetryProvider(throwingProvider);
|
|
174
|
+
|
|
175
|
+
// Hot path pattern with guard
|
|
176
|
+
const p = providerRef.current;
|
|
177
|
+
if (p) {
|
|
178
|
+
try {
|
|
179
|
+
p.onReceiptIssued({ receiptHash: 'sha256:test' });
|
|
180
|
+
} catch {
|
|
181
|
+
// Telemetry MUST NOT break core flow - swallow silently
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Should not throw, test passes if we get here
|
|
186
|
+
expect(true).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should have zero overhead when disabled (structural)', () => {
|
|
190
|
+
// This test documents the performance contract structurally
|
|
191
|
+
// When providerRef.current is undefined, no provider methods are called
|
|
192
|
+
// Time-based assertions are avoided to prevent CI flakes
|
|
193
|
+
|
|
194
|
+
const iterations = 1000;
|
|
195
|
+
let callCount = 0;
|
|
196
|
+
|
|
197
|
+
for (let i = 0; i < iterations; i++) {
|
|
198
|
+
const p = providerRef.current;
|
|
199
|
+
if (p) {
|
|
200
|
+
callCount++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Structural assertion: no calls when disabled
|
|
205
|
+
expect(callCount).toBe(0);
|
|
206
|
+
// The pattern above is the only overhead (single truthiness check)
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('concurrent access', () => {
|
|
211
|
+
beforeEach(() => {
|
|
212
|
+
providerRef.current = undefined;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should handle rapid enable/disable', () => {
|
|
216
|
+
for (let i = 0; i < 100; i++) {
|
|
217
|
+
setTelemetryProvider(noopProvider);
|
|
218
|
+
expect(isTelemetryEnabled()).toBe(true);
|
|
219
|
+
setTelemetryProvider(undefined);
|
|
220
|
+
expect(isTelemetryEnabled()).toBe(false);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @peac/telemetry - Type tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import type {
|
|
7
|
+
TelemetryDecision,
|
|
8
|
+
PrivacyMode,
|
|
9
|
+
TelemetryConfig,
|
|
10
|
+
TelemetryProvider,
|
|
11
|
+
ReceiptIssuedInput,
|
|
12
|
+
ReceiptVerifiedInput,
|
|
13
|
+
AccessDecisionInput,
|
|
14
|
+
HttpContext,
|
|
15
|
+
PaymentContext,
|
|
16
|
+
} from '../src/types.js';
|
|
17
|
+
|
|
18
|
+
describe('TelemetryDecision', () => {
|
|
19
|
+
it('should accept valid decision values', () => {
|
|
20
|
+
const allow: TelemetryDecision = 'allow';
|
|
21
|
+
const deny: TelemetryDecision = 'deny';
|
|
22
|
+
const unknown: TelemetryDecision = 'unknown';
|
|
23
|
+
|
|
24
|
+
expect(allow).toBe('allow');
|
|
25
|
+
expect(deny).toBe('deny');
|
|
26
|
+
expect(unknown).toBe('unknown');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('PrivacyMode', () => {
|
|
31
|
+
it('should accept valid privacy mode values', () => {
|
|
32
|
+
const strict: PrivacyMode = 'strict';
|
|
33
|
+
const balanced: PrivacyMode = 'balanced';
|
|
34
|
+
const custom: PrivacyMode = 'custom';
|
|
35
|
+
|
|
36
|
+
expect(strict).toBe('strict');
|
|
37
|
+
expect(balanced).toBe('balanced');
|
|
38
|
+
expect(custom).toBe('custom');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('TelemetryConfig', () => {
|
|
43
|
+
it('should require serviceName', () => {
|
|
44
|
+
const config: TelemetryConfig = {
|
|
45
|
+
serviceName: 'my-service',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
expect(config.serviceName).toBe('my-service');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should accept optional privacy mode', () => {
|
|
52
|
+
const config: TelemetryConfig = {
|
|
53
|
+
serviceName: 'my-service',
|
|
54
|
+
privacyMode: 'strict',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
expect(config.privacyMode).toBe('strict');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should accept optional allowlist', () => {
|
|
61
|
+
const config: TelemetryConfig = {
|
|
62
|
+
serviceName: 'my-service',
|
|
63
|
+
privacyMode: 'custom',
|
|
64
|
+
allowAttributes: ['peac.receipt.hash', 'peac.decision'],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
expect(config.allowAttributes).toEqual(['peac.receipt.hash', 'peac.decision']);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should accept optional redaction hook', () => {
|
|
71
|
+
const redact = (attrs: Record<string, unknown>) => {
|
|
72
|
+
const result = { ...attrs };
|
|
73
|
+
delete result['sensitive'];
|
|
74
|
+
return result;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const config: TelemetryConfig = {
|
|
78
|
+
serviceName: 'my-service',
|
|
79
|
+
redact,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
expect(config.redact?.({ sensitive: 'data', safe: 'value' })).toEqual({
|
|
83
|
+
safe: 'value',
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should accept enableExperimentalGenAI flag', () => {
|
|
88
|
+
const config: TelemetryConfig = {
|
|
89
|
+
serviceName: 'my-service',
|
|
90
|
+
enableExperimentalGenAI: true,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
expect(config.enableExperimentalGenAI).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('ReceiptIssuedInput', () => {
|
|
98
|
+
it('should require receiptHash', () => {
|
|
99
|
+
const input: ReceiptIssuedInput = {
|
|
100
|
+
receiptHash: 'sha256:abc123',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
expect(input.receiptHash).toBe('sha256:abc123');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should accept optional fields', () => {
|
|
107
|
+
const input: ReceiptIssuedInput = {
|
|
108
|
+
receiptHash: 'sha256:abc123',
|
|
109
|
+
policyHash: 'sha256:policy',
|
|
110
|
+
issuer: 'https://api.example.com',
|
|
111
|
+
kid: '2025-01-01',
|
|
112
|
+
http: { method: 'POST', path: '/api/resource' },
|
|
113
|
+
durationMs: 42,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
expect(input.policyHash).toBe('sha256:policy');
|
|
117
|
+
expect(input.issuer).toBe('https://api.example.com');
|
|
118
|
+
expect(input.kid).toBe('2025-01-01');
|
|
119
|
+
expect(input.http?.method).toBe('POST');
|
|
120
|
+
expect(input.durationMs).toBe(42);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('ReceiptVerifiedInput', () => {
|
|
125
|
+
it('should require receiptHash and valid', () => {
|
|
126
|
+
const input: ReceiptVerifiedInput = {
|
|
127
|
+
receiptHash: 'sha256:abc123',
|
|
128
|
+
valid: true,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
expect(input.receiptHash).toBe('sha256:abc123');
|
|
132
|
+
expect(input.valid).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should accept reason code for failed verification', () => {
|
|
136
|
+
const input: ReceiptVerifiedInput = {
|
|
137
|
+
receiptHash: 'sha256:abc123',
|
|
138
|
+
valid: false,
|
|
139
|
+
reasonCode: 'SIGNATURE_INVALID',
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
expect(input.valid).toBe(false);
|
|
143
|
+
expect(input.reasonCode).toBe('SIGNATURE_INVALID');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('AccessDecisionInput', () => {
|
|
148
|
+
it('should require decision', () => {
|
|
149
|
+
const input: AccessDecisionInput = {
|
|
150
|
+
decision: 'allow',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
expect(input.decision).toBe('allow');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should accept payment context', () => {
|
|
157
|
+
const input: AccessDecisionInput = {
|
|
158
|
+
decision: 'allow',
|
|
159
|
+
payment: {
|
|
160
|
+
rail: 'stripe',
|
|
161
|
+
amount: 9999,
|
|
162
|
+
currency: 'USD',
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
expect(input.payment?.rail).toBe('stripe');
|
|
167
|
+
expect(input.payment?.amount).toBe(9999);
|
|
168
|
+
expect(input.payment?.currency).toBe('USD');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('HttpContext', () => {
|
|
173
|
+
it('should accept method and path', () => {
|
|
174
|
+
const ctx: HttpContext = {
|
|
175
|
+
method: 'GET',
|
|
176
|
+
path: '/api/v1/resource',
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
expect(ctx.method).toBe('GET');
|
|
180
|
+
expect(ctx.path).toBe('/api/v1/resource');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should allow partial context', () => {
|
|
184
|
+
const methodOnly: HttpContext = { method: 'POST' };
|
|
185
|
+
const pathOnly: HttpContext = { path: '/health' };
|
|
186
|
+
|
|
187
|
+
expect(methodOnly.method).toBe('POST');
|
|
188
|
+
expect(pathOnly.path).toBe('/health');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('PaymentContext', () => {
|
|
193
|
+
it('should accept rail, amount, and currency', () => {
|
|
194
|
+
const ctx: PaymentContext = {
|
|
195
|
+
rail: 'x402',
|
|
196
|
+
amount: 1500,
|
|
197
|
+
currency: 'USD',
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
expect(ctx.rail).toBe('x402');
|
|
201
|
+
expect(ctx.amount).toBe(1500);
|
|
202
|
+
expect(ctx.currency).toBe('USD');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('TelemetryProvider', () => {
|
|
207
|
+
it('should define all required methods', () => {
|
|
208
|
+
const provider: TelemetryProvider = {
|
|
209
|
+
onReceiptIssued: () => {},
|
|
210
|
+
onReceiptVerified: () => {},
|
|
211
|
+
onAccessDecision: () => {},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
expect(typeof provider.onReceiptIssued).toBe('function');
|
|
215
|
+
expect(typeof provider.onReceiptVerified).toBe('function');
|
|
216
|
+
expect(typeof provider.onAccessDecision).toBe('function');
|
|
217
|
+
});
|
|
218
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"composite": true,
|
|
7
|
+
"noEmit": false
|
|
8
|
+
},
|
|
9
|
+
"include": ["src/**/*.ts"],
|
|
10
|
+
"exclude": ["node_modules", "dist", "tests"],
|
|
11
|
+
"references": [{ "path": "../kernel" }]
|
|
12
|
+
}
|