@oxy.so/telemetry 1.1.1 → 2.0.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.
package/src/collector.ts CHANGED
@@ -21,11 +21,34 @@ export interface TrafficAggregate extends TrafficFlow {
21
21
  }
22
22
 
23
23
  const TYPES: Record<string, TrafficType> = {
24
- auth: 'identity', session: 'identity', users: 'identity', profiles: 'identity', accounts: 'identity',
25
- ai: 'ai', inference: 'ai', models: 'ai', chat: 'ai', completions: 'ai',
26
- stream: 'media', tracks: 'media', episodes: 'media', recordings: 'media', rooms: 'media',
27
- messages: 'communication', notifications: 'communication', mail: 'communication',
28
- media: 'media', files: 'media', assets: 'media', upload: 'media', uploads: 'media', cdn: 'media', storage: 'media', images: 'media', audio: 'media', videos: 'media',
24
+ auth: 'identity',
25
+ session: 'identity',
26
+ users: 'identity',
27
+ profiles: 'identity',
28
+ accounts: 'identity',
29
+ ai: 'ai',
30
+ inference: 'ai',
31
+ models: 'ai',
32
+ chat: 'ai',
33
+ completions: 'ai',
34
+ stream: 'media',
35
+ tracks: 'media',
36
+ episodes: 'media',
37
+ recordings: 'media',
38
+ rooms: 'media',
39
+ messages: 'communication',
40
+ notifications: 'communication',
41
+ mail: 'communication',
42
+ media: 'media',
43
+ files: 'media',
44
+ assets: 'media',
45
+ upload: 'media',
46
+ uploads: 'media',
47
+ cdn: 'media',
48
+ storage: 'media',
49
+ images: 'media',
50
+ audio: 'media',
51
+ videos: 'media',
29
52
  };
