@multiplier-labs/stepflow 0.3.3 → 0.3.4
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/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
- package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
- package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
- package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
- package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
- package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
- package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
- package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
- package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
- package/.github/workflows/publish.yml +6 -0
- package/dist/{chunk-4VOKMGP6.js → chunk-5XJBAPXF.js} +517 -522
- package/dist/chunk-5XJBAPXF.js.map +1 -0
- package/dist/{chunk-KHTUWLVZ.js → chunk-FMPJBK2N.js} +55 -10
- package/dist/chunk-FMPJBK2N.js.map +1 -0
- package/dist/{chunk-ML35PIHX.js → chunk-JPXPB7U5.js} +52 -2
- package/dist/chunk-JPXPB7U5.js.map +1 -0
- package/dist/{chunk-GYPNJBDP.js → chunk-U3PCVK6O.js} +266 -47
- package/dist/chunk-U3PCVK6O.js.map +1 -0
- package/dist/events/index.d.ts +56 -3
- package/dist/events/index.js +1 -1
- package/dist/{index-Dzr437iW.d.ts → index-Cd0IZ40L.d.ts} +50 -11
- package/dist/index.d.ts +2 -12
- package/dist/index.js +764 -485
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/index.js +2 -2
- package/dist/storage/index.d.ts +313 -47
- package/dist/storage/index.js +4 -2
- package/docs/stepflow-api-reference.md +3 -1
- package/package.json +7 -4
- package/src/core/engine/concurrency.ts +229 -0
- package/src/core/engine/lifecycle.ts +176 -0
- package/src/core/engine.test.ts +70 -0
- package/src/core/engine.ts +91 -185
- package/src/core/orchestrator/persistence.ts +165 -0
- package/src/core/orchestrator/plan-resolution.ts +140 -0
- package/src/core/orchestrator/state-transitions.ts +109 -0
- package/src/core/orchestrator/step-runner.ts +427 -0
- package/src/core/orchestrator.ts +40 -359
- package/src/events/webhook.test.ts +292 -0
- package/src/events/webhook.ts +320 -77
- package/src/planning/evaluate-condition.test.ts +153 -0
- package/src/planning/evaluate-condition.ts +105 -0
- package/src/planning/planner.ts +1 -83
- package/src/planning/registry.ts +3 -85
- package/src/scheduler/cron.test.ts +9 -1
- package/src/scheduler/cron.ts +26 -2
- package/src/scheduler/postgres-persistence.ts +27 -15
- package/src/scheduler/sqlite-persistence.ts +28 -3
- package/src/storage/postgres-core.ts +550 -0
- package/src/storage/postgres-migration.test.ts +69 -0
- package/src/storage/postgres-transaction-pagination.test.ts +107 -0
- package/src/storage/postgres.ts +171 -690
- package/src/storage/sqlite-core.ts +151 -0
- package/src/storage/sqlite.test.ts +177 -1
- package/src/storage/sqlite.ts +183 -93
- package/src/utils/index.ts +2 -0
- package/src/utils/pinned-fetch.test.ts +84 -0
- package/src/utils/pinned-fetch.ts +118 -0
- package/src/utils/postgres-deps.ts +16 -0
- package/src/utils/safe-json.test.ts +166 -0
- package/src/utils/safe-json.ts +107 -0
- package/src/utils/webhook-signature.test.ts +111 -0
- package/src/utils/webhook-signature.ts +74 -0
- package/dist/chunk-4VOKMGP6.js.map +0 -1
- package/dist/chunk-GYPNJBDP.js.map +0 -1
- package/dist/chunk-KHTUWLVZ.js.map +0 -1
- package/dist/chunk-ML35PIHX.js.map +0 -1
|
@@ -65,6 +65,44 @@ function createScopedLogger(logger, runId, stepKey) {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// src/utils/safe-json.ts
|
|
69
|
+
var corruptionCount = 0;
|
|
70
|
+
function safeJsonParse(json, fallback, ctx) {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(json);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error instanceof SyntaxError) {
|
|
75
|
+
corruptionCount++;
|
|
76
|
+
ctx.logger?.warn(
|
|
77
|
+
`[${ctx.component}] Corrupted JSON in database row, using fallback`,
|
|
78
|
+
{
|
|
79
|
+
rowId: ctx.rowId,
|
|
80
|
+
column: ctx.column,
|
|
81
|
+
length: typeof json === "string" ? json.length : void 0,
|
|
82
|
+
type: typeof json,
|
|
83
|
+
error: error.message,
|
|
84
|
+
corruptionCount
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
return fallback;
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function safeParseField(value, fallback, ctx) {
|
|
93
|
+
if (typeof value === "string") {
|
|
94
|
+
return safeJsonParse(value, fallback, ctx);
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
function safeParseOptionalField(value, ctx) {
|
|
99
|
+
if (!value) return void 0;
|
|
100
|
+
if (typeof value === "string") {
|
|
101
|
+
return safeJsonParse(value, void 0, ctx);
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
68
106
|
// src/utils/postgres-deps.ts
|
|
69
107
|
var cached;
|
|
70
108
|
var loadingPromise;
|
|
@@ -99,6 +137,14 @@ async function loadPostgresDeps() {
|
|
|
99
137
|
})();
|
|
100
138
|
return loadingPromise;
|
|
101
139
|
}
|
|
140
|
+
function requirePostgresDeps() {
|
|
141
|
+
if (!cached) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
"PostgreSQL dependencies have not been loaded yet. Call loadPostgresDeps() (or PostgresStorageAdapter.initialize()) first."
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return cached;
|
|
147
|
+
}
|
|
102
148
|
|
|
103
149
|
export {
|
|
104
150
|
generateId,
|
|
@@ -106,6 +152,10 @@ export {
|
|
|
106
152
|
SilentLogger,
|
|
107
153
|
sanitizeErrorForStorage,
|
|
108
154
|
createScopedLogger,
|
|
109
|
-
|
|
155
|
+
safeJsonParse,
|
|
156
|
+
safeParseField,
|
|
157
|
+
safeParseOptionalField,
|
|
158
|
+
loadPostgresDeps,
|
|
159
|
+
requirePostgresDeps
|
|
110
160
|
};
|
|
111
|
-
//# sourceMappingURL=chunk-
|
|
161
|
+
//# sourceMappingURL=chunk-JPXPB7U5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/id.ts","../src/utils/logger.ts","../src/utils/safe-json.ts","../src/utils/postgres-deps.ts"],"sourcesContent":["/**\n * ID generation utilities for the workflow engine.\n */\n\nimport { randomBytes } from 'node:crypto';\n\n/**\n * Generate a unique ID for database records.\n * Uses a ULID-like format: base36 timestamp + random suffix.\n * This provides:\n * - Rough time-ordering (useful for debugging)\n * - High collision resistance\n * - URL-safe characters\n *\n * @returns A unique identifier string (approx 16 characters)\n */\nexport function generateId(): string {\n const timestamp = Date.now().toString(36);\n const randomPart = randomBytes(6).toString('base64url');\n return `${timestamp}${randomPart}`;\n}\n","/**\n * Logger utilities for the workflow engine.\n */\n\nimport type { Logger, WorkflowError } from '../core/types';\n\n/** Log level for ConsoleLogger. Levels are ordered: debug < info < warn < error. */\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nconst LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n};\n\n/**\n * Console-based logger implementation.\n * Uses console.log with prefixes for different levels.\n * Supports a configurable minimum log level (default: 'info').\n */\nexport class ConsoleLogger implements Logger {\n private prefix: string;\n private minLevel: number;\n\n constructor(prefix = '[workflow]', level: LogLevel = 'info') {\n this.prefix = prefix;\n this.minLevel = LOG_LEVEL_PRIORITY[level];\n }\n\n debug(message: string, ...args: unknown[]): void {\n if (this.minLevel <= LOG_LEVEL_PRIORITY.debug) {\n console.debug(`${this.prefix} [DEBUG]`, message, ...args);\n }\n }\n\n info(message: string, ...args: unknown[]): void {\n if (this.minLevel <= LOG_LEVEL_PRIORITY.info) {\n console.info(`${this.prefix} [INFO]`, message, ...args);\n }\n }\n\n warn(message: string, ...args: unknown[]): void {\n if (this.minLevel <= LOG_LEVEL_PRIORITY.warn) {\n console.warn(`${this.prefix} [WARN]`, message, ...args);\n }\n }\n\n error(message: string, ...args: unknown[]): void {\n if (this.minLevel <= LOG_LEVEL_PRIORITY.error) {\n console.error(`${this.prefix} [ERROR]`, message, ...args);\n }\n }\n}\n\n/**\n * Silent logger that does nothing.\n * Useful for testing or when logs are not wanted.\n */\nexport class SilentLogger implements Logger {\n debug(): void {}\n info(): void {}\n warn(): void {}\n error(): void {}\n}\n\n/**\n * Strip stack traces from a WorkflowError before persisting to storage.\n * Stack traces expose internal file paths and should be kept in logs only.\n */\nexport function sanitizeErrorForStorage(error: WorkflowError): WorkflowError {\n const { stack: _stack, ...rest } = error;\n return rest;\n}\n\n/**\n * Create a scoped logger that includes run/step context.\n */\nexport function createScopedLogger(\n logger: Logger,\n runId: string,\n stepKey?: string\n): Logger {\n const prefix = stepKey ? `[run:${runId}][step:${stepKey}]` : `[run:${runId}]`;\n\n return {\n debug: (message: string, ...args: unknown[]) =>\n logger.debug(`${prefix} ${message}`, ...args),\n info: (message: string, ...args: unknown[]) =>\n logger.info(`${prefix} ${message}`, ...args),\n warn: (message: string, ...args: unknown[]) =>\n logger.warn(`${prefix} ${message}`, ...args),\n error: (message: string, ...args: unknown[]) =>\n logger.error(`${prefix} ${message}`, ...args),\n };\n}\n","/**\n * Safe JSON-parse helpers shared by storage and scheduler adapters.\n *\n * When a stored JSON column is corrupted, we want to surface the problem\n * operationally — structured log + counter — instead of silently degrading.\n * The logger payload is restricted to safe metadata (component, row id,\n * column, payload length, payload type, parser error message). The raw\n * value is intentionally never logged because it may contain user-provided\n * run / step input.\n *\n * Audit: 2026-04-27 findings L3 (corruption visibility) and M2 (do not\n * bypass the configured `Logger` or leak the raw value via `console`).\n */\n\nimport type { Logger } from '../core/types';\n\nlet corruptionCount = 0;\n\n/** Cumulative number of `safeJsonParse` failures since process start. */\nexport function getJsonParseCorruptionCount(): number {\n return corruptionCount;\n}\n\n/** Reset the corruption counter. Tests only. */\nexport function resetJsonParseCorruptionCount(): void {\n corruptionCount = 0;\n}\n\nexport interface SafeJsonParseContext {\n /** Component label, e.g. `SQLiteStorageAdapter`. */\n component: string;\n /** Optional database row id, surfaced in the warning so operators can locate the bad row. */\n rowId?: string;\n /** Optional column name for additional context. */\n column?: string;\n /** Optional structured logger; without one, only the counter is bumped. */\n logger?: Logger;\n}\n\n/**\n * Storage-adapter-level slice of `SafeJsonParseContext` — the part each\n * adapter binds once (its component label + optional logger). Row mappers\n * extend this with `rowId` and `column` per field, so corruption logs always\n * identify the bad row.\n *\n * Shared between `src/storage/postgres-core.ts` and\n * `src/storage/sqlite-core.ts` so all storage adapters use one mapper-context\n * shape regardless of backend.\n */\nexport type MapperContext = Pick<SafeJsonParseContext, 'component' | 'logger'>;\n\n/**\n * Parse a JSON string; on `SyntaxError` return `fallback`, increment the\n * corruption counter, and emit a structured warn log via `ctx.logger` (if\n * supplied). Non-syntax errors propagate so unexpected runtime issues are\n * still loud.\n */\nexport function safeJsonParse<T = unknown>(\n json: string,\n fallback: T,\n ctx: SafeJsonParseContext\n): T | unknown {\n try {\n return JSON.parse(json);\n } catch (error) {\n if (error instanceof SyntaxError) {\n corruptionCount++;\n ctx.logger?.warn(\n `[${ctx.component}] Corrupted JSON in database row, using fallback`,\n {\n rowId: ctx.rowId,\n column: ctx.column,\n length: typeof json === 'string' ? json.length : undefined,\n type: typeof json,\n error: error.message,\n corruptionCount,\n }\n );\n return fallback;\n }\n throw error;\n }\n}\n\n/** Parse a column value if it's still a JSON string; pass through otherwise. */\nexport function safeParseField(\n value: unknown,\n fallback: unknown,\n ctx: SafeJsonParseContext\n): unknown {\n if (typeof value === 'string') {\n return safeJsonParse(value, fallback, ctx);\n }\n return value;\n}\n\n/** Same as `safeParseField` but returns `undefined` for falsy/missing values. */\nexport function safeParseOptionalField(\n value: unknown,\n ctx: SafeJsonParseContext\n): unknown {\n if (!value) return undefined;\n if (typeof value === 'string') {\n return safeJsonParse(value, undefined, ctx);\n }\n return value;\n}\n","/**\n * Shared lazy-loader for PostgreSQL dependencies (kysely + pg).\n *\n * Both `PostgresStorageAdapter` and `PostgresSchedulePersistence` need the same\n * set of optional peer dependencies. This module loads them once and caches the\n * result so the logic isn't duplicated across files.\n */\n\n/**\n * Dynamic imports prevent static type resolution for these modules;\n * `any` is intentional here — callers cast to the appropriate Kysely/pg types.\n */\nexport interface PostgresDeps {\n Kysely: any;\n PostgresDialect: any;\n sql: any;\n pgModule: any;\n}\n\nlet cached: PostgresDeps | undefined;\nlet loadingPromise: Promise<PostgresDeps> | undefined;\n\n/**\n * Lazily load and cache the PostgreSQL peer dependencies (kysely + pg).\n * @returns The cached `{Kysely, PostgresDialect, sql, pgModule}` object.\n * @throws {Error} If the `kysely` optional peer dependency is not installed.\n * @throws {Error} If the `pg` optional peer dependency is not installed.\n */\nexport async function loadPostgresDeps(): Promise<PostgresDeps> {\n if (cached) return cached;\n if (loadingPromise) return loadingPromise;\n\n loadingPromise = (async () => {\n let Kysely: any;\n let PostgresDialect: any;\n let sql: any;\n let pgModule: any;\n\n try {\n const kyselyMod = await import('kysely');\n Kysely = kyselyMod.Kysely;\n PostgresDialect = kyselyMod.PostgresDialect;\n sql = kyselyMod.sql;\n } catch {\n throw new Error(\n 'PostgreSQL support requires the \"kysely\" package. Install it with: npm install kysely'\n );\n }\n try {\n const pg = await import('pg');\n pgModule = pg.default ?? pg;\n } catch {\n throw new Error(\n 'PostgreSQL support requires the \"pg\" package. Install it with: npm install pg'\n );\n }\n\n cached = { Kysely, PostgresDialect, sql, pgModule };\n return cached;\n })();\n\n return loadingPromise;\n}\n\n/**\n * Synchronously access the cached PostgreSQL deps. Throws if\n * `loadPostgresDeps()` has not yet completed. Used by code paths that run\n * after `PostgresStorageAdapter.initialize()` and need direct access to\n * `sql` / `Kysely` without re-awaiting.\n */\nexport function requirePostgresDeps(): PostgresDeps {\n if (!cached) {\n throw new Error(\n 'PostgreSQL dependencies have not been loaded yet. ' +\n 'Call loadPostgresDeps() (or PostgresStorageAdapter.initialize()) first.'\n );\n }\n return cached;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -218,18 +218,96 @@ var SocketIOEventTransport = class {
|
|
|
218
218
|
|
|
219
219
|
// src/events/webhook.ts
|
|
220
220
|
import { promises as dns } from "dns";
|
|
221
|
-
|
|
221
|
+
import { isIPv4, isIPv6 } from "net";
|
|
222
|
+
|
|
223
|
+
// src/utils/pinned-fetch.ts
|
|
224
|
+
import * as http from "http";
|
|
225
|
+
import * as https from "https";
|
|
226
|
+
function pinnedFetch(url, init, address, family = 4) {
|
|
227
|
+
const parsed = new URL(url);
|
|
228
|
+
const isHttps = parsed.protocol === "https:";
|
|
229
|
+
const lib = isHttps ? https : http;
|
|
230
|
+
const port = parsed.port ? Number(parsed.port) : isHttps ? 443 : 80;
|
|
231
|
+
const lookup = (_hostname, options, cb) => {
|
|
232
|
+
if (options && options.all === true) {
|
|
233
|
+
cb(null, [{ address, family }]);
|
|
234
|
+
} else {
|
|
235
|
+
cb(null, address, family);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
const req = lib.request(
|
|
240
|
+
{
|
|
241
|
+
hostname: parsed.hostname,
|
|
242
|
+
port,
|
|
243
|
+
path: `${parsed.pathname || "/"}${parsed.search}`,
|
|
244
|
+
method: init.method ?? "GET",
|
|
245
|
+
headers: init.headers,
|
|
246
|
+
lookup,
|
|
247
|
+
servername: parsed.hostname
|
|
248
|
+
},
|
|
249
|
+
(res) => {
|
|
250
|
+
res.resume();
|
|
251
|
+
res.on("end", () => {
|
|
252
|
+
const status = res.statusCode ?? 0;
|
|
253
|
+
resolve({
|
|
254
|
+
ok: status >= 200 && status < 300,
|
|
255
|
+
status,
|
|
256
|
+
statusText: res.statusMessage ?? ""
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
res.on("error", reject);
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
req.on("error", reject);
|
|
263
|
+
if (init.signal) {
|
|
264
|
+
if (init.signal.aborted) {
|
|
265
|
+
req.destroy(new Error("Request aborted"));
|
|
266
|
+
} else {
|
|
267
|
+
init.signal.addEventListener(
|
|
268
|
+
"abort",
|
|
269
|
+
() => req.destroy(new Error("Request aborted")),
|
|
270
|
+
{ once: true }
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (init.body !== void 0) {
|
|
275
|
+
req.write(init.body);
|
|
276
|
+
}
|
|
277
|
+
req.end();
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/events/webhook.ts
|
|
282
|
+
var WebhookEventTransport = class _WebhookEventTransport {
|
|
222
283
|
endpoints = /* @__PURE__ */ new Map();
|
|
223
284
|
defaultTimeout;
|
|
224
285
|
defaultRetries;
|
|
225
286
|
retryDelay;
|
|
226
287
|
fetchFn;
|
|
288
|
+
/**
|
|
289
|
+
* Whether `fetchFn` was caller-supplied. When `false` the default fetch is
|
|
290
|
+
* replaced with `pinnedFetch` so the TCP connection can target the IP that
|
|
291
|
+
* was just validated (closes the DNS-rebinding TOCTOU window). When `true`
|
|
292
|
+
* the custom fetch is used as-is so test injection still works.
|
|
293
|
+
*/
|
|
294
|
+
hasCustomFetch;
|
|
227
295
|
allowInsecureUrls;
|
|
228
296
|
maxPayloadBytes;
|
|
229
297
|
maxConcurrentRequests;
|
|
298
|
+
maxQueueDepth;
|
|
299
|
+
queueDropRetryAfterMs;
|
|
230
300
|
activeRequests = 0;
|
|
231
301
|
requestQueue = [];
|
|
302
|
+
droppedRequests = 0;
|
|
232
303
|
logger;
|
|
304
|
+
// DNS resolution cache with TTL — stores `family` so the pin can reuse it
|
|
305
|
+
// without a second lookup.
|
|
306
|
+
dnsCache = /* @__PURE__ */ new Map();
|
|
307
|
+
static DNS_CACHE_TTL_MS = 3e4;
|
|
308
|
+
// 30 seconds
|
|
309
|
+
static DNS_TIMEOUT_MS = 5e3;
|
|
310
|
+
// 5 seconds
|
|
233
311
|
// Local subscribers for server-side subscriptions
|
|
234
312
|
runSubscribers = /* @__PURE__ */ new Map();
|
|
235
313
|
globalSubscribers = /* @__PURE__ */ new Set();
|
|
@@ -237,6 +315,7 @@ var WebhookEventTransport = class {
|
|
|
237
315
|
this.defaultTimeout = config.defaultTimeout ?? 5e3;
|
|
238
316
|
this.defaultRetries = config.defaultRetries ?? 3;
|
|
239
317
|
this.retryDelay = config.retryDelay ?? 1e3;
|
|
318
|
+
this.hasCustomFetch = config.fetchFn !== void 0;
|
|
240
319
|
this.fetchFn = config.fetchFn ?? globalThis.fetch;
|
|
241
320
|
this.allowInsecureUrls = config.allowInsecureUrls ?? false;
|
|
242
321
|
this.maxPayloadBytes = config.maxPayloadBytes ?? 1048576;
|
|
@@ -244,6 +323,11 @@ var WebhookEventTransport = class {
|
|
|
244
323
|
if (this.maxConcurrentRequests <= 0) {
|
|
245
324
|
throw new Error("maxConcurrentRequests must be greater than 0");
|
|
246
325
|
}
|
|
326
|
+
this.maxQueueDepth = resolveMaxQueueDepth(config.maxQueueDepth);
|
|
327
|
+
if (this.maxQueueDepth < 0) {
|
|
328
|
+
throw new Error("maxQueueDepth must be >= 0 (0 disables the cap)");
|
|
329
|
+
}
|
|
330
|
+
this.queueDropRetryAfterMs = config.queueDropRetryAfterMs ?? 1e3;
|
|
247
331
|
this.logger = config.logger ?? {
|
|
248
332
|
debug() {
|
|
249
333
|
},
|
|
@@ -318,13 +402,33 @@ var WebhookEventTransport = class {
|
|
|
318
402
|
for (const endpoint of this.endpoints.values()) {
|
|
319
403
|
if (!endpoint.enabled) continue;
|
|
320
404
|
if (!this.matchesFilter(event, endpoint)) continue;
|
|
321
|
-
this.enqueueRequest(
|
|
405
|
+
const accepted = this.enqueueRequest(
|
|
322
406
|
() => this.sendWebhook(endpoint, event).catch((error) => {
|
|
323
407
|
this.logger.error(`Webhook ${endpoint.id} failed:`, error);
|
|
324
|
-
})
|
|
408
|
+
}),
|
|
409
|
+
endpoint.id
|
|
325
410
|
);
|
|
411
|
+
if (!accepted) {
|
|
412
|
+
this.droppedRequests++;
|
|
413
|
+
this.logger.warn(
|
|
414
|
+
`Webhook ${endpoint.id} delivery dropped: queue full (depth ${this.requestQueue.length}/${this.maxQueueDepth}, active ${this.activeRequests}/${this.maxConcurrentRequests}, total drops ${this.droppedRequests}, retryAfterMs ${this.queueDropRetryAfterMs})`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
326
417
|
}
|
|
327
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Number of webhook deliveries dropped due to queue overflow since startup.
|
|
421
|
+
* Useful for surfacing back-pressure to operators (metrics/health checks).
|
|
422
|
+
*/
|
|
423
|
+
getDroppedRequestCount() {
|
|
424
|
+
return this.droppedRequests;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Current depth of the pending request queue.
|
|
428
|
+
*/
|
|
429
|
+
getQueueDepth() {
|
|
430
|
+
return this.requestQueue.length;
|
|
431
|
+
}
|
|
328
432
|
/**
|
|
329
433
|
* Subscribe to events for a specific run (server-side).
|
|
330
434
|
*/
|
|
@@ -403,18 +507,23 @@ var WebhookEventTransport = class {
|
|
|
403
507
|
const signature = await this.signPayload(body, endpoint.secret);
|
|
404
508
|
headers["X-Webhook-Signature"] = signature;
|
|
405
509
|
}
|
|
406
|
-
await this.validateResolvedHost(endpoint.url);
|
|
407
510
|
let lastError;
|
|
408
511
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
409
512
|
try {
|
|
513
|
+
const pinned = await this.validateResolvedHost(endpoint.url);
|
|
410
514
|
const controller = new AbortController();
|
|
411
515
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
412
|
-
const response = await this.fetchFn(endpoint.url, {
|
|
516
|
+
const response = this.hasCustomFetch || !pinned ? await this.fetchFn(endpoint.url, {
|
|
413
517
|
method: "POST",
|
|
414
518
|
headers,
|
|
415
519
|
body,
|
|
416
520
|
signal: controller.signal
|
|
417
|
-
})
|
|
521
|
+
}) : await pinnedFetch(
|
|
522
|
+
endpoint.url,
|
|
523
|
+
{ method: "POST", headers, body, signal: controller.signal },
|
|
524
|
+
pinned.address,
|
|
525
|
+
pinned.family
|
|
526
|
+
);
|
|
418
527
|
clearTimeout(timeoutId);
|
|
419
528
|
if (response.ok) {
|
|
420
529
|
return;
|
|
@@ -455,26 +564,65 @@ var WebhookEventTransport = class {
|
|
|
455
564
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
456
565
|
}
|
|
457
566
|
/**
|
|
458
|
-
* Resolve
|
|
459
|
-
*
|
|
567
|
+
* Resolve a webhook URL's hostname exactly once, verify the resolved IP is
|
|
568
|
+
* not private/reserved, and return the address + family so the caller can
|
|
569
|
+
* pin the connection to it.
|
|
570
|
+
*
|
|
571
|
+
* Returns `null` for raw-IP URLs (already validated by `validateWebhookUrl`).
|
|
572
|
+
*
|
|
573
|
+
* Pinning the resolved IP closes the DNS rebinding TOCTOU window (audit M1):
|
|
574
|
+
* with the original implementation `fetch()` re-resolved the hostname,
|
|
575
|
+
* giving a hostile DNS server a chance to flip the answer to an internal
|
|
576
|
+
* address between SSRF validation and connect.
|
|
577
|
+
*
|
|
578
|
+
* Uses a short-TTL cache and per-call timeout to avoid blocking the event loop.
|
|
460
579
|
*/
|
|
461
580
|
async validateResolvedHost(url) {
|
|
462
581
|
const parsed = new URL(url);
|
|
463
582
|
const hostname = parsed.hostname.toLowerCase();
|
|
464
|
-
|
|
465
|
-
|
|
583
|
+
const stripped = hostname.startsWith("[") ? hostname.slice(1, -1) : hostname;
|
|
584
|
+
if (isIPv4(stripped) || isIPv6(stripped)) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
const cached = this.dnsCache.get(hostname);
|
|
588
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
589
|
+
if (isBlockedIp(cached.address)) {
|
|
590
|
+
throw new Error(
|
|
591
|
+
`Webhook URL hostname "${hostname}" resolves to blocked IP ${cached.address}`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
return { address: cached.address, family: cached.family };
|
|
466
595
|
}
|
|
467
596
|
try {
|
|
468
|
-
const
|
|
597
|
+
const ac = new AbortController();
|
|
598
|
+
const timeoutId = setTimeout(() => ac.abort(), _WebhookEventTransport.DNS_TIMEOUT_MS);
|
|
599
|
+
let address;
|
|
600
|
+
let family;
|
|
601
|
+
try {
|
|
602
|
+
const result = await dns.lookup(hostname, { signal: ac.signal });
|
|
603
|
+
address = result.address;
|
|
604
|
+
family = result.family === 6 ? 6 : 4;
|
|
605
|
+
} finally {
|
|
606
|
+
clearTimeout(timeoutId);
|
|
607
|
+
}
|
|
608
|
+
this.dnsCache.set(hostname, {
|
|
609
|
+
address,
|
|
610
|
+
family,
|
|
611
|
+
expiresAt: Date.now() + _WebhookEventTransport.DNS_CACHE_TTL_MS
|
|
612
|
+
});
|
|
469
613
|
if (isBlockedIp(address)) {
|
|
470
614
|
throw new Error(
|
|
471
615
|
`Webhook URL hostname "${hostname}" resolves to blocked IP ${address}`
|
|
472
616
|
);
|
|
473
617
|
}
|
|
618
|
+
return { address, family };
|
|
474
619
|
} catch (error) {
|
|
475
620
|
if (error instanceof Error && error.message.includes("resolves to blocked IP")) {
|
|
476
621
|
throw error;
|
|
477
622
|
}
|
|
623
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
624
|
+
throw new Error(`DNS lookup for webhook URL hostname "${hostname}" timed out`);
|
|
625
|
+
}
|
|
478
626
|
throw new Error(`Failed to resolve webhook URL hostname "${hostname}": ${error}`);
|
|
479
627
|
}
|
|
480
628
|
}
|
|
@@ -504,9 +652,13 @@ var WebhookEventTransport = class {
|
|
|
504
652
|
}
|
|
505
653
|
}
|
|
506
654
|
/**
|
|
507
|
-
* Enqueue a webhook request with concurrency limiting.
|
|
655
|
+
* Enqueue a webhook request with concurrency limiting and a queue-depth cap.
|
|
656
|
+
* Returns `true` if the request was accepted (executed or queued), or
|
|
657
|
+
* `false` if the queue is full and the request was rejected. The caller is
|
|
658
|
+
* responsible for logging the drop and incrementing any drop metrics, since
|
|
659
|
+
* it has the endpoint context.
|
|
508
660
|
*/
|
|
509
|
-
enqueueRequest(fn) {
|
|
661
|
+
enqueueRequest(fn, _endpointId) {
|
|
510
662
|
const execute = () => {
|
|
511
663
|
this.activeRequests++;
|
|
512
664
|
fn().finally(() => {
|
|
@@ -517,55 +669,122 @@ var WebhookEventTransport = class {
|
|
|
517
669
|
};
|
|
518
670
|
if (this.activeRequests < this.maxConcurrentRequests) {
|
|
519
671
|
execute();
|
|
520
|
-
|
|
521
|
-
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
if (this.maxQueueDepth > 0 && this.requestQueue.length >= this.maxQueueDepth) {
|
|
675
|
+
return false;
|
|
522
676
|
}
|
|
677
|
+
this.requestQueue.push(execute);
|
|
678
|
+
return true;
|
|
523
679
|
}
|
|
524
680
|
};
|
|
681
|
+
function resolveMaxQueueDepth(configured) {
|
|
682
|
+
if (typeof configured === "number") return configured;
|
|
683
|
+
const env = typeof process !== "undefined" ? process.env?.STEPFLOW_WEBHOOK_MAX_QUEUE_DEPTH : void 0;
|
|
684
|
+
if (env !== void 0 && env !== "") {
|
|
685
|
+
const parsed = Number(env);
|
|
686
|
+
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
|
687
|
+
}
|
|
688
|
+
return 1e3;
|
|
689
|
+
}
|
|
525
690
|
function isBlockedHost(hostname) {
|
|
526
|
-
if (hostname === "localhost"
|
|
527
|
-
|
|
691
|
+
if (hostname === "localhost") return true;
|
|
692
|
+
if (hostname.startsWith("[") && hostname.endsWith("]")) {
|
|
693
|
+
const bare = hostname.slice(1, -1);
|
|
694
|
+
return isIPv6(bare) ? isBlockedIPv6(bare) : true;
|
|
528
695
|
}
|
|
529
|
-
if (hostname
|
|
530
|
-
return
|
|
696
|
+
if (isIPv6(hostname)) {
|
|
697
|
+
return isBlockedIPv6(hostname);
|
|
531
698
|
}
|
|
532
|
-
if (hostname
|
|
533
|
-
return
|
|
534
|
-
}
|
|
535
|
-
const ipv4Match = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
536
|
-
if (ipv4Match) {
|
|
537
|
-
const [, a, b] = ipv4Match.map(Number);
|
|
538
|
-
if (a === 10) return true;
|
|
539
|
-
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
540
|
-
if (a === 192 && b === 168) return true;
|
|
541
|
-
if (a === 127) return true;
|
|
542
|
-
if (a === 169 && b === 254) return true;
|
|
543
|
-
if (a === 0 && b === 0) return true;
|
|
544
|
-
}
|
|
545
|
-
if (hostname.startsWith("fe80:") || hostname.startsWith("[fe80:")) {
|
|
546
|
-
return true;
|
|
699
|
+
if (isIPv4(hostname)) {
|
|
700
|
+
return isBlockedIPv4(hostname);
|
|
547
701
|
}
|
|
548
702
|
return false;
|
|
549
703
|
}
|
|
550
704
|
function isBlockedIp(ip) {
|
|
551
|
-
|
|
552
|
-
if (
|
|
553
|
-
const [, a, b] = ipv4Match.map(Number);
|
|
554
|
-
if (a === 127) return true;
|
|
555
|
-
if (a === 10) return true;
|
|
556
|
-
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
557
|
-
if (a === 192 && b === 168) return true;
|
|
558
|
-
if (a === 169 && b === 254) return true;
|
|
559
|
-
if (a === 0 && b === 0) return true;
|
|
560
|
-
}
|
|
561
|
-
if (ip === "::1") return true;
|
|
562
|
-
if (ip.startsWith("fe80:")) return true;
|
|
705
|
+
if (isIPv4(ip)) return isBlockedIPv4(ip);
|
|
706
|
+
if (isIPv6(ip)) return isBlockedIPv6(ip);
|
|
563
707
|
return false;
|
|
564
708
|
}
|
|
709
|
+
function isBlockedIPv4(ip) {
|
|
710
|
+
const match = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
711
|
+
if (!match) return false;
|
|
712
|
+
const [, a, b] = match.map(Number);
|
|
713
|
+
return isBlockedIPv4Octets(a, b);
|
|
714
|
+
}
|
|
715
|
+
function isBlockedIPv4Octets(a, b) {
|
|
716
|
+
if (a === 0) return true;
|
|
717
|
+
if (a === 10) return true;
|
|
718
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
719
|
+
if (a === 127) return true;
|
|
720
|
+
if (a === 169 && b === 254) return true;
|
|
721
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
722
|
+
if (a === 192 && b === 168) return true;
|
|
723
|
+
if (a >= 224) return true;
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
function isBlockedIPv6(ip) {
|
|
727
|
+
const groups = expandIPv6(ip);
|
|
728
|
+
if (!groups) return false;
|
|
729
|
+
const allButLastZero = groups.slice(0, 7).every((g) => g === 0);
|
|
730
|
+
if (allButLastZero && groups[7] === 1) return true;
|
|
731
|
+
if (groups.every((g) => g === 0)) return true;
|
|
732
|
+
if (groups.slice(0, 5).every((g) => g === 0) && groups[5] === 65535) {
|
|
733
|
+
return isBlockedIPv4Octets(groups[6] >> 8 & 255, groups[6] & 255);
|
|
734
|
+
}
|
|
735
|
+
if (groups[0] === 100 && groups[1] === 65435 && groups.slice(2, 6).every((g) => g === 0)) {
|
|
736
|
+
return isBlockedIPv4Octets(groups[6] >> 8 & 255, groups[6] & 255);
|
|
737
|
+
}
|
|
738
|
+
if (groups[0] === 8194) {
|
|
739
|
+
return isBlockedIPv4Octets(groups[1] >> 8 & 255, groups[1] & 255);
|
|
740
|
+
}
|
|
741
|
+
if (groups[0] === 8193 && groups[1] === 0) return true;
|
|
742
|
+
if (groups[0] === 256 && groups.slice(1, 4).every((g) => g === 0)) return true;
|
|
743
|
+
if ((groups[0] & 65024) === 64512) return true;
|
|
744
|
+
if ((groups[0] & 65472) === 65152) return true;
|
|
745
|
+
if ((groups[0] & 65280) === 65280) return true;
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
function expandIPv6(addr) {
|
|
749
|
+
const noZone = addr.split("%")[0];
|
|
750
|
+
if (!isIPv6(noZone)) return null;
|
|
751
|
+
let normalized = noZone.toLowerCase();
|
|
752
|
+
const v4Tail = normalized.match(/^(.*:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
753
|
+
if (v4Tail) {
|
|
754
|
+
const a = parseInt(v4Tail[2], 10);
|
|
755
|
+
const b = parseInt(v4Tail[3], 10);
|
|
756
|
+
const c = parseInt(v4Tail[4], 10);
|
|
757
|
+
const d = parseInt(v4Tail[5], 10);
|
|
758
|
+
if ([a, b, c, d].some((n) => n > 255)) return null;
|
|
759
|
+
const hi = (a << 8 | b).toString(16);
|
|
760
|
+
const lo = (c << 8 | d).toString(16);
|
|
761
|
+
normalized = `${v4Tail[1]}${hi}:${lo}`;
|
|
762
|
+
}
|
|
763
|
+
const halves = normalized.split("::");
|
|
764
|
+
let parts;
|
|
765
|
+
if (halves.length === 1) {
|
|
766
|
+
parts = halves[0].split(":");
|
|
767
|
+
if (parts.length !== 8) return null;
|
|
768
|
+
} else if (halves.length === 2) {
|
|
769
|
+
const head = halves[0] ? halves[0].split(":") : [];
|
|
770
|
+
const tail = halves[1] ? halves[1].split(":") : [];
|
|
771
|
+
const missing = 8 - head.length - tail.length;
|
|
772
|
+
if (missing < 0) return null;
|
|
773
|
+
parts = [...head, ...Array(missing).fill("0"), ...tail];
|
|
774
|
+
} else {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
const groups = [];
|
|
778
|
+
for (const part of parts) {
|
|
779
|
+
if (!/^[0-9a-f]{1,4}$/.test(part)) return null;
|
|
780
|
+
groups.push(parseInt(part, 16));
|
|
781
|
+
}
|
|
782
|
+
return groups;
|
|
783
|
+
}
|
|
565
784
|
|
|
566
785
|
export {
|
|
567
786
|
MemoryEventTransport,
|
|
568
787
|
SocketIOEventTransport,
|
|
569
788
|
WebhookEventTransport
|
|
570
789
|
};
|
|
571
|
-
//# sourceMappingURL=chunk-
|
|
790
|
+
//# sourceMappingURL=chunk-U3PCVK6O.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/events/memory.ts","../src/events/socketio.ts","../src/events/webhook.ts","../src/utils/pinned-fetch.ts"],"sourcesContent":["/**\n * In-memory event transport using Node.js EventEmitter.\n * Default transport for development and single-process deployments.\n *\n * NOTE: This adapter is intended for development and testing only. It stores\n * all state in-process with no persistence or multi-process safety guarantees.\n */\n\nimport { EventEmitter } from 'events';\nimport type {\n EventTransport,\n WorkflowEvent,\n WorkflowEventType,\n EventCallback,\n Unsubscribe,\n} from './types';\n\n/**\n * In-memory event transport implementation using EventEmitter.\n * Events are emitted to subscribers in the same process.\n */\nexport class MemoryEventTransport implements EventTransport {\n private emitter = new EventEmitter();\n\n constructor() {\n // Increase max listeners to avoid warnings in workflows with many steps\n this.emitter.setMaxListeners(100);\n }\n\n /**\n * Emit an event to all subscribers.\n */\n emit(event: WorkflowEvent): void {\n // Emit to global subscribers\n this.emitter.emit('event', event);\n\n // Emit to run-specific subscribers\n this.emitter.emit(`run:${event.runId}`, event);\n\n // Emit to event-type subscribers\n this.emitter.emit(`type:${event.eventType}`, event);\n }\n\n /**\n * Subscribe to events for a specific run.\n */\n subscribe(runId: string, callback: EventCallback): Unsubscribe {\n const channel = `run:${runId}`;\n this.emitter.on(channel, callback);\n return () => {\n this.emitter.off(channel, callback);\n };\n }\n\n /**\n * Subscribe to all events.\n */\n subscribeAll(callback: EventCallback): Unsubscribe {\n this.emitter.on('event', callback);\n return () => {\n this.emitter.off('event', callback);\n };\n }\n\n /**\n * Subscribe to events of a specific type.\n */\n subscribeToType(eventType: WorkflowEventType, callback: EventCallback): Unsubscribe {\n const channel = `type:${eventType}`;\n this.emitter.on(channel, callback);\n return () => {\n this.emitter.off(channel, callback);\n };\n }\n\n /**\n * Close the transport and remove all listeners.\n */\n close(): void {\n this.emitter.removeAllListeners();\n }\n\n /**\n * Get the number of listeners for a channel (for testing).\n */\n getListenerCount(channel?: string): number {\n if (channel) {\n return this.emitter.listenerCount(channel);\n }\n return this.emitter.listenerCount('event');\n }\n}\n","/**\n * Socket.IO Event Transport\n *\n * Broadcasts workflow events to connected clients via Socket.IO.\n * Clients can subscribe to specific run IDs or receive all events.\n */\n\nimport type { EventTransport, EventCallback, Unsubscribe, WorkflowEvent } from './types';\nimport type { Logger } from '../core/types';\n\n/**\n * Minimal Socket.IO Server interface.\n * This allows using the adapter without a direct dependency on socket.io.\n */\nexport interface SocketIOServer {\n to(room: string): {\n emit(event: string, ...args: unknown[]): void;\n };\n emit(event: string, ...args: unknown[]): void;\n}\n\n/**\n * Minimal Socket.IO Socket interface.\n */\nexport interface SocketIOSocket {\n join(room: string): void;\n leave(room: string): void;\n on(event: string, callback: (...args: unknown[]) => void): void;\n}\n\n/**\n * Authorization callback for Socket.IO run subscriptions.\n * Return true to allow the subscription, false to deny it.\n */\nexport type SocketIOAuthorizeFn = (\n runId: string,\n socket: SocketIOSocket\n) => boolean | Promise<boolean>;\n\n/**\n * Configuration for SocketIOEventTransport.\n */\nexport interface SocketIOEventTransportConfig {\n /** Socket.IO server instance */\n io: SocketIOServer;\n\n /** Event name to use for workflow events (default: 'workflow:event') */\n eventName?: string;\n\n /** Room prefix for run-specific subscriptions (default: 'run:') */\n roomPrefix?: string;\n\n /** Whether to also broadcast to a global room (default: true) */\n broadcastGlobal?: boolean;\n\n /** Global room name (default: 'workflow:all') */\n globalRoom?: string;\n\n /** Logger for transport errors (default: console-based) */\n logger?: Logger;\n}\n\n/**\n * Socket.IO-based event transport for real-time workflow events.\n *\n * @example\n * ```typescript\n * import { Server } from 'socket.io';\n * import { SocketIOEventTransport } from 'stepflow';\n *\n * const io = new Server(httpServer);\n * const eventTransport = new SocketIOEventTransport({ io });\n *\n * const engine = new WorkflowEngine({\n * storage,\n * events: eventTransport,\n * });\n *\n * // Client-side:\n * // socket.emit('workflow:subscribe', runId);\n * // socket.on('workflow:event', (event) => console.log(event));\n * ```\n */\nexport class SocketIOEventTransport implements EventTransport {\n private io: SocketIOServer;\n private eventName: string;\n private roomPrefix: string;\n private broadcastGlobal: boolean;\n private globalRoom: string;\n private logger: Logger;\n\n // Local subscribers for server-side subscriptions\n private runSubscribers = new Map<string, Set<EventCallback>>();\n private globalSubscribers = new Set<EventCallback>();\n\n constructor(config: SocketIOEventTransportConfig) {\n this.io = config.io;\n this.eventName = config.eventName ?? 'workflow:event';\n this.roomPrefix = config.roomPrefix ?? 'run:';\n this.broadcastGlobal = config.broadcastGlobal ?? true;\n this.globalRoom = config.globalRoom ?? 'workflow:all';\n this.logger = config.logger ?? {\n debug() {},\n info() {},\n warn() {},\n error(message: string, ...args: unknown[]) { console.error(message, ...args); },\n };\n }\n\n /**\n * Emit an event to Socket.IO clients and local subscribers.\n */\n emit(event: WorkflowEvent): void {\n // Serialize the event (convert Date to ISO string for transport)\n const serializedEvent = {\n ...event,\n timestamp: event.timestamp.toISOString(),\n };\n\n // Emit to run-specific room\n const room = `${this.roomPrefix}${event.runId}`;\n this.io.to(room).emit(this.eventName, serializedEvent);\n\n // Emit to global room\n if (this.broadcastGlobal) {\n this.io.to(this.globalRoom).emit(this.eventName, serializedEvent);\n }\n\n // Notify local subscribers\n const runSubs = this.runSubscribers.get(event.runId);\n if (runSubs) {\n for (const callback of runSubs) {\n try {\n callback(event);\n } catch (error) {\n this.logger.error('Event callback error:', error);\n }\n }\n }\n\n for (const callback of this.globalSubscribers) {\n try {\n callback(event);\n } catch (error) {\n this.logger.error('Event callback error:', error);\n }\n }\n }\n\n /**\n * Subscribe to events for a specific run (server-side).\n */\n subscribe(runId: string, callback: EventCallback): Unsubscribe {\n let subs = this.runSubscribers.get(runId);\n if (!subs) {\n subs = new Set();\n this.runSubscribers.set(runId, subs);\n }\n subs.add(callback);\n\n return () => {\n subs!.delete(callback);\n if (subs!.size === 0) {\n this.runSubscribers.delete(runId);\n }\n };\n }\n\n /**\n * Subscribe to all events (server-side).\n */\n subscribeAll(callback: EventCallback): Unsubscribe {\n this.globalSubscribers.add(callback);\n return () => {\n this.globalSubscribers.delete(callback);\n };\n }\n\n /**\n * Set up client subscription handlers on a socket.\n * Call this when a client connects to enable subscription commands.\n *\n * An `authorize` callback is required to prevent unauthorized access to run events.\n * The callback receives the run ID (or `'*'` for global subscriptions) and the socket,\n * and must return `true` to allow or `false` to deny the subscription.\n *\n * @param socket - The Socket.IO socket to set up handlers on\n * @param authorize - Callback to check if a socket can access a run.\n *\n * @example\n * ```typescript\n * io.on('connection', (socket) => {\n * eventTransport.setupClientHandlers(socket, async (runId, sock) => {\n * // Check if the authenticated user owns this run\n * const userId = sock.data?.userId;\n * return userId ? await canUserAccessRun(userId, runId) : false;\n * });\n * });\n * ```\n */\n setupClientHandlers(\n socket: SocketIOSocket,\n authorize: SocketIOAuthorizeFn\n ): void {\n // Subscribe to a specific run\n socket.on('workflow:subscribe', async (...args: unknown[]) => {\n const runId = args[0];\n if (typeof runId !== 'string') return;\n\n try {\n const allowed = await authorize(runId, socket);\n if (!allowed) {\n this.logger.warn(`Subscription denied for run ${runId}`);\n return;\n }\n } catch (error) {\n this.logger.error('Authorization check failed:', error);\n return;\n }\n\n socket.join(`${this.roomPrefix}${runId}`);\n });\n\n // Unsubscribe from a specific run\n socket.on('workflow:unsubscribe', (...args: unknown[]) => {\n const runId = args[0];\n if (typeof runId === 'string') {\n socket.leave(`${this.roomPrefix}${runId}`);\n }\n });\n\n // Subscribe to all events (requires authorization to pass with '*' as runId)\n socket.on('workflow:subscribe:all', async () => {\n try {\n const allowed = await authorize('*', socket);\n if (!allowed) {\n this.logger.warn('Global subscription denied');\n return;\n }\n } catch (error) {\n this.logger.error('Authorization check failed:', error);\n return;\n }\n\n socket.join(this.globalRoom);\n });\n\n // Unsubscribe from all events\n socket.on('workflow:unsubscribe:all', () => {\n socket.leave(this.globalRoom);\n });\n }\n\n /**\n * Clear all in-process server-side subscribers.\n * The underlying Socket.IO server socket is not closed here — manage its lifecycle externally.\n */\n close(): void {\n this.runSubscribers.clear();\n this.globalSubscribers.clear();\n }\n}\n","/**\n * Webhook Event Transport\n *\n * Posts workflow events to HTTP endpoints via webhooks.\n * Supports multiple webhooks with optional filtering by event type.\n */\n\nimport { promises as dns } from 'node:dns';\nimport { isIPv4, isIPv6 } from 'node:net';\nimport { pinnedFetch } from '../utils/pinned-fetch';\nimport type { EventTransport, EventCallback, Unsubscribe, WorkflowEvent, WorkflowEventType } from './types';\nimport type { Logger } from '../core/types';\n\n/**\n * Webhook endpoint configuration.\n */\nexport interface WebhookEndpoint {\n /** Unique identifier for this endpoint */\n id: string;\n\n /** URL to POST events to */\n url: string;\n\n /** Optional secret for signing payloads (HMAC-SHA256) */\n secret?: string;\n\n /** Optional filter for specific event types (empty = all events) */\n eventTypes?: WorkflowEventType[];\n\n /** Optional filter for specific workflow kinds (empty = all kinds) */\n workflowKinds?: string[];\n\n /** Optional custom headers to include */\n headers?: Record<string, string>;\n\n /** Whether this endpoint is enabled (default: true) */\n enabled?: boolean;\n\n /** Timeout in milliseconds (default: 5000) */\n timeout?: number;\n\n /** Number of retry attempts on failure (default: 3) */\n retries?: number;\n}\n\n/**\n * Configuration for WebhookEventTransport.\n */\nexport interface WebhookEventTransportConfig {\n /** Initial webhook endpoints */\n endpoints?: WebhookEndpoint[];\n\n /** Default timeout for all webhooks in ms (default: 5000) */\n defaultTimeout?: number;\n\n /** Default retry attempts (default: 3) */\n defaultRetries?: number;\n\n /** Retry delay in ms (default: 1000) */\n retryDelay?: number;\n\n /** Custom fetch function (for testing or custom HTTP clients) */\n fetchFn?: typeof fetch;\n\n /**\n * Whether to allow non-HTTPS URLs (default: false).\n * Set to true only in development environments.\n */\n allowInsecureUrls?: boolean;\n\n /**\n * Maximum payload size in bytes (default: 1048576 = 1 MB).\n * Payloads exceeding this limit will be rejected.\n */\n maxPayloadBytes?: number;\n\n /**\n * Maximum concurrent webhook requests across all endpoints (default: 50).\n * Requests beyond this limit are queued.\n */\n maxConcurrentRequests?: number;\n\n /**\n * Maximum number of queued webhook deliveries waiting for a concurrency slot\n * (default: 1000, env override: `STEPFLOW_WEBHOOK_MAX_QUEUE_DEPTH`).\n *\n * When the queue is full, incoming `emit()` deliveries are dropped to\n * protect the process from memory exhaustion under burst traffic. Drops\n * are logged via `logger.warn` with the endpoint id, current queue depth,\n * and a recommended `retryAfterMs`. A value of `0` disables the cap (not\n * recommended in production).\n */\n maxQueueDepth?: number;\n\n /**\n * Suggested back-off in milliseconds reported on dropped deliveries\n * (default: 1000). Surfaced in the warning log so external orchestrators\n * (sidecars, sweeper jobs) can respect it like an HTTP `Retry-After`.\n */\n queueDropRetryAfterMs?: number;\n\n /** Logger for webhook transport errors (default: console-based) */\n logger?: import('../core/types').Logger;\n}\n\n/**\n * Webhook payload structure sent to external endpoints.\n */\nexport interface WebhookPayload {\n /**\n * The workflow event with `timestamp` serialized as an ISO 8601 string.\n * The `Date` is converted to a string before delivery so that JSON consumers\n * receive a portable, timezone-aware representation without needing Date parsing.\n */\n event: Omit<WorkflowEvent, 'timestamp'> & { timestamp: string };\n /** ISO 8601 timestamp of when this payload was delivered. */\n deliveredAt: string;\n /** ID of the webhook endpoint that received this delivery. */\n webhookId: string;\n}\n\n/**\n * Webhook-based event transport for integrating with external systems.\n *\n * @example\n * ```typescript\n * const webhookTransport = new WebhookEventTransport({\n * endpoints: [\n * {\n * id: 'slack-notifications',\n * url: 'https://hooks.slack.com/...',\n * eventTypes: ['run.completed', 'run.failed'],\n * },\n * {\n * id: 'analytics',\n * url: 'https://api.analytics.com/events',\n * secret: process.env.WEBHOOK_SECRET, // Use a cryptographically random secret (≥ 32 bytes)\n * },\n * ],\n * });\n *\n * const engine = new WorkflowEngine({\n * storage,\n * events: webhookTransport,\n * });\n * ```\n */\nexport class WebhookEventTransport implements EventTransport {\n private endpoints = new Map<string, WebhookEndpoint>();\n private defaultTimeout: number;\n private defaultRetries: number;\n private retryDelay: number;\n private fetchFn: typeof fetch;\n /**\n * Whether `fetchFn` was caller-supplied. When `false` the default fetch is\n * replaced with `pinnedFetch` so the TCP connection can target the IP that\n * was just validated (closes the DNS-rebinding TOCTOU window). When `true`\n * the custom fetch is used as-is so test injection still works.\n */\n private hasCustomFetch: boolean;\n private allowInsecureUrls: boolean;\n private maxPayloadBytes: number;\n private maxConcurrentRequests: number;\n private maxQueueDepth: number;\n private queueDropRetryAfterMs: number;\n private activeRequests = 0;\n private requestQueue: Array<() => void> = [];\n private droppedRequests = 0;\n private logger: Logger;\n\n // DNS resolution cache with TTL — stores `family` so the pin can reuse it\n // without a second lookup.\n private dnsCache = new Map<string, { address: string; family: 4 | 6; expiresAt: number }>();\n private static readonly DNS_CACHE_TTL_MS = 30_000; // 30 seconds\n private static readonly DNS_TIMEOUT_MS = 5_000; // 5 seconds\n\n // Local subscribers for server-side subscriptions\n private runSubscribers = new Map<string, Set<EventCallback>>();\n private globalSubscribers = new Set<EventCallback>();\n\n constructor(config: WebhookEventTransportConfig = {}) {\n this.defaultTimeout = config.defaultTimeout ?? 5000;\n this.defaultRetries = config.defaultRetries ?? 3;\n this.retryDelay = config.retryDelay ?? 1000;\n this.hasCustomFetch = config.fetchFn !== undefined;\n this.fetchFn = config.fetchFn ?? globalThis.fetch;\n this.allowInsecureUrls = config.allowInsecureUrls ?? false;\n this.maxPayloadBytes = config.maxPayloadBytes ?? 1_048_576; // 1 MB\n this.maxConcurrentRequests = config.maxConcurrentRequests ?? 50;\n if (this.maxConcurrentRequests <= 0) {\n throw new Error('maxConcurrentRequests must be greater than 0');\n }\n this.maxQueueDepth = resolveMaxQueueDepth(config.maxQueueDepth);\n if (this.maxQueueDepth < 0) {\n throw new Error('maxQueueDepth must be >= 0 (0 disables the cap)');\n }\n this.queueDropRetryAfterMs = config.queueDropRetryAfterMs ?? 1000;\n this.logger = config.logger ?? {\n debug() {},\n info() {},\n warn() {},\n error(message: string, ...args: unknown[]) { console.error(message, ...args); },\n };\n\n if (this.allowInsecureUrls) {\n this.logger.warn('WebhookEventTransport: allowInsecureUrls is enabled. Webhook payloads will be sent over HTTP. Do not use this setting in production.');\n }\n\n // Register initial endpoints (validates URLs)\n if (config.endpoints) {\n for (const endpoint of config.endpoints) {\n this.addEndpoint(endpoint);\n }\n }\n }\n\n /**\n * Add a webhook endpoint.\n * Validates the URL to prevent SSRF attacks.\n * @throws Error if the URL scheme is not allowed\n */\n addEndpoint(endpoint: WebhookEndpoint): void {\n this.validateWebhookUrl(endpoint.url);\n this.endpoints.set(endpoint.id, { enabled: true, ...endpoint });\n }\n\n /**\n * Remove a webhook endpoint.\n */\n removeEndpoint(id: string): boolean {\n return this.endpoints.delete(id);\n }\n\n /**\n * Get all registered endpoints.\n */\n getEndpoints(): WebhookEndpoint[] {\n return Array.from(this.endpoints.values());\n }\n\n /**\n * Enable or disable an endpoint.\n */\n setEndpointEnabled(id: string, enabled: boolean): void {\n const endpoint = this.endpoints.get(id);\n if (endpoint) {\n endpoint.enabled = enabled;\n }\n }\n\n /**\n * Emit an event to all matching webhook endpoints.\n */\n emit(event: WorkflowEvent): void {\n // Notify local subscribers first (synchronously)\n const runSubs = this.runSubscribers.get(event.runId);\n if (runSubs) {\n for (const callback of runSubs) {\n try {\n callback(event);\n } catch (error) {\n this.logger.error('Event callback error:', error);\n }\n }\n }\n\n for (const callback of this.globalSubscribers) {\n try {\n callback(event);\n } catch (error) {\n this.logger.error('Event callback error:', error);\n }\n }\n\n // Send to webhooks asynchronously with concurrency control\n for (const endpoint of this.endpoints.values()) {\n if (!endpoint.enabled) continue;\n if (!this.matchesFilter(event, endpoint)) continue;\n\n const accepted = this.enqueueRequest(\n () =>\n this.sendWebhook(endpoint, event).catch((error) => {\n this.logger.error(`Webhook ${endpoint.id} failed:`, error);\n }),\n endpoint.id\n );\n\n if (!accepted) {\n // Queue overflow — equivalent to a server returning 429 with Retry-After.\n // The delivery is dropped to protect the process from memory exhaustion.\n this.droppedRequests++;\n this.logger.warn(\n `Webhook ${endpoint.id} delivery dropped: queue full ` +\n `(depth ${this.requestQueue.length}/${this.maxQueueDepth}, ` +\n `active ${this.activeRequests}/${this.maxConcurrentRequests}, ` +\n `total drops ${this.droppedRequests}, retryAfterMs ${this.queueDropRetryAfterMs})`\n );\n }\n }\n }\n\n /**\n * Number of webhook deliveries dropped due to queue overflow since startup.\n * Useful for surfacing back-pressure to operators (metrics/health checks).\n */\n getDroppedRequestCount(): number {\n return this.droppedRequests;\n }\n\n /**\n * Current depth of the pending request queue.\n */\n getQueueDepth(): number {\n return this.requestQueue.length;\n }\n\n /**\n * Subscribe to events for a specific run (server-side).\n */\n subscribe(runId: string, callback: EventCallback): Unsubscribe {\n let subs = this.runSubscribers.get(runId);\n if (!subs) {\n subs = new Set();\n this.runSubscribers.set(runId, subs);\n }\n subs.add(callback);\n\n return () => {\n subs!.delete(callback);\n if (subs!.size === 0) {\n this.runSubscribers.delete(runId);\n }\n };\n }\n\n /**\n * Subscribe to all events (server-side).\n */\n subscribeAll(callback: EventCallback): Unsubscribe {\n this.globalSubscribers.add(callback);\n return () => {\n this.globalSubscribers.delete(callback);\n };\n }\n\n /**\n * Close the transport.\n */\n close(): void {\n this.runSubscribers.clear();\n this.globalSubscribers.clear();\n this.endpoints.clear();\n }\n\n /**\n * Check if an event matches an endpoint's filters.\n */\n private matchesFilter(event: WorkflowEvent, endpoint: WebhookEndpoint): boolean {\n // Check event type filter\n if (endpoint.eventTypes && endpoint.eventTypes.length > 0) {\n if (!endpoint.eventTypes.includes(event.eventType)) {\n return false;\n }\n }\n\n // Check workflow kind filter\n if (endpoint.workflowKinds && endpoint.workflowKinds.length > 0) {\n if (!endpoint.workflowKinds.includes(event.kind)) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Send a webhook with retry support.\n */\n private async sendWebhook(endpoint: WebhookEndpoint, event: WorkflowEvent): Promise<void> {\n const timeout = endpoint.timeout ?? this.defaultTimeout;\n const maxRetries = endpoint.retries ?? this.defaultRetries;\n\n const payload: WebhookPayload = {\n event: {\n ...event,\n timestamp: event.timestamp.toISOString(),\n },\n deliveredAt: new Date().toISOString(),\n webhookId: endpoint.id,\n };\n\n const body = JSON.stringify(payload);\n\n // Enforce payload size limit to prevent excessive memory use and network abuse\n if (body.length > this.maxPayloadBytes) {\n throw new Error(\n `Webhook payload exceeds maximum size (${body.length} bytes > ${this.maxPayloadBytes} bytes)`\n );\n }\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...endpoint.headers,\n };\n\n // Sign payload if secret is provided\n if (endpoint.secret) {\n const signature = await this.signPayload(body, endpoint.secret);\n headers['X-Webhook-Signature'] = signature;\n }\n\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n // Resolve the hostname once and validate the resulting IP is not\n // private/reserved (SSRF protection). When the default fetch is used,\n // the resolved IP is then pinned for the connection so a hostile DNS\n // server cannot rebind the hostname to a different address between\n // validation and connect (M1: rebinding TOCTOU). The lookup runs\n // inside the retry loop so DNS failures are retried along with\n // network errors.\n const pinned = await this.validateResolvedHost(endpoint.url);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n const response = this.hasCustomFetch || !pinned\n ? await this.fetchFn(endpoint.url, {\n method: 'POST',\n headers,\n body,\n signal: controller.signal,\n })\n : await pinnedFetch(\n endpoint.url,\n { method: 'POST', headers, body, signal: controller.signal },\n pinned.address,\n pinned.family,\n );\n\n clearTimeout(timeoutId);\n\n if (response.ok) {\n return; // Success\n }\n\n lastError = new Error(`Webhook returned ${response.status}: ${response.statusText}`);\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n }\n\n // Wait before retrying (except on last attempt)\n if (attempt < maxRetries) {\n await this.sleep(this.retryDelay * Math.pow(2, attempt));\n }\n }\n\n throw lastError;\n }\n\n /**\n * Sign a payload using HMAC-SHA256.\n */\n private async signPayload(payload: string, secret: string): Promise<string> {\n // Use Web Crypto API for HMAC-SHA256\n const encoder = new TextEncoder();\n const keyData = encoder.encode(secret);\n const data = encoder.encode(payload);\n\n const key = await crypto.subtle.importKey(\n 'raw',\n keyData,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n\n const signature = await crypto.subtle.sign('HMAC', key, data);\n const hashArray = Array.from(new Uint8Array(signature));\n const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\n\n return `sha256=${hashHex}`;\n }\n\n /**\n * Sleep helper.\n */\n private sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n }\n\n /**\n * Resolve a webhook URL's hostname exactly once, verify the resolved IP is\n * not private/reserved, and return the address + family so the caller can\n * pin the connection to it.\n *\n * Returns `null` for raw-IP URLs (already validated by `validateWebhookUrl`).\n *\n * Pinning the resolved IP closes the DNS rebinding TOCTOU window (audit M1):\n * with the original implementation `fetch()` re-resolved the hostname,\n * giving a hostile DNS server a chance to flip the answer to an internal\n * address between SSRF validation and connect.\n *\n * Uses a short-TTL cache and per-call timeout to avoid blocking the event loop.\n */\n private async validateResolvedHost(\n url: string,\n ): Promise<{ address: string; family: 4 | 6 } | null> {\n const parsed = new URL(url);\n const hostname = parsed.hostname.toLowerCase();\n\n // Skip DNS resolution for raw IP literals — already checked by isBlockedHost in validateWebhookUrl.\n // Node's URL parser wraps IPv6 literals in brackets (e.g., \"[2001:db8::1]\").\n const stripped = hostname.startsWith('[') ? hostname.slice(1, -1) : hostname;\n if (isIPv4(stripped) || isIPv6(stripped)) {\n return null;\n }\n\n // Check cache first\n const cached = this.dnsCache.get(hostname);\n if (cached && cached.expiresAt > Date.now()) {\n if (isBlockedIp(cached.address)) {\n throw new Error(\n `Webhook URL hostname \"${hostname}\" resolves to blocked IP ${cached.address}`\n );\n }\n return { address: cached.address, family: cached.family };\n }\n\n try {\n const ac = new AbortController();\n const timeoutId = setTimeout(() => ac.abort(), WebhookEventTransport.DNS_TIMEOUT_MS);\n\n let address: string;\n let family: 4 | 6;\n try {\n const result = await dns.lookup(hostname, { signal: ac.signal } as any);\n address = result.address;\n family = (result.family === 6 ? 6 : 4) as 4 | 6;\n } finally {\n clearTimeout(timeoutId);\n }\n\n // Cache the result\n this.dnsCache.set(hostname, {\n address,\n family,\n expiresAt: Date.now() + WebhookEventTransport.DNS_CACHE_TTL_MS,\n });\n\n if (isBlockedIp(address)) {\n throw new Error(\n `Webhook URL hostname \"${hostname}\" resolves to blocked IP ${address}`\n );\n }\n\n return { address, family };\n } catch (error) {\n if (error instanceof Error && error.message.includes('resolves to blocked IP')) {\n throw error;\n }\n if (error instanceof Error && error.name === 'AbortError') {\n throw new Error(`DNS lookup for webhook URL hostname \"${hostname}\" timed out`);\n }\n throw new Error(`Failed to resolve webhook URL hostname \"${hostname}\": ${error}`);\n }\n }\n\n /**\n * Validate a webhook URL to prevent SSRF attacks.\n * Blocks non-HTTPS schemes (unless allowInsecureUrls is set),\n * and rejects private/reserved IP ranges and cloud metadata endpoints.\n */\n private validateWebhookUrl(url: string): void {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(`Invalid webhook URL: ${url}`);\n }\n\n // Scheme check\n if (!this.allowInsecureUrls && parsed.protocol !== 'https:') {\n throw new Error(\n `Webhook URL must use HTTPS (got ${parsed.protocol}). Set allowInsecureUrls for development.`\n );\n }\n\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n throw new Error(`Webhook URL must use HTTP(S) scheme (got ${parsed.protocol})`);\n }\n\n // Block dangerous hostnames\n const hostname = parsed.hostname.toLowerCase();\n if (isBlockedHost(hostname)) {\n throw new Error(`Webhook URL hostname is blocked (private/reserved): ${hostname}`);\n }\n }\n\n /**\n * Enqueue a webhook request with concurrency limiting and a queue-depth cap.\n * Returns `true` if the request was accepted (executed or queued), or\n * `false` if the queue is full and the request was rejected. The caller is\n * responsible for logging the drop and incrementing any drop metrics, since\n * it has the endpoint context.\n */\n private enqueueRequest(fn: () => Promise<void>, _endpointId: string): boolean {\n const execute = () => {\n this.activeRequests++;\n fn().finally(() => {\n this.activeRequests--;\n const next = this.requestQueue.shift();\n if (next) next();\n });\n };\n\n if (this.activeRequests < this.maxConcurrentRequests) {\n execute();\n return true;\n }\n\n if (this.maxQueueDepth > 0 && this.requestQueue.length >= this.maxQueueDepth) {\n return false;\n }\n\n this.requestQueue.push(execute);\n return true;\n }\n}\n\n/**\n * Resolve the effective `maxQueueDepth` from explicit config and the\n * `STEPFLOW_WEBHOOK_MAX_QUEUE_DEPTH` environment variable.\n *\n * Precedence: explicit config value > env var > default (1000).\n */\nfunction resolveMaxQueueDepth(configured: number | undefined): number {\n if (typeof configured === 'number') return configured;\n const env = typeof process !== 'undefined' ? process.env?.STEPFLOW_WEBHOOK_MAX_QUEUE_DEPTH : undefined;\n if (env !== undefined && env !== '') {\n const parsed = Number(env);\n if (Number.isFinite(parsed) && parsed >= 0) return parsed;\n }\n return 1000;\n}\n\n/**\n * Check if a hostname (as it appears in a URL) is blocked.\n * Handles bare hostnames, IPv4 literals, and bracketed IPv6 literals.\n */\nfunction isBlockedHost(hostname: string): boolean {\n if (hostname === 'localhost') return true;\n\n // IPv6 literals are wrapped in brackets by Node's URL parser, e.g. \"[::1]\".\n if (hostname.startsWith('[') && hostname.endsWith(']')) {\n const bare = hostname.slice(1, -1);\n return isIPv6(bare) ? isBlockedIPv6(bare) : true;\n }\n\n // Some callers (or legacy code paths) may pass an unwrapped IPv6 literal.\n if (isIPv6(hostname)) {\n return isBlockedIPv6(hostname);\n }\n\n if (isIPv4(hostname)) {\n return isBlockedIPv4(hostname);\n }\n\n return false;\n}\n\n/**\n * Check if a resolved IP address falls within a private/reserved range.\n * Used for runtime DNS resolution validation to prevent SSRF via DNS rebinding.\n */\nfunction isBlockedIp(ip: string): boolean {\n if (isIPv4(ip)) return isBlockedIPv4(ip);\n if (isIPv6(ip)) return isBlockedIPv6(ip);\n return false;\n}\n\n/**\n * Block IPv4 addresses in private, loopback, link-local, and cloud-metadata ranges.\n */\nfunction isBlockedIPv4(ip: string): boolean {\n const match = ip.match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/);\n if (!match) return false;\n const [, a, b] = match.map(Number);\n return isBlockedIPv4Octets(a, b);\n}\n\nfunction isBlockedIPv4Octets(a: number, b: number): boolean {\n // 0.0.0.0/8 — \"this network\"\n if (a === 0) return true;\n // 10.0.0.0/8 — RFC 1918 private\n if (a === 10) return true;\n // 100.64.0.0/10 — CGNAT\n if (a === 100 && b >= 64 && b <= 127) return true;\n // 127.0.0.0/8 — loopback\n if (a === 127) return true;\n // 169.254.0.0/16 — link-local (covers cloud metadata 169.254.169.254)\n if (a === 169 && b === 254) return true;\n // 172.16.0.0/12 — RFC 1918 private\n if (a === 172 && b >= 16 && b <= 31) return true;\n // 192.168.0.0/16 — RFC 1918 private\n if (a === 192 && b === 168) return true;\n // 224.0.0.0/4 multicast and 240.0.0.0/4 reserved (incl. 255.255.255.255 broadcast)\n if (a >= 224) return true;\n return false;\n}\n\n/**\n * Block IPv6 addresses in reserved, private, loopback, link-local, IPv4-mapped,\n * IPv4-translated, 6to4, and Teredo ranges. For mapped/embedded IPv4 forms,\n * the embedded IPv4 is decoded and rechecked against the IPv4 blocklist.\n */\nfunction isBlockedIPv6(ip: string): boolean {\n const groups = expandIPv6(ip);\n if (!groups) return false;\n\n // ::1 loopback and :: unspecified\n const allButLastZero = groups.slice(0, 7).every(g => g === 0);\n if (allButLastZero && groups[7] === 1) return true;\n if (groups.every(g => g === 0)) return true;\n\n // ::ffff:0:0/96 — IPv4-mapped IPv6 (decode embedded IPv4)\n if (groups.slice(0, 5).every(g => g === 0) && groups[5] === 0xffff) {\n return isBlockedIPv4Octets((groups[6] >> 8) & 0xff, groups[6] & 0xff);\n }\n\n // 64:ff9b::/96 — IPv4/IPv6 translation (RFC 6052) and 64:ff9b:1::/48 (RFC 8215)\n if (groups[0] === 0x64 && groups[1] === 0xff9b && groups.slice(2, 6).every(g => g === 0)) {\n return isBlockedIPv4Octets((groups[6] >> 8) & 0xff, groups[6] & 0xff);\n }\n\n // 2002::/16 — 6to4 (next 32 bits are the embedded IPv4)\n if (groups[0] === 0x2002) {\n return isBlockedIPv4Octets((groups[1] >> 8) & 0xff, groups[1] & 0xff);\n }\n\n // 2001::/32 — Teredo. Block conservatively; embedded server/client v4 fields\n // can mask private destinations and Teredo isn't a normal webhook target.\n if (groups[0] === 0x2001 && groups[1] === 0) return true;\n\n // 100::/64 — RFC 6666 Discard prefix\n if (groups[0] === 0x0100 && groups.slice(1, 4).every(g => g === 0)) return true;\n\n // fc00::/7 — Unique Local Addresses (fc00:: through fdff::)\n if ((groups[0] & 0xfe00) === 0xfc00) return true;\n\n // fe80::/10 — Link-local (fe80:: through febf::)\n if ((groups[0] & 0xffc0) === 0xfe80) return true;\n\n // ff00::/8 — Multicast\n if ((groups[0] & 0xff00) === 0xff00) return true;\n\n return false;\n}\n\n/**\n * Expand an IPv6 string into an array of 8 numeric hextets (0–0xffff).\n * Handles \"::\" abbreviation, embedded IPv4 dotted-quad, and zone IDs.\n * Returns null if the input is not a valid IPv6 address.\n */\nfunction expandIPv6(addr: string): number[] | null {\n // Strip zone identifier (e.g., \"fe80::1%eth0\")\n const noZone = addr.split('%')[0];\n if (!isIPv6(noZone)) return null;\n\n let normalized = noZone.toLowerCase();\n\n // Convert trailing dotted-quad (e.g., \"::ffff:192.168.0.1\") into two hextets.\n const v4Tail = normalized.match(/^(.*:)(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/);\n if (v4Tail) {\n const a = parseInt(v4Tail[2], 10);\n const b = parseInt(v4Tail[3], 10);\n const c = parseInt(v4Tail[4], 10);\n const d = parseInt(v4Tail[5], 10);\n if ([a, b, c, d].some(n => n > 255)) return null;\n const hi = ((a << 8) | b).toString(16);\n const lo = ((c << 8) | d).toString(16);\n normalized = `${v4Tail[1]}${hi}:${lo}`;\n }\n\n const halves = normalized.split('::');\n let parts: string[];\n if (halves.length === 1) {\n parts = halves[0].split(':');\n if (parts.length !== 8) return null;\n } else if (halves.length === 2) {\n const head = halves[0] ? halves[0].split(':') : [];\n const tail = halves[1] ? halves[1].split(':') : [];\n const missing = 8 - head.length - tail.length;\n if (missing < 0) return null;\n parts = [...head, ...Array(missing).fill('0'), ...tail];\n } else {\n return null;\n }\n\n const groups: number[] = [];\n for (const part of parts) {\n if (!/^[0-9a-f]{1,4}$/.test(part)) return null;\n groups.push(parseInt(part, 16));\n }\n return groups;\n}\n","/**\n * DNS-pinned fetch helper.\n *\n * Performs an HTTP(S) request that connects to a caller-supplied IP address\n * instead of re-resolving the URL hostname. Used by the webhook transport to\n * close the SSRF DNS-rebinding TOCTOU window: the hostname is resolved\n * exactly once (by the caller), the resolved IP is validated, and the\n * resolved IP is then pinned for the connection so the request cannot be\n * rerouted by a hostile DNS server.\n *\n * The original hostname is preserved for TLS SNI / certificate verification\n * and for the HTTP `Host` header.\n *\n * Audit: 2026-04-27 finding M1 — DNS TOCTOU between SSRF validation and\n * connect.\n */\n\nimport * as http from 'node:http';\nimport * as https from 'node:https';\nimport type { LookupAddress, LookupOptions } from 'node:dns';\n\n/** Minimal fetch-compatible Response surface used by the webhook transport. */\nexport interface PinnedFetchResponse {\n ok: boolean;\n status: number;\n statusText: string;\n}\n\n/** Subset of `RequestInit` accepted by `pinnedFetch`. */\nexport interface PinnedFetchInit {\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n signal?: AbortSignal;\n}\n\n/**\n * Make an HTTP(S) request that connects to `address` instead of resolving\n * `url`'s hostname. Returns a minimal `Response`-shaped object compatible\n * with the webhook transport's status / ok checks.\n *\n * @throws Error on transport failure or abort\n */\nexport function pinnedFetch(\n url: string,\n init: PinnedFetchInit,\n address: string,\n family: 4 | 6 = 4,\n): Promise<PinnedFetchResponse> {\n const parsed = new URL(url);\n const isHttps = parsed.protocol === 'https:';\n const lib = isHttps ? https : http;\n const port = parsed.port ? Number(parsed.port) : isHttps ? 443 : 80;\n\n // `lookup` is called by the agent's socket-create path. By returning the\n // pre-validated address here we guarantee the TCP connection target is the\n // IP whose safety the caller already verified. Node may invoke this with\n // `{ all: true }` (expects an array result) or single-address mode\n // (expects positional address/family) — handle both for cross-version safety.\n const lookup = (\n _hostname: string,\n options: LookupOptions,\n cb: (err: NodeJS.ErrnoException | null, addr: string | LookupAddress[], fam?: number) => void,\n ): void => {\n if (options && options.all === true) {\n cb(null, [{ address, family }]);\n } else {\n cb(null, address, family);\n }\n };\n\n return new Promise<PinnedFetchResponse>((resolve, reject) => {\n const req = lib.request(\n {\n hostname: parsed.hostname,\n port,\n path: `${parsed.pathname || '/'}${parsed.search}`,\n method: init.method ?? 'GET',\n headers: init.headers,\n lookup,\n servername: parsed.hostname,\n },\n (res) => {\n // Drain the body — we only inspect status, but leaving the socket\n // half-read would leak it.\n res.resume();\n res.on('end', () => {\n const status = res.statusCode ?? 0;\n resolve({\n ok: status >= 200 && status < 300,\n status,\n statusText: res.statusMessage ?? '',\n });\n });\n res.on('error', reject);\n },\n );\n\n req.on('error', reject);\n\n if (init.signal) {\n if (init.signal.aborted) {\n req.destroy(new Error('Request aborted'));\n } else {\n init.signal.addEventListener(\n 'abort',\n () => req.destroy(new Error('Request aborted')),\n { once: true },\n );\n }\n }\n\n if (init.body !== undefined) {\n req.write(init.body);\n }\n req.end();\n });\n}\n"],"mappings":"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","names":[]}
|