@oxy.so/core 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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/OxyServices.errors.js +28 -1
- package/dist/cjs/crypto/keyManager.js +5 -3
- package/dist/cjs/i18n/locales/en-US.json +18 -5
- package/dist/cjs/i18n/locales/es-ES.json +18 -5
- package/dist/cjs/i18n/locales/locales/en-US.json +18 -5
- package/dist/cjs/i18n/locales/locales/es-ES.json +18 -5
- package/dist/cjs/mixins/OxyServices.agency.js +113 -0
- package/dist/cjs/mixins/OxyServices.assets.js +90 -0
- package/dist/cjs/mixins/OxyServices.connectedApps.js +20 -3
- package/dist/cjs/mixins/OxyServices.utility.js +1 -21
- package/dist/cjs/mixins/index.js +3 -4
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/server/index.js +6 -1
- package/dist/cjs/server/traffic.js +107 -0
- package/dist/cjs/server/trafficNodeHttp.js +86 -0
- package/dist/cjs/utils/textNormalization.js +1 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/OxyServices.errors.js +26 -0
- package/dist/esm/crypto/keyManager.js +5 -3
- package/dist/esm/i18n/locales/en-US.json +18 -5
- package/dist/esm/i18n/locales/es-ES.json +18 -5
- package/dist/esm/i18n/locales/locales/en-US.json +18 -5
- package/dist/esm/i18n/locales/locales/es-ES.json +18 -5
- package/dist/esm/mixins/OxyServices.agency.js +110 -0
- package/dist/esm/mixins/OxyServices.assets.js +91 -1
- package/dist/esm/mixins/OxyServices.connectedApps.js +20 -3
- package/dist/esm/mixins/OxyServices.utility.js +1 -21
- package/dist/esm/mixins/index.js +3 -4
- package/dist/esm/package.json +3 -0
- package/dist/esm/server/index.js +2 -0
- package/dist/esm/server/traffic.js +104 -0
- package/dist/esm/server/trafficNodeHttp.js +79 -0
- package/dist/esm/utils/textNormalization.js +1 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/OxyServices.errors.d.ts +24 -0
- package/dist/types/crypto/keyManager.d.ts +4 -2
- package/dist/types/index.d.ts +2 -1
- package/dist/types/mixins/OxyServices.agency.d.ts +135 -0
- package/dist/types/mixins/OxyServices.assets.d.ts +43 -1
- package/dist/types/mixins/OxyServices.connectedApps.d.ts +15 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +0 -9
- package/dist/types/mixins/index.d.ts +2 -2
- package/dist/types/models/interfaces.d.ts +26 -0
- package/dist/types/server/index.d.ts +3 -0
- package/dist/types/server/traffic.d.ts +27 -0
- package/dist/types/server/trafficNodeHttp.d.ts +6 -0
- package/dist/types/utils/textNormalization.d.ts +1 -1
- package/package.json +7 -6
- package/src/OxyServices.errors.ts +36 -0
- package/src/crypto/keyManager.ts +5 -4
- package/src/i18n/locales/en-US.json +18 -5
- package/src/i18n/locales/es-ES.json +18 -5
- package/src/index.ts +15 -0
- package/src/mixins/OxyServices.agency.ts +277 -0
- package/src/mixins/OxyServices.assets.ts +105 -2
- package/src/mixins/OxyServices.connectedApps.ts +42 -4
- package/src/mixins/OxyServices.utility.ts +1 -26
- package/src/mixins/__tests__/OxyServices.serviceLinkedDownloadUrl.test.ts +139 -0
- package/src/mixins/__tests__/agency.test.ts +120 -0
- package/src/mixins/__tests__/connectedApps.test.ts +23 -0
- package/src/mixins/index.ts +4 -5
- package/src/models/interfaces.ts +27 -0
- package/src/server/__tests__/traffic.test.ts +49 -0
- package/src/server/__tests__/trafficNodeHttp.test.ts +38 -0
- package/src/server/index.ts +5 -0
- package/src/server/traffic.ts +116 -0
- package/src/server/trafficNodeHttp.ts +69 -0
- package/src/utils/textNormalization.ts +1 -1
- package/dist/cjs/mixins/OxyServices.links.js +0 -72
- package/dist/esm/mixins/OxyServices.links.js +0 -69
- package/dist/types/mixins/OxyServices.links.d.ts +0 -104
- package/src/mixins/OxyServices.links.ts +0 -107
- package/src/mixins/__tests__/OxyServices.links.test.ts +0 -154
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createEcosystemTraffic = createEcosystemTraffic;
|
|
4
|
+
const trafficNodeHttp_1 = require("./trafficNodeHttp");
|
|
5
|
+
const socket_1 = require("@oxy.so/telemetry/socket");
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
const OxyServices_1 = require("../OxyServices");
|
|
8
|
+
const collector_1 = require("@oxy.so/telemetry/collector");
|
|
9
|
+
/** Install once per service process, before registering HTTP routes. */
|
|
10
|
+
function createEcosystemTraffic(options) {
|
|
11
|
+
const region = options.region ?? process.env.AWS_REGION ?? 'unknown';
|
|
12
|
+
const baseURL = options.baseURL ?? process.env.OXY_API_URL ?? 'https://api.oxy.so';
|
|
13
|
+
const activityKey = process.env.OXY_ACTIVITY_API_KEY;
|
|
14
|
+
const activitySecret = process.env.OXY_ACTIVITY_API_SECRET;
|
|
15
|
+
if (!options.credential && Boolean(activityKey) !== Boolean(activitySecret)) {
|
|
16
|
+
throw new Error('Ecosystem activity requires a complete OXY_ACTIVITY_API_KEY and OXY_ACTIVITY_API_SECRET pair');
|
|
17
|
+
}
|
|
18
|
+
const apiKey = activityKey || process.env.OXY_SERVICE_API_KEY;
|
|
19
|
+
const apiSecret = activitySecret || process.env.OXY_SERVICE_API_SECRET;
|
|
20
|
+
if (!options.credential && (!apiKey || !apiSecret))
|
|
21
|
+
throw new Error('Ecosystem activity requires OXY_SERVICE_API_KEY and OXY_SERVICE_API_SECRET');
|
|
22
|
+
const oxy = new OxyServices_1.OxyServices({ baseURL });
|
|
23
|
+
if (apiKey && apiSecret)
|
|
24
|
+
oxy.configureServiceAuth(apiKey, apiSecret);
|
|
25
|
+
const credential = options.credential ?? (() => oxy.getServiceToken());
|
|
26
|
+
const fetcher = globalThis.fetch.bind(globalThis);
|
|
27
|
+
const instanceId = (0, node_crypto_1.randomUUID)();
|
|
28
|
+
const location = options.location ?? (0, collector_1.infrastructureLocation)(region);
|
|
29
|
+
if (!location)
|
|
30
|
+
throw new Error('Ecosystem activity requires a known infrastructure region or explicit location');
|
|
31
|
+
const post = async (path, data) => {
|
|
32
|
+
const token = await credential();
|
|
33
|
+
const response = await fetcher(new URL(path, baseURL), {
|
|
34
|
+
method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
35
|
+
body: JSON.stringify(data), signal: AbortSignal.timeout(4000),
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok)
|
|
38
|
+
throw new Error('Activity collection failed');
|
|
39
|
+
};
|
|
40
|
+
const collector = (0, collector_1.createTrafficCollector)(events => post('/internal/activity', events));
|
|
41
|
+
let stopped = false;
|
|
42
|
+
let heartbeatWork = Promise.resolve();
|
|
43
|
+
let heartbeatPending = false;
|
|
44
|
+
const reportError = options.onError ?? (() => console.warn('Ecosystem activity publisher unavailable'));
|
|
45
|
+
const heartbeat = (removed = false) => {
|
|
46
|
+
if (!removed && (stopped || heartbeatPending))
|
|
47
|
+
return heartbeatWork;
|
|
48
|
+
heartbeatPending = true;
|
|
49
|
+
heartbeatWork = heartbeatWork.then(() => post('/internal/activity/infrastructure', { instanceId, service: options.service, region, ...location, status: options.ready?.() ? 'online' : 'unknown', removed })).catch(reportError).finally(() => { heartbeatPending = false; });
|
|
50
|
+
return heartbeatWork;
|
|
51
|
+
};
|
|
52
|
+
const flush = () => collector.flush().catch(reportError);
|
|
53
|
+
const cleanups = new Set();
|
|
54
|
+
const sockets = new WeakMap();
|
|
55
|
+
let fetchInstalled = false;
|
|
56
|
+
const wrapFetch = (fetcher) => {
|
|
57
|
+
const wrapped = (0, collector_1.instrumentTrafficFetch)(fetcher, collector, options.service, region, options.resolveEndpoint ?? collector_1.resolveOxyServiceEndpoint);
|
|
58
|
+
return Object.assign((...args) => (0, trafficNodeHttp_1.withoutNodeHttpObservation)(() => wrapped(...args)), fetcher);
|
|
59
|
+
};
|
|
60
|
+
const observeHttp = (0, collector_1.trafficMiddleware)(collector, options.service, region);
|
|
61
|
+
const timer = setInterval(() => { void flush(); }, 2000);
|
|
62
|
+
timer.unref?.();
|
|
63
|
+
void heartbeat();
|
|
64
|
+
const heartbeatTimer = setInterval(() => { void heartbeat(); }, 10000);
|
|
65
|
+
heartbeatTimer.unref?.();
|
|
66
|
+
return {
|
|
67
|
+
record: flow => collector.record({ ...flow, service: options.service, region }),
|
|
68
|
+
middleware: observeHttp,
|
|
69
|
+
observeHttp,
|
|
70
|
+
wrapFetch,
|
|
71
|
+
installFetch() {
|
|
72
|
+
if (fetchInstalled || stopped)
|
|
73
|
+
return;
|
|
74
|
+
fetchInstalled = true;
|
|
75
|
+
const original = globalThis.fetch;
|
|
76
|
+
const wrapped = wrapFetch(original);
|
|
77
|
+
globalThis.fetch = wrapped;
|
|
78
|
+
cleanups.add(() => { if (globalThis.fetch === wrapped)
|
|
79
|
+
globalThis.fetch = original; });
|
|
80
|
+
cleanups.add((0, trafficNodeHttp_1.observeNodeHttp)(collector, options.service, region, options.resolveEndpoint ?? collector_1.resolveOxyServiceEndpoint));
|
|
81
|
+
},
|
|
82
|
+
observeWebSocket(socket, socketOptions) {
|
|
83
|
+
const existing = sockets.get(socket);
|
|
84
|
+
if (existing)
|
|
85
|
+
return existing;
|
|
86
|
+
const unobserve = (0, socket_1.observeTrafficWebSocket)(socket, collector, options.service, region, socketOptions);
|
|
87
|
+
const cleanup = () => { unobserve(); socket.off('close', cleanup); cleanups.delete(cleanup); sockets.delete(socket); };
|
|
88
|
+
sockets.set(socket, cleanup);
|
|
89
|
+
socket.once('close', cleanup);
|
|
90
|
+
cleanups.add(cleanup);
|
|
91
|
+
return cleanup;
|
|
92
|
+
},
|
|
93
|
+
observeSocket(socket, socketOptions) {
|
|
94
|
+
const existing = sockets.get(socket);
|
|
95
|
+
if (existing)
|
|
96
|
+
return existing;
|
|
97
|
+
const unobserve = (0, socket_1.observeTrafficSocket)(socket, collector, options.service, region, socketOptions);
|
|
98
|
+
const cleanup = () => { unobserve(); socket.off('disconnect', cleanup); cleanups.delete(cleanup); sockets.delete(socket); };
|
|
99
|
+
sockets.set(socket, cleanup);
|
|
100
|
+
socket.once('disconnect', cleanup);
|
|
101
|
+
cleanups.add(cleanup);
|
|
102
|
+
return cleanup;
|
|
103
|
+
},
|
|
104
|
+
async stop() { if (stopped)
|
|
105
|
+
return; stopped = true; cleanups.forEach(cleanup => cleanup()); cleanups.clear(); clearInterval(timer); clearInterval(heartbeatTimer); await flush(); await heartbeat(true); },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.withoutNodeHttpObservation = withoutNodeHttpObservation;
|
|
7
|
+
exports.observeNodeHttp = observeNodeHttp;
|
|
8
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
9
|
+
const node_https_1 = __importDefault(require("node:https"));
|
|
10
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
11
|
+
const node_module_1 = require("node:module");
|
|
12
|
+
const collector_1 = require("@oxy.so/telemetry/collector");
|
|
13
|
+
const suppressed = new node_async_hooks_1.AsyncLocalStorage();
|
|
14
|
+
function withoutNodeHttpObservation(callback) { return suppressed.run(true, callback); }
|
|
15
|
+
/** Node and Bun share the HTTP facade; Bun does not emit Node's HTTP diagnostic channels. */
|
|
16
|
+
function observeNodeHttp(collector, service, region, resolveEndpoint) {
|
|
17
|
+
const seen = new WeakSet();
|
|
18
|
+
const restoreRequests = new Set();
|
|
19
|
+
const observe = (request) => {
|
|
20
|
+
if (seen.has(request) || suppressed.getStore())
|
|
21
|
+
return request;
|
|
22
|
+
seen.add(request);
|
|
23
|
+
const host = request.getHeader('host') ?? request.host;
|
|
24
|
+
if (typeof host !== 'string')
|
|
25
|
+
return request;
|
|
26
|
+
const url = new URL(request.path || '/', `${request.protocol || 'http:'}//${host}`);
|
|
27
|
+
if (/^\/(?:internal\/activity|auth\/service-token|platform-infrastructure)(?:\/|$)/.test(url.pathname))
|
|
28
|
+
return request;
|
|
29
|
+
const peer = resolveEndpoint(url);
|
|
30
|
+
if (peer && !request.headersSent)
|
|
31
|
+
request.setHeader('X-Oxy-Source-Region', region);
|
|
32
|
+
const flow = { service, region, activityType: (0, collector_1.trafficType)(url.pathname), scope: peer ? 'internal' : 'external', direction: 'outbound', sourceRegion: region, sourceService: service, targetService: peer?.service };
|
|
33
|
+
const emit = request.emit;
|
|
34
|
+
let recorded = false;
|
|
35
|
+
const restore = () => { if (request.emit === observedEmit)
|
|
36
|
+
request.emit = emit; restoreRequests.delete(restore); };
|
|
37
|
+
// Intercept notifications without adding an 'error' listener: observation
|
|
38
|
+
// must not turn an unhandled application error into a swallowed error.
|
|
39
|
+
const observedEmit = function (event, ...args) {
|
|
40
|
+
try {
|
|
41
|
+
if (!recorded && event === 'response') {
|
|
42
|
+
recorded = true;
|
|
43
|
+
const response = args[0];
|
|
44
|
+
const targetRegion = peer ? (0, collector_1.normalizeInfrastructureRegion)(response.headers['x-oxy-region']) : undefined;
|
|
45
|
+
collector.record({ ...flow, targetRegion });
|
|
46
|
+
collector.record({ ...flow, direction: 'inbound', sourceRegion: targetRegion, targetRegion: region, sourceService: peer?.service, targetService: service });
|
|
47
|
+
}
|
|
48
|
+
else if (!recorded && (event === 'error' || event === 'close')) {
|
|
49
|
+
recorded = true;
|
|
50
|
+
collector.record(flow);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch { /* A metrics failure cannot change a request's behavior. */ }
|
|
54
|
+
if (event === 'close')
|
|
55
|
+
restore();
|
|
56
|
+
return emit.call(this, event, ...args);
|
|
57
|
+
};
|
|
58
|
+
request.emit = observedEmit;
|
|
59
|
+
restoreRequests.add(restore);
|
|
60
|
+
return request;
|
|
61
|
+
};
|
|
62
|
+
const safelyObserve = (request) => {
|
|
63
|
+
try {
|
|
64
|
+
return observe(request);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return request;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const patches = [node_http_1.default, node_https_1.default].map(module => {
|
|
71
|
+
const request = module.request;
|
|
72
|
+
const get = module.get;
|
|
73
|
+
const wrappedRequest = function (...args) { return safelyObserve(request.apply(module, args)); };
|
|
74
|
+
const wrappedGet = function (...args) { return safelyObserve(get.apply(module, args)); };
|
|
75
|
+
module.request = wrappedRequest;
|
|
76
|
+
module.get = wrappedGet;
|
|
77
|
+
return () => {
|
|
78
|
+
if (module.request === wrappedRequest)
|
|
79
|
+
module.request = request;
|
|
80
|
+
if (module.get === wrappedGet)
|
|
81
|
+
module.get = get;
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
(0, node_module_1.syncBuiltinESMExports)();
|
|
85
|
+
return () => { patches.forEach(restore => restore()); restoreRequests.forEach(restore => restore()); (0, node_module_1.syncBuiltinESMExports)(); };
|
|
86
|
+
}
|
|
@@ -87,7 +87,7 @@ const LEADING_HORIZONTAL_WHITESPACE = /\n +/g;
|
|
|
87
87
|
/** Three or more line breaks: more than one blank line between paragraphs. */
|
|
88
88
|
const EXCESS_BLANK_LINES = /\n{3,}/g;
|
|
89
89
|
/**
|
|
90
|
-
* Normalize a SINGLE-LINE text value
|
|
90
|
+
* Normalize a SINGLE-LINE text value such as a page title or `siteName`,
|
|
91
91
|
* display names, image alt text, handles, profile field labels.
|
|
92
92
|
*
|
|
93
93
|
* A line break in such a value is never meaningful — it is an artifact of the
|