@bloomneo/appkit 1.5.1 → 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.
Files changed (111) hide show
  1. package/AGENTS.md +195 -0
  2. package/CHANGELOG.md +253 -0
  3. package/README.md +147 -799
  4. package/bin/commands/generate.js +7 -7
  5. package/cookbook/README.md +26 -0
  6. package/cookbook/api-key-service.ts +106 -0
  7. package/cookbook/auth-protected-crud.ts +112 -0
  8. package/cookbook/file-upload-pipeline.ts +113 -0
  9. package/cookbook/multi-tenant-saas.ts +87 -0
  10. package/cookbook/real-time-chat.ts +121 -0
  11. package/dist/auth/auth.d.ts +21 -4
  12. package/dist/auth/auth.d.ts.map +1 -1
  13. package/dist/auth/auth.js +56 -44
  14. package/dist/auth/auth.js.map +1 -1
  15. package/dist/auth/defaults.d.ts +1 -1
  16. package/dist/auth/defaults.js +35 -35
  17. package/dist/cache/cache.d.ts +29 -6
  18. package/dist/cache/cache.d.ts.map +1 -1
  19. package/dist/cache/cache.js +72 -44
  20. package/dist/cache/cache.js.map +1 -1
  21. package/dist/cache/defaults.js +25 -25
  22. package/dist/cache/index.d.ts +19 -10
  23. package/dist/cache/index.d.ts.map +1 -1
  24. package/dist/cache/index.js +21 -18
  25. package/dist/cache/index.js.map +1 -1
  26. package/dist/config/defaults.d.ts +1 -1
  27. package/dist/config/defaults.js +8 -8
  28. package/dist/config/index.d.ts +3 -3
  29. package/dist/config/index.js +4 -4
  30. package/dist/database/adapters/mongoose.js +2 -2
  31. package/dist/database/adapters/prisma.js +2 -2
  32. package/dist/database/defaults.d.ts +1 -1
  33. package/dist/database/defaults.js +4 -4
  34. package/dist/database/index.js +2 -2
  35. package/dist/database/index.js.map +1 -1
  36. package/dist/email/defaults.js +20 -20
  37. package/dist/error/defaults.d.ts +1 -1
  38. package/dist/error/defaults.js +12 -12
  39. package/dist/error/error.d.ts +12 -0
  40. package/dist/error/error.d.ts.map +1 -1
  41. package/dist/error/error.js +19 -0
  42. package/dist/error/error.js.map +1 -1
  43. package/dist/error/index.d.ts +14 -3
  44. package/dist/error/index.d.ts.map +1 -1
  45. package/dist/error/index.js +14 -3
  46. package/dist/error/index.js.map +1 -1
  47. package/dist/event/defaults.js +30 -30
  48. package/dist/logger/defaults.d.ts +1 -1
  49. package/dist/logger/defaults.js +40 -40
  50. package/dist/logger/index.d.ts +1 -0
  51. package/dist/logger/index.d.ts.map +1 -1
  52. package/dist/logger/index.js.map +1 -1
  53. package/dist/logger/logger.d.ts +8 -0
  54. package/dist/logger/logger.d.ts.map +1 -1
  55. package/dist/logger/logger.js +13 -3
  56. package/dist/logger/logger.js.map +1 -1
  57. package/dist/logger/transports/console.js +1 -1
  58. package/dist/logger/transports/http.d.ts +1 -1
  59. package/dist/logger/transports/http.js +1 -1
  60. package/dist/logger/transports/webhook.d.ts +1 -1
  61. package/dist/logger/transports/webhook.js +1 -1
  62. package/dist/queue/defaults.d.ts +2 -2
  63. package/dist/queue/defaults.js +38 -38
  64. package/dist/security/defaults.d.ts +1 -1
  65. package/dist/security/defaults.js +29 -29
  66. package/dist/security/index.d.ts +1 -1
  67. package/dist/security/index.js +3 -3
  68. package/dist/security/security.d.ts +1 -1
  69. package/dist/security/security.js +4 -4
  70. package/dist/storage/defaults.js +19 -19
  71. package/dist/util/defaults.d.ts +1 -1
  72. package/dist/util/defaults.js +34 -34
  73. package/dist/util/env.d.ts +35 -0
  74. package/dist/util/env.d.ts.map +1 -0
  75. package/dist/util/env.js +50 -0
  76. package/dist/util/env.js.map +1 -0
  77. package/dist/util/errors.d.ts +52 -0
  78. package/dist/util/errors.d.ts.map +1 -0
  79. package/dist/util/errors.js +82 -0
  80. package/dist/util/errors.js.map +1 -0
  81. package/examples/.env.example +80 -0
  82. package/examples/README.md +16 -0
  83. package/examples/auth.ts +228 -0
  84. package/examples/cache.ts +36 -0
  85. package/examples/config.ts +45 -0
  86. package/examples/database.ts +69 -0
  87. package/examples/email.ts +53 -0
  88. package/examples/error.ts +50 -0
  89. package/examples/event.ts +42 -0
  90. package/examples/logger.ts +41 -0
  91. package/examples/queue.ts +58 -0
  92. package/examples/security.ts +46 -0
  93. package/examples/storage.ts +44 -0
  94. package/examples/util.ts +47 -0
  95. package/llms.txt +591 -0
  96. package/package.json +19 -10
  97. package/src/auth/README.md +850 -0
  98. package/src/cache/README.md +756 -0
  99. package/src/config/README.md +604 -0
  100. package/src/database/README.md +818 -0
  101. package/src/email/README.md +759 -0
  102. package/src/error/README.md +660 -0
  103. package/src/event/README.md +729 -0
  104. package/src/logger/README.md +435 -0
  105. package/src/queue/README.md +851 -0
  106. package/src/security/README.md +612 -0
  107. package/src/storage/README.md +1008 -0
  108. package/src/util/README.md +955 -0
  109. package/bin/templates/backend/docs/APPKIT_CLI.md +0 -507
  110. package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +0 -61
  111. package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +0 -2539
