@routedock/routedock 0.3.0 → 0.4.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 (58) hide show
  1. package/README.md +113 -12
  2. package/dist/{MppSessionHandler-1EL8QBAn.d.cts → MppSessionHandler-3QfCHI5x.d.ts} +40 -2
  3. package/dist/{MppSessionHandler-Bq4S-JTE.d.ts → MppSessionHandler-xu4Kxu6N.d.cts} +40 -2
  4. package/dist/{ProviderRegistry-CA_TdREG.d.ts → ProviderRegistry-D9KqyuH-.d.cts} +16 -36
  5. package/dist/{ProviderRegistry-D-0rqK0N.d.cts → ProviderRegistry-DVi2uzgk.d.ts} +16 -36
  6. package/dist/{RouteDockClient-go9aopfF.d.cts → RouteDockClient-6rmgkbYb.d.cts} +124 -142
  7. package/dist/{RouteDockClient-Bh86bVly.d.ts → RouteDockClient-CnAkxwrW.d.ts} +124 -142
  8. package/dist/{SessionReconciler-Bw-PoBVl.d.ts → SessionReconciler-Cce9MXO1.d.ts} +10 -3
  9. package/dist/{SessionReconciler-Cz2nuOlJ.d.cts → SessionReconciler-Dpe9qXdD.d.cts} +10 -3
  10. package/dist/{SessionStore-BaYiegP0.d.ts → SessionStore-DkLBxFQu.d.cts} +28 -3
  11. package/dist/{SessionStore-B4-Hi4H8.d.cts → SessionStore-_ZkoBzFY.d.ts} +28 -3
  12. package/dist/SpendStore-BUclV02i.d.ts +73 -0
  13. package/dist/SpendStore-s_dfcemj.d.cts +73 -0
  14. package/dist/client.cjs +767 -487
  15. package/dist/client.d.cts +80 -10
  16. package/dist/client.d.ts +80 -10
  17. package/dist/client.js +733 -468
  18. package/dist/index.cjs +1295 -590
  19. package/dist/index.d.cts +13 -10
  20. package/dist/index.d.ts +13 -10
  21. package/dist/index.js +1247 -559
  22. package/dist/logger-DVa7VT4c.d.cts +41 -0
  23. package/dist/logger-DVa7VT4c.d.ts +41 -0
  24. package/dist/network.cjs +57 -0
  25. package/dist/network.d.cts +72 -0
  26. package/dist/network.d.ts +72 -0
  27. package/dist/network.js +49 -0
  28. package/dist/node.cjs +166 -0
  29. package/dist/node.d.cts +77 -0
  30. package/dist/node.d.ts +77 -0
  31. package/dist/node.js +159 -0
  32. package/dist/provider/fastify.cjs +847 -62
  33. package/dist/provider/fastify.d.cts +10 -3
  34. package/dist/provider/fastify.d.ts +10 -3
  35. package/dist/provider/fastify.js +848 -63
  36. package/dist/provider/hono.cjs +936 -60
  37. package/dist/provider/hono.d.cts +23 -8
  38. package/dist/provider/hono.d.ts +23 -8
  39. package/dist/provider/hono.js +937 -61
  40. package/dist/provider.cjs +894 -70
  41. package/dist/provider.d.cts +9 -8
  42. package/dist/provider.d.ts +9 -8
  43. package/dist/provider.js +895 -72
  44. package/dist/react.cjs +736 -436
  45. package/dist/react.d.cts +7 -5
  46. package/dist/react.d.ts +7 -5
  47. package/dist/react.js +725 -425
  48. package/dist/{routedockMiddleware-57gUaEMf.d.ts → routedockMiddleware-Cq9HOJEa.d.ts} +9 -2
  49. package/dist/{routedockMiddleware-DlyLrxp7.d.cts → routedockMiddleware-CxsWkSKa.d.cts} +9 -2
  50. package/dist/schema.cjs +7 -0
  51. package/dist/schema.js +7 -0
  52. package/dist/{sign-AKH67sft.d.cts → sign-Bq08SeFq.d.ts} +2 -2
  53. package/dist/{sign-BEq-OsSq.d.ts → sign-DqGE0Plv.d.cts} +2 -2
  54. package/dist/testing.d.cts +4 -3
  55. package/dist/testing.d.ts +4 -3
  56. package/dist/{logger-D8_Q2ytp.d.cts → types-DXaS5Wip.d.cts} +85 -47
  57. package/dist/{logger-D8_Q2ytp.d.ts → types-DXaS5Wip.d.ts} +85 -47
  58. package/package.json +50 -4
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Structured logging hook shared by the client and provider adapters.
3
+ *
4
+ * The SDK used to write straight to `console` in a couple of dozen error
5
+ * paths, which is unusable on structured-logging runtimes (Cloudflare Workers,
6
+ * Datadog, …) and untestable without spying on `console`. Every internal
7
+ * diagnostic now goes through a {@link RouteDockLogger}, so a consumer can
8
+ * silence it, redirect it, or attach correlation fields.
9
+ *
10
+ * The default implementation is console-backed, so behaviour is unchanged when
11
+ * no logger is supplied.
12
+ */
13
+ /** Severity of a log line. */
14
+ type RouteDockLogLevel = 'debug' | 'info' | 'warn' | 'error';
15
+ /**
16
+ * Structured context attached to a log line, typically `{ error: err }`. Sinks
17
+ * receive the values unresolved, so a serialising sink must project non-JSON
18
+ * values such as `Error` itself.
19
+ */
20
+ type RouteDockLogFields = Record<string, unknown>;
21
+ /**
22
+ * Consumer-supplied log sink.
23
+ *
24
+ * `fields` carries the structured payload (the error, the channel id, …) that
25
+ * a bare string would drop at the boundary.
26
+ */
27
+ type RouteDockLogger = (level: RouteDockLogLevel, message: string, fields?: RouteDockLogFields) => void;
28
+ /**
29
+ * Default logger that forwards to the console at the matching level. Field
30
+ * values are spread as additional console arguments so `console.error(msg, err)`
31
+ * spies (and existing log shippers) keep seeing the raw error object.
32
+ */
33
+ declare function createConsoleLogger(): RouteDockLogger;
34
+ /** Shared console-backed logger — the default when none is configured. */
35
+ declare const consoleLogger: RouteDockLogger;
36
+ /** Logger that discards every line. */
37
+ declare const noopLogger: RouteDockLogger;
38
+ /** Resolve an optional logger to a concrete sink, defaulting to `fallback`. */
39
+ declare function resolveLogger(logger: RouteDockLogger | undefined, fallback?: RouteDockLogger): RouteDockLogger;
40
+
41
+ export { type RouteDockLogFields as R, type RouteDockLogLevel as a, type RouteDockLogger as b, consoleLogger as c, createConsoleLogger as d, noopLogger as n, resolveLogger as r };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Structured logging hook shared by the client and provider adapters.
3
+ *
4
+ * The SDK used to write straight to `console` in a couple of dozen error
5
+ * paths, which is unusable on structured-logging runtimes (Cloudflare Workers,
6
+ * Datadog, …) and untestable without spying on `console`. Every internal
7
+ * diagnostic now goes through a {@link RouteDockLogger}, so a consumer can
8
+ * silence it, redirect it, or attach correlation fields.
9
+ *
10
+ * The default implementation is console-backed, so behaviour is unchanged when
11
+ * no logger is supplied.
12
+ */
13
+ /** Severity of a log line. */
14
+ type RouteDockLogLevel = 'debug' | 'info' | 'warn' | 'error';
15
+ /**
16
+ * Structured context attached to a log line, typically `{ error: err }`. Sinks
17
+ * receive the values unresolved, so a serialising sink must project non-JSON
18
+ * values such as `Error` itself.
19
+ */
20
+ type RouteDockLogFields = Record<string, unknown>;
21
+ /**
22
+ * Consumer-supplied log sink.
23
+ *
24
+ * `fields` carries the structured payload (the error, the channel id, …) that
25
+ * a bare string would drop at the boundary.
26
+ */
27
+ type RouteDockLogger = (level: RouteDockLogLevel, message: string, fields?: RouteDockLogFields) => void;
28
+ /**
29
+ * Default logger that forwards to the console at the matching level. Field
30
+ * values are spread as additional console arguments so `console.error(msg, err)`
31
+ * spies (and existing log shippers) keep seeing the raw error object.
32
+ */
33
+ declare function createConsoleLogger(): RouteDockLogger;
34
+ /** Shared console-backed logger — the default when none is configured. */
35
+ declare const consoleLogger: RouteDockLogger;
36
+ /** Logger that discards every line. */
37
+ declare const noopLogger: RouteDockLogger;
38
+ /** Resolve an optional logger to a concrete sink, defaulting to `fallback`. */
39
+ declare function resolveLogger(logger: RouteDockLogger | undefined, fallback?: RouteDockLogger): RouteDockLogger;
40
+
41
+ export { type RouteDockLogFields as R, type RouteDockLogLevel as a, type RouteDockLogger as b, consoleLogger as c, createConsoleLogger as d, noopLogger as n, resolveLogger as r };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ // src/network.ts
4
+ var USDC_ISSUERS = {
5
+ testnet: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
6
+ mainnet: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
7
+ };
8
+ var TESTNET_USDC_CONTRACT = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
9
+ var HORIZON_URLS = {
10
+ testnet: "https://horizon-testnet.stellar.org",
11
+ mainnet: "https://horizon.stellar.org"
12
+ };
13
+ function resolveNetwork(raw) {
14
+ return raw === "mainnet" ? "mainnet" : "testnet";
15
+ }
16
+ function resolveAssetContract(env, network) {
17
+ if (env.USDC_ASSET_CONTRACT) return env.USDC_ASSET_CONTRACT;
18
+ if (network === "testnet") return TESTNET_USDC_CONTRACT;
19
+ throw new Error("USDC_ASSET_CONTRACT is required for mainnet");
20
+ }
21
+ function findStellarConfigProblems(env, network) {
22
+ const problems = [];
23
+ if (!env.STELLAR_PAYEE_SECRET || !env.STELLAR_PAYEE_SECRET.startsWith("S")) {
24
+ problems.push("STELLAR_PAYEE_SECRET");
25
+ }
26
+ if (!env.STELLAR_PAYEE_ADDRESS) {
27
+ problems.push("STELLAR_PAYEE_ADDRESS");
28
+ }
29
+ if (network === "mainnet" && !env.USDC_ASSET_CONTRACT) {
30
+ problems.push("USDC_ASSET_CONTRACT");
31
+ }
32
+ return problems;
33
+ }
34
+ async function fetchXlmUsdcOrderBook(network, limit) {
35
+ const params = new URLSearchParams({
36
+ selling_asset_type: "native",
37
+ buying_asset_type: "credit_alphanum4",
38
+ buying_asset_code: "USDC",
39
+ buying_asset_issuer: USDC_ISSUERS[network],
40
+ limit: String(limit)
41
+ });
42
+ const response = await fetch(`${HORIZON_URLS[network]}/order_book?${params.toString()}`, {
43
+ headers: { accept: "application/json" }
44
+ });
45
+ if (!response.ok) {
46
+ throw new Error(`Horizon responded ${response.status}`);
47
+ }
48
+ return await response.json();
49
+ }
50
+
51
+ exports.HORIZON_URLS = HORIZON_URLS;
52
+ exports.TESTNET_USDC_CONTRACT = TESTNET_USDC_CONTRACT;
53
+ exports.USDC_ISSUERS = USDC_ISSUERS;
54
+ exports.fetchXlmUsdcOrderBook = fetchXlmUsdcOrderBook;
55
+ exports.findStellarConfigProblems = findStellarConfigProblems;
56
+ exports.resolveAssetContract = resolveAssetContract;
57
+ exports.resolveNetwork = resolveNetwork;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Canonical Stellar network constants and the small helpers shared by the
3
+ * client, the provider middleware and the provider apps.
4
+ *
5
+ * Kept in its own entry point (`@routedock/routedock/network`) so Workers
6
+ * bundles can import it without pulling in the whole client or provider
7
+ * surface: plain constants plus one fetch-based helper, no Node APIs, no
8
+ * crypto. Before this module existed each app kept its own copy of the USDC
9
+ * issuer table, the testnet SAC and the Horizon URLs, and a mainnet change
10
+ * had to be made in three places (issue #578).
11
+ */
12
+ /** Canonical Circle USDC issuers keyed by Stellar network. */
13
+ declare const USDC_ISSUERS: {
14
+ readonly testnet: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
15
+ readonly mainnet: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
16
+ };
17
+ type Network = keyof typeof USDC_ISSUERS;
18
+ /** Well-known USDC stablecoin asset contract on testnet. */
19
+ declare const TESTNET_USDC_CONTRACT = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
20
+ /** Horizon REST endpoints keyed by Stellar network. */
21
+ declare const HORIZON_URLS: {
22
+ readonly testnet: "https://horizon-testnet.stellar.org";
23
+ readonly mainnet: "https://horizon.stellar.org";
24
+ };
25
+ /**
26
+ * Normalize a STELLAR_NETWORK-style string to a {@link Network}.
27
+ * Anything other than the exact string 'mainnet' falls back to testnet, the
28
+ * safe default.
29
+ */
30
+ declare function resolveNetwork(raw: string | undefined): Network;
31
+ /**
32
+ * Resolve the USDC asset contract for `network`: an explicit
33
+ * `USDC_ASSET_CONTRACT` always wins, testnet falls back to the well-known
34
+ * testnet SAC, and mainnet without one configured is a hard failure rather
35
+ * than a silently empty asset contract in the manifest.
36
+ */
37
+ declare function resolveAssetContract(env: {
38
+ USDC_ASSET_CONTRACT?: string | undefined;
39
+ }, network: Network): string;
40
+ /** The env fields the shared config check understands. */
41
+ interface StellarConfigEnv {
42
+ STELLAR_PAYEE_SECRET?: string | undefined;
43
+ STELLAR_PAYEE_ADDRESS?: string | undefined;
44
+ USDC_ASSET_CONTRACT?: string | undefined;
45
+ }
46
+ /**
47
+ * Return the names of environment variables that are unset or malformed.
48
+ * Values are never included — only names, so this is safe to log or return
49
+ * to a caller directly. Providers append their own extra requirements
50
+ * (channel contract, commitment key) to this shared base list.
51
+ */
52
+ declare function findStellarConfigProblems(env: StellarConfigEnv, network: Network): string[];
53
+ interface OrderBookLevel {
54
+ price: string;
55
+ amount: string;
56
+ }
57
+ interface XlmUsdcOrderBook {
58
+ asks: OrderBookLevel[];
59
+ bids: OrderBookLevel[];
60
+ }
61
+ /**
62
+ * Fetch the XLM/USDC orderbook straight from Horizon's REST API.
63
+ *
64
+ * The provider Express build used `Horizon.Server(...).orderbook(...)`, which
65
+ * pulls the whole `@stellar/stellar-sdk` (and axios) into the bundle for what
66
+ * is a single GET. Calling the endpoint keeps Workers bundles small and drops
67
+ * the dependency, while the issuer and Horizon URL come from the canonical
68
+ * tables above.
69
+ */
70
+ declare function fetchXlmUsdcOrderBook(network: Network, limit: number): Promise<XlmUsdcOrderBook>;
71
+
72
+ export { HORIZON_URLS, type Network, type OrderBookLevel, type StellarConfigEnv, TESTNET_USDC_CONTRACT, USDC_ISSUERS, type XlmUsdcOrderBook, fetchXlmUsdcOrderBook, findStellarConfigProblems, resolveAssetContract, resolveNetwork };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Canonical Stellar network constants and the small helpers shared by the
3
+ * client, the provider middleware and the provider apps.
4
+ *
5
+ * Kept in its own entry point (`@routedock/routedock/network`) so Workers
6
+ * bundles can import it without pulling in the whole client or provider
7
+ * surface: plain constants plus one fetch-based helper, no Node APIs, no
8
+ * crypto. Before this module existed each app kept its own copy of the USDC
9
+ * issuer table, the testnet SAC and the Horizon URLs, and a mainnet change
10
+ * had to be made in three places (issue #578).
11
+ */
12
+ /** Canonical Circle USDC issuers keyed by Stellar network. */
13
+ declare const USDC_ISSUERS: {
14
+ readonly testnet: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
15
+ readonly mainnet: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
16
+ };
17
+ type Network = keyof typeof USDC_ISSUERS;
18
+ /** Well-known USDC stablecoin asset contract on testnet. */
19
+ declare const TESTNET_USDC_CONTRACT = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
20
+ /** Horizon REST endpoints keyed by Stellar network. */
21
+ declare const HORIZON_URLS: {
22
+ readonly testnet: "https://horizon-testnet.stellar.org";
23
+ readonly mainnet: "https://horizon.stellar.org";
24
+ };
25
+ /**
26
+ * Normalize a STELLAR_NETWORK-style string to a {@link Network}.
27
+ * Anything other than the exact string 'mainnet' falls back to testnet, the
28
+ * safe default.
29
+ */
30
+ declare function resolveNetwork(raw: string | undefined): Network;
31
+ /**
32
+ * Resolve the USDC asset contract for `network`: an explicit
33
+ * `USDC_ASSET_CONTRACT` always wins, testnet falls back to the well-known
34
+ * testnet SAC, and mainnet without one configured is a hard failure rather
35
+ * than a silently empty asset contract in the manifest.
36
+ */
37
+ declare function resolveAssetContract(env: {
38
+ USDC_ASSET_CONTRACT?: string | undefined;
39
+ }, network: Network): string;
40
+ /** The env fields the shared config check understands. */
41
+ interface StellarConfigEnv {
42
+ STELLAR_PAYEE_SECRET?: string | undefined;
43
+ STELLAR_PAYEE_ADDRESS?: string | undefined;
44
+ USDC_ASSET_CONTRACT?: string | undefined;
45
+ }
46
+ /**
47
+ * Return the names of environment variables that are unset or malformed.
48
+ * Values are never included — only names, so this is safe to log or return
49
+ * to a caller directly. Providers append their own extra requirements
50
+ * (channel contract, commitment key) to this shared base list.
51
+ */
52
+ declare function findStellarConfigProblems(env: StellarConfigEnv, network: Network): string[];
53
+ interface OrderBookLevel {
54
+ price: string;
55
+ amount: string;
56
+ }
57
+ interface XlmUsdcOrderBook {
58
+ asks: OrderBookLevel[];
59
+ bids: OrderBookLevel[];
60
+ }
61
+ /**
62
+ * Fetch the XLM/USDC orderbook straight from Horizon's REST API.
63
+ *
64
+ * The provider Express build used `Horizon.Server(...).orderbook(...)`, which
65
+ * pulls the whole `@stellar/stellar-sdk` (and axios) into the bundle for what
66
+ * is a single GET. Calling the endpoint keeps Workers bundles small and drops
67
+ * the dependency, while the issuer and Horizon URL come from the canonical
68
+ * tables above.
69
+ */
70
+ declare function fetchXlmUsdcOrderBook(network: Network, limit: number): Promise<XlmUsdcOrderBook>;
71
+
72
+ export { HORIZON_URLS, type Network, type OrderBookLevel, type StellarConfigEnv, TESTNET_USDC_CONTRACT, USDC_ISSUERS, type XlmUsdcOrderBook, fetchXlmUsdcOrderBook, findStellarConfigProblems, resolveAssetContract, resolveNetwork };
@@ -0,0 +1,49 @@
1
+ // src/network.ts
2
+ var USDC_ISSUERS = {
3
+ testnet: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
4
+ mainnet: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
5
+ };
6
+ var TESTNET_USDC_CONTRACT = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
7
+ var HORIZON_URLS = {
8
+ testnet: "https://horizon-testnet.stellar.org",
9
+ mainnet: "https://horizon.stellar.org"
10
+ };
11
+ function resolveNetwork(raw) {
12
+ return raw === "mainnet" ? "mainnet" : "testnet";
13
+ }
14
+ function resolveAssetContract(env, network) {
15
+ if (env.USDC_ASSET_CONTRACT) return env.USDC_ASSET_CONTRACT;
16
+ if (network === "testnet") return TESTNET_USDC_CONTRACT;
17
+ throw new Error("USDC_ASSET_CONTRACT is required for mainnet");
18
+ }
19
+ function findStellarConfigProblems(env, network) {
20
+ const problems = [];
21
+ if (!env.STELLAR_PAYEE_SECRET || !env.STELLAR_PAYEE_SECRET.startsWith("S")) {
22
+ problems.push("STELLAR_PAYEE_SECRET");
23
+ }
24
+ if (!env.STELLAR_PAYEE_ADDRESS) {
25
+ problems.push("STELLAR_PAYEE_ADDRESS");
26
+ }
27
+ if (network === "mainnet" && !env.USDC_ASSET_CONTRACT) {
28
+ problems.push("USDC_ASSET_CONTRACT");
29
+ }
30
+ return problems;
31
+ }
32
+ async function fetchXlmUsdcOrderBook(network, limit) {
33
+ const params = new URLSearchParams({
34
+ selling_asset_type: "native",
35
+ buying_asset_type: "credit_alphanum4",
36
+ buying_asset_code: "USDC",
37
+ buying_asset_issuer: USDC_ISSUERS[network],
38
+ limit: String(limit)
39
+ });
40
+ const response = await fetch(`${HORIZON_URLS[network]}/order_book?${params.toString()}`, {
41
+ headers: { accept: "application/json" }
42
+ });
43
+ if (!response.ok) {
44
+ throw new Error(`Horizon responded ${response.status}`);
45
+ }
46
+ return await response.json();
47
+ }
48
+
49
+ export { HORIZON_URLS, TESTNET_USDC_CONTRACT, USDC_ISSUERS, fetchXlmUsdcOrderBook, findStellarConfigProblems, resolveAssetContract, resolveNetwork };
package/dist/node.cjs ADDED
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var fs = require('fs/promises');
5
+ var path = require('path');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var fs__default = /*#__PURE__*/_interopDefault(fs);
10
+ var path__default = /*#__PURE__*/_interopDefault(path);
11
+
12
+ // src/store/FileSpendStore.ts
13
+ var DEFAULT_LOCK_STALE_MS = 1e4;
14
+ var DEFAULT_LOCK_TIMEOUT_MS = 15e3;
15
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
+ var FileSpendStore = class {
17
+ filePath;
18
+ lockPath;
19
+ lockStaleMs;
20
+ lockTimeoutMs;
21
+ constructor(filePath, options = {}) {
22
+ this.filePath = filePath;
23
+ this.lockPath = `${filePath}.lock`;
24
+ this.lockStaleMs = options.lockStaleMs ?? DEFAULT_LOCK_STALE_MS;
25
+ this.lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
26
+ }
27
+ async read() {
28
+ let data;
29
+ try {
30
+ data = await fs__default.default.readFile(this.filePath, "utf-8");
31
+ } catch (err) {
32
+ if (err.code === "ENOENT") {
33
+ return null;
34
+ }
35
+ throw new Error(
36
+ `[FileSpendStore] Failed to read spend file at ${this.filePath}: ${err.message}`
37
+ );
38
+ }
39
+ try {
40
+ return JSON.parse(data);
41
+ } catch {
42
+ throw new Error(
43
+ `[FileSpendStore] Spend file at ${this.filePath} contains invalid JSON. Delete or repair it before resuming. Raw content: ${data.slice(0, 120)}`
44
+ );
45
+ }
46
+ }
47
+ async write(state) {
48
+ await this.withLock(async (assertHeld) => {
49
+ await this.writeFile(state, assertHeld);
50
+ });
51
+ }
52
+ /**
53
+ * Atomically read-modify-write the accumulator under the cross-process lock.
54
+ * `fn` receives the current state (or null) and returns the state to persist,
55
+ * or `undefined` to leave the file untouched. If `fn` throws, nothing is
56
+ * written and the lock is released.
57
+ */
58
+ async update(fn) {
59
+ return this.withLock(async (assertHeld) => {
60
+ const prev = await this.read();
61
+ const next = fn(prev);
62
+ if (next === void 0) return prev;
63
+ await this.writeFile(next, assertHeld);
64
+ return next;
65
+ });
66
+ }
67
+ // ── internals ──────────────────────────────────────────────────────────────
68
+ async writeFile(state, assertHeld) {
69
+ const tmpPath = `${this.filePath}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`;
70
+ try {
71
+ await fs__default.default.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
72
+ await assertHeld();
73
+ await fs__default.default.rename(tmpPath, this.filePath);
74
+ } catch (err) {
75
+ await fs__default.default.rm(tmpPath, { force: true }).catch(() => {
76
+ });
77
+ throw err;
78
+ }
79
+ }
80
+ /** Run `fn` while holding the lock file; always releases it. */
81
+ async withLock(fn) {
82
+ await fs__default.default.mkdir(path__default.default.dirname(this.filePath), { recursive: true });
83
+ const token = `${process.pid}:${crypto.randomBytes(8).toString("hex")}`;
84
+ await this.acquireLock(token);
85
+ const heartbeat = setInterval(() => {
86
+ const now = /* @__PURE__ */ new Date();
87
+ fs__default.default.utimes(this.lockPath, now, now).catch(() => {
88
+ });
89
+ }, Math.max(10, Math.floor(this.lockStaleMs / 3)));
90
+ heartbeat.unref();
91
+ const assertHeld = async () => {
92
+ let current;
93
+ try {
94
+ current = await fs__default.default.readFile(this.lockPath, "utf-8");
95
+ } catch {
96
+ current = "";
97
+ }
98
+ if (current !== token) {
99
+ throw new Error(
100
+ `[FileSpendStore] Lost the lock on ${this.lockPath} before committing; refusing to overwrite the spend file. Retry the operation.`
101
+ );
102
+ }
103
+ };
104
+ try {
105
+ return await fn(assertHeld);
106
+ } finally {
107
+ clearInterval(heartbeat);
108
+ await this.releaseLock(token);
109
+ }
110
+ }
111
+ async acquireLock(token) {
112
+ const deadline = Date.now() + this.lockTimeoutMs;
113
+ let delay = 5;
114
+ for (; ; ) {
115
+ try {
116
+ const handle = await fs__default.default.open(this.lockPath, "wx");
117
+ try {
118
+ await handle.writeFile(token, "utf-8");
119
+ } finally {
120
+ await handle.close();
121
+ }
122
+ return;
123
+ } catch (err) {
124
+ if (err.code !== "EEXIST") {
125
+ throw new Error(
126
+ `[FileSpendStore] Failed to create lock file ${this.lockPath}: ${err.message}`
127
+ );
128
+ }
129
+ }
130
+ await this.breakIfStale();
131
+ if (Date.now() >= deadline) {
132
+ throw new Error(
133
+ `[FileSpendStore] Timed out after ${this.lockTimeoutMs}ms waiting for ${this.lockPath}. Another process is holding the spend file lock; if none is running, delete the lock file.`
134
+ );
135
+ }
136
+ await sleep(delay + Math.floor(Math.random() * delay));
137
+ delay = Math.min(delay * 2, 50);
138
+ }
139
+ }
140
+ /** Remove the lock if its holder stopped refreshing it (crashed process). */
141
+ async breakIfStale() {
142
+ let before;
143
+ try {
144
+ before = await fs__default.default.stat(this.lockPath);
145
+ } catch {
146
+ return;
147
+ }
148
+ if (Date.now() - before.mtimeMs <= this.lockStaleMs) return;
149
+ try {
150
+ const after = await fs__default.default.stat(this.lockPath);
151
+ if (after.ino !== before.ino || after.mtimeMs !== before.mtimeMs) return;
152
+ await fs__default.default.unlink(this.lockPath);
153
+ } catch {
154
+ }
155
+ }
156
+ async releaseLock(token) {
157
+ try {
158
+ if (await fs__default.default.readFile(this.lockPath, "utf-8") === token) {
159
+ await fs__default.default.unlink(this.lockPath);
160
+ }
161
+ } catch {
162
+ }
163
+ }
164
+ };
165
+
166
+ exports.FileSpendStore = FileSpendStore;
@@ -0,0 +1,77 @@
1
+ import { S as SpendStore, D as DailySpend } from './SpendStore-s_dfcemj.cjs';
2
+ import './logger-DVa7VT4c.cjs';
3
+
4
+ /** Tuning for the cross-process lock that guards read-modify-write. */
5
+ interface FileSpendStoreOptions {
6
+ /**
7
+ * A lock file whose mtime is older than this is treated as left behind by a
8
+ * crashed process and broken. A live holder refreshes it, so this only needs
9
+ * to exceed the refresh interval. Default 10 000 ms.
10
+ */
11
+ lockStaleMs?: number;
12
+ /**
13
+ * How long to wait for the lock before giving up. A spend cap must fail
14
+ * closed, so a timeout rejects instead of proceeding unlocked. Must be longer
15
+ * than `lockStaleMs` to survive a crashed holder. Default 15 000 ms.
16
+ */
17
+ lockTimeoutMs?: number;
18
+ }
19
+ /**
20
+ * File-backed durable {@link SpendStore}.
21
+ *
22
+ * Persists the daily spend accumulator to a JSON file on disk, preserving
23
+ * cumulative spending totals across process restarts, crashes, and redeploys.
24
+ *
25
+ * ### Error semantics
26
+ * - File absent (ENOENT): returns null — legitimately no spend recorded yet.
27
+ * - File present but unreadable or malformed: throws — a safety control must
28
+ * fail closed, not assume nothing was spent.
29
+ *
30
+ * ### Write atomicity
31
+ * Writes go to a tmp file that is unique per write (`<filePath>.<pid>.<random>.tmp`),
32
+ * then `fs.rename` onto the target. `rename` is atomic within a filesystem, so
33
+ * there is no window where the target file is truncated but not yet fully
34
+ * written, and two writers can never share (and clobber, or `ENOENT` on) one
35
+ * tmp file.
36
+ *
37
+ * ### Sharing the file between processes
38
+ * `read()` + `write()` alone is not enough when several processes use the same
39
+ * file (the MCP server and the E2E agent both default to
40
+ * `~/.routedock/spend.json`): both can read the same total and both write back
41
+ * `total + amount`. {@link FileSpendStore.update} closes that race — it holds an
42
+ * exclusive lock file (`<filePath>.lock`, created with `O_EXCL`) across the
43
+ * read, the callback and the write, and `RouteDockClient` uses it automatically.
44
+ * `write()` takes the same lock, so every mutation is serialized.
45
+ *
46
+ * The lock is advisory and file-based: it works for processes on one machine
47
+ * (and on filesystems with working `O_EXCL`), not for a file on a flaky network
48
+ * share. A lock whose mtime goes stale (crashed holder) is broken after
49
+ * `lockStaleMs`; a live holder keeps refreshing it. If the lock cannot be
50
+ * acquired within `lockTimeoutMs`, the call rejects — it never proceeds
51
+ * unlocked.
52
+ */
53
+ declare class FileSpendStore implements SpendStore {
54
+ readonly filePath: string;
55
+ private readonly lockPath;
56
+ private readonly lockStaleMs;
57
+ private readonly lockTimeoutMs;
58
+ constructor(filePath: string, options?: FileSpendStoreOptions);
59
+ read(): Promise<DailySpend | null>;
60
+ write(state: DailySpend): Promise<void>;
61
+ /**
62
+ * Atomically read-modify-write the accumulator under the cross-process lock.
63
+ * `fn` receives the current state (or null) and returns the state to persist,
64
+ * or `undefined` to leave the file untouched. If `fn` throws, nothing is
65
+ * written and the lock is released.
66
+ */
67
+ update(fn: (prev: DailySpend | null) => DailySpend | undefined): Promise<DailySpend | null>;
68
+ private writeFile;
69
+ /** Run `fn` while holding the lock file; always releases it. */
70
+ private withLock;
71
+ private acquireLock;
72
+ /** Remove the lock if its holder stopped refreshing it (crashed process). */
73
+ private breakIfStale;
74
+ private releaseLock;
75
+ }
76
+
77
+ export { FileSpendStore, type FileSpendStoreOptions };
package/dist/node.d.ts ADDED
@@ -0,0 +1,77 @@
1
+ import { S as SpendStore, D as DailySpend } from './SpendStore-BUclV02i.js';
2
+ import './logger-DVa7VT4c.js';
3
+
4
+ /** Tuning for the cross-process lock that guards read-modify-write. */
5
+ interface FileSpendStoreOptions {
6
+ /**
7
+ * A lock file whose mtime is older than this is treated as left behind by a
8
+ * crashed process and broken. A live holder refreshes it, so this only needs
9
+ * to exceed the refresh interval. Default 10 000 ms.
10
+ */
11
+ lockStaleMs?: number;
12
+ /**
13
+ * How long to wait for the lock before giving up. A spend cap must fail
14
+ * closed, so a timeout rejects instead of proceeding unlocked. Must be longer
15
+ * than `lockStaleMs` to survive a crashed holder. Default 15 000 ms.
16
+ */
17
+ lockTimeoutMs?: number;
18
+ }
19
+ /**
20
+ * File-backed durable {@link SpendStore}.
21
+ *
22
+ * Persists the daily spend accumulator to a JSON file on disk, preserving
23
+ * cumulative spending totals across process restarts, crashes, and redeploys.
24
+ *
25
+ * ### Error semantics
26
+ * - File absent (ENOENT): returns null — legitimately no spend recorded yet.
27
+ * - File present but unreadable or malformed: throws — a safety control must
28
+ * fail closed, not assume nothing was spent.
29
+ *
30
+ * ### Write atomicity
31
+ * Writes go to a tmp file that is unique per write (`<filePath>.<pid>.<random>.tmp`),
32
+ * then `fs.rename` onto the target. `rename` is atomic within a filesystem, so
33
+ * there is no window where the target file is truncated but not yet fully
34
+ * written, and two writers can never share (and clobber, or `ENOENT` on) one
35
+ * tmp file.
36
+ *
37
+ * ### Sharing the file between processes
38
+ * `read()` + `write()` alone is not enough when several processes use the same
39
+ * file (the MCP server and the E2E agent both default to
40
+ * `~/.routedock/spend.json`): both can read the same total and both write back
41
+ * `total + amount`. {@link FileSpendStore.update} closes that race — it holds an
42
+ * exclusive lock file (`<filePath>.lock`, created with `O_EXCL`) across the
43
+ * read, the callback and the write, and `RouteDockClient` uses it automatically.
44
+ * `write()` takes the same lock, so every mutation is serialized.
45
+ *
46
+ * The lock is advisory and file-based: it works for processes on one machine
47
+ * (and on filesystems with working `O_EXCL`), not for a file on a flaky network
48
+ * share. A lock whose mtime goes stale (crashed holder) is broken after
49
+ * `lockStaleMs`; a live holder keeps refreshing it. If the lock cannot be
50
+ * acquired within `lockTimeoutMs`, the call rejects — it never proceeds
51
+ * unlocked.
52
+ */
53
+ declare class FileSpendStore implements SpendStore {
54
+ readonly filePath: string;
55
+ private readonly lockPath;
56
+ private readonly lockStaleMs;
57
+ private readonly lockTimeoutMs;
58
+ constructor(filePath: string, options?: FileSpendStoreOptions);
59
+ read(): Promise<DailySpend | null>;
60
+ write(state: DailySpend): Promise<void>;
61
+ /**
62
+ * Atomically read-modify-write the accumulator under the cross-process lock.
63
+ * `fn` receives the current state (or null) and returns the state to persist,
64
+ * or `undefined` to leave the file untouched. If `fn` throws, nothing is
65
+ * written and the lock is released.
66
+ */
67
+ update(fn: (prev: DailySpend | null) => DailySpend | undefined): Promise<DailySpend | null>;
68
+ private writeFile;
69
+ /** Run `fn` while holding the lock file; always releases it. */
70
+ private withLock;
71
+ private acquireLock;
72
+ /** Remove the lock if its holder stopped refreshing it (crashed process). */
73
+ private breakIfStale;
74
+ private releaseLock;
75
+ }
76
+
77
+ export { FileSpendStore, type FileSpendStoreOptions };