@ossy/platform 3.22.0 → 3.23.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/platform",
3
- "version": "3.22.0",
3
+ "version": "3.23.0",
4
4
  "description": "Ossy application server runtime",
5
5
  "repository": {
6
6
  "type": "git",
@@ -49,7 +49,7 @@
49
49
  "@aws-sdk/util-format-url": "^3.972.17",
50
50
  "@modelcontextprotocol/sdk": "^1.12.1",
51
51
  "@ossy/config": "^3.0.9",
52
- "@ossy/email": "^3.22.0",
52
+ "@ossy/email": "^3.23.0",
53
53
  "@ossy/event-store": "^3.20.1",
54
54
  "@ossy/locale": "^3.4.0",
55
55
  "@ossy/manifest": "^3.20.1",
@@ -81,5 +81,5 @@
81
81
  "Dockerfile",
82
82
  "docker-healthcheck.js"
83
83
  ],
84
- "gitHead": "29de300fd5e2c36f9d6d00f09eb9f1508997a904"
84
+ "gitHead": "fba601366c69184bbd82d990429d1a43152645c9"
85
85
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The website task also has static AWS keys in its environment, and the SDK
3
+ * uses those before the task role. Inbound mail is granted to the task role,
4
+ * so the poller has to ask ECS for that role when it is running in a task.
5
+ *
6
+ * @param {NodeJS.ProcessEnv} [env]
7
+ * @param {typeof fetch} [fetchImpl]
8
+ */
9
+ export function inboundAwsClientConfig (env = process.env, fetchImpl = globalThis.fetch) {
10
+ const relative = env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
11
+ const full = env.AWS_CONTAINER_CREDENTIALS_FULL_URI
12
+ if (!relative && !full) return {}
13
+
14
+ const url = full || `http://169.254.170.2${relative}`
15
+ return {
16
+ credentials: async () => {
17
+ const headers = {}
18
+ if (env.AWS_CONTAINER_AUTHORIZATION_TOKEN) {
19
+ headers.Authorization = env.AWS_CONTAINER_AUTHORIZATION_TOKEN
20
+ }
21
+ const response = await fetchImpl(url, { headers })
22
+ if (!response.ok) {
23
+ throw new Error('Task role credentials could not be read')
24
+ }
25
+ const body = await response.json()
26
+ return {
27
+ accessKeyId: body.AccessKeyId,
28
+ secretAccessKey: body.SecretAccessKey,
29
+ sessionToken: body.Token,
30
+ expiration: body.Expiration ? new Date(body.Expiration) : undefined,
31
+ }
32
+ },
33
+ }
34
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from '@jest/globals'
2
+ import { inboundAwsClientConfig } from './inbound-aws-credentials.js'
3
+
4
+ describe('inboundAwsClientConfig', () => {
5
+ it('uses the default chain when the process is not in a task', () => {
6
+ expect(inboundAwsClientConfig({ AWS_ACCESS_KEY_ID: 'static' })).toEqual({})
7
+ })
8
+
9
+ it('reads the task role and ignores static keys', async () => {
10
+ const fetchImpl = async (url, { headers }) => {
11
+ expect(url).toBe('http://169.254.170.2/v2/credentials/task')
12
+ expect(headers.Authorization).toBe('token')
13
+ return {
14
+ ok: true,
15
+ json: async () => ({
16
+ AccessKeyId: 'task-key',
17
+ SecretAccessKey: 'task-secret',
18
+ Token: 'task-token',
19
+ Expiration: '2026-10-17T20:00:00.000Z',
20
+ }),
21
+ }
22
+ }
23
+ const config = inboundAwsClientConfig({
24
+ AWS_ACCESS_KEY_ID: 'cms-prod',
25
+ AWS_SECRET_ACCESS_KEY: 'static',
26
+ AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: '/v2/credentials/task',
27
+ AWS_CONTAINER_AUTHORIZATION_TOKEN: 'token',
28
+ }, fetchImpl)
29
+ await expect(config.credentials()).resolves.toEqual({
30
+ accessKeyId: 'task-key',
31
+ secretAccessKey: 'task-secret',
32
+ sessionToken: 'task-token',
33
+ expiration: new Date('2026-10-17T20:00:00.000Z'),
34
+ })
35
+ })
36
+ })
@@ -2,11 +2,11 @@ import { DeleteMessageCommand, ReceiveMessageCommand, SQSClient } from '@aws-sdk
2
2
  import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'
3
3
  import { fileSesReceipt, sesReceiptFromBody } from '@ossy/email/ses-inbound'
4
4
  import { createInProcessSdk } from '../tasks/in-process-sdk.js'
5
+ import { inboundAwsClientConfig } from './inbound-aws-credentials.js'
5
6
 
6
7
  const ReceiveInbound = { id: '@ossy/email/actions/receive-inbound' }
7
8
 
8
- async function readObject ({ bucket, key }) {
9
- const client = new S3Client({})
9
+ async function readObject (client, { bucket, key }) {
10
10
  const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }))
11
11
  return Buffer.from(await response.Body.transformToByteArray())
12
12
  }
@@ -39,12 +39,14 @@ export async function ackInboundMessages (messages, { file, deleteMessage, log }
39
39
  * message to one of them. Returns a stop function for shutdown.
40
40
  */
41
41
  export function startInboundMailPoller ({ queueUrl, bucket, log } = {}) {
42
- const sqs = new SQSClient({})
42
+ const clientConfig = inboundAwsClientConfig()
43
+ const sqs = new SQSClient(clientConfig)
44
+ const objects = new S3Client(clientConfig)
43
45
  const abort = new AbortController()
44
46
 
45
47
  const file = (body) => fileSesReceipt(sesReceiptFromBody(body), {
46
48
  bucket,
47
- getObject: readObject,
49
+ getObject: (location) => readObject(objects, location),
48
50
  invoke: ({ mime, workspaceId }) => createInProcessSdk()
49
51
  .forWorkspace(workspaceId)
50
52
  .invoke(ReceiveInbound, { mime, workspaceId }),