@ossy/platform 1.32.0 → 1.34.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 +10 -6
- package/src/actions/action.service.js +73 -0
- package/src/config.service.js +48 -0
- package/src/index.js +2 -0
- package/src/server.js +49 -0
- package/src/storage/local-storage.client.js +65 -0
- package/src/storage/s3.client.js +100 -0
- package/src/storage/storage.client.js +12 -0
- package/src/email.integration.js +0 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/platform",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.0",
|
|
4
4
|
"description": "Ossy application server runtime",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,11 +29,15 @@
|
|
|
29
29
|
"author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@aws-sdk/client-s3": "^3.1057.0",
|
|
32
33
|
"@aws-sdk/client-ses": "^3.0.0",
|
|
33
|
-
"@
|
|
34
|
-
"@
|
|
35
|
-
"@
|
|
36
|
-
"@ossy/
|
|
34
|
+
"@aws-sdk/s3-request-presigner": "^3.1057.0",
|
|
35
|
+
"@aws-sdk/util-create-request": "^3.972.26",
|
|
36
|
+
"@aws-sdk/util-format-url": "^3.972.17",
|
|
37
|
+
"@ossy/event-store": "^1.3.0",
|
|
38
|
+
"@ossy/observability": "^1.3.0",
|
|
39
|
+
"@ossy/router": "^1.35.0",
|
|
40
|
+
"@ossy/sdk": "^1.35.0",
|
|
37
41
|
"cookie-parser": "^1.4.7",
|
|
38
42
|
"dotenv": ">=16.0.0 <18.0.0",
|
|
39
43
|
"express": ">=5.0.0 <6.0.0",
|
|
@@ -44,5 +48,5 @@
|
|
|
44
48
|
"src",
|
|
45
49
|
"Dockerfile"
|
|
46
50
|
],
|
|
47
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "c6697078268867d88553ca0bac08faad5cea1546"
|
|
48
52
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createLogger } from '@ossy/observability'
|
|
2
|
+
|
|
3
|
+
const log = createLogger('platform/actions')
|
|
4
|
+
|
|
5
|
+
/** @type {Map<string, { id: string, access: string, run: Function }>} */
|
|
6
|
+
const _actions = new Map()
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Registry and invoker for `*.action.js` command handlers.
|
|
10
|
+
*
|
|
11
|
+
* Actions are named, discoverable functions exposed at `POST /actions/:id`.
|
|
12
|
+
* Each action exports:
|
|
13
|
+
* - `id` {string} — unique slug, e.g. `'authentication/request-sign-in'`
|
|
14
|
+
* - `access` {string} — `'public'` | `'authenticated'` | `'workspace'` (default `'authenticated'`)
|
|
15
|
+
* - `run` {Function} — async handler receiving `{ payload, sdk, log, integrations, req }`
|
|
16
|
+
*/
|
|
17
|
+
export const ActionService = {
|
|
18
|
+
/**
|
|
19
|
+
* Register an action module (as imported from a bundled entry).
|
|
20
|
+
* Validates that `id` is a non-empty string and `run` is a function.
|
|
21
|
+
*
|
|
22
|
+
* @param {{ id: string, access?: string, run: Function }} mod
|
|
23
|
+
*/
|
|
24
|
+
register (mod) {
|
|
25
|
+
const { id, run, access = 'authenticated' } = mod ?? {}
|
|
26
|
+
|
|
27
|
+
if (typeof id !== 'string' || id.trim() === '') {
|
|
28
|
+
throw new Error(`[ActionService] Action module must export a non-empty string "id" (got ${JSON.stringify(id)})`)
|
|
29
|
+
}
|
|
30
|
+
if (typeof run !== 'function') {
|
|
31
|
+
throw new Error(`[ActionService] Action "${id}" must export a "run" function`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (_actions.has(id)) {
|
|
35
|
+
log.warn(`[ActionService] Action "${id}" already registered — overwriting`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_actions.set(id, { id, access, run })
|
|
39
|
+
log.info(`[ActionService] Registered action "${id}" (access: ${access})`)
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Look up a registered action by id. Returns `null` when not found.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} id
|
|
46
|
+
* @returns {{ id: string, access: string, run: Function } | null}
|
|
47
|
+
*/
|
|
48
|
+
get (id) {
|
|
49
|
+
return _actions.get(id) ?? null
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Returns all registered actions.
|
|
54
|
+
*
|
|
55
|
+
* @returns {{ id: string, access: string, run: Function }[]}
|
|
56
|
+
*/
|
|
57
|
+
all () {
|
|
58
|
+
return [..._actions.values()]
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Invoke an action by id, forwarding the provided context.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} id
|
|
65
|
+
* @param {{ payload?: unknown, sdk?: unknown, log?: unknown, integrations?: unknown, req?: unknown }} context
|
|
66
|
+
* @returns {Promise<unknown>}
|
|
67
|
+
*/
|
|
68
|
+
async invoke (id, context = {}) {
|
|
69
|
+
const action = _actions.get(id)
|
|
70
|
+
if (!action) throw new Error(`[ActionService] Action not found: "${id}"`)
|
|
71
|
+
return action.run(context)
|
|
72
|
+
},
|
|
73
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Configuration
|
|
3
|
+
* @class
|
|
4
|
+
*/
|
|
5
|
+
export class ConfigService {
|
|
6
|
+
|
|
7
|
+
static Port = process.env.BE_PORT || 3000
|
|
8
|
+
static Domain = process.env.DOMAIN || `localhost:${ConfigService.Port}`
|
|
9
|
+
static WebClientDomain = process.env.WEB_CLIENT_DOMAIN || `http://localhost:3000`
|
|
10
|
+
static MongoUrl = process.env.DB_URL || 'mongodb://mongodb:27017/'
|
|
11
|
+
static DbName = process.env.DB_NAME || 'test'
|
|
12
|
+
static TokenSecret = process.env.TOKEN_SECRET || 'testsecret2'
|
|
13
|
+
static TokenValidity = process.env.TOKEN_VALIDITY || 60 * 60 * 24 * 14
|
|
14
|
+
static LimitedAccessCode = process.env.LIMITED_ACCESS_CODE || 'test'
|
|
15
|
+
static BuildEnvironment = process.env.BUILD_ENVIRONMENT
|
|
16
|
+
static BotUserEmail = process.env.BOT_USER_EMAIL || 'ossybot@ossy.se'
|
|
17
|
+
static BotUserId = process.env.BOT_USER_ID || 'Mil5qAL7jDFCTyuKD_BKb'
|
|
18
|
+
/** Set on `req.userId` when there is no valid session; role checks can treat this as unauthenticated. */
|
|
19
|
+
static AnonymousUserId = process.env.ANONYMOUS_USER_ID || 'anonymous'
|
|
20
|
+
static MediaRepository = process.env.MEDIA_REPOSITORY
|
|
21
|
+
static MediaCdnDomainName = process.env.MEDIA_CDN_DOMAIN_NAME
|
|
22
|
+
static awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID
|
|
23
|
+
static awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY
|
|
24
|
+
static SesRegion = process.env.SES_REGION || 'eu-north-1'
|
|
25
|
+
static Debug = process.env.DEBUG
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Derives the web client base URL from the incoming request's Origin or Referer
|
|
29
|
+
* header so that email links point back to whichever domain the user came from
|
|
30
|
+
* (e.g. http://ossy.local, http://localhost:3002). Falls back to the
|
|
31
|
+
* WEB_CLIENT_DOMAIN env var / default when no header is present.
|
|
32
|
+
*/
|
|
33
|
+
static getWebClientBaseUrl(req) {
|
|
34
|
+
const origin = req?.headers?.origin
|
|
35
|
+
if (origin && origin !== 'null') return origin
|
|
36
|
+
|
|
37
|
+
const referer = req?.headers?.referer
|
|
38
|
+
if (referer) {
|
|
39
|
+
try {
|
|
40
|
+
const { origin: refOrigin } = new URL(referer)
|
|
41
|
+
if (refOrigin && refOrigin !== 'null') return refOrigin
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return ConfigService.WebClientDomain
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
}
|
package/src/index.js
CHANGED
|
@@ -5,3 +5,5 @@ export { registerResourceTemplate, getSystemResourceTemplates } from './resource
|
|
|
5
5
|
export { normalizeAndValidateDocumentContent, validateResourceTemplatesForImport, ALLOWED_FIELD_TYPES } from './resources/resource-template.validation.js'
|
|
6
6
|
export { Definition } from './Definition.js'
|
|
7
7
|
export { IntegrationService } from './integration.service.js'
|
|
8
|
+
export { ConfigService } from './config.service.js'
|
|
9
|
+
export { ActionService } from './actions/action.service.js'
|
package/src/server.js
CHANGED
|
@@ -12,6 +12,7 @@ import { TaskService } from './tasks/task-service.js'
|
|
|
12
12
|
import { ChangeStream } from './tasks/change-stream.js'
|
|
13
13
|
import { registerResourceTemplate } from './resources/resource-template.registry.js'
|
|
14
14
|
import { IntegrationService } from './integration.service.js'
|
|
15
|
+
import { ActionService } from './actions/action.service.js'
|
|
15
16
|
import { createLogger } from '@ossy/observability'
|
|
16
17
|
|
|
17
18
|
const log = createLogger('@ossy/platform')
|
|
@@ -56,6 +57,7 @@ export function loadManifest (buildDir) {
|
|
|
56
57
|
const aggregates = Array.isArray(manifest.aggregates) ? manifest.aggregates : []
|
|
57
58
|
const integrations = Array.isArray(manifest.integrations) ? manifest.integrations : []
|
|
58
59
|
const startups = Array.isArray(manifest.startups) ? manifest.startups : []
|
|
60
|
+
const actions = Array.isArray(manifest.actions) ? manifest.actions : []
|
|
59
61
|
for (const e of entries) {
|
|
60
62
|
if ((e.type === 'page' || e.type === 'api') && !validRoutable(e)) {
|
|
61
63
|
log.warn(`Skipping ${e.type} "${e.id}" — manifest entry has no path.`)
|
|
@@ -71,6 +73,7 @@ export function loadManifest (buildDir) {
|
|
|
71
73
|
aggregates,
|
|
72
74
|
integrations,
|
|
73
75
|
startups,
|
|
76
|
+
actions,
|
|
74
77
|
config: manifest.config || {},
|
|
75
78
|
}
|
|
76
79
|
}
|
|
@@ -159,6 +162,17 @@ export async function startServer (options = {}) {
|
|
|
159
162
|
}
|
|
160
163
|
}
|
|
161
164
|
|
|
165
|
+
// Action registration — import each bundled action module and register it
|
|
166
|
+
// with ActionService so it can be looked up and invoked at runtime.
|
|
167
|
+
for (const actionEntry of manifest.actions ?? []) {
|
|
168
|
+
try {
|
|
169
|
+
const mod = await import(resolveEntryUrl(actionEntry.entry, buildDir))
|
|
170
|
+
ActionService.register(mod)
|
|
171
|
+
} catch (err) {
|
|
172
|
+
log.error(`Failed to load action "${actionEntry.id}"`, undefined, err)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
162
176
|
// Register the SDK so all tasks receive it as `sdk`.
|
|
163
177
|
// Priority: explicit options.sdk → SDK.of() from env vars → null (direct-DB fallback in tasks).
|
|
164
178
|
const botSdk = (process.env.API_URL && process.env.OSSY_API_KEY)
|
|
@@ -210,6 +224,36 @@ export async function startServer (options = {}) {
|
|
|
210
224
|
if (fs.existsSync(publicDir)) app.use(express.static(publicDir))
|
|
211
225
|
app.use(ProxyInternal())
|
|
212
226
|
|
|
227
|
+
// Actions endpoint — auto-exposes every registered `*.action.js` handler.
|
|
228
|
+
// Access rules are enforced here; business logic lives in the action's `run`.
|
|
229
|
+
app.post('/actions/:id', async (req, res) => {
|
|
230
|
+
const actionId = req.params.id
|
|
231
|
+
const action = ActionService.get(actionId)
|
|
232
|
+
if (!action) return res.status(404).json({ error: 'Action not found' })
|
|
233
|
+
|
|
234
|
+
if (action.access === 'authenticated' && (!req.userId || req.userId === 'anonymous')) {
|
|
235
|
+
return res.status(401).json({ error: 'Unauthorized' })
|
|
236
|
+
}
|
|
237
|
+
if (action.access === 'workspace' && !req.workspaceId) {
|
|
238
|
+
return res.status(403).json({ error: 'Forbidden' })
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const actionLog = createLogger(actionId)
|
|
242
|
+
try {
|
|
243
|
+
const result = await ActionService.invoke(actionId, {
|
|
244
|
+
payload: req.body,
|
|
245
|
+
sdk: req.sdk ?? null,
|
|
246
|
+
log: actionLog,
|
|
247
|
+
integrations: IntegrationService,
|
|
248
|
+
req,
|
|
249
|
+
})
|
|
250
|
+
res.json(result ?? { ok: true })
|
|
251
|
+
} catch (err) {
|
|
252
|
+
actionLog.error('Action failed', { id: actionId }, err)
|
|
253
|
+
res.status(500).json({ error: err && err.message ? err.message : 'Internal error' })
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
|
|
213
257
|
app.all('*all', async (req, res) => {
|
|
214
258
|
const requestUrl = req.originalUrl || '/'
|
|
215
259
|
try {
|
|
@@ -314,3 +358,8 @@ async function loadMiddleware (buildDir) {
|
|
|
314
358
|
}
|
|
315
359
|
|
|
316
360
|
export default startServer
|
|
361
|
+
export { ConfigService } from './config.service.js'
|
|
362
|
+
export { ActionService } from './actions/action.service.js'
|
|
363
|
+
export { StorageClient } from './storage/storage.client.js'
|
|
364
|
+
export { S3Client } from './storage/s3.client.js'
|
|
365
|
+
export { LocalStorageClient } from './storage/local-storage.client.js'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import os from 'os'
|
|
3
|
+
import { mkdir, writeFile, readFile, stat } from 'fs/promises'
|
|
4
|
+
import { existsSync } from 'fs'
|
|
5
|
+
|
|
6
|
+
const LOCAL_STORAGE_DIR = path.join(os.tmpdir(), 'ossy-local-media')
|
|
7
|
+
|
|
8
|
+
function normalizeKey(keyOrShape) {
|
|
9
|
+
const Key = typeof keyOrShape === 'string' ? keyOrShape : keyOrShape?.Key
|
|
10
|
+
if (!Key || typeof Key !== 'string') {
|
|
11
|
+
throw new Error('[LocalStorageClient] missing Key')
|
|
12
|
+
}
|
|
13
|
+
return Key
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function keyToLocalPath(key) {
|
|
17
|
+
const resolved = path.resolve(LOCAL_STORAGE_DIR, key)
|
|
18
|
+
if (!resolved.startsWith(LOCAL_STORAGE_DIR)) {
|
|
19
|
+
throw new Error('[LocalStorageClient] invalid key: path traversal detected')
|
|
20
|
+
}
|
|
21
|
+
return resolved
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildUrl(key) {
|
|
25
|
+
const base = (process.env.LOCAL_API_URL || 'http://localhost:3001').replace(/\/$/, '')
|
|
26
|
+
return `${base}/local-storage?key=${encodeURIComponent(key)}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class LocalStorageClient {
|
|
30
|
+
|
|
31
|
+
static createUploadUrl({ Key }) {
|
|
32
|
+
return Promise.resolve(buildUrl(Key))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static createPresignedDownloadUrl(keyOrShape) {
|
|
36
|
+
const Key = normalizeKey(keyOrShape)
|
|
37
|
+
return Promise.resolve(buildUrl(Key))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static createDownloadUrl(keyOrShape) {
|
|
41
|
+
const Key = normalizeKey(keyOrShape)
|
|
42
|
+
return buildUrl(Key)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static async save(key, body) {
|
|
46
|
+
const localPath = keyToLocalPath(key)
|
|
47
|
+
await mkdir(path.dirname(localPath), { recursive: true })
|
|
48
|
+
await writeFile(localPath, body)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static async load(key) {
|
|
52
|
+
const localPath = keyToLocalPath(key)
|
|
53
|
+
if (!existsSync(localPath)) return { buffer: null, exists: false }
|
|
54
|
+
const buffer = await readFile(localPath)
|
|
55
|
+
return { buffer, exists: true }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static async headObject({ Key }) {
|
|
59
|
+
const localPath = keyToLocalPath(Key)
|
|
60
|
+
const s = await stat(localPath).catch(() => null)
|
|
61
|
+
if (!s) throw Object.assign(new Error('Not Found'), { $metadata: { httpStatusCode: 404 } })
|
|
62
|
+
return { ContentLength: s.size }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { S3, PutObjectCommand, GetObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3'
|
|
2
|
+
import { S3RequestPresigner } from '@aws-sdk/s3-request-presigner'
|
|
3
|
+
import { createRequest } from '@aws-sdk/util-create-request'
|
|
4
|
+
import { formatUrl } from '@aws-sdk/util-format-url'
|
|
5
|
+
import { ConfigService } from '../config.service.js'
|
|
6
|
+
import { createLogger } from '@ossy/observability'
|
|
7
|
+
|
|
8
|
+
const log = createLogger('storage')
|
|
9
|
+
|
|
10
|
+
const REGION = 'eu-north-1'
|
|
11
|
+
|
|
12
|
+
function createS3Client() {
|
|
13
|
+
return new S3({
|
|
14
|
+
region: REGION,
|
|
15
|
+
credentials: {
|
|
16
|
+
accessKeyId: ConfigService.awsAccessKeyId,
|
|
17
|
+
secretAccessKey: ConfigService.awsSecretAccessKey
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeKey(keyOrShape) {
|
|
23
|
+
const Key = typeof keyOrShape === 'string' ? keyOrShape : keyOrShape?.Key
|
|
24
|
+
if (!Key || typeof Key !== 'string') {
|
|
25
|
+
throw new Error('[S3Client] missing Key')
|
|
26
|
+
}
|
|
27
|
+
return Key
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class S3Client {
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Public CDN URL (CloudFront) for objects under `media/` — use only when `access === 'public'`.
|
|
34
|
+
*/
|
|
35
|
+
static createDownloadUrl(keyOrShape) {
|
|
36
|
+
const Key = normalizeKey(keyOrShape)
|
|
37
|
+
const pathAfterMedia = Key.includes('media/') ? Key.split('media/')[1] : Key
|
|
38
|
+
const base = ConfigService.MediaCdnDomainName || ''
|
|
39
|
+
return `${base.replace(/\/$/, '')}/${pathAfterMedia}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Short-lived presigned GET for private bucket reads (restricted resources, workspace members).
|
|
44
|
+
*/
|
|
45
|
+
static createPresignedDownloadUrl(keyOrShape, { expiresIn = 15 * 60 } = {}) {
|
|
46
|
+
const Key = normalizeKey(keyOrShape)
|
|
47
|
+
const Bucket = ConfigService.MediaRepository
|
|
48
|
+
if (!Bucket) {
|
|
49
|
+
return Promise.reject(new Error('[S3Client] MEDIA_REPOSITORY is not configured'))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const s3Client = createS3Client()
|
|
53
|
+
const signedRequest = new S3RequestPresigner(s3Client.config)
|
|
54
|
+
|
|
55
|
+
return createRequest(s3Client, new GetObjectCommand({ Bucket, Key }))
|
|
56
|
+
.then(request => signedRequest.presign(request, { expiresIn }))
|
|
57
|
+
.then(formatUrl)
|
|
58
|
+
.catch(error => {
|
|
59
|
+
log.error('S3Client.createPresignedDownloadUrl error', undefined, error)
|
|
60
|
+
return Promise.reject(error)
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static createUploadUrl({ ContentType, ContentLength, Key, Bucket, expiresIn = 60 * 60 * 24 }) {
|
|
65
|
+
log.info('[S3Client][createUploadUrl()] Creating upload URL')
|
|
66
|
+
|
|
67
|
+
const bucket = Bucket ?? ConfigService.MediaRepository
|
|
68
|
+
if (!bucket) {
|
|
69
|
+
return Promise.reject(new Error('[S3Client] Bucket is not configured'))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const clientParams = {
|
|
73
|
+
Bucket: bucket,
|
|
74
|
+
Key,
|
|
75
|
+
ContentType,
|
|
76
|
+
ContentLength
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const s3Client = createS3Client()
|
|
80
|
+
const signedRequest = new S3RequestPresigner(s3Client.config)
|
|
81
|
+
|
|
82
|
+
return createRequest(s3Client, new PutObjectCommand(clientParams))
|
|
83
|
+
.then(request => signedRequest.presign(request, { expiresIn }))
|
|
84
|
+
.then(formatUrl)
|
|
85
|
+
.catch(error => {
|
|
86
|
+
log.error('S3Client.createUploadUrl error', undefined, error)
|
|
87
|
+
return Promise.reject(error)
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static headObject({ Key, Bucket }) {
|
|
92
|
+
const bucket = Bucket ?? ConfigService.MediaRepository
|
|
93
|
+
if (!bucket) {
|
|
94
|
+
return Promise.reject(new Error('[S3Client] Bucket is not configured'))
|
|
95
|
+
}
|
|
96
|
+
const s3Client = createS3Client()
|
|
97
|
+
return s3Client.send(new HeadObjectCommand({ Bucket: bucket, Key }))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { S3Client } from './s3.client.js'
|
|
2
|
+
import { LocalStorageClient } from './local-storage.client.js'
|
|
3
|
+
import { ConfigService } from '../config.service.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Active storage backend.
|
|
7
|
+
*
|
|
8
|
+
* - When `MEDIA_REPOSITORY` is set (production): delegates to S3.
|
|
9
|
+
* - When `MEDIA_REPOSITORY` is unset (local dev): delegates to LocalStorageClient,
|
|
10
|
+
* which stores files in /tmp/ossy-local-media/ and serves them via GET /local-storage.
|
|
11
|
+
*/
|
|
12
|
+
export const StorageClient = ConfigService.MediaRepository ? S3Client : LocalStorageClient
|
package/src/email.integration.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { SESClient } from '@aws-sdk/client-ses'
|
|
2
|
-
|
|
3
|
-
export const id = 'email'
|
|
4
|
-
|
|
5
|
-
export const credentials = [
|
|
6
|
-
'SES_REGION',
|
|
7
|
-
'SES_ACCESS_KEY_ID',
|
|
8
|
-
'SES_SECRET_ACCESS_KEY',
|
|
9
|
-
]
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Returns an AWS SES client configured from env vars.
|
|
13
|
-
* Tasks receive this via `integrations.get('email')`.
|
|
14
|
-
*
|
|
15
|
-
* @param {{ env: NodeJS.ProcessEnv }} opts
|
|
16
|
-
* @returns {SESClient}
|
|
17
|
-
*/
|
|
18
|
-
export async function connect ({ env }) {
|
|
19
|
-
return new SESClient({
|
|
20
|
-
region: env.SES_REGION,
|
|
21
|
-
credentials: {
|
|
22
|
-
accessKeyId: env.SES_ACCESS_KEY_ID,
|
|
23
|
-
secretAccessKey: env.SES_SECRET_ACCESS_KEY,
|
|
24
|
-
},
|
|
25
|
-
})
|
|
26
|
-
}
|