chore: Inject sandbox handle in cloud config (#13879)

* chore: Inject sandbox handle in cloud config

* Create wet-seas-lie.md

* chore: rename medusaCloudOptions to cloud

* fix tests
This commit is contained in:
Oli Juhl
2025-10-29 10:02:37 +01:00
committed by GitHub
parent 85b1f3d43a
commit 1defb3c29b
7 changed files with 248 additions and 22 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/notification": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---
chore: Inject sandbox handle in cloud config

View File

@@ -231,6 +231,10 @@ export type MedusaCloudOptions = {
* The endpoint of the Medusa Cloud email service.
*/
emailsEndpoint?: string
/**
* The sandbox handle of the Medusa Cloud sandbox.
*/
sandboxHandle?: string
}
/**
@@ -890,7 +894,7 @@ export type ProjectConfigOptions = {
* This property holds configurations for running in Medusa Cloud.
* It gets automatically populated in the cloud, and is not needed outside of it.
*/
medusaCloudOptions?: MedusaCloudOptions
cloud?: MedusaCloudOptions
}
/**

View File

@@ -2091,6 +2091,7 @@ describe("defineConfig", function () {
"api_key": "test-api-key",
"endpoint": "test-emails-endpoint",
"environment_handle": "test-environment",
"sandbox_handle": undefined,
},
"providers": [
{
@@ -2160,6 +2161,12 @@ describe("defineConfig", function () {
},
],
"projectConfig": {
"cloud": {
"apiKey": "test-api-key",
"emailsEndpoint": "test-emails-endpoint",
"environmentHandle": "test-environment",
"sandboxHandle": undefined,
},
"databaseUrl": "postgres://localhost/medusa-starter-default",
"http": {
"adminCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
@@ -2176,10 +2183,186 @@ describe("defineConfig", function () {
},
"storeCors": "http://localhost:8000",
},
"medusaCloudOptions": {
"redisOptions": {
"retryStrategy": [Function],
},
"sessionOptions": {},
},
}
`)
})
it("should add cloud options to the project config and relevant modules if the environment varianbles is set for a sandbox", function () {
const originalEnv = { ...process.env }
process.env.MEDUSA_CLOUD_SANDBOX_HANDLE = "test-sandbox"
process.env.MEDUSA_CLOUD_API_KEY = "test-api-key"
process.env.MEDUSA_CLOUD_EMAILS_ENDPOINT = "test-emails-endpoint"
const config = defineConfig()
process.env = { ...originalEnv }
expect(config).toMatchInlineSnapshot(`
{
"admin": {
"backendUrl": "/",
"path": "/app",
},
"featureFlags": {},
"logger": undefined,
"modules": {
"api_key": {
"resolve": "@medusajs/medusa/api-key",
},
"auth": {
"options": {
"providers": [
{
"id": "emailpass",
"resolve": "@medusajs/medusa/auth-emailpass",
},
],
},
"resolve": "@medusajs/medusa/auth",
},
"cache": {
"resolve": "@medusajs/medusa/cache-inmemory",
},
"cart": {
"resolve": "@medusajs/medusa/cart",
},
"currency": {
"resolve": "@medusajs/medusa/currency",
},
"customer": {
"resolve": "@medusajs/medusa/customer",
},
"event_bus": {
"resolve": "@medusajs/medusa/event-bus-local",
},
"file": {
"options": {
"providers": [
{
"id": "local",
"resolve": "@medusajs/medusa/file-local",
},
],
},
"resolve": "@medusajs/medusa/file",
},
"fulfillment": {
"options": {
"providers": [
{
"id": "manual",
"resolve": "@medusajs/medusa/fulfillment-manual",
},
],
},
"resolve": "@medusajs/medusa/fulfillment",
},
"inventory": {
"resolve": "@medusajs/medusa/inventory",
},
"locking": {
"resolve": "@medusajs/medusa/locking",
},
"notification": {
"options": {
"cloud": {
"api_key": "test-api-key",
"endpoint": "test-emails-endpoint",
"environment_handle": undefined,
"sandbox_handle": "test-sandbox",
},
"providers": [
{
"id": "local",
"options": {
"channels": [
"feed",
],
"name": "Local Notification Provider",
},
"resolve": "@medusajs/medusa/notification-local",
},
],
},
"resolve": "@medusajs/medusa/notification",
},
"order": {
"resolve": "@medusajs/medusa/order",
},
"payment": {
"resolve": "@medusajs/medusa/payment",
},
"pricing": {
"resolve": "@medusajs/medusa/pricing",
},
"product": {
"resolve": "@medusajs/medusa/product",
},
"promotion": {
"resolve": "@medusajs/medusa/promotion",
},
"region": {
"resolve": "@medusajs/medusa/region",
},
"sales_channel": {
"resolve": "@medusajs/medusa/sales-channel",
},
"settings": {
"resolve": "@medusajs/medusa/settings",
},
"stock_location": {
"resolve": "@medusajs/medusa/stock-location",
},
"store": {
"resolve": "@medusajs/medusa/store",
},
"tax": {
"resolve": "@medusajs/medusa/tax",
},
"user": {
"options": {
"jwt_options": undefined,
"jwt_public_key": undefined,
"jwt_secret": "supersecret",
"jwt_verify_options": undefined,
},
"resolve": "@medusajs/medusa/user",
},
"workflows": {
"resolve": "@medusajs/medusa/workflow-engine-inmemory",
},
},
"plugins": [
{
"options": {},
"resolve": "@medusajs/draft-order",
},
],
"projectConfig": {
"cloud": {
"apiKey": "test-api-key",
"emailsEndpoint": "test-emails-endpoint",
"environmentHandle": "test-environment",
"environmentHandle": undefined,
"sandboxHandle": "test-sandbox",
},
"databaseUrl": "postgres://localhost/medusa-starter-default",
"http": {
"adminCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
"authCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
"cookieSecret": "supersecret",
"jwtPublicKey": undefined,
"jwtSecret": "supersecret",
"restrictedFields": {
"store": [
${DEFAULT_STORE_RESTRICTED_FIELDS.map((v) => `"${v}"`).join(
",\n "
)},
],
},
"storeCors": "http://localhost:8000",
},
"redisOptions": {
"retryStrategy": [Function],
@@ -2190,7 +2373,7 @@ describe("defineConfig", function () {
`)
})
it("should merge custom projectConfig.medusaCloudOptions", function () {
it("should merge custom projectConfig.cloud", function () {
const originalEnv = { ...process.env }
process.env.MEDUSA_CLOUD_ENVIRONMENT_HANDLE = "test-environment"
process.env.MEDUSA_CLOUD_API_KEY = "test-api-key"
@@ -2198,7 +2381,7 @@ describe("defineConfig", function () {
const config = defineConfig({
projectConfig: {
http: {} as any,
medusaCloudOptions: {
cloud: {
environmentHandle: "overriden-environment",
apiKey: "overriden-api-key",
emailsEndpoint: "overriden-emails-endpoint",
@@ -2279,6 +2462,7 @@ describe("defineConfig", function () {
"api_key": "overriden-api-key",
"endpoint": "overriden-emails-endpoint",
"environment_handle": "overriden-environment",
"sandbox_handle": undefined,
},
"providers": [
{
@@ -2348,6 +2532,12 @@ describe("defineConfig", function () {
},
],
"projectConfig": {
"cloud": {
"apiKey": "overriden-api-key",
"emailsEndpoint": "overriden-emails-endpoint",
"environmentHandle": "overriden-environment",
"sandboxHandle": undefined,
},
"databaseUrl": "postgres://localhost/medusa-starter-default",
"http": {
"adminCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
@@ -2364,11 +2554,6 @@ describe("defineConfig", function () {
},
"storeCors": "http://localhost:8000",
},
"medusaCloudOptions": {
"apiKey": "overriden-api-key",
"emailsEndpoint": "overriden-emails-endpoint",
"environmentHandle": "overriden-environment",
},
"redisOptions": {
"retryStrategy": [Function],
},

View File

@@ -50,7 +50,7 @@ export function defineConfig(config: InputConfig = {}): ConfigModule {
const projectConfig = normalizeProjectConfig(config.projectConfig, options)
const adminConfig = normalizeAdminConfig(config.admin)
const modules = resolveModules(config.modules, options, config.projectConfig)
applyCloudOptionsToModules(modules, projectConfig?.medusaCloudOptions)
applyCloudOptionsToModules(modules, projectConfig?.cloud)
const plugins = resolvePlugins(config.plugins, options)
return {
@@ -369,15 +369,16 @@ function normalizeProjectConfig(
http,
redisOptions,
sessionOptions,
medusaCloudOptions,
cloud,
...restOfProjectConfig
} = projectConfig || {}
const mergedCloudOptions: MedusaCloudOptions = {
environmentHandle: process.env.MEDUSA_CLOUD_ENVIRONMENT_HANDLE,
sandboxHandle: process.env.MEDUSA_CLOUD_SANDBOX_HANDLE,
apiKey: process.env.MEDUSA_CLOUD_API_KEY,
emailsEndpoint: process.env.MEDUSA_CLOUD_EMAILS_ENDPOINT,
...medusaCloudOptions,
...cloud,
}
const hasCloudOptions = Object.values(mergedCloudOptions).some(
(value) => value !== undefined
@@ -444,7 +445,7 @@ function normalizeProjectConfig(
...sessionOptions,
},
// If there are no cloud options, we better don't pollute the project config for people not using the cloud
...(hasCloudOptions ? { medusaCloudOptions: mergedCloudOptions } : {}),
...(hasCloudOptions ? { cloud: mergedCloudOptions } : {}),
...restOfProjectConfig,
} satisfies ConfigModule["projectConfig"]
@@ -486,6 +487,7 @@ function applyCloudOptionsToModules(
api_key: config.apiKey,
endpoint: config.emailsEndpoint,
environment_handle: config.environmentHandle,
sandbox_handle: config.sandboxHandle,
},
...(module.options ?? {}),
}

View File

@@ -15,6 +15,21 @@ import {
} from "@types"
import { MedusaCloudEmailNotificationProvider } from "../providers/medusa-cloud-email"
const validateCloudOptions = (options: NotificationModuleOptions["cloud"]) => {
const { api_key, endpoint, environment_handle, sandbox_handle } =
options ?? {}
if (!environment_handle && !sandbox_handle) {
return false
}
if (!api_key || !endpoint) {
return false
}
return true
}
const registrationFn = async (klass, container, pluginOptions) => {
container.register({
[NotificationProviderRegistrationPrefix + pluginOptions.id]: asFunction(
@@ -48,8 +63,11 @@ export default async ({
provider.options?.channels?.some((channel) => channel === "email")
)
if (!hasEmailProvider) {
const { api_key, endpoint, environment_handle } = options?.cloud ?? {}
if (api_key && endpoint && environment_handle) {
const shouldRegisterMedusaCloudEmailProvider = validateCloudOptions(
options?.cloud
)
if (shouldRegisterMedusaCloudEmailProvider) {
await registrationFn(MedusaCloudEmailNotificationProvider, container, {
options: options?.cloud,
id: "cloud",

View File

@@ -16,14 +16,23 @@ export class MedusaCloudEmailNotificationProvider extends AbstractNotificationPr
async send(
notification: NotificationTypes.ProviderSendNotificationDTO
): Promise<NotificationTypes.ProviderSendNotificationResultsDTO> {
const headers = {
"Content-Type": "application/json",
Authorization: `Basic ${this.options_.api_key}`,
}
if (this.options_.sandbox_handle) {
headers["x-medusa-sandbox-handle"] = this.options_.sandbox_handle
}
if (this.options_.environment_handle) {
headers["x-medusa-environment-handle"] = this.options_.environment_handle
}
try {
const response = await fetch(`${this.options_.endpoint}/send`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${this.options_.api_key}`,
"x-medusa-environment-handle": this.options_.environment_handle,
},
headers,
body: JSON.stringify({
to: notification.to,
from: notification.from,

View File

@@ -42,5 +42,6 @@ export type NotificationModuleOptions =
export type MedusaCloudEmailOptions = {
api_key: string
endpoint: string
environment_handle: string
environment_handle?: string
sandbox_handle?: string
}