@benup/bensdk 1.11.16 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,80 +1,278 @@
1
+ // @ts-nocheck
1
2
  import { serve } from '@hono/node-server';
2
3
  import { Hono } from 'hono';
3
4
  import { cors } from 'hono/cors';
5
+ import { existsSync, readFileSync } from 'fs';
6
+ import { join } from 'path';
4
7
  import pino from 'pino';
5
8
  import { PassThrough } from 'stream';
6
- import { WebSocketServer } from 'ws';
9
+ import { WebSocket, WebSocketServer } from 'ws';
7
10
  import benefitsDefinition from '../../src/benefit-definition';
8
11
 
12
+ // Disable TLS certificate validation in non-production environments
13
+ // (e.g. self-signed certs used in zero-trust networks)
14
+ if (process.env['NODE_ENV'] !== 'production') {
15
+ process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
16
+ }
17
+
18
+ // logStream is used to forward raw pino JSON logs to WebSocket connections (viewer)
9
19
  const logStream = new PassThrough();
10
- const logger = pino(logStream);
20
+
21
+ const logger = pino(
22
+ {
23
+ level: 'info',
24
+ transport: {
25
+ target: 'pino-pretty',
26
+ options: { colorize: true }
27
+ }
28
+ }
29
+ );
30
+
11
31
  const app = new Hono();
12
32
  const connections = new Set<WebSocket>();
13
- const onLog = (chunk: Buffer) => {
33
+
34
+ // Forward raw log data to WebSocket connections (used by the viewer)
35
+ logStream.on('data', (chunk: Buffer) => {
14
36
  connections.forEach((ws) => {
15
37
  if (ws.readyState === WebSocket.OPEN) {
16
38
  ws.send(chunk.toString());
17
39
  }
18
40
  });
19
- };
20
-
21
- logStream.on('data', onLog);
41
+ });
22
42
 
23
43
  app.use(cors());
24
44
 
