kuzzle 2.60.0-beta.4 → 2.60.0-beta.5

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 (38) hide show
  1. package/dist/lib/api/controllers/adminController.d.ts +7 -1
  2. package/dist/lib/api/controllers/adminController.js +9 -2
  3. package/dist/lib/api/funnel.js +12 -1
  4. package/dist/lib/api/httpRoutes.js +52 -20
  5. package/dist/lib/api/request/kuzzleRequest.d.ts +7 -2
  6. package/dist/lib/api/request/kuzzleRequest.js +20 -10
  7. package/dist/lib/api/request/requestContext.d.ts +1 -1
  8. package/dist/lib/api/request/requestResponse.js +2 -1
  9. package/dist/lib/config/index.js +60 -3
  10. package/dist/lib/core/backend/backend.d.ts +3 -2
  11. package/dist/lib/core/backend/internalLogger.d.ts +6 -0
  12. package/dist/lib/core/backend/internalLogger.js +6 -0
  13. package/dist/lib/core/network/router.d.ts +18 -0
  14. package/dist/lib/core/network/router.js +44 -10
  15. package/dist/lib/core/security/authTokenSettings.d.ts +21 -0
  16. package/dist/lib/core/security/authTokenSettings.js +61 -0
  17. package/dist/lib/core/security/tokenRepository.js +8 -14
  18. package/dist/lib/core/shared/KoncordeWrapper.d.ts +9 -0
  19. package/dist/lib/core/shared/KoncordeWrapper.js +8 -8
  20. package/dist/lib/core/storage/clientAdapter.js +7 -2
  21. package/dist/lib/kuzzle/Logger.d.ts +4 -0
  22. package/dist/lib/kuzzle/Logger.js +9 -2
  23. package/dist/lib/kuzzle/internalIndexHandler.js +4 -5
  24. package/dist/lib/kuzzle/kuzzle.d.ts +17 -0
  25. package/dist/lib/kuzzle/kuzzle.js +62 -0
  26. package/dist/lib/kuzzle/vault.d.ts +13 -0
  27. package/dist/lib/kuzzle/vault.js +25 -7
  28. package/dist/lib/service/storage/7/elasticsearch.d.ts +6 -0
  29. package/dist/lib/service/storage/7/elasticsearch.js +23 -14
  30. package/dist/lib/service/storage/8/elasticsearch.d.ts +6 -0
  31. package/dist/lib/service/storage/8/elasticsearch.js +23 -14
  32. package/dist/lib/types/config/KuzzleConfiguration.d.ts +7 -0
  33. package/dist/lib/types/config/SecurityConfiguration.d.ts +4 -3
  34. package/dist/lib/types/controllers/ControllerDefinition.d.ts +4 -4
  35. package/dist/lib/util/mutex.d.ts +3 -3
  36. package/dist/lib/util/mutex.js +1 -1
  37. package/dist/package.json +1 -1
  38. package/package.json +1 -1
