Skip to content
sasjsPublic

About

Build Apps on Base SAS

Topics

Resources

Contributing

Security policy

Stars

22 stars

Watchers

1 watching

Forks

Repository files navigation

SASjs Server

OpenSSF Scorecard All Contributors

SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or locally on your desktop. It provides:

  • Virtual filesystem for storing SAS programs and other content
  • Ability to execute Stored Programs from a URL
  • Ability to create web apps using simple Desktop SAS
  • REST API with Swagger Docs

One major benefit of using SASjs Server alongside other components of the SASjs framework such as the CLI, Adapter and Core library, is that the projects you create can be very easily ported to SAS 9 (Stored Process server) or Viya (Job Execution server).

SASjs Server is available in two modes - Desktop (without authentication) and Server (with authentication, and a database)

Installation

Two channels - the npm package and the standalone executable. Both read the same environment variables and serve the same API.

npm

Run the latest release without installing it:

npx @sasjs/server

Or install it once and run sasjs-server from then on:

npm install -g @sasjs/server
sasjs-server

Requires Node 22 or later. Useful where Node is already managed by the platform, and the only channel that runs on arm64 (Apple Silicon, Graviton, a Pi), because the standalone executables are built for x64 only.

Three things to know before choosing it:

  • Pin the version for a deployment. The command above resolves latest on every run, so a restart becomes a silent upgrade. Install a specific release instead - they are listed on the npm page - so the release you tested is the one that runs.
  • .env is read from the working directory. dotenv.config() is relative to where the process starts, so run it from the project directory you want the server to own (that directory also gets the ./sasjs_root). Under npx the package itself lives in an evictable ~/.npm/_npx/<hash> cache, so an .env placed there would be lost - it belongs beside the command, not the code.
  • It starts in desktop mode, the single-user mode. The RUN_AS check still refuses to run as root, so install it under the account that should own the SAS sessions rather than starting it with sudo.

Installing the package runs no install script, so it makes no network call of its own.

Standalone executable

Download the relevant package from the releases page, or fetch it programmatically (linux):

curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
unzip linux.zip

Then launch ./api-linux (on windows, double click api-win.exe - no administrative privileges are needed) and answer the two prompts, unless the ENV vars are set:

  • Location of your sas.exe / sas.sh executable
  • Path to a filesystem location for Stored Programs and temporary files

Deployment options

  • Embedded - the server's pieces are also used inside other SASjs tooling, such as the VS Code extension, and the mock backend runs in the browser for front-end development without a server at all.

There is no container image. The previous ghcr.io/sasjs/server image could not carry SAS - the licence does not permit redistribution - so it could only serve the js and py runtimes, and it needed a SAS installation mounted from the host to do the work the server exists for. It has been withdrawn along with the Cloudron packaging built on it.

That also withdraws the OS-layer scan the image pipeline ran: nothing in the remaining pipeline looks for CVEs in a bundled runtime, because there is no bundled runtime. The js and py interpreters a deployment uses are the ones on its own host, and patching them is part of that host's maintenance.

ENV Var configuration

When launching the app, it will make use of specific environment variables. These can be set in the following places:

  • Configured globally in /etc/environment file
  • Export in terminal or shell script (export VAR=VALUE)
  • Prepended in the command
  • Enter in the .env file alongside the executable

Example contents of a .env file:

#
## Core Settings
#


# MODE options: [desktop|server] default: `desktop`
# Desktop mode is single user and designed for workstation use
# Server mode is multi-user and suitable for intranet / internet use
MODE=

# The account the server must run as.
# Unset: the server refuses to start as root.
# Set to a username: the server must be running as that account, or it refuses
# to start - so it is an assertion, not a hint.
# RUN_AS=root is the deliberate override.
# This application executes uploaded code by design, so the identity the server
# runs under is the privilege every connected user gets. In desktop mode there
# is no authentication at all.
# default: unset (root refused)
RUN_AS=

# A comma separated string that defines the available runTimes.
# Priority is given to the runtime that comes first in the string.
# Possible options at the moment are sas, js, py and r

