Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 177x 177x 177x 177x 177x 177x 1x 1x 1x 1x 1x 1x 1x 13x 9x 1x 1x 1x 1x 1x 9x 8x 8x 8x 13x 13x 1x 1x 1x 1x 1x 1x 1x 1x 1x 167x 154x 154x 2x 2x 2x 2x 2x 2x 2x 2x 154x 152x 167x 167x 4x 4x 4x 4x 4x 4x 167x 1x 1x 1x 1x 1x 1x 1x 1x 8x 8x 8x 8x 8x 1x 1x 1x 1x 1x 1x 8x 1x 1x 1x 1x 1x 1x 8x 8x 2x 2x 2x 2x 2x 8x 4x 4x 8x | "use strict";
/**
* Resolution of secret references (`{ env: NAME }` or `{ secret: key }`)
* used in the configuration file. Values are read at start-up from the
* environment or from a protected local secrets file, never from the
* configuration file itself.
*
* @module util/secrets
*/
import fs from "node:fs";
import path from "node:path";
import YAML from "yaml";
import type { SecretRef } from "../config.js";
import { OagMcpError } from "../errors.js";
/**
* Resolves secret references from the environment and a secrets file.
*/
export class SecretResolver {
private fileSecrets?: Record<string, string>;
/**
* @param {Object} opts - options
* @param {String} [opts.secretsFile] - path to the secrets file
* @param {String} opts.baseDir - base for relative paths
* @param {Object} [opts.env] - environment (defaults to process.env)
*/
constructor(
private readonly opts: {
secretsFile?: string;
baseDir: string;
env?: NodeJS.ProcessEnv;
},
) {}
/**
* Load the secrets file once.
*
* @private
*/
private _load(): Record<string, string> {
if (!this.fileSecrets) {
if (!this.opts.secretsFile) {
throw new OagMcpError(
"SECRETS_FILE_MISSING",
"A { secret: ... } reference was used but upstreamAuth.secretsFile is not configured",
);
}
this.fileSecrets = _readSecretsFile(
path.resolve(this.opts.baseDir, this.opts.secretsFile),
);
}
return this.fileSecrets;
}
/**
* Resolve a reference to its value.
*
* @param {Object} ref - secret reference
* @param {String} label - what the value is for (for messages, never the value)
* @return {String} secret value
*/
resolve(ref: SecretRef, label: string): string {
if ("env" in ref) {
const v = (this.opts.env ?? process.env)[ref.env];
if (v === undefined || v === "") {
throw new OagMcpError(
"SECRET_NOT_FOUND",
`Environment variable ${ref.env} (for ${label}) is not set`,
{
hint: `Export ${ref.env} in the environment of the oag-mcp server.`,
},
);
}
return v;
}
const v = this._load()[ref.secret];
if (v === undefined || v === "") {
throw new OagMcpError(
"SECRET_NOT_FOUND",
`Secret "${ref.secret}" (for ${label}) is not in the secrets file`,
);
}
return v;
}
}
/**
* Read a secrets file: a YAML mapping of key to string value, readable by
* its owner only on POSIX. Values that are not strings are ignored.
*
* @private
*/
function _readSecretsFile(file: string): Record<string, string> {
let stat: fs.Stats;
try {
stat = fs.statSync(file);
} catch (err) {
throw new OagMcpError(
"SECRETS_FILE_MISSING",
`Secrets file ${file} cannot be read`,
{ cause: err },
);
}
if (process.platform !== "win32" && (stat.mode & 0o077) !== 0) {
throw new OagMcpError(
"SECRETS_FILE_PERMISSIONS",
`Secrets file ${file} is readable by group or others (mode ${(stat.mode & 0o777).toString(8)})`,
{ hint: `Restrict it with: chmod 600 ${file}` },
);
}
const parsed = YAML.parse(fs.readFileSync(file, "utf8")) ?? {};
if (typeof parsed !== "object" || Array.isArray(parsed)) {
throw new OagMcpError(
"SECRETS_FILE_INVALID",
`Secrets file ${file} must be a mapping of key to string value`,
);
}
return Object.fromEntries(
Object.entries(parsed).filter(([, v]) => typeof v === "string"),
) as Record<string, string>;
}
|