@uipath/auth 1.204.0-preview.254 → 1.205.0-preview.256

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/README.md CHANGED
@@ -16,7 +16,7 @@ npm install @uipath/auth
16
16
 
17
17
  ### Log in and save credentials
18
18
 
19
- `interactiveLogin` runs the whole login and writes the result to a credentials file. The flow is chosen by the options: the browser (authorization code + PKCE) by default, client credentials when a client secret is set, federated login when `clientAssertion` is set. The client secret can come from the `clientSecret` option or from a `clientSecret` pushed with `setAuthFileConfig` (the CLI pushes `auth.clientSecret` from `.uipath/config.json`), so with that set, a call with no `clientSecret` uses client credentials, not the browser.
19
+ `interactiveLogin` runs the whole login and writes the result to a credentials file. The flow is chosen by the options: the browser (authorization code + PKCE) by default, client credentials when a client secret is set, federated login when `clientAssertion` is set.
20
20
 
21
21
  ```ts
22
22
  import { interactiveLogin } from "@uipath/auth";
package/dist/config.d.ts CHANGED
@@ -6,8 +6,6 @@
6
6
  * slot is keyed by `Symbol.for()` on `globalThis`.
7
7
  */
8
8
  export interface AuthFileConfig {
9
- clientId?: string;
10
- clientSecret?: string;
11
9
  authority?: string;
12
10
  scopes?: string[];
13
11
  }
@@ -4,7 +4,7 @@ export * from "./clientCredentials";
4
4
  export { type AuthFileConfig, InvalidBaseUrlError, setAuthFileConfig, } from "./config";
5
5
  export * from "./loginStatus";
6
6
  export * from "./logout";
7
- export { type IdentityType, parseAuthFlow, readTokenScopes, resolveSessionIdentity, type SessionIdentity, } from "./sessionIdentity";
7
+ export { type ApplicationSubtype, type IdentityType, parseAuthFlow, readTokenScopes, resolveSessionIdentity, type SessionIdentity, } from "./sessionIdentity";
8
8
  export { fetchTenantsAndOrganizations, type Tenant, type TenantsAndOrganizations, } from "./tenantSelection";
9
9
  export * from "./tokenRefresh";
10
10
  export { AUTH_FLOW_ENV_VAR, type AuthFlow, type BaseCredentials, } from "./types";
@@ -2769,7 +2769,7 @@ var require_stream = __commonJS(function(exports, module) {
2769
2769
  Object.defineProperty(exports2, "__esModule", { value: true });
2770
2770
  var eventTargetShim = require_event_target_shim();
2771
2771
 
2772
- class AbortSignal extends eventTargetShim.EventTarget {
2772
+ class AbortSignal2 extends eventTargetShim.EventTarget {
2773
2773
  constructor() {
2774
2774
  super();
2775
2775
  throw TypeError("AbortSignal cannot be constructed directly");
@@ -2781,9 +2781,9 @@ var require_stream = __commonJS(function(exports, module) {
2781
2781
  return aborted;
2782
2782
  }
2783
2783
  }
2784
- eventTargetShim.defineEventAttribute(AbortSignal.prototype, "abort");
2784
+ eventTargetShim.defineEventAttribute(AbortSignal2.prototype, "abort");
2785
2785
  function createAbortSignal() {
2786
- let signal = Object.create(AbortSignal.prototype);
2786
+ let signal = Object.create(AbortSignal2.prototype);
2787
2787
  return eventTargetShim.EventTarget.call(signal), abortedFlags.set(signal, false), signal;
2788
2788
  }
2789
2789
  function abortSignal(signal) {
@@ -2792,9 +2792,9 @@ var require_stream = __commonJS(function(exports, module) {
2792
2792
  abortedFlags.set(signal, true), signal.dispatchEvent({ type: "abort" });
2793
2793
  }
2794
2794
  var abortedFlags = new WeakMap;
2795
- Object.defineProperties(AbortSignal.prototype, { aborted: { enumerable: true } });
2795
+ Object.defineProperties(AbortSignal2.prototype, { aborted: { enumerable: true } });
2796
2796
  if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol")
2797
- Object.defineProperty(AbortSignal.prototype, Symbol.toStringTag, { configurable: true, value: "AbortSignal" });
2797
+ Object.defineProperty(AbortSignal2.prototype, Symbol.toStringTag, { configurable: true, value: "AbortSignal" });
2798
2798
 
2799
2799
  class AbortController {
2800
2800
  constructor() {
@@ -2818,14 +2818,14 @@ var require_stream = __commonJS(function(exports, module) {
2818
2818
  if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol")
2819
2819
  Object.defineProperty(AbortController.prototype, Symbol.toStringTag, { configurable: true, value: "AbortController" });
2820
2820
  exports2.AbortController = AbortController;
2821
- exports2.AbortSignal = AbortSignal;
2821
+ exports2.AbortSignal = AbortSignal2;
2822
2822
  exports2.default = AbortController;
2823
2823
  module2.exports = AbortController;
2824
2824
  module2.exports.AbortController = module2.exports.default = AbortController;
2825
- module2.exports.AbortSignal = AbortSignal;
2825
+ module2.exports.AbortSignal = AbortSignal2;
2826
2826
  });
2827
2827
  var require_util = __commonJS2((exports2, module2) => {
2828
- var bufferModule = (init_buffer(), __toCommonJS(exports_buffer)), { format: format2, inspect: inspect2 } = require_inspect(), { codes: { ERR_INVALID_ARG_TYPE: ERR_INVALID_ARG_TYPE3 } } = require_errors(), { kResistStopPropagation, AggregateError: AggregateError2, SymbolDispose } = require_primordials(), AbortSignal = globalThis.AbortSignal || require_abort_controller().AbortSignal, AbortController = globalThis.AbortController || require_abort_controller().AbortController, AsyncFunction = Object.getPrototypeOf(async function() {}).constructor, Blob2 = globalThis.Blob || bufferModule.Blob, isBlob = typeof Blob2 < "u" ? function(b) {
2828
+ var bufferModule = (init_buffer(), __toCommonJS(exports_buffer)), { format: format2, inspect: inspect2 } = require_inspect(), { codes: { ERR_INVALID_ARG_TYPE: ERR_INVALID_ARG_TYPE3 } } = require_errors(), { kResistStopPropagation, AggregateError: AggregateError2, SymbolDispose } = require_primordials(), AbortSignal2 = globalThis.AbortSignal || require_abort_controller().AbortSignal, AbortController = globalThis.AbortController || require_abort_controller().AbortController, AsyncFunction = Object.getPrototypeOf(async function() {}).constructor, Blob2 = globalThis.Blob || bufferModule.Blob, isBlob = typeof Blob2 < "u" ? function(b) {
2829
2829
  return b instanceof Blob2;
2830
2830
  } : function(b) {
2831
2831
  return false;
@@ -2879,7 +2879,7 @@ var require_stream = __commonJS(function(exports, module) {
2879
2879
  var _removeEventListener;
2880
2880
  (_removeEventListener = removeEventListener) === null || _removeEventListener === undefined || _removeEventListener();
2881
2881
  } };
2882
- }, AbortSignalAny: AbortSignal.any || function(signals) {
2882
+ }, AbortSignalAny: AbortSignal2.any || function(signals) {
2883
2883
  if (signals.length === 1)
2884
2884
  return signals[0];
2885
2885
  let ac = new AbortController, abort = () => ac.abort();
@@ -18413,22 +18413,12 @@ var resolveConfigAsync = async ({
18413
18413
  baseUrl = DEFAULT_BASE_URL;
18414
18414
  }
18415
18415
  baseUrl = normalizeAndValidateBaseUrl(baseUrl);
18416
- let clientId = customClientId;
18417
- if (!clientId && fileAuth.clientId) {
18418
- clientId = fileAuth.clientId;
18419
- }
18420
- if (!clientId) {
18421
- clientId = DEFAULT_CLIENT_ID;
18422
- }
18423
- let clientSecret = customClientSecret;
18424
- if (!clientSecret && fileAuth.clientSecret) {
18425
- clientSecret = fileAuth.clientSecret;
18426
- }
18427
- const isExternalAppAuth = clientId !== DEFAULT_CLIENT_ID && (Boolean(clientSecret) || Boolean(customClientAssertion));
18416
+ const clientId = customClientId || DEFAULT_CLIENT_ID;
18417
+ const isExternalAppAuth = clientId !== DEFAULT_CLIENT_ID && (Boolean(customClientSecret) || Boolean(customClientAssertion));
18428
18418
  const scopes = resolveScopes(isExternalAppAuth, customScopes, fileAuth.scopes);
18429
18419
  return {
18430
18420
  clientId,
18431
- clientSecret,
18421
+ clientSecret: customClientSecret,
18432
18422
  scopes,
18433
18423
  baseUrl,
18434
18424
  authorizationEndpoint: `${baseUrl}/identity_/connect/authorize`,
@@ -18537,6 +18527,16 @@ var resolveIdentityType = (claims, authFlow, email) => {
18537
18527
  }
18538
18528
  return;
18539
18529
  };
18530
+ var resolveApplicationSubtype = (claims, authFlow) => {
18531
+ switch (asString(claims?.sub_type)) {
18532
+ case "service.external":
18533
+ return "External";
18534
+ case "service.internal":
18535
+ return "Internal";
18536
+ default:
18537
+ return authFlow === "client_credentials" || authFlow === "federated_credentials" ? "External" : undefined;
18538
+ }
18539
+ };
18540
18540
  var looksLikeEmail = (value) => value?.includes("@") ?? false;
18541
18541
  var pickEmail = (claims) => {
18542
18542
  for (const candidate of [claims?.email, claims?.preferred_username]) {
@@ -18578,6 +18578,12 @@ var resolveSessionIdentity = (accessToken, authFlow) => {
18578
18578
  const clientId = asString(claims?.client_id);
18579
18579
  if (clientId)
18580
18580
  identity.clientId = clientId;
18581
+ const subType = asString(claims?.sub_type);
18582
+ if (subType)
18583
+ identity.subType = subType;
18584
+ const applicationSubtype = resolveApplicationSubtype(claims, authFlow);
18585
+ if (applicationSubtype)
18586
+ identity.applicationSubtype = applicationSubtype;
18581
18587
  return identity;
18582
18588
  };
18583
18589
  var readTokenScopes = (accessToken) => accessToken ? pickScopes(decodeClaims(accessToken)) : undefined;
@@ -18872,6 +18878,40 @@ var tryRobotClientFallback = async (options = {}) => {
18872
18878
  }
18873
18879
  };
18874
18880
 
18881
+ // src/tenantSelection.ts
18882
+ var LOGIN_STATUS_HINT = "Run 'uip login status' to see the organization the CLI is signed in to.";
18883
+ var organizationHint = (organizationId) => `The request used '${organizationId}' as the organization. ${LOGIN_STATUS_HINT}`;
18884
+ var IDENTIFIER_STATUSES = new Set([400, 403, 404]);
18885
+ var describeNonJsonResponse = (organizationId, response, parseError) => {
18886
+ if (response.redirected) {
18887
+ return `Could not find organization '${organizationId}': the request was ` + `redirected to a sign-in page instead of the organization's tenant ` + `list. ${LOGIN_STATUS_HINT}`;
18888
+ }
18889
+ const contentType = response.headers.get("content-type") ?? "unknown";
18890
+ return `The organization service returned a '${contentType}' response instead ` + `of JSON: ${parseError.message}. ${organizationHint(organizationId)}`;
18891
+ };
18892
+ var fetchTenantsAndOrganizations = async (baseUrl, accessToken, organizationId, signal) => {
18893
+ const url = `${baseUrl}/${organizationId}/portal_/api/filtering/leftnav/tenantsAndOrganizationInfo`;
18894
+ const response = await fetch(url, {
18895
+ headers: {
18896
+ Authorization: `Bearer ${accessToken}`
18897
+ },
18898
+ ...signal ? { signal } : {}
18899
+ });
18900
+ if (!response.ok) {
18901
+ if (response.status === 401) {
18902
+ throw new Error(`Unauthorized: the access token was rejected, it was not ` + `issued for organization '${organizationId}', or ` + `'${organizationId}' was not accepted as an organization ` + `URL segment in this environment. ` + LOGIN_STATUS_HINT);
18903
+ }
18904
+ const errorText = await response.text();
18905
+ const message = `Failed to get tenants and organizations: ${response.status} ${errorText}`;
18906
+ throw new Error(IDENTIFIER_STATUSES.has(response.status) ? `${message}. ${organizationHint(organizationId)}` : message);
18907
+ }
18908
+ const [parseError, data] = await catchError(response.json());
18909
+ if (parseError) {
18910
+ throw new Error(describeNonJsonResponse(organizationId, response, parseError));
18911
+ }
18912
+ return data;
18913
+ };
18914
+
18875
18915
  // src/tokenRefresh.ts