# This string sets the priority of the available analytic runtimes
# Valid runtimes are SAS (sas), JavaScript (js), Python (py) and R (r)
# For each option provided, there should be a corresponding path,
# eg SAS_PATH, NODE_PATH, PYTHON_PATH or RSCRIPT_PATH
# Priority is given to runtimes earlier in the string
# Example options:  [sas,js,py | js,py | sas | sas,js | r | sas,r]
RUN_TIMES=

# Path to SAS executable (sas.exe / sas.sh)
SAS_PATH=/path/to/sas/executable.exe

# Path to Node.js executable
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node

# Path to Python executable
PYTHON_PATH=/usr/bin/python

# Path to R executable
R_PATH=/usr/bin/Rscript

# Path to working directory
# This location is for SAS WORK, staged files, DRIVE, configuration etc
SASJS_ROOT=./sasjs_root


# This location is for files, sasjs packages and appStreamConfig.json
DRIVE_LOCATION=./sasjs_root/drive


# Listener configuration. Each variable is a listener, enabled by carrying a value.
# Unset, empty and 0 all disable it. At least one must be set.
#
# Plain HTTP. Nothing terminates TLS for this listener, so it binds 127.0.0.1 by
# default: set HTTP_BIND_ADDRESS to widen it, and HTTP_ALLOW_REMOTE=true to
# confirm that you intend to publish an unencrypted API. Credentials cross this
# listener in clear text, so put it behind a reverse proxy if it leaves the host.
HTTP_PORT=

# Interface the plain HTTP listener binds to. default: 127.0.0.1
HTTP_BIND_ADDRESS=

# Required to bind the plain HTTP listener to a non-loopback address.
HTTP_ALLOW_REMOTE=

# TLS listener. Requires CERT_CHAIN and PRIVATE_KEY.
HTTPS_PORT=

# Interface the TLS listener binds to. default: every interface
HTTPS_BIND_ADDRESS=

# Both listeners can run at once and serve the same API: routes, authentication,
# CSRF protection and sessions are identical on each, and GET /SASjsApi/info
# reports `httpPort` and `httpsPort` (null for a disabled listener).
#
# Stored programs call back into this server through the SAS macro variable
# `_sasjs_apiserverurl`, which the generated program derives from the listener
# configuration: when a plain HTTP listener is enabled the callback uses it
# (loopback for a wildcard bind, the bound address otherwise), so the SAS session
# never has to trust this server's own certificate. With only a TLS listener the
# value follows the request that started the program.
#
# PROTOCOL and PORT are deprecated: for one release they are translated into the
# matching port variable with a warning, and removed after that.
#
# MIGRATION NOTE: an install that ran with PROTOCOL=http and no
# HTTP_BIND_ADDRESS used to accept connections on every interface, because the app
# was started with a bare app.listen(port). It now listens on 127.0.0.1 only, so a
# remote client gets connection refused until you set HTTP_BIND_ADDRESS=0.0.0.0
# (or the interface you serve on) and HTTP_ALLOW_REMOTE=true.

# Optional. The URL stored programs use to call back into this server, when the
# derived value is wrong for the topology - a program that runs on another host
# cannot reach 127.0.0.1. Set it with a scheme and no trailing slash:
# API_SERVER_URL=https://sasjs.internal.example.com
API_SERVER_URL=

# Optional. The site this server reports to programs as SYSSITE.
#
# SAS has SYSSITE as a global macro variable, so a SAS service reads the site it
# runs on without being told anything; a JS stored program has no such variable,
# and takes it from here instead. It matters as soon as a service compares
# something against the site - Data Controller checks a licence key against
# SYSSITE, and on a JS runtime that check sees whatever this reports.
#
# Unset, every program reports the default site 123 (the SASjs demo estate). Set
# it when the applications on this server are licensed for a particular site -
# uncomment the line below to do that. Leave it commented rather than blank: an
# empty value is treated as unset, but a commented line says what it means.
# SITE_ID=123

