@bloomneo/appkit 1.2.9 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +195 -0
- package/CHANGELOG.md +253 -0
- package/README.md +147 -799
- package/bin/commands/generate.js +7 -7
- package/cookbook/README.md +26 -0
- package/cookbook/api-key-service.ts +106 -0
- package/cookbook/auth-protected-crud.ts +112 -0
- package/cookbook/file-upload-pipeline.ts +113 -0
- package/cookbook/multi-tenant-saas.ts +87 -0
- package/cookbook/real-time-chat.ts +121 -0
- package/dist/auth/auth.d.ts +21 -4
- package/dist/auth/auth.d.ts.map +1 -1
- package/dist/auth/auth.js +56 -44
- package/dist/auth/auth.js.map +1 -1
- package/dist/auth/defaults.d.ts +1 -1
- package/dist/auth/defaults.js +35 -35
- package/dist/cache/cache.d.ts +29 -6
- package/dist/cache/cache.d.ts.map +1 -1
- package/dist/cache/cache.js +72 -44
- package/dist/cache/cache.js.map +1 -1
- package/dist/cache/defaults.js +29 -29
- package/dist/cache/index.d.ts +19 -10
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +21 -18
- package/dist/cache/index.js.map +1 -1
- package/dist/config/defaults.d.ts +1 -1
- package/dist/config/defaults.js +11 -11
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.js +4 -4
- package/dist/database/adapters/mongoose.d.ts +4 -4
- package/dist/database/adapters/mongoose.js +7 -7
- package/dist/database/adapters/prisma.d.ts +4 -4
- package/dist/database/adapters/prisma.js +7 -7
- package/dist/database/defaults.d.ts +1 -1
- package/dist/database/defaults.js +4 -4
- package/dist/database/index.js +2 -2
- package/dist/database/index.js.map +1 -1
- package/dist/email/defaults.js +26 -26
- package/dist/email/index.js +7 -7
- package/dist/email/strategies/resend.js +1 -1
- package/dist/error/defaults.d.ts +1 -1
- package/dist/error/defaults.js +13 -13
- package/dist/error/error.d.ts +12 -0
- package/dist/error/error.d.ts.map +1 -1
- package/dist/error/error.js +19 -0
- package/dist/error/error.js.map +1 -1
- package/dist/error/index.d.ts +14 -3
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +14 -3
- package/dist/error/index.js.map +1 -1
- package/dist/event/defaults.js +35 -35
- package/dist/event/index.js +7 -7
- package/dist/logger/defaults.d.ts +1 -1
- package/dist/logger/defaults.js +40 -40
- package/dist/logger/index.d.ts +1 -0
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/logger/logger.d.ts +8 -0
- package/dist/logger/logger.d.ts.map +1 -1
- package/dist/logger/logger.js +13 -3
- package/dist/logger/logger.js.map +1 -1
- package/dist/logger/transports/console.js +2 -2
- package/dist/logger/transports/http.d.ts +1 -1
- package/dist/logger/transports/http.js +2 -2
- package/dist/logger/transports/webhook.d.ts +1 -1
- package/dist/logger/transports/webhook.js +3 -3
- package/dist/queue/defaults.d.ts +2 -2
- package/dist/queue/defaults.js +38 -38
- package/dist/security/defaults.d.ts +1 -1
- package/dist/security/defaults.js +30 -30
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +3 -3
- package/dist/security/security.d.ts +1 -1
- package/dist/security/security.js +4 -4
- package/dist/storage/defaults.js +26 -26
- package/dist/storage/index.js +3 -3
- package/dist/util/defaults.d.ts +1 -1
- package/dist/util/defaults.js +41 -41
- package/dist/util/env.d.ts +35 -0
- package/dist/util/env.d.ts.map +1 -0
- package/dist/util/env.js +50 -0
- package/dist/util/env.js.map +1 -0
- package/dist/util/errors.d.ts +52 -0
- package/dist/util/errors.d.ts.map +1 -0
- package/dist/util/errors.js +82 -0
- package/dist/util/errors.js.map +1 -0
- package/dist/util/util.js +1 -1
- package/examples/.env.example +80 -0
- package/examples/README.md +16 -0
- package/examples/auth.ts +228 -0
- package/examples/cache.ts +36 -0
- package/examples/config.ts +45 -0
- package/examples/database.ts +69 -0
- package/examples/email.ts +53 -0
- package/examples/error.ts +50 -0
- package/examples/event.ts +42 -0
- package/examples/logger.ts +41 -0
- package/examples/queue.ts +58 -0
- package/examples/security.ts +46 -0
- package/examples/storage.ts +44 -0
- package/examples/util.ts +47 -0
- package/llms.txt +591 -0
- package/package.json +19 -10
- package/src/auth/README.md +850 -0
- package/src/cache/README.md +756 -0
- package/src/config/README.md +604 -0
- package/src/database/README.md +818 -0
- package/src/email/README.md +759 -0
- package/src/error/README.md +660 -0
- package/src/event/README.md +729 -0
- package/src/logger/README.md +435 -0
- package/src/queue/README.md +851 -0
- package/src/security/README.md +612 -0
- package/src/storage/README.md +1008 -0
- package/src/util/README.md +955 -0
- package/bin/templates/backend/docs/APPKIT_CLI.md +0 -507
- package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +0 -61
- package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +0 -2539
package/llms.txt
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
# @bloomneo/appkit — full reference
|
|
2
|
+
|
|
3
|
+
> Machine-readable API reference for AI coding agents.
|
|
4
|
+
> For **rules** (what to always/never do), read [`AGENTS.md`](./AGENTS.md) in this directory first.
|
|
5
|
+
> Both files ship with the package at `node_modules/@bloomneo/appkit/`.
|
|
6
|
+
|
|
7
|
+
## Setup (every project)
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// 1. Install
|
|
11
|
+
// npm install @bloomneo/appkit
|
|
12
|
+
|
|
13
|
+
// 2. Set env vars in .env (minimum)
|
|
14
|
+
// BLOOM_AUTH_SECRET=<min 32 chars>
|
|
15
|
+
// DATABASE_URL=postgresql://...
|
|
16
|
+
|
|
17
|
+
// 3. Import what you need (the canonical pattern is one xxxClass per module)
|
|
18
|
+
import {
|
|
19
|
+
authClass,
|
|
20
|
+
databaseClass,
|
|
21
|
+
errorClass,
|
|
22
|
+
loggerClass,
|
|
23
|
+
cacheClass,
|
|
24
|
+
storageClass,
|
|
25
|
+
queueClass,
|
|
26
|
+
emailClass,
|
|
27
|
+
eventClass,
|
|
28
|
+
configClass,
|
|
29
|
+
securityClass,
|
|
30
|
+
utilClass,
|
|
31
|
+
} from '@bloomneo/appkit';
|
|
32
|
+
|
|
33
|
+
// 4. Get instances at module scope (NEVER inside route handlers)
|
|
34
|
+
const auth = authClass.get();
|
|
35
|
+
const database = await databaseClass.get();
|
|
36
|
+
const error = errorClass.get();
|
|
37
|
+
const logger = loggerClass.get('my-app');
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Universal pattern
|
|
41
|
+
|
|
42
|
+
Every module is `xxxClass.get()`. There are **no exceptions**, no constructors,
|
|
43
|
+
no factories with custom names. If you remember one rule, it's this one.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const auth = authClass.get(); // synchronous
|
|
47
|
+
const database = await databaseClass.get(); // async (initializes connection)
|
|
48
|
+
const cache = cacheClass.get('users'); // optional namespace param
|
|
49
|
+
const logger = loggerClass.get('api'); // optional component param
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Module 1 — Auth (`@bloomneo/appkit/auth`)
|
|
55
|
+
|
|
56
|
+
JWT tokens, role.level permissions, Express middleware. Two token types:
|
|
57
|
+
**login tokens** (humans) and **API tokens** (services/webhooks).
|
|
58
|
+
|
|
59
|
+
### Methods (verified by src/auth/auth.test.ts — 47/47 passing)
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const auth = authClass.get();
|
|
63
|
+
|
|
64
|
+
// Token generation — these are the ONLY two public token-creation methods.
|
|
65
|
+
// (`signToken` is a private internal — never call it from consumer code.)
|
|
66
|
+
auth.generateLoginToken(payload, expiresIn?: string): string
|
|
67
|
+
// payload: { userId: number, role: string, level: string, permissions?: string[] }
|
|
68
|
+
// default expiresIn: '7d'
|
|
69
|
+
|
|
70
|
+
auth.generateApiToken(payload, expiresIn?: string): string
|
|
71
|
+
// payload: { keyId: string, role: string, level: string, permissions?: string[] }
|
|
72
|
+
// default expiresIn: '1y'
|
|
73
|
+
|
|
74
|
+
// Token verification — handles BOTH login and API tokens. Throws on bad token.
|
|
75
|
+
auth.verifyToken(token: string): JwtPayload
|
|
76
|
+
|
|
77
|
+
// Password hashing
|
|
78
|
+
auth.hashPassword(plain: string, rounds?: number): Promise<string>
|
|
79
|
+
auth.comparePassword(plain: string, hash: string): Promise<boolean> // never throws
|
|
80
|
+
|
|
81
|
+
// Express middleware factories — return ExpressMiddleware functions
|
|
82
|
+
auth.requireLoginToken(options?): ExpressMiddleware // user authentication
|
|
83
|
+
auth.requireApiToken(options?): ExpressMiddleware // API token authentication
|
|
84
|
+
auth.requireUserRoles(['admin.tenant']): ExpressMiddleware // role check (OR within array)
|
|
85
|
+
auth.requireUserRoles(['admin.tenant', 'admin.org']): ExpressMiddleware
|
|
86
|
+
auth.requireUserPermissions(['manage:users']): ExpressMiddleware // permission check (AND within array)
|
|
87
|
+
|
|
88
|
+
// Request helpers
|
|
89
|
+
auth.user(req: ExpressRequest): JwtPayload | null // null-safe extract from req.user OR req.token
|
|
90
|
+
auth.hasRole(userRoleLevel: string, requiredRoleLevel: string): boolean // inheritance-aware
|
|
91
|
+
auth.can(user: JwtPayload, permission: string): boolean // permission inheritance
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Middleware chaining rules
|
|
95
|
+
|
|
96
|
+
- `requireLoginToken()` MUST come first on user routes — it sets `req.user` for downstream.
|
|
97
|
+
- `requireUserRoles([...])` and `requireUserPermissions([...])` chain AFTER `requireLoginToken()`.
|
|
98
|
+
Never use them standalone, never on API token routes (API tokens don't have user roles).
|
|
99
|
+
- `requireApiToken()` is for SERVICE routes only. Use it alone — never chain `requireUserRoles`
|
|
100
|
+
after it.
|
|
101
|
+
|
|
102
|
+
### Role hierarchy (9 levels, automatic inheritance)
|
|
103
|
+
|
|
104
|
+
The default hierarchy ships with these 9 role.level values:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
Level 1: user.basic
|
|
108
|
+
Level 2: user.pro (NOT 'user.premium')
|
|
109
|
+
Level 3: user.max (NOT 'user.enterprise')
|
|
110
|
+
Level 4: moderator.review
|
|
111
|
+
Level 5: moderator.approve
|
|
112
|
+
Level 6: moderator.manage
|
|
113
|
+
Level 7: admin.tenant
|
|
114
|
+
Level 8: admin.org
|
|
115
|
+
Level 9: admin.system
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`requireUserRoles(['admin.tenant'])` accepts `admin.tenant`, `admin.org`, AND `admin.system`
|
|
119
|
+
via inheritance. To register additional roles, set the `BLOOM_AUTH_ROLES` env var:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
BLOOM_AUTH_ROLES=user.basic:1,user.pro:2,...,service.webhook:10
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
If you call `generateLoginToken({ role: 'service', level: 'webhook' })` without
|
|
126
|
+
registering the role, you get `Error: Invalid role.level: "service.webhook"` at runtime.
|
|
127
|
+
|
|
128
|
+
### Example — login flow
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { authClass, databaseClass, errorClass } from '@bloomneo/appkit';
|
|
132
|
+
|
|
133
|
+
const auth = authClass.get();
|
|
134
|
+
const database = await databaseClass.get();
|
|
135
|
+
const error = errorClass.get();
|
|
136
|
+
|
|
137
|
+
app.post('/auth/login', error.asyncRoute(async (req, res) => {
|
|
138
|
+
const { email, password } = req.body ?? {};
|
|
139
|
+
if (!email || !password) throw error.badRequest('Email and password required');
|
|
140
|
+
|
|
141
|
+
const user = await database.user.findUnique({ where: { email } });
|
|
142
|
+
if (!user) throw error.unauthorized('Invalid credentials');
|
|
143
|
+
|
|
144
|
+
const valid = await auth.comparePassword(password, user.password);
|
|
145
|
+
if (!valid) throw error.unauthorized('Invalid credentials');
|
|
146
|
+
|
|
147
|
+
const token = auth.generateLoginToken({
|
|
148
|
+
userId: user.id,
|
|
149
|
+
role: user.role,
|
|
150
|
+
level: user.level,
|
|
151
|
+
});
|
|
152
|
+
res.json({ token, user: { id: user.id, email: user.email } });
|
|
153
|
+
}));
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Example — protected admin route
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
app.delete(
|
|
160
|
+
'/admin/users/:id',
|
|
161
|
+
auth.requireLoginToken(), // 1. authenticate
|
|
162
|
+
auth.requireUserRoles(['admin.tenant']), // 2. authorize (chained)
|
|
163
|
+
error.asyncRoute(async (req, res) => {
|
|
164
|
+
const me = auth.user(req); // null-safe; non-null after middleware
|
|
165
|
+
if (!me) throw error.unauthorized();
|
|
166
|
+
await database.user.delete({ where: { id: Number(req.params.id) } });
|
|
167
|
+
res.json({ deleted: true });
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Module 2 — Database (`@bloomneo/appkit/database`)
|
|
175
|
+
|
|
176
|
+
Multi-tenant Prisma/Mongoose with automatic tenant filtering.
|
|
177
|
+
|
|
178
|
+
### Methods
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const database = await databaseClass.get();
|
|
182
|
+
// `database` is the Prisma client, scoped to current tenant
|
|
183
|
+
|
|
184
|
+
// Tenant management (when BLOOM_DB_TENANT=auto)
|
|
185
|
+
database.user.findMany() // auto-filtered by tenant_id
|
|
186
|
+
database.user.create({ data: {...} }) // auto-stamps tenant_id
|
|
187
|
+
|
|
188
|
+
// Cross-tenant queries (admin only)
|
|
189
|
+
const dbAll = await databaseClass.getTenants(); // unfiltered, all tenants
|
|
190
|
+
dbAll.user.groupBy({ by: ['tenant_id'], _count: true })
|
|
191
|
+
|
|
192
|
+
// Org-level (multi-org SaaS)
|
|
193
|
+
const acmeDb = await databaseClass.org('acme').get(); // ORG_ACME=postgresql://acme.../prod
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Required env
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
DATABASE_URL=postgresql://... # required
|
|
200
|
+
BLOOM_DB_TENANT=auto # optional, enables multi-tenant filtering
|
|
201
|
+
ORG_<NAME>=postgresql://... # optional, enables per-org databases
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Module 3 — Security (`@bloomneo/appkit/security`)
|
|
207
|
+
|
|
208
|
+
CSRF protection, rate limiting, AES-256-GCM encryption, input sanitization.
|
|
209
|
+
|
|
210
|
+
### Methods
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const security = securityClass.get();
|
|
214
|
+
|
|
215
|
+
// Rate limiting middleware
|
|
216
|
+
security.requests(maxRequests: number, windowMs: number)
|
|
217
|
+
// e.g. security.requests(100, 900000) → 100 req per 15 min per IP
|
|
218
|
+
|
|
219
|
+
// CSRF protection middleware — ONE method that does both injection AND validation.
|
|
220
|
+
// Mount once on HTML form routes. No separate requireCsrf() — forms() handles it.
|
|
221
|
+
security.forms(): ExpressMiddleware
|
|
222
|
+
|
|
223
|
+
// Encryption
|
|
224
|
+
security.encrypt(plaintext: string): string // AES-256-GCM
|
|
225
|
+
security.decrypt(ciphertext: string): string
|
|
226
|
+
|
|
227
|
+
// Input sanitization (strip XSS + control chars — NOT email/URL validation)
|
|
228
|
+
security.input(value: string): string
|
|
229
|
+
security.html(value: string): string // strip disallowed HTML tags, keep safe ones
|
|
230
|
+
security.escape(value: string): string // escape HTML special chars (&, <, >, etc.)
|
|
231
|
+
|
|
232
|
+
// NOTE: there is no security.email() or security.url() — use zod or validator.js for those.
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Required env
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
BLOOM_SECURITY_CSRF_SECRET=<min 32 chars>
|
|
239
|
+
BLOOM_SECURITY_ENCRYPTION_KEY=<64 hex chars for AES-256>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Module 4 — Error (`@bloomneo/appkit/error`)
|
|
245
|
+
|
|
246
|
+
HTTP errors with semantic types and centralized middleware.
|
|
247
|
+
|
|
248
|
+
### Methods
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
const error = errorClass.get();
|
|
252
|
+
|
|
253
|
+
// Semantic error throwers (each sets correct HTTP status)
|
|
254
|
+
throw error.badRequest('message') // 400
|
|
255
|
+
throw error.unauthorized('message') // 401
|
|
256
|
+
throw error.forbidden('message') // 403
|
|
257
|
+
throw error.notFound('message') // 404
|
|
258
|
+
throw error.conflict('message') // 409
|
|
259
|
+
throw error.tooMany('message') // 429
|
|
260
|
+
throw error.internal('message') // 500
|
|
261
|
+
|
|
262
|
+
// Async route wrapper (catches throws)
|
|
263
|
+
app.get('/x', error.asyncRoute(async (req, res) => {
|
|
264
|
+
if (!req.params.id) throw error.badRequest('id required');
|
|
265
|
+
// ...
|
|
266
|
+
}));
|
|
267
|
+
|
|
268
|
+
// Error-handling middleware (mount LAST in stack)
|
|
269
|
+
app.use(error.handleErrors());
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Module 5 — Cache (`@bloomneo/appkit/cache`)
|
|
275
|
+
|
|
276
|
+
Memory → Redis auto-scaling. Same API across both backends.
|
|
277
|
+
|
|
278
|
+
### Methods
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
const cache = cacheClass.get(); // default 'app' namespace
|
|
282
|
+
const userCache = cacheClass.get('users'); // custom namespace (isolation)
|
|
283
|
+
|
|
284
|
+
await cache.set(key: string, value: any, ttlSeconds?: number)
|
|
285
|
+
await cache.get<T>(key: string): Promise<T | null>
|
|
286
|
+
await cache.delete(key: string): Promise<boolean> // NOTE: delete(), NOT del()
|
|
287
|
+
|
|
288
|
+
// Presence check — there is no cache.has(). Check for null instead:
|
|
289
|
+
const v = await cache.get('foo');
|
|
290
|
+
if (v !== null) { /* key exists */ }
|
|
291
|
+
|
|
292
|
+
// THE pattern — use this 90% of the time
|
|
293
|
+
await cache.getOrSet<T>(key, fetcher: () => Promise<T>, ttlSeconds?): Promise<T>
|
|
294
|
+
// e.g. await cache.getOrSet('users:list', () => database.user.findMany(), 300)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Set `REDIS_URL` to auto-upgrade from memory to Redis. No code changes needed.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Module 6 — Storage (`@bloomneo/appkit/storage`)
|
|
302
|
+
|
|
303
|
+
Local → S3/R2 auto-scaling. Same API across all providers.
|
|
304
|
+
|
|
305
|
+
### Methods
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
const storage = storageClass.get();
|
|
309
|
+
|
|
310
|
+
await storage.put(key: string, buffer: Buffer, opts?: { contentType?: string })
|
|
311
|
+
await storage.get(key: string): Promise<Buffer>
|
|
312
|
+
await storage.delete(key: string): Promise<boolean> // NOTE: delete(), NOT del()
|
|
313
|
+
await storage.exists(key: string): Promise<boolean> // NOTE: exists(), NOT has()
|
|
314
|
+
await storage.list(prefix?: string, limit?: number): Promise<StorageFile[]>
|
|
315
|
+
storage.url(key: string): string // public URL
|
|
316
|
+
await storage.signedUrl(key, expiresIn?: number): Promise<string> // presigned
|
|
317
|
+
await storage.copy(sourceKey: string, destKey: string): Promise<string>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Set `AWS_S3_BUCKET` (or `R2_BUCKET`) to auto-upgrade from local disk to cloud.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Module 7 — Queue (`@bloomneo/appkit/queue`)
|
|
325
|
+
|
|
326
|
+
Memory → Redis → DB auto-scaling background jobs.
|
|
327
|
+
|
|
328
|
+
### Methods
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
const queue = queueClass.get();
|
|
332
|
+
|
|
333
|
+
// Enqueue immediately (or with a fixed delay)
|
|
334
|
+
await queue.add(jobType: string, data: any, opts?: {
|
|
335
|
+
delay?: number; // ms to wait before running (default 0)
|
|
336
|
+
attempts?: number; // max retry attempts (NOTE: "attempts" not "retries")
|
|
337
|
+
priority?: number; // lower number = higher priority
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
// Process — register a worker for a job type
|
|
341
|
+
queue.process(jobType, async (data) => {
|
|
342
|
+
// return the result; throw to trigger a retry
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Schedule — enqueue with a delay in milliseconds (NOT a cron expression)
|
|
346
|
+
await queue.schedule(jobType: string, data: any, delayMs: number): Promise<string>
|
|
347
|
+
// e.g. run once 8 hours from now:
|
|
348
|
+
await queue.schedule('cleanup-tokens', {}, 8 * 60 * 60 * 1000)
|
|
349
|
+
// For recurring/cron jobs use a cron library (node-cron etc.) to call queue.add().
|
|
350
|
+
|
|
351
|
+
// Other instance methods
|
|
352
|
+
await queue.pause(jobType?: string)
|
|
353
|
+
await queue.resume(jobType?: string)
|
|
354
|
+
await queue.getStats(jobType?: string): Promise<QueueStats>
|
|
355
|
+
await queue.retry(jobId: string)
|
|
356
|
+
await queue.remove(jobId: string)
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Memory by default; Redis when `REDIS_URL` is set.
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Module 8 — Email (`@bloomneo/appkit/email`)
|
|
364
|
+
|
|
365
|
+
Console → SMTP → Resend auto-scaling. Templates supported.
|
|
366
|
+
|
|
367
|
+
### Methods
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
const email = emailClass.get();
|
|
371
|
+
|
|
372
|
+
// Send a plain or HTML email. EmailData fields:
|
|
373
|
+
await email.send({
|
|
374
|
+
to: string | string[], // required
|
|
375
|
+
subject: string, // required
|
|
376
|
+
text?: string, // plain-text body
|
|
377
|
+
html?: string, // HTML body (send both for best deliverability)
|
|
378
|
+
from?: string, // override sender
|
|
379
|
+
replyTo?: string,
|
|
380
|
+
cc?: string | string[],
|
|
381
|
+
bcc?: string | string[],
|
|
382
|
+
// NOTE: there are NO "template" or "data" fields — use sendTemplate() below.
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// Convenience shortcuts
|
|
386
|
+
await email.sendText(to: string, subject: string, text: string)
|
|
387
|
+
await email.sendHtml(to: string, subject: string, html: string, text?: string)
|
|
388
|
+
|
|
389
|
+
// Templated email — pass the template name + all variables in one object
|
|
390
|
+
await email.sendTemplate(templateName: string, data: Record<string, any>)
|
|
391
|
+
// e.g. await email.sendTemplate('password-reset', { to: addr, resetLink, expiresIn: '1 hour' })
|
|
392
|
+
|
|
393
|
+
await email.sendBatch(emails: EmailData[], batchSize?: number)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
`console` strategy in dev (logs to terminal), `smtp` if `SMTP_HOST` set,
|
|
397
|
+
`resend` if `RESEND_API_KEY` set.
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Module 9 — Event (`@bloomneo/appkit/event`)
|
|
402
|
+
|
|
403
|
+
Memory → Redis pub/sub for distributed events. Wildcard pattern matching.
|
|
404
|
+
|
|
405
|
+
### Methods
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
const events = eventClass.get(); // default namespace
|
|
409
|
+
const userEvents = eventClass.get('users'); // namespace isolation
|
|
410
|
+
|
|
411
|
+
events.on(eventName: string, handler: (data) => void | Promise<void>)
|
|
412
|
+
events.on('user.*', handler) // wildcard
|
|
413
|
+
await events.emit(eventName: string, data: any)
|
|
414
|
+
events.off(eventName, handler)
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Set `REDIS_URL` to distribute events across processes/servers.
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## Module 10 — Util (`@bloomneo/appkit/util`)
|
|
422
|
+
|
|
423
|
+
12 small zero-dependency helpers for common Node.js tasks.
|
|
424
|
+
|
|
425
|
+
### Methods
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
const util = utilClass.get();
|
|
429
|
+
|
|
430
|
+
util.get(obj, 'a.b.c.d', defaultValue) // safe deep property access (read-only)
|
|
431
|
+
// NOTE: there is no util.set() — use plain assignment for mutation.
|
|
432
|
+
util.pick(obj, ['a', 'b', 'c']) // subset (keep listed keys)
|
|
433
|
+
// NOTE: there is no util.omit() — use util.pick() with the keys you want to keep.
|
|
434
|
+
util.isEmpty(value: any): boolean // true for null, undefined, '', [], {}
|
|
435
|
+
util.chunk(array, size) // split into batches
|
|
436
|
+
util.unique(array): any[] // deduplicate
|
|
437
|
+
util.clamp(value, min, max): number // clamp(150, 0, 100) → 100
|
|
438
|
+
util.debounce(fn, ms)
|
|
439
|
+
// NOTE: there is no util.throttle() — only debounce.
|
|
440
|
+
util.sleep(ms): Promise<void>
|
|
441
|
+
util.uuid(): string // v4 UUID
|
|
442
|
+
util.slugify(text, opts?) // url-safe slug
|
|
443
|
+
util.formatBytes(bytes): string // "1.5 MB"
|
|
444
|
+
util.truncate(text, maxLength): string // truncate with ellipsis
|
|
445
|
+
// NOTE: there is no util.retry() — implement retry with a loop or a dedicated library.
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## Module 11 — Config (`@bloomneo/appkit/config`)
|
|
451
|
+
|
|
452
|
+
Type-safe environment variable access with validation.
|
|
453
|
+
|
|
454
|
+
### Methods
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
// Instance methods — on the object returned by configClass.get()
|
|
458
|
+
const config = configClass.get();
|
|
459
|
+
|
|
460
|
+
config.get('section.key', defaultValue?) // always returns string | undefined
|
|
461
|
+
config.has('section.key'): boolean
|
|
462
|
+
config.getRequired('section.key'): string // throws if missing
|
|
463
|
+
config.getMany(['KEY_A', 'KEY_B']): Record<string, string | undefined>
|
|
464
|
+
config.getAll(): Record<string, string | undefined>
|
|
465
|
+
|
|
466
|
+
// NOTE: getNumber() and getBoolean() do NOT exist on the config instance.
|
|
467
|
+
// Parse manually:
|
|
468
|
+
const port = Number(config.get('api.port') ?? 3000);
|
|
469
|
+
const debug = config.get('app.debug') === 'true';
|
|
470
|
+
|
|
471
|
+
// Environment helpers — on configClass directly (NOT on the instance)
|
|
472
|
+
configClass.isDevelopment(): boolean
|
|
473
|
+
configClass.isProduction(): boolean
|
|
474
|
+
configClass.isTest(): boolean
|
|
475
|
+
configClass.validateRequired(['BLOOM_AUTH_SECRET', 'DATABASE_URL']) // throws if missing
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
`config.get('auth.secret')` reads `BLOOM_AUTH_SECRET` (and falls back to
|
|
479
|
+
`BLOOM_AUTH_SECRET` for backwards compatibility, with a dev-mode warning).
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Module 12 — Logger (`@bloomneo/appkit/logger`)
|
|
484
|
+
|
|
485
|
+
Multi-transport structured logging. Console → File → HTTP auto-scaling.
|
|
486
|
+
|
|
487
|
+
### Methods
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
const logger = loggerClass.get('api'); // component-tagged
|
|
491
|
+
|
|
492
|
+
logger.debug(message: string, meta?)
|
|
493
|
+
logger.info(message: string, meta?)
|
|
494
|
+
logger.warn(message: string, meta?)
|
|
495
|
+
logger.error(message: string, meta?)
|
|
496
|
+
logger.fatal(message: string, meta?)
|
|
497
|
+
|
|
498
|
+
// Multi-transport — set env vars to enable
|
|
499
|
+
// BLOOM_LOGGER_FILE_PATH=/var/log/app.log → file transport
|
|
500
|
+
// BLOOM_LOGGER_HTTP_URL=https://logs.example.com → HTTP transport
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## Environment variable reference
|
|
506
|
+
|
|
507
|
+
| Variable | Purpose | Required? |
|
|
508
|
+
|---|---|---|
|
|
509
|
+
| `BLOOM_AUTH_SECRET` | JWT signing key (min 32 chars) | yes |
|
|
510
|
+
| `DATABASE_URL` | Prisma connection string | yes |
|
|
511
|
+
| `BLOOM_SECURITY_CSRF_SECRET` | CSRF token signing (min 32 chars) | recommended |
|
|
512
|
+
| `BLOOM_SECURITY_ENCRYPTION_KEY` | AES-256-GCM key (64 hex chars) | recommended |
|
|
513
|
+
| `REDIS_URL` | Distributed cache + queue + events | optional |
|
|
514
|
+
| `AWS_S3_BUCKET` | Cloud storage | optional |
|
|
515
|
+
| `BLOOM_DB_TENANT` | `auto` enables multi-tenant filtering | optional |
|
|
516
|
+
| `RESEND_API_KEY` | Production email | optional |
|
|
517
|
+
| `SMTP_HOST` + `SMTP_USER` + `SMTP_PASS` | SMTP email | optional |
|
|
518
|
+
| `BLOOM_LOGGER_FILE_PATH` | File transport for logger | optional |
|
|
519
|
+
| `BLOOM_LOGGER_HTTP_URL` | HTTP transport for logger | optional |
|
|
520
|
+
| `ORG_<NAME>` | Per-org database (multi-org SaaS) | optional |
|
|
521
|
+
|
|
522
|
+
**No backwards compatibility.** The legacy `VOILA_*` env var prefix from
|
|
523
|
+
`@voilajsx/appkit` was removed entirely in 1.5.2. There is no fallback,
|
|
524
|
+
no deprecation warning, no compatibility shim. Consumers upgrading must
|
|
525
|
+
rename `VOILA_FOO` → `BLOOM_FOO` in their `.env` files in one go.
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Subpath imports
|
|
530
|
+
|
|
531
|
+
Each module is also available as a subpath for tree-shaking:
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
import { authClass } from '@bloomneo/appkit/auth';
|
|
535
|
+
import { databaseClass } from '@bloomneo/appkit/database';
|
|
536
|
+
import { securityClass } from '@bloomneo/appkit/security';
|
|
537
|
+
import { errorClass } from '@bloomneo/appkit/error';
|
|
538
|
+
import { cacheClass } from '@bloomneo/appkit/cache';
|
|
539
|
+
import { storageClass } from '@bloomneo/appkit/storage';
|
|
540
|
+
import { queueClass } from '@bloomneo/appkit/queue';
|
|
541
|
+
import { emailClass } from '@bloomneo/appkit/email';
|
|
542
|
+
import { eventClass } from '@bloomneo/appkit/event';
|
|
543
|
+
import { loggerClass } from '@bloomneo/appkit/logger';
|
|
544
|
+
import { configClass } from '@bloomneo/appkit/config';
|
|
545
|
+
import { utilClass } from '@bloomneo/appkit/util';
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
The flat `from '@bloomneo/appkit'` import is preferred when multiple
|
|
549
|
+
modules are used in the same file (most common case).
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## Migration from `@voilajsx/appkit`
|
|
554
|
+
|
|
555
|
+
```diff
|
|
556
|
+
- import { authClass } from '@voilajsx/appkit/auth';
|
|
557
|
+
+ import { authClass } from '@bloomneo/appkit/auth';
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
```diff
|
|
561
|
+
- VOILA_AUTH_SECRET=...
|
|
562
|
+
+ BLOOM_AUTH_SECRET=...
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**BREAKING in 1.5.2:** the legacy `VOILA_*` env var prefix is gone. Rename
|
|
566
|
+
every `VOILA_*` in your `.env` files to `BLOOM_*` in one go. There is no
|
|
567
|
+
fallback, no deprecation warning.
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
## CLI
|
|
572
|
+
|
|
573
|
+
```bash
|
|
574
|
+
npm install -g @bloomneo/appkit
|
|
575
|
+
appkit generate app myproject # full backend scaffold
|
|
576
|
+
appkit generate feature product # basic feature
|
|
577
|
+
appkit generate feature order --db # database-enabled feature
|
|
578
|
+
appkit generate feature user # full auth system
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
For frontend + backend together, use `@bloomneo/bloom` which scaffolds both.
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
## Where to look next
|
|
586
|
+
|
|
587
|
+
- **Rules** (always/never): `AGENTS.md` in this same directory
|
|
588
|
+
- **Source code**: `dist/` (compiled TypeScript with .d.ts)
|
|
589
|
+
- **Per-module READMEs**: `https://github.com/bloomneo/appkit/tree/main/src` (not shipped in tarball after 1.5.2 — they're verbose human-facing docs available on GitHub for browsing)
|
|
590
|
+
- **CHANGELOG**: `CHANGELOG.md`
|
|
591
|
+
- **Issues**: https://github.com/bloomneo/appkit/issues
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bloomneo/appkit",
|
|
3
|
-
"version": "1.2
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "Minimal and framework agnostic Node.js toolkit designed for AI agentic backend development. Previously published as @voilajsx/appkit.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -62,7 +62,24 @@
|
|
|
62
62
|
"files": [
|
|
63
63
|
"dist/",
|
|
64
64
|
"bin/",
|
|
65
|
+
"examples/",
|
|
66
|
+
"cookbook/",
|
|
67
|
+
"src/auth/README.md",
|
|
68
|
+
"src/cache/README.md",
|
|
69
|
+
"src/config/README.md",
|
|
70
|
+
"src/database/README.md",
|
|
71
|
+
"src/email/README.md",
|
|
72
|
+
"src/error/README.md",
|
|
73
|
+
"src/event/README.md",
|
|
74
|
+
"src/logger/README.md",
|
|
75
|
+
"src/queue/README.md",
|
|
76
|
+
"src/security/README.md",
|
|
77
|
+
"src/storage/README.md",
|
|
78
|
+
"src/util/README.md",
|
|
65
79
|
"README.md",
|
|
80
|
+
"AGENTS.md",
|
|
81
|
+
"llms.txt",
|
|
82
|
+
"CHANGELOG.md",
|
|
66
83
|
"LICENSE"
|
|
67
84
|
],
|
|
68
85
|
"scripts": {
|
|
@@ -126,15 +143,7 @@
|
|
|
126
143
|
"fbca",
|
|
127
144
|
"code-generation"
|
|
128
145
|
],
|
|
129
|
-
"author": "
|
|
130
|
-
"contributors": [
|
|
131
|
-
{
|
|
132
|
-
"name": "Krishna Teja GS",
|
|
133
|
-
"email": "kt@voilacode.com",
|
|
134
|
-
"url": "https://bloomneo.com"
|
|
135
|
-
}
|
|
136
|
-
],
|
|
137
|
-
"creator": "Krishna Teja GS",
|
|
146
|
+
"author": "Krishna Teja GS <kt@voilacode.com> (https://github.com/ktvoilacode)",
|
|
138
147
|
"license": "MIT",
|
|
139
148
|
"dependencies": {
|
|
140
149
|
"@prisma/client": "^6.16.2",
|