@metaplay/metaplay-auth 1.3.0 → 1.4.1

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/index.ts CHANGED
@@ -1,17 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander'
3
+ import Docker from 'dockerode'
3
4
  import { loginAndSaveTokens, machineLoginAndSaveTokens, extendCurrentSession, loadTokens, removeTokens } from './src/auth.js'
4
- import { StackAPI } from './src/stackapi.js'
5
- import type { GameserverId } from './src/stackapi.js'
5
+ import { StackAPI, type GameserverId } from './src/stackapi.js'
6
6
  import { checkGameServerDeployment } from './src/deployment.js'
7
7
  import { logger, setLogLevel } from './src/logging.js'
8
- import { isValidFQDN } from './src/utils.js'
8
+ import { isValidFQDN, executeCommand } from './src/utils.js'
9
9
  import { exit } from 'process'
10
10
  import { ECRClient, GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr'
11
+ import { tmpdir } from 'os'
12
+ import { join } from 'path'
13
+ import { randomBytes } from 'crypto'
14
+ import { writeFile, unlink } from 'fs/promises'
15
+ import { existsSync } from 'fs'
16
+ import { KubeConfig } from '@kubernetes/client-node'
17
+ import * as semver from 'semver'
11
18
 
12
19
  /**
13
- * Helper for parsing the GameserverId type from the command line arguments. Accepts either the gameserver address or the
14
- * (organization, project, environment) tuple from options.
20
+ * Helper for parsing the GameserverId type from the command line arguments. Accepts either the gameserver address
21
+ * (idler-test.p1.metaplay.io), a shorthand address (metaplay-idler-test) or the (organization, project, environment)
22
+ * tuple from options.
15
23
  */
16
24
  function resolveGameserverId (address: string | undefined, options: any): GameserverId {
17
25
  // If address is specified, use it, otherwise assume options has organization, project, and environment
@@ -21,7 +29,7 @@ function resolveGameserverId (address: string | undefined, options: any): Gamese
21
29
  } else {
22
30
  const parts = address.split('-')
23
31
  if (parts.length !== 3) {
24
- throw new Error('Invalid gameserver address syntax: specify either <organiation>-<project>-<environment> or a fully-qualified domain name (eg, idler-develop.p1.metaplay.io)')
32
+ throw new Error('Invalid gameserver address syntax: specify either <organization>-<project>-<environment> or a fully-qualified domain name (eg, idler-develop.p1.metaplay.io)')
25
33
  }
26
34
  return { organization: parts[0], project: parts[1], environment: parts[2] }
27
35
  }
@@ -37,7 +45,7 @@ const program = new Command()
37
45
  program
38
46
  .name('metaplay-auth')
39
47
  .description('Authenticate with Metaplay and get AWS and Kubernetes credentials for game servers.')
40
- .version('1.3.0')
48
+ .version('1.4.1')
41
49
  .option('-d, --debug', 'enable debug output')
42
50
  .hook('preAction', (thisCommand) => {
43
51
  // Handle debug flag for all commands.
@@ -125,7 +133,8 @@ program.command('get-kubeconfig')
125
133
  .option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
126
134
  .option('-p, --project <project>', 'project name (e.g. idler)')
127
135
  .option('-e, --environment <environment>', 'environment name (e.g. develop)')
128
- .option('-t, --type <format>', 'output format (static or dynamic)', 'static')
136
+ .option('-t, --type <credentials-type>', 'type of credentials handling in kubeconfig (static or dynamic)')
137
+ .option('--output <kubeconfig-path>', 'path of the output file where to write kubeconfig (written to stdout if not specified)')
129
138
  .hook('preAction', async () => {
130
139
  await extendCurrentSession()
131
140
  })
@@ -135,14 +144,29 @@ program.command('get-kubeconfig')
135
144
  const stackApi = new StackAPI(tokens.access_token, options.stackApi)
136
145
  const gameserverId = resolveGameserverId(gameserver, options)
137
146
 
138
- if (options.type === 'dynamic') {
139
- const credentials = await stackApi.getKubeConfigExecCredential(gameserverId)
140
- console.log(credentials)
141
- } else if (options.type === 'static') {
142
- const credentials = await stackApi.getKubeConfig(gameserverId)
143
- console.log(credentials)
147
+ // Default to credentialsType==dynamic for human users, and credentialsType==static for machine users
148
+ const isHumanUser = !!tokens.refresh_token
149
+ const credentialsType = options.credentialsType ?? (isHumanUser ? 'dynamic' : 'static')
150
+
151
+ // Generate kubeconfig
152
+ let kubeconfigPayload
153
+ if (credentialsType === 'dynamic') {
154
+ logger.debug('Fetching kubeconfig with execcredential')
155
+ kubeconfigPayload = await stackApi.getKubeConfigExecCredential(gameserverId)
156
+ } else if (credentialsType === 'static') {
157
+ logger.debug('Fetching kubeconfig with embedded secret')
158
+ kubeconfigPayload = await stackApi.getKubeConfig(gameserverId)
144
159
  } else {
145
- throw new Error('Invalid type; must be one of static or dynamic')
160
+ throw new Error('Invalid credentials type; must be either "static" or "dynamic"')
161
+ }
162
+
163
+ // Write kubeconfig to output (file or stdout)
164
+ if (options.output) {
165
+ logger.debug(`Writing kubeconfig to file ${options.output}`)
166
+ await writeFile(options.output, kubeconfigPayload, { mode: 0o600 })
167
+ console.log(`Wrote kubeconfig to ${options.output}`)
168
+ } else {
169
+ console.log(kubeconfigPayload)
146
170
  }
147
171
  } catch (error) {
148
172
  if (error instanceof Error) {
@@ -161,7 +185,7 @@ program.command('get-kubeconfig')
161
185
  */
162
186
  // todo: maybe this should be a hidden command as it's not very useful for end users and clutters help?
163
187
  program.command('get-kubernetes-execcredential')
164
- .description('get kubernetes credentials in execcredential format (only intended to be used within a kubeconfig)')
188
+ .description('[internal] get kubernetes credentials in execcredential format (used from the generated kubeconfigs)')
165
189
  .argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
166
190
  .option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
167
191
  .option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
@@ -328,19 +352,329 @@ program.command('get-environment')
328
352
  }
329
353
  })
330
354
 
355
+ program.command('push-docker-image')
356
+ .description('push docker image into the target environment image registry')
357
+ .argument('gameserver', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
358
+ .argument('image-name', 'full name of the docker image to push (eg, the gameserver:<sha>)')
359
+ .hook('preAction', async () => {
360
+ await extendCurrentSession()
361
+ })
362
+ .action(async (gameserver, imageName, options) => {
363
+ try {
364
+ console.log(`Pushing docker image ${imageName} to target environment ${gameserver}...`)
365
+
366
+ const tokens = await loadTokens()
367
+ const stackApi = new StackAPI(tokens.access_token, options.stackApi)
368
+ const gameserverId = resolveGameserverId(gameserver, options)
369
+
370
+ // Fetch AWS credentials from Metaplay cloud
371
+ logger.debug('Get AWS credentials from Metaplay')
372
+ const credentials = await stackApi.getAwsCredentials(gameserverId)
373
+
374
+ // Get environment info (region is needed for ECR)
375
+ logger.debug('Get environment info')
376
+ const envInfo = await stackApi.getEnvironmentDetails(gameserverId)
377
+ const awsRegion = envInfo.deployment.aws_region
378
+
379
+ // Create ECR client with credentials
380
+ logger.debug('Create ECR client')
381
+ const client = new ECRClient({
382
+ credentials: {
383
+ accessKeyId: credentials.AccessKeyId,
384
+ secretAccessKey: credentials.SecretAccessKey,
385
+ sessionToken: credentials.SessionToken
386
+ },
387
+ region: awsRegion
388
+ })
389
+
390
+ // Fetch the ECR docker authentication token
391
+ logger.debug('Fetch ECR login credentials from AWS')
392
+ const command = new GetAuthorizationTokenCommand({})
393
+ const response = await client.send(command)
394
+ if (!response.authorizationData || response.authorizationData.length === 0 || !response.authorizationData[0].authorizationToken) {
395
+ throw new Error('Received an empty authorization token response for ECR repository')
396
+ }
397
+
398
+ // Parse username and password from the response (separated by a ':')
399
+ logger.debug('Parse ECR response')
400
+ const authorization64 = response.authorizationData[0].authorizationToken
401
+ const authorization = Buffer.from(authorization64, 'base64').toString()
402
+ const [username, password] = authorization.split(':')
403
+ const authConfig = { username, password, serveraddress: '' } // \todo serveraddress value? https://stackoverflow.com/questions/54252777/how-to-push-image-with-dockerode-image-not-pushed-but-no-error
404
+
405
+ // Resolve tag from src image
406
+ if (!imageName) {
407
+ throw new Error('Must specify a valid docker image name as the image-name argument')
408
+ }
409
+ const srcImageParts = imageName.split(':')
410
+ if (srcImageParts.length !== 2 || srcImageParts[0].length === 0 || srcImageParts[1].length === 0) {
411
+ throw new Error(`Invalid docker image name '${imageName}', expecting the name in format 'name:tag'`)
412
+ }
413
+ const imageTag = srcImageParts[1]
414
+
415
+ // Resolve source image
416
+ const srcImageName = imageName
417
+ const dstRepoName = envInfo.deployment.ecr_repo
418
+ const dstImageName = `${dstRepoName}:${imageTag}`
419
+ const dockerApi = new Docker()
420
+ const srcDockerImage = dockerApi.getImage(srcImageName)
421
+
422
+ // If names don't match, tag the src image as dst
423
+ if (srcImageName !== dstImageName) {
424
+ logger.debug(`Tagging image ${srcImageName} as ${dstImageName}`)
425
+ await srcDockerImage.tag({ repo: dstRepoName, tag: imageTag })
426
+ }
427
+
428
+ // Push the image
429
+ logger.debug(`Push image ${dstImageName}`)
430
+ const dstDockerImage = dockerApi.getImage(dstImageName)
431
+ const pushStream = await dstDockerImage.push({ authconfig: authConfig, tag: options.imageTag })
432
+
433
+ // Follow push progress & wait until completed
434
+ logger.debug('Following push stream...')
435
+ await new Promise((resolve, reject) => {
436
+ dockerApi.modem.followProgress(
437
+ pushStream,
438
+ (error: Error | null, result: any[]) => {
439
+ if (error) {
440
+ logger.debug('Failed to push image:', error)
441
+ reject(error)
442
+ } else {
443
+ // result contains an array of all the progress objects
444
+ logger.debug('Succesfully finished pushing image')
445
+ resolve(result)
446
+ }
447
+ },
448
+ (obj: any) => {
449
+ // console.log('Progress:', obj)
450
+ // { status: 'Preparing', progressDetail: {}, id: '82730adcaeb0' }
451
+ // { status: 'Layer already exists', progressDetail: {}, id: '7cd701fff13a' }
452
+ })
453
+ })
454
+
455
+ console.log(`Successfully pushed docker image to ${dstImageName}!`)
456
+ } catch (error) {
457
+ if (error instanceof Error) {
458
+ console.error(`Failed to push docker image: ${error.message}`)
459
+ }
460
+ exit(1)
461
+ }
462
+ })
463
+
464
+ program.command('deploy-server')
465
+ .description('deploy a game server image to target environment')
466
+ .argument('gameserver', 'address of gameserver (e.g. idler-develop.p1.metaplay.io)')
467
+ .argument('image-tag', 'docker image tag to deploy (usually the SHA of the build)')
468
+ .option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
469
+ .option('-f, --values <path-to-values-file>', 'path to Helm values file to use for this deployment')
470
+ .option('--local-chart-path <path-to-chart-directory>', 'path to local Helm chart directory (to use a chart from local disk)')
471
+ .option('--helm-chart-repo <url>', 'override the URL of the Helm chart repository (eg, https://charts.metaplay.dev/testing)')
472
+ .option('--helm-chart-version <version>', 'override the Helm chart version (eg, 0.6.0)')
473
+ .option('--deployment-name', 'Helm deployment name to use', 'gameserver')
474
+ .hook('preAction', async () => {
475
+ await extendCurrentSession()
476
+ })
477
+ .action(async (gameserver, imageTag, options) => {
478
+ try {
479
+ const tokens = await loadTokens()
480
+ const stackApi = new StackAPI(tokens.access_token, options.stackApi)
481
+ const gameserverId = resolveGameserverId(gameserver, options)
482
+
483
+ console.log(`Deploying server to ${gameserver} with image tag ${imageTag}...`)
484
+
485
+ // Fetch target environment details
486
+ const envInfo = await stackApi.getEnvironmentDetails(gameserverId)
487
+
488
+ if (!imageTag) {
489
+ throw new Error('Must specify a valid docker image tag as the image-tag argument, usually the SHA of the build')
490
+ }
491
+
492
+ if (!options.deploymentName) {
493
+ throw new Error(`Invalid Helm deployment name '${options.deploymentName}'`)
494
+ }
495
+
496
+ // Resolve information about docker image
497
+ const dockerRepo = envInfo.deployment.ecr_repo
498
+ const imageName = `${dockerRepo}:${imageTag}`
499
+ logger.debug(`Fetch docker image information for ${imageName}`)
500
+ let imageLabels
501
+ try {
502
+ const dockerApi = new Docker()
503
+ const dockerImage = dockerApi.getImage(imageName)
504
+ imageLabels = (await dockerImage.inspect()).Config.Labels || {}
505
+ } catch (err) {
506
+ throw new Error(`Unable to fetch metadata for docker image ${imageName}: ${err}`)
507
+ }
508
+
509
+ // Try to resolve SDK version and Helm repo and chart version from the docker labels
510
+ // \note These only exist for SDK R28 and newer images
511
+ logger.debug('Docker image labels: ', JSON.stringify(imageLabels))
512
+ const sdkVersion = imageLabels['io.metaplay.sdk_version']
513
+
514
+ // Resolve helmChartRepo, in order of precedence:
515
+ // - Specified in the cli options
516
+ // - Specified in the docker image label
517
+ // - Fall back to 'https://charts.metaplay.dev'
518
+ const helmChartRepo = options.helmChartRepo ?? imageLabels['io.metaplay.default_helm_repo'] ?? 'https://charts.metaplay.dev'
519
+
520
+ // Resolve helmChartVersion, in order of precedence:
521
+ // - Specified in the cli options
522
+ // - Specified in the docker image label
523
+ // - Unknown, error out!
524
+ const helmChartVersion = options.helmChartVersion ?? imageLabels['io.metaplay.default_server_chart_version']
525
+ if (!helmChartVersion) {
526
+ throw new Error('No Helm chart version defined. With pre-R28 SDK versions, you must specify the Helm chart repository explicitly with --helm-chart-version.')
527
+ }
528
+
529
+ // A values file is required (at least for now it makes no sense to deploy without one)
530
+ if (!options.values) {
531
+ throw new Error('Path to a Helm values file must be specified with --values')
532
+ }
533
+
534
+ // \If Helm chart >= 0.7.0, check that sdkVersion is defined in docker image labels (the labels were added in R28)
535
+ const helmChartSemver = semver.parse(helmChartVersion)
536
+ if (!helmChartSemver) {
537
+ throw new Error(`Resolve Helm chart version '${helmChartVersion}' is not a valid SemVer!`)
538
+ }
539
+ if (semver.gte(helmChartSemver, new semver.SemVer('0.7.0'), true) && !sdkVersion) {
540
+ throw new Error('Helm chart versions >=0.7.0 are only compatible with SDK versions 28.0.0 and above.')
541
+ }
542
+
543
+ // Fetch kubeconfig and write it to a temporary file
544
+ // \todo allow passing a custom kubeconfig file?
545
+ const kubeconfigPayload = await stackApi.getKubeConfig(gameserverId)
546
+ const kubeconfigPath = join(tmpdir(), randomBytes(20).toString('hex'))
547
+ logger.debug(`Write temporary kubeconfig in ${kubeconfigPath}`)
548
+ await writeFile(kubeconfigPath, kubeconfigPayload, { mode: 0o600 })
549
+
550
+ try {
551
+ // Construct Helm invocation
552
+ const chartNameOrPath = options.localChartPath ?? 'metaplay-gameserver'
553
+ const helmArgs =
554
+ ['upgrade', '--install', '--wait'] // \note wait for the pods to stabilize -- otherwise status check can read state before any changes to pods are applied
555
+ .concat(['--kubeconfig', kubeconfigPath])
556
+ .concat(['-n', envInfo.deployment.kubernetes_namespace])
557
+ .concat(['--values', options.values])
558
+ .concat(['--set-string', `image.tag=${imageTag}`])
559
+ .concat(sdkVersion ? ['--set-string', `sdk.version=${sdkVersion}`] : [])
560
+ .concat(!options.chartPath ? ['--repo', helmChartRepo, '--version', helmChartVersion] : [])
561
+ .concat([options.deploymentName])
562
+ .concat([chartNameOrPath])
563
+ logger.info(`Execute: helm ${helmArgs.join(' ')}`)
564
+
565
+ // Execute Helm
566
+ let helmResult
567
+ try {
568
+ helmResult = await executeCommand('helm', helmArgs)
569
+ // \todo output something from Helm result?
570
+ } catch (err) {
571
+ throw new Error(`Failed to execute 'helm': ${err}. You need to have Helm v3 installed to deploy a game server with metaplay-auth.`)
572
+ }
573
+
574
+ // Throw on Helm non-success exit code
575
+ if (helmResult.code !== 0) {
576
+ throw new Error(`Helm deploy failed with exit code ${helmResult.code}: ${helmResult.stderr}`)
577
+ }
578
+
579
+ const testingRepoSuffix = (!options.chartPath && helmChartRepo !== 'https://charts.metaplay.dev') ? ` from repo ${helmChartRepo}` : ''
580
+ console.log(`Game server deployed to ${gameserver} with tag ${imageTag} using chart version ${helmChartVersion}${testingRepoSuffix}!`)
581
+ } finally {
582
+ // Remove temporary kubeconfig file
583
+ await unlink(kubeconfigPath)
584
+ }
585
+
586
+ // Check the status of the game server deployment
587
+ try {
588
+ const kubeconfig = new KubeConfig()
589
+ kubeconfig.loadFromString(kubeconfigPayload)
590
+
591
+ console.log('Validating game server deployment...')
592
+ const exitCode = await checkGameServerDeployment(envInfo.deployment.kubernetes_namespace, kubeconfig)
593
+ exit(exitCode)
594
+ } catch (error) {
595
+ console.error(`Failed to resolve game server deployment status: ${error}`)
596
+ exit(2)
597
+ }
598
+ } catch (error) {
599
+ if (error instanceof Error) {
600
+ console.error(`Error deploying game server into target environment: ${error.message}`)
601
+ }
602
+ exit(1)
603
+ }
604
+ })
605
+
606
+ program.command('check-server-status')
607
+ .description('check the status of a deployed server and print out information that is helpful in debugging failed deployments')
608
+ .argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
609
+ .hook('preAction', async () => {
610
+ await extendCurrentSession()
611
+ })
612
+ .action(async (gameserver: string | undefined, options) => {
613
+ const tokens = await loadTokens()
614
+ const stackApi = new StackAPI(tokens.access_token, options.stackApi)
615
+ const gameserverId = resolveGameserverId(gameserver, options)
616
+
617
+ try {
618
+ logger.debug('Get environment info')
619
+ const envInfo = await stackApi.getEnvironmentDetails(gameserverId)
620
+ const kubernetesNamespace = envInfo.deployment.kubernetes_namespace
621
+
622
+ // Load kubeconfig from file and throw error if validation fails.
623
+ logger.debug('Get kubeconfig')
624
+ const kubeconfig = new KubeConfig()
625
+ try {
626
+ // Fetch kubeconfig and write it to a temporary file
627
+ // \todo allow passing a custom kubeconfig file?
628
+ const kubeconfigPayload = await stackApi.getKubeConfig(gameserverId)
629
+ kubeconfig.loadFromString(kubeconfigPayload)
630
+ } catch (error) {
631
+ throw new Error(`Failed to load or validate kubeconfig. ${error}`)
632
+ }
633
+
634
+ // Run the checks and exit with success/failure exitCode depending on result
635
+ console.log(`Validating game server deployment in namespace ${kubernetesNamespace}`)
636
+ const exitCode = await checkGameServerDeployment(kubernetesNamespace, kubeconfig)
637
+ exit(exitCode)
638
+ } catch (error: any) {
639
+ console.error(`Failed to check deployment status: ${error.message}`)
640
+ exit(1)
641
+ }
642
+ })
643
+
331
644
  program.command('check-deployment')
332
- .description('[experimental] check that a game server was successfully deployed, or print out useful error messages in case of failure')
645
+ .description('[deprecated] check that a game server was successfully deployed, or print out useful error messages in case of failure')
333
646
  .argument('[namespace]', 'kubernetes namespace of the deployment')
334
647
  .action(async (namespace: string) => {
335
- setLogLevel(0)
648
+ console.error('DEPRECATED! Use the "metaplay-auth check-server-status [gameserver]" command instead! This command will be removed soon.')
336
649
 
337
650
  try {
338
651
  if (!namespace) {
339
652
  throw new Error('Must specify value for argument "namespace"')
340
653
  }
341
654
 
655
+ // Check that the KUBECONFIG environment variable exists
656
+ const kubeconfigPath = process.env.KUBECONFIG
657
+ if (!kubeconfigPath) {
658
+ throw new Error('The KUBECONFIG environment variable must be specified')
659
+ }
660
+
661
+ // Check that the kubeconfig file exists
662
+ if (!existsSync(kubeconfigPath)) {
663
+ throw new Error(`The environment variable KUBECONFIG points to a file '${kubeconfigPath}' that doesn't exist`)
664
+ }
665
+
666
+ // Create Kubernetes API instance (with default kubeconfig)
667
+ const kubeconfig = new KubeConfig()
668
+ // Load kubeconfig from file and throw error if validation fails.
669
+ try {
670
+ kubeconfig.loadFromFile(kubeconfigPath)
671
+ } catch (error) {
672
+ throw new Error(`Failed to load or validate kubeconfig. ${error}`)
673
+ }
674
+
342
675
  // Run the checks and exit with success/failure exitCode depending on result
343
- const exitCode = await checkGameServerDeployment(namespace)
676
+ console.log(`Validating game server deployment in namespace ${namespace}`)
677
+ const exitCode = await checkGameServerDeployment(namespace, kubeconfig)
344
678
  exit(exitCode)
345
679
  } catch (error: any) {
346
680
  console.error(`Failed to check deployment status: ${error.message}`)
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@metaplay/metaplay-auth",
3
3
  "description": "Utility CLI for authenticating with the Metaplay Auth and making authenticated calls to infrastructure endpoints.",
4
- "version": "1.3.0",
4
+ "version": "1.4.1",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
7
7
  "homepage": "https://metaplay.io",
8
- "bin": "dist/index.js",
8
+ "bin": {
9
+ "metaplay-auth": "dist/index.js"
10
+ },
9
11
  "scripts": {
10
12
  "dev": "tsx index.ts",
11
13
  "prepublish": "tsc"
@@ -16,6 +18,7 @@
16
18
  "devDependencies": {
17
19
  "@metaplay/eslint-config": "workspace:*",
18
20
  "@metaplay/typescript-config": "workspace:*",
21
+ "@types/dockerode": "^3.3.28",
19
22
  "@types/express": "^4.17.21",
20
23
  "@types/js-yaml": "^4.0.9",
21
24
  "@types/jsonwebtoken": "^9.0.5",
@@ -29,12 +32,15 @@
29
32
  "@aws-sdk/client-ecr": "^3.549.0",
30
33
  "@kubernetes/client-node": "^1.0.0-rc4",
31
34
  "@ory/client": "^1.9.0",
35
+ "@types/semver": "^7.5.8",
32
36
  "commander": "^12.0.0",
37
+ "dockerode": "^4.0.2",
33
38
  "h3": "^1.11.1",
34
39
  "js-yaml": "^4.1.0",
35
40
  "jsonwebtoken": "^9.0.2",
36
41
  "jwk-to-pem": "^2.0.5",
37
42
  "open": "^10.1.0",
43
+ "semver": "^7.6.0",
38
44
  "tslog": "^4.9.2"
39
45
  }
40
- }
46
+ }
package/src/auth.ts CHANGED
@@ -254,13 +254,24 @@ async function extendCurrentSessionWithRefreshToken (refreshToken: string): Prom
254
254
  logger.debug('Refreshing tokens...')
255
255
 
256
256
  // Send a POST request to refresh tokens
257
- const response = await fetch(tokenEndpoint, {
258
- method: 'POST',
259
- headers: {
260
- 'Content-Type': 'application/x-www-form-urlencoded',
261
- },
262
- body: params.toString(),
263
- })
257
+ let response
258
+ try {
259
+ response = await fetch(tokenEndpoint, {
260
+ method: 'POST',
261
+ headers: {
262
+ 'Content-Type': 'application/x-www-form-urlencoded',
263
+ },
264
+ body: params.toString(),
265
+ })
266
+ } catch (error: any) {
267
+ // \todo duplicate code in stackapi.ts -- refactor
268
+ logger.error(`Failed to refresh tokens via endpoint ${tokenEndpoint}`)
269
+ logger.error('Fetch error details:', error)
270
+ if (error.cause?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
271
+ throw new Error(`Failed to refresh tokens: SSL certificate validation failed for ${tokenEndpoint}. Is someone trying to tamper with your internet connection?`)
272
+ }
273
+ throw new Error(`Failed to refresh tokens via ${tokenEndpoint}: ${error}`)
274
+ }
264
275
 
265
276
  // Check if the response is OK
266
277
  if (!response.ok) {