Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions .trivyignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,20 +130,3 @@ CVE-2026-4800
CVE-2026-26996
CVE-2026-27903
CVE-2026-27904

# ─────────────────────────────────────────────────────────────────────────────
# brace-expansion@2.1.3
# Source: typeorm@0.3.30 → glob@10.5.0 → minimatch@9.0.9
# Risk: minimatch@9 requires brace-expansion@^2.0.2; the fix for this CVE
# only ever shipped in the 5.x line (the 2.x line stops at 2.1.3),
# which minimatch@9 cannot accept without a major bump that typeorm
# doesn't support. Overridden to 2.1.3, which does fix the sibling
# GHSA-3jxr-9vmj-r5cp (exponential-time expansion) advisory.
# The remaining DoS requires a crafted glob-pattern *string* to be
# expanded — but wallet-sdk-data-store-typeorm's DataSource config
# passes concrete, already-imported entity/migration classes, never
# glob-pattern strings, so glob/minimatch/brace-expansion's expansion
# logic is never actually invoked (confirmed by inspecting
# @docknetwork/wallet-sdk-data-store-typeorm's helpers.js).
# ─────────────────────────────────────────────────────────────────────────────
CVE-2026-14257
16 changes: 13 additions & 3 deletions apps/wallet-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,16 @@ This outputs two PEM blocks:

```bash
node scripts/mint-jwt.js <tenantId> --secret <secretManagerId> [--profile <awsProfile>] [--region <region>] [--expires-in <duration>]
node scripts/mint-jwt.js <tenantId> --key-file <path> [--expires-in <duration>]
```

| Argument | Description |
|----------|-------------|
| `<tenantId>` | Unique identifier for the tenant (e.g. `alice`). Becomes the wallet path `/data/wallets/alice`. |
| `--secret` | AWS Secrets Manager secret name or ARN containing the private key PEM. Defaults to `MCP_JWT_PRIVATE_KEY_SECRET` env var. |
| `--profile` | AWS profile to use (e.g. `dev`, `prod`). Defaults to `AWS_PROFILE` env var or the default profile. |
| `--region` | AWS region (e.g. `us-east-1`). Defaults to `AWS_REGION` env var. |
| `--secret` | AWS Secrets Manager secret name or ARN containing the private key PEM. Defaults to `MCP_JWT_PRIVATE_KEY_SECRET` env var. Mutually exclusive with `--key-file`. |
| `--key-file` | Path to a local PEM private key file (from `scripts/generate-keypair.js`). Defaults to `MCP_JWT_PRIVATE_KEY_FILE` env var. Dev-only alternative to `--secret` — skips AWS entirely. Mutually exclusive with `--secret`. |
| `--profile` | AWS profile to use (e.g. `dev`, `prod`). Defaults to `AWS_PROFILE` env var or the default profile. Ignored with `--key-file`. |
| `--region` | AWS region (e.g. `us-east-1`). Defaults to `AWS_REGION` env var. Ignored with `--key-file`. |
| `--expires-in` | Token lifetime (e.g. `30d`, `90d`, `1y`). Default: `1y`. |

**Examples:**
Expand All @@ -178,10 +180,18 @@ node scripts/mint-jwt.js alice \

# Using npm script shorthand (prompts for args)
npm run admin:mint-jwt -- alice --secret dev/wallet-server/jwt-private-key --profile dev

# Local dev: no AWS account needed, key lives on disk
node scripts/generate-keypair.js
# save the printed private key PEM to, e.g., ./dev-jwt-private-key.pem
# set MCP_JWT_PUBLIC_KEY on the server to the printed public key PEM
node scripts/mint-jwt.js alice --key-file ./dev-jwt-private-key.pem --expires-in 30d
```

The token is printed to stdout — send it to the tenant to use as their Bearer token.

**Never use a `--key-file`-minted keypair for a real deployment** — keep dev and prod keypairs separate, and never commit a private key PEM to the repo.

### Revoke a tenant

JWTs are otherwise valid until they expire — there's no built-in per-token blacklist. Instead, revocation moves a per-tenant cutoff timestamp forward: any JWT whose `iat` predates the cutoff is rejected, even if unexpired. Tokens minted for that tenant *after* the revocation call remain valid.
Expand Down
4 changes: 2 additions & 2 deletions apps/wallet-server/scripts/generate-keypair.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ console.log(`
╚══════════════════════════════════════════════════════╝

▼ PRIVATE KEY — keep this secret ▼
Store in a password manager or AWS Secrets Manager.
Set as MCP_JWT_PRIVATE_KEY when running scripts/mint-jwt.js.
Store in AWS Secrets Manager and use with mint-jwt.js --secret (prod/shared deployments),
or save to a local file and use with mint-jwt.js --key-file (local dev only).
Never commit it or add it to an ECS task definition.