18876
18916
  var TOKEN_REFRESH_REAUTHENTICATE_MESSAGE = "Token refresh failed. Run 'uip login' to re-authenticate.";
18877
18917
 
@@ -19092,6 +19132,8 @@ var CredentialSource;
19092
19132
  CredentialSource2["Robot"] = "robot";
19093
19133
  CredentialSource2["EnvironmentVariables"] = "env-vars";
19094
19134
  })(CredentialSource ||= {});
19135
+ var defaultRobotTenantIdCache = new Map;
19136
+ var ROBOT_TENANT_LOOKUP_TIMEOUT_MS = 5000;
19095
19137
  var getLoginStatusAsync = async (options = {}) => {
19096
19138
  return getLoginStatusWithDeps(options);
19097
19139
  };
@@ -19104,12 +19146,20 @@ var getLoginStatusWithDeps = async (options = {}, deps = {}) => {
19104
19146
  refreshToken: refreshTokenFn = refreshAccessToken,
19105
19147
  resolveConfig = resolveConfigAsync,
19106
19148
  robotFallback = tryRobotClientFallback,
19149
+ fetchTenantsAndOrgs = fetchTenantsAndOrganizations,
19150
+ robotTenantIdCache = defaultRobotTenantIdCache,
19151
+ robotTenantLookupTimeoutMs = ROBOT_TENANT_LOOKUP_TIMEOUT_MS,
19107
19152
  loadBreaker = loadRefreshBreaker,
19108
19153
  saveBreaker = saveRefreshBreaker,
19109
19154
  clearBreaker = clearRefreshBreaker
19110
19155
  } = deps;
