@bloomneo/appkit 1.2.9 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +195 -0
- package/CHANGELOG.md +253 -0
- package/README.md +147 -799
- package/bin/commands/generate.js +7 -7
- package/cookbook/README.md +26 -0
- package/cookbook/api-key-service.ts +106 -0
- package/cookbook/auth-protected-crud.ts +112 -0
- package/cookbook/file-upload-pipeline.ts +113 -0
- package/cookbook/multi-tenant-saas.ts +87 -0
- package/cookbook/real-time-chat.ts +121 -0
- package/dist/auth/auth.d.ts +21 -4
- package/dist/auth/auth.d.ts.map +1 -1
- package/dist/auth/auth.js +56 -44
- package/dist/auth/auth.js.map +1 -1
- package/dist/auth/defaults.d.ts +1 -1
- package/dist/auth/defaults.js +35 -35
- package/dist/cache/cache.d.ts +29 -6
- package/dist/cache/cache.d.ts.map +1 -1
- package/dist/cache/cache.js +72 -44
- package/dist/cache/cache.js.map +1 -1
- package/dist/cache/defaults.js +29 -29
- package/dist/cache/index.d.ts +19 -10
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +21 -18
- package/dist/cache/index.js.map +1 -1
- package/dist/config/defaults.d.ts +1 -1
- package/dist/config/defaults.js +11 -11
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.js +4 -4
- package/dist/database/adapters/mongoose.d.ts +4 -4
- package/dist/database/adapters/mongoose.js +7 -7
- package/dist/database/adapters/prisma.d.ts +4 -4
- package/dist/database/adapters/prisma.js +7 -7
- package/dist/database/defaults.d.ts +1 -1
- package/dist/database/defaults.js +4 -4
- package/dist/database/index.js +2 -2
- package/dist/database/index.js.map +1 -1
- package/dist/email/defaults.js +26 -26
- package/dist/email/index.js +7 -7
- package/dist/email/strategies/resend.js +1 -1
- package/dist/error/defaults.d.ts +1 -1
- package/dist/error/defaults.js +13 -13
- package/dist/error/error.d.ts +12 -0
- package/dist/error/error.d.ts.map +1 -1
- package/dist/error/error.js +19 -0
- package/dist/error/error.js.map +1 -1
- package/dist/error/index.d.ts +14 -3
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +14 -3
- package/dist/error/index.js.map +1 -1
- package/dist/event/defaults.js +35 -35
- package/dist/event/index.js +7 -7
- package/dist/logger/defaults.d.ts +1 -1
- package/dist/logger/defaults.js +40 -40
- package/dist/logger/index.d.ts +1 -0
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/logger/logger.d.ts +8 -0
- package/dist/logger/logger.d.ts.map +1 -1
- package/dist/logger/logger.js +13 -3
- package/dist/logger/logger.js.map +1 -1
- package/dist/logger/transports/console.js +2 -2
- package/dist/logger/transports/http.d.ts +1 -1
- package/dist/logger/transports/http.js +2 -2
- package/dist/logger/transports/webhook.d.ts +1 -1
- package/dist/logger/transports/webhook.js +3 -3
- package/dist/queue/defaults.d.ts +2 -2
- package/dist/queue/defaults.js +38 -38
- package/dist/security/defaults.d.ts +1 -1
- package/dist/security/defaults.js +30 -30
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +3 -3
- package/dist/security/security.d.ts +1 -1
- package/dist/security/security.js +4 -4
- package/dist/storage/defaults.js +26 -26
- package/dist/storage/index.js +3 -3
- package/dist/util/defaults.d.ts +1 -1
- package/dist/util/defaults.js +41 -41
- package/dist/util/env.d.ts +35 -0
- package/dist/util/env.d.ts.map +1 -0
- package/dist/util/env.js +50 -0
- package/dist/util/env.js.map +1 -0
- package/dist/util/errors.d.ts +52 -0
- package/dist/util/errors.d.ts.map +1 -0
- package/dist/util/errors.js +82 -0
- package/dist/util/errors.js.map +1 -0
- package/dist/util/util.js +1 -1
- package/examples/.env.example +80 -0
- package/examples/README.md +16 -0
- package/examples/auth.ts +228 -0
- package/examples/cache.ts +36 -0
- package/examples/config.ts +45 -0
- package/examples/database.ts +69 -0
- package/examples/email.ts +53 -0
- package/examples/error.ts +50 -0
- package/examples/event.ts +42 -0
- package/examples/logger.ts +41 -0
- package/examples/queue.ts +58 -0
- package/examples/security.ts +46 -0
- package/examples/storage.ts +44 -0
- package/examples/util.ts +47 -0
- package/llms.txt +591 -0
- package/package.json +19 -10
- package/src/auth/README.md +850 -0
- package/src/cache/README.md +756 -0
- package/src/config/README.md +604 -0
- package/src/database/README.md +818 -0
- package/src/email/README.md +759 -0
- package/src/error/README.md +660 -0
- package/src/event/README.md +729 -0
- package/src/logger/README.md +435 -0
- package/src/queue/README.md +851 -0
- package/src/security/README.md +612 -0
- package/src/storage/README.md +1008 -0
- package/src/util/README.md +955 -0
- package/bin/templates/backend/docs/APPKIT_CLI.md +0 -507
- package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +0 -61
- package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +0 -2539
|
@@ -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
|
+
});
|
|
@@ -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
|
+
});
|
package/examples/util.ts
ADDED
|
@@ -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 };
|