db-model-router 1.0.6 → 1.0.8

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 (140) hide show
  1. package/README.md +150 -11
  2. package/TODO.md +0 -15
  3. package/db-manager/.dbmanager.sqlite +0 -0
  4. package/db-manager/README.md +223 -0
  5. package/db-manager/adapter-proxy.js +361 -0
  6. package/db-manager/demo/cockroachdb.env +6 -0
  7. package/db-manager/demo/demo.sqlite +0 -0
  8. package/db-manager/demo/dynamodb.env +7 -0
  9. package/db-manager/demo/mongodb.env +4 -0
  10. package/db-manager/demo/mssql.env +6 -0
  11. package/db-manager/demo/mysql.env +6 -0
  12. package/db-manager/demo/oracle.env +6 -0
  13. package/db-manager/demo/postgres.env +6 -0
  14. package/db-manager/demo/redis.env +4 -0
  15. package/db-manager/demo/seeds/cockroachdb.sql +32 -0
  16. package/db-manager/demo/seeds/mssql.sql +32 -0
  17. package/db-manager/demo/seeds/mysql.sql +32 -0
  18. package/db-manager/demo/seeds/oracle.sql +43 -0
  19. package/db-manager/demo/seeds/postgres.sql +32 -0
  20. package/db-manager/demo/seeds/sqlite3.sql +32 -0
  21. package/db-manager/demo/sqlite3.env +2 -0
  22. package/db-manager/metadata-db.js +170 -0
  23. package/db-manager/public/.gitkeep +1 -0
  24. package/db-manager/public/css/style.css +1413 -0
  25. package/db-manager/public/js/app.js +1370 -0
  26. package/db-manager/routes/api.js +388 -0
  27. package/db-manager/routes/views.js +61 -0
  28. package/db-manager/server.js +39 -0
  29. package/db-manager/utils/build-filter-config.js +18 -0
  30. package/db-manager/utils/csv-export.js +59 -0
  31. package/db-manager/utils/export-filename.js +39 -0
  32. package/db-manager/utils/filter-tables.js +20 -0
  33. package/db-manager/utils/parse-filters.js +93 -0
  34. package/db-manager/utils/sort-state.js +35 -0
  35. package/db-manager/views/.gitkeep +1 -0
  36. package/db-manager/views/dashboard.ejs +53 -0
  37. package/db-manager/views/history.ejs +52 -0
  38. package/db-manager/views/index.ejs +35 -0
  39. package/db-manager/views/layout.ejs +31 -0
  40. package/db-manager/views/partials/data-panel.ejs +74 -0
  41. package/db-manager/views/partials/header.ejs +36 -0
  42. package/db-manager/views/partials/sidebar.ejs +30 -0
  43. package/db-manager/views/query.ejs +58 -0
  44. package/dbmr.schema.json +22 -44
  45. package/demo/.dockerignore +7 -0
  46. package/demo/.env.example +15 -0
  47. package/demo/Dockerfile +20 -0
  48. package/demo/app.js +39 -0
  49. package/demo/commons/add_migration.js +43 -0
  50. package/demo/commons/db.js +17 -0
  51. package/demo/commons/migrate.js +68 -0
  52. package/demo/commons/modules.js +18 -0
  53. package/demo/commons/password.js +36 -0
  54. package/demo/commons/security.js +30 -0
  55. package/demo/commons/session.js +13 -0
  56. package/demo/commons/webhook.js +81 -0
  57. package/demo/dbmr.schema.json +338 -0
  58. package/demo/middleware/authenticate.js +14 -0
  59. package/demo/middleware/hasPermission.js +30 -0
  60. package/demo/middleware/logger.js +67 -0
  61. package/demo/middleware/tenantIsolation.js +19 -0
  62. package/demo/migrations/20260510092158_create_migrations_table.sql +6 -0
  63. package/demo/migrations/20260510092159_create_saas_tables.sql +69 -0
  64. package/demo/migrations/20260510092159_create_tables.sql +193 -0
  65. package/demo/models/addresses.js +24 -0
  66. package/demo/models/cart_items.js +20 -0
  67. package/demo/models/carts.js +18 -0
  68. package/demo/models/categories.js +22 -0
  69. package/demo/models/coupons.js +25 -0
  70. package/demo/models/index.js +43 -0
  71. package/demo/models/order_items.js +23 -0
  72. package/demo/models/orders.js +27 -0
  73. package/demo/models/payments.js +23 -0
  74. package/demo/models/product_images.js +20 -0
  75. package/demo/models/product_reviews.js +22 -0
  76. package/demo/models/product_variants.js +22 -0
  77. package/demo/models/products.js +32 -0
  78. package/demo/models/role_permissions.js +17 -0
  79. package/demo/models/roles.js +17 -0
  80. package/demo/models/shipments.js +21 -0
  81. package/demo/models/tenants.js +18 -0
  82. package/demo/models/users.js +23 -0
  83. package/demo/models/webhook_logs.js +22 -0
  84. package/demo/models/webhooks.js +19 -0
  85. package/demo/models/wishlists.js +17 -0
  86. package/demo/openapi.json +7000 -0
  87. package/demo/package-lock.json +2827 -0
  88. package/demo/package.json +42 -0
  89. package/demo/routes/addresses/index.js +10 -0
  90. package/demo/routes/auth/index.js +55 -0
  91. package/demo/routes/carts/cart_items/index.js +11 -0
  92. package/demo/routes/carts/index.js +14 -0
  93. package/demo/routes/categories/index.js +10 -0
  94. package/demo/routes/coupons/index.js +10 -0
  95. package/demo/routes/docs.js +18 -0
  96. package/demo/routes/health.js +35 -0
  97. package/demo/routes/index.js +54 -0
  98. package/demo/routes/orders/index.js +18 -0
  99. package/demo/routes/orders/order_items/index.js +11 -0
  100. package/demo/routes/orders/payments/index.js +11 -0
  101. package/demo/routes/orders/shipments/index.js +11 -0
  102. package/demo/routes/products/index.js +18 -0
  103. package/demo/routes/products/product_images/index.js +11 -0
  104. package/demo/routes/products/product_reviews/index.js +11 -0
  105. package/demo/routes/products/product_variants/index.js +11 -0
  106. package/demo/routes/roles/index.js +75 -0
  107. package/demo/routes/roles/permissions/index.js +47 -0
  108. package/demo/routes/tenants/index.js +45 -0
  109. package/demo/routes/users/index.js +45 -0
  110. package/demo/routes/wishlists/index.js +10 -0
  111. package/demo/seeds/saas-seed.js +329 -0
  112. package/docker-compose.yml +61 -0
  113. package/package.json +120 -113
  114. package/scripts/demo-create.js +1 -1
  115. package/skill/SKILL.md +119 -3
  116. package/src/cli/commands/db-manager.js +134 -0
  117. package/src/cli/commands/generate.js +106 -60
  118. package/src/cli/commands/help.js +0 -1
  119. package/src/cli/generate-route.js +66 -27
  120. package/src/cli/generate-saas-structure.js +129 -0
  121. package/src/cli/init/dependencies.js +1 -1
  122. package/src/cli/init/generators.js +6 -77
  123. package/src/cli/init.js +9 -2
  124. package/src/cli/main.js +8 -1
  125. package/src/cli/saas/generate-saas-middleware.js +110 -0
  126. package/src/cli/saas/generate-saas-migrations.js +480 -0
  127. package/src/cli/saas/generate-saas-models.js +211 -0
  128. package/src/cli/saas/generate-saas-openapi.js +419 -0
  129. package/src/cli/saas/generate-saas-routes.js +435 -0
  130. package/src/cli/saas/generate-saas-seeds.js +243 -0
  131. package/src/cli/saas/generate-saas-tests.js +473 -0
  132. package/src/cli/saas/generate-saas-utils.js +176 -0
  133. package/src/commons/kafka.js +139 -0
  134. package/src/commons/model.js +29 -9
  135. package/src/commons/route.js +6 -6
  136. package/src/index.js +2 -0
  137. package/src/mssql/db.js +41 -3
  138. package/src/mysql/db.js +3 -0
  139. package/src/postgres/db.js +6 -0
  140. package/src/cli/generate-db-manager.js +0 -1573
