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.
- package/.env.example +8 -0
- package/README.md +176 -2
- package/package.json +21 -4
- package/public/elpay-checkout.js +769 -0
- package/public/elpay-sms-gateway.apk +0 -0
- package/public/elpay-sms-gateway.apk.sha256 +1 -0
- package/public/elpay.js +785 -0
- package/public/fonts/dm-sans.woff2 +0 -0
- package/public/fonts/sora.woff2 +0 -0
- package/public/views/checkout.html +2839 -0
- package/public/views/partials/auth.html +110 -0
- package/public/views/partials/footer.html +54 -0
- package/public/views/partials/header.html +101 -0
- package/public/views/partials/modals.html +179 -0
- package/public/views/partials/payment-methods.html +526 -0
- package/public/views/partials/sidebar.html +75 -0
- package/public/views/partials/splash.html +35 -0
- package/src/__tests__/smoke.test.js +8 -0
- package/src/auth-guard.js +95 -0
- package/src/index.js +137 -0
- package/src/router.js +46 -0
- package/src/routes/admin.routes.js +2421 -0
- package/src/routes/checkout.routes.js +2000 -0
- package/src/routes/customer.routes.js +650 -0
- package/src/routes/device.routes.js +303 -0
- package/src/routes/payments.routes.js +446 -0
- package/src/routes/pollers.routes.js +653 -0
- package/src/services/observer-poller.service.js +369 -0
- package/src/services/tunnel-manager.js +130 -0
- package/src/services/whatsapp-berry.service.js +743 -0
- package/src/vendor/common/__tests__/common.test.js +297 -0
- package/src/vendor/common/__tests__/common.test.ts +160 -0
- package/src/vendor/common/contracts.js +64 -0
- package/src/vendor/common/contracts.ts +188 -0
- package/src/vendor/common/crypto.js +132 -0
- package/src/vendor/common/crypto.ts +112 -0
- package/src/vendor/common/enums.js +91 -0
- package/src/vendor/common/enums.ts +91 -0
- package/src/vendor/common/index.js +12 -0
- package/src/vendor/common/index.ts +8 -0
- package/src/vendor/common/money.js +62 -0
- package/src/vendor/common/money.ts +56 -0
- package/src/vendor/common/observation.js +27 -0
- package/src/vendor/common/observation.ts +52 -0
- package/src/vendor/common/runtime-config.js +246 -0
- package/src/vendor/common/secrets.js +374 -0
- package/src/vendor/common/state-machine.d.ts +1 -0
- package/src/vendor/common/state-machine.js +1 -0
- package/src/vendor/common/state-transitions.js +85 -0
- package/src/vendor/common/tunnel-command.js +59 -0
- package/src/vendor/common/upi-providers.js +389 -0
- package/src/vendor/common/upi-providers.ts +400 -0
- package/src/vendor/common/upi.js +69 -0
- package/src/vendor/common/upi.ts +61 -0
- package/src/vendor/core/__tests__/core.test.js +683 -0
- package/src/vendor/core/__tests__/queue-timing.test.js +237 -0
- package/src/vendor/core/__tests__/webhook-engine.test.js +243 -0
- package/src/vendor/core/attribution-engine.js +855 -0
- package/src/vendor/core/bank-email-parser.js +105 -0
- package/src/vendor/core/bank-sms-parser.js +183 -0
- package/src/vendor/core/device-security.js +506 -0
- package/src/vendor/core/index.js +13 -0
- package/src/vendor/core/normalizer.js +165 -0
- package/src/vendor/core/qr-lib.js +8 -0
- package/src/vendor/core/qr.js +62 -0
- package/src/vendor/core/queue-engine.js +153 -0
- package/src/vendor/core/recovery-engine.js +159 -0
- package/src/vendor/core/state-machine.js +42 -0
- package/src/vendor/core/timing-engine.js +272 -0
- package/src/vendor/core/verification-pipeline.js +1164 -0
- package/src/vendor/core/webhook-engine.js +280 -0
- package/src/vendor/db/__tests__/auth-matrix.test.js +564 -0
- package/src/vendor/db/__tests__/customer-reset.test.js +95 -0
- package/src/vendor/db/__tests__/db.test.js +265 -0
- package/src/vendor/db/__tests__/schema-parity.test.js +137 -0
- package/src/vendor/db/__tests__/sqlite_verify.test.js +15 -0
- package/src/vendor/db/database.js +181 -0
- package/src/vendor/db/index.js +12 -0
- package/src/vendor/db/migrate.js +210 -0
- package/src/vendor/db/repositories/audit-log.repository.js +68 -0
- package/src/vendor/db/repositories/customer-session.repository.js +181 -0
- package/src/vendor/db/repositories/customer.repository.js +636 -0
- package/src/vendor/db/repositories/device.repository.js +285 -0
- package/src/vendor/db/repositories/merchant.repository.js +158 -0
- package/src/vendor/db/repositories/observation.repository.js +124 -0
- package/src/vendor/db/repositories/payment-session.repository.js +618 -0
- package/src/vendor/db/repositories/receiving-account.repository.js +244 -0
- package/src/vendor/db/repositories/support-ticket.repository.js +140 -0
- package/src/vendor/db/schema.sql +266 -0
- 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
|
+
|