@@ -0,0 +1,44 @@
1
+ /**
2
+ * CANONICAL PATTERN — file upload + retrieval with auto-scaling backend.
3
+ *
4
+ * Copy this file when you need to store files. The same code works against
5
+ * local disk (default), AWS S3 (set AWS_S3_BUCKET), or Cloudflare R2
6
+ * (set R2_BUCKET). No code changes needed — just .env.
7
+ */
8
+
9
+ import { storageClass, errorClass, securityClass } from '@bloomneo/appkit';
10
+ import type { Request, Response } from 'express';
11
+
12
+ const storage = storageClass.get();
13
+ const error = errorClass.get();
14
+ const security = securityClass.get();
15
+
16
+ // ── Upload (with rate limit + filename sanitization) ────────────────
17
+ export const uploadRoute = [
18
+ security.requests(10, 60_000), // max 10 uploads per minute per IP
19
+ error.asyncRoute(async (req: Request & { file: any }, res: Response) => {
20
+ if (!req.file) throw error.badRequest('File required (use multer middleware)');
21
+
22
+ // Sanitize the filename so a malicious user can't write to ../../../etc/...
23
+ const safeName = security.input(req.file.originalname);
24
+ const key = `uploads/${Date.now()}-${safeName}`;
25
+
26
+ await storage.put(key, req.file.buffer, {
27
+ contentType: req.file.mimetype,
28
+ });
29
+
30
+ res.json({
31
+ key,
32
+ url: storage.url(key), // public URL (or signed URL for private buckets)
33
+ });
34
+ }),
35
+ ];
36
+
37
+ // ── Download (signed URL — works with private S3 buckets) ───────────
38
+ export const downloadRoute = error.asyncRoute(async (req, res) => {
39
+ const key = req.params.key;
40
+ if (!(await storage.exists(key))) throw error.notFound('File not found');
41
+
42
+ const signedUrl = await storage.signedUrl(key, 3600); // valid for 1 hour
43
+ res.json({ url: signedUrl });
44
+ });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * CANONICAL PATTERN — small zero-dependency utility helpers.
3
+ *
4
+ * Copy this file when you need any of the util methods. These are the
5
+ * common Node.js helpers that get reinvented in every project — appkit
6
+ * ships them once with consistent semantics.
7
+ *
8
+ * Public methods: get, isEmpty, slugify, chunk, debounce, pick,
9
+ * unique, clamp, formatBytes, truncate, sleep, uuid
10
+ */
11
+
12
+ import { utilClass } from '@bloomneo/appkit';
13
+
14
+ const util = utilClass.get();
15
+
16
+ // ── Safe deep property access (read-only) ───────────────────────────
17
+ const obj = { a: { b: { c: 42 } } };
18
+ const value = util.get(obj, 'a.b.c', 0); // → 42
19
+ const missing = util.get(obj, 'a.b.x.y', 'default'); // → 'default' (safe)
20
+ // Note: there is no util.set() — use plain assignment for mutation.
21
+
22
+ // ── Subset ──────────────────────────────────────────────────────────
23
+ const user = { id: 1, email: 'a@b', password: 'secret', role: 'admin' };
24
+ const minimal = util.pick(user, ['id', 'email']); // → { id, email }
25
+ // Note: there is no util.omit() — use util.pick() with the keys you want to keep.
26
+
27
+ // ── Array helpers ───────────────────────────────────────────────────
28
+ const items = [1, 2, 3, 4, 5, 6, 7, 8, 9];
29
+ const batches = util.chunk(items, 3); // → [[1,2,3], [4,5,6], [7,8,9]]
30
+ const deduped = util.unique([1, 2, 2, 3, 3]); // → [1, 2, 3]
31
+
32
+ // ── Function helpers ────────────────────────────────────────────────
33
+ const debouncedSearch = util.debounce((q: string) => {
34
+ // ...api call
35
+ }, 300);
36
+ // Note: there is no util.throttle() — use util.debounce() for rate-limiting calls.
37
+
38
+ // ── Misc ────────────────────────────────────────────────────────────
39
+ const id = util.uuid(); // → 'a1b2c3d4-...'
40
+ const slug = util.slugify('My Awesome Post!'); // → 'my-awesome-post'
41
+ const size = util.formatBytes(1_572_864); // → '1.5 MB'
42
+ const clamped = util.clamp(150, 0, 100); // → 100
43
+ const short = util.truncate('A very long string', 10); // → 'A very lon...'
44
+
45
+ await util.sleep(100); // → wait 100ms
46
+
47
+ export { value, missing, minimal, batches, deduped, id, slug, size, clamped, short };
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