19156
+ const robotDeps = {
19157
+ fetchTenantsAndOrgs,
19158
+ cache: robotTenantIdCache,
19159
+ lookupTimeoutMs: robotTenantLookupTimeoutMs
19160
+ };
19111
19161
  if (isRobotAuthEnforced()) {
19112
- return resolveRobotEnforcedStatus(robotFallback);
19162
+ return resolveRobotEnforcedStatus(robotFallback, robotDeps);
19113
19163
  }
19114
19164
  if (isEnvAuthEnabled()) {
19115
19165
  return readAuthFromEnv();
@@ -19127,7 +19177,7 @@ var getLoginStatusWithDeps = async (options = {}, deps = {}) => {
19127
19177
  hint: `No credentials found for profile "${activeProfile}". Run 'uip login --profile ${activeProfile}' to authenticate this profile.`
19128
19178
  };
19129
19179
  }
19130
- return resolveBorrowedRobotStatus(robotFallback);
19180
+ return resolveBorrowedRobotStatus(robotFallback, robotDeps);
19131
19181
  }
19132
19182
  const loaded = await loadFileCredentials(loadEnvFile, absolutePath);
19133
19183
  if ("status" in loaded) {
@@ -19169,7 +19219,7 @@ var getLoginStatusWithDeps = async (options = {}, deps = {}) => {
19169
19219
  }
19170
19220
  return buildFileStatus(tokens, credentials, globalHint);
19171
19221
  };
19172
- async function resolveRobotEnforcedStatus(robotFallback) {
19222
+ async function resolveRobotEnforcedStatus(robotFallback, deps) {
19173
19223
  if (isEnvAuthEnabled()) {
19174
19224
  throw new EnvAuthConfigError(`${ENV_AUTH_ENABLE_VAR}=true and ${ENFORCE_ROBOT_AUTH_VAR}=true ` + `are mutually exclusive. Unset one of them and re-run.`);
19175
19225
  }
@@ -19180,11 +19230,11 @@ async function resolveRobotEnforcedStatus(robotFallback) {
19180
19230
  hint: `${ENFORCE_ROBOT_AUTH_VAR}=true but the UiPath Robot ` + `session is unavailable. Start and sign in to the Assistant, ` + `or unset ${ENFORCE_ROBOT_AUTH_VAR} to fall back to file or ` + `env-var authentication.`
19181
19231
  };
19182
19232
  }
19183
- return buildRobotStatus(robotCreds);
19233
+ return buildRobotStatus(robotCreds, deps);
19184
19234
  }
