@metaplay/metaplay-auth 1.4.1 → 1.5.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/src/stackapi.ts CHANGED
@@ -1,80 +1,11 @@
1
- import { isValidFQDN, getGameserverAdminUrl } from './utils.js'
2
1
  import { logger } from './logging.js'
3
- import { dump } from 'js-yaml'
4
- import { getUserinfo } from './auth.js'
2
+ import { type EnvironmentDetails } from './targetenvironment.js'
5
3
 
6
- interface AwsCredentialsResponse {
7
- AccessKeyId: string
8
- SecretAccessKey: string
9
- SessionToken: string
10
- Expiration: string
11
- }
12
-
13
- export interface GameserverId {
14
- gameserver?: string
15
- organization?: string
16
- project?: string
17
- environment?: string
18
- }
19
-
20
- interface KubeConfig {
21
- apiVersion?: string
22
- kind: string
23
- 'current-context': string
24
- clusters: KubeConfigCluster[]
25
- contexts: KubeConfigContext[]
26
- users: KubeConfigUser[]
27
- preferences?: any
28
- }
29
-
30
- interface KubeConfigCluster {
31
- cluster: {
32
- 'certificate-authority-data': string
33
- server: string
34
- }
35
- name: string
36
- }
37
-
38
- interface KubeConfigContext {
39
- context: {
40
- cluster: string
41
- user: string
42
- namespace?: string
43
- }
44
- name: string
45
- }
46
-
47
- interface KubeConfigUser {
48
- name: string
49
- user: {
50
- token?: string
51
- exec?: {
52
- command: string
53
- args: string[]
54
- apiVersion: string
55
- }
56
- }
57
- }
58
-
59
- interface KubeExecCredential {
60
- apiVersion: string
61
- kind: string
62
- spec: {
63
- cluster?: {
64
- server: string
65
- certificateAuthorityData: string
66
- }
67
- }
68
- status: {
69
- token: string
70
- expirationTimestamp: string
71
- }
72
- }
4
+ export const defaultStackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
73
5
 