${privatePem}
Expand Down
65 changes: 45 additions & 20 deletions apps/wallet-server/scripts/mint-jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,32 @@
*
* Usage:
* node scripts/mint-jwt.js <tenantId> --secret <secretId> [options]
* node scripts/mint-jwt.js <tenantId> --key-file <path> [options]
*
* Arguments:
* <tenantId> Unique identifier for the tenant (becomes the JWT `sub` claim).
* Determines wallet path on the server: /data/wallets/<tenantId>
* --secret <secretId> AWS Secrets Manager secret name or ARN containing the PEM private key.
* Defaults to MCP_JWT_PRIVATE_KEY_SECRET env var.
* --key-file <path> Path to a local PEM private key file (from scripts/generate-keypair.js).
* Defaults to MCP_JWT_PRIVATE_KEY_FILE env var. Dev-only alternative to
* --secret — mutually exclusive with it. Never use a --key-file key for
* a real deployment; keep dev and prod keypairs separate.
* --profile <name> AWS profile to use (e.g. dev, prod). Defaults to AWS_PROFILE env var
* or the default profile.
* or the default profile. Ignored with --key-file.
* --region <region> AWS region (e.g. us-east-1). Defaults to AWS_REGION env var.
* Ignored with --key-file.
* --expires-in <dur> Token lifetime. Examples: 30d, 90d, 1y (default: 1y)
*
* Output: the signed JWT token on stdout; info on stderr.
*
* Examples:
* node scripts/mint-jwt.js alice --secret prod/wallet-server/jwt-key --profile prod --expires-in 90d
* node scripts/mint-jwt.js bob --secret dev/wallet-server/jwt-key --profile dev
* node scripts/mint-jwt.js carol --key-file ./dev-jwt-private-key.pem --expires-in 30d
*/

import { readFile } from "node:fs/promises";
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
import { fromIni } from "@aws-sdk/credential-providers";
import { SignJWT, importPKCS8, decodeJwt } from "jose";
Expand All @@ -36,40 +44,57 @@ function flag(name) {

const tenantId = args.find((a) => !a.startsWith("--"));
const secretId = flag("--secret") ?? process.env.MCP_JWT_PRIVATE_KEY_SECRET;
const keyFile = flag("--key-file") ?? process.env.MCP_JWT_PRIVATE_KEY_FILE;
const awsProfile = flag("--profile") ?? process.env.AWS_PROFILE;
const awsRegion = flag("--region") ?? process.env.AWS_REGION;
const expiresIn = flag("--expires-in") ?? "1y";

if (!tenantId) {
console.error("Usage: node scripts/mint-jwt.js <tenantId> --secret <secretId> [--profile <name>] [--region <region>] [--expires-in <duration>]");
console.error("Usage: node scripts/mint-jwt.js <tenantId> (--secret <secretId> | --key-file <path>) [--profile <name>] [--region <region>] [--expires-in <duration>]");
console.error("Example: node scripts/mint-jwt.js alice --secret prod/wallet-server/jwt-private-key --profile prod --expires-in 90d");
console.error("Example: node scripts/mint-jwt.js alice --key-file ./dev-jwt-private-key.pem --expires-in 30d");
process.exit(1);
}

if (!secretId) {
console.error("Error: --secret <secretId> is required (or set MCP_JWT_PRIVATE_KEY_SECRET env var).");
console.error("The secret should contain the PEM private key from scripts/generate-keypair.js");
if (secretId && keyFile) {
console.error("Error: --secret and --key-file are mutually exclusive. Pick one key source.");
process.exit(1);
}

// --- build AWS client ---
const smConfig = {};
if (awsRegion) smConfig.region = awsRegion;
if (awsProfile) smConfig.credentials = fromIni({ profile: awsProfile });
if (!secretId && !keyFile) {
console.error("Error: --secret <secretId> or --key-file <path> is required");
console.error("(or set MCP_JWT_PRIVATE_KEY_SECRET / MCP_JWT_PRIVATE_KEY_FILE env var).");
console.error("The key should be the PEM private key from scripts/generate-keypair.js");
process.exit(1);
}

const smClient = new SecretsManagerClient(smConfig);
// --- fetch private key ---
let privatePem;
if (keyFile) {
process.stderr.write(`Reading private key from local file: ${keyFile}\n`);
try {
privatePem = await readFile(keyFile, "utf8");
} catch (err) {
console.error(`Error reading private key file: ${err.message}`);
process.exit(1);
}
} else {
const smConfig = {};
if (awsRegion) smConfig.region = awsRegion;
if (awsProfile) smConfig.credentials = fromIni({ profile: awsProfile });

// --- fetch private key from Secrets Manager ---
process.stderr.write(`Fetching private key from Secrets Manager${awsProfile ? ` (profile: ${awsProfile})` : ""}\n`);
const smClient = new SecretsManagerClient(smConfig);

let privatePem;
try {
const response = await smClient.send(new GetSecretValueCommand({ SecretId: secretId }));
privatePem = response.SecretString;
if (!privatePem) throw new Error("Secret has no string value (binary secrets are not supported)");
} catch (err) {
console.error("Error fetching private key from Secrets Manager.");
process.exit(1);
process.stderr.write(`Fetching private key from Secrets Manager${awsProfile ? ` (profile: ${awsProfile})` : ""}\n`);

try {
const response = await smClient.send(new GetSecretValueCommand({ SecretId: secretId }));
privatePem = response.SecretString;
if (!privatePem) throw new Error("Secret has no string value (binary secrets are not supported)");
} catch (err) {
console.error("Error fetching private key from Secrets Manager.");
process.exit(1);
}
}

let privateKey;
Expand Down
Loading
Loading