@walkeros/server-source-express 0.0.0-next-20251219153324

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/README.md ADDED
@@ -0,0 +1,567 @@
1
+ # @walkeros/server-source-express
2
+
3
+ Express server source for walkerOS - turn-key HTTP event collection server with
4
+ Express.js.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ npm install @walkeros/server-source-express
10
+ ```
11
+
12
+ ## Quick Start
13
+
14
+ ### Standalone Server (Docker-style)
15
+
16
+ ```typescript
17
+ import { startFlow } from '@walkeros/collector';
18
+ import { sourceExpress } from '@walkeros/server-source-express';
19
+
20
+ const { collector } = await startFlow({
21
+ sources: {
22
+ express: {
23
+ code: sourceExpress,
24
+ config: {
25
+ settings: {
26
+ port: 8080, // Start server on port 8080
27
+ },
28
+ },
29
+ },
30
+ },
31
+ destinations: {
32
+ // Your destinations here
33
+ },
34
+ });
35
+
36
+ // Server is now running!
37
+ // POST http://localhost:8080/collect - JSON event ingestion
38
+ // GET http://localhost:8080/collect - Pixel tracking
39
+ // GET http://localhost:8080/health - Health check
40
+ ```
41
+
42
+ ### App-Only Mode (Custom Integration)
43
+
44
+ ```typescript
45
+ const { collector } = await startFlow({
46
+ sources: {
47
+ express: {
48
+ code: sourceExpress,
49
+ config: {
50
+ settings: {
51
+ // No port = app only, no server started
52
+ path: '/events',
53
+ cors: false, // Handle CORS with your own middleware
54
+ },
55
+ },
56
+ },
57
+ },
58
+ });
59
+
60
+ // Access the Express app
61
+ const expressSource = collector.sources.express;
62
+ const app = expressSource.app;
63
+
64
+ // Add custom middleware
65
+ app.use(yourAuthMiddleware);
66
+
67
+ // Add custom routes
68
+ app.get('/custom', customHandler);
69
+
70
+ // Start server manually
71
+ app.listen(3000);
72
+ ```
73
+
74
+ ## Configuration
75
+
76
+ ### Settings
77
+
78
+ ```typescript
79
+ interface Settings {
80
+ /**
81
+ * HTTP server port to listen on
82
+ * If not provided, server will not start (app-only mode)
83
+ * @optional
84
+ */
85
+ port?: number;
86
+
87
+ /**
88
+ * Event collection endpoint path
89
+ * All HTTP methods (POST, GET, OPTIONS) registered on this path
90
+ * @default '/collect'
91
+ */
92
+ path?: string;
93
+
94
+ /**
95
+ * CORS configuration
96
+ * - false: Disabled
97
+ * - true: Allow all origins (default)
98
+ * - object: Custom CORS options
99
+ * @default true
100
+ */
101
+ cors?: boolean | CorsOptions;
102
+
103
+ /**
104
+ * Enable health check endpoints
105
+ * - GET /health (liveness check)
106
+ * - GET /ready (readiness check)
107
+ * @default true
108
+ */
109
+ status?: boolean;
110
+ }
111
+ ```
112
+
113
+ ### CORS Options
114
+
115
+ ```typescript
116
+ interface CorsOptions {
117
+ /** Allowed origins (string, array, or '*') */
118
+ origin?: string | string[] | '*';
119
+
120
+ /** Allowed HTTP methods */
121
+ methods?: string[];
122
+
123
+ /** Allowed request headers */
124
+ headers?: string[];
125
+
126
+ /** Allow credentials (cookies, authorization) */
127
+ credentials?: boolean;
128
+
129
+ /** Preflight cache duration in seconds */
130
+ maxAge?: number;
131
+ }
132
+ ```
133
+
134
+ ## HTTP Methods
135
+
136
+ ### POST - Standard Event Ingestion
137
+
138
+ Send events as JSON in the request body.
139
+
140
+ **Request:**
141
+
142
+ ```bash
143
+ curl -X POST http://localhost:8080/collect \
144
+ -H "Content-Type: application/json" \
145
+ -d '{
146
+ "event": "page view",
147
+ "data": {
148
+ "title": "Home Page",
149
+ "path": "/"
150
+ },
151
+ "user": {
152
+ "id": "user123"
153
+ }
154
+ }'
155
+ ```
156
+
157
+ **Response:**
158
+
159
+ ```json
160
+ {
161
+ "success": true,
162
+ "timestamp": 1647261462000
163
+ }
164
+ ```
165
+
166
+ ### GET - Pixel Tracking
167
+
168
+ Send events as query parameters. Returns a 1x1 transparent GIF.
169
+
170
+ **Request:**
171
+
172
+ ```html
173
+ <!-- In your HTML -->
174
+ <img
175
+ src="http://localhost:8080/collect?event=page%20view&data[title]=Home&user[id]=user123"
176
+ width="1"
177
+ height="1"
178
+ alt=""
179
+ />
180
+ ```
181
+
182
+ **Response:**
183
+
184
+ ```
185
+ Content-Type: image/gif
186
+ Cache-Control: no-cache, no-store, must-revalidate
187
+
188
+ [1x1 transparent GIF binary]
189
+ ```
190
+
191
+ ### OPTIONS - CORS Preflight
192
+
193
+ Automatically handled for cross-origin requests.
194
+
195
+ **Request:**
196
+
197
+ ```bash
198
+ curl -X OPTIONS http://localhost:8080/collect \
199
+ -H "Origin: https://example.com"
200
+ ```
201
+
202
+ **Response:**
203
+
204
+ ```
205
+ Access-Control-Allow-Origin: *
206
+ Access-Control-Allow-Methods: GET, POST, OPTIONS
207
+ Access-Control-Allow-Headers: Content-Type
208
+
209
+ 204 No Content
210
+ ```
211
+
212
+ ## Health Checks
213
+
214
+ ### GET /health - Liveness Check
215
+
216
+ Returns server status (always returns 200 if server is running).
217
+
218
+ **Response:**
219
+
220
+ ```json
221
+ {
222
+ "status": "ok",
223
+ "timestamp": 1647261462000,
224
+ "source": "express"
225
+ }
226
+ ```
227
+
228
+ ### GET /ready - Readiness Check
229
+
230
+ Returns readiness status (same as health for Express source).
231
+
232
+ **Response:**
233
+
234
+ ```json
235
+ {
236
+ "status": "ready",
237
+ "timestamp": 1647261462000,
238
+ "source": "express"
239
+ }
240
+ ```
241
+
242
+ ## Advanced Examples
243
+
244
+ ### Custom CORS Configuration
245
+
246
+ ```typescript
247
+ await startFlow({
248
+ sources: {
249
+ express: {
250
+ code: sourceExpress,
251
+ config: {
252
+ settings: {
253
+ port: 8080,
254
+ cors: {
255
+ origin: ['https://app.example.com', 'https://admin.example.com'],
256
+ credentials: true,
257
+ methods: ['GET', 'POST', 'OPTIONS'],
258
+ headers: ['Content-Type', 'Authorization'],
259
+ maxAge: 86400, // 24 hours
260
+ },
261
+ },
262
+ },
263
+ },
264
+ },
265
+ });
266
+ ```
267
+
268
+ ### Disable Health Checks
269
+
270
+ ```typescript
271
+ await startFlow({
272
+ sources: {
273
+ express: {
274
+ code: sourceExpress,
275
+ config: {
276
+ settings: {
277
+ port: 8080,
278
+ status: false, // Disable /health and /ready endpoints
279
+ },
280
+ },
281
+ },
282
+ },
283
+ });
284
+ ```
285
+
286
+ ### Custom Endpoint Path
287
+
288
+ ```typescript
289
+ await startFlow({
290
+ sources: {
291
+ express: {
292
+ code: sourceExpress,
293
+ config: {
294
+ settings: {
295
+ port: 8080,
296
+ path: '/api/v1/events', // Custom path
297
+ },
298
+ },
299
+ },
300
+ },
301
+ });
302
+
303
+ // All methods now work on /api/v1/events
304
+ // POST /api/v1/events
305
+ // GET /api/v1/events
306
+ // OPTIONS /api/v1/events
307
+ ```
308
+
309
+ ### Extend Express App
310
+
311
+ ```typescript
312
+ const { collector } = await startFlow({
313
+ sources: {
314
+ express: {
315
+ code: sourceExpress,
316
+ config: {
317
+ settings: { port: 8080 },
318
+ },
319
+ },
320
+ },
321
+ });
322
+
323
+ // Access Express app for advanced customization
324
+ const expressSource = collector.sources.express;
325
+ const app = expressSource.app;
326
+
327
+ // Add authentication middleware
328
+ app.use('/collect', authMiddleware);
329
+
330
+ // Add rate limiting
331
+ import rateLimit from 'express-rate-limit';
332
+ app.use(
333
+ '/collect',
334
+ rateLimit({
335
+ windowMs: 15 * 60 * 1000, // 15 minutes
336
+ max: 100, // Limit each IP to 100 requests per windowMs
337
+ }),
338
+ );
339
+
340
+ // Add custom logging
341
+ app.use((req, res, next) => {
342
+ console.log(`${req.method} ${req.path}`);
343
+ next();
344
+ });
345
+ ```
346
+
347
+ ## Event Format
348
+
349
+ ### Single Event
350
+
351
+ ```json
352
+ {
353
+ "event": "page view",
354
+ "data": {
355
+ "title": "Home Page",
356
+ "path": "/"
357
+ },
358
+ "context": {
359
+ "language": ["en", 0],
360
+ "currency": ["USD", 0]
361
+ },
362
+ "user": {
363
+ "id": "user123",
364
+ "device": "device456"
365
+ },
366
+ "globals": {
367
+ "appVersion": "1.0.0"
368
+ },
369
+ "consent": {
370
+ "functional": true,
371
+ "marketing": true
372
+ }
373
+ }
374
+ ```
375
+
376
+ ### Query Parameters (GET)
377
+
378
+ For pixel tracking, use nested bracket notation:
379
+
380
+ ```
381
+ ?event=page%20view
382
+ &data[title]=Home%20Page
383
+ &data[path]=/
384
+ &user[id]=user123
385
+ &consent[marketing]=true
386
+ ```
387
+
388
+ This is automatically parsed by `requestToData` from `@walkeros/core`.
389
+
390
+ ## Architecture
391
+
392
+ ### Infrastructure Ownership
393
+
394
+ The Express source **owns its HTTP infrastructure**:
395
+
396
+ - ✅ Creates Express application
397
+ - ✅ Configures middleware (JSON parsing, CORS)
398
+ - ✅ Registers routes (POST, GET, OPTIONS)
399
+ - ✅ Starts HTTP server (if port configured)
400
+ - ✅ Handles graceful shutdown (SIGTERM, SIGINT)
401
+
402
+ This design enables:
403
+
404
+ 1. **Turn-key deployment** - Just specify a port and deploy
405
+ 2. **Docker-friendly** - Perfect for containerized environments
406
+ 3. **Flexibility** - App-only mode for custom integrations
407
+
408
+ ### Request Flow
409
+
410
+ ```
411
+ ┌─────────────────────────────────────────┐
412
+ │ HTTP Client (Browser, Server, etc.) │
413
+ └─────────────────────────────────────────┘
414
+
415
+ ┌─────────────────────────────────────────┐
416
+ │ EXPRESS SOURCE │
417
+ │ - Receives HTTP request │
418
+ │ - Parses body/query params │
419
+ │ - Validates request structure │
420
+ │ - Calls env.push() → Collector │
421
+ │ - Returns HTTP response │
422
+ └─────────────────────────────────────────┘
423
+
424
+ ┌─────────────────────────────────────────┐
425
+ │ COLLECTOR │
426
+ │ - Event validation & processing │
427
+ │ - Consent management │
428
+ │ - Mapping rules │
429
+ │ - Routes to destinations │
430
+ └─────────────────────────────────────────┘
431
+ ```
432
+
433
+ ## Deployment
434
+
435
+ ### Docker
436
+
437
+ ```dockerfile
438
+ FROM node:18-alpine
439
+
440
+ WORKDIR /app
441
+
442
+ COPY package*.json ./
443
+ RUN npm install
444
+
445
+ COPY . .
446
+
447
+ ENV PORT=8080
448
+ EXPOSE 8080
449
+
450
+ CMD ["node", "server.js"]
451
+ ```
452
+
453
+ **server.js:**
454
+
455
+ ```javascript
456
+ import { startFlow } from '@walkeros/collector';
457
+ import { sourceExpress } from '@walkeros/server-source-express';
458
+
459
+ await startFlow({
460
+ sources: {
461
+ express: {
462
+ code: sourceExpress,
463
+ config: {
464
+ settings: {
465
+ port: process.env.PORT || 8080,
466
+ },
467
+ },
468
+ },
469
+ },
470
+ // Your destinations...
471
+ });
472
+ ```
473
+
474
+ ### Kubernetes
475
+
476
+ ```yaml
477
+ apiVersion: apps/v1
478
+ kind: Deployment
479
+ metadata:
480
+ name: walkeros-collector
481
+ spec:
482
+ replicas: 3
483
+ selector:
484
+ matchLabels:
485
+ app: walkeros-collector
486
+ template:
487
+ metadata:
488
+ labels:
489
+ app: walkeros-collector
490
+ spec:
491
+ containers:
492
+ - name: collector
493
+ image: your-registry/walkeros-collector:latest
494
+ ports:
495
+ - containerPort: 8080
496
+ livenessProbe:
497
+ httpGet:
498
+ path: /health
499
+ port: 8080
500
+ initialDelaySeconds: 10
501
+ periodSeconds: 5
502
+ readinessProbe:
503
+ httpGet:
504
+ path: /ready
505
+ port: 8080
506
+ initialDelaySeconds: 5
507
+ periodSeconds: 3
508
+ ```
509
+
510
+ ## Testing
511
+
512
+ The package includes comprehensive tests using mocked Express Request/Response
513
+ objects.
514
+
515
+ **Run tests:**
516
+
517
+ ```bash
518
+ npm test
519
+ ```
520
+
521
+ **Example test:**
522
+
523
+ ```typescript
524
+ import { sourceExpress } from '@walkeros/server-source-express';
525
+
526
+ test('should process POST event', async () => {
527
+ const mockPush = jest.fn().mockResolvedValue({ event: { id: 'test' } });
528
+
529
+ const source = await sourceExpress(
530
+ {},
531
+ {
532
+ push: mockPush,
533
+ command: jest.fn(),
534
+ elb: jest.fn(),
535
+ },
536
+ );
537
+
538
+ const req = {
539
+ method: 'POST',
540
+ body: { event: 'page view', data: { title: 'Home' } },
541
+ headers: {},
542
+ get: () => undefined,
543
+ };
544
+ const res = {
545
+ status: jest.fn().returnThis(),
546
+ json: jest.fn(),
547
+ send: jest.fn(),
548
+ set: jest.fn(),
549
+ };
550
+
551
+ await source.push(req, res);
552
+
553
+ expect(res.status).toHaveBeenCalledWith(200);
554
+ expect(mockPush).toHaveBeenCalled();
555
+ });
556
+ ```
557
+
558
+ ## License
559
+
560
+ MIT
561
+
562
+ ## Links
563
+
564
+ - [walkerOS Documentation](https://github.com/elbwalker/walkerOS)
565
+ - [Express.js](https://expressjs.com/)
566
+ - [GitHub Repository](https://github.com/elbwalker/walkerOS)
567
+ - [Report Issues](https://github.com/elbwalker/walkerOS/issues)
package/dist/dev.d.mts ADDED
@@ -0,0 +1,79 @@
1
+ import { z } from '@walkeros/core/dev';
2
+
3
+ /**
4
+ * HTTP methods enum
5
+ */
6
+ declare const HttpMethod: z.ZodEnum<{
7
+ GET: "GET";
8
+ POST: "POST";
9
+ PUT: "PUT";
10
+ PATCH: "PATCH";
11
+ DELETE: "DELETE";
12
+ OPTIONS: "OPTIONS";
13
+ HEAD: "HEAD";
14
+ }>;
15
+ /**
16
+ * CORS origin configuration
17
+ * Accepts:
18
+ * - '*' for all origins
19
+ * - Single URL string
20
+ * - Array of URL strings
21
+ */
22
+ declare const CorsOrigin: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodLiteral<"*">]>;
23
+ /**
24
+ * CORS options schema
25
+ * Configuration for Cross-Origin Resource Sharing
26
+ */
27
+ declare const CorsOptionsSchema: z.ZodObject<{
28
+ origin: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodLiteral<"*">]>>;
29
+ methods: z.ZodOptional<z.ZodArray<z.ZodEnum<{
30
+ GET: "GET";
31
+ POST: "POST";
32
+ PUT: "PUT";
33
+ PATCH: "PATCH";
34
+ DELETE: "DELETE";
35
+ OPTIONS: "OPTIONS";
36
+ HEAD: "HEAD";
37
+ }>>>;
38
+ headers: z.ZodOptional<z.ZodArray<z.ZodString>>;
39
+ credentials: z.ZodOptional<z.ZodBoolean>;
40
+ maxAge: z.ZodOptional<z.ZodNumber>;
41
+ }, z.core.$strip>;
42
+ type CorsOptions = z.infer<typeof CorsOptionsSchema>;
43
+
44
+ /**
45
+ * Express source settings schema
46
+ */
47
+ declare const SettingsSchema: z.ZodObject<{
48
+ port: z.ZodOptional<z.ZodNumber>;
49
+ path: z.ZodDefault<z.ZodString>;
50
+ cors: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
51
+ origin: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodLiteral<"*">]>>;
52
+ methods: z.ZodOptional<z.ZodArray<z.ZodEnum<{
53
+ GET: "GET";
54
+ POST: "POST";
55
+ PUT: "PUT";
56
+ PATCH: "PATCH";
57
+ DELETE: "DELETE";
58
+ OPTIONS: "OPTIONS";
59
+ HEAD: "HEAD";
60
+ }>>>;
61
+ headers: z.ZodOptional<z.ZodArray<z.ZodString>>;
62
+ credentials: z.ZodOptional<z.ZodBoolean>;
63
+ maxAge: z.ZodOptional<z.ZodNumber>;
64
+ }, z.core.$strip>]>>;
65
+ status: z.ZodDefault<z.ZodBoolean>;
66
+ }, z.core.$strip>;
67
+ type Settings = z.infer<typeof SettingsSchema>;
68
+
69
+ type index_CorsOptions = CorsOptions;
70
+ declare const index_CorsOptionsSchema: typeof CorsOptionsSchema;
71
+ declare const index_CorsOrigin: typeof CorsOrigin;
72
+ declare const index_HttpMethod: typeof HttpMethod;
73
+ type index_Settings = Settings;
74
+ declare const index_SettingsSchema: typeof SettingsSchema;
75
+ declare namespace index {
76
+ export { type index_CorsOptions as CorsOptions, index_CorsOptionsSchema as CorsOptionsSchema, index_CorsOrigin as CorsOrigin, index_HttpMethod as HttpMethod, type index_Settings as Settings, index_SettingsSchema as SettingsSchema };
77
+ }
78
+
79
+ export { index as schemas };
package/dist/dev.d.ts ADDED
@@ -0,0 +1,79 @@
1
+ import { z } from '@walkeros/core/dev';
2
+
3
+ /**
4
+ * HTTP methods enum
5
+ */
6
+ declare const HttpMethod: z.ZodEnum<{
7
+ GET: "GET";
8
+ POST: "POST";
9
+ PUT: "PUT";
10
+ PATCH: "PATCH";
11
+ DELETE: "DELETE";
12
+ OPTIONS: "OPTIONS";
13
+ HEAD: "HEAD";
14
+ }>;
15
+ /**
16
+ * CORS origin configuration
17
+ * Accepts:
18
+ * - '*' for all origins
19
+ * - Single URL string
20
+ * - Array of URL strings
21
+ */
22
+ declare const CorsOrigin: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodLiteral<"*">]>;
23
+ /**
24
+ * CORS options schema
25
+ * Configuration for Cross-Origin Resource Sharing
26
+ */
27
+ declare const CorsOptionsSchema: z.ZodObject<{
28
+ origin: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodLiteral<"*">]>>;
29
+ methods: z.ZodOptional<z.ZodArray<z.ZodEnum<{
30
+ GET: "GET";
31
+ POST: "POST";
32
+ PUT: "PUT";
33
+ PATCH: "PATCH";
34
+ DELETE: "DELETE";
35
+ OPTIONS: "OPTIONS";
36
+ HEAD: "HEAD";
37
+ }>>>;
38
+ headers: z.ZodOptional<z.ZodArray<z.ZodString>>;
39
+ credentials: z.ZodOptional<z.ZodBoolean>;
40
+ maxAge: z.ZodOptional<z.ZodNumber>;
41
+ }, z.core.$strip>;
42
+ type CorsOptions = z.infer<typeof CorsOptionsSchema>;
43
+
44
+ /**
45
+ * Express source settings schema
46
+ */
47
+ declare const SettingsSchema: z.ZodObject<{
48
+ port: z.ZodOptional<z.ZodNumber>;
49
+ path: z.ZodDefault<z.ZodString>;
50
+ cors: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
51
+ origin: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodLiteral<"*">]>>;
52
+ methods: z.ZodOptional<z.ZodArray<z.ZodEnum<{
53
+ GET: "GET";
54
+ POST: "POST";
55
+ PUT: "PUT";
56
+ PATCH: "PATCH";
57
+ DELETE: "DELETE";
58
+ OPTIONS: "OPTIONS";
59
+ HEAD: "HEAD";
60
+ }>>>;
61
+ headers: z.ZodOptional<z.ZodArray<z.ZodString>>;
62
+ credentials: z.ZodOptional<z.ZodBoolean>;
63
+ maxAge: z.ZodOptional<z.ZodNumber>;
64
+ }, z.core.$strip>]>>;
65
+ status: z.ZodDefault<z.ZodBoolean>;
66
+ }, z.core.$strip>;
67
+ type Settings = z.infer<typeof SettingsSchema>;
68
+
69
+ type index_CorsOptions = CorsOptions;
70
+ declare const index_CorsOptionsSchema: typeof CorsOptionsSchema;
71
+ declare const index_CorsOrigin: typeof CorsOrigin;
72
+ declare const index_HttpMethod: typeof HttpMethod;
73
+ type index_Settings = Settings;
74
+ declare const index_SettingsSchema: typeof SettingsSchema;
75
+ declare namespace index {
76
+ export { type index_CorsOptions as CorsOptions, index_CorsOptionsSchema as CorsOptionsSchema, index_CorsOrigin as CorsOrigin, index_HttpMethod as HttpMethod, type index_Settings as Settings, index_SettingsSchema as SettingsSchema };
77
+ }
78
+
79
+ export { index as schemas };
package/dist/dev.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";var e,o=Object.defineProperty,r=Object.getOwnPropertyDescriptor,t=Object.getOwnPropertyNames,a=Object.prototype.hasOwnProperty,i=(e,r)=>{for(var t in r)o(e,t,{get:r[t],enumerable:!0})},n={};i(n,{schemas:()=>l}),module.exports=(e=n,((e,i,n,l)=>{if(i&&"object"==typeof i||"function"==typeof i)for(let s of t(i))a.call(e,s)||s===n||o(e,s,{get:()=>i[s],enumerable:!(l=r(i,s))||l.enumerable});return e})(o({},"__esModule",{value:!0}),e));var l={};i(l,{CorsOptionsSchema:()=>p,CorsOrigin:()=>d,HttpMethod:()=>c,SettingsSchema:()=>u});var s=require("@walkeros/core/dev"),c=s.z.enum(["GET","POST","PUT","PATCH","DELETE","OPTIONS","HEAD"]),d=s.z.union([s.z.string(),s.z.array(s.z.string()),s.z.literal("*")]),p=s.z.object({origin:d.describe("Allowed origins (* for all, URL string, or array of URLs)").optional(),methods:s.z.array(c).describe("Allowed HTTP methods").optional(),headers:s.z.array(s.z.string()).describe("Allowed request headers").optional(),credentials:s.z.boolean().describe("Allow credentials (cookies, authorization headers)").optional(),maxAge:s.z.number().int().positive().describe("Preflight cache duration in seconds").optional()}),b=require("@walkeros/core/dev"),u=b.z.object({port:b.z.number().int().min(0).max(65535).describe("HTTP server port to listen on. Use 0 for random available port. If not provided, server will not start (app only mode)").optional(),path:b.z.string().describe("Event collection endpoint path").default("/collect"),cors:b.z.union([b.z.boolean(),p]).describe("CORS configuration: false = disabled, true = allow all origins (default), object = custom configuration").default(!0),status:b.z.boolean().describe("Enable health check endpoints (/health, /ready)").default(!0)});//# sourceMappingURL=dev.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dev.ts","../src/schemas/index.ts","../src/schemas/primitives.ts","../src/schemas/settings.ts"],"sourcesContent":["export * as schemas from './schemas';\n","export * from './primitives';\nexport * from './settings';\n","import { z } from '@walkeros/core/dev';\n\n/**\n * HTTP methods enum\n */\nexport const HttpMethod = z.enum([\n 'GET',\n 'POST',\n 'PUT',\n 'PATCH',\n 'DELETE',\n 'OPTIONS',\n 'HEAD',\n]);\n\n/**\n * CORS origin configuration\n * Accepts:\n * - '*' for all origins\n * - Single URL string\n * - Array of URL strings\n */\nexport const CorsOrigin = z.union([\n z.string(),\n z.array(z.string()),\n z.literal('*'),\n]);\n\n/**\n * CORS options schema\n * Configuration for Cross-Origin Resource Sharing\n */\nexport const CorsOptionsSchema = z.object({\n origin: CorsOrigin.describe(\n 'Allowed origins (* for all, URL string, or array of URLs)',\n ).optional(),\n\n methods: z.array(HttpMethod).describe('Allowed HTTP methods').optional(),\n\n headers: z.array(z.string()).describe('Allowed request headers').optional(),\n\n credentials: z\n .boolean()\n .describe('Allow credentials (cookies, authorization headers)')\n .optional(),\n\n maxAge: z\n .number()\n .int()\n .positive()\n .describe('Preflight cache duration in seconds')\n .optional(),\n});\n\nexport type CorsOptions = z.infer<typeof CorsOptionsSchema>;\n","import { z } from '@walkeros/core/dev';\nimport { CorsOptionsSchema } from './primitives';\n\n/**\n * Express source settings schema\n */\nexport const SettingsSchema = z.object({\n port: z\n .number()\n .int()\n .min(0) // 0 = random available port\n .max(65535)\n .describe(\n 'HTTP server port to listen on. Use 0 for random available port. If not provided, server will not start (app only mode)',\n )\n .optional(),\n\n path: z\n .string()\n .describe('Event collection endpoint path')\n .default('/collect'),\n\n cors: z\n .union([z.boolean(), CorsOptionsSchema])\n .describe(\n 'CORS configuration: false = disabled, true = allow all origins (default), object = custom configuration',\n )\n .default(true),\n\n status: z\n .boolean()\n .describe('Enable health check endpoints (/health, /ready)')\n .default(true),\n});\n\nexport type Settings = z.infer<typeof SettingsSchema>;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,iBAAkB;AAKX,IAAM,aAAa,aAAE,KAAK;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AASM,IAAM,aAAa,aAAE,MAAM;AAAA,EAChC,aAAE,OAAO;AAAA,EACT,aAAE,MAAM,aAAE,OAAO,CAAC;AAAA,EAClB,aAAE,QAAQ,GAAG;AACf,CAAC;AAMM,IAAM,oBAAoB,aAAE,OAAO;AAAA,EACxC,QAAQ,WAAW;AAAA,IACjB;AAAA,EACF,EAAE,SAAS;AAAA,EAEX,SAAS,aAAE,MAAM,UAAU,EAAE,SAAS,sBAAsB,EAAE,SAAS;AAAA,EAEvE,SAAS,aAAE,MAAM,aAAE,OAAO,CAAC,EAAE,SAAS,yBAAyB,EAAE,SAAS;AAAA,EAE1E,aAAa,aACV,QAAQ,EACR,SAAS,oDAAoD,EAC7D,SAAS;AAAA,EAEZ,QAAQ,aACL,OAAO,EACP,IAAI,EACJ,SAAS,EACT,SAAS,qCAAqC,EAC9C,SAAS;AACd,CAAC;;;ACpDD,IAAAA,cAAkB;AAMX,IAAM,iBAAiB,cAAE,OAAO;AAAA,EACrC,MAAM,cACH,OAAO,EACP,IAAI,EACJ,IAAI,CAAC,EACL,IAAI,KAAK,EACT;AAAA,IACC;AAAA,EACF,EACC,SAAS;AAAA,EAEZ,MAAM,cACH,OAAO,EACP,SAAS,gCAAgC,EACzC,QAAQ,UAAU;AAAA,EAErB,MAAM,cACH,MAAM,CAAC,cAAE,QAAQ,GAAG,iBAAiB,CAAC,EACtC;AAAA,IACC;AAAA,EACF,EACC,QAAQ,IAAI;AAAA,EAEf,QAAQ,cACL,QAAQ,EACR,SAAS,iDAAiD,EAC1D,QAAQ,IAAI;AACjB,CAAC;","names":["import_dev"]}
package/dist/dev.mjs ADDED
@@ -0,0 +1 @@
1
+ var e=Object.defineProperty,o={};((o,r)=>{for(var t in r)e(o,t,{get:r[t],enumerable:!0})})(o,{CorsOptionsSchema:()=>i,CorsOrigin:()=>a,HttpMethod:()=>t,SettingsSchema:()=>s});import{z as r}from"@walkeros/core/dev";var t=r.enum(["GET","POST","PUT","PATCH","DELETE","OPTIONS","HEAD"]),a=r.union([r.string(),r.array(r.string()),r.literal("*")]),i=r.object({origin:a.describe("Allowed origins (* for all, URL string, or array of URLs)").optional(),methods:r.array(t).describe("Allowed HTTP methods").optional(),headers:r.array(r.string()).describe("Allowed request headers").optional(),credentials:r.boolean().describe("Allow credentials (cookies, authorization headers)").optional(),maxAge:r.number().int().positive().describe("Preflight cache duration in seconds").optional()});import{z as n}from"@walkeros/core/dev";var s=n.object({port:n.number().int().min(0).max(65535).describe("HTTP server port to listen on. Use 0 for random available port. If not provided, server will not start (app only mode)").optional(),path:n.string().describe("Event collection endpoint path").default("/collect"),cors:n.union([n.boolean(),i]).describe("CORS configuration: false = disabled, true = allow all origins (default), object = custom configuration").default(!0),status:n.boolean().describe("Enable health check endpoints (/health, /ready)").default(!0)});export{o as schemas};//# sourceMappingURL=dev.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schemas/index.ts","../src/schemas/primitives.ts","../src/schemas/settings.ts"],"sourcesContent":["export * from './primitives';\nexport * from './settings';\n","import { z } from '@walkeros/core/dev';\n\n/**\n * HTTP methods enum\n */\nexport const HttpMethod = z.enum([\n 'GET',\n 'POST',\n 'PUT',\n 'PATCH',\n 'DELETE',\n 'OPTIONS',\n 'HEAD',\n]);\n\n/**\n * CORS origin configuration\n * Accepts:\n * - '*' for all origins\n * - Single URL string\n * - Array of URL strings\n */\nexport const CorsOrigin = z.union([\n z.string(),\n z.array(z.string()),\n z.literal('*'),\n]);\n\n/**\n * CORS options schema\n * Configuration for Cross-Origin Resource Sharing\n */\nexport const CorsOptionsSchema = z.object({\n origin: CorsOrigin.describe(\n 'Allowed origins (* for all, URL string, or array of URLs)',\n ).optional(),\n\n methods: z.array(HttpMethod).describe('Allowed HTTP methods').optional(),\n\n headers: z.array(z.string()).describe('Allowed request headers').optional(),\n\n credentials: z\n .boolean()\n .describe('Allow credentials (cookies, authorization headers)')\n .optional(),\n\n maxAge: z\n .number()\n .int()\n .positive()\n .describe('Preflight cache duration in seconds')\n .optional(),\n});\n\nexport type CorsOptions = z.infer<typeof CorsOptionsSchema>;\n","import { z } from '@walkeros/core/dev';\nimport { CorsOptionsSchema } from './primitives';\n\n/**\n * Express source settings schema\n */\nexport const SettingsSchema = z.object({\n port: z\n .number()\n .int()\n .min(0) // 0 = random available port\n .max(65535)\n .describe(\n 'HTTP server port to listen on. Use 0 for random available port. If not provided, server will not start (app only mode)',\n )\n .optional(),\n\n path: z\n .string()\n .describe('Event collection endpoint path')\n .default('/collect'),\n\n cors: z\n .union([z.boolean(), CorsOptionsSchema])\n .describe(\n 'CORS configuration: false = disabled, true = allow all origins (default), object = custom configuration',\n )\n .default(true),\n\n status: z\n .boolean()\n .describe('Enable health check endpoints (/health, /ready)')\n .default(true),\n});\n\nexport type Settings = z.infer<typeof SettingsSchema>;\n"],"mappings":";;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,SAAS,SAAS;AAKX,IAAM,aAAa,EAAE,KAAK;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AASM,IAAM,aAAa,EAAE,MAAM;AAAA,EAChC,EAAE,OAAO;AAAA,EACT,EAAE,MAAM,EAAE,OAAO,CAAC;AAAA,EAClB,EAAE,QAAQ,GAAG;AACf,CAAC;AAMM,IAAM,oBAAoB,EAAE,OAAO;AAAA,EACxC,QAAQ,WAAW;AAAA,IACjB;AAAA,EACF,EAAE,SAAS;AAAA,EAEX,SAAS,EAAE,MAAM,UAAU,EAAE,SAAS,sBAAsB,EAAE,SAAS;AAAA,EAEvE,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,yBAAyB,EAAE,SAAS;AAAA,EAE1E,aAAa,EACV,QAAQ,EACR,SAAS,oDAAoD,EAC7D,SAAS;AAAA,EAEZ,QAAQ,EACL,OAAO,EACP,IAAI,EACJ,SAAS,EACT,SAAS,qCAAqC,EAC9C,SAAS;AACd,CAAC;;;ACpDD,SAAS,KAAAA,UAAS;AAMX,IAAM,iBAAiBC,GAAE,OAAO;AAAA,EACrC,MAAMA,GACH,OAAO,EACP,IAAI,EACJ,IAAI,CAAC,EACL,IAAI,KAAK,EACT;AAAA,IACC;AAAA,EACF,EACC,SAAS;AAAA,EAEZ,MAAMA,GACH,OAAO,EACP,SAAS,gCAAgC,EACzC,QAAQ,UAAU;AAAA,EAErB,MAAMA,GACH,MAAM,CAACA,GAAE,QAAQ,GAAG,iBAAiB,CAAC,EACtC;AAAA,IACC;AAAA,EACF,EACC,QAAQ,IAAI;AAAA,EAEf,QAAQA,GACL,QAAQ,EACR,SAAS,iDAAiD,EAC1D,QAAQ,IAAI;AACjB,CAAC;","names":["z","z"]}
@@ -0,0 +1,113 @@
1
+ import { Source, WalkerOS } from '@walkeros/core';
2
+ import { Request, Response, Application } from 'express';
3
+ import { z } from '@walkeros/core/dev';
4
+
5
+ /**
6
+ * CORS options schema
7
+ * Configuration for Cross-Origin Resource Sharing
8
+ */
9
+ declare const CorsOptionsSchema: z.ZodObject<{
10
+ origin: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodLiteral<"*">]>>;
11
+ methods: z.ZodOptional<z.ZodArray<z.ZodEnum<{
12
+ GET: "GET";
13
+ POST: "POST";
14
+ PUT: "PUT";
15
+ PATCH: "PATCH";
16
+ DELETE: "DELETE";
17
+ OPTIONS: "OPTIONS";
18
+ HEAD: "HEAD";
19
+ }>>>;
20
+ headers: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
+ credentials: z.ZodOptional<z.ZodBoolean>;
22
+ maxAge: z.ZodOptional<z.ZodNumber>;
23
+ }, z.core.$strip>;
24
+ type CorsOptions = z.infer<typeof CorsOptionsSchema>;
25
+
26
+ /**
27
+ * Express source settings schema
28
+ */
29
+ declare const SettingsSchema: z.ZodObject<{
30
+ port: z.ZodOptional<z.ZodNumber>;
31
+ path: z.ZodDefault<z.ZodString>;
32
+ cors: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
33
+ origin: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodLiteral<"*">]>>;
34
+ methods: z.ZodOptional<z.ZodArray<z.ZodEnum<{
35
+ GET: "GET";
36
+ POST: "POST";
37
+ PUT: "PUT";
38
+ PATCH: "PATCH";
39
+ DELETE: "DELETE";
40
+ OPTIONS: "OPTIONS";
41
+ HEAD: "HEAD";
42
+ }>>>;
43
+ headers: z.ZodOptional<z.ZodArray<z.ZodString>>;
44
+ credentials: z.ZodOptional<z.ZodBoolean>;
45
+ maxAge: z.ZodOptional<z.ZodNumber>;
46
+ }, z.core.$strip>]>>;
47
+ status: z.ZodDefault<z.ZodBoolean>;
48
+ }, z.core.$strip>;
49
+
50
+ type Settings = z.infer<typeof SettingsSchema>;
51
+ type InitSettings = Partial<Settings>;
52
+ interface Mapping {
53
+ }
54
+ type Push = (req: Request, res: Response) => Promise<void>;
55
+ interface Env extends Source.Env {
56
+ req?: Request;
57
+ res?: Response;
58
+ }
59
+ type Types = Source.Types<Settings, Mapping, Push, Env, InitSettings>;
60
+ type Config = Source.Config<Types>;
61
+ type PartialConfig = Source.PartialConfig<Types>;
62
+ interface ExpressSource extends Omit<Source.Instance<Types>, 'push'> {
63
+ push: Push;
64
+ app: Application;
65
+ server?: ReturnType<Application['listen']>;
66
+ }
67
+ interface EventRequest {
68
+ event: string;
69
+ data?: WalkerOS.AnyObject;
70
+ context?: WalkerOS.AnyObject;
71
+ user?: WalkerOS.AnyObject;
72
+ globals?: WalkerOS.AnyObject;
73
+ consent?: WalkerOS.AnyObject;
74
+ }
75
+ interface EventResponse {
76
+ success: boolean;
77
+ id?: string;
78
+ timestamp?: number;
79
+ error?: string;
80
+ }
81
+ type RequestBody = EventRequest;
82
+ type ResponseBody = EventResponse;
83
+
84
+ /**
85
+ * Set CORS headers on response
86
+ *
87
+ * @param res Express response object
88
+ * @param corsConfig CORS configuration (false = disabled, true = allow all, object = custom)
89
+ */
90
+ declare function setCorsHeaders(res: Response, corsConfig?: boolean | CorsOptions): void;
91
+ /**
92
+ * 1x1 transparent GIF for pixel tracking
93
+ * Base64-encoded GIF (43 bytes)
94
+ */
95
+ declare const TRANSPARENT_GIF: Buffer<ArrayBuffer>;
96
+
97
+ /**
98
+ * Express source initialization
99
+ *
100
+ * This source OWNS its HTTP server infrastructure:
101
+ * - Creates Express application
102
+ * - Sets up middleware (JSON parsing, CORS)
103
+ * - Registers event collection endpoints (POST, GET, OPTIONS)
104
+ * - Starts HTTP server (if port configured)
105
+ * - Handles graceful shutdown
106
+ *
107
+ * @param config Partial source configuration
108
+ * @param env Source environment with push, command, elb functions
109
+ * @returns Express source instance with app and push handler
110
+ */
111
+ declare const sourceExpress: (config: PartialConfig, env: Types["env"]) => Promise<ExpressSource>;
112
+
113
+ export { type Config, type Env, type EventRequest, type EventResponse, type ExpressSource, type InitSettings, type Mapping, type PartialConfig, type Push, type RequestBody, type ResponseBody, type Settings, TRANSPARENT_GIF, type Types, setCorsHeaders, sourceExpress };
@@ -0,0 +1,113 @@
1
+ import { Source, WalkerOS } from '@walkeros/core';
2
+ import { Request, Response, Application } from 'express';
3
+ import { z } from '@walkeros/core/dev';
4
+
5
+ /**
6
+ * CORS options schema
7
+ * Configuration for Cross-Origin Resource Sharing
8
+ */
9
+ declare const CorsOptionsSchema: z.ZodObject<{
10
+ origin: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodLiteral<"*">]>>;
11
+ methods: z.ZodOptional<z.ZodArray<z.ZodEnum<{
12
+ GET: "GET";
13
+ POST: "POST";
14
+ PUT: "PUT";
15
+ PATCH: "PATCH";
16
+ DELETE: "DELETE";
17
+ OPTIONS: "OPTIONS";
18
+ HEAD: "HEAD";
19
+ }>>>;
20
+ headers: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
+ credentials: z.ZodOptional<z.ZodBoolean>;
22
+ maxAge: z.ZodOptional<z.ZodNumber>;
23
+ }, z.core.$strip>;
24
+ type CorsOptions = z.infer<typeof CorsOptionsSchema>;
25
+
26
+ /**
27
+ * Express source settings schema
28
+ */
29
+ declare const SettingsSchema: z.ZodObject<{
30
+ port: z.ZodOptional<z.ZodNumber>;
31
+ path: z.ZodDefault<z.ZodString>;
32
+ cors: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
33
+ origin: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodLiteral<"*">]>>;
34
+ methods: z.ZodOptional<z.ZodArray<z.ZodEnum<{
35
+ GET: "GET";
36
+ POST: "POST";
37
+ PUT: "PUT";
38
+ PATCH: "PATCH";
39
+ DELETE: "DELETE";
40
+ OPTIONS: "OPTIONS";
41
+ HEAD: "HEAD";
42
+ }>>>;
43
+ headers: z.ZodOptional<z.ZodArray<z.ZodString>>;
44
+ credentials: z.ZodOptional<z.ZodBoolean>;
45
+ maxAge: z.ZodOptional<z.ZodNumber>;
46
+ }, z.core.$strip>]>>;
47
+ status: z.ZodDefault<z.ZodBoolean>;
48
+ }, z.core.$strip>;
49
+
50
+ type Settings = z.infer<typeof SettingsSchema>;
51
+ type InitSettings = Partial<Settings>;
52
+ interface Mapping {
53
+ }
54
+ type Push = (req: Request, res: Response) => Promise<void>;
55
+ interface Env extends Source.Env {
56
+ req?: Request;
57
+ res?: Response;
58
+ }
59
+ type Types = Source.Types<Settings, Mapping, Push, Env, InitSettings>;
60
+ type Config = Source.Config<Types>;
61
+ type PartialConfig = Source.PartialConfig<Types>;
62
+ interface ExpressSource extends Omit<Source.Instance<Types>, 'push'> {
63
+ push: Push;
64
+ app: Application;
65
+ server?: ReturnType<Application['listen']>;
66
+ }
67
+ interface EventRequest {
68
+ event: string;
69
+ data?: WalkerOS.AnyObject;
70
+ context?: WalkerOS.AnyObject;
71
+ user?: WalkerOS.AnyObject;
72
+ globals?: WalkerOS.AnyObject;
73
+ consent?: WalkerOS.AnyObject;
74
+ }
75
+ interface EventResponse {
76
+ success: boolean;
77
+ id?: string;
78
+ timestamp?: number;
79
+ error?: string;
80
+ }
81
+ type RequestBody = EventRequest;
82
+ type ResponseBody = EventResponse;
83
+
84
+ /**
85
+ * Set CORS headers on response
86
+ *
87
+ * @param res Express response object
88
+ * @param corsConfig CORS configuration (false = disabled, true = allow all, object = custom)
89
+ */
90
+ declare function setCorsHeaders(res: Response, corsConfig?: boolean | CorsOptions): void;
91
+ /**
92
+ * 1x1 transparent GIF for pixel tracking
93
+ * Base64-encoded GIF (43 bytes)
94
+ */
95
+ declare const TRANSPARENT_GIF: Buffer<ArrayBuffer>;
96
+
97
+ /**
98
+ * Express source initialization
99
+ *
100
+ * This source OWNS its HTTP server infrastructure:
101
+ * - Creates Express application
102
+ * - Sets up middleware (JSON parsing, CORS)
103
+ * - Registers event collection endpoints (POST, GET, OPTIONS)
104
+ * - Starts HTTP server (if port configured)
105
+ * - Handles graceful shutdown
106
+ *
107
+ * @param config Partial source configuration
108
+ * @param env Source environment with push, command, elb functions
109
+ * @returns Express source instance with app and push handler
110
+ */
111
+ declare const sourceExpress: (config: PartialConfig, env: Types["env"]) => Promise<ExpressSource>;
112
+
113
+ export { type Config, type Env, type EventRequest, type EventResponse, type ExpressSource, type InitSettings, type Mapping, type PartialConfig, type Push, type RequestBody, type ResponseBody, type Settings, TRANSPARENT_GIF, type Types, setCorsHeaders, sourceExpress };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";var mod,__create=Object.create,__defProp=Object.defineProperty,__getOwnPropDesc=Object.getOwnPropertyDescriptor,__getOwnPropNames=Object.getOwnPropertyNames,__getProtoOf=Object.getPrototypeOf,__hasOwnProp=Object.prototype.hasOwnProperty,__copyProps=(to,from,except,desc)=>{if(from&&"object"==typeof from||"function"==typeof from)for(let key of __getOwnPropNames(from))__hasOwnProp.call(to,key)||key===except||__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to},__toESM=(mod,isNodeMode,target)=>(target=null!=mod?__create(__getProtoOf(mod)):{},__copyProps(!isNodeMode&&mod&&mod.__esModule?target:__defProp(target,"default",{value:mod,enumerable:!0}),mod)),index_exports={};((target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})})(index_exports,{TRANSPARENT_GIF:()=>TRANSPARENT_GIF,setCorsHeaders:()=>setCorsHeaders,sourceExpress:()=>sourceExpress}),module.exports=(mod=index_exports,__copyProps(__defProp({},"__esModule",{value:!0}),mod));var import_express=__toESM(require("express")),import_cors=__toESM(require("cors")),import_core=require("@walkeros/core"),import_dev=require("@walkeros/core/dev"),HttpMethod=import_dev.z.enum(["GET","POST","PUT","PATCH","DELETE","OPTIONS","HEAD"]),CorsOrigin=import_dev.z.union([import_dev.z.string(),import_dev.z.array(import_dev.z.string()),import_dev.z.literal("*")]),CorsOptionsSchema=import_dev.z.object({origin:CorsOrigin.describe("Allowed origins (* for all, URL string, or array of URLs)").optional(),methods:import_dev.z.array(HttpMethod).describe("Allowed HTTP methods").optional(),headers:import_dev.z.array(import_dev.z.string()).describe("Allowed request headers").optional(),credentials:import_dev.z.boolean().describe("Allow credentials (cookies, authorization headers)").optional(),maxAge:import_dev.z.number().int().positive().describe("Preflight cache duration in seconds").optional()}),import_dev2=require("@walkeros/core/dev"),SettingsSchema=import_dev2.z.object({port:import_dev2.z.number().int().min(0).max(65535).describe("HTTP server port to listen on. Use 0 for random available port. If not provided, server will not start (app only mode)").optional(),path:import_dev2.z.string().describe("Event collection endpoint path").default("/collect"),cors:import_dev2.z.union([import_dev2.z.boolean(),CorsOptionsSchema]).describe("CORS configuration: false = disabled, true = allow all origins (default), object = custom configuration").default(!0),status:import_dev2.z.boolean().describe("Enable health check endpoints (/health, /ready)").default(!0)});function setCorsHeaders(res,corsConfig=!0){if(!1!==corsConfig)if(!0===corsConfig)res.set("Access-Control-Allow-Origin","*"),res.set("Access-Control-Allow-Methods","GET, POST, OPTIONS"),res.set("Access-Control-Allow-Headers","Content-Type");else{if(corsConfig.origin){const origin=Array.isArray(corsConfig.origin)?corsConfig.origin.join(", "):corsConfig.origin;res.set("Access-Control-Allow-Origin",origin)}corsConfig.methods&&res.set("Access-Control-Allow-Methods",corsConfig.methods.join(", ")),corsConfig.headers&&res.set("Access-Control-Allow-Headers",corsConfig.headers.join(", ")),corsConfig.credentials&&res.set("Access-Control-Allow-Credentials","true"),corsConfig.maxAge&&res.set("Access-Control-Max-Age",String(corsConfig.maxAge))}}var TRANSPARENT_GIF=Buffer.from("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7","base64"),sourceExpress=async(config,env)=>{const settings=SettingsSchema.parse(config.settings||{}),app=(0,import_express.default)();if(app.use(import_express.default.json({limit:"1mb"})),!1!==settings.cors){const corsOptions=!0===settings.cors?{}:settings.cors;app.use((0,import_cors.default)(corsOptions))}const push=async(req,res)=>{try{if("OPTIONS"===req.method)return setCorsHeaders(res,settings.cors),void res.status(204).send();if("GET"===req.method){const parsedData=(0,import_core.requestToData)(req.url);return parsedData&&"object"==typeof parsedData&&await env.push(parsedData),res.set("Content-Type","image/gif"),res.set("Cache-Control","no-cache, no-store, must-revalidate"),void res.send(TRANSPARENT_GIF)}if("POST"===req.method){const eventData=req.body;return eventData&&"object"==typeof eventData?(await env.push(eventData),void res.json({success:!0,timestamp:Date.now()})):void res.status(400).json({success:!1,error:"Invalid event: body must be an object"})}res.status(405).json({success:!1,error:"Method not allowed. Use POST, GET, or OPTIONS."})}catch(error){res.status(500).json({success:!1,error:error instanceof Error?error.message:"Internal server error"})}};let server;if(app.post(settings.path,push),app.get(settings.path,push),app.options(settings.path,push),settings.status&&(app.get("/health",(req,res)=>{res.json({status:"ok",timestamp:Date.now(),source:"express"})}),app.get("/ready",(req,res)=>{res.json({status:"ready",timestamp:Date.now(),source:"express"})})),void 0!==settings.port){server=app.listen(settings.port,()=>{const statusRoutes=settings.status?"\n GET /health - Health check\n GET /ready - Readiness check":"";env.logger.info(`Express source listening on port ${settings.port}\n POST ${settings.path} - Event collection (JSON body)\n GET ${settings.path} - Pixel tracking (query params)\n OPTIONS ${settings.path} - CORS preflight`+statusRoutes)});const shutdownHandler=()=>{server&&server.close()};process.on("SIGTERM",shutdownHandler),process.on("SIGINT",shutdownHandler)}return{type:"express",config:{...config,settings:settings},push:push,app:app,server:server}};//# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/schemas/primitives.ts","../src/schemas/settings.ts","../src/utils.ts"],"sourcesContent":["import express, { type Request, type Response } from 'express';\nimport cors from 'cors';\nimport { requestToData } from '@walkeros/core';\nimport type {\n ExpressSource,\n PartialConfig,\n Types,\n EventRequest,\n} from './types';\nimport { SettingsSchema } from './schemas';\nimport { setCorsHeaders, TRANSPARENT_GIF } from './utils';\n\n/**\n * Express source initialization\n *\n * This source OWNS its HTTP server infrastructure:\n * - Creates Express application\n * - Sets up middleware (JSON parsing, CORS)\n * - Registers event collection endpoints (POST, GET, OPTIONS)\n * - Starts HTTP server (if port configured)\n * - Handles graceful shutdown\n *\n * @param config Partial source configuration\n * @param env Source environment with push, command, elb functions\n * @returns Express source instance with app and push handler\n */\nexport const sourceExpress = async (\n config: PartialConfig,\n env: Types['env'],\n): Promise<ExpressSource> => {\n // Validate and apply default settings\n const settings = SettingsSchema.parse(config.settings || {});\n\n const app = express();\n\n // Middleware setup - JSON body parsing with 10mb default limit\n app.use(express.json({ limit: '1mb' }));\n\n // CORS middleware (enabled by default)\n if (settings.cors !== false) {\n const corsOptions = settings.cors === true ? {} : settings.cors;\n app.use(cors(corsOptions));\n }\n\n /**\n * Request handler - transforms HTTP requests into walker events\n * Supports POST (JSON body), GET (query params), and OPTIONS (CORS preflight)\n */\n const push = async (req: Request, res: Response): Promise<void> => {\n try {\n // Handle OPTIONS for CORS preflight\n if (req.method === 'OPTIONS') {\n setCorsHeaders(res, settings.cors);\n res.status(204).send();\n return;\n }\n\n // Handle GET requests (pixel tracking)\n if (req.method === 'GET') {\n // Parse query parameters to event data using requestToData\n const parsedData = requestToData(req.url);\n\n // Send to collector\n if (parsedData && typeof parsedData === 'object') {\n await env.push(parsedData);\n }\n\n // Return 1x1 transparent GIF for pixel tracking\n res.set('Content-Type', 'image/gif');\n res.set('Cache-Control', 'no-cache, no-store, must-revalidate');\n res.send(TRANSPARENT_GIF);\n return;\n }\n\n // Handle POST requests (standard event ingestion)\n if (req.method === 'POST') {\n const eventData = req.body;\n\n if (!eventData || typeof eventData !== 'object') {\n res.status(400).json({\n success: false,\n error: 'Invalid event: body must be an object',\n });\n return;\n }\n\n // Send event to collector\n await env.push(eventData);\n\n res.json({\n success: true,\n timestamp: Date.now(),\n });\n return;\n }\n\n // Unsupported method\n res.status(405).json({\n success: false,\n error: 'Method not allowed. Use POST, GET, or OPTIONS.',\n });\n } catch (error) {\n res.status(500).json({\n success: false,\n error: error instanceof Error ? error.message : 'Internal server error',\n });\n }\n };\n\n // Register event collection endpoint (handles POST, GET, OPTIONS)\n app.post(settings.path, push);\n app.get(settings.path, push);\n app.options(settings.path, push);\n\n // Health check endpoints (if enabled)\n if (settings.status) {\n app.get('/health', (req, res) => {\n res.json({\n status: 'ok',\n timestamp: Date.now(),\n source: 'express',\n });\n });\n\n app.get('/ready', (req, res) => {\n res.json({\n status: 'ready',\n timestamp: Date.now(),\n source: 'express',\n });\n });\n }\n\n // Source owns the HTTP server (if port configured)\n let server: ReturnType<typeof app.listen> | undefined;\n\n if (settings.port !== undefined) {\n server = app.listen(settings.port, () => {\n const statusRoutes = settings.status\n ? `\\n GET /health - Health check\\n GET /ready - Readiness check`\n : '';\n env.logger.info(\n `Express source listening on port ${settings.port}\\n` +\n ` POST ${settings.path} - Event collection (JSON body)\\n` +\n ` GET ${settings.path} - Pixel tracking (query params)\\n` +\n ` OPTIONS ${settings.path} - CORS preflight` +\n statusRoutes,\n );\n });\n\n // Graceful shutdown handlers\n const shutdownHandler = () => {\n if (server) {\n server.close();\n }\n };\n\n process.on('SIGTERM', shutdownHandler);\n process.on('SIGINT', shutdownHandler);\n }\n\n const instance: ExpressSource = {\n type: 'express',\n config: {\n ...config,\n settings,\n },\n push,\n app, // Expose app for advanced usage\n server, // Expose server (if started)\n };\n\n return instance;\n};\n\n// Export types (avoid re-exporting duplicates from schemas)\nexport type {\n ExpressSource,\n Config,\n PartialConfig,\n Types,\n EventRequest,\n EventResponse,\n RequestBody,\n ResponseBody,\n Push,\n Env,\n Mapping,\n InitSettings,\n Settings,\n} from './types';\n\n// Export utils\nexport { setCorsHeaders, TRANSPARENT_GIF } from './utils';\n","import { z } from '@walkeros/core/dev';\n\n/**\n * HTTP methods enum\n */\nexport const HttpMethod = z.enum([\n 'GET',\n 'POST',\n 'PUT',\n 'PATCH',\n 'DELETE',\n 'OPTIONS',\n 'HEAD',\n]);\n\n/**\n * CORS origin configuration\n * Accepts:\n * - '*' for all origins\n * - Single URL string\n * - Array of URL strings\n */\nexport const CorsOrigin = z.union([\n z.string(),\n z.array(z.string()),\n z.literal('*'),\n]);\n\n/**\n * CORS options schema\n * Configuration for Cross-Origin Resource Sharing\n */\nexport const CorsOptionsSchema = z.object({\n origin: CorsOrigin.describe(\n 'Allowed origins (* for all, URL string, or array of URLs)',\n ).optional(),\n\n methods: z.array(HttpMethod).describe('Allowed HTTP methods').optional(),\n\n headers: z.array(z.string()).describe('Allowed request headers').optional(),\n\n credentials: z\n .boolean()\n .describe('Allow credentials (cookies, authorization headers)')\n .optional(),\n\n maxAge: z\n .number()\n .int()\n .positive()\n .describe('Preflight cache duration in seconds')\n .optional(),\n});\n\nexport type CorsOptions = z.infer<typeof CorsOptionsSchema>;\n","import { z } from '@walkeros/core/dev';\nimport { CorsOptionsSchema } from './primitives';\n\n/**\n * Express source settings schema\n */\nexport const SettingsSchema = z.object({\n port: z\n .number()\n .int()\n .min(0) // 0 = random available port\n .max(65535)\n .describe(\n 'HTTP server port to listen on. Use 0 for random available port. If not provided, server will not start (app only mode)',\n )\n .optional(),\n\n path: z\n .string()\n .describe('Event collection endpoint path')\n .default('/collect'),\n\n cors: z\n .union([z.boolean(), CorsOptionsSchema])\n .describe(\n 'CORS configuration: false = disabled, true = allow all origins (default), object = custom configuration',\n )\n .default(true),\n\n status: z\n .boolean()\n .describe('Enable health check endpoints (/health, /ready)')\n .default(true),\n});\n\nexport type Settings = z.infer<typeof SettingsSchema>;\n","import type { Response } from 'express';\nimport type { CorsOptions } from './schemas';\n\n/**\n * Set CORS headers on response\n *\n * @param res Express response object\n * @param corsConfig CORS configuration (false = disabled, true = allow all, object = custom)\n */\nexport function setCorsHeaders(\n res: Response,\n corsConfig: boolean | CorsOptions = true,\n): void {\n if (corsConfig === false) return;\n\n if (corsConfig === true) {\n // Simple CORS - allow all\n res.set('Access-Control-Allow-Origin', '*');\n res.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n res.set('Access-Control-Allow-Headers', 'Content-Type');\n } else {\n // Custom CORS configuration\n if (corsConfig.origin) {\n const origin = Array.isArray(corsConfig.origin)\n ? corsConfig.origin.join(', ')\n : corsConfig.origin;\n res.set('Access-Control-Allow-Origin', origin);\n }\n\n if (corsConfig.methods) {\n res.set('Access-Control-Allow-Methods', corsConfig.methods.join(', '));\n }\n\n if (corsConfig.headers) {\n res.set('Access-Control-Allow-Headers', corsConfig.headers.join(', '));\n }\n\n if (corsConfig.credentials) {\n res.set('Access-Control-Allow-Credentials', 'true');\n }\n\n if (corsConfig.maxAge) {\n res.set('Access-Control-Max-Age', String(corsConfig.maxAge));\n }\n }\n}\n\n/**\n * 1x1 transparent GIF for pixel tracking\n * Base64-encoded GIF (43 bytes)\n */\nexport const TRANSPARENT_GIF = Buffer.from(\n 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',\n 'base64',\n);\n"],"mappings":"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","names":["import_dev","express","cors"]}
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ import express from"express";import cors from"cors";import{requestToData}from"@walkeros/core";import{z}from"@walkeros/core/dev";var HttpMethod=z.enum(["GET","POST","PUT","PATCH","DELETE","OPTIONS","HEAD"]),CorsOrigin=z.union([z.string(),z.array(z.string()),z.literal("*")]),CorsOptionsSchema=z.object({origin:CorsOrigin.describe("Allowed origins (* for all, URL string, or array of URLs)").optional(),methods:z.array(HttpMethod).describe("Allowed HTTP methods").optional(),headers:z.array(z.string()).describe("Allowed request headers").optional(),credentials:z.boolean().describe("Allow credentials (cookies, authorization headers)").optional(),maxAge:z.number().int().positive().describe("Preflight cache duration in seconds").optional()});import{z as z2}from"@walkeros/core/dev";var SettingsSchema=z2.object({port:z2.number().int().min(0).max(65535).describe("HTTP server port to listen on. Use 0 for random available port. If not provided, server will not start (app only mode)").optional(),path:z2.string().describe("Event collection endpoint path").default("/collect"),cors:z2.union([z2.boolean(),CorsOptionsSchema]).describe("CORS configuration: false = disabled, true = allow all origins (default), object = custom configuration").default(!0),status:z2.boolean().describe("Enable health check endpoints (/health, /ready)").default(!0)});function setCorsHeaders(res,corsConfig=!0){if(!1!==corsConfig)if(!0===corsConfig)res.set("Access-Control-Allow-Origin","*"),res.set("Access-Control-Allow-Methods","GET, POST, OPTIONS"),res.set("Access-Control-Allow-Headers","Content-Type");else{if(corsConfig.origin){const origin=Array.isArray(corsConfig.origin)?corsConfig.origin.join(", "):corsConfig.origin;res.set("Access-Control-Allow-Origin",origin)}corsConfig.methods&&res.set("Access-Control-Allow-Methods",corsConfig.methods.join(", ")),corsConfig.headers&&res.set("Access-Control-Allow-Headers",corsConfig.headers.join(", ")),corsConfig.credentials&&res.set("Access-Control-Allow-Credentials","true"),corsConfig.maxAge&&res.set("Access-Control-Max-Age",String(corsConfig.maxAge))}}var TRANSPARENT_GIF=Buffer.from("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7","base64"),sourceExpress=async(config,env)=>{const settings=SettingsSchema.parse(config.settings||{}),app=express();if(app.use(express.json({limit:"1mb"})),!1!==settings.cors){const corsOptions=!0===settings.cors?{}:settings.cors;app.use(cors(corsOptions))}const push=async(req,res)=>{try{if("OPTIONS"===req.method)return setCorsHeaders(res,settings.cors),void res.status(204).send();if("GET"===req.method){const parsedData=requestToData(req.url);return parsedData&&"object"==typeof parsedData&&await env.push(parsedData),res.set("Content-Type","image/gif"),res.set("Cache-Control","no-cache, no-store, must-revalidate"),void res.send(TRANSPARENT_GIF)}if("POST"===req.method){const eventData=req.body;return eventData&&"object"==typeof eventData?(await env.push(eventData),void res.json({success:!0,timestamp:Date.now()})):void res.status(400).json({success:!1,error:"Invalid event: body must be an object"})}res.status(405).json({success:!1,error:"Method not allowed. Use POST, GET, or OPTIONS."})}catch(error){res.status(500).json({success:!1,error:error instanceof Error?error.message:"Internal server error"})}};let server;if(app.post(settings.path,push),app.get(settings.path,push),app.options(settings.path,push),settings.status&&(app.get("/health",(req,res)=>{res.json({status:"ok",timestamp:Date.now(),source:"express"})}),app.get("/ready",(req,res)=>{res.json({status:"ready",timestamp:Date.now(),source:"express"})})),void 0!==settings.port){server=app.listen(settings.port,()=>{const statusRoutes=settings.status?"\n GET /health - Health check\n GET /ready - Readiness check":"";env.logger.info(`Express source listening on port ${settings.port}\n POST ${settings.path} - Event collection (JSON body)\n GET ${settings.path} - Pixel tracking (query params)\n OPTIONS ${settings.path} - CORS preflight`+statusRoutes)});const shutdownHandler=()=>{server&&server.close()};process.on("SIGTERM",shutdownHandler),process.on("SIGINT",shutdownHandler)}return{type:"express",config:{...config,settings:settings},push:push,app:app,server:server}};export{TRANSPARENT_GIF,setCorsHeaders,sourceExpress};//# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/schemas/primitives.ts","../src/schemas/settings.ts","../src/utils.ts"],"sourcesContent":["import express, { type Request, type Response } from 'express';\nimport cors from 'cors';\nimport { requestToData } from '@walkeros/core';\nimport type {\n ExpressSource,\n PartialConfig,\n Types,\n EventRequest,\n} from './types';\nimport { SettingsSchema } from './schemas';\nimport { setCorsHeaders, TRANSPARENT_GIF } from './utils';\n\n/**\n * Express source initialization\n *\n * This source OWNS its HTTP server infrastructure:\n * - Creates Express application\n * - Sets up middleware (JSON parsing, CORS)\n * - Registers event collection endpoints (POST, GET, OPTIONS)\n * - Starts HTTP server (if port configured)\n * - Handles graceful shutdown\n *\n * @param config Partial source configuration\n * @param env Source environment with push, command, elb functions\n * @returns Express source instance with app and push handler\n */\nexport const sourceExpress = async (\n config: PartialConfig,\n env: Types['env'],\n): Promise<ExpressSource> => {\n // Validate and apply default settings\n const settings = SettingsSchema.parse(config.settings || {});\n\n const app = express();\n\n // Middleware setup - JSON body parsing with 10mb default limit\n app.use(express.json({ limit: '1mb' }));\n\n // CORS middleware (enabled by default)\n if (settings.cors !== false) {\n const corsOptions = settings.cors === true ? {} : settings.cors;\n app.use(cors(corsOptions));\n }\n\n /**\n * Request handler - transforms HTTP requests into walker events\n * Supports POST (JSON body), GET (query params), and OPTIONS (CORS preflight)\n */\n const push = async (req: Request, res: Response): Promise<void> => {\n try {\n // Handle OPTIONS for CORS preflight\n if (req.method === 'OPTIONS') {\n setCorsHeaders(res, settings.cors);\n res.status(204).send();\n return;\n }\n\n // Handle GET requests (pixel tracking)\n if (req.method === 'GET') {\n // Parse query parameters to event data using requestToData\n const parsedData = requestToData(req.url);\n\n // Send to collector\n if (parsedData && typeof parsedData === 'object') {\n await env.push(parsedData);\n }\n\n // Return 1x1 transparent GIF for pixel tracking\n res.set('Content-Type', 'image/gif');\n res.set('Cache-Control', 'no-cache, no-store, must-revalidate');\n res.send(TRANSPARENT_GIF);\n return;\n }\n\n // Handle POST requests (standard event ingestion)\n if (req.method === 'POST') {\n const eventData = req.body;\n\n if (!eventData || typeof eventData !== 'object') {\n res.status(400).json({\n success: false,\n error: 'Invalid event: body must be an object',\n });\n return;\n }\n\n // Send event to collector\n await env.push(eventData);\n\n res.json({\n success: true,\n timestamp: Date.now(),\n });\n return;\n }\n\n // Unsupported method\n res.status(405).json({\n success: false,\n error: 'Method not allowed. Use POST, GET, or OPTIONS.',\n });\n } catch (error) {\n res.status(500).json({\n success: false,\n error: error instanceof Error ? error.message : 'Internal server error',\n });\n }\n };\n\n // Register event collection endpoint (handles POST, GET, OPTIONS)\n app.post(settings.path, push);\n app.get(settings.path, push);\n app.options(settings.path, push);\n\n // Health check endpoints (if enabled)\n if (settings.status) {\n app.get('/health', (req, res) => {\n res.json({\n status: 'ok',\n timestamp: Date.now(),\n source: 'express',\n });\n });\n\n app.get('/ready', (req, res) => {\n res.json({\n status: 'ready',\n timestamp: Date.now(),\n source: 'express',\n });\n });\n }\n\n // Source owns the HTTP server (if port configured)\n let server: ReturnType<typeof app.listen> | undefined;\n\n if (settings.port !== undefined) {\n server = app.listen(settings.port, () => {\n const statusRoutes = settings.status\n ? `\\n GET /health - Health check\\n GET /ready - Readiness check`\n : '';\n env.logger.info(\n `Express source listening on port ${settings.port}\\n` +\n ` POST ${settings.path} - Event collection (JSON body)\\n` +\n ` GET ${settings.path} - Pixel tracking (query params)\\n` +\n ` OPTIONS ${settings.path} - CORS preflight` +\n statusRoutes,\n );\n });\n\n // Graceful shutdown handlers\n const shutdownHandler = () => {\n if (server) {\n server.close();\n }\n };\n\n process.on('SIGTERM', shutdownHandler);\n process.on('SIGINT', shutdownHandler);\n }\n\n const instance: ExpressSource = {\n type: 'express',\n config: {\n ...config,\n settings,\n },\n push,\n app, // Expose app for advanced usage\n server, // Expose server (if started)\n };\n\n return instance;\n};\n\n// Export types (avoid re-exporting duplicates from schemas)\nexport type {\n ExpressSource,\n Config,\n PartialConfig,\n Types,\n EventRequest,\n EventResponse,\n RequestBody,\n ResponseBody,\n Push,\n Env,\n Mapping,\n InitSettings,\n Settings,\n} from './types';\n\n// Export utils\nexport { setCorsHeaders, TRANSPARENT_GIF } from './utils';\n","import { z } from '@walkeros/core/dev';\n\n/**\n * HTTP methods enum\n */\nexport const HttpMethod = z.enum([\n 'GET',\n 'POST',\n 'PUT',\n 'PATCH',\n 'DELETE',\n 'OPTIONS',\n 'HEAD',\n]);\n\n/**\n * CORS origin configuration\n * Accepts:\n * - '*' for all origins\n * - Single URL string\n * - Array of URL strings\n */\nexport const CorsOrigin = z.union([\n z.string(),\n z.array(z.string()),\n z.literal('*'),\n]);\n\n/**\n * CORS options schema\n * Configuration for Cross-Origin Resource Sharing\n */\nexport const CorsOptionsSchema = z.object({\n origin: CorsOrigin.describe(\n 'Allowed origins (* for all, URL string, or array of URLs)',\n ).optional(),\n\n methods: z.array(HttpMethod).describe('Allowed HTTP methods').optional(),\n\n headers: z.array(z.string()).describe('Allowed request headers').optional(),\n\n credentials: z\n .boolean()\n .describe('Allow credentials (cookies, authorization headers)')\n .optional(),\n\n maxAge: z\n .number()\n .int()\n .positive()\n .describe('Preflight cache duration in seconds')\n .optional(),\n});\n\nexport type CorsOptions = z.infer<typeof CorsOptionsSchema>;\n","import { z } from '@walkeros/core/dev';\nimport { CorsOptionsSchema } from './primitives';\n\n/**\n * Express source settings schema\n */\nexport const SettingsSchema = z.object({\n port: z\n .number()\n .int()\n .min(0) // 0 = random available port\n .max(65535)\n .describe(\n 'HTTP server port to listen on. Use 0 for random available port. If not provided, server will not start (app only mode)',\n )\n .optional(),\n\n path: z\n .string()\n .describe('Event collection endpoint path')\n .default('/collect'),\n\n cors: z\n .union([z.boolean(), CorsOptionsSchema])\n .describe(\n 'CORS configuration: false = disabled, true = allow all origins (default), object = custom configuration',\n )\n .default(true),\n\n status: z\n .boolean()\n .describe('Enable health check endpoints (/health, /ready)')\n .default(true),\n});\n\nexport type Settings = z.infer<typeof SettingsSchema>;\n","import type { Response } from 'express';\nimport type { CorsOptions } from './schemas';\n\n/**\n * Set CORS headers on response\n *\n * @param res Express response object\n * @param corsConfig CORS configuration (false = disabled, true = allow all, object = custom)\n */\nexport function setCorsHeaders(\n res: Response,\n corsConfig: boolean | CorsOptions = true,\n): void {\n if (corsConfig === false) return;\n\n if (corsConfig === true) {\n // Simple CORS - allow all\n res.set('Access-Control-Allow-Origin', '*');\n res.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n res.set('Access-Control-Allow-Headers', 'Content-Type');\n } else {\n // Custom CORS configuration\n if (corsConfig.origin) {\n const origin = Array.isArray(corsConfig.origin)\n ? corsConfig.origin.join(', ')\n : corsConfig.origin;\n res.set('Access-Control-Allow-Origin', origin);\n }\n\n if (corsConfig.methods) {\n res.set('Access-Control-Allow-Methods', corsConfig.methods.join(', '));\n }\n\n if (corsConfig.headers) {\n res.set('Access-Control-Allow-Headers', corsConfig.headers.join(', '));\n }\n\n if (corsConfig.credentials) {\n res.set('Access-Control-Allow-Credentials', 'true');\n }\n\n if (corsConfig.maxAge) {\n res.set('Access-Control-Max-Age', String(corsConfig.maxAge));\n }\n }\n}\n\n/**\n * 1x1 transparent GIF for pixel tracking\n * Base64-encoded GIF (43 bytes)\n */\nexport const TRANSPARENT_GIF = Buffer.from(\n 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',\n 'base64',\n);\n"],"mappings":"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","names":["z","z"]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@walkeros/server-source-express",
3
+ "description": "Express server source for walkerOS",
4
+ "version": "0.0.0-next-20251219153324",
5
+ "license": "MIT",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist/**"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup --silent",
14
+ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
15
+ "dev": "jest --watchAll --colors",
16
+ "lint": "tsc && eslint \"**/*.ts*\"",
17
+ "test": "jest",
18
+ "update": "npx npm-check-updates -u && npm update"
19
+ },
20
+ "dependencies": {
21
+ "@walkeros/core": "0.0.0-next-20251219153324",
22
+ "express": "^5.2.1",
23
+ "cors": "^2.8.5"
24
+ },
25
+ "devDependencies": {
26
+ "@types/express": "^5.0.6",
27
+ "@types/cors": "^2.8.19"
28
+ },
29
+ "repository": {
30
+ "url": "git+https://github.com/elbwalker/walkerOS.git",
31
+ "directory": "packages/server/sources/express"
32
+ },
33
+ "author": "elbwalker <hello@elbwalker.com>",
34
+ "homepage": "https://github.com/elbwalker/walkerOS#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/elbwalker/walkerOS/issues"
37
+ },
38
+ "keywords": [
39
+ "walker",
40
+ "walkerOS",
41
+ "source",
42
+ "server",
43
+ "express",
44
+ "http"
45
+ ],
46
+ "funding": [
47
+ {
48
+ "type": "GitHub Sponsors",
49
+ "url": "https://github.com/sponsors/elbwalker"
50
+ }
51
+ ],
52
+ "exports": {
53
+ ".": {
54
+ "types": "./dist/index.d.ts",
55
+ "import": "./dist/index.mjs",
56
+ "require": "./dist/index.js"
57
+ },
58
+ "./dev": {
59
+ "types": "./dist/dev.d.ts",
60
+ "import": "./dist/dev.mjs",
61
+ "require": "./dist/dev.js"
62
+ }
63
+ }
64
+ }