@twasik4/pocket-service 1.0.4 → 1.1.0

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 (41) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE +21 -0
  3. package/README.md +637 -297
  4. package/dist/index.d.mts +747 -0
  5. package/dist/index.d.mts.map +1 -0
  6. package/dist/index.mjs +4 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/dist/runtime/worker-thread.d.mts +1 -0
  9. package/dist/runtime/worker-thread.mjs +2 -0
  10. package/dist/runtime/worker-thread.mjs.map +1 -0
  11. package/package.json +53 -10
  12. package/dist/index.d.ts +0 -734
  13. package/dist/index.js +0 -6
  14. package/dist/index.js.map +0 -1
  15. package/dist/worker-thread.d.ts +0 -2
  16. package/dist/worker-thread.js +0 -2
  17. package/dist/worker-thread.js.map +0 -1
  18. package/src/index.ts +0 -1
  19. package/src/worker-thread.ts +0 -159
  20. package/src/workers/auth/strategies.ts +0 -304
  21. package/src/workers/db/clickhouse/index.ts +0 -76
  22. package/src/workers/db/mongo/collection.ts +0 -293
  23. package/src/workers/db/mongo/mongo.ts +0 -401
  24. package/src/workers/express-types.ts +0 -442
  25. package/src/workers/index.ts +0 -7
  26. package/src/workers/logger.ts +0 -19
  27. package/src/workers/service.ts +0 -1015
  28. package/src/workers/stream-handler.ts +0 -25
  29. package/src/workers/types.ts +0 -59
  30. package/src/workers/utils.ts +0 -19
  31. package/tests/auth-strategies.spec.ts +0 -150
  32. package/tests/clickhouse.spec.ts +0 -102
  33. package/tests/express-types.spec.ts +0 -232
  34. package/tests/mongo-advanced.spec.ts +0 -172
  35. package/tests/mongo.spec.ts +0 -141
  36. package/tests/redis.spec.ts +0 -36
  37. package/tests/service.spec.ts +0 -82
  38. package/tests/utils.spec.ts +0 -30
  39. package/tsconfig.json +0 -12
  40. package/tsup.config.ts +0 -11
  41. package/vitest.config.ts +0 -8
