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
@@ -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();
@@ -77,7 +77,7 @@ declare class DocumentController extends NativeController {
77
77
  errors: any;
78
78
  successes: any;
79
79
  }>;
80
- export(request: KuzzleRequest): Promise<import("../../..").HttpStream>;
80
+ export(request: KuzzleRequest): Promise<import("../../types").HttpStream>;
81
81
  /**
82
82
  * @param {Request} request
83
83
  * @returns {Promise<Object>}
@@ -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":
@@ -21,6 +21,8 @@ type CommandingNode = {
21
21
  };
22
22
  ip: string;
23
23
  nodeId: string;
24
+ /** Whether `nodeId` can be read: false until the handshake sets it. */
25
+ hasNodeId: boolean;
24
26
  activity: Activity[];
25
27
  /** Only `serialize()`: the command layer never mutates the state. */
26
28
  fullState: {
@@ -102,6 +104,11 @@ declare class ClusterCommand {
102
104
  * This method can (and should) never return a rejected promise.
103
105
  */
104
106
  listen(): Promise<void>;
107
+ /**
108
+ * Answers one request. Every request gets exactly one reply: REP/REQ
109
+ * requires it, and a request left unanswered wedges the peer's socket.
110
+ */
111
+ private answer;
105
112
  /**
106
113
  * Closes opened sockets and marks this server as closed.
107
114
  */
@@ -159,32 +159,9 @@ class ClusterCommand {
159
159
  while (this.state === stateEnum.RUNNING) {
160
160
  try {
161
161
  const [type, data] = await this.server.receive();
162
- // A REQ frame carries a topic and, for a handshake, a payload. A
163
- // request missing either is answered with DISCARDED, which is what
164
- // this socket owes every REQ — the two frames were read unchecked.
165
- switch (type?.toString()) {
166
- case commandMessages_1.commandTopic.FULLSTATE:
167
- await this.sendFullState();
168
- break;
169
- case commandMessages_1.commandTopic.HANDSHAKE:
170
- if (data === undefined) {
171
- await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
172
- break;
173
- }
174
- await this.handleHandshake(data);
175
- break;
176
- case commandMessages_1.commandTopic.RETRANSMIT:
177
- if (data === undefined) {
178
- await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
179
- break;
180
- }
181
- await this.handleRetransmit(data);
182
- break;
183
- default:
184
- // REP/REQ sockets expect a reply to each request made, so we have
185
- // to send some kind of response on an invalid request received
186
- await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
187
- }
162
+ // NOSONAR (S9382): a REP socket strictly alternates receive and send,
163
+ // so a request must be answered before the next one can be read.
164
+ await this.answer(type, data); // NOSONAR
188
165
  }
189
166
  catch (e) {
190
167
  if (this.state !== stateEnum.CLOSED) {
@@ -193,6 +170,47 @@ class ClusterCommand {
193
170
  }
194
171
  }
195
172
  }
173
+ /**
174
+ * Answers one request. Every request gets exactly one reply: REP/REQ
175
+ * requires it, and a request left unanswered wedges the peer's socket.
176
+ */
177
+ async answer(type, data) {
178
+ // The socket is bound before the handshake sets this node's id, and a
179
+ // peer still talking to a dead node on this IP can reach it (#3031). A
180
+ // node with no id yet has no state to serve and cannot take a peer in:
181
+ // every request is answered DISCARDED, which the requester treats as no
182
+ // answer.
183
+ if (!this.node.hasNodeId) {
184
+ await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
185
+ return;
186
+ }
187
+ // A REQ frame carries a topic and, for a handshake, a payload. A request
188
+ // missing either is answered with DISCARDED, which is what this socket
189
+ // owes every REQ — the two frames were read unchecked.
190
+ switch (type?.toString()) {
191
+ case commandMessages_1.commandTopic.FULLSTATE:
192
+ await this.sendFullState();
193
+ break;
194
+ case commandMessages_1.commandTopic.HANDSHAKE:
195
+ if (data === undefined) {
196
+ await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
197
+ break;
198
+ }
199
+ await this.handleHandshake(data);
200
+ break;
201
+ case commandMessages_1.commandTopic.RETRANSMIT:
202
+ if (data === undefined) {
203
+ await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
204
+ break;
205
+ }
206
+ await this.handleRetransmit(data);
207
+ break;
208
+ default:
209
+ // REP/REQ sockets expect a reply to each request made, so we have
210
+ // to send some kind of response on an invalid request received
211
+ await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
212
+ }
213
+ }
196
214
  /**
197
215
  * Closes opened sockets and marks this server as closed.
198
216
  */
@@ -339,7 +357,19 @@ class ClusterCommand {
339
357
  // No payload message for a full state request
340
358
  await req.send([commandMessages_1.commandTopic.FULLSTATE, null]);
341
359
  try {
342
- [, fullState = null] = await req.receive();
360
+ // NOSONAR (S9382): one node at a time by design, the loop stops at
361
+ // the first one that serves a full state.
362
+ const [topic, payload] = await req.receive(); // NOSONAR
363
+ // DISCARDED carries an empty frame, which would decode as an empty
364
+ // full state: a node that cannot serve one (#3031) is skipped like
365
+ // one that does not answer.
366
+ if (topic?.toString() === commandMessages_1.commandTopic.FULLSTATE && payload) {
367
+ fullState = payload;
368
+ }
369
+ else {
370
+ this.logger.warn(`Unable to fetch a full state from node ${id} (request discarded)`);
371
+ idx = (idx + 1) % nodes.length;
372
+ }
343
373
  }
344
374
  catch {
345
375
  // no response from the remote node in a timely fashion... retrying
@@ -403,7 +433,16 @@ class ClusterCommand {
403
433
  await req.send([commandMessages_1.commandTopic.HANDSHAKE, payload]);
404
434
  let response = null;
405
435
  try {
406
- [, response = null] = await req.receive();
436
+ const [topic, answer] = await req.receive();
437
+ // DISCARDED carries an empty frame, which would decode as a handshake
438
+ // answer with nothing in it: a node that refused the handshake (#3031)
439
+ // is one that did not answer.
440
+ if (topic?.toString() === commandMessages_1.commandTopic.HANDSHAKE && answer) {
441
+ response = answer;
442
+ }
443
+ else {
444
+ this.logger.warn(`Couldn't complete handshake with node ${id}: request discarded`);
445
+ }
407
446
  }
408
447
  catch {
409
448
  this.logger.warn(`Couldn't complete handshake with node ${id}: no response received`);
@@ -41,10 +41,21 @@ declare class ClusterNode {
41
41
  * The id of the ID card this node reserved during `handshake()`.
42
42
  *
43
43
  * Null until the handshake gets that far, and read as a `string` at
44
- * seventeen sites here plus `subscriber.ts` and `command.ts` — all of which
45
- * run after it. The accessor says once where that can fail.
44
+ * seventeen sites here plus `subscriber.ts` and `command.ts`. The accessor
45
+ * says once where that can fail: an internal read before the handshake is a
46
+ * bug, and it throws.
47
+ *
48
+ * Network input is not internal: the command socket is bound before the
49
+ * handshake, and a node restarted on the IP of a dead one can be reached by
50
+ * peers still talking to that dead node (#3031). The paths network input
51
+ * reaches check `hasNodeId` first.
46
52
  */
47
53
  get nodeId(): string;
54
+ /**
55
+ * Whether the handshake has set this node's id yet: the non-throwing check
56
+ * for the paths network input reaches before it has (#3031).
57
+ */
58
+ get hasNodeId(): boolean;
48
59
  set nodeId(id: string);
49
60
  private heartbeatTimer;
50
61
  readonly idCardHandler: ClusterIdCardHandler;
@@ -200,8 +200,14 @@ class ClusterNode {
200
200
  * The id of the ID card this node reserved during `handshake()`.
201
201
  *
202
202
  * Null until the handshake gets that far, and read as a `string` at
203
- * seventeen sites here plus `subscriber.ts` and `command.ts` — all of which
204
- * run after it. The accessor says once where that can fail.
203
+ * seventeen sites here plus `subscriber.ts` and `command.ts`. The accessor
204
+ * says once where that can fail: an internal read before the handshake is a
205
+ * bug, and it throws.
206
+ *
207
+ * Network input is not internal: the command socket is bound before the
208
+ * handshake, and a node restarted on the IP of a dead one can be reached by
209
+ * peers still talking to that dead node (#3031). The paths network input
210
+ * reaches check `hasNodeId` first.
205
211
  */
206
212
  get nodeId() {
207
213
  if (this._nodeId === null) {
@@ -209,6 +215,13 @@ class ClusterNode {
209
215
  }
210
216
  return this._nodeId;
211
217
  }
218
+ /**
219
+ * Whether the handshake has set this node's id yet: the non-throwing check
220
+ * for the paths network input reaches before it has (#3031).
221
+ */
222
+ get hasNodeId() {
223
+ return this._nodeId !== null;
224
+ }
212
225
  set nodeId(id) {
213
226
  this._nodeId = id;
214
227
  }
@@ -389,8 +402,11 @@ class ClusterNode {
389
402
  this.remoteNodes.delete(nodeId);
390
403
  this.fullState.removeNode(nodeId);
391
404
  subscriber.dispose();
392
- if (broadcast) {
393
- this.publisher.sendNodeEvicted(this.nodeId, nodeId, reason);
405
+ // A subscriber can evict its remote node (heartbeat lost, gap, invalid
406
+ // message) before this node's handshake: there is no evictor id to
407
+ // broadcast then, and the eviction stays local (#3031).
408
+ if (broadcast && this._nodeId !== null) {
409
+ this.publisher.sendNodeEvicted(this._nodeId, nodeId, reason);
394
410
  }
395
411
  if (this.countActiveNodes() < this.config.minimumNodes) {
396
412
  global.kuzzle.state = kuzzleStateEnum_1.default.NOT_ENOUGH_NODES;
@@ -15,6 +15,8 @@ type SubscribingNode = {
15
15
  config: IKuzzleConfiguration["cluster"];
16
16
  heartbeatDelay: number;
17
17
  nodeId: string;
18
+ /** Whether `nodeId` can be read: false until the handshake sets it. */
19
+ hasNodeId: boolean;
18
20
  fullState: State;
19
21
  eventEmitter: {
20
22
  emit(event: string, payload: unknown): void;
@@ -383,6 +383,12 @@ class ClusterSubscriber {
383
383
  * @param message - decoded NodeEvicted protobuf message
384
384
  */
385
385
  async handleNodeEviction(message) {
386
+ // Before the handshake this node has no id to compare, and is not part of
387
+ // the cluster the eviction is about: the message is ignored (#3031).
388
+ if (!this.localNode.hasNodeId) {
389
+ debug("[CLUSTER] Eviction of node %s by %s ignored: the handshake has not run", message.nodeId, message.evictor);
390
+ return;
391
+ }
386
392
  if (message.nodeId === this.localNode.nodeId) {
387
393
  this.logger.error(`[CLUSTER] Node evicted by ${message.evictor}. Reason: ${message.reason}`);
388
394
  // A failure, like `evictSelf`'s: exit non-zero (#2785).
@@ -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.
@@ -1,6 +1,6 @@
1
1
  import type { JSONObject } from "../../types/JSONObject";
2
2
  import { EmbeddedSDK } from "../shared/sdk/embeddedSdk";
3
- import type { EventDefinition } from "../../../index";
3
+ import type { EventDefinition } from "../../types/EventHandler";
4
4
  import { BackendCluster, BackendConfig, BackendController, BackendHook, BackendImport, BackendPipe, BackendPlugin, BackendStorage, BackendVault, BackendOpenApi, BackendErrors, BackendSubscription } from "./index";
5
5
  import { Logger } from "../../kuzzle/Logger";
6
6
  import Plugin from "../plugin/plugin";
@@ -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
  /**