kuzzle 2.60.0-beta.3 → 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 (56) 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/controllers/documentController.d.ts +1 -1
  4. package/dist/lib/api/funnel.js +12 -1
  5. package/dist/lib/api/httpRoutes.js +52 -20
  6. package/dist/lib/api/request/kuzzleRequest.d.ts +7 -2
  7. package/dist/lib/api/request/kuzzleRequest.js +20 -10
  8. package/dist/lib/api/request/requestContext.d.ts +1 -1
  9. package/dist/lib/api/request/requestResponse.js +2 -1
  10. package/dist/lib/cluster/command.d.ts +7 -0
  11. package/dist/lib/cluster/command.js +67 -28
  12. package/dist/lib/cluster/node.d.ts +13 -2
  13. package/dist/lib/cluster/node.js +20 -4
  14. package/dist/lib/cluster/subscriber.d.ts +2 -0
  15. package/dist/lib/cluster/subscriber.js +6 -0
  16. package/dist/lib/config/index.js +60 -3
  17. package/dist/lib/core/backend/backend.d.ts +4 -3
  18. package/dist/lib/core/backend/backend.js +15 -15
  19. package/dist/lib/core/backend/backendCluster.d.ts +1 -1
  20. package/dist/lib/core/backend/internalLogger.d.ts +6 -0
  21. package/dist/lib/core/backend/internalLogger.js +6 -0
  22. package/dist/lib/core/network/router.d.ts +18 -0
  23. package/dist/lib/core/network/router.js +44 -10
  24. package/dist/lib/core/security/authTokenSettings.d.ts +21 -0
  25. package/dist/lib/core/security/authTokenSettings.js +61 -0
  26. package/dist/lib/core/security/tokenRepository.js +8 -14
  27. package/dist/lib/core/shared/KoncordeWrapper.d.ts +9 -0
  28. package/dist/lib/core/shared/KoncordeWrapper.js +8 -8
  29. package/dist/lib/core/shared/store.js +4 -0
  30. package/dist/lib/core/storage/clientAdapter.d.ts +6 -0
  31. package/dist/lib/core/storage/clientAdapter.js +40 -5
  32. package/dist/lib/kuzzle/Logger.d.ts +4 -0
  33. package/dist/lib/kuzzle/Logger.js +9 -2
  34. package/dist/lib/kuzzle/internalIndexHandler.js +28 -11
  35. package/dist/lib/kuzzle/kuzzle.d.ts +17 -0
  36. package/dist/lib/kuzzle/kuzzle.js +62 -0
  37. package/dist/lib/kuzzle/vault.d.ts +13 -0
  38. package/dist/lib/kuzzle/vault.js +25 -7
  39. package/dist/lib/model/security/profile.d.ts +1 -1
  40. package/dist/lib/model/security/role.d.ts +1 -1
  41. package/dist/lib/model/security/user.d.ts +1 -1
  42. package/dist/lib/service/storage/7/elasticsearch.d.ts +18 -0
  43. package/dist/lib/service/storage/7/elasticsearch.js +50 -10
  44. package/dist/lib/service/storage/7/esWrapper.js +31 -2
  45. package/dist/lib/service/storage/8/elasticsearch.d.ts +18 -0
  46. package/dist/lib/service/storage/8/elasticsearch.js +50 -10
  47. package/dist/lib/service/storage/8/esWrapper.js +31 -2
  48. package/dist/lib/types/config/KuzzleConfiguration.d.ts +7 -0
  49. package/dist/lib/types/config/SecurityConfiguration.d.ts +4 -3
  50. package/dist/lib/types/controllers/ControllerDefinition.d.ts +4 -4
  51. package/dist/lib/types/events/EventGenericDocument.d.ts +2 -1
  52. package/dist/lib/types/events/EventProtocol.d.ts +1 -1
  53. package/dist/lib/util/mutex.d.ts +5 -4
  54. package/dist/lib/util/mutex.js +8 -2
  55. package/dist/package.json +1 -1
  56. package/package.json +1 -1
@@ -61,8 +61,8 @@ const node_fs_1 = __importDefault(require("node:fs"));
61
61
  const kuzzle_1 = __importDefault(require("../../kuzzle"));
62
62
  const embeddedSdk_1 = require("../shared/sdk/embeddedSdk");
63
63
  const kerror = __importStar(require("../../kerror"));
64
- const index_1 = require("../../../index");
65
- const index_2 = require("./index");
64
+ const name_generator_1 = require("../../util/name-generator");
65
+ const index_1 = require("./index");
66
66
  const Logger_1 = require("../../kuzzle/Logger");
67
67
  const plugin_1 = __importDefault(require("../plugin/plugin"));
68
68
  const assertionError = kerror.wrap("plugin", "assert");
@@ -130,7 +130,7 @@ class Backend {
130
130
  throw assertionError.get("invalid_application_name", name);
131
131
  }
132
132
  this._name = name;
133
- this.nodeId = index_1.NameGenerator.generateRandomName({ prefix: "knode" });
133
+ this.nodeId = name_generator_1.NameGenerator.generateRandomName({ prefix: "knode" });
134
134
  Reflect.defineProperty(this, "_kuzzle", {
135
135
  writable: true,
136
136
  });
