@frontmcp/utils 1.11.3 → 1.12.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +4 -2
  2. package/browser/crypto/index.mjs +25 -1
  3. package/browser/crypto/standard-webhooks.mjs +96 -0
  4. package/browser/fs/index.mjs +3 -0
  5. package/browser/fs/watch-files.mjs +177 -0
  6. package/browser/index.mjs +45 -2
  7. package/browser/net/index.mjs +15 -2
  8. package/browser/net/ip-address.mjs +30 -0
  9. package/browser/net/outbound-request.mjs +190 -0
  10. package/browser/net/outbound-url.mjs +43 -0
  11. package/browser/serialization/canonical-json.mjs +35 -0
  12. package/browser/serialization/index.mjs +2 -0
  13. package/browser/stdio-bridge/claude-code-channels.mjs +73 -0
  14. package/browser/stdio-bridge/notification-stream.mjs +111 -0
  15. package/browser/stdio-bridge/sse-frames.mjs +48 -0
  16. package/browser/stdio-bridge/upstream-client.mjs +89 -37
  17. package/crypto/index.d.ts +1 -0
  18. package/crypto/standard-webhooks.d.ts +35 -0
  19. package/env/browser-runtime-context.d.ts +1 -1
  20. package/env/runtime-context.d.ts +1 -1
  21. package/esm/index.mjs +1 -1
  22. package/fs/index.d.ts +2 -0
  23. package/fs/watch-files.d.ts +22 -0
  24. package/index.d.ts +6 -6
  25. package/index.js +1 -1
  26. package/net/index.d.ts +5 -1
  27. package/net/ip-address.d.ts +2 -0
  28. package/net/outbound-request.d.ts +46 -0
  29. package/net/outbound-url.d.ts +18 -0
  30. package/package.json +2 -2
  31. package/serialization/canonical-json.d.ts +2 -0
  32. package/serialization/index.d.ts +1 -0
  33. package/stdio-bridge/claude-code-channels.d.ts +28 -0
  34. package/stdio-bridge/notification-stream.d.ts +29 -0
  35. package/stdio-bridge/sse-frames.d.ts +8 -0
  36. package/stdio-bridge/upstream-client.d.ts +13 -0
package/README.md CHANGED
@@ -42,12 +42,14 @@ await storage.get('ticket:T-1'); // 'open'
42
42
  | Naming | `splitWords`, `toCase`, `shortHash`, `ensureMaxLen`, `idFromString` |
43
43
  | URIs | `isValidMcpUri`, `extractUriScheme`, `parseUriTemplate`, `matchUriTemplate`, `expandUriTemplate`, `extractTemplateParams` (RFC 3986 URIs, RFC 6570 templates) |
44
44
  | Paths | `trimSlashes`, `joinPath`, and cross-platform `pathJoin`, `pathResolve`, `basename`, `dirname`, `extname` |
45
- | Content | `sanitizeToJson`, `inferMimeType`, `safeStringify` |
45
+ | Content | `sanitizeToJson`, `inferMimeType`, `safeStringify`, `canonicalJson` |
46
46
  | HTTP | `validateBaseUrl`, `buildSetCookie`, `parseCookies`, `getCookie`, `isLocalhost`, `isSecureRequest` |
47
- | Network | `classifyIpAddress`, `parseIpv4`, `expandIpv6`, `isRedirectResponse`, for SSRF (server-side request forgery) checks |
47
+ | Network | `classifyIpAddress`, `isGloballyRoutableIp`, `parseIpv4`, `expandIpv6`, `isRedirectResponse`, for SSRF (server-side request forgery) checks |
48
+ | Outbound HTTP | `validateOutboundUrl`, `sendOutboundRequest`, `outboundFailureCategory`, for a request to a URL that a client chose |
48
49
  | Escaping | `escapeHtml`, `escapeHtmlAttr`, `escapeJsString`, `safeJsonForScript` |
