Resonate token authentication typescript
Skill resonatehq/resonate-skills/resonate-token-authentication-typescript
Agent skills for building with Resonate — durable execution for long-running, crash-safe workflows.
npx -y skills add resonatehq/resonate-skills --skill resonate-token-authentication-typescriptAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Secure Resonate applications with JWT-based authentication and prefix-based authorization. Use for multi-tenant systems, role-based access control, and isolating workers or services from accessing each other's promises.
The file declares its own license as Apache-2.0. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
13.7 KB, as published. Nobody here has run it
Resonate Token Authentication & Authorization (TypeScript)
Overview
Resonate supports JWT-based authentication and prefix-based authorization to secure access to the Resonate server and control which promises clients can access. This enables multi-tenant systems, role-based access control, and isolation between different workers or services.
Core principle: Generate JWT tokens signed with a private key, configure the Resonate server with the public key, and clients present tokens to authenticate. Optionally use the prefix claim for fine-grained authorization.
Mental Model
Without Auth:
Client → Resonate Server → All Promises Accessible
With Token Auth:
Client + Valid JWT → Resonate Server → All Promises Accessible
Client + Invalid JWT → Resonate Server → REJECTED (401 Unauthorized)
With Prefix Auth (Multi-Tenant):
Client + JWT(prefix="tenant-1") → Access only "tenant-1:*" promises
Client + JWT(prefix="tenant-2") → Access only "tenant-2:*" promises
Client + JWT(prefix="worker-a") → Access only "worker-a:*" promises
Setup: Generate Keys and Tokens
1. Generate RSA Key Pair
# Generate private key (keep this secret!)
openssl genrsa -out private_key.pem 2048
# Extract public key (share with Resonate server)
openssl rsa -in private_key.pem -pubout -out public_key.pem
Security:
- Store
private_key.pemsecurely (never commit to git, use secrets manager) public_key.pemcan be deployed with server configuration
2. Install JWT CLI Tool
# macOS
brew install mike-engel/jwt-cli/jwt-cli
# Other platforms
# https://github.com/mike-engel/jwt-cli#installation
3. Generate JWT Tokens
Basic authentication token (no prefix):
jwt encode --secret @private_key.pem -A RS256 '{}'
Token with prefix claim (for authorization):
jwt encode --secret @private_key.pem -A RS256 '{"prefix":"tenant-1"}'
Store token in environment variable:
export MY_TOKEN=$(jwt encode --secret @private_key.pem -A RS256 '{"prefix":"worker-1"}')
Server Configuration
Start Resonate Server with Authentication
# Enable JWT authentication
resonate dev --auth-publickey public_key.pem
What this does:
- Enables JWT authentication on all HTTP API endpoints
- Validates JWT signatures using the provided public key
- Rejects requests with invalid or missing tokens (401 Unauthorized)
- Enforces prefix-based authorization if
prefixclaim is present
Production Deployment
# Using Docker
docker run -v $(pwd)/public_key.pem:/keys/public_key.pem \
resonatehq/resonate \
serve --auth-publickey /keys/public_key.pem
# Using systemd service (see `resonate-server-deployment`)
resonate serve \
--auth-publickey /etc/resonate/public_key.pem \
--storage-type postgres \
--storage-postgres-url postgres://user:[email protected]/resonate
For fully worked deploys of the server with auth wired in, see resonate-server-deployment (Linux/systemd) or resonate-server-deployment-cloud-run (GCP Cloud Run + Cloud SQL).
Pattern 1: Basic Token Authentication
Use when: You want to restrict access to trusted clients only, but all authenticated clients have equal access.
import { Resonate, Context } from "@resonatehq/sdk";
function* helloAuth(ctx: Context, greeting: string) {
const result = yield* ctx.run((ctx: Context) => {
return `${greeting} world!`;
});
return result;
}
// Authenticated client
const resonate = new Resonate({
url: "http://localhost:8001",
token: process.env.MY_TOKEN // JWT token from environment
});
// Register and run workflows
const workflow = resonate.register("workflow", helloAuth);
const result = await workflow.run("workflow.id", "hello");
console.log(result); // "hello world!"
resonate.stop();
Without token:
// ❌ This will fail with "ResonateError: The request is unauthorized"
const resonateNoAuth = new Resonate({
url: "http://localhost:8001"
// No token provided
});
const workflow = resonateNoAuth.register("workflow", helloAuth);
await workflow.run("workflow.id", "hello"); // THROWS 401 ERROR
Pattern 2: Prefix-Based Authorization (Multi-Tenant)
Use when: You need to isolate promises between tenants, workers, or services.
Server-Side Prefix Enforcement
When a client connects with a token containing a prefix claim, the Resonate server enforces that the client can only:
- Create promises with IDs starting with that prefix
- Access promises with IDs starting with that prefix
Generate tenant-specific tokens:
# Token for Tenant 1
export TENANT1_TOKEN=$(jwt encode -S @private_key.pem -A RS256 '{"prefix":"tenant-1"}')
# Token for Tenant 2
export TENANT2_TOKEN=$(jwt encode -S @private_key.pem -A RS256 '{"prefix":"tenant-2"}')
# Token for Worker A
export WORKER_A_TOKEN=$(jwt encode -S @private_key.pem -A RS256 '{"prefix":"worker-a"}')
Client with Automatic Prefix
// Tenant 1 client
const tenant1 = new Resonate({
url: "http://localhost:8001",
token: process.env.TENANT1_TOKEN,
prefix: "tenant-1" // SDK automatically prefixes all promise IDs
});
const workflow1 = tenant1.register("workflow", processOrder);
// Creates promise with ID "tenant-1:order-123"
await workflow1.run("order-123", orderData);
// Tenant 2 client (isolated from Tenant 1)
const tenant2 = new Resonate({
url: "http://localhost:8001",
token: process.env.TENANT2_TOKEN,
prefix: "tenant-2"
});
const workflow2 = tenant2.register("workflow", processOrder);
// Creates promise with ID "tenant-2:order-123"
await workflow2.run("order-123", orderData);
// Tenant 1 cannot access Tenant 2's promises (and vice versa)
Client with Manual Prefix
const resonate = new Resonate({
url: "http://localhost:8001",
token: process.env.TENANT1_TOKEN
// No prefix set in SDK
});
const workflow = resonate.register("workflow", processOrder);
// Manually prefix the promise ID
await workflow.run("tenant-1:order-123", orderData);
Why manual prefixing?
- More control over promise ID structure
- Can use different prefixes for different workflows
- Useful for migration or complex ID schemes
Pattern 3: Multi-Worker Isolation
Use when: Multiple worker groups should not interfere with each other's promises.
// Worker Group A
const workerA = new Resonate({
url: "http://localhost:8001",
token: process.env.WORKER_A_TOKEN,
prefix: "worker-a",
group: "workers-a"
});
workerA.register("processTask", processTask);
// Worker Group B (isolated from A)
const workerB = new Resonate({
url: "http://localhost:8001",
token: process.env.WORKER_B_TOKEN,
prefix: "worker-b",
group: "workers-b"
});
workerB.register("processTask", processTask);
// Worker A can only claim tasks prefixed with "worker-a:"
// Worker B can only claim tasks prefixed with "worker-b:"
Pattern 4: Environment Variable Configuration
Use when: You want to avoid hardcoding credentials in source code.
// Set environment variables
// RESONATE_TOKEN=<jwt-token>
// RESONATE_PREFIX=tenant-1
// SDK automatically reads from environment
const resonate = new Resonate({
url: "http://localhost:8001"
// Token and prefix read from RESONATE_TOKEN and RESONATE_PREFIX
});
const workflow = resonate.register("workflow", myWorkflow);
await workflow.run("order-123", data); // Creates "tenant-1:order-123"
Environment variables:
RESONATE_TOKEN: JWT tokenRESONATE_PREFIX: Promise ID prefix
Pattern 5: Role-Based Access Control (RBAC)
Use when: Different roles need different levels of access.
# Admin token (access all promises)
export ADMIN_TOKEN=$(jwt encode -S @private_key.pem -A RS256 '{}')
# Service token (access only service-specific promises)
export SERVICE_TOKEN=$(jwt encode -S @private_key.pem -A RS256 '{"prefix":"service-api"}')
# User token (access only user-specific promises)
export USER_TOKEN=$(jwt encode -S @private_key.pem -A RS256 '{"prefix":"user-${userId}"}')
Client code:
// Admin client (no prefix restriction)
const admin = new Resonate({
url: "http://localhost:8001",
token: process.env.ADMIN_TOKEN
});
// Can access any promise ID
await admin.promises.get("tenant-1:order-123");
await admin.promises.get("tenant-2:order-456");
// Service client (restricted to service-api:* promises)
const service = new Resonate({
url: "http://localhost:8001",
token: process.env.SERVICE_TOKEN,
prefix: "service-api"
});
// Can only access "service-api:*" promises
await service.promises.get("request-123"); // OK (becomes "service-api:request-123")
Token Claims and Validation
Required claims:
- None (empty JWT
{}is valid for basic authentication)
Optional claims:
prefix(string): Restricts promise access to IDs starting with this prefixexp(number): Token expiration timestamp (Unix epoch)iat(number): Token issued-at timestamp
Token validation:
# Generate token with expiration (1 hour)
jwt encode -S @private_key.pem -A RS256 \
--exp='+1 hour' \
'{"prefix":"tenant-1"}'
# Decode and verify token
jwt decode -S @public_key.pem $MY_TOKEN
Security Best Practices
1. Protect Private Keys
# ❌ WRONG - Committing keys to git
git add private_key.pem # NEVER DO THIS
# ✅ CORRECT - Use secrets manager
aws secretsmanager create-secret \
--name resonate-jwt-private-key \
--secret-string file://private_key.pem
# ✅ CORRECT - Add to .gitignore
echo "*.pem" >> .gitignore
echo "*.key" >> .gitignore
2. Rotate Keys Periodically
# Generate new key pair
openssl genrsa -out private_key_v2.pem 2048
openssl rsa -in private_key_v2.pem -pubout -out public_key_v2.pem
# Update server configuration
resonate serve --auth-publickey public_key_v2.pem
# Issue new tokens with new private key
jwt encode -S @private_key_v2.pem -A RS256 '{"prefix":"tenant-1"}'
3. Use Token Expiration
# Generate short-lived tokens (1 hour)
jwt encode -S @private_key.pem -A RS256 --exp='+1 hour' '{}'
# Generate long-lived tokens (30 days)
jwt encode -S @private_key.pem -A RS256 --exp='+30 days' '{}'
4. Least Privilege Prefixes
// ✅ GOOD - Narrow prefix
const client = new Resonate({
token: token,
prefix: "user-123" // Can only access user-123's promises
});
// ❌ BAD - Wide prefix
const client = new Resonate({
token: token,
prefix: "user" // Can access all users' promises (user-*, user-123, etc.)
});
Production Recommendations
1. Use External Identity Provider
For production systems, integrate with identity providers like:
- Keycloak: Open-source identity and access management
- Auth0: Managed authentication service
- Okta: Enterprise identity platform
- AWS Cognito: AWS-managed identity service
2. Token Refresh Strategy
// Implement token refresh before expiration
let token = await getInitialToken();
let resonate = new Resonate({ url: serverUrl, token });
// Refresh token periodically
setInterval(async () => {
token = await refreshToken(token);
resonate = new Resonate({ url: serverUrl, token });
}, 30 * 60 * 1000); // Refresh every 30 minutes
3. Audit Logging
// Log authentication events
resonate.on("error", (error) => {
if (error.message.includes("unauthorized")) {
logger.warn("Authentication failed", {
timestamp: Date.now(),
error: error.message
});
}
});
Common Pitfalls
1. Forgetting to Configure Server
# ❌ WRONG - Server not configured for auth
resonate dev # No --auth-publickey flag
# Client with token connects but auth is not enforced
# ✅ CORRECT - Server configured for auth
resonate dev --auth-publickey public_key.pem
2. Mismatched Prefix in Token and SDK
// Token has prefix="tenant-1"
const token = generateToken({ prefix: "tenant-1" });
// ❌ WRONG - SDK prefix doesn't match token
const resonate = new Resonate({
token: token,
prefix: "tenant-2" // Server will reject requests
});
// ✅ CORRECT - SDK prefix matches token
const resonate = new Resonate({
token: token,
prefix: "tenant-1" // Matches token claim
});
3. Exposing Tokens in Logs
// ❌ WRONG - Token exposed in logs
console.log(`Token: ${process.env.MY_TOKEN}`);
// ✅ CORRECT - Redact tokens
console.log(`Token: ${process.env.MY_TOKEN?.slice(0, 10)}...`);
Decision Tree
Use token authentication when:
- Restricting access to trusted clients only
- Deploying to production environments
- Exposing Resonate server over public networks
Use prefix-based authorization when:
- Multi-tenant systems (isolate tenant data)
- Role-based access control
- Isolating worker groups
- Implementing data access policies
Don't use authentication when:
- Local development (use
resonate devwithout flags) - Internal private networks (though still recommended)
- Single-tenant systems with trusted clients
Summary
Token authentication and prefix-based authorization enable:
- Secure access control with JWT tokens
- Multi-tenant isolation via prefix claims
- Role-based access control
- Fine-grained promise access policies
- Integration with external identity providers
Core recipe: Generate RSA key pair → Configure server with public key → Generate JWT tokens with prefix claims → Clients authenticate with tokens → Server enforces prefix-based access control