elpaypg 0.0.0-stage → 2.0.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.
Files changed (90) hide show
  1. package/.env.example +8 -0
  2. package/README.md +176 -2
  3. package/package.json +21 -4
  4. package/public/elpay-checkout.js +769 -0
  5. package/public/elpay-sms-gateway.apk +0 -0
  6. package/public/elpay-sms-gateway.apk.sha256 +1 -0
  7. package/public/elpay.js +785 -0
  8. package/public/fonts/dm-sans.woff2 +0 -0
  9. package/public/fonts/sora.woff2 +0 -0
  10. package/public/views/checkout.html +2839 -0
  11. package/public/views/partials/auth.html +110 -0
  12. package/public/views/partials/footer.html +54 -0
  13. package/public/views/partials/header.html +101 -0
  14. package/public/views/partials/modals.html +179 -0
  15. package/public/views/partials/payment-methods.html +526 -0
  16. package/public/views/partials/sidebar.html +75 -0
  17. package/public/views/partials/splash.html +35 -0
  18. package/src/__tests__/smoke.test.js +8 -0
  19. package/src/auth-guard.js +95 -0
  20. package/src/index.js +137 -0
  21. package/src/router.js +46 -0
  22. package/src/routes/admin.routes.js +2421 -0
  23. package/src/routes/checkout.routes.js +2000 -0
  24. package/src/routes/customer.routes.js +650 -0
  25. package/src/routes/device.routes.js +303 -0
  26. package/src/routes/payments.routes.js +446 -0
  27. package/src/routes/pollers.routes.js +653 -0
  28. package/src/services/observer-poller.service.js +369 -0
  29. package/src/services/tunnel-manager.js +130 -0
  30. package/src/services/whatsapp-berry.service.js +743 -0
  31. package/src/vendor/common/__tests__/common.test.js +297 -0
  32. package/src/vendor/common/__tests__/common.test.ts +160 -0
  33. package/src/vendor/common/contracts.js +64 -0
  34. package/src/vendor/common/contracts.ts +188 -0
  35. package/src/vendor/common/crypto.js +132 -0
  36. package/src/vendor/common/crypto.ts +112 -0
  37. package/src/vendor/common/enums.js +91 -0
  38. package/src/vendor/common/enums.ts +91 -0
  39. package/src/vendor/common/index.js +12 -0
  40. package/src/vendor/common/index.ts +8 -0
  41. package/src/vendor/common/money.js +62 -0
  42. package/src/vendor/common/money.ts +56 -0
  43. package/src/vendor/common/observation.js +27 -0
  44. package/src/vendor/common/observation.ts +52 -0
  45. package/src/vendor/common/runtime-config.js +246 -0
  46. package/src/vendor/common/secrets.js +374 -0
  47. package/src/vendor/common/state-machine.d.ts +1 -0
  48. package/src/vendor/common/state-machine.js +1 -0
  49. package/src/vendor/common/state-transitions.js +85 -0
  50. package/src/vendor/common/tunnel-command.js +59 -0
  51. package/src/vendor/common/upi-providers.js +389 -0
  52. package/src/vendor/common/upi-providers.ts +400 -0
  53. package/src/vendor/common/upi.js +69 -0
  54. package/src/vendor/common/upi.ts +61 -0
  55. package/src/vendor/core/__tests__/core.test.js +683 -0
  56. package/src/vendor/core/__tests__/queue-timing.test.js +237 -0
  57. package/src/vendor/core/__tests__/webhook-engine.test.js +243 -0
  58. package/src/vendor/core/attribution-engine.js +855 -0
  59. package/src/vendor/core/bank-email-parser.js +105 -0
  60. package/src/vendor/core/bank-sms-parser.js +183 -0
  61. package/src/vendor/core/device-security.js +506 -0
  62. package/src/vendor/core/index.js +13 -0
  63. package/src/vendor/core/normalizer.js +165 -0
  64. package/src/vendor/core/qr-lib.js +8 -0
  65. package/src/vendor/core/qr.js +62 -0
  66. package/src/vendor/core/queue-engine.js +153 -0
  67. package/src/vendor/core/recovery-engine.js +159 -0
  68. package/src/vendor/core/state-machine.js +42 -0
  69. package/src/vendor/core/timing-engine.js +272 -0
  70. package/src/vendor/core/verification-pipeline.js +1164 -0
  71. package/src/vendor/core/webhook-engine.js +280 -0
  72. package/src/vendor/db/__tests__/auth-matrix.test.js +564 -0
  73. package/src/vendor/db/__tests__/customer-reset.test.js +95 -0
  74. package/src/vendor/db/__tests__/db.test.js +265 -0
  75. package/src/vendor/db/__tests__/schema-parity.test.js +137 -0
  76. package/src/vendor/db/__tests__/sqlite_verify.test.js +15 -0
  77. package/src/vendor/db/database.js +181 -0
  78. package/src/vendor/db/index.js +12 -0
  79. package/src/vendor/db/migrate.js +210 -0
  80. package/src/vendor/db/repositories/audit-log.repository.js +68 -0
  81. package/src/vendor/db/repositories/customer-session.repository.js +181 -0
  82. package/src/vendor/db/repositories/customer.repository.js +636 -0
  83. package/src/vendor/db/repositories/device.repository.js +285 -0
  84. package/src/vendor/db/repositories/merchant.repository.js +158 -0
  85. package/src/vendor/db/repositories/observation.repository.js +124 -0
  86. package/src/vendor/db/repositories/payment-session.repository.js +618 -0
  87. package/src/vendor/db/repositories/receiving-account.repository.js +244 -0
  88. package/src/vendor/db/repositories/support-ticket.repository.js +140 -0
  89. package/src/vendor/db/schema.sql +266 -0
  90. package/src/vendor/db/sqlite-schema.sql +270 -0