# options: [sas9|sasviya]
# If not present, mocking function is disabled
MOCK_SERVERTYPE=

# default: /api/mocks
# Path to mocking folder, for generic responses, it's sub directories should be: sas9, viya, sasjs
# Server will automatically use subdirectory accordingly
STATIC_MOCK_LOCATION=

# Optional. Serves a page at /ddc-host that embeds an application the way SAS
# Visual Analytics embeds data-driven content: an iframe carrying VA's sandbox
# attribute set, plus a message posted into it. Use it to test that an embedded
# app works inside that sandbox, which a top-level page load does not exercise.
# `timing=early` (or `both`) delivers the message repeatedly from the moment the
# frame is created, so a copy lands after the app's earliest message listener
# exists and before it bootstraps - the window a VA report refresh exercises.
# Off unless set: it frames whatever URL it is given, so it is a testing tool and
# not something to leave switched on. Query parameters: url, sandbox, message
# (JSON), timing [load|early|both], interval, height, title - see
# api/src/views/ddc-host.html.
# options: [true|false]
DDC_HOST_ENABLED=

#
## Additional SAS Options
#


# On windows use SAS_OPTIONS and on unix use SASV9_OPTIONS
# Any options set here are automatically applied in the SAS session
# See: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostunx/p0wrdmqp8k0oyyn1xbx3bp3qy2wl.htm
# And: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostwin/p0drw76qo0gig2n1kcoliekh605k.htm#p09y7hx0grw1gin1giuvrjyx61m6
SAS_OPTIONS= -NOXCMD
SASV9_OPTIONS= -NOXCMD


#
## Additional Web Server Options
#

# TLS material, required when HTTPS_PORT is set and ignored otherwise
PRIVATE_KEY=privkey.pem (required)
CERT_CHAIN=certificate.pem (required)
CA_ROOT=fullchain.pem (optional)

## ENV variables required for MODE: `server`
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority

# options: [mongodb|cosmos_mongodb] default: mongodb
DB_TYPE=

# AUTH_PROVIDERS options: [ldap|oidc] default: ``
# More than one provider may be listed, space or comma separated
AUTH_PROVIDERS=

## ENV variables required for AUTH_MECHANISM: `ldap`
LDAP_URL= <LDAP_SERVER_URL>
LDAP_BIND_DN= <cn=admin,ou=system,dc=example>
LDAP_BIND_PASSWORD = <password>
LDAP_USERS_BASE_DN = <ou=users,dc=example>
LDAP_GROUPS_BASE_DN = <ou=groups,dc=example>

## ENV variables required for AUTH_PROVIDERS: `oidc`

# Sign-in uses PKCE (RFC 7636) with the S256 challenge method. No variable
# controls it, and the provider needs no extra registration for it.

# Sign-in also requires group membership, by fixed name: the provider's
# `groups` claim must contain `sasjs-users` (ordinary user) or `sasjs-admins`
# (administrator), and a user in neither is refused. The names are not
# configurable. The `groups` scope is requested automatically from providers
# that advertise it, and providers that cannot express groups are exempt.

# Each sign-in also mirrors those memberships onto local groups of the same
# name, so a Permission can be granted to a provider group rather than to each
# of its members in turn. The mirror is authoritative: a membership the
# provider stops asserting is removed at the next sign-in. A group whose name
# an administrator has already used locally, or that another provider owns, is
# left alone and reported in the log rather than adopted.

# The issuer URL of your provider. Endpoints are read from
# <OIDC_ISSUER_URL>/.well-known/openid-configuration
# Supply OIDC_DISCOVERY_URL instead if discovery is served elsewhere.
# One of the two is required.
OIDC_ISSUER_URL=
OIDC_DISCOVERY_URL=

# Required
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=

# The callback URL, registered with the provider and matching exactly.
# The path is fixed by the server.
# Required
OIDC_REDIRECT_URI=

# The label shown on the sign-in button, as in "Sign in with <name>"
# default: OpenID Connect
OIDC_PROVIDER_NAME=