@@ -44,7 +44,13 @@ declare class AdminController extends NativeController {
44
44
  shutdown(): Promise<{
45
45
  acknowledge: boolean;
46
46
  }>;
47
- loadFixtures(request: KuzzleRequest): Promise<any>;
47
+ /**
48
+ * Always waits for the import, so that a failure is reported to the caller:
49
+ * `refresh=false` only skips waiting for the documents to be indexed.
50
+ */
51
+ loadFixtures(request: KuzzleRequest): Promise<{
52
+ acknowledge: boolean;
53
+ }>;
48
54
  loadMappings(request: KuzzleRequest): Promise<{
49
55
  acknowledge: boolean;
50
56
  }>;
@@ -161,15 +161,22 @@ class AdminController extends baseController_1.NativeController {
161
161
  if (this.shuttingDown) {
162
162
  throw kerror.get("api", "process", "action_locked", "Kuzzle is already shutting down.");
163
163
  }
164
+ this.shuttingDown = true;
164
165
  global.kuzzle.shutdown();
165
166
  return { acknowledge: true };
166
167
  }
167
- loadFixtures(request) {
168
+ /**
169
+ * Always waits for the import, so that a failure is reported to the caller:
170
+ * `refresh=false` only skips waiting for the documents to be indexed.
171
+ */
172
+ async loadFixtures(request) {
168
173
  const fixtures = request.getBody();
169
174
  const refresh = request.getRefresh("wait_for");
170
- return global.kuzzle.ask("core:storage:public:document:import", fixtures, {
175
+ await global.kuzzle.ask("core:storage:public:document:import", fixtures, {
171
176
  refresh,
172
177
  });
178
+ // The documented answer. 2.17.6 to 2.59 answered `null` instead.
179
+ return { acknowledge: true };
173
180
  }
174
181
  loadMappings(request) {
175
182
  const mappings = request.getBody();
@@ -664,13 +664,24 @@ class Funnel {
664
664
  catch (e) {
665
665
  error = e;
666
666
  debug("Error processing request %s: %a", request.id, error);
667
+ // processRequest does not dump: do it as the event-less path does.
668
+ // Rethrown, never falling through to the event-less execution below,
669
+ // which ran a failed (or pipe-rejected) request a second time.
670
+ this.handleErrorDump((0, thrown_1.causeOf)(e));
671
+ throw e;
667
672
  }
668
673
  finally {
669
- global.kuzzle.pipe("request:afterExecution", {
674
+ // Not awaited, as it always was: the caller's result does not depend
675
+ // on it. Its rejection is logged instead of left unhandled.
676
+ global.kuzzle
677
+ .pipe("request:afterExecution", {
670
678
  error: error,
671
679
  request: request,
672
680
  result: res,
673
681
  success: error === undefined,
682
+ })
683
+ ?.catch((pipeError) => {
684
+ this.logger.error(pipeError, `request:afterExecution pipe failed for plugin request ${request.id}`);
674
685
  });
675
686
  }
676
687
  }
@@ -184,12 +184,6 @@ const routes = [
184
184
  controller: "admin",
185
185
  action: "resetDatabase",
186
186
  },
187
- {
188
- verb: "post",
189
- path: "/admin/_resetKuzzleData",
190
- controller: "admin",
191
- action: "resetKuzzleData",
192
- },
193
187
  { verb: "post", path: "/admin/_dump", controller: "admin", action: "dump" },
194
188
  {
195
189
  verb: "post",
@@ -251,12 +245,6 @@ const routes = [
251
245
  controller: "index",
252
246
  action: "exists",
253
247
  },
254
- {
255
- verb: "get",
256
- path: "/:index/_autoRefresh",
257
- controller: "index",
258
- action: "getAutoRefresh",
259
- },
260
248
  { verb: "get", path: "/_list", controller: "index", action: "list" },
261
249
  { verb: "get", path: "/_storageStats", controller: "index", action: "stats" },
262
250
  {
@@ -1133,13 +1121,6 @@ const routes = [
1133
1121
  controller: "ms",
1134
1122
  action: "append",
1135
1123
  },
1136
- {
1137
- verb: "post",
1138
- path: "/ms/_bgrewriteaof",
1139
- controller: "ms",
1140
- action: "bgrewriteaof",
1141
- },
1142
- { verb: "post", path: "/ms/_bgsave", controller: "ms", action: "bgsave" },
1143
1124
  { verb: "post", path: "/ms/_bitop/:_id", controller: "ms", action: "bitop" },
1144
1125
  { verb: "post", path: "/ms/_decr/:_id", controller: "ms", action: "decr" },
1145
1126
  {
@@ -1514,7 +1495,7 @@ const routes = [
1514
1495
  controller: "security",
1515
1496
  action: "deleteApiKey",
1516
1497
  openapi: {
1517
- description: "Deletes a user API key, identified by its clear-text `key` or its `fingerprint` instead of its `_id`. Exactly one of `key` or `fingerprint` must be provided. If you are looking to manage your own API keys, use the `auth` controller instead.",
1498
+ description: "Deletes a user API key, identified by its clear-text `key` or its `fingerprint` instead of its `_id`. One of them must be provided; if several are given, `_id` is used first, then `key`, then `fingerprint`, and the others are ignored. A `key` in the query string is a secret written in the URL: prefer the `fingerprint`. If you are looking to manage your own API keys, use the `auth` controller instead.",
1518
1499
  parameters: [
1519
1500
  {
1520
1501
  in: "path",
@@ -1572,6 +1553,57 @@ const routes = [
1572
1553
  controller: "auth",
1573
1554
  action: "deleteApiKey",
1574
1555
  },
1556
+ {
1557
+ verb: "delete",
1558
+ path: "/api-keys",
1559
+ controller: "auth",
1560
+ action: "deleteApiKey",
1561
+ openapi: {
1562
+ description: "Deletes an API key of the current user, identified by its clear-text `key` or its `fingerprint` instead of its `_id`. One of them must be provided; if several are given, `_id` is used first, then `key`, then `fingerprint`, and the others are ignored. A `key` in the query string is a secret written in the URL: prefer the `fingerprint`.",
1563
+ parameters: [
1564
+ {
1565
+ in: "query",
1566
+ name: "key",
1567
+ schema: { type: "string" },
1568
+ required: false,
1569
+ description: "Clear-text API key to delete",
1570
+ },
1571
+ {
1572
+ in: "query",
1573
+ name: "fingerprint",
1574
+ schema: { type: "string" },
1575
+ required: false,
1576
+ description: "SHA256 fingerprint of the API key to delete",
1577
+ },
1578
+ {
1579
+ in: "query",
1580
+ name: "refresh",
1581
+ schema: {
1582
+ type: "string",
1583
+ enum: ["wait_for", "false"],
1584
+ default: "wait_for",
1585
+ },
1586
+ required: false,
1587
+ description: "If set to `wait_for`, waits for the deletion to be indexed before responding",
1588
+ },
1589
+ ],
1590
+ responses: {
1591
+ 200: {
1592
+ description: "The deleted API key ID",
1593
+ content: {
1594
+ "application/json": {
1595
+ schema: {
1596
+ type: "object",
1597
+ properties: {
1598
+ _id: { type: "string" },
1599
+ },
1600
+ },
1601
+ },
1602
+ },
1603
+ },
1604
+ },
1605
+ },
1606
+ },
1575
1607
  // PUT (idempotent)
1576
1608
  { verb: "put", path: "/_me", controller: "auth", action: "updateSelf" },
1577
1609
  {
@@ -385,12 +385,17 @@ export declare class KuzzleRequest {
385
385
  */
386
386
  getDate(name: string, format?: string): string;
387
387
  /**
388
- * Gets a parameter from a request arguments and returns it to timestamp format.
388
+ * Gets a parameter from a request arguments and checks that it is an integer
389
+ * timestamp (e.g. milliseconds since Epoch).
390
+ *
391
+ * An integer string, such as an HTTP query string value, is accepted and
392
+ * converted. A date string (e.g. `"2026-10-27"`) is rejected.
389
393
  *
390
394
  * @param name parameter name.
391
395
  * @throws {api.assert.missing_argument} If parameter not found and no default
392
396
  * value provided
393
- * @throws {api.assert.invalid_type} If parameter value is not a valid date.
397
+ * @throws {api.assert.invalid_type} If parameter value is not an integer
398
+ * timestamp.
394
399
  */
395
400
  getTimestamp(name: string): number;
396
401
  /**
@@ -676,31 +676,41 @@ class KuzzleRequest {
676
676
  if (args[name] === undefined) {
677
677
  throw assertionError.get("missing_argument", name);
678
678
  }
679
- if (format && !(0, moment_1.default)(args[name], format, true).isValid()) {
680
- throw assertionError.get("invalid_type", name, "date");
681
- }
682
- if (!(0, moment_1.default)(args[name], moment_1.default.ISO_8601).isValid()) {
679
+ // A custom format replaces the ISO 8601 check: applying both rejected
680
+ // every value of a format that is not ISO 8601.
681
+ const isValid = format
682
+ ? (0, moment_1.default)(args[name], format, true).isValid()
683
+ : (0, moment_1.default)(args[name], moment_1.default.ISO_8601).isValid();
684
+ if (!isValid) {
683
685
  throw assertionError.get("invalid_type", name, "date");
684
686
  }
685
687
  return this.getString(name);
686
688
  }
687
689
  /**
688
- * Gets a parameter from a request arguments and returns it to timestamp format.
690
+ * Gets a parameter from a request arguments and checks that it is an integer
691
+ * timestamp (e.g. milliseconds since Epoch).
692
+ *
693
+ * An integer string, such as an HTTP query string value, is accepted and
694
+ * converted. A date string (e.g. `"2026-10-27"`) is rejected.
689
695
  *
690
696
  * @param name parameter name.
691
697
  * @throws {api.assert.missing_argument} If parameter not found and no default
692
698
  * value provided
693
- * @throws {api.assert.invalid_type} If parameter value is not a valid date.
699
+ * @throws {api.assert.invalid_type} If parameter value is not an integer
700
+ * timestamp.
694
701
  */
695
702
  getTimestamp(name) {
696
- const args = this.input.args;
697
- if (args[name] === undefined) {
703
+ const value = this.input.args[name];
704
+ if (value === undefined) {
698
705
  throw assertionError.get("missing_argument", name);
699
706
  }
700
- if ((0, moment_1.default)(args[name], true).isValid() === false) {
707
+ const timestamp = typeof value === "string" && /^-?\d+$/.test(value)
708
+ ? Number(value)
709
+ : value;
710
+ if (!Number.isSafeInteger(timestamp)) {
701
711
  throw assertionError.get("invalid_type", name, "date");
702
712
  }
703
- return this.getInteger(name);
713
+ return timestamp;
704
714
  }
705
715
  /**
706
716
  * Returns the index specified in the request
@@ -19,7 +19,7 @@ export type ContextMisc = {
19
19
  */
20
20
  path?: string;
21
21
  /**
22
- * HTTP headers
22
+ * HTTP method, uppercase (e.g. `GET`)
23
23
  */
24
24
  verb?: string;
25
25
  /**
@@ -127,7 +127,8 @@ class Headers {
127
127
  case "host":
128
128
  case "if-modified-since":
129
129
  case "if-unmodified-since":
130
- case "last-modified, location":
130
+ case "last-modified":
131
+ case "location":
131
132
  case "max-forwards":
132
133
  case "proxy-authorization":
133
134
  case "referer":
@@ -28,21 +28,27 @@ const assert_1 = __importDefault(require("assert"));
28
28
  const node_util_1 = require("node:util");
29
29
  const rc_1 = __importDefault(require("rc"));
30
30
  const default_config_1 = __importDefault(require("./default.config"));
31
+ const authTokenSettings_1 = require("../core/security/authTokenSettings");
31
32
  const package_json_1 = __importDefault(require("../../package.json"));
32
33
  const kerror_1 = require("../kerror");
33
34
  const safeObject_1 = require("../util/safeObject");
34
35
  const bytes_1 = __importDefault(require("../util/bytes"));
35
36
  const wrapped = (0, kerror_1.wrap)("core", "configuration");
36
37
  function loadConfig() {
37
- let loaded;
38
+ let raw;
38
39
  try {
39
- loaded = (0, rc_1.default)("kuzzle", default_config_1.default);
40
+ raw = (0, rc_1.default)("kuzzle", withoutTokenDefaults(default_config_1.default));
40
41
  }
41
42
  catch (e) {
42
43
  throw wrapped.get("cannot_parse", e instanceof Error ? e.message : (0, node_util_1.inspect)(e));
43
44
  }
44
45
  // In place: what `unstringify` answers is the object it was given.
45
- unstringify(loaded);
46
+ unstringify(raw);
47
+ const { authToken, deprecatedSettings, jwt } = resolveTokenSettings(raw.security);
48
+ const loaded = {
49
+ ...raw,
50
+ security: { ...raw.security, authToken, jwt },
51
+ };
46
52
  checkLimitsConfig(loaded);
47
53
  checkHttpOptions(loaded);
48
54
  checkWebSocketOptions(loaded);
@@ -56,6 +62,7 @@ function loadConfig() {
56
62
  hash: {
57
63
  seed: Buffer.from("^m&mOISKBvb1xpl1mRsrylaQXpjb&IJX"),
58
64
  },
65
+ deprecatedSettings,
59
66
  notifiableProtocols: [],
60
67
  },
61
68
  version: package_json_1.default.version,
@@ -66,6 +73,56 @@ function loadConfig() {
66
73
  preprocessRedisOptions(config.services.memoryStorage);
67
74
  return config;
68
75
  }
76
+ /**
77
+ * The packaged defaults, minus the values of `security.authToken` and of its
78
+ * deprecated spelling `security.jwt`: both sections ship the same non-null
79
+ * defaults, so once `rc` has merged them nothing tells a user's
80
+ * `jwt.expiresIn` from the packaged one. Left empty here, they hold only what
81
+ * the user set, and {@link resolveTokenSettings} applies the defaults after.
82
+ *
83
+ * A shallow copy: the other sections stay the packaged objects `rc` merges
84
+ * into, as before.
85
+ */
86
+ function withoutTokenDefaults(defaults) {
87
+ const authToken = {};
88
+ const jwt = {};
89
+ return { ...defaults, security: { ...defaults.security, authToken, jwt } };
90
+ }
91
+ /**
92
+ * Resolves the authentication token settings: for each one, the value set
93
+ * under `security.authToken` wins, then the one set under the deprecated
94
+ * `security.jwt`, then the packaged default. A `null` counts as unset, as the
95
+ * readers' `authToken.x ?? jwt.x` always did. Exception: `gracePeriod` set in
96
+ * both sections keeps the `jwt` one, as it always did (see
97
+ * {@link LEGACY_FIRST}).
98
+ *
99
+ * Both sections get the resolved values, so a reader of either — a plugin
100
+ * may still read `jwt` — sees the settings in effect. `deprecatedSettings`
101
+ * lists the `security.jwt.*` keys the user set, for the deprecation warning.
102
+ */
103
+ function resolveTokenSettings(security) {
104
+ const packaged = default_config_1.default.security.authToken;
105
+ const fromPackage = packaged;
106
+ const fromAuthToken = (0, safeObject_1.isPlainObject)(security.authToken)
107
+ ? security.authToken
108
+ : {};
109
+ const fromJwt = (0, safeObject_1.isPlainObject)(security.jwt) ? security.jwt : {};
110
+ const resolved = {};
111
+ for (const key of new Set([
112
+ ...Object.keys(packaged),
113
+ ...Object.keys(fromJwt),
114
+ ...Object.keys(fromAuthToken),
115
+ ])) {
116
+ resolved[key] = authTokenSettings_1.LEGACY_FIRST.has(key)
117
+ ? (fromJwt[key] ?? fromAuthToken[key] ?? fromPackage[key])
118
+ : (fromAuthToken[key] ?? fromJwt[key] ?? fromPackage[key]);
119
+ }
120
+ return {
121
+ authToken: { ...packaged, ...resolved },
122
+ deprecatedSettings: Object.keys(fromJwt).map((key) => `security.jwt.${key}`),
123
+ jwt: { ...resolved },
124
+ };
125
+ }
69
126
  /**
70
127
  * RC params can be overriden using environment variables,
71
128
  * in which case all values are passed as strings.
@@ -99,13 +99,14 @@ export declare class Backend {
99
99
  */
100
100
  plugin: BackendPlugin;
101
101
  /**
102
- * InternalLogger
102
+ * Application logger
103
103
  *
104
+ * @method trace
104
105
  * @method debug
105
106
  * @method info
106
107
  * @method warn
107
108
  * @method error
108
- * @method verbose
109
+ * @method fatal
109
110
  */
110
111
  log: Logger;
111
112
  /**
@@ -1,4 +1,10 @@
1
1
  import { ApplicationManager } from "./index";
2
+ /**
3
+ * Kuzzle no longer creates instances of this class: `Backend.log` is a
4
+ * `Logger` since 2.45.0.
5
+ *
6
+ * @deprecated Use `Logger` (`app.log`) instead.
7
+ */
2
8
  export declare class InternalLogger extends ApplicationManager {
3
9
  /**
4
10
  * Logs a trace message
@@ -26,6 +26,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.InternalLogger = void 0;
27
27
  const util_1 = __importDefault(require("util"));
28
28
  const index_1 = require("./index");
29
+ /**
30
+ * Kuzzle no longer creates instances of this class: `Backend.log` is a
31
+ * `Logger` since 2.45.0.
32
+ *
33
+ * @deprecated Use `Logger` (`app.log`) instead.
34
+ */
29
35
  class InternalLogger extends index_1.ApplicationManager {
30
36
  /**
31
37
  * Logs a trace message
@@ -32,9 +32,27 @@ declare class Router {
32
32
  * Transmit HTTP requests to the funnel controller and forward its response
33
33
  * back to the client
34
34
  *
35
+ * Triggers the `http:<verb>` pipe with the verb lowercased, as documented.
36
+ * A route declared with another case (e.g. `verb: 'GET'`) also triggers the
37
+ * event under the declared case (`http:GET`), which is the one it used to
38
+ * trigger: see docs/v3-breaking-changes.md.
39
+ *
40
+ * @param verb - the route's HTTP verb, as declared
35
41
  * @param request - includes URL and POST query data
36
42
  * @param cb - callback to invoke with the result
37
43
  */
38
44
  _executeFromHttp(verb: string, request: Request, cb: RouteCallback): void;
45
+ /**
46
+ * Triggers an `http:<verb>` pipe, and answers its error through `cb` or
47
+ * hands the piped request over to `next`.
48
+ *
49
+ * Callbacks rather than promises: this runs before the funnel.
50
+ */
51
+ private _pipeHttpEvent;
52
+ /**
53
+ * Executes the piped request in the funnel and answers its result through
54
+ * `cb`, falling back on the original request when there is none.
55
+ */
56
+ private _executeInFunnel;
39
57
  }
40
58
  export = Router;
@@ -190,25 +190,59 @@ class Router {
190
190
  * Transmit HTTP requests to the funnel controller and forward its response
191
191
  * back to the client
192
192
  *
193
+ * Triggers the `http:<verb>` pipe with the verb lowercased, as documented.
194
+ * A route declared with another case (e.g. `verb: 'GET'`) also triggers the
195
+ * event under the declared case (`http:GET`), which is the one it used to
196
+ * trigger: see docs/v3-breaking-changes.md.
197
+ *
198
+ * @param verb - the route's HTTP verb, as declared
193
199
  * @param request - includes URL and POST query data
194
200
  * @param cb - callback to invoke with the result
195
201
  */
196
202
  _executeFromHttp(verb, request, cb) {
197
- global.kuzzle.pipe(`http:${verb}`, request, (error, mutatedRequest) => {
203
+ const event = `http:${verb.toLowerCase()}`;
204
+ const declaredEvent = `http:${verb}`;
205
+ this._pipeHttpEvent(event, request, cb, (pipedRequest) => {
206
+ if (declaredEvent === event) {
207
+ this._executeInFunnel(pipedRequest, request, cb);
208
+ return;
209
+ }
210
+ // @deprecated - declared-case event, to drop in v3
211
+ this._pipeHttpEvent(declaredEvent, pipedRequest, cb, (legacyRequest) => {
212
+ this._executeInFunnel(legacyRequest, request, cb);
213
+ });
214
+ });
215
+ }
216
+ /**
217
+ * Triggers an `http:<verb>` pipe, and answers its error through `cb` or
218
+ * hands the piped request over to `next`.
219
+ *
220
+ * Callbacks rather than promises: this runs before the funnel.
221
+ */
222
+ _pipeHttpEvent(event, request, cb, next) {
223
+ // callback form: the outcome, error included, goes through the callback
224
+ void global.kuzzle.pipe(event, request, (error, mutatedRequest) => {
198
225
  if (error) {
199
226
  request.setError(error);
200
227
  cb(request);
201
228
  return;
202
229
  }
203
- global.kuzzle.funnel.execute(mutatedRequest, (err, result) => {
204
- const _res = result || request;
205
- if (err && !_res.error) {
206
- _res.setError(err);
207
- }
208
- // no sanitisation here: `_res` is a KuzzleRequest, and the stack
209
- // is stripped by the protocols, on the serialized response
210
- cb(_res);
211
- });
230
+ next(mutatedRequest);
231
+ });
232
+ }
233
+ /**
234
+ * Executes the piped request in the funnel and answers its result through
235
+ * `cb`, falling back on the original request when there is none.
236
+ */
237
+ _executeInFunnel(pipedRequest, request, cb) {
238
+ global.kuzzle.funnel.execute(pipedRequest, (err, result) => {
239
+ const _res = result || request;
240
+ if (err && !_res.error) {
241
+ _res.setError(err);
242
+ }
243
+ // no sanitisation here: `_res` is a KuzzleRequest, and the stack
244
+ // is stripped by the protocols, on the serialized response
245
+ cb(_res);
212
246
  });
213
247
  }
214
248
  }
@@ -0,0 +1,21 @@
1
+ import type { SecurityConfiguration } from "../../types/config/SecurityConfiguration";
2
+ type AuthTokenSettings = SecurityConfiguration["authToken"];
3
+ /**
4
+ * The settings for which a value set under the deprecated `security.jwt`
5
+ * wins over one set under `security.authToken`: `gracePeriod` used to be
6
+ * read from `jwt` only, so a configuration setting both keeps working as it
7
+ * did. To drop in v3 (docs/v3-breaking-changes.md).
8
+ */
9
+ export declare const LEGACY_FIRST: ReadonlySet<string>;
10
+ /**
11
+ * Reads one authentication token setting: the value set under
12
+ * `security.authToken` wins, then the one set under its deprecated spelling
13
+ * `security.jwt`, then the packaged default (except for {@link LEGACY_FIRST}).
14
+ *
15
+ * `loadConfig()` already resolves both sections from what the configuration
16
+ * files and the environment set. This covers a change made afterwards, e.g.
17
+ * `app.config.set("security.jwt.expiresIn", "2h")` before `app.start()`: a
18
+ * value that differs from the packaged default is one somebody set.
19
+ */
20
+ export declare function authTokenSetting<K extends keyof AuthTokenSettings>(key: K): AuthTokenSettings[K];
21
+ export {};
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ /*
3
+ * Kuzzle, a backend software, self-hostable and ready to use
4
+ * to power modern apps
5
+ *
6
+ * Copyright 2015-2022 Kuzzle
7
+ * mailto: support AT kuzzle.io
8
+ * website: http://kuzzle.io
9
+ *
10
+ * Licensed under the Apache License, Version 2.0 (the "License");
11
+ * you may not use this file except in compliance with the License.
12
+ * You may obtain a copy of the License at
13
+ *
14
+ * https://www.apache.org/licenses/LICENSE-2.0
15
+ *
16
+ * Unless required by applicable law or agreed to in writing, software
17
+ * distributed under the License is distributed on an "AS IS" BASIS,
18
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ * See the License for the specific language governing permissions and
20
+ * limitations under the License.
21
+ */
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.LEGACY_FIRST = void 0;
27
+ exports.authTokenSetting = authTokenSetting;
28
+ const default_config_1 = __importDefault(require("../../config/default.config"));
29
+ /**
30
+ * The settings for which a value set under the deprecated `security.jwt`
31
+ * wins over one set under `security.authToken`: `gracePeriod` used to be
32
+ * read from `jwt` only, so a configuration setting both keeps working as it
33
+ * did. To drop in v3 (docs/v3-breaking-changes.md).
34
+ */
35
+ exports.LEGACY_FIRST = new Set(["gracePeriod"]);
36
+ function isSet(value) {
37
+ return value !== undefined && value !== null;
38
+ }
39
+ /**
40
+ * Reads one authentication token setting: the value set under
41
+ * `security.authToken` wins, then the one set under its deprecated spelling
42
+ * `security.jwt`, then the packaged default (except for {@link LEGACY_FIRST}).
43
+ *
44
+ * `loadConfig()` already resolves both sections from what the configuration
45
+ * files and the environment set. This covers a change made afterwards, e.g.
46
+ * `app.config.set("security.jwt.expiresIn", "2h")` before `app.start()`: a
47
+ * value that differs from the packaged default is one somebody set.
48
+ */
49
+ function authTokenSetting(key) {
50
+ const { authToken, jwt } = global.kuzzle.config.security; // NOSONAR: the deprecated section is read on purpose, as the documented fallback
51
+ const packaged = default_config_1.default.security.authToken[key];
52
+ const current = authToken?.[key];
53
+ const legacy = jwt?.[key]; // NOSONAR
54
+ const candidates = exports.LEGACY_FIRST.has(key)
55
+ ? [legacy, current]
56
+ : [current, legacy];
57
+ return (candidates.find((value) => isSet(value) && value !== packaged) ??
58
+ candidates.find(isSet) ??
59
+ packaged);
60
+ }
61
+ //# sourceMappingURL=authTokenSettings.js.map
@@ -66,15 +66,8 @@ const token_1 = require("../../model/security/token");
66
66
  const apiKey_1 = __importDefault(require("../../model/storage/apiKey"));
67
67
  const ObjectRepository_1 = require("../shared/ObjectRepository");
68
68
  const crypto_1 = require("../../util/crypto");
69
+ const authTokenSettings_1 = require("./authTokenSettings");
69
70
  const securityError = kerror.wrap("security", "token");
70
- /**
71
- * The deprecated `security.jwt` section, which the `authToken` settings fall
72
- * back to. The packaged defaults always ship it; the type says optional only
73
- * because v2.56.0's did (see SecurityConfiguration.jwt).
74
- */
75
- function legacyJwtConfig() {
76
- return global.kuzzle.config.security.jwt; // NOSONAR: the deprecated section is read on purpose, as the documented fallback
77
- }
78
71
  class TokenRepository extends ObjectRepository_1.ObjectRepository {
79
72
  constructor(opts = {}) {
80
73
  super();
@@ -83,7 +76,11 @@ class TokenRepository extends ObjectRepository_1.ObjectRepository {
83
76
  if (opts.ttl !== undefined) {
84
77
  this.ttl = opts.ttl;
85
78
  }
86
- this.tokenGracePeriod = Math.floor(legacyJwtConfig().gracePeriod);
79
+ this.tokenGracePeriod = Math.floor((0, authTokenSettings_1.authTokenSetting)("gracePeriod"));
80
+ const deprecated = global.kuzzle.config.internal?.deprecatedSettings;
81
+ if (deprecated?.length) {
82
+ global.kuzzle.log.warn(`[DEPRECATED] The configuration section "security.jwt" is deprecated, use "security.authToken" instead (set: ${deprecated.join(", ")}). A value set under "security.authToken" takes precedence.`);
83
+ }
87
84
  this.anonymousToken = new token_1.Token({ userId: "-1" });
88
85
  }
89
86
  async init() {
@@ -194,17 +191,14 @@ class TokenRepository extends ObjectRepository_1.ObjectRepository {
194
191
  *
195
192
  * @returns {Promise.<Object>} { _id, jwt, userId, ttl, expiresAt }
196
193
  */
197
- async generateToken(user, { algorithm = global.kuzzle.config.security.authToken.algorithm ??
198
- legacyJwtConfig().algorithm, expiresIn = global.kuzzle.config.security.authToken.expiresIn ??
199
- legacyJwtConfig().expiresIn, bypassMaxTTL = false, type = "authToken", singleUse = false, } = {}) {
194
+ async generateToken(user, { algorithm = (0, authTokenSettings_1.authTokenSetting)("algorithm"), expiresIn = (0, authTokenSettings_1.authTokenSetting)("expiresIn"), bypassMaxTTL = false, type = "authToken", singleUse = false, } = {}) {
200
195
  if (!user || user._id === null) {
201
196
  throw securityError.get("unknown_user");
202
197
  }
203
198
  const parsedExpiresIn = parseTimespan(expiresIn);
204
199
  const maxTTL = type === "apiKey"
205
200
  ? global.kuzzle.config.security.apiKey.maxTTL
206
- : (global.kuzzle.config.security.authToken.maxTTL ??
207
- legacyJwtConfig().maxTTL);
201
+ : (0, authTokenSettings_1.authTokenSetting)("maxTTL");
208
202
  if (!bypassMaxTTL &&
209
203
  maxTTL > -1 &&
210
204
  (parsedExpiresIn > maxTTL || parsedExpiresIn === -1)) {
@@ -29,6 +29,15 @@ export declare class NormalizedFilterV3 {
29
29
  normalized: JSONObject[][];
30
30
  constructor(index: string, collection: string, normalized: JSONObject);
31
31
  }
32
+ /**
33
+ * Exposes Koncorde v4 through an interface emulating Koncorde v3, to prevent
34
+ * breaking changes in Kuzzle.
35
+ *
36
+ * This class should either be removed entirely in the next major version, or
37
+ * perhaps proposed in a compatibility mode to help clients migrate smoothly.
38
+ *
39
+ * @deprecated Use the `koncorde` package (v4) directly.
40
+ */
32
41
  export declare class Koncorde {
33
42
  private filterIdIndexMap;
34
43
  private koncorde;
@@ -35,14 +35,14 @@ class NormalizedFilterV3 {
35
35
  }
36
36
  }
37
37
  exports.NormalizedFilterV3 = NormalizedFilterV3;
38
- /*
39
- @deprecated
40
-
41
- Exposes Koncorde v4 through an interface emulating Koncorde v3, to prevent
42
- breaking changes in Kuzzle.
43
-
44
- This class should either be removed entirely in the next major version, or
45
- perhaps proposed in a compatibility mode to help clients migrate smoothly.
38
+ /**
39
+ * Exposes Koncorde v4 through an interface emulating Koncorde v3, to prevent
40
+ * breaking changes in Kuzzle.
41
+ *
42
+ * This class should either be removed entirely in the next major version, or
43
+ * perhaps proposed in a compatibility mode to help clients migrate smoothly.
44
+ *
45
+ * @deprecated Use the `koncorde` package (v4) directly.
46
46
  */
47
47
  class Koncorde {
48
48
  constructor(options = {}) {
@@ -96,8 +96,13 @@ class ClientAdapter {
96
96
  this.registerLockEvents();
97
97
  // The cache and the locks were first loaded before this node joined the
98
98
  // cluster: an index, a collection or a lock a peer added in between was
99
- // broadcast before this node could hear it. Reading them again once the
100
- // cluster is joined closes that gap.
99
+ // broadcast before this node could hear it. `Kuzzle.start()` asks for this
100
+ // reload right after the cluster is joined, before the plugins' `init()`
101
+ // runs (#3044).
102
+ global.kuzzle.onAsk(`core:storage:${this.scope}:cache:reload`, () => this.reloadCache());
103
+ // Read again once the node goes live: a peer that joined after this node
104
+ // may have created indexes or collections before its own join (e.g. its
105
+ // internal index), and that broadcast reached no one.
101
106
  global.kuzzle.onPipe("kuzzle:state:live", () => this.reloadCache());
102
107
  // Global store events registration
103
108
  /**
@@ -28,5 +28,9 @@ export declare class Logger extends KuzzleLogger {
28
28
  verbose(message: string): void;
29
29
  getLevel(): string;
30
30
  setLevel(level: string): void;
31
+ /**
32
+ * Creates a child logger with the given namespace, appended to the parent's
33
+ * one. Its level follows the parent's until it is set on the child.
34
+ */
31
35
  child(namespace: string): Logger;
32
36
  }
@@ -128,9 +128,16 @@ class Logger extends kuzzle_logger_1.KuzzleLogger {
128
128
  setLevel(level) {
129
129
  this.level = level;
130
130
  }
131
+ /**
132
+ * Creates a child logger with the given namespace, appended to the parent's
133
+ * one. Its level follows the parent's until it is set on the child.
134
+ */
131
135
  child(namespace) {
132
- // TODO, this is fishy we will probably need to properly remove the "as Logger"
133
- return super.child(namespace);
136
+ // kuzzle-logger builds the child as a plain KuzzleLogger (no getLevel,
137
+ // setLevel, silly, verbose): give it this class's prototype so it is the
138
+ // Logger it is declared to be. Its state is all KuzzleLogger's, and the
139
+ // deprecation flags start unset, as on a new Logger.
140
+ return Object.setPrototypeOf(super.child(namespace), Logger.prototype);
134
141
  }
135
142
  }
136
143
  exports.Logger = Logger;
@@ -57,6 +57,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
57
57
  };
58
58
  const node_crypto_1 = __importDefault(require("node:crypto"));
59
59
  const bluebird_1 = __importDefault(require("bluebird"));
60
+ const authTokenSettings_1 = require("../core/security/authTokenSettings");
60
61
  const store_1 = require("../core/shared/store");
61
62
  const storeScopeEnum_1 = require("../core/storage/storeScopeEnum");
62
63
  const specification_1 = require("../core/validation/specification");
@@ -191,11 +192,9 @@ class InternalIndexHandler extends store_1.Store {
191
192
  await bluebird_1.default.all(promises);
192
193
  }
193
194
  async _initSecret() {
194
- // NOSONAR: `jwt` is the deprecated spelling of `authToken`, read second so
195
- // an existing configuration keeps working. Dropping it is a breaking change
196
- // for anyone who has not migrated, not a conversion's to make.
197
- const { authToken, jwt } = global.kuzzle.config.security; // NOSONAR
198
- const configSeed = authToken?.secret ?? jwt?.secret; // NOSONAR
195
+ // `jwt` is the deprecated spelling of `authToken`, read second so an
196
+ // existing configuration keeps working (see authTokenSetting).
197
+ const configSeed = (0, authTokenSettings_1.authTokenSetting)("secret");
199
198
  let storedSeed = await this.exists("config", this._JWT_SECRET_ID);
200
199
  if (!configSeed) {
201
200
  if (!storedSeed) {
@@ -107,6 +107,23 @@ declare class Kuzzle extends KuzzleEventEmitter {
107
107
  * it hands the application wrapper the implementation reads.
108
108
  */
109
109
  start(application: unknown, options?: StartOptions): Promise<void>;
110
+ /**
111
+ * A node asked to stop while it starts must not go on taking cluster-wide
112
+ * locks or running the application's installations: if it is killed before
113
+ * its shutdown releases them, the other nodes wait for their TTL (#3045).
114
+ */
115
+ private stopIfShuttingDown;
116
+ /**
117
+ * The shutdown began during the startup, which stopped (or failed because
118
+ * of it): no "Cannot start" error and no second shutdown, the one running
119
+ * keeps its exit code and ends the process.
120
+ *
121
+ * The returned promise never settles, as `withLock` once the process is
122
+ * exiting: rejecting would send the caller down its failed-start path
123
+ * (often a `process.exit(1)`, which would cut the shutdown short), and
124
+ * resolving would tell it that Kuzzle is running.
125
+ */
126
+ private leaveStartupToShutdown;
110
127
  /**
111
128
  * Generates the node ID.
112
129
  *
@@ -99,6 +99,17 @@ let _kuzzle = null;
99
99
  * not keep a failed node alive, which a bare `process.exit(1)` never did
100
100
  */
101
101
  const FAILURE_SHUTDOWN_TIMEOUT = 30000;
102
+ /**
103
+ * Stops the startup sequence once a shutdown has begun (#3045): `start()`
104
+ * catches it and leaves the shutdown to end the process. Internal: never
105
+ * handed to the caller of `start()`.
106
+ */
107
+ class StartupInterruptedError extends Error {
108
+ constructor() {
109
+ super("Kuzzle is shutting down");
110
+ this.name = "StartupInterruptedError";
111
+ }
112
+ }
102
113
  Reflect.defineProperty(global, "kuzzle", {
103
114
  configurable: true,
104
115
  enumerable: false,
@@ -175,9 +186,11 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
175
186
  await new cacheEngine_1.default().init();
176
187
  await new storageEngine_1.default().init();
177
188
  await new realtime_1.default().init();
189
+ this.stopIfShuttingDown();
178
190
  await this.internalIndex.init();
179
191
  await new security_1.default().init();
180
192
  // This will init the cluster module if enabled
193
+ this.stopIfShuttingDown();
181
194
  await this.initKuzzleNode();
182
195
  this.vault = vault_1.default.load(options?.vaultKey, options?.secretsFile, this.config.vault?.newAlgorithm);
183
196
  this.validation.init();
@@ -189,21 +202,25 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
189
202
  // before opening connections to external users
190
203
  await this.entryPoint.init();
191
204
  await this.debugger.init();
205
+ this.stopIfShuttingDown();
192
206
  this.pluginsManager.application = application;
193
207
  const pluginImports = await this.pluginsManager.init(options?.plugins);
194
208
  const imports = lodash_1.default.merge({}, pluginImports, options?.import);
195
209
  // Authentification plugins must be loaded before users import to avoid
196
210
  // credentials related error which would prevent Kuzzle from starting
211
+ this.stopIfShuttingDown();
197
212
  await this.loadInitialState(imports, options?.support);
198
213
  await this.ask("core:security:verify");
199
214
  this.router.init();
200
215
  this.log.info("[✔] Core components loaded");
216
+ this.stopIfShuttingDown();
201
217
  await this.install(options?.installations);
202
218
  // `application` is the very wrapper handed to the plugins manager six
203
219
  // lines up, so reading the name back off the manager was a round trip
204
220
  // through a getter that can answer `undefined`.
205
221
  this.log.info(`[✔] Start "${application.name}" application`);
206
222
  this.openApiManager = new openapi_1.OpenApiManager(application.openApi, this.config.http.routes, this.pluginsManager.routes);
223
+ this.stopIfShuttingDown();
207
224
  // @deprecated
208
225
  await this.pipe("kuzzle:start");
209
226
  await this.pipe("kuzzle:state:live");
@@ -215,6 +232,9 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
215
232
  this._state = kuzzleStateEnum_1.default.RUNNING;
216
233
  }
217
234
  catch (error) {
235
+ if (this._state === kuzzleStateEnum_1.default.SHUTTING_DOWN) {
236
+ return this.leaveStartupToShutdown(error);
237
+ }
218
238
  this.log.error(`[X] Cannot start Kuzzle ${this.version}: ${error instanceof Error ? error.message : (0, node_util_1.inspect)(error)}`);
219
239
  // A failed start cannot be retried (a second Kuzzle instance cannot be
220
240
  // built), and what it did start keeps the process alive: in production,
@@ -229,6 +249,36 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
229
249
  throw error;
230
250
  }
231
251
  }
252
+ /**
253
+ * A node asked to stop while it starts must not go on taking cluster-wide
254
+ * locks or running the application's installations: if it is killed before
255
+ * its shutdown releases them, the other nodes wait for their TTL (#3045).
256
+ */
257
+ stopIfShuttingDown() {
258
+ if (this._state === kuzzleStateEnum_1.default.SHUTTING_DOWN) {
259
+ throw new StartupInterruptedError();
260
+ }
261
+ }
262
+ /**
263
+ * The shutdown began during the startup, which stopped (or failed because
264
+ * of it): no "Cannot start" error and no second shutdown, the one running
265
+ * keeps its exit code and ends the process.
266
+ *
267
+ * The returned promise never settles, as `withLock` once the process is
268
+ * exiting: rejecting would send the caller down its failed-start path
269
+ * (often a `process.exit(1)`, which would cut the shutdown short), and
270
+ * resolving would tell it that Kuzzle is running.
271
+ */
272
+ leaveStartupToShutdown(error) {
273
+ let message = "[ℹ] Startup stopped: Kuzzle is shutting down";
274
+ // A step that failed because of the shutdown, rather than stopped by it
275
+ if (!(error instanceof StartupInterruptedError)) {
276
+ const reason = error instanceof Error ? error.message : (0, node_util_1.inspect)(error);
277
+ message += ` (${reason})`;
278
+ }
279
+ this.log.info(message);
280
+ return new Promise(() => undefined);
281
+ }
232
282
  /**
233
283
  * Generates the node ID.
234
284
  *
@@ -238,6 +288,13 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
238
288
  if (this.config.cluster.enabled) {
239
289
  await new cluster_1.default().init();
240
290
  this.log.info("[✔] Cluster initialized");
291
+ // The storage caches were loaded before the join: what a peer created
292
+ // in between was broadcast before this node could hear it. Read them
293
+ // again before the plugins' `init()` relies on them (#3044).
294
+ await Promise.all([
295
+ this.ask("core:storage:public:cache:reload"),
296
+ this.ask("core:storage:private:cache:reload"),
297
+ ]);
241
298
  }
242
299
  else {
243
300
  this.log.info("[X] Cluster disabled: single node mode.");
@@ -317,6 +374,9 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
317
374
  // one must not start before they are recorded
318
375
  await (0, distributedLock_1.withLock)("backend:installations", async () => {
319
376
  for (const installation of installations) {
377
+ // The lock may come long after it was asked for, once the node
378
+ // holding it is done: by then, this one may be shutting down
379
+ this.stopIfShuttingDown();
320
380
  const isAlreadyInstalled = await this.ask("core:storage:private:document:exist", "kuzzle", "installations", installation.id);
321
381
  if (!isAlreadyInstalled) {
322
382
  try {
@@ -508,6 +568,8 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
508
568
  // cannot serialise, and the payload is built right above.
509
569
  const importPayloadHash = (0, crypto_1.sha256)((0, json_stable_stringify_1.default)(importPayload) ?? "");
510
570
  const importType = async () => {
571
+ // Same as for the installations: the lock may come after a SIGTERM
572
+ this.stopIfShuttingDown();
511
573
  // Read the bookkeeping *after* the lock, never before: a read taken
512
574
  // before acquiring it is not protected by it, and the holder that
513
575
  // acted on a stale "not initialized" used to create a document the
@@ -1,6 +1,19 @@
1
1
  import { Vault } from "kuzzle-vault";
2
+ /**
3
+ * The default secrets file, `config/secrets.enc.json` at the project root.
4
+ *
5
+ * Installed as an npm package (KaaF), Kuzzle lives under `node_modules/kuzzle`
6
+ * and the project root is above it: from `node_modules/kuzzle/dist/lib/kuzzle`
7
+ * since the build outputs to `dist/` (2.50.0), from
8
+ * `node_modules/kuzzle/lib/kuzzle` before. Anywhere else (a Kuzzle checkout),
9
+ * the file is the checkout's own `config/secrets.enc.json`.
10
+ *
11
+ * @param dirname - the directory of this module
12
+ */
13
+ declare function defaultSecretsFile(dirname: string): string;
2
14
  declare function load(vaultKey?: string, secretsFile?: string, useNewAlgorithm?: boolean): Vault;
3
15
  declare const _default: {
16
+ defaultSecretsFile: typeof defaultSecretsFile;
4
17
  load: typeof load;
5
18
  };
6
19
  export = _default;
@@ -35,15 +35,33 @@ const isEmpty_1 = __importDefault(require("lodash/isEmpty"));
35
35
  // strict while being false for every read before that first assignment
36
36
  // (TD-57, #2760).
37
37
  let ENV_VAULT_KEY;
38
+ /**
39
+ * The default secrets file, `config/secrets.enc.json` at the project root.
40
+ *
41
+ * Installed as an npm package (KaaF), Kuzzle lives under `node_modules/kuzzle`
42
+ * and the project root is above it: from `node_modules/kuzzle/dist/lib/kuzzle`
43
+ * since the build outputs to `dist/` (2.50.0), from
44
+ * `node_modules/kuzzle/lib/kuzzle` before. Anywhere else (a Kuzzle checkout),
45
+ * the file is the checkout's own `config/secrets.enc.json`.
46
+ *
47
+ * @param dirname - the directory of this module
48
+ */
49
+ function defaultSecretsFile(dirname) {
50
+ const dir = dirname.split(node_path_1.default.sep).join("/");
51
+ for (const installed of [
52
+ "/node_modules/kuzzle/dist/lib/kuzzle",
53
+ "/node_modules/kuzzle/lib/kuzzle",
54
+ ]) {
55
+ if (dir.endsWith(installed)) {
56
+ return node_path_1.default.resolve(dirname.slice(0, dirname.length - installed.length + 1), "config/secrets.enc.json");
57
+ }
58
+ }
59
+ return node_path_1.default.resolve(dirname, "../../config/secrets.enc.json");
60
+ }
38
61
  function load(vaultKey, secretsFile, useNewAlgorithm = false) {
39
- // Using KaaF kuzzle is an npm package and is located under node_modules folder
40
- // We need to get back to root folder of the project to get the secret file
41
- const defaultEncryptedSecretsFile = __dirname.endsWith("/node_modules/kuzzle/lib/kuzzle")
42
- ? node_path_1.default.resolve(`${__dirname}/../../../../config/secrets.enc.json`)
43
- : node_path_1.default.resolve(`${__dirname}/../../config/secrets.enc.json`);
44
62
  const encryptedSecretsFile = secretsFile ||
45
63
  process.env.KUZZLE_SECRETS_FILE ||
46
- defaultEncryptedSecretsFile;
64
+ defaultSecretsFile(__dirname);
47
65
  let key = vaultKey;
48
66
  if ((0, isEmpty_1.default)(vaultKey) &&
49
67
  (!(0, isEmpty_1.default)(process.env.KUZZLE_VAULT_KEY) || !(0, isEmpty_1.default)(ENV_VAULT_KEY))) {
@@ -71,5 +89,5 @@ function load(vaultKey, secretsFile, useNewAlgorithm = false) {
71
89
  }
72
90
  return vault;
73
91
  }
74
- module.exports = { load };
92
+ module.exports = { defaultSecretsFile, load };
75
93
  //# sourceMappingURL=vault.js.map
@@ -452,6 +452,12 @@ export declare class ES7 {
452
452
  mappings?: TypeMapping;
453
453
  settings?: JSONObject;
454
454
  }): Promise<null>;
455
+ /**
456
+ * Applies the mappings and settings of a `createCollection` call to a
457
+ * collection that already exists because another request created it,
458
+ * maybe so recently that its primary shards are still initializing.
459
+ */
460
+ private _updateConcurrentlyCreatedCollection;
455
461
  /**
456
462
  * Waits until the primary shards of a collection are active, for at most
457
463
  * `timeout`, and rejects if they are not by then.
@@ -1149,23 +1149,23 @@ class ES7 {
1149
1149
  this._checkDynamicProperty(mappings);
1150
1150
  const exists = await this.hasCollection(index, collection);
1151
1151
  if (exists) {
1152
- // Another node created it, maybe so recently that its primary shards
1153
- // are still initializing
1154
- await this.waitForCollectionShards(index, collection);
1155
- return this.updateCollection(index, collection, { mappings, settings });
1152
+ return this._updateConcurrentlyCreatedCollection(index, collection, mappings, settings);
1156
1153
  }
1157
1154
  this._checkMappings(mappings);
1155
+ // Copies, not in-place changes: the race branch below must hand the
1156
+ // caller's own mappings and settings to `updateCollection`
1158
1157
  requestBody.mappings = {
1159
1158
  _meta: mappings._meta || this._config.commonMapping._meta,
1160
1159
  dynamic: mappings.dynamic || this._config.commonMapping.dynamic,
1161
- properties: lodash_1.default.merge(mappings.properties, this._config.commonMapping.properties),
1160
+ properties: lodash_1.default.merge({}, mappings.properties, this._config.commonMapping.properties),
1161
+ };
1162
+ requestBody.settings = {
1163
+ ...settings,
1164
+ number_of_replicas: settings.number_of_replicas ||
1165
+ this._config.defaultSettings.number_of_replicas,
1166
+ number_of_shards: settings.number_of_shards ||
1167
+ this._config.defaultSettings.number_of_shards,
1162
1168
  };
1163
- settings.number_of_replicas =
1164
- settings.number_of_replicas ||
1165
- this._config.defaultSettings.number_of_replicas;
1166
- settings.number_of_shards =
1167
- settings.number_of_shards ||
1168
- this._config.defaultSettings.number_of_shards;
1169
1169
  try {
1170
1170
  await this._client.indices.create(esRequest);
1171
1171
  }
@@ -1174,9 +1174,9 @@ class ES7 {
1174
1174
  "resource_already_exists_exception") {
1175
1175
  // race condition: the indice has been created between the "exists"
1176
1176
  // check above and this "create" attempt, by a request that may not
1177
- // have waited for its shards yet
1178
- await this.waitForCollectionShards(index, collection);
1179
- return null;
1177
+ // have waited for its shards yet. This request's mappings and
1178
+ // settings must still be applied, as if it had seen it exist
1179
+ return this._updateConcurrentlyCreatedCollection(index, collection, mappings, settings);
1180
1180
  }
1181
1181
  throw this._esWrapper.formatESError(error);
1182
1182
  }
@@ -1195,6 +1195,15 @@ class ES7 {
1195
1195
  }
1196
1196
  return null;
1197
1197
  }
1198
+ /**
1199
+ * Applies the mappings and settings of a `createCollection` call to a
1200
+ * collection that already exists because another request created it,
1201
+ * maybe so recently that its primary shards are still initializing.
1202
+ */
1203
+ async _updateConcurrentlyCreatedCollection(index, collection, mappings, settings) {
1204
+ await this.waitForCollectionShards(index, collection);
1205
+ return this.updateCollection(index, collection, { mappings, settings });
1206
+ }
1198
1207
  /**
1199
1208
  * Waits until the primary shards of a collection are active, for at most
1200
1209
  * `timeout`, and rejects if they are not by then.
@@ -443,6 +443,12 @@ export declare class ES8 {
443
443
  mappings?: estypes.MappingTypeMapping;
444
444
  settings?: estypes.IndicesIndexSettings;
445
445
  }): Promise<null>;
446
+ /**
447
+ * Applies the mappings and settings of a `createCollection` call to a
448
+ * collection that already exists because another request created it,
449
+ * maybe so recently that its primary shards are still initializing.
450
+ */
451
+ private _updateConcurrentlyCreatedCollection;
446
452
  /**
447
453
  * Waits until the primary shards of a collection are active, for at most
448
454
  * `timeout`, and rejects if they are not by then.
@@ -1171,23 +1171,23 @@ class ES8 {
1171
1171
  this._checkDynamicProperty(mappings);
1172
1172
  const exists = await this.hasCollection(index, collection);
1173
1173
  if (exists) {
1174
- // Another node created it, maybe so recently that its primary shards
1175
- // are still initializing
1176
- await this.waitForCollectionShards(index, collection);
1177
- return this.updateCollection(index, collection, { mappings, settings });
1174
+ return this._updateConcurrentlyCreatedCollection(index, collection, mappings, settings);
1178
1175
  }
1179
1176
  this._checkMappings(mappings);
1177
+ // Copies, not in-place changes: the race branch below must hand the
1178
+ // caller's own mappings and settings to `updateCollection`
1180
1179
  esRequest.mappings = {
1181
1180
  _meta: mappings._meta || this._config.commonMapping._meta,
1182
1181
  dynamic: mappings.dynamic || this._config.commonMapping.dynamic,
1183
- properties: lodash_1.default.merge(mappings.properties, this._config.commonMapping.properties),
1182
+ properties: lodash_1.default.merge({}, mappings.properties, this._config.commonMapping.properties),
1183
+ };
1184
+ esRequest.settings = {
1185
+ ...settings,
1186
+ number_of_replicas: settings.number_of_replicas ||
1187
+ this._config.defaultSettings.number_of_replicas,
1188
+ number_of_shards: settings.number_of_shards ||
1189
+ this._config.defaultSettings.number_of_shards,
1184
1190
  };
1185
- settings.number_of_replicas =
1186
- settings.number_of_replicas ||
1187
- this._config.defaultSettings.number_of_replicas;
1188
- settings.number_of_shards =
1189
- settings.number_of_shards ||
1190
- this._config.defaultSettings.number_of_shards;
1191
1191
  try {
1192
1192
  await this._client.indices.create(esRequest);
1193
1193
  }
@@ -1196,9 +1196,9 @@ class ES8 {
1196
1196
  "resource_already_exists_exception") {
1197
1197
  // race condition: the indice has been created between the "exists"
1198
1198
  // check above and this "create" attempt, by a request that may not
1199
- // have waited for its shards yet
1200
- await this.waitForCollectionShards(index, collection);
1201
- return null;
1199
+ // have waited for its shards yet. This request's mappings and
1200
+ // settings must still be applied, as if it had seen it exist
1201
+ return this._updateConcurrentlyCreatedCollection(index, collection, mappings, settings);
1202
1202
  }
1203
1203
  throw this._esWrapper.formatESError(error);
1204
1204
  }
@@ -1217,6 +1217,15 @@ class ES8 {
1217
1217
  }
1218
1218
  return null;
1219
1219
  }
1220
+ /**
1221
+ * Applies the mappings and settings of a `createCollection` call to a
1222
+ * collection that already exists because another request created it,
1223
+ * maybe so recently that its primary shards are still initializing.
1224
+ */
1225
+ async _updateConcurrentlyCreatedCollection(index, collection, mappings, settings) {
1226
+ await this.waitForCollectionShards(index, collection);
1227
+ return this.updateCollection(index, collection, { mappings, settings });
1228
+ }
1220
1229
  /**
1221
1230
  * Waits until the primary shards of a collection are active, for at most
1222
1231
  * `timeout`, and rejects if they are not by then.
@@ -163,6 +163,13 @@ export interface IKuzzleConfiguration {
163
163
  * Optional here for the reason given on {@link IKuzzleConfiguration.version}.
164
164
  */
165
165
  allowAllOrigins?: boolean;
166
+ /**
167
+ * Derived at startup: the deprecated configuration keys the user set
168
+ * (today, those of "security.jwt"), logged as a warning.
169
+ *
170
+ * Optional here for the reason given on {@link IKuzzleConfiguration.version}.
171
+ */
172
+ deprecatedSettings?: string[];
166
173
  };
167
174
  /**
168
175
  * Collection specifications to apply at startup, by index then collection.
@@ -21,9 +21,10 @@ export type SecurityConfiguration = {
21
21
  /**
22
22
  * @deprecated Use `security.authToken` instead.
23
23
  *
24
- * `default.config.ts` always ships it, and `authToken`'s readers fall back
25
- * to it — `authToken.algorithm ?? jwt.algorithm`. Optional all the same, as
26
- * v2.56.0 declared it: a required member breaks code that builds this type.
24
+ * Each setting the configuration sets here applies unless `authToken` sets
25
+ * it too (`loadConfig()` resolves both sections to the values in effect,
26
+ * see `authTokenSetting`). Optional all the same, as v2.56.0 declared it:
27
+ * a required member breaks code that builds this type.
27
28
  */
28
29
  jwt?: JSONObject;
29
30
  /**
@@ -54,8 +54,8 @@ export type ControllerDefinition = {
54
54
  handler: (request: KuzzleRequest) => Promise<any>;
55
55
  /**
56
56
  * Declare HTTP routes (optional).
57
- * Http routes will be auto-generated unless at least one is provided
58
- * or an empty array is provided.
57
+ * When omitted or empty, a default route is generated:
58
+ * `GET /_/<controller>/<action>`, both names in kebab-case.
59
59
  */
60
60
  http?: HttpRoute[];
61
61
  };
@@ -71,8 +71,8 @@ export type HttpRoute = {
71
71
  verb: "get" | "head" | "post" | "put" | "delete" | "patch" | "options";
72
72
  /**
73
73
  * Route path.
74
- * A route starting with `/` will be prefixed by `/_` otherwise the route
75
- * will be prefixed by `/_/<application-name>/`.
74
+ * A route starting with `/` is used as is, any other route is prefixed
75
+ * with `/_/`.
76
76
  */
77
77
  path: string;
78
78
  /**
@@ -86,9 +86,9 @@ export declare class Mutex {
86
86
  *
87
87
  * @return {Promise.<boolean>} True if the ressource has been unlocked before the `timeout`
88
88
  */
89
- wait({ attemptDelay, timeout, }: {
90
- attemptDelay?: number | undefined;
91
- timeout?: number | undefined;
89
+ wait({ attemptDelay, timeout, }?: {
90
+ attemptDelay?: number;
91
+ timeout?: number;
92
92
  }): Promise<boolean>;
93
93
  /**
94
94
  * Frees every lock this process holds, and stops it from taking new ones.
@@ -196,7 +196,7 @@ class Mutex {
196
196
  *
197
197
  * @return {Promise.<boolean>} True if the ressource has been unlocked before the `timeout`
198
198
  */
199
- async wait({ attemptDelay = this.attemptDelay, timeout = this.timeout, }) {
199
+ async wait({ attemptDelay = this.attemptDelay, timeout = this.timeout, } = {}) {
200
200
  let duration = 0;
201
201
  let isLocked;
202
202
  do {
package/dist/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kuzzle",
3
3
  "author": "The Kuzzle Team <support@kuzzle.io>",
4
- "version": "2.60.0-beta.4",
4
+ "version": "2.60.0-beta.5",
5
5
  "description": "Kuzzle is an open-source solution that handles all the data management through a secured API, with a large choice of protocols.",
6
6
  "scripts": {
7
7
  "build": "rm -Rf ./dist && tsc && node -r ts-node/register/transpile-only ./bin/copy-binaries.ts",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kuzzle",
3
3
  "author": "The Kuzzle Team <support@kuzzle.io>",
4
- "version": "2.60.0-beta.4",
4
+ "version": "2.60.0-beta.5",
5
5
  "description": "Kuzzle is an open-source solution that handles all the data management through a secured API, with a large choice of protocols.",
6
6
  "scripts": {
7
7
  "build": "rm -Rf ./dist && tsc && node -r ts-node/register/transpile-only ./bin/copy-binaries.ts",