@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.
Files changed (118) 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 +29 -29
  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 +11 -11
  28. package/dist/config/index.d.ts +3 -3
  29. package/dist/config/index.js +4 -4
  30. package/dist/database/adapters/mongoose.d.ts +4 -4
  31. package/dist/database/adapters/mongoose.js +7 -7
  32. package/dist/database/adapters/prisma.d.ts +4 -4
  33. package/dist/database/adapters/prisma.js +7 -7
  34. package/dist/database/defaults.d.ts +1 -1
  35. package/dist/database/defaults.js +4 -4
  36. package/dist/database/index.js +2 -2
  37. package/dist/database/index.js.map +1 -1
  38. package/dist/email/defaults.js +26 -26
  39. package/dist/email/index.js +7 -7
  40. package/dist/email/strategies/resend.js +1 -1
  41. package/dist/error/defaults.d.ts +1 -1
  42. package/dist/error/defaults.js +13 -13
  43. package/dist/error/error.d.ts +12 -0
  44. package/dist/error/error.d.ts.map +1 -1
  45. package/dist/error/error.js +19 -0
  46. package/dist/error/error.js.map +1 -1
  47. package/dist/error/index.d.ts +14 -3
  48. package/dist/error/index.d.ts.map +1 -1
  49. package/dist/error/index.js +14 -3
  50. package/dist/error/index.js.map +1 -1
  51. package/dist/event/defaults.js +35 -35
  52. package/dist/event/index.js +7 -7
  53. package/dist/logger/defaults.d.ts +1 -1
  54. package/dist/logger/defaults.js +40 -40
  55. package/dist/logger/index.d.ts +1 -0
  56. package/dist/logger/index.d.ts.map +1 -1
  57. package/dist/logger/index.js.map +1 -1
  58. package/dist/logger/logger.d.ts +8 -0
  59. package/dist/logger/logger.d.ts.map +1 -1
  60. package/dist/logger/logger.js +13 -3
  61. package/dist/logger/logger.js.map +1 -1
  62. package/dist/logger/transports/console.js +2 -2
  63. package/dist/logger/transports/http.d.ts +1 -1
  64. package/dist/logger/transports/http.js +2 -2
  65. package/dist/logger/transports/webhook.d.ts +1 -1
  66. package/dist/logger/transports/webhook.js +3 -3
  67. package/dist/queue/defaults.d.ts +2 -2
  68. package/dist/queue/defaults.js +38 -38
  69. package/dist/security/defaults.d.ts +1 -1
  70. package/dist/security/defaults.js +30 -30
  71. package/dist/security/index.d.ts +1 -1
  72. package/dist/security/index.js +3 -3
  73. package/dist/security/security.d.ts +1 -1
  74. package/dist/security/security.js +4 -4
  75. package/dist/storage/defaults.js +26 -26
  76. package/dist/storage/index.js +3 -3
  77. package/dist/util/defaults.d.ts +1 -1
  78. package/dist/util/defaults.js +41 -41
  79. package/dist/util/env.d.ts +35 -0
  80. package/dist/util/env.d.ts.map +1 -0
  81. package/dist/util/env.js +50 -0
  82. package/dist/util/env.js.map +1 -0
  83. package/dist/util/errors.d.ts +52 -0
  84. package/dist/util/errors.d.ts.map +1 -0
  85. package/dist/util/errors.js +82 -0
  86. package/dist/util/errors.js.map +1 -0
  87. package/dist/util/util.js +1 -1
  88. package/examples/.env.example +80 -0
  89. package/examples/README.md +16 -0
  90. package/examples/auth.ts +228 -0
  91. package/examples/cache.ts +36 -0
  92. package/examples/config.ts +45 -0
  93. package/examples/database.ts +69 -0
  94. package/examples/email.ts +53 -0
  95. package/examples/error.ts +50 -0
  96. package/examples/event.ts +42 -0
  97. package/examples/logger.ts +41 -0
  98. package/examples/queue.ts +58 -0
  99. package/examples/security.ts +46 -0
  100. package/examples/storage.ts +44 -0
  101. package/examples/util.ts +47 -0
  102. package/llms.txt +591 -0
  103. package/package.json +19 -10
  104. package/src/auth/README.md +850 -0
  105. package/src/cache/README.md +756 -0
  106. package/src/config/README.md +604 -0
  107. package/src/database/README.md +818 -0
  108. package/src/email/README.md +759 -0
  109. package/src/error/README.md +660 -0
  110. package/src/event/README.md +729 -0
  111. package/src/logger/README.md +435 -0
  112. package/src/queue/README.md +851 -0
  113. package/src/security/README.md +612 -0
  114. package/src/storage/README.md +1008 -0
  115. package/src/util/README.md +955 -0
  116. package/bin/templates/backend/docs/APPKIT_CLI.md +0 -507
  117. package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +0 -61
  118. package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +0 -2539
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/util/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,SAAS,GAAG,wDAAwD,CAAC;AAE3E,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC3B,MAAM,CAAS;IACf,OAAO,CAAS;IAEzB,YAAY,MAAc,EAAE,OAAe,EAAE,IAAa;QACxD,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,SAAS,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QACtD,KAAK,CAAC,sBAAsB,MAAM,KAAK,OAAO,UAAU,GAAG,EAAE,CAAC,CAAC;QAC/D,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;QAC1B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC;IACrB,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,MAAc,EAAE,OAAe,EAAE,IAAa;IACvE,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QACxC,MAAM,GAAG,GAAG,IAAI;YACd,CAAC,CAAC,sBAAsB,OAAO,iBAAiB,IAAI,EAAE;YACtD,CAAC,CAAC,sBAAsB,OAAO,eAAe,CAAC;QACjD,MAAM,IAAI,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE,uBAAuB,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CACzB,MAAc,EACd,IAAY,EACZ,KAA2B,EAC3B,IAAa;IAEb,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAC1C,MAAM,GAAG,GAAG,IAAI;YACd,CAAC,CAAC,KAAK,IAAI,mBAAmB,IAAI,EAAE;YACpC,CAAC,CAAC,KAAK,IAAI,iBAAiB,CAAC;QAC/B,MAAM,IAAI,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,MAAc,EAAE,OAAe,EAAE,IAAa;IACtE,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY;QAAE,OAAO;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,SAAS,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IACtD,sCAAsC;IACtC,OAAO,CAAC,IAAI,CAAC,sBAAsB,MAAM,KAAK,OAAO,UAAU,GAAG,EAAE,CAAC,CAAC;AACxE,CAAC"}
package/dist/util/util.js CHANGED
@@ -152,7 +152,7 @@ export class UtilClass {
152
152
  }
