elpaypg 0.0.0-stage → 2.0.1
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 +2424 -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,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Financial utilities enforcing integer minor units (paise).
|
|
3
|
+
* Floating-point representation is forbidden in financial calculations.
|
|
4
|
+
* 1 INR = 100 paise.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Converts a rupee amount (string or number) into integer paise (bigint).
|
|
9
|
+
* Throws an error if invalid, negative, zero, or exceeds 2 decimal places.
|
|
10
|
+
*/
|
|
11
|
+
export function toPaise(rupees: string | number): bigint {
|
|
12
|
+
if (typeof rupees === 'number') {
|
|
13
|
+
if (!Number.isFinite(rupees) || isNaN(rupees)) {
|
|
14
|
+
throw new Error(`Invalid monetary amount: ${rupees}`);
|
|
15
|
+
}
|
|
16
|
+
rupees = rupees.toString();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const clean = rupees.trim();
|
|
20
|
+
if (!/^\d+(\.\d{1,2})?$/.test(clean)) {
|
|
21
|
+
throw new Error(`Invalid INR rupee format: "${rupees}". Must be non-negative with max 2 decimals.`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const parts = clean.split('.');
|
|
25
|
+
const whole = BigInt(parts[0] || '0');
|
|
26
|
+
const fractionStr = parts[1] || '';
|
|
27
|
+
const fraction = BigInt(fractionStr.padEnd(2, '0'));
|
|
28
|
+
|
|
29
|
+
const totalPaise = whole * 100n + fraction;
|
|
30
|
+
if (totalPaise <= 0n) {
|
|
31
|
+
throw new Error(`Monetary amount must be strictly greater than 0: ${rupees}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return totalPaise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Formats integer paise into a standard decimal string ("200.00").
|
|
39
|
+
*/
|
|
40
|
+
export function toRupeesString(paise: bigint | number): string {
|
|
41
|
+
const p = typeof paise === 'number' ? BigInt(Math.floor(paise)) : paise;
|
|
42
|
+
if (p < 0n) {
|
|
43
|
+
throw new Error(`Negative paise value not permitted: ${p}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const whole = p / 100n;
|
|
47
|
+
const fraction = p % 100n;
|
|
48
|
+
return `${whole}.${fraction.toString().padStart(2, '0')}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Formats integer paise into a localized INR display string ("₹200.00").
|
|
53
|
+
*/
|
|
54
|
+
export function formatINR(paise: bigint | number): string {
|
|
55
|
+
return `₹${toRupeesString(paise)}`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calculates a deterministic fingerprint for an observation to prevent duplicate processing.
|
|
5
|
+
* If bankReferenceNumber exists, it hashes receivingAccount + amount + referenceNumber.
|
|
6
|
+
* Otherwise, it hashes receivingAccount + amount + 60-second time bucket.
|
|
7
|
+
* @param {{
|
|
8
|
+
* receivingAccountId: string,
|
|
9
|
+
* amountPaise: bigint,
|
|
10
|
+
* bankReferenceNumber?: string,
|
|
11
|
+
* observedAt: Date
|
|
12
|
+
* }} input
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function calculateObservationFingerprint(input) {
|
|
16
|
+
let entropy;
|
|
17
|
+
if (input.bankReferenceNumber && input.bankReferenceNumber.trim().length > 0) {
|
|
18
|
+
entropy = `REF:${input.bankReferenceNumber.trim().toUpperCase()}`;
|
|
19
|
+
} else {
|
|
20
|
+
// 60-second temporal cluster bucket partitioned by channel source
|
|
21
|
+
const bucket = Math.floor(input.observedAt.getTime() / 60000);
|
|
22
|
+
entropy = `TIME_BUCKET:${bucket}:${input.source || ''}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const raw = `${input.receivingAccountId}:${input.amountPaise.toString()}:${entropy}`;
|
|
26
|
+
return crypto.createHash('sha256').update(raw, 'utf8').digest('hex');
|
|
27
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { ObserverSource, ObservationAttributionStatus } from './enums.js';
|
|
3
|
+
|
|
4
|
+
export interface CanonicalObservation {
|
|
5
|
+
id: string;
|
|
6
|
+
receivingAccountId: string;
|
|
7
|
+
source: ObserverSource;
|
|
8
|
+
amountPaise: bigint;
|
|
9
|
+
observedAt: Date;
|
|
10
|
+
ingestedAt: Date;
|
|
11
|
+
senderVpa?: string;
|
|
12
|
+
bankReferenceNumber?: string;
|
|
13
|
+
fingerprintHash: string;
|
|
14
|
+
attributionStatus: ObservationAttributionStatus;
|
|
15
|
+
attributedPaymentId?: string;
|
|
16
|
+
rawPayload: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IngestObservationInput {
|
|
20
|
+
receivingAccountId: string;
|
|
21
|
+
source: ObserverSource;
|
|
22
|
+
amountPaise: bigint;
|
|
23
|
+
observedAt: Date;
|
|
24
|
+
senderVpa?: string;
|
|
25
|
+
bankReferenceNumber?: string;
|
|
26
|
+
rawPayload?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Calculates a deterministic fingerprint for an observation to prevent duplicate processing.
|
|
31
|
+
* If bankReferenceNumber exists, it hashes receivingAccount + amount + referenceNumber.
|
|
32
|
+
* Otherwise, it hashes receivingAccount + amount + 60-second time bucket.
|
|
33
|
+
*/
|
|
34
|
+
export function calculateObservationFingerprint(input: {
|
|
35
|
+
receivingAccountId: string;
|
|
36
|
+
amountPaise: bigint;
|
|
37
|
+
source?: string;
|
|
38
|
+
bankReferenceNumber?: string;
|
|
39
|
+
observedAt: Date;
|
|
40
|
+
}): string {
|
|
41
|
+
let entropy: string;
|
|
42
|
+
if (input.bankReferenceNumber && input.bankReferenceNumber.trim().length > 0) {
|
|
43
|
+
entropy = `REF:${input.bankReferenceNumber.trim().toUpperCase()}`;
|
|
44
|
+
} else {
|
|
45
|
+
// 60-second temporal cluster bucket partitioned by channel source
|
|
46
|
+
const bucket = Math.floor(input.observedAt.getTime() / 60000);
|
|
47
|
+
entropy = `TIME_BUCKET:${bucket}:${input.source || ''}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const raw = `${input.receivingAccountId}:${input.amountPaise.toString()}:${entropy}`;
|
|
51
|
+
return crypto.createHash('sha256').update(raw, 'utf8').digest('hex');
|
|
52
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Cloudflare Tunnel and Runtime URL Resolution Engine.
|
|
3
|
+
* Single source of truth for runtime mode, derived hostnames, and service mappings.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const SERVICE_PORTS = Object.freeze({
|
|
7
|
+
CHECKOUT: 7000,
|
|
8
|
+
API: 6100,
|
|
9
|
+
ADMIN: 6001,
|
|
10
|
+
GMAIL: 6003,
|
|
11
|
+
STORE: 5000
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Derives public hostnames and Cloudflare published application mappings.
|
|
16
|
+
* @param {{ domain?: string, prefix?: string }} config
|
|
17
|
+
* @returns {Array<{
|
|
18
|
+
* role: string,
|
|
19
|
+
* name: string,
|
|
20
|
+
* publicHostname: string,
|
|
21
|
+
* publicUrl: string,
|
|
22
|
+
* serviceUrl: string,
|
|
23
|
+
* port: number,
|
|
24
|
+
* purpose: string,
|
|
25
|
+
* optional: boolean
|
|
26
|
+
* }>}
|
|
27
|
+
*/
|
|
28
|
+
export function deriveCloudflareRoutes(config = {}) {
|
|
29
|
+
const rawDomain = (config.domain || config.domainTld || '').trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/+$/, '');
|
|
30
|
+
const rawPrefix = (config.prefix || config.domainPrefix || 'pay').trim().toLowerCase().replace(/[^a-z0-9-_]/g, '') || 'pay';
|
|
31
|
+
|
|
32
|
+
if (!rawDomain) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
role: 'checkout',
|
|
39
|
+
name: 'Checkout',
|
|
40
|
+
publicHostname: `${rawPrefix}.${rawDomain}`,
|
|
41
|
+
publicUrl: `https://${rawPrefix}.${rawDomain}`,
|
|
42
|
+
serviceUrl: `http://localhost:${SERVICE_PORTS.CHECKOUT}`,
|
|
43
|
+
port: SERVICE_PORTS.CHECKOUT,
|
|
44
|
+
purpose: 'Customer checkout / checkout portal',
|
|
45
|
+
optional: false
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
role: 'api',
|
|
49
|
+
name: 'Payment API',
|
|
50
|
+
publicHostname: `${rawPrefix}-api.${rawDomain}`,
|
|
51
|
+
publicUrl: `https://${rawPrefix}-api.${rawDomain}`,
|
|
52
|
+
serviceUrl: `http://localhost:${SERVICE_PORTS.API}`,
|
|
53
|
+
port: SERVICE_PORTS.API,
|
|
54
|
+
purpose: 'Payment API / merchant API / SSE / observer ingestion',
|
|
55
|
+
optional: false
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
role: 'admin',
|
|
59
|
+
name: 'Merchant Admin',
|
|
60
|
+
publicHostname: `${rawPrefix}-admin.${rawDomain}`,
|
|
61
|
+
publicUrl: `https://${rawPrefix}-admin.${rawDomain}`,
|
|
62
|
+
serviceUrl: `http://localhost:${SERVICE_PORTS.ADMIN}`,
|
|
63
|
+
port: SERVICE_PORTS.ADMIN,
|
|
64
|
+
purpose: 'Merchant/Admin panel',
|
|
65
|
+
optional: false
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
role: 'gmail',
|
|
69
|
+
name: 'Gmail OAuth / Gmail Observer',
|
|
70
|
+
publicHostname: `${rawPrefix}-gmail.${rawDomain}`,
|
|
71
|
+
publicUrl: `https://${rawPrefix}-gmail.${rawDomain}`,
|
|
72
|
+
serviceUrl: `http://localhost:${SERVICE_PORTS.GMAIL}`,
|
|
73
|
+
port: SERVICE_PORTS.GMAIL,
|
|
74
|
+
purpose: 'Gmail OAuth callback and Gmail observation service',
|
|
75
|
+
optional: false
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
role: 'store',
|
|
79
|
+
name: 'Demo Store',
|
|
80
|
+
publicHostname: `${rawPrefix}-store.${rawDomain}`,
|
|
81
|
+
publicUrl: `https://${rawPrefix}-store.${rawDomain}`,
|
|
82
|
+
serviceUrl: `http://localhost:${SERVICE_PORTS.STORE}`,
|
|
83
|
+
port: SERVICE_PORTS.STORE,
|
|
84
|
+
purpose: 'Reference/demo store',
|
|
85
|
+
optional: true
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolves authoritative runtime URLs for all system components.
|
|
92
|
+
* @param {{
|
|
93
|
+
* envMode?: string,
|
|
94
|
+
* domain?: string,
|
|
95
|
+
* domainTld?: string,
|
|
96
|
+
* prefix?: string,
|
|
97
|
+
* domainPrefix?: string
|
|
98
|
+
* }} options
|
|
99
|
+
*/
|
|
100
|
+
export function resolveRuntimeUrls(options = {}) {
|
|
101
|
+
let mode = options.envMode;
|
|
102
|
+
if (!mode) {
|
|
103
|
+
if (process.env.ELPAY_MODE) {
|
|
104
|
+
mode = process.env.ELPAY_MODE;
|
|
105
|
+
} else if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
|
|
106
|
+
mode = 'localhost';
|
|
107
|
+
} else if (process.env.CLOUDFLARE_DOMAIN) {
|
|
108
|
+
mode = 'production';
|
|
109
|
+
} else {
|
|
110
|
+
mode = 'localhost';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
mode = String(mode).toLowerCase();
|
|
114
|
+
const domain = (options.domain || options.domainTld || process.env.CLOUDFLARE_DOMAIN || '').trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/+$/, '');
|
|
115
|
+
const prefix = (options.prefix || options.domainPrefix || process.env.DEPLOYMENT_PREFIX || 'pay').trim().toLowerCase().replace(/[^a-z0-9-_]/g, '') || 'pay';
|
|
116
|
+
|
|
117
|
+
if (mode === 'production') {
|
|
118
|
+
if (!domain) {
|
|
119
|
+
if ((process.env.NODE_ENV === 'test' || process.env.ELPAY_MODE === 'localhost') && options.envMode !== 'production') {
|
|
120
|
+
mode = 'localhost';
|
|
121
|
+
} else {
|
|
122
|
+
throw new Error('Production Configuration Incomplete: Cloudflare Domain is required when running in PRODUCTION mode.');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (mode === 'production') {
|
|
128
|
+
|
|
129
|
+
const routes = deriveCloudflareRoutes({ domain, prefix });
|
|
130
|
+
const getRoute = (role) => routes.find(r => r.role === role);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
mode: 'production',
|
|
134
|
+
isProduction: true,
|
|
135
|
+
domain,
|
|
136
|
+
prefix,
|
|
137
|
+
routes,
|
|
138
|
+
checkoutUrl: getRoute('checkout')?.publicUrl || `https://${prefix}.${domain}`,
|
|
139
|
+
checkoutHostname: getRoute('checkout')?.publicHostname || `${prefix}.${domain}`,
|
|
140
|
+
apiUrl: getRoute('api')?.publicUrl || `https://${prefix}-api.${domain}`,
|
|
141
|
+
apiHostname: getRoute('api')?.publicHostname || `${prefix}-api.${domain}`,
|
|
142
|
+
adminUrl: getRoute('admin')?.publicUrl || `https://${prefix}-admin.${domain}`,
|
|
143
|
+
adminHostname: getRoute('admin')?.publicHostname || `${prefix}-admin.${domain}`,
|
|
144
|
+
gmailUrl: getRoute('gmail')?.publicUrl || `https://${prefix}-gmail.${domain}`,
|
|
145
|
+
gmailHostname: getRoute('gmail')?.publicHostname || `${prefix}-gmail.${domain}`,
|
|
146
|
+
storeUrl: getRoute('store')?.publicUrl || `https://${prefix}-store.${domain}`,
|
|
147
|
+
storeHostname: getRoute('store')?.publicHostname || `${prefix}-store.${domain}`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Localhost / Development Mode
|
|
152
|
+
return {
|
|
153
|
+
mode: 'localhost',
|
|
154
|
+
isProduction: false,
|
|
155
|
+
domain: domain || 'localhost',
|
|
156
|
+
prefix: prefix || 'pay',
|
|
157
|
+
routes: [
|
|
158
|
+
{ role: 'checkout', name: 'Checkout', publicHostname: 'localhost:7000', publicUrl: 'http://localhost:7000', serviceUrl: 'http://localhost:7000', port: 7000, purpose: 'Customer checkout / checkout portal', optional: false },
|
|
159
|
+
{ role: 'api', name: 'Payment API', publicHostname: 'localhost:6100', publicUrl: 'http://localhost:6100', serviceUrl: 'http://localhost:6100', port: 6100, purpose: 'Payment API / merchant API / SSE / observer ingestion', optional: false },
|
|
160
|
+
{ role: 'admin', name: 'Merchant Admin', publicHostname: 'localhost:6001', publicUrl: 'http://localhost:6001', serviceUrl: 'http://localhost:6001', port: 6001, purpose: 'Merchant/Admin panel', optional: false },
|
|
161
|
+
{ role: 'gmail', name: 'Gmail OAuth / Gmail Observer', publicHostname: 'localhost:6003', publicUrl: 'http://localhost:6003', serviceUrl: 'http://localhost:6003', port: 6003, purpose: 'Gmail OAuth callback and Gmail observation service', optional: false },
|
|
162
|
+
{ role: 'store', name: 'Demo Store', publicHostname: 'localhost:5000', publicUrl: 'http://localhost:5000', serviceUrl: 'http://localhost:5000', port: 5000, purpose: 'Reference/demo store', optional: true }
|
|
163
|
+
],
|
|
164
|
+
checkoutUrl: 'http://localhost:7000',
|
|
165
|
+
checkoutHostname: 'localhost:7000',
|
|
166
|
+
apiUrl: 'http://localhost:6100',
|
|
167
|
+
apiHostname: 'localhost:6100',
|
|
168
|
+
adminUrl: 'http://localhost:6001',
|
|
169
|
+
adminHostname: 'localhost:6001',
|
|
170
|
+
gmailUrl: 'http://localhost:6003',
|
|
171
|
+
gmailHostname: 'localhost:6003',
|
|
172
|
+
storeUrl: 'http://localhost:5000',
|
|
173
|
+
storeHostname: 'localhost:5000'
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Generates structured SMS Gateway QR pairing payload according to Part C1 & C2.
|
|
179
|
+
*/
|
|
180
|
+
export function createPairingPayload(options = {}) {
|
|
181
|
+
const {
|
|
182
|
+
serverUrl,
|
|
183
|
+
sessionId = `pair_sess_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
|
|
184
|
+
token = `tok_${Math.random().toString(36).substring(2, 12)}`,
|
|
185
|
+
merchantId = '00000000-0000-0000-0000-000000000001',
|
|
186
|
+
ttlMs = 10 * 60 * 1000 // 10 minutes default
|
|
187
|
+
} = options;
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
version: 'v1',
|
|
191
|
+
type: 'ELPAY_SMS_GATEWAY_PAIR',
|
|
192
|
+
serverUrl: serverUrl || 'http://localhost:6100',
|
|
193
|
+
sessionId,
|
|
194
|
+
token,
|
|
195
|
+
merchantId,
|
|
196
|
+
expiresAt: Date.now() + ttlMs,
|
|
197
|
+
issuedAt: Date.now()
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Validates pairing payload according to Part C5.
|
|
203
|
+
*/
|
|
204
|
+
export function validatePairingPayload(payload) {
|
|
205
|
+
if (!payload || typeof payload !== 'object') {
|
|
206
|
+
return { valid: false, error: 'INVALID ELPAY PAIRING QR' };
|
|
207
|
+
}
|
|
208
|
+
if (payload.type !== 'ELPAY_SMS_GATEWAY_PAIR') {
|
|
209
|
+
return { valid: false, error: 'INVALID ELPAY PAIRING QR' };
|
|
210
|
+
}
|
|
211
|
+
if (payload.version !== 'v1') {
|
|
212
|
+
return { valid: false, error: 'INVALID ELPAY PAIRING QR VERSION' };
|
|
213
|
+
}
|
|
214
|
+
if (!payload.serverUrl || !payload.token || !payload.sessionId) {
|
|
215
|
+
return { valid: false, error: 'MALFORMED ELPAY PAIRING QR: Missing required fields' };
|
|
216
|
+
}
|
|
217
|
+
if (!payload.serverUrl.startsWith('http://') && !payload.serverUrl.startsWith('https://')) {
|
|
218
|
+
return { valid: false, error: 'INVALID SERVER URL PROTOCOL' };
|
|
219
|
+
}
|
|
220
|
+
if (payload.expiresAt && Date.now() > payload.expiresAt) {
|
|
221
|
+
return { valid: false, error: 'PAIRING QR EXPIRED' };
|
|
222
|
+
}
|
|
223
|
+
return { valid: true };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Resolves active runtime pairing API URL.
|
|
228
|
+
*/
|
|
229
|
+
export function resolvePairingUrl(options = {}) {
|
|
230
|
+
const urls = resolveRuntimeUrls(options);
|
|
231
|
+
return urls.apiUrl || 'http://localhost:6100';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* OAuth redirect URI for the Gmail observer. `GMAIL_REDIRECT_URI` wins; otherwise it is derived
|
|
236
|
+
* from the configured domain, falling back to the local observer port.
|
|
237
|
+
* @param {object} [branding]
|
|
238
|
+
*/
|
|
239
|
+
export function resolveGmailRedirectUri(branding = {}) {
|
|
240
|
+
if (process.env.GMAIL_REDIRECT_URI) return process.env.GMAIL_REDIRECT_URI;
|
|
241
|
+
try {
|
|
242
|
+
return `${resolveRuntimeUrls(branding || {}).gmailUrl}/oauth/callback`;
|
|
243
|
+
} catch {
|
|
244
|
+
return `http://localhost:${process.env.GMAIL_OBSERVER_PORT || 6003}/oauth/callback`;
|
|
245
|
+
}
|
|
246
|
+
}
|