25
- app.get('/state-machine', async (c) => {
26
- const response = benefitsDefinition.stateMachine;
45
+ /**
46
+ * dev-test.json structure:
47
+ * {
48
+ * "credentials": { ...your real credentials here },
49
+ * "eligibilityOptions": {
50
+ * "GRANT": { ...options },
51
+ * "RECHARGE": { ...options },
52
+ * "GRANT_DEPENDENT": { ...options }
53
+ * }
54
+ * }
55
+ *
56
+ * Copy dev-test.example.json to dev-test.json and fill in your real values.
57
+ * dev-test.json is gitignored — never commit real credentials.
58
+ */
59
+ const devTestPath = join(process.cwd(), 'dev-test.json');
60
+ let devTestConfig: { credentials?: Record<string, unknown>; eligibilityOptions?: Record<string, unknown> } = {};
27
61
 
28
- return c.json(response, 200);
62
+ if (existsSync(devTestPath)) {
63
+ devTestConfig = JSON.parse(readFileSync(devTestPath, 'utf-8'));
64
+ logger.info('[benSDK] Loaded dev-test.json');
65
+ } else {
66
+ logger.warn('[benSDK] dev-test.json not found — create it from dev-test.example.json to enable real credentials and eligibilityOptions presets');
67
+ }
68
+
69
+ // Persistent in-memory token cache (survives between requests within the same server session)
70
+ const tokenCache = new Map<string, string>();
71
+ const tokenHelper = {
72
+ get: async ({ accountID, companyID, benefitID }: { accountID: string; companyID: string; benefitID: string }) => {
73
+ const key = `${accountID}:${companyID}:${benefitID}`;
74
+ return tokenCache.get(key) ?? null;
75
+ },
76
+ set: async ({
77
+ accountID,
78
+ companyID,
79
+ benefitID,
80
+ valueToBeStored
81
+ }: {
82
+ accountID: string;
83
+ companyID: string;
84
+ benefitID: string;
85
+ valueToBeStored: string;
86
+ storeForSeconds?: number;
87
+ }) => {
88
+ const key = `${accountID}:${companyID}:${benefitID}`;
89
+ tokenCache.set(key, valueToBeStored);
90
+ }
91
+ };
92
+
93
+ app.get('/state-machine', async (c) => {
94
+ return c.json(benefitsDefinition.stateMachine, 200);
29
95
  });
30
96
 
31
- app.post('/run-action', async (c) => {
32
- const payload = await c.req.json();
33
- const { run, requestPayload, message, currentContext } = payload;
97
+ /**
98
+ * Find which action type (GRANT, REVOKE, RECHARGE, etc.) a handler state belongs to
99
+ * by looking up the state key in the stateMachine definition.
100
+ */
101
+ function getActionTypeForHandler(handlerName: string): string | null {
102
+ const stateKey = handlerName.replace(/-/g, '_').toUpperCase();
103
+ for (const [action, states] of Object.entries(benefitsDefinition.stateMachine)) {
104
+ if (stateKey in (states as Record<string, unknown>)) {
105
+ return action;
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+
111
+ const runAction = async (c) => {
112
+ let body: Record<string, unknown>;
113
+ try {
114
+ body = await c.req.json();
115
+ } catch {
116
+ return c.json({ error: 'Invalid JSON body' }, 400);
117
+ }
118
+
119
+ // Support both simplified format (payload, ctx) and legacy format (requestPayload, currentContext)
120
+ const run = body.run as string | undefined;
121
+ const requestPayload = (body.requestPayload ?? body.payload) as Record<string, unknown> | undefined;
122
+ const message = body.message ?? {};
123
+ const currentContext = (body.currentContext ?? body.ctx ?? {}) as Record<string, unknown>;
124
+
125
+ // Validate ctx against the schema defined in benefit-definition.ts
126
+ if (benefitsDefinition.actions?.ctx) {
127
+ const ctxValidation = benefitsDefinition.actions.ctx.safeParse(currentContext);
128
+ if (!ctxValidation.success) {
129
+ logger.warn(
130
+ `[benSDK] ctx failed schema validation: ${JSON.stringify(ctxValidation.error.format())}`
131
+ );
132
+ }
133
+ }
134
+
135
+ if (!run) {
136
+ return c.json(
137
+ { error: 'Missing required field: "run" (handler state name, e.g. "SYNC_EXISTING_GRANT")' },
138
+ 400
139
+ );
140
+ }
141
+
142
+ // Dynamically import the handler file
34
143
  const fileName = `${run.toLowerCase()}.handler.ts`;
35
144
  const fileDir = `../../src/handlers/${fileName}`;
36
- const handler = await import(fileDir);
145
+ let handler: { default: (...args: unknown[]) => Promise<unknown> };
146
+ try {
147
+ handler = await import(fileDir);
148
+ } catch {
149
+ return c.json(
150
+ { error: `Handler not found: ${fileName}`, hint: 'Make sure the file exists in src/handlers/' },
151
+ 404
152
+ );
153
+ }
154
+
155
+ // Authenticate with real credentials if auth is defined and credentials are available in dev-test.json
156
+ let authInstance;
157
+ if (benefitsDefinition.auth && devTestConfig.credentials) {
158
+ logger.info('[benSDK] Authenticating with credentials from dev-test.json...');
159
+ const authResult = await benefitsDefinition.auth.handler(devTestConfig.credentials, {
160
+ tokenHelper,
161
+ logger
162
+ });
37
163
 
38
- const { ctxMock } = (await import('../../context.config')).createStateHandlerCtxMock(
164
+ if (authResult.outcome === 'FAILED') {
165
+ logger.error(`[benSDK] Authentication failed: ${authResult.reason}`);
166
+ return c.json({ error: 'Authentication failed', reason: authResult.reason }, 401);
167
+ }
168
+
169
+ authInstance = authResult.instance;
170
+ logger.info('[benSDK] Authentication succeeded');
171
+ }
172
+
173
+ // Create context mock (disableMocks=true when we have a real authenticated instance)
174
+ const { ctxMock, apisMocks } = (await import('../../context.config')).createStateHandlerCtxMock(
39
175
  benefitsDefinition,
40
- logger
176
+ logger,
177
+ !!authInstance
41
178
  );
42
179
 
43
- logger.info(`Running: ${fileDir} to ${requestPayload.target.employee.name}`);
44
- logger.info(`Current context: ${JSON.stringify(currentContext)}`);
45
- logger.info(`Request Payload: ${JSON.stringify(requestPayload)}`);
180
+ // Inject the real authenticated axios instance into benefitPartnerAPI
181
+ if (authInstance) {
182
+ ctxMock.benefitPartnerAPI = authInstance;
183
+ // Apply any additional mocks from context.config to the authenticated instance
184
+ // (e.g. presigned URL mocks, specific endpoint stubs that don't need real calls)
185
+ if (apisMocks.benefitPartnerAPI && typeof apisMocks.benefitPartnerAPI === 'function') {
186
+ apisMocks.benefitPartnerAPI(authInstance);
187
+ }
188
+ }
189
+
190
+ // Determine which action type this handler belongs to (for eligibilityOptions lookup)
191
+ const actionType = getActionTypeForHandler(run);
192
+
193
+ // dev-test.json eligibilityOptions serve as defaults; request body overrides them
194
+ const devEligibilityOptions =
195
+ actionType && devTestConfig.eligibilityOptions?.[actionType]
196
+ ? devTestConfig.eligibilityOptions[actionType]
197
+ : {};
46
198
 
47
- const response = await handler.default(
48
- message,
49
- {
50
- ...requestPayload,
51
- ctx: currentContext
199
+ // Validate dev-test.json eligibilityOptions against the schema defined in benefit-definition.ts
200
+ if (actionType && devTestConfig.eligibilityOptions?.[actionType]) {
201
+ const schema = benefitsDefinition.actions?.eligibilityOptions?.[actionType];
202
+ if (schema) {
203
+ const validation = schema.safeParse(devTestConfig.eligibilityOptions[actionType]);
204
+ if (!validation.success) {
205
+ logger.warn(
206
+ `[benSDK] dev-test.json eligibilityOptions.${actionType} failed schema validation: ${JSON.stringify(validation.error.format())}`
207
+ );
208
+ }
209
+ }
210
+ }
211
+
212
+ const mergedPayload = {
213
+ ...requestPayload,
214
+ eligibilityOptions: {
215
+ ...devEligibilityOptions,
216
+ ...(requestPayload?.eligibilityOptions ?? {})
52
217
  },
53
- ctxMock
54
- );
55
- return c.json(response, 200);
56
- });
218
+ ctx: currentContext
219
+ };
220
+
221
+ logger.info(`[benSDK] Running handler: ${run}`);
222
+
223
+ try {
224
+ const response = await handler.default(message, mergedPayload, ctxMock);
225
+ logger.info(`[benSDK] Handler completed: ${run}`);
226
+ return c.json(response, 200);
227
+ } catch (error) {
228
+ logger.error(`[benSDK] Handler threw an exception: ${error?.message}`);
229
+ return c.json(
230
+ { error: 'Handler threw an exception', message: error?.message, stack: error?.stack },
231
+ 500
232
+ );
233
+ }
234
+ };
235
+
236
+ // Simplified endpoint: POST /
237
+ app.post('/', runAction);
238
+ // Legacy endpoint for backward compatibility: POST /run-action
239
+ app.post('/run-action', runAction);
57
240
 
58
241
  const wss = new WebSocketServer({ noServer: true });
59
242
  wss.on('connection', (ws) => {
60
243
  connections.add(ws);
61
244
 
62
245
  ws.on('message', (message) => {
63
- console.log('Message received:', message.toString());
246
+ logger.debug(`WebSocket message: ${message.toString()}`);
64
247
  });
65
248
 
66
249
  ws.on('close', () => {
67
- console.log('Close connection');
68
250
  connections.delete(ws);
69
251
  });
70
252
  });
71
253
 
72
254
  async function startServer() {
73
- return new Promise((res: any, rej: any) => {
74
- const server = serve({ fetch: app.fetch, port: 3000 });
255
+ return new Promise((res: () => void) => {
256
+ const server = serve({ fetch: app.fetch, port: 3000 }, () => {
257
+ logger.info('benSDK dev server running at http://localhost:3000');
258
+ logger.info(' POST / — Run a handler (e.g. { "run": "SYNC_EXISTING_GRANT", "payload": {}, "ctx": {} })');
259
+ logger.info(' POST /run-action — Run a handler (legacy format)');
260
+ logger.info(' GET /state-machine — Get the state machine definition');
261
+
262
+ if (devTestConfig.credentials) {
263
+ logger.info(' ✓ Real credentials loaded — benefitPartnerAPI will use real API calls');
264
+ } else {
265
+ logger.warn(' ⚠ No credentials found — benefitPartnerAPI will use mocked responses from context.config.ts');
266
+ }
267
+
268
+ if (devTestConfig.eligibilityOptions) {
269
+ const actions = Object.keys(devTestConfig.eligibilityOptions).join(', ');
270
+ logger.info(` ✓ EligibilityOptions presets loaded for: ${actions}`);
271
+ }
272
+ });
273
+
75
274
  server.on('upgrade', (request, socket, head) => {
76
- const { url } = request;
77
- if (url === '/ws') {
275
+ if (request.url === '/ws') {
78
276
  wss.handleUpgrade(request, socket, head, (ws) => {
79
277
  wss.emit('connection', ws, request);
80
278
  });
@@ -82,8 +280,11 @@ async function startServer() {
82
280
  socket.destroy();
83
281
  }
84
282
  });
283
+
85
284
  res();
86
285
  });
87
286
  }
88
287
 
288
+ startServer();
289
+
89
290
  export { app, startServer };
@@ -8,6 +8,8 @@
8
8
  "format": "npx prettier --write .",
9
9
  "lint": "npx tsc --noemit && npx prettier --check . && npx eslint .",
10
10
  "start": "cd bin/viewer && tsx startup.ts",
11
+ "dev:start": "tsx bin/server/app.ts",
12
+ "docs:update": "tsx bin/docs/update-readme.ts",
11
13
  "generate": "tsx bin/cli/generate.ts",
12
14
  "build": "tsc"
13
15
  },
@@ -48,6 +50,7 @@
48
50
  "svelte-check": "^4.0.0",
49
51
  "vite": "^6.2.5",
50
52
  "marked": "^15.0.10",
51
- "open": "^10.1.1"
53
+ "open": "^10.1.1",
54
+ "pino-pretty": "^13.0.0"
52
55
  }
53
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benup/bensdk",
3
- "version": "1.11.16",
3
+ "version": "1.12.0",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "scripts": {