# Scopes requested from the provider - must include `openid`
# default: openid profile email
OIDC_SCOPE=

# Claim used to derive the SASjs username for a new user, normalised to
# lowercase alphanumerics, max 16 characters
# default: preferred_username (falling back to `sub` when absent)
OIDC_USERNAME_CLAIM=

# Algorithm used to verify the provider's id_token signature - set it to match
# what the provider signs with
# options: [RS256|RS384|RS512|ES256|ES384|ES512|EdDSA] default: RS256
OIDC_SIGNING_ALG=

# Whether a successful sign-in with no SASjs account creates one.
# The first user provisioned becomes an admin; later ones do not.
# options: [true|false] default: true
OIDC_JIT_PROVISION=

# Where the provider sends the browser after the single sign-on session closes.
# Optional - logout returns to the home page when omitted.
OIDC_POST_LOGOUT_REDIRECT_URI=

# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
# If enabled, be sure to also configure the WHITELIST of third party servers.
CORS=

# options: <http://localhost:3000 https://abc.com ...> space separated urls
WHITELIST=

# HELMET Cross Origin Embedder Policy
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
# options: [true|false] default: true
# Docs: https://helmetjs.github.io/#reference (`crossOriginEmbedderPolicy`)
HELMET_COEP=

# HELMET Content Security Policy
# Path to a json file containing HELMET `contentSecurityPolicy` directives
# Docs: https://helmetjs.github.io/#reference
#
# The default policy allows no inline scripts and no inline event handlers, so
# a page with an injected <script> tag cannot run it. An application deployed
# on the server that needs inline scripts (many Angular and Data Controller
# builds inject them) loosens the policy with its own config file.
#
# Example config:
# {
#   "img-src": ["'self'", "data:"],
#   "script-src": ["'self'"],
#   "script-src-attr": ["'none'"]
# }
#
# Loosened config for an app that requires inline scripts:
# {
#   "img-src": ["'self'", "data:"],
#   "script-src": ["'self'", "'unsafe-inline'"],
#   "script-src-attr": ["'self'", "'unsafe-inline'"]
# }
HELMET_CSP_CONFIG_PATH=./csp.config.json

# Failed password attempts on the login route are throttled by username.
# Only valid for MODE: server.
#
# The lockout is keyed on the username rather than the IP address: behind a
# reverse proxy every client shares the proxy's address, so an IP-keyed limit
# locks out the whole deployment rather than an attacker.

# Failed attempts against one username before further attempts are refused
# with `429 Too Many Failed Attempts`
# default: 5
MAX_LOGIN_FAILURES=5

# How long the username stays locked out once that threshold is reached
# default: 15
LOGIN_LOCKOUT_MINUTES=15

# Password sign-in for local (database) accounts.
# Set to false to refuse it outright: the stored password is never compared, so
# a local account cannot sign in at all. Accounts that authenticate through a
# provider (eg OIDC) or through LDAP are unaffected. Use it on a deployment
# whose accounts all live in the provider, where the local password is the only
# credential an unauthenticated caller can attack.
# The login screen hides the password form while this is false, so a deployment
# that signs everyone in through a provider shows only the provider's button.
# Cannot be false when AUTH_PROVIDERS is empty, since then no account could
# sign in.
# options: [true|false] default: true
LOCAL_LOGIN_ENABLED=true

# Name of the local admin user, created on startup only when
# ADMIN_PASSWORD_INITIAL is set
# default: `secretuser`
ADMIN_USERNAME=secretuser

# Password for the ADMIN_USERNAME, which is in place until the first login
# There is no default: a default would ship a publicly-known credential.
# In server mode it is required unless an external auth provider is enabled -
# with a provider, leaving it unset seeds no local admin at all and the first
# user to sign in through the provider becomes the administrator.
ADMIN_PASSWORD_INITIAL=

