@moncircle/sdk 1.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/README.md +83 -0
- package/dist/bin.js +24 -0
- package/dist/index.js +96 -0
- package/dist/middleware/auth.js +22 -0
- package/dist/models/Order.js +47 -0
- package/dist/models/Platform.js +47 -0
- package/dist/models/Seller.js +44 -0
- package/dist/models/User.js +48 -0
- package/dist/prisma.js +21 -0
- package/dist/routes/order.routes.js +37 -0
- package/dist/routes/platform.routes.js +26 -0
- package/dist/routes/seller.routes.js +27 -0
- package/dist/routes/user.routes.js +44 -0
- package/dist/routes/withdraw.routes.js +48 -0
- package/dist/server.js +23 -0
- package/dist/services/reward.service.js +60 -0
- package/package.json +53 -0
- package/scripts/postinstall.js +62 -0
- package/scripts/setup.js +162 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# MON Loyalty API
|
|
2
|
+
|
|
3
|
+
A production-ready, multi-tenant loyalty infrastructure for e-commerce platforms.
|
|
4
|
+
|
|
5
|
+
## Core Features
|
|
6
|
+
- **Reward Isolation**: Isolated balances per platform and seller.
|
|
7
|
+
- **Flexible Modes**:
|
|
8
|
+
- `SINGLE`: Unified rewards at the platform level.
|
|
9
|
+
- `MULTI`: Isolated reward pools for different sellers.
|
|
10
|
+
- `HYBRID`: Split rewards between platform and seller pools (70/30).
|
|
11
|
+
- **Stripe-Level API**: Secure API key authentication and clean REST endpoints.
|
|
12
|
+
|
|
13
|
+
## Tech Stack
|
|
14
|
+
- Node.js & Express
|
|
15
|
+
- TypeScript
|
|
16
|
+
- Prisma ORM (PostgreSQL)
|
|
17
|
+
- JWT-based Auth
|
|
18
|
+
|
|
19
|
+
## Getting Started
|
|
20
|
+
|
|
21
|
+
### 1. Install Dependencies
|
|
22
|
+
```bash
|
|
23
|
+
npm install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 2. Configure Environment
|
|
27
|
+
Copy `.env.example` to `.env` and provide your `DATABASE_URL`.
|
|
28
|
+
```bash
|
|
29
|
+
cp .env.example .env
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 3. Database Migration
|
|
33
|
+
```bash
|
|
34
|
+
npx prisma migrate dev --name init
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 4. Run Development Server
|
|
38
|
+
```bash
|
|
39
|
+
npm run dev
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## API Usage Example
|
|
43
|
+
|
|
44
|
+
### Create a Platform
|
|
45
|
+
```bash
|
|
46
|
+
POST /v1/platforms
|
|
47
|
+
{
|
|
48
|
+
"name": "Stond Emporium",
|
|
49
|
+
"mode": "HYBRID",
|
|
50
|
+
"platformRewardRate": 0.0002
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Create a Seller (MULTI/HYBRID only)
|
|
55
|
+
**Header**: `Authorization: Bearer sk_xxxx`
|
|
56
|
+
```bash
|
|
57
|
+
POST /v1/sellers
|
|
58
|
+
{
|
|
59
|
+
"name": "Nike Official",
|
|
60
|
+
"rewardRate": 0.0005
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Record an Order
|
|
65
|
+
**Header**: `Authorization: Bearer sk_xxxx`
|
|
66
|
+
```bash
|
|
67
|
+
POST /v1/orders
|
|
68
|
+
{
|
|
69
|
+
"platformId": "...",
|
|
70
|
+
"sellerId": "...",
|
|
71
|
+
"userId": "user_123",
|
|
72
|
+
"amount": 5000,
|
|
73
|
+
"orderId": "ord_001"
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Check Balance
|
|
78
|
+
```bash
|
|
79
|
+
GET /v1/users/user_123/balance?platformId=...&sellerId=...
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
MIT
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// src/bin.ts ā CLI entry point for Option B (standalone server)
|
|
4
|
+
// Run with: npx mon-loyalty-api start
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
require("dotenv/config");
|
|
10
|
+
const express_1 = __importDefault(require("express"));
|
|
11
|
+
const index_1 = require("./index");
|
|
12
|
+
const mongoUri = process.env.MONGODB_URI;
|
|
13
|
+
const port = parseInt(process.env.PORT || "4000", 10);
|
|
14
|
+
if (!mongoUri) {
|
|
15
|
+
console.error("\n[mon-loyalty-api] ā MONGODB_URI is not set in .env\n");
|
|
16
|
+
console.error(" Add MONGODB_URI to your .env file and try again.\n");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const app = (0, express_1.default)();
|
|
20
|
+
app.use("/v1", (0, index_1.createMonLoyalty)({ mongoUri }));
|
|
21
|
+
app.listen(port, () => {
|
|
22
|
+
console.log(`\nš MON Loyalty API running on http://localhost:${port}`);
|
|
23
|
+
console.log(` Health: http://localhost:${port}/v1/health\n`);
|
|
24
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/index.ts ā Public npm package entry point
|
|
3
|
+
// Supports both:
|
|
4
|
+
// Option A: Embeddable middleware ā app.use('/loyalty', createMonLoyalty({ ... }))
|
|
5
|
+
// Option B: Standalone ā handled by src/bin.ts / src/server.ts
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
40
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.RewardService = exports.Order = exports.User = exports.Seller = exports.Platform = void 0;
|
|
44
|
+
exports.createMonLoyalty = createMonLoyalty;
|
|
45
|
+
const express_1 = __importStar(require("express"));
|
|
46
|
+
const mongoose_1 = __importDefault(require("mongoose"));
|
|
47
|
+
const cors_1 = __importDefault(require("cors"));
|
|
48
|
+
const platform_routes_1 = __importDefault(require("./routes/platform.routes"));
|
|
49
|
+
const seller_routes_1 = __importDefault(require("./routes/seller.routes"));
|
|
50
|
+
const order_routes_1 = __importDefault(require("./routes/order.routes"));
|
|
51
|
+
const user_routes_1 = __importDefault(require("./routes/user.routes"));
|
|
52
|
+
const withdraw_routes_1 = __importDefault(require("./routes/withdraw.routes"));
|
|
53
|
+
/**
|
|
54
|
+
* createMonLoyalty ā Embeddable Express Router (Option A)
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* import { createMonLoyalty } from "mon-loyalty-api"
|
|
58
|
+
* app.use("/loyalty", createMonLoyalty({ mongoUri: process.env.MONGODB_URI }))
|
|
59
|
+
*/
|
|
60
|
+
function createMonLoyalty(config) {
|
|
61
|
+
if (!config.mongoUri)
|
|
62
|
+
throw new Error("[mon-loyalty-api] mongoUri is required");
|
|
63
|
+
// Connect to MongoDB (idempotent)
|
|
64
|
+
if (mongoose_1.default.connection.readyState === 0) {
|
|
65
|
+
mongoose_1.default.connect(config.mongoUri).then(() => {
|
|
66
|
+
console.log("[mon-loyalty-api] ā
MongoDB connected");
|
|
67
|
+
}).catch((err) => {
|
|
68
|
+
console.error("[mon-loyalty-api] ā MongoDB connection failed:", err.message);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const router = (0, express_1.Router)();
|
|
72
|
+
if (config.cors !== false) {
|
|
73
|
+
router.use((0, cors_1.default)());
|
|
74
|
+
}
|
|
75
|
+
router.use(express_1.default.json());
|
|
76
|
+
router.use("/platforms", platform_routes_1.default);
|
|
77
|
+
router.use("/sellers", seller_routes_1.default);
|
|
78
|
+
router.use("/orders", order_routes_1.default);
|
|
79
|
+
router.use("/users", user_routes_1.default);
|
|
80
|
+
router.use("/withdraw", withdraw_routes_1.default);
|
|
81
|
+
router.get("/health", (_req, res) => {
|
|
82
|
+
res.json({ status: "ok", package: "mon-loyalty-api" });
|
|
83
|
+
});
|
|
84
|
+
return router;
|
|
85
|
+
}
|
|
86
|
+
// Re-export models for advanced use
|
|
87
|
+
var Platform_1 = require("./models/Platform");
|
|
88
|
+
Object.defineProperty(exports, "Platform", { enumerable: true, get: function () { return Platform_1.Platform; } });
|
|
89
|
+
var Seller_1 = require("./models/Seller");
|
|
90
|
+
Object.defineProperty(exports, "Seller", { enumerable: true, get: function () { return Seller_1.Seller; } });
|
|
91
|
+
var User_1 = require("./models/User");
|
|
92
|
+
Object.defineProperty(exports, "User", { enumerable: true, get: function () { return User_1.User; } });
|
|
93
|
+
var Order_1 = require("./models/Order");
|
|
94
|
+
Object.defineProperty(exports, "Order", { enumerable: true, get: function () { return Order_1.Order; } });
|
|
95
|
+
var reward_service_1 = require("./services/reward.service");
|
|
96
|
+
Object.defineProperty(exports, "RewardService", { enumerable: true, get: function () { return reward_service_1.RewardService; } });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authenticate = void 0;
|
|
4
|
+
const Platform_1 = require("../models/Platform");
|
|
5
|
+
const authenticate = async (req, res, next) => {
|
|
6
|
+
const authHeader = req.headers.authorization;
|
|
7
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
8
|
+
return res.status(401).json({ error: "Missing or invalid API key" });
|
|
9
|
+
}
|
|
10
|
+
const secretKey = authHeader.split(" ")[1];
|
|
11
|
+
try {
|
|
12
|
+
const platform = await Platform_1.Platform.findOne({ secretKey });
|
|
13
|
+
if (!platform)
|
|
14
|
+
return res.status(403).json({ error: "Unauthorized" });
|
|
15
|
+
req.platform = platform;
|
|
16
|
+
next();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
res.status(500).json({ error: "Authentication error" });
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
exports.authenticate = authenticate;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Order = void 0;
|
|
37
|
+
const mongoose_1 = __importStar(require("mongoose"));
|
|
38
|
+
const OrderSchema = new mongoose_1.Schema({
|
|
39
|
+
orderId: { type: String, required: true, unique: true },
|
|
40
|
+
platformId: { type: String, required: true },
|
|
41
|
+
sellerId: { type: String, default: null },
|
|
42
|
+
userId: { type: String, required: true },
|
|
43
|
+
amount: { type: Number, required: true },
|
|
44
|
+
earned: { type: Number, required: true },
|
|
45
|
+
createdAt: { type: Date, default: Date.now }
|
|
46
|
+
});
|
|
47
|
+
exports.Order = mongoose_1.default.model("Order", OrderSchema);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Platform = void 0;
|
|
37
|
+
const mongoose_1 = __importStar(require("mongoose"));
|
|
38
|
+
const PlatformSchema = new mongoose_1.Schema({
|
|
39
|
+
name: { type: String, required: true },
|
|
40
|
+
publicKey: { type: String, required: true, unique: true },
|
|
41
|
+
secretKey: { type: String, required: true, unique: true },
|
|
42
|
+
mode: { type: String, enum: ["SINGLE", "MULTI", "HYBRID"], required: true },
|
|
43
|
+
platformRewardRate: { type: Number, default: 0.0002 },
|
|
44
|
+
monadWalletAddress: { type: String, default: null },
|
|
45
|
+
createdAt: { type: Date, default: Date.now }
|
|
46
|
+
});
|
|
47
|
+
exports.Platform = mongoose_1.default.model("Platform", PlatformSchema);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Seller = void 0;
|
|
37
|
+
const mongoose_1 = __importStar(require("mongoose"));
|
|
38
|
+
const SellerSchema = new mongoose_1.Schema({
|
|
39
|
+
name: { type: String, required: true },
|
|
40
|
+
platformId: { type: String, required: true, index: true },
|
|
41
|
+
rewardRate: { type: Number, default: 0.0002 },
|
|
42
|
+
createdAt: { type: Date, default: Date.now }
|
|
43
|
+
});
|
|
44
|
+
exports.Seller = mongoose_1.default.model("Seller", SellerSchema);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.User = void 0;
|
|
37
|
+
const mongoose_1 = __importStar(require("mongoose"));
|
|
38
|
+
const UserSchema = new mongoose_1.Schema({
|
|
39
|
+
userId: { type: String, required: true },
|
|
40
|
+
platformId: { type: String, required: true },
|
|
41
|
+
sellerId: { type: String, default: null },
|
|
42
|
+
balance: { type: Number, default: 0 },
|
|
43
|
+
lastActive: { type: Date, default: Date.now }
|
|
44
|
+
});
|
|
45
|
+
// Compound unique index ā this is the scoping logic
|
|
46
|
+
// A user has one record per (userId + platformId + sellerId) combination
|
|
47
|
+
UserSchema.index({ userId: 1, platformId: 1, sellerId: 1 }, { unique: true });
|
|
48
|
+
exports.User = mongoose_1.default.model("User", UserSchema);
|
package/dist/prisma.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.connectDB = void 0;
|
|
7
|
+
require("dotenv/config");
|
|
8
|
+
const mongoose_1 = __importDefault(require("mongoose"));
|
|
9
|
+
let isConnected = false;
|
|
10
|
+
const connectDB = async () => {
|
|
11
|
+
if (isConnected)
|
|
12
|
+
return;
|
|
13
|
+
const uri = process.env.MONGODB_URI;
|
|
14
|
+
if (!uri)
|
|
15
|
+
throw new Error("MONGODB_URI is not defined in .env");
|
|
16
|
+
await mongoose_1.default.connect(uri);
|
|
17
|
+
isConnected = true;
|
|
18
|
+
console.log("ā
MongoDB connected");
|
|
19
|
+
};
|
|
20
|
+
exports.connectDB = connectDB;
|
|
21
|
+
exports.default = mongoose_1.default;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const express_1 = require("express");
|
|
4
|
+
const Order_1 = require("../models/Order");
|
|
5
|
+
const auth_1 = require("../middleware/auth");
|
|
6
|
+
const reward_service_1 = require("../services/reward.service");
|
|
7
|
+
const router = (0, express_1.Router)();
|
|
8
|
+
router.post("/", auth_1.authenticate, async (req, res) => {
|
|
9
|
+
const { platformId, sellerId, userId, amount, orderId } = req.body;
|
|
10
|
+
const platform = req.platform;
|
|
11
|
+
if (platform._id.toString() !== platformId) {
|
|
12
|
+
return res.status(403).json({ error: "platformId mismatch" });
|
|
13
|
+
}
|
|
14
|
+
if (!userId || !amount || !orderId) {
|
|
15
|
+
return res.status(400).json({ error: "Missing required fields: userId, amount, orderId" });
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const { platformEarned, sellerEarned } = await reward_service_1.RewardService.calculateReward(platformId, amount, sellerId);
|
|
19
|
+
await Order_1.Order.create({
|
|
20
|
+
orderId,
|
|
21
|
+
platformId,
|
|
22
|
+
sellerId: sellerId || null,
|
|
23
|
+
userId,
|
|
24
|
+
amount,
|
|
25
|
+
earned: platformEarned + sellerEarned
|
|
26
|
+
});
|
|
27
|
+
const newBalances = await reward_service_1.RewardService.updateBalances(platformId, userId, platformEarned, sellerEarned, sellerId);
|
|
28
|
+
res.json({
|
|
29
|
+
earned: { platform: platformEarned, seller: sellerEarned },
|
|
30
|
+
newBalances
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
res.status(400).json({ error: err.message });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
exports.default = router;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const express_1 = require("express");
|
|
4
|
+
const Platform_1 = require("../models/Platform");
|
|
5
|
+
const uuid_1 = require("uuid");
|
|
6
|
+
const router = (0, express_1.Router)();
|
|
7
|
+
router.post("/", async (req, res) => {
|
|
8
|
+
const { name, mode, platformRewardRate, monadWalletAddress } = req.body;
|
|
9
|
+
if (!name || !mode)
|
|
10
|
+
return res.status(400).json({ error: "name and mode are required" });
|
|
11
|
+
try {
|
|
12
|
+
const platform = await Platform_1.Platform.create({
|
|
13
|
+
name,
|
|
14
|
+
mode,
|
|
15
|
+
platformRewardRate: platformRewardRate || 0.0002,
|
|
16
|
+
monadWalletAddress: monadWalletAddress || null,
|
|
17
|
+
publicKey: "pk_" + (0, uuid_1.v4)().replace(/-/g, ""),
|
|
18
|
+
secretKey: "sk_" + (0, uuid_1.v4)().replace(/-/g, "")
|
|
19
|
+
});
|
|
20
|
+
res.status(201).json(platform);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
res.status(500).json({ error: err.message });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
exports.default = router;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const express_1 = require("express");
|
|
4
|
+
const Seller_1 = require("../models/Seller");
|
|
5
|
+
const auth_1 = require("../middleware/auth");
|
|
6
|
+
const router = (0, express_1.Router)();
|
|
7
|
+
router.post("/", auth_1.authenticate, async (req, res) => {
|
|
8
|
+
const { name, rewardRate } = req.body;
|
|
9
|
+
const platform = req.platform;
|
|
10
|
+
if (platform.mode === "SINGLE") {
|
|
11
|
+
return res.status(400).json({ error: "Sellers are not used in SINGLE mode" });
|
|
12
|
+
}
|
|
13
|
+
if (!name)
|
|
14
|
+
return res.status(400).json({ error: "name is required" });
|
|
15
|
+
try {
|
|
16
|
+
const seller = await Seller_1.Seller.create({
|
|
17
|
+
name,
|
|
18
|
+
platformId: platform._id.toString(),
|
|
19
|
+
rewardRate: rewardRate || 0.0002
|
|
20
|
+
});
|
|
21
|
+
res.status(201).json(seller);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
res.status(500).json({ error: err.message });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
exports.default = router;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const express_1 = require("express");
|
|
4
|
+
const User_1 = require("../models/User");
|
|
5
|
+
const auth_1 = require("../middleware/auth");
|
|
6
|
+
const reward_service_1 = require("../services/reward.service");
|
|
7
|
+
const router = (0, express_1.Router)();
|
|
8
|
+
// GET /v1/users/:userId/balance?platformId=...&sellerId=...
|
|
9
|
+
router.get("/:userId/balance", async (req, res) => {
|
|
10
|
+
const { userId } = req.params;
|
|
11
|
+
const { platformId, sellerId } = req.query;
|
|
12
|
+
if (!platformId)
|
|
13
|
+
return res.status(400).json({ error: "platformId is required" });
|
|
14
|
+
try {
|
|
15
|
+
const user = await User_1.User.findOne({
|
|
16
|
+
userId,
|
|
17
|
+
platformId,
|
|
18
|
+
sellerId: sellerId || null
|
|
19
|
+
});
|
|
20
|
+
res.json({ balance: user?.balance || 0 });
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
res.status(500).json({ error: err.message });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
// POST /v1/users/redeem
|
|
27
|
+
router.post("/redeem", auth_1.authenticate, async (req, res) => {
|
|
28
|
+
const { platformId, sellerId, userId, amount } = req.body;
|
|
29
|
+
const platform = req.platform;
|
|
30
|
+
if (platform._id.toString() !== platformId) {
|
|
31
|
+
return res.status(403).json({ error: "platformId mismatch" });
|
|
32
|
+
}
|
|
33
|
+
if (!userId || !amount) {
|
|
34
|
+
return res.status(400).json({ error: "Missing required fields: userId, amount" });
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const newBalance = await reward_service_1.RewardService.redeem(platformId, userId, amount, sellerId);
|
|
38
|
+
res.json({ message: "Redemption successful", newBalance });
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
res.status(400).json({ error: err.message });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
exports.default = router;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const express_1 = require("express");
|
|
4
|
+
const auth_1 = require("../middleware/auth");
|
|
5
|
+
const reward_service_1 = require("../services/reward.service");
|
|
6
|
+
const router = (0, express_1.Router)();
|
|
7
|
+
/**
|
|
8
|
+
* POST /v1/withdraw
|
|
9
|
+
*
|
|
10
|
+
* Off-chain: Deducts user balance from DB.
|
|
11
|
+
* On-chain (future): Will trigger Monad smart contract to mint/transfer MON tokens.
|
|
12
|
+
*
|
|
13
|
+
* Architecture: Hybrid ā track in DB, settle on-chain on request.
|
|
14
|
+
*/
|
|
15
|
+
router.post("/", auth_1.authenticate, async (req, res) => {
|
|
16
|
+
const { platformId, sellerId, userId, amount, walletAddress } = req.body;
|
|
17
|
+
const platform = req.platform;
|
|
18
|
+
if (platform._id.toString() !== platformId) {
|
|
19
|
+
return res.status(403).json({ error: "platformId mismatch" });
|
|
20
|
+
}
|
|
21
|
+
if (!userId || !amount || !walletAddress) {
|
|
22
|
+
return res.status(400).json({ error: "Missing required fields: userId, amount, walletAddress" });
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
// Step 1: Deduct balance in MongoDB (off-chain)
|
|
26
|
+
const newBalance = await reward_service_1.RewardService.redeem(platformId, userId, amount, sellerId);
|
|
27
|
+
// Step 2: TODO ā Monad on-chain settlement
|
|
28
|
+
// Will call MonLoyalty smart contract to mint/transfer MON tokens
|
|
29
|
+
// const tx = await monadService.settle(walletAddress, amount)
|
|
30
|
+
const withdrawal = {
|
|
31
|
+
userId,
|
|
32
|
+
platformId,
|
|
33
|
+
sellerId: sellerId || null,
|
|
34
|
+
amount,
|
|
35
|
+
walletAddress,
|
|
36
|
+
status: "PENDING_CHAIN", // Becomes "SETTLED" after Monad TX
|
|
37
|
+
newBalance
|
|
38
|
+
};
|
|
39
|
+
res.json({
|
|
40
|
+
message: "Withdrawal initiated (off-chain deducted, on-chain settlement pending)",
|
|
41
|
+
withdrawal
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
res.status(400).json({ error: err.message });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
exports.default = router;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
require("dotenv/config");
|
|
7
|
+
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const index_1 = require("./index");
|
|
9
|
+
// Standalone dev server ā uses the same factory as the npm package
|
|
10
|
+
const app = (0, express_1.default)();
|
|
11
|
+
const PORT = process.env.PORT || 4000;
|
|
12
|
+
app.use("/v1", (0, index_1.createMonLoyalty)({
|
|
13
|
+
mongoUri: process.env.MONGODB_URI
|
|
14
|
+
}));
|
|
15
|
+
// Global error handler
|
|
16
|
+
app.use((err, _req, res, _next) => {
|
|
17
|
+
console.error(err.stack);
|
|
18
|
+
res.status(err.status || 500).json({ error: err.message || "Internal Server Error" });
|
|
19
|
+
});
|
|
20
|
+
app.listen(PORT, () => {
|
|
21
|
+
console.log(`š MON Loyalty API running on http://localhost:${PORT}`);
|
|
22
|
+
console.log(` Health: http://localhost:${PORT}/v1/health`);
|
|
23
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RewardService = void 0;
|
|
4
|
+
const Platform_1 = require("../models/Platform");
|
|
5
|
+
const Seller_1 = require("../models/Seller");
|
|
6
|
+
const User_1 = require("../models/User");
|
|
7
|
+
class RewardService {
|
|
8
|
+
static async calculateReward(platformId, amount, sellerId) {
|
|
9
|
+
const platform = await Platform_1.Platform.findById(platformId);
|
|
10
|
+
if (!platform)
|
|
11
|
+
throw new Error("Platform not found");
|
|
12
|
+
let platformEarned = 0;
|
|
13
|
+
let sellerEarned = 0;
|
|
14
|
+
if (platform.mode === "SINGLE") {
|
|
15
|
+
platformEarned = amount * platform.platformRewardRate;
|
|
16
|
+
}
|
|
17
|
+
else if (platform.mode === "MULTI") {
|
|
18
|
+
if (!sellerId)
|
|
19
|
+
throw new Error("sellerId required for MULTI mode");
|
|
20
|
+
const seller = await Seller_1.Seller.findById(sellerId);
|
|
21
|
+
if (!seller || seller.platformId !== platformId)
|
|
22
|
+
throw new Error("Seller not found");
|
|
23
|
+
sellerEarned = amount * seller.rewardRate;
|
|
24
|
+
}
|
|
25
|
+
else if (platform.mode === "HYBRID") {
|
|
26
|
+
if (!sellerId)
|
|
27
|
+
throw new Error("sellerId required for HYBRID mode");
|
|
28
|
+
const seller = await Seller_1.Seller.findById(sellerId);
|
|
29
|
+
if (!seller || seller.platformId !== platformId)
|
|
30
|
+
throw new Error("Seller not found");
|
|
31
|
+
sellerEarned = amount * seller.rewardRate * 0.7;
|
|
32
|
+
platformEarned = amount * platform.platformRewardRate * 0.3;
|
|
33
|
+
}
|
|
34
|
+
return { platformEarned, sellerEarned, mode: platform.mode };
|
|
35
|
+
}
|
|
36
|
+
static async updateBalances(platformId, userId, platformEarned, sellerEarned, sellerId) {
|
|
37
|
+
const results = [];
|
|
38
|
+
// Platform-level balance (sellerId = null)
|
|
39
|
+
if (platformEarned > 0) {
|
|
40
|
+
const user = await User_1.User.findOneAndUpdate({ userId, platformId, sellerId: null }, { $inc: { balance: platformEarned }, $set: { lastActive: new Date() } }, { upsert: true, new: true });
|
|
41
|
+
results.push({ scope: "PLATFORM", newBalance: user.balance });
|
|
42
|
+
}
|
|
43
|
+
// Seller-level balance
|
|
44
|
+
if (sellerEarned > 0 && sellerId) {
|
|
45
|
+
const user = await User_1.User.findOneAndUpdate({ userId, platformId, sellerId }, { $inc: { balance: sellerEarned }, $set: { lastActive: new Date() } }, { upsert: true, new: true });
|
|
46
|
+
results.push({ scope: "SELLER", sellerId, newBalance: user.balance });
|
|
47
|
+
}
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
static async redeem(platformId, userId, amount, sellerId) {
|
|
51
|
+
const user = await User_1.User.findOne({ userId, platformId, sellerId: sellerId || null });
|
|
52
|
+
if (!user || user.balance < amount)
|
|
53
|
+
throw new Error("Insufficient balance");
|
|
54
|
+
user.balance -= amount;
|
|
55
|
+
user.lastActive = new Date();
|
|
56
|
+
await user.save();
|
|
57
|
+
return user.balance;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.RewardService = RewardService;
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@moncircle/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Self-hosted MON loyalty reward infrastructure for e-commerce platforms. Supports SINGLE, MULTI, and HYBRID reward modes with Monad on-chain settlement.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mon-loyalty-api": "dist/bin.js",
|
|
9
|
+
"mon-loyalty-setup": "scripts/setup.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/**/*",
|
|
13
|
+
"scripts/setup.js",
|
|
14
|
+
"scripts/postinstall.js",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"postinstall": "node scripts/postinstall.js",
|
|
19
|
+
"setup": "node scripts/setup.js",
|
|
20
|
+
"dev": "ts-node-dev --respawn --transpile-only src/bin.ts",
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"start": "node dist/bin.js",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"loyalty",
|
|
27
|
+
"rewards",
|
|
28
|
+
"monad",
|
|
29
|
+
"web3",
|
|
30
|
+
"ecommerce",
|
|
31
|
+
"mon"
|
|
32
|
+
],
|
|
33
|
+
"author": "",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"cors": "^2.8.6",
|
|
37
|
+
"dotenv": "^17.3.1",
|
|
38
|
+
"express": "^5.2.1",
|
|
39
|
+
"mongoose": "^8.0.0",
|
|
40
|
+
"uuid": "^13.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/cors": "^2.8.19",
|
|
44
|
+
"@types/express": "^5.0.6",
|
|
45
|
+
"@types/node": "^25.3.0",
|
|
46
|
+
"@types/uuid": "^10.0.0",
|
|
47
|
+
"ts-node-dev": "^2.0.0",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"express": ">=4.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mon-loyalty-api postinstall hook
|
|
4
|
+
*
|
|
5
|
+
* Runs automatically after: npm install mon-loyalty-api
|
|
6
|
+
* Smart detection:
|
|
7
|
+
* - Skips in CI environments (CI=true)
|
|
8
|
+
* - Skips if .env already exists in the consumer's project
|
|
9
|
+
* - Skips if not running in an interactive terminal (piped/non-TTY)
|
|
10
|
+
* - Writes .env to INIT_CWD (the consumer's project root, not node_modules)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require("fs")
|
|
14
|
+
const path = require("path")
|
|
15
|
+
const { execSync } = require("child_process")
|
|
16
|
+
|
|
17
|
+
const CYAN = "\x1b[36m"
|
|
18
|
+
const GREEN = "\x1b[32m"
|
|
19
|
+
const YELLOW = "\x1b[33m"
|
|
20
|
+
const BOLD = "\x1b[1m"
|
|
21
|
+
const RESET = "\x1b[0m"
|
|
22
|
+
|
|
23
|
+
// Where the consumer ran `npm install` (their project root)
|
|
24
|
+
const projectRoot = process.env.INIT_CWD || process.cwd()
|
|
25
|
+
const envPath = path.join(projectRoot, ".env")
|
|
26
|
+
const setupScript = path.join(__dirname, "setup.js")
|
|
27
|
+
|
|
28
|
+
console.log(`\n${BOLD}${CYAN} mon-loyalty-api installed!${RESET}`)
|
|
29
|
+
|
|
30
|
+
// Skip in CI
|
|
31
|
+
if (process.env.CI === "true" || process.env.CI === "1") {
|
|
32
|
+
console.log(` ${YELLOW}ā CI environment detected ā skipping interactive setup.${RESET}`)
|
|
33
|
+
console.log(` Run ${CYAN}npx mon-loyalty-setup${RESET} manually to configure your .env\n`)
|
|
34
|
+
process.exit(0)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Skip if not interactive TTY (e.g. piped output)
|
|
38
|
+
if (!process.stdin.isTTY) {
|
|
39
|
+
console.log(` ${YELLOW}ā Non-interactive terminal ā skipping setup wizard.${RESET}`)
|
|
40
|
+
console.log(` Run ${CYAN}npm run setup${RESET} to configure your .env\n`)
|
|
41
|
+
process.exit(0)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Skip if .env already exists (don't overwrite a dev's existing config)
|
|
45
|
+
if (fs.existsSync(envPath)) {
|
|
46
|
+
console.log(` ${GREEN}ā .env already exists at: ${envPath}${RESET}`)
|
|
47
|
+
console.log(` Run ${CYAN}npm run setup${RESET} to reconfigure.\n`)
|
|
48
|
+
process.exit(0)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Run the interactive setup wizard
|
|
52
|
+
console.log(` ${BOLD}Let's configure your environment...\n${RESET}`)
|
|
53
|
+
try {
|
|
54
|
+
execSync(`node ${setupScript}`, {
|
|
55
|
+
stdio: "inherit",
|
|
56
|
+
cwd: projectRoot,
|
|
57
|
+
env: { ...process.env, MON_LOYALTY_ENV_PATH: envPath }
|
|
58
|
+
})
|
|
59
|
+
} catch {
|
|
60
|
+
// User may have Ctrl+C'd ā that's fine
|
|
61
|
+
console.log(`\n ${YELLOW}Setup cancelled. Run ${CYAN}npm run setup${RESET}${YELLOW} anytime to configure.${RESET}\n`)
|
|
62
|
+
}
|
package/scripts/setup.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mon-loyalty-api setup script
|
|
4
|
+
* Run: npm run setup OR npx mon-loyalty-api setup
|
|
5
|
+
*
|
|
6
|
+
* Walks the dev through all required ENV vars and writes them to .env
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const readline = require("readline")
|
|
10
|
+
const fs = require("fs")
|
|
11
|
+
const path = require("path")
|
|
12
|
+
|
|
13
|
+
const ENV_PATH = process.env.MON_LOYALTY_ENV_PATH || path.join(process.cwd(), ".env")
|
|
14
|
+
|
|
15
|
+
const CYAN = "\x1b[36m"
|
|
16
|
+
const GREEN = "\x1b[32m"
|
|
17
|
+
const YELLOW = "\x1b[33m"
|
|
18
|
+
const RED = "\x1b[31m"
|
|
19
|
+
const BOLD = "\x1b[1m"
|
|
20
|
+
const RESET = "\x1b[0m"
|
|
21
|
+
|
|
22
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
23
|
+
|
|
24
|
+
const ask = (question, defaultVal = "") =>
|
|
25
|
+
new Promise((resolve) => {
|
|
26
|
+
const hint = defaultVal ? ` ${YELLOW}[${defaultVal}]${RESET}` : ""
|
|
27
|
+
rl.question(` ${question}${hint}: `, (answer) => {
|
|
28
|
+
resolve(answer.trim() || defaultVal)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const fields = [
|
|
33
|
+
{
|
|
34
|
+
key: "MONGODB_URI",
|
|
35
|
+
label: "MongoDB URI",
|
|
36
|
+
hint: "e.g. mongodb+srv://user:pass@cluster.mongodb.net/mon-loyalty",
|
|
37
|
+
required: true,
|
|
38
|
+
default: ""
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: "PORT",
|
|
42
|
+
label: "Server Port",
|
|
43
|
+
hint: "Port to run the API on",
|
|
44
|
+
required: false,
|
|
45
|
+
default: "4000"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: "JWT_SECRET",
|
|
49
|
+
label: "JWT Secret",
|
|
50
|
+
hint: "Any long random string for signing tokens",
|
|
51
|
+
required: false,
|
|
52
|
+
default: "change_me_in_production_" + Math.random().toString(36).slice(2)
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "MONAD_RPC_URL",
|
|
56
|
+
label: "Monad RPC URL",
|
|
57
|
+
hint: "Your Monad node RPC endpoint (required for on-chain withdrawals)",
|
|
58
|
+
required: false,
|
|
59
|
+
default: "https://testnet-rpc.monad.xyz"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "MONAD_PRIVATE_KEY",
|
|
63
|
+
label: "Monad Private Key",
|
|
64
|
+
hint: "Your master wallet private key (used for on-chain settlement ā KEEP SECRET)",
|
|
65
|
+
required: false,
|
|
66
|
+
default: ""
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
key: "MONAD_CONTRACT_ADDRESS",
|
|
70
|
+
label: "MonLoyalty Contract Address",
|
|
71
|
+
hint: "Deployed MonLoyalty smart contract address (leave blank for now)",
|
|
72
|
+
required: false,
|
|
73
|
+
default: ""
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
async function loadExistingEnv() {
|
|
78
|
+
if (!fs.existsSync(ENV_PATH)) return {}
|
|
79
|
+
const content = fs.readFileSync(ENV_PATH, "utf-8")
|
|
80
|
+
const existing = {}
|
|
81
|
+
for (const line of content.split("\n")) {
|
|
82
|
+
const match = line.match(/^([^#=]+)=(.*)$/)
|
|
83
|
+
if (match) existing[match[1].trim()] = match[2].trim().replace(/^"|"$/g, "")
|
|
84
|
+
}
|
|
85
|
+
return existing
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function writeEnv(values) {
|
|
89
|
+
const lines = [
|
|
90
|
+
"# Generated by mon-loyalty-api setup",
|
|
91
|
+
"# Run: npx mon-loyalty-api setup to reconfigure",
|
|
92
|
+
"",
|
|
93
|
+
"# āā Required āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā",
|
|
94
|
+
`MONGODB_URI="${values.MONGODB_URI}"`,
|
|
95
|
+
"",
|
|
96
|
+
"# āā Server āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā",
|
|
97
|
+
`PORT=${values.PORT}`,
|
|
98
|
+
`NODE_ENV=development`,
|
|
99
|
+
"",
|
|
100
|
+
"# āā Auth āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā",
|
|
101
|
+
`JWT_SECRET="${values.JWT_SECRET}"`,
|
|
102
|
+
"",
|
|
103
|
+
"# āā Monad On-Chain (for withdrawal/settlement) ā",
|
|
104
|
+
`MONAD_RPC_URL="${values.MONAD_RPC_URL}"`,
|
|
105
|
+
`MONAD_PRIVATE_KEY="${values.MONAD_PRIVATE_KEY}"`,
|
|
106
|
+
`MONAD_CONTRACT_ADDRESS="${values.MONAD_CONTRACT_ADDRESS}"`,
|
|
107
|
+
""
|
|
108
|
+
]
|
|
109
|
+
fs.writeFileSync(ENV_PATH, lines.join("\n"), "utf-8")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function main() {
|
|
113
|
+
console.log(`\n${BOLD}${CYAN}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`)
|
|
114
|
+
console.log(`ā MON Loyalty API ā Setup Wizard ā`)
|
|
115
|
+
console.log(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${RESET}\n`)
|
|
116
|
+
console.log(` This will create a ${BOLD}.env${RESET} file in: ${YELLOW}${process.cwd()}${RESET}\n`)
|
|
117
|
+
|
|
118
|
+
const existing = await loadExistingEnv()
|
|
119
|
+
const hasExisting = Object.keys(existing).length > 0
|
|
120
|
+
|
|
121
|
+
if (hasExisting) {
|
|
122
|
+
console.log(` ${YELLOW}ā An existing .env was found. Press Enter to keep current values.${RESET}\n`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const values = {}
|
|
126
|
+
|
|
127
|
+
for (const field of fields) {
|
|
128
|
+
const currentVal = existing[field.key] || field.default
|
|
129
|
+
const requiredTag = field.required ? ` ${RED}*required${RESET}` : ""
|
|
130
|
+
console.log(` ${BOLD}${field.label}${RESET}${requiredTag}`)
|
|
131
|
+
console.log(` ${CYAN}${field.hint}${RESET}`)
|
|
132
|
+
|
|
133
|
+
let val = ""
|
|
134
|
+
while (true) {
|
|
135
|
+
val = await ask(` ā Enter value`, currentVal)
|
|
136
|
+
if (field.required && !val) {
|
|
137
|
+
console.log(` ${RED}ā This field is required.${RESET}`)
|
|
138
|
+
} else {
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
values[field.key] = val
|
|
144
|
+
console.log()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
writeEnv(values)
|
|
148
|
+
|
|
149
|
+
console.log(`${GREEN}${BOLD} ā
.env file written successfully!${RESET}\n`)
|
|
150
|
+
console.log(` Next steps:`)
|
|
151
|
+
console.log(` ${CYAN}npm run dev${RESET} ā start the development server`)
|
|
152
|
+
console.log(` ${CYAN}npm run build${RESET} ā build for production`)
|
|
153
|
+
console.log(` ${CYAN}npm start${RESET} ā run production build\n`)
|
|
154
|
+
|
|
155
|
+
rl.close()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
main().catch((err) => {
|
|
159
|
+
console.error(`\n${RED}Setup failed:${RESET}`, err.message)
|
|
160
|
+
rl.close()
|
|
161
|
+
process.exit(1)
|
|
162
|
+
})
|