@frontmcp/utils 1.11.3 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -1,10 +1,20 @@
1
1
  // libs/utils/src/stdio-bridge/upstream-client.ts
2
- var SESSION_HEADER = "mcp-session-id";
2
+ import {
3
+ advertisesChannels,
4
+ declaringChannels,
5
+ DISCOVER_HEADERS,
6
+ discoverRequest,
7
+ namesProtocol20260728,
8
+ serverRevisionsOf,
9
+ unsupportedProtocolVersion
10
+ } from "./claude-code-channels.mjs";
11
+ import { createNotificationStream, SESSION_HEADER } from "./notification-stream.mjs";
12
+ import { relaySseFrames } from "./sse-frames.mjs";
3
13
  var END_SESSION_TIMEOUT_MS = 2e3;
4
- var reinitCounter = 0;
5
- function nextReinitId() {
6
- reinitCounter += 1;
7
- return `frontmcp-dev-bridge:reinitialize:${reinitCounter}`;
14
+ var bridgeRequestCounter = 0;
15
+ function nextBridgeId(purpose) {
16
+ bridgeRequestCounter += 1;
17
+ return `frontmcp-dev-bridge:${purpose}:${bridgeRequestCounter}`;
8
18
  }
9
19
  function initializedNotification() {
10
20
  return { jsonrpc: "2.0", method: "notifications/initialized" };
@@ -18,17 +28,31 @@ function createHttpUpstream(options) {
18
28
  const { log, url, onFrame } = options;
19
29
  const abortControllers = /* @__PURE__ */ new Set();
20
30
  let sessionId;
21
- async function post(frame, deliver) {
31
+ let revisions;
32
+ let streamAnswered = Promise.resolve();
33
+ const notifications = createNotificationStream({
34
+ url,
35
+ headers: options.headers,
36
+ fetch: options.fetch,
37
+ log,
38
+ onFrame,
39
+ onLost: options.onNotificationStreamLost
40
+ });
41
+ function sessionHeaders(frame) {
42
+ return sessionId && frame.method !== "initialize" ? { [SESSION_HEADER]: sessionId } : {};
43
+ }
44
+ async function post(frame, deliver, statelessHeaders) {
22
45
  const abortController = new AbortController();
23
46
  abortControllers.add(abortController);
24
47
  const signal = abortController.signal;
25
48
  try {
49
+ if (frame.method !== "initialize") await streamAnswered;
26
50
  const headers = {
27
51
  ...options.headers,
52
+ ...statelessHeaders ?? sessionHeaders(frame),
28
53
  "content-type": "application/json",
29
54
  accept: "application/json, text/event-stream"
30
55
  };
31
- if (sessionId && frame.method !== "initialize") headers[SESSION_HEADER] = sessionId;
32
56
  const res = await (options.fetch ?? fetch)(url, {
33
57
  method: "POST",
34
58
  redirect: "error",
@@ -40,40 +64,21 @@ function createHttpUpstream(options) {
40
64
  log.warn("http-upstream-non-ok", { status: res.status, method: frame.method });
41
65
  throw new Error(`upstream returned ${res.status} for ${frame.method ?? "request"}`);
42
66
  }
43
- const issued = res.headers.get(SESSION_HEADER);
44
- if (issued && frame.method === "initialize") {
45
- sessionId = issued;
67
+ const startedSession = frame.method === "initialize" ? res.headers.get(SESSION_HEADER) : null;
68
+ if (startedSession) {
69
+ sessionId = startedSession;
46
70
  log.info("http-upstream-session");
47
71
  }
72
+ const streamOfSession = startedSession && !signal.aborted ? notifications.follow(startedSession) : void 0;
73
+ if (streamOfSession) streamAnswered = streamOfSession;
48
74
  const contentType = res.headers.get("content-type") ?? "";
49
75
  if (contentType.includes("text/event-stream")) {
50
- const reader = res.body?.getReader();
51
- if (!reader) return;
52
- const decoder = new TextDecoder();
53
- let buffer = "";
54
- while (true) {
55
- const { done, value } = await reader.read();
56
- if (done) break;
57
- buffer += decoder.decode(value, { stream: true });
58
- let nl;
59
- while ((nl = buffer.indexOf("\n")) >= 0) {
60
- const line = buffer.slice(0, nl).trim();
61
- buffer = buffer.slice(nl + 1);
62
- if (!line.startsWith("data:")) continue;
63
- const json = line.slice(5).trim();
64
- if (!json) continue;
65
- try {
66
- const parsed = JSON.parse(json);
67
- await deliver(parsed);
68
- } catch (err) {
69
- log.warn("sse-parse-error", { error: err.message, raw: json.slice(0, 200) });
70
- }
71
- }
72
- }
76
+ if (res.body) await relaySseFrames(res.body, deliver, log);
73
77
  } else if (contentType.includes("application/json")) {
74
78
  const body = await res.json();
75
79
  await deliver(body);
76
80
  }
81
+ await streamOfSession;
77
82
  } catch (err) {
78
83
  if (err.name === "AbortError") {
79
84
  log.info("http-upstream-aborted", { method: frame.method });
@@ -85,23 +90,69 @@ function createHttpUpstream(options) {
85
90
  abortControllers.delete(abortController);
86
91
  }
87
92
  }
93
+ function toServer(initialize) {
94
+ return options.claudeCodeChannels ? declaringChannels(initialize) : initialize;
95
+ }
96
+ function refreshRevisions(initializeResult) {
97
+ if (revisions && revisions.pushesChannels !== advertisesChannels(initializeResult)) revisions = void 0;
98
+ }
99
+ async function discoverRevisions() {
100
+ const request = discoverRequest(nextBridgeId("discover"));
101
+ let result;
102
+ try {
103
+ await post(
104
+ request,
105
+ (frame) => {
106
+ if (frame.id === request.id) result = frame.result;
107
+ },
108
+ DISCOVER_HEADERS
109
+ );
110
+ } catch {
111
+ return void 0;
112
+ }
113
+ return serverRevisionsOf(result);
114
+ }
115
+ async function declined(frame) {
116
+ revisions ??= await discoverRevisions();
117
+ if (!revisions?.pushesChannels) return false;
118
+ if (frame.id !== void 0 && frame.id !== null) {
119
+ await onFrame(unsupportedProtocolVersion(frame, revisions.supported));
120
+ }
121
+ return true;
122
+ }
123
+ async function sendInitialize(initialize) {
124
+ await post(toServer(initialize), async (frame) => {
125
+ if (frame.id === initialize.id) refreshRevisions(frame.result);
126
+ await onFrame(frame);
127
+ });
128
+ }
88
129
  return {
89
- send: (frame) => post(frame, onFrame),
130
+ async send(frame) {
131
+ if (frame.method === "initialize") return sendInitialize(frame);
132
+ if (options.claudeCodeChannels && namesProtocol20260728(frame) && await declined(frame)) return;
133
+ await post(frame, onFrame);
134
+ },
90
135
  async reinitialize(initialize, initialized) {
91
- const id = nextReinitId();
136
+ const id = nextBridgeId("reinitialize");
92
137
  let response;
93
- await post({ ...initialize, id }, async (frame) => {
138
+ await post({ ...toServer(initialize), id }, async (frame) => {
94
139
  if (frame.id === id) response = frame;
95
140
  else await onFrame(frame);
96
141
  });
97
142
  const result = resultOf(response, "initialize");
143
+ refreshRevisions(result);
98
144
  if (initialized) await post(initializedNotification(), onFrame);
99
145
  return result;
100
146
  },
147
+ leaveSession() {
148
+ sessionId = void 0;
149
+ void notifications.follow(void 0);
150
+ },
101
151
  async endSession() {
102
152
  const ending = sessionId;
103
153
  if (!ending) return;
104
154
  sessionId = void 0;
155
+ void notifications.follow(void 0);
105
156
  try {
106
157
  const res = await (options.fetch ?? fetch)(url, {
107
158
  method: "DELETE",
@@ -115,6 +166,7 @@ function createHttpUpstream(options) {
115
166
  }
116
167
  },
117
168
  close: async () => {
169
+ void notifications.follow(void 0);
118
170
  for (const ac of abortControllers) ac.abort();
119
171
  abortControllers.clear();
120
172
  }
@@ -158,7 +210,7 @@ function createPipeUpstream(options) {
158
210
  return {
159
211
  send,
160
212
  async reinitialize(initialize, initialized) {
161
- const id = nextReinitId();
213
+ const id = nextBridgeId("reinitialize");
162
214
  let timer;
163
215
  const response = new Promise((resolve, reject) => {
164
216
  pending.set(id, resolve);
package/crypto/index.d.ts CHANGED
@@ -162,4 +162,5 @@ export { type EncryptedBlob, EncryptedBlobError, encryptValue, decryptValue, try
162
162
  export { MIN_CODE_VERIFIER_LENGTH, MAX_CODE_VERIFIER_LENGTH, DEFAULT_CODE_VERIFIER_LENGTH, PkceError, generateCodeVerifier, generateCodeChallenge, verifyCodeChallenge, generatePkcePair, isValidCodeVerifier, isValidCodeChallenge, type PkcePair, } from './pkce';
163
163
  export { type SecretData, type SecretPersistenceOptions, type SecretValidationResult, secretDataSchema, validateSecretData, parseSecretData, isSecretPersistenceEnabled, resolveSecretPath, loadSecret, saveSecret, deleteSecret, generateSecret, createSecretData, getOrCreateSecret, clearCachedSecret, isSecretCached, } from './secret-persistence';
164
164
  export { type SignedData, type HmacSigningConfig, signData, verifyData, isSignedData, verifyOrParseData, } from './hmac-signing';
165
+ export { WEBHOOK_ID_HEADER, WEBHOOK_TIMESTAMP_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, signingKeyOf, isStandardWebhookSecret, generateStandardWebhookSecret, generateWebhookMessageId, signWebhookMessage, webhookSignatureHeaders, verifyWebhookSignature, type WebhookMessage, type SignedWebhookMessage, type WebhookSignatureCheck, } from './standard-webhooks';
165
166
  export { type BaseKeyData, type SecretKeyData, type AsymmetricKeyData, type AnyKeyData, type KeyPersistenceOptions, type CreateKeyPersistenceOptions, type CreateSecretOptions, type CreateAsymmetricOptions, type KeyValidationResult, asymmetricAlgSchema, secretKeyDataSchema, asymmetricKeyDataSchema, anyKeyDataSchema, validateKeyData, parseKeyData, isSecretKeyData, isAsymmetricKeyData, KeyPersistence, createKeyPersistence, createKeyPersistenceWithStorage, } from './key-persistence';
@@ -0,0 +1,35 @@
1
+ /** Standard Webhooks signatures (https://www.standardwebhooks.com): HMAC-SHA256 over `<id>.<timestamp>.<body>`. */
2
+ export declare const WEBHOOK_ID_HEADER = "webhook-id";
3
+ export declare const WEBHOOK_TIMESTAMP_HEADER = "webhook-timestamp";
4
+ export declare const WEBHOOK_SIGNATURE_HEADER = "webhook-signature";
5
+ /** How far a signed timestamp may be from the verifier's clock, in seconds. */
6
+ export declare const WEBHOOK_SIGNATURE_TOLERANCE_SECONDS = 300;
7
+ export interface WebhookMessage {
8
+ /** The `webhook-id`, kept across the retries of one message. */
9
+ id: string;
10
+ /** The Unix time of this attempt, in whole seconds. */
11
+ timestamp: number;
12
+ /** The request body, exactly as it is sent. */
13
+ body: string;
14
+ }
15
+ export interface SignedWebhookMessage {
16
+ id?: string;
17
+ timestamp?: string;
18
+ signature?: string;
19
+ body: string;
20
+ }
21
+ export type WebhookSignatureCheck = 'valid' | 'missing' | 'stale' | 'mismatch';
22
+ /** The HMAC key of a secret: a `whsec_` secret is base64 after the prefix, any other its UTF-8 bytes; never empty. */
23
+ export declare function signingKeyOf(secret: string): Uint8Array;
24
+ /** Whether a secret is `whsec_` followed by base64, padded or not, that decodes to 24 to 64 bytes. */
25
+ export declare function isStandardWebhookSecret(secret: string): boolean;
26
+ /** A new `whsec_` secret of 32 random bytes. */
27
+ export declare function generateStandardWebhookSecret(): string;
28
+ /** A new `webhook-id`, `msg_<random>` or `msg_<kind>_<random>`: ASCII letters, digits, `_` and `-` only. */
29
+ export declare function generateWebhookMessageId(kind?: string): string;
30
+ /** The `webhook-signature` of a message: one `v1,<base64>` per signing key, separated by a space. */
31
+ export declare function signWebhookMessage(message: WebhookMessage, signingKeys: readonly Uint8Array[]): string;
32
+ /** The three headers that sign a message with every signing key. */
33
+ export declare function webhookSignatureHeaders(message: WebhookMessage, signingKeys: readonly Uint8Array[]): Record<string, string>;
34
+ /** Checks a signed message: a timestamp within the tolerance of `now`, and a `v1` signature one of the keys gives. */
35
+ export declare function verifyWebhookSignature(message: SignedWebhookMessage, signingKeys: readonly Uint8Array[], now?: number): WebhookSignatureCheck;
@@ -42,11 +42,11 @@ export declare const entryAvailabilitySchema: import("@frontmcp/lazy-zod").ZodOb
42
42
  provider: import("@frontmcp/lazy-zod").ZodOptional<import("@frontmcp/lazy-zod").ZodArray<import("@frontmcp/lazy-zod").ZodString>>;
43
43
  target: import("@frontmcp/lazy-zod").ZodOptional<import("@frontmcp/lazy-zod").ZodArray<import("@frontmcp/lazy-zod").ZodString>>;
44
44
  surface: import("@frontmcp/lazy-zod").ZodOptional<import("@frontmcp/lazy-zod").ZodArray<import("@frontmcp/lazy-zod").ZodEnum<{
45
+ agent: "agent";
45
46
  cli: "cli";
46
47
  mcp: "mcp";
47
48
  "http-trigger": "http-trigger";
48
49
  job: "job";
49
- agent: "agent";
50
50
  webmcp: "webmcp";
51
51
  }>>>;
52
52
  env: import("@frontmcp/lazy-zod").ZodOptional<import("@frontmcp/lazy-zod").ZodArray<import("@frontmcp/lazy-zod").ZodString>>;
@@ -98,11 +98,11 @@ export declare const entryAvailabilitySchema: import("@frontmcp/lazy-zod").ZodOb
98
98
  provider: import("@frontmcp/lazy-zod").ZodOptional<import("@frontmcp/lazy-zod").ZodArray<import("@frontmcp/lazy-zod").ZodString>>;
99
99
  target: import("@frontmcp/lazy-zod").ZodOptional<import("@frontmcp/lazy-zod").ZodArray<import("@frontmcp/lazy-zod").ZodString>>;
100
100
  surface: import("@frontmcp/lazy-zod").ZodOptional<import("@frontmcp/lazy-zod").ZodArray<import("@frontmcp/lazy-zod").ZodEnum<{
101
+ agent: "agent";
101
102
  cli: "cli";
102
103
  mcp: "mcp";
103
104
  "http-trigger": "http-trigger";
104
105
  job: "job";
105
- agent: "agent";
106
106
  webmcp: "webmcp";
107
107
  }>>>;
108
108
  env: import("@frontmcp/lazy-zod").ZodOptional<import("@frontmcp/lazy-zod").ZodArray<import("@frontmcp/lazy-zod").ZodString>>;