@lenne.tech/nest-server 11.27.5 → 11.27.7

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 (147) hide show
  1. package/.claude/rules/architecture.md +66 -0
  2. package/.claude/rules/better-auth.md +88 -2
  3. package/.claude/rules/configurable-features.md +2 -2
  4. package/.claude/rules/package-management.md +23 -14
  5. package/.claude/rules/testing.md +27 -9
  6. package/CLAUDE.md +12 -4
  7. package/FRAMEWORK-API.md +1 -1
  8. package/bin/migrate.js +84 -25
  9. package/dist/core/common/decorators/restricted.decorator.d.ts +4 -4
  10. package/dist/core/common/decorators/restricted.decorator.js +21 -22
  11. package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
  12. package/dist/core/common/helpers/clone.helper.d.ts +6 -0
  13. package/dist/core/common/helpers/clone.helper.js +67 -0
  14. package/dist/core/common/helpers/clone.helper.js.map +1 -0
  15. package/dist/core/common/helpers/cookies.helper.d.ts +1 -0
  16. package/dist/core/common/helpers/cookies.helper.js +33 -16
  17. package/dist/core/common/helpers/cookies.helper.js.map +1 -1
  18. package/dist/core/common/helpers/db.helper.d.ts +2 -14
  19. package/dist/core/common/helpers/db.helper.js +13 -106
  20. package/dist/core/common/helpers/db.helper.js.map +1 -1
  21. package/dist/core/common/helpers/id.helper.d.ts +15 -0
  22. package/dist/core/common/helpers/id.helper.js +104 -0
  23. package/dist/core/common/helpers/id.helper.js.map +1 -0
  24. package/dist/core/common/helpers/input.helper.d.ts +1 -6
  25. package/dist/core/common/helpers/input.helper.js +11 -70
  26. package/dist/core/common/helpers/input.helper.js.map +1 -1
  27. package/dist/core/common/inputs/combined-filter.input.d.ts +1 -12
  28. package/dist/core/common/inputs/combined-filter.input.js +2 -51
  29. package/dist/core/common/inputs/combined-filter.input.js.map +1 -1
  30. package/dist/core/common/inputs/filter.input.d.ts +10 -1
  31. package/dist/core/common/inputs/filter.input.js +40 -5
  32. package/dist/core/common/inputs/filter.input.js.map +1 -1
  33. package/dist/core/common/services/config.service.js +5 -5
  34. package/dist/core/common/services/config.service.js.map +1 -1
  35. package/dist/core/modules/ai/core-ai.constants.d.ts +22 -0
  36. package/dist/core/modules/ai/core-ai.constants.js +26 -0
  37. package/dist/core/modules/ai/core-ai.constants.js.map +1 -0
  38. package/dist/core/modules/ai/index.d.ts +1 -0
  39. package/dist/core/modules/ai/index.js +1 -0
  40. package/dist/core/modules/ai/index.js.map +1 -1
  41. package/dist/core/modules/ai/interfaces/ai-interaction-record.interface.d.ts +17 -0
  42. package/dist/core/modules/ai/interfaces/ai-interaction-record.interface.js +3 -0
  43. package/dist/core/modules/ai/interfaces/ai-interaction-record.interface.js.map +1 -0
  44. package/dist/core/modules/ai/services/core-ai-budget.service.d.ts +1 -2
  45. package/dist/core/modules/ai/services/core-ai-budget.service.js +7 -5
  46. package/dist/core/modules/ai/services/core-ai-budget.service.js.map +1 -1
  47. package/dist/core/modules/ai/services/core-ai-connection-preference.service.d.ts +1 -2
  48. package/dist/core/modules/ai/services/core-ai-connection-preference.service.js +7 -5
  49. package/dist/core/modules/ai/services/core-ai-connection-preference.service.js.map +1 -1
  50. package/dist/core/modules/ai/services/core-ai-connection.service.d.ts +1 -2
  51. package/dist/core/modules/ai/services/core-ai-connection.service.js +7 -5
  52. package/dist/core/modules/ai/services/core-ai-connection.service.js.map +1 -1
  53. package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +1 -2
  54. package/dist/core/modules/ai/services/core-ai-conversation.service.js +7 -5
  55. package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
  56. package/dist/core/modules/ai/services/core-ai-interaction.service.d.ts +2 -3
  57. package/dist/core/modules/ai/services/core-ai-interaction.service.js +7 -5
  58. package/dist/core/modules/ai/services/core-ai-interaction.service.js.map +1 -1
  59. package/dist/core/modules/ai/services/core-ai-mode.service.d.ts +1 -2
  60. package/dist/core/modules/ai/services/core-ai-mode.service.js +7 -5
  61. package/dist/core/modules/ai/services/core-ai-mode.service.js.map +1 -1
  62. package/dist/core/modules/ai/services/core-ai-prompt-hint.service.d.ts +1 -3
  63. package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js +7 -5
  64. package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js.map +1 -1
  65. package/dist/core/modules/ai/services/core-ai-prompt.service.d.ts +1 -2
  66. package/dist/core/modules/ai/services/core-ai-prompt.service.js +7 -5
  67. package/dist/core/modules/ai/services/core-ai-prompt.service.js.map +1 -1
  68. package/dist/core/modules/ai/services/core-ai-slot.service.d.ts +1 -2
  69. package/dist/core/modules/ai/services/core-ai-slot.service.js +7 -5
  70. package/dist/core/modules/ai/services/core-ai-slot.service.js.map +1 -1
  71. package/dist/core/modules/ai/services/core-ai-tool-grant.service.d.ts +1 -2
  72. package/dist/core/modules/ai/services/core-ai-tool-grant.service.js +7 -5
  73. package/dist/core/modules/ai/services/core-ai-tool-grant.service.js.map +1 -1
  74. package/dist/core/modules/ai/services/core-ai-tool-policy.service.d.ts +1 -2
  75. package/dist/core/modules/ai/services/core-ai-tool-policy.service.js +7 -5
  76. package/dist/core/modules/ai/services/core-ai-tool-policy.service.js.map +1 -1
  77. package/dist/core/modules/ai/services/core-ai.service.d.ts +2 -17
  78. package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
  79. package/dist/core/modules/better-auth/better-auth-roles.guard.js +2 -2
  80. package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -1
  81. package/dist/core/modules/better-auth/better-auth.config.js +12 -8
  82. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  83. package/dist/core/modules/better-auth/core-better-auth.constants.d.ts +3 -0
  84. package/dist/core/modules/better-auth/core-better-auth.constants.js +7 -0
  85. package/dist/core/modules/better-auth/core-better-auth.constants.js.map +1 -0
  86. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +1 -2
  87. package/dist/core/modules/better-auth/core-better-auth.module.js +23 -20
  88. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  89. package/dist/core/modules/better-auth/core-better-auth.registry.d.ts +4 -0
  90. package/dist/core/modules/better-auth/core-better-auth.registry.js +16 -0
  91. package/dist/core/modules/better-auth/core-better-auth.registry.js.map +1 -0
  92. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +1 -2
  93. package/dist/core/modules/better-auth/core-better-auth.service.js +7 -6
  94. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  95. package/dist/core/modules/better-auth/index.d.ts +1 -0
  96. package/dist/core/modules/better-auth/index.js +1 -0
  97. package/dist/core/modules/better-auth/index.js.map +1 -1
  98. package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
  99. package/dist/core/modules/migrate/migration-runner.js +3 -2
  100. package/dist/core/modules/migrate/migration-runner.js.map +1 -1
  101. package/dist/core/modules/tus/tus.constants.d.ts +1 -0
  102. package/dist/core/modules/tus/tus.constants.js +5 -0
  103. package/dist/core/modules/tus/tus.constants.js.map +1 -0
  104. package/dist/core/modules/tus/tus.module.d.ts +1 -1
  105. package/dist/core/modules/tus/tus.module.js +8 -6
  106. package/dist/core/modules/tus/tus.module.js.map +1 -1
  107. package/dist/tsconfig.build.tsbuildinfo +1 -1
  108. package/docs/REQUEST-LIFECYCLE.md +17 -4
  109. package/migration-guides/11.27.5-to-11.27.6.md +359 -0
  110. package/migration-guides/11.27.6-to-11.27.7.md +374 -0
  111. package/package.json +11 -104
  112. package/src/core/common/decorators/restricted.decorator.ts +33 -8
  113. package/src/core/common/helpers/clone.helper.ts +110 -0
  114. package/src/core/common/helpers/cookies.helper.ts +101 -26
  115. package/src/core/common/helpers/db.helper.ts +14 -161
  116. package/src/core/common/helpers/id.helper.ts +198 -0
  117. package/src/core/common/helpers/input.helper.ts +12 -84
  118. package/src/core/common/inputs/combined-filter.input.ts +10 -57
  119. package/src/core/common/inputs/filter.input.ts +92 -1
  120. package/src/core/common/interfaces/server-options.interface.ts +35 -6
  121. package/src/core/common/services/config.service.ts +4 -1
  122. package/src/core/modules/ai/core-ai.constants.ts +92 -0
  123. package/src/core/modules/ai/index.ts +1 -0
  124. package/src/core/modules/ai/interfaces/ai-interaction-record.interface.ts +34 -0
  125. package/src/core/modules/ai/services/core-ai-budget.service.ts +8 -4
  126. package/src/core/modules/ai/services/core-ai-connection-preference.service.ts +8 -4
  127. package/src/core/modules/ai/services/core-ai-connection.service.ts +5 -6
  128. package/src/core/modules/ai/services/core-ai-conversation.service.ts +5 -6
  129. package/src/core/modules/ai/services/core-ai-interaction.service.ts +6 -7
  130. package/src/core/modules/ai/services/core-ai-mode.service.ts +8 -2
  131. package/src/core/modules/ai/services/core-ai-prompt-hint.service.ts +7 -4
  132. package/src/core/modules/ai/services/core-ai-prompt.service.ts +8 -2
  133. package/src/core/modules/ai/services/core-ai-slot.service.ts +7 -4
  134. package/src/core/modules/ai/services/core-ai-tool-grant.service.ts +8 -4
  135. package/src/core/modules/ai/services/core-ai-tool-policy.service.ts +8 -4
  136. package/src/core/modules/ai/services/core-ai.service.ts +8 -11
  137. package/src/core/modules/better-auth/README.md +10 -4
  138. package/src/core/modules/better-auth/better-auth-roles.guard.ts +11 -5
  139. package/src/core/modules/better-auth/better-auth.config.ts +90 -15
  140. package/src/core/modules/better-auth/core-better-auth.constants.ts +73 -0
  141. package/src/core/modules/better-auth/core-better-auth.module.ts +34 -8
  142. package/src/core/modules/better-auth/core-better-auth.registry.ts +53 -0
  143. package/src/core/modules/better-auth/core-better-auth.service.ts +19 -12
  144. package/src/core/modules/better-auth/index.ts +9 -0
  145. package/src/core/modules/migrate/migration-runner.ts +16 -2
  146. package/src/core/modules/tus/tus.constants.ts +25 -0
  147. package/src/core/modules/tus/tus.module.ts +7 -2
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Cloning / freezing helpers.
3
+ *
4
+ * These live in their own file — instead of `input.helper.ts` — so that `config.service` can reach
5
+ * them without importing `input.helper`, which imports `restricted.decorator` behind it.
6
+ *
7
+ * That edge closed a runtime cycle:
8
+ *
9
+ * restricted.decorator → tenant/core-tenant.helpers → config.service → input.helper
10
+ * → restricted.decorator
11
+ *
12
+ * so `input.helper` evaluated while `restricted.decorator` was still initializing. Nothing crashed,
13
+ * but only because every cross-cycle dereference sat inside a function body. A single top-level line
14
+ * in `input.helper` — a module-level alias of `checkRestricted`, an `@Restricted`-decorated class,
15
+ * `design:type` metadata — would have thrown under SWC → CommonJS:
16
+ *
17
+ * ReferenceError: Cannot access 'checkRestricted' before initialization
18
+ *
19
+ * …in the file that drives field-level access control, on a compiler this repo's default build never
20
+ * runs, with a green test suite. Moving these two pure functions out removes the edge, so the cycle
21
+ * no longer exists rather than merely being disarmed.
22
+ *
23
+ * This file imports only Node built-ins, lodash and rfdc — no framework code — so it can never be
24
+ * part of a cycle itself. Keep it that way.
25
+ *
26
+ * `input.helper` re-exports both functions, so every existing import path keeps working.
27
+ * See .claude/rules/architecture.md → "DI Token Placement (SWC-Safe)".
28
+ */
29
+ import * as inspector from 'inspector';
30
+ import _ = require('lodash');
31
+ import rfdc = require('rfdc');
32
+ import * as util from 'util';
33
+
34
+ /**
35
+ * Get clone of object
36
+ *
37
+ * @param object Object to clone
38
+ * @param options Options for cloning
39
+ * @param options.checkResult Whether to check the result of the cloning process
40
+ * @param options.circles Keeping track of circular references will slow down performance with an additional 25% overhead.
41
+ * Even if an object doesn't have any circular references, the tracking overhead is the cost.
42
+ * By default if an object with a circular reference is passed to rfdc, it will throw
43
+ * (similar to how JSON.stringify would throw). Use the circles option to detect and preserve
44
+ * circular references in the object. If performance is important, try removing the circular
45
+ * reference from the object (set to undefined) and then add it back manually after cloning
46
+ * instead of using this option.
47
+ * @param options.proto Copy prototype properties as well as own properties into the new object.
48
+ * It's marginally faster to allow enumerable properties on the prototype to be copied into the
49
+ * cloned object (not onto it's prototype, directly onto the object).
50
+ */
51
+ export function clone(object: any, options?: { checkResult?: boolean; circles?: boolean; proto?: boolean }) {
52
+ const config = {
53
+ checkResult: true,
54
+ circles: true,
55
+ debug: inspector.url() !== undefined,
56
+ proto: false,
57
+ ...options,
58
+ };
59
+
60
+ try {
61
+ const cloned = rfdc(config)(object);
62
+ if (config.checkResult && !util.isDeepStrictEqual(object, cloned)) {
63
+ throw new Error('Cloned object differs from original object');
64
+ }
65
+ return cloned;
66
+ } catch (e) {
67
+ if (!config.circles) {
68
+ if (config.debug) {
69
+ console.debug(e, config, object, 'automatic try to use rfdc with circles');
70
+ }
71
+ try {
72
+ const clonedWithCircles = rfdc({
73
+ ...config,
74
+ circles: true,
75
+ })(object);
76
+ if (config.checkResult && !util.isDeepStrictEqual(object, clonedWithCircles)) {
77
+ throw new Error('Cloned object differs from original object', { cause: e });
78
+ }
79
+ return clonedWithCircles;
80
+ } catch (innerError) {
81
+ if (config.debug) {
82
+ console.debug(innerError, 'rfcd with circles did not work => automatic use of _.clone!');
83
+ }
84
+ return _.cloneDeep(object);
85
+ }
86
+ } else {
87
+ if (config.debug) {
88
+ console.debug(e, config, object, 'automatic try to use _.clone instead rfdc');
89
+ }
90
+ return _.cloneDeep(object);
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Get deep frozen object
97
+ */
98
+ export function deepFreeze(object: any, visited: WeakSet<object> = new WeakSet()) {
99
+ if (!object || typeof object !== 'object') {
100
+ return object;
101
+ }
102
+ if (visited.has(object)) {
103
+ return object;
104
+ }
105
+ visited.add(object);
106
+ for (const [key, value] of Object.entries(object)) {
107
+ object[key] = deepFreeze(value, visited);
108
+ }
109
+ return Object.freeze(object);
110
+ }
@@ -273,8 +273,7 @@ export const LOCALHOST_URL_ENVS: readonly string[] = ['ci', 'e2e', 'local'];
273
273
  const API_HOST_LABEL = 'api.';
274
274
 
275
275
  /**
276
- * Normalizes a URL string to its http(s) origin, or `undefined` when it is not a usable
277
- * http(s) URL.
276
+ * Parses a URL string into a `URL`, or returns `undefined` when it is not a usable http(s) URL.
278
277
  *
279
278
  * The `protocol` guard is security-relevant, not cosmetic: `URL.origin` serializes to the
280
279
  * literal string `'null'` for opaque origins (any non-special scheme, e.g. `custom://host`).
@@ -282,7 +281,7 @@ const API_HOST_LABEL = 'api.';
282
281
  * `credentials: true` allowlist would grant credentialed access to any site able to frame a
283
282
  * sandboxed document.
284
283
  */
285
- function toHttpOrigin(value: string): string | undefined {
284
+ function toHttpUrl(value: string): undefined | URL {
286
285
  let url: URL;
287
286
  try {
288
287
  url = new URL(value);
@@ -294,7 +293,15 @@ function toHttpOrigin(value: string): string | undefined {
294
293
  return undefined;
295
294
  }
296
295
 
297
- return url.origin;
296
+ return url;
297
+ }
298
+
299
+ /**
300
+ * Normalizes a URL string to its http(s) origin, or `undefined` when it is not a usable
301
+ * http(s) URL. See {@link toHttpUrl} for why the protocol guard matters.
302
+ */
303
+ function toHttpOrigin(value: string): string | undefined {
304
+ return toHttpUrl(value)?.origin;
298
305
  }
299
306
 
300
307
  /**
@@ -316,7 +323,8 @@ function isLocalhostUrl(value: string | undefined): boolean {
316
323
  }
317
324
 
318
325
  /**
319
- * Whether stripping the leading `api.` label from the hostname leaves a deployable host.
326
+ * The hostname with its leading `api.` label removed, or `undefined` when the label is absent
327
+ * or stripping it would not leave a deployable host.
320
328
  *
321
329
  * Guards two cases where a naive strip produces a bogus origin:
322
330
  * - `api.dev` → `dev` — a bare TLD. `api.dev`/`api.io`/`api.co` are registrable domains, so
@@ -326,14 +334,65 @@ function isLocalhostUrl(value: string | undefined): boolean {
326
334
  * `URL` setter for special schemes, so the strip would appear to succeed but do nothing.
327
335
  *
328
336
  * `localhost` is the one legitimate single-label host (`api.localhost` → `localhost`).
337
+ *
338
+ * Exported for reuse by BetterAuth's `deriveCookieDomainFromUrls()`, so the cookie-domain
339
+ * derivation shares this bare-TLD/empty guard instead of re-implementing a naive `api.`-strip.
329
340
  */
330
- function canStripApiLabel(hostname: string): boolean {
341
+ export function strippedApiHostname(hostname: string): string | undefined {
331
342
  if (!hostname.startsWith(API_HOST_LABEL)) {
332
- return false;
343
+ return undefined;
333
344
  }
334
345
 
335
346
  const remainder = hostname.slice(API_HOST_LABEL.length);
336
- return remainder === 'localhost' || remainder.includes('.');
347
+ if (remainder !== 'localhost' && !remainder.includes('.')) {
348
+ return undefined;
349
+ }
350
+
351
+ return remainder;
352
+ }
353
+
354
+ /**
355
+ * Derives the app origin from an already-parsed API base `URL` by stripping a leading `api.`
356
+ * label from its hostname. Mutates and re-serializes the passed `URL`, so callers must not
357
+ * reuse it afterwards. Shared by {@link deriveAppUrlFromBaseUrl} (string entry point) and
358
+ * {@link resolveServerUrls} (which parses `baseUrl` once and threads the result through).
359
+ */
360
+ function deriveAppOriginFromUrl(url: URL): string {
361
+ const stripped = strippedApiHostname(url.hostname);
362
+ if (stripped !== undefined) {
363
+ url.hostname = stripped;
364
+ }
365
+
366
+ return url.origin;
367
+ }
368
+
369
+ /**
370
+ * Whether stripping the `api.` label from `baseUrl` names a host the API itself is NOT served
371
+ * from — i.e. API and app are separated by HOST rather than by PORT.
372
+ *
373
+ * {@link LOCALHOST_URL_DEFAULTS} encode a port split: one host, API on `:3000`, app on `:3001`.
374
+ * `http://api.localhost:3000` and `https://api.localhost` are both that shape — the label
375
+ * strips to the bare `localhost` the API already answers on, so only the port tells app and API
376
+ * apart, and the flat `http://localhost:3001` default is the right answer.
377
+ *
378
+ * `https://api.crm.localhost` (as served by `lt dev up` behind Caddy) strips to the sibling host
379
+ * `crm.localhost`, which the API never answers on. There the derivation names the real app
380
+ * origin and must win over the flat default.
381
+ *
382
+ * Keyed on the stripped LABEL, never on the presence of a port: a host split stays a host split
383
+ * behind a non-default port (`https://api.crm.localhost:8443` → `https://crm.localhost:8443`),
384
+ * and a port split stays a port split on the default port (`https://api.localhost`).
385
+ *
386
+ * Takes an already-parsed `URL` (or `undefined` for an unparseable/absent `baseUrl`) so the
387
+ * caller parses `baseUrl` only once.
388
+ */
389
+ function separatesApiAndAppByHost(url: undefined | URL): boolean {
390
+ if (!url) {
391
+ return false;
392
+ }
393
+
394
+ const stripped = strippedApiHostname(url.hostname);
395
+ return stripped !== undefined && stripped !== 'localhost';
337
396
  }
338
397
 
339
398
  /**
@@ -341,6 +400,8 @@ function canStripApiLabel(hostname: string): boolean {
341
400
  * label from the hostname (e.g. `https://api.example.com` → `https://example.com`,
342
401
  * `https://api.dev.example.com` → `https://dev.example.com`).
343
402
  *
403
+ * The port is preserved (`https://api.example.com:8443` → `https://example.com:8443`).
404
+ *
344
405
  * Returns the origin unchanged when there is no strippable `api.` prefix, and returns the
345
406
  * input unchanged when it is not an http(s) URL — callers decide what to do with a value
346
407
  * they cannot normalize.
@@ -348,17 +409,8 @@ function canStripApiLabel(hostname: string): boolean {
348
409
  * @since 11.27.5
349
410
  */
350
411
  export function deriveAppUrlFromBaseUrl(baseUrl: string): string {
351
- const origin = toHttpOrigin(baseUrl);
352
- if (!origin) {
353
- return baseUrl;
354
- }
355
-
356
- const url = new URL(origin);
357
- if (canStripApiLabel(url.hostname)) {
358
- url.hostname = url.hostname.slice(API_HOST_LABEL.length);
359
- }
360
-
361
- return url.origin;
412
+ const url = toHttpUrl(baseUrl);
413
+ return url ? deriveAppOriginFromUrl(url) : baseUrl;
362
414
  }
363
415
 
364
416
  /**
@@ -378,15 +430,22 @@ export interface IResolvedServerUrls {
378
430
  * Resolves the effective app/API URLs for a server configuration.
379
431
  *
380
432
  * `baseUrl`: explicit → localhost default (local/ci/e2e only) → none.
381
- * `appUrl`: explicit → localhost default (local/ci/e2e with a localhost `baseUrl`) →
382
- * derived from `baseUrl` → none.
433
+ * `appUrl`: explicit → derived from a host-split localhost `baseUrl` (local/ci/e2e) →
434
+ * localhost default (local/ci/e2e with a localhost `baseUrl`) → derived from `baseUrl` → none.
435
+ *
436
+ * The localhost defaults assume a PORT split (API `:3000`, app `:3001`, one host). A localhost
437
+ * `baseUrl` whose `api.` label strips to a SIBLING host — `https://api.crm.localhost` from
438
+ * `lt dev up` — derives that host on whatever port it carries, because the flat
439
+ * `http://localhost:3001` default would name a host the app never serves from. See
440
+ * {@link separatesApiAndAppByHost}.
383
441
  *
384
442
  * `baseUrl` is returned verbatim (not origin-normalized) because BetterAuth passes it
385
443
  * straight through as its `baseURL`; normalization for origin matching is the caller's job.
386
444
  *
387
445
  * @param input.deriveAppUrl - Set to `false` to disable the `api.`-strip derivation. The
388
446
  * localhost defaults are unaffected — they are an explicit,
389
- * documented behavior of the `local`/`ci`/`e2e` environments.
447
+ * documented behavior of the `local`/`ci`/`e2e` environments, and
448
+ * a host-split localhost `baseUrl` falls back to them.
390
449
  *
391
450
  * @since 11.27.5
392
451
  */
@@ -409,13 +468,29 @@ export function resolveServerUrls(input: {
409
468
  return { appUrl: input.appUrl, appUrlSource: 'explicit', baseUrl, baseUrlSource };
410
469
  }
411
470
 
412
- // API on :3000 and app on :3001 — deriving from baseUrl would yield the API's own origin.
413
- if (usesLocalhostDefaults && isLocalhostUrl(baseUrl)) {
471
+ const mayDerive = input.deriveAppUrl !== false;
472
+
473
+ // Parse `baseUrl` once and reuse the result for both the host-split check and the derivation
474
+ // below (the localhost check keeps its own string parse — its loopback-IP matching differs
475
+ // from `toHttpUrl`'s http(s)-only guard). Gated on `mayDerive`: both consumers of
476
+ // `parsedBaseUrl` sit behind it (the host-split check via short-circuit, the derivation via its
477
+ // own guard), so with `deriveAppUrl: false` the parse is never needed and is skipped entirely.
478
+ const parsedBaseUrl = baseUrl && mayDerive ? toHttpUrl(baseUrl) : undefined;
479
+
480
+ // API on :3000 and app on :3001 — deriving from a port-split baseUrl would yield the API's
481
+ // own origin. A baseUrl that splits API and app by host instead (`https://api.crm.localhost`,
482
+ // from `lt dev up`) derives the real app origin, so prefer it over the flat default.
483
+ // `separatesApiAndAppByHost` is evaluated last, so every deployed (non-localhost) environment
484
+ // short-circuits before paying for the parse.
485
+ if (usesLocalhostDefaults && isLocalhostUrl(baseUrl) && !(mayDerive && separatesApiAndAppByHost(parsedBaseUrl))) {
414
486
  return { appUrl: LOCALHOST_URL_DEFAULTS.appUrl, appUrlSource: 'localhost-default', baseUrl, baseUrlSource };
415
487
  }
416
488
 
417
- if (baseUrl && input.deriveAppUrl !== false) {
418
- return { appUrl: deriveAppUrlFromBaseUrl(baseUrl), appUrlSource: 'derived', baseUrl, baseUrlSource };
489
+ if (baseUrl && mayDerive) {
490
+ // `parsedBaseUrl` is mutated by `deriveAppOriginFromUrl`; a non-http(s) `baseUrl` (parse
491
+ // failed) is returned verbatim, matching `deriveAppUrlFromBaseUrl`.
492
+ const appUrl = parsedBaseUrl ? deriveAppOriginFromUrl(parsedBaseUrl) : baseUrl;
493
+ return { appUrl, appUrlSource: 'derived', baseUrl, baseUrlSource };
419
494
  }
420
495
 
421
496
  return { appUrl: undefined, appUrlSource: 'none', baseUrl, baseUrlSource };
@@ -1,5 +1,4 @@
1
1
  import { FieldNode, GraphQLResolveInfo, SelectionNode } from 'graphql';
2
- import _ = require('lodash');
3
2
  import { Document, Model, PopulateOptions, Query, Types } from 'mongoose';
4
3
 
5
4
  import { ResolveSelector } from '../interfaces/resolve-selector.interface';
@@ -7,8 +6,19 @@ import { CoreModel } from '../models/core-model.model';
7
6
  import { FieldSelection } from '../types/field-selection.type';
8
7
  import { IdsType } from '../types/ids.type';
9
8
  import { StringOrObjectId } from '../types/string-or-object-id.type';
9
+ import { equalIds, getObjectIds, getStringIds } from './id.helper';
10
10
  import { removePropertiesDeep } from './input.helper';
11
11
 
12
+ /**
13
+ * The ID helpers moved to `./id.helper` and are re-exported here so every existing import path keeps
14
+ * working. They were extracted because `restricted.decorator` needs `equalIds` / `getIncludedIds`,
15
+ * and pulling them from HERE put that decorator on a runtime import cycle
16
+ * (restricted.decorator → db.helper → input.helper → restricted.decorator) that is one top-level
17
+ * line away from an SWC temporal-dead-zone crash. `id.helper` imports no framework code, so it can
18
+ * never be part of a cycle. Do NOT move them back.
19
+ */
20
+ export { equalIds, getIncludedIds, getObjectIds, getStringIds } from './id.helper';
21
+
12
22
  // =====================================================================================================================
13
23
  // Export functions
14
24
  // =====================================================================================================================
@@ -71,7 +81,9 @@ export function addIds(
71
81
  // Convert array
72
82
  if (['object', 'string'].includes(convert as string)) {
73
83
  for (let i = 0; i < result.length; i++) {
74
- result[i] = convert === 'string' ? getStringId(result[i]) : getObjectIds(result[i]);
84
+ // getStringIds() delegates to the (module-private) getStringId() for a non-array argument, so
85
+ // this is the same conversion the removed private helper performed — result[i] is a single ID.
86
+ result[i] = convert === 'string' ? getStringIds(result[i]) : getObjectIds(result[i]);
75
87
  }
76
88
  }
77
89
 
@@ -97,20 +109,6 @@ export function checkStringIds(ids: string | string[]): boolean {
97
109
  return false;
98
110
  }
99
111
 
100
- /**
101
- * Checks if all IDs are equal
102
- */
103
- export function equalIds(...ids: IdsType[]): boolean {
104
- if (!ids) {
105
- return false;
106
- }
107
- const compare = getStringIds(ids[0]);
108
- if (!compare) {
109
- return false;
110
- }
111
- return ids.every((id) => getStringIds(id) === compare);
112
- }
113
-
114
112
  /**
115
113
  * Get (and remove) elements with specific IDs from array
116
114
  */
@@ -160,48 +158,6 @@ export function getElementsViaIds<T = any>(
160
158
  // Return elements
161
159
  return elements;
162
160
  }
163
- /**
164
- * Get included ids
165
- * @param includes IdsType, which should be checked if it contains the ID
166
- * @param ids IdsType, which should be included
167
- * @param convert If set the result array will be converted to pure type String array or ObjectId array
168
- * @return IdsType with IDs which are included, undefined if includes or ids are missing or null if none is included
169
- */
170
- export function getIncludedIds(includes: IdsType, ids: IdsType, convert?: 'string'): string[];
171
- export function getIncludedIds(includes: IdsType, ids: IdsType, convert?: 'object'): Types.ObjectId[];
172
-
173
- export function getIncludedIds<T = IdsType>(
174
- includes: IdsType,
175
- ids: IdsType | T,
176
- convert?: 'object' | 'string',
177
- ): null | T[] | undefined {
178
- if (!includes || !ids) {
179
- return undefined;
180
- }
181
-
182
- if (!Array.isArray(includes)) {
183
- includes = [includes];
184
- }
185
-
186
- if (!Array.isArray(ids)) {
187
- ids = [ids];
188
- }
189
-
190
- let result = [];
191
- const includesStrings = getStringIds(includes);
192
- for (const id of ids) {
193
- if (includesStrings.includes(getStringIds(id))) {
194
- result.push(id);
195
- }
196
- }
197
-
198
- if (convert) {
199
- result = convert === 'string' ? getStringIds(result) : getObjectIds(result);
200
- }
201
-
202
- return result.length ? result : null;
203
- }
204
-
205
161
  /**
206
162
  * Get indexes of IDs in an array
207
163
  */
@@ -252,18 +208,6 @@ export function getNextFieldNodes(nodes: readonly SelectionNode[]): FieldNode[]
252
208
  return result;
253
209
  }
254
210
 
255
- /**
256
- * Convert string(s) to ObjectId(s)
257
- */
258
- export function getObjectIds(ids: any[]): Types.ObjectId[];
259
- export function getObjectIds(ids: any): Types.ObjectId;
260
- export function getObjectIds<T extends any | any[]>(ids: T): Types.ObjectId | Types.ObjectId[] {
261
- if (Array.isArray(ids)) {
262
- return ids.map((id) => new Types.ObjectId(getStringId(id)));
263
- }
264
- return new Types.ObjectId(getStringId(ids));
265
- }
266
-
267
211
  /**
268
212
  * Get populate options from GraphQL resolve info
269
213
  */
@@ -356,55 +300,6 @@ export function getPopulatOptionsFromSelections(selectionNodes: readonly Selecti
356
300
  return populateOptions;
357
301
  }
358
302
 
359
- /**
360
- * Get IDs from string of ObjectId array in a flat string array
361
- */
362
- export function getStringIds(elements: any[], options?: { deep?: boolean; unique?: boolean }): string[];
363
-
364
- export function getStringIds(elements: any, options?: { deep?: boolean; unique?: boolean }): string;
365
-
366
- export function getStringIds<T extends any | any[]>(
367
- elements: T,
368
- options?: { deep?: boolean; unique?: boolean },
369
- ): string | string[] {
370
- // Process options
371
- const { deep, unique } = {
372
- deep: false,
373
- unique: false,
374
- ...options,
375
- };
376
-
377
- // Check elements
378
- if (!elements) {
379
- return elements as any;
380
- }
381
-
382
- // Init ids
383
- let ids = [];
384
-
385
- // Process non array
386
- if (!Array.isArray(elements)) {
387
- return getStringId(elements);
388
- }
389
-
390
- // Process array
391
- for (const element of elements) {
392
- if (Array.isArray(element)) {
393
- if (deep) {
394
- ids = ids.concat(getStringIds(element, { deep }));
395
- }
396
- } else {
397
- const id = getStringId(element);
398
- if (id) {
399
- ids.push(id);
400
- }
401
- }
402
- }
403
-
404
- // Return (unique) ID array
405
- return unique ? _.uniq(ids) : ids;
406
- }
407
-
408
303
  /**
409
304
  * Convert all ObjectIds to strings
410
305
  */
@@ -664,45 +559,3 @@ export async function setPopulates<T = Document | Query<any, any>>(
664
559
  // =====================================================================================================================
665
560
  // Not exported helper functions
666
561
  // =====================================================================================================================
667
- /**
668
- * Get ID of element as string
669
- */
670
- function getStringId(element: any): string {
671
- // Check element
672
- if (!element) {
673
- return element;
674
- }
675
-
676
- // Buffer handling
677
- if (element instanceof Buffer) {
678
- return element.toString();
679
- }
680
-
681
- // String handling
682
- if (typeof element === 'string') {
683
- return element;
684
- }
685
-
686
- // Object handling
687
- if (typeof element === 'object') {
688
- if (element instanceof Types.ObjectId) {
689
- return element.toHexString();
690
- }
691
-
692
- if (element.id) {
693
- if (element.id instanceof Buffer && element.toHexString) {
694
- return element.toHexString();
695
- }
696
- return getStringId(element.id);
697
- } else if (element._id) {
698
- return getStringId(element._id);
699
- }
700
- }
701
-
702
- // Other types
703
- if (typeof element.toString === 'function') {
704
- return element.toString();
705
- }
706
-
707
- return undefined;
708
- }