@mariachi/core 0.0.1

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.
@@ -0,0 +1,169 @@
1
+ # Recipe: Add a Webhook Endpoint
2
+
3
+ This walks through creating a webhook endpoint that receives callbacks from third-party services (e.g. Stripe, GitHub). Mariachi's `@mariachi/webhooks` package provides `WebhookController` with two processing modes: `direct` (synchronous via communication layer) and `queue` (asynchronous via job queue).
4
+
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ ```
10
+ Third-party POST → WebhookServer → AuthController.auth() → WebhookController handler
11
+ ├── mode: 'direct' → communication.call(procedure, payload)
12
+ └── mode: 'queue' → jobQueue.enqueue(jobName, payload)
13
+ ```
14
+
15
+ All webhooks are logged via `WebhookLogStore`.
16
+
17
+ ---
18
+
19
+ ## 1. Create an Auth Controller
20
+
21
+ Each webhook controller needs an `AuthController` that verifies the incoming request. Extend either `ApiKeyAuthController` or `OAuthAuthController`.
22
+
23
+ **File:** e.g. `src/webhooks/github-auth.ts`
24
+
25
+ ```ts
26
+ import type { RequestContext } from '@mariachi/server';
27
+ import { ApiKeyAuthController } from '@mariachi/webhooks';
28
+
29
+ export class GitHubWebhookAuth extends ApiKeyAuthController {
30
+ readonly provider = 'github';
31
+ protected readonly headerName = 'x-hub-signature-256';
32
+
33
+ protected async verify(signature: string, ctx: RequestContext): Promise<boolean> {
34
+ // Verify the HMAC signature against the webhook secret
35
+ return signature.length > 0;
36
+ }
37
+ }
38
+ ```
39
+
40
+ **Auth controller types:**
41
+ - `ApiKeyAuthController` — verifies a header value. Override `headerName` for custom headers.
42
+ - `OAuthAuthController` — verifies a Bearer token.
43
+ - Custom: extend `AuthController` directly and implement `auth(req, ctx)`.
44
+
45
+ ---
46
+
47
+ ## 2. Create the Webhook Controller
48
+
49
+ Extend `WebhookController` with a `prefix`, `auth`, and route definitions in `init()`.
50
+
51
+ **File:** e.g. `src/webhooks/github.controller.ts`
52
+
53
+ ```ts
54
+ import { WebhookController, type WebhookContext, type WebhookRouteOpts } from '@mariachi/webhooks';
55
+ import { GitHubWebhookAuth } from './github-auth';
56
+
57
+ export class GitHubWebhookController extends WebhookController {
58
+ readonly prefix = 'github';
59
+ readonly auth = new GitHubWebhookAuth();
60
+
61
+ init() {
62
+ this.post(this.buildPath('push'), {
63
+ mode: 'direct',
64
+ procedure: 'github.handlePush',
65
+ ttl: '30d',
66
+ }, this.handlePush);
67
+
68
+ this.post(this.buildPath('issue'), {
69
+ mode: 'queue',
70
+ jobName: 'github.processIssue',
71
+ ttl: '30d',
72
+ }, this.handleIssue);
73
+ }
74
+
75
+ handlePush = async (ctx: WebhookContext, body: unknown) => {
76
+ ctx.logger.info('Received GitHub push webhook');
77
+ return body;
78
+ };
79
+
80
+ handleIssue = async (ctx: WebhookContext, body: unknown) => {
81
+ ctx.logger.info('Received GitHub issue webhook');
82
+ return body;
83
+ };
84
+ }
85
+ ```
86
+
87
+ **Route options (`WebhookRouteOpts`):**
88
+
89
+ | Field | Required | Description |
90
+ |-------|----------|-------------|
91
+ | `mode` | Yes | `'direct'` (sync) or `'queue'` (async) |
92
+ | `procedure` | When `mode: 'direct'` | Communication procedure name |
93
+ | `jobName` | When `mode: 'queue'` | Job name for `@mariachi/jobs` |
94
+ | `ttl` | No | Log retention duration (e.g. `'7d'`, `'30d'`) |
95
+
96
+ ---
97
+
98
+ ## 3. Register on a WebhookServer
99
+
100
+ **File:** e.g. `src/index.ts`
101
+
102
+ ```ts
103
+ import { WebhookServer } from '@mariachi/webhooks';
104
+ import { GitHubWebhookController } from './webhooks/github.controller';
105
+
106
+ const webhookServer = new WebhookServer(
107
+ { name: 'webhooks', defaultTtl: '7d' },
108
+ { communication, jobQueue, logStore },
109
+ );
110
+
111
+ webhookServer.registerController(new GitHubWebhookController());
112
+
113
+ startup.register({
114
+ name: 'webhook-server',
115
+ priority: 100,
116
+ fn: async () => {
117
+ await webhookServer.listen(WEBHOOK_PORT);
118
+ },
119
+ });
120
+ ```
121
+
122
+ ---
123
+
124
+ ## 4. Register the Handler or Job
125
+
126
+ **For `mode: 'direct'`** — register a communication handler:
127
+
128
+ ```ts
129
+ communication.register('github.handlePush', {
130
+ schema: { input: GitHubPushInput, output: z.object({ ok: z.boolean() }) },
131
+ handler: async (ctx, input) => {
132
+ return { ok: true };
133
+ },
134
+ });
135
+ ```
136
+
137
+ **For `mode: 'queue'`** — register a job:
138
+
139
+ ```ts
140
+ export const ProcessGitHubIssueJob = {
141
+ name: 'github.processIssue',
142
+ schema: z.object({ action: z.string(), issue: z.object({ id: z.number() }) }),
143
+ retry: { attempts: 3, backoff: 'exponential' as const },
144
+ handler: async (data, ctx) => {
145
+ ctx.logger.info({ issueId: data.issue.id }, 'Processing GitHub issue');
146
+ },
147
+ };
148
+ ```
149
+
150
+ ---
151
+
152
+ ## When to Use Direct vs Queue
153
+
154
+ | | Direct (`mode: 'direct'`) | Queue (`mode: 'queue'`) |
155
+ |-|---------------------------|-------------------------|
156
+ | **Use when** | Response must be synchronous | Processing is slow or can be retried |
157
+ | **Backed by** | `communication.call()` | `jobQueue.enqueue()` (BullMQ/Redis) |
158
+ | **Retries** | No built-in retry | BullMQ retry with backoff |
159
+
160
+ ---
161
+
162
+ ## Checklist
163
+
164
+ - [ ] Auth controller created (extends `ApiKeyAuthController` or `OAuthAuthController`)
165
+ - [ ] Webhook controller created with `prefix`, `auth`, and routes in `init()`
166
+ - [ ] Routes configured with correct `mode`, `procedure`/`jobName`, and optional `ttl`
167
+ - [ ] Controller registered on `WebhookServer`
168
+ - [ ] Communication handler or job registered for each route
169
+ - [ ] Webhook secret stored via `@mariachi/config` (never hardcoded)
@@ -0,0 +1,250 @@
1
+ # Recipe: Wiring and Bootstrap
2
+
3
+ This shows the full initialization sequence for a Mariachi application — from loading config to accepting requests. Understanding the wiring order is critical because components depend on each other: communication handlers must be registered before controllers call them, and infrastructure (DB, Redis) must connect before services that use them.
4
+
5
+ ---
6
+
7
+ ## Initialization Order
8
+
9
+ ```
10
+ 1. Load config loadConfig() or bootstrap()
11
+ 2. Create observability createObservability() → logger, tracer, metrics
12
+ 3. Register in container container.register(KEYS.Logger, logger) etc.
13
+ 4. Create infrastructure createPostgresDatabase(), createCache(), createEventBus()
14
+ 5. Register infra in DI container.register(KEYS.Database, db) etc.
15
+ 6. Create communication createCommunication()
16
+ 7. Register handlers registerServiceHandlers(communication)
17
+ 8. Create servers new FastifyAdapter() with auth + rate limiting
18
+ 9. Register controllers server.registerController(new XxxController())
19
+ 10. Connect infra startup hooks: db.connect(), cache.connect()
20
+ 11. Start servers server.listen(port)
21
+ ```
22
+
23
+ Steps 6-7 must happen before 8-9. If a controller calls `communication.call('users.create', ...)` but no handler is registered for `users.create`, it will fail at runtime.
24
+
25
+ ---
26
+
27
+ ## Minimal Bootstrap
28
+
29
+ The simplest wiring uses `bootstrap()` from `@mariachi/lifecycle`, which handles steps 1-3:
30
+
31
+ ```ts
32
+ import { bootstrap } from '@mariachi/lifecycle';
33
+ import { createCommunication } from '@mariachi/communication';
34
+ import { registerServiceHandlers } from './services';
35
+ import { FastifyAdapter } from '@mariachi/api-facade';
36
+ import { UsersController } from './controllers/users.controller';
37
+
38
+ async function main() {
39
+ const { logger, startup, shutdown } = bootstrap();
40
+
41
+ const communication = createCommunication();
42
+ registerServiceHandlers(communication);
43
+
44
+ const server = new FastifyAdapter({ name: 'public' })
45
+ .withAuth(['session', 'api-key'])
46
+ .withRateLimit({ perUser: 1000, perApiKey: 5000, window: '1h' });
47
+
48
+ server.registerController(new UsersController());
49
+
50
+ startup.register({
51
+ name: 'server',
52
+ priority: 100,
53
+ fn: async () => {
54
+ await server.listen(3000);
55
+ logger.info({ port: 3000 }, 'Server started');
56
+ },
57
+ });
58
+
59
+ shutdown.register({
60
+ name: 'server',
61
+ priority: 100,
62
+ fn: () => server.close(),
63
+ });
64
+
65
+ await startup.runAll(logger);
66
+ }
67
+
68
+ main().catch((err) => {
69
+ console.error(err);
70
+ process.exit(1);
71
+ });
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Full Bootstrap (with all infrastructure)
77
+
78
+ For a production app that needs database, cache, events, auth, and billing:
79
+
80
+ ```ts
81
+ import { getContainer, KEYS } from '@mariachi/core';
82
+ import { loadConfig } from '@mariachi/config';
83
+ import { createObservability } from '@mariachi/observability';
84
+ import { bootstrap } from '@mariachi/lifecycle';
85
+ import { createPostgresDatabase } from '@mariachi/database-postgres';
86
+ import { createCache } from '@mariachi/cache';
87
+ import { createEventBus } from '@mariachi/events';
88
+ import { createAuth } from '@mariachi/auth';
89
+ import { createRateLimiter } from '@mariachi/rate-limit';
90
+ import { createCommunication } from '@mariachi/communication';
91
+
92
+ async function main() {
93
+ const config = loadConfig();
94
+
95
+ const { logger, tracer, metrics } = createObservability({
96
+ logging: { adapter: 'pino', level: 'info' },
97
+ });
98
+
99
+ const container = getContainer();
100
+ container.register(KEYS.Config, config);
101
+ container.register(KEYS.Logger, logger);
102
+ container.register(KEYS.Tracer, tracer);
103
+ container.register(KEYS.Metrics, metrics);
104
+
105
+ const db = createPostgresDatabase({ url: config.database.url });
106
+ container.register(KEYS.Database, db);
107
+
108
+ const cache = createCache({ adapter: 'redis', url: config.redis!.url });
109
+ container.register(KEYS.Cache, cache);
110
+
111
+ const events = createEventBus({ adapter: 'redis', url: config.redis!.url });
112
+ container.register(KEYS.EventBus, events);
113
+
114
+ const auth = createAuth({ adapter: 'jwt', jwtSecret: config.auth!.jwtSecret! });
115
+ container.register(KEYS.Auth, auth);
116
+
117
+ const rateLimiter = createRateLimiter({ adapter: 'redis', url: config.redis!.url });
118
+ container.register(KEYS.RateLimit, rateLimiter);
119
+
120
+ const { startup, shutdown, health } = bootstrap();
121
+
122
+ const communication = createCommunication();
123
+ container.register(KEYS.Communication, communication);
124
+ registerServiceHandlers(communication);
125
+
126
+ const publicServer = new FastifyAdapter({ name: 'public' })
127
+ .withAuth(['session', 'api-key'])
128
+ .withRateLimit({ perUser: 1000, perApiKey: 5000, window: '1h' });
129
+
130
+ publicServer.registerController(new UsersController());
131
+ publicServer.registerController(new OrdersController());
132
+
133
+ startup.register({ name: 'database', priority: 1, fn: () => db.connect() });
134
+ startup.register({ name: 'cache', priority: 2, fn: () => cache.connect() });
135
+ startup.register({ name: 'events', priority: 3, fn: () => events.connect() });
136
+
137
+ startup.register({ name: 'servers', priority: 100, fn: async () => {
138
+ await publicServer.listen(3000);
139
+ logger.info({ port: 3000 }, 'Server started');
140
+ }});
141
+
142
+ shutdown.register({ name: 'servers', priority: 1, fn: () => publicServer.close() });
143
+ shutdown.register({ name: 'events', priority: 10, fn: () => events.disconnect() });
144
+ shutdown.register({ name: 'cache', priority: 20, fn: () => cache.disconnect() });
145
+ shutdown.register({ name: 'database', priority: 30, fn: () => db.disconnect() });
146
+
147
+ await startup.runAll(logger);
148
+ }
149
+ ```
150
+
151
+ ---
152
+
153
+ ## Startup/Shutdown Priorities
154
+
155
+ Hooks run in order of priority (lowest first). Convention:
156
+
157
+ | Priority | Phase | What |
158
+ |----------|-------|------|
159
+ | 1-10 | Infrastructure | Database, cache, event bus connections |
160
+ | 50 | Services | Communication handler registration, event subscribers |
161
+ | 100 | Servers | HTTP server listen, WebSocket server start |
162
+
163
+ For shutdown, use the inverse: stop servers first (priority 1), then services, then infrastructure.
164
+
165
+ ---
166
+
167
+ ## Worker Bootstrap
168
+
169
+ Workers follow the same pattern but with job queues:
170
+
171
+ ```ts
172
+ import { bootstrap } from '@mariachi/lifecycle';
173
+ import { createJobQueue } from '@mariachi/jobs';
174
+ import { SendEmailJob } from './jobs/send-email.job';
175
+ import { schedules } from './jobs/schedules';
176
+
177
+ const { config, logger, startup, shutdown } = bootstrap();
178
+
179
+ const jobQueue = createJobQueue({
180
+ adapter: 'bullmq',
181
+ redisUrl: config.redis!.url,
182
+ }, logger);
183
+
184
+ jobQueue.registerJob(SendEmailJob);
185
+ for (const schedule of schedules) {
186
+ jobQueue.register(schedule);
187
+ }
188
+
189
+ startup.register({
190
+ name: 'job-queue',
191
+ priority: 10,
192
+ fn: async () => {
193
+ await jobQueue.connect();
194
+ await jobQueue.start();
195
+ },
196
+ });
197
+
198
+ shutdown.register({
199
+ name: 'job-queue',
200
+ priority: 10,
201
+ fn: async () => {
202
+ await jobQueue.stop();
203
+ await jobQueue.disconnect();
204
+ },
205
+ });
206
+
207
+ startup.runAll(logger).catch((err) => {
208
+ logger.error({ err }, 'Worker failed to start');
209
+ process.exit(1);
210
+ });
211
+ ```
212
+
213
+ ---
214
+
215
+ ## DI Container Keys
216
+
217
+ All well-known keys are defined in `@mariachi/core`:
218
+
219
+ ```ts
220
+ import { KEYS } from '@mariachi/core';
221
+
222
+ KEYS.Config // AppConfig
223
+ KEYS.Logger // Logger
224
+ KEYS.Tracer // TracerAdapter
225
+ KEYS.Metrics // MetricsAdapter
226
+ KEYS.Database // Database / PostgresAdapter
227
+ KEYS.Cache // Cache / CacheClient
228
+ KEYS.EventBus // EventBus
229
+ KEYS.JobQueue // JobQueue
230
+ KEYS.Auth // Auth
231
+ KEYS.Communication // CommunicationLayer
232
+ // ... and others
233
+ ```
234
+
235
+ Services that extend `Instrumentable` automatically resolve `Logger`, `Tracer`, and `Metrics` from the container.
236
+
237
+ ---
238
+
239
+ ## Checklist
240
+
241
+ - [ ] Config loaded and validated
242
+ - [ ] Logger + tracer + metrics created and registered in container
243
+ - [ ] Infrastructure created (DB, cache, events) and registered in container
244
+ - [ ] Communication layer created
245
+ - [ ] Service handlers registered on communication layer
246
+ - [ ] Servers created with auth and rate limiting
247
+ - [ ] Controllers registered on servers
248
+ - [ ] Startup hooks registered (infra connect at low priority, servers at high priority)
249
+ - [ ] Shutdown hooks registered (reverse order)
250
+ - [ ] `startup.runAll(logger)` called
@@ -0,0 +1,84 @@
1
+ # Mariachi Runbook
2
+
3
+ Operational guide for developing and running Mariachi applications.
4
+
5
+ ## Prerequisites
6
+
7
+ - **Node.js** ≥ 20
8
+ - **pnpm** (package manager)
9
+
10
+ ## Getting Started
11
+
12
+ ```bash
13
+ pnpm install
14
+ pnpm run build
15
+ pnpm run dev
16
+ ```
17
+
18
+ ## Running Tests
19
+
20
+ ```bash
21
+ pnpm run test
22
+ ```
23
+
24
+ For a single run (CI):
25
+
26
+ ```bash
27
+ pnpm run test:run
28
+ ```
29
+
30
+ ## Adding a New Service
31
+
32
+ Generate a service with handlers and tests (if using `@mariachi/cli`):
33
+
34
+ ```bash
35
+ mariachi generate service <name>
36
+ ```
37
+
38
+ Example:
39
+
40
+ ```bash
41
+ mariachi generate service orders
42
+ ```
43
+
44
+ Creates a domain folder with `orders.service.ts`, `orders.handler.ts`, and tests.
45
+
46
+ ## Adding a New Integration
47
+
48
+ Generate an integration scaffold (if using `@mariachi/cli`):
49
+
50
+ ```bash
51
+ mariachi generate integration <name>
52
+ ```
53
+
54
+ ## Validating
55
+
56
+ Validate project structure and conventions (if using `@mariachi/cli`):
57
+
58
+ ```bash
59
+ mariachi validate
60
+ ```
61
+
62
+ Optionally specify a path:
63
+
64
+ ```bash
65
+ mariachi validate ./apps
66
+ ```
67
+
68
+ ## Environment Variables
69
+
70
+ | Variable | Description | Default |
71
+ |----------|-------------|---------|
72
+ | `NODE_ENV` / `ENV` | Environment (`development`, `test`, `production`) | — |
73
+ | `PORT` | Public API port | `3000` |
74
+ | `ADMIN_PORT` | Admin API port | `3001` |
75
+ | `WEBHOOK_PORT` | Webhook server port | `3002` |
76
+ | `DATABASE_URL` | PostgreSQL connection string | — |
77
+ | `DATABASE_ADAPTER` | Database adapter | `postgres` |
78
+ | `DATABASE_POOL_MIN` | Connection pool minimum | `2` |
79
+ | `DATABASE_POOL_MAX` | Connection pool maximum | `10` |
80
+ | `REDIS_URL` | Redis connection string | — |
81
+ | `JWT_SECRET` | JWT signing secret | — |
82
+ | `SESSION_SECRET` | Session secret (fallback for JWT) | — |
83
+
84
+ Configuration is loaded via `@mariachi/config`; avoid using `process.env` directly outside that package.
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@mariachi/core",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.js",
9
+ "require": "./dist/index.cjs"
10
+ }
11
+ },
12
+ "main": "./dist/index.cjs",
13
+ "module": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "files": [
16
+ "dist",
17
+ "docs",
18
+ "README.md",
19
+ ".mariachi",
20
+ ".cursor"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "dependencies": {
26
+ "zod": "^3.24"
27
+ },
28
+ "devDependencies": {
29
+ "typescript": "^5.7",
30
+ "tsup": "^8"
31
+ },
32
+ "scripts": {
33
+ "build": "tsup",
34
+ "dev": "tsup --watch",
35
+ "typecheck": "tsc --noEmit",
36
+ "clean": "rm -rf dist .turbo"
37
+ }
38
+ }