@twasik4/pocket-service 1.0.3 → 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.
- package/CHANGELOG.md +34 -0
- package/LICENSE +21 -0
- package/README.md +637 -297
- package/dist/index.d.mts +747 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +4 -0
- package/dist/index.mjs.map +1 -0
- package/dist/runtime/worker-thread.d.mts +1 -0
- package/dist/runtime/worker-thread.mjs +2 -0
- package/dist/runtime/worker-thread.mjs.map +1 -0
- package/package.json +53 -10
- package/dist/index.d.ts +0 -702
- package/dist/index.js +0 -6
- package/dist/index.js.map +0 -1
- package/dist/worker-thread.d.ts +0 -2
- package/dist/worker-thread.js +0 -2
- package/dist/worker-thread.js.map +0 -1
- package/src/index.ts +0 -1
- package/src/worker-thread.ts +0 -159
- package/src/workers/auth/strategies.ts +0 -304
- package/src/workers/db/clickhouse/index.ts +0 -76
- package/src/workers/db/mongo/collection.ts +0 -292
- package/src/workers/db/mongo/mongo.ts +0 -285
- package/src/workers/express-types.ts +0 -442
- package/src/workers/index.ts +0 -7
- package/src/workers/logger.ts +0 -19
- package/src/workers/service.ts +0 -1015
- package/src/workers/stream-handler.ts +0 -25
- package/src/workers/types.ts +0 -59
- package/src/workers/utils.ts +0 -19
- package/tests/mongo.spec.ts +0 -92
- package/tests/redis.spec.ts +0 -36
- package/tests/service.spec.ts +0 -82
- package/tsconfig.json +0 -12
- package/tsup.config.ts +0 -11
- package/vitest.config.ts +0 -8
package/README.md
CHANGED
|
@@ -1,297 +1,637 @@
|
|
|
1
|
-
# Pocket Service
|
|
2
|
-
|
|
3
|
-
Pocket service is a wrapper
|
|
4
|
-
|
|
5
|
-
Pocket service comes with the following
|
|
6
|
-
|
|
7
|
-
- Express API with a few built
|
|
8
|
-
- Full type support via a builder pattern
|
|
9
|
-
- An optional
|
|
10
|
-
- An optional
|
|
11
|
-
- An optional
|
|
12
|
-
- An optional
|
|
13
|
-
- A way to add your own modules/startup hooks that run during the build phase
|
|
14
|
-
|
|
15
|
-
##
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
2.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
{
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const service = new Service()
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
+
```
|