Framework-agnostic server package for signing Cuttlefish queries with EdDSA (Ed25519) JWTs. Use it anywhere you can run Node.js to authorize live SQL subscriptions against Cuttlefish.
npm install @cuttlefish-sync/query-token
import { signQuery } from '@cuttlefish-sync/query-token'
const { token, expires_in } = await signQuery({
sql: 'SELECT * FROM users WHERE id = $1',
params: [123],
user_id: 'user_456',
})
// Use token to connect your client WebSocket before expires_in seconds pass
Set these values in your server environment:
CUTTLEFISH_CLIENT_ID=your_client_id
CUTTLEFISH_PRIVATE_KEY='{"kty":"OKP","crv":"Ed25519",...}'
CUTTLEFISH_UPSTREAM_ID=your_upstream_id
CUTTLEFISH_EXPIRY_SECONDS=60 # optional override
import { generateKeyPair, exportJWK } from 'jose'
const { privateKey, publicKey } = await generateKeyPair('EdDSA', { crv: 'Ed25519' })
const privateJWK = await exportJWK(privateKey)
const publicJWK = await exportJWK(publicKey)
console.log('Private Key (keep secret):', JSON.stringify(privateJWK))
console.log('Public Key (upload to Cuttlefish):', JSON.stringify(publicJWK))
client_id.await signQuery({
sql: 'SELECT 1',
params: [],
client_id: 'override_client',
private_key: '{"kty":"OKP","crv":"Ed25519",...}',
upstream_id: 'custom_upstream',
expiry_seconds: 120,
})
signQuery(params: SignQueryParams): Promise<SignQueryResult>Signs a SQL query and returns an EdDSA JWT token.
Required parameters
sql: SQL query string using positional parameters ($1, $2, ...)params: Array of parameter valuesOptional parameters
user_id: Defaults to 'anonymous'client_id, private_key, upstream_id, expiry_seconds: Override environment defaultsReturn value
{
token: string
expires_in: number
}
Errors
CUTTLEFISH_CLIENT_ID is required)expiry_secondsexamples/basic.ts)import { signQuery } from '@cuttlefish-sync/server'
async function main() {
const result = await signQuery({
sql: 'SELECT * FROM todos WHERE user_id = $1',
params: ['user_123'],
user_id: 'user_123',
})
console.log('Token:', result.token)
console.log('Expires in:', result.expires_in, 'seconds')
}
main().catch((error) => {
console.error('Failed to sign query:', error)
process.exitCode = 1
})
examples/nextjs-server-action.ts)'use server'
import { signQuery } from '@cuttlefish-sync/query-token'
import { auth } from '@/lib/auth'
export async function signTodosQuery() {
const session = await auth()
if (!session?.user) {
throw new Error('Unauthorized')
}
return await signQuery({
sql: `
SELECT id, title, completed, created_at
FROM todos
WHERE user_id = $1
ORDER BY created_at DESC
`,
params: [session.user.id],
user_id: session.user.id,
expiry_seconds: 60,
})
}
import express from 'express'
import { signQuery } from '@cuttlefish-sync/query-token'
const app = express()
app.post('/api/sign-query', async (req, res) => {
const user = req.user
if (!user) {
res.status(401).json({ error: 'Unauthorized' })
return
}
const result = await signQuery({
sql: 'SELECT * FROM messages WHERE org_id = $1',
params: [user.orgId],
user_id: user.id,
})
res.json(result)
})
// Short-lived token (30s)
await signQuery({
sql: 'SELECT * FROM sensitive_data',
params: [],
expiry_seconds: 30,
})
// Longer window (5m)
await signQuery({
sql: 'SELECT * FROM public_posts',
params: [],
expiry_seconds: 300,
})
await signQuery({
sql: 'SELECT * FROM public_posts LIMIT 10',
params: [],
// user_id defaults to 'anonymous'
})
iss: client_idaud: upstream_idsub: user identifier (or 'anonymous')exp: expiry (seconds)nbf: not-before (seconds)iat: issued-at (seconds)jti: random UUID per tokenquery: { sql, params }jtisubimport type {
Config,
SignQueryParams,
SignQueryResult,
} from '@cuttlefish-sync/query-token'
FSL-1.1-Apache-2.0