@uptimizr/collector-server 2.1.0 → 2.2.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/AGENTS.md +179 -6
- package/README.md +69 -0
- package/dist/__tests__/support/registryRequests.d.ts +2 -1
- package/dist/__tests__/support/registryRequests.d.ts.map +1 -1
- package/dist/__tests__/support/registryRequests.js +17 -1
- package/dist/__tests__/support/registryRequests.js.map +1 -1
- package/dist/__tests__/support/subscriptionFixtures.d.ts +63 -0
- package/dist/__tests__/support/subscriptionFixtures.d.ts.map +1 -0
- package/dist/__tests__/support/subscriptionFixtures.js +17 -0
- package/dist/__tests__/support/subscriptionFixtures.js.map +1 -0
- package/dist/agentReport.d.ts +256 -0
- package/dist/agentReport.d.ts.map +1 -0
- package/dist/agentReport.js +861 -0
- package/dist/agentReport.js.map +1 -0
- package/dist/app.d.ts +13 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +112 -5
- package/dist/app.js.map +1 -1
- package/dist/audit.d.ts +5 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +24 -4
- package/dist/audit.js.map +1 -1
- package/dist/auth.d.ts +41 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +47 -0
- package/dist/auth.js.map +1 -1
- package/dist/cli.js +99 -2
- package/dist/cli.js.map +1 -1
- package/dist/clickhouseStore.d.ts.map +1 -1
- package/dist/clickhouseStore.js +44 -2
- package/dist/clickhouseStore.js.map +1 -1
- package/dist/config.d.ts +49 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -0
- package/dist/config.js.map +1 -1
- package/dist/connectionLimiter.d.ts +23 -0
- package/dist/connectionLimiter.d.ts.map +1 -0
- package/dist/connectionLimiter.js +24 -0
- package/dist/connectionLimiter.js.map +1 -0
- package/dist/duckdbStore.d.ts.map +1 -1
- package/dist/duckdbStore.js +44 -1
- package/dist/duckdbStore.js.map +1 -1
- package/dist/internalDispatch.d.ts +36 -0
- package/dist/internalDispatch.d.ts.map +1 -0
- package/dist/internalDispatch.js +50 -0
- package/dist/internalDispatch.js.map +1 -0
- package/dist/memoryStore.d.ts.map +1 -1
- package/dist/memoryStore.js +314 -1
- package/dist/memoryStore.js.map +1 -1
- package/dist/mssqlStore.d.ts.map +1 -1
- package/dist/mssqlStore.js +44 -2
- package/dist/mssqlStore.js.map +1 -1
- package/dist/postgresStore.d.ts.map +1 -1
- package/dist/postgresStore.js +44 -2
- package/dist/postgresStore.js.map +1 -1
- package/dist/projectMetadata.d.ts +82 -0
- package/dist/projectMetadata.d.ts.map +1 -0
- package/dist/projectMetadata.js +50 -0
- package/dist/projectMetadata.js.map +1 -0
- package/dist/routes/context.d.ts +135 -0
- package/dist/routes/context.d.ts.map +1 -0
- package/dist/routes/context.js +306 -0
- package/dist/routes/context.js.map +1 -0
- package/dist/routes/insights.d.ts +36 -0
- package/dist/routes/insights.d.ts.map +1 -0
- package/dist/routes/insights.js +810 -0
- package/dist/routes/insights.js.map +1 -0
- package/dist/routes/live.d.ts +8 -0
- package/dist/routes/live.d.ts.map +1 -1
- package/dist/routes/live.js +8 -7
- package/dist/routes/live.js.map +1 -1
- package/dist/routes/mcp.d.ts +36 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +262 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/meta.d.ts +16 -2
- package/dist/routes/meta.d.ts.map +1 -1
- package/dist/routes/meta.js +699 -10
- package/dist/routes/meta.js.map +1 -1
- package/dist/routes/metadata.d.ts +31 -0
- package/dist/routes/metadata.d.ts.map +1 -0
- package/dist/routes/metadata.js +163 -0
- package/dist/routes/metadata.js.map +1 -0
- package/dist/routes/narrative.d.ts +36 -0
- package/dist/routes/narrative.d.ts.map +1 -0
- package/dist/routes/narrative.js +95 -0
- package/dist/routes/narrative.js.map +1 -0
- package/dist/routes/panels.d.ts +44 -0
- package/dist/routes/panels.d.ts.map +1 -0
- package/dist/routes/panels.js +108 -0
- package/dist/routes/panels.js.map +1 -0
- package/dist/routes/query-dsl.d.ts +50 -0
- package/dist/routes/query-dsl.d.ts.map +1 -0
- package/dist/routes/query-dsl.js +359 -0
- package/dist/routes/query-dsl.js.map +1 -0
- package/dist/routes/query.d.ts +36 -0
- package/dist/routes/query.d.ts.map +1 -1
- package/dist/routes/query.js +101 -13
- package/dist/routes/query.js.map +1 -1
- package/dist/routes/subscriptions.d.ts +52 -0
- package/dist/routes/subscriptions.d.ts.map +1 -0
- package/dist/routes/subscriptions.js +290 -0
- package/dist/routes/subscriptions.js.map +1 -0
- package/dist/store.d.ts +113 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/subscriptions/cli.d.ts +36 -0
- package/dist/subscriptions/cli.d.ts.map +1 -0
- package/dist/subscriptions/cli.js +143 -0
- package/dist/subscriptions/cli.js.map +1 -0
- package/dist/subscriptions/delivery.d.ts +101 -0
- package/dist/subscriptions/delivery.d.ts.map +1 -0
- package/dist/subscriptions/delivery.js +165 -0
- package/dist/subscriptions/delivery.js.map +1 -0
- package/dist/subscriptions/evaluate.d.ts +119 -0
- package/dist/subscriptions/evaluate.d.ts.map +1 -0
- package/dist/subscriptions/evaluate.js +326 -0
- package/dist/subscriptions/evaluate.js.map +1 -0
- package/dist/subscriptions/scheduler.d.ts +97 -0
- package/dist/subscriptions/scheduler.d.ts.map +1 -0
- package/dist/subscriptions/scheduler.js +332 -0
- package/dist/subscriptions/scheduler.js.map +1 -0
- package/dist/subscriptions/stream.d.ts +40 -0
- package/dist/subscriptions/stream.d.ts.map +1 -0
- package/dist/subscriptions/stream.js +38 -0
- package/dist/subscriptions/stream.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +25 -0
- package/dist/version.js.map +1 -0
- package/dist/webhookSignature.d.ts +64 -0
- package/dist/webhookSignature.d.ts.map +1 -0
- package/dist/webhookSignature.js +91 -0
- package/dist/webhookSignature.js.map +1 -0
- package/llms.txt +38 -1
- package/package.json +17 -14
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { isBusBackedPredicate, parseDurationMs } from "@uptimizr/schema";
|
|
2
|
+
import { getMetric } from "@uptimizr/metrics";
|
|
3
|
+
import { evaluateSubscription, summaryFor, toFiring } from "./evaluate.js";
|
|
4
|
+
import { deliver } from "./delivery.js";
|
|
5
|
+
/**
|
|
6
|
+
* **The subscription scheduler** (#311, ADR 0051 §6 / sketch §F.2).
|
|
7
|
+
*
|
|
8
|
+
* One in-process scheduler for the whole collector. Its job is small and its
|
|
9
|
+
* constraints are the interesting part:
|
|
10
|
+
*
|
|
11
|
+
* - **Every subscription owns one timer**, firing on its own `evaluate.every`.
|
|
12
|
+
* Even the bus-backed predicates have one: `presence <= 0` ("nobody is in the
|
|
13
|
+
* scene") can only become true when events *stop* arriving, so a scheduler
|
|
14
|
+
* that only woke on bus traffic could never report it.
|
|
15
|
+
* - **The bus is an accelerator, not the clock.** `presence` and `new_value`
|
|
16
|
+
* additionally watch `LiveBus.subscribe` so they react in seconds rather than
|
|
17
|
+
* at the next tick. The state each one keeps is bounded
|
|
18
|
+
* ({@link NEW_VALUE_MAX_TRIGGERS}) and the trigger is rate-limited
|
|
19
|
+
* ({@link BUS_MIN_INTERVAL_MS}), so a firehose cannot turn into a query storm.
|
|
20
|
+
* The *answer* still comes from the store evaluation, so the bus can only
|
|
21
|
+
* change when a subscription is checked, never what it concludes.
|
|
22
|
+
* - **At most {@link CollectorConfig.subscriptionsMaxConcurrent} evaluations run
|
|
23
|
+
* at once**, and at most one per subscription. A tick that arrives while the
|
|
24
|
+
* previous one is still in flight is dropped, not queued — a subscription that
|
|
25
|
+
* cannot keep up with its own interval must not grow a backlog.
|
|
26
|
+
* - **Cooldown is enforced before delivery, not before evaluation.** The
|
|
27
|
+
* evaluation is cheap and its result is what `POST …/test` and the dashboard
|
|
28
|
+
* read; the *firing* is what costs a webhook.
|
|
29
|
+
* - **Every timer is `unref`'d and cleared by {@link SubscriptionScheduler.stop}.**
|
|
30
|
+
* A test that starts the scheduler and closes the app leaves nothing behind.
|
|
31
|
+
*/
|
|
32
|
+
/** Smallest gap between two bus-triggered evaluations of one subscription. */
|
|
33
|
+
export const BUS_MIN_INTERVAL_MS = 5_000;
|
|
34
|
+
/**
|
|
35
|
+
* Distinct dimension values one `new_value` watcher remembers having triggered
|
|
36
|
+
* on. Past this the set is cleared and the watcher falls back to its timer — the
|
|
37
|
+
* predicate is for low-cardinality dimensions ("a new scene", "a new custom
|
|
38
|
+
* event"), and an unbounded set on a hot bus is exactly the kind of state
|
|
39
|
+
* ADR 0032 §6 exists to prevent.
|
|
40
|
+
*/
|
|
41
|
+
export const NEW_VALUE_MAX_TRIGGERS = 256;
|
|
42
|
+
/** How often the scheduler re-reads the store to pick up external changes. */
|
|
43
|
+
export const RELOAD_INTERVAL_MS = 60_000;
|
|
44
|
+
/** Promoted event fields a `new_value` dimension maps to on a live event. */
|
|
45
|
+
const EVENT_FIELD = {
|
|
46
|
+
scene: "sceneId",
|
|
47
|
+
name: "name",
|
|
48
|
+
mesh: "mesh",
|
|
49
|
+
source: "source",
|
|
50
|
+
};
|
|
51
|
+
/** Read one promoted dimension value off a live event. */
|
|
52
|
+
function dimensionValueOf(event, dimension) {
|
|
53
|
+
const field = EVENT_FIELD[dimension];
|
|
54
|
+
if (field == null)
|
|
55
|
+
return null;
|
|
56
|
+
const value = event[field];
|
|
57
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create the scheduler. It does nothing until {@link SubscriptionScheduler.reload}
|
|
61
|
+
* is called, which `app.ts` does once at startup and every CRUD write does after
|
|
62
|
+
* it changes something.
|
|
63
|
+
*/
|
|
64
|
+
export function createSubscriptionScheduler(deps) {
|
|
65
|
+
const now = deps.now ?? Date.now;
|
|
66
|
+
const maxConcurrent = Math.max(1, deps.config.subscriptionsMaxConcurrent);
|
|
67
|
+
const slots = new Map();
|
|
68
|
+
const watchers = new Map();
|
|
69
|
+
let inFlight = 0;
|
|
70
|
+
let stopped = false;
|
|
71
|
+
let reloadTimer = null;
|
|
72
|
+
/** The registry entry a subscription watches, or `null` if it went away. */
|
|
73
|
+
function metricOf(sub) {
|
|
74
|
+
return getMetric(sub.metric) ?? null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Run one evaluation, respecting the global concurrency cap and the
|
|
78
|
+
* one-per-subscription rule.
|
|
79
|
+
*
|
|
80
|
+
* Returns `null` when the slot was busy or the cap was full — a dropped tick,
|
|
81
|
+
* which is the correct answer for a periodic check whose previous run has not
|
|
82
|
+
* finished. Queueing them would build exactly the backlog the cap exists to
|
|
83
|
+
* prevent.
|
|
84
|
+
*/
|
|
85
|
+
async function tick(slot, options = {}) {
|
|
86
|
+
if (stopped || slot.running || inFlight >= maxConcurrent)
|
|
87
|
+
return null;
|
|
88
|
+
slot.running = true;
|
|
89
|
+
inFlight += 1;
|
|
90
|
+
try {
|
|
91
|
+
return await evaluateAndDeliver(slot.sub, options);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
deps.log.warn({ err, subscription: slot.sub.id }, "subscription evaluation failed");
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
inFlight -= 1;
|
|
99
|
+
slot.running = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Evaluate, then record + deliver when it fired and is out of cooldown. */
|
|
103
|
+
async function evaluateAndDeliver(sub, options) {
|
|
104
|
+
const metric = metricOf(sub);
|
|
105
|
+
if (metric == null) {
|
|
106
|
+
throw new Error(`subscription ${sub.id} names unknown metric ${JSON.stringify(sub.metric)}`);
|
|
107
|
+
}
|
|
108
|
+
const result = await evaluateSubscription({
|
|
109
|
+
readBuckets: (opts) => deps.store.metricBuckets(sub.projectId, opts),
|
|
110
|
+
activeSessions: () => deps.liveBus.presence(sub.projectId).activeSessions,
|
|
111
|
+
now,
|
|
112
|
+
}, sub, metric);
|
|
113
|
+
if (!result.fired || options.deliver === false)
|
|
114
|
+
return result;
|
|
115
|
+
const at = now();
|
|
116
|
+
const cooldownMs = parseDurationMs(sub.cooldown) ?? 0;
|
|
117
|
+
if (sub.lastFiredAt != null && at - sub.lastFiredAt.getTime() < cooldownMs)
|
|
118
|
+
return result;
|
|
119
|
+
const firing = toFiring(sub, result, at);
|
|
120
|
+
const summary = summaryFor(sub, result);
|
|
121
|
+
// The firing is recorded before it is delivered: a webhook that times out
|
|
122
|
+
// must still leave evidence that the condition was met.
|
|
123
|
+
await deps.store.recordSubscriptionEvent({
|
|
124
|
+
subscriptionId: sub.id,
|
|
125
|
+
projectId: sub.projectId,
|
|
126
|
+
at: new Date(at),
|
|
127
|
+
payload: { ...firing, summary },
|
|
128
|
+
});
|
|
129
|
+
let outcome;
|
|
130
|
+
try {
|
|
131
|
+
outcome = await deliver(sub, firing, summary, {
|
|
132
|
+
broadcast: (f, s) => deps.stream.publish(sub.projectId, { firing: f, summary: s }),
|
|
133
|
+
secret: await deps.store.getWebhookSecret(sub.projectId, sub.id),
|
|
134
|
+
allowedHosts: deps.config.webhookAllowedHosts,
|
|
135
|
+
fetchImpl: deps.fetchImpl,
|
|
136
|
+
sleep: deps.sleep,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
outcome = {
|
|
141
|
+
ok: false,
|
|
142
|
+
error: err instanceof Error ? err.message : String(err),
|
|
143
|
+
attempts: 0,
|
|
144
|
+
egress: false,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// `lastFiredAt` is stamped whether or not delivery succeeded: the condition
|
|
148
|
+
// fired, and a receiver that is down must not turn the cooldown off and let
|
|
149
|
+
// the subscription re-fire on every tick.
|
|
150
|
+
const failures = outcome.ok ? 0 : sub.failures + 1;
|
|
151
|
+
await deps.store.recordSubscriptionOutcome(sub.projectId, sub.id, {
|
|
152
|
+
firedAt: new Date(at),
|
|
153
|
+
lastError: outcome.error,
|
|
154
|
+
failures,
|
|
155
|
+
});
|
|
156
|
+
// Keep the in-memory slot in step so the cooldown holds until the next
|
|
157
|
+
// reload, rather than only in the row.
|
|
158
|
+
sub.lastFiredAt = new Date(at);
|
|
159
|
+
sub.failures = failures;
|
|
160
|
+
sub.lastError = outcome.error;
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
/** Attach the per-project bus watcher, creating it on first use. */
|
|
164
|
+
function ensureWatcher(projectId) {
|
|
165
|
+
if (watchers.has(projectId))
|
|
166
|
+
return;
|
|
167
|
+
const subscriber = deps.liveBus.subscribe({ projectId });
|
|
168
|
+
const watcher = { subscriber, stopped: false };
|
|
169
|
+
watchers.set(projectId, watcher);
|
|
170
|
+
const pump = async () => {
|
|
171
|
+
for await (const event of subscriber) {
|
|
172
|
+
if (watcher.stopped || stopped)
|
|
173
|
+
break;
|
|
174
|
+
onEvent(projectId, event);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
void pump();
|
|
178
|
+
}
|
|
179
|
+
/** Dispatch one arriving event to this project's bus-backed slots. */
|
|
180
|
+
function onEvent(projectId, event) {
|
|
181
|
+
const at = now();
|
|
182
|
+
for (const slot of slots.values()) {
|
|
183
|
+
if (slot.sub.projectId !== projectId)
|
|
184
|
+
continue;
|
|
185
|
+
if (!isBusBackedPredicate(slot.sub.predicate.kind))
|
|
186
|
+
continue;
|
|
187
|
+
if (at - slot.lastBusRun < BUS_MIN_INTERVAL_MS)
|
|
188
|
+
continue;
|
|
189
|
+
if (slot.sub.predicate.kind === "new_value") {
|
|
190
|
+
const value = dimensionValueOf(event, slot.sub.predicate.dimension);
|
|
191
|
+
// Only an unseen value is worth waking the store for. The set is this
|
|
192
|
+
// watcher's whole memory, and it is bounded.
|
|
193
|
+
if (value == null || slot.triggered.has(value))
|
|
194
|
+
continue;
|
|
195
|
+
if (slot.triggered.size >= NEW_VALUE_MAX_TRIGGERS)
|
|
196
|
+
slot.triggered.clear();
|
|
197
|
+
slot.triggered.add(value);
|
|
198
|
+
}
|
|
199
|
+
slot.lastBusRun = at;
|
|
200
|
+
void tick(slot);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Start one subscription's timer.
|
|
205
|
+
*
|
|
206
|
+
* The first evaluation is delayed by a jittered fraction of the interval, so
|
|
207
|
+
* a hundred subscriptions restored from the store on one boot spread their
|
|
208
|
+
* first reads across the interval instead of hitting the store together. From
|
|
209
|
+
* then on it is a plain interval — the phase set by the jitter persists,
|
|
210
|
+
* because `reload` deliberately does not restart a timer whose cadence is
|
|
211
|
+
* unchanged.
|
|
212
|
+
*/
|
|
213
|
+
function schedule(sub) {
|
|
214
|
+
const everyMs = Math.max(parseDurationMs(sub.evaluate.every) ?? 60_000, 60_000);
|
|
215
|
+
const jitter = Math.floor(Math.min(Math.max(deps.jitter?.() ?? Math.random(), 0), 0.999) * everyMs);
|
|
216
|
+
const slot = {
|
|
217
|
+
sub,
|
|
218
|
+
start: null,
|
|
219
|
+
timer: null,
|
|
220
|
+
running: false,
|
|
221
|
+
lastBusRun: 0,
|
|
222
|
+
triggered: new Set(),
|
|
223
|
+
};
|
|
224
|
+
slot.start = setTimeout(() => {
|
|
225
|
+
slot.start = null;
|
|
226
|
+
void tick(slot);
|
|
227
|
+
slot.timer = setInterval(() => void tick(slot), everyMs);
|
|
228
|
+
slot.timer.unref?.();
|
|
229
|
+
}, jitter);
|
|
230
|
+
slot.start.unref?.();
|
|
231
|
+
slots.set(sub.id, slot);
|
|
232
|
+
if (isBusBackedPredicate(sub.predicate.kind))
|
|
233
|
+
ensureWatcher(sub.projectId);
|
|
234
|
+
}
|
|
235
|
+
function unschedule(id) {
|
|
236
|
+
const slot = slots.get(id);
|
|
237
|
+
if (slot == null)
|
|
238
|
+
return;
|
|
239
|
+
if (slot.start != null)
|
|
240
|
+
clearTimeout(slot.start);
|
|
241
|
+
if (slot.timer != null)
|
|
242
|
+
clearInterval(slot.timer);
|
|
243
|
+
slots.delete(id);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Reconcile timers and bus watchers against the store.
|
|
247
|
+
*
|
|
248
|
+
* A named function rather than only a method, because the periodic reconcile
|
|
249
|
+
* below schedules it: an arrow calling `this.reload()` would break the moment
|
|
250
|
+
* anyone destructured the scheduler.
|
|
251
|
+
*/
|
|
252
|
+
async function reloadNow() {
|
|
253
|
+
if (stopped)
|
|
254
|
+
return;
|
|
255
|
+
let enabled;
|
|
256
|
+
try {
|
|
257
|
+
enabled = await deps.store.listEnabledSubscriptions();
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
deps.log.warn({ err }, "failed to read subscriptions; keeping the current schedule");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const wanted = new Map(enabled.map((sub) => [sub.id, sub]));
|
|
264
|
+
for (const id of [...slots.keys()])
|
|
265
|
+
if (!wanted.has(id))
|
|
266
|
+
unschedule(id);
|
|
267
|
+
for (const [id, sub] of wanted) {
|
|
268
|
+
const slot = slots.get(id);
|
|
269
|
+
if (slot == null) {
|
|
270
|
+
schedule(sub);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
// Reschedule only when the cadence changed; otherwise keep the timer
|
|
274
|
+
// (and its phase) and just refresh the declaration, so a reload does not
|
|
275
|
+
// silently re-align every subscription to the same instant.
|
|
276
|
+
if (slot.sub.evaluate.every !== sub.evaluate.every) {
|
|
277
|
+
unschedule(id);
|
|
278
|
+
schedule(sub);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
slot.sub = sub;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (reloadTimer == null) {
|
|
285
|
+
// Another instance sharing a Postgres/SQL Server database can change the
|
|
286
|
+
// set without going through this process's routes.
|
|
287
|
+
reloadTimer = setInterval(() => void reloadNow(), RELOAD_INTERVAL_MS);
|
|
288
|
+
reloadTimer.unref?.();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
reload: reloadNow,
|
|
293
|
+
async runOnce(sub, options = {}) {
|
|
294
|
+
// An out-of-band run of a *scheduled* subscription takes the slot's
|
|
295
|
+
// one-in-flight guard, so `POST …/test` cannot race its own timer. A
|
|
296
|
+
// disabled (therefore unscheduled) subscription gets a fresh slot-less
|
|
297
|
+
// run, still under the global cap.
|
|
298
|
+
const slot = slots.get(sub.id);
|
|
299
|
+
if (slot != null)
|
|
300
|
+
return tick(slot, options);
|
|
301
|
+
if (stopped || inFlight >= maxConcurrent)
|
|
302
|
+
return null;
|
|
303
|
+
inFlight += 1;
|
|
304
|
+
try {
|
|
305
|
+
return await evaluateAndDeliver(sub, options);
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
inFlight -= 1;
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
get scheduledCount() {
|
|
312
|
+
return slots.size;
|
|
313
|
+
},
|
|
314
|
+
get inFlight() {
|
|
315
|
+
return inFlight;
|
|
316
|
+
},
|
|
317
|
+
stop() {
|
|
318
|
+
stopped = true;
|
|
319
|
+
if (reloadTimer != null)
|
|
320
|
+
clearInterval(reloadTimer);
|
|
321
|
+
reloadTimer = null;
|
|
322
|
+
for (const id of [...slots.keys()])
|
|
323
|
+
unschedule(id);
|
|
324
|
+
for (const watcher of watchers.values()) {
|
|
325
|
+
watcher.stopped = true;
|
|
326
|
+
watcher.subscriber.close();
|
|
327
|
+
}
|
|
328
|
+
watchers.clear();
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/subscriptions/scheduler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { SubscriptionFiring } from "@uptimizr/schema";
|
|
2
|
+
/**
|
|
3
|
+
* **In-process fan-out for subscription firings** (#311, sketch §F.3).
|
|
4
|
+
*
|
|
5
|
+
* The SSE half of delivery. It is deliberately *not* the live bus: the live bus
|
|
6
|
+
* carries every ingested event and is hot, whereas this carries at most one
|
|
7
|
+
* message per subscription per cooldown and exists only so
|
|
8
|
+
* `GET /api/v1/subscriptions/stream` has something to read from. Keeping them
|
|
9
|
+
* apart means a firing can never be dropped by a slow live consumer's queue, and
|
|
10
|
+
* a subscription listener can never back-pressure ingest.
|
|
11
|
+
*
|
|
12
|
+
* Listeners are plain callbacks rather than async iterators because the SSE
|
|
13
|
+
* route writes synchronously to a socket; there is nothing to buffer, and
|
|
14
|
+
* nothing to bound beyond the connection cap the route already enforces.
|
|
15
|
+
*/
|
|
16
|
+
/** What an SSE listener receives. */
|
|
17
|
+
export interface SubscriptionStreamMessage {
|
|
18
|
+
firing: SubscriptionFiring;
|
|
19
|
+
/** The same bounded `format=summary` block a webhook would carry. */
|
|
20
|
+
summary: Record<string, unknown> | null;
|
|
21
|
+
}
|
|
22
|
+
export type SubscriptionStreamListener = (message: SubscriptionStreamMessage) => void;
|
|
23
|
+
export interface SubscriptionStreamOptions {
|
|
24
|
+
projectId: string;
|
|
25
|
+
/** Deliver only this subscription's firings; omit for the whole project. */
|
|
26
|
+
subscriptionId?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface SubscriptionStream {
|
|
29
|
+
/** Register a listener. Returns an idempotent unsubscribe. */
|
|
30
|
+
subscribe(options: SubscriptionStreamOptions, listener: SubscriptionStreamListener): () => void;
|
|
31
|
+
/** Fan a firing out to matching listeners. */
|
|
32
|
+
publish(projectId: string, message: SubscriptionStreamMessage): void;
|
|
33
|
+
/** Open listener count (for tests and the connection accounting). */
|
|
34
|
+
readonly listenerCount: number;
|
|
35
|
+
/** Drop every listener. */
|
|
36
|
+
stop(): void;
|
|
37
|
+
}
|
|
38
|
+
/** Create the default in-process subscription stream. */
|
|
39
|
+
export declare function createSubscriptionStream(): SubscriptionStream;
|
|
40
|
+
//# sourceMappingURL=stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/subscriptions/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D;;;;;;;;;;;;;GAaG;AAEH,qCAAqC;AACrC,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACzC;AAED,MAAM,MAAM,0BAA0B,GAAG,CAAC,OAAO,EAAE,yBAAyB,KAAK,IAAI,CAAC;AAEtF,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,8DAA8D;IAC9D,SAAS,CAAC,OAAO,EAAE,yBAAyB,EAAE,QAAQ,EAAE,0BAA0B,GAAG,MAAM,IAAI,CAAC;IAChG,8CAA8C;IAC9C,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,yBAAyB,GAAG,IAAI,CAAC;IACrE,qEAAqE;IACrE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,2BAA2B;IAC3B,IAAI,IAAI,IAAI,CAAC;CACd;AAOD,yDAAyD;AACzD,wBAAgB,wBAAwB,IAAI,kBAAkB,CAuC7D"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Create the default in-process subscription stream. */
|
|
2
|
+
export function createSubscriptionStream() {
|
|
3
|
+
const entries = new Set();
|
|
4
|
+
return {
|
|
5
|
+
subscribe(options, listener) {
|
|
6
|
+
const entry = { options, listener };
|
|
7
|
+
entries.add(entry);
|
|
8
|
+
return () => {
|
|
9
|
+
entries.delete(entry);
|
|
10
|
+
};
|
|
11
|
+
},
|
|
12
|
+
publish(projectId, message) {
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
if (entry.options.projectId !== projectId)
|
|
15
|
+
continue;
|
|
16
|
+
if (entry.options.subscriptionId != null &&
|
|
17
|
+
entry.options.subscriptionId !== message.firing.subscriptionId) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
// A throwing listener is a broken socket, not a reason to abandon the
|
|
21
|
+
// rest of the fan-out.
|
|
22
|
+
try {
|
|
23
|
+
entry.listener(message);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* ignore */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
get listenerCount() {
|
|
31
|
+
return entries.size;
|
|
32
|
+
},
|
|
33
|
+
stop() {
|
|
34
|
+
entries.clear();
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/subscriptions/stream.ts"],"names":[],"mappings":"AAgDA,yDAAyD;AACzD,MAAM,UAAU,wBAAwB;IACtC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAS,CAAC;IAEjC,OAAO;QACL,SAAS,CAAC,OAAO,EAAE,QAAQ;YACzB,MAAM,KAAK,GAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;YAC3C,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACnB,OAAO,GAAG,EAAE;gBACV,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACxB,CAAC,CAAC;QACJ,CAAC;QAED,OAAO,CAAC,SAAS,EAAE,OAAO;YACxB,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;gBAC5B,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,KAAK,SAAS;oBAAE,SAAS;gBACpD,IACE,KAAK,CAAC,OAAO,CAAC,cAAc,IAAI,IAAI;oBACpC,KAAK,CAAC,OAAO,CAAC,cAAc,KAAK,OAAO,CAAC,MAAM,CAAC,cAAc,EAC9D,CAAC;oBACD,SAAS;gBACX,CAAC;gBACD,sEAAsE;gBACtE,uBAAuB;gBACvB,IAAI,CAAC;oBACH,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;gBAC1B,CAAC;gBAAC,MAAM,CAAC;oBACP,YAAY;gBACd,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,aAAa;YACf,OAAO,OAAO,CAAC,IAAI,CAAC;QACtB,CAAC;QAED,IAAI;YACF,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAuBA,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,EAAE,MAAsB,CAAC"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
/**
|
|
3
|
+
* The running collector's package version, reported in the project context
|
|
4
|
+
* document (`GET /api/v1/context`, ADR 0051 §5) so an agent can tell an old
|
|
5
|
+
* self-hosted collector from a current one before it blames the data.
|
|
6
|
+
*
|
|
7
|
+
* Read from the package's own `package.json` at load time rather than hard-coded,
|
|
8
|
+
* so a Changesets release bump cannot drift from what the server reports. The
|
|
9
|
+
* lookup resolves relative to this module, which sits one directory below the
|
|
10
|
+
* package root both in `src/` (under Vitest) and in `dist/` (built). Any failure
|
|
11
|
+
* degrades to `"unknown"`: the version is descriptive metadata, and a collector
|
|
12
|
+
* that cannot read its own manifest must still serve.
|
|
13
|
+
*/
|
|
14
|
+
function readVersion() {
|
|
15
|
+
try {
|
|
16
|
+
const manifest = createRequire(import.meta.url)("../package.json");
|
|
17
|
+
return typeof manifest.version === "string" ? manifest.version : "unknown";
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Semver of the running `@uptimizr/collector-server`, or `"unknown"`. */
|
|
24
|
+
export const COLLECTOR_VERSION = readVersion();
|
|
25
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C;;;;;;;;;;;GAWG;AACH,SAAS,WAAW;IAClB,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAA0B,CAAC;QAC5F,OAAO,OAAO,QAAQ,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,0EAA0E;AAC1E,MAAM,CAAC,MAAM,iBAAiB,GAAW,WAAW,EAAE,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signing an outbound webhook body (ADR 0051 §6, design sketch §F.3/§F.4).
|
|
3
|
+
*
|
|
4
|
+
* Everything Uptimizr pushes out of a self-hoster's process — the scheduled
|
|
5
|
+
* agent report today, conditional-subscription deliveries next — is POSTed with
|
|
6
|
+
* an HMAC-SHA-256 of the **exact bytes sent**, so a receiver (Slack relay, a
|
|
7
|
+
* GitHub Action, the operator's own agent) can prove the payload came from their
|
|
8
|
+
* collector and was not modified in transit.
|
|
9
|
+
*
|
|
10
|
+
* The scheme, deliberately boring and identical to what every webhook consumer
|
|
11
|
+
* already knows how to verify:
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* X-Uptimizr-Signature: sha256=<lowercase hex HMAC-SHA-256(secret, rawBody)>
|
|
15
|
+
* X-Uptimizr-Delivery: <random id, unique per delivery attempt>
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Verification MUST be done over the raw request body **before** JSON parsing
|
|
19
|
+
* (re-serialising changes bytes) and MUST use a constant-time comparison —
|
|
20
|
+
* {@link verifyWebhookSignature} does both.
|
|
21
|
+
*
|
|
22
|
+
* No secret is ever logged, persisted or echoed: it is read from
|
|
23
|
+
* `UPTIMIZR_WEBHOOK_SECRET` at the point of use and passed straight to the HMAC.
|
|
24
|
+
*/
|
|
25
|
+
/** Header carrying `sha256=<hex>` over the raw body. */
|
|
26
|
+
export declare const WEBHOOK_SIGNATURE_HEADER = "X-Uptimizr-Signature";
|
|
27
|
+
/** Header carrying a unique id per delivery attempt (for receiver-side dedupe). */
|
|
28
|
+
export declare const WEBHOOK_DELIVERY_HEADER = "X-Uptimizr-Delivery";
|
|
29
|
+
/**
|
|
30
|
+
* Sign a raw body with the shared secret.
|
|
31
|
+
*
|
|
32
|
+
* @param secret The shared secret (`UPTIMIZR_WEBHOOK_SECRET`).
|
|
33
|
+
* @param body The exact bytes that will be sent as the request body.
|
|
34
|
+
* @returns The value for {@link WEBHOOK_SIGNATURE_HEADER} (`sha256=<hex>`).
|
|
35
|
+
*/
|
|
36
|
+
export declare function signWebhookBody(secret: string, body: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Verify a received signature header against the raw body, in constant time.
|
|
39
|
+
*
|
|
40
|
+
* Returns `false` — never throws — for a missing header, the wrong prefix, a
|
|
41
|
+
* wrong-length digest or a mismatch, so a receiver can reject uniformly without
|
|
42
|
+
* leaking which check failed.
|
|
43
|
+
*/
|
|
44
|
+
export declare function verifyWebhookSignature(secret: string, body: string, header: string): boolean;
|
|
45
|
+
/** A fresh delivery id for {@link WEBHOOK_DELIVERY_HEADER}. */
|
|
46
|
+
export declare function newDeliveryId(): string;
|
|
47
|
+
/**
|
|
48
|
+
* Validate an operator-supplied webhook URL.
|
|
49
|
+
*
|
|
50
|
+
* The URL comes from the command line (or, later, a stored subscription), so it
|
|
51
|
+
* is request-controlled input to an outbound request: only `http:` and `https:`
|
|
52
|
+
* are accepted, which rules out `file:`, `data:` and any other scheme a fetch
|
|
53
|
+
* implementation might honour. Host reachability is deliberately **not**
|
|
54
|
+
* restricted — a self-hoster posting to `http://localhost:3000/hook` or to an
|
|
55
|
+
* internal Slack relay is the normal case, and the collector is not a
|
|
56
|
+
* multi-tenant proxy — so the SSRF boundary here is the operator's own shell.
|
|
57
|
+
* Anything that later accepts a webhook URL over HTTP must add its own
|
|
58
|
+
* allow-list on top of this check.
|
|
59
|
+
*
|
|
60
|
+
* @returns The parsed URL.
|
|
61
|
+
* @throws If the value is not a syntactically valid `http(s)` URL.
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseWebhookUrl(value: string): URL;
|
|
64
|
+
//# sourceMappingURL=webhookSignature.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhookSignature.d.ts","sourceRoot":"","sources":["../src/webhookSignature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,yBAAyB,CAAC;AAE/D,mFAAmF;AACnF,eAAO,MAAM,uBAAuB,wBAAwB,CAAC;AAK7D;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAO5F;AAED,+DAA+D;AAC/D,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAalD"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signing an outbound webhook body (ADR 0051 §6, design sketch §F.3/§F.4).
|
|
3
|
+
*
|
|
4
|
+
* Everything Uptimizr pushes out of a self-hoster's process — the scheduled
|
|
5
|
+
* agent report today, conditional-subscription deliveries next — is POSTed with
|
|
6
|
+
* an HMAC-SHA-256 of the **exact bytes sent**, so a receiver (Slack relay, a
|
|
7
|
+
* GitHub Action, the operator's own agent) can prove the payload came from their
|
|
8
|
+
* collector and was not modified in transit.
|
|
9
|
+
*
|
|
10
|
+
* The scheme, deliberately boring and identical to what every webhook consumer
|
|
11
|
+
* already knows how to verify:
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* X-Uptimizr-Signature: sha256=<lowercase hex HMAC-SHA-256(secret, rawBody)>
|
|
15
|
+
* X-Uptimizr-Delivery: <random id, unique per delivery attempt>
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Verification MUST be done over the raw request body **before** JSON parsing
|
|
19
|
+
* (re-serialising changes bytes) and MUST use a constant-time comparison —
|
|
20
|
+
* {@link verifyWebhookSignature} does both.
|
|
21
|
+
*
|
|
22
|
+
* No secret is ever logged, persisted or echoed: it is read from
|
|
23
|
+
* `UPTIMIZR_WEBHOOK_SECRET` at the point of use and passed straight to the HMAC.
|
|
24
|
+
*/
|
|
25
|
+
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
26
|
+
/** Header carrying `sha256=<hex>` over the raw body. */
|
|
27
|
+
export const WEBHOOK_SIGNATURE_HEADER = "X-Uptimizr-Signature";
|
|
28
|
+
/** Header carrying a unique id per delivery attempt (for receiver-side dedupe). */
|
|
29
|
+
export const WEBHOOK_DELIVERY_HEADER = "X-Uptimizr-Delivery";
|
|
30
|
+
/** The algorithm prefix the signature header always carries. */
|
|
31
|
+
const PREFIX = "sha256=";
|
|
32
|
+
/**
|
|
33
|
+
* Sign a raw body with the shared secret.
|
|
34
|
+
*
|
|
35
|
+
* @param secret The shared secret (`UPTIMIZR_WEBHOOK_SECRET`).
|
|
36
|
+
* @param body The exact bytes that will be sent as the request body.
|
|
37
|
+
* @returns The value for {@link WEBHOOK_SIGNATURE_HEADER} (`sha256=<hex>`).
|
|
38
|
+
*/
|
|
39
|
+
export function signWebhookBody(secret, body) {
|
|
40
|
+
return `${PREFIX}${createHmac("sha256", secret).update(body, "utf8").digest("hex")}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Verify a received signature header against the raw body, in constant time.
|
|
44
|
+
*
|
|
45
|
+
* Returns `false` — never throws — for a missing header, the wrong prefix, a
|
|
46
|
+
* wrong-length digest or a mismatch, so a receiver can reject uniformly without
|
|
47
|
+
* leaking which check failed.
|
|
48
|
+
*/
|
|
49
|
+
export function verifyWebhookSignature(secret, body, header) {
|
|
50
|
+
if (!header.startsWith(PREFIX))
|
|
51
|
+
return false;
|
|
52
|
+
const expected = Buffer.from(signWebhookBody(secret, body), "utf8");
|
|
53
|
+
const received = Buffer.from(header, "utf8");
|
|
54
|
+
// `timingSafeEqual` throws on a length mismatch, which is not secret (the
|
|
55
|
+
// digest length is fixed and public), so compare lengths first.
|
|
56
|
+
return expected.length === received.length && timingSafeEqual(expected, received);
|
|
57
|
+
}
|
|
58
|
+
/** A fresh delivery id for {@link WEBHOOK_DELIVERY_HEADER}. */
|
|
59
|
+
export function newDeliveryId() {
|
|
60
|
+
return randomUUID();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validate an operator-supplied webhook URL.
|
|
64
|
+
*
|
|
65
|
+
* The URL comes from the command line (or, later, a stored subscription), so it
|
|
66
|
+
* is request-controlled input to an outbound request: only `http:` and `https:`
|
|
67
|
+
* are accepted, which rules out `file:`, `data:` and any other scheme a fetch
|
|
68
|
+
* implementation might honour. Host reachability is deliberately **not**
|
|
69
|
+
* restricted — a self-hoster posting to `http://localhost:3000/hook` or to an
|
|
70
|
+
* internal Slack relay is the normal case, and the collector is not a
|
|
71
|
+
* multi-tenant proxy — so the SSRF boundary here is the operator's own shell.
|
|
72
|
+
* Anything that later accepts a webhook URL over HTTP must add its own
|
|
73
|
+
* allow-list on top of this check.
|
|
74
|
+
*
|
|
75
|
+
* @returns The parsed URL.
|
|
76
|
+
* @throws If the value is not a syntactically valid `http(s)` URL.
|
|
77
|
+
*/
|
|
78
|
+
export function parseWebhookUrl(value) {
|
|
79
|
+
let url;
|
|
80
|
+
try {
|
|
81
|
+
url = new URL(value);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new Error(`--webhook must be an absolute http(s) URL (got ${JSON.stringify(value)}).`);
|
|
85
|
+
}
|
|
86
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
87
|
+
throw new Error(`--webhook must use http or https (got ${JSON.stringify(url.protocol.replace(":", ""))}).`);
|
|
88
|
+
}
|
|
89
|
+
return url;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=webhookSignature.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhookSignature.js","sourceRoot":"","sources":["../src/webhookSignature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEtE,wDAAwD;AACxD,MAAM,CAAC,MAAM,wBAAwB,GAAG,sBAAsB,CAAC;AAE/D,mFAAmF;AACnF,MAAM,CAAC,MAAM,uBAAuB,GAAG,qBAAqB,CAAC;AAE7D,gEAAgE;AAChE,MAAM,MAAM,GAAG,SAAS,CAAC;AAEzB;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,IAAY;IAC1D,OAAO,GAAG,MAAM,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AACvF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAc,EAAE,IAAY,EAAE,MAAc;IACjF,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7C,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;IACpE,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,0EAA0E;IAC1E,gEAAgE;IAChE,OAAO,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,eAAe,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AACpF,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,aAAa;IAC3B,OAAO,UAAU,EAAE,CAAC;AACtB,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,kDAAkD,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/F,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CACb,yCAAyC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,IAAI,CAC3F,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|