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