@@ -162,18 +162,18 @@ class Backend {
162
162
  // Silent if no version can be found
163
163
  }
164
164
  global.app = this;
165
- this.pipe = new index_2.BackendPipe(this);
166
- this.hook = new index_2.BackendHook(this);
167
- this.config = new index_2.BackendConfig(this);
168
- this.vault = new index_2.BackendVault(this);
169
- this.controller = new index_2.BackendController(this);
170
- this.plugin = new index_2.BackendPlugin(this);
171
- this.storage = new index_2.BackendStorage(this);
172
- this.import = new index_2.BackendImport(this);
173
- this.cluster = new index_2.BackendCluster();
174
- this.openApi = new index_2.BackendOpenApi(this);
175
- this.errors = new index_2.BackendErrors(this);
176
- this.subscription = new index_2.BackendSubscription(this);
165
+ this.pipe = new index_1.BackendPipe(this);
166
+ this.hook = new index_1.BackendHook(this);
167
+ this.config = new index_1.BackendConfig(this);
168
+ this.vault = new index_1.BackendVault(this);
169
+ this.controller = new index_1.BackendController(this);
170
+ this.plugin = new index_1.BackendPlugin(this);
171
+ this.storage = new index_1.BackendStorage(this);
172
+ this.import = new index_1.BackendImport(this);
173
+ this.cluster = new index_1.BackendCluster();
174
+ this.openApi = new index_1.BackendOpenApi(this);
175
+ this.errors = new index_1.BackendErrors(this);
176
+ this.subscription = new index_1.BackendSubscription(this);
177
177
  this.log = new Logger_1.Logger(this.config.content, "kuzzle:app");
178
178
  this.kerror = kerror;
