@oxy.so/telemetry 1.0.0 → 1.1.1

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/edge.ts ADDED
@@ -0,0 +1,117 @@
1
+ import { trafficType, type TrafficAggregate } from './collector.js';
2
+ import { normalizeEdgePop } from './server.js';
3
+
4
+ /** Server-only bindings; never expose them through frontend build variables. */
5
+ export interface EdgeActivityEnv {
6
+ OXY_EDGE_ACTIVITY_ENABLED?: string;
7
+ OXY_EDGE_ACTIVITY_API_KEY?: string;
8
+ OXY_EDGE_ACTIVITY_API_SECRET?: string;
9
+ OXY_EDGE_ACTIVITY_API_URL?: string;
10
+ }
11
+ export interface EdgeActivityContext { waitUntil(promise: Promise<unknown>): void }
12
+ export interface EdgeActivityOptions {
13
+ service: string;
14
+ request: Request & { cf?: { colo?: unknown } };
15
+ env: EdgeActivityEnv;
16
+ ctx: EdgeActivityContext;
17
+ next: () => Response | Promise<Response>;
18
+ credential?: () => Promise<string>;
19
+ fetcher?: typeof fetch;
20
+ /** Only supply after existing authentication verified this service identity. */
21
+ peer?: { service: string; region?: string };
22
+ /** Fixed message only: never credentials, URLs or upstream error bodies. */
23
+ onError?: (message: string) => void;
24
+ }
25
+ interface CachedToken { key: string; secret: string; origin: string; token: string; expiresAt: number }
26
+ // Share values only: Workers prohibit I/O promises/bodies crossing request contexts.
27
+ const tokens = new WeakMap<EdgeActivityEnv, CachedToken>();
28
+ const CONTROL = /^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|platform-infrastructure|infra-status|internal\/activity|auth\/service-token|cdn-cgi)(?:\/|$)/;
29
+
30
+ async function smallJson(response: Response): Promise<unknown> {
31
+ if (!response.body) throw new Error('Missing token response');
32
+ const reader = response.body.getReader();
33
+ const chunks: Uint8Array[] = [];
34
+ let size = 0;
35
+ try {
36
+ for (;;) {
37
+ const { done, value } = await reader.read();
38
+ if (done) break;
39
+ size += value.byteLength;
40
+ if (size > 16_384) { await reader.cancel(); throw new Error('Oversized token response'); }
41
+ chunks.push(value);
42
+ }
43
+ } finally { reader.releaseLock(); }
44
+ const bytes = new Uint8Array(size);
45
+ let offset = 0;
46
+ for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
47
+ return JSON.parse(new TextDecoder().decode(bytes));
48
+ }
49
+
50
+ async function publish(options: EdgeActivityOptions, events: TrafficAggregate[]): Promise<void> {
51
+ const { env } = options;
52
+ const base = new URL(env.OXY_EDGE_ACTIVITY_API_URL ?? 'https://api.oxy.so');
53
+ if (base.protocol !== 'https:' || base.username || base.password || base.pathname !== '/' || base.search || base.hash) throw new Error('Invalid activity origin');
54
+ const fetcher = options.fetcher ?? fetch;
55
+ const abort = new AbortController();
56
+ const timeout = setTimeout(() => abort.abort(), 4_000);
57
+ try {
58
+ let token: string;
59
+ if (options.credential) token = await Promise.race([
60
+ options.credential(),
61
+ new Promise<never>((_, reject) => abort.signal.addEventListener('abort', () => reject(new Error('Activity credential timeout')), { once: true })),
62
+ ]);
63
+ else {
64
+ const key = env.OXY_EDGE_ACTIVITY_API_KEY;
65
+ const secret = env.OXY_EDGE_ACTIVITY_API_SECRET;
66
+ if (!key || !secret) throw new Error('Missing edge activity credentials');
67
+ const cached = tokens.get(env);
68
+ if (cached && cached.key === key && cached.secret === secret && cached.origin === base.origin && cached.expiresAt > Date.now() + 60_000) token = cached.token;
69
+ else {
70
+ // Same exchange and expiry margin as OxyServices.getServiceToken().
71
+ const response = await fetcher(new URL('/auth/service-token', base), {
72
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
73
+ body: JSON.stringify({ apiKey: key, apiSecret: secret }), signal: abort.signal,
74
+ });
75
+ if (!response.ok) { await response.body?.cancel(); throw new Error('Activity authentication failed'); }
76
+ const data = await smallJson(response) as { token?: unknown; expiresIn?: unknown };
77
+ if (typeof data.token !== 'string' || !data.token || typeof data.expiresIn !== 'number' || !Number.isFinite(data.expiresIn) || data.expiresIn <= 0) throw new Error('Invalid activity token');
78
+ token = data.token;
79
+ tokens.set(env, { key, secret, origin: base.origin, token, expiresAt: Date.now() + data.expiresIn * 1_000 });
80
+ }
81
+ }
82
+ const response = await fetcher(new URL('/internal/activity', base), {
83
+ method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
84
+ body: JSON.stringify(events), signal: abort.signal,
85
+ });
86
+ if (response.status === 401) tokens.delete(env);
87
+ await response.body?.cancel();
88
+ if (!response.ok) throw new Error('Activity ingestion failed');
89
+ } finally { clearTimeout(timeout); }
90
+ }
91
+
92
+ /** Returns the original response/stream and preserves original handler errors. */
93
+ export async function observeEdgeRequest(options: EdgeActivityOptions): Promise<Response> {
94
+ const report = () => { try { (options.onError ?? console.error)('Edge activity publication failed'); } catch { /* Never break serving. */ } };
95
+ const enabled = options.env.OXY_EDGE_ACTIVITY_ENABLED;
96
+ if (enabled === undefined || enabled === 'false') return options.next();
97
+ if (enabled !== 'true' || !/^[a-z][a-z0-9-]{0,39}$/.test(options.service)) { report(); return options.next(); }
98
+ const path = new URL(options.request.url).pathname;
99
+ if (CONTROL.test(path)) return options.next();
100
+ const pop = normalizeEdgePop(typeof options.request.cf?.colo === 'string' ? options.request.cf.colo : undefined);
101
+ const region = pop ? `edge-${pop}` : 'unknown';
102
+ const startedAt = new Date().toISOString();
103
+ const complete = (response?: Response) => {
104
+ const contentType = response?.headers.get('Content-Type') ?? '';
105
+ const activityType = /^(?:image|audio|video)\//i.test(contentType) ? 'media' : trafficType(path);
106
+ const common = { service: options.service, region, scope: options.peer ? 'internal' as const : 'external' as const, activityType, requests: 1, windowStartedAt: startedAt, emittedAt: new Date().toISOString() };
107
+ // The serving PoP is infrastructure metadata. Do not invent visitor location.
108
+ const events: TrafficAggregate[] = [{ ...common, direction: 'inbound', sourceRegion: options.peer?.region, sourceService: options.peer?.service, targetRegion: region, targetService: options.service }];
109
+ if (response) events.push({ ...common, direction: 'outbound', sourceRegion: region, sourceService: options.service, targetRegion: options.peer?.region, targetService: options.peer?.service });
110
+ try { options.ctx.waitUntil(publish(options, events).catch(report)); } catch { report(); }
111
+ };
112
+ let response: Response;
113
+ try { response = await options.next(); }
114
+ catch (error) { complete(); throw error; }
115
+ complete(response);
116
+ return response;
117
+ }
package/src/socket.ts ADDED
@@ -0,0 +1,131 @@
1
+ import { normalizeInfrastructureRegion, type TrafficFlow, type TrafficType } from './collector.js';
2
+ import { metadataFromHeaders, type TelemetryHeaders } from './server.js';
3
+
4
+ export interface TrafficSocket {
5
+ handshake: { headers: TelemetryHeaders };
6
+ nsp?: { name: string };
7
+ onAny(listener: (event: string) => void): unknown;
8
+ onAnyOutgoing(listener: (event: string) => void): unknown;
9
+ offAny(listener: (event: string) => void): unknown;
10
+ offAnyOutgoing(listener: (event: string) => void): unknown;
11
+ once(event: 'disconnect', listener: () => void): unknown;
12
+ off(event: 'disconnect', listener: () => void): unknown;
13
+ }
14
+
15
+ const CATEGORIES: Record<string, TrafficType> = {
16
+ auth: 'identity', session: 'identity', user: 'identity', profile: 'identity',
17
+ chat: 'ai', agent: 'ai', inference: 'ai', completion: 'ai',
18
+ message: 'communication', notification: 'communication', typing: 'communication',
19
+ media: 'media', audio: 'media', video: 'media', stream: 'media', player: 'media',
20
+ track: 'media', playback: 'media', playlist: 'media', room: 'media',
21
+ };
22
+
23
+ /** Event names are reduced locally to five categories; payloads are never read. */
24
+ export function socketTrafficType(event: string): TrafficType {
25
+ return CATEGORIES[event.toLowerCase().split(/[:/._-]/, 1)[0]] ?? 'platform';
26
+ }
27
+
28
+ /**
29
+ * Observes application events only, once per local socket. Socket.IO catch-all
30
+ * hooks exclude acknowledgements, ping/pong and transport frames. Outgoing
31
+ * packets describe sends, not delivery acknowledgements. No connection ID,
32
+ * event name, IP, credential, room name or payload enters a traffic record.
33
+ */
34
+ export function observeTrafficSocket(
35
+ socket: TrafficSocket,
36
+ collector: { record(flow: TrafficFlow): void },
37
+ service: string,
38
+ region: string,
39
+ options: { getPeer?: () => { service: string; region?: string } | undefined } = {},
40
+ ): () => void {
41
+ if (/^\/platform-(?:activity|stats)(?:\/|$)/.test(socket.nsp?.name ?? '')) return () => {};
42
+ let stopped = false;
43
+ const record = (event: string, direction: 'inbound' | 'outbound') => {
44
+ if (stopped) return;
45
+ const peer = options.getPeer?.();
46
+ const peerService = peer && /^[a-z][a-z0-9-]{0,39}$/.test(peer.service) ? peer.service : undefined;
47
+ const edgePop = metadataFromHeaders(socket.handshake.headers).edgePop;
48
+ const peerRegion = peerService ? normalizeInfrastructureRegion(peer?.region) : edgePop ? `edge-${edgePop}` : undefined;
49
+ collector.record({
50
+ region, service, scope: peerService ? 'internal' : 'external', direction,
51
+ activityType: socketTrafficType(event),
52
+ sourceRegion: direction === 'inbound' ? peerRegion : region,
53
+ targetRegion: direction === 'inbound' ? region : peerRegion,
54
+ sourceService: direction === 'inbound' ? peerService : service,
55
+ targetService: direction === 'inbound' ? service : peerService,
56
+ });
57
+ };
58
+ const inbound = (event: string) => record(event, 'inbound');
59
+ const outbound = (event: string) => record(event, 'outbound');
60
+ const stop = () => {
61
+ if (stopped) return;
62
+ stopped = true;
63
+ socket.offAny(inbound);
64
+ socket.offAnyOutgoing(outbound);
65
+ socket.off('disconnect', stop);
66
+ };
67
+ socket.onAny(inbound);
68
+ socket.onAnyOutgoing(outbound);
69
+ socket.once('disconnect', stop);
70
+ return stop;
71
+ }
72
+
73
+ export interface TrafficWebSocket {
74
+ readonly readyState: number;
75
+ // ws accepts data/options overloads; they are forwarded without inspecting
76
+ // or changing them. Keep this boundary permissive for Node ws versions.
77
+ send(...args: any[]): unknown;
78
+ on(event: 'message', listener: () => void): unknown;
79
+ once(event: 'close', listener: () => void): unknown;
80
+ off(event: 'message' | 'close', listener: () => void): unknown;
81
+ }
82
+
83
+ /** Raw ws application messages. Sends are attempts, never delivery receipts. */
84
+ export function observeTrafficWebSocket(
85
+ socket: TrafficWebSocket,
86
+ collector: { record(flow: TrafficFlow): void },
87
+ service: string,
88
+ region: string,
89
+ options: {
90
+ headers?: TelemetryHeaders;
91
+ activityType?: TrafficType;
92
+ getPeer?: () => { service: string; region?: string } | undefined;
93
+ } = {},
94
+ ): () => void {
95
+ let stopped = false;
96
+ const record = (direction: 'inbound' | 'outbound') => {
97
+ if (stopped) return;
98
+ const peer = options.getPeer?.();
99
+ const peerService = peer && /^[a-z][a-z0-9-]{0,39}$/.test(peer.service) ? peer.service : undefined;
100
+ const edgePop = metadataFromHeaders(options.headers ?? {}).edgePop;
101
+ const peerRegion = peerService ? normalizeInfrastructureRegion(peer?.region) : edgePop ? `edge-${edgePop}` : undefined;
102
+ collector.record({
103
+ service, region, scope: peerService ? 'internal' : 'external', direction,
104
+ activityType: options.activityType ?? 'communication',
105
+ sourceRegion: direction === 'inbound' ? peerRegion : region,
106
+ targetRegion: direction === 'inbound' ? region : peerRegion,
107
+ sourceService: direction === 'inbound' ? peerService : service,
108
+ targetService: direction === 'inbound' ? service : peerService,
109
+ });
110
+ };
111
+ const inbound = () => record('inbound');
112
+ const originalSend = socket.send;
113
+ const send: TrafficWebSocket['send'] = function (this: TrafficWebSocket, ...args) {
114
+ const open = this.readyState === 1;
115
+ // Preserve overload arguments, callback identity, return value and throws.
116
+ const result = Reflect.apply(originalSend, this, args);
117
+ if (open) record('outbound');
118
+ return result;
119
+ };
120
+ const stop = () => {
121
+ if (stopped) return;
122
+ stopped = true;
123
+ socket.off('message', inbound);
124
+ socket.off('close', stop);
125
+ if (socket.send === send) socket.send = originalSend;
126
+ };
127
+ socket.send = send;
128
+ socket.on('message', inbound);
129
+ socket.once('close', stop);
130
+ return stop;
131
+ }