@walkeros/server-source-express 4.2.1-next-1781538735002 → 4.2.1-next-1781682752679
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 +12 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/walkerOS.json +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
# @walkeros/server-source-express
|
|
2
2
|
|
|
3
|
-
## 4.2.1-next-
|
|
3
|
+
## 4.2.1-next-1781682752679
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
|
+
- 0a8a08b: Add an optional `async` option to the source config
|
|
8
|
+
(`Source.Config.async`) for respond-first acknowledgement on
|
|
9
|
+
response-producing server sources. The express source now reads `config.async`
|
|
10
|
+
(default `true`): a 2xx response means the event was accepted, not yet
|
|
11
|
+
delivered.
|
|
7
12
|
- Updated dependencies [bd9188d]
|
|
13
|
+
- Updated dependencies [d8aebd1]
|
|
8
14
|
- Updated dependencies [5cbcd23]
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
- Updated dependencies [0a8a08b]
|
|
16
|
+
- Updated dependencies [8afb7cc]
|
|
17
|
+
- Updated dependencies [8afb7cc]
|
|
18
|
+
- @walkeros/collector@4.2.1-next-1781682752679
|
|
19
|
+
- @walkeros/core@4.2.1-next-1781682752679
|
|
11
20
|
|
|
12
21
|
## 4.2.0
|
|
13
22
|
|
package/dist/index.js
CHANGED
|
@@ -1 +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,default:()=>index_default,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");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 context=>{const{config:config={},env:env}=context,expressLib=env.express??import_express.default,corsLib=env.cors??import_cors.default,userSettings=config.settings||{},settings={...userSettings,cors:userSettings.cors??!0,paths:userSettings.paths??(userSettings.path?[userSettings.path]:["/collect"])},app=expressLib();if(app.use(expressLib.json({limit:"1mb",type:["application/json","text/plain"]})),!1!==settings.cors){const corsOptions=!0===settings.cors?{}:settings.cors;app.use(corsLib(corsOptions))}const push=async(req,res)=>{try{if("OPTIONS"===req.method)return setCorsHeaders(res,settings.cors),void res.status(204).send();const respond=(0,import_core.createRespond)(options=>{const status=options.status??200;if(options.headers)for(const[key,value]of Object.entries(options.headers))res.set(key,value);res.status(status);const body=options.body;"string"==typeof body||Buffer.isBuffer(body)?res.send(body):body instanceof Uint8Array?res.send(Buffer.from(body)):res.json(body)});await context.withScope(req,respond,async env2=>{if("GET"===req.method){const parsedData=(0,import_core.requestToData)(req.url)
|
|
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,default:()=>index_default,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");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"),toError=value=>value instanceof Error?value:new Error(String(value)),sourceExpress=async context=>{const{config:config={},env:env}=context,expressLib=env.express??import_express.default,corsLib=env.cors??import_cors.default,userSettings=config.settings||{},settings={...userSettings,cors:userSettings.cors??!0,paths:userSettings.paths??(userSettings.path?[userSettings.path]:["/collect"])},respondFirst=config.async??!0,app=expressLib();if(app.use(expressLib.json({limit:"1mb",type:["application/json","text/plain"]})),!1!==settings.cors){const corsOptions=!0===settings.cors?{}:settings.cors;app.use(corsLib(corsOptions))}const push=async(req,res)=>{try{if("OPTIONS"===req.method)return setCorsHeaders(res,settings.cors),void res.status(204).send();const respond=(0,import_core.createRespond)(options=>{const status=options.status??200;if(options.headers)for(const[key,value]of Object.entries(options.headers))res.set(key,value);res.status(status);const body=options.body;"string"==typeof body||Buffer.isBuffer(body)?res.send(body):body instanceof Uint8Array?res.send(Buffer.from(body)):res.json(body)});await context.withScope(req,respond,async env2=>{if("GET"===req.method){const parsedData=(0,import_core.requestToData)(req.url),respondGif=()=>respond({body:TRANSPARENT_GIF,headers:{"Content-Type":"image/gif","Cache-Control":"no-cache, no-store, must-revalidate"}});return void(parsedData&&"object"==typeof parsedData?respondFirst?(respondGif(),env2.push(parsedData).catch(err=>{env2.logger.error(toError(err))})):(await env2.push(parsedData),respondGif()):respondGif())}if("POST"===req.method){const eventData=req.body&&"object"==typeof req.body?req.body:{};return void(respondFirst?(respond({body:{success:!0,timestamp:Date.now()}}),env2.push(eventData).catch(err=>{env2.logger.error(toError(err))})):(await env2.push(eventData),respond({body:{success:!0,timestamp:Date.now()}})))}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"})}},resolvedPaths=settings.paths.map(entry=>"string"==typeof entry?{path:entry,methods:["GET","POST"]}:{path:entry.path,methods:entry.methods||["GET","POST"]});for(const route of resolvedPaths)route.methods.includes("POST")&&app.post(route.path,push),route.methods.includes("GET")&&app.get(route.path,push),app.options(route.path,push);let server;void 0!==settings.port&&(server=app.listen(settings.port,()=>{const routeLines=resolvedPaths.map(r=>` ${[...r.methods,"OPTIONS"].join(", ")} ${r.path}`).join("\n");env.logger.info(`Express source listening on port ${settings.port}\n`+routeLines)}));return{type:"express",config:{...config,settings:settings},push:push,httpHandler:app,app:app,server:server,destroy:_context=>new Promise((resolve,reject)=>{if(!server)return resolve();server.close(err=>err?reject(err):resolve())})}},index_default=sourceExpress;//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/utils.ts"],"sourcesContent":["import express, { type Request, type Response } from 'express';\nimport cors from 'cors';\nimport { requestToData, createRespond } from '@walkeros/core';\nimport type { Source } from '@walkeros/core';\nimport type { ExpressSource, Types, EventRequest } from './types';\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 * - Provides destroy() for graceful shutdown (called by runner)\n *\n * @param context Source context with config, env, logger, id\n * @returns Express source instance with app and push handler\n */\nexport const sourceExpress = async (\n context: Source.Context<Types>,\n): Promise<ExpressSource> => {\n const { config = {}, env } = context;\n const expressLib = env.express ?? express;\n const corsLib = env.cors ?? cors;\n\n // Apply defaults (no runtime validation — flow.json is developer-controlled).\n const userSettings = config.settings || {};\n const settings = {\n ...userSettings,\n cors: userSettings.cors ?? true,\n paths:\n userSettings.paths ??\n (userSettings.path ? [userSettings.path] : ['/collect']),\n };\n\n const app = expressLib();\n\n // Body parsing — JSON content-type plus text/plain so navigator.sendBeacon\n // payloads (which the browser forces to text/plain;charset=UTF-8) are also\n // parsed as JSON. 1mb default limit.\n app.use(\n expressLib.json({\n limit: '1mb',\n type: ['application/json', 'text/plain'],\n }),\n );\n\n // CORS middleware (enabled by default)\n if (settings.cors !== false) {\n const corsOptions = settings.cors === true ? {} : settings.cors;\n app.use(corsLib(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 * Each inbound request gets its own `withScope` invocation. The per-scope\n * env carries this request's `ingest` and `respond` end to end, so\n * concurrent requests never crosstalk through source-factory state.\n */\n const push = async (req: Request, res: Response): Promise<void> => {\n try {\n // Handle OPTIONS for CORS preflight (no scope needed: no event, no ingest)\n if (req.method === 'OPTIONS') {\n setCorsHeaders(res, settings.cors);\n res.status(204).send();\n return;\n }\n\n // Create per-request respond — first call wins (idempotent)\n const respond = createRespond((options) => {\n const status = options.status ?? 200;\n if (options.headers) {\n for (const [key, value] of Object.entries(options.headers)) {\n res.set(key, value);\n }\n }\n res.status(status);\n const body = options.body;\n if (typeof body === 'string' || Buffer.isBuffer(body)) {\n res.send(body);\n } else if (body instanceof Uint8Array) {\n // A decoded cache value surfaces binary as a plain Uint8Array,\n // not a Node Buffer; send it as bytes (res.json would corrupt it).\n res.send(Buffer.from(body));\n } else {\n res.json(body);\n }\n });\n\n await context.withScope(req, respond, async (env) => {\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 // Default: 1x1 GIF (skipped if a step already called respond)\n respond({\n body: TRANSPARENT_GIF,\n headers: {\n 'Content-Type': 'image/gif',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n });\n return;\n }\n\n // Handle POST requests (standard event ingestion)\n if (req.method === 'POST') {\n const eventData =\n req.body && typeof req.body === 'object' ? req.body : {};\n\n await env.push(eventData);\n\n respond({ body: { success: true, timestamp: Date.now() } });\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 });\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 handlers per route config\n const resolvedPaths = settings.paths.map((entry) =>\n typeof entry === 'string'\n ? { path: entry, methods: ['GET', 'POST'] as const }\n : {\n path: entry.path,\n methods: entry.methods || (['GET', 'POST'] as const),\n },\n );\n\n for (const route of resolvedPaths) {\n if (route.methods.includes('POST')) app.post(route.path, push);\n if (route.methods.includes('GET')) app.get(route.path, push);\n app.options(route.path, push); // Always register OPTIONS for CORS\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 routeLines = resolvedPaths\n .map((r) => {\n const methods = [...r.methods, 'OPTIONS'].join(', ');\n return ` ${methods} ${r.path}`;\n })\n .join('\\n');\n env.logger.info(\n `Express source listening on port ${settings.port}\\n` + routeLines,\n );\n });\n }\n\n const instance: ExpressSource = {\n type: 'express',\n config: {\n ...config,\n settings,\n },\n push,\n httpHandler: app,\n app,\n server,\n destroy: (_context) =>\n new Promise<void>((resolve, reject) => {\n if (!server) return resolve();\n server.close((err) => (err ? reject(err) : resolve()));\n }),\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 RouteConfig,\n RouteMethod,\n} from './types';\n\n// Export utils\nexport { setCorsHeaders, TRANSPARENT_GIF } from './utils';\n\nexport default sourceExpress;\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":["express","cors","env"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/utils.ts"],"sourcesContent":["import express, { type Request, type Response } from 'express';\nimport cors from 'cors';\nimport { requestToData, createRespond } from '@walkeros/core';\nimport type { Source } from '@walkeros/core';\nimport type { ExpressSource, Types, EventRequest } from './types';\nimport { setCorsHeaders, TRANSPARENT_GIF } from './utils';\n\n/**\n * Normalize an unknown rejection reason into an Error for the logger.\n * A fire-and-forget push can reject with any value; the logger accepts\n * `string | Error`, so non-Error reasons are wrapped.\n */\nconst toError = (value: unknown): Error =>\n value instanceof Error ? value : new Error(String(value));\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 * - Provides destroy() for graceful shutdown (called by runner)\n *\n * @param context Source context with config, env, logger, id\n * @returns Express source instance with app and push handler\n */\nexport const sourceExpress = async (\n context: Source.Context<Types>,\n): Promise<ExpressSource> => {\n const { config = {}, env } = context;\n const expressLib = env.express ?? express;\n const corsLib = env.cors ?? cors;\n\n // Apply defaults (no runtime validation — flow.json is developer-controlled).\n const userSettings = config.settings || {};\n const settings = {\n ...userSettings,\n cors: userSettings.cors ?? true,\n paths:\n userSettings.paths ??\n (userSettings.path ? [userSettings.path] : ['/collect']),\n };\n\n // Respond-first by default: a 2xx means \"accepted\", not \"delivered\".\n // Standardized on the source config (Source.Config.async), not settings.\n const respondFirst = config.async ?? true;\n\n const app = expressLib();\n\n // Body parsing — JSON content-type plus text/plain so navigator.sendBeacon\n // payloads (which the browser forces to text/plain;charset=UTF-8) are also\n // parsed as JSON. 1mb default limit.\n app.use(\n expressLib.json({\n limit: '1mb',\n type: ['application/json', 'text/plain'],\n }),\n );\n\n // CORS middleware (enabled by default)\n if (settings.cors !== false) {\n const corsOptions = settings.cors === true ? {} : settings.cors;\n app.use(corsLib(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 * Each inbound request gets its own `withScope` invocation. The per-scope\n * env carries this request's `ingest` and `respond` end to end, so\n * concurrent requests never crosstalk through source-factory state.\n */\n const push = async (req: Request, res: Response): Promise<void> => {\n try {\n // Handle OPTIONS for CORS preflight (no scope needed: no event, no ingest)\n if (req.method === 'OPTIONS') {\n setCorsHeaders(res, settings.cors);\n res.status(204).send();\n return;\n }\n\n // Create per-request respond — first call wins (idempotent)\n const respond = createRespond((options) => {\n const status = options.status ?? 200;\n if (options.headers) {\n for (const [key, value] of Object.entries(options.headers)) {\n res.set(key, value);\n }\n }\n res.status(status);\n const body = options.body;\n if (typeof body === 'string' || Buffer.isBuffer(body)) {\n res.send(body);\n } else if (body instanceof Uint8Array) {\n // A decoded cache value surfaces binary as a plain Uint8Array,\n // not a Node Buffer; send it as bytes (res.json would corrupt it).\n res.send(Buffer.from(body));\n } else {\n res.json(body);\n }\n });\n\n await context.withScope(req, respond, async (env) => {\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 // Default GIF body (idempotent fallback; skipped if a step already\n // called respond, e.g. a cache/asset destination serving real bytes).\n const respondGif = () =>\n respond({\n body: TRANSPARENT_GIF,\n headers: {\n 'Content-Type': 'image/gif',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n });\n\n if (parsedData && typeof parsedData === 'object') {\n if (respondFirst) {\n // Respond-first: the tracking pixel must return instantly and\n // never block on backend delivery. Fire the push without\n // awaiting; a rejected push is logged (destination errors are\n // DLQ'd inside the collector). A 2xx means \"accepted\", not\n // \"delivered\".\n respondGif();\n env.push(parsedData).catch((err: unknown) => {\n env.logger.error(toError(err));\n });\n } else {\n // Synchronous: await the push so a step (e.g. a cache/asset\n // destination) can respond with real content before the GIF\n // fallback applies.\n await env.push(parsedData);\n respondGif();\n }\n } else {\n respondGif();\n }\n return;\n }\n\n // Handle POST requests (standard event ingestion)\n if (req.method === 'POST') {\n const eventData =\n req.body && typeof req.body === 'object' ? req.body : {};\n\n if (respondFirst) {\n // Respond-first (\"accepted\"), then deliver asynchronously. A\n // rejected push is logged, not surfaced to the client and not left\n // unhandled (destination errors are DLQ'd inside the collector).\n respond({ body: { success: true, timestamp: Date.now() } });\n env.push(eventData).catch((err: unknown) => {\n env.logger.error(toError(err));\n });\n } else {\n // Synchronous ack: wait for delivery to settle before responding.\n await env.push(eventData);\n respond({ body: { success: true, 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 });\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 handlers per route config\n const resolvedPaths = settings.paths.map((entry) =>\n typeof entry === 'string'\n ? { path: entry, methods: ['GET', 'POST'] as const }\n : {\n path: entry.path,\n methods: entry.methods || (['GET', 'POST'] as const),\n },\n );\n\n for (const route of resolvedPaths) {\n if (route.methods.includes('POST')) app.post(route.path, push);\n if (route.methods.includes('GET')) app.get(route.path, push);\n app.options(route.path, push); // Always register OPTIONS for CORS\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 routeLines = resolvedPaths\n .map((r) => {\n const methods = [...r.methods, 'OPTIONS'].join(', ');\n return ` ${methods} ${r.path}`;\n })\n .join('\\n');\n env.logger.info(\n `Express source listening on port ${settings.port}\\n` + routeLines,\n );\n });\n }\n\n const instance: ExpressSource = {\n type: 'express',\n config: {\n ...config,\n settings,\n },\n push,\n httpHandler: app,\n app,\n server,\n destroy: (_context) =>\n new Promise<void>((resolve, reject) => {\n if (!server) return resolve();\n server.close((err) => (err ? reject(err) : resolve()));\n }),\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 RouteConfig,\n RouteMethod,\n} from './types';\n\n// Export utils\nexport { setCorsHeaders, TRANSPARENT_GIF } from './utils';\n\nexport default sourceExpress;\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":["express","cors","env"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import express from"express";import cors from"cors";import{requestToData,createRespond}from"@walkeros/core";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 context=>{const{config:config={},env:env}=context,expressLib=env.express??express,corsLib=env.cors??cors,userSettings=config.settings||{},settings={...userSettings,cors:userSettings.cors??!0,paths:userSettings.paths??(userSettings.path?[userSettings.path]:["/collect"])},app=expressLib();if(app.use(expressLib.json({limit:"1mb",type:["application/json","text/plain"]})),!1!==settings.cors){const corsOptions=!0===settings.cors?{}:settings.cors;app.use(corsLib(corsOptions))}const push=async(req,res)=>{try{if("OPTIONS"===req.method)return setCorsHeaders(res,settings.cors),void res.status(204).send();const respond=createRespond(options=>{const status=options.status??200;if(options.headers)for(const[key,value]of Object.entries(options.headers))res.set(key,value);res.status(status);const body=options.body;"string"==typeof body||Buffer.isBuffer(body)?res.send(body):body instanceof Uint8Array?res.send(Buffer.from(body)):res.json(body)});await context.withScope(req,respond,async env2=>{if("GET"===req.method){const parsedData=requestToData(req.url)
|
|
1
|
+
import express from"express";import cors from"cors";import{requestToData,createRespond}from"@walkeros/core";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"),toError=value=>value instanceof Error?value:new Error(String(value)),sourceExpress=async context=>{const{config:config={},env:env}=context,expressLib=env.express??express,corsLib=env.cors??cors,userSettings=config.settings||{},settings={...userSettings,cors:userSettings.cors??!0,paths:userSettings.paths??(userSettings.path?[userSettings.path]:["/collect"])},respondFirst=config.async??!0,app=expressLib();if(app.use(expressLib.json({limit:"1mb",type:["application/json","text/plain"]})),!1!==settings.cors){const corsOptions=!0===settings.cors?{}:settings.cors;app.use(corsLib(corsOptions))}const push=async(req,res)=>{try{if("OPTIONS"===req.method)return setCorsHeaders(res,settings.cors),void res.status(204).send();const respond=createRespond(options=>{const status=options.status??200;if(options.headers)for(const[key,value]of Object.entries(options.headers))res.set(key,value);res.status(status);const body=options.body;"string"==typeof body||Buffer.isBuffer(body)?res.send(body):body instanceof Uint8Array?res.send(Buffer.from(body)):res.json(body)});await context.withScope(req,respond,async env2=>{if("GET"===req.method){const parsedData=requestToData(req.url),respondGif=()=>respond({body:TRANSPARENT_GIF,headers:{"Content-Type":"image/gif","Cache-Control":"no-cache, no-store, must-revalidate"}});return void(parsedData&&"object"==typeof parsedData?respondFirst?(respondGif(),env2.push(parsedData).catch(err=>{env2.logger.error(toError(err))})):(await env2.push(parsedData),respondGif()):respondGif())}if("POST"===req.method){const eventData=req.body&&"object"==typeof req.body?req.body:{};return void(respondFirst?(respond({body:{success:!0,timestamp:Date.now()}}),env2.push(eventData).catch(err=>{env2.logger.error(toError(err))})):(await env2.push(eventData),respond({body:{success:!0,timestamp:Date.now()}})))}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"})}},resolvedPaths=settings.paths.map(entry=>"string"==typeof entry?{path:entry,methods:["GET","POST"]}:{path:entry.path,methods:entry.methods||["GET","POST"]});for(const route of resolvedPaths)route.methods.includes("POST")&&app.post(route.path,push),route.methods.includes("GET")&&app.get(route.path,push),app.options(route.path,push);let server;void 0!==settings.port&&(server=app.listen(settings.port,()=>{const routeLines=resolvedPaths.map(r=>` ${[...r.methods,"OPTIONS"].join(", ")} ${r.path}`).join("\n");env.logger.info(`Express source listening on port ${settings.port}\n`+routeLines)}));return{type:"express",config:{...config,settings:settings},push:push,httpHandler:app,app:app,server:server,destroy:_context=>new Promise((resolve,reject)=>{if(!server)return resolve();server.close(err=>err?reject(err):resolve())})}},index_default=sourceExpress;export{TRANSPARENT_GIF,index_default as default,setCorsHeaders,sourceExpress};//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/utils.ts"],"sourcesContent":["import express, { type Request, type Response } from 'express';\nimport cors from 'cors';\nimport { requestToData, createRespond } from '@walkeros/core';\nimport type { Source } from '@walkeros/core';\nimport type { ExpressSource, Types, EventRequest } from './types';\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 * - Provides destroy() for graceful shutdown (called by runner)\n *\n * @param context Source context with config, env, logger, id\n * @returns Express source instance with app and push handler\n */\nexport const sourceExpress = async (\n context: Source.Context<Types>,\n): Promise<ExpressSource> => {\n const { config = {}, env } = context;\n const expressLib = env.express ?? express;\n const corsLib = env.cors ?? cors;\n\n // Apply defaults (no runtime validation — flow.json is developer-controlled).\n const userSettings = config.settings || {};\n const settings = {\n ...userSettings,\n cors: userSettings.cors ?? true,\n paths:\n userSettings.paths ??\n (userSettings.path ? [userSettings.path] : ['/collect']),\n };\n\n const app = expressLib();\n\n // Body parsing — JSON content-type plus text/plain so navigator.sendBeacon\n // payloads (which the browser forces to text/plain;charset=UTF-8) are also\n // parsed as JSON. 1mb default limit.\n app.use(\n expressLib.json({\n limit: '1mb',\n type: ['application/json', 'text/plain'],\n }),\n );\n\n // CORS middleware (enabled by default)\n if (settings.cors !== false) {\n const corsOptions = settings.cors === true ? {} : settings.cors;\n app.use(corsLib(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 * Each inbound request gets its own `withScope` invocation. The per-scope\n * env carries this request's `ingest` and `respond` end to end, so\n * concurrent requests never crosstalk through source-factory state.\n */\n const push = async (req: Request, res: Response): Promise<void> => {\n try {\n // Handle OPTIONS for CORS preflight (no scope needed: no event, no ingest)\n if (req.method === 'OPTIONS') {\n setCorsHeaders(res, settings.cors);\n res.status(204).send();\n return;\n }\n\n // Create per-request respond — first call wins (idempotent)\n const respond = createRespond((options) => {\n const status = options.status ?? 200;\n if (options.headers) {\n for (const [key, value] of Object.entries(options.headers)) {\n res.set(key, value);\n }\n }\n res.status(status);\n const body = options.body;\n if (typeof body === 'string' || Buffer.isBuffer(body)) {\n res.send(body);\n } else if (body instanceof Uint8Array) {\n // A decoded cache value surfaces binary as a plain Uint8Array,\n // not a Node Buffer; send it as bytes (res.json would corrupt it).\n res.send(Buffer.from(body));\n } else {\n res.json(body);\n }\n });\n\n await context.withScope(req, respond, async (env) => {\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 // Default: 1x1 GIF (skipped if a step already called respond)\n respond({\n body: TRANSPARENT_GIF,\n headers: {\n 'Content-Type': 'image/gif',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n });\n return;\n }\n\n // Handle POST requests (standard event ingestion)\n if (req.method === 'POST') {\n const eventData =\n req.body && typeof req.body === 'object' ? req.body : {};\n\n await env.push(eventData);\n\n respond({ body: { success: true, timestamp: Date.now() } });\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 });\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 handlers per route config\n const resolvedPaths = settings.paths.map((entry) =>\n typeof entry === 'string'\n ? { path: entry, methods: ['GET', 'POST'] as const }\n : {\n path: entry.path,\n methods: entry.methods || (['GET', 'POST'] as const),\n },\n );\n\n for (const route of resolvedPaths) {\n if (route.methods.includes('POST')) app.post(route.path, push);\n if (route.methods.includes('GET')) app.get(route.path, push);\n app.options(route.path, push); // Always register OPTIONS for CORS\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 routeLines = resolvedPaths\n .map((r) => {\n const methods = [...r.methods, 'OPTIONS'].join(', ');\n return ` ${methods} ${r.path}`;\n })\n .join('\\n');\n env.logger.info(\n `Express source listening on port ${settings.port}\\n` + routeLines,\n );\n });\n }\n\n const instance: ExpressSource = {\n type: 'express',\n config: {\n ...config,\n settings,\n },\n push,\n httpHandler: app,\n app,\n server,\n destroy: (_context) =>\n new Promise<void>((resolve, reject) => {\n if (!server) return resolve();\n server.close((err) => (err ? reject(err) : resolve()));\n }),\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 RouteConfig,\n RouteMethod,\n} from './types';\n\n// Export utils\nexport { setCorsHeaders, TRANSPARENT_GIF } from './utils';\n\nexport default sourceExpress;\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":["env"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/utils.ts"],"sourcesContent":["import express, { type Request, type Response } from 'express';\nimport cors from 'cors';\nimport { requestToData, createRespond } from '@walkeros/core';\nimport type { Source } from '@walkeros/core';\nimport type { ExpressSource, Types, EventRequest } from './types';\nimport { setCorsHeaders, TRANSPARENT_GIF } from './utils';\n\n/**\n * Normalize an unknown rejection reason into an Error for the logger.\n * A fire-and-forget push can reject with any value; the logger accepts\n * `string | Error`, so non-Error reasons are wrapped.\n */\nconst toError = (value: unknown): Error =>\n value instanceof Error ? value : new Error(String(value));\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 * - Provides destroy() for graceful shutdown (called by runner)\n *\n * @param context Source context with config, env, logger, id\n * @returns Express source instance with app and push handler\n */\nexport const sourceExpress = async (\n context: Source.Context<Types>,\n): Promise<ExpressSource> => {\n const { config = {}, env } = context;\n const expressLib = env.express ?? express;\n const corsLib = env.cors ?? cors;\n\n // Apply defaults (no runtime validation — flow.json is developer-controlled).\n const userSettings = config.settings || {};\n const settings = {\n ...userSettings,\n cors: userSettings.cors ?? true,\n paths:\n userSettings.paths ??\n (userSettings.path ? [userSettings.path] : ['/collect']),\n };\n\n // Respond-first by default: a 2xx means \"accepted\", not \"delivered\".\n // Standardized on the source config (Source.Config.async), not settings.\n const respondFirst = config.async ?? true;\n\n const app = expressLib();\n\n // Body parsing — JSON content-type plus text/plain so navigator.sendBeacon\n // payloads (which the browser forces to text/plain;charset=UTF-8) are also\n // parsed as JSON. 1mb default limit.\n app.use(\n expressLib.json({\n limit: '1mb',\n type: ['application/json', 'text/plain'],\n }),\n );\n\n // CORS middleware (enabled by default)\n if (settings.cors !== false) {\n const corsOptions = settings.cors === true ? {} : settings.cors;\n app.use(corsLib(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 * Each inbound request gets its own `withScope` invocation. The per-scope\n * env carries this request's `ingest` and `respond` end to end, so\n * concurrent requests never crosstalk through source-factory state.\n */\n const push = async (req: Request, res: Response): Promise<void> => {\n try {\n // Handle OPTIONS for CORS preflight (no scope needed: no event, no ingest)\n if (req.method === 'OPTIONS') {\n setCorsHeaders(res, settings.cors);\n res.status(204).send();\n return;\n }\n\n // Create per-request respond — first call wins (idempotent)\n const respond = createRespond((options) => {\n const status = options.status ?? 200;\n if (options.headers) {\n for (const [key, value] of Object.entries(options.headers)) {\n res.set(key, value);\n }\n }\n res.status(status);\n const body = options.body;\n if (typeof body === 'string' || Buffer.isBuffer(body)) {\n res.send(body);\n } else if (body instanceof Uint8Array) {\n // A decoded cache value surfaces binary as a plain Uint8Array,\n // not a Node Buffer; send it as bytes (res.json would corrupt it).\n res.send(Buffer.from(body));\n } else {\n res.json(body);\n }\n });\n\n await context.withScope(req, respond, async (env) => {\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 // Default GIF body (idempotent fallback; skipped if a step already\n // called respond, e.g. a cache/asset destination serving real bytes).\n const respondGif = () =>\n respond({\n body: TRANSPARENT_GIF,\n headers: {\n 'Content-Type': 'image/gif',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n });\n\n if (parsedData && typeof parsedData === 'object') {\n if (respondFirst) {\n // Respond-first: the tracking pixel must return instantly and\n // never block on backend delivery. Fire the push without\n // awaiting; a rejected push is logged (destination errors are\n // DLQ'd inside the collector). A 2xx means \"accepted\", not\n // \"delivered\".\n respondGif();\n env.push(parsedData).catch((err: unknown) => {\n env.logger.error(toError(err));\n });\n } else {\n // Synchronous: await the push so a step (e.g. a cache/asset\n // destination) can respond with real content before the GIF\n // fallback applies.\n await env.push(parsedData);\n respondGif();\n }\n } else {\n respondGif();\n }\n return;\n }\n\n // Handle POST requests (standard event ingestion)\n if (req.method === 'POST') {\n const eventData =\n req.body && typeof req.body === 'object' ? req.body : {};\n\n if (respondFirst) {\n // Respond-first (\"accepted\"), then deliver asynchronously. A\n // rejected push is logged, not surfaced to the client and not left\n // unhandled (destination errors are DLQ'd inside the collector).\n respond({ body: { success: true, timestamp: Date.now() } });\n env.push(eventData).catch((err: unknown) => {\n env.logger.error(toError(err));\n });\n } else {\n // Synchronous ack: wait for delivery to settle before responding.\n await env.push(eventData);\n respond({ body: { success: true, 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 });\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 handlers per route config\n const resolvedPaths = settings.paths.map((entry) =>\n typeof entry === 'string'\n ? { path: entry, methods: ['GET', 'POST'] as const }\n : {\n path: entry.path,\n methods: entry.methods || (['GET', 'POST'] as const),\n },\n );\n\n for (const route of resolvedPaths) {\n if (route.methods.includes('POST')) app.post(route.path, push);\n if (route.methods.includes('GET')) app.get(route.path, push);\n app.options(route.path, push); // Always register OPTIONS for CORS\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 routeLines = resolvedPaths\n .map((r) => {\n const methods = [...r.methods, 'OPTIONS'].join(', ');\n return ` ${methods} ${r.path}`;\n })\n .join('\\n');\n env.logger.info(\n `Express source listening on port ${settings.port}\\n` + routeLines,\n );\n });\n }\n\n const instance: ExpressSource = {\n type: 'express',\n config: {\n ...config,\n settings,\n },\n push,\n httpHandler: app,\n app,\n server,\n destroy: (_context) =>\n new Promise<void>((resolve, reject) => {\n if (!server) return resolve();\n server.close((err) => (err ? reject(err) : resolve()));\n }),\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 RouteConfig,\n RouteMethod,\n} from './types';\n\n// Export utils\nexport { setCorsHeaders, TRANSPARENT_GIF } from './utils';\n\nexport default sourceExpress;\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":["env"]}
|
package/dist/walkerOS.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walkeros/server-source-express",
|
|
3
3
|
"description": "Express server source for walkerOS",
|
|
4
|
-
"version": "4.2.1-next-
|
|
4
|
+
"version": "4.2.1-next-1781682752679",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.mjs",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"update": "npx npm-check-updates -u && npm update"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@walkeros/collector": "4.2.1-next-
|
|
24
|
-
"@walkeros/core": "4.2.1-next-
|
|
23
|
+
"@walkeros/collector": "4.2.1-next-1781682752679",
|
|
24
|
+
"@walkeros/core": "4.2.1-next-1781682752679",
|
|
25
25
|
"express": "^5.2.1",
|
|
26
26
|
"cors": "^2.8.5"
|
|
27
27
|
},
|