intlayer-editor 9.5.12 → 9.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/bin/intlayer-editor.mjs +26 -6
  2. package/client/dist/assets/CodeBlockShiki-B-OcGOY1.js +2 -0
  3. package/client/dist/assets/MarkDownRender-BzY_8i9Z.js +1 -0
  4. package/client/dist/assets/MarkDownRender-CIkPpFUW.js +25 -0
  5. package/client/dist/assets/MarkdownEditor-BpbsoP4E.js +161 -0
  6. package/client/dist/assets/{astro-JHDYjn39.js → astro-BZ97kZFP.js} +1 -1
  7. package/client/dist/assets/codeTransformer-aP9a_qk2.js +9 -0
  8. package/client/dist/assets/compat-r4Owm_RW.js +1 -0
  9. package/client/dist/assets/css-BGIxQoYs.js +1 -0
  10. package/client/dist/assets/css-DUjzNfgv.js +1 -0
  11. package/client/dist/assets/dist-DRjLHkdX.js +11 -0
  12. package/client/dist/assets/extractUrlFromSitemap-Chj1JVpo.js +1 -0
  13. package/client/dist/assets/{html-B2cKP6UN.js → html-DSw1lh5G.js} +1 -1
  14. package/client/dist/assets/html-WQKKblVZ.js +1 -0
  15. package/client/dist/assets/index-C5SpC4ap.css +2 -0
  16. package/client/dist/assets/index-CmQV7Z_A.js +105 -0
  17. package/client/dist/assets/shikiBundle-BIG1bVgc.js +12 -0
  18. package/client/dist/assets/{svelte-tEuLYt3E.js → svelte-CdeT-Ich.js} +1 -1
  19. package/client/dist/assets/{vue-Bg0byEaI.js → vue-BqdMUtqy.js} +1 -1
  20. package/client/dist/index.html +3 -6
  21. package/package.json +28 -28
  22. package/server/dist/controllers/auth.controller.cjs +80 -0
  23. package/server/dist/controllers/auth.controller.cjs.map +1 -0
  24. package/server/dist/controllers/auth.controller.d.ts +36 -0
  25. package/server/dist/controllers/auth.controller.mjs +76 -0
  26. package/server/dist/controllers/auth.controller.mjs.map +1 -0
  27. package/server/dist/controllers/configuration.controller.cjs +5 -1
  28. package/server/dist/controllers/configuration.controller.cjs.map +1 -1
  29. package/server/dist/controllers/configuration.controller.mjs +5 -1
  30. package/server/dist/controllers/configuration.controller.mjs.map +1 -1
  31. package/server/dist/controllers/dictionary.controller.cjs +2 -1
  32. package/server/dist/controllers/dictionary.controller.cjs.map +1 -1
  33. package/server/dist/controllers/dictionary.controller.mjs +2 -1
  34. package/server/dist/controllers/dictionary.controller.mjs.map +1 -1
  35. package/server/dist/index.cjs +41 -13
  36. package/server/dist/index.cjs.map +1 -1
  37. package/server/dist/index.mjs +45 -17
  38. package/server/dist/index.mjs.map +1 -1
  39. package/server/dist/routes/auth.routes.cjs +35 -0
  40. package/server/dist/routes/auth.routes.cjs.map +1 -0
  41. package/server/dist/routes/auth.routes.d.ts +22 -0
  42. package/server/dist/routes/auth.routes.mjs +33 -0
  43. package/server/dist/routes/auth.routes.mjs.map +1 -0
  44. package/server/dist/services/editorAuth.service.cjs +143 -0
  45. package/server/dist/services/editorAuth.service.cjs.map +1 -0
  46. package/server/dist/services/editorAuth.service.d.ts +43 -0
  47. package/server/dist/services/editorAuth.service.mjs +137 -0
  48. package/server/dist/services/editorAuth.service.mjs.map +1 -0
  49. package/server/dist/utils/checkPortAvailability.cjs +38 -12
  50. package/server/dist/utils/checkPortAvailability.cjs.map +1 -1
  51. package/server/dist/utils/checkPortAvailability.d.ts +14 -1
  52. package/server/dist/utils/checkPortAvailability.mjs +38 -13
  53. package/server/dist/utils/checkPortAvailability.mjs.map +1 -1
  54. package/client/dist/assets/CodeBlockShiki-df1uxmSd.js +0 -2
  55. package/client/dist/assets/MarkDownRender-DYcnaCQP.js +0 -1
  56. package/client/dist/assets/MarkDownRender-MV3lk0jo.js +0 -25
  57. package/client/dist/assets/MarkdownEditor-CWVj-dFt.js +0 -160
  58. package/client/dist/assets/codeTransformer-DVPQQcdP.js +0 -9
  59. package/client/dist/assets/css-CdtkT07m.js +0 -1
  60. package/client/dist/assets/css-LbU1hoFO.js +0 -1
  61. package/client/dist/assets/dist-BHukwvnD.js +0 -11
  62. package/client/dist/assets/dist-CcmWBeAo.js +0 -1
  63. package/client/dist/assets/dynamic-OZWF58s3.js +0 -41
  64. package/client/dist/assets/html-DYtkB_T1.js +0 -1
  65. package/client/dist/assets/index-D82ZIHVs.css +0 -2
  66. package/client/dist/assets/index-DPHRjaWO.js +0 -59
  67. package/client/dist/assets/react-Cvdyeg_0.js +0 -1
  68. package/client/dist/assets/rolldown-runtime-hePW80VL.js +0 -1
  69. package/client/dist/assets/shikiBundle-C3MOPHUk.js +0 -12
