@frontmcp/utils 1.10.16 → 1.11.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 (135) hide show
  1. package/README.md +1 -0
  2. package/browser/async-context/browser-async-context.mjs +177 -0
  3. package/browser/async-context/index.mjs +15 -0
  4. package/browser/async-context/overlap-error.mjs +10 -0
  5. package/browser/class-brand/class-brand.mjs +27 -0
  6. package/browser/class-brand/index.mjs +6 -0
  7. package/browser/content/content.mjs +151 -0
  8. package/browser/content/index.mjs +8 -0
  9. package/browser/crypto/browser.mjs +103 -0
  10. package/browser/crypto/encrypted-blob.mjs +119 -0
  11. package/browser/crypto/hmac-signing.mjs +70 -0
  12. package/browser/crypto/index.mjs +281 -0
  13. package/browser/crypto/jwt-alg.mjs +40 -0
  14. package/browser/crypto/key-persistence/factory.mjs +134 -0
  15. package/browser/crypto/key-persistence/index.mjs +26 -0
  16. package/browser/crypto/key-persistence/key-persistence.mjs +185 -0
  17. package/browser/crypto/key-persistence/schemas.mjs +82 -0
  18. package/browser/crypto/pkce/index.mjs +25 -0
  19. package/browser/crypto/pkce/pkce.mjs +59 -0
  20. package/browser/crypto/runtime.mjs +17 -0
  21. package/browser/crypto/secret-persistence/index.mjs +29 -0
  22. package/browser/crypto/secret-persistence/persistence.mjs +181 -0
  23. package/browser/crypto/secret-persistence/schema.mjs +40 -0
  24. package/browser/crypto/web-rsa.mjs +36 -0
  25. package/browser/env/browser-env.mjs +56 -0
  26. package/browser/env/browser-runtime-context.mjs +85 -0
  27. package/browser/env/build-target.mjs +25 -0
  28. package/browser/env/edge-scope.mjs +17 -0
  29. package/browser/env/index.mjs +47 -0
  30. package/browser/env/provider.mjs +46 -0
  31. package/browser/escape/escape.mjs +43 -0
  32. package/browser/escape/index.mjs +9 -0
  33. package/browser/event-emitter/browser-event-emitter.mjs +49 -0
  34. package/browser/event-emitter/index.mjs +5 -0
  35. package/browser/fs/fs.mjs +227 -0
  36. package/browser/fs/index.mjs +65 -0
  37. package/browser/http/cookie.mjs +85 -0
  38. package/browser/http/http.mjs +18 -0
  39. package/browser/http/index.mjs +11 -0
  40. package/browser/index.mjs +526 -0
  41. package/browser/llm/index.mjs +5 -0
  42. package/browser/llm/llm-tool-handlers.mjs +55 -0
  43. package/browser/machine-id/index.mjs +7 -0
  44. package/browser/machine-id/machine-id.mjs +108 -0
  45. package/browser/naming/index.mjs +10 -0
  46. package/browser/naming/naming.mjs +68 -0
  47. package/browser/net/index.mjs +10 -0
  48. package/browser/net/ip-address.mjs +128 -0
  49. package/browser/net/redirect.mjs +7 -0
  50. package/browser/path/browser-path.mjs +47 -0
  51. package/browser/path/index.mjs +24 -0
  52. package/browser/path/path.mjs +13 -0
  53. package/browser/process/child-process.mjs +23 -0
  54. package/browser/process/index.mjs +15 -0
  55. package/browser/process/owned-process.mjs +175 -0
  56. package/browser/process/process-tree.mjs +149 -0
  57. package/browser/process/process-watch.mjs +48 -0
  58. package/browser/redaction/index.mjs +6 -0
  59. package/browser/redaction/redaction.mjs +74 -0
  60. package/browser/regex/index.mjs +3 -0
  61. package/browser/regex/patterns.mjs +167 -0
  62. package/browser/regex/safe-regex.mjs +93 -0
  63. package/browser/serialization/index.mjs +5 -0
  64. package/browser/serialization/serialization.mjs +22 -0
  65. package/browser/stdio-bridge/errors.mjs +27 -0
  66. package/browser/stdio-bridge/index.mjs +24 -0
  67. package/browser/stdio-bridge/list-changed.mjs +21 -0
  68. package/browser/stdio-bridge/state-machine.mjs +211 -0
  69. package/browser/stdio-bridge/stdio-framer.mjs +91 -0
  70. package/browser/stdio-bridge/upstream-client.mjs +189 -0
  71. package/browser/storage/adapters/base.mjs +139 -0
  72. package/browser/storage/adapters/cloudflare-kv.mjs +149 -0
  73. package/browser/storage/adapters/filesystem.mjs +279 -0
  74. package/browser/storage/adapters/index.mjs +23 -0
  75. package/browser/storage/adapters/indexeddb.mjs +198 -0
  76. package/browser/storage/adapters/localstorage.mjs +222 -0
  77. package/browser/storage/adapters/memory.mjs +350 -0
  78. package/browser/storage/adapters/redis.mjs +497 -0
  79. package/browser/storage/adapters/upstash.mjs +299 -0
  80. package/browser/storage/adapters/vercel-kv.mjs +226 -0
  81. package/browser/storage/change-feed.mjs +189 -0
  82. package/browser/storage/encrypted-typed-storage.mjs +329 -0
  83. package/browser/storage/errors.mjs +92 -0
  84. package/browser/storage/factory.mjs +155 -0
  85. package/browser/storage/index.mjs +96 -0
  86. package/browser/storage/namespace.mjs +186 -0
  87. package/browser/storage/redis-error-listener.mjs +32 -0
  88. package/browser/storage/redis-url.mjs +66 -0
  89. package/browser/storage/typed-storage.mjs +171 -0
  90. package/browser/storage/utils/compare-and-delete.mjs +9 -0
  91. package/browser/storage/utils/index.mjs +24 -0
  92. package/browser/storage/utils/pattern.mjs +68 -0
  93. package/browser/storage/utils/ttl.mjs +48 -0
  94. package/browser/uri/index.mjs +19 -0
  95. package/browser/uri/uri-template.mjs +86 -0
  96. package/browser/uri/uri-validation.mjs +18 -0
  97. package/crypto/browser.d.ts +1 -13
  98. package/crypto/browser.js +1 -1
  99. package/crypto/node.js +1 -1
  100. package/crypto/web-rsa.d.ts +13 -0
  101. package/env/browser-runtime-context.js +1 -1
  102. package/env/runtime-context.js +1 -1
  103. package/esm/crypto/browser.mjs +1 -1
  104. package/esm/crypto/node.mjs +1 -1
  105. package/esm/env/browser-runtime-context.mjs +1 -1
  106. package/esm/env/runtime-context.mjs +1 -1
  107. package/esm/index.mjs +1 -1
  108. package/fs/fs.d.ts +20 -4
  109. package/fs/index.d.ts +1 -1
  110. package/index.d.ts +9 -3
  111. package/index.js +1 -1
  112. package/package.json +24 -8
  113. package/process/child-process.d.ts +2 -0
  114. package/process/index.d.ts +6 -0
  115. package/process/owned-process.d.ts +59 -0
  116. package/process/process-tree.d.ts +78 -0
  117. package/process/process-watch.d.ts +41 -0
  118. package/redaction/index.d.ts +2 -0
  119. package/redaction/redaction.d.ts +24 -0
  120. package/stdio-bridge/errors.d.ts +23 -0
  121. package/stdio-bridge/index.d.ts +9 -0
  122. package/stdio-bridge/list-changed.d.ts +3 -0
  123. package/stdio-bridge/state-machine.d.ts +61 -0
  124. package/stdio-bridge/stdio-framer.d.ts +49 -0
  125. package/stdio-bridge/types.d.ts +10 -0
  126. package/stdio-bridge/upstream-client.d.ts +66 -0
  127. package/storage/adapters/base.d.ts +2 -0
  128. package/storage/adapters/memory.d.ts +1 -0
  129. package/storage/adapters/redis.d.ts +3 -0
  130. package/storage/change-feed.d.ts +47 -0
  131. package/storage/factory.d.ts +6 -0
  132. package/storage/index.d.ts +3 -1
  133. package/storage/namespace.d.ts +2 -0
  134. package/storage/types.d.ts +46 -1
  135. package/storage/utils/compare-and-delete.d.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/utils",