49
50
  | Crypto | `sha256`, `sha256Hex`, `hmacSha256`, `hkdfSha256`, `encryptAesGcm`, `decryptAesGcm`, `randomBytes`, `randomUUID`, `timingSafeEqual`, `base64urlEncode`, `base64urlDecode` |
50
51
  | PKCE | `generateCodeVerifier`, `generateCodeChallenge`, `verifyCodeChallenge`, `generatePkcePair` (RFC 7636) |
52
+ | Webhooks | `signWebhookMessage`, `webhookSignatureHeaders`, `verifyWebhookSignature`, `isStandardWebhookSecret`, `generateWebhookMessageId` (Standard Webhooks) |
51
53
  | Encryption, keys | `encryptValue`, `decryptValue`, `signData`, `verifyData`, `KeyPersistence`, `createKeyPersistence` |
52
54
  | Storage | `createStorage`, `createMemoryStorage`, `TypedStorage`, `EncryptedTypedStorage`, and adapters for memory, Redis, Vercel KV, Upstash and the file system |
53
55
  | File system | `readFile`, `writeFile`, `mkdir`, `stat`, `fileExists`, `readJSON`, `writeJSON`, `ensureDir`, `isDirEmpty`, `runCmd` and more. Node.js only; `fs` loads on first use. |
@@ -50,6 +50,19 @@ import {
50
50
  isSignedData,
51
51
  verifyOrParseData
52
52
  } from "./hmac-signing.mjs";