package/dbmr.schema.json CHANGED
@@ -9,28 +9,6 @@
9
9
  "loki": false
10
10
  },
11
11
  "tables": {
12
- "users": {
13
- "columns": {
14
- "user_id": "auto_increment",
15
- "name": "required|string",
16
- "email": "required|string",
17
- "password_hash": "required|string",
18
- "phone": "string",
19
- "avatar_url": "string",
20
- "role": "required|string",
21
- "is_deleted": "boolean",
22
- "created_at": "datetime",
23
- "updated_at": "datetime"
24
- },
25
- "pk": "user_id",
26
- "unique": ["email"],
27
- "softDelete": "is_deleted",
28
- "timestamps": {
29
- "created_at": "created_at",
30
- "modified_at": "updated_at"
31
- },
32
- "parent": null
33
- },
34
12
  "addresses": {
35
13
  "columns": {
36
14
  "address_id": "auto_increment",
@@ -44,13 +22,13 @@
44
22
  "country": "required|string",
45
23
  "is_default": "boolean",
46
24
  "created_at": "datetime",
47
- "updated_at": "datetime"
25
+ "modified_at": "datetime"
48
26
  },
49
27
  "pk": "address_id",
50
28
  "unique": ["address_id"],
51
29
  "timestamps": {
52
30
  "created_at": "created_at",
53
- "modified_at": "updated_at"
31
+ "modified_at": "modified_at"
54
32
  },
55
33
  "parent": null
56
34
  },
@@ -65,13 +43,13 @@
65
43
  "sort_order": "integer",
66
44
  "is_active": "boolean",
67
45
  "created_at": "datetime",
68
- "updated_at": "datetime"
46
+ "modified_at": "datetime"
69
47
  },