30
53
  export function trafficType(path: string): TrafficType {
31
54
  const parts = path.split('?')[0].split('/').filter(Boolean);
@@ -35,7 +58,10 @@ export function trafficType(path: string): TrafficType {
35
58
  return TYPES[parts[0] ?? ''] ?? 'platform';
36
59
  }
37
60
 
38
- export function createTrafficCollector(publish: (events: TrafficAggregate[]) => Promise<unknown>, now = Date.now) {
61
+ export function createTrafficCollector(
62
+ publish: (events: TrafficAggregate[]) => Promise<unknown>,
63
+ now = Date.now,
64
+ ) {
39
65
  const pending = new Map<string, { flow: TrafficFlow; requests: number; startedAt: number }>();
40
66
  let flushing: Promise<void> | null = null;
41
67
  return {
@@ -49,12 +75,22 @@ export function createTrafficCollector(publish: (events: TrafficAggregate[]) =>
49
75
  while (flushing) await flushing;
50
76
  if (pending.size === 0) return;
51
77
  const emittedAt = new Date(now()).toISOString();
52
- const events = [...pending.values()].map(({ flow, requests, startedAt }) => ({ ...flow, requests, windowStartedAt: new Date(startedAt).toISOString(), emittedAt }));
78
+ const events = [...pending.values()].map(({ flow, requests, startedAt }) => ({
79
+ ...flow,
80
+ requests,
81
+ windowStartedAt: new Date(startedAt).toISOString(),
82
+ emittedAt,
83
+ }));
53
84
  pending.clear();
54
85
  flushing = (async () => {
55
- for (let offset = 0; offset < events.length; offset += 256) await publish(events.slice(offset, offset + 256));
86
+ for (let offset = 0; offset < events.length; offset += 256)
87
+ await publish(events.slice(offset, offset + 256));
56
88
  })();
57
- try { await flushing; } finally { flushing = null; }
89
+ try {
90
+ await flushing;
91
+ } finally {
92
+ flushing = null;
93
+ }
58
94
  },
59
95
  };
60
96
  }
@@ -65,7 +101,10 @@ interface HttpRequest {
65
101
  url?: string;
66
102
  serviceApp?: { appName?: string };
67
103
  }
68
- interface HttpResponse { once(event: string, listener: () => void): unknown; setHeader?(name: string, value: string): unknown }
104
+ interface HttpResponse {
105
+ once(event: string, listener: () => void): unknown;
106
+ setHeader?(name: string, value: string): unknown;
107
+ }
69
108
 
70
109
  export function trafficMiddleware(
71
110
  collector: Pick<ReturnType<typeof createTrafficCollector>, 'record'>,
@@ -76,24 +115,58 @@ export function trafficMiddleware(
76
115
  response.setHeader?.('X-Oxy-Region', region);
77
116
  const path = request.path ?? request.url ?? '/';
78
117
  // Never report collection itself, probes or the dashboard transport.
79
- if (/^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|platform-infrastructure|infra-status)(?:\/|$)/.test(path) || path.startsWith('/internal/activity') || path.startsWith('/auth/service-token') || path.startsWith('/cdn-cgi/')) { next(); return; }
118
+ if (
119
+ /^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|platform-infrastructure|infra-status)(?:\/|$)/.test(
120
+ path,
121
+ ) ||
122
+ path.startsWith('/internal/activity') ||
123
+ path.startsWith('/auth/service-token') ||
124
+ path.startsWith('/cdn-cgi/')
125
+ ) {
126
+ next();
127
+ return;
128
+ }
80
129
  const { edgePop } = metadataFromHeaders(request.headers);
81
130
  const activityType = trafficType(path);
82
131
  let finished = false;
83
132
  const inboundFlow = (): TrafficFlow => {
84
133
  // Read service identity AFTER the route's shared auth middleware resolved it.
85
- const name = request.serviceApp?.appName?.toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 40);
134
+ const name = request.serviceApp?.appName
135
+ ?.toLowerCase()
136
+ .replace(/[^a-z0-9-]/g, '-')
137
+ .slice(0, 40);
86
138
  const internal = Boolean(name);
87
139
  const forwardedRegion = request.headers['x-oxy-source-region'];
88
- const peerRegion = internal ? normalizeInfrastructureRegion(forwardedRegion) : edgePop ? `edge-${edgePop}` : undefined;
89
- return { region, service, activityType, scope: internal ? 'internal' : 'external', direction: 'inbound', sourceRegion: peerRegion, targetRegion: region, sourceService: name, targetService: service };
140
+ const peerRegion = internal
141
+ ? normalizeInfrastructureRegion(forwardedRegion)
142
+ : edgePop
143
+ ? `edge-${edgePop}`
144
+ : undefined;
145
+ return {
146
+ region,
147
+ service,
148
+ activityType,
149
+ scope: internal ? 'internal' : 'external',
150
+ direction: 'inbound',
151
+ sourceRegion: peerRegion,
152
+ targetRegion: region,
153
+ sourceService: name,
154
+ targetService: service,
155
+ };
90
156
  };
91
157
  const finish = () => {
92
158
  if (finished) return;
93
159
  finished = true;
94
160
  const flow = inboundFlow();
95
161
  collector.record(flow);
96
- collector.record({ ...flow, direction: 'outbound', sourceRegion: region, targetRegion: flow.sourceRegion, sourceService: service, targetService: flow.sourceService });
162
+ collector.record({
163
+ ...flow,
164
+ direction: 'outbound',
165
+ sourceRegion: region,
166
+ targetRegion: flow.sourceRegion,
167
+ sourceService: service,
168
+ targetService: flow.sourceService,
169
+ });
97
170
  };
98
171
  response.once('finish', finish);
99
172
  // Failed/aborted requests are still real inbound activity. Do not invent a
@@ -107,7 +180,10 @@ export function trafficMiddleware(
107
180
  };
108
181
  }
109
182
 
