@novominteractive/anyware-server-auth
v1.0.0-rc.2
Published
Server-side token signing for the Anyware Serverless Engine (ASE). This package runs in **trusted server environments only** — it holds signing secrets and issues short-lived credentials that the `@novominteractive/anyware-stateless-client` (ASEClient) us
Readme
@novominteractive/anyware-server-auth
Server-side token signing for the Anyware Serverless Engine (ASE). This package runs in trusted server environments only — it holds signing secrets and issues short-lived credentials that the @novominteractive/anyware-stateless-client (ASEClient) uses to connect to the message broker.
Overview
ASEClient is transport-agnostic. Two transports are supported, each with its own credential format:
| Transport | Credential format | Signing algorithm |
|-----------|------------------|-------------------|
| aws-iot | Standard JWT (HS256) | HMAC-SHA256 with a shared secret |
| nats | NATS User JWT v2 + NKey seed (.creds format) | Ed25519 via account signing key |
Your server fetches a token from this package, returns it to the client over a secured endpoint, and the client uses it to connect directly to the broker. The signing secrets never leave your server.
Installation
npm install @novominteractive/anyware-server-auth
# or
yarn add @novominteractive/anyware-server-authQuick start
Unified API
Use signASEToken when your endpoint needs to support multiple transports without branching:
import { signASEToken } from '@novominteractive/anyware-server-auth';
// AWS IoT
const awsToken = await signASEToken({
transport: 'aws-iot',
payload: { subscribeTopics: ['myApp/events'] },
config: {
secretKey: process.env.SECRET_KEY!,
licenseKey: 'my-license-key',
},
});
// NATS
const natsToken = await signASEToken({
transport: 'nats',
payload: { subscribeTopics: ['myApp/events'] },
config: {
signingKeySeed: process.env.NATS_SIGNING_SEED!,
accountPublicKey: process.env.NATS_ACCOUNT_PUBLIC_KEY!,
licenseKey: 'my-license-key',
},
});Transport-specific functions
Call signAwsIotToken or signNatsToken directly when you know the transport at compile time:
import { signAwsIotToken, signNatsToken } from '@novominteractive/anyware-server-auth';
const token = await signNatsToken(
{ publishTopics: ['myApp/commands'], subscribeTopics: ['myApp/events'] },
{
signingKeySeed: process.env.NATS_SIGNING_SEED!,
accountPublicKey: process.env.NATS_ACCOUNT_PUBLIC_KEY!,
licenseKey: 'my-license-key',
},
);API
signASEToken(options)
Unified signing function that dispatches to the appropriate transport signer.
function signASEToken(options: SignTokenOptions): Promise<string>options is a discriminated union — set transport to select the signer and provide the matching config:
type SignTokenOptions =
| { transport: 'nats'; payload: TokenPayload; config: NatsSigningConfig }
| { transport: 'aws-iot'; payload: TokenPayload; config: AwsIotSigningConfig }signNatsToken(payload, config)
Signs a NATS User JWT v2 and returns it in the standard .creds file format (JWT + NKey seed), which ASEClient passes directly to credsAuthenticator.
function signNatsToken(payload: TokenPayload, config: NatsSigningConfig): Promise<string>A fresh ephemeral user keypair is generated for every call. The returned credential is single-use by convention — issue one per client session.
Config:
interface NatsSigningConfig {
/** Ed25519 seed of the account signing key (starts with "SA"). */
signingKeySeed: string;
/** Public key of the NATS account (starts with "A"). */
accountPublicKey: string;
/** License key — used as the subject namespace prefix. */
licenseKey: string;
}Subject namespacing: every topic is prefixed with <licenseKey>. and MQTT-style wildcards are translated to NATS syntax (* → >). For example, myApp/events becomes my-license-key.myApp.events.
signAwsIotToken(payload, config)
Signs a standard HS256 JWT that the AWS IoT custom authorizer verifies.
function signAwsIotToken(payload: TokenPayload, config: AwsIotSigningConfig): Promise<string>Config:
interface AwsIotSigningConfig {
/** Shared secret used to sign the JWT (HS256). */
secretKey: string;
/** License key — set as the JWT `iss` (issuer) claim. */
licenseKey: string;
}TokenPayload
Controls what the connecting client is allowed to do. Omitting a field leaves that permission unrestricted.
interface TokenPayload {
/** Topics the client may publish to. */
publishTopics?: string[];
/** Topics the client may subscribe to. */
subscribeTopics?: string[];
/** Whether to enable last-will functionality. */
lastWill?: boolean;
/** Token lifetime in seconds. When omitted the token never expires. */
ttlSeconds?: number;
}Topic syntax: use / as a separator and * as a wildcard (same convention as MQTT). The library translates this to the correct format for each transport.
// Short-lived subscriber token (1 hour)
{
subscribeTopics: ['myApp/events'],
ttlSeconds: 3600,
}
// Separate publish and subscribe permissions
{
publishTopics: ['myApp/commands'],
subscribeTopics: ['myApp/events', 'myApp/status'],
ttlSeconds: 3600,
}Security notes
- Never expose signing credentials to clients.
signingKeySeedandsecretKeymust stay server-side. - Issue short-lived tokens. Neither signer sets an expiry by default; add your own TTL at the token-endpoint layer (e.g., refuse to reuse a token after N minutes).
- Scope permissions tightly. A subscriber-only client should receive a token with
subscribeTopicsset and nopublishTopics. A publisher should receive the inverse. - NATS tokens include the user NKey seed. The seed is transmitted to the client (it's required for the NATS auth challenge-response). This is by design — mitigate exposure with short session lifetimes and HTTPS transport.
Environment variables (recommended)
# AWS IoT transport
SECRET_KEY=<your-hs256-secret>
# NATS transport
NATS_SIGNING_SEED=SA... # account signing key seed
NATS_ACCOUNT_PUBLIC_KEY=A... # account public key