account-pool-mcp 0.1.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.
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/bootstrap.d.ts +10 -0
- package/dist/bootstrap.js +28 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +126 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +115 -0
- package/dist/config.js.map +1 -0
- package/dist/db.d.ts +13 -0
- package/dist/db.js +83 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.js +44 -0
- package/dist/logger.js.map +1 -0
- package/dist/pool.d.ts +50 -0
- package/dist/pool.js +190 -0
- package/dist/pool.js.map +1 -0
- package/dist/schemas.d.ts +57 -0
- package/dist/schemas.js +43 -0
- package/dist/schemas.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +75 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +77 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/examples/accounts.example.json +21 -0
- package/examples/claude-mcp-config.json +19 -0
- package/examples/playwright-usage.md +48 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ankit Sachdeva
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# account-pool-mcp
|
|
2
|
+
|
|
3
|
+
An MCP server that hands out test accounts to agent sessions one at a time, so two sessions never
|
|
4
|
+
end up logged into the same account.
|
|
5
|
+
|
|
6
|
+
## The problem
|
|
7
|
+
|
|
8
|
+
When you run several agent sessions at once — say a few Claude sessions each driving their own
|
|
9
|
+
Playwright browser — they all need to log in, and left alone they'll grab the same test account and
|
|
10
|
+
step on each other. Two sessions on one account corrupt each other's state and your test results
|
|
11
|
+
become meaningless. Picking a random account doesn't really help either: with 10 accounts and 5
|
|
12
|
+
sessions, a collision is already more likely than not.
|
|
13
|
+
|
|
14
|
+
The fix is to lease accounts. A session checks one out, uses it, and returns it. While it's checked
|
|
15
|
+
out, no one else can be handed it.
|
|
16
|
+
|
|
17
|
+
## How it works
|
|
18
|
+
|
|
19
|
+
The server keeps a pool of accounts in a small SQLite database and gives them out one at a time.
|
|
20
|
+
Allocation happens inside a `BEGIN IMMEDIATE` transaction, so even if several sessions ask at the
|
|
21
|
+
exact same moment, they can't be handed the same account. Each lease has a TTL, so if a session
|
|
22
|
+
crashes without returning its account, it gets reclaimed automatically — there's nothing to clean up.
|
|
23
|
+
|
|
24
|
+
All of this happens in the background. The agent just asks for an account when it needs one; the
|
|
25
|
+
broker decides which one it gets and guarantees no one else has it. There's no shared parent process
|
|
26
|
+
— unrelated sessions coordinate purely through the database file.
|
|
27
|
+
|
|
28
|
+
## Tools
|
|
29
|
+
|
|
30
|
+
- `lease_account(pool, holder?)` — check out an account. Returns the account, its credentials, and a
|
|
31
|
+
`lease_token`. Hold it until you're done.
|
|
32
|
+
- `release_account(lease_token)` — give it back. Idempotent.
|
|
33
|
+
- `renew_lease(lease_token)` — extend the lease if your work runs long (a heartbeat).
|
|
34
|
+
- `pool_status(pool?)` — what's leased vs. free. Never returns credential values.
|
|
35
|
+
|
|
36
|
+
There's also a small `account-pool` CLI (`lease` / `release` / `renew` / `status`) over the same
|
|
37
|
+
database, for scripts and humans.
|
|
38
|
+
|
|
39
|
+
## Setup
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cp examples/accounts.example.json accounts.json # define your pools
|
|
43
|
+
# then register the server with your MCP client — see examples/claude-mcp-config.json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Point every session's `APM_DB_PATH` at the same file — that shared file is how they coordinate.
|
|
47
|
+
|
|
48
|
+
| Env var | Default | What it does |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `APM_ACCOUNTS_FILE` | `./accounts.json` | Pools + accounts to load on startup. |
|
|
51
|
+
| `APM_DB_PATH` | `./account-pool.db` | The SQLite file. Same path for every session. |
|
|
52
|
+
| `APM_DEFAULT_TTL_SECONDS` | `1800` | How long a lease lasts before it's reclaimable. |
|
|
53
|
+
| `APM_LEASE_WAIT_MS` | `0` | `0` = fail fast when the pool is empty; `>0` = wait this long for one to free up. |
|
|
54
|
+
|
|
55
|
+
A credential value can be `{ "env": "VAR_NAME" }` instead of a literal, so real secrets stay in the
|
|
56
|
+
environment and out of the accounts file.
|
|
57
|
+
|
|
58
|
+
## Security
|
|
59
|
+
|
|
60
|
+
These are test accounts, not a secrets vault. Credential values are never logged or returned by
|
|
61
|
+
`pool_status` — a redacting logger masks them, and all logs go to stderr so they can't corrupt the
|
|
62
|
+
MCP stream. Keep `accounts.json` and `*.db` out of git (only the `.example` files are committed). The
|
|
63
|
+
stdio server trusts whoever runs it locally, so don't point it at production credentials.
|
|
64
|
+
|
|
65
|
+
## Limitations
|
|
66
|
+
|
|
67
|
+
Single host for now: coordination is through one SQLite file, so all sessions have to share a
|
|
68
|
+
filesystem. The storage layer is isolated behind one module, so a Postgres or Redis backend could
|
|
69
|
+
swap in later for multi-host coordination without changing the tools.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Db } from './db.js';
|
|
2
|
+
import { AccountPool } from './pool.js';
|
|
3
|
+
import type { AppConfig } from './types.js';
|
|
4
|
+
export interface Booted {
|
|
5
|
+
db: Db;
|
|
6
|
+
pool: AccountPool;
|
|
7
|
+
}
|
|
8
|
+
export declare function bootstrap(config: AppConfig, opts?: {
|
|
9
|
+
quiet?: boolean;
|
|
10
|
+
}): Booted;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Shared startup: open the DB, seed accounts (idempotent), optionally reset leases, build the pool.
|
|
2
|
+
// Used by both the MCP server entry (index.ts) and the CLI face (cli.ts) so they share one DB.
|
|
3
|
+
import { loadSeed } from './config.js';
|
|
4
|
+
import { openDb, resetLeases, seedAccounts } from './db.js';
|
|
5
|
+
import { logger } from './logger.js';
|
|
6
|
+
import { AccountPool } from './pool.js';
|
|
7
|
+
export function bootstrap(config, opts = {}) {
|
|
8
|
+
const db = openDb(config.dbPath);
|
|
9
|
+
if (config.resetLeases) {
|
|
10
|
+
const cleared = resetLeases(db);
|
|
11
|
+
if (!opts.quiet)
|
|
12
|
+
logger.info('reset-leases: cleared lease state', { cleared });
|
|
13
|
+
}
|
|
14
|
+
const seed = loadSeed(config.accountsFile);
|
|
15
|
+
if (seed) {
|
|
16
|
+
const n = seedAccounts(db, seed);
|
|
17
|
+
if (!opts.quiet)
|
|
18
|
+
logger.info('seeded accounts (idempotent upsert)', { count: n });
|
|
19
|
+
}
|
|
20
|
+
else if (!opts.quiet) {
|
|
21
|
+
logger.warn('no accounts file found — pools will be empty', {
|
|
22
|
+
accountsFile: config.accountsFile,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const pool = new AccountPool({ db, defaultTtlSeconds: config.defaultTtlSeconds });
|
|
26
|
+
return { db, pool };
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=bootstrap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,+FAA+F;AAE/F,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAW,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACrE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAQxC,MAAM,UAAU,SAAS,CAAC,MAAiB,EAAE,OAA4B,EAAE;IACzE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAEjC,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;QAChC,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,MAAM,CAAC,IAAI,CAAC,mCAAmC,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IACjF,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAC3C,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QACjC,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,MAAM,CAAC,IAAI,CAAC,qCAAqC,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IACpF,CAAC;SAAM,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QACvB,MAAM,CAAC,IAAI,CAAC,8CAA8C,EAAE;YAC1D,YAAY,EAAE,MAAM,CAAC,YAAY;SAClC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,WAAW,CAAC,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC;IAClF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC"}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// account-pool — CLI face for the same broker. Lets shell scripts and humans lease/release/renew/
|
|
3
|
+
// status against the SAME SQLite database the MCP server uses (no MCP round-trip). This is what an
|
|
4
|
+
// existing auth script shells out to so account assignment becomes an invisible step.
|
|
5
|
+
//
|
|
6
|
+
// account-pool lease <pool> [--holder h] [--ttl secs] [--wait-ms ms] [--json] [--field key]
|
|
7
|
+
// account-pool release <lease_token>
|
|
8
|
+
// account-pool renew <lease_token> [--ttl secs]
|
|
9
|
+
// account-pool status [pool] [--json]
|
|
10
|
+
import { bootstrap } from './bootstrap.js';
|
|
11
|
+
import { loadConfig, parseArgs } from './config.js';
|
|
12
|
+
import { PoolExhaustedError } from './types.js';
|
|
13
|
+
function out(s) {
|
|
14
|
+
process.stdout.write(`${s}\n`);
|
|
15
|
+
}
|
|
16
|
+
async function main() {
|
|
17
|
+
const [, , sub, ...rest] = process.argv;
|
|
18
|
+
// positional args (before any --flag) and flags, parsed separately
|
|
19
|
+
const positionals = rest.filter((a) => !a.startsWith('--'));
|
|
20
|
+
const flags = parseArgs(rest);
|
|
21
|
+
const config = loadConfig(rest);
|
|
22
|
+
const { pool } = bootstrap(config, { quiet: true });
|
|
23
|
+
switch (sub) {
|
|
24
|
+
case 'lease': {
|
|
25
|
+
const poolName = positionals[0];
|
|
26
|
+
if (!poolName) {
|
|
27
|
+
out('usage: account-pool lease <pool> [--holder h] [--ttl secs] [--wait-ms ms] [--json] [--field key]');
|
|
28
|
+
return 2;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const result = await pool.acquire({
|
|
32
|
+
pool: poolName,
|
|
33
|
+
holder: typeof flags.holder === 'string' ? flags.holder : undefined,
|
|
34
|
+
ttlSeconds: typeof flags.ttl === 'string' ? Number.parseInt(flags.ttl, 10) : undefined,
|
|
35
|
+
waitMs: typeof flags['wait-ms'] === 'string'
|
|
36
|
+
? Number.parseInt(flags['wait-ms'], 10)
|
|
37
|
+
: config.leaseWaitMs,
|
|
38
|
+
});
|
|
39
|
+
if (typeof flags.field === 'string') {
|
|
40
|
+
const v = result.credentials[flags.field];
|
|
41
|
+
if (v === undefined) {
|
|
42
|
+
process.stderr.write(`credential field "${flags.field}" not present on ${result.account_id}\n`);
|
|
43
|
+
return 4;
|
|
44
|
+
}
|
|
45
|
+
out(String(v));
|
|
46
|
+
}
|
|
47
|
+
else if (flags.json) {
|
|
48
|
+
out(JSON.stringify(result));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
out(`account_id=${result.account_id}`);
|
|
52
|
+
out(`lease_token=${result.lease_token}`);
|
|
53
|
+
out(`expires_at=${result.expires_at}`);
|
|
54
|
+
for (const [k, v] of Object.entries(result.credentials))
|
|
55
|
+
out(`${k}=${v}`);
|
|
56
|
+
}
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (err instanceof PoolExhaustedError) {
|
|
61
|
+
process.stderr.write(`${err.message}\n`);
|
|
62
|
+
return 3;
|
|
63
|
+
}
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
case 'release': {
|
|
68
|
+
const token = positionals[0];
|
|
69
|
+
if (!token) {
|
|
70
|
+
out('usage: account-pool release <lease_token>');
|
|
71
|
+
return 2;
|
|
72
|
+
}
|
|
73
|
+
const result = pool.release(token);
|
|
74
|
+
out(flags.json
|
|
75
|
+
? JSON.stringify(result)
|
|
76
|
+
: `released=${result.released}${result.account_id ? ` account_id=${result.account_id}` : ''}`);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
case 'renew': {
|
|
80
|
+
const token = positionals[0];
|
|
81
|
+
if (!token) {
|
|
82
|
+
out('usage: account-pool renew <lease_token> [--ttl secs]');
|
|
83
|
+
return 2;
|
|
84
|
+
}
|
|
85
|
+
const result = pool.renew(token, typeof flags.ttl === 'string' ? Number.parseInt(flags.ttl, 10) : undefined);
|
|
86
|
+
out(flags.json
|
|
87
|
+
? JSON.stringify(result)
|
|
88
|
+
: `renewed=${result.renewed}${result.expires_at ? ` expires_at=${result.expires_at}` : ''}`);
|
|
89
|
+
return result.renewed ? 0 : 5;
|
|
90
|
+
}
|
|
91
|
+
case 'status': {
|
|
92
|
+
const pools = pool.status(positionals[0]);
|
|
93
|
+
if (flags.json) {
|
|
94
|
+
out(JSON.stringify(pools, null, 2));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
for (const p of pools) {
|
|
98
|
+
out(`# ${p.pool}: ${p.available}/${p.total} available, ${p.leased} leased`);
|
|
99
|
+
for (const a of p.accounts) {
|
|
100
|
+
const tail = a.state === 'leased'
|
|
101
|
+
? ` held by ${a.holder} until ${new Date((a.expires_at ?? 0) * 1000).toISOString()}`
|
|
102
|
+
: a.state === 'expired'
|
|
103
|
+
? ` (expired; reclaimable — was ${a.holder})`
|
|
104
|
+
: '';
|
|
105
|
+
out(` ${a.account_id.padEnd(16)} ${a.state}${tail}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
default:
|
|
112
|
+
out('usage: account-pool <lease|release|renew|status> ...');
|
|
113
|
+
out(' account-pool lease <pool> [--holder h] [--ttl secs] [--wait-ms ms] [--json] [--field key]');
|
|
114
|
+
out(' account-pool release <lease_token>');
|
|
115
|
+
out(' account-pool renew <lease_token> [--ttl secs]');
|
|
116
|
+
out(' account-pool status [pool] [--json]');
|
|
117
|
+
return sub ? 2 : 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
main()
|
|
121
|
+
.then((code) => process.exit(code))
|
|
122
|
+
.catch((err) => {
|
|
123
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|
|
126
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,kGAAkG;AAClG,mGAAmG;AACnG,sFAAsF;AACtF,EAAE;AACF,8FAA8F;AAC9F,uCAAuC;AACvC,kDAAkD;AAClD,wCAAwC;AAExC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhD,SAAS,GAAG,CAAC,CAAS;IACpB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,CAAC,EAAE,AAAD,EAAG,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IACxC,mEAAmE;IACnE,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAEpD,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;YAChC,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,GAAG,CACD,kGAAkG,CACnG,CAAC;gBACF,OAAO,CAAC,CAAC;YACX,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC;oBAChC,IAAI,EAAE,QAAQ;oBACd,MAAM,EAAE,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;oBACnE,UAAU,EAAE,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS;oBACtF,MAAM,EACJ,OAAO,KAAK,CAAC,SAAS,CAAC,KAAK,QAAQ;wBAClC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;wBACvC,CAAC,CAAC,MAAM,CAAC,WAAW;iBACzB,CAAC,CAAC;gBACH,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;oBACpC,MAAM,CAAC,GAAG,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;oBAC1C,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;wBACpB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,qBAAqB,KAAK,CAAC,KAAK,oBAAoB,MAAM,CAAC,UAAU,IAAI,CAC1E,CAAC;wBACF,OAAO,CAAC,CAAC;oBACX,CAAC;oBACD,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;gBACjB,CAAC;qBAAM,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;oBACtB,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;gBAC9B,CAAC;qBAAM,CAAC;oBACN,GAAG,CAAC,cAAc,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;oBACvC,GAAG,CAAC,eAAe,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;oBACzC,GAAG,CAAC,cAAc,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;oBACvC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC;wBAAE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBAC5E,CAAC;gBACD,OAAO,CAAC,CAAC;YACX,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,GAAG,YAAY,kBAAkB,EAAE,CAAC;oBACtC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC;oBACzC,OAAO,CAAC,CAAC;gBACX,CAAC;gBACD,MAAM,GAAG,CAAC;YACZ,CAAC;QACH,CAAC;QAED,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;YAC7B,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,GAAG,CAAC,2CAA2C,CAAC,CAAC;gBACjD,OAAO,CAAC,CAAC;YACX,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACnC,GAAG,CACD,KAAK,CAAC,IAAI;gBACR,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;gBACxB,CAAC,CAAC,YAAY,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,eAAe,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAChG,CAAC;YACF,OAAO,CAAC,CAAC;QACX,CAAC;QAED,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;YAC7B,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,GAAG,CAAC,sDAAsD,CAAC,CAAC;gBAC5D,OAAO,CAAC,CAAC;YACX,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CACvB,KAAK,EACL,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAC3E,CAAC;YACF,GAAG,CACD,KAAK,CAAC,IAAI;gBACR,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;gBACxB,CAAC,CAAC,WAAW,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,eAAe,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAC9F,CAAC;YACF,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAChC,CAAC;QAED,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1C,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;gBACf,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YACtC,CAAC;iBAAM,CAAC;gBACN,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;oBACtB,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,KAAK,eAAe,CAAC,CAAC,MAAM,SAAS,CAAC,CAAC;oBAC5E,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;wBAC3B,MAAM,IAAI,GACR,CAAC,CAAC,KAAK,KAAK,QAAQ;4BAClB,CAAC,CAAC,aAAa,CAAC,CAAC,MAAM,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE;4BACrF,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS;gCACrB,CAAC,CAAC,iCAAiC,CAAC,CAAC,MAAM,GAAG;gCAC9C,CAAC,CAAC,EAAE,CAAC;wBACX,GAAG,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG,IAAI,EAAE,CAAC,CAAC;oBACxD,CAAC;gBACH,CAAC;YACH,CAAC;YACD,OAAO,CAAC,CAAC;QACX,CAAC;QAED;YACE,GAAG,CAAC,sDAAsD,CAAC,CAAC;YAC5D,GAAG,CACD,6FAA6F,CAC9F,CAAC;YACF,GAAG,CAAC,sCAAsC,CAAC,CAAC;YAC5C,GAAG,CAAC,iDAAiD,CAAC,CAAC;YACvD,GAAG,CAAC,uCAAuC,CAAC,CAAC;YAC7C,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvB,CAAC;AACH,CAAC;AAED,IAAI,EAAE;KACH,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;KAClC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,UAAW,GAAa,CAAC,OAAO,IAAI,CAAC,CAAC;IAC3D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AppConfig, Credentials, PoolsSeed, ResolvedCredentials } from './types.js';
|
|
2
|
+
/** Minimal `--flag value` / `--flag` parser (no dependency). */
|
|
3
|
+
export declare function parseArgs(argv: string[]): Record<string, string | boolean>;
|
|
4
|
+
/** Build the runtime config from CLI flags (highest precedence) then env then defaults. */
|
|
5
|
+
export declare function loadConfig(argv?: string[], env?: NodeJS.ProcessEnv): AppConfig;
|
|
6
|
+
/** Load + shape-validate the accounts seed file. Returns `null` if the file is absent. */
|
|
7
|
+
export declare function loadSeed(accountsFile: string): PoolsSeed | null;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve credential env-indirection at lease time. A value `{ env: "X" }` becomes
|
|
10
|
+
* `process.env.X`; a missing env var is a clear, actionable error (never a silent empty string).
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveCredentials(credentials: Credentials, accountId: string, env?: NodeJS.ProcessEnv): ResolvedCredentials;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Configuration: env vars, CLI flags, the accounts seed file, and credential env-indirection.
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
dbPath: './account-pool.db',
|
|
6
|
+
accountsFile: './accounts.json',
|
|
7
|
+
defaultTtlSeconds: 1800,
|
|
8
|
+
leaseWaitMs: 0,
|
|
9
|
+
};
|
|
10
|
+
/** Minimal `--flag value` / `--flag` parser (no dependency). */
|
|
11
|
+
export function parseArgs(argv) {
|
|
12
|
+
const args = {};
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
const cur = argv[i];
|
|
15
|
+
if (!cur?.startsWith('--'))
|
|
16
|
+
continue;
|
|
17
|
+
const key = cur.slice(2);
|
|
18
|
+
const next = argv[i + 1];
|
|
19
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
20
|
+
args[key] = next;
|
|
21
|
+
i++;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
args[key] = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return args;
|
|
28
|
+
}
|
|
29
|
+
function intEnv(env, name, fallback) {
|
|
30
|
+
const raw = env[name];
|
|
31
|
+
if (raw === undefined || raw === '')
|
|
32
|
+
return fallback;
|
|
33
|
+
const n = Number.parseInt(raw, 10);
|
|
34
|
+
if (Number.isNaN(n) || n < 0) {
|
|
35
|
+
throw new Error(`Invalid ${name}="${raw}" — expected a non-negative integer.`);
|
|
36
|
+
}
|
|
37
|
+
return n;
|
|
38
|
+
}
|
|
39
|
+
/** Build the runtime config from CLI flags (highest precedence) then env then defaults. */
|
|
40
|
+
export function loadConfig(argv = process.argv.slice(2), env = process.env) {
|
|
41
|
+
const flags = parseArgs(argv);
|
|
42
|
+
const flagStr = (k) => (typeof flags[k] === 'string' ? flags[k] : undefined);
|
|
43
|
+
return {
|
|
44
|
+
dbPath: flagStr('db') ?? env.APM_DB_PATH ?? DEFAULTS.dbPath,
|
|
45
|
+
accountsFile: flagStr('accounts') ?? env.APM_ACCOUNTS_FILE ?? DEFAULTS.accountsFile,
|
|
46
|
+
defaultTtlSeconds: intEnv(env, 'APM_DEFAULT_TTL_SECONDS', DEFAULTS.defaultTtlSeconds),
|
|
47
|
+
leaseWaitMs: intEnv(env, 'APM_LEASE_WAIT_MS', DEFAULTS.leaseWaitMs),
|
|
48
|
+
resetLeases: flags['reset-leases'] === true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Load + shape-validate the accounts seed file. Returns `null` if the file is absent. */
|
|
52
|
+
export function loadSeed(accountsFile) {
|
|
53
|
+
let raw;
|
|
54
|
+
try {
|
|
55
|
+
raw = readFileSync(resolve(accountsFile), 'utf8');
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (err.code === 'ENOENT')
|
|
59
|
+
return null;
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(raw);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
throw new Error(`Accounts file ${accountsFile} is not valid JSON: ${err.message}`);
|
|
68
|
+
}
|
|
69
|
+
const pools = parsed?.pools;
|
|
70
|
+
if (!pools || typeof pools !== 'object') {
|
|
71
|
+
throw new Error(`Accounts file ${accountsFile} must have a top-level "pools" object.`);
|
|
72
|
+
}
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
for (const [pool, accounts] of Object.entries(pools)) {
|
|
75
|
+
if (!Array.isArray(accounts)) {
|
|
76
|
+
throw new Error(`Pool "${pool}" in ${accountsFile} must be an array of accounts.`);
|
|
77
|
+
}
|
|
78
|
+
for (const acc of accounts) {
|
|
79
|
+
if (!acc?.id || typeof acc.id !== 'string') {
|
|
80
|
+
throw new Error(`An account in pool "${pool}" is missing a string "id".`);
|
|
81
|
+
}
|
|
82
|
+
if (seen.has(acc.id)) {
|
|
83
|
+
throw new Error(`Duplicate account id "${acc.id}" in ${accountsFile} — ids must be unique.`);
|
|
84
|
+
}
|
|
85
|
+
seen.add(acc.id);
|
|
86
|
+
if (!acc.credentials || typeof acc.credentials !== 'object') {
|
|
87
|
+
throw new Error(`Account "${acc.id}" is missing a "credentials" object.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return parsed;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolve credential env-indirection at lease time. A value `{ env: "X" }` becomes
|
|
95
|
+
* `process.env.X`; a missing env var is a clear, actionable error (never a silent empty string).
|
|
96
|
+
*/
|
|
97
|
+
export function resolveCredentials(credentials, accountId, env = process.env) {
|
|
98
|
+
const out = {};
|
|
99
|
+
for (const [key, value] of Object.entries(credentials)) {
|
|
100
|
+
if (value && typeof value === 'object' && 'env' in value) {
|
|
101
|
+
const envName = value.env;
|
|
102
|
+
const resolved = env[envName];
|
|
103
|
+
if (resolved === undefined || resolved === '') {
|
|
104
|
+
throw new Error(`Account "${accountId}" credential "${key}" requires environment variable ` +
|
|
105
|
+
`"${envName}", which is not set. Export it before leasing.`);
|
|
106
|
+
}
|
|
107
|
+
out[key] = resolved;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
out[key] = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import type { PoolsSeed } from './types.js';
|
|
3
|
+
export type Db = Database.Database;
|
|
4
|
+
/** Open the database, apply pragmas (WAL + busy_timeout) and run migrations. */
|
|
5
|
+
export declare function openDb(dbPath: string, busyTimeoutMs?: number): Db;
|
|
6
|
+
/**
|
|
7
|
+
* Idempotent seed upsert. Adding accounts to the seed file and restarting must NOT clobber live
|
|
8
|
+
* lease state — so this updates only `pool` + `credentials`, never the lease columns.
|
|
9
|
+
* Returns the number of accounts inserted or updated.
|
|
10
|
+
*/
|
|
11
|
+
export declare function seedAccounts(db: Db, seed: PoolsSeed): number;
|
|
12
|
+
/** Force-clear all lease state (the `--reset-leases` startup flag). */
|
|
13
|
+
export declare function resetLeases(db: Db): number;
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// SQLite layer: open in WAL mode, run migrations, seed accounts with an idempotent upsert.
|
|
2
|
+
//
|
|
3
|
+
// WAL + a busy_timeout is what lets MANY independent server processes (one per agent session)
|
|
4
|
+
// safely share one database file: writers serialize, readers don't block, and a writer that finds
|
|
5
|
+
// the lock held waits up to busy_timeout instead of erroring out immediately.
|
|
6
|
+
import Database from 'better-sqlite3';
|
|
7
|
+
const SCHEMA_VERSION = 1;
|
|
8
|
+
const MIGRATIONS = {
|
|
9
|
+
1: `
|
|
10
|
+
CREATE TABLE IF NOT EXISTS accounts (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
pool TEXT NOT NULL,
|
|
13
|
+
credentials TEXT NOT NULL,
|
|
14
|
+
leased_by TEXT,
|
|
15
|
+
lease_token TEXT,
|
|
16
|
+
leased_at INTEGER,
|
|
17
|
+
ttl_seconds INTEGER,
|
|
18
|
+
lease_count INTEGER NOT NULL DEFAULT 0
|
|
19
|
+
);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_accounts_pool_leasedby ON accounts (pool, leased_by);
|
|
21
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_lease_token ON accounts (lease_token)
|
|
22
|
+
WHERE lease_token IS NOT NULL;
|
|
23
|
+
`,
|
|
24
|
+
};
|
|
25
|
+
/** Open the database, apply pragmas (WAL + busy_timeout) and run migrations. */
|
|
26
|
+
export function openDb(dbPath, busyTimeoutMs = 5000) {
|
|
27
|
+
const db = new Database(dbPath);
|
|
28
|
+
db.pragma('journal_mode = WAL');
|
|
29
|
+
db.pragma('synchronous = NORMAL');
|
|
30
|
+
db.pragma(`busy_timeout = ${busyTimeoutMs}`);
|
|
31
|
+
db.pragma('foreign_keys = ON');
|
|
32
|
+
migrate(db);
|
|
33
|
+
return db;
|
|
34
|
+
}
|
|
35
|
+
function migrate(db) {
|
|
36
|
+
db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)');
|
|
37
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
38
|
+
const current = row ? Number.parseInt(row.value, 10) : 0;
|
|
39
|
+
const apply = db.transaction((from) => {
|
|
40
|
+
for (let v = from + 1; v <= SCHEMA_VERSION; v++) {
|
|
41
|
+
const sql = MIGRATIONS[v];
|
|
42
|
+
if (sql)
|
|
43
|
+
db.exec(sql);
|
|
44
|
+
}
|
|
45
|
+
db.prepare("INSERT INTO meta (key, value) VALUES ('schema_version', ?) " +
|
|
46
|
+
'ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(String(SCHEMA_VERSION));
|
|
47
|
+
});
|
|
48
|
+
if (current < SCHEMA_VERSION)
|
|
49
|
+
apply(current);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Idempotent seed upsert. Adding accounts to the seed file and restarting must NOT clobber live
|
|
53
|
+
* lease state — so this updates only `pool` + `credentials`, never the lease columns.
|
|
54
|
+
* Returns the number of accounts inserted or updated.
|
|
55
|
+
*/
|
|
56
|
+
export function seedAccounts(db, seed) {
|
|
57
|
+
const upsert = db.prepare(`INSERT INTO accounts (id, pool, credentials, lease_count)
|
|
58
|
+
VALUES (@id, @pool, @credentials, 0)
|
|
59
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
60
|
+
pool = excluded.pool,
|
|
61
|
+
credentials = excluded.credentials`);
|
|
62
|
+
let count = 0;
|
|
63
|
+
const tx = db.transaction(() => {
|
|
64
|
+
for (const [pool, accounts] of Object.entries(seed.pools)) {
|
|
65
|
+
for (const acc of accounts) {
|
|
66
|
+
upsert.run({ id: acc.id, pool, credentials: JSON.stringify(acc.credentials) });
|
|
67
|
+
count++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
tx();
|
|
72
|
+
return count;
|
|
73
|
+
}
|
|
74
|
+
/** Force-clear all lease state (the `--reset-leases` startup flag). */
|
|
75
|
+
export function resetLeases(db) {
|
|
76
|
+
const info = db
|
|
77
|
+
.prepare(`UPDATE accounts
|
|
78
|
+
SET leased_by = NULL, lease_token = NULL, leased_at = NULL, ttl_seconds = NULL
|
|
79
|
+
WHERE leased_by IS NOT NULL`)
|
|
80
|
+
.run();
|
|
81
|
+
return info.changes;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=db.js.map
|
package/dist/db.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,EAAE;AACF,8FAA8F;AAC9F,kGAAkG;AAClG,8EAA8E;AAE9E,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAKtC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEzB,MAAM,UAAU,GAA2B;IACzC,CAAC,EAAE;;;;;;;;;;;;;;GAcF;CACF,CAAC;AAEF,gFAAgF;AAChF,MAAM,UAAU,MAAM,CAAC,MAAc,EAAE,aAAa,GAAG,IAAI;IACzD,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAChC,EAAE,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;IAClC,EAAE,CAAC,MAAM,CAAC,kBAAkB,aAAa,EAAE,CAAC,CAAC;IAC7C,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,CAAC;IACZ,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,SAAS,OAAO,CAAC,EAAM;IACrB,EAAE,CAAC,IAAI,CAAC,6EAA6E,CAAC,CAAC;IACvF,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,qDAAqD,CAAC,CAAC,GAAG,EAEpE,CAAC;IACd,MAAM,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEzD,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC,IAAY,EAAE,EAAE;QAC5C,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,IAAI,cAAc,EAAE,CAAC,EAAE,EAAE,CAAC;YAChD,MAAM,GAAG,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;YAC1B,IAAI,GAAG;gBAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACxB,CAAC;QACD,EAAE,CAAC,OAAO,CACR,6DAA6D;YAC3D,uDAAuD,CAC1D,CAAC,GAAG,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IACH,IAAI,OAAO,GAAG,cAAc;QAAE,KAAK,CAAC,OAAO,CAAC,CAAC;AAC/C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,EAAM,EAAE,IAAe;IAClD,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CACvB;;;;0CAIsC,CACvC,CAAC;IACF,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;QAC7B,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1D,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;gBAC3B,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;gBAC/E,KAAK,EAAE,CAAC;YACV,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IACH,EAAE,EAAE,CAAC;IACL,OAAO,KAAK,CAAC;AACf,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,WAAW,CAAC,EAAM;IAChC,MAAM,IAAI,GAAG,EAAE;SACZ,OAAO,CACN;;mCAE6B,CAC9B;SACA,GAAG,EAAE,CAAC;IACT,OAAO,IAAI,CAAC,OAAO,CAAC;AACtB,CAAC"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// account-pool-mcp — stdio MCP server entry point.
|
|
3
|
+
//
|
|
4
|
+
// Brokers exclusive, crash-safe leases on a pool of credentials across independent agent sessions.
|
|
5
|
+
// Flags: --db <path> --accounts <path> --reset-leases (env: APM_DB_PATH, APM_ACCOUNTS_FILE,
|
|
6
|
+
// APM_DEFAULT_TTL_SECONDS, APM_LEASE_WAIT_MS). All logging goes to stderr; stdout is the MCP channel.
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { bootstrap } from './bootstrap.js';
|
|
9
|
+
import { loadConfig } from './config.js';
|
|
10
|
+
import { logger } from './logger.js';
|
|
11
|
+
import { buildServer } from './server.js';
|
|
12
|
+
async function main() {
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
const { pool } = bootstrap(config);
|
|
15
|
+
const server = buildServer(pool, config);
|
|
16
|
+
const transport = new StdioServerTransport();
|
|
17
|
+
await server.connect(transport);
|
|
18
|
+
logger.info('account-pool-mcp ready (stdio)', {
|
|
19
|
+
db: config.dbPath,
|
|
20
|
+
defaultTtlSeconds: config.defaultTtlSeconds,
|
|
21
|
+
leaseWaitMs: config.leaseWaitMs,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
main().catch((err) => {
|
|
25
|
+
logger.error('fatal', { message: err.message, stack: err.stack });
|
|
26
|
+
process.exit(1);
|
|
27
|
+
});
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,mDAAmD;AACnD,EAAE;AACF,mGAAmG;AACnG,gGAAgG;AAChG,sGAAsG;AAEtG,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAEzC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEhC,MAAM,CAAC,IAAI,CAAC,gCAAgC,EAAE;QAC5C,EAAE,EAAE,MAAM,CAAC,MAAM;QACjB,iBAAiB,EAAE,MAAM,CAAC,iBAAiB;QAC3C,WAAW,EAAE,MAAM,CAAC,WAAW;KAChC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,OAAO,EAAG,GAAa,CAAC,OAAO,EAAE,KAAK,EAAG,GAAa,CAAC,KAAK,EAAE,CAAC,CAAC;IACxF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Deep-redact any object so no secret value can ever be logged or returned in observability output. */
|
|
2
|
+
export declare function redact<T>(value: T): T;
|
|
3
|
+
export declare const logger: {
|
|
4
|
+
debug: (msg: string, meta?: unknown) => void;
|
|
5
|
+
info: (msg: string, meta?: unknown) => void;
|
|
6
|
+
warn: (msg: string, meta?: unknown) => void;
|
|
7
|
+
error: (msg: string, meta?: unknown) => void;
|
|
8
|
+
};
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Redacting logger.
|
|
2
|
+
//
|
|
3
|
+
// TWO hard rules live here:
|
|
4
|
+
// 1. NEVER emit a credential value. Any key whose name looks secret is replaced with "***",
|
|
5
|
+
// recursively, before anything is written.
|
|
6
|
+
// 2. On the stdio MCP transport, STDOUT is the JSON-RPC channel. A stray byte on stdout
|
|
7
|
+
// corrupts the protocol. So every log line goes to STDERR, always.
|
|
8
|
+
const SECRET_KEY = /pass(word)?|secret|token|credential|cred|api[-_]?key|private[-_]?key|salt|ssn/i;
|
|
9
|
+
const REDACTED = '***';
|
|
10
|
+
/** Deep-redact any object so no secret value can ever be logged or returned in observability output. */
|
|
11
|
+
export function redact(value) {
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
return value.map((v) => redact(v));
|
|
14
|
+
}
|
|
15
|
+
if (value && typeof value === 'object') {
|
|
16
|
+
const out = {};
|
|
17
|
+
for (const [k, v] of Object.entries(value)) {
|
|
18
|
+
out[k] = SECRET_KEY.test(k) ? REDACTED : redact(v);
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function emit(level, msg, meta) {
|
|
25
|
+
const line = {
|
|
26
|
+
t: new Date().toISOString(),
|
|
27
|
+
level,
|
|
28
|
+
msg,
|
|
29
|
+
};
|
|
30
|
+
if (meta !== undefined)
|
|
31
|
+
line.meta = redact(meta);
|
|
32
|
+
// STDERR only — stdout belongs to the MCP protocol.
|
|
33
|
+
process.stderr.write(`${JSON.stringify(line)}\n`);
|
|
34
|
+
}
|
|
35
|
+
export const logger = {
|
|
36
|
+
debug: (msg, meta) => {
|
|
37
|
+
if (process.env.APM_DEBUG)
|
|
38
|
+
emit('debug', msg, meta);
|
|
39
|
+
},
|
|
40
|
+
info: (msg, meta) => emit('info', msg, meta),
|
|
41
|
+
warn: (msg, meta) => emit('warn', msg, meta),
|
|
42
|
+
error: (msg, meta) => emit('error', msg, meta),
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB,EAAE;AACF,4BAA4B;AAC5B,8FAA8F;AAC9F,gDAAgD;AAChD,0FAA0F;AAC1F,wEAAwE;AAExE,MAAM,UAAU,GAAG,gFAAgF,CAAC;AACpG,MAAM,QAAQ,GAAG,KAAK,CAAC;AAEvB,wGAAwG;AACxG,MAAM,UAAU,MAAM,CAAI,KAAQ;IAChC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAiB,CAAC;IACrD,CAAC;IACD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,MAAM,GAAG,GAA4B,EAAE,CAAC;QACxC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;YACtE,GAAG,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACrD,CAAC;QACD,OAAO,GAAmB,CAAC;IAC7B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAID,SAAS,IAAI,CAAC,KAAY,EAAE,GAAW,EAAE,IAAc;IACrD,MAAM,IAAI,GAA4B;QACpC,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QAC3B,KAAK;QACL,GAAG;KACJ,CAAC;IACF,IAAI,IAAI,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IACjD,oDAAoD;IACpD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,KAAK,EAAE,CAAC,GAAW,EAAE,IAAc,EAAE,EAAE;QACrC,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS;YAAE,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,EAAE,CAAC,GAAW,EAAE,IAAc,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC;IAC9D,IAAI,EAAE,CAAC,GAAW,EAAE,IAAc,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC;IAC9D,KAAK,EAAE,CAAC,GAAW,EAAE,IAAc,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC;CACjE,CAAC"}
|