@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.
- package/bin/lib/schemas/action.schema.d.ts +3423 -1190
- package/bin/lib/schemas/benefit-definition.schema.d.ts +94 -2
- package/bin/src/cli/init.js +224 -7
- package/bin/src/cli/init.js.map +1 -1
- package/bin/src/cli/templates/bensdk-base/.benSDKIgnore.template +3 -2
- package/bin/src/cli/templates/bensdk-base/.gitignore.template +4 -1
- package/bin/src/cli/templates/bensdk-base/README.md.template +52 -74
- package/bin/src/cli/templates/bensdk-base/context.config.ts.template +97 -21
- package/bin/src/cli/templates/bensdk-base/dev-test.example.json +22 -0
- package/bin/src/cli/templates/bensdk-base/docs/DEV_SERVER.md +201 -0
- package/bin/src/cli/templates/bensdk-docs/update-readme.ts +96 -0
- package/bin/src/cli/templates/bensdk-local-server/app.ts +234 -33
- package/bin/src/cli/templates/package.template.json +4 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
}
|