Zipped Action Folder Resources in Project Firefly and Adobe I/O Runtime
NOTICE: This post is related to Adobe App Builder which previosly went by "Project Firefly". Learn more about the new Adobe Firefly, AI Art Generator here.
In this post, I explain how to include resources in a zipped action folder when working with Project Firefly and Adobe I/O Runtime actions.
Use Case
There are a number of use cases that require using zipped action folders. At a high level, zipped action folders allow us to make a specific resource or resources exclusively available to an action. I'm going to refer to a previous post, Cloud Manager Slack Notifications with Project Firefly, to help me illustrate the use case of generating an access token for service-to-service authentication.
In the previous post, I used an environment variable to store a private key. The private key is needed to create the access token which is needed to authenticate with the Cloud Manager API. Using an environment variable to store a private key goes against best practice. A better approach is to store the private key in a file that is only available to be read by our action.
Create a new Action
Using the terminal, navigate to your code project and run the following commands:
npm install jsrsasign --save
aio app:action:add
The action creation wizard will walk you through generating a new action. Select, Generic
as the action type and give the action an appropriate name (e.g. zipped-resource
). Agree to overwrite the package.json and manifest.yml files when prompted.
Open the code project within an editor and open the index.js file for the new action. Replace the contents with the code below.
const fetch = require('node-fetch');
const { Core } = require('@adobe/aio-sdk')
const fs = require('fs');
const { getAccessToken } = require('./token');
const { errorResponse, getBearerToken, stringParameters, checkMissingRequestInputs } = require('./utils')
async function main (params) {
const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' })
try {
const requiredParams = ['SERVICE_API_KEY', 'CLIENT_SECRET', 'IMS_ORG_ID', 'TECHNICAL_ACCOUNT_ID']
const requiredHeaders = []
const errorMessage = checkMissingRequestInputs(params, requiredParams, requiredHeaders)
if (errorMessage) {
return errorResponse(400, errorMessage, logger)
}
const privateKey = fs.readFileSync(__dirname + '/private.key','UTF-8');
const accessToken = await getAccessToken(params, privateKey);
const response = {
statusCode: 200,
body: accessToken
}
return response;
} catch (error) {
logger.error(error)
return errorResponse(500, 'server error', logger)
}
}
exports.main = main
Create a new file within the zipped-resource action folder called token.js
. I'll reuse the same code from the previous post, but with a few minor changes to pass the private key into the module as a unique parameter instead of an argument derived from environment variables.
const jsrsasign = require("jsrsasign");
const fetch = require("node-fetch");
const { URLSearchParams } = require("url");
async function getAccessToken(params, privateKey) {
const EXPIRATION = 60 * 60; // 1 hour
const header = {
alg: "RS256",
typ: "JWT",
};
const payload = {
exp: Math.round(new Date().getTime() / 1000) + EXPIRATION,
iss: params.IMS_ORG_ID,
sub: params.TECHNICAL_ACCOUNT_ID,
aud: `https://ims-na1.adobelogin.com/c/${params.SERVICE_API_KEY}`,
"https://ims-na1.adobelogin.com/s/ent_cloudmgr_sdk": true,
};
const jwtToken = jsrsasign.jws.JWS.sign(
"RS256",
JSON.stringify(header),
JSON.stringify(payload),
privateKey
);
const response = await fetch(
"https://ims-na1.adobelogin.com/ims/exchange/jwt",
{
method: "POST",
body: new URLSearchParams({
client_id: params.SERVICE_API_KEY,
client_secret: params.CLIENT_SECRET,
jwt_token: jwtToken,
}),
}
);
const json = await response.json();
return json["access_token"];
}
module.exports = {
getAccessToken
}
Next, copy the private.key
file into the zipped-resources action folder so that it is next to the index.js and token.js files. The private key should have been downloaded when the JWT credential was generated. Review Getting Started with Project Firefly if necessary to learn how to create a new JWT credential.
The zipped-resource
action folder needs to contain all of the resources required by the action. Copy the utils.js
file that was generated with the project and paste it into the zipped-resource action folder. For convenience, the code can also be copied below.
function stringParameters (params) {
// hide authorization token without overriding params
let headers = params.__ow_headers || {}
if (headers.authorization) {
headers = { ...headers, authorization: '<hidden>' }
}
return JSON.stringify({ ...params, __ow_headers: headers })
}
function getMissingKeys (obj, required) {
return required.filter(r => {
const splits = r.split('.')
const last = splits[splits.length - 1]
const traverse = splits.slice(0, -1).reduce((tObj, split) => { tObj = (tObj[split] || {}); return tObj }, obj)
return traverse[last] === undefined || traverse[last] === '' // missing default params are empty string
})
}
function checkMissingRequestInputs (params, requiredParams = [], requiredHeaders = []) {
let errorMessage = null
// input headers are always lowercase
requiredHeaders = requiredHeaders.map(h => h.toLowerCase())
// check for missing headers
const missingHeaders = getMissingKeys(params.__ow_headers || {}, requiredHeaders)
if (missingHeaders.length > 0) {
errorMessage = `missing header(s) '${missingHeaders}'`
}
// check for missing parameters
const missingParams = getMissingKeys(params, requiredParams)
if (missingParams.length > 0) {
if (errorMessage) {
errorMessage += ' and '
} else {
errorMessage = ''
}
errorMessage += `missing parameter(s) '${missingParams}'`
}
return errorMessage
}
function getBearerToken (params) {
if (params.__ow_headers &&
params.__ow_headers.authorization &&
params.__ow_headers.authorization.startsWith('Bearer ')) {
return params.__ow_headers.authorization.substring('Bearer '.length)
}
return undefined
}
function errorResponse (statusCode, message, logger) {
if (logger && typeof logger.info === 'function') {
logger.info(`${statusCode}: ${message}`)
}
return {
error: {
statusCode,
body: {
error: message
}
}
}
}
module.exports = {
errorResponse,
getBearerToken,
stringParameters,
checkMissingRequestInputs
}
Create a package.json file within the zipped-resource folder. Copy and paste the contents below into the package.json file.
{
"name": "zipped-resource",
"main": "index.js",
"version": "0.0.1",
"private": true,
"dependencies": {
"@adobe/aio-sdk": "^3.0.0",
"@adobe/exc-app": "^0.2.21",
"@adobe/jwt-auth": "^1.0.1",
"cloudevents-sdk": "^1.0.0",
"core-js": "^3.6.4",
"jsrsasign": "^10.2.0",
"node-fetch": "^2.6.0"
},
"engines": {
"node": "^10 || ^12"
}
}
Using the terminal, navigate into the actions > zipped-resources folder and run the following command to install the 3rd party dependencies for the action.
npm install
The contents of the zipped-resources action folder should resemble the following.
Update the Manifest
Open the project manifest.yml
and make the following two changes:
- Include the environment variable mappings for the
SERVICE_API_KEY
,CLIENT_SECRET
,IMG_ORG_ID
andTECHNICAL_ACCOUNT_ID
. These are the same mappings defined in the previous post with thePRIVATE_KEY
intentionally omitted. - Modify the function property path to point at the directory instead of the index.js file
Try it Out
To see the application in action, return to the terminal and run one more command:
aio app run
Select the zipped-resource action from the menu and confirm your access token was generated and returned.