70
48
  "pk": "category_id",
71
49
  "unique": ["slug"],
72
50
  "timestamps": {
73
51
  "created_at": "created_at",
74
- "modified_at": "updated_at"
52
+ "modified_at": "modified_at"
75
53
  },
76
54
  "parent": null
77
55
  },
@@ -97,14 +75,14 @@
97
75
  "is_deleted": "boolean",
98
76
  "meta": "object",
99
77
  "created_at": "datetime",
100
- "updated_at": "datetime"
78
+ "modified_at": "datetime"
101
79
  },
102
80
  "pk": "product_id",
103
81
  "unique": ["sku", "slug"],
104
82
  "softDelete": "is_deleted",
105
83
  "timestamps": {
106
84
  "created_at": "created_at",
107
- "modified_at": "updated_at"
85
+ "modified_at": "modified_at"
108
86
  },
109
87
  "parent": null
110
88
  },
@@ -136,13 +114,13 @@
136
114
  "attributes": "object",
137
115
  "is_active": "boolean",
138
116
  "created_at": "datetime",
139
- "updated_at": "datetime"
117
+ "modified_at": "datetime"
140
118
  },
141
119
  "pk": "variant_id",
142
120
  "unique": ["sku"],
143
121
  "timestamps": {
144
122
  "created_at": "created_at",
145
- "modified_at": "updated_at"
123
+ "modified_at": "modified_at"
146
124
  },
147
125
  "parent": "products"
148
126
  },
@@ -157,13 +135,13 @@
157
135
  "is_verified": "boolean",
158
136
  "is_approved": "boolean",
159
137
  "created_at": "datetime",
160
- "updated_at": "datetime"
138
+ "modified_at": "datetime"
161
139
  },
162
140
  "pk": "review_id",
163
141
  "unique": ["review_id"],
164
142
  "timestamps": {
165
143
  "created_at": "created_at",
166
- "modified_at": "updated_at"
144
+ "modified_at": "modified_at"
167
145
  },
168
146
  "parent": "products"
169
147
  },
@@ -174,13 +152,13 @@
174
152
  "session_id": "string",
175
153
  "currency": "required|string",
176
154
  "created_at": "datetime",
177
- "updated_at": "datetime"
155
+ "modified_at": "datetime"
178
156
  },
179
157
  "pk": "cart_id",
180
158
  "unique": ["cart_id"],
181
159
  "timestamps": {
182
160
  "created_at": "created_at",
183
- "modified_at": "updated_at"
161
+ "modified_at": "modified_at"
184
162
  },
185
163
  "parent": null
186
164
  },
@@ -193,13 +171,13 @@
193
171
  "quantity": "required|integer",
