diff --git a/src/cli.mjs b/src/cli.mjs index 470c5e35..e571ac4e 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -9,8 +9,8 @@ import keyCommand from "./commands/key.mjs"; import loginCommand from "./commands/login.mjs"; import schemaCommand from "./commands/schema/schema.mjs"; import shellCommand from "./commands/shell.mjs"; -import { checkForUpdates, fixPaths, logArgv } from "./lib/middleware.mjs"; import { cleanupSecretsFile } from "./lib/auth/authNZ.mjs"; +import { checkForUpdates, fixPaths, logArgv } from "./lib/middleware.mjs"; /** @typedef {import('awilix').AwilixContainer} cliContainer */ diff --git a/src/commands/database.mjs b/src/commands/database.mjs deleted file mode 100644 index c74d6087..00000000 --- a/src/commands/database.mjs +++ /dev/null @@ -1,90 +0,0 @@ -//@ts-check - -import { container } from "../cli.mjs"; -import { performQuery } from "./eval.mjs"; - -async function listDatabases(profile) { - const logger = container.resolve("logger"); - const AccountClient = container.resolve("AccountClient"); - logger.stdout("Listing Databases..."); - const databases = await new AccountClient(profile).listDatabases(); - logger.stdout(databases); -} - -async function createDatabase(argv) { - const client = await container.resolve("getSimpleClient")(argv); - const logger = container.resolve("logger"); - // performQuery only gives us an error code if we ask for the json-tagged format. - // so gotta go deeper to keep it agnostic of the format. - // And it can't be only on initial client init, because repl needs to - // refresh on the fly w/out kicking out. - const result = await performQuery(client, "1 + 1", undefined, { - ...argv, - format: "json-tagged", - }); - const result2 = await performQuery(client, "2 + 2", undefined, { - ...argv, - format: "json-tagged", - }); - logger.stdout(result, result2); -} - -function buildDatabaseCommand(yargs) { - return yargs - .positional("method", { - type: "string", - choices: ["create", "list", "delete"], - describe: "choose a method to interact with your databases", - }) - .options({ - profile: { - type: "string", - description: "a user profile", - default: "default", - }, - authRequired: { - default: true, - }, - url: { - type: "string", - description: "the Fauna URL to query", - default: "https://db.fauna.com:443", - }, - secret: { - type: "string", - description: "the secret to use", - // default: "somesecret", - }, - }) - .help("help", "show help") - .example([["$0 db list"]]); -} - -async function databaseHandler(argv) { - const logger = container.resolve("logger"); - const method = argv.method; - let result; - switch (method) { - case "create": - logger.stdout("Creating database..."); - result = await createDatabase(argv); - break; - case "delete": - logger.stdout("Deleting database..."); - break; - case "list": - result = await listDatabases(argv.profile); - break; - default: - break; - } - return result; -} - -export default { - command: "database ", - aliases: ["db"], - description: "Interact with your databases:", - builder: buildDatabaseCommand, - handler: databaseHandler, -}; diff --git a/src/lib/auth/authNZ.mjs b/src/lib/auth/authNZ.mjs index 9e56fa68..c04330dd 100644 --- a/src/lib/auth/authNZ.mjs +++ b/src/lib/auth/authNZ.mjs @@ -15,13 +15,14 @@ export async function refreshDBKey({ profile, database, role }) { const secretCreds = container.resolve("secretCreds"); const AccountClient = new (container.resolve("AccountClient"))(profile); const newSecret = await AccountClient.createKey({ path: database, role }); + const accountKey = getAccountKey(profile).accountKey; secretCreds.save({ creds: { path: database, role, secret: newSecret.secret, }, - key: getAccountKey(profile).accountKey, + key: accountKey, }); return newSecret; } diff --git a/src/lib/command-helpers.mjs b/src/lib/command-helpers.mjs index 1770afc9..d1dcb59a 100644 --- a/src/lib/command-helpers.mjs +++ b/src/lib/command-helpers.mjs @@ -14,9 +14,21 @@ function buildHeaders() { return headers; } +/** + * This function will return a v4 or v10 client based on the version provided in the argv. + * The client will be configured with a secret provided via: + * - command line flag + * - environment variable + * - stored in the credentials file + * If provided via command line flag or env var, 401s from the client will show an error to the user. + * If the secret is stored in the credentials file, the client will attempt to refresh the secret + * and retry the query. + * @param {*} argv + * @returns + */ export async function getSimpleClient(argv) { const logger = container.resolve("logger"); - const { profile, database: path, role, secret } = argv; + const { profile, secret } = argv; const accountKey = getAccountKey(profile).accountKey; if (secret) { logger.debug("Using Database secret from command line flag"); @@ -48,21 +60,53 @@ export async function getSimpleClient(argv) { logger.debug( "No secret provided, checking for stored secret in credentials file", ); - const existingSecret = getDBKey({ accountKey, path, role })?.secret; - if (existingSecret) { - logger.debug("Found stored secret in credentials file"); - client = await clientFromStoredSecret({ - argv, - storedSecret: existingSecret, - }); - } else { - logger.debug("No stored secret found, minting new secret"); - client = await clientFromNewSecret({ argv }); - } + client = await clientFromStoredSecret({ + argv, + accountKey, + }); } return client; } +/** + * Build a client where the secret isn't provided as a class argument, but is instead + * fetched from the credentials file during client.query + * @param {*} param0 + * @returns + */ +async function clientFromStoredSecret({ argv, accountKey }) { + const logger = container.resolve("logger"); + const { database: path, role } = argv; + let client = await buildClient({ + ...argv, + // client.query will handle getting/refreshing the secret + secret: undefined, + }); + const originalQuery = client.query.bind(client); + client.query = async function (...args) { + // Get latest stored secret for the requested path + const existingSecret = getDBKey({ accountKey, path, role })?.secret; + const newArgs = [args[0], { ...args[1], secret: existingSecret }]; + return originalQuery(...newArgs).then(async (result) => { + if (result.status === 401) { + logger.debug("stored secret is invalid, refreshing"); + // Refresh the secret, store it, and use it to try again + const newSecret = await refreshDBKey(argv); + const newArgs = [args[0], { ...args[1], secret: newSecret.secret }]; + const result = await originalQuery(...newArgs); + return result; + } + return result; + }); + }; + return client; +} + +/** + * Build a client based on the command line options provided + * @param {*} argv + * @returns + */ async function buildClient(argv) { let client; if (argv.version === "4") { @@ -92,39 +136,6 @@ async function buildClient(argv) { return client; } -async function clientFromStoredSecret({ argv, storedSecret }) { - const logger = container.resolve("logger"); - let client = await buildClient({ - ...argv, - secret: storedSecret, - }); - const originalQuery = client.query.bind(client); - client.query = async function (...args) { - return originalQuery(...args).then(async (result) => { - if (result.status === 401) { - logger.debug("stored secret is invalid, refreshing"); - // TODO: this refreshes the db key and stores in local storage, but the client instance - // is not updated with the new secret. - const newSecret = await refreshDBKey(argv); - const newArgs = [args[0], { ...args[1], secret: newSecret.secret }]; - const result = await originalQuery(...newArgs); - return result; - } - return result; - }); - }; - return client; -} - -async function clientFromNewSecret({ argv }) { - const newSecret = await refreshDBKey(argv); - const client = await buildClient({ - ...argv, - secret: newSecret.secret, - }); - return client; -} - // export async function ensureDbScopeClient({ scope, version, argv }) { // const path = scope.split("/"); diff --git a/src/lib/fauna-account-client.mjs b/src/lib/fauna-account-client.mjs index 0b78529e..2e037c94 100644 --- a/src/lib/fauna-account-client.mjs +++ b/src/lib/fauna-account-client.mjs @@ -171,6 +171,8 @@ export class FaunaAccountClient { * @throws {Error} - Throws an error if there is an issue during key creation. */ async createKey({ path, role = "admin" }) { + // TODO: improve key lifecycle management, incl tracking expiration in filesystem + const TTL_DEFAULT_MS = 1000 * 60 * 15; // 15 minutes // TODO: specify a ttl return await this.makeAccountRequest({ method: "POST", @@ -178,6 +180,8 @@ export class FaunaAccountClient { body: JSON.stringify({ path, role, + name: "System generated shell key", + ttl: new Date(Date.now() + TTL_DEFAULT_MS).toISOString(), }), secret: this.accountKey, });