@@ -0,0 +1,143 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_runtime = require('../_virtual/_rolldown/runtime.cjs');
3
+ let _intlayer_config_colors = require("@intlayer/config/colors");
4
+ _intlayer_config_colors = require_runtime.__toESM(_intlayer_config_colors, 1);
5
+ let _intlayer_config_logger = require("@intlayer/config/logger");
6
+ let _intlayer_api = require("@intlayer/api");
7
+ let _intlayer_cli = require("@intlayer/cli");
8
+
9
+ //#region src/services/editorAuth.service.ts
10
+ /** Time after which an unanswered login is given up. */
11
+ const LOGIN_TIMEOUT_MS = 3e5;
12
+ /** Margin before expiry under which a cached access token is renewed. */
13
+ const TOKEN_RENEWAL_MARGIN_MS = 6e4;
14
+ /** Credentials picked during an editor-triggered login, kept for this run. */
15
+ let runtimeCredentials = null;
16
+ /** Access token exchanged from the access key, reused until it expires. */
17
+ let accessKeyAuthCache = null;
18
+ /** Identity bound to each CLI session token, resolved once per token. */
19
+ const sessionIdentityCache = /* @__PURE__ */ new Map();
20
+ /** Aborts the login flow waiting for the browser callback, if any. */
21
+ let pendingLoginController = null;
22
+ const isExpiringSoon = (expiresAt) => expiresAt !== null && new Date(expiresAt).getTime() - TOKEN_RENEWAL_MARGIN_MS <= Date.now();
23
+ const getCredentials = (configuration) => {
24
+ if (runtimeCredentials) return runtimeCredentials;
25
+ const { clientId, clientSecret } = configuration.editor;
26
+ return clientId && clientSecret ? {
27
+ clientId,
28
+ clientSecret
29
+ } : null;
30
+ };
31
+ /**
32
+ * Resolves the `intlayer login` session token, shared with the CLI through
33
+ * the temp directory. `null` when absent, expired, or rejected by the CMS.
34
+ */
35
+ const resolveSessionAuth = async (configuration) => {
36
+ const sessionData = await (0, _intlayer_cli.readCliSessionToken)(configuration);
37
+ if (!sessionData) return null;
38
+ if (!sessionIdentityCache.has(sessionData.token)) try {
39
+ const result = await (0, _intlayer_api.getIntlayerAPIProxy)(void 0, configuration, sessionData.token).oAuth.getCliSessionMe();
40
+ sessionIdentityCache.set(sessionData.token, {
41
+ user: result.data?.user ?? null,
42
+ organization: result.data?.organization ?? null,
43
+ project: result.data?.project ?? null
44
+ });
45
+ } catch {
46
+ return null;
47
+ }
48
+ return {
49
+ accessToken: sessionData.token,
50
+ expiresAt: sessionData.expiresAt,
51
+ authType: "session",
52
+ ...sessionIdentityCache.get(sessionData.token)
53
+ };
54
+ };
55
+ /**
56
+ * Exchanges the access key for an access token. The client secret stays on
57
+ * the server: only the resulting short-lived token reaches the browser.
58
+ */
59
+ const resolveAccessKeyAuth = async (configuration) => {
60
+ const credentials = getCredentials(configuration);
61
+ if (!credentials) return null;
62
+ if (accessKeyAuthCache?.clientId === credentials.clientId && !isExpiringSoon(accessKeyAuthCache.expiresAt)) {
63
+ const { clientId: _clientId, ...cachedAuth } = accessKeyAuthCache;
64
+ return cachedAuth;
65
+ }
66
+ try {
67
+ const token = (await (0, _intlayer_api.getOAuthAPI)(void 0, { editor: {
68
+ ...configuration.editor,
69
+ ...credentials
70
+ } }).getOAuth2AccessToken()).data;
71
+ if (!token?.accessToken) return null;
72
+ const accessKeyAuth = {
73
+ accessToken: token.accessToken,
74
+ expiresAt: token.accessTokenExpiresAt ? new Date(token.accessTokenExpiresAt).toISOString() : null,
75
+ authType: "accessKey",
76
+ user: token.user,
77
+ organization: token.organization,
78
+ project: token.project
79
+ };
80
+ accessKeyAuthCache = {
81
+ ...accessKeyAuth,
82
+ clientId: credentials.clientId
83
+ };
84
+ return accessKeyAuth;
85
+ } catch {
86
+ accessKeyAuthCache = null;
87
+ return null;
88
+ }
89
+ };
90
+ /**
91
+ * Resolves the CMS identity of the editor, in the same order as the CLI:
92
+ * `intlayer login` session first, then the access key.
93
+ */
94
+ const resolveEditorAuth = async (configuration) => await resolveSessionAuth(configuration) ?? await resolveAccessKeyAuth(configuration);
95
+ /**
96
+ * Signs the editor out: drops the `intlayer login` session (shared with the
97
+ * CLI), the pending login and the access key loaded during this run. An access
98
+ * key set in the configuration stays in use, as for the CLI.
99
+ */
100
+ const logoutEditor = async (configuration) => {
101
+ pendingLoginController?.abort();
102
+ runtimeCredentials = null;
103
+ accessKeyAuthCache = null;
104
+ sessionIdentityCache.clear();
105
+ await (0, _intlayer_cli.clearCliSessionToken)(configuration);
106
+ };
107
+ /** Whether an editor-triggered login is waiting for the browser callback. */
108
+ const isEditorLoginPending = () => pendingLoginController !== null;
109
+ /**
110
+ * Starts the `intlayer login` browser flow. A login still pending is dropped
111
+ * first, so a closed login tab can be reopened. Resolves once the CMS has
112
+ * called back (the session token being stored for both the editor and the
113
+ * CLI), or when the login is dropped or times out.
114
+ */
115
+ const startEditorLogin = async (configuration, configOptions) => {
116
+ pendingLoginController?.abort();
117
+ const appLogger = (0, _intlayer_config_logger.getAppLogger)(configuration);
118
+ const loginController = new AbortController();
119
+ const timeout = setTimeout(() => loginController.abort(), LOGIN_TIMEOUT_MS);
120
+ pendingLoginController = loginController;
121
+ await (0, _intlayer_cli.login)({
122
+ exitAfter: false,
123
+ configOptions,
124
+ signal: loginController.signal,
125
+ onCredentials: (credentials) => {
126
+ runtimeCredentials = credentials;
127
+ accessKeyAuthCache = null;
128
+ appLogger("Access key loaded for this editor session. Insert it in your .env file to keep it:");
129
+ appLogger(`${(0, _intlayer_config_logger.colorize)("INTLAYER_CLIENT_ID=", _intlayer_config_colors.GREY_LIGHT)}${(0, _intlayer_config_logger.colorize)(credentials.clientId, _intlayer_config_colors.BLUE)}`);
130
+ appLogger(`${(0, _intlayer_config_logger.colorize)("INTLAYER_CLIENT_SECRET=", _intlayer_config_colors.GREY_LIGHT)}${(0, _intlayer_config_logger.colorize)(credentials.clientSecret, _intlayer_config_colors.BLUE)}`);
131
+ }
132
+ }).finally(() => {
133
+ clearTimeout(timeout);
134
+ if (pendingLoginController === loginController) pendingLoginController = null;
135
+ });
136
+ };
137
+
138
+ //#endregion
139
+ exports.isEditorLoginPending = isEditorLoginPending;
140
+ exports.logoutEditor = logoutEditor;
141
+ exports.resolveEditorAuth = resolveEditorAuth;
142
+ exports.startEditorLogin = startEditorLogin;
143
+ //# sourceMappingURL=editorAuth.service.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editorAuth.service.cjs","names":["readCliSessionToken","getIntlayerAPIProxy","getOAuthAPI","clearCliSessionToken","getAppLogger","login","colorize","ANSIColors"],"sources":["../../src/services/editorAuth.service.ts"],"sourcesContent":["import { getIntlayerAPIProxy, getOAuthAPI } from '@intlayer/api';\nimport type { OrganizationAPI } from '@intlayer/backend-contract/organization';\nimport type { ProjectAPI } from '@intlayer/backend-contract/project';\nimport type { UserAPI } from '@intlayer/backend-contract/user';\nimport {\n clearCliSessionToken,\n login,\n readCliSessionToken,\n} from '@intlayer/cli';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport type { GetConfigurationOptions } from '@intlayer/config/node';\nimport type { IntlayerConfig } from '@intlayer/types/config';\n\n/** Access-key credentials used for the client-credentials exchange. */\ntype EditorCredentials = {\n clientId: string;\n clientSecret: string;\n};\n\n/**\n * Identity the editor client uses to call the CMS, sent as a bearer token.\n *\n * - `session`: the user's short-lived `intlayer login` session token\n * - `accessKey`: an access token exchanged from the project access key\n */\nexport type EditorAuth = {\n accessToken: string;\n /** ISO date, `null` when the token never expires. */\n expiresAt: string | null;\n authType: 'session' | 'accessKey';\n user?: UserAPI | null;\n organization?: OrganizationAPI | null;\n project?: ProjectAPI | null;\n};\n\n/** Time after which an unanswered login is given up. */\nconst LOGIN_TIMEOUT_MS = 5 * 60 * 1000;\n\n/** Margin before expiry under which a cached access token is renewed. */\nconst TOKEN_RENEWAL_MARGIN_MS = 60 * 1000;\n\n/** Credentials picked during an editor-triggered login, kept for this run. */\nlet runtimeCredentials: EditorCredentials | null = null;\n\n/** Access token exchanged from the access key, reused until it expires. */\nlet accessKeyAuthCache: (EditorAuth & { clientId: string }) | null = null;\n\n/** Identity bound to a CLI session token. */\ntype SessionIdentity = Pick<EditorAuth, 'user' | 'organization' | 'project'>;\n\n/** Identity bound to each CLI session token, resolved once per token. */\nconst sessionIdentityCache = new Map<string, SessionIdentity>();\n\n/** Aborts the login flow waiting for the browser callback, if any. */\nlet pendingLoginController: AbortController | null = null;\n\nconst isExpiringSoon = (expiresAt: string | null): boolean =>\n expiresAt !== null &&\n new Date(expiresAt).getTime() - TOKEN_RENEWAL_MARGIN_MS <= Date.now();\n\nconst getCredentials = (\n configuration: IntlayerConfig\n): EditorCredentials | null => {\n if (runtimeCredentials) return runtimeCredentials;\n\n const { clientId, clientSecret } = configuration.editor;\n\n return clientId && clientSecret ? { clientId, clientSecret } : null;\n};\n\n/**\n * Resolves the `intlayer login` session token, shared with the CLI through\n * the temp directory. `null` when absent, expired, or rejected by the CMS.\n */\nconst resolveSessionAuth = async (\n configuration: IntlayerConfig\n): Promise<EditorAuth | null> => {\n const sessionData = await readCliSessionToken(configuration);\n\n if (!sessionData) return null;\n\n if (!sessionIdentityCache.has(sessionData.token)) {\n try {\n const result = await getIntlayerAPIProxy(\n undefined,\n configuration,\n sessionData.token\n ).oAuth.getCliSessionMe();\n\n sessionIdentityCache.set(sessionData.token, {\n user: result.data?.user ?? null,\n organization: result.data?.organization ?? null,\n project: result.data?.project ?? null,\n });\n } catch {\n // Revoked or unreachable: fall back to the access key, if any\n return null;\n }\n }\n\n return {\n accessToken: sessionData.token,\n expiresAt: sessionData.expiresAt,\n authType: 'session',\n ...sessionIdentityCache.get(sessionData.token),\n };\n};\n\n/**\n * Exchanges the access key for an access token. The client secret stays on\n * the server: only the resulting short-lived token reaches the browser.\n */\nconst resolveAccessKeyAuth = async (\n configuration: IntlayerConfig\n): Promise<EditorAuth | null> => {\n const credentials = getCredentials(configuration);\n\n if (!credentials) return null;\n\n if (\n accessKeyAuthCache?.clientId === credentials.clientId &&\n !isExpiringSoon(accessKeyAuthCache.expiresAt)\n ) {\n const { clientId: _clientId, ...cachedAuth } = accessKeyAuthCache;\n return cachedAuth;\n }\n\n try {\n const result = await getOAuthAPI(undefined, {\n editor: { ...configuration.editor, ...credentials },\n }).getOAuth2AccessToken();\n\n const token = result.data;\n\n if (!token?.accessToken) return null;\n\n const accessKeyAuth: EditorAuth = {\n accessToken: token.accessToken,\n expiresAt: token.accessTokenExpiresAt\n ? new Date(token.accessTokenExpiresAt).toISOString()\n : null,\n authType: 'accessKey',\n user: token.user,\n organization: token.organization,\n project: token.project,\n };\n\n accessKeyAuthCache = { ...accessKeyAuth, clientId: credentials.clientId };\n\n return accessKeyAuth;\n } catch {\n accessKeyAuthCache = null;\n return null;\n }\n};\n\n/**\n * Resolves the CMS identity of the editor, in the same order as the CLI:\n * `intlayer login` session first, then the access key.\n */\nexport const resolveEditorAuth = async (\n configuration: IntlayerConfig\n): Promise<EditorAuth | null> =>\n (await resolveSessionAuth(configuration)) ??\n (await resolveAccessKeyAuth(configuration));\n\n/**\n * Signs the editor out: drops the `intlayer login` session (shared with the\n * CLI), the pending login and the access key loaded during this run. An access\n * key set in the configuration stays in use, as for the CLI.\n */\nexport const logoutEditor = async (\n configuration: IntlayerConfig\n): Promise<void> => {\n pendingLoginController?.abort();\n runtimeCredentials = null;\n accessKeyAuthCache = null;\n sessionIdentityCache.clear();\n\n await clearCliSessionToken(configuration);\n};\n\n/** Whether an editor-triggered login is waiting for the browser callback. */\nexport const isEditorLoginPending = (): boolean =>\n pendingLoginController !== null;\n\n/**\n * Starts the `intlayer login` browser flow. A login still pending is dropped\n * first, so a closed login tab can be reopened. Resolves once the CMS has\n * called back (the session token being stored for both the editor and the\n * CLI), or when the login is dropped or times out.\n */\nexport const startEditorLogin = async (\n configuration: IntlayerConfig,\n configOptions?: GetConfigurationOptions\n): Promise<void> => {\n pendingLoginController?.abort();\n\n const appLogger = getAppLogger(configuration);\n const loginController = new AbortController();\n const timeout = setTimeout(() => loginController.abort(), LOGIN_TIMEOUT_MS);\n\n pendingLoginController = loginController;\n\n await login({\n exitAfter: false,\n configOptions,\n signal: loginController.signal,\n onCredentials: (credentials) => {\n runtimeCredentials = credentials;\n accessKeyAuthCache = null;\n\n appLogger(\n 'Access key loaded for this editor session. Insert it in your .env file to keep it:'\n );\n appLogger(\n `${colorize('INTLAYER_CLIENT_ID=', ANSIColors.GREY_LIGHT)}${colorize(credentials.clientId, ANSIColors.BLUE)}`\n );\n appLogger(\n `${colorize('INTLAYER_CLIENT_SECRET=', ANSIColors.GREY_LIGHT)}${colorize(credentials.clientSecret, ANSIColors.BLUE)}`\n );\n },\n }).finally(() => {\n clearTimeout(timeout);\n\n if (pendingLoginController === loginController) {\n pendingLoginController = null;\n }\n });\n};\n"],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import { OrganizationAPI } from "@intlayer/backend-contract/organization";
2
+ import { ProjectAPI } from "@intlayer/backend-contract/project";
3
+ import { UserAPI } from "@intlayer/backend-contract/user";
4
+ import { GetConfigurationOptions } from "@intlayer/config/node";
5
+ import { IntlayerConfig } from "@intlayer/types/config";
6
+ //#region src/services/editorAuth.service.d.ts
7
+ /**
8
+ * Identity the editor client uses to call the CMS, sent as a bearer token.
9
+ *
10
+ * - `session`: the user's short-lived `intlayer login` session token
11
+ * - `accessKey`: an access token exchanged from the project access key
12
+ */
13
+ export type EditorAuth = {
14
+ accessToken: string;
15
+ /** ISO date, `null` when the token never expires. */
16
+ expiresAt: string | null;
17
+ authType: 'session' | 'accessKey';
18
+ user?: UserAPI | null;
19
+ organization?: OrganizationAPI | null;
20
+ project?: ProjectAPI | null;
21
+ };
22
+ /**
23
+ * Resolves the CMS identity of the editor, in the same order as the CLI:
24
+ * `intlayer login` session first, then the access key.
25
+ */
26
+ export declare const resolveEditorAuth: (configuration: IntlayerConfig) => Promise<EditorAuth | null>;
27
+ /**
28
+ * Signs the editor out: drops the `intlayer login` session (shared with the
29
+ * CLI), the pending login and the access key loaded during this run. An access
30
+ * key set in the configuration stays in use, as for the CLI.
31
+ */
32
+ export declare const logoutEditor: (configuration: IntlayerConfig) => Promise<void>;
33
+ /** Whether an editor-triggered login is waiting for the browser callback. */
34
+ export declare const isEditorLoginPending: () => boolean;
35
+ /**
36
+ * Starts the `intlayer login` browser flow. A login still pending is dropped
37
+ * first, so a closed login tab can be reopened. Resolves once the CMS has
38
+ * called back (the session token being stored for both the editor and the
39
+ * CLI), or when the login is dropped or times out.
40
+ */
41
+ export declare const startEditorLogin: (configuration: IntlayerConfig, configOptions?: GetConfigurationOptions) => Promise<void>;
42
+ //#endregion
43
+ //# sourceMappingURL=editorAuth.service.d.ts.map
@@ -0,0 +1,137 @@
1
+ import * as ANSIColors from "@intlayer/config/colors";
2
+ import { colorize, getAppLogger } from "@intlayer/config/logger";
3
+ import { getIntlayerAPIProxy, getOAuthAPI } from "@intlayer/api";
4
+ import { clearCliSessionToken, login, readCliSessionToken } from "@intlayer/cli";
5
+
6
+ //#region src/services/editorAuth.service.ts
7
+ /** Time after which an unanswered login is given up. */
8
+ const LOGIN_TIMEOUT_MS = 3e5;
9
+ /** Margin before expiry under which a cached access token is renewed. */
10
+ const TOKEN_RENEWAL_MARGIN_MS = 6e4;
11
+ /** Credentials picked during an editor-triggered login, kept for this run. */
12
+ let runtimeCredentials = null;
13
+ /** Access token exchanged from the access key, reused until it expires. */
14
+ let accessKeyAuthCache = null;
15
+ /** Identity bound to each CLI session token, resolved once per token. */
16
+ const sessionIdentityCache = /* @__PURE__ */ new Map();
17
+ /** Aborts the login flow waiting for the browser callback, if any. */
18
+ let pendingLoginController = null;
19
+ const isExpiringSoon = (expiresAt) => expiresAt !== null && new Date(expiresAt).getTime() - TOKEN_RENEWAL_MARGIN_MS <= Date.now();
20
+ const getCredentials = (configuration) => {
21
+ if (runtimeCredentials) return runtimeCredentials;
22
+ const { clientId, clientSecret } = configuration.editor;
23
+ return clientId && clientSecret ? {
24
+ clientId,
25
+ clientSecret
26
+ } : null;
27
+ };
28
+ /**
29
+ * Resolves the `intlayer login` session token, shared with the CLI through
30
+ * the temp directory. `null` when absent, expired, or rejected by the CMS.
31
+ */
32
+ const resolveSessionAuth = async (configuration) => {
33
+ const sessionData = await readCliSessionToken(configuration);
34
+ if (!sessionData) return null;
35
+ if (!sessionIdentityCache.has(sessionData.token)) try {
36
+ const result = await getIntlayerAPIProxy(void 0, configuration, sessionData.token).oAuth.getCliSessionMe();
37
+ sessionIdentityCache.set(sessionData.token, {
38
+ user: result.data?.user ?? null,
39
+ organization: result.data?.organization ?? null,
40
+ project: result.data?.project ?? null
41
+ });
42
+ } catch {
43
+ return null;
44
+ }
45
+ return {
46
+ accessToken: sessionData.token,
47
+ expiresAt: sessionData.expiresAt,
48
+ authType: "session",
49
+ ...sessionIdentityCache.get(sessionData.token)
50
+ };
51
+ };
52
+ /**
53
+ * Exchanges the access key for an access token. The client secret stays on
54
+ * the server: only the resulting short-lived token reaches the browser.
55
+ */
56
+ const resolveAccessKeyAuth = async (configuration) => {
57
+ const credentials = getCredentials(configuration);
58
+ if (!credentials) return null;
59
+ if (accessKeyAuthCache?.clientId === credentials.clientId && !isExpiringSoon(accessKeyAuthCache.expiresAt)) {
60
+ const { clientId: _clientId, ...cachedAuth } = accessKeyAuthCache;
61
+ return cachedAuth;
62
+ }
63
+ try {
64
+ const token = (await getOAuthAPI(void 0, { editor: {
65
+ ...configuration.editor,
66
+ ...credentials
67
+ } }).getOAuth2AccessToken()).data;
68
+ if (!token?.accessToken) return null;
69
+ const accessKeyAuth = {
70
+ accessToken: token.accessToken,
71
+ expiresAt: token.accessTokenExpiresAt ? new Date(token.accessTokenExpiresAt).toISOString() : null,
72
+ authType: "accessKey",
73
+ user: token.user,
74
+ organization: token.organization,
75
+ project: token.project
76
+ };
77
+ accessKeyAuthCache = {
78
+ ...accessKeyAuth,
79
+ clientId: credentials.clientId
80
+ };
81
+ return accessKeyAuth;
82
+ } catch {
83
+ accessKeyAuthCache = null;
84
+ return null;
85
+ }
86
+ };
87
+ /**
88
+ * Resolves the CMS identity of the editor, in the same order as the CLI:
89
+ * `intlayer login` session first, then the access key.
90
+ */
91
+ const resolveEditorAuth = async (configuration) => await resolveSessionAuth(configuration) ?? await resolveAccessKeyAuth(configuration);
92
+ /**
93
+ * Signs the editor out: drops the `intlayer login` session (shared with the
94
+ * CLI), the pending login and the access key loaded during this run. An access
95
+ * key set in the configuration stays in use, as for the CLI.
96
+ */
97
+ const logoutEditor = async (configuration) => {
98
+ pendingLoginController?.abort();
99
+ runtimeCredentials = null;
100
+ accessKeyAuthCache = null;
101
+ sessionIdentityCache.clear();
102
+ await clearCliSessionToken(configuration);
103
+ };
104
+ /** Whether an editor-triggered login is waiting for the browser callback. */
105
+ const isEditorLoginPending = () => pendingLoginController !== null;
106
+ /**
107
+ * Starts the `intlayer login` browser flow. A login still pending is dropped
108
+ * first, so a closed login tab can be reopened. Resolves once the CMS has
109
+ * called back (the session token being stored for both the editor and the
110
+ * CLI), or when the login is dropped or times out.
111
+ */
112
+ const startEditorLogin = async (configuration, configOptions) => {
113
+ pendingLoginController?.abort();
114
+ const appLogger = getAppLogger(configuration);
115
+ const loginController = new AbortController();
116
+ const timeout = setTimeout(() => loginController.abort(), LOGIN_TIMEOUT_MS);
117
+ pendingLoginController = loginController;
118
+ await login({
119
+ exitAfter: false,
120
+ configOptions,
121
+ signal: loginController.signal,
122
+ onCredentials: (credentials) => {
123
+ runtimeCredentials = credentials;
124
+ accessKeyAuthCache = null;
125
+ appLogger("Access key loaded for this editor session. Insert it in your .env file to keep it:");
126
+ appLogger(`${colorize("INTLAYER_CLIENT_ID=", ANSIColors.GREY_LIGHT)}${colorize(credentials.clientId, ANSIColors.BLUE)}`);
127
+ appLogger(`${colorize("INTLAYER_CLIENT_SECRET=", ANSIColors.GREY_LIGHT)}${colorize(credentials.clientSecret, ANSIColors.BLUE)}`);
128
+ }
129
+ }).finally(() => {
130
+ clearTimeout(timeout);
131
+ if (pendingLoginController === loginController) pendingLoginController = null;
132
+ });
133
+ };
134
+
135
+ //#endregion
136
+ export { isEditorLoginPending, logoutEditor, resolveEditorAuth, startEditorLogin };
137
+ //# sourceMappingURL=editorAuth.service.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editorAuth.service.mjs","names":[],"sources":["../../src/services/editorAuth.service.ts"],"sourcesContent":["import { getIntlayerAPIProxy, getOAuthAPI } from '@intlayer/api';\nimport type { OrganizationAPI } from '@intlayer/backend-contract/organization';\nimport type { ProjectAPI } from '@intlayer/backend-contract/project';\nimport type { UserAPI } from '@intlayer/backend-contract/user';\nimport {\n clearCliSessionToken,\n login,\n readCliSessionToken,\n} from '@intlayer/cli';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport type { GetConfigurationOptions } from '@intlayer/config/node';\nimport type { IntlayerConfig } from '@intlayer/types/config';\n\n/** Access-key credentials used for the client-credentials exchange. */\ntype EditorCredentials = {\n clientId: string;\n clientSecret: string;\n};\n\n/**\n * Identity the editor client uses to call the CMS, sent as a bearer token.\n *\n * - `session`: the user's short-lived `intlayer login` session token\n * - `accessKey`: an access token exchanged from the project access key\n */\nexport type EditorAuth = {\n accessToken: string;\n /** ISO date, `null` when the token never expires. */\n expiresAt: string | null;\n authType: 'session' | 'accessKey';\n user?: UserAPI | null;\n organization?: OrganizationAPI | null;\n project?: ProjectAPI | null;\n};\n\n/** Time after which an unanswered login is given up. */\nconst LOGIN_TIMEOUT_MS = 5 * 60 * 1000;\n\n/** Margin before expiry under which a cached access token is renewed. */\nconst TOKEN_RENEWAL_MARGIN_MS = 60 * 1000;\n\n/** Credentials picked during an editor-triggered login, kept for this run. */\nlet runtimeCredentials: EditorCredentials | null = null;\n\n/** Access token exchanged from the access key, reused until it expires. */\nlet accessKeyAuthCache: (EditorAuth & { clientId: string }) | null = null;\n\n/** Identity bound to a CLI session token. */\ntype SessionIdentity = Pick<EditorAuth, 'user' | 'organization' | 'project'>;\n\n/** Identity bound to each CLI session token, resolved once per token. */\nconst sessionIdentityCache = new Map<string, SessionIdentity>();\n\n/** Aborts the login flow waiting for the browser callback, if any. */\nlet pendingLoginController: AbortController | null = null;\n\nconst isExpiringSoon = (expiresAt: string | null): boolean =>\n expiresAt !== null &&\n new Date(expiresAt).getTime() - TOKEN_RENEWAL_MARGIN_MS <= Date.now();\n\nconst getCredentials = (\n configuration: IntlayerConfig\n): EditorCredentials | null => {\n if (runtimeCredentials) return runtimeCredentials;\n\n const { clientId, clientSecret } = configuration.editor;\n\n return clientId && clientSecret ? { clientId, clientSecret } : null;\n};\n\n/**\n * Resolves the `intlayer login` session token, shared with the CLI through\n * the temp directory. `null` when absent, expired, or rejected by the CMS.\n */\nconst resolveSessionAuth = async (\n configuration: IntlayerConfig\n): Promise<EditorAuth | null> => {\n const sessionData = await readCliSessionToken(configuration);\n\n if (!sessionData) return null;\n\n if (!sessionIdentityCache.has(sessionData.token)) {\n try {\n const result = await getIntlayerAPIProxy(\n undefined,\n configuration,\n sessionData.token\n ).oAuth.getCliSessionMe();\n\n sessionIdentityCache.set(sessionData.token, {\n user: result.data?.user ?? null,\n organization: result.data?.organization ?? null,\n project: result.data?.project ?? null,\n });\n } catch {\n // Revoked or unreachable: fall back to the access key, if any\n return null;\n }\n }\n\n return {\n accessToken: sessionData.token,\n expiresAt: sessionData.expiresAt,\n authType: 'session',\n ...sessionIdentityCache.get(sessionData.token),\n };\n};\n\n/**\n * Exchanges the access key for an access token. The client secret stays on\n * the server: only the resulting short-lived token reaches the browser.\n */\nconst resolveAccessKeyAuth = async (\n configuration: IntlayerConfig\n): Promise<EditorAuth | null> => {\n const credentials = getCredentials(configuration);\n\n if (!credentials) return null;\n\n if (\n accessKeyAuthCache?.clientId === credentials.clientId &&\n !isExpiringSoon(accessKeyAuthCache.expiresAt)\n ) {\n const { clientId: _clientId, ...cachedAuth } = accessKeyAuthCache;\n return cachedAuth;\n }\n\n try {\n const result = await getOAuthAPI(undefined, {\n editor: { ...configuration.editor, ...credentials },\n }).getOAuth2AccessToken();\n\n const token = result.data;\n\n if (!token?.accessToken) return null;\n\n const accessKeyAuth: EditorAuth = {\n accessToken: token.accessToken,\n expiresAt: token.accessTokenExpiresAt\n ? new Date(token.accessTokenExpiresAt).toISOString()\n : null,\n authType: 'accessKey',\n user: token.user,\n organization: token.organization,\n project: token.project,\n };\n\n accessKeyAuthCache = { ...accessKeyAuth, clientId: credentials.clientId };\n\n return accessKeyAuth;\n } catch {\n accessKeyAuthCache = null;\n return null;\n }\n};\n\n/**\n * Resolves the CMS identity of the editor, in the same order as the CLI:\n * `intlayer login` session first, then the access key.\n */\nexport const resolveEditorAuth = async (\n configuration: IntlayerConfig\n): Promise<EditorAuth | null> =>\n (await resolveSessionAuth(configuration)) ??\n (await resolveAccessKeyAuth(configuration));\n\n/**\n * Signs the editor out: drops the `intlayer login` session (shared with the\n * CLI), the pending login and the access key loaded during this run. An access\n * key set in the configuration stays in use, as for the CLI.\n */\nexport const logoutEditor = async (\n configuration: IntlayerConfig\n): Promise<void> => {\n pendingLoginController?.abort();\n runtimeCredentials = null;\n accessKeyAuthCache = null;\n sessionIdentityCache.clear();\n\n await clearCliSessionToken(configuration);\n};\n\n/** Whether an editor-triggered login is waiting for the browser callback. */\nexport const isEditorLoginPending = (): boolean =>\n pendingLoginController !== null;\n\n/**\n * Starts the `intlayer login` browser flow. A login still pending is dropped\n * first, so a closed login tab can be reopened. Resolves once the CMS has\n * called back (the session token being stored for both the editor and the\n * CLI), or when the login is dropped or times out.\n */\nexport const startEditorLogin = async (\n configuration: IntlayerConfig,\n configOptions?: GetConfigurationOptions\n): Promise<void> => {\n pendingLoginController?.abort();\n\n const appLogger = getAppLogger(configuration);\n const loginController = new AbortController();\n const timeout = setTimeout(() => loginController.abort(), LOGIN_TIMEOUT_MS);\n\n pendingLoginController = loginController;\n\n await login({\n exitAfter: false,\n configOptions,\n signal: loginController.signal,\n onCredentials: (credentials) => {\n runtimeCredentials = credentials;\n accessKeyAuthCache = null;\n\n appLogger(\n 'Access key loaded for this editor session. Insert it in your .env file to keep it:'\n );\n appLogger(\n `${colorize('INTLAYER_CLIENT_ID=', ANSIColors.GREY_LIGHT)}${colorize(credentials.clientId, ANSIColors.BLUE)}`\n );\n appLogger(\n `${colorize('INTLAYER_CLIENT_SECRET=', ANSIColors.GREY_LIGHT)}${colorize(credentials.clientSecret, ANSIColors.BLUE)}`\n );\n },\n }).finally(() => {\n clearTimeout(timeout);\n\n if (pendingLoginController === loginController) {\n pendingLoginController = null;\n }\n });\n};\n"],"mappings":"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"}
@@ -4,21 +4,47 @@ let node_net = require("node:net");
4
4
  node_net = require_runtime.__toESM(node_net, 1);
