From b1b27b4662cedba17231bd25c631ede79a08584a Mon Sep 17 00:00:00 2001 From: moritzraho Date: Thu, 22 Jan 2026 20:55:02 +0100 Subject: [PATCH 1/2] feat: support include-ims-credentials annotation --- src/lib/constants.js | 3 ++- src/lib/run-dev.js | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/lib/constants.js b/src/lib/constants.js index d6df93d..d32b383 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -40,5 +40,6 @@ module.exports = { STAGE_LAUNCH_PREFIX: 'https://experience-stage.adobe.com/?devMode=true#/custom-apps/?localDevUrl=', PRIVATE_KEY_PATH: `${DEV_KEYS_DIR}/private.key`, PUB_CERT_PATH: `${DEV_KEYS_DIR}/cert-pub.crt`, - BUNDLE_OPTIONS + BUNDLE_OPTIONS, + IMS_OAUTH_S2S_ENV_KEY: 'IMS_OAUTH_S2S' } diff --git a/src/lib/run-dev.js b/src/lib/run-dev.js index d90e39f..e59e016 100644 --- a/src/lib/run-dev.js +++ b/src/lib/run-dev.js @@ -25,9 +25,12 @@ const coreLogger = require('@adobe/aio-lib-core-logging') const { getReasonPhrase } = require('http-status-codes') const utils = require('./app-helper') -const { SERVER_HOST, SERVER_DEFAULT_PORT, BUNDLER_DEFAULT_PORT, DEV_API_PREFIX, DEV_API_WEB_PREFIX, BUNDLE_OPTIONS, CHANGED_ASSETS_PRINT_LIMIT } = require('./constants') +const { SERVER_HOST, SERVER_DEFAULT_PORT, BUNDLER_DEFAULT_PORT, DEV_API_PREFIX, DEV_API_WEB_PREFIX, BUNDLE_OPTIONS, CHANGED_ASSETS_PRINT_LIMIT, IMS_OAUTH_S2S_ENV_KEY } = require('./constants') const RAW_CONTENT_TYPES = ['application/octet-stream', 'multipart/form-data'] +// for the include-ims-credentials annotation +let imsAuthObject = null + /* global Request, Response */ /** @@ -86,6 +89,11 @@ async function runDev (runOptions, config, _inprocHookRunner) { // ex. console.log('AIO_DEV ', process.env.AIO_DEV ? 'dev' : 'prod') process.env.AIO_DEV = 'true' + // for the include-ims-credentials annotation + try { + imsAuthObject = JSON.parse(process.env[IMS_OAUTH_S2S_ENV_KEY]) + } catch (e) {} + const serverPortToUse = parseInt(process.env.PORT) || SERVER_DEFAULT_PORT const serverPort = await getPort({ port: serverPortToUse }) @@ -346,6 +354,13 @@ async function invokeAction ({ actionRequestContext, logger }) { } } + // process the include-ims-credentials annotation + const newInputs = rtLib.utils.getIncludeIMSCredentialsAnnotationInputs(action, imsAuthObject) + if (newInputs) { + Object.entries(newInputs).forEach(([k, v]) => { params[k] = v }) + logger.debug(`Added IMS credentials to action params for action '${actionName}'.`) + } + // if we run an action, we will restore the process.env after the call // we must do this before we load the action because code can execute on require/import const preCallEnv = { ...process.env } From 96eed6ad3708346ad31452684dc40da14b7cd8f6 Mon Sep 17 00:00:00 2001 From: moritzraho Date: Thu, 22 Jan 2026 21:38:40 +0100 Subject: [PATCH 2/2] chore: tests --- test/__mocks__/@adobe/aio-lib-runtime.js | 3 +- test/lib/constants.test.js | 4 +- test/lib/run-dev.test.js | 148 +++++++++++++++++++++++ 3 files changed, 153 insertions(+), 2 deletions(-) diff --git a/test/__mocks__/@adobe/aio-lib-runtime.js b/test/__mocks__/@adobe/aio-lib-runtime.js index 2027a4c..af8c82e 100644 --- a/test/__mocks__/@adobe/aio-lib-runtime.js +++ b/test/__mocks__/@adobe/aio-lib-runtime.js @@ -86,7 +86,8 @@ const mockRtLibInstance = { const mockRtUtils = { getActionUrls: jest.fn(), - checkOpenWhiskCredentials: jest.fn() + checkOpenWhiskCredentials: jest.fn(), + getIncludeIMSCredentialsAnnotationInputs: jest.fn() } const init = jest.fn().mockReturnValue(mockRtLibInstance) diff --git a/test/lib/constants.test.js b/test/lib/constants.test.js index 7618b69..fa17102 100644 --- a/test/lib/constants.test.js +++ b/test/lib/constants.test.js @@ -23,7 +23,8 @@ const { STAGE_LAUNCH_PREFIX, PRIVATE_KEY_PATH, PUB_CERT_PATH, - BUNDLE_OPTIONS + BUNDLE_OPTIONS, + IMS_OAUTH_S2S_ENV_KEY } = require(CONSTANTS_PATH) test('exports', () => { @@ -39,6 +40,7 @@ test('exports', () => { expect(PRIVATE_KEY_PATH).toBeDefined() expect(PUB_CERT_PATH).toBeDefined() expect(BUNDLE_OPTIONS).toBeDefined() + expect(IMS_OAUTH_S2S_ENV_KEY).toBeDefined() }) describe('override via env vars', () => { diff --git a/test/lib/run-dev.test.js b/test/lib/run-dev.test.js index aeefc9e..28094e5 100644 --- a/test/lib/run-dev.test.js +++ b/test/lib/run-dev.test.js @@ -1231,6 +1231,12 @@ describe('invokeSequence', () => { }) describe('runDev', () => { + const originalEnv = process.env + + afterEach(() => { + process.env = { ...originalEnv } + }) + test('no front end, no back end', async () => { const actionPath = fixturePath('actions/successNoReturnAction.js') const config = createConfig({ @@ -1253,6 +1259,53 @@ describe('runDev', () => { expect(Object.keys(actionUrls).length).toEqual(0) }) + test('parses IMS_OAUTH_S2S environment variable', async () => { + const imsAuthData = { access_token: 'test-token', org_id: 'test-org' } + process.env.IMS_OAUTH_S2S = JSON.stringify(imsAuthData) + + const actionPath = fixturePath('actions/successNoReturnAction.js') + const config = createConfig({ + hasFrontend: false, + hasBackend: true, + packageName: 'mypackage', + actions: { + myaction: { + function: actionPath + } + } + }) + const runOptions = createRunOptions({ cert: 'my-cert', key: 'my-key' }) + const hookRunner = () => {} + const { actionUrls, serverCleanup } = await runDev(runOptions, config, hookRunner) + + await serverCleanup() + // Verify runDev completes successfully when IMS_OAUTH_S2S env var is set with valid JSON + expect(Object.keys(actionUrls).length).toBeGreaterThan(0) + }) + + test('handles invalid IMS_OAUTH_S2S JSON gracefully', async () => { + process.env.IMS_OAUTH_S2S = 'not-valid-json' + + const actionPath = fixturePath('actions/successNoReturnAction.js') + const config = createConfig({ + hasFrontend: false, + hasBackend: true, + packageName: 'mypackage', + actions: { + myaction: { + function: actionPath + } + } + }) + const runOptions = createRunOptions({ cert: 'my-cert', key: 'my-key' }) + const hookRunner = () => {} + const { actionUrls, serverCleanup } = await runDev(runOptions, config, hookRunner) + + await serverCleanup() + // Verify runDev completes successfully even when IMS_OAUTH_S2S contains invalid JSON + expect(Object.keys(actionUrls).length).toBeGreaterThan(0) + }) + test('no front end, has back end', async () => { const actionPath = fixturePath('actions/successNoReturnAction.js') const config = createConfig({ @@ -1706,6 +1759,101 @@ describe('invokeAction', () => { statusCode: 400 }) }) + + describe('include-ims-credentials annotation', () => { + const rtLib = jest.requireActual('@adobe/aio-lib-runtime') + let getIncludeIMSCredentialsAnnotationInputsSpy + + beforeEach(() => { + getIncludeIMSCredentialsAnnotationInputsSpy = jest.spyOn(rtLib.utils, 'getIncludeIMSCredentialsAnnotationInputs') + }) + + afterEach(() => { + getIncludeIMSCredentialsAnnotationInputsSpy.mockRestore() + }) + + test('adds IMS credentials to params when getIncludeIMSCredentialsAnnotationInputs returns inputs', async () => { + const packageName = 'foo' + const actionPath = fixturePath('actions/successReturnAction.js') + const actionLoader = createActionLoader(actionPath) + + const action = { + function: actionPath, + annotations: { + 'include-ims-credentials': true + } + } + const actionParams = { existingParam: 'value' } + const actionName = 'a' + const actionConfig = { + [packageName]: { + actions: { + [actionName]: action + } + } + } + + // Mock the function to return IMS credentials + const mockImsInputs = { + __ims_oauth_s2s: { client_id: 'mock-access-token', org_id: 'mock-org-id' }, + __ims_env: 'stage' + } + getIncludeIMSCredentialsAnnotationInputsSpy.mockReturnValue(mockImsInputs) + + const actionRequestContext = { + contextActionLoader: actionLoader, + contextItem: action, + contextItemParams: actionParams, + contextItemName: actionName, + packageName, + actionConfig + } + const response = await invokeAction({ actionRequestContext, logger: mockLogger }) + + expect(getIncludeIMSCredentialsAnnotationInputsSpy).toHaveBeenCalledWith(action, expect.anything()) + expect(actionParams.__ims_oauth_s2s).toEqual({ client_id: 'mock-access-token', org_id: 'mock-org-id' }) + expect(actionParams.__ims_env).toEqual('stage') + expect(actionParams.existingParam).toEqual('value') + expect(mockLogger.debug).toHaveBeenCalledWith(`Added IMS credentials to action params for action '${actionName}'.`) + expect(response.statusCode).toEqual(200) + }) + + test('does not add IMS credentials when getIncludeIMSCredentialsAnnotationInputs returns null', async () => { + const packageName = 'foo' + const actionPath = fixturePath('actions/successReturnAction.js') + const actionLoader = createActionLoader(actionPath) + + const action = { function: actionPath } + const actionParams = { existingParam: 'value' } + const actionName = 'a' + const actionConfig = { + [packageName]: { + actions: { + [actionName]: action + } + } + } + + // Mock the function to return null (no annotation or no IMS auth object) + getIncludeIMSCredentialsAnnotationInputsSpy.mockReturnValue(null) + + const actionRequestContext = { + contextActionLoader: actionLoader, + contextItem: action, + contextItemParams: actionParams, + contextItemName: actionName, + packageName, + actionConfig + } + const response = await invokeAction({ actionRequestContext, logger: mockLogger }) + + expect(getIncludeIMSCredentialsAnnotationInputsSpy).toHaveBeenCalledWith(action, expect.anything()) + expect(actionParams.__ow_ims_access_token).toBeUndefined() + expect(actionParams.__ow_ims_org_id).toBeUndefined() + expect(actionParams.existingParam).toEqual('value') + expect(response.statusCode).toEqual(200) + }) + }) }) describe('defaultActionLoader', () => {