@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.
- package/README.md +4 -2
- package/browser/crypto/index.mjs +25 -1
- package/browser/crypto/standard-webhooks.mjs +96 -0
- package/browser/fs/index.mjs +3 -0
- package/browser/fs/watch-files.mjs +177 -0
- package/browser/index.mjs +45 -2
- package/browser/net/index.mjs +15 -2
- package/browser/net/ip-address.mjs +30 -0
- package/browser/net/outbound-request.mjs +190 -0
- package/browser/net/outbound-url.mjs +43 -0
- package/browser/serialization/canonical-json.mjs +35 -0
- package/browser/serialization/index.mjs +2 -0
- package/browser/stdio-bridge/claude-code-channels.mjs +73 -0
- package/browser/stdio-bridge/notification-stream.mjs +111 -0
- package/browser/stdio-bridge/sse-frames.mjs +48 -0
- package/browser/stdio-bridge/upstream-client.mjs +89 -37
- package/crypto/index.d.ts +1 -0
- package/crypto/standard-webhooks.d.ts +35 -0
- package/env/browser-runtime-context.d.ts +1 -1
- package/env/runtime-context.d.ts +1 -1
- package/esm/index.mjs +1 -1
- package/fs/index.d.ts +2 -0
- package/fs/watch-files.d.ts +22 -0
- package/index.d.ts +6 -6
- package/index.js +1 -1
- package/net/index.d.ts +5 -1
- package/net/ip-address.d.ts +2 -0
- package/net/outbound-request.d.ts +46 -0
- package/net/outbound-url.d.ts +18 -0
- package/package.json +2 -2
- package/serialization/canonical-json.d.ts +2 -0
- package/serialization/index.d.ts +1 -0
- package/stdio-bridge/claude-code-channels.d.ts +28 -0
- package/stdio-bridge/notification-stream.d.ts +29 -0
- package/stdio-bridge/sse-frames.d.ts +8 -0
- 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. |
|
package/browser/crypto/index.mjs
CHANGED
|
@@ -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
|
+
};
|
package/browser/fs/index.mjs
CHANGED
|
@@ -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 {
|
|
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
|
};
|
package/browser/net/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|