5
5
 
6
6
  //#region src/utils/checkPortAvailability.ts
7
- const checkPortAvailability = (port) => {
8
- return new Promise((resolve) => {
9
- const server = node_net.default.createServer();
10
- server.once("error", (err) => {
11
- if (err.code === "EADDRINUSE") resolve(false);
12
- else resolve(true);
13
- });
14
- server.once("listening", () => {
15
- server.close();
16
- resolve(true);
17
- });
18
- server.listen(port);
7
+ /**
8
+ * Resolves `false` when `port` is already bound on `host`.
9
+ *
10
+ * Probe the same host the server listens on: on macOS an IPv6 `::` probe
11
+ * succeeds even when `0.0.0.0:port` is taken.
12
+ */
13
+ const checkPortAvailability = (port, host = "0.0.0.0") => new Promise((resolve) => {
14
+ const server = node_net.default.createServer();
15
+ server.once("error", (error) => {
16
+ resolve(error.code !== "EADDRINUSE");
19
17
  });
18
+ server.once("listening", () => {
19
+ server.close();
20
+ resolve(true);
21
+ });
22
+ server.listen(port, host);
23
+ });
24
+ /**
25
+ * Loopback hosts probed besides the listening one: a server bound to a single
26
+ * loopback address (e.g. a dev server on `::1`) would shadow the editor for
27
+ * `localhost` while leaving `0.0.0.0` free.
28
+ */
29
+ const LOOPBACK_HOSTS = ["127.0.0.1", "::1"];
30
+ /**
31
+ * First port from `startPort` free on `host` and on the loopback addresses,
32
+ * trying the next ones in order.
33
+ *
34
+ * @returns The port, or `undefined` when the `attempts` ports are all taken.
35
+ */
36
+ const findAvailablePort = async (startPort, host = "0.0.0.0", attempts = 20) => {
37
+ for (let port = startPort; port < startPort + attempts; port++) {
38
+ let isAvailable = true;
39
+ for (const probedHost of [host, ...LOOPBACK_HOSTS]) {
40
+ isAvailable = await checkPortAvailability(port, probedHost);
41
+ if (!isAvailable) break;
42
+ }
43
+ if (isAvailable) return port;
44
+ }
20
45
  };
21
46
 
22
47
  //#endregion
23
48
  exports.checkPortAvailability = checkPortAvailability;
49
+ exports.findAvailablePort = findAvailablePort;
24
50
  //# sourceMappingURL=checkPortAvailability.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"checkPortAvailability.cjs","names":["net"],"sources":["../../src/utils/checkPortAvailability.ts"],"sourcesContent":["import net from 'node:net';\n\nexport const checkPortAvailability = (port: number): Promise<boolean> => {\n return new Promise((resolve) => {\n const server = net.createServer();\n\n server.once('error', (err: any) => {\n if (err.code === 'EADDRINUSE') {\n resolve(false); // Port is in use\n } else {\n resolve(true);\n }\n });\n\n server.once('listening', () => {\n server.close();\n resolve(true); // Port is available\n });\n\n server.listen(port);\n });\n};\n"],"mappings":";;;;;;AAEA,MAAa,yBAAyB,SAAmC;CACvE,OAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,SAASA,iBAAI,aAAa;EAEhC,OAAO,KAAK,UAAU,QAAa;GACjC,IAAI,IAAI,SAAS,cACf,QAAQ,KAAK;QAEb,QAAQ,IAAI;EAEhB,CAAC;EAED,OAAO,KAAK,mBAAmB;GAC7B,OAAO,MAAM;GACb,QAAQ,IAAI;EACd,CAAC;EAED,OAAO,OAAO,IAAI;CACpB,CAAC;AACH"}
1
+ {"version":3,"file":"checkPortAvailability.cjs","names":["net"],"sources":["../../src/utils/checkPortAvailability.ts"],"sourcesContent":["import net from 'node:net';\n\n/**\n * Resolves `false` when `port` is already bound on `host`.\n *\n * Probe the same host the server listens on: on macOS an IPv6 `::` probe\n * succeeds even when `0.0.0.0:port` is taken.\n */\nexport const checkPortAvailability = (\n port: number,\n host = '0.0.0.0'\n): Promise<boolean> =>\n new Promise((resolve) => {\n const server = net.createServer();\n\n server.once('error', (error: NodeJS.ErrnoException) => {\n resolve(error.code !== 'EADDRINUSE');\n });\n\n server.once('listening', () => {\n server.close();\n resolve(true);\n });\n\n server.listen(port, host);\n });\n\n/**\n * Loopback hosts probed besides the listening one: a server bound to a single\n * loopback address (e.g. a dev server on `::1`) would shadow the editor for\n * `localhost` while leaving `0.0.0.0` free.\n */\nconst LOOPBACK_HOSTS = ['127.0.0.1', '::1'] as const;\n\n/**\n * First port from `startPort` free on `host` and on the loopback addresses,\n * trying the next ones in order.\n *\n * @returns The port, or `undefined` when the `attempts` ports are all taken.\n */\nexport const findAvailablePort = async (\n startPort: number,\n host = '0.0.0.0',\n attempts = 20\n): Promise<number | undefined> => {\n for (let port = startPort; port < startPort + attempts; port++) {\n let isAvailable = true;\n\n for (const probedHost of [host, ...LOOPBACK_HOSTS]) {\n isAvailable = await checkPortAvailability(port, probedHost);\n if (!isAvailable) break;\n }\n\n if (isAvailable) return port;\n }\n\n return undefined;\n};\n"],"mappings":";;;;;;;;;;;;AAQA,MAAa,yBACX,MACA,OAAO,cAEP,IAAI,SAAS,YAAY;CACvB,MAAM,SAASA,iBAAI,aAAa;CAEhC,OAAO,KAAK,UAAU,UAAiC;EACrD,QAAQ,MAAM,SAAS,YAAY;CACrC,CAAC;CAED,OAAO,KAAK,mBAAmB;EAC7B,OAAO,MAAM;EACb,QAAQ,IAAI;CACd,CAAC;CAED,OAAO,OAAO,MAAM,IAAI;AAC1B,CAAC;;;;;;AAOH,MAAM,iBAAiB,CAAC,aAAa,KAAK;;;;;;;AAQ1C,MAAa,oBAAoB,OAC/B,WACA,OAAO,WACP,WAAW,OACqB;CAChC,KAAK,IAAI,OAAO,WAAW,OAAO,YAAY,UAAU,QAAQ;EAC9D,IAAI,cAAc;EAElB,KAAK,MAAM,cAAc,CAAC,MAAM,GAAG,cAAc,GAAG;GAClD,cAAc,MAAM,sBAAsB,MAAM,UAAU;GAC1D,IAAI,CAAC,aAAa;EACpB;EAEA,IAAI,aAAa,OAAO;CAC1B;AAGF"}
@@ -1,4 +1,17 @@
1
1
  //#region src/utils/checkPortAvailability.d.ts
2
- export declare const checkPortAvailability: (port: number) => Promise<boolean>;
2
+ /**
3
+ * Resolves `false` when `port` is already bound on `host`.
4
+ *
5
+ * Probe the same host the server listens on: on macOS an IPv6 `::` probe
6
+ * succeeds even when `0.0.0.0:port` is taken.
7
+ */
8
+ export declare const checkPortAvailability: (port: number, host?: string) => Promise<boolean>;
9
+ /**
10
+ * First port from `startPort` free on `host` and on the loopback addresses,
11
+ * trying the next ones in order.
12
+ *
13
+ * @returns The port, or `undefined` when the `attempts` ports are all taken.
14
+ */
15
+ export declare const findAvailablePort: (startPort: number, host?: string, attempts?: number) => Promise<number | undefined>;
3
16
  //#endregion
4
17
  //# sourceMappingURL=checkPortAvailability.d.ts.map
@@ -1,21 +1,46 @@
1
1
  import net from "node:net";
2
2
 
3
3
  //#region src/utils/checkPortAvailability.ts
4
- const checkPortAvailability = (port) => {
5
- return new Promise((resolve) => {
6
- const server = net.createServer();
7
- server.once("error", (err) => {
8
- if (err.code === "EADDRINUSE") resolve(false);
9
- else resolve(true);
10
- });
11
- server.once("listening", () => {
12
- server.close();
13
- resolve(true);
14
- });
15
- server.listen(port);
4
+ /**
5
+ * Resolves `false` when `port` is already bound on `host`.
6
+ *
7
+ * Probe the same host the server listens on: on macOS an IPv6 `::` probe
8
+ * succeeds even when `0.0.0.0:port` is taken.
9
+ */
10
+ const checkPortAvailability = (port, host = "0.0.0.0") => new Promise((resolve) => {
11
+ const server = net.createServer();
12
+ server.once("error", (error) => {
13
+ resolve(error.code !== "EADDRINUSE");
16
14
  });
15
+ server.once("listening", () => {
16
+ server.close();
17
+ resolve(true);
18
+ });
19
+ server.listen(port, host);
20
+ });
21
+ /**
22
+ * Loopback hosts probed besides the listening one: a server bound to a single
23
+ * loopback address (e.g. a dev server on `::1`) would shadow the editor for
24
+ * `localhost` while leaving `0.0.0.0` free.
25
+ */
26
+ const LOOPBACK_HOSTS = ["127.0.0.1", "::1"];
27
+ /**
28
+ * First port from `startPort` free on `host` and on the loopback addresses,
29
+ * trying the next ones in order.
30
+ *
31
+ * @returns The port, or `undefined` when the `attempts` ports are all taken.
32
+ */
33
+ const findAvailablePort = async (startPort, host = "0.0.0.0", attempts = 20) => {
34
+ for (let port = startPort; port < startPort + attempts; port++) {
35
+ let isAvailable = true;
36
+ for (const probedHost of [host, ...LOOPBACK_HOSTS]) {
37
+ isAvailable = await checkPortAvailability(port, probedHost);
38
+ if (!isAvailable) break;
39
+ }
40
+ if (isAvailable) return port;
41
+ }
17
42
  };
18
43
 
19
44
  //#endregion
20
- export { checkPortAvailability };
45
+ export { checkPortAvailability, findAvailablePort };
21
46
  //# sourceMappingURL=checkPortAvailability.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"checkPortAvailability.mjs","names":[],"sources":["../../src/utils/checkPortAvailability.ts"],"sourcesContent":["import net from 'node:net';\n\nexport const checkPortAvailability = (port: number): Promise<boolean> => {\n return new Promise((resolve) => {\n const server = net.createServer();\n\n server.once('error', (err: any) => {\n if (err.code === 'EADDRINUSE') {\n resolve(false); // Port is in use\n } else {\n resolve(true);\n }\n });\n\n server.once('listening', () => {\n server.close();\n resolve(true); // Port is available\n });\n\n server.listen(port);\n });\n};\n"],"mappings":";;;AAEA,MAAa,yBAAyB,SAAmC;CACvE,OAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,SAAS,IAAI,aAAa;EAEhC,OAAO,KAAK,UAAU,QAAa;GACjC,IAAI,IAAI,SAAS,cACf,QAAQ,KAAK;QAEb,QAAQ,IAAI;EAEhB,CAAC;EAED,OAAO,KAAK,mBAAmB;GAC7B,OAAO,MAAM;GACb,QAAQ,IAAI;EACd,CAAC;EAED,OAAO,OAAO,IAAI;CACpB,CAAC;AACH"}
1
+ {"version":3,"file":"checkPortAvailability.mjs","names":[],"sources":["../../src/utils/checkPortAvailability.ts"],"sourcesContent":["import net from 'node:net';\n\n/**\n * Resolves `false` when `port` is already bound on `host`.\n *\n * Probe the same host the server listens on: on macOS an IPv6 `::` probe\n * succeeds even when `0.0.0.0:port` is taken.\n */\nexport const checkPortAvailability = (\n port: number,\n host = '0.0.0.0'\n): Promise<boolean> =>\n new Promise((resolve) => {\n const server = net.createServer();\n\n server.once('error', (error: NodeJS.ErrnoException) => {\n resolve(error.code !== 'EADDRINUSE');\n });\n\n server.once('listening', () => {\n server.close();\n resolve(true);\n });\n\n server.listen(port, host);\n });\n\n/**\n * Loopback hosts probed besides the listening one: a server bound to a single\n * loopback address (e.g. a dev server on `::1`) would shadow the editor for\n * `localhost` while leaving `0.0.0.0` free.\n */\nconst LOOPBACK_HOSTS = ['127.0.0.1', '::1'] as const;\n\n/**\n * First port from `startPort` free on `host` and on the loopback addresses,\n * trying the next ones in order.\n *\n * @returns The port, or `undefined` when the `attempts` ports are all taken.\n */\nexport const findAvailablePort = async (\n startPort: number,\n host = '0.0.0.0',\n attempts = 20\n): Promise<number | undefined> => {\n for (let port = startPort; port < startPort + attempts; port++) {\n let isAvailable = true;\n\n for (const probedHost of [host, ...LOOPBACK_HOSTS]) {\n isAvailable = await checkPortAvailability(port, probedHost);\n if (!isAvailable) break;\n }\n\n if (isAvailable) return port;\n }\n\n return undefined;\n};\n"],"mappings":";;;;;;;;;AAQA,MAAa,yBACX,MACA,OAAO,cAEP,IAAI,SAAS,YAAY;CACvB,MAAM,SAAS,IAAI,aAAa;CAEhC,OAAO,KAAK,UAAU,UAAiC;EACrD,QAAQ,MAAM,SAAS,YAAY;CACrC,CAAC;CAED,OAAO,KAAK,mBAAmB;EAC7B,OAAO,MAAM;EACb,QAAQ,IAAI;CACd,CAAC;CAED,OAAO,OAAO,MAAM,IAAI;AAC1B,CAAC;;;;;;AAOH,MAAM,iBAAiB,CAAC,aAAa,KAAK;;;;;;;AAQ1C,MAAa,oBAAoB,OAC/B,WACA,OAAO,WACP,WAAW,OACqB;CAChC,KAAK,IAAI,OAAO,WAAW,OAAO,YAAY,UAAU,QAAQ;EAC9D,IAAI,cAAc;EAElB,KAAK,MAAM,cAAc,CAAC,MAAM,GAAG,cAAc,GAAG;GAClD,cAAc,MAAM,sBAAsB,MAAM,UAAU;GAC1D,IAAI,CAAC,aAAa;EACpB;EAEA,IAAI,aAAa,OAAO;CAC1B;AAGF"}
@@ -1,2 +0,0 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/shikiBundle-C3MOPHUk.js","assets/rolldown-runtime-hePW80VL.js","assets/MarkDownRender-MV3lk0jo.js","assets/react-Cvdyeg_0.js","assets/dynamic-OZWF58s3.js","assets/dist-CcmWBeAo.js","assets/index-DPHRjaWO.js","assets/index-D82ZIHVs.css"])))=>i.map(i=>d[i]);
2
- import{r as e}from"./rolldown-runtime-hePW80VL.js";import{t}from"./react-Cvdyeg_0.js";import{n}from"./dist-CcmWBeAo.js";import{d as r,l as i,o as a,u as o}from"./MarkDownRender-MV3lk0jo.js";import{l as s}from"./index-DPHRjaWO.js";var c=e(t(),1),l=n(),u=async(e,t)=>{let{codeToHtml:n}=await s(async()=>{let{codeToHtml:e}=await import(`./shikiBundle-C3MOPHUk.js`).then(e=>e.i);return{codeToHtml:e}},__vite__mapDeps([0,1,2,3,4,5,6,7]));return n(String(e),{lang:i(t).id,themes:a,defaultColor:!1})},d=({children:e,lang:t})=>{let[n,i]=(0,c.useState)(null);return(0,c.useEffect)(()=>{let n=!1;return u(e,t).then(e=>{n||i(e)}).catch(e=>{console.error(`Failed to highlight code:`,e),n||i(``)}),()=>{n=!0}},[e,t]),(0,l.jsx)(`div`,{className:r,children:n?(0,l.jsx)(`div`,{dangerouslySetInnerHTML:{__html:n}}):(0,l.jsx)(o,{children:e})})};export{d as CodeBlockShiki};
@@ -1 +0,0 @@
1
- import{t as e}from"./MarkDownRender-MV3lk0jo.js";export{e as MarkdownRenderer};