74
6
  export class StackAPI {
75
7
  private readonly accessToken: string
76
-
77
- private readonly _stackApiBaseUrl: string
8
+ private readonly stackApiBaseUrl: string
78
9
 
79
10
  constructor (accessToken: string, stackApiBaseUrl: string | undefined) {
80
11
  if (accessToken == null) {
@@ -84,262 +15,36 @@ export class StackAPI {
84
15
  this.accessToken = accessToken
85
16
 
86
17
  if (stackApiBaseUrl) {
87
- this._stackApiBaseUrl = stackApiBaseUrl.replace(/\/$/, '') // Remove trailing slash
88
- } else {
89
- this._stackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
90
- }
91
- }
92
-
93
- async getAwsCredentials (gs: GameserverId): Promise<AwsCredentialsResponse> {
94
- let url = ''
95
- if (gs.gameserver != null) {
96
- if (isValidFQDN(gs.gameserver)) {
97
- const adminUrl = getGameserverAdminUrl(gs.gameserver)
98
- url = `https://${adminUrl}/.infra/credentials/aws`
99
- } else {
100
- url = `${this._stackApiBaseUrl}/v0/credentials/${gs.gameserver}/aws`
101
- }
102
- } else if (gs.organization != null && gs.project != null && gs.environment != null) {
103
- url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/aws`
104
- } else {
105
- throw new Error('Invalid arguments for getAwsCredentials')
106
- }
107
-
108
- logger.debug(`Getting AWS credentials from ${url}...`)
109
- const response = await fetch(url, {
110
- method: 'POST',
111
- headers: {
112
- Authorization: `Bearer ${this.accessToken}`,
113
- 'Content-Type': 'application/json'
114
- }
115
- })
116
-
117
- if (response.status !== 200) {
118
- throw new Error(`Failed to fetch AWS credentials: ${response.statusText}, response code=${response.status}`)
119
- }
120
-
121
- return await response.json()
122
- }
123
-
124
- async getKubeConfig (gs: GameserverId): Promise<string> {
125
- let url = ''
126
- if (gs.gameserver != null) {
127
- if (isValidFQDN(gs.gameserver)) {
128
- const adminUrl = getGameserverAdminUrl(gs.gameserver)
129
- url = `https://${adminUrl}/.infra/credentials/k8s`
130
- } else {
131
- url = `${this._stackApiBaseUrl}/v0/credentials/${gs.gameserver}/k8s`
132
- }
133
- } else if (gs.organization != null && gs.project != null && gs.environment != null) {
134
- url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/k8s`
135
- } else {
136
- throw new Error('Invalid arguments for getKubeConfig')
137
- }
138
-
139
- logger.debug(`Getting KubeConfig from ${url}...`)
140
-
141
- let response
142
- try {
143
- response = await fetch(url, {
144
- method: 'POST',
145
- headers: {
146
- Authorization: `Bearer ${this.accessToken}`,
147
- 'Content-Type': 'application/json'
148
- }
149
- })
150
- } catch (error: any) {
151
- logger.error(`Failed to fetch kubeconfig from ${url}`)
152
- logger.error('Fetch error details:', error)
153
- if (error.cause?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
154
- throw new Error(`Failed to to fetch kubeconfig: SSL certificate validation failed for ${url}. Is someone trying to tamper with your internet connection?`)
155
- }
156
- throw new Error(`Failed to fetch kubeconfig from ${url}: ${error}`)
157
- }
158
-
159
- if (response.status !== 200) {
160
- throw new Error(`Failed to fetch KubeConfigs: ${response.statusText}`)
161
- }
162
-
163
- return await response.text()
164
- }
165
-
166
- /**
167
- * Get a `kubeconfig` payload which invokes `metaplay-auth get-kubernetes-execcredential` to get the actual
168
- * access credentials each time the kubeconfig is used.
169
- * @param gs Game server environment to get credentials for.
170
- * @returns The kubeconfig YAML.
171
- */
172
- async getKubeConfigExecCredential (gs: GameserverId): Promise<string> {
173
- let url = ''
174
- let gsSlug = ''
175
- const execArgs = ['get-kubernetes-execcredential']
176
- if (gs.gameserver != null) {
177
- const adminUrl = getGameserverAdminUrl(gs.gameserver)
178
- url = `https://${adminUrl}/.infra/credentials/k8s?type=execcredential`
179
- gsSlug = gs.gameserver
180
- execArgs.push('--gameserver', gs.gameserver)
181
- } else if (gs.organization != null && gs.project != null && gs.environment != null) {
182
- url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/k8s?type=execcredential`
183
- gsSlug = `${gs.organization}.${gs.project}.${gs.environment}`
184
- execArgs.push(`${gs.organization}-${gs.project}-${gs.environment}`)
18
+ this.stackApiBaseUrl = stackApiBaseUrl.replace(/\/$/, '') // Remove trailing slash
185
19
  } else {
186
- throw new Error('Invalid arguments for getKubeConfigExecCredential')
187
- }
188
-
189
- logger.debug(`Getting Kubernetes KubeConfig from ${url}...`)
190
-
191
- let response
192
- try {
193
- response = await fetch(url, {
194
- method: 'POST',
195
- headers: {
196
- Authorization: `Bearer ${this.accessToken}`,
197
- 'Content-Type': 'application/json'
198
- }
199
- })
200
- } catch (error: any) {
201
- logger.error(`Failed to fetch kubeconfig from ${url}`)
202
- logger.error('Fetch error details:', error)
203
- if (error.cause?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
204
- throw new Error(`Failed to to fetch kubeconfig: SSL certificate validation failed for ${url}. Is someone trying to tamper with your internet connection?`)
205
- }
206
- throw new Error(`Failed to fetch kubeconfig from ${url}: ${error}`)
20
+ this.stackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
207
21
  }
208
-
209
- if (response.status !== 200) {
210
- throw new Error(`Failed to fetch Kubernetes KubeConfig: ${response.statusText}`)
211
- }
212
-
213
- // get the execcredential and morph it into a kubeconfig which calls metaplay-auth for the token
214
- let kubeExecCredential
215
- try {
216
- kubeExecCredential = await response.json() as KubeExecCredential
217
- } catch {
218
- throw new Error('Failed to fetch Kubernetes KubeConfig: the server response is not JSON')
219
- }
220
-
221
- if (!kubeExecCredential.spec.cluster) {
222
- throw new Error('Received kubeExecCredential with missing spec.cluster')
223
- }
224
-
225
- let namespace = 'default'
226
- let user = 'user'
227
-
228
- try {
229
- const environment = await this.getEnvironmentDetails(gs)
230
- namespace = environment.deployment?.kubernetes_namespace ?? namespace
231
- } catch (e) {
232
- logger.debug('Failed to get environment details, using defaults', e)
233
- }
234
-
235
- try {
236
- const userinfo = await getUserinfo(this.accessToken)
237
- user = userinfo.sub ?? user
238
- } catch (e) {
239
- logger.debug('Failed to get userinfo, using defaults', e)
240
- }
241
-
242
- const kubeConfig: KubeConfig = {
243
- apiVersion: 'v1',
244
- kind: 'Config',
245
- 'current-context': gsSlug,
246
- clusters: [
247
- {
248
- cluster: {
249
- 'certificate-authority-data': kubeExecCredential.spec.cluster.certificateAuthorityData,
250
- server: kubeExecCredential.spec.cluster.server
251
- },
252
- name: kubeExecCredential.spec.cluster.server
253
- }
254
- ],
255
- contexts: [
256
- {
257
- context: {
258
- cluster: kubeExecCredential.spec.cluster.server,
259
- namespace,
260
- user
261
- },
262
- name: gsSlug,
263
- }
264
- ],
265
- users: [
266
- {
267
- name: user,
268
- user: {
269
- exec: {
270
- apiVersion: 'client.authentication.k8s.io/v1beta1',
271
- command: 'metaplay-auth', // todo: figure out how to refer to metaplay-auth itself
272
- args: execArgs,
273
- }
274
- }
275
- }
276
- ],
277
- }
278
-
279
- // return as yaml for easier consumption
280
- return dump(kubeConfig)
281
22
  }
282
23
 
283
- async getKubeExecCredential (gs: GameserverId): Promise<string> {
284
- let url = ''
285
- if (gs.gameserver != null) {
286
- if (isValidFQDN(gs.gameserver)) {
287
- const adminUrl = getGameserverAdminUrl(gs.gameserver)
288
- url = `https://${adminUrl}/.infra/credentials/k8s?type=execcredential`
289
- } else {
290
- url = `${this._stackApiBaseUrl}/v0/credentials/${gs.gameserver}/k8s?type=execcredential`
291
- }
292
- } else if (gs.organization != null && gs.project != null && gs.environment != null) {
293
- url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/k8s?type=execcredential`
294
- } else {
295
- throw new Error('Invalid arguments for getKubeConfig')
296
- }
297
-
298
- logger.debug(`Getting Kubernetes ExecCredential from ${url}...`)
299
-
24
+ async resolveManagedEnvironmentFQDN (organization: string, project: string, environment: string): Promise<string> {
25
+ const url = `${this.stackApiBaseUrl}/v1/servers/${organization}/${project}/${environment}`
26
+ logger.debug(`Getting environment information from ${url}...`)
300
27
  const response = await fetch(url, {
301
- method: 'POST',
28
+ method: 'GET',
302
29
  headers: {
303
30
  Authorization: `Bearer ${this.accessToken}`,
304
31
  'Content-Type': 'application/json'
305
32
  }
306
33
  })
307
34
 
308
- if (response.status !== 200) {
309
- throw new Error(`Failed to fetch Kubernetes ExecCredential: ${response.statusText}`)
310
- }
311
-
312
- return await response.text()
313
- }
314
-
315
- async getEnvironmentDetails (gs: GameserverId): Promise<any> {
316
- let url = ''
317
- if (gs.gameserver != null) {
318
- if (isValidFQDN(gs.gameserver)) {
319
- const adminUrl = getGameserverAdminUrl(gs.gameserver)
320
- url = `https://${adminUrl}/.infra/environment`
321
- } else {
322
- url = `${this._stackApiBaseUrl}/v0/deployments/${gs.gameserver}`
323
- }
324
- } else if (gs.organization != null && gs.project != null && gs.environment != null) {
325
- url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}`
326
- } else {
327
- throw new Error('Invalid arguments for environment details')
35
+ // Throw on server errors (eg, forbidden)
36
+ if (!response.ok) {
37
+ const errorData = await response.json()
38
+ throw new Error(`Failed to fetch environment details with error ${response.status}: ${JSON.stringify(errorData)}`)
328
39
  }
329
40
 
330
- logger.debug(`Getting environment details from ${url}...`)
331
- const response = await fetch(url, {
332
- method: 'GET',
333
- headers: {
334
- Authorization: `Bearer ${this.accessToken}`,
335
- 'Content-Type': 'application/json'
336
- }
337
- })
338
-
339
- if (response.status !== 200) {
340
- throw new Error(`Failed to fetch environment details: ${response.statusText}`)
41
+ // Sanity check that response is of the right structure
42
+ const envDetails = await response.json() as EnvironmentDetails
43
+ logger.debug(`Received environment details: ${JSON.stringify(envDetails)}`)
44
+ if (!envDetails.deployment) {
45
+ throw new Error(`Received invalid environment details -- missing .deployment: ${JSON.stringify(envDetails)}`)
341
46
  }
342
47
 
343
- return await response.json()
48
+ return envDetails.deployment.server_hostname
344
49
  }
345
50
  }
@@ -0,0 +1,307 @@
1
+ import { logger } from './logging.js'
2
+ import { dump } from 'js-yaml'
3
+ import { getUserinfo } from './auth.js'
4
+ import { isRunningUnderNpx } from './utils.js'
5
+ import { ECRClient, GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr'
6
+ import { defaultStackApiBaseUrl } from './stackapi.js'
7
+
8
+ interface AwsCredentialsResponse {
9
+ AccessKeyId: string
10
+ SecretAccessKey: string
11
+ SessionToken: string
12
+ Expiration: string
13
+ }
14
+
15
+ export interface DeploymentDetails {
16
+ server_hostname: string
17
+ kubernetes_namespace: string
18
+ ecr_repo: string
19
+ aws_region: string
20
+ }
21
+
22
+ export interface EnvironmentDetails {
23
+ deployment: DeploymentDetails
24
+ }
25
+
26
+ interface KubeConfig {
27
+ apiVersion?: string
28
+ kind: string
29
+ 'current-context': string
30
+ clusters: KubeConfigCluster[]
31
+ contexts: KubeConfigContext[]
32
+ users: KubeConfigUser[]
33
+ preferences?: any
34
+ }
35
+
36
+ interface KubeConfigCluster {
37
+ cluster: {
38
+ 'certificate-authority-data': string
39
+ server: string
40
+ }
41
+ name: string
42
+ }
43
+
44
+ interface KubeConfigContext {
45
+ context: {
46
+ cluster: string
47
+ user: string
48
+ namespace?: string
49
+ }
50
+ name: string
51
+ }
52
+
53
+ interface KubeConfigUser {
54
+ name: string
55
+ user: {
56
+ token?: string
57
+ exec?: {
58
+ command: string
59
+ args: string[]
60
+ apiVersion: string
61
+ }
62
+ }
63
+ }
64
+
65
+ interface KubeExecCredential {
66
+ apiVersion: string
67
+ kind: string
68
+ spec: {
69
+ cluster?: {
70
+ server: string
71
+ certificateAuthorityData: string
72
+ }
73
+ }
74
+ status: {
75
+ token: string
76
+ expirationTimestamp: string
77
+ }
78
+ }
79
+
80
+ export interface DockerCredentials {
81
+ username: string
82
+ password: string
83
+ registryUrl: string
84
+ }
85
+
86
+ export class TargetEnvironment {
87
+ private readonly accessToken: string
88
+ private readonly environmentDomain: string
89
+ private readonly stackApiBaseUrl: string
90
+ private readonly environmentApiBaseUrl: string
91
+ private readonly organization?: string
92
+ private readonly project?: string
93
+ private readonly environment?: string
94
+
95
+ constructor (accessToken: string, environmentDomain: string, stackApiBaseUrl: string, environmentApiBaseUrl: string, organization?: string, project?: string, environment?: string) {
96
+ this.accessToken = accessToken
97
+ this.environmentDomain = environmentDomain
98
+ this.stackApiBaseUrl = stackApiBaseUrl
99
+ this.environmentApiBaseUrl = environmentApiBaseUrl
100
+ this.organization = organization
101
+ this.project = project
102
+ this.environment = environment
103
+ }
104
+
105
+ async fetchJson<T> (url: string, method: string): Promise<T> {
106
+ const response = await fetch(url, {
107
+ method,
108
+ headers: {
109
+ Authorization: `Bearer ${this.accessToken}`,
110
+ 'Content-Type': 'application/json'
111
+ }
112
+ })
113
+
114
+ if (response.status !== 200) {
115
+ throw new Error(`Failed to fetch ${method} ${url}: ${response.statusText}, response code=${response.status}`)
116
+ }
117
+
118
+ return await response.json() as T
119
+ }
120
+
121
+ async fetchText (url: string, method: string): Promise<string> {
122
+ let response
123
+ try {
124
+ response = await fetch(url, {
125
+ method,
126
+ headers: {
127
+ Authorization: `Bearer ${this.accessToken}`,
128
+ 'Content-Type': 'application/json'
129
+ }
130
+ })
131
+ } catch (error: any) {
132
+ logger.error(`Failed to fetch ${method} ${url}:`, error)
133
+ if (error.cause?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
134
+ throw new Error(`Failed to to fetch ${url}: SSL certificate validation failed. Is someone trying to tamper with your internet connection?`)
135
+ }
136
+ throw new Error(`Failed to fetch ${url}: ${error}`)
137
+ }
138
+
139
+ if (response.status !== 200) {
140
+ throw new Error(`Failed to fetch ${method} ${url}: ${response.statusText}`)
141
+ }
142
+
143
+ return await response.text()
144
+ }
145
+
146
+ async getAwsCredentials (): Promise<AwsCredentialsResponse> {
147
+ const url = `${this.environmentApiBaseUrl}/credentials/aws`
148
+ logger.debug(`Fetching AWS credentials from ${url}...`)
149
+ return await this.fetchJson<AwsCredentialsResponse>(url, 'POST')
150
+ }
151
+
152
+ async getKubeConfig (): Promise<string> {
153
+ const url = `${this.environmentApiBaseUrl}/credentials/k8s`
154
+ logger.debug(`Getting KubeConfig from ${url}...`)
155
+ return await this.fetchText(url, 'POST')
156
+ }
157
+
158
+ /**
159
+ * Get a `kubeconfig` payload which invokes `metaplay-auth get-kubernetes-execcredential` to get the actual
160
+ * access credentials each time the kubeconfig is used.
161
+ * @returns The kubeconfig YAML.
162
+ */
163
+ async getKubeConfigExecCredential (): Promise<string> {
164
+ const url = `${this.environmentApiBaseUrl}/credentials/k8s?type=execcredential`
165
+ logger.debug(`Getting Kubernetes KubeConfig from ${url}...`)
166
+
167
+ // get the execcredential and morph it into a kubeconfig which calls metaplay-auth for the token
168
+ let kubeExecCredential: KubeExecCredential
169
+ try {
170
+ kubeExecCredential = await this.fetchJson<KubeExecCredential>(url, 'POST')
171
+ } catch {
172
+ throw new Error('Failed to fetch Kubernetes KubeConfig')
173
+ }
174
+
175
+ if (!kubeExecCredential.spec.cluster) {
176
+ throw new Error('Received kubeExecCredential with missing spec.cluster')
177
+ }
178
+
179
+ // Fetch environment namespace
180
+ const environment = await this.getEnvironmentDetails()
181
+ const namespace = environment.deployment?.kubernetes_namespace
182
+ if (!namespace) {
183
+ throw new Error('Environment details did not contain a valid Kubernetes namespace')
184
+ }
185
+
186
+ // Fetch user id
187
+ const userinfo = await getUserinfo(this.accessToken)
188
+ const userId = userinfo.email
189
+
190
+ // Resolve the environment identity (prefer org-proj-env or fall back to FQDN)
191
+ const envId = this.organization ? `${this.organization}-${this.project}-${this.environment}` : this.environmentDomain
192
+
193
+ // Resolve invocation for getting the kubeconfig exec credential
194
+ let execCmd = 'metaplay-auth'
195
+ let execArgs =
196
+ ['get-kubernetes-execcredential']
197
+ .concat(this.stackApiBaseUrl !== defaultStackApiBaseUrl ? ['--stack-api', this.stackApiBaseUrl] : [])
198
+ .concat([envId])
199
+
200
+ // If running under npx, use npx also for getting the kube exec credential
201
+ if (isRunningUnderNpx()) {
202
+ execCmd = 'npx'
203
+ execArgs = [
204
+ '--yes', // For NPX to silently install or update
205
+ '@metaplay/metaplay-auth@latest',
206
+ ...execArgs
207
+ ]
208
+ }
209
+
210
+ const kubeConfig: KubeConfig = {
211
+ apiVersion: 'v1',
212
+ kind: 'Config',
213
+ 'current-context': envId,
214
+ clusters: [
215
+ {
216
+ cluster: {
217
+ 'certificate-authority-data': kubeExecCredential.spec.cluster.certificateAuthorityData,
218
+ server: kubeExecCredential.spec.cluster.server
219
+ },
220
+ name: kubeExecCredential.spec.cluster.server
221
+ }
222
+ ],
223
+ contexts: [
224
+ {
225
+ context: {
226
+ cluster: kubeExecCredential.spec.cluster.server,
227
+ namespace,
228
+ user: userId
229
+ },
230
+ name: envId,
231
+ }
232
+ ],
233
+ users: [
234
+ {
235
+ name: userId,
236
+ user: {
237
+ exec: {
238
+ apiVersion: 'client.authentication.k8s.io/v1beta1',
239
+ command: execCmd,
240
+ args: execArgs
241
+ }
242
+ }
243
+ }
244
+ ],
245
+ }
246
+
247
+ // return as yaml for easier consumption
248
+ return dump(kubeConfig)
249
+ }
250
+
251
+ async getKubeExecCredential (): Promise<string> {
252
+ const url = `${this.environmentApiBaseUrl}/credentials/k8s?type=execcredential`
253
+ logger.debug(`Getting Kubernetes ExecCredential from ${url}...`)
254
+ return await this.fetchText(url, 'POST')
255
+ }
256
+
257
+ async getEnvironmentDetails (): Promise<EnvironmentDetails> {
258
+ // \note If invoking StackAPI via the https://<environment>/.infra, we need to add '/environment' to the path
259
+ const urlSuffix = this.environmentApiBaseUrl.endsWith('.infra') ? '/environment' : ''
260
+ const url = this.environmentApiBaseUrl + urlSuffix
261
+ logger.debug(`Getting environment details from ${url}...`)
262
+ return await this.fetchJson<EnvironmentDetails>(url, 'GET')
263
+ }
264
+
265
+ async getDockerCredentials (): Promise<DockerCredentials> {
266
+ // Get environment info (for AWS region)
267
+ logger.debug('Get environment info')
268
+ const envInfo = await this.getEnvironmentDetails()
269
+
270
+ // Fetch AWS credentials from Metaplay cloud
271
+ logger.debug('Get AWS credentials from Metaplay')
272
+ const awsCredentials = await this.getAwsCredentials()
273
+
274
+ // Create ECR client with credentials
275
+ logger.debug('Create ECR client')
276
+ const client = new ECRClient({
277
+ credentials: {
278
+ accessKeyId: awsCredentials.AccessKeyId,
279
+ secretAccessKey: awsCredentials.SecretAccessKey,
280
+ sessionToken: awsCredentials.SessionToken
281
+ },
282
+ region: envInfo.deployment.aws_region
283
+ })
284
+
285
+ // Fetch the ECR docker authentication token
286
+ logger.debug('Fetch ECR login credentials from AWS')
287
+ const command = new GetAuthorizationTokenCommand({})
288
+ const response = await client.send(command)
289
+ if (!response.authorizationData || response.authorizationData.length === 0 || !response.authorizationData[0].authorizationToken || !response.authorizationData[0].proxyEndpoint) {
290
+ throw new Error('Received an empty authorization token response for ECR repository')
291
+ }
292
+
293
+ // Parse username and password from the response (separated by a ':')
294
+ logger.debug('Parse ECR response')
295
+ const registryUrl = response.authorizationData[0].proxyEndpoint
296
+ const authorization64 = response.authorizationData[0].authorizationToken
297
+ const authorization = Buffer.from(authorization64, 'base64').toString()
298
+ const [username, password] = authorization.split(':')
299
+ logger.debug(`ECR: username=${username}, proxyEndpoint=${registryUrl}`)
300
+
301
+ return {
302
+ username,
303
+ password,
304
+ registryUrl
305
+ } satisfies DockerCredentials
306
+ }
307
+ }