3
- "version": "1.10.16",
3
+ "version": "1.11.0-rc.1",
4
4
  "description": "Shared helpers for FrontMCP: URI templates, naming, cross-platform crypto and PKCE, key-value storage, file system and environment detection",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "license": "Apache-2.0",
@@ -33,7 +33,7 @@
33
33
  "@noble/hashes": "^2.0.1"
34
34
  },
35
35
  "peerDependencies": {
36
- "@frontmcp/lazy-zod": "1.10.16",
36
+ "@frontmcp/lazy-zod": "1.11.0-rc.1",
37
37
  "@upstash/redis": "^1.34.0",
38
38
  "@vercel/kv": "^2.0.0 || ^3.0.0",
39
39
  "ioredis": "^5.0.0"
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "imports": {
53
53
  "#crypto-provider": {
54
- "worker": "./crypto/node.js",
54
+ "workerd": "./crypto/node.js",
55
55
  "browser": {
56
56
  "import": "./esm/crypto/browser.mjs",
57
57
  "default": "./crypto/browser.js"
@@ -59,7 +59,7 @@
59
59
  "default": "./crypto/node.js"
60
60
  },
61
61
  "#async-context": {
62
- "worker": "./async-context/node-async-context.js",
62
+ "workerd": "./async-context/node-async-context.js",
63
63
  "browser": {
64
64
  "import": "./esm/async-context/browser-async-context.mjs",
65
65
  "default": "./async-context/browser-async-context.js"
@@ -67,7 +67,7 @@
67
67
  "default": "./async-context/node-async-context.js"
68
68
  },
69
69
  "#event-emitter": {
70
- "worker": "./event-emitter/node-event-emitter.js",
70
+ "workerd": "./event-emitter/node-event-emitter.js",
71
71
  "browser": {
72
72
  "import": "./esm/event-emitter/browser-event-emitter.mjs",
73
73
  "default": "./event-emitter/browser-event-emitter.js"
@@ -75,7 +75,7 @@
75
75
  "default": "./event-emitter/node-event-emitter.js"
76
76
  },
77
77
  "#env": {
78
- "worker": "./env/node-env.js",
78
+ "workerd": "./env/node-env.js",
79
79
  "browser": {
80
80
  "import": "./esm/env/browser-env.mjs",
81
81
  "default": "./env/browser-env.js"
@@ -83,7 +83,7 @@
83
83
  "default": "./env/node-env.js"
84
84
  },
85
85
  "#runtime-context": {
86
- "worker": "./env/runtime-context.js",
86
+ "workerd": "./env/runtime-context.js",
87
87
  "browser": {
88
88
  "import": "./esm/env/browser-runtime-context.mjs",
89
89
  "default": "./env/browser-runtime-context.js"
@@ -91,7 +91,7 @@
91
91
  "default": "./env/runtime-context.js"
92
92
  },
93
93
  "#path": {
94
- "worker": "./path/node-path.js",
94
+ "workerd": "./path/node-path.js",
95
95
  "browser": {
96
96
  "import": "./esm/path/browser-path.mjs",
97
97
  "default": "./path/browser-path.js"
@@ -107,6 +107,22 @@
107
107
  "exports": {
108
108
  "./package.json": "./package.json",
109
109
  ".": {
110
+ "workerd": {
111
+ "require": {
112
+ "types": "./index.d.ts",
113
+ "default": "./index.js"
114
+ },
115
+ "import": {
116
+ "types": "./index.d.ts",
117
+ "default": "./esm/index.mjs"
118
+ }
119
+ },
120
+ "browser": {
121
+ "types": "./index.d.ts",
122
+ "import": "./browser/index.mjs",
123
+ "module": "./browser/index.mjs",
124
+ "default": "./index.js"
125
+ },
110
126
  "require": {
111
127
  "types": "./index.d.ts",
112
128
  "default": "./index.js"
@@ -0,0 +1,2 @@
1
+ /** `child_process`, loaded on first use so the browser and edge builds never import it. */
2
+ export declare function getChildProcess(): typeof import('child_process');
@@ -0,0 +1,6 @@
1
+ export { processTreeSpawnOptions, signalProcessTree, isProcessTreeAlive, stopProcessTree } from './process-tree';
2
+ export type { ProcessTreeDeps } from './process-tree';
3
+ export { spawnOwned, reapOrphans, OwnedProcessError } from './owned-process';
4
+ export type { SpawnOwnedOptions, OwnedProcess, ReapOrphansOptions } from './owned-process';
5
+ export { watchParent, watchPath } from './process-watch';
6
+ export type { ParentGoneReason, ProcessWatcher, WatchParentOptions, WatchPathOptions } from './process-watch';
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Child processes that carry their owner's mark, so the ones whose owner died can be found and stopped.
3
+ *
4
+ * **Node.js only.**
5
+ */
6
+ import type { ChildProcess, SpawnOptions } from 'child_process';
7
+ import { type ProcessTreeDeps } from './process-tree';
8
+ export declare class OwnedProcessError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ export interface SpawnOwnedOptions extends SpawnOptions {
12
+ /** Names the kind of process, for {@link reapOrphans}: up to 64 letters, digits, `.`, `_` and `-`. */
13
+ marker: string;
14
+ }
15
+ export interface OwnedProcess {
16
+ readonly child: ChildProcess;
17
+ /**
18
+ * Stop the child, with every process it started when it was spawned `detached` (always on Windows), killing what
19
+ * outlives `graceMs`; resolves `true` once they are gone.
20
+ */
21
+ stop(signal?: NodeJS.Signals, graceMs?: number): Promise<boolean>;
22
+ }
23
+ export interface ProcessInfo {
24
+ pid: number;
25
+ ppid: number;
26
+ pgid: number;
27
+ /** Exited, but not yet collected by its parent: `kill(pid, 0)` still reaches it. */
28
+ zombie: boolean;
29
+ command: string;
30
+ }
31
+ export interface ReapOrphansOptions {
32
+ /** How long the orphans get to exit after `SIGTERM` before they are killed. @default 2000 */
33
+ graceMs?: number;
34
+ }
35
+ export interface ReapOrphansDeps extends Pick<ProcessTreeDeps, 'platform' | 'kill' | 'sleep'> {
36
+ listProcesses: () => ProcessInfo[];
37
+ }
38
+ /**
39
+ * Spawn `command` marked as owned by this process: the child gets `FRONTMCP_OWNER=<marker>:<owner pid>` in its
40
+ * environment and `--frontmcp-owner=<marker>:<owner pid>` as its last argument, which {@link reapOrphans} finds
41
+ * with `ps`. The command must take that last argument as an argument: a script run by node does, while
42
+ * `node -e <code>` reads it as a node option unless the arguments end with `--`.
43
+ *
44
+ * @throws OwnedProcessError when the marker is invalid
45
+ */
46
+ export declare function spawnOwned(command: string, args: readonly string[], options: SpawnOwnedOptions): OwnedProcess;
47
+ /** Parse `ps -o pid=,ppid=,pgid=,stat=,command=` output; lines it cannot read are skipped. */
48
+ export declare function parseProcessList(output: string): ProcessInfo[];
49
+ /**
50
+ * Stop the processes {@link spawnOwned} marked with `marker` whose owner no longer runs, with every process they
51
+ * started: `SIGTERM`, then `SIGKILL` for what is still running after `graceMs`. A process whose owner still runs,
52
+ * or that carries another marker or none, is left alone, and so are this process, its ancestors and the leader of
53
+ * its process group. Resolves with the pids of the marked orphans it stopped.
54
+ *
55
+ * POSIX only: it reads the process table with `ps`. On Windows it does nothing and resolves with `[]`.
56
+ *
57
+ * @throws OwnedProcessError when the marker is invalid, or `ps` cannot list the processes
58
+ */
59
+ export declare function reapOrphans(marker: string, options?: ReapOrphansOptions, deps?: Partial<ReapOrphansDeps>): Promise<number[]>;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Process-tree lifecycle for long-lived children, such as the ones `frontmcp dev` spawns.
3
+ *
4
+ * The process that actually holds the dev server's port is not always our
5
+ * direct child: `tsx --watch main.ts` forks the server, and the `npx` fallback
6
+ * puts npm in between as well. Signalling only the direct child let it exit
7
+ * while the server kept listening — `kill <frontmcp dev pid>` exited 0 and left
8
+ * the port bound (#679). Ctrl+C worked only because the terminal signals the
9
+ * whole foreground process group.
10
+ *
11
+ * On POSIX every child is therefore started as the leader of its own process
12
+ * group and shutdown signals the whole group, whoever sits in the middle.
13
+ *
14
+ * Windows has no process groups (#731). `taskkill /T` ends a tree, but only
15
+ * while its root is alive — it walks parent links down from the root. So:
16
+ * - a stop first asks the tree to close (`taskkill /T`, no `/F`), and forces
17
+ * it (`/T /F`) when taskkill reports failure or the grace period runs out.
18
+ * A windowless console process (node) usually only stops when forced;
19
+ * checking taskkill's exit status makes that escalation immediate;
20
+ * - when the direct child has already exited, the processes it started are
21
+ * found by their parent PID (`Get-CimInstance Win32_Process`) and ended one
22
+ * tree at a time, and the tree only counts as gone once none is left.
23
+ * Spawning tools directly with `process.execPath` (the CLI's `tool-command.ts`)
24
+ * removes the `cmd.exe` shim from the tree, so a dying parent's kill-on-close
25
+ * job object (libuv puts every non-detached child in one) takes its children
26
+ * with it; the lookup above covers what is left.
27
+ *
28
+ * **Node.js only**: `child_process` is loaded on first use, as the file system helpers load `fs`.
29
+ */
30
+ import type { ChildProcess, SpawnOptions } from 'child_process';
31
+ export interface ProcessTreeDeps {
32
+ platform: NodeJS.Platform;
33
+ /** `process.kill` — injectable for tests. */
34
+ kill: (pid: number, signal: NodeJS.Signals | 0) => boolean | void;
35
+ /**
36
+ * Runs `taskkill /pid <pid> /T` (plus `/F` when `force`) on Windows. Returns
37
+ * `true` only when taskkill reported success.
38
+ */
39
+ taskkill: (pid: number, force: boolean) => boolean;
40
+ /** PIDs of the running processes whose parent PID is `pid` (Windows). */
41
+ childPids: (pid: number) => number[];
42
+ sleep: (ms: number) => Promise<void>;
43
+ }
44
+ /** Runs taskkill and reports whether it succeeded — `spawnSync` itself never throws. */
45
+ export declare function runTaskkill(pid: number, force: boolean): boolean;
46
+ /** Child PIDs of `pid` from `Get-CimInstance Win32_Process`; `[]` when the lookup fails. */
47
+ export declare function queryChildPids(pid: number): number[];
48
+ /** One PID per line; anything else is ignored. */
49
+ export declare function parsePidList(output: string): number[];
50
+ export declare function sleep(ms: number): Promise<void>;
51
+ /**
52
+ * Spawn options that make the child the leader of a new process group (POSIX),
53
+ * so {@link signalProcessTree} can reach every process it starts. Children in
54
+ * their own group no longer receive the terminal's Ctrl+C directly — the parent
55
+ * must forward the signal, which `frontmcp dev` does.
56
+ */
57
+ export declare function processTreeSpawnOptions(platform?: NodeJS.Platform): Pick<SpawnOptions, 'detached'>;
58
+ /** True while `pid` runs; a process this user may not signal (`EPERM`) counts as running. */
59
+ export declare function isPidRunning(pid: number, d: Pick<ProcessTreeDeps, 'kill'>): boolean;
60
+ /**
61
+ * Send `signal` to `child` and every process it started. Returns `false` when
62
+ * nothing was left to signal (or, on Windows, when taskkill failed).
63
+ *
64
+ * On Windows `SIGKILL` forces the tree down (`taskkill /T /F`); any other
65
+ * signal asks it to close (`taskkill /T`).
66
+ */
67
+ export declare function signalProcessTree(child: ChildProcess | undefined, signal: NodeJS.Signals, deps?: Partial<ProcessTreeDeps>): boolean;
68
+ /**
69
+ * True while any process of the child's tree is still running. On Windows an
70
+ * exited child still counts as alive while a process it started runs.
71
+ */
72
+ export declare function isProcessTreeAlive(child: ChildProcess | undefined, deps?: Partial<ProcessTreeDeps>): boolean;
73
+ /**
74
+ * Signal the tree and wait until every process in it has exited. After
75
+ * `graceMs` the tree is killed (`SIGKILL`, or `taskkill /T /F` on Windows);
76
+ * resolves `true` when the tree is gone.
77
+ */
78
+ export declare function stopProcessTree(child: ChildProcess | undefined, signal: NodeJS.Signals, graceMs: number, deps?: Partial<ProcessTreeDeps>): Promise<boolean>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Watchers that tell a long-lived process its reason to run is gone: its parent exited, its stdin ended, or a
3
+ * directory it serves was removed. Their timers are unref'd, so they never keep the process alive.
4
+ *
5
+ * **Node.js only.**
6
+ */
7
+ import type { Readable } from 'stream';
8
+ import { type ProcessTreeDeps } from './process-tree';
9
+ export type ParentGoneReason = 'parent-exited' | 'stdin-ended';
10
+ export interface ProcessWatcher {
11
+ /** Stop watching. Safe to call more than once. */
12
+ close(): void;
13
+ }
14
+ export interface WatchParentOptions {
15
+ /** Called once, when the parent is gone. */
16
+ onGone: (reason: ParentGoneReason) => void;
17
+ /** How often the parent is checked. @default 1000 */
18
+ intervalMs?: number;
19
+ /** A stream whose end also counts as the parent going away, such as `process.stdin` for a stdio server. */
20
+ stdin?: Readable;
21
+ }
22
+ export interface WatchParentDeps extends Pick<ProcessTreeDeps, 'kill'> {
23
+ ppid: () => number;
24
+ }
25
+ export interface WatchPathOptions {
26
+ /** Called once, when the path no longer exists. */
27
+ onGone: () => void;
28
+ /** How often the path is checked. @default 1000 */
29
+ intervalMs?: number;
30
+ }
31
+ /**
32
+ * Report when this process's parent is gone: this process was handed to another parent (the parent pid changed),
33
+ * or the parent pid no longer runs (Windows does not reparent). A process started by init has no parent to watch,
34
+ * so only `stdin` is watched then.
35
+ */
36
+ export declare function watchParent(options: WatchParentOptions, deps?: Partial<WatchParentDeps>): ProcessWatcher;
37
+ /**
38
+ * Report when `path` no longer exists, for example a checkout that was deleted. A check that fails for another reason,
39
+ * such as a folder it may not search or a network file system timing out, is not taken as the path being gone.
40
+ */
41
+ export declare function watchPath(path: string, options: WatchPathOptions): ProcessWatcher;
@@ -0,0 +1,2 @@
1
+ export { redactFields, redactJson } from './redaction';
2
+ export type { RedactedFields } from './redaction';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Field redaction for structured log entries and recorded calls.
3
+ *
4
+ * Replaces the values of sensitive fields with '[REDACTED]'.
5
+ */
6
+ /** The fields a redaction hides: field names, matched case-insensitively, or a test on each key. */
7
+ export type RedactedFields = readonly string[] | ((key: string) => boolean);
8
+ /**
9
+ * Redact sensitive fields from an object.
10
+ *
11
+ * Performs a shallow clone with redacted values — does not mutate the input.
12
+ *
13
+ * @param obj - Object to redact fields from
14
+ * @param fields - Field names to redact (case-insensitive), or a test on each key
15
+ * @param maxDepth - Maximum recursion depth (default: 5)
16
+ * @returns New object with sensitive fields replaced by '[REDACTED]'
17
+ */
18
+ export declare function redactFields(obj: Record<string, unknown>, fields: RedactedFields, maxDepth?: number): Record<string, unknown>;
19
+ /**
20
+ * A copy of a JSON value with its sensitive fields redacted at any depth, also inside strings that hold JSON.
21
+ * In other strings it redacts bearer and basic credentials, JWTs, the credentials of a URL, and a value given to a
22
+ * sensitive name (`?access_token=…`, `apiKey: "…"`, `TOKEN=…`).
23
+ */
24
+ export declare function redactJson(value: unknown, fields: RedactedFields): unknown;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * JSON-RPC error codes emitted by the dev bridge (issue #399).
3
+ *
4
+ * Reserved in the implementation-defined `-32099 .. -32000` range so they
5
+ * never collide with the JSON-RPC 2.0 reserved range. Clients (Claude
6
+ * Code, etc.) receive these in `error.code` and can render structured
7
+ * feedback instead of sitting on an indefinite `Calling…` spinner.
8
+ */
9
+ export declare const DEV_SERVER_UNREACHABLE = -32099;
10
+ export declare const DEV_BUFFER_FULL = -32098;
11
+ export declare const DEV_RELOAD_DEADLINE = -32097;
12
+ /** Human-readable label for each code. Used in the `error.message` field. */
13
+ export declare const DEV_ERROR_MESSAGE: Record<number, string>;
14
+ /** Build a JSON-RPC error response for a given inbound request id. */
15
+ export declare function makeDevError(id: string | number | null, code: number, data?: Record<string, unknown>): {
16
+ jsonrpc: '2.0';
17
+ id: string | number | null;
18
+ error: {
19
+ code: number;
20
+ message: string;
21
+ data?: Record<string, unknown>;
22
+ };
23
+ };
@@ -0,0 +1,9 @@
1
+ export { DEV_BUFFER_FULL, DEV_ERROR_MESSAGE, DEV_RELOAD_DEADLINE, DEV_SERVER_UNREACHABLE, makeDevError, } from './errors';
2
+ export { listChangedNotifications } from './list-changed';
3
+ export { createBridgeStateMachine } from './state-machine';
4
+ export type { BridgeState, BridgeStateMachine, BridgeStateMachineOptions, InflightRequest } from './state-machine';
5
+ export { createStdioFramer } from './stdio-framer';
6
+ export type { JsonRpcFrame, StdioFramer, StdioFramerOptions } from './stdio-framer';
7
+ export type { BridgeLogger } from './types';
8
+ export { createHttpUpstream, createPipeUpstream } from './upstream-client';
9
+ export type { HttpUpstreamClient, HttpUpstreamOptions, PipeUpstreamOptions, UpstreamClient, UpstreamClientOptions, } from './upstream-client';
@@ -0,0 +1,3 @@
1
+ import type { JsonRpcFrame } from './stdio-framer';
2
+ /** The `list_changed` notifications the capabilities of a fresh server advertise. */
3
+ export declare function listChangedNotifications(initializeResult: Record<string, unknown> | undefined): JsonRpcFrame[];
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Bridge state machine (issue #399).
3
+ *
4
+ * Idle → Booting → Ready ⇄ Reloading
5
+ * ↓ deadline
6
+ * Degraded
7
+ *
8
+ * Owns the request buffer that absorbs inbound JSON-RPC frames while the
9
+ * upstream child is mid-restart. When buffered requests exceed the
10
+ * configured cap (default 8), the FSM synthesises an immediate
11
+ * `dev_buffer_full` response so clients never silently lose frames.
12
+ *
13
+ * The FSM is transport-agnostic — `child-supervisor` and `upstream-client`
14
+ * call into it; this module never touches sockets, child processes, or
15
+ * file descriptors.
16
+ */
17
+ import type { JsonRpcFrame } from './stdio-framer';
18
+ import type { BridgeLogger } from './types';
19
+ export type BridgeState = 'Idle' | 'Booting' | 'Ready' | 'Reloading' | 'Degraded' | 'Stopping';
20
+ export interface InflightRequest {
21
+ /** Original frame (kept for replay if the child dies before responding). */
22
+ frame: JsonRpcFrame;
23
+ /** Wall-clock when the bridge forwarded the frame to upstream. */
24
+ forwardedAt: number;
25
+ }
26
+ export interface BridgeStateMachineOptions {
27
+ log: BridgeLogger;
28
+ bufferSize: number;
29
+ reloadDeadlineMs: number;
30
+ /** Send a JSON-RPC frame back to the client (stdio out). */
31
+ respond(frame: JsonRpcFrame): void | Promise<void>;
32
+ /** Forward a request frame to the upstream child. Implemented by the supervisor. */
33
+ forward(frame: JsonRpcFrame): void | Promise<void>;
34
+ }
35
+ export interface BridgeStateMachine {
36
+ readonly state: BridgeState;
37
+ /** Bridge has begun launching the child — buffer inbound frames. */
38
+ onBootStart(): void;
39
+ /** Child reports ready — drain the buffer through `forward`. */
40
+ onChildReady(): void;
41
+ /** Child exited unexpectedly. If we were Ready, transition to Reloading. */
42
+ onChildExit(reason: string): void;
43
+ /** The upstream went away while its requests in flight still settle on their own: buffer new frames until ready. */
44
+ onUpstreamLost(reason: string): void;
45
+ /** Put back a request the upstream never took: sent at once when Ready, otherwise first in the buffer. */
46
+ requeue(frame: JsonRpcFrame): Promise<void>;
47
+ /** Watcher fired. Buffer inbound, start reload timer. */
48
+ onWatcherEvent(trigger: string): void;
49
+ /** Reload deadline elapsed without a ready signal. */
50
+ /** `detail` (e.g. why the first boot failed) is added to every buffered request's error data. */
51
+ onReloadDeadline(detail?: Record<string, unknown>): void;
52
+ /** Inbound JSON-RPC frame from stdin. Routes to `forward` or buffers. */
53
+ enqueue(frame: JsonRpcFrame): Promise<void>;
54
+ /** Outbound JSON-RPC frame from upstream — relay to client. */
55
+ relayUpstream(frame: JsonRpcFrame): Promise<void>;
56
+ /** SIGTERM/SIGINT — flush buffer with `dev_server_unreachable` and stop. */
57
+ stop(): Promise<void>;
58
+ /** Number of frames currently buffered (testing hook). */
59
+ bufferDepth(): number;
60
+ }
61
+ export declare function createBridgeStateMachine(options: BridgeStateMachineOptions): BridgeStateMachine;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Newline-delimited JSON-RPC framer for the dev bridge (issue #399).
3
+ *
4
+ * Reads `process.stdin` and yields complete JSON-RPC frames. Writes
5
+ * complete frames to `process.stdout` (atomic per `\n` boundary). Parser
6
+ * state is preserved across chunks so frames split arbitrarily on the
7
+ * wire still parse.
8
+ *
9
+ * MCP stdio framing per spec is newline-delimited JSON (`\n`-terminated
10
+ * UTF-8). We do NOT implement LSP-style `Content-Length` framing — it's
11
+ * not in the MCP spec and adding it would silently shadow real JSON
12
+ * bodies that happen to start with `C`.
13
+ */
14
+ import type { Readable, Writable } from 'node:stream';
15
+ import type { BridgeLogger } from './types';
16
+ export interface JsonRpcFrame {
17
+ jsonrpc: '2.0';
18
+ id?: string | number | null;
19
+ method?: string;
20
+ params?: unknown;
21
+ result?: unknown;
22
+ error?: {
23
+ code: number;
24
+ message: string;
25
+ data?: unknown;
26
+ };
27
+ }
28
+ export interface StdioFramerOptions {
29
+ input: Readable;
30
+ output: Writable;
31
+ log: BridgeLogger;
32
+ onFrame: (frame: JsonRpcFrame) => void | Promise<void>;
33
+ /** Called once when the input stream ends or closes (the MCP client went away). */
34
+ onClose?: () => void;
35
+ }
36
+ export interface StdioFramer {
37
+ start(): void;
38
+ /** Write a single frame as a newline-terminated JSON line. Resolves on `'drain'` when backpressure kicks in. */
39
+ write(frame: JsonRpcFrame): Promise<void>;
40
+ stop(): void;
41
+ }
42
+ /**
43
+ * Build a newline-delimited JSON framer bound to the supplied streams.
44
+ *
45
+ * Parse errors (malformed JSON between newlines) emit a `-32700` Parse
46
+ * error response back on the output stream and continue — a malformed
47
+ * frame must not kill the bridge.
48
+ */
49
+ export declare function createStdioFramer(options: StdioFramerOptions): StdioFramer;
@@ -0,0 +1,10 @@
1
+ /** Where a stdio bridge logs: never stdout, which carries the JSON-RPC frames. */
2
+ export interface BridgeLogger {
3
+ readonly path: string | undefined;
4
+ debug(message: string, data?: Record<string, unknown>): void;
5
+ info(message: string, data?: Record<string, unknown>): void;
6
+ warn(message: string, data?: Record<string, unknown>): void;
7
+ error(message: string, data?: Record<string, unknown>): void;
8
+ reloadEvent(kind: string, data?: Record<string, unknown>): void;
9
+ close(): Promise<void>;
10
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Upstream MCP client for the dev bridge (issue #399).
3
+ *
4
+ * Two transport variants:
5
+ *
6
+ * - **HTTP mode**: speaks streamable-HTTP JSON-RPC to the child's HTTP
7
+ * listener. The session id is the one the SERVER issues on `initialize`
8
+ * (`mcp-session-id` response header) and is sent on every later request.
9
+ * The bridge used to pin a uuid of its own, which the server never issued,
10
+ * so every request after `initialize` answered 404 (#679).
11
+ *
12
+ * - **Pipe mode (--serve)**: forwards JSON-RPC frames over the Node IPC
13
+ * channel paired with the child (`FRONTMCP_DEV_STDIO_FD=3`); the SDK serves
14
+ * MCP on it. No HTTP layer, no session header.
15
+ *
16
+ * A restarted child has no session at all, so the bridge replays the client's
17
+ * MCP handshake against it (`reinitialize`) before draining buffered requests.
18
+ * The client keeps its stdio connection and never learns the server restarted.
19
+ */
20
+ import type { ChildProcess } from 'node:child_process';
21
+ import type { JsonRpcFrame } from './stdio-framer';
22
+ import type { BridgeLogger } from './types';
23
+ export interface UpstreamClient {
24
+ send(frame: JsonRpcFrame): Promise<void>;
25
+ /**
26
+ * Replay the client's handshake on a fresh child: send `initialize` (under a
27
+ * bridge-private id, its response is not relayed) and, when the client had
28
+ * sent it, `notifications/initialized`. Resolves with the `initialize` result.
29
+ */
30
+ reinitialize(initialize: JsonRpcFrame, initialized: boolean): Promise<Record<string, unknown> | undefined>;
31
+ /** Stop background tasks (in-flight requests, pipe listener). */
32
+ close(): Promise<void>;
33
+ }
34
+ export interface UpstreamClientOptions {
35
+ log: BridgeLogger;
36
+ /** Called for every frame the upstream child sends back. */
37
+ onFrame: (frame: JsonRpcFrame) => void | Promise<void>;
38
+ }
39
+ export interface HttpUpstreamOptions extends UpstreamClientOptions {
40
+ /** Loopback URL of the user-code MCP endpoint. */
41
+ url: string;
42
+ /** Sent with every request, such as the credential a shared local server asks for. */
43
+ headers?: Record<string, string>;
44
+ /** Sends the requests instead of the global `fetch`, such as one that reaches a Unix socket. */
45
+ fetch?: typeof fetch;
46
+ }
47
+ export interface HttpUpstreamClient extends UpstreamClient {
48
+ /** End the MCP session the server issued (`DELETE` with its id); the next `initialize` starts a new one. */
49
+ endSession(): Promise<void>;
50
+ }
51
+ export declare function createHttpUpstream(options: HttpUpstreamOptions): HttpUpstreamClient;
52
+ export interface PipeUpstreamOptions extends UpstreamClientOptions {
53
+ /** The spawned child; we write to / read from its IPC channel (FD 3). */
54
+ child: ChildProcess;
55
+ /** Called with each batch of dev events the child sends (`{ __frontmcp: 'dev-events', events }`). */
56
+ onDevEvents?: (events: unknown[]) => void;
57
+ /** How long a replayed `initialize` may take. */
58
+ reinitializeTimeoutMs?: number;
59
+ }
60
+ /**
61
+ * Pipe mode: the child speaks JSON-RPC on FD 3 (set via
62
+ * `FRONTMCP_DEV_STDIO_FD=3`). We use Node's IPC channel for the same wire
63
+ * — `child.send(...)` forwards a structured message and `child.on('message', …)`
64
+ * yields whatever the child writes back.
65
+ */
66
+ export declare function createPipeUpstream(options: PipeUpstreamOptions): UpstreamClient;
@@ -70,6 +70,8 @@ export declare abstract class BaseStorageAdapter implements StorageAdapter {
70
70
  deleteIfEquals(_key: string, _expectedValue: string): Promise<boolean>;
71
71
  /** Default: not supported, for the same reason as {@link deleteIfEquals}. */
72
72
  expireIfEquals(_key: string, _expectedValue: string, _ttlSeconds: number): Promise<boolean>;
73
+ /** Default: not supported, for the same reason as {@link deleteIfEquals}. */
74
+ setIfEquals(_key: string, _expectedValue: string, _value: string): Promise<boolean>;
73
75
  /**
74
76
  * Default: pub/sub not supported.
75
77
  * Override in adapters that support it.
@@ -44,6 +44,7 @@ export declare class MemoryStorageAdapter extends BaseStorageAdapter {
44
44
  delete(key: string): Promise<boolean>;
45
45
  deleteIfEquals(key: string, expectedValue: string): Promise<boolean>;
46
46
  expireIfEquals(key: string, expectedValue: string, ttlSeconds: number): Promise<boolean>;
47
+ setIfEquals(key: string, expectedValue: string, value: string): Promise<boolean>;
47
48
  exists(key: string): Promise<boolean>;
48
49
  expire(key: string, ttlSeconds: number): Promise<boolean>;
49
50
  ttl(key: string): Promise<number | null>;
@@ -39,6 +39,7 @@ export declare class RedisStorageAdapter extends BaseStorageAdapter {
39
39
  private connecting?;
40
40
  private readonly keyPrefix;
41
41
  private readonly subscriptionHandlers;
42
+ private readonly reconnectListeners;
42
43
  private readonly connectionUrl?;
43
44
  /** The connection options of a URL this adapter reads itself; undefined for one only ioredis reads. */
44
45
  private readonly urlOptions?;
@@ -60,6 +61,7 @@ export declare class RedisStorageAdapter extends BaseStorageAdapter {
60
61
  delete(key: string): Promise<boolean>;
61
62
  deleteIfEquals(key: string, expectedValue: string): Promise<boolean>;
62
63
  expireIfEquals(key: string, expectedValue: string, ttlSeconds: number): Promise<boolean>;
64
+ setIfEquals(key: string, expectedValue: string, value: string): Promise<boolean>;
63
65
  exists(key: string): Promise<boolean>;
64
66
  mget(keys: string[]): Promise<(string | null)[]>;
65
67
  mset(entries: import('../types').SetEntry[]): Promise<void>;
@@ -73,6 +75,7 @@ export declare class RedisStorageAdapter extends BaseStorageAdapter {
73
75
  supportsPubSub(): boolean;
74
76
  publish(channel: string, message: string): Promise<number>;
75
77
  subscribe(channel: string, handler: MessageHandler): Promise<Unsubscribe>;
78
+ onPubSubReconnect(listener: () => void): () => void;
76
79
  /**
77
80
  * Build Redis options from config.
78
81
  */
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Change feed: named topics whose listeners learn that something changed, in this process and in the
3
+ * other processes that share the storage. A change carries no payload: a listener reads the current state.
4
+ *
5
+ * The backend follows what the storage can do:
6
+ * - `pubsub` (Redis, Upstash, memory): a publish is a message on the topic's channel; when a lost Redis connection
7
+ * comes back, every followed topic is told, since its messages were missed meanwhile;
8
+ * - `poll` (SQLite): a publish increments the topic's counter, and each process polls `PRAGMA data_version`,
9
+ * reading the counters of its topics only when another process wrote the file;
10
+ * - `local` (no pub/sub and no data version): this process only.
11
+ */
12
+ import type { NamespacedStorage } from './types';
13
+ export type ChangeFeedKind = 'pubsub' | 'poll' | 'local';
14
+ export interface ChangeFeedOptions {
15
+ /** How often a `poll` feed checks whether another process wrote, in milliseconds. @default 250 */
16
+ pollIntervalMs?: number;
17
+ /** Changes of one topic within this window reach its listeners once, in milliseconds. @default 50 */
18
+ coalesceMs?: number;
19
+ /** Told of a failed poll or subscription, once until it works again, and of a listener that threw. */
20
+ onError?: (error: unknown) => void;
21
+ }
22
+ export interface ChangeFeed {
23
+ readonly kind: ChangeFeedKind;
24
+ /** Tell the topic's listeners, here and in the other processes, that it changed. */
25
+ publish(topic: string): Promise<void>;
26
+ /**
27
+ * Call `listener` when `topic` changes, and when it may have changed unheard (after a pub/sub reconnect); returns
28
+ * the function that stops it.
29
+ */
30
+ subscribe(topic: string, listener: () => void): () => void;
31
+ /** Stop every listener and the polling; the storage stays connected. */
32
+ close(): Promise<void>;
33
+ }
34
+ export declare const DEFAULT_CHANGE_FEED_POLL_INTERVAL_MS = 250;
35
+ export declare const DEFAULT_CHANGE_FEED_COALESCE_MS = 50;
36
+ /**
37
+ * A change feed over `storage`, with the backend its adapter supports (see the module comment).
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const storage = await createStorage({ type: 'sqlite', sqlite: { path: '/var/lib/app/state.sqlite' } });
42
+ * const feed = createChangeFeed(storage);
43
+ * const stop = feed.subscribe('board', () => console.log('board changed'));
44
+ * await feed.publish('board');
45
+ * ```
46
+ */
47
+ export declare function createChangeFeed(storage: NamespacedStorage, options?: ChangeFeedOptions): ChangeFeed;
@@ -22,6 +22,12 @@ import type { RootStorage, StorageConfig, StorageType } from './types';
22
22
  * await store.connect();
23
23
  * ```
24
24
  *
25
+ * @example A SQLite file shared by the processes on one machine (needs `@frontmcp/storage-sqlite`)
26
+ * ```typescript
27
+ * const store = await createStorage({ type: 'sqlite', sqlite: { path: '/var/lib/my-app/state.sqlite' } });
28
+ * await store.incr('runs');
29
+ * ```
30
+ *
25
31
  * @example With namespaces
26
32
  * ```typescript
27
33
  * const store = await createStorage({ type: 'memory' });