110
- export interface ServiceEndpoint { service: string; region: string }
183
+ export interface ServiceEndpoint {
184
+ service: string;
185
+ region: string;
186
+ }
111
187
  export function instrumentTrafficFetch(
112
188
  fetcher: typeof fetch,
113
189
  collector: Pick<ReturnType<typeof createTrafficCollector>, 'record'>,
@@ -116,23 +192,51 @@ export function instrumentTrafficFetch(
116
192
  resolveEndpoint: (url: URL) => ServiceEndpoint | undefined,
117
193
  ): typeof fetch {
118
194
  return Object.assign(async (input: RequestInfo | URL, init?: RequestInit) => {
119
- const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url);
195
+ const url = new URL(
196
+ typeof input === 'string' ? input : input instanceof URL ? input.href : input.url,
197
+ );
120
198
  // Collection/auth control traffic cannot generate a feedback loop.
121
- if (url.pathname.startsWith('/internal/activity') || url.pathname.startsWith('/auth/service-token') || url.pathname === '/platform-infrastructure') return fetcher(input, init);
199
+ if (
200
+ url.pathname.startsWith('/internal/activity') ||
201
+ url.pathname.startsWith('/auth/service-token') ||
202
+ url.pathname === '/platform-infrastructure'
203
+ )
204
+ return fetcher(input, init);
122
205
  const peer = resolveEndpoint(url);
123
206
  const flow: TrafficFlow = {
124
- service, region, scope: peer ? 'internal' : 'external', direction: 'outbound',
125
- activityType: trafficType(url.pathname), sourceRegion: region, targetRegion: peer?.region,
126
- sourceService: service, targetService: peer?.service,
207
+ service,
208
+ region,
209
+ scope: peer ? 'internal' : 'external',
210
+ direction: 'outbound',
211
+ activityType: trafficType(url.pathname),
212
+ sourceRegion: region,
213
+ targetRegion: peer?.region,
214
+ sourceService: service,
215
+ targetService: peer?.service,
127
216
  };
128
- const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
217
+ const headers = new Headers(
218
+ init?.headers ?? (input instanceof Request ? input.headers : undefined),
219
+ );
129
220
  if (peer) headers.set('X-Oxy-Source-Region', region);
130
221
  let response: Response;
131
- try { response = await fetcher(input, { ...init, headers }); }
132
- catch (error) { collector.record({ ...flow, targetRegion: undefined }); throw error; }
133
- const targetRegion = peer ? normalizeInfrastructureRegion(response.headers.get('X-Oxy-Region') ?? undefined) : undefined;
222
+ try {
223
+ response = await fetcher(input, { ...init, headers });
224
+ } catch (error) {
225
+ collector.record({ ...flow, targetRegion: undefined });
226
+ throw error;
227
+ }
228
+ const targetRegion = peer
229
+ ? normalizeInfrastructureRegion(response.headers.get('X-Oxy-Region') ?? undefined)
230
+ : undefined;
134
231
  collector.record({ ...flow, targetRegion });
135
- collector.record({ ...flow, direction: 'inbound', sourceRegion: targetRegion, targetRegion: region, sourceService: peer?.service, targetService: service });
232
+ collector.record({
233
+ ...flow,
234
+ direction: 'inbound',
235
+ sourceRegion: targetRegion,
236
+ targetRegion: region,
237
+ sourceService: peer?.service,
238
+ targetService: service,
239
+ });
136
240
  return response;
137
241
  }, fetcher) as typeof fetch;
138
242
  }
