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.
- package/bin/intlayer-editor.mjs +26 -6
- package/client/dist/assets/CodeBlockShiki-B-OcGOY1.js +2 -0
- package/client/dist/assets/MarkDownRender-BzY_8i9Z.js +1 -0
- package/client/dist/assets/MarkDownRender-CIkPpFUW.js +25 -0
- package/client/dist/assets/MarkdownEditor-BpbsoP4E.js +161 -0
- package/client/dist/assets/{astro-JHDYjn39.js → astro-BZ97kZFP.js} +1 -1
- package/client/dist/assets/codeTransformer-aP9a_qk2.js +9 -0
- package/client/dist/assets/compat-r4Owm_RW.js +1 -0
- package/client/dist/assets/css-BGIxQoYs.js +1 -0
- package/client/dist/assets/css-DUjzNfgv.js +1 -0
- package/client/dist/assets/dist-DRjLHkdX.js +11 -0
- package/client/dist/assets/extractUrlFromSitemap-Chj1JVpo.js +1 -0
- package/client/dist/assets/{html-B2cKP6UN.js → html-DSw1lh5G.js} +1 -1
- package/client/dist/assets/html-WQKKblVZ.js +1 -0
- package/client/dist/assets/index-C5SpC4ap.css +2 -0
- package/client/dist/assets/index-CmQV7Z_A.js +105 -0
- package/client/dist/assets/shikiBundle-BIG1bVgc.js +12 -0
- package/client/dist/assets/{svelte-tEuLYt3E.js → svelte-CdeT-Ich.js} +1 -1
- package/client/dist/assets/{vue-Bg0byEaI.js → vue-BqdMUtqy.js} +1 -1
- package/client/dist/index.html +3 -6
- package/package.json +28 -28
- package/server/dist/controllers/auth.controller.cjs +80 -0
- package/server/dist/controllers/auth.controller.cjs.map +1 -0
- package/server/dist/controllers/auth.controller.d.ts +36 -0
- package/server/dist/controllers/auth.controller.mjs +76 -0
- package/server/dist/controllers/auth.controller.mjs.map +1 -0
- package/server/dist/controllers/configuration.controller.cjs +5 -1
- package/server/dist/controllers/configuration.controller.cjs.map +1 -1
- package/server/dist/controllers/configuration.controller.mjs +5 -1
- package/server/dist/controllers/configuration.controller.mjs.map +1 -1
- package/server/dist/controllers/dictionary.controller.cjs +2 -1
- package/server/dist/controllers/dictionary.controller.cjs.map +1 -1
- package/server/dist/controllers/dictionary.controller.mjs +2 -1
- package/server/dist/controllers/dictionary.controller.mjs.map +1 -1
- package/server/dist/index.cjs +41 -13
- package/server/dist/index.cjs.map +1 -1
- package/server/dist/index.mjs +45 -17
- package/server/dist/index.mjs.map +1 -1
- package/server/dist/routes/auth.routes.cjs +35 -0
- package/server/dist/routes/auth.routes.cjs.map +1 -0
- package/server/dist/routes/auth.routes.d.ts +22 -0
- package/server/dist/routes/auth.routes.mjs +33 -0
- package/server/dist/routes/auth.routes.mjs.map +1 -0
- package/server/dist/services/editorAuth.service.cjs +143 -0
- package/server/dist/services/editorAuth.service.cjs.map +1 -0
- package/server/dist/services/editorAuth.service.d.ts +43 -0
- package/server/dist/services/editorAuth.service.mjs +137 -0
- package/server/dist/services/editorAuth.service.mjs.map +1 -0
- package/server/dist/utils/checkPortAvailability.cjs +38 -12
- package/server/dist/utils/checkPortAvailability.cjs.map +1 -1
- package/server/dist/utils/checkPortAvailability.d.ts +14 -1
- package/server/dist/utils/checkPortAvailability.mjs +38 -13
- package/server/dist/utils/checkPortAvailability.mjs.map +1 -1
- package/client/dist/assets/CodeBlockShiki-df1uxmSd.js +0 -2
- package/client/dist/assets/MarkDownRender-DYcnaCQP.js +0 -1
- package/client/dist/assets/MarkDownRender-MV3lk0jo.js +0 -25
- package/client/dist/assets/MarkdownEditor-CWVj-dFt.js +0 -160
- package/client/dist/assets/codeTransformer-DVPQQcdP.js +0 -9
- package/client/dist/assets/css-CdtkT07m.js +0 -1
- package/client/dist/assets/css-LbU1hoFO.js +0 -1
- package/client/dist/assets/dist-BHukwvnD.js +0 -11
- package/client/dist/assets/dist-CcmWBeAo.js +0 -1
- package/client/dist/assets/dynamic-OZWF58s3.js +0 -41
- package/client/dist/assets/html-DYtkB_T1.js +0 -1
- package/client/dist/assets/index-D82ZIHVs.css +0 -2
- package/client/dist/assets/index-DPHRjaWO.js +0 -59
- package/client/dist/assets/react-Cvdyeg_0.js +0 -1
- package/client/dist/assets/rolldown-runtime-hePW80VL.js +0 -1
- 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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>
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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>
|
|
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};
|