@sutando/keeper
v0.1.0
Published
Lightweight API token authentication plugin for Sutando ORM.
Maintainers
Readme
@sutando/keeper
🛡️ A lightweight authentication & API token plugin for Sutando ORM, inspired by Laravel Sanctum.
@sutando/keeper provides sessionless, token-based authentication for modern applications including SPAs, mobile clients, and traditional backends.
✨ Features
- 🔐 Personal access tokens scoped to users
- ⚙️ Extensible and database-agnostic
- 🧩 Compatible with any Sutando Model (e.g.
User)
📦 Installation
npm install @sutando/keeperOr using pnpm:
pnpm add @sutando/keeperAuto create migration file and run migration
sutando migrate:publish @sutando/keeper
sutando migrate:runawait sutando.connection().schema.createTable('personal_access_tokens', (table) => {
table.increments('id');
table.string('tokenable_type').index();
table.integer('tokenable_id').index();
table.string('name');
table.string('token', 64).unique();
table.string('abilities').nullable();
table.datetime('last_used_at').nullable();
table.datetime('expires_at').nullable();
table.timestamps();
table.index(['tokenable_type', 'tokenable_id'], 'tokenable_index');
});🔐 Usage
Setup
import { HasApiTokens, PersonalAccessToken } from '@sutando/keeper'
import { sutando, Model } from 'sutando'
class User extends HasApiTokens()(Model) {
// your model definition
}Issue Token
const user = await User.query().find(1);
const token = await user.createToken('mobile-app');
// Issue token with abilities
const token = await user.createToken('admin', ['read', 'write']);
// Issue token with expiration date
const token = await user.createToken(
'mobile-app', ['read', 'write'], new Date(Date.now() + 7 * 86400000);
);Validate Token
const user = await User.findByToken(tokenString);
if (user.tokenCan('read')) {
// Access granted
}Revoking Tokens
// Revoke all tokens...
await user.tokens().delete();
// Revoke the token that was used to authenticate the current request...
await user.currentAccessToken().delete();
// Revoke a specific token...
await user.tokens().where('id', tokenId).delete();🧪 Example with Hono
import User from './models/user'
import { bearerAuth } from 'hono/bearer-auth'
const auth = (ability?: string) =>
bearerAuth({
verifyToken: async (token, c) => {
const user = await User.findByToken(token)
if (!user || (ability && user.tokenCant(ability))) {
return false
}
c.set('user', user)
return true
},
})
app.post('/tokens/create', async (c) => {
const user = await User.query().find(1)
const token = await user.createToken('mobile-app')
return c.json({ token: token.plainTextToken })
})
app.get('/api/user', auth(), async (c) => {
const user = c.get('user')
return c.json(user)
})
app.get('/admin', auth('write'), handler)📌 API Reference
HasApiTokens(options)
accessTokenModel: Model used for token storage (optional, default:PersonalAccessToken)token_prefix: Prefix for token string (optional, default:'')type: Token type (optional, default: Model name)separator: Separator for returned token string (optional, default:|)
PersonalAccessToken
personalAccessToken.findToken(token: string): Promise<PersonalAccessToken | null>personalAccessToken.can(ability: string): booleanpersonalAccessToken.cant(ability: string): boolean
NewAccessToken
newAccessToken.accessToken: PersonalAccessTokennewAccessToken.plainTextToken: string
User Model Extensions
createToken(name: string, abilities?: string[], expires_at?: Date | string): Promise<NewAccessToken>findByToken(token: string, last_used_at?: Date | string): Promise<User | null>tokenCan(ability: string): booleantokenCant(ability: string): boolean
🔐 Security Notes
All tokens are stored as SHA-256 hashes, ensuring they cannot be reverse-engineered if leaked.
📄 License
MIT © Kidd Yu