53
+ import {
54
+ WEBHOOK_ID_HEADER,
55
+ WEBHOOK_TIMESTAMP_HEADER,
56
+ WEBHOOK_SIGNATURE_HEADER,
57
+ WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,
58
+ signingKeyOf,
59
+ isStandardWebhookSecret,
60
+ generateStandardWebhookSecret,
61
+ generateWebhookMessageId,
62
+ signWebhookMessage,
63
+ webhookSignatureHeaders,
64
+ verifyWebhookSignature
65
+ } from "./standard-webhooks.mjs";
53
66
  import {
54
67
  asymmetricAlgSchema,
55
68
  secretKeyDataSchema,
@@ -208,6 +221,10 @@ export {
208
221
  MAX_CODE_VERIFIER_LENGTH,
209
222
  MIN_CODE_VERIFIER_LENGTH,
210
223
  PkceError,
224
+ WEBHOOK_ID_HEADER,
225
+ WEBHOOK_SIGNATURE_HEADER,
226
+ WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,
227
+ WEBHOOK_TIMESTAMP_HEADER,
211
228
  anyKeyDataSchema,
212
229
  assertNode,
213
230
  asymmetricAlgSchema,
@@ -234,6 +251,8 @@ export {
234
251
  generatePkcePair,
235
252
  generateRsaKeyPair2 as generateRsaKeyPair,
236
253
  generateSecret,
254
+ generateStandardWebhookSecret,
255
+ generateWebhookMessageId,
237
256
  getCrypto,
238
257
  getOrCreateSecret,
239
258
  hkdfSha256,
@@ -246,6 +265,7 @@ export {
246
265
  isSecretKeyData,
247
266
  isSecretPersistenceEnabled,
248
267
  isSignedData,
268
+ isStandardWebhookSecret,
249
269
  isValidCodeChallenge,
250
270
  isValidCodeVerifier,
251
271
  isValidEncryptedBlob,
@@ -269,6 +289,8 @@ export {
269
289
  sha256Base64url,
270
290
  sha256Hex,
271
291
  signData,
292
+ signWebhookMessage,
293
+ signingKeyOf,
272
294
  timingSafeEqual,
273
295
  tryDecryptValue,
274
296
  tryDeserializeAndDecrypt,
@@ -277,5 +299,7 @@ export {
277
299
  validateSecretData,
278
300
  verifyCodeChallenge,
279
301
  verifyData,
280
- verifyOrParseData
302
+ verifyOrParseData,
303
+ verifyWebhookSignature,
304
+ webhookSignatureHeaders
281
305
  };
@@ -0,0 +1,96 @@
1
+ // libs/utils/src/crypto/standard-webhooks.ts
2
+ import { base64Decode, base64Encode, base64urlEncode, hmacSha256, randomBytes, timingSafeEqual } from "./index.mjs";
3
+ var WEBHOOK_ID_HEADER = "webhook-id";
4
+ var WEBHOOK_TIMESTAMP_HEADER = "webhook-timestamp";
5
+ var WEBHOOK_SIGNATURE_HEADER = "webhook-signature";
6
+ var WEBHOOK_SIGNATURE_TOLERANCE_SECONDS = 300;
7
+ var SECRET_PREFIX = "whsec_";
8
+ var MIN_SECRET_BYTES = 24;
9
+ var MAX_SECRET_BYTES = 64;
10
+ var GENERATED_SECRET_BYTES = 32;
11
+ var MESSAGE_ID_RANDOM_BYTES = 18;
12
+ var BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
13
+ var WELL_FORMED_BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}(?:==)?|[A-Za-z0-9+/]{3}=?)?$/;
14
+ var TIMESTAMP_PATTERN = /^\d{1,12}$/;
15
+ var MESSAGE_KIND_PATTERN = /^[A-Za-z0-9_-]+$/;
16
+ var SIGNATURE_VERSION = "v1";
17
+ var encoder = new TextEncoder();
18
+ function decodedKeyOf(secret) {
19
+ if (!secret.startsWith(SECRET_PREFIX)) return encoder.encode(secret);
20
+ const encoded = secret.slice(SECRET_PREFIX.length);
21
+ if (!BASE64_PATTERN.test(encoded)) throw new TypeError("not base64");
22
+ return base64Decode(encoded);
23
+ }
24
+ function signingKeyOf(secret) {
25
+ const signingKey = decodedKeyOf(secret);
26
+ if (signingKey.length === 0) throw new TypeError("empty signing key");
27
+ return signingKey;
28
+ }
29
+ function isStandardWebhookSecret(secret) {
30
+ if (!secret.startsWith(SECRET_PREFIX)) return false;
31
+ const encoded = secret.slice(SECRET_PREFIX.length);
32
+ if (!WELL_FORMED_BASE64_PATTERN.test(encoded)) return false;
33
+ const symbolCount = encoded.split("=")[0].length;
34
+ const byteCount = Math.floor(symbolCount * 3 / 4);
35
+ return byteCount >= MIN_SECRET_BYTES && byteCount <= MAX_SECRET_BYTES;
36
+ }
37
+ function generateStandardWebhookSecret() {
38
+ return `${SECRET_PREFIX}${base64Encode(randomBytes(GENERATED_SECRET_BYTES))}`;
39
+ }
40
+ function generateWebhookMessageId(kind) {
41
+ const random = base64urlEncode(randomBytes(MESSAGE_ID_RANDOM_BYTES));
42
+ if (kind === void 0) return `msg_${random}`;
43
+ if (!MESSAGE_KIND_PATTERN.test(kind)) {
44
+ throw new TypeError('a webhook message kind holds only ASCII letters, digits, "_" and "-"');
45
+ }
46
+ return `msg_${kind}_${random}`;
47
+ }
48
+ function signatureOf(signingKey, id, timestamp, body) {
49
+ return base64Encode(hmacSha256(signingKey, encoder.encode(`${id}.${timestamp}.${body}`)));
50
+ }
51
+ function signWebhookMessage(message, signingKeys) {
52
+ const timestamp = String(message.timestamp);
53
+ if (!message.id || !TIMESTAMP_PATTERN.test(timestamp) || signingKeys.length === 0) {
54
+ throw new TypeError("a webhook signature needs a message id, a timestamp in whole seconds and a signing key");
55
+ }
56
+ return signingKeys.map((signingKey) => `${SIGNATURE_VERSION},${signatureOf(signingKey, message.id, timestamp, message.body)}`).join(" ");
57
+ }
58
+ function webhookSignatureHeaders(message, signingKeys) {
59
+ return {
60
+ [WEBHOOK_ID_HEADER]: message.id,
61
+ [WEBHOOK_TIMESTAMP_HEADER]: String(message.timestamp),
62
+ [WEBHOOK_SIGNATURE_HEADER]: signWebhookMessage(message, signingKeys)
63
+ };
64
+ }
65
+ function isSameSignature(provided, expected) {
66
+ return provided.length === expected.length && timingSafeEqual(provided, expected);
67
+ }
68
+ function verifyWebhookSignature(message, signingKeys, now = Date.now()) {
69
+ const { id, timestamp, signature, body } = message;
70
+ if (!id || !timestamp || !signature) return "missing";
71
+ const isTimestamp = TIMESTAMP_PATTERN.test(timestamp);
72
+ if (!isTimestamp || Math.abs(now / 1e3 - Number(timestamp)) > WEBHOOK_SIGNATURE_TOLERANCE_SECONDS) return "stale";
73
+ const expectedSignatures = signingKeys.map(
74
+ (signingKey) => encoder.encode(signatureOf(signingKey, id, timestamp, body))
75
+ );
76
+ const matches = signature.split(" ").some((entry) => {
77
+ const [version, value] = entry.split(",");
78
+ if (version !== SIGNATURE_VERSION || value === void 0) return false;
79
+ const provided = encoder.encode(value);
80
+ return expectedSignatures.some((expected) => isSameSignature(provided, expected));
81
+ });
82
+ return matches ? "valid" : "mismatch";
83
+ }
84
+ export {
85
+ WEBHOOK_ID_HEADER,
86
+ WEBHOOK_SIGNATURE_HEADER,
87
+ WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,
88
+ WEBHOOK_TIMESTAMP_HEADER,
89
+ generateStandardWebhookSecret,
90
+ generateWebhookMessageId,
91
+ isStandardWebhookSecret,
92
+ signWebhookMessage,
93
+ signingKeyOf,
94
+ verifyWebhookSignature,
95
+ webhookSignatureHeaders
96
+ };
@@ -31,7 +31,9 @@ import {
31
31
  watchFile,
32
32
  getSpawnFn
33
33
  } from "./fs.mjs";
34
+ import { watchFiles, WATCH_FILES_RETRY_MS } from "./watch-files.mjs";
34
35
  export {
36
+ WATCH_FILES_RETRY_MS,
35
37
  access,
36
38
  appendFile,
37
39
  chmod,
@@ -60,6 +62,7 @@ export {
60
62
  symlink,
61
63
  unlink,
62
64
  watchFile,
65
+ watchFiles,
63
66
  writeFile,
64
67
  writeJSON
65
68
  };
@@ -0,0 +1,177 @@
1
+ // libs/utils/src/fs/watch-files.ts
2
+ import { assertNode } from "../crypto/runtime.mjs";
3
+ import { dirname, pathJoin, pathResolve } from "../path/index.mjs";
4
+ import { readdir, stat, watchFile } from "./fs.mjs";
5
+ var WATCH_FILES_RETRY_MS = 500;
6
+ var DEFAULT_DEBOUNCE_MS = 100;
7
+ function fingerprintOf(stats) {
8
+ return `${stats.ino}:${stats.size}:${stats.mtimeMs}`;
9
+ }
10
+ function isAbsence(error) {
11
+ const code = error.code;
12
+ return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
13
+ }
14
+ async function statsOf(path) {
15
+ try {
16
+ return await stat(path);
17
+ } catch (error) {
18
+ if (isAbsence(error)) return void 0;
19
+ throw error;
20
+ }
21
+ }
22
+ async function entriesOf(directory) {
23
+ try {
24
+ return await readdir(directory);
25
+ } catch (error) {
26
+ if (isAbsence(error)) return [];
27
+ throw error;
28
+ }
29
+ }
30
+ async function entryFingerprintOf(entry, known) {
31
+ try {
32
+ const stats = await statsOf(entry);
33
+ return stats?.isFile() ? fingerprintOf(stats) : void 0;
34
+ } catch {
35
+ return known;
36
+ }
37
+ }
38
+ async function lookAt(target, before) {
39
+ const files = /* @__PURE__ */ new Map();
40
+ const stats = await statsOf(target);
41
+ if (stats?.isDirectory()) {
42
+ for (const name of await entriesOf(target)) {
43
+ const child = pathJoin(target, name);
44
+ const fingerprint = await entryFingerprintOf(child, before?.get(child));
45
+ if (fingerprint !== void 0) files.set(child, fingerprint);
46
+ }
47
+ return { files, directory: target };
48
+ }
49
+ if (stats?.isFile()) files.set(target, fingerprintOf(stats));
50
+ const parent = dirname(target);
51
+ const parentStats = await statsOf(parent);
52
+ return { files, directory: parentStats?.isDirectory() ? parent : void 0 };
53
+ }
54
+ function changesBetween(before, after) {
55
+ const changes = [];
56
+ for (const [path, fingerprint] of after) {
57
+ const known = before.get(path);
58
+ if (known === void 0) changes.push({ type: "add", path });
59
+ else if (known !== fingerprint) changes.push({ type: "change", path });
60
+ }
61
+ for (const path of before.keys()) {
62
+ if (!after.has(path)) changes.push({ type: "unlink", path });
63
+ }
64
+ return changes;
65
+ }
66
+ var WatchedPath = class {
67
+ /** Takes the path to watch, how long to wait for changes to settle, and where to report each one. */
68
+ constructor(target, debounceMs, report) {
69
+ this.target = target;
70
+ this.debounceMs = debounceMs;
71
+ this.report = report;
72
+ }
73
+ target;
74
+ debounceMs;
75
+ report;
76
+ files;
77
+ watched;
78
+ timer;
79
+ looking = Promise.resolve();
80
+ closed = false;
81
+ /** Take the first look, which reports nothing, and start watching. */
82
+ start() {
83
+ return this.lookAgain();
84
+ }
85
+ /** Stop watching and looking. */
86
+ close() {
87
+ this.closed = true;
88
+ clearTimeout(this.timer);
89
+ this.watched?.handle.close();
90
+ }
91
+ /** Look again once `delayMs` passed without another reason to. */
92
+ lookLater(delayMs) {
93
+ clearTimeout(this.timer);
94
+ this.timer = setTimeout(() => {
95
+ this.looking = this.looking.then(() => this.lookAgain());
96
+ }, delayMs);
97
+ this.timer.unref?.();
98
+ }
99
+ /** Report what changed since the last look, if any; a path that cannot be read keeps its last look until it can. */
100
+ async lookAgain() {
101
+ const sighting = await lookAt(this.target, this.files).catch(() => void 0);
102
+ if (this.closed) return;
103
+ if (!sighting) {
104
+ this.lookLater(WATCH_FILES_RETRY_MS);
105
+ return;
106
+ }
107
+ if (this.files) for (const change of changesBetween(this.files, sighting.files)) this.report(change);
108
+ this.files = sighting.files;
109
+ this.follow(sighting.directory);
110
+ }
111
+ /** Watch the directory that shows the path's changes; while there is none, look again after a wait. */
112
+ follow(directory) {
113
+ if (this.watched?.directory !== directory) {
114
+ this.watched?.handle.close();
115
+ this.watched = directory === void 0 ? void 0 : this.watch(directory);
116
+ }
117
+ if (!this.watched) this.lookLater(WATCH_FILES_RETRY_MS);
118
+ }
119
+ /** Open a watcher on a directory, dropped for a new one when it fails; nothing when the directory went away. */
120
+ watch(directory) {
121
+ try {
122
+ const handle = watchFile(directory, () => this.lookLater(this.debounceMs));
123
+ const watched = { directory, handle };
124
+ handle.onError(() => {
125
+ if (this.watched !== watched) return;
126
+ handle.close();
127
+ this.watched = void 0;
128
+ this.lookLater(this.debounceMs);
129
+ });
130
+ return watched;
131
+ } catch {
132
+ return void 0;
133
+ }
134
+ }
135
+ };
136
+ async function* watchFiles(paths, options = {}) {
137
+ assertNode("watchFiles");
138
+ const { events, debounceMs = DEFAULT_DEBOUNCE_MS, signal } = options;
139
+ const pending = [];
140
+ let wake;
141
+ const wakeUp = () => wake?.();
142
+ const report = (event) => {
143
+ if (events && !events.includes(event.type)) return;
144
+ pending.push(event);
145
+ wakeUp();
146
+ };
147
+ const targets = Array.isArray(paths) ? paths : [paths];
148
+ const watchedPaths = targets.map((path) => new WatchedPath(pathResolve(path), debounceMs, report));
149
+ const closeWatchers = () => {
150
+ for (const watchedPath of watchedPaths) watchedPath.close();
151
+ };
152
+ const endOnAbort = () => {
153
+ closeWatchers();
154
+ wakeUp();
155
+ };
156
+ signal?.addEventListener("abort", endOnAbort, { once: true });
157
+ try {
158
+ await Promise.all(watchedPaths.map((watchedPath) => watchedPath.start()));
159
+ while (!signal?.aborted) {
160
+ const event = pending.shift();
161
+ if (event) {
162
+ yield event;
163
+ continue;
164
+ }
165
+ await new Promise((resolve) => {
166
+ wake = resolve;
167
+ });
168
+ }
169
+ } finally {
170
+ signal?.removeEventListener("abort", endOnAbort);
171
+ closeWatchers();
172
+ }
173
+ }
174
+ export {
175
+ WATCH_FILES_RETRY_MS,
176
+ watchFiles
177
+ };
package/browser/index.mjs CHANGED
@@ -24,7 +24,19 @@ import {
24
24
  } from "./path/index.mjs";
25
25
  import { sanitizeToJson, inferMimeType, findNonFiniteNumber, findDroppedKeys } from "./content/index.mjs";
26
26
  import { validateBaseUrl, buildSetCookie, parseCookies, getCookie, isLocalhost, isSecureRequest } from "./http/index.mjs";
27
- import { classifyIpAddress, expandIpv6, extractEmbeddedIpv4, parseIpv4, isRedirectResponse } from "./net/index.mjs";
27
+ import {
28
+ classifyIpAddress,
29
+ expandIpv6,
30
+ extractEmbeddedIpv4,
31
+ isGloballyRoutableIp,
32
+ parseIpv4,
33
+ isRedirectResponse,
34
+ validateOutboundUrl,
35
+ sendOutboundRequest,
36
+ outboundFailureCategory,
37
+ OUTBOUND_TIMEOUT_MS,
38
+ OUTBOUND_MAX_REQUEST_BYTES
39
+ } from "./net/index.mjs";
28
40
  import {
29
41
  readFile,
30
42
  readFileSync,
@@ -55,6 +67,7 @@ import {
55
67
  isDirEmpty,
56
68
  runCmd,
57
69
  watchFile,
70
+ watchFiles,
58
71
  getSpawnFn
59
72
  } from "./fs/index.mjs";
60
73
  import {
@@ -83,7 +96,7 @@ import {
83
96
  import { redactFields, redactJson } from "./redaction/index.mjs";
84
97
  import { escapeHtml, escapeHtmlAttr, escapeJsString, escapeScriptClose, safeJsonForScript } from "./escape/index.mjs";
85
98
  import { brandClass, isBrandedInstance } from "./class-brand/index.mjs";
86
- import { safeStringify } from "./serialization/index.mjs";
99
+ import { safeStringify, canonicalJson } from "./serialization/index.mjs";
87
100
  import {
88
101
  getCrypto,
89
102
  rsaVerify,
@@ -149,6 +162,17 @@ import {
149
162
  verifyData,
150
163
  isSignedData,
151
164
  verifyOrParseData,
165
+ WEBHOOK_ID_HEADER,
166
+ WEBHOOK_TIMESTAMP_HEADER,
167
+ WEBHOOK_SIGNATURE_HEADER,
168
+ WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,
169
+ signingKeyOf,
170
+ isStandardWebhookSecret,
171
+ generateStandardWebhookSecret,
172
+ generateWebhookMessageId,
173
+ signWebhookMessage,
174
+ webhookSignatureHeaders,
175
+ verifyWebhookSignature,
152
176
  asymmetricAlgSchema,
153
177
  secretKeyDataSchema,
154
178
  asymmetricKeyDataSchema,
@@ -283,6 +307,8 @@ export {
283
307
  MemoryStorageAdapter,
284
308
  NAMESPACE_SEPARATOR,
285
309
  NamespacedStorageImpl,
310
+ OUTBOUND_MAX_REQUEST_BYTES,
311
+ OUTBOUND_TIMEOUT_MS,
286
312
  OwnedProcessError,
287
313
  PkceError,
288
314
  REDOS_THRESHOLDS,
@@ -298,6 +324,10 @@ export {
298
324
  TypedStorage,
299
325
  UpstashStorageAdapter,
300
326
  VercelKvStorageAdapter,
327
+ WEBHOOK_ID_HEADER,
328
+ WEBHOOK_SIGNATURE_HEADER,
329
+ WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,
330
+ WEBHOOK_TIMESTAMP_HEADER,
301
331
  access,
302
332
  analyzePattern,
303
333
  anyKeyDataSchema,
@@ -316,6 +346,7 @@ export {
316
346
  buildPrefix,
317
347
  buildSetCookie,
318
348
  bytesToHex,
349
+ canonicalJson,
319
350
  checkEntryAvailability,
320
351
  chmod,
321
352
  classifyIpAddress,
@@ -377,6 +408,8 @@ export {
377
408
  generatePkcePair,
378
409
  generateRsaKeyPair,
379
410
  generateSecret,
411
+ generateStandardWebhookSecret,
412
+ generateWebhookMessageId,
380
413
  getAsyncContextMode,
381
414
  getCookie,
382
415
  getCrypto,
@@ -406,6 +439,7 @@ export {
406
439
  isEdgeRuntime,
407
440
  isEntryAvailable,
408
441
  isExpired,
442
+ isGloballyRoutableIp,
409
443
  isInputLengthSafe,
410
444
  isLocalhost,
411
445
  isNode,
@@ -420,6 +454,7 @@ export {
420
454
  isSecureRequest,
421
455
  isServerless,
422
456
  isSignedData,
457
+ isStandardWebhookSecret,
423
458
  isUriTemplate,
424
459
  isValidCodeChallenge,
425
460
  isValidCodeVerifier,
@@ -438,6 +473,7 @@ export {
438
473
  mkdir,
439
474
  mkdtemp,
440
475
  normalizeTTL,
476
+ outboundFailureCategory,
441
477
  parseCookies,
442
478
  parseIpv4,
443
479
  parseKeyData,
@@ -480,6 +516,7 @@ export {
480
516
  saveSecret,
481
517
  secretDataSchema,
482
518
  secretKeyDataSchema,
519
+ sendOutboundRequest,
483
520
  sep,
484
521
  sepFor,
485
522
  serializeBlob,
@@ -490,7 +527,9 @@ export {
490
527
  sha256Hex,
491
528
  shortHash,
492
529
  signData,
530
+ signWebhookMessage,
493
531
  signalProcessTree,
532
+ signingKeyOf,
494
533
  spawnOwned,
495
534
  splitWords,
496
535
  stat,
@@ -512,15 +551,19 @@ export {
512
551
  validateBaseUrl,
513
552
  validateKeyData,
514
553
  validateOptionalTTL,
554
+ validateOutboundUrl,
515
555
  validatePattern,
516
556
  validateSecretData,
517
557
  validateTTL,
518
558
  verifyCodeChallenge,
519
559
  verifyData,
520
560
  verifyOrParseData,
561
+ verifyWebhookSignature,
521
562
  watchFile,
563
+ watchFiles,
522
564
  watchParent,
523
565
  watchPath,
566
+ webhookSignatureHeaders,
524
567
  writeFile,
525
568
  writeJSON
526
569
  };
@@ -1,10 +1,23 @@
1
1
  // libs/utils/src/net/index.ts
2
- import { classifyIpAddress, expandIpv6, extractEmbeddedIpv4, parseIpv4 } from "./ip-address.mjs";
2
+ import { classifyIpAddress, expandIpv6, extractEmbeddedIpv4, isGloballyRoutableIp, parseIpv4 } from "./ip-address.mjs";
3
3
  import { isRedirectResponse } from "./redirect.mjs";
4
+ import { validateOutboundUrl } from "./outbound-url.mjs";
5
+ import {
6
+ OUTBOUND_MAX_REQUEST_BYTES,
7
+ OUTBOUND_TIMEOUT_MS,
8
+ outboundFailureCategory,
9
+ sendOutboundRequest
10
+ } from "./outbound-request.mjs";
4
11
  export {
12
+ OUTBOUND_MAX_REQUEST_BYTES,
13
+ OUTBOUND_TIMEOUT_MS,
5
14
  classifyIpAddress,
6
15
  expandIpv6,
7
16
  extractEmbeddedIpv4,
17
+ isGloballyRoutableIp,
8
18
  isRedirectResponse,
9
- parseIpv4
19
+ outboundFailureCategory,
20
+ parseIpv4,
21
+ sendOutboundRequest,
22
+ validateOutboundUrl
10
23
  };
@@ -32,6 +32,25 @@ var IPV6_BLOCKS = compileBlocks(
32
32
  ],
33
33
  expandIpv6
34
34
  );
35
+ var UNROUTED_IPV4_BLOCKS = compileBlocks(
36
+ [
37
+ ["192.0.2.0/24", "documentation"],
38
+ ["198.51.100.0/24", "documentation"],
39
+ ["203.0.113.0/24", "documentation"]
40
+ ],
41
+ parseIpv4
42
+ );
43
+ var UNROUTED_IPV6_BLOCKS = compileBlocks(
44
+ [
45
+ ["100::/64", "discard-only"],
46
+ ["100:0:0:1::/64", "dummy-prefix"],
47
+ ["2001::/23", "ietf-protocol"],
48
+ ["2001:db8::/32", "documentation"],
49
+ ["3fff::/20", "documentation"],
50
+ ["5f00::/16", "segment-routing"]
51
+ ],
52
+ expandIpv6
53
+ );
35
54
  var IPV4_EMBEDDINGS = compileBlocks(
36
55
  [
37
56
  ["::ffff:0:0/96", 6],
@@ -85,6 +104,16 @@ function classifyIpAddress(address) {
85
104
  if (!embeddedOctets) return { family: 6, range: "public" };
86
105
  return { ...classifyIpv4(embeddedOctets), family: 6, embeddedIpv4: embeddedOctets.join(".") };
87
106
  }
107
+ function isGloballyRoutableIp(address) {
108
+ const classification = classifyIpAddress(address);
109
+ if (classification?.range !== "public") return false;
110
+ const isBracketed = address.startsWith("[") && address.endsWith("]");
111
+ const bareAddress = isBracketed ? address.slice(1, -1) : address;
112
+ const octets = parseIpv4(classification.embeddedIpv4 ?? bareAddress);
113
+ if (octets) return !UNROUTED_IPV4_BLOCKS.some((block) => matchesBlock(octets, block, 8));
114
+ const groups = expandIpv6(bareAddress);
115
+ return groups !== void 0 && !UNROUTED_IPV6_BLOCKS.some((block) => matchesBlock(groups, block, 16));
116
+ }
88
117
  function classifyIpv4(octets) {
89
118
  const block = IPV4_BLOCKS.find((candidate) => matchesBlock(octets, candidate, 8));
90
119
  return block ? { family: 4, range: block.value, cidr: block.cidr } : { family: 4, range: "public" };
@@ -124,5 +153,6 @@ export {
124
153
  classifyIpAddress,
125
154
  expandIpv6,
126
155
  extractEmbeddedIpv4,
156
+ isGloballyRoutableIp,
127
157
  parseIpv4
128
158
  };