# Specify whether app has to reset the ADMIN_USERNAME's password or not
# Default is NO. Possible options are YES and NO
# If ADMIN_PASSWORD_RESET is YES then the ADMIN_USERNAME will be prompted to change the password from ADMIN_PASSWORD_INITIAL on their next login. This will repeat on every server restart, unless the option is removed / set to NO.
ADMIN_PASSWORD_RESET=NO

# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
LOG_FORMAT_MORGAN=

# This location is for server logs with classical UNIX logrotate behavior
LOG_LOCATION=./sasjs_root/logs

Persisting the Session

The server runs in the foreground and never detaches, so it lives exactly as long as whatever started it - a terminal, a task, a unit. Give it a supervisor for anything other than a workstation session. The full recipes (systemd, launchd, Windows Task Scheduler, WinSW/NSSM, pm2, nohup) are in the deployment docs.

The short version:

  • Linux and macOS: a systemd unit with Restart=on-failure, or a launchd plist with KeepAlive. Set the working directory, because .env and sasjs_root resolve against it.
  • Windows: Task Scheduler at logon, or a WinSW/NSSM service. Set the working directory explicitly - a task's default is C:\Windows\System32.
  • pm2: pm2 start sasjs-server --name sasjs-server, then pm2 startup (which prints a command to run once) and pm2 save. Without those two a reboot starts nothing, and pm2 startup does nothing on Windows - use pm2-installer, Task Scheduler or a service wrapper there.
  • nohup ./api-linux > server.log 2>&1 & plus disown: survives the terminal and nothing else.
  • npx: a launcher, not a supervisor. It exits with the terminal, so for anything supervised install globally and hand sasjs-server to the supervisor.

Whichever you pick, capture stderr. The process exits 1 on a configuration error and on an unhandled rejection, and that line is the diagnosis; a console window that closes takes it with it.

Stopping the server

SIGTERM and SIGINT stop the server in two stages. Listeners stop accepting first, and in-flight requests get up to 5 seconds to finish. Then every session the server is holding is ended: a SAS session waiting for work is handed an endsas; program so SAS terminates itself, and a session that is still running gets 2 seconds to finish, then SIGTERM with 1 second, then SIGKILL with 1 second. The three phases are added, not nested, so no phase can borrow another's time. A session's folder is removed only after its process has gone - if a process survives both signals (uninterruptible I/O), the folder is left in place and an error is logged, because that folder is where the process is still writing.

The worst case is therefore 9 seconds - 5 for the listener close plus 4 for the drain - so give a supervisor 10 seconds before it escalates. That gap is deliberate: a supervisor's SIGKILL cannot be caught, so a shutdown cut short at that point would orphan the very processes this ends, and a SAS session left behind keeps a licence seat busy until its own 15 minute idle ceiling expires. A stop with nothing in flight is far quicker than the ceiling; measure one before raising the value:

  • pm2 waits 1600ms by default, which is shorter than the listener close alone: set kill_timeout: 10000 in the ecosystem file, or start with --kill-timeout 10000.
  • docker waits 10 seconds by default, which is already the right value: --stop-timeout 10 (or stop_grace_period: 10s in a compose file) makes it explicit.
  • systemd waits 90 seconds by default, so a unit needs no change; TimeoutStopSec=10 is still worth setting explicitly.

GET /SASjsApi/info answers without authentication and is the probe to use.

Server Version

The following credentials can be used for the initial connection to SASjs/server. It is highly recommended to change these on first use.

  • CLIENTID: clientID1
  • USERNAME: secretuser
  • PASSWORD: secretpassword

Contributors ✨

Thanks goes to these wonderful people (emoji key):


Saad Jutt

💻 ⚠️

Sabir Hassan

💻 ⚠️

Yury Shkoda

💻 ⚠️

Mihajlo Medjedovic

💻 ⚠️

Allan Bowe

💻 📖

Vladislav Parhomchik

⚠️

Koen Knapen

📓

This project follows the all-contributors specification. Contributions of any kind welcome!

About

Build Apps on Base SAS

Topics

Resources

Contributing

Security policy

Stars

22 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages