https://github.com/sutandojs/keeper
Lightweight API token authentication plugin for Sutando ORM
https://github.com/sutandojs/keeper
Last synced: 10 months ago
JSON representation
Lightweight API token authentication plugin for Sutando ORM
- Host: GitHub
- URL: https://github.com/sutandojs/keeper
- Owner: sutandojs
- Created: 2025-06-27T18:09:15.000Z (12 months ago)
- Default Branch: main
- Last Pushed: 2025-06-27T18:48:36.000Z (12 months ago)
- Last Synced: 2025-07-28T18:55:32.055Z (11 months ago)
- Language: JavaScript
- Size: 4.88 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# @sutando/keeper
> ๐ก๏ธ A lightweight authentication & API token plugin for [Sutando ORM](https://sutando.org), 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
```bash
npm install @sutando/keeper
```
Or using pnpm:
```bash
pnpm add @sutando/keeper
```
Auto create migration file and run migration
```bash
sutando migrate:publish @sutando/keeper
sutando migrate:run
```
Alternatively, you can create the token table manually
```js
await 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
```ts
import { HasApiTokens, PersonalAccessToken } from '@sutando/keeper'
import { sutando, Model } from 'sutando'
class User extends HasApiTokens()(Model) {
// your model definition
}
```
### Issue Token
```ts
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
```ts
const user = await User.findByToken(tokenString);
if (user.tokenCan('read')) {
// Access granted
}
```
### Revoking Tokens
```ts
// 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
```ts
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.can(ability: string): boolean`
- `personalAccessToken.cant(ability: string): boolean`
### `NewAccessToken`
- `newAccessToken.accessToken: PersonalAccessToken`
- `newAccessToken.plainTextToken: string`
### User Model Extensions
- `createToken(name: string, abilities?: string[], expires_at?: Date | string): Promise`
- `findByToken(token: string, last_used_at?: Date | string): Promise`
- `tokenCan(ability: string): boolean`
- `tokenCant(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