package/README.md CHANGED
@@ -1,297 +1,637 @@
1
- # Pocket Service
2
-
3
- Pocket service is a wrapper that provides you a variety of options for a easy, boiler plate free, typescript service.
4
-
5
- Pocket service comes with the following features built in:
6
-
7
- - Express API with a few built in endpoints
8
- - Full type support via a builder pattern
9
- - An optional mongo module
10
- - An optional clickhouse module
11
- - An optional redis module
12
- - An optional redis worker module for consuming redis streams
13
- - A way to add your own modules/startup hooks that run during the build phase
14
-
15
- ## Creating a service
16
-
17
- Creating a service is done with the service class. It will streamline the creation process and reduce boilerplate.
18
-
19
- The following is the most basic service you can create.
20
-
21
- ```ts
22
- const service = new Service().build();
23
- ```
24
-
25
- This basic service will include an express server with a few [built in endpoints](#built-in-endpoints) as well as a built in winston instance for logging.
26
-
27
- ### Built-in Endpoints
28
-
29
- The following are the base routes available:
30
-
31
- | Method | Route | Description |
32
- | ------ | ------- | ------------------------------------------ |
33
- | GET | / | OK Check |
34
- | GET | /health | Used for health information on the service |
35
-
36
- ### Modules
37
-
38
- 1. withMongo()
39
-
40
- A full MongoDB instance with a custom collection abstraction that offers full type support and auto completion.
41
-
42
- 2. withRedis()
43
-
44
- A full Redis instance, this has built in service registration.
45
-
46
- 3. withClickhouse()
47
-
48
- A WIP, this will be similar to the mongo abstraction and offer strong typing for queries.
49
-
50
- 4. withWorkers()
51
-
52
- This adds built in workers to consume Redis streams off the main loop.
53
-
54
- The following additional routes will get added to express:
55
-
56
- | Method | Route | Description |
57
- | ------ | ------------------------------ | --------------------------------------------------- |
58
- | GET | /workers | Used for worker health information |
59
- | GET | /workers/:stream | Gets all messages from this services stream |
60
- | POST | /workers/:stream/:id/ack | Force acknowledge a message in this services stream |
61
- | GET | /workers/:stream/dlq | Gets all messages from this services DLQ stream |
62
- | POST | /workers/:stream/dlq/:id/retry | Force retry a message in this services DLQ stream |
63
-
64
- 5. withModules(customModules: CustomModule[])
65
-
66
- Use this to register custom modules to the service.
67
-
68
- ### Configurations
69
-
70
- 1. withName()
71
-
72
- Sets the name of the service, used primarily with service registration via Redis. Defaults to a random byte string.
73
-
74
- ```ts
75
- const service = new Service().withName("Service Name").build();
76
- ```
77
-
78
- 2. withPort()
79
-
80
- Set the port of the express server. Defaults to 3100.
81
-
82
- ```ts
83
- const service = new Service().withPort(8111).build();
84
- ```
85
-
86
- Setting the port to the above would make the express instance listen on that port. So your root endpoint would be http://localhost:8111/ and display the following
87
-
88
- ```ts
89
- {
90
- "isSuccess": true,
91
- "message": "Service Name service is running",
92
- "uptime": {
93
- "days": 0,
94
- "hours": 0,
95
- "minutes": 0,
96
- "seconds": 4
97
- }
98
- }
99
- ```
100
-
101
- 3. withUrl()
102
-
103
- Sets the base url of the service. Useful if you use service registration via Redis.
104
-
105
- ```ts
106
- const service = new Service().withUrl("https://my-domain.com").build();
107
- ```
108
-
109
- 4. withExpressOptions()
110
-
111
- Manually configure various express options like CORS, custom middleware, etc.
112
-
113
- ```ts
114
- const service = new Service()
115
- .withExpressOptions({
116
- asJson: true,
117
- corsWhitelist: ["http://localhost:5173"],
118
- credentials: true,
119
- })
120
- .build();
121
- ```
122
-
123
- The above code snippet would parse all incoming bodies with the express JSON parser as well as enforce CORS, whitelisting localhost on 5173 and pass credentials.
124
-
125
- 5. addRouter()
126
-
127
- Registers a custom router with express. This offers ways to set additional metadata and offers the ability to add validation via Zod schemas.
128
-
129
- An example auth router could look like the following.
130
-
131
- ```ts
132
- const { router, routes, addRoute } = createMetaRouter();
133
-
134
- addRoute(
135
- {
136
- fullPath: "/login",
137
- method: "POST",
138
- requireAuth: false,
139
- bodyValidator: z.object({
140
- email: z.email(),
141
- password: z.string(),
142
- }),
143
- meta: {
144
- description: "Login endpoint",
145
- },
146
- },
147
- async (req, res) => {
148
- // contents here
149
- res
150
- .status(200)
151
- .json({ isSuccess: true, message: "Successfully logged in." });
152
- },
153
- );
154
-
155
- addRoute(
156
- {
157
- fullPath: "/register",
158
- method: "POST",
159
- requireAuth: false,
160
- bodyValidator: z.object({
161
- email: z.email(),
162
- password: z.string().min(6),
163
- }),
164
- meta: {
165
- description: "Register endpoint",
166
- },
167
- },
168
- async (req, res) => {
169
- // contents here
170
- res.status(200).json({ isSuccess: true, message: "Registered" });
171
- },
172
- );
173
-
174
- addRoute(
175
- {
176
- fullPath: "/logout",
177
- method: "POST",
178
- requireAuth: true,
179
- bodyValidator: z.object({
180
- refreshToken: z.string(),
181
- }),
182
- meta: {
183
- description: "Logout endpoint",
184
- },
185
- },
186
- async (req, res) => {
187
- // contents here
188
- res.status(200).json({ isSuccess: true, message: "Logged out" });
189
- },
190
- );
191
-
192
- addRoute(
193
- {
194
- fullPath: "/me",
195
- method: "GET",
196
- requireAuth: true,
197
- meta: {
198
- description: "Get current authenticated user info",
199
- },
200
- },
201
- async (req, res) => {
202
- // contents here
203
- res.status(200).json({ isSuccess: true, message: "Me" });
204
- },
205
- );
206
-
207
- addRoute(
208
- {
209
- fullPath: "/refresh",
210
- method: "POST",
211
- requireAuth: false,
212
- meta: {
213
- description: "Register endpoint",
214
- },
215
- },
216
- async (req, res) => {
217
- // contents here
218
- res.json({ isSuccess: true, message: "Token refreshed" });
219
- },
220
- );
221
-
222
- export { router as AuthRouter, routes as AuthRoutes };
223
- ```
224
-
225
- 6. withAuthStrategy() / withAuthStrategies()
226
-
227
- Use these hooks to plug in JWT, mTLS, or custom auth behavior while keeping route-level `requireAuth` unchanged.
228
-
229
- ```ts
230
- import {
231
- Service,
232
- createJoseJwtVerifier,
233
- createJwtAuthStrategy,
234
- createMtlsAuthStrategy,
235
- } from "@twasik4/pocket-service";
236
-
237
- const mtlsStrategy = createMtlsAuthStrategy({
238
- requireAuthorized: true,
239
- });
240
-
241
- const jwtStrategy = createJwtAuthStrategy({
242
- verifyToken: createJoseJwtVerifier({
243
- jwksUri: "http://auth-service:3101/.well-known/jwks.json",
244
- issuer: "auth-service",
245
- audience: "gateway-service",
246
- }),
247
- });
248
-
249
- const service = new Service()
250
- .withAuthStrategies([mtlsStrategy, jwtStrategy])
251
- .withoutDefaultHeaderAuthFallback()
252
- .build();
253
- ```
254
-
255
- If you use HMAC-signed tokens instead of JWKS:
256
-
257
- ```ts
258
- import {
259
- Service,
260
- createJoseJwtVerifier,
261
- createJwtAuthStrategy,
262
- } from "@twasik4/pocket-service";
263
-
264
- const jwtStrategy = createJwtAuthStrategy({
265
- verifyToken: createJoseJwtVerifier({
266
- secret: process.env.INTERNAL_JWT_SECRET!,
267
- issuer: "auth-service",
268
- audience: "gateway-service",
269
- algorithms: ["HS256"],
270
- }),
271
- });
272
-
273
- const service = new Service()
274
- .withAuthStrategy(jwtStrategy)
275
- .withoutDefaultHeaderAuthFallback()
276
- .build();
277
- ```
278
-
279
- Default behavior remains backward compatible. If no strategy authenticates and fallback is enabled, the service still reads `x-user-id` / `x-user-meta` headers.
280
-
281
- ### Methods
282
-
283
- 1. shutdown()
284
-
285
- This shuts down the service, gracefully stopping all modules and the service itself.
286
-
287
- 2. build()
288
-
289
- The build phase, this is where all modules get initialized and loaded as well as what starts up the express server.
290
-
291
- 3. heartbeat()
292
-
293
- A method to start an interval for service registration with Redis. This gets automatically called during the build phase if the Redis module has been loaded.
294
-
295
- 4. register()
296
-
297
- A method to immediately register the service with Redis.
1
+ # Pocket Service
2
+
3
+ Pocket service is a wrapper for building easy, boilerplate-free TypeScript services.
4
+
5
+ Pocket service comes with the following built-in features:
6
+
7
+ - Express API with a few built-in endpoints
8
+ - Full type support via a builder pattern
9
+ - An optional Mongo module
10
+ - An optional ClickHouse module
11
+ - An optional Redis module
12
+ - An optional Redis worker module for consuming Redis streams
13
+ - A way to add your own modules/startup hooks that run during the build phase
14
+
15
+ ## Public imports
16
+
17
+ Import from the package root only:
18
+
19
+ ```ts
20
+ import { Service } from "@twasik4/pocket-service";
21
+ ```
22
+
23
+ Deep imports into `src/*` or `dist/*` are not part of the public API surface.
24
+
25
+ ## Contributor layout
26
+
27
+ Current source layout:
28
+
29
+ 1. `src/core` for `Service` orchestration and core helpers
30
+ 2. `src/api` for Express route metadata/types helpers
31
+ 3. `src/auth` for auth strategies
32
+ 4. `src/modules` for Mongo/ClickHouse adapters
33
+ 5. `src/runtime` for worker runtime and stream handling
34
+
35
+ ## Service Class API
36
+
37
+ Use this as the quick reference, then jump to examples.
38
+
39
+ ### Lifecycle and runtime
40
+
41
+ 1. `build()` -> [Example: Basic Service](#example-basic-service)
42
+ 2. `register()` -> [Modules](#modules)
43
+ 3. `heartbeat()` -> [Modules](#modules)
44
+ 4. `shutdown(opts?)` -> [Modules](#modules)
45
+
46
+ ### Builder methods (chainable)
47
+
48
+ 1. `withName(name)` -> [Configurations](#configurations)
49
+ 2. `withPort(port?)` -> [Configurations](#configurations)
50
+ 3. `withUrl(url)` -> [Configurations](#configurations)
51
+ 4. `withRedis(url?)` -> [Modules](#modules)
52
+ 5. `withMongo(config)` -> [Mongo Collection Helpers](#mongo-collection-helpers)
53
+ 6. `withClickhouse(config)` -> [Modules](#modules)
54
+ 7. `withWorkers(workerCount, serviceStreams?)` -> [Example: withWorkers](#example-withworkers)
55
+ 8. `withModules(customModules)` -> [Modules](#modules)
56
+ 9. `withExpressOptions(options)` -> [Example: withExpressOptions](#example-withexpressoptions)
57
+ 10. `withAuthStrategy(strategy)` -> [Example: withAuthStrategies (JWT + mTLS)](#example-withauthstrategies-jwt--mtls)
58
+ 11. `withAuthStrategies(strategies)` -> [Example: withAuthStrategies (JWT + mTLS)](#example-withauthstrategies-jwt--mtls)
59
+ 12. `withAuthResolver(resolver)` -> [Example: withAuthResolver](#example-withauthresolver)
60
+ 13. `withoutDefaultHeaderAuthFallback()` -> [Example: withAuthStrategies (JWT + mTLS)](#example-withauthstrategies-jwt--mtls)
61
+ 14. `withoutExpress()` -> [Configurations](#configurations)
62
+ 15. `addRouter(base, router, routes?)` -> [Example: addRouter + createMetaRouter](#example-addrouter--createmetarouter)
63
+
64
+ ### Public getters
65
+
66
+ 1. `streamKey`
67
+ 2. `simpleName`
68
+ 3. `fullUrl`
69
+ 4. `status`
70
+ 5. `routes`
71
+ 6. `api`
72
+ 7. `log`
73
+ 8. `redis`
74
+ 9. `mongo`
75
+ 10. `db`
76
+ 11. `clickhouse`
77
+
78
+ ### Advanced public symbols
79
+
80
+ 1. `[Symbol.dispose]()`
81
+ 2. `[Symbol.iterator]()`
82
+
83
+ ## Examples
84
+
85
+ Use the Service class to streamline setup and reduce boilerplate.
86
+
87
+ ### Example: Basic Service
88
+
89
+ ```ts
90
+ const service = new Service().build();
91
+ ```
92
+
93
+ This includes an Express server with a few [built-in endpoints](#built-in-endpoints), plus a built-in Winston logger.
94
+
95
+ ### Built-in Endpoints
96
+
97
+ Base routes:
98
+
99
+ | Method | Route | Description |
100
+ | ------ | ------- | ------------------------------------------ |
101
+ | GET | / | OK Check |
102
+ | GET | /health | Used for health information on the service |
103
+
104
+ ### Modules
105
+
106
+ 1. withMongo()
107
+
108
+ A full MongoDB instance with a custom collection abstraction that offers full type support and auto-completion.
109
+
110
+ This also supports:
111
+
112
+ - Collection-level indexes
113
+ - Collection-level TTL without manually defining indexes
114
+ - Time-series collections (with granularity + retention support)
115
+ - Optional revision tracking for updates
116
+
117
+ 2. withRedis()
118
+
119
+ A full Redis instance with built-in service registration.
120
+
121
+ 3. withClickhouse()
122
+
123
+ Typed wrapper for table `select` and `insert`, plus custom methods with strongly typed inputs.
124
+
125
+ ### Example: withClickhouse
126
+
127
+ ```ts
128
+ const service = new Service()
129
+ .withClickhouse({
130
+ url: "http://localhost:8123",
131
+ username: "default",
132
+ password: "",
133
+ tables: {
134
+ events: {
135
+ name: "events",
136
+ createSQL: `
137
+ CREATE TABLE IF NOT EXISTS events (
138
+ id String,
139
+ type String,
140
+ createdAt DateTime
141
+ )
142
+ ENGINE = MergeTree
143
+ ORDER BY (createdAt, id)
144
+ `,
145
+ schema: () => ({
146
+ id: "",
147
+ type: "",
148
+ createdAt: "",
149
+ }),
150
+ },
151
+ },
152
+ })
153
+ .build();
154
+
155
+ await service.clickhouse.events.insert([
156
+ { id: "evt_1", type: "user.created", createdAt: "2026-01-01 00:00:00" },
157
+ ]);
158
+
159
+ const rows = await service.clickhouse.events.select({ type: "user.created" });
160
+ ```
161
+
162
+ You can also define custom methods with named inputs (`:inputName`) and strong typing:
163
+
164
+ ```ts
165
+ import { Service, defineClickhouseMethod } from "@twasik4/pocket-service";
166
+
167
+ const service = new Service().withClickhouse({
168
+ url: "http://localhost:8123",
169
+ tables: {
170
+ events: {
171
+ name: "events",
172
+ createSQL: "CREATE TABLE IF NOT EXISTS events (...)",
173
+ schema: () => ({ id: "", type: "", createdAt: "" }),
174
+ methods: () => ({
175
+ byType: defineClickhouseMethod<
176
+ { type: string },
177
+ Array<{ id: string; type: string; createdAt: string }>
178
+ >({
179
+ sql: "SELECT id, type, createdAt FROM events WHERE type = :type",
180
+ }),
181
+ }),
182
+ },
183
+ },
184
+ });
185
+
186
+ const typedRows = await service.clickhouse.events.byType({
187
+ type: "user.created",
188
+ });
189
+ ```
190
+
191
+ 4. withWorkers()
192
+
193
+ Adds built-in workers to consume Redis streams off the main loop.
194
+
195
+ ### Example: withWorkers
196
+
197
+ `withWorkers` only initializes workers when at least one stream is passed in `serviceStreams`.
198
+
199
+ ```ts
200
+ const service = new Service()
201
+ .withRedis("redis://localhost:6379")
202
+ .withWorkers(2, ["auth:events", "billing:events"])
203
+ .build();
204
+ ```
205
+
206
+ If `serviceStreams` is empty, workers are not initialized.
207
+
208
+ Worker files are loaded from `src/handlers`.
209
+
210
+ Additional routes added to Express:
211
+
212
+ | Method | Route | Description |
213
+ | ------ | ------------------------------ | ---------------------------------------------------- |
214
+ | GET | /workers | Used for worker health information |
215
+ | GET | /workers/:stream | Gets all messages from this service's stream |
216
+ | POST | /workers/:stream/:id/ack | Force acknowledge a message in this service's stream |
217
+ | GET | /workers/:stream/dlq | Gets all messages from this service's DLQ stream |
218
+ | POST | /workers/:stream/dlq/:id/retry | Force retries a message in this service's DLQ stream |
219
+
220
+ 5. withModules(customModules: CustomModules)
221
+
222
+ Use this to register custom modules.
223
+
224
+ You can also add optional `shutdown` hooks:
225
+
226
+ ```ts
227
+ const service = new Service().withModules([
228
+ {
229
+ name: "analytics",
230
+ init: async (log) => {
231
+ log.info("analytics init");
232
+ return true;
233
+ },
234
+ shutdown: async (log) => {
235
+ log.info("analytics shutdown");
236
+ return true;
237
+ },
238
+ },
239
+ ]);
240
+ ```
241
+
242
+ ### Configurations
243
+
244
+ 1. withName()
245
+
246
+ Sets the service name, used primarily for service registration via Redis. Defaults to a random hex string.
247
+
248
+ ```ts
249
+ const service = new Service().withName("Service Name").build();
250
+ ```
251
+
252
+ 2. withPort()
253
+
254
+ Sets the port of the Express server. Defaults to 3100.
255
+
256
+ ```ts
257
+ const service = new Service().withPort(8111).build();
258
+ ```
259
+
260
+ Setting the port as above makes Express listen on that port. Your root endpoint at http://localhost:8111/ returns:
261
+
262
+ ```ts
263
+ OK from "Service Name" service
264
+ ```
265
+
266
+ 3. withUrl()
267
+
268
+ Sets the base URL of the service. Useful for service registration via Redis.
269
+
270
+ ```ts
271
+ const service = new Service().withUrl("https://my-domain.com").build();
272
+ ```
273
+
274
+ 4. withExpressOptions()
275
+
276
+ Manually configures Express options like CORS and custom middleware.
277
+
278
+ ### Example: withExpressOptions
279
+
280
+ ```ts
281
+ const service = new Service()
282
+ .withExpressOptions({
283
+ asJson: true,
284
+ corsWhitelist: ["http://localhost:5173"],
285
+ credentials: true,
286
+ })
287
+ .build();
288
+ ```
289
+
290
+ The snippet above parses incoming bodies with the Express JSON parser, enforces CORS, whitelists localhost on 5173, and passes credentials.
291
+
292
+ You can also use `corsFn` instead of `corsWhitelist`, and `omitDefaultRoutes` if you do not want `/` and `/health`.
293
+
294
+ ```ts
295
+ const service = new Service()
296
+ .withExpressOptions({
297
+ asJson: true,
298
+ omitDefaultRoutes: true,
299
+ corsFn: (origin, cb) => {
300
+ cb(null, origin === "http://localhost:5173");
301
+ },
302
+ })
303
+ .build();
304
+ ```
305
+
306
+ 5. addRouter()
307
+
308
+ Registers a custom router with Express. This lets you add metadata and validation with Zod schemas.
309
+
310
+ ### Example: addRouter + createMetaRouter
311
+
312
+ ```ts
313
+ const { router, routes, addRoute } = createMetaRouter();
314
+
315
+ addRoute(
316
+ {
317
+ fullPath: "/login",
318
+ method: "POST",
319
+ requireAuth: false,
320
+ bodyValidator: z.object({
321
+ email: z.email(),
322
+ password: z.string(),
323
+ }),
324
+ meta: {
325
+ description: "Login endpoint",
326
+ },
327
+ },
328
+ async (req, res) => {
329
+ // contents here
330
+ res
331
+ .status(200)
332
+ .json({ isSuccess: true, message: "Successfully logged in." });
333
+ },
334
+ );
335
+
336
+ addRoute(
337
+ {
338
+ fullPath: "/register",
339
+ method: "POST",
340
+ requireAuth: false,
341
+ bodyValidator: z.object({
342
+ email: z.email(),
343
+ password: z.string().min(6),
344
+ }),
345
+ meta: {
346
+ description: "Register endpoint",
347
+ },
348
+ },
349
+ async (req, res) => {
350
+ // contents here
351
+ res.status(200).json({ isSuccess: true, message: "Registered" });
352
+ },
353
+ );
354
+
355
+ addRoute(
356
+ {
357
+ fullPath: "/logout",
358
+ method: "POST",
359
+ requireAuth: true,
360
+ bodyValidator: z.object({
361
+ refreshToken: z.string(),
362
+ }),
363
+ meta: {
364
+ description: "Logout endpoint",
365
+ },
366
+ },
367
+ async (req, res) => {
368
+ // contents here
369
+ res.status(200).json({ isSuccess: true, message: "Logged out" });
370
+ },
371
+ );
372
+
373
+ addRoute(
374
+ {
375
+ fullPath: "/me",
376
+ method: "GET",
377
+ requireAuth: true,
378
+ meta: {
379
+ description: "Get current authenticated user info",
380
+ },
381
+ },
382
+ async (req, res) => {
383
+ // contents here
384
+ res.status(200).json({ isSuccess: true, message: "Me" });
385
+ },
386
+ );
387
+
388
+ addRoute(
389
+ {
390
+ fullPath: "/refresh",
391
+ method: "POST",
392
+ requireAuth: false,
393
+ meta: {
394
+ description: "Register endpoint",
395
+ },
396
+ },
397
+ async (req, res) => {
398
+ // contents here
399
+ res.json({ isSuccess: true, message: "Token refreshed" });
400
+ },
401
+ );
402
+
403
+ export { router as AuthRouter, routes as AuthRoutes };
404
+ ```
405
+
406
+ `createMetaRouter` also accepts optional auth hooks:
407
+
408
+ ```ts
409
+ const { router, routes, addRoute } = createMetaRouter({
410
+ authResolver: async (req, route) => {
411
+ // custom auth logic
412
+ return null;
413
+ },
414
+ onUnauthorized: (req, res) => {
415
+ res.status(401).json({ isSuccess: false, message: "Unauthorized" });
416
+ },
417
+ onAuthError: (err, req, res) => {
418
+ res.status(401).json({ isSuccess: false, message: "Unauthorized" });
419
+ },
420
+ });
421
+ ```
422
+
423
+ If no authResolver is provided, route auth falls back to the built-in header resolver (`x-user-id` / `x-user-meta`).
424
+
425
+ 6. withAuthStrategy() / withAuthStrategies()
426
+
427
+ Use these hooks to plug in JWT, mTLS, or custom auth behavior while keeping route-level `requireAuth` unchanged.
428
+
429
+ ### Example: withAuthStrategies (JWT + mTLS)
430
+
431
+ ```ts
432
+ import {
433
+ Service,
434
+ createJoseJwtVerifier,
435
+ createJwtAuthStrategy,
436
+ createMtlsAuthStrategy,
437
+ } from "@twasik4/pocket-service";
438
+
439
+ const mtlsStrategy = createMtlsAuthStrategy({
440
+ requireAuthorized: true,
441
+ });
442
+
443
+ const jwtStrategy = createJwtAuthStrategy({
444
+ verifyToken: createJoseJwtVerifier({
445
+ jwksUri: "http://auth-service:3101/.well-known/jwks.json",
446
+ issuer: "auth-service",
447
+ audience: "gateway-service",
448
+ }),
449
+ });
450
+
451
+ const service = new Service()
452
+ .withAuthStrategies([mtlsStrategy, jwtStrategy])
453
+ .withoutDefaultHeaderAuthFallback()
454
+ .build();
455
+ ```
456
+
457
+ If you use HMAC-signed tokens instead of JWKS:
458
+
459
+ ```ts
460
+ import {
461
+ Service,
462
+ createJoseJwtVerifier,
463
+ createJwtAuthStrategy,
464
+ } from "@twasik4/pocket-service";
465
+
466
+ const jwtStrategy = createJwtAuthStrategy({
467
+ verifyToken: createJoseJwtVerifier({
468
+ secret: process.env.INTERNAL_JWT_SECRET!,
469
+ issuer: "auth-service",
470
+ audience: "gateway-service",
471
+ algorithms: ["HS256"],
472
+ }),
473
+ });
474
+
475
+ const service = new Service()
476
+ .withAuthStrategy(jwtStrategy)
477
+ .withoutDefaultHeaderAuthFallback()
478
+ .build();
479
+ ```
480
+
481
+ Default behavior remains backward compatible. If no strategy authenticates and fallback is enabled, the service still reads `x-user-id` / `x-user-meta` headers.
482
+
483
+ You can also customize strategy behavior further:
484
+
485
+ ```ts
486
+ const jwtStrategy = createJwtAuthStrategy({
487
+ headerName: "x-internal-auth",
488
+ tokenPrefix: "Token",
489
+ canHandle: async (req) => req.path.startsWith("/internal"),
490
+ verifyToken: createJoseJwtVerifier({
491
+ secret: process.env.INTERNAL_JWT_SECRET!,
492
+ requiredClaims: ["sub", "scope"],
493
+ clockTolerance: "30s",
494
+ algorithms: ["HS256"],
495
+ }),
496
+ mapPayloadToUser: async (claims) => ({
497
+ userId: String(claims.sub),
498
+ scope: String(claims.scope || ""),
499
+ }),
500
+ });
501
+
502
+ const mtlsStrategy = createMtlsAuthStrategy({
503
+ requireAuthorized: true,
504
+ canHandle: async (req) => req.path.startsWith("/internal"),
505
+ mapCertificateToUser: async (cert) => ({
506
+ userId: cert.fingerprint256 || "unknown",
507
+ }),
508
+ });
509
+ ```
510
+
511
+ 7. withAuthResolver()
512
+
513
+ Use this when you want a fully custom auth resolver at the service level. This overrides strategy + fallback resolution.
514
+
515
+ ### Example: withAuthResolver
516
+
517
+ ```ts
518
+ const service = new Service()
519
+ .withAuthResolver(async (req, route) => {
520
+ // fully custom auth
521
+ return { userId: "internal-user" };
522
+ })
523
+ .build();
524
+ ```
525
+
526
+ 8. withoutDefaultHeaderAuthFallback()
527
+
528
+ Disables the built-in `x-user-id` / `x-user-meta` fallback when using auth strategies.
529
+
530
+ 9. withoutExpress()
531
+
532
+ Builds the service without an Express server (useful for worker-only or module-only services).
533
+
534
+ ### Mongo Collection Helpers
535
+
536
+ Mongo collections can be defined with options beyond schema:
537
+
538
+ ```ts
539
+ import { defineMongoCollection } from "@twasik4/pocket-service";
540
+ import z from "zod";
541
+
542
+ const sessions = defineMongoCollection({
543
+ schema: z.object({
544
+ userId: z.string(),
545
+ expiresAt: z.date(),
546
+ }),
547
+ ttl: {
548
+ field: "expiresAt",
549
+ expireAfterSeconds: 3600,
550
+ name: "sessions_ttl",
551
+ },
552
+ indexes: [{ key: { userId: 1 }, name: "sessions_user_id" }],
553
+ });
554
+ ```
555
+
556
+ You can also add custom collection methods using the `methods` callback:
557
+
558
+ ```ts
559
+ const users = defineMongoCollection({
560
+ schema: z.object({
561
+ email: z.string(),
562
+ status: z.string(),
563
+ }),
564
+ methods: ({ collection }) => ({
565
+ async getActive() {
566
+ return collection.find({ status: "active" }).toArray();
567
+ },
568
+ }),
569
+ });
570
+ ```
571
+
572
+ If you want revision tracking, set `trackRevisions: true` on the collection config inside `withMongo`:
573
+
574
+ ```ts
575
+ const service = new Service().withMongo({
576
+ dbName: "app",
577
+ collections: {
578
+ accounts: {
579
+ ...defineMongoCollection({
580
+ schema: z.object({
581
+ email: z.string(),
582
+ revision: z.number().default(0),
583
+ }),
584
+ }),
585
+ trackRevisions: true,
586
+ },
587
+ },
588
+ });
589
+ ```
590
+
591
+ `deletedAt` enables soft-delete behavior. It does not automatically enable revision tracking.
592
+
593
+ For time-series collections:
594
+
595
+ ```ts
596
+ const metrics = defineMongoCollection({
597
+ schema: z.object({
598
+ timestamp: z.date(),
599
+ host: z.string(),
600
+ value: z.number(),
601
+ }),
602
+ timeSeries: {
603
+ timeField: "timestamp",
604
+ metaField: "host",
605
+ granularity: "seconds",
606
+ expireAfterSeconds: 3600,
607
+ },
608
+ });
609
+ ```
610
+
611
+ ### Utilities You Can Use
612
+
613
+ The package also exports:
614
+
615
+ - `redisStreamHandler()` to build strongly typed Redis stream handlers
616
+ - `createMetaRouter()` and route typing helpers
617
+ - Mongo helpers (`defineMongoCollection`, `createMongo`, `CollectionWrapper`)
618
+
619
+ Worker handler example:
620
+
621
+ ### Example: withWorkers + redisStreamHandler
622
+
623
+ ```ts
624
+ import { redisStreamHandler } from "@twasik4/pocket-service";
625
+
626
+ export default redisStreamHandler("user:created", async (event, ctx) => {
627
+ ctx.log.info(`Handling event for service ${ctx.service}`);
628
+
629
+ await ctx.emit({
630
+ type: "audit:user-created",
631
+ data: {
632
+ userId: String(event.userId || ""),
633
+ ok: "true",
634
+ },
635
+ });
636
+ });
637
+ ```