@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/LICENSE +676 -0
- package/README.md +6 -3
- package/dist/cjs/browser.js +46 -8
- package/dist/cjs/collector.js +111 -26
- package/dist/cjs/server.js +3 -1
- package/dist/cjs/socket.js +38 -9
- package/dist/esm/browser.js +44 -7
- package/dist/esm/collector.js +111 -26
- package/dist/esm/server.js +3 -1
- package/dist/esm/socket.js +38 -9
- package/dist/types/browser.d.ts +14 -0
- package/package.json +5 -17
- package/src/__tests__/browser.test.ts +42 -4
- package/src/__tests__/collector.test.ts +132 -27
- package/src/__tests__/server.test.ts +7 -5
- package/src/__tests__/socket.test.ts +80 -29
- package/src/browser.ts +71 -10
- package/src/collector.ts +152 -35
- package/src/server.ts +8 -2
- package/src/socket.ts +43 -11
- package/dist/cjs/edge.js +0 -137
- package/dist/esm/edge.js +0 -134
- package/dist/types/edge.d.ts +0 -32
- package/src/__tests__/edge.test.ts +0 -110
- package/src/edge.ts +0 -117
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',
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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(
|
|
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 }) => ({
|
|
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)
|
|
86
|
+
for (let offset = 0; offset < events.length; offset += 256)
|
|
87
|
+
await publish(events.slice(offset, offset + 256));
|
|
56
88
|
})();
|
|
57
|
-
try {
|
|
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 {
|
|
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 (
|
|
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
|
|
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
|
|
89
|
-
|
|
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({
|
|
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 {
|
|
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(
|
|
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 (
|
|
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,
|
|
125
|
-
|
|
126
|
-
|
|
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(
|
|
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 {
|
|
132
|
-
|
|
133
|
-
|
|
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({
|
|
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',
|
|
144
|
-
'
|
|
145
|
-
'api.
|
|
146
|
-
'api.
|
|
147
|
-
'
|
|
148
|
-
'api.
|
|
149
|
-
'
|
|
150
|
-
'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) {
|
|
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)
|
|
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(
|
|
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',
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 =
|
|
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
|
|
64
|
+
const peerRegion = peerService
|
|
65
|
+
? normalizeInfrastructureRegion(peer?.region)
|
|
66
|
+
: edgePop
|
|
67
|
+
? `edge-${edgePop}`
|
|
68
|
+
: undefined;
|
|
49
69
|
collector.record({
|
|
50
|
-
region,
|
|
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 =
|
|
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
|
|
126
|
+
const peerRegion = peerService
|
|
127
|
+
? normalizeInfrastructureRegion(peer?.region)
|
|
128
|
+
: edgePop
|
|
129
|
+
? `edge-${edgePop}`
|
|
130
|
+
: undefined;
|
|
102
131
|
collector.record({
|
|
103
|
-
service,
|
|
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
|
-
}
|