153
153
  // Performance warning for large arrays
154
154
  if (this.config.performance.enabled && array.length > this.config.performance.chunkSizeLimit) {
155
- console.warn(`[VoilaJSX Utils] Chunking large array (${array.length} items). Consider streaming or pagination.`);
155
+ console.warn(`[Bloomneo Utils] Chunking large array (${array.length} items). Consider streaming or pagination.`);
156
156
  }
157
157
  const result = [];
158
158
  for (let i = 0; i < array.length; i += size) {
@@ -0,0 +1,80 @@
1
+ # Bloomneo AppKit — canonical environment template
2
+ #
3
+ # Copy this to `.env` in your project root and fill in real values.
4
+ # All env vars use the BLOOM_ prefix. The legacy VOILA_ prefix from
5
+ # @voilajsx/appkit was removed in 1.5.2 — there is no fallback.
6
+ #
7
+ # Generate secrets with: openssl rand -base64 32
8
+
9
+ # ──────────────────────────────────────────────────────────────────────
10
+ # REQUIRED for production
11
+ # ──────────────────────────────────────────────────────────────────────
12
+
13
+ # JWT signing key for authClass.get().signToken() / verifyToken()
14
+ # Minimum 32 characters.
15
+ BLOOM_AUTH_SECRET=replace-me-with-openssl-rand-base64-32
16
+
17
+ # Prisma connection string. Any Prisma-supported URL works.
18
+ DATABASE_URL=postgresql://user:pass@localhost:5432/myapp
19
+
20
+ # CSRF token signing for securityClass.get().csrf() / requireCsrf()
21
+ # Minimum 32 characters.
22
+ BLOOM_SECURITY_CSRF_SECRET=replace-me-with-openssl-rand-base64-32
23
+
24
+ # AES-256-GCM encryption key for securityClass.get().encrypt() / decrypt()
25
+ # Exactly 64 hex characters (32 bytes).
26
+ BLOOM_SECURITY_ENCRYPTION_KEY=replace-me-with-openssl-rand-hex-32
27
+
28
+ # ──────────────────────────────────────────────────────────────────────
29
+ # OPTIONAL — auto-scaling kicks in when set
30
+ # ──────────────────────────────────────────────────────────────────────
31
+
32
+ # Distributed cache + queue + events (memory by default)
33
+ # REDIS_URL=redis://localhost:6379
34
+
35
+ # Cloud storage (local disk by default)
36
+ # AWS_S3_BUCKET=my-bucket
37
+ # AWS_REGION=us-east-1
38
+ # AWS_ACCESS_KEY_ID=...
39
+ # AWS_SECRET_ACCESS_KEY=...
40
+
41
+ # Cloudflare R2 (alternative to S3)
42
+ # R2_BUCKET=my-bucket
43
+ # R2_ACCOUNT_ID=...
44
+ # R2_ACCESS_KEY_ID=...
45
+ # R2_SECRET_ACCESS_KEY=...
46
+
47
+ # Production email (console transport by default)
48
+ # RESEND_API_KEY=re_...
49
+ # or
50
+ # SMTP_HOST=smtp.example.com
51
+ # SMTP_PORT=587
52
+ # SMTP_USER=...
53
+ # SMTP_PASS=...
54
+
55
+ # Multi-tenant database filtering
56
+ # When set to "auto", database queries auto-filter by tenant_id.
57
+ # BLOOM_DB_TENANT=auto
58
+
59
+ # Per-organization databases (multi-org SaaS)
60
+ # Each ORG_<NAME> is a separate connection string used by databaseClass.org('<name>').get()
61
+ # ORG_ACME=postgresql://acme.aws.com/prod
62
+ # ORG_GLOBEX=postgresql://globex.aws.com/prod
63
+
64
+ # Background queue persistence (memory by default)
65
+ # BLOOM_QUEUE_DB=true # use database-backed queue when REDIS_URL not set
66
+
67
+ # Logger transports
68
+ # BLOOM_LOGGER_FILE_PATH=/var/log/myapp.log
69
+ # BLOOM_LOGGER_HTTP_URL=https://logs.example.com/ingest
70
+ # BLOOM_LOGGER_WEBHOOK_URL=https://hooks.slack.com/services/...
71
+
72
+ # Service identification (used by logger metadata)
73
+ # BLOOM_SERVICE_NAME=myapp
74
+ # BLOOM_SERVICE_VERSION=1.0.0
75
+
76
+ # Default user password for seeds (development only)
77
+ # DEFAULT_USER_PASSWORD=changeme123
78
+
79
+ # Frontend API key (for browser → API auth — generated by `appkit generate app`)
80
+ # BLOOM_FRONTEND_KEY=bloom_<random>
@@ -0,0 +1,16 @@
1
+ # Examples
2
+
3
+ One minimal `.ts` file per module. Each example is the smallest runnable shape
4
+ of the canonical pattern — copy, modify the data, ship.
5
+
6
+ For composed multi-module patterns (auth-protected CRUD, multi-tenant API,
7
+ file upload + queue, real-time chat), see [`../cookbook/`](../cookbook/).
8
+
9
+ ## Conventions
10
+
11
+ - Always import from `@bloomneo/appkit` (or the subpath form).
12
+ - Always use `xxxClass.get()` to obtain a module instance.
13
+ - Always cache the module instance at module scope, not inside route handlers.
14
+ - Use semantic error throwers (`error.badRequest()`, `error.unauthorized()`),
15
+ never `throw new Error(...)` in route handlers.
16
+ - See [`.env.example`](./.env.example) for the canonical environment template.
@@ -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
+ });