@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,228 @@
1
+ /**
2
+ * CANONICAL PATTERN — JWT auth with role-based middleware.
3
+ *
4
+ * Verified against src/auth/README.md (lines 426-479: API Reference)
5
+ * and src/auth/auth.ts (real public method signatures).
6
+ *
7
+ * Two token types: LOGIN tokens for users, API tokens for services.
8
+ * Middleware chains: `requireLoginToken()` FIRST, then `requireUserRoles()`
9
+ * — never the reverse, never standalone, never with API tokens.
10
+ *
11
+ * Required env: BLOOM_AUTH_SECRET (min 32 chars)
12
+ */
13
+
14
+ import { authClass } from '@bloomneo/appkit/auth';
15
+ import type {
16
+ ExpressRequest,
17
+ ExpressResponse,
18
+ ExpressMiddleware,
19
+ } from '@bloomneo/appkit/auth';
20
+
21
+ const auth = authClass.get();
22
+
23
+ // ── Token generation ────────────────────────────────────────────────
24
+
25
+ // Login token: for human users (web/mobile login). 7-day expiry typical.
26
+ const loginToken: string = auth.generateLoginToken(
27
+ {
28
+ userId: 123,
29
+ role: 'user',
30
+ level: 'basic',
31
+ },
32
+ '7d',
33
+ );
34
+
35
+ // API token: for services / webhooks / integrations. 1-year expiry typical.
36
+ //
37
+ // IMPORTANT: the role.level you pass MUST exist in the configured role
38
+ // hierarchy or generateApiToken throws "Invalid role.level". The default
39
+ // hierarchy ships with `user.basic` → `admin.system` (9 user roles, no
40
+ // service roles). For service tokens you have two options:
41
+ //
42
+ // 1. Use one of the default roles (e.g. `admin.system`) — what we do here.
43
+ // 2. Register custom service roles via the BLOOM_AUTH_ROLES env var, e.g.
44
+ // BLOOM_AUTH_ROLES=user.basic:1,...,service.webhook:10
45
+ // and then pass `role: 'service', level: 'webhook'` here.
46
+ //
47
+ // Option 1 is simpler when the API token holder needs full system access.
48
+ // Option 2 is correct when you want narrower scopes for different services.
49
+ const apiToken: string = auth.generateApiToken(
50
+ {
51
+ keyId: 'webhook_payment_service',
52
+ role: 'admin',
53
+ level: 'system',
54
+ },
55
+ '1y',
56
+ );
57
+
58
+ // ── Middleware (use these in route definitions) ─────────────────────
59
+
60
+ // 1. Authenticate ANY logged-in user
61
+ const protectAuthenticated: ExpressMiddleware = auth.requireLoginToken();
62
+
63
+ // 2. Require specific user role (chained AFTER requireLoginToken)
64
+ const requireAdmin: ExpressMiddleware = auth.requireUserRoles(['admin.tenant']);
65
+
66
+ // 3. Require any of multiple roles (OR semantics)
67
+ const requireAdminOrOrg: ExpressMiddleware = auth.requireUserRoles([
68
+ 'admin.tenant',
69
+ 'admin.org',
70
+ ]);
71
+
72
+ // 4. Require specific permissions (AND semantics across the array)
73
+ const requireUserManagement: ExpressMiddleware = auth.requireUserPermissions([
74
+ 'manage:users',
75
+ 'edit:tenant',
76
+ ]);
77
+
78
+ // 5. API token authentication (services only — DO NOT chain with requireUserRoles)
79
+ const protectApi: ExpressMiddleware = auth.requireApiToken();
80
+
81
+ // ── Route handlers (called by Express after middleware passes) ──────
82
+
83
+ // Login handler: bcrypt check + token issue
84
+ async function loginHandler(req: ExpressRequest, res: ExpressResponse) {
85
+ const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
86
+
87
+ // Look up the user from your database. Placeholder.
88
+ const user = {
89
+ id: 1,
90
+ email,
91
+ passwordHash: '$2b$10$placeholder',
92
+ role: 'user',
93
+ level: 'basic',
94
+ };
95
+
96
+ const valid = await auth.comparePassword(password ?? '', user.passwordHash);
97
+ if (!valid) {
98
+ res.status?.(401).json?.({ error: 'Invalid credentials' });
99
+ return;
100
+ }
101
+
102
+ const token = auth.generateLoginToken({
103
+ userId: user.id,
104
+ role: user.role,
105
+ level: user.level,
106
+ });
107
+
108
+ res.json?.({ token });
109
+ }
110
+
111
+ // Registration handler: bcrypt hash + token issue
112
+ async function registerHandler(req: ExpressRequest, res: ExpressResponse) {
113
+ const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
114
+ const hashedPassword = await auth.hashPassword(password ?? '');
115
+ // Save { email, password: hashedPassword } to your database. Placeholder.
116
+ void hashedPassword;
117
+ void email;
118
+ const newUserId = 42;
119
+
120
+ const token = auth.generateLoginToken({
121
+ userId: newUserId,
122
+ role: 'user',
123
+ level: 'basic',
124
+ });
125
+
126
+ res.json?.({ token });
127
+ }
128
+
129
+ // Authenticated handler: extract user safely from req
130
+ function profileHandler(req: ExpressRequest, res: ExpressResponse) {
131
+ const user = auth.user(req); // null-safe; non-null after requireLoginToken
132
+ if (!user) {
133
+ res.status?.(401).json?.({ error: 'Not authenticated' });
134
+ return;
135
+ }
136
+ res.json?.({ user });
137
+ }
138
+
139
+ // ── Programmatic role hierarchy check (outside middleware) ──────────
140
+
141
+ const orgIncludesTenant: boolean = auth.hasRole('admin.org', 'admin.tenant'); // true
142
+ const userIsAdmin: boolean = auth.hasRole('user.basic', 'admin.tenant'); // false
143
+
144
+ // ── Verify a token manually (outside middleware) ────────────────────
145
+ //
146
+ // `verifyToken` is what the middleware uses internally. You normally don't
147
+ // call it yourself — `requireLoginToken()` and `requireApiToken()` handle
148
+ // it for you. Use this only when you need to validate a token in a
149
+ // non-Express context (e.g. WebSocket auth, queue worker auth, scheduled
150
+ // task auth). Throws on bad token; never returns null.
151
+ function verifyTokenManually(rawToken: string) {
152
+ try {
153
+ const payload = auth.verifyToken(rawToken);
154
+ // payload.userId is set for login tokens; payload.keyId is set for API tokens.
155
+ // payload.type tells you which: 'login' or 'api_key'.
156
+ return payload;
157
+ } catch (e) {
158
+ // Possible messages: 'Token has expired', 'Invalid token',
159
+ // 'Token must be a string', 'Token verification failed: ...'
160
+ return null;
161
+ }
162
+ }
163
+
164
+ // ── Permission check with inheritance (auth.can) ────────────────────
165
+ //
166
+ // `can` checks fine-grained permissions on a user payload. Permission format
167
+ // is `action:scope` (e.g. 'edit:tenant', 'manage:users'). Inheritance rule:
168
+ // `manage:scope` automatically grants `view:scope`, `edit:scope`,
169
+ // `delete:scope`, etc. for the same scope.
170
+ //
171
+ // Use this inside a route handler AFTER `requireLoginToken()` middleware
172
+ // has validated the user and attached the payload.
173
+ function permissionCheckHandler(req: ExpressRequest, res: ExpressResponse) {
174
+ const user = auth.user(req);
175
+ if (!user) {
176
+ res.status?.(401).json?.({ error: 'Not authenticated' });
177
+ return;
178
+ }
179
+
180
+ // Check if the user can edit their tenant. Returns true if the user has
181
+ // 'edit:tenant' OR 'manage:tenant' (manage inherits all actions).
182
+ const canEdit: boolean = auth.can(user, 'edit:tenant');
183
+ if (!canEdit) {
184
+ res.status?.(403).json?.({ error: 'Cannot edit tenant' });
185
+ return;
186
+ }
187
+
188
+ res.json?.({ allowed: true });
189
+ }
190
+
191
+ // ── Express wiring (commented — uncomment in your app) ──────────────
192
+ //
193
+ // import express from 'express';
194
+ // const app = express();
195
+ //
196
+ // app.post('/login', loginHandler);
197
+ // app.post('/register', registerHandler);
198
+ //
199
+ // // User route: login + role required
200
+ // app.get(
201
+ // '/admin/users',
202
+ // auth.requireLoginToken(),
203
+ // auth.requireUserRoles(['admin.tenant']),
204
+ // profileHandler,
205
+ // );
206
+ //
207
+ // // API route: API token only (no user roles)
208
+ // app.post('/webhook/payment', auth.requireApiToken(), (req, res) => {
209
+ // res.json({ ok: true });
210
+ // });
211
+
212
+ export {
213
+ auth,
214
+ loginToken,
215
+ apiToken,
216
+ protectAuthenticated,
217
+ requireAdmin,
218
+ requireAdminOrOrg,
219
+ requireUserManagement,
220
+ protectApi,
221
+ verifyTokenManually,
222
+ permissionCheckHandler,
223
+ loginHandler,
224
+ registerHandler,
225
+ profileHandler,
226
+ orgIncludesTenant,
227
+ userIsAdmin,
228
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * CANONICAL PATTERN — cache.getOrSet for "fetch once, cache the result".
3
+ *
4
+ * Copy this file when you need to cache anything. The getOrSet pattern is
5
+ * the right answer 90% of the time — never write the cache-check-then-fetch
6
+ * pattern manually.
7
+ *
8
+ * Set REDIS_URL in .env to upgrade from in-process memory to distributed
9
+ * Redis cache. Same code works for both — no changes needed.
10
+ */
11
+
12
+ import { cacheClass, databaseClass, errorClass } from '@bloomneo/appkit';
13
+
14
+ const error = errorClass.get();
15
+ const database = await databaseClass.get();
16
+
17
+ // Custom namespace isolates this cache from others (default is 'app').
18
+ const cache = cacheClass.get('users');
19
+
20
+ // ── Cached database query ─────────────────────────────────────────────
21
+ export const listProductsRoute = error.asyncRoute(async (req, res) => {
22
+ const products = await cache.getOrSet(
23
+ 'products:list',
24
+ () => database.product.findMany({ where: { published: true } }),
25
+ 300, // 5 minutes
26
+ );
27
+ res.json({ products });
28
+ });
29
+
30
+ // ── Manual operations (use sparingly — getOrSet is preferred) ────────
31
+ export async function manualCacheExample() {
32
+ await cache.set('foo', 'bar', 60); // 1 minute TTL
33
+ const v = await cache.get('foo'); // → 'bar'
34
+ const exists = v !== null; // presence check: get() returns null on miss
35
+ await cache.delete('foo'); // delete() — not del()
36
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * CANONICAL PATTERN — type-safe environment variable access.
3
+ *
4
+ * Copy this file when you need to read configuration. Always go through
5
+ * configClass.get() instead of touching process.env directly — config
6
+ * validates types, provides defaults, and supports the BLOOM_* prefix
7
+ * convention.
8
+ *
9
+ * Read AGENTS.md for the full env var reference.
10
+ */
11
+
12
+ import { configClass } from '@bloomneo/appkit';
13
+
14
+ const config = configClass.get();
15
+
16
+ // ── Read string config (with default) ───────────────────────────────
17
+ const apiHost = config.get('api.host', 'localhost');
18
+ const dbUrl = config.get('database.url'); // throws if not set
19
+
20
+ // ── Read typed config ───────────────────────────────────────────────
21
+ // config.get() returns the value as-is. Cast to number/boolean yourself.
22
+ const port = Number(config.get('api.port') ?? 3000);
23
+ const debugMode = config.get('app.debug') === 'true';
24
+
25
+ // ── Environment detection ───────────────────────────────────────────
26
+ // isDevelopment() / isProduction() / isTest() live on configClass, NOT on the
27
+ // instance returned by configClass.get(). Always call them via configClass.*.
28
+ if (configClass.isDevelopment()) {
29
+ console.log('Running in dev mode');
30
+ }
31
+ if (configClass.isProduction()) {
32
+ // tighter security defaults, etc.
33
+ }
34
+
35
+ // ── Validate required env vars at startup ───────────────────────────
36
+ // Call this once in your server bootstrap. Throws with a clear error message
37
+ // listing every missing variable.
38
+ config.validateRequired([
39
+ 'BLOOM_AUTH_SECRET',
40
+ 'DATABASE_URL',
41
+ 'BLOOM_SECURITY_CSRF_SECRET',
42
+ 'BLOOM_SECURITY_ENCRYPTION_KEY',
43
+ ]);
44
+
45
+ export { config, apiHost, dbUrl, port, debugMode };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * CANONICAL PATTERN — multi-tenant database access.
3
+ *
4
+ * Verified against src/database/README.md (Core API + LLM Guidelines sections)
5
+ * and src/database/index.ts.
6
+ *
7
+ * Three core methods:
8
+ * • databaseClass.get() → tenant-aware client (single user mode)
9
+ * • databaseClass.getTenants() → unfiltered client (admin / cross-tenant)
10
+ * • databaseClass.org('name').get() / .getTenants() → per-org databases
11
+ *
12
+ * Required env: DATABASE_URL
13
+ * Optional env: BLOOM_DB_TENANT=auto (enables multi-tenant filtering)
14
+ */
15
+
16
+ import { databaseClass } from '@bloomneo/appkit/database';
17
+
18
+ // ── Normal user access (auto-filtered by tenant when BLOOM_DB_TENANT=auto) ──
19
+ async function listUsers() {
20
+ const database = await databaseClass.get();
21
+ // Returns only rows belonging to the current tenant.
22
+ // The shape of `database.user` matches your Prisma / Mongoose model.
23
+ // Placeholder query — replace with your actual model.
24
+ const users = await (database as any).user?.findMany?.() ?? [];
25
+ return users;
26
+ }
27
+
28
+ // ── Admin: cross-tenant query (unfiltered) ──────────────────────────
29
+ async function listAllUsersAcrossTenants() {
30
+ const dbTenants = await databaseClass.getTenants();
31
+ const allUsers = await (dbTenants as any).user?.findMany?.() ?? [];
32
+ return allUsers;
33
+ }
34
+
35
+ // ── Admin: per-tenant analytics ─────────────────────────────────────
36
+ async function tenantAnalytics() {
37
+ const dbTenants = await databaseClass.getTenants();
38
+ const stats = await (dbTenants as any).user?.groupBy?.({
39
+ by: ['tenant_id'],
40
+ _count: { _all: true },
41
+ }) ?? [];
42
+ return stats;
43
+ }
44
+
45
+ // ── Multi-org: per-organization database ────────────────────────────
46
+ // Set ORG_ACME=postgresql://acme.aws.com/prod in .env, then:
47
+ async function listAcmeUsers() {
48
+ const acmedatabase = await databaseClass.org('acme').get();
49
+ const users = await (acmedatabase as any).user?.findMany?.() ?? [];
50
+ return users;
51
+ }
52
+
53
+ // ── Multi-org admin: all tenants in a single org ────────────────────
54
+ async function acmeTenantStats() {
55
+ const acmeDbTenants = await databaseClass.org('acme').getTenants();
56
+ const stats = await (acmeDbTenants as any).user?.groupBy?.({
57
+ by: ['tenant_id'],
58
+ _count: { _all: true },
59
+ }) ?? [];
60
+ return stats;
61
+ }
62
+
63
+ export {
64
+ listUsers,
65
+ listAllUsersAcrossTenants,
66
+ tenantAnalytics,
67
+ listAcmeUsers,
68
+ acmeTenantStats,
69
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * CANONICAL PATTERN — send email with auto-scaling provider.
3
+ *
4
+ * Copy this file when you need to send email. Default is console (logs to
5
+ * terminal — perfect for development). Set SMTP_HOST + SMTP_USER + SMTP_PASS
6
+ * to use SMTP. Set RESEND_API_KEY to use Resend.
7
+ *
8
+ * Same code works for all three providers — no changes needed.
9
+ */
10
+
11
+ import { emailClass, errorClass } from '@bloomneo/appkit';
12
+
13
+ const email = emailClass.get();
14
+ const error = errorClass.get();
15
+
16
+ // ── Plain text email ────────────────────────────────────────────────
17
+ export async function sendWelcomeEmail(toAddress: string, userName: string) {
18
+ await email.send({
19
+ to: toAddress,
20
+ subject: 'Welcome to MyApp',
21
+ text: `Hi ${userName},\n\nThanks for signing up!\n\n— The Team`,
22
+ });
23
+ }
24
+
25
+ // ── HTML email with plain-text fallback ─────────────────────────────
26
+ export async function sendInvoiceEmail(toAddress: string, invoiceUrl: string) {
27
+ await email.send({
28
+ to: toAddress,
29
+ subject: 'Your invoice is ready',
30
+ text: `Your invoice: ${invoiceUrl}`,
31
+ html: `<p>Your invoice: <a href="${invoiceUrl}">${invoiceUrl}</a></p>`,
32
+ });
33
+ }
34
+
35
+ // ── Templated email ─────────────────────────────────────────────────
36
+ // EmailData (used by email.send()) only accepts: to, from, subject, text,
37
+ // html, attachments, replyTo, cc, bcc — it has no template/data fields.
38
+ // Use email.sendTemplate() for template-driven emails.
39
+ export async function sendPasswordResetEmail(toAddress: string, resetLink: string) {
40
+ await email.sendTemplate('password-reset', {
41
+ to: toAddress,
42
+ resetLink,
43
+ expiresIn: '1 hour',
44
+ });
45
+ }
46
+
47
+ // ── Inside a route handler ──────────────────────────────────────────
48
+ export const sendNotificationRoute = error.asyncRoute(async (req, res) => {
49
+ const { to, message } = req.body;
50
+ if (!to || !message) throw error.badRequest('to and message required');
51
+ await email.send({ to, subject: 'Notification', text: message });
52
+ res.json({ sent: true });
53
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * CANONICAL PATTERN — semantic HTTP errors + centralized middleware.
3
+ *
4
+ * Copy this file when you need error handling in routes. The pattern:
5
+ * 1. Wrap async route handlers in error.asyncRoute(...)
6
+ * 2. Throw semantic errors (badRequest, unauthorized, notFound, etc.)
7
+ * — never `throw new Error(...)` in route handlers
8
+ * 3. Mount error.handleErrors() as the LAST middleware in the Express stack
9
+ *
10
+ * The asyncRoute wrapper catches the throw, and handleErrors() converts
11
+ * the semantic error into the right HTTP status + JSON response.
12
+ */
13
+
14
+ import express from 'express';
15
+ import { errorClass, databaseClass } from '@bloomneo/appkit';
16
+
17
+ const error = errorClass.get();
18
+ const database = await databaseClass.get();
19
+ const app = express();
20
+
21
+ // ── Routes use asyncRoute + semantic error throws ───────────────────
22
+ app.get('/api/users/:id', error.asyncRoute(async (req, res) => {
23
+ const id = Number(req.params.id);
24
+ if (!Number.isFinite(id)) throw error.badRequest('id must be a number');
25
+
26
+ const user = await database.user.findUnique({ where: { id } });
27
+ if (!user) throw error.notFound('User not found');
28
+
29
+ res.json({ user });
30
+ }));
31
+
32
+ app.post('/api/admin', error.asyncRoute(async (req, res) => {
33
+ if (req.body.role !== 'admin') throw error.forbidden('Admin role required');
34
+ // ...
35
+ }));
36
+
37
+ // ── Available semantic error throwers ───────────────────────────────
38
+ //
39
+ // throw error.badRequest('message') → 400
40
+ // throw error.unauthorized('message') → 401
41
+ // throw error.forbidden('message') → 403
42
+ // throw error.notFound('message') → 404
43
+ // throw error.conflict('message') → 409
44
+ // throw error.tooMany('message') → 429 (rate limiting)
45
+ // throw error.serverError('message') → 500
46
+ // throw error.internal('message') → 500 (alias for serverError)
47
+ // throw error.createError(503, 'message') → any code
48
+
49
+ // ── MOUNT error.handleErrors() LAST in the middleware stack ─────────
50
+ app.use(error.handleErrors());
@@ -0,0 +1,42 @@
1
+ /**
2
+ * CANONICAL PATTERN — pub/sub events with auto-scaling backend.
3
+ *
4
+ * Copy this file when you need events. Default is in-process memory (single
5
+ * Node process). Set REDIS_URL to distribute events across processes / servers
6
+ * — same code works in both modes.
7
+ *
8
+ * Use namespaces (eventClass.get('users')) to isolate event streams.
9
+ */
10
+
11
+ import { eventClass, loggerClass } from '@bloomneo/appkit';
12
+
13
+ const events = eventClass.get('users'); // namespaced
14
+ const logger = loggerClass.get('events');
15
+
16
+ // ── Subscribe to a single event ─────────────────────────────────────
17
+ events.on('user.created', async (data) => {
18
+ logger.info('New user', { userId: data.userId });
19
+ // ...trigger welcome email, analytics, etc.
20
+ });
21
+
22
+ // ── Subscribe to a wildcard pattern ─────────────────────────────────
23
+ events.on('user.*', async (eventName, data) => {
24
+ logger.debug(`User event: ${eventName}`, data);
25
+ });
26
+
27
+ // ── Emit an event from a route handler ──────────────────────────────
28
+ import { errorClass } from '@bloomneo/appkit';
29
+ const error = errorClass.get();
30
+
31
+ export const createUserRoute = error.asyncRoute(async (req, res) => {
32
+ // ...create the user in the database
33
+ const newUser = { id: 123, email: req.body.email };
34
+
35
+ await events.emit('user.created', {
36
+ userId: newUser.id,
37
+ email: newUser.email,
38
+ timestamp: new Date(),
39
+ });
40
+
41
+ res.json({ user: newUser });
42
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * CANONICAL PATTERN — structured logging with component tagging.
3
+ *
4
+ * Copy this file when you need logging. The pattern: get a component-tagged
5
+ * logger at module scope, then call .info() / .warn() / .error() with a
6
+ * message + meta object. Logs are structured JSON in production, pretty
7
+ * console output in development.
8
+ *
9
+ * Set BLOOM_LOGGER_FILE_PATH to also write to a file.
10
+ * Set BLOOM_LOGGER_HTTP_URL to ship to a centralized log collector.
11
+ * Same code works in all three modes.
12
+ */
13
+
14
+ import { loggerClass } from '@bloomneo/appkit';
15
+
16
+ // Component-tagged logger — use one per file or per logical area.
17
+ const logger = loggerClass.get('users');
18
+
19
+ export function exampleLogging() {
20
+ logger.debug('Lookup started', { userId: 42 }); // dev only
21
+ logger.info('User created', { userId: 42, email: 'a@b' });
22
+ logger.warn('Slow query', { ms: 3200, table: 'users' });
23
+ logger.error('Database connection lost', { host: 'db1' });
24
+ logger.fatal('OOM, exiting', { rss: process.memoryUsage().rss });
25
+ }
26
+
27
+ // ── Inside a route handler ──────────────────────────────────────────
28
+ import { errorClass } from '@bloomneo/appkit';
29
+ const error = errorClass.get();
30
+
31
+ export const createUserRoute = error.asyncRoute(async (req, res) => {
32
+ logger.info('Creating user', { ip: req.ip, email: req.body.email });
33
+ try {
34
+ // ...create logic
35
+ logger.info('User created successfully', { userId: 123 });
36
+ res.json({ ok: true });
37
+ } catch (e) {
38
+ logger.error('User creation failed', { error: (e as Error).message });
39
+ throw e;
40
+ }
41
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * CANONICAL PATTERN — background job queue with auto-scaling backend.
3
+ *
4
+ * Copy this file when you need background jobs. Default backend is in-process
5
+ * memory (good for dev). Set REDIS_URL to upgrade to distributed Redis.
6
+ *
7
+ * Same code works for all backends — no changes needed.
8
+ *
9
+ * queue.add(jobType, data, options) — enqueue immediately or with a fixed delay
10
+ * queue.schedule(jobType, data, delayMs) — alias: enqueue with a delay in ms
11
+ * queue.process(jobType, handler) — register a worker for that job type
12
+ *
13
+ * For cron-style recurring jobs: use a cron library (node-cron, etc.) to call
14
+ * queue.add() at the right interval — there is no built-in cron scheduler.
15
+ */
16
+
17
+ import { queueClass, loggerClass, errorClass } from '@bloomneo/appkit';
18
+
19
+ const queue = queueClass.get();
20
+ const logger = loggerClass.get('queue');
21
+ const error = errorClass.get();
22
+
23
+ // ── Producer: enqueue jobs from a route ─────────────────────────────
24
+ export const enqueueEmailRoute = error.asyncRoute(async (req, res) => {
25
+ const { to, subject, body } = req.body;
26
+
27
+ await queue.add(
28
+ 'send-email',
29
+ { to, subject, body },
30
+ {
31
+ delay: 0, // run immediately (ms)
32
+ attempts: 3, // retry up to 3 times on failure (not "retries")
33
+ priority: 1, // lower number = higher priority
34
+ },
35
+ );
36
+
37
+ res.json({ queued: true });
38
+ });
39
+
40
+ // ── Consumer: process jobs (run in a worker process or main app) ────
41
+ queue.process('send-email', async (data) => {
42
+ const { to, subject, body } = data as { to: string; subject: string; body: string };
43
+ logger.info('Email sent', { to, subject });
44
+ return { sent: true };
45
+ });
46
+
47
+ // ── Delayed job (run once after N ms, not cron-style) ───────────────
48
+ // To run cleanup every day at 03:00, use node-cron to call queue.add() at the
49
+ // right time — queue.schedule() accepts a delay in milliseconds, not a cron expression.
50
+ await queue.schedule(
51
+ 'cleanup-expired-tokens',
52
+ {},
53
+ 8 * 60 * 60 * 1000, // run once, 8 hours from now
54
+ );
55
+
56
+ queue.process('cleanup-expired-tokens', async () => {
57
+ logger.info('Token cleanup ran');
58
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * CANONICAL PATTERN — rate limiting, CSRF, encryption, input sanitization.
3
+ *
4
+ * Copy this file when you need security middleware. AppKit's security module
5
+ * covers four common needs from one xxxClass.get() instance.
6
+ *
7
+ * Required env: BLOOM_SECURITY_CSRF_SECRET, BLOOM_SECURITY_ENCRYPTION_KEY
8
+ */
9
+
10
+ import { securityClass, errorClass } from '@bloomneo/appkit';
11
+
12
+ const security = securityClass.get();
13
+ const error = errorClass.get();
14
+
15
+ // ── Rate limiting middleware ────────────────────────────────────────
16
+ // 100 requests per 15-minute window per IP
17
+ export const apiRateLimit = security.requests(100, 15 * 60 * 1000);
18
+
19
+ // 5 requests per minute (e.g. for a login endpoint)
20
+ export const loginRateLimit = security.requests(5, 60 * 1000);
21
+
22
+ // ── CSRF protection ─────────────────────────────────────────────────
23
+ // security.forms() is a single middleware that handles BOTH:
24
+ // - Injecting a CSRF token into the response (GET requests)
25
+ // - Validating the token on state-changing requests (POST/PUT/DELETE)
26
+ // Mount it once globally — no separate "require" middleware needed.
27
+ export const csrfMiddleware = security.forms();
28
+
29
+ // ── Encryption (AES-256-GCM) ────────────────────────────────────────
30
+ export function encryptApiKey(plaintext: string): string {
31
+ return security.encrypt(plaintext); // → opaque ciphertext string
32
+ }
33
+
34
+ export function decryptApiKey(ciphertext: string): string {
35
+ return security.decrypt(ciphertext);
36
+ }
37
+
38
+ // ── Input sanitization ──────────────────────────────────────────────
39
+ // security.input() strips XSS payloads and control characters from free-form text.
40
+ // For email/URL validation, use a dedicated validation library (e.g. zod, validator.js).
41
+ export const submitRoute = error.asyncRoute(async (req, res) => {
42
+ const safeMessage = security.input(req.body.message); // strip XSS
43
+ const safeHtml = security.html(req.body.bio); // allow safe HTML tags only
44
+
45
+ res.json({ ok: true, sanitized: { message: safeMessage, bio: safeHtml } });
46
+ });