The fact that Caspio access tokens are valid for only 24
hours poses a maintenance problem:
How can we always ensure we have a valid access token to issue properly authenticated requests against Caspio's REST API?
Manually updating the access token is a valid solution, albeit impractical. Devising a programmatic way of renewing access tokens is our best bet. Essentially, we need to schedule a Node.js script to run on a recurring basis (i.e., within every 24
hours). This tutorial assumes you are developing on a Mac. Specifically, this tutorial has been tested successfully on a Mac running on macOS Monterey 12.1.
Contents
- External Dependencies
- Necessary Files - Descriptions
- Necessary Files - Contents
- Starting and Stopping the Launch Agent
External Dependencies
This tutorial requires two external dependencies, namely dotenv
and replace-in-file
:
npm i dotenv replace-in-file
Necessary Files - Descriptions
Please use the file structure outlined below to follow along and/or reproduce your own configuration.
Files at Project Root
We will create the following files at the root of our project folder:
-
.env
: Holds environment variables that should be kept private (e.g., Caspio credentials). -
caspio.auth.stdout.log
: Logs when an access token was successfully updated. -
caspio.auth.stderr.log
: Logs when an access token update was attempted (and failed). -
caspio-refresh-access-token.js
: Node.js script that will run on a recurring schedule. -
caspio-config.js
: Configuration file for Caspio credentials. This file will berequire
d so we can easily make token requests. For ease of use, this file should also berequire
d elsewhere in our project when utilizing thecaspio-sdk
package:const caspioCredentials = require('./caspio-config'); // for making access token requests const getNewAccessToken = require('caspio-sdk/auth')(caspioCredentials); // for general package use const caspio = require('caspio-sdk')(caspioCredentials);
Launch Agent File
Assuming we are user someuser
(this can usually be ascertained by running echo $USER
in most shells), we will create the following file in the /Users/someuser/Library/LaunchAgents
directory:
com.caspioRenewToken.daemon.plist
: Property list file that specifies how automation of thecaspio-refresh-access-token.js
script will be configured.
As this Node.js script automation guide details:
launchd
is a tool for running daemons and agents on macOS. If you aren't familiar with these terms, a daemon is a system-wide service that is always running in the background, while agents are services that are executed on user-specific requests. [...]We will be making user-specific requests to run a Node.js script, so we will make use of launch agents. These agents allow a user to define a task by using a property list (
.plist
) file that can be executed on a regular schedule as set by the user.launchd
can automate tasks both periodically (using a set interval between executions) and on a calendar-based schedule, allowing for flexibility in the way you schedule your scripts to run.
To begin automation, our goal will be to put together an effective .plist
file and then to load the .plist
file into launchd
to set the automation schedule in motion.
Necessary Files - Contents
Start by creating the files at the project root folder:
touch .env \
caspio.auth.stdout.log \
caspio.auth.stderr.log \
caspio-config.js \
caspio-refresh-access-token.js
Then create the preference list file that will automate the execution of our script on a recurring schedule (swap out someuser
with your username):
touch /Users/someuser/Library/LaunchAgents/com.caspioRenewToken.daemon.plist
The following sections detail the contents of each file described in the previous section. Modify contents to suit your functional and preferential needs.
.env
CASPIO_ACCESS_TOKEN=*****
CASPIO_ACCOUNT_ID=*****
CASPIO_CLIENT_ID=*****
CASPIO_CLIENT_SECRET=*****
CASPIO_TOKEN_ENDPOINT_URL=*****
Replace each *****
block with the described credential. Generally speaking, try to avoid spaces before or after the =
symbol in the .env
file.
caspio.auth.stdout.log
No contents until successful script execution. Will eventually contain lines such as the following:
2022-02-19T18:26:34 <- CASPIO_ACCESS_TOKEN last updated
caspio.auth.stderr.log
No contents until failed script execution. Will eventually contain lines such as
2022-02-19T18:27:44 <- CASPIO_ACCESS_TOKEN last attempted update (failed)
or
2022-02-19T18:29:38 <- CASPIO_ACCESS_TOKEN access token acquired but not written
where the first example likely indicates your machine executed the script but could not obtain an access token (e.g., no internet connection, Caspio server issue, etc.) while the second example likely indicates your .env
file is not properly configured (i.e., an access token was obtained but nothing was updated in the .env
file since no variable value was matched to replace).
caspio-refresh-access-token.js
const fsp = require('fs').promises;
const caspioCredentials = require('./caspio-config');
const getNewAccessToken = require('caspio-sdk/auth')(caspioCredentials);
const replaceInFile = require('replace-in-file');
async function writeNewAccessToken(envVariableName) {
// replaces value for variable in .env file
// Example: suppose the following line exists in the .env file:
// CASPIO_ACCESS_TOKEN=vc389QtgGxl8Np48FSjUxqtPzDfROdU_C_y4WPqB
// the 'vc389QtgGxl8Np48FSjUxqtPzDfROdU_C_y4WPqB' value will be
// replaced by the new access_token value obtained by getNewAccessToken
const regReplace = `(?<=${envVariableName} *= *)([-a-zA-Z0-9._~+/]+=*)+(?:\\\\n|$)`;
const re = new RegExp(regReplace, 'm');
const isoDateStr = (new Date()).toISOString().replace(/\.([^.]*)$/, '');
const reqMsgSuccess = `${isoDateStr} <- ${envVariableName} last updated\n`;
const reqMsgWarning = `${isoDateStr} <- ${envVariableName} access token acquired but not written\n`;
const reqMsgFailire = `${isoDateStr} <- ${envVariableName} last attempted update (failed)\n`;
try {
const { access_token: newAccessToken } = await getNewAccessToken();
const replaceOptions = {
files: './.env',
from: re,
to: newAccessToken,
};
const result = await replaceInFile(replaceOptions);
const resultStatus = result[0].hasChanged;
if (resultStatus) {
await fsp.writeFile('./caspio.auth.stdout.log', reqMsgSuccess, { flag: 'a' });
} else {
await fsp.writeFile('./caspio.auth.stderr.log', reqMsgWarning, { flag: 'a' });
}
} catch(error) {
await fsp.writeFile('./caspio.auth.stderr.log', reqMsgFailire, { flag: 'a' });
}
}
writeNewAccessToken('CASPIO_ACCESS_TOKEN');
The last line, writeNewAccessToken('CASPIO_ACCESS_TOKEN');
, is important since we want this script to be run automatically. If you are managing more than one Caspio account, then you can imagine adding the following at the end of the caspio-refresh-access-token.js
file:
writeNewAccessToken('CASPIO_ACCESS_TOKEN_ACCOUNT_1');
...
writeNewAccessToken('CASPIO_ACCESS_TOKEN_ACCOUNT_N');
caspio-config.js
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '.env') });
const caspioCredentials = {
accessToken: process.env.CASPIO_ACCESS_TOKEN,
accountID: process.env.CASPIO_ACCOUNT_ID,
clientID: process.env.CASPIO_CLIENT_ID,
clientSecret: process.env.CASPIO_CLIENT_SECRET,
tokenEndpointURL: process.env.CASPIO_TOKEN_ENDPOINT_URL,
};
module.exports = caspioCredentials;
As mentioned at the beginning of this tutorial, the caspio-config.js
file can be put to good use well beyond acquiring new access tokens, namely by require
ing the file whenever utilizing the caspio-sdk
package:
const caspioCredentials = require('./caspio-config');
const caspio = require('caspio-sdk')(caspioCredentials);
com.caspioRenewToken.daemon.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.caspioRenewToken.daemon.plist</string>
<key>RunAtLoad</key>
<true/>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>3</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string><![CDATA[/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin]]></string>
</dict>
<key>WorkingDirectory</key>
<string>{directory where the script caspio-refresh-access-token.js lives}</string>
<key>ProgramArguments</key>
<array>
<string>{path to node executable}</string>
<string>caspio-refresh-access-token.js</string>
</array>
</dict>
</plist>
Update the contents within {}
in the file above with details specific to your development environment.
Some notes about the configuration above by key
(this launchd
tutorial may be helpful for reference):
-
Label
: The name of the daemon (identifies the job and has to be unique for thelaunchd
instance).In the file above,
com.caspioRenewToken.daemon.plist
identifies the job to be executed. -
RunAtLoad
: This is one of several optional keys specifying when the job should be run (in this case right after it has been loaded). Only whenRunAtLoad
orKeepAlive
(not used above) have been specified willlaunchd
start the job unconditionally when it has been loaded.true
means what it sounds like: we want the job to be executed right after it has been loaded. -
StartCalendarInterval
: Apple'slaunchd.plist
man page, accessible by runningman launchd.plist
, has the following helpful information (this guide provides a less technical approach): This optional key causes the job to be started every calendar interval as specified. Missing arguments are considered to be wildcard. The semantics are similar tocrontab(5)
in how firing dates are specified. Multiple dictionaries may be specified in an array to schedule multiple calendar intervals. Unlikecron
which skips job invocations when the computer is asleep,launchd
will start the job the next time the computer wakes up. If multiple intervals transpire before the computer is woken, those events will be coalesced into one event upon wake from sleep.An
Hour
of3
andMinute
of0
indicate the job should be executed at 3:00 in the morning each day. -
EnvironmentVariables
: The environment path is set, which is useful if a program uses other commands.<![CDATA[/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin]]>
is simply boilerplate. -
WorkingDirectory
: Specify the directory from which the job should be executed.As expressed by the braces, the
string
value here should be the absolute path of your project's root folder that should contain thecaspio-refresh-access-token.js
script to be executed. -
ProgramArguments
: The argument vector to be passed to the job when a process is spawned. In English:The first
string
value should be the path to thenode
executable, something you should be able to ascertain by runningwhich node
(or by specifying your own preferred path to anode
executable). The secondstring
value should be the file name of the script we want to run when the job is executed, namelycaspio-refresh-access-token.js
in this case.If our first
string
value is/usr/local/bin/node
, then our script will ultimately be run as follows when our job executes:/usr/local/bin/node caspio-refresh-access-token.js
It is worth noting that we might normally be inclined to include StandardErrorPath
and StandardOutPath
keys for logging purposes (as opposed to directly within our script as is being done currently), but include them at your own risk.
Starting and Stopping the Launch Agent
Start: You may start the launch agent (i.e., load the .plist
file into launchd
) as follows:
launchctl load ~/Library/LaunchAgents/com.caspioRenewToken.daemon.plist
Stop: To stop the schedule, run the following:
launchctl unload ~/Library/LaunchAgents/com.caspioRenewToken.daemon.plist
Edit: To edit the launch agent, run the following:
code ~/Library/LaunchAgents/com.caspioRenewToken.daemon.plist
Note that the editing line above only works if you have VSCode installed as an editor. Of course, you can swap out code
above to open the .plist
file in an editor of your choice if you do not use VSCode.
Shell aliases (for convenience): It may be useful to define shell aliases to handle the actions referenced above:
alias caspioAutoAuthStart='launchctl load ~/Library/LaunchAgents/com.caspioRenewToken.daemon.plist'
alias caspioAutoAuthStop='launchctl unload ~/Library/LaunchAgents/com.caspioRenewToken.daemon.plist'
alias caspioAutoAuthEdit='code ~/Library/LaunchAgents/com.caspioRenewToken.daemon.plist'