194
172
  "unit_price": "required|numeric",
195
173
  "created_at": "datetime",
196
- "updated_at": "datetime"
174
+ "modified_at": "datetime"
197
175
  },
198
176
  "pk": "cart_item_id",
199
177
  "unique": ["cart_item_id"],
200
178
  "timestamps": {
201
179
  "created_at": "created_at",
202
- "modified_at": "updated_at"
180
+ "modified_at": "modified_at"
203
181
  },
204
182
  "parent": "carts"
205
183
  },
@@ -219,13 +197,13 @@
219
197
  "billing_address_id": "integer",
220
198
  "notes": "string",
221
199
  "created_at": "datetime",
222
- "updated_at": "datetime"
200
+ "modified_at": "datetime"
223
201
  },
224
202
  "pk": "order_id",
225
203
  "unique": ["order_number"],
226
204
  "timestamps": {
227
205
  "created_at": "created_at",
228
- "modified_at": "updated_at"
206
+ "modified_at": "modified_at"
229
207
  },
230
208
  "parent": null
231
209
  },
@@ -261,13 +239,13 @@
261
239
  "status": "required|string",
262
240
  "paid_at": "datetime",
263
241
  "created_at": "datetime",
264
- "updated_at": "datetime"
242
+ "modified_at": "datetime"
265
243
  },
266
244
  "pk": "payment_id",
267
245
  "unique": ["payment_id"],
268
246
  "timestamps": {
269
247
  "created_at": "created_at",
270
- "modified_at": "updated_at"
248
+ "modified_at": "modified_at"
271
249
  },
272
250
  "parent": "orders"
273
251
  },
@@ -281,13 +259,13 @@
281
259
  "shipped_at": "datetime",
282
260
  "delivered_at": "datetime",
283
261
  "created_at": "datetime",
284
- "updated_at": "datetime"
262
+ "modified_at": "datetime"
285
263
  },
286
264
  "pk": "shipment_id",
287
265
  "unique": ["shipment_id"],
288
266
  "timestamps": {
289
267
  "created_at": "created_at",
290
- "modified_at": "updated_at"
268
+ "modified_at": "modified_at"
291
269
  },
292
270
  "parent": "orders"
293
271
  },
@@ -305,13 +283,13 @@
305
283
  "expires_at": "datetime",
306
284
  "is_active": "boolean",
307
285
  "created_at": "datetime",
308
- "updated_at": "datetime"
286
+ "modified_at": "datetime"
309
287
  },
310
288
  "pk": "coupon_id",
311
289
  "unique": ["code"],
312
290
  "timestamps": {
313
291
  "created_at": "created_at",
314
- "modified_at": "updated_at"
292
+ "modified_at": "modified_at"
315
293
  },
316
294
  "parent": null
