Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ Currently, the CLI supports the following commands:
- `fusionauth email:create` - Create a new email template locally.
- Lambdas
- `fusionauth lambda:create` - Upload a lambda to a FusionAuth server.
- `fusionauth lambda:update` - Update a lambda on a FusionAuth server.
- `fusionauth lambda:delete` - Delete a lambda from a FusionAuth server.
- `fusionauth lambda:retrieve` - Download a lambda from a FusionAuth server.
- `fusionauth lambda:link-to-application` - Link a lambda to an application on a FusionAuth server.
- `fusionauth lambda:unlink-from-application` - Unlink a lambda from an application on a FusionAuth server.
- Themes
- `fusionauth theme:download` - Download a theme from a FusionAuth server.
- `fusionauth theme:upload` - Upload a theme to a FusionAuth server.
Expand Down Expand Up @@ -72,6 +75,9 @@ npm run build;

# now you can use it
npx fusionauth --version;

# to get help on a command
npm run build; npx fusionauth lambda:link-to-application --help
```

To see examples of use look at https://fusionauth.io/docs/v1/tech/lambdas/testing.
Expand Down
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"figlet": "1.6.0",
"fs-extra": "11.1.1",
"html-to-text": "9.0.5",
"js-yaml": "^4.1.0",
"log-symbols": "5.1.0",
"log-update": "5.0.1",
"merge": "2.1.1",
Expand All @@ -42,6 +43,7 @@
"@types/figlet": "1.5.6",
"@types/fs-extra": "11.0.1",
"@types/html-to-text": "9.0.1",
"@types/js-yaml": "^4.0.5",
"@types/node": "20.4.5",
"@types/uuid": "9.0.2",
"ts-node": "10.9.1",
Expand Down
5 changes: 4 additions & 1 deletion src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ export * from './email-duplicate.js';
export * from './email-html-to-text.js';
export * from './email-upload.js';
export * from './email-watch.js';
export * from './lambda-update.js';
export * from './lambda-create.js';
export * from './lambda-delete.js';
export * from './lambda-link-to-application.js';
export * from './lambda-retrieve.js';
export * from './lambda-unlink-from-application.js';
export * from './lambda-update.js';
export * from './theme-watch.js';
export * from './theme-upload.js';
export * from './theme-download.js';
53 changes: 53 additions & 0 deletions src/commands/lambda-create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {Command} from '@commander-js/extra-typings';
import {FusionAuthClient} from '@fusionauth/typescript-client';
import {readFile} from 'fs/promises';
import chalk from 'chalk';
import {join} from 'path';
import {errorAndExit} from '../utils.js';
import {apiKeyOption, hostOption} from "../options.js";
import {load as loadYaml} from 'js-yaml';

const action = async function (lambdaId: string, {input, key: apiKey, host}: {
input: string;
key: string;
host: string
}): Promise<void> {
console.log(`Creating lambda ${lambdaId} on ${host}`);
try {
const filename = join(input, lambdaId + ".yaml");
const data = await readFile(filename, 'utf-8');
const lambda = loadYaml(data) as object;
const fusionAuthClient = new FusionAuthClient(apiKey, host);
const clientResponse = await fusionAuthClient.createLambda(lambdaId, { lambda });
if (!clientResponse.wasSuccessful())
errorAndExit(`Error creating lambda: `, clientResponse);
console.log(chalk.green(`Lambda created`));
}
catch (e: unknown) {
errorAndExit(`Error creating lambda: `, e);
}
}

// noinspection JSUnusedGlobalSymbols
export const lambdaCreate = new Command('lambda:create')
.description(`Create a lambda on FusionAuth.
Copy link
Collaborator

@ColinFrick ColinFrick Aug 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a short .summary('Create a lambda on FusionAuth') for commands with a large description. (lamdba:create, lambda:link-to-application, lamdba:unlink-from-application)

Example lambda .yaml file:

body: |
function populate(jwt, user, registration) {
jwt.message = 'Hello World!';
console.info('Hello World!');
}
debug: true
engineType: GraalJS
id: f3b3b547-7754-452d-8729-21b50d111505
insertInstant: 1692177291178
lastUpdateInstant: 1692211131823
name: '[ATestLambda]'
type: JWTPopulate
`)
.argument('<lambdaId>', 'The lambda id to create. The lambda is read from the file <id>.yaml in the <input> directory.')
.option('-i, --input <input>', 'The input directory', './lambdas/')
.addOption(apiKeyOption)
.addOption(hostOption)
.action(action);
48 changes: 48 additions & 0 deletions src/commands/lambda-link-to-application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {Command} from '@commander-js/extra-typings';
import {FusionAuthClient, ApplicationRequest} from '@fusionauth/typescript-client';
import chalk from 'chalk';
import {errorAndExit, getApplicationName} from '../utils.js';
import {apiKeyOption, hostOption} from "../options.js";

const action = async function ( applicationId: string,
lambdaId: string,
{key: apiKey, host}:
{
key: string;
host: string
}
): Promise<void>
{
console.log(`Linking lambda ${lambdaId} to application ${applicationId} on ${host}`);
try {
const applicationName = await getApplicationName(applicationId, {key: apiKey, host});
const request: ApplicationRequest = {
"application": {
"name": applicationName,
"lambdaConfiguration": {
"accessTokenPopulateId": lambdaId,
"idTokenPopulateId": lambdaId
}
}
};
const fusionAuthClient = new FusionAuthClient(apiKey, host);
const clientResponse = await fusionAuthClient.updateApplication(applicationId, request)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is dangerous. As the API documentation states, with PUT / updateApplication you must specify all of the properties of the Application. This will cause data loss in the application configuration.

Use patchApplication instead, then you can also drop the name property in the request.

if (!clientResponse.wasSuccessful())
errorAndExit(`Error linking lambda: `, clientResponse);
console.log(chalk.green(`Lambda linked`));
}
catch (e: unknown) {
errorAndExit(`Error linking lambda: `, e);
}
}

// noinspection JSUnusedGlobalSymbols
export const lambdaLinkToApplication = new Command('lambda:link-to-application')
.description(`Link an existing lambda to an application on FusionAuth as both the "Access Token populate lambda" and the "Id Token populate lambda".
Example use:
npx fusionauth lambda:link-to-application e9fdb985-9173-4e01-9d73-ac2d60d1dc8e f3b3b547-7754-452d-8729-21b50d111505 --key lambda_testing_key;`)
.argument('<applicationId>', 'The application id to update.')
.argument('<lambdaId>', 'The lambda id to link to the application.')
.addOption(apiKeyOption)
.addOption(hostOption)
.action(action);
9 changes: 7 additions & 2 deletions src/commands/lambda-retrieve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {join} from 'path';
import {mkdir, writeFile} from 'fs/promises';
import {errorAndExit, toJson} from '../utils.js';
import {apiKeyOption, hostOption} from "../options.js";
import {dump as dumpYaml} from 'js-yaml';

const action = async function (lambdaId: string, {output, key: apiKey, host}: {
output: string;
Expand All @@ -20,8 +21,12 @@ const action = async function (lambdaId: string, {output, key: apiKey, host}: {
errorAndExit(`Error retrieving lambda: `, clientResponse);
if (!existsSync(output))
await mkdir(output);
const filename = join(output, clientResponse.response.lambda?.id + ".json");
await writeFile(filename, toJson(clientResponse.response.lambda));
const filename = join(output, clientResponse.response.lambda?.id + ".yaml");
const lambdaContent = clientResponse.response.lambda;
if (lambdaContent)
lambdaContent.body = lambdaContent?.body?.replace(/\r\n/g, '\n'); // allow newlines in .yaml file
const yamlData = dumpYaml(lambdaContent, { styles: { '!!str': '|' } });
await writeFile(filename, yamlData);
console.log(chalk.green(`Lambda downloaded to ${filename}`));
}
catch (e: unknown) {
Expand Down
59 changes: 59 additions & 0 deletions src/commands/lambda-unlink-from-application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {Command} from '@commander-js/extra-typings';
import {FusionAuthClient, ApplicationRequest} from '@fusionauth/typescript-client';
import chalk from 'chalk';
import {errorAndExit, getApplication} from '../utils.js';
import {apiKeyOption, hostOption} from "../options.js";

const action = async function ( applicationId: string,
lambdaId: string,
{key: apiKey, host}:
{
key: string;
host: string
}
): Promise<void>
{
console.log(`Unlinking lambda ${lambdaId} from application ${applicationId} on ${host}`);
try {
const application = await getApplication(applicationId, {key: apiKey, host});
const request: ApplicationRequest = {
"application": {
"name": application.name,
"lambdaConfiguration": {}
}
};
const accessTokenPopulateId = application.lambdaConfiguration?.accessTokenPopulateId;
const idTokenPopulateId = application.lambdaConfiguration?.idTokenPopulateId;

if (!accessTokenPopulateId && !idTokenPopulateId)
errorAndExit(`No existing lambdas were linked`);
if (accessTokenPopulateId && accessTokenPopulateId == lambdaId)
request.application!.lambdaConfiguration!.accessTokenPopulateId = null;
else
request.application!.lambdaConfiguration!.accessTokenPopulateId = accessTokenPopulateId;
if (idTokenPopulateId && idTokenPopulateId == lambdaId)
request.application!.lambdaConfiguration!.idTokenPopulateId = null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you set it to undefined instead of null?

TS2322: Type  null  is not assignable to type  string | undefined 

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add brackets to this if statements? It's hard to read.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ColinFrick Cool will fix this. I forgot to mention I had opened a PR on the typescript client FusionAuth/fusionauth-typescript-client#86 to allow for null which this PR depends on. Though after Dan's comment I realised that change cannot be made directly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting the properties to undefined and updating the application with patchApplication works for me

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setting to an empty string seems to work better to reset the value otherwise using undefined will not actually update the property

else
request.application!.lambdaConfiguration!.idTokenPopulateId = idTokenPopulateId;

const fusionAuthClient = new FusionAuthClient(apiKey, host);
const clientResponse = await fusionAuthClient.updateApplication(applicationId, request)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same problem as before, use patchApplication

if (!clientResponse.wasSuccessful())
errorAndExit(`Error unlinking lambda: `, clientResponse);
console.log(chalk.green(`Lambda unlinked`));
}
catch (e: unknown) {
errorAndExit(`Error unlinking lambda: `, e);
}
}

// noinspection JSUnusedGlobalSymbols
export const lambdaUnlinkFromApplication = new Command('lambda:unlink-from-application')
.description(`Unlink an existing lambda from an application on FusionAuth from both "Access Token populate lambda" and the "Id Token populate lambda" if it was used as either or both.
Example use:
npx fusionauth lambda:unlink-from-application e9fdb985-9173-4e01-9d73-ac2d60d1dc8e f3b3b547-7754-452d-8729-21b50d111505 --key lambda_testing_key;`)
.argument('<applicationId>', 'The application id to update.')
.argument('<lambdaId>', 'The lambda id to unlink from the application.')
.addOption(apiKeyOption)
.addOption(hostOption)
.action(action);
10 changes: 5 additions & 5 deletions src/commands/lambda-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import chalk from 'chalk';
import {join} from 'path';
import {errorAndExit} from '../utils.js';
import {apiKeyOption, hostOption} from "../options.js";
import {load as loadYaml} from 'js-yaml';

const action = async function (lambdaId: string, {input, key: apiKey, host}: {
input: string;
Expand All @@ -13,12 +14,11 @@ const action = async function (lambdaId: string, {input, key: apiKey, host}: {
}): Promise<void> {
console.log(`Updating lambda ${lambdaId} on ${host}`);
try {
const filename = join(input, lambdaId + ".json");
const filename = join(input, lambdaId + ".yaml");
const data = await readFile(filename, 'utf-8');
const lambda = JSON.parse(data);
const request = { lambda };
const lambda = loadYaml(data) as object;
const fusionAuthClient = new FusionAuthClient(apiKey, host);
const clientResponse = await fusionAuthClient.updateLambda(lambdaId, request);
const clientResponse = await fusionAuthClient.updateLambda(lambdaId, {lambda} );
if (!clientResponse.wasSuccessful())
errorAndExit(`Error updating lambda: `, clientResponse);
console.log(chalk.green(`Lambda updated`));
Expand All @@ -31,7 +31,7 @@ const action = async function (lambdaId: string, {input, key: apiKey, host}: {
// noinspection JSUnusedGlobalSymbols
export const lambdaUpdate = new Command('lambda:update')
.description('Update a lambda on FusionAuth')
.argument('<lambdaId>', 'The lambda id to update. The lambda is read from the file <id>.json in the <input> directory.')
.argument('<lambdaId>', 'The lambda id to update. The lambda is read from the file <id>.yaml in the <input> directory.')
.option('-i, --input <input>', 'The input directory', './lambdas/')
.addOption(apiKeyOption)
.addOption(hostOption)
Expand Down
10 changes: 5 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ const fusionString = figlet.textSync('Fusion').split('\n');
const authString = figlet.textSync('Auth').split('\n');

fusionString.forEach((line, i) => {
console.log(chalk.white(line) + chalk.hex('#F58320')(authString[i]));
console.log(chalk.white(line) + chalk.hex('#F58320')(authString[i]));
});

const program = new Command();
program
.name('@fusionauth/cli')
.description('CLI for FusionAuth')
.version(pkg.version);
.name('@fusionauth/cli')
.description('CLI for FusionAuth')
.version(pkg.version);

Object.values(commands).forEach(command => program.addCommand(command));

program.parse();
program.parse();
28 changes: 28 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ClientResponse from '@fusionauth/typescript-client/build/src/ClientResponse.js';
import {FusionAuthClient, Application} from '@fusionauth/typescript-client';
import {Errors} from '@fusionauth/typescript-client';
import chalk from 'chalk';

Expand Down Expand Up @@ -126,3 +127,30 @@ export function errorAndExit(message: string, error?: any) {
reportError(message, error);
process.exit(1);
}

export async function getApplicationName(applicationId: string,
{key: apiKey, host}:
{
key: string;
host: string
}
): Promise<string>
{
const app = await getApplication(applicationId, {key: apiKey, host});
return app?.name || "";
}

export async function getApplication(applicationId: string,
{key: apiKey, host}:
{
key: string;
host: string
}
): Promise<Application>
{
const fusionAuthClient = new FusionAuthClient(apiKey, host);
const clientResponse = await fusionAuthClient.retrieveApplication(applicationId);
if (!clientResponse.wasSuccessful() || !clientResponse.response.application)
errorAndExit(`Error retrieving application: `, clientResponse);
return clientResponse.response.application as Application;
}