@@ -140,14 +244,25 @@ export function instrumentTrafficFetch(
140
244
  // Exact deployed API hosts; an arbitrary caller-supplied hostname cannot claim
141
245
  // to be an internal service. Region is infrastructure metadata, never IP geo.
142
246
  const OXY_API_SERVICES: Record<string, string> = {
143
- 'api.oxy.so': 'oxy-api', 'api.website.oxy.so': 'website',
144
- 'website-api.oxy.so': 'website', 'api.mention.earth': 'mention',
145
- 'api.mercaria.co': 'mercaria', 'api.alia.onl': 'alia', 'kaana.ai': 'kaana',
146
- 'api.homiio.com': 'homiio', 'api.syra.fm': 'syra',
147
- 'mention.earth': 'mention', 'mcp.mention.earth': 'mention-mcp',
148
- 'api.clarity.surf': 'clarity', 'api.peable.to': 'peable',
149
- 'api.crowdsource.oxy.so': 'crowdsource', 'api.moovo.now': 'moovo',
150
- 'api.allo.you': 'allo', 'api.noted.oxy.so': 'noted', 'api.schedio.app': 'schedio', 'api.tnp.network': 'tnp-api',
247
+ 'api.oxy.so': 'oxy-api',
248
+ 'api.website.oxy.so': 'website',
249
+ 'website-api.oxy.so': 'website',
250
+ 'api.mention.earth': 'mention',
251
+ 'api.mercaria.co': 'mercaria',
252
+ 'api.alia.onl': 'alia',
253
+ 'kaana.ai': 'kaana',
254
+ 'api.homiio.com': 'homiio',
255
+ 'api.syra.fm': 'syra',
256
+ 'mention.earth': 'mention',
257
+ 'mcp.mention.earth': 'mention-mcp',
258
+ 'api.clarity.surf': 'clarity',
259
+ 'api.peable.to': 'peable',
260
+ 'api.crowdsource.oxy.so': 'crowdsource',
261
+ 'api.moovo.now': 'moovo',
262
+ 'api.allo.you': 'allo',
263
+ 'api.noted.oxy.so': 'noted',
264
+ 'api.schedio.app': 'schedio',
265
+ 'api.tnp.network': 'tnp-api',
151
266
  };
152
267
  export function resolveOxyServiceEndpoint(url: URL): ServiceEndpoint | undefined {
153
268
  const service = OXY_API_SERVICES[url.hostname];
@@ -185,7 +300,9 @@ const REGION_LOCATIONS: Record<string, { label: string; coordinates: [number, nu
185
300
  'me-central-1': { label: 'UAE', coordinates: [54.37, 24.45] },
186
301
  'il-central-1': { label: 'Tel Aviv', coordinates: [34.78, 32.09] },
187
302
  };
188
- export function infrastructureLocation(region: string) { return REGION_LOCATIONS[region]; }
303
+ export function infrastructureLocation(region: string) {
304
+ return REGION_LOCATIONS[region];
305
+ }
189
306
 
190
307
  export function normalizeInfrastructureRegion(value: unknown): string | undefined {
191
308
  return typeof value === 'string' && /^[a-z]{2}(?:-[a-z]+)+-\d$/.test(value) ? value : undefined;
package/src/server.ts CHANGED
@@ -24,7 +24,9 @@ function firstHeaderValue(value: TelemetryHeaderValue): string | undefined {
24
24
  }
25
25
 
26
26
  export function normalizeEdgePop(value: TelemetryHeaderValue): string | undefined {
27
- return firstHeaderValue(value)?.match(/^[a-z]{3}$/i)?.[0]?.toLowerCase();
27
+ return firstHeaderValue(value)
28
+ ?.match(/^[a-z]{3}$/i)?.[0]
29
+ ?.toLowerCase();
28
30
  }
29
31
 
30
32
  export function normalizeActivityId(value: TelemetryHeaderValue): string | undefined {
@@ -59,7 +61,11 @@ export function presenceBucket(observedAt: number, bucketMs = DEFAULT_PRESENCE_B
59
61
  return Math.floor(observedAt / bucketMs);
60
62
  }
61
63
 
62
- export function presenceKeys(sourceRegion: string, observedAt: number, bucketMs = DEFAULT_PRESENCE_BUCKET_MS): readonly [string, string] {
64
+ export function presenceKeys(
65
+ sourceRegion: string,
66
+ observedAt: number,
67
+ bucketMs = DEFAULT_PRESENCE_BUCKET_MS,
68
+ ): readonly [string, string] {
63
69
  const current = presenceBucket(observedAt, bucketMs);
64
70
  return [
65
71
  `platform-activity:clients:${sourceRegion}:${current}`,
package/src/socket.ts CHANGED
@@ -13,11 +13,26 @@ export interface TrafficSocket {
13
13
  }
14
14
 
15
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',
16
+ auth: 'identity',
17
+ session: 'identity',
18
+ user: 'identity',
19
+ profile: 'identity',
20
+ chat: 'ai',
21
+ agent: 'ai',
22
+ inference: 'ai',
23
+ completion: 'ai',
24
+ message: 'communication',
25
+ notification: 'communication',
26
+ typing: 'communication',
27
+ media: 'media',
28
+ audio: 'media',
29
+ video: 'media',
30
+ stream: 'media',
31
+ player: 'media',
32
+ track: 'media',
33
+ playback: 'media',
34
+ playlist: 'media',
35
+ room: 'media',
21
36
  };
22
37
 
23
38
  /** Event names are reduced locally to five categories; payloads are never read. */
@@ -43,11 +58,19 @@ export function observeTrafficSocket(
43
58
  const record = (event: string, direction: 'inbound' | 'outbound') => {
44
59
  if (stopped) return;
45
60
  const peer = options.getPeer?.();
46
- const peerService = peer && /^[a-z][a-z0-9-]{0,39}$/.test(peer.service) ? peer.service : undefined;
61
+ const peerService =
62
+ peer && /^[a-z][a-z0-9-]{0,39}$/.test(peer.service) ? peer.service : undefined;
47
63
  const edgePop = metadataFromHeaders(socket.handshake.headers).edgePop;
48
- const peerRegion = peerService ? normalizeInfrastructureRegion(peer?.region) : edgePop ? `edge-${edgePop}` : undefined;
64
+ const peerRegion = peerService
65
+ ? normalizeInfrastructureRegion(peer?.region)
66
+ : edgePop
67
+ ? `edge-${edgePop}`
68
+ : undefined;
49
69
  collector.record({
50
- region, service, scope: peerService ? 'internal' : 'external', direction,
70
+ region,
71
+ service,
72
+ scope: peerService ? 'internal' : 'external',
73
+ direction,
51
74
  activityType: socketTrafficType(event),
52
75
  sourceRegion: direction === 'inbound' ? peerRegion : region,
53
76
  targetRegion: direction === 'inbound' ? region : peerRegion,
@@ -74,6 +97,7 @@ export interface TrafficWebSocket {
74
97
  readonly readyState: number;
75
98
  // ws accepts data/options overloads; they are forwarded without inspecting
76
99
  // or changing them. Keep this boundary permissive for Node ws versions.
100
+ // biome-ignore lint/suspicious/noExplicitAny: ws overloads are forwarded unchanged; this boundary stays permissive for Node ws versions
77
101
  send(...args: any[]): unknown;
78
102
  on(event: 'message', listener: () => void): unknown;
79
103
  once(event: 'close', listener: () => void): unknown;
@@ -96,11 +120,19 @@ export function observeTrafficWebSocket(
96
120
  const record = (direction: 'inbound' | 'outbound') => {
97
121
  if (stopped) return;
98
122
  const peer = options.getPeer?.();
99
- const peerService = peer && /^[a-z][a-z0-9-]{0,39}$/.test(peer.service) ? peer.service : undefined;
123
+ const peerService =
124
+ peer && /^[a-z][a-z0-9-]{0,39}$/.test(peer.service) ? peer.service : undefined;
100
125
  const edgePop = metadataFromHeaders(options.headers ?? {}).edgePop;
101
- const peerRegion = peerService ? normalizeInfrastructureRegion(peer?.region) : edgePop ? `edge-${edgePop}` : undefined;
126
+ const peerRegion = peerService
127
+ ? normalizeInfrastructureRegion(peer?.region)
128
+ : edgePop
129
+ ? `edge-${edgePop}`
130
+ : undefined;
102
131
  collector.record({
103
- service, region, scope: peerService ? 'internal' : 'external', direction,
132
+ service,
133
+ region,
134
+ scope: peerService ? 'internal' : 'external',
135
+ direction,
104
136
  activityType: options.activityType ?? 'communication',
105
137
  sourceRegion: direction === 'inbound' ? peerRegion : region,
106
138
  targetRegion: direction === 'inbound' ? region : peerRegion,
package/dist/cjs/edge.js DELETED
@@ -1,137 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.observeEdgeRequest = observeEdgeRequest;
4
- const collector_js_1 = require("./collector.js");
5
- const server_js_1 = require("./server.js");
6
- // Share values only: Workers prohibit I/O promises/bodies crossing request contexts.
7
- const tokens = new WeakMap();
8
- const CONTROL = /^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|platform-infrastructure|infra-status|internal\/activity|auth\/service-token|cdn-cgi)(?:\/|$)/;
9
- async function smallJson(response) {
10
- if (!response.body)
11
- throw new Error('Missing token response');
12
- const reader = response.body.getReader();
13
- const chunks = [];
14
- let size = 0;
15
- try {
16
- for (;;) {
17
- const { done, value } = await reader.read();
18
- if (done)
19
- break;
20
- size += value.byteLength;
21
- if (size > 16384) {
22
- await reader.cancel();
23
- throw new Error('Oversized token response');
24
- }
25
- chunks.push(value);
26
- }
27
- }
28
- finally {
29
- reader.releaseLock();
30
- }
31
- const bytes = new Uint8Array(size);
32
- let offset = 0;
33
- for (const chunk of chunks) {
34
- bytes.set(chunk, offset);
35
- offset += chunk.byteLength;
36
- }
37
- return JSON.parse(new TextDecoder().decode(bytes));
38
- }
39
- async function publish(options, events) {
40
- const { env } = options;
41
- const base = new URL(env.OXY_EDGE_ACTIVITY_API_URL ?? 'https://api.oxy.so');
42
- if (base.protocol !== 'https:' || base.username || base.password || base.pathname !== '/' || base.search || base.hash)
43
- throw new Error('Invalid activity origin');
44
- const fetcher = options.fetcher ?? fetch;
45
- const abort = new AbortController();
46
- const timeout = setTimeout(() => abort.abort(), 4000);
47
- try {
48
- let token;
49
- if (options.credential)
50
- token = await Promise.race([
51
- options.credential(),
52
- new Promise((_, reject) => abort.signal.addEventListener('abort', () => reject(new Error('Activity credential timeout')), { once: true })),
53
- ]);
54
- else {
55
- const key = env.OXY_EDGE_ACTIVITY_API_KEY;
56
- const secret = env.OXY_EDGE_ACTIVITY_API_SECRET;
57
- if (!key || !secret)
58
- throw new Error('Missing edge activity credentials');
59
- const cached = tokens.get(env);
60
- if (cached && cached.key === key && cached.secret === secret && cached.origin === base.origin && cached.expiresAt > Date.now() + 60000)
61
- token = cached.token;
62
- else {
63
- // Same exchange and expiry margin as OxyServices.getServiceToken().
64
- const response = await fetcher(new URL('/auth/service-token', base), {
65
- method: 'POST', headers: { 'Content-Type': 'application/json' },
66
- body: JSON.stringify({ apiKey: key, apiSecret: secret }), signal: abort.signal,
67
- });
68
- if (!response.ok) {
69
- await response.body?.cancel();
70
- throw new Error('Activity authentication failed');
71
- }
72
- const data = await smallJson(response);
73
- if (typeof data.token !== 'string' || !data.token || typeof data.expiresIn !== 'number' || !Number.isFinite(data.expiresIn) || data.expiresIn <= 0)
74
- throw new Error('Invalid activity token');
75
- token = data.token;
76
- tokens.set(env, { key, secret, origin: base.origin, token, expiresAt: Date.now() + data.expiresIn * 1000 });
77
- }
78
- }
79
- const response = await fetcher(new URL('/internal/activity', base), {
80
- method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
81
- body: JSON.stringify(events), signal: abort.signal,
82
- });
83
- if (response.status === 401)
84
- tokens.delete(env);
85
- await response.body?.cancel();
86
- if (!response.ok)
87
- throw new Error('Activity ingestion failed');
88
- }
89
- finally {
90
- clearTimeout(timeout);
91
- }
92
- }
93
- /** Returns the original response/stream and preserves original handler errors. */
94
- async function observeEdgeRequest(options) {
95
- const report = () => { try {
96
- (options.onError ?? console.error)('Edge activity publication failed');
97
- }
98
- catch { /* Never break serving. */ } };
99
- const enabled = options.env.OXY_EDGE_ACTIVITY_ENABLED;
100
- if (enabled === undefined || enabled === 'false')
101
- return options.next();
102
- if (enabled !== 'true' || !/^[a-z][a-z0-9-]{0,39}$/.test(options.service)) {
103
- report();
104
- return options.next();
105
- }
106
- const path = new URL(options.request.url).pathname;
107
- if (CONTROL.test(path))
108
- return options.next();
109
- const pop = (0, server_js_1.normalizeEdgePop)(typeof options.request.cf?.colo === 'string' ? options.request.cf.colo : undefined);
110
- const region = pop ? `edge-${pop}` : 'unknown';
111
- const startedAt = new Date().toISOString();
112
- const complete = (response) => {
113
- const contentType = response?.headers.get('Content-Type') ?? '';
114
- const activityType = /^(?:image|audio|video)\//i.test(contentType) ? 'media' : (0, collector_js_1.trafficType)(path);
115
- const common = { service: options.service, region, scope: options.peer ? 'internal' : 'external', activityType, requests: 1, windowStartedAt: startedAt, emittedAt: new Date().toISOString() };
116
- // The serving PoP is infrastructure metadata. Do not invent visitor location.
117
- const events = [{ ...common, direction: 'inbound', sourceRegion: options.peer?.region, sourceService: options.peer?.service, targetRegion: region, targetService: options.service }];
118
- if (response)
119
- events.push({ ...common, direction: 'outbound', sourceRegion: region, sourceService: options.service, targetRegion: options.peer?.region, targetService: options.peer?.service });
120
- try {
121
- options.ctx.waitUntil(publish(options, events).catch(report));
122
- }
123
- catch {
124
- report();
125
- }
126
- };
127
- let response;
128
- try {
129
- response = await options.next();
130
- }
131
- catch (error) {
132
- complete();
133
- throw error;
134
- }
135
- complete(response);
136
- return response;
137
- }
package/dist/esm/edge.js DELETED
@@ -1,134 +0,0 @@
1
- import { trafficType } from './collector.js';
2
- import { normalizeEdgePop } from './server.js';
3
- // Share values only: Workers prohibit I/O promises/bodies crossing request contexts.
4
- const tokens = new WeakMap();
5
- const CONTROL = /^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|platform-infrastructure|infra-status|internal\/activity|auth\/service-token|cdn-cgi)(?:\/|$)/;
6
- async function smallJson(response) {
7
- if (!response.body)
8
- throw new Error('Missing token response');
9
- const reader = response.body.getReader();
10
- const chunks = [];
11
- let size = 0;
12
- try {
13
- for (;;) {
14
- const { done, value } = await reader.read();
15
- if (done)
16
- break;
17
- size += value.byteLength;
18
- if (size > 16384) {
19
- await reader.cancel();
20
- throw new Error('Oversized token response');
21
- }
22
- chunks.push(value);
23
- }
24
- }
25
- finally {
26
- reader.releaseLock();
27
- }
28
- const bytes = new Uint8Array(size);
29
- let offset = 0;
30
- for (const chunk of chunks) {
31
- bytes.set(chunk, offset);
32
- offset += chunk.byteLength;
33
- }
34
- return JSON.parse(new TextDecoder().decode(bytes));
35
- }
36
- async function publish(options, events) {
37
- const { env } = options;
38
- const base = new URL(env.OXY_EDGE_ACTIVITY_API_URL ?? 'https://api.oxy.so');
39
- if (base.protocol !== 'https:' || base.username || base.password || base.pathname !== '/' || base.search || base.hash)
40
- throw new Error('Invalid activity origin');
41
- const fetcher = options.fetcher ?? fetch;
42
- const abort = new AbortController();
43
- const timeout = setTimeout(() => abort.abort(), 4000);
44
- try {
45
- let token;
46
- if (options.credential)
47
- token = await Promise.race([
48
- options.credential(),
49
- new Promise((_, reject) => abort.signal.addEventListener('abort', () => reject(new Error('Activity credential timeout')), { once: true })),
50
- ]);
51
- else {
52
- const key = env.OXY_EDGE_ACTIVITY_API_KEY;
53
- const secret = env.OXY_EDGE_ACTIVITY_API_SECRET;
54
- if (!key || !secret)
55
- throw new Error('Missing edge activity credentials');
56
- const cached = tokens.get(env);
57
- if (cached && cached.key === key && cached.secret === secret && cached.origin === base.origin && cached.expiresAt > Date.now() + 60000)
58
- token = cached.token;
59
- else {
60
- // Same exchange and expiry margin as OxyServices.getServiceToken().
61
- const response = await fetcher(new URL('/auth/service-token', base), {
62
- method: 'POST', headers: { 'Content-Type': 'application/json' },
63
- body: JSON.stringify({ apiKey: key, apiSecret: secret }), signal: abort.signal,
64
- });
65
- if (!response.ok) {
66
- await response.body?.cancel();
67
- throw new Error('Activity authentication failed');
68
- }
69
- const data = await smallJson(response);
70
- if (typeof data.token !== 'string' || !data.token || typeof data.expiresIn !== 'number' || !Number.isFinite(data.expiresIn) || data.expiresIn <= 0)
71
- throw new Error('Invalid activity token');
72
- token = data.token;
73
- tokens.set(env, { key, secret, origin: base.origin, token, expiresAt: Date.now() + data.expiresIn * 1000 });
74
- }
75
- }
76
- const response = await fetcher(new URL('/internal/activity', base), {
77
- method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
78
- body: JSON.stringify(events), signal: abort.signal,
79
- });
80
- if (response.status === 401)
81
- tokens.delete(env);
82
- await response.body?.cancel();
83
- if (!response.ok)
84
- throw new Error('Activity ingestion failed');
85
- }
86
- finally {
87
- clearTimeout(timeout);
88
- }
89
- }
90
- /** Returns the original response/stream and preserves original handler errors. */
91
- export async function observeEdgeRequest(options) {
92
- const report = () => { try {
93
- (options.onError ?? console.error)('Edge activity publication failed');
94
- }
95
- catch { /* Never break serving. */ } };
96
- const enabled = options.env.OXY_EDGE_ACTIVITY_ENABLED;
97
- if (enabled === undefined || enabled === 'false')
98
- return options.next();
99
- if (enabled !== 'true' || !/^[a-z][a-z0-9-]{0,39}$/.test(options.service)) {
100
- report();
101
- return options.next();
102
- }
103
- const path = new URL(options.request.url).pathname;
104
- if (CONTROL.test(path))
105
- return options.next();
106
- const pop = normalizeEdgePop(typeof options.request.cf?.colo === 'string' ? options.request.cf.colo : undefined);
107
- const region = pop ? `edge-${pop}` : 'unknown';
108
- const startedAt = new Date().toISOString();
109
- const complete = (response) => {
110
- const contentType = response?.headers.get('Content-Type') ?? '';
111
- const activityType = /^(?:image|audio|video)\//i.test(contentType) ? 'media' : trafficType(path);
112
- const common = { service: options.service, region, scope: options.peer ? 'internal' : 'external', activityType, requests: 1, windowStartedAt: startedAt, emittedAt: new Date().toISOString() };
113
- // The serving PoP is infrastructure metadata. Do not invent visitor location.
114
- const events = [{ ...common, direction: 'inbound', sourceRegion: options.peer?.region, sourceService: options.peer?.service, targetRegion: region, targetService: options.service }];
115
- if (response)
116
- events.push({ ...common, direction: 'outbound', sourceRegion: region, sourceService: options.service, targetRegion: options.peer?.region, targetService: options.peer?.service });
117
- try {
118
- options.ctx.waitUntil(publish(options, events).catch(report));
119
- }
120
- catch {
121
- report();
122
- }
123
- };
124
- let response;
125
- try {
126
- response = await options.next();
127
- }
128
- catch (error) {
129
- complete();
130
- throw error;
131
- }
132
- complete(response);
133
- return response;
134
- }