317
295
  },
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ npm-debug.log
3
+ .env
4
+ .env.example
5
+ .git
6
+ .gitignore
7
+ data
@@ -0,0 +1,15 @@
1
+ # Server
2
+ PORT=3000
3
+ API_BASE_PATH=/api
4
+
5
+ # Database
6
+ DB_TYPE=sqlite3
7
+ DB_NAME=./data/data.db
8
+
9
+ # Session
10
+ SESSION_SECRET=your_session_secret
11
+
12
+ # Logging
13
+ APP_NAME=your_app_name
14
+ LOG_LEVEL=info
15
+ LOKI_HOST=http://your-loki-host:3100
@@ -0,0 +1,20 @@
1
+ FROM node:alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY package*.json ./
7
+ RUN npm ci --omit=dev
8
+
9
+ # Copy application files
10
+ COPY app.js ./
11
+ COPY commons/ ./commons/
12
+ COPY middleware/ ./middleware/
13
+ COPY route/ ./route/
14
+ COPY migrations/ ./migrations/
15
+
16
+ # Expose port
17
+ EXPOSE 3000
18
+
19
+ # Start the application
20
+ CMD ["node", "app.js"]
package/demo/app.js ADDED
@@ -0,0 +1,39 @@
1
+ import express from "express";
2
+ import "./commons/db.js";
3
+ import configureSession from "./commons/session.js";
4
+ import applySecurity from "./commons/security.js";
5
+ import logger from "./middleware/logger.js";
6
+ import routes from "./routes/index.js";
7
+ import { fileURLToPath } from 'node:url';
8
+ import path from "path";
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const app = express();
11
+ const PORT = process.env.PORT || 3000;
12
+
13
+ // Middleware
14
+ app.use(express.json());
15
+ app.use(express.urlencoded({ extended: true }));
16
+
17
+ // Security (helmet, rate limiting, custom headers)
18
+ applySecurity(app);
19
+
20
+ // Session
21
+ app.use(configureSession());
22
+
23
+ // Logger
24
+ app.use(logger);
25
+
26
+ // Routes
27
+ app.use(process.env.API_BASE_PATH || "/api", routes);
28
+
29
+ // Error handler
30
+ app.use((err, req, res, next) => {
31
+ console.error(err.stack);
32
+ res.status(500).json({ type: "danger", message: "Internal Server Error" });
33
+ });
34
+
35
+ app.listen(PORT, () => {
36
+ console.log(`Server running on port ${PORT}`);
37
+ });
38
+
39
+ export default app;
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ /**
9
+ * Create a new timestamped migration file.
10
+ * @param {string} migrationsDir - absolute path to migrations folder
11
+ * @param {string} [name] - migration name (default: "migration")
12
+ * @returns {string} the created filename
13
+ */
14
+ export default function addMigration(migrationsDir, name) {
15
+ const migrationName = name || "migration";
16
+ const now = new Date();
17
+ const y = String(now.getFullYear()).padStart(4, "0");
18
+ const mo = String(now.getMonth() + 1).padStart(2, "0");
19
+ const d = String(now.getDate()).padStart(2, "0");
20
+ const h = String(now.getHours()).padStart(2, "0");
21
+ const mi = String(now.getMinutes()).padStart(2, "0");
22
+ const s = String(now.getSeconds()).padStart(2, "0");
23
+ const ts = `${y}${mo}${d}${h}${mi}${s}`;
24
+
25
+ const filename = `${ts}_${migrationName}.sql`;
26
+ const filePath = path.join(migrationsDir, filename);
27
+
28
+ if (!fs.existsSync(migrationsDir)) {
29
+ fs.mkdirSync(migrationsDir, { recursive: true });
30
+ }
31
+
32
+ fs.writeFileSync(filePath, "-- Write your migration SQL here\n");
33
+ console.log(`Created migration: ${filename}`);
34
+ return filename;
35
+ }
36
+
37
+ // Run as standalone script
38
+ const isMain = process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
39
+ if (isMain) {
40
+ const migrationsDir = path.join(__dirname, "../migrations");
41
+ const name = process.argv[2] || "migration";
42
+ addMigration(migrationsDir, name);
43
+ }
@@ -0,0 +1,17 @@
1
+ import "dotenv/config";
2
+ import dbModelRouter from "db-model-router";
3
+
4
+ // Initialize database adapter
5
+ dbModelRouter.init("sqlite3");
6
+
7
+ // Connect to database
8
+ dbModelRouter.db.connect({
9
+ database: process.env.DB_NAME || "./data/data.db",
10
+ });
11
+
12
+ // Make db available globally across the application
13
+ const db = dbModelRouter.db;
14
+ global.db = db;
15
+
16
+ export { db };
17
+ export default db;
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import crypto from "crypto";
5
+ import { fileURLToPath } from "url";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ /**
10
+ * Run all pending SQL migrations from the migrations directory.
11
+ * @param {object} db - db-model-router db instance
12
+ * @param {string} migrationsDir - absolute path to migrations folder
13
+ */
14
+ export default async function runMigrations(db, migrationsDir) {
15
+ const files = fs.readdirSync(migrationsDir)
16
+ .filter(f => f.endsWith(".sql"))
17
+ .sort();
18
+
19
+ let executed;
20
+ try {
21
+ const result = await db.query("SELECT filename FROM _migrations");
22
+ executed = new Set((result || []).map(r => r.filename));
23
+ } catch (e) {
24
+ executed = new Set();
25
+ }
26
+
27
+ let ran = 0;
28
+ for (const file of files) {
29
+ if (executed.has(file)) {
30
+ console.log(` Skipping (already executed): ${file}`);
31
+ continue;
32
+ }
33
+ const filePath = path.join(migrationsDir, file);
34
+ const content = fs.readFileSync(filePath, "utf8");
35
+ const checksum = crypto.createHash("md5").update(content).digest("hex");
36
+
37
+ console.log(` Running migration: ${file}`);
38
+ await db.query(content);
39
+ await db.query(
40
+ "INSERT INTO _migrations (filename, checksum) VALUES (?, ?)",
41
+ [file, checksum]
42
+ );
43
+ console.log(` Completed: ${file}`);
44
+ ran++;
45
+ }
46
+
47
+ if (ran === 0) {
48
+ console.log("No pending migrations.");
49
+ } else {
50
+ console.log(`\n${ran} migration(s) complete.`);
51
+ }
52
+ }
53
+
54
+ // Run as standalone script
55
+ const isMain = process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
56
+ if (isMain) {
57
+ await import("dotenv/config");
58
+ const pkg = await import("db-model-router");
59
+ const mod = pkg.default || pkg;
60
+ mod.init("sqlite3");
61
+ mod.db.connect({
62
+ database: process.env.DB_NAME || "./data/data.db",
63
+ });
64
+ const migrationsDir = path.join(__dirname, "../migrations");
65
+ runMigrations(mod.db, migrationsDir)
66
+ .then(() => process.exit(0))
67
+ .catch(err => { console.error("Migration failed:", err); process.exit(1); });
68
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Registry of all SaaS module names.
3
+ * This is the single source of truth for valid module identifiers
4
+ * used by the permission system.
5
+ *
6
+ * @type {string[]}
7
+ */
8
+ export const modules = ["users", "tenants", "roles", "permissions", "webhooks"];
9
+
10
+ /**
11
+ * Check whether a given name is a registered module.
12
+ *
13
+ * @param {string} name - The module name to validate
14
+ * @returns {boolean} True if the name exists in the modules registry
15
+ */
16
+ export function isValidModule(name) {
17
+ return modules.includes(name);
18
+ }
@@ -0,0 +1,36 @@
1
+ import crypto from "crypto";
2
+
3
+ /**
4
+ * Hash a password using scrypt with a random salt.
5
+ * Returns a string in the format "salt:derivedKey" (both hex-encoded).
6
+ *
7
+ * @param {string} password - The plaintext password to hash
8
+ * @returns {Promise<string>} The hashed password string
9
+ */
10
+ export function hashPassword(password) {
11
+ const salt = crypto.randomBytes(16).toString("hex");
12
+ return new Promise((resolve, reject) => {
13
+ crypto.scrypt(password, salt, 64, (err, derivedKey) => {
14
+ if (err) reject(err);
15
+ resolve(salt + ":" + derivedKey.toString("hex"));
16
+ });
17
+ });
18
+ }
19
+
20
+ /**
21
+ * Verify a password against a previously hashed value.
22
+ * Uses timing-safe comparison to prevent timing attacks.
23
+ *
24
+ * @param {string} password - The plaintext password to verify
25
+ * @param {string} hash - The stored hash in "salt:derivedKey" format
26
+ * @returns {Promise<boolean>} True if the password matches, false otherwise
27
+ */
28
+ export function verifyPassword(password, hash) {
29
+ const [salt, key] = hash.split(":");
30
+ return new Promise((resolve, reject) => {
31
+ crypto.scrypt(password, salt, 64, (err, derivedKey) => {
32
+ if (err) reject(err);
33
+ resolve(crypto.timingSafeEqual(Buffer.from(key, "hex"), derivedKey));
34
+ });
35
+ });
36
+ }
@@ -0,0 +1,30 @@
1
+ import helmet from "helmet";
2
+ import rateLimit from "express-rate-limit";
3
+
4
+ /**
5
+ * Apply security middleware to the Express app.
6
+ * Includes: Helmet, rate limiting, custom security headers.
7
+ * @param {import("express").Application} app
8
+ */
9
+ export default function applySecurity(app) {
10
+ // Helmet — sets various HTTP headers for security
11
+ app.use(helmet());
12
+
13
+ // Rate limiting
14
+ app.use(rateLimit({
15
+ windowMs: 15 * 60 * 1000,
16
+ max: 100,
17
+ standardHeaders: true,
18
+ legacyHeaders: false,
19
+ }));
20
+
21
+ // Custom security headers (override or extend as needed)
22
+ app.use((req, res, next) => {
23
+ res.setHeader("X-Content-Type-Options", "nosniff");
24
+ res.setHeader("X-Frame-Options", "DENY");
25
+ res.setHeader("X-XSS-Protection", "1; mode=block");
26
+ res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
27
+ res.removeHeader("X-Powered-By");
28
+ next();
29
+ });
30
+ }
@@ -0,0 +1,13 @@
1
+ import session from "express-session";
2
+
3
+ /**
4
+ * Configure and return session middleware.
5
+ * Session store: memory
6
+ */
7
+ export default function configureSession() {
8
+ return session({
9
+ secret: process.env.SESSION_SECRET || "change-me",
10
+ resave: false,
11
+ saveUninitialized: false,
12
+ });
13
+ }
@@ -0,0 +1,81 @@
1
+ import crypto from "crypto";
2
+
3
+ /**
4
+ * Retry delay schedule in seconds.
5
+ * Attempt 0: immediate, 1: 1 min, 2: 5 min, 3: 1 hour, 4: 1 day.
6
+ *
7
+ * @type {number[]}
8
+ */
9
+ export const RETRY_DELAYS = [0, 60, 300, 3600, 86400];
10
+
11
+ /**
12
+ * Sign a webhook payload using HMAC-SHA256.
13
+ *
14
+ * @param {object} payload - The payload object to sign
15
+ * @param {string} secret - The tenant's webhook secret
16
+ * @returns {string} Hex-encoded HMAC-SHA256 signature
17
+ */
18
+ export function signPayload(payload, secret) {
19
+ const body = JSON.stringify(payload);
20
+ return crypto.createHmac("sha256", secret).update(body).digest("hex");
21
+ }
22
+
23
+ /**
24
+ * Look up the configured webhook for a tenant.
25
+ * TODO: Replace this stub with actual database lookup.
26
+ */
27
+ export async function lookupWebhook(tenantId) {
28
+ return null;
29
+ }
30
+
31
+ /**
32
+ * Log a webhook delivery event.
33
+ * TODO: Replace this stub with actual database insert into webhook_logs.
34
+ */
35
+ export async function logWebhookEvent(webhookId, tenantId, eventType, payload, status, responseBody, responseStatusCode) {
36
+ // Stub: replace with actual DB insert
37
+ }
38
+
39
+ /**
40
+ * Delay execution for the specified number of milliseconds.
41
+ */
42
+ export function delay(ms) {
43
+ return new Promise((resolve) => setTimeout(resolve, ms));
44
+ }
45
+
46
+ /**
47
+ * Send a webhook notification to the configured endpoint for a tenant.
48
+ * Retries delivery up to 5 times with exponential backoff.
49
+ */
50
+ export async function sendWebhook(tenantId, event, context) {
51
+ const webhook = await lookupWebhook(tenantId);
52
+ if (!webhook) return;
53
+
54
+ const payload = { context, event, timestamp: new Date().toISOString() };
55
+ payload.signature = signPayload(payload, webhook.secret);
56
+
57
+ for (let attempt = 0; attempt < RETRY_DELAYS.length; attempt++) {
58
+ if (attempt > 0) {
59
+ await delay(RETRY_DELAYS[attempt] * 1000);
60
+ console.log(`Webhook retry attempt ${attempt}, delay: ${RETRY_DELAYS[attempt]}s`);
61
+ }
62
+ try {
63
+ const response = await fetch(webhook.url, {
64
+ method: "POST",
65
+ headers: { "Content-Type": "application/json", "X-Webhook-Key": webhook.key },
66
+ body: JSON.stringify(payload),
67
+ });
68
+ await logWebhookEvent(
69
+ webhook.id, tenantId, event.type, payload,
70
+ response.ok ? "success" : "failed",
71
+ await response.text(), response.status
72
+ );
73
+ if (response.ok) return;
74
+ } catch (err) {
75
+ await logWebhookEvent(
76
+ webhook.id, tenantId, event.type, payload,
77
+ "error", err.message, null
78
+ );
79
+ }
80
+ }
81
+ }