@@ -0,0 +1,303 @@
1
+ import { Router } from 'express';
2
+ import {
3
+ generateFingerprintHash,
4
+ normalizeDeviceSignals,
5
+ verifyDeviceSignature,
6
+ createDeviceChallenge,
7
+ consumeDeviceChallenge
8
+ } from '../vendor/core/index.js';
9
+
10
+ /**
11
+ * Resolves a verified customer ID strictly from a valid customer session token.
12
+ */
13
+ function resolveAuthenticatedCustomerId(req, body, ctx) {
14
+ if (!ctx.customerSessionRepo) return null;
15
+ let token = body?.token || req.headers['x-customer-token'] || null;
16
+ if (!token) {
17
+ const authHeader = req.headers['authorization'] || '';
18
+ if (authHeader.startsWith('Bearer ')) {
19
+ token = authHeader.slice(7).trim();
20
+ }
21
+ }
22
+ if (!token || typeof token !== 'string') return null;
23
+ const authCtx = ctx.customerSessionRepo.findByToken(token.trim());
24
+ return authCtx?.customer?.id || null;
25
+ }
26
+
27
+ /**
28
+ * Registers device identity and cryptographic binding routes.
29
+ * @param {HttpServer} server
30
+ * @param {{
31
+ * deviceRepo: import('../vendor/db/index.js').DeviceRepository,
32
+ * customerRepo: import('../vendor/db/index.js').CustomerRepository,
33
+ * customerSessionRepo?: import('../vendor/db/index.js').CustomerSessionRepository,
34
+ * auditRepo?: import('../vendor/db/index.js').AuditLogRepository
35
+ * }} ctx
36
+ */
37
+ export function registerDeviceRoutes(router, ctx) {
38
+ // 0. POST /api/v1/devices/challenge
39
+ router.post('/api/v1/devices/challenge', async (req, res) => {
40
+ const body = req.body || {};
41
+ const { publicKey, deviceId } = body;
42
+
43
+ if (!publicKey && !deviceId) {
44
+ res.status(400).json({ error: 'publicKey or deviceId is required' });
45
+ return;
46
+ }
47
+
48
+ const existing = publicKey
49
+ ? ctx.deviceRepo.findByPublicKey(String(publicKey).trim())
50
+ : ctx.deviceRepo.findById(String(deviceId).trim());
51
+
52
+ if (!existing || existing.status !== 'ACTIVE') {
53
+ res.status(404).json({ error: 'Device not found or inactive' });
54
+ return;
55
+ }
56
+
57
+ const challenge = createDeviceChallenge(existing.id);
58
+ res.status(200).json({
59
+ success: true,
60
+ deviceId: existing.id,
61
+ challenge
62
+ });
63
+ });
64
+
65
+ // 1. POST /api/v1/devices/register
66
+ router.post('/api/v1/devices/register', async (req, res) => {
67
+ const body = req.body || {};
68
+ const { publicKey, signals, challenge, signature, payload } = body;
69
+
70
+ if (!publicKey || typeof publicKey !== 'string' || !publicKey.trim()) {
71
+ res.status(400).json({ error: 'Device public key is required for registration.' });
72
+ return;
73
+ }
74
+
75
+ const trimmedPublicKey = publicKey.trim();
76
+ if (trimmedPublicKey.startsWith('adhoc_')) {
77
+ res.status(400).json({ error: 'Invalid device public key format.' });
78
+ return;
79
+ }
80
+
81
+ // Resolve or generate fingerprint hash
82
+ const clientSignals = signals || {};
83
+ if (!clientSignals.userAgent && req.headers['user-agent']) {
84
+ clientSignals.userAgent = req.headers['user-agent'];
85
+ }
86
+ if (!clientSignals.language && req.headers['accept-language']) {
87
+ clientSignals.language = req.headers['accept-language'].split(',')[0];
88
+ }
89
+
90
+ const fingerprintHash = body.fingerprintHash || generateFingerprintHash(clientSignals);
91
+ const normalized = normalizeDeviceSignals(clientSignals);
92
+
93
+ // Optional customer association strictly from verified customer session token (never bare customerId)
94
+ const customerId = resolveAuthenticatedCustomerId(req, body, ctx);
95
+
96
+ // Check if device with this exact public key already exists
97
+ const existing = ctx.deviceRepo.findByPublicKey(trimmedPublicKey);
98
+ if (existing) {
99
+ if (existing.status !== 'ACTIVE') {
100
+ res.status(403).json({ error: 'DEVICE_REVOKED', message: 'Device has been revoked or deactivated.' });
101
+ return;
102
+ }
103
+
104
+ // Never return or rotate a deviceToken for an existing publicKey unless the caller
105
+ // proves possession of the private key by signing a valid server-issued challenge.
106
+ const hasChallengeProof =
107
+ typeof challenge === 'string' &&
108
+ challenge.trim().length > 0 &&
109
+ typeof signature === 'string' &&
110
+ signature.trim().length > 0 &&
111
+ payload !== undefined &&
112
+ payload !== null;
113
+
114
+ if (!hasChallengeProof) {
115
+ const freshChallenge = createDeviceChallenge(existing.id);
116
+ res.status(409).json({
117
+ error: 'DEVICE_ALREADY_REGISTERED',
118
+ code: 'CHALLENGE_REQUIRED',
119
+ message: 'This public key is already registered. Sign the server challenge with your private key to rotate credentials.',
120
+ deviceId: existing.id,
121
+ challenge: freshChallenge,
122
+ isExisting: true
123
+ });
124
+ return;
125
+ }
126
+
127
+ const challengeOk = consumeDeviceChallenge(challenge.trim(), existing.id);
128
+ const sigOk = challengeOk && verifyDeviceSignature(
129
+ existing.publicKey,
130
+ payload,
131
+ signature,
132
+ { expectedDeviceId: existing.id, expectedChallenge: challenge.trim() }
133
+ );
134
+
135
+ if (!sigOk) {
136
+ res.status(401).json({
137
+ error: 'DEVICE_CHALLENGE_FAILED',
138
+ message: 'Failed to verify private-key ownership for existing device public key.'
139
+ });
140
+ return;
141
+ }
142
+
143
+ if (customerId && !existing.customerId) {
144
+ ctx.deviceRepo.associateCustomer(existing.id, customerId);
145
+ }
146
+ ctx.deviceRepo.touch(existing.id);
147
+ const rotatedToken = ctx.deviceRepo.rotateToken(existing.id);
148
+
149
+ res.status(200).json({
150
+ success: true,
151
+ deviceId: existing.id,
152
+ deviceToken: rotatedToken,
153
+ fingerprintHash: existing.fingerprintHash,
154
+ status: existing.status,
155
+ isExisting: true
156
+ });
157
+ return;
158
+ }
159
+
160
+ // Register new device
161
+ const device = ctx.deviceRepo.create({
162
+ customerId,
163
+ publicKey: trimmedPublicKey,
164
+ fingerprintHash,
165
+ userAgent: normalized.userAgent,
166
+ platform: normalized.platform,
167
+ browser: normalized.webglRenderer ? 'WebBrowser' : null,
168
+ timezone: normalized.timezone,
169
+ language: normalized.language,
170
+ screenSignature: normalized.screenSignature
171
+ });
172
+
173
+ if (ctx.auditRepo) {
174
+ ctx.auditRepo.log({
175
+ entityType: 'DEVICE',
176
+ entityId: device.id,
177
+ action: 'DEVICE_REGISTERED',
178
+ actorType: customerId ? 'CUSTOMER' : 'SYSTEM',
179
+ actorId: customerId || 'ANONYMOUS',
180
+ details: {
181
+ fingerprintHash: device.fingerprintHash,
182
+ platform: normalized.platform,
183
+ screenSignature: normalized.screenSignature
184
+ }
185
+ });
186
+ }
187
+
188
+ res.status(201).json({
189
+ success: true,
190
+ deviceId: device.id,
191
+ deviceToken: device.deviceToken,
192
+ fingerprintHash: device.fingerprintHash,
193
+ status: device.status,
194
+ isExisting: false
195
+ });
196
+ });
197
+
198
+ // 2. POST /api/v1/devices/verify
199
+ router.post('/api/v1/devices/verify', async (req, res) => {
200
+ const body = req.body || {};
201
+ const { deviceId, payload, signature, deviceToken } = body;
202
+
203
+ if (!deviceId || typeof deviceId !== 'string') {
204
+ res.status(400).json({ valid: false, error: 'deviceId is required' });
205
+ return;
206
+ }
207
+
208
+ const device = ctx.deviceRepo.findById(deviceId);
209
+ if (!device || device.status !== 'ACTIVE' || (typeof device.publicKey === 'string' && device.publicKey.startsWith('adhoc_'))) {
210
+ res.status(404).json({ valid: false, error: 'Device not found or inactive' });
211
+ return;
212
+ }
213
+
214
+ const hasToken = typeof deviceToken === 'string' && deviceToken.trim().length > 0;
215
+ const hasSig = typeof signature === 'string' && signature.trim().length > 0;
216
+ const hasPayload = payload !== undefined && payload !== null && String(payload).trim().length > 0;
217
+
218
+ // Require at least one cryptographic credential; never default to valid: true!
219
+ if (!hasToken && !hasSig) {
220
+ res.status(401).json({
221
+ valid: false,
222
+ error: 'DEVICE_CREDENTIALS_REQUIRED',
223
+ message: 'A deviceToken or cryptographic signature with payload is required.',
224
+ deviceId: device.id,
225
+ tokenValid: false,
226
+ signatureValid: false
227
+ });
228
+ return;
229
+ }
230
+
231
+ if ((hasSig && !hasPayload) || (!hasSig && hasPayload)) {
232
+ res.status(401).json({
233
+ valid: false,
234
+ error: 'DEVICE_SIGNATURE_INVALID',
235
+ message: 'Both signature and payload are required for signature verification.',
236
+ deviceId: device.id,
237
+ tokenValid: false,
238
+ signatureValid: false
239
+ });
240
+ return;
241
+ }
242
+
243
+ const tokenValid = hasToken ? ctx.deviceRepo.verifyToken(device.id, deviceToken.trim()) : true;
244
+ const signatureValid = hasSig
245
+ ? verifyDeviceSignature(device.publicKey, payload, signature, { expectedDeviceId: device.id })
246
+ : true;
247
+
248
+ const isValid = tokenValid && signatureValid;
249
+
250
+ res.status(isValid ? 200 : 401).json({
251
+ valid: isValid,
252
+ deviceId: device.id,
253
+ customerId: device.customerId,
254
+ tokenValid: hasToken ? tokenValid : false,
255
+ signatureValid: hasSig ? signatureValid : false
256
+ });
257
+ });
258
+
259
+ // 3. GET /api/v1/devices/:id
260
+ router.get('/api/v1/devices/:id', async (req, res) => {
261
+ const device = ctx.deviceRepo.findById(req.params.id);
262
+ if (!device) {
263
+ res.status(404).json({ error: 'Device not found' });
264
+ return;
265
+ }
266
+
267
+ res.status(200).json({
268
+ id: device.id,
269
+ customerId: device.customerId,
270
+ fingerprintHash: device.fingerprintHash,
271
+ platform: device.platform,
272
+ timezone: device.timezone,
273
+ language: device.language,
274
+ status: device.status,
275
+ createdAt: device.createdAt,
276
+ lastSeenAt: device.lastSeenAt
277
+ });
278
+ });
279
+
280
+ // 4. GET /api/v1/devices/customer/:customerId
281
+ router.get('/api/v1/devices/customer/:customerId', async (req, res) => {
282
+ const devices = ctx.deviceRepo.findByCustomerId(req.params.customerId);
283
+ res.status(200).json({
284
+ customerId: req.params.customerId,
285
+ count: devices.length,
286
+ devices: devices.map(d => ({
287
+ id: d.id,
288
+ fingerprintHash: d.fingerprintHash,
289
+ platform: d.platform,
290
+ timezone: d.timezone,
291
+ status: d.status,
292
+ createdAt: d.createdAt,
293
+ lastSeenAt: d.lastSeenAt
294
+ }))
295
+ });
296
+ });
297
+ }
298
+
299
+
300
+
301
+
302
+
303
+