@peac/telemetry 0.10.8 → 0.10.10

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/src/provider.ts DELETED
@@ -1,64 +0,0 @@
1
- /**
2
- * @peac/telemetry - Provider registry
3
- *
4
- * Zero-throw provider ref pattern for hot-path performance.
5
- * When undefined, telemetry is disabled with NO function calls.
6
- */
7
-
8
- import type { TelemetryProvider } from './types.js';
9
-
10
- /**
11
- * Singleton provider reference for zero-overhead hot path.
12
- *
13
- * When undefined, telemetry is disabled with NO function calls
14
- * beyond the initial `if (!p)` check.
15
- *
16
- * @example
17
- * ```typescript
18
- * // In hot path (issue/verify)
19
- * const p = providerRef.current;
20
- * if (p) {
21
- * try {
22
- * p.onReceiptIssued({ receiptHash: '...' });
23
- * } catch {
24
- * // Telemetry MUST NOT break core flow
25
- * }
26
- * }
27
- * ```
28
- */
29
- export const providerRef: { current?: TelemetryProvider } = {
30
- current: undefined,
31
- };
32
-
33
- /**
34
- * Set the telemetry provider.
35
- *
36
- * Idempotent, no-throw, safe to call multiple times.
37
- * Pass undefined to disable telemetry.
38
- *
39
- * @param provider - The provider to use, or undefined to disable
40
- */
41
- export function setTelemetryProvider(provider: TelemetryProvider | undefined): void {
42
- providerRef.current = provider;
43
- }
44
-
45
- /**
46
- * Get the current telemetry provider.
47
- *
48
- * Returns undefined if no provider is set (telemetry disabled).
49
- *
50
- * For hot paths, prefer direct access to `providerRef.current`
51
- * to avoid the function call overhead.
52
- */
53
- export function getTelemetryProvider(): TelemetryProvider | undefined {
54
- return providerRef.current;
55
- }
56
-
57
- /**
58
- * Check if telemetry is enabled.
59
- *
60
- * Convenience function for conditional logic outside hot paths.
61
- */
62
- export function isTelemetryEnabled(): boolean {
63
- return providerRef.current !== undefined;
64
- }
package/src/types.ts DELETED
@@ -1,163 +0,0 @@
1
- /**
2
- * @peac/telemetry - Telemetry types and interfaces
3
- *
4
- * This module defines the core telemetry interfaces for PEAC protocol.
5
- * These are runtime-portable and have no external dependencies.
6
- */
7
-
8
- /**
9
- * Telemetry decision outcome
10
- */
11
- export type TelemetryDecision = 'allow' | 'deny' | 'unknown';
12
-
13
- /**
14
- * Privacy mode for telemetry emission
15
- *
16
- * - strict: Hash all identifiers, emit minimal data (production default)
17
- * - balanced: Hash identifiers but include rail + amounts (debugging)
18
- * - custom: Use allowlist-based filtering
19
- */
20
- export type PrivacyMode = 'strict' | 'balanced' | 'custom';
21
-
22
- /**
23
- * Telemetry configuration
24
- */
25
- export interface TelemetryConfig {
26
- /** Service name for resource identification */
27
- serviceName: string;
28
-
29
- /** Privacy mode: strict (hashes only), balanced, custom */
30
- privacyMode?: PrivacyMode;
31
-
32
- /** Allowlist for custom mode - only these attribute keys are emitted */
33
- allowAttributes?: string[];
34
-
35
- /** Custom redaction hook for edge cases */
36
- redact?: (attrs: Record<string, unknown>) => Record<string, unknown>;
37
-
38
- /** Enable experimental GenAI semantic conventions (default: false) */
39
- enableExperimentalGenAI?: boolean;
40
-
41
- /** Salt for privacy-preserving hashing (should be configured per deployment) */
42
- hashSalt?: string;
43
- }
44
-
45
- /**
46
- * Input for receipt issued telemetry event
47
- */
48
- export interface ReceiptIssuedInput {
49
- /** Hash of the receipt (never raw content) */
50
- receiptHash: string;
51
-
52
- /** Hash of the policy used */
53
- policyHash?: string;
54
-
55
- /** Issuer identifier (may be hashed based on privacy mode) */
56
- issuer?: string;
57
-
58
- /** Key ID used for signing */
59
- kid?: string;
60
-
61
- /** HTTP context (privacy-safe: path only, no query) */
62
- http?: HttpContext;
63
-
64
- /** Duration of issue operation in milliseconds */
65
- durationMs?: number;
66
- }
67
-
68
- /**
69
- * Input for receipt verified telemetry event
70
- */
71
- export interface ReceiptVerifiedInput {
72
- /** Hash of the receipt */
73
- receiptHash: string;
74
-
75
- /** Issuer identifier */
76
- issuer?: string;
77
-
78
- /** Key ID used */
79
- kid?: string;
80
-
81
- /** Whether verification succeeded */
82
- valid: boolean;
83
-
84
- /** Reason code if verification failed */
85
- reasonCode?: string;
86
-
87
- /** HTTP context */
88
- http?: HttpContext;
89
-
90
- /** Duration of verify operation in milliseconds */
91
- durationMs?: number;
92
- }
93
-
94
- /**
95
- * Input for access decision telemetry event
96
- */
97
- export interface AccessDecisionInput {
98
- /** Hash of the receipt (if present) */
99
- receiptHash?: string;
100
-
101
- /** Hash of the policy evaluated */
102
- policyHash?: string;
103
-
104
- /** Decision outcome */
105
- decision: TelemetryDecision;
106
-
107
- /** Reason code for the decision */
108
- reasonCode?: string;
109
-
110
- /** Payment context (balanced/custom mode only) */
111
- payment?: PaymentContext;
112
-
113
- /** HTTP context */
114
- http?: HttpContext;
115
- }
116
-
117
- /**
118
- * HTTP context for telemetry (privacy-safe subset)
119
- */
120
- export interface HttpContext {
121
- /** HTTP method */
122
- method?: string;
123
-
124
- /** URL path (no query string) */
125
- path?: string;
126
- }
127
-
128
- /**
129
- * Payment context for telemetry (balanced/custom mode only)
130
- */
131
- export interface PaymentContext {
132
- /** Payment rail identifier */
133
- rail?: string;
134
-
135
- /** Amount in minor units */
136
- amount?: number;
137
-
138
- /** Currency code */
139
- currency?: string;
140
- }
141
-
142
- /**
143
- * Telemetry provider interface
144
- *
145
- * Implementations SHOULD be no-throw. PEAC guards all calls,
146
- * but well-behaved providers should not throw.
147
- */
148
- export interface TelemetryProvider {
149
- /**
150
- * Called when a receipt is issued
151
- */
152
- onReceiptIssued(input: ReceiptIssuedInput): void;
153
-
154
- /**
155
- * Called when a receipt is verified
156
- */
157
- onReceiptVerified(input: ReceiptVerifiedInput): void;
158
-
159
- /**
160
- * Called when an access decision is made
161
- */
162
- onAccessDecision(input: AccessDecisionInput): void;
163
- }
@@ -1,126 +0,0 @@
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
- });
@@ -1,109 +0,0 @@
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
- });
@@ -1,223 +0,0 @@
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
- });