@terreno/api 0.13.3 → 0.14.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/dist/__tests__/versionCheckPlugin.test.js +53 -3
- package/dist/api.arrayOperations.test.js +1 -0
- package/dist/api.d.ts +15 -4
- package/dist/api.errors.test.js +1 -0
- package/dist/api.hooks.test.js +1 -0
- package/dist/api.js +153 -104
- package/dist/api.query.test.js +1 -0
- package/dist/api.test.js +174 -0
- package/dist/auth.d.ts +10 -5
- package/dist/auth.js +163 -90
- package/dist/auth.test.js +159 -0
- package/dist/betterAuthApp.test.js +1 -0
- package/dist/betterAuthSetup.d.ts +5 -6
- package/dist/betterAuthSetup.js +17 -14
- package/dist/betterAuthSetup.test.js +1 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +248 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +328 -0
- package/dist/configuration.test.js +1 -0
- package/dist/configurationApp.d.ts +1 -1
- package/dist/configurationApp.js +17 -13
- package/dist/configurationPlugin.test.js +1 -0
- package/dist/consentApp.test.js +1 -0
- package/dist/envConfigurationPlugin.d.ts +2 -0
- package/dist/envConfigurationPlugin.js +173 -0
- package/dist/envConfigurationPlugin.test.d.ts +1 -0
- package/dist/envConfigurationPlugin.test.js +322 -0
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +106 -10
- package/dist/errors.test.js +16 -1
- package/dist/example.js +16 -7
- package/dist/expressServer.d.ts +10 -9
- package/dist/expressServer.js +62 -53
- package/dist/expressServer.test.js +53 -2
- package/dist/githubAuth.d.ts +2 -1
- package/dist/githubAuth.js +41 -26
- package/dist/githubAuth.test.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +42 -20
- package/dist/models/versionConfig.d.ts +2 -0
- package/dist/models/versionConfig.js +8 -0
- package/dist/notifiers/googleChatNotifier.js +14 -16
- package/dist/notifiers/googleChatNotifier.test.js +1 -0
- package/dist/notifiers/slackNotifier.js +16 -14
- package/dist/notifiers/slackNotifier.test.js +41 -3
- package/dist/notifiers/zoomNotifier.js +7 -10
- package/dist/notifiers/zoomNotifier.test.js +1 -0
- package/dist/openApi.d.ts +1 -1
- package/dist/openApi.test.js +1 -0
- package/dist/openApiBuilder.d.ts +39 -6
- package/dist/openApiBuilder.js +1 -31
- package/dist/openApiBuilder.test.js +1 -0
- package/dist/openApiValidator.js +1 -0
- package/dist/openApiValidator.test.js +1 -0
- package/dist/permissions.d.ts +4 -4
- package/dist/permissions.js +67 -65
- package/dist/permissions.middleware.test.js +1 -0
- package/dist/permissions.test.js +1 -0
- package/dist/plugins.d.ts +5 -5
- package/dist/plugins.js +18 -9
- package/dist/plugins.test.js +1 -1
- package/dist/populate.d.ts +15 -8
- package/dist/populate.js +23 -24
- package/dist/populate.test.js +1 -0
- package/dist/realtime/changeStreamWatcher.d.ts +73 -0
- package/dist/realtime/changeStreamWatcher.js +720 -0
- package/dist/realtime/index.d.ts +6 -0
- package/dist/realtime/index.js +27 -0
- package/dist/realtime/queryMatcher.d.ts +14 -0
- package/dist/realtime/queryMatcher.js +250 -0
- package/dist/realtime/queryStore.d.ts +37 -0
- package/dist/realtime/queryStore.js +195 -0
- package/dist/realtime/realtime.test.d.ts +10 -0
- package/dist/realtime/realtime.test.js +2158 -0
- package/dist/realtime/realtimeApp.d.ts +93 -0
- package/dist/realtime/realtimeApp.js +560 -0
- package/dist/realtime/registry.d.ts +40 -0
- package/dist/realtime/registry.js +38 -0
- package/dist/realtime/socketUser.d.ts +10 -0
- package/dist/realtime/socketUser.js +17 -0
- package/dist/realtime/types.d.ts +100 -0
- package/dist/realtime/types.js +2 -0
- package/dist/requestContext.d.ts +37 -0
- package/dist/requestContext.js +344 -0
- package/dist/requestContext.test.d.ts +1 -0
- package/dist/requestContext.test.js +241 -0
- package/dist/terrenoApp.d.ts +8 -0
- package/dist/terrenoApp.js +50 -13
- package/dist/terrenoApp.test.js +194 -21
- package/dist/terrenoPlugin.d.ts +11 -0
- package/dist/tests/bunSetup.js +1 -0
- package/dist/tests.js +1 -1
- package/dist/transformers.d.ts +2 -2
- package/dist/transformers.js +5 -3
- package/dist/transformers.test.js +90 -0
- package/dist/types/consentResponse.d.ts +6 -3
- package/dist/versionCheckPlugin.d.ts +2 -0
- package/dist/versionCheckPlugin.js +18 -12
- package/package.json +4 -2
- package/src/__tests__/versionCheckPlugin.test.ts +37 -3
- package/src/api.arrayOperations.test.ts +1 -0
- package/src/api.errors.test.ts +1 -0
- package/src/api.hooks.test.ts +1 -0
- package/src/api.query.test.ts +1 -0
- package/src/api.test.ts +132 -0
- package/src/api.ts +199 -84
- package/src/auth.test.ts +160 -0
- package/src/auth.ts +120 -50
- package/src/betterAuthApp.test.ts +1 -0
- package/src/betterAuthSetup.test.ts +1 -0
- package/src/betterAuthSetup.ts +46 -19
- package/src/config.test.ts +255 -0
- package/src/config.ts +206 -0
- package/src/configuration.test.ts +1 -0
- package/src/configurationApp.ts +59 -24
- package/src/configurationPlugin.test.ts +1 -0
- package/src/consentApp.test.ts +1 -0
- package/src/envConfigurationPlugin.test.ts +143 -0
- package/src/envConfigurationPlugin.ts +100 -0
- package/src/errors.test.ts +19 -1
- package/src/errors.ts +94 -20
- package/src/example.ts +46 -21
- package/src/express.d.ts +18 -1
- package/src/expressServer.test.ts +50 -2
- package/src/expressServer.ts +80 -50
- package/src/githubAuth.test.ts +1 -0
- package/src/githubAuth.ts +59 -38
- package/src/index.ts +4 -0
- package/src/logger.ts +47 -17
- package/src/models/versionConfig.ts +13 -2
- package/src/notifiers/googleChatNotifier.test.ts +1 -0
- package/src/notifiers/googleChatNotifier.ts +7 -9
- package/src/notifiers/slackNotifier.test.ts +29 -3
- package/src/notifiers/slackNotifier.ts +9 -7
- package/src/notifiers/zoomNotifier.test.ts +1 -0
- package/src/notifiers/zoomNotifier.ts +8 -11
- package/src/openApi.test.ts +1 -0
- package/src/openApi.ts +4 -4
- package/src/openApiBuilder.test.ts +1 -0
- package/src/openApiBuilder.ts +14 -11
- package/src/openApiValidator.test.ts +1 -0
- package/src/openApiValidator.ts +3 -2
- package/src/permissions.middleware.test.ts +1 -0
- package/src/permissions.test.ts +1 -0
- package/src/permissions.ts +30 -25
- package/src/plugins.test.ts +1 -1
- package/src/plugins.ts +21 -14
- package/src/populate.test.ts +1 -0
- package/src/populate.ts +44 -36
- package/src/realtime/changeStreamWatcher.ts +568 -0
- package/src/realtime/index.ts +34 -0
- package/src/realtime/queryMatcher.ts +179 -0
- package/src/realtime/queryStore.ts +132 -0
- package/src/realtime/realtime.test.ts +1755 -0
- package/src/realtime/realtimeApp.ts +478 -0
- package/src/realtime/registry.ts +64 -0
- package/src/realtime/socketUser.ts +25 -0
- package/src/realtime/types.ts +112 -0
- package/src/requestContext.test.ts +196 -0
- package/src/requestContext.ts +368 -0
- package/src/terrenoApp.test.ts +137 -11
- package/src/terrenoApp.ts +64 -17
- package/src/terrenoPlugin.ts +12 -0
- package/src/tests/bunSetup.ts +1 -0
- package/src/tests.ts +7 -2
- package/src/transformers.test.ts +70 -2
- package/src/transformers.ts +15 -7
- package/src/types/consentResponse.ts +8 -10
- package/src/versionCheckPlugin.ts +15 -7
package/dist/api.test.js
CHANGED
|
@@ -88,7 +88,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
88
88
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
89
89
|
};
|
|
90
90
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
91
|
+
// biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
|
|
92
|
+
// biome-ignore-all lint/suspicious/noImplicitAnyLet: test mock typing
|
|
91
93
|
var bun_test_1 = require("bun:test");
|
|
94
|
+
var luxon_1 = require("luxon");
|
|
92
95
|
var supertest_1 = __importDefault(require("supertest"));
|
|
93
96
|
var api_1 = require("./api");
|
|
94
97
|
var auth_1 = require("./auth");
|
|
@@ -2405,4 +2408,175 @@ var transformers_1 = require("./transformers");
|
|
|
2405
2408
|
});
|
|
2406
2409
|
}); });
|
|
2407
2410
|
});
|
|
2411
|
+
(0, bun_test_1.describe)("conflict detection (If-Unmodified-Since)", function () {
|
|
2412
|
+
var admin;
|
|
2413
|
+
var _notAdmin;
|
|
2414
|
+
var agent;
|
|
2415
|
+
var spinach;
|
|
2416
|
+
(0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
2417
|
+
var _a;
|
|
2418
|
+
return __generator(this, function (_b) {
|
|
2419
|
+
switch (_b.label) {
|
|
2420
|
+
case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
|
|
2421
|
+
case 1:
|
|
2422
|
+
_a = __read.apply(void 0, [_b.sent(), 2]), admin = _a[0], _notAdmin = _a[1];
|
|
2423
|
+
return [4 /*yield*/, tests_1.FoodModel.create({
|
|
2424
|
+
calories: 10,
|
|
2425
|
+
created: luxon_1.DateTime.fromISO("2025-06-15T12:00:00.000Z").toJSDate(),
|
|
2426
|
+
hidden: false,
|
|
2427
|
+
name: "Spinach",
|
|
2428
|
+
ownerId: admin._id,
|
|
2429
|
+
})];
|
|
2430
|
+
case 2:
|
|
2431
|
+
spinach = _b.sent();
|
|
2432
|
+
return [4 /*yield*/, tests_1.FoodModel.collection.updateOne({ _id: spinach._id }, { $set: { updated: luxon_1.DateTime.fromISO("2025-06-15T12:00:00.000Z").toJSDate() } })];
|
|
2433
|
+
case 3:
|
|
2434
|
+
_b.sent();
|
|
2435
|
+
app = (0, tests_1.getBaseServer)();
|
|
2436
|
+
(0, auth_1.setupAuth)(app, tests_1.UserModel);
|
|
2437
|
+
(0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
|
|
2438
|
+
app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
|
|
2439
|
+
permissions: {
|
|
2440
|
+
create: [permissions_1.Permissions.IsAny],
|
|
2441
|
+
delete: [permissions_1.Permissions.IsAny],
|
|
2442
|
+
list: [permissions_1.Permissions.IsAny],
|
|
2443
|
+
read: [permissions_1.Permissions.IsAny],
|
|
2444
|
+
update: [permissions_1.Permissions.IsAny],
|
|
2445
|
+
},
|
|
2446
|
+
}));
|
|
2447
|
+
server = (0, supertest_1.default)(app);
|
|
2448
|
+
return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
|
|
2449
|
+
case 4:
|
|
2450
|
+
agent = _b.sent();
|
|
2451
|
+
return [2 /*return*/];
|
|
2452
|
+
}
|
|
2453
|
+
});
|
|
2454
|
+
}); });
|
|
2455
|
+
(0, bun_test_1.it)("returns 409 when If-Unmodified-Since is older than doc.updated", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
2456
|
+
var staleTimestamp, res;
|
|
2457
|
+
return __generator(this, function (_a) {
|
|
2458
|
+
switch (_a.label) {
|
|
2459
|
+
case 0:
|
|
2460
|
+
staleTimestamp = luxon_1.DateTime.fromISO("2025-06-15T11:00:00.000Z").toHTTP();
|
|
2461
|
+
return [4 /*yield*/, agent
|
|
2462
|
+
.patch("/food/".concat(spinach._id))
|
|
2463
|
+
.set("If-Unmodified-Since", staleTimestamp)
|
|
2464
|
+
.send({ name: "Should Fail" })
|
|
2465
|
+
.expect(409)];
|
|
2466
|
+
case 1:
|
|
2467
|
+
res = _a.sent();
|
|
2468
|
+
(0, bun_test_1.expect)(res.body.error).toBe("Conflict");
|
|
2469
|
+
(0, bun_test_1.expect)(res.body.message).toBe("Document was modified since your last read");
|
|
2470
|
+
(0, bun_test_1.expect)(res.body.data).toBeDefined();
|
|
2471
|
+
// The response should contain the current server version
|
|
2472
|
+
(0, bun_test_1.expect)(res.body.data.name).toBe("Spinach");
|
|
2473
|
+
return [2 /*return*/];
|
|
2474
|
+
}
|
|
2475
|
+
});
|
|
2476
|
+
}); });
|
|
2477
|
+
(0, bun_test_1.it)("succeeds when If-Unmodified-Since matches or is newer than doc.updated", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
2478
|
+
var freshTimestamp, res;
|
|
2479
|
+
return __generator(this, function (_a) {
|
|
2480
|
+
switch (_a.label) {
|
|
2481
|
+
case 0:
|
|
2482
|
+
freshTimestamp = luxon_1.DateTime.fromISO("2025-06-15T13:00:00.000Z").toHTTP();
|
|
2483
|
+
return [4 /*yield*/, agent
|
|
2484
|
+
.patch("/food/".concat(spinach._id))
|
|
2485
|
+
.set("If-Unmodified-Since", freshTimestamp)
|
|
2486
|
+
.send({ name: "Updated Spinach" })
|
|
2487
|
+
.expect(200)];
|
|
2488
|
+
case 1:
|
|
2489
|
+
res = _a.sent();
|
|
2490
|
+
(0, bun_test_1.expect)(res.body.data.name).toBe("Updated Spinach");
|
|
2491
|
+
return [2 /*return*/];
|
|
2492
|
+
}
|
|
2493
|
+
});
|
|
2494
|
+
}); });
|
|
2495
|
+
(0, bun_test_1.it)("succeeds normally when If-Unmodified-Since header is not present", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
2496
|
+
var res;
|
|
2497
|
+
return __generator(this, function (_a) {
|
|
2498
|
+
switch (_a.label) {
|
|
2499
|
+
case 0: return [4 /*yield*/, agent
|
|
2500
|
+
.patch("/food/".concat(spinach._id))
|
|
2501
|
+
.send({ name: "No Header Update" })
|
|
2502
|
+
.expect(200)];
|
|
2503
|
+
case 1:
|
|
2504
|
+
res = _a.sent();
|
|
2505
|
+
(0, bun_test_1.expect)(res.body.data.name).toBe("No Header Update");
|
|
2506
|
+
return [2 /*return*/];
|
|
2507
|
+
}
|
|
2508
|
+
});
|
|
2509
|
+
}); });
|
|
2510
|
+
(0, bun_test_1.it)("succeeds when If-Unmodified-Since exactly matches doc.updated", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
2511
|
+
var exactTimestamp, res;
|
|
2512
|
+
return __generator(this, function (_a) {
|
|
2513
|
+
switch (_a.label) {
|
|
2514
|
+
case 0:
|
|
2515
|
+
exactTimestamp = luxon_1.DateTime.fromISO("2025-06-15T12:00:00.000Z").toHTTP();
|
|
2516
|
+
return [4 /*yield*/, agent
|
|
2517
|
+
.patch("/food/".concat(spinach._id))
|
|
2518
|
+
.set("If-Unmodified-Since", exactTimestamp)
|
|
2519
|
+
.send({ name: "Exact Match" })
|
|
2520
|
+
.expect(200)];
|
|
2521
|
+
case 1:
|
|
2522
|
+
res = _a.sent();
|
|
2523
|
+
(0, bun_test_1.expect)(res.body.data.name).toBe("Exact Match");
|
|
2524
|
+
return [2 /*return*/];
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
}); });
|
|
2528
|
+
(0, bun_test_1.it)("prefers precise conflict timestamp header when present", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
2529
|
+
var roundedStaleTimestamp, res;
|
|
2530
|
+
return __generator(this, function (_a) {
|
|
2531
|
+
switch (_a.label) {
|
|
2532
|
+
case 0:
|
|
2533
|
+
roundedStaleTimestamp = luxon_1.DateTime.fromISO("2025-06-15T11:59:59.000Z").toHTTP();
|
|
2534
|
+
return [4 /*yield*/, agent
|
|
2535
|
+
.patch("/food/".concat(spinach._id))
|
|
2536
|
+
.set("If-Unmodified-Since", roundedStaleTimestamp)
|
|
2537
|
+
.set("X-Unmodified-Since-ISO", "2025-06-15T12:00:00.750Z")
|
|
2538
|
+
.send({ name: "Precise Match" })
|
|
2539
|
+
.expect(200)];
|
|
2540
|
+
case 1:
|
|
2541
|
+
res = _a.sent();
|
|
2542
|
+
(0, bun_test_1.expect)(res.body.data.name).toBe("Precise Match");
|
|
2543
|
+
return [2 /*return*/];
|
|
2544
|
+
}
|
|
2545
|
+
});
|
|
2546
|
+
}); });
|
|
2547
|
+
(0, bun_test_1.it)("returns 409 when precise conflict timestamp is older than doc.updated", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
2548
|
+
return __generator(this, function (_a) {
|
|
2549
|
+
switch (_a.label) {
|
|
2550
|
+
case 0: return [4 /*yield*/, agent
|
|
2551
|
+
.patch("/food/".concat(spinach._id))
|
|
2552
|
+
.set("If-Unmodified-Since", luxon_1.DateTime.fromISO("2025-06-15T12:00:01.000Z").toHTTP())
|
|
2553
|
+
.set("X-Unmodified-Since-ISO", "2025-06-15T11:59:59.500Z")
|
|
2554
|
+
.send({ name: "Precise Stale" })
|
|
2555
|
+
.expect(409)];
|
|
2556
|
+
case 1:
|
|
2557
|
+
_a.sent();
|
|
2558
|
+
return [2 /*return*/];
|
|
2559
|
+
}
|
|
2560
|
+
});
|
|
2561
|
+
}); });
|
|
2562
|
+
(0, bun_test_1.it)("falls back to doc.created when doc.updated is unavailable", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
2563
|
+
var res;
|
|
2564
|
+
return __generator(this, function (_a) {
|
|
2565
|
+
switch (_a.label) {
|
|
2566
|
+
case 0: return [4 /*yield*/, tests_1.FoodModel.collection.updateOne({ _id: spinach._id }, { $unset: { updated: "" } })];
|
|
2567
|
+
case 1:
|
|
2568
|
+
_a.sent();
|
|
2569
|
+
return [4 /*yield*/, agent
|
|
2570
|
+
.patch("/food/".concat(spinach._id))
|
|
2571
|
+
.set("If-Unmodified-Since", luxon_1.DateTime.fromISO("2025-06-15T11:59:59.999Z").toHTTP())
|
|
2572
|
+
.send({ name: "Created Fallback" })
|
|
2573
|
+
.expect(409)];
|
|
2574
|
+
case 2:
|
|
2575
|
+
res = _a.sent();
|
|
2576
|
+
(0, bun_test_1.expect)(res.body.data.name).toBe("Spinach");
|
|
2577
|
+
return [2 /*return*/];
|
|
2578
|
+
}
|
|
2579
|
+
});
|
|
2580
|
+
}); });
|
|
2581
|
+
});
|
|
2408
2582
|
});
|
package/dist/auth.d.ts
CHANGED
|
@@ -13,14 +13,17 @@ export interface User {
|
|
|
13
13
|
}
|
|
14
14
|
export interface UserModel extends Model<User> {
|
|
15
15
|
createAnonymousUser?: (id?: string) => Promise<User>;
|
|
16
|
-
postCreate?: (body:
|
|
16
|
+
postCreate?: (body: Record<string, unknown>) => Promise<void>;
|
|
17
17
|
createStrategy(): any;
|
|
18
18
|
serializeUser(): any;
|
|
19
19
|
deserializeUser(): any;
|
|
20
20
|
findByUsername(username: string, findOpts: any): any;
|
|
21
21
|
}
|
|
22
|
-
export
|
|
23
|
-
|
|
22
|
+
export interface GenerateTokensOptions {
|
|
23
|
+
sessionId?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function authenticateMiddleware(anonymous?: boolean): (req: express.Request, res: express.Response, next: express.NextFunction) => any;
|
|
26
|
+
export declare function signupUser(userModel: UserModel, email: string, password: string, body?: Record<string, unknown>): Promise<any>;
|
|
24
27
|
/**
|
|
25
28
|
* Generates both an access token (JWT) and a refresh token for a given user.
|
|
26
29
|
*
|
|
@@ -38,11 +41,13 @@ export declare function signupUser(userModel: UserModel, email: string, password
|
|
|
38
41
|
* authentication providers) to reuse and customize the same token generation logic.
|
|
39
42
|
* This ensures consistent and secure token issuance across different authentication flows.
|
|
40
43
|
*/
|
|
41
|
-
export declare const generateTokens: (user:
|
|
44
|
+
export declare const generateTokens: (user: unknown, authOptions?: AuthOptions, options?: GenerateTokensOptions) => Promise<{
|
|
42
45
|
refreshToken: null;
|
|
43
46
|
token: null;
|
|
47
|
+
sessionId?: undefined;
|
|
44
48
|
} | {
|
|
45
|
-
refreshToken:
|
|
49
|
+
refreshToken: string | undefined;
|
|
50
|
+
sessionId: string;
|
|
46
51
|
token: string;
|
|
47
52
|
}>;
|
|
48
53
|
export declare function setupAuth(app: express.Application, userModel: UserModel): void;
|
package/dist/auth.js
CHANGED
|
@@ -57,6 +57,31 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
57
57
|
}
|
|
58
58
|
return t;
|
|
59
59
|
};
|
|
60
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
61
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
62
|
+
if (!m) return o;
|
|
63
|
+
var i = m.call(o), r, ar = [], e;
|
|
64
|
+
try {
|
|
65
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
66
|
+
}
|
|
67
|
+
catch (error) { e = { error: error }; }
|
|
68
|
+
finally {
|
|
69
|
+
try {
|
|
70
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
71
|
+
}
|
|
72
|
+
finally { if (e) throw e.error; }
|
|
73
|
+
}
|
|
74
|
+
return ar;
|
|
75
|
+
};
|
|
76
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
77
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
78
|
+
if (ar || !(i in from)) {
|
|
79
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
80
|
+
ar[i] = from[i];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
84
|
+
};
|
|
60
85
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
61
86
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
62
87
|
};
|
|
@@ -67,6 +92,7 @@ exports.signupUser = signupUser;
|
|
|
67
92
|
exports.setupAuth = setupAuth;
|
|
68
93
|
exports.addAuthRoutes = addAuthRoutes;
|
|
69
94
|
exports.addMeRoutes = addMeRoutes;
|
|
95
|
+
var node_crypto_1 = require("node:crypto");
|
|
70
96
|
var express_1 = __importDefault(require("express"));
|
|
71
97
|
var jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
72
98
|
var ms_1 = __importDefault(require("ms"));
|
|
@@ -76,6 +102,7 @@ var passport_jwt_1 = require("passport-jwt");
|
|
|
76
102
|
var passport_local_1 = require("passport-local");
|
|
77
103
|
var errors_1 = require("./errors");
|
|
78
104
|
var logger_1 = require("./logger");
|
|
105
|
+
var requestContext_1 = require("./requestContext");
|
|
79
106
|
function authenticateMiddleware(anonymous) {
|
|
80
107
|
if (anonymous === void 0) { anonymous = false; }
|
|
81
108
|
var strategies = ["jwt"];
|
|
@@ -96,36 +123,37 @@ function authenticateMiddleware(anonymous) {
|
|
|
96
123
|
}
|
|
97
124
|
function signupUser(userModel, email, password, body) {
|
|
98
125
|
return __awaiter(this, void 0, void 0, function () {
|
|
99
|
-
var _email, _password, bodyRest, user, error_1, error_2;
|
|
100
|
-
return __generator(this, function (
|
|
101
|
-
switch (
|
|
126
|
+
var _a, _email, _password, bodyRest, user, error_1, error_2, message;
|
|
127
|
+
return __generator(this, function (_b) {
|
|
128
|
+
switch (_b.label) {
|
|
102
129
|
case 0:
|
|
103
|
-
_email =
|
|
104
|
-
|
|
130
|
+
_a = body !== null && body !== void 0 ? body : {}, _email = _a.email, _password = _a.password, bodyRest = __rest(_a, ["email", "password"]);
|
|
131
|
+
_b.label = 1;
|
|
105
132
|
case 1:
|
|
106
|
-
|
|
133
|
+
_b.trys.push([1, 8, , 9]);
|
|
107
134
|
return [4 /*yield*/, userModel.register(__assign({ email: email }, bodyRest), password)];
|
|
108
135
|
case 2:
|
|
109
|
-
user =
|
|
136
|
+
user = _b.sent();
|
|
110
137
|
if (!user.postCreate) return [3 /*break*/, 6];
|
|
111
|
-
|
|
138
|
+
_b.label = 3;
|
|
112
139
|
case 3:
|
|
113
|
-
|
|
140
|
+
_b.trys.push([3, 5, , 6]);
|
|
114
141
|
return [4 /*yield*/, user.postCreate(bodyRest)];
|
|
115
142
|
case 4:
|
|
116
|
-
|
|
143
|
+
_b.sent();
|
|
117
144
|
return [3 /*break*/, 6];
|
|
118
145
|
case 5:
|
|
119
|
-
error_1 =
|
|
146
|
+
error_1 = _b.sent();
|
|
120
147
|
logger_1.logger.error("Error in user.postCreate: ".concat(error_1));
|
|
121
148
|
throw error_1;
|
|
122
149
|
case 6: return [4 /*yield*/, user.save()];
|
|
123
150
|
case 7:
|
|
124
|
-
|
|
151
|
+
_b.sent();
|
|
125
152
|
return [2 /*return*/, user];
|
|
126
153
|
case 8:
|
|
127
|
-
error_2 =
|
|
128
|
-
|
|
154
|
+
error_2 = _b.sent();
|
|
155
|
+
message = (0, errors_1.errorMessage)(error_2);
|
|
156
|
+
throw new errors_1.APIError({ title: message });
|
|
129
157
|
case 9: return [2 /*return*/];
|
|
130
158
|
}
|
|
131
159
|
});
|
|
@@ -148,71 +176,81 @@ function signupUser(userModel, email, password, body) {
|
|
|
148
176
|
* authentication providers) to reuse and customize the same token generation logic.
|
|
149
177
|
* This ensures consistent and secure token issuance across different authentication flows.
|
|
150
178
|
*/
|
|
151
|
-
var generateTokens = function (
|
|
152
|
-
var
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
payload = __assign(__assign({}, authOptions.generateJWTPayload(user)), payload);
|
|
165
|
-
}
|
|
166
|
-
tokenOptions = {
|
|
167
|
-
expiresIn: "15m",
|
|
168
|
-
};
|
|
169
|
-
if (authOptions === null || authOptions === void 0 ? void 0 : authOptions.generateTokenExpiration) {
|
|
170
|
-
tokenOptions.expiresIn = authOptions.generateTokenExpiration(user);
|
|
171
|
-
}
|
|
172
|
-
else if (process.env.TOKEN_EXPIRES_IN) {
|
|
173
|
-
try {
|
|
174
|
-
// this call to ms is purely for validation of the env variable. If it is invalid,
|
|
175
|
-
// we want to be able to log the error and use the default.
|
|
176
|
-
(0, ms_1.default)(process.env.TOKEN_EXPIRES_IN);
|
|
177
|
-
tokenOptions.expiresIn = process.env.TOKEN_EXPIRES_IN;
|
|
179
|
+
var generateTokens = function (user_1, authOptions_1) {
|
|
180
|
+
var args_1 = [];
|
|
181
|
+
for (var _i = 2; _i < arguments.length; _i++) {
|
|
182
|
+
args_1[_i - 2] = arguments[_i];
|
|
183
|
+
}
|
|
184
|
+
return __awaiter(void 0, __spreadArray([user_1, authOptions_1], __read(args_1), false), void 0, function (user, authOptions, options) {
|
|
185
|
+
var tokenSecretOrKey, tokenUser, sessionId, payload, tokenOptions, token, refreshTokenSecretOrKey, refreshToken, refreshTokenOptions;
|
|
186
|
+
var _a;
|
|
187
|
+
if (options === void 0) { options = {}; }
|
|
188
|
+
return __generator(this, function (_b) {
|
|
189
|
+
tokenSecretOrKey = process.env.TOKEN_SECRET;
|
|
190
|
+
if (!tokenSecretOrKey) {
|
|
191
|
+
throw new Error("TOKEN_SECRET must be set in env.");
|
|
178
192
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
logger_1.logger.
|
|
193
|
+
tokenUser = user;
|
|
194
|
+
if (!(tokenUser === null || tokenUser === void 0 ? void 0 : tokenUser._id)) {
|
|
195
|
+
logger_1.logger.warn("No user found for token generation");
|
|
196
|
+
return [2 /*return*/, { refreshToken: null, token: null }];
|
|
182
197
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
refreshTokenOptions = {
|
|
191
|
-
expiresIn: "30d",
|
|
198
|
+
sessionId = (_a = options.sessionId) !== null && _a !== void 0 ? _a : (0, node_crypto_1.randomUUID)();
|
|
199
|
+
payload = { id: String(tokenUser._id), sid: sessionId };
|
|
200
|
+
if (authOptions === null || authOptions === void 0 ? void 0 : authOptions.generateJWTPayload) {
|
|
201
|
+
payload = __assign(__assign({}, authOptions.generateJWTPayload(user)), payload);
|
|
202
|
+
}
|
|
203
|
+
tokenOptions = {
|
|
204
|
+
expiresIn: "15m",
|
|
192
205
|
};
|
|
193
|
-
if (authOptions === null || authOptions === void 0 ? void 0 : authOptions.
|
|
194
|
-
|
|
206
|
+
if (authOptions === null || authOptions === void 0 ? void 0 : authOptions.generateTokenExpiration) {
|
|
207
|
+
tokenOptions.expiresIn = authOptions.generateTokenExpiration(user);
|
|
195
208
|
}
|
|
196
|
-
else if (process.env.
|
|
209
|
+
else if (process.env.TOKEN_EXPIRES_IN) {
|
|
197
210
|
try {
|
|
198
211
|
// this call to ms is purely for validation of the env variable. If it is invalid,
|
|
199
212
|
// we want to be able to log the error and use the default.
|
|
200
|
-
(0, ms_1.default)(process.env.
|
|
201
|
-
|
|
213
|
+
(0, ms_1.default)(process.env.TOKEN_EXPIRES_IN);
|
|
214
|
+
tokenOptions.expiresIn = process.env.TOKEN_EXPIRES_IN;
|
|
202
215
|
}
|
|
203
216
|
catch (error) {
|
|
204
|
-
// This error will result in using the default value above of
|
|
217
|
+
// This error will result in using the default value above of 15m.
|
|
205
218
|
logger_1.logger.error(error);
|
|
206
219
|
}
|
|
207
220
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
221
|
+
if (process.env.TOKEN_ISSUER) {
|
|
222
|
+
tokenOptions.issuer = process.env.TOKEN_ISSUER;
|
|
223
|
+
}
|
|
224
|
+
token = jsonwebtoken_1.default.sign(payload, tokenSecretOrKey, tokenOptions);
|
|
225
|
+
refreshTokenSecretOrKey = process.env.REFRESH_TOKEN_SECRET;
|
|
226
|
+
if (refreshTokenSecretOrKey) {
|
|
227
|
+
refreshTokenOptions = {
|
|
228
|
+
expiresIn: "30d",
|
|
229
|
+
};
|
|
230
|
+
if (authOptions === null || authOptions === void 0 ? void 0 : authOptions.generateRefreshTokenExpiration) {
|
|
231
|
+
refreshTokenOptions.expiresIn = authOptions.generateRefreshTokenExpiration(user);
|
|
232
|
+
}
|
|
233
|
+
else if (process.env.REFRESH_TOKEN_EXPIRES_IN) {
|
|
234
|
+
try {
|
|
235
|
+
// this call to ms is purely for validation of the env variable. If it is invalid,
|
|
236
|
+
// we want to be able to log the error and use the default.
|
|
237
|
+
(0, ms_1.default)(process.env.REFRESH_TOKEN_EXPIRES_IN);
|
|
238
|
+
refreshTokenOptions.expiresIn = process.env.REFRESH_TOKEN_EXPIRES_IN;
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
// This error will result in using the default value above of 30d.
|
|
242
|
+
logger_1.logger.error(error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
refreshToken = jsonwebtoken_1.default.sign(payload, refreshTokenSecretOrKey, refreshTokenOptions);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
logger_1.logger.info("REFRESH_TOKEN_SECRET not set so refresh tokens will not be issued");
|
|
249
|
+
}
|
|
250
|
+
return [2 /*return*/, { refreshToken: refreshToken, sessionId: sessionId, token: token }];
|
|
251
|
+
});
|
|
214
252
|
});
|
|
215
|
-
}
|
|
253
|
+
};
|
|
216
254
|
exports.generateTokens = generateTokens;
|
|
217
255
|
// TODO allow customization
|
|
218
256
|
function setupAuth(app, userModel) {
|
|
@@ -274,6 +312,7 @@ function setupAuth(app, userModel) {
|
|
|
274
312
|
return __generator(this, function (_a) {
|
|
275
313
|
switch (_a.label) {
|
|
276
314
|
case 0:
|
|
315
|
+
user = null;
|
|
277
316
|
if (!jwtPayload) {
|
|
278
317
|
return [2 /*return*/, done(null, false)];
|
|
279
318
|
}
|
|
@@ -309,17 +348,17 @@ function setupAuth(app, userModel) {
|
|
|
309
348
|
// but passport doesn't give us req.user early enough.
|
|
310
349
|
function decodeJWTMiddleware(req, res, next) {
|
|
311
350
|
return __awaiter(this, void 0, void 0, function () {
|
|
312
|
-
var token, decoded, userText, details,
|
|
313
|
-
var _b, _c, _d
|
|
314
|
-
return __generator(this, function (
|
|
315
|
-
switch (
|
|
351
|
+
var token, decoded, userText, expiredAt, message, details, sessionId, user, error_5;
|
|
352
|
+
var _a, _b, _c, _d;
|
|
353
|
+
return __generator(this, function (_e) {
|
|
354
|
+
switch (_e.label) {
|
|
316
355
|
case 0:
|
|
317
356
|
if (!process.env.TOKEN_SECRET) {
|
|
318
357
|
return [2 /*return*/, next()];
|
|
319
358
|
}
|
|
320
359
|
// Allow requests with a "Secret" prefix to pass through since this is a string value,
|
|
321
360
|
// not a jwt that needs to be decoded
|
|
322
|
-
if (((
|
|
361
|
+
if (((_b = (_a = req === null || req === void 0 ? void 0 : req.headers) === null || _a === void 0 ? void 0 : _a.authorization) === null || _b === void 0 ? void 0 : _b.split(" ")[0]) === "Secret") {
|
|
323
362
|
return [2 /*return*/, next()];
|
|
324
363
|
}
|
|
325
364
|
token = customTokenExtractor(req);
|
|
@@ -334,26 +373,37 @@ function setupAuth(app, userModel) {
|
|
|
334
373
|
});
|
|
335
374
|
}
|
|
336
375
|
catch (error) {
|
|
337
|
-
userText = ((
|
|
338
|
-
|
|
376
|
+
userText = ((_c = req.user) === null || _c === void 0 ? void 0 : _c._id) ? " for user ".concat(req.user._id, " ") : "";
|
|
377
|
+
expiredAt = error && typeof error === "object" && "expiredAt" in error
|
|
378
|
+
? error.expiredAt
|
|
379
|
+
: undefined;
|
|
380
|
+
message = (0, errors_1.errorMessage)(error);
|
|
381
|
+
details = "[jwt] Error decoding token".concat(userText, ": ").concat(error, ", expired at ").concat(expiredAt, ", current time: ").concat(Date.now());
|
|
339
382
|
logger_1.logger.debug(details);
|
|
340
|
-
return [2 /*return*/, res.status(401).json({ details: details, message:
|
|
383
|
+
return [2 /*return*/, res.status(401).json({ details: details, message: message })];
|
|
384
|
+
}
|
|
385
|
+
if (!(decoded === null || decoded === void 0 ? void 0 : decoded.id)) return [3 /*break*/, 4];
|
|
386
|
+
sessionId = (0, requestContext_1.getSessionIdFromJwtPayload)(decoded);
|
|
387
|
+
req.authTokenPayload = decoded;
|
|
388
|
+
if (sessionId) {
|
|
389
|
+
req.sessionId = sessionId;
|
|
390
|
+
(0, requestContext_1.setRequestContext)({ sessionId: sessionId });
|
|
341
391
|
}
|
|
342
|
-
|
|
343
|
-
_f.label = 1;
|
|
392
|
+
_e.label = 1;
|
|
344
393
|
case 1:
|
|
345
|
-
|
|
346
|
-
_a = req;
|
|
394
|
+
_e.trys.push([1, 3, , 4]);
|
|
347
395
|
return [4 /*yield*/, userModel.findById(decoded.id)];
|
|
348
396
|
case 2:
|
|
349
|
-
|
|
350
|
-
|
|
397
|
+
user = _e.sent();
|
|
398
|
+
req.user = user;
|
|
399
|
+
(0, requestContext_1.updateRequestContextFromRequest)(req, res);
|
|
400
|
+
if ((_d = req.user) === null || _d === void 0 ? void 0 : _d.disabled) {
|
|
351
401
|
logger_1.logger.warn("[jwt] User ".concat(req.user.id, " is disabled"));
|
|
352
402
|
return [2 /*return*/, res.status(401).json({ status: 401, title: "User is disabled" })];
|
|
353
403
|
}
|
|
354
404
|
return [3 /*break*/, 4];
|
|
355
405
|
case 3:
|
|
356
|
-
error_5 =
|
|
406
|
+
error_5 = _e.sent();
|
|
357
407
|
logger_1.logger.warn("[jwt] Error finding user from id: ".concat(error_5));
|
|
358
408
|
return [3 /*break*/, 4];
|
|
359
409
|
case 4: return [2 /*return*/, next()];
|
|
@@ -362,6 +412,7 @@ function setupAuth(app, userModel) {
|
|
|
362
412
|
});
|
|
363
413
|
}
|
|
364
414
|
app.use(decodeJWTMiddleware);
|
|
415
|
+
// biome-ignore lint/suspicious/noExplicitAny: express 5 type for urlencoded doesn't match RequestHandler
|
|
365
416
|
app.use(express_1.default.urlencoded({ extended: false }));
|
|
366
417
|
}
|
|
367
418
|
function addAuthRoutes(app, userModel, authOptions) {
|
|
@@ -389,6 +440,10 @@ function addAuthRoutes(app, userModel, authOptions) {
|
|
|
389
440
|
return [4 /*yield*/, (0, exports.generateTokens)(user, authOptions)];
|
|
390
441
|
case 1:
|
|
391
442
|
tokens = _a.sent();
|
|
443
|
+
if (tokens.sessionId) {
|
|
444
|
+
(0, requestContext_1.setRequestContext)({ sessionId: tokens.sessionId, userId: String(user._id) });
|
|
445
|
+
res.setHeader("X-Session-ID", tokens.sessionId);
|
|
446
|
+
}
|
|
392
447
|
return [2 /*return*/, res.json({
|
|
393
448
|
data: { refreshToken: tokens.refreshToken, token: tokens.token, userId: user === null || user === void 0 ? void 0 : user._id },
|
|
394
449
|
})];
|
|
@@ -399,7 +454,7 @@ function addAuthRoutes(app, userModel, authOptions) {
|
|
|
399
454
|
});
|
|
400
455
|
}); });
|
|
401
456
|
router.post("/refresh_token", function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
402
|
-
var refreshTokenSecretOrKey, decoded, user, tokens;
|
|
457
|
+
var refreshTokenSecretOrKey, decoded, message, user, sessionId, tokens;
|
|
403
458
|
var _a, _b, _c, _d;
|
|
404
459
|
return __generator(this, function (_e) {
|
|
405
460
|
switch (_e.label) {
|
|
@@ -420,15 +475,24 @@ function addAuthRoutes(app, userModel, authOptions) {
|
|
|
420
475
|
}
|
|
421
476
|
catch (error) {
|
|
422
477
|
logger_1.logger.error("Error refreshing token for user ".concat((_c = req.user) === null || _c === void 0 ? void 0 : _c.id, ": ").concat(error));
|
|
423
|
-
|
|
478
|
+
message = (0, errors_1.errorMessage)(error);
|
|
479
|
+
return [2 /*return*/, res.status(401).json({ message: message })];
|
|
424
480
|
}
|
|
425
481
|
if (!(decoded === null || decoded === void 0 ? void 0 : decoded.id)) return [3 /*break*/, 3];
|
|
426
482
|
return [4 /*yield*/, userModel.findById(decoded.id)];
|
|
427
483
|
case 1:
|
|
428
484
|
user = _e.sent();
|
|
429
|
-
|
|
485
|
+
sessionId = (0, requestContext_1.getSessionIdFromJwtPayload)(decoded);
|
|
486
|
+
return [4 /*yield*/, (0, exports.generateTokens)(user, authOptions, { sessionId: sessionId })];
|
|
430
487
|
case 2:
|
|
431
488
|
tokens = _e.sent();
|
|
489
|
+
if (tokens.sessionId) {
|
|
490
|
+
(0, requestContext_1.setRequestContext)({
|
|
491
|
+
sessionId: tokens.sessionId,
|
|
492
|
+
userId: (user === null || user === void 0 ? void 0 : user._id) ? String(user._id) : undefined,
|
|
493
|
+
});
|
|
494
|
+
res.setHeader("X-Session-ID", tokens.sessionId);
|
|
495
|
+
}
|
|
432
496
|
logger_1.logger.debug("Refreshed token for ".concat(user === null || user === void 0 ? void 0 : user.id));
|
|
433
497
|
return [2 /*return*/, res.json({ data: { refreshToken: tokens.refreshToken, token: tokens.token } })];
|
|
434
498
|
case 3:
|
|
@@ -441,13 +505,21 @@ function addAuthRoutes(app, userModel, authOptions) {
|
|
|
441
505
|
if (!signupDisabled) {
|
|
442
506
|
router.post("/signup", passport_1.default.authenticate("signup", { failWithError: true, session: false }), function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
443
507
|
var tokens;
|
|
444
|
-
|
|
445
|
-
|
|
508
|
+
var _a, _b;
|
|
509
|
+
return __generator(this, function (_c) {
|
|
510
|
+
switch (_c.label) {
|
|
446
511
|
case 0: return [4 /*yield*/, (0, exports.generateTokens)(req.user, authOptions)];
|
|
447
512
|
case 1:
|
|
448
|
-
tokens =
|
|
513
|
+
tokens = _c.sent();
|
|
514
|
+
if (tokens.sessionId) {
|
|
515
|
+
(0, requestContext_1.setRequestContext)({
|
|
516
|
+
sessionId: tokens.sessionId,
|
|
517
|
+
userId: ((_a = req.user) === null || _a === void 0 ? void 0 : _a._id) ? String(req.user._id) : undefined,
|
|
518
|
+
});
|
|
519
|
+
res.setHeader("X-Session-ID", tokens.sessionId);
|
|
520
|
+
}
|
|
449
521
|
return [2 /*return*/, res.json({
|
|
450
|
-
data: { refreshToken: tokens.refreshToken, token: tokens.token, userId: req.user._id },
|
|
522
|
+
data: { refreshToken: tokens.refreshToken, token: tokens.token, userId: (_b = req.user) === null || _b === void 0 ? void 0 : _b._id },
|
|
451
523
|
})];
|
|
452
524
|
}
|
|
453
525
|
});
|
|
@@ -483,7 +555,7 @@ function addMeRoutes(app, userModel, _authOptions) {
|
|
|
483
555
|
});
|
|
484
556
|
}); });
|
|
485
557
|
router.patch("/me", authenticateMiddleware(), function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
486
|
-
var doc, dataObject, error_6;
|
|
558
|
+
var doc, dataObject, error_6, message;
|
|
487
559
|
var _a;
|
|
488
560
|
return __generator(this, function (_b) {
|
|
489
561
|
switch (_b.label) {
|
|
@@ -509,7 +581,8 @@ function addMeRoutes(app, userModel, _authOptions) {
|
|
|
509
581
|
return [2 /*return*/, res.json({ data: dataObject })];
|
|
510
582
|
case 4:
|
|
511
583
|
error_6 = _b.sent();
|
|
512
|
-
|
|
584
|
+
message = (0, errors_1.errorMessage)(error_6);
|
|
585
|
+
return [2 /*return*/, res.status(403).send({ message: message })];
|
|
513
586
|
case 5: return [2 /*return*/];
|
|
514
587
|
}
|
|
515
588
|
});
|