179
179
  try {
@@ -1,4 +1,4 @@
1
- import type { EventDefinition, ClusterEventHandler } from "../../../index";
1
+ import type { EventDefinition, ClusterEventHandler } from "../../types/EventHandler";
2
2
  export declare class BackendCluster {
3
3
  /**
4
4
  * Broadcasts an event to listeners across all registered Kuzzle nodes
@@ -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 = {}) {
@@ -156,6 +156,10 @@ class Store {
156
156
  }
157
157
  const exist = await global.kuzzle.ask(`core:storage:${this.scope}:collection:exist`, this.index, collection);
158
158
  if (exist) {
159
+ // Another node created it, maybe so recently that its primary
160
+ // shards are still initializing: a document request on it would
161
+ // get a 503 until they are active
162
+ await global.kuzzle.ask(`core:storage:${this.scope}:collection:shards:wait`, this.index, collection);
159
163
  // @deprecated
160
164
  const dynamicSettings = isConfigDeprecated
161
165
  ? null
@@ -41,6 +41,12 @@ declare class ClientAdapter {
41
41
  * Also checks for duplicated index names.
42
42
  */
43
43
  populateCache(): Promise<void>;
44
+ /**
45
+ * Adds the indexes and collections found in storage to the index cache,
46
+ * then reloads the collection write locks. Unlike `populateCache`, it never
47
+ * writes to storage.
48
+ */
49
+ reloadCache(): Promise<void>;
44
50
  /**
45
51
  * Replaces the cached collection write locks with the cluster-wide ones,
46
52
  * stored in Redis.
@@ -56,6 +56,7 @@ const Elasticsearch_1 = require("../../service/storage/Elasticsearch");
56
56
  const indexCache_1 = require("./indexCache");
57
57
  const safeObject_1 = require("../../util/safeObject");
58
58
  const kerror = __importStar(require("../../kerror"));
59
+ const kuzzleError_1 = require("../../kerror/errors/kuzzleError");
59
60
  const distributedLock_1 = require("../../util/distributedLock");
60
61
  require("../../types/Global");
61
62
  const servicesError = kerror.wrap("services", "storage");
@@ -93,10 +94,16 @@ class ClientAdapter {
93
94
  this.registerMappingEvents();
94
95
  this.registerCacheEvents();
95
96
  this.registerLockEvents();
96
- // The locks were first loaded with the cache, before this node joined the
97
- // cluster: one taken in between was broadcast before this node could hear
98
- // it. Reading them again once the cluster is joined closes that gap.
99
- global.kuzzle.onPipe("kuzzle:state:live", () => this.loadLocks());
97
+ // The cache and the locks were first loaded before this node joined the
98
+ // cluster: an index, a collection or a lock a peer added in between was
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.
106
+ global.kuzzle.onPipe("kuzzle:state:live", () => this.reloadCache());
100
107
  // Global store events registration
101
108
  /**
102
109
  * Manually refresh the index cache (e.g. after alias creation)
@@ -120,7 +127,20 @@ class ClientAdapter {
120
127
  throw servicesError.get("index_already_exists", this.scope, index);
121
128
  }
122
129
  if (!indexCacheOnly) {
123
- await this.client.createIndex(index);
130
+ try {
131
+ await this.client.createIndex(index);
132
+ }
133
+ catch (error) {
134
+ // The index exists in storage but not in this node's cache: a peer
135
+ // created it and its cache broadcast never reached this node (e.g. the
136
+ // peer stopped before this node joined the cluster). Cache it so that
137
+ // the next request to that index finds it, then report the conflict.
138
+ if (error instanceof kuzzleError_1.KuzzleError &&
139
+ error.id === "services.storage.index_already_exists") {
140
+ this.cache.addIndex(index);
141
+ }
142
+ throw error;
143
+ }
124
144
  }
125
145
  this.cache.addIndex(index);
126
146
  if (propagate) {
@@ -191,6 +211,14 @@ class ClientAdapter {
191
211
  if (global.kuzzle.config.services.storageEngine.generateMissingAliases) {
192
212
  await this.client.generateMissingAliases();
193
213
  }
214
+ await this.reloadCache();
215
+ }
216
+ /**
217
+ * Adds the indexes and collections found in storage to the index cache,
218
+ * then reloads the collection write locks. Unlike `populateCache`, it never
219
+ * writes to storage.
220
+ */
221
+ async reloadCache() {
194
222
  // `client` is untyped (see the field's comment), so the schema shape is
195
223
  // stated here: index name -> its collection names.
196
224
  const schema = await this.client.getSchema();
@@ -311,6 +339,13 @@ class ClientAdapter {
311
339
  this.cache.assertCollectionExists(index, collection);
312
340
  return this.client.getSettings(index, collection);
313
341
  });
342
+ /**
343
+ * Wait, for a bounded time, until the primary shards of a collection
344
+ * another node has just created are active
345
+ * @param {string} index
346
+ * @param {string} collection
347
+ */
348
+ global.kuzzle.onAsk(`core:storage:${this.scope}:collection:shards:wait`, (index, collection) => this.client.waitForCollectionShards(index, collection));
314
349
  /**
315
350
  * Check a collection existence
316
351
  * @param {string} index
@@ -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");
@@ -132,8 +133,11 @@ class InternalIndexHandler extends store_1.Store {
132
133
  return;
133
134
  }
134
135
  await bluebird_1.default.resolve(this._bootstrapSequence()).timeout(this.timeout);
135
- await this.create("config", { timestamp: Date.now() }, {
136
- id: this._BOOTSTRAP_DONE_ID,
136
+ // A node that stopped while holding the lock may still have its
137
+ // own bootstrap in flight, and write this marker after the check
138
+ // above: replacing it keeps this node from failing on it
139
+ await this.createOrReplace("config", this._BOOTSTRAP_DONE_ID, {
140
+ timestamp: Date.now(),
137
141
  });
138
142
  }
139
143
  catch (error) {
@@ -154,9 +158,11 @@ class InternalIndexHandler extends store_1.Store {
154
158
  await this.createInitialValidations();
155
159
  debug("Bootstrapping JWT secret");
156
160
  await this._initSecret();
157
- // Create datamodel version
158
- await this.create("config", { version: dataModelVersion }, {
159
- id: this._DATAMODEL_VERSION_ID,
161
+ // Replaced rather than created: a bootstrap interrupted after this write
162
+ // but before the `.done` marker leaves it behind, and the next attempt
163
+ // must not fail on it (every step above is idempotent too)
164
+ await this.createOrReplace("config", this._DATAMODEL_VERSION_ID, {
165
+ version: dataModelVersion,
160
166
  });
161
167
  }
162
168
  /**
@@ -186,16 +192,27 @@ class InternalIndexHandler extends store_1.Store {
186
192
  await bluebird_1.default.all(promises);
187
193
  }
188
194
  async _initSecret() {
189
- // NOSONAR: `jwt` is the deprecated spelling of `authToken`, read second so
190
- // an existing configuration keeps working. Dropping it is a breaking change
191
- // for anyone who has not migrated, not a conversion's to make.
192
- const { authToken, jwt } = global.kuzzle.config.security; // NOSONAR
193
- 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");
194
198
  let storedSeed = await this.exists("config", this._JWT_SECRET_ID);
195
199
  if (!configSeed) {
196
200
  if (!storedSeed) {
197
201
  storedSeed = node_crypto_1.default.randomBytes(512).toString("hex");
198
- await this.create("config", { seed: storedSeed }, { id: this._JWT_SECRET_ID });
202
+ try {
203
+ await this.create("config", { seed: storedSeed }, { id: this._JWT_SECRET_ID });
204
+ }
205
+ catch (error) {
206
+ // An interrupted bootstrap stored one in the meantime: it is the
207
+ // seed other nodes may already sign with, so keep it, and read it
208
+ // back below
209
+ const alreadyStored = error instanceof Error &&
210
+ "id" in error &&
211
+ error.id === "services.storage.document_already_exists";
212
+ if (!alreadyStored) {
213
+ throw error;
214
+ }
215
+ }
199
216
  }
200
217
  this.logger.warn("[!] Kuzzle is using a generated seed for authentication. This is suitable for development but should NEVER be used in production. See https://docs.kuzzle.io/core/2/guides/getting-started/deploy-your-application/");
201
218
  }
@@ -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
  *