19185
- async function resolveBorrowedRobotStatus(robotFallback) {
19235
+ async function resolveBorrowedRobotStatus(robotFallback, deps) {
19186
19236
  const robotCreds = await robotFallback();
19187
- return robotCreds ? buildRobotStatus(robotCreds) : { loginStatus: "Not logged in" };
19237
+ return robotCreds ? buildRobotStatus(robotCreds, deps) : { loginStatus: "Not logged in" };
19188
19238
  }
19189
19239
  async function loadFileCredentials(loadEnvFile, absolutePath) {
19190
19240
  let credentials;
@@ -19319,9 +19369,10 @@ function identityFields(accessToken, credentials) {
19319
19369
  ...scopes ? { scopes } : {}
19320
19370
  };
19321
19371
  }
19322
- function buildRobotStatus(robotCreds) {
19372
+ async function buildRobotStatus(robotCreds, deps) {
19323
19373
  const identity = resolveSessionIdentity(robotCreds.accessToken);
19324
19374
  const scopes = readTokenScopes(robotCreds.accessToken);
19375
+ const tenant = robotCreds.tenantId ? { tenantId: robotCreds.tenantId } : await resolveRobotTenantId(robotCreds, deps);
19325
19376
  return {
19326
19377
  loginStatus: "Logged in",
19327
19378
  accessToken: robotCreds.accessToken,
@@ -19329,14 +19380,62 @@ function buildRobotStatus(robotCreds) {
19329
19380
  organizationName: robotCreds.organizationName,
19330
19381
  organizationId: robotCreds.organizationId,
19331
19382
  tenantName: robotCreds.tenantName,
19332
- tenantId: robotCreds.tenantId,
19383
+ tenantId: tenant.tenantId,
19333
19384
  issuer: robotCreds.issuer,
19334
19385
  expiration: getTokenExpiration(robotCreds.accessToken),
19335
19386
  source: "robot" /* Robot */,
19387
+ ...tenant.hint ? { hint: tenant.hint } : {},
19336
19388
  ...identity ? { identity } : {},
19337
19389
  ...scopes ? { scopes } : {}
19338
19390
  };
19339
19391
  }
19392
+ async function resolveRobotTenantId(robotCreds, deps) {
19393
+ const { baseUrl, accessToken, tenantName } = robotCreds;
19394
+ const organization = robotCreds.organizationName ?? robotCreds.organizationId;
19395
+ if (!tenantName || !organization) {
19396
+ return {};
19397
+ }
19398
+ const key = `${baseUrl}|${organization}|${tenantName}|${accessToken}`;
19399
+ let pending = deps.cache.get(key);
19400
+ if (!pending) {
19401
+ pending = catchError(lookupRobotTenantId({ baseUrl, accessToken, organization, tenantName }, deps)).then(([error, resolution]) => {
19402
+ const settled = error ? { hint: tenantLookupFailureHint(tenantName, error.message) } : resolution;
19403
+ if (!settled.tenantId) {
19404
+ deps.cache.delete(key);
19405
+ }
19406
+ return settled;
19407
+ });
19408
+ deps.cache.set(key, pending);
19409
+ }
19410
+ return pending;
19411
+ }
19412
+ async function lookupRobotTenantId(target, deps) {
19413
+ const { baseUrl, accessToken, organization, tenantName } = target;
19414
+ const [error, data] = await catchError(deps.fetchTenantsAndOrgs(baseUrl, accessToken, organization, AbortSignal.timeout(deps.lookupTimeoutMs)));
19415
+ if (error) {
19416
+ const reason = error.name === "TimeoutError" ? `the tenant list did not answer within ${deps.lookupTimeoutMs} ms.` : error.message;
19417
+ return { hint: tenantLookupFailureHint(tenantName, reason) };
19418
+ }
19419
+ const rows = data?.tenants;
19420
+ const tenants = Array.isArray(rows) ? rows.filter((tenant) => typeof tenant?.id === "string" && typeof tenant?.name === "string") : [];
19421
+ const wantedName = decodePathSegment(tenantName);
19422
+ const wanted = wantedName.toLowerCase();
19423
+ const match = tenants.find((tenant) => tenant.name === wantedName) ?? tenants.find((tenant) => tenant.name.toLowerCase() === wanted);
19424
+ if (!match) {
19425
+ const names = tenants.map((tenant) => tenant.name).join(", ");
19426
+ return {
19427
+ hint: `The Robot session targets tenant '${tenantName}', but ` + `organization '${organization}' lists no tenant of that name` + `${names ? ` (available: ${names})` : ""}. Commands that need ` + `the tenant id will fail until it can be resolved.`
19428
+ };
19429
+ }
19430
+ return { tenantId: match.id };
19431
+ }
19432
+ function tenantLookupFailureHint(tenantName, reason) {
19433
+ return `Could not resolve the id of tenant '${tenantName}' from the ` + `Robot session: ${reason} Commands that need the tenant id will ` + `fail until it can be resolved.`;
19434
+ }
19435
+ function decodePathSegment(segment) {
19436
+ const [error, decoded] = catchError(() => decodeURIComponent(segment));
19437
+ return error ? segment : decoded;
19438
+ }
19340
19439
  var isFileNotFoundError = (error) => {
19341
19440
  if (!(error instanceof Object))
19342
19441
  return false;
@@ -19780,38 +19879,6 @@ async function logoutWithDeps(options, deps = {}) {
19780
19879
  async function logout(options) {
19781
19880
  return logoutWithDeps(options);
19782
19881
  }
19783
- // src/tenantSelection.ts
19784
- var LOGIN_STATUS_HINT = "Run 'uip login status' to see the organization the CLI is signed in to.";
19785
- var organizationHint = (organizationId) => `The request used '${organizationId}' as the organization. ${LOGIN_STATUS_HINT}`;
19786
- var IDENTIFIER_STATUSES = new Set([400, 403, 404]);
19787
- var describeNonJsonResponse = (organizationId, response, parseError) => {
19788
- if (response.redirected) {
19789
- return `Could not find organization '${organizationId}': the request was ` + `redirected to a sign-in page instead of the organization's tenant ` + `list. ${LOGIN_STATUS_HINT}`;
19790
- }
19791
- const contentType = response.headers.get("content-type") ?? "unknown";
19792
- return `The organization service returned a '${contentType}' response instead ` + `of JSON: ${parseError.message}. ${organizationHint(organizationId)}`;
19793
- };
19794
- var fetchTenantsAndOrganizations = async (baseUrl, accessToken, organizationId) => {
19795
- const url = `${baseUrl}/${organizationId}/portal_/api/filtering/leftnav/tenantsAndOrganizationInfo`;
19796
- const response = await fetch(url, {
19797
- headers: {
19798
- Authorization: `Bearer ${accessToken}`
19799
- }
19800
- });
19801
- if (!response.ok) {
19802
- if (response.status === 401) {
19803
- throw new Error(`Unauthorized: the access token was rejected, it was not ` + `issued for organization '${organizationId}', or ` + `'${organizationId}' was not accepted as an organization ` + `URL segment in this environment. ` + LOGIN_STATUS_HINT);
19804
- }
19805
- const errorText = await response.text();
19806
- const message = `Failed to get tenants and organizations: ${response.status} ${errorText}`;
19807
- throw new Error(IDENTIFIER_STATUSES.has(response.status) ? `${message}. ${organizationHint(organizationId)}` : message);
19808
- }
19809
- const [parseError, data] = await catchError(response.json());
19810
- if (parseError) {
19811
- throw new Error(describeNonJsonResponse(organizationId, response, parseError));
19812
- }
19813
- return data;
19814
- };
19815
19882
  export {
19816
19883
  AUTH_FLOW_ENV_VAR,
19817
19884
  AuthProfileValidationError,
@@ -19857,4 +19924,4 @@ export {
19857
19924
  setAuthFileConfig
19858
19925
  };
19859
19926
 
19860
- //# debugId=444F587B7EE9CE7764756E2164756E21
19927
+ //# debugId=0E042F445778F04E64756E2164756E21
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export * from "./logout";
12
12
  export { clearRefreshBreaker, loadRefreshBreaker, type RefreshBreakerState, refreshTokenFingerprint, saveRefreshBreaker, } from "./refreshCircuitBreaker";
13
13
  export { type RobotClientModuleLoader, registerRobotClientLoader, } from "./robotClientFallback";
14
14
  export { INVALID_TENANT_CODE, InvalidTenantError, isTenantSelectionError, type SelectFromList, selectTenantWithDeps, TENANT_SELECTION_REQUIRED_CODE, TenantSelectionError, TenantSelectionRequiredError, } from "./selectTenant";
15
- export { type IdentityType, parseAuthFlow, readTokenScopes, resolveSessionIdentity, type SessionIdentity, } from "./sessionIdentity";
15
+ export { type ApplicationSubtype, type IdentityType, parseAuthFlow, readTokenScopes, resolveSessionIdentity, type SessionIdentity, } from "./sessionIdentity";
16
16
  export { fetchTenantsAndOrganizations, type Tenant, type TenantsAndOrganizations, } from "./tenantSelection";
17
17
  export * from "./tokenRefresh";
18
18
  export { AUTH_FLOW_ENV_VAR, type AuthFlow, type BaseCredentials, } from "./types";
package/dist/index.js CHANGED
@@ -892,6 +892,16 @@ class NodeFileSystem {
892
892
  async rename(oldPath, newPath) {
893
893
  await fs6.rename(oldPath, newPath);
894
894
  }
895
+ async isSymbolicLink(filePath) {
896
+ try {
897
+ return (await fs6.lstat(filePath)).isSymbolicLink();
898
+ } catch (error) {
899
+ if (this.isEnoent(error) || this.hasErrnoCode(error, "ENOTDIR")) {
900
+ return false;
901
+ }
902
+ throw error;
903
+ }
904
+ }
895
905
  async realpath(filePath) {
896
906
  try {
897
907
  return await fs6.realpath(filePath);
@@ -1571,22 +1581,12 @@ var resolveConfigAsync = async ({
1571
1581
  baseUrl = DEFAULT_BASE_URL;
1572
1582
  }
1573
1583
  baseUrl = normalizeAndValidateBaseUrl(baseUrl);
1574
- let clientId = customClientId;
1575
- if (!clientId && fileAuth.clientId) {
1576
- clientId = fileAuth.clientId;
1577
- }
1578
- if (!clientId) {
1579
- clientId = DEFAULT_CLIENT_ID;
1580
- }
1581
- let clientSecret = customClientSecret;
1582
- if (!clientSecret && fileAuth.clientSecret) {
1583
- clientSecret = fileAuth.clientSecret;
1584
- }
1585
- const isExternalAppAuth = clientId !== DEFAULT_CLIENT_ID && (Boolean(clientSecret) || Boolean(customClientAssertion));
1584
+ const clientId = customClientId || DEFAULT_CLIENT_ID;
1585
+ const isExternalAppAuth = clientId !== DEFAULT_CLIENT_ID && (Boolean(customClientSecret) || Boolean(customClientAssertion));
1586
1586
  const scopes = resolveScopes(isExternalAppAuth, customScopes, fileAuth.scopes);
1587
1587
  return {
1588
1588
  clientId,
1589
- clientSecret,
1589
+ clientSecret: customClientSecret,
1590
1590
  scopes,
1591
1591
  baseUrl,
1592
1592
  authorizationEndpoint: `${baseUrl}/identity_/connect/authorize`,
@@ -2200,6 +2200,16 @@ var resolveIdentityType = (claims, authFlow, email) => {
2200
2200
  }
2201
2201
  return;
2202
2202
  };
2203
+ var resolveApplicationSubtype = (claims, authFlow) => {
2204
+ switch (asString(claims?.sub_type)) {
2205
+ case "service.external":
2206
+ return "External";
2207
+ case "service.internal":
2208
+ return "Internal";
2209
+ default:
2210
+ return authFlow === "client_credentials" || authFlow === "federated_credentials" ? "External" : undefined;
2211
+ }
2212
+ };
2203
2213
  var looksLikeEmail = (value) => value?.includes("@") ?? false;
2204
2214
  var pickEmail = (claims) => {
2205
2215
  for (const candidate of [claims?.email, claims?.preferred_username]) {
@@ -2241,6 +2251,12 @@ var resolveSessionIdentity = (accessToken, authFlow) => {
2241
2251
  const clientId = asString(claims?.client_id);
2242
2252
  if (clientId)
2243
2253
  identity.clientId = clientId;
2254
+ const subType = asString(claims?.sub_type);
2255
+ if (subType)
2256
+ identity.subType = subType;
2257
+ const applicationSubtype = resolveApplicationSubtype(claims, authFlow);
2258
+ if (applicationSubtype)
2259
+ identity.applicationSubtype = applicationSubtype;
2244
2260
  return identity;
2245
2261
  };
2246
2262
  var readTokenScopes = (accessToken) => accessToken ? pickScopes(decodeClaims(accessToken)) : undefined;
@@ -2538,6 +2554,40 @@ var tryRobotClientFallback = async (options = {}) => {
2538
2554
  }
2539
2555
  };
2540
2556
 
2557
+ // src/tenantSelection.ts
2558
+ var LOGIN_STATUS_HINT = "Run 'uip login status' to see the organization the CLI is signed in to.";
2559
+ var organizationHint = (organizationId) => `The request used '${organizationId}' as the organization. ${LOGIN_STATUS_HINT}`;
2560
+ var IDENTIFIER_STATUSES = new Set([400, 403, 404]);
2561
+ var describeNonJsonResponse = (organizationId, response, parseError) => {
2562
+ if (response.redirected) {
2563
+ return `Could not find organization '${organizationId}': the request was ` + `redirected to a sign-in page instead of the organization's tenant ` + `list. ${LOGIN_STATUS_HINT}`;
2564
+ }
2565
+ const contentType = response.headers.get("content-type") ?? "unknown";
2566
+ return `The organization service returned a '${contentType}' response instead ` + `of JSON: ${parseError.message}. ${organizationHint(organizationId)}`;
2567
+ };
2568
+ var fetchTenantsAndOrganizations = async (baseUrl, accessToken, organizationId, signal) => {
2569
+ const url = `${baseUrl}/${organizationId}/portal_/api/filtering/leftnav/tenantsAndOrganizationInfo`;
2570
+ const response = await fetch(url, {
2571
+ headers: {
2572
+ Authorization: `Bearer ${accessToken}`
2573
+ },
2574
+ ...signal ? { signal } : {}
2575
+ });
2576
+ if (!response.ok) {
2577
+ if (response.status === 401) {
2578
+ throw new Error(`Unauthorized: the access token was rejected, it was not ` + `issued for organization '${organizationId}', or ` + `'${organizationId}' was not accepted as an organization ` + `URL segment in this environment. ` + LOGIN_STATUS_HINT);
2579
+ }
2580
+ const errorText = await response.text();
2581
+ const message = `Failed to get tenants and organizations: ${response.status} ${errorText}`;
2582
+ throw new Error(IDENTIFIER_STATUSES.has(response.status) ? `${message}. ${organizationHint(organizationId)}` : message);
2583
+ }
2584
+ const [parseError, data] = await catchError(response.json());
2585
+ if (parseError) {
2586
+ throw new Error(describeNonJsonResponse(organizationId, response, parseError));
2587
+ }
2588
+ return data;
2589
+ };
2590
+
2541
2591
  // src/tokenRefresh.ts
2542
2592
  var TOKEN_REFRESH_REAUTHENTICATE_MESSAGE = "Token refresh failed. Run 'uip login' to re-authenticate.";
2543
2593
 
@@ -2759,6 +2809,8 @@ var CredentialSource;
2759
2809
  CredentialSource2["Robot"] = "robot";
2760
2810
  CredentialSource2["EnvironmentVariables"] = "env-vars";
2761
2811
  })(CredentialSource ||= {});
2812
+ var defaultRobotTenantIdCache = new Map;
2813
+ var ROBOT_TENANT_LOOKUP_TIMEOUT_MS = 5000;
2762
2814
  var getLoginStatusAsync = async (options = {}) => {
2763
2815
  return getLoginStatusWithDeps(options);
2764
2816
  };
@@ -2771,12 +2823,20 @@ var getLoginStatusWithDeps = async (options = {}, deps = {}) => {
2771
2823
  refreshToken: refreshTokenFn = refreshAccessToken,
2772
2824
  resolveConfig = resolveConfigAsync,
2773
2825
  robotFallback = tryRobotClientFallback,
2826
+ fetchTenantsAndOrgs = fetchTenantsAndOrganizations,
2827
+ robotTenantIdCache = defaultRobotTenantIdCache,
2828
+ robotTenantLookupTimeoutMs = ROBOT_TENANT_LOOKUP_TIMEOUT_MS,
2774
2829
  loadBreaker = loadRefreshBreaker,
2775
2830
  saveBreaker = saveRefreshBreaker,
2776
2831
  clearBreaker = clearRefreshBreaker
2777
2832
  } = deps;
2833
+ const robotDeps = {
2834
+ fetchTenantsAndOrgs,
2835
+ cache: robotTenantIdCache,
2836
+ lookupTimeoutMs: robotTenantLookupTimeoutMs
2837
+ };
2778
2838
  if (isRobotAuthEnforced()) {
2779
- return resolveRobotEnforcedStatus(robotFallback);
2839
+ return resolveRobotEnforcedStatus(robotFallback, robotDeps);
2780
2840
  }
2781
2841
  if (isEnvAuthEnabled()) {
2782
2842
  return readAuthFromEnv();
@@ -2794,7 +2854,7 @@ var getLoginStatusWithDeps = async (options = {}, deps = {}) => {
2794
2854
  hint: `No credentials found for profile "${activeProfile}". Run 'uip login --profile ${activeProfile}' to authenticate this profile.`
2795
2855
  };
2796
2856
  }
2797
- return resolveBorrowedRobotStatus(robotFallback);
2857
+ return resolveBorrowedRobotStatus(robotFallback, robotDeps);
2798
2858
  }
2799
2859
  const loaded = await loadFileCredentials(loadEnvFile, absolutePath);
2800
2860
  if ("status" in loaded) {
@@ -2836,7 +2896,7 @@ var getLoginStatusWithDeps = async (options = {}, deps = {}) => {
2836
2896
  }
2837
2897
  return buildFileStatus(tokens, credentials, globalHint);
2838
2898
  };
2839
- async function resolveRobotEnforcedStatus(robotFallback) {
2899
+ async function resolveRobotEnforcedStatus(robotFallback, deps) {
2840
2900
  if (isEnvAuthEnabled()) {
2841
2901
  throw new EnvAuthConfigError(`${ENV_AUTH_ENABLE_VAR}=true and ${ENFORCE_ROBOT_AUTH_VAR}=true ` + `are mutually exclusive. Unset one of them and re-run.`);
2842
2902
  }
@@ -2847,11 +2907,11 @@ async function resolveRobotEnforcedStatus(robotFallback) {
2847
2907
  hint: `${ENFORCE_ROBOT_AUTH_VAR}=true but the UiPath Robot ` + `session is unavailable. Start and sign in to the Assistant, ` + `or unset ${ENFORCE_ROBOT_AUTH_VAR} to fall back to file or ` + `env-var authentication.`
2848
2908
  };
2849
2909
  }
2850
- return buildRobotStatus(robotCreds);
2910
+ return buildRobotStatus(robotCreds, deps);
2851
2911
  }
2852
- async function resolveBorrowedRobotStatus(robotFallback) {
2912
+ async function resolveBorrowedRobotStatus(robotFallback, deps) {
2853
2913
  const robotCreds = await robotFallback();
2854
- return robotCreds ? buildRobotStatus(robotCreds) : { loginStatus: "Not logged in" };
2914
+ return robotCreds ? buildRobotStatus(robotCreds, deps) : { loginStatus: "Not logged in" };
2855
2915
  }
2856
2916
  async function loadFileCredentials(loadEnvFile, absolutePath) {
2857
2917
  let credentials;
@@ -2986,9 +3046,10 @@ function identityFields(accessToken, credentials) {
2986
3046
  ...scopes ? { scopes } : {}
2987
3047
  };
2988
3048
  }
2989
- function buildRobotStatus(robotCreds) {
3049
+ async function buildRobotStatus(robotCreds, deps) {
2990
3050
  const identity = resolveSessionIdentity(robotCreds.accessToken);
2991
3051
  const scopes = readTokenScopes(robotCreds.accessToken);
3052
+ const tenant = robotCreds.tenantId ? { tenantId: robotCreds.tenantId } : await resolveRobotTenantId(robotCreds, deps);
2992
3053
  return {
2993
3054
  loginStatus: "Logged in",
2994
3055
  accessToken: robotCreds.accessToken,
@@ -2996,14 +3057,62 @@ function buildRobotStatus(robotCreds) {
2996
3057
  organizationName: robotCreds.organizationName,
2997
3058
  organizationId: robotCreds.organizationId,
2998
3059
  tenantName: robotCreds.tenantName,
2999
- tenantId: robotCreds.tenantId,
3060
+ tenantId: tenant.tenantId,
3000
3061
  issuer: robotCreds.issuer,
3001
3062
  expiration: getTokenExpiration(robotCreds.accessToken),
3002
3063
  source: "robot" /* Robot */,
3064
+ ...tenant.hint ? { hint: tenant.hint } : {},
3003
3065
  ...identity ? { identity } : {},
3004
3066
  ...scopes ? { scopes } : {}
3005
3067
  };
3006
3068
  }
3069
+ async function resolveRobotTenantId(robotCreds, deps) {
3070
+ const { baseUrl, accessToken, tenantName } = robotCreds;
3071
+ const organization = robotCreds.organizationName ?? robotCreds.organizationId;
3072
+ if (!tenantName || !organization) {
3073
+ return {};
3074
+ }
3075
+ const key = `${baseUrl}|${organization}|${tenantName}|${accessToken}`;
3076
+ let pending = deps.cache.get(key);
3077
+ if (!pending) {
3078
+ pending = catchError(lookupRobotTenantId({ baseUrl, accessToken, organization, tenantName }, deps)).then(([error, resolution]) => {
3079
+ const settled = error ? { hint: tenantLookupFailureHint(tenantName, error.message) } : resolution;
3080
+ if (!settled.tenantId) {
3081
+ deps.cache.delete(key);
3082
+ }
3083
+ return settled;
3084
+ });
3085
+ deps.cache.set(key, pending);
3086
+ }
3087
+ return pending;
3088
+ }
3089
+ async function lookupRobotTenantId(target, deps) {
3090
+ const { baseUrl, accessToken, organization, tenantName } = target;
3091
+ const [error, data] = await catchError(deps.fetchTenantsAndOrgs(baseUrl, accessToken, organization, AbortSignal.timeout(deps.lookupTimeoutMs)));
3092
+ if (error) {
3093
+ const reason = error.name === "TimeoutError" ? `the tenant list did not answer within ${deps.lookupTimeoutMs} ms.` : error.message;
3094
+ return { hint: tenantLookupFailureHint(tenantName, reason) };
3095
+ }
3096
+ const rows = data?.tenants;
3097
+ const tenants = Array.isArray(rows) ? rows.filter((tenant) => typeof tenant?.id === "string" && typeof tenant?.name === "string") : [];
3098
+ const wantedName = decodePathSegment(tenantName);
3099
+ const wanted = wantedName.toLowerCase();
3100
+ const match = tenants.find((tenant) => tenant.name === wantedName) ?? tenants.find((tenant) => tenant.name.toLowerCase() === wanted);
3101
+ if (!match) {
3102
+ const names = tenants.map((tenant) => tenant.name).join(", ");
3103
+ return {
3104
+ hint: `The Robot session targets tenant '${tenantName}', but ` + `organization '${organization}' lists no tenant of that name` + `${names ? ` (available: ${names})` : ""}. Commands that need ` + `the tenant id will fail until it can be resolved.`
3105
+ };
3106
+ }
3107
+ return { tenantId: match.id };
3108
+ }
3109
+ function tenantLookupFailureHint(tenantName, reason) {
3110
+ return `Could not resolve the id of tenant '${tenantName}' from the ` + `Robot session: ${reason} Commands that need the tenant id will ` + `fail until it can be resolved.`;
3111
+ }
3112
+ function decodePathSegment(segment) {
3113
+ const [error, decoded] = catchError(() => decodeURIComponent(segment));
3114
+ return error ? segment : decoded;
3115
+ }
3007
3116
  var isFileNotFoundError = (error) => {
3008
3117
  if (!(error instanceof Object))
3009
3118
  return false;
@@ -3534,39 +3643,6 @@ var federatedCredentialsLogin = async ({
3534
3643
  init_src();
3535
3644
  init_constants();
3536
3645
 
3537
- // src/tenantSelection.ts
3538
- var LOGIN_STATUS_HINT = "Run 'uip login status' to see the organization the CLI is signed in to.";
3539
- var organizationHint = (organizationId) => `The request used '${organizationId}' as the organization. ${LOGIN_STATUS_HINT}`;
3540
- var IDENTIFIER_STATUSES = new Set([400, 403, 404]);
3541
- var describeNonJsonResponse = (organizationId, response, parseError) => {
3542
- if (response.redirected) {
3543
- return `Could not find organization '${organizationId}': the request was ` + `redirected to a sign-in page instead of the organization's tenant ` + `list. ${LOGIN_STATUS_HINT}`;
3544
- }
3545
- const contentType = response.headers.get("content-type") ?? "unknown";
3546
- return `The organization service returned a '${contentType}' response instead ` + `of JSON: ${parseError.message}. ${organizationHint(organizationId)}`;
3547
- };
3548
- var fetchTenantsAndOrganizations = async (baseUrl, accessToken, organizationId) => {
3549
- const url = `${baseUrl}/${organizationId}/portal_/api/filtering/leftnav/tenantsAndOrganizationInfo`;
3550
- const response = await fetch(url, {
3551
- headers: {
3552
- Authorization: `Bearer ${accessToken}`
3553
- }
3554
- });
3555
- if (!response.ok) {
3556
- if (response.status === 401) {
3557
- throw new Error(`Unauthorized: the access token was rejected, it was not ` + `issued for organization '${organizationId}', or ` + `'${organizationId}' was not accepted as an organization ` + `URL segment in this environment. ` + LOGIN_STATUS_HINT);
3558
- }
3559
- const errorText = await response.text();
3560
- const message = `Failed to get tenants and organizations: ${response.status} ${errorText}`;
3561
- throw new Error(IDENTIFIER_STATUSES.has(response.status) ? `${message}. ${organizationHint(organizationId)}` : message);
3562
- }
3563
- const [parseError, data] = await catchError(response.json());
3564
- if (parseError) {
3565
- throw new Error(describeNonJsonResponse(organizationId, response, parseError));
3566
- }
3567
- return data;
3568
- };
3569
-
3570
3646
  // src/selectTenant.ts
3571
3647
  var TENANT_SELECTION_REQUIRED_CODE = "TENANT_SELECTION_REQUIRED";
3572
3648
  var INVALID_TENANT_CODE = "INVALID_TENANT";
@@ -4021,4 +4097,4 @@ export {
4021
4097
  setAuthFileConfig
4022
4098
  };
4023
4099
 
4024
- //# debugId=4C387A5C17766AD564756E2164756E21
4100
+ //# debugId=3C4FEB497AB4F8C264756E2164756E21
@@ -3,6 +3,7 @@ import { resolveConfigAsync } from "./config";
3
3
  import { clearRefreshBreaker, loadRefreshBreaker, saveRefreshBreaker } from "./refreshCircuitBreaker";
4
4
  import { tryRobotClientFallback } from "./robotClientFallback";
5
5
  import { type SessionIdentity } from "./sessionIdentity";
6
+ import { fetchTenantsAndOrganizations } from "./tenantSelection";
6
7
  import { refreshAccessToken } from "./tokenRefresh";
7
8
  import { loadEnvFileAsync, resolveEnvFilePathAsync, saveEnvFileAsync } from "./utils/envFile";
8
9
  /**
@@ -92,10 +93,25 @@ export interface LoginStatusDependencies {
92
93
  refreshToken?: typeof refreshAccessToken;
93
94
  resolveConfig?: typeof resolveConfigAsync;
94
95
  robotFallback?: typeof tryRobotClientFallback;
96
+ /**
97
+ * Portal lookup that turns the tenant named in the Robot's Orchestrator
98
+ * URL into the tenant id — see {@link resolveRobotTenantId}.
99
+ */
100
+ fetchTenantsAndOrgs?: typeof fetchTenantsAndOrganizations;
101
+ /** Memo for {@link resolveRobotTenantId}; tests pass a fresh one. */
102
+ robotTenantIdCache?: RobotTenantIdCache;
103
+ /** Timeout for that lookup; tests pass a short one. */
104
+ robotTenantLookupTimeoutMs?: number;
95
105
  loadBreaker?: typeof loadRefreshBreaker;
96
106
  saveBreaker?: typeof saveRefreshBreaker;
97
107
  clearBreaker?: typeof clearRefreshBreaker;
98
108
  }
109
+ /** What the tenant lookup settled to; `hint` explains a missing `tenantId`. */
110
+ interface RobotTenantResolution {
111
+ tenantId?: string;
112
+ hint?: string;
113
+ }
114
+ type RobotTenantIdCache = Map<string, Promise<RobotTenantResolution>>;
99
115
  /**
100
116
  * Get the current login status by reading credentials from the credentials file
101
117
  * @param envFilePath - Path to the credentials file (defaults to ".uipath/.auth")
@@ -107,3 +123,4 @@ export declare const getLoginStatusAsync: (options?: GetLoginStatusOptions) => P
107
123
  * Get the current login status by reading credentials from the credentials file (with DI for testing)
108
124
  */
109
125
  export declare const getLoginStatusWithDeps: (options?: GetLoginStatusOptions, deps?: LoginStatusDependencies) => Promise<LoginStatus>;
126
+ export {};
@@ -4,6 +4,8 @@ import type { AuthFlow } from "./types";
4
4
  * Deliberately coarse — `authFlow` carries the precision.
5
5
  */
6
6
  export type IdentityType = "User" | "Application";
7
+ /** The token subtype when Identity explicitly identifies an application. */
8
+ export type ApplicationSubtype = "External" | "Internal";
7
9
  /**
8
10
  * Who the current session acts as, as reported by the access token.
9
11
  *
@@ -24,6 +26,17 @@ export interface SessionIdentity {
24
26
  * CLI's own — and surfacing that would read as if an app were acting.
25
27
  */
26
28
  clientId?: string;
29
+ /**
30
+ * The token's own `sub_type` claim, when it carries one. {@link type} is
31
+ * deliberately coarse — `service.internal` (S2S) and `service.external`
32
+ * (an external application) are both "Application" — and a caller that
33
+ * has to tell those two apart has nothing else to read. Unverified like
34
+ * every other field here, so it decides display and convenience, never
35
+ * access.
36
+ */
37
+ subType?: string;
38
+ /** Application identities only, when the token states its service subtype. */
39
+ applicationSubtype?: ApplicationSubtype;
27
40
  }
28
41
  /** Narrow an on-disk `UIPATH_AUTH_FLOW` value to a known flow. */
29
42
  export declare const parseAuthFlow: (value: string | undefined) => AuthFlow | undefined;
@@ -24,6 +24,10 @@ export interface TenantsAndOrganizations {
24
24
  * later on: the portal has been seen answering `401` to the id (and to a
25
25
  * case-variant of the name) on an aged-but-valid session that the exact name
26
26
  * still resolves. So the id is a last-resort fallback, not an equal option.
27
+ *
28
+ * @param signal - Optional abort signal. Interactive login passes none; the
29
+ * Robot status path passes a timeout so a hung portal cannot hang every
30
+ * command.
27
31
  */
28
- export declare const fetchTenantsAndOrganizations: (baseUrl: string, accessToken: string, organizationId: string) => Promise<TenantsAndOrganizations>;
32
+ export declare const fetchTenantsAndOrganizations: (baseUrl: string, accessToken: string, organizationId: string, signal?: AbortSignal) => Promise<TenantsAndOrganizations>;
29
33
  export {};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@uipath/auth",
3
3
  "author": "UiPath",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
- "version": "1.204.0-preview.254",
5
+ "version": "1.205.0-preview.256",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/UiPath/cli.git",
@@ -38,5 +38,5 @@
38
38
  "mihaigirleanu",
39
39
  "vlad-uipath"
40
40
  ],
41
- "gitHead": "3ee79cdec05b36231a8da9e8de6bf0043585526a"
41
+ "gitHead": "63740c6cbb7ff282fa9118cc16a148eb030b245f"
42
42
  }