oilpriceapi 0.7.0 → 0.9.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/README.md +244 -30
- package/dist/cjs/client.js +610 -0
- package/dist/cjs/errors.js +80 -0
- package/dist/cjs/index.js +96 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/resources/alerts.js +387 -0
- package/dist/cjs/resources/analytics.js +188 -0
- package/dist/cjs/resources/bunker-fuels.js +210 -0
- package/dist/cjs/resources/commodities.js +115 -0
- package/dist/cjs/resources/data-quality.js +144 -0
- package/dist/cjs/resources/data-sources.js +298 -0
- package/dist/cjs/resources/diesel.js +119 -0
- package/dist/cjs/resources/drilling.js +269 -0
- package/dist/cjs/resources/ei/drilling-productivity.js +108 -0
- package/dist/cjs/resources/ei/forecasts.js +106 -0
- package/dist/cjs/resources/ei/frac-focus.js +165 -0
- package/dist/cjs/resources/ei/index.js +98 -0
- package/dist/cjs/resources/ei/oil-inventories.js +97 -0
- package/dist/cjs/resources/ei/opec-production.js +97 -0
- package/dist/cjs/resources/ei/rig-counts.js +93 -0
- package/dist/cjs/resources/ei/well-permits.js +136 -0
- package/dist/cjs/resources/forecasts.js +168 -0
- package/dist/cjs/resources/futures.js +424 -0
- package/dist/cjs/resources/indicators.js +79 -0
- package/dist/cjs/resources/raw.js +128 -0
- package/dist/cjs/resources/rig-counts.js +164 -0
- package/dist/cjs/resources/spreads.js +105 -0
- package/dist/cjs/resources/storage.js +166 -0
- package/dist/cjs/resources/streaming.js +350 -0
- package/dist/cjs/resources/webhooks.js +283 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/version.js +24 -0
- package/dist/client.d.ts +130 -3
- package/dist/client.js +206 -30
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +25 -16
- package/dist/index.d.ts +28 -5
- package/dist/index.js +29 -1
- package/dist/resources/alerts.js +31 -77
- package/dist/resources/analytics.d.ts +147 -214
- package/dist/resources/analytics.js +104 -141
- package/dist/resources/bunker-fuels.d.ts +35 -12
- package/dist/resources/bunker-fuels.js +41 -26
- package/dist/resources/commodities.js +2 -1
- package/dist/resources/data-quality.js +2 -1
- package/dist/resources/data-sources.d.ts +31 -31
- package/dist/resources/data-sources.js +30 -85
- package/dist/resources/diesel.d.ts +1 -1
- package/dist/resources/diesel.js +9 -38
- package/dist/resources/drilling.js +2 -1
- package/dist/resources/ei/drilling-productivity.js +2 -1
- package/dist/resources/ei/forecasts.js +2 -1
- package/dist/resources/ei/frac-focus.d.ts +23 -9
- package/dist/resources/ei/frac-focus.js +20 -9
- package/dist/resources/ei/index.js +2 -1
- package/dist/resources/ei/oil-inventories.js +2 -1
- package/dist/resources/ei/opec-production.js +2 -1
- package/dist/resources/ei/rig-counts.js +2 -1
- package/dist/resources/ei/well-permits.d.ts +25 -9
- package/dist/resources/ei/well-permits.js +20 -7
- package/dist/resources/forecasts.d.ts +4 -1
- package/dist/resources/forecasts.js +13 -6
- package/dist/resources/futures.d.ts +178 -1
- package/dist/resources/futures.js +199 -8
- package/dist/resources/indicators.d.ts +170 -0
- package/dist/resources/indicators.js +75 -0
- package/dist/resources/raw.d.ts +94 -0
- package/dist/resources/raw.js +124 -0
- package/dist/resources/rig-counts.js +5 -2
- package/dist/resources/spreads.d.ts +121 -0
- package/dist/resources/spreads.js +101 -0
- package/dist/resources/storage.d.ts +5 -4
- package/dist/resources/storage.js +7 -6
- package/dist/resources/streaming.d.ts +272 -0
- package/dist/resources/streaming.js +342 -0
- package/dist/resources/webhooks.d.ts +73 -23
- package/dist/resources/webhooks.js +59 -77
- package/dist/types.d.ts +43 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -2
- package/package.json +21 -6
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* WebSocket Streaming Resource
|
|
4
|
+
*
|
|
5
|
+
* Real-time price streaming via the OilPriceAPI ActionCable endpoint
|
|
6
|
+
* (`wss://api.oilpriceapi.com/cable`).
|
|
7
|
+
*
|
|
8
|
+
* Streaming is a **Reservoir Mastery (Professional+)** feature. Connections
|
|
9
|
+
* authenticate with your API key and subscribe to the `EnergyPricesChannel`,
|
|
10
|
+
* which pushes an initial `welcome` snapshot followed by live `price_update`
|
|
11
|
+
* and (for drilling-tier accounts) `rig_count_update` messages.
|
|
12
|
+
*
|
|
13
|
+
* The implementation speaks the raw ActionCable JSON subprotocol over the
|
|
14
|
+
* `ws` package: it performs the `welcome` -> `subscribe` ->
|
|
15
|
+
* `confirm_subscription` handshake, answers server `ping` frames, and
|
|
16
|
+
* surfaces decoded channel messages as typed events. Auto-reconnect with
|
|
17
|
+
* exponential backoff keeps the stream alive across transient network drops.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const sub = client.stream.prices({}, (update) => {
|
|
22
|
+
* console.log(update.prices.oil.wti?.original_price);
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* sub.on("rig_count_update", (m) => console.log(m.rig_count.region, m.rig_count.count));
|
|
26
|
+
* sub.on("error", (err) => console.error(err));
|
|
27
|
+
*
|
|
28
|
+
* // later
|
|
29
|
+
* sub.close();
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
33
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
34
|
+
};
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.StreamingResource = exports.PriceStreamSubscription = exports.ENERGY_PRICES_CHANNEL = void 0;
|
|
37
|
+
const node_events_1 = require("node:events");
|
|
38
|
+
const ws_1 = __importDefault(require("ws"));
|
|
39
|
+
/**
|
|
40
|
+
* The ActionCable channel exposed by the OilPriceAPI server.
|
|
41
|
+
*
|
|
42
|
+
* Confirmed from `app/channels/energy_prices_channel.rb`
|
|
43
|
+
* (`class EnergyPricesChannel`).
|
|
44
|
+
*/
|
|
45
|
+
exports.ENERGY_PRICES_CHANNEL = "EnergyPricesChannel";
|
|
46
|
+
/** Maps streamed slug <-> upstream code so `commodities` filtering accepts either. */
|
|
47
|
+
const COMMODITY_CODE_TO_SLUG = {
|
|
48
|
+
BRENT_CRUDE_USD: "brent",
|
|
49
|
+
WTI_USD: "wti",
|
|
50
|
+
NATURAL_GAS_GBP: "uk",
|
|
51
|
+
NATURAL_GAS_USD: "us",
|
|
52
|
+
DUTCH_TTF_EUR: "eu",
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Handle for an active price stream.
|
|
56
|
+
*
|
|
57
|
+
* Extends `EventEmitter`. Emitted events:
|
|
58
|
+
* - `"connected"` — transport connected and subscription confirmed
|
|
59
|
+
* - `"welcome"` — initial snapshot ({@link WelcomeMessage})
|
|
60
|
+
* - `"price_update"` — live price broadcast ({@link PriceUpdateMessage})
|
|
61
|
+
* - `"rig_count_update"` — drilling broadcast ({@link RigCountUpdateMessage})
|
|
62
|
+
* - `"message"` — every decoded channel message ({@link StreamMessage})
|
|
63
|
+
* - `"reconnecting"` — a reconnect attempt is scheduled (`{ attempt, delay }`)
|
|
64
|
+
* - `"disconnected"` — transport closed (`{ code, reason }`)
|
|
65
|
+
* - `"error"` — an `Error` (transport error, unauthorized, or retries exhausted)
|
|
66
|
+
* - `"close"` — the subscription was closed via {@link PriceStreamSubscription.close}
|
|
67
|
+
*/
|
|
68
|
+
class PriceStreamSubscription extends node_events_1.EventEmitter {
|
|
69
|
+
/**
|
|
70
|
+
* @param url - The `wss://.../cable` endpoint.
|
|
71
|
+
* @param apiKey - API key sent as the ActionCable `Authorization: Token <key>` header.
|
|
72
|
+
* @param options - Reconnect + filter options.
|
|
73
|
+
* @param wsImpl - Injectable WebSocket constructor (used by tests to mock).
|
|
74
|
+
* @internal Construct via {@link StreamingResource.prices}.
|
|
75
|
+
*/
|
|
76
|
+
constructor(url, apiKey, options, wsImpl = ws_1.default) {
|
|
77
|
+
super();
|
|
78
|
+
this.wsImpl = wsImpl;
|
|
79
|
+
this.ws = null;
|
|
80
|
+
this.closed = false;
|
|
81
|
+
this.subscribed = false;
|
|
82
|
+
this.reconnectAttempts = 0;
|
|
83
|
+
this.reconnectTimer = null;
|
|
84
|
+
this.url = url;
|
|
85
|
+
this.apiKey = apiKey;
|
|
86
|
+
this.options = {
|
|
87
|
+
autoReconnect: options.autoReconnect ?? true,
|
|
88
|
+
reconnectDelay: options.reconnectDelay ?? 1000,
|
|
89
|
+
maxReconnectDelay: options.maxReconnectDelay ?? 30000,
|
|
90
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
|
|
91
|
+
commodities: options.commodities,
|
|
92
|
+
};
|
|
93
|
+
// The ActionCable subscription identifier. The channel takes no params;
|
|
94
|
+
// we additionally pass `api_key` in the identifier for parity with the
|
|
95
|
+
// documented native-WebSocket clients, while the server's
|
|
96
|
+
// ApplicationCable::Connection authenticates from the Authorization header.
|
|
97
|
+
this.identifier = JSON.stringify({
|
|
98
|
+
channel: exports.ENERGY_PRICES_CHANNEL,
|
|
99
|
+
api_key: apiKey,
|
|
100
|
+
});
|
|
101
|
+
if (options.commodities && options.commodities.length > 0) {
|
|
102
|
+
this.commodityFilter = new Set(options.commodities.map((c) => COMMODITY_CODE_TO_SLUG[c] ?? c.toLowerCase()));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.commodityFilter = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Open the transport and begin the ActionCable handshake.
|
|
110
|
+
* @internal Called once by {@link StreamingResource.prices}.
|
|
111
|
+
*/
|
|
112
|
+
connect() {
|
|
113
|
+
if (this.closed)
|
|
114
|
+
return;
|
|
115
|
+
const ws = new this.wsImpl(this.url, {
|
|
116
|
+
headers: {
|
|
117
|
+
Authorization: `Token ${this.apiKey}`,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
this.ws = ws;
|
|
121
|
+
ws.on("open", () => {
|
|
122
|
+
// ActionCable sends a transport `welcome` frame first; we subscribe on
|
|
123
|
+
// open (the server tolerates an early subscribe and replies with
|
|
124
|
+
// confirm_subscription once the connection is established).
|
|
125
|
+
this.send({ command: "subscribe", identifier: this.identifier });
|
|
126
|
+
});
|
|
127
|
+
ws.on("message", (raw) => this.handleRaw(raw));
|
|
128
|
+
ws.on("error", (err) => {
|
|
129
|
+
this.emit("error", err);
|
|
130
|
+
});
|
|
131
|
+
ws.on("close", (code, reason) => {
|
|
132
|
+
this.ws = null;
|
|
133
|
+
this.subscribed = false;
|
|
134
|
+
this.emit("disconnected", { code, reason: reason?.toString() ?? "" });
|
|
135
|
+
if (!this.closed) {
|
|
136
|
+
this.scheduleReconnect();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
handleRaw(raw) {
|
|
141
|
+
let frame;
|
|
142
|
+
try {
|
|
143
|
+
frame = JSON.parse(raw.toString());
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Ignore malformed frames rather than crash the stream.
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// ActionCable transport-level frames carry a top-level `type`.
|
|
150
|
+
const transportType = frame["type"];
|
|
151
|
+
if (transportType === "ping") {
|
|
152
|
+
// Heartbeat — nothing to do; presence of frames keeps `ws` alive.
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (transportType === "welcome") {
|
|
156
|
+
// Transport handshake complete. (subscribe already sent on open.)
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (transportType === "confirm_subscription") {
|
|
160
|
+
this.subscribed = true;
|
|
161
|
+
this.reconnectAttempts = 0;
|
|
162
|
+
this.emit("connected");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (transportType === "reject_subscription") {
|
|
166
|
+
this.emit("error", new Error("WebSocket subscription rejected. Streaming requires a Reservoir Mastery " +
|
|
167
|
+
"(Professional+) plan and a valid API key."));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (transportType === "disconnect") {
|
|
171
|
+
// Server-initiated disconnect (e.g. auth failure). Let `close` drive reconnect.
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Channel message: payload lives under `message`.
|
|
175
|
+
const payload = frame["message"];
|
|
176
|
+
if (payload && typeof payload === "object") {
|
|
177
|
+
this.dispatch(payload);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
dispatch(message) {
|
|
181
|
+
this.emit("message", message);
|
|
182
|
+
switch (message.type) {
|
|
183
|
+
case "welcome":
|
|
184
|
+
this.emit("welcome", message);
|
|
185
|
+
break;
|
|
186
|
+
case "price_update": {
|
|
187
|
+
const update = message;
|
|
188
|
+
if (this.matchesFilter(update)) {
|
|
189
|
+
this.emit("price_update", update);
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case "rig_count_update":
|
|
194
|
+
this.emit("rig_count_update", message);
|
|
195
|
+
break;
|
|
196
|
+
default:
|
|
197
|
+
// Unknown message types are still emitted via `message` above.
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
matchesFilter(update) {
|
|
202
|
+
if (!this.commodityFilter)
|
|
203
|
+
return true;
|
|
204
|
+
const { oil, natural_gas } = update.prices ?? {};
|
|
205
|
+
const present = {
|
|
206
|
+
brent: oil?.brent,
|
|
207
|
+
wti: oil?.wti,
|
|
208
|
+
uk: natural_gas?.uk,
|
|
209
|
+
us: natural_gas?.us,
|
|
210
|
+
eu: natural_gas?.eu,
|
|
211
|
+
};
|
|
212
|
+
for (const slug of this.commodityFilter) {
|
|
213
|
+
if (present[slug])
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
scheduleReconnect() {
|
|
219
|
+
if (this.closed || !this.options.autoReconnect)
|
|
220
|
+
return;
|
|
221
|
+
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
|
|
222
|
+
this.emit("error", new Error(`WebSocket reconnect failed after ${this.reconnectAttempts} attempt(s); giving up.`));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const attempt = this.reconnectAttempts++;
|
|
226
|
+
const delay = Math.min(this.options.reconnectDelay * Math.pow(2, attempt), this.options.maxReconnectDelay);
|
|
227
|
+
this.emit("reconnecting", { attempt: attempt + 1, delay });
|
|
228
|
+
this.reconnectTimer = setTimeout(() => {
|
|
229
|
+
this.reconnectTimer = null;
|
|
230
|
+
this.connect();
|
|
231
|
+
}, delay);
|
|
232
|
+
}
|
|
233
|
+
send(payload) {
|
|
234
|
+
if (this.ws && this.ws.readyState === this.wsImpl.OPEN) {
|
|
235
|
+
this.ws.send(JSON.stringify(payload));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Whether the channel subscription has been confirmed by the server. */
|
|
239
|
+
get isSubscribed() {
|
|
240
|
+
return this.subscribed;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Cleanly tear down the stream: cancels any pending reconnect, unsubscribes
|
|
244
|
+
* from the channel, and closes the socket. Safe to call multiple times.
|
|
245
|
+
* Emits `"close"` once.
|
|
246
|
+
*/
|
|
247
|
+
close() {
|
|
248
|
+
if (this.closed)
|
|
249
|
+
return;
|
|
250
|
+
this.closed = true;
|
|
251
|
+
if (this.reconnectTimer) {
|
|
252
|
+
clearTimeout(this.reconnectTimer);
|
|
253
|
+
this.reconnectTimer = null;
|
|
254
|
+
}
|
|
255
|
+
if (this.ws) {
|
|
256
|
+
const ws = this.ws;
|
|
257
|
+
try {
|
|
258
|
+
if (ws.readyState === this.wsImpl.OPEN) {
|
|
259
|
+
ws.send(JSON.stringify({ command: "unsubscribe", identifier: this.identifier }));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// ignore — we're closing anyway
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
ws.close();
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// ignore
|
|
270
|
+
}
|
|
271
|
+
this.ws = null;
|
|
272
|
+
}
|
|
273
|
+
this.subscribed = false;
|
|
274
|
+
this.emit("close");
|
|
275
|
+
this.removeAllListeners();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
exports.PriceStreamSubscription = PriceStreamSubscription;
|
|
279
|
+
/**
|
|
280
|
+
* Streaming resource — entry point for real-time price subscriptions.
|
|
281
|
+
*
|
|
282
|
+
* Accessed via `client.stream`.
|
|
283
|
+
*/
|
|
284
|
+
class StreamingResource {
|
|
285
|
+
constructor(client,
|
|
286
|
+
/**
|
|
287
|
+
* Injectable WebSocket implementation. Defaults to the `ws` package;
|
|
288
|
+
* tests pass a mock constructor.
|
|
289
|
+
* @internal
|
|
290
|
+
*/
|
|
291
|
+
wsImpl = ws_1.default) {
|
|
292
|
+
this.client = client;
|
|
293
|
+
this.wsImpl = wsImpl;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Derive the `wss://.../cable` endpoint from the client's REST base URL.
|
|
297
|
+
*
|
|
298
|
+
* `https://api.oilpriceapi.com` -> `wss://api.oilpriceapi.com/cable`
|
|
299
|
+
* `http://localhost:5000` -> `ws://localhost:5000/cable`
|
|
300
|
+
*/
|
|
301
|
+
cableUrl() {
|
|
302
|
+
const base = this.client["baseUrl"];
|
|
303
|
+
const wsBase = base
|
|
304
|
+
.replace(/^http:/, "ws:")
|
|
305
|
+
.replace(/^https:/, "wss:")
|
|
306
|
+
.replace(/\/$/, "");
|
|
307
|
+
return `${wsBase}/cable`;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Open a real-time price stream over the `EnergyPricesChannel`.
|
|
311
|
+
*
|
|
312
|
+
* Returns a {@link PriceStreamSubscription} handle (an `EventEmitter`) you
|
|
313
|
+
* can attach further listeners to and `.close()` when done. The optional
|
|
314
|
+
* `onUpdate` callback is a convenience wired to the `price_update` event.
|
|
315
|
+
*
|
|
316
|
+
* @param options - Filtering and reconnect options.
|
|
317
|
+
* @param onUpdate - Optional callback for each `price_update` message.
|
|
318
|
+
* @returns The subscription handle.
|
|
319
|
+
*
|
|
320
|
+
* @throws {OilPriceAPIError} If no API key is configured on the client.
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```typescript
|
|
324
|
+
* const client = new OilPriceAPI({ apiKey: process.env.OILPRICEAPI_KEY });
|
|
325
|
+
*
|
|
326
|
+
* const sub = client.stream.prices(
|
|
327
|
+
* { commodities: ["WTI_USD", "BRENT_CRUDE_USD"] },
|
|
328
|
+
* (update) => {
|
|
329
|
+
* const wti = update.prices.oil.wti;
|
|
330
|
+
* if (wti) console.log(`WTI ${wti.original_price} @ ${update.timestamp}`);
|
|
331
|
+
* },
|
|
332
|
+
* );
|
|
333
|
+
*
|
|
334
|
+
* sub.on("connected", () => console.log("streaming live"));
|
|
335
|
+
* sub.on("error", (err) => console.error("stream error:", err));
|
|
336
|
+
*
|
|
337
|
+
* process.on("SIGINT", () => sub.close());
|
|
338
|
+
* ```
|
|
339
|
+
*/
|
|
340
|
+
prices(options = {}, onUpdate) {
|
|
341
|
+
const apiKey = this.client["apiKey"];
|
|
342
|
+
const sub = new PriceStreamSubscription(this.cableUrl(), apiKey, options, this.wsImpl);
|
|
343
|
+
if (onUpdate) {
|
|
344
|
+
sub.on("price_update", onUpdate);
|
|
345
|
+
}
|
|
346
|
+
sub.connect();
|
|
347
|
+
return sub;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
exports.StreamingResource = StreamingResource;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Webhooks Resource
|
|
4
|
+
*
|
|
5
|
+
* Manage webhook endpoints for real-time event notifications.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.WebhooksResource = void 0;
|
|
9
|
+
const errors_js_1 = require("../errors.js");
|
|
10
|
+
const index_js_1 = require("../index.js");
|
|
11
|
+
/**
|
|
12
|
+
* Webhooks Resource
|
|
13
|
+
*
|
|
14
|
+
* Manage webhook endpoints for real-time notifications about price changes,
|
|
15
|
+
* alerts, and other events.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { OilPriceAPI } from 'oilpriceapi';
|
|
20
|
+
*
|
|
21
|
+
* const client = new OilPriceAPI({ apiKey: 'your_key' });
|
|
22
|
+
*
|
|
23
|
+
* // Create a webhook
|
|
24
|
+
* const webhook = await client.webhooks.create({
|
|
25
|
+
* name: 'Price Updates',
|
|
26
|
+
* url: 'https://myapp.com/webhooks/prices',
|
|
27
|
+
* events: ['price.updated', 'alert.triggered'],
|
|
28
|
+
* enabled: true
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* // Test the webhook
|
|
32
|
+
* const test = await client.webhooks.test(webhook.id);
|
|
33
|
+
* console.log(`Test result: ${test.success ? 'passed' : 'failed'}`);
|
|
34
|
+
*
|
|
35
|
+
* // List all webhooks
|
|
36
|
+
* const webhooks = await client.webhooks.list();
|
|
37
|
+
* webhooks.forEach(wh => {
|
|
38
|
+
* console.log(`${wh.name}: ${wh.successful_deliveries} successful`);
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* // Update webhook
|
|
42
|
+
* await client.webhooks.update(webhook.id, {
|
|
43
|
+
* enabled: false
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* // Delete webhook
|
|
47
|
+
* await client.webhooks.delete(webhook.id);
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
class WebhooksResource {
|
|
51
|
+
constructor(client) {
|
|
52
|
+
this.client = client;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* List all webhook endpoints
|
|
56
|
+
*
|
|
57
|
+
* @returns Array of webhook endpoints
|
|
58
|
+
*
|
|
59
|
+
* @throws {OilPriceAPIError} If API request fails
|
|
60
|
+
* @throws {AuthenticationError} If API key is invalid
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const webhooks = await client.webhooks.list();
|
|
65
|
+
* webhooks.forEach(wh => {
|
|
66
|
+
* console.log(`${wh.name} (${wh.enabled ? 'enabled' : 'disabled'})`);
|
|
67
|
+
* console.log(` Events: ${wh.events.join(', ')}`);
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
async list() {
|
|
72
|
+
const response = await this.client["request"]("/v1/webhooks", {});
|
|
73
|
+
return Array.isArray(response) ? response : response.webhooks;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get a specific webhook endpoint
|
|
77
|
+
*
|
|
78
|
+
* @param id - Webhook ID
|
|
79
|
+
* @returns Webhook endpoint details
|
|
80
|
+
*
|
|
81
|
+
* @throws {NotFoundError} If webhook not found
|
|
82
|
+
* @throws {OilPriceAPIError} If API request fails
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const webhook = await client.webhooks.get('webhook-id');
|
|
87
|
+
* console.log(`${webhook.name}: ${webhook.url}`);
|
|
88
|
+
* console.log(`Success rate: ${webhook.successful_deliveries}/${webhook.successful_deliveries + webhook.failed_deliveries}`);
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
async get(id) {
|
|
92
|
+
if (!id || typeof id !== "string") {
|
|
93
|
+
throw new errors_js_1.ValidationError("Webhook ID must be a non-empty string");
|
|
94
|
+
}
|
|
95
|
+
const response = await this.client["request"](`/v1/webhooks/${id}`, {});
|
|
96
|
+
return "webhook" in response ? response.webhook : response;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Create a new webhook endpoint
|
|
100
|
+
*
|
|
101
|
+
* @param params - Webhook configuration
|
|
102
|
+
* @returns Created webhook endpoint
|
|
103
|
+
*
|
|
104
|
+
* @throws {OilPriceAPIError} If API request fails
|
|
105
|
+
* @throws {AuthenticationError} If API key is invalid
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* const webhook = await client.webhooks.create({
|
|
110
|
+
* name: 'Production Alerts',
|
|
111
|
+
* url: 'https://api.myapp.com/webhooks',
|
|
112
|
+
* events: ['alert.triggered', 'price.updated'],
|
|
113
|
+
* secret: 'my-webhook-secret',
|
|
114
|
+
* enabled: true
|
|
115
|
+
* });
|
|
116
|
+
* console.log(`Webhook created: ${webhook.id}`);
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
async create(params) {
|
|
120
|
+
if (!params.url || !params.url.startsWith("https://")) {
|
|
121
|
+
throw new errors_js_1.ValidationError("Webhook URL must use HTTPS protocol");
|
|
122
|
+
}
|
|
123
|
+
if (!params.events || !Array.isArray(params.events) || params.events.length === 0) {
|
|
124
|
+
throw new errors_js_1.ValidationError("At least one event type is required");
|
|
125
|
+
}
|
|
126
|
+
// The controller reads a flat (un-nested) body via params.permit(...).
|
|
127
|
+
const response = await this.client["request"]("/v1/webhooks", {}, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
body: { ...params },
|
|
130
|
+
});
|
|
131
|
+
return "webhook" in response ? response.webhook : response;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Update a webhook endpoint
|
|
135
|
+
*
|
|
136
|
+
* @param id - Webhook ID
|
|
137
|
+
* @param params - Fields to update
|
|
138
|
+
* @returns Updated webhook endpoint
|
|
139
|
+
*
|
|
140
|
+
* @throws {NotFoundError} If webhook not found
|
|
141
|
+
* @throws {OilPriceAPIError} If API request fails
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```typescript
|
|
145
|
+
* // Disable a webhook
|
|
146
|
+
* await client.webhooks.update(webhookId, { enabled: false });
|
|
147
|
+
*
|
|
148
|
+
* // Change events
|
|
149
|
+
* await client.webhooks.update(webhookId, {
|
|
150
|
+
* events: ['alert.triggered']
|
|
151
|
+
* });
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
async update(id, params) {
|
|
155
|
+
if (!id || typeof id !== "string") {
|
|
156
|
+
throw new errors_js_1.ValidationError("Webhook ID must be a non-empty string");
|
|
157
|
+
}
|
|
158
|
+
if (params.url !== undefined && !params.url.startsWith("https://")) {
|
|
159
|
+
throw new errors_js_1.ValidationError("Webhook URL must use HTTPS protocol");
|
|
160
|
+
}
|
|
161
|
+
if (params.events !== undefined &&
|
|
162
|
+
(!Array.isArray(params.events) || params.events.length === 0)) {
|
|
163
|
+
throw new errors_js_1.ValidationError("Events must be a non-empty array");
|
|
164
|
+
}
|
|
165
|
+
const response = await this.client["request"](`/v1/webhooks/${id}`, {}, {
|
|
166
|
+
method: "PATCH",
|
|
167
|
+
body: { ...params },
|
|
168
|
+
});
|
|
169
|
+
return "webhook" in response ? response.webhook : response;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Delete a webhook endpoint
|
|
173
|
+
*
|
|
174
|
+
* @param id - Webhook ID
|
|
175
|
+
*
|
|
176
|
+
* @throws {NotFoundError} If webhook not found
|
|
177
|
+
* @throws {OilPriceAPIError} If API request fails
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* await client.webhooks.delete(webhookId);
|
|
182
|
+
* console.log('Webhook deleted');
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
async delete(id) {
|
|
186
|
+
if (!id || typeof id !== "string") {
|
|
187
|
+
throw new errors_js_1.ValidationError("Webhook ID must be a non-empty string");
|
|
188
|
+
}
|
|
189
|
+
await this.client["request"](`/v1/webhooks/${id}`, {}, { method: "DELETE" });
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Test a webhook endpoint
|
|
193
|
+
*
|
|
194
|
+
* Sends a test payload to the webhook URL to verify it's reachable.
|
|
195
|
+
*
|
|
196
|
+
* @param id - Webhook ID
|
|
197
|
+
* @returns Test results
|
|
198
|
+
*
|
|
199
|
+
* @throws {NotFoundError} If webhook not found
|
|
200
|
+
* @throws {OilPriceAPIError} If API request fails
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```typescript
|
|
204
|
+
* const test = await client.webhooks.test(webhookId);
|
|
205
|
+
* console.log(`Test ${test.success ? 'passed' : 'failed'}`);
|
|
206
|
+
* console.log(`Response time: ${test.response_time_ms}ms`);
|
|
207
|
+
* if (!test.success) {
|
|
208
|
+
* console.log(`Error: ${test.error}`);
|
|
209
|
+
* }
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
async test(id) {
|
|
213
|
+
if (!id || typeof id !== "string") {
|
|
214
|
+
throw new errors_js_1.ValidationError("Webhook ID must be a non-empty string");
|
|
215
|
+
}
|
|
216
|
+
return this.client["request"](`/v1/webhooks/${id}/test`, {}, { method: "POST" });
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Get webhook event history
|
|
220
|
+
*
|
|
221
|
+
* Returns recent delivery events for a webhook.
|
|
222
|
+
*
|
|
223
|
+
* @param id - Webhook ID
|
|
224
|
+
* @returns Array of webhook events
|
|
225
|
+
*
|
|
226
|
+
* @throws {NotFoundError} If webhook not found
|
|
227
|
+
* @throws {OilPriceAPIError} If API request fails
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```typescript
|
|
231
|
+
* const events = await client.webhooks.events(webhookId);
|
|
232
|
+
* events.forEach(event => {
|
|
233
|
+
* console.log(`${event.event_type}: ${event.status} (${event.attempts} attempts)`);
|
|
234
|
+
* });
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
async events(id) {
|
|
238
|
+
if (!id || typeof id !== "string") {
|
|
239
|
+
throw new errors_js_1.ValidationError("Webhook ID must be a non-empty string");
|
|
240
|
+
}
|
|
241
|
+
const response = await this.client["request"](`/v1/webhooks/${id}/events`, {});
|
|
242
|
+
return Array.isArray(response) ? response : response.events;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Verify a webhook signature.
|
|
246
|
+
*
|
|
247
|
+
* Validates that a webhook payload was sent by OilPriceAPI by checking
|
|
248
|
+
* the HMAC-SHA256 signature. Uses constant-time comparison to prevent
|
|
249
|
+
* timing attacks.
|
|
250
|
+
*
|
|
251
|
+
* @param payload - Raw request body (string or Buffer)
|
|
252
|
+
* @param signature - Value of the X-OilPriceAPI-Signature header (e.g., "sha256=abc123...")
|
|
253
|
+
* @param secret - Your webhook signing secret
|
|
254
|
+
* @returns true if signature is valid
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```typescript
|
|
258
|
+
* import express from 'express';
|
|
259
|
+
* import { OilPriceAPI } from 'oilpriceapi';
|
|
260
|
+
*
|
|
261
|
+
* const app = express();
|
|
262
|
+
* const client = new OilPriceAPI({ apiKey: 'your_key' });
|
|
263
|
+
*
|
|
264
|
+
* // Use raw body parser for webhook routes
|
|
265
|
+
* app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
266
|
+
* const signature = req.headers['x-oilpriceapi-signature'] as string;
|
|
267
|
+
* const isValid = client.webhooks.verifySignature(req.body, signature, 'your_secret');
|
|
268
|
+
*
|
|
269
|
+
* if (!isValid) {
|
|
270
|
+
* return res.status(401).send('Invalid signature');
|
|
271
|
+
* }
|
|
272
|
+
*
|
|
273
|
+
* const event = JSON.parse(req.body.toString());
|
|
274
|
+
* console.log('Verified webhook:', event.type);
|
|
275
|
+
* res.sendStatus(200);
|
|
276
|
+
* });
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
verifySignature(payload, signature, secret) {
|
|
280
|
+
return (0, index_js_1.verifyWebhookSignature)(payload, signature, secret);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
exports.WebhooksResource = WebhooksResource;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SDK_NAME = exports.SDK_VERSION = void 0;
|
|
4
|
+
exports.buildUserAgent = buildUserAgent;
|
|
5
|
+
/**
|
|
6
|
+
* SDK Version - centralized to ensure consistency across all headers
|
|
7
|
+
*
|
|
8
|
+
* This version must be updated when publishing a new release.
|
|
9
|
+
* It's used in:
|
|
10
|
+
* - User-Agent header: oilpriceapi-node/{version}
|
|
11
|
+
* - X-Client-Version header
|
|
12
|
+
* - Package.json (should match)
|
|
13
|
+
*/
|
|
14
|
+
exports.SDK_VERSION = "0.9.0";
|
|
15
|
+
/**
|
|
16
|
+
* SDK identifier used in User-Agent and X-Api-Client headers
|
|
17
|
+
*/
|
|
18
|
+
exports.SDK_NAME = "oilpriceapi-node";
|
|
19
|
+
/**
|
|
20
|
+
* Build the full User-Agent string
|
|
21
|
+
*/
|
|
22
|
+
function buildUserAgent() {
|
|
23
|
+
return `${exports.SDK_NAME}/${exports.SDK_VERSION} node/${process.version}`;
|
|
24
|
+
}
|