configuration-management 0.1.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/README.md +199 -0
- package/actions/configurations/commerce/index.js +55 -0
- package/actions/configurations/export-config/index.js +259 -0
- package/actions/configurations/ext.config.yaml +151 -0
- package/actions/configurations/import-config/index.js +544 -0
- package/actions/configurations/registration/index.js +37 -0
- package/actions/configurations/sync-store-mappings-from-commerce/index.js +199 -0
- package/actions/configurations/system-config-list/index.js +127 -0
- package/actions/configurations/system-config-save/index.js +160 -0
- package/actions/configurations/system-config-schema/index.js +327 -0
- package/actions/utils.js +73 -0
- package/package.json +74 -0
- package/scripts/setup-app-config.js +114 -0
- package/src/abdb-config.js +241 -0
- package/src/abdb-helper.js +476 -0
- package/src/index.js +20 -0
- package/src/oauth1a.js +135 -0
- package/src/system-config-crypto.js +113 -0
- package/src/system-config-shared.js +89 -0
- package/web/src/components/App.js +47 -0
- package/web/src/components/AppSectionNav.js +49 -0
- package/web/src/components/ExtensionRegistration.js +33 -0
- package/web/src/components/MainPage.js +46 -0
- package/web/src/components/SystemConfig.js +1464 -0
- package/web/src/components/SystemConfigSchemaEditor.js +459 -0
- package/web/src/hooks/useConfirm.js +355 -0
- package/web/src/hooks/useSystemConfig.js +238 -0
- package/web/src/hooks/useSystemConfigSchema.js +102 -0
- package/web/src/index.js +41 -0
- package/web/src/schema/systemConfigSchema.js +82 -0
- package/web/src/settings.js +57 -0
- package/web/src/styles/index.css +326 -0
- package/web/src/theme.js +104 -0
- package/web/src/utils/storeMappingsFromCommerceRest.js +73 -0
- package/web/src/utils.js +52 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Equivalent of Magento's env.php `crypt.key`: a project-wide secret used to
|
|
9
|
+
// derive an AES-256-GCM key that protects sensitive system_config values at
|
|
10
|
+
// rest in App Builder DB (aio-lib-state).
|
|
11
|
+
//
|
|
12
|
+
// Key material is taken from action params/env (never from request payload):
|
|
13
|
+
// SYSTEM_CONFIG_CRYPT_KEY – preferred, dedicated secret
|
|
14
|
+
// OAUTH_CLIENT_SECRET – fallback, the workspace's client secret
|
|
15
|
+
//
|
|
16
|
+
// Wire format for ciphertext (string):
|
|
17
|
+
// enc:v1:<base64url(salt)>:<base64url(iv)>:<base64url(tag)>:<base64url(ct)>
|
|
18
|
+
// `v1` lets us rotate the algorithm later without breaking previously stored
|
|
19
|
+
// values. `salt` is per-record so the derived key changes even if the same
|
|
20
|
+
// master secret is reused across records.
|
|
21
|
+
|
|
22
|
+
const crypto = require('crypto')
|
|
23
|
+
|
|
24
|
+
const ENC_PREFIX = 'enc:v1:'
|
|
25
|
+
const KEY_BYTES = 32
|
|
26
|
+
const IV_BYTES = 12
|
|
27
|
+
const SALT_BYTES = 16
|
|
28
|
+
const SCRYPT_PARAMS = { N: 16384, r: 8, p: 1 }
|
|
29
|
+
|
|
30
|
+
function b64uEncode (buf) {
|
|
31
|
+
return Buffer.from(buf).toString('base64url')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function b64uDecode (str) {
|
|
35
|
+
return Buffer.from(str, 'base64url')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveMasterSecret (params = {}) {
|
|
39
|
+
const secret =
|
|
40
|
+
params.SYSTEM_CONFIG_CRYPT_KEY ||
|
|
41
|
+
params.OAUTH_CLIENT_SECRET ||
|
|
42
|
+
process.env.SYSTEM_CONFIG_CRYPT_KEY ||
|
|
43
|
+
process.env.OAUTH_CLIENT_SECRET ||
|
|
44
|
+
''
|
|
45
|
+
if (!secret || typeof secret !== 'string' || secret.length < 8) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
'Encryption key not configured: set SYSTEM_CONFIG_CRYPT_KEY or OAUTH_CLIENT_SECRET'
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
return secret
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function deriveKey (masterSecret, salt) {
|
|
54
|
+
return crypto.scryptSync(masterSecret, salt, KEY_BYTES, SCRYPT_PARAMS)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isEncrypted (value) {
|
|
58
|
+
return typeof value === 'string' && value.startsWith(ENC_PREFIX)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function encrypt (plaintext, params) {
|
|
62
|
+
if (plaintext == null || plaintext === '') {
|
|
63
|
+
return plaintext
|
|
64
|
+
}
|
|
65
|
+
const text = typeof plaintext === 'string' ? plaintext : String(plaintext)
|
|
66
|
+
const secret = resolveMasterSecret(params)
|
|
67
|
+
const salt = crypto.randomBytes(SALT_BYTES)
|
|
68
|
+
const iv = crypto.randomBytes(IV_BYTES)
|
|
69
|
+
const key = deriveKey(secret, salt)
|
|
70
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
|
|
71
|
+
const ct = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()])
|
|
72
|
+
const tag = cipher.getAuthTag()
|
|
73
|
+
return [
|
|
74
|
+
ENC_PREFIX,
|
|
75
|
+
b64uEncode(salt),
|
|
76
|
+
':',
|
|
77
|
+
b64uEncode(iv),
|
|
78
|
+
':',
|
|
79
|
+
b64uEncode(tag),
|
|
80
|
+
':',
|
|
81
|
+
b64uEncode(ct)
|
|
82
|
+
].join('')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function decrypt (encrypted, params) {
|
|
86
|
+
if (!isEncrypted(encrypted)) {
|
|
87
|
+
return encrypted
|
|
88
|
+
}
|
|
89
|
+
const body = encrypted.slice(ENC_PREFIX.length)
|
|
90
|
+
const parts = body.split(':')
|
|
91
|
+
if (parts.length !== 4) {
|
|
92
|
+
throw new Error('Malformed encrypted value')
|
|
93
|
+
}
|
|
94
|
+
const [saltB64, ivB64, tagB64, ctB64] = parts
|
|
95
|
+
const secret = resolveMasterSecret(params)
|
|
96
|
+
const salt = b64uDecode(saltB64)
|
|
97
|
+
const iv = b64uDecode(ivB64)
|
|
98
|
+
const tag = b64uDecode(tagB64)
|
|
99
|
+
const ct = b64uDecode(ctB64)
|
|
100
|
+
const key = deriveKey(secret, salt)
|
|
101
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv)
|
|
102
|
+
decipher.setAuthTag(tag)
|
|
103
|
+
const pt = Buffer.concat([decipher.update(ct), decipher.final()])
|
|
104
|
+
return pt.toString('utf8')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
ENC_PREFIX,
|
|
109
|
+
isEncrypted,
|
|
110
|
+
encrypt,
|
|
111
|
+
decrypt,
|
|
112
|
+
resolveMasterSecret
|
|
113
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Mirrors Magento's core_config_data: (scope, scope_id, path, value).
|
|
9
|
+
// aio-lib-state keys allow [a-zA-Z0-9_-] only, so we encode
|
|
10
|
+
// scope=`default` scopeId=0 path=`web/secure/base_url`
|
|
11
|
+
// as the state key
|
|
12
|
+
// sysconfig__default__0__web__secure__base_url
|
|
13
|
+
|
|
14
|
+
const STATE_KEY_PREFIX = 'sysconfig__'
|
|
15
|
+
const SCOPES = ['default', 'websites', 'stores']
|
|
16
|
+
const PATH_SEGMENT = /^[a-zA-Z0-9_-]+$/
|
|
17
|
+
const SCOPE_ID_RE = /^[a-zA-Z0-9_-]+$/
|
|
18
|
+
const SENSITIVE_PLACEHOLDER = '__SENSITIVE_UNCHANGED__'
|
|
19
|
+
const USE_DEFAULT_SENTINEL = '__USE_DEFAULT__'
|
|
20
|
+
|
|
21
|
+
function isValidPath (path) {
|
|
22
|
+
if (typeof path !== 'string') return false
|
|
23
|
+
const parts = path.split('/')
|
|
24
|
+
if (parts.length !== 3) return false
|
|
25
|
+
return parts.every((p) => PATH_SEGMENT.test(p))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeScope (scope) {
|
|
29
|
+
if (!scope) return 'default'
|
|
30
|
+
if (!SCOPES.includes(scope)) {
|
|
31
|
+
throw new Error(`Invalid scope "${scope}". Expected one of: ${SCOPES.join(', ')}`)
|
|
32
|
+
}
|
|
33
|
+
return scope
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeScopeId (scope, scopeId) {
|
|
37
|
+
if (scope === 'default') return '0'
|
|
38
|
+
const id = String(scopeId ?? '').trim()
|
|
39
|
+
if (!id || !SCOPE_ID_RE.test(id)) {
|
|
40
|
+
throw new Error(`Invalid scopeId "${scopeId}" for scope "${scope}"`)
|
|
41
|
+
}
|
|
42
|
+
return id
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toStateKey (scope, scopeId, path) {
|
|
46
|
+
if (!isValidPath(path)) {
|
|
47
|
+
throw new Error(`Invalid config path: ${path}`)
|
|
48
|
+
}
|
|
49
|
+
const s = normalizeScope(scope)
|
|
50
|
+
const sid = normalizeScopeId(s, scopeId)
|
|
51
|
+
return [STATE_KEY_PREFIX, s, '__', sid, '__', path.split('/').join('__')].join('')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Magento-style fallback chain. When reading at store scope we look up:
|
|
56
|
+
* stores:<storeId> → websites:<websiteId> → default:0
|
|
57
|
+
* `parentWebsiteId` is supplied by the caller (resolved from /rest/V1/store/storeViews).
|
|
58
|
+
*/
|
|
59
|
+
function buildInheritanceChain (scope, scopeId, parentWebsiteId) {
|
|
60
|
+
const s = normalizeScope(scope)
|
|
61
|
+
if (s === 'default') {
|
|
62
|
+
return [{ scope: 'default', scopeId: '0' }]
|
|
63
|
+
}
|
|
64
|
+
if (s === 'websites') {
|
|
65
|
+
return [
|
|
66
|
+
{ scope: 'websites', scopeId: normalizeScopeId('websites', scopeId) },
|
|
67
|
+
{ scope: 'default', scopeId: '0' }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
// stores
|
|
71
|
+
const chain = [{ scope: 'stores', scopeId: normalizeScopeId('stores', scopeId) }]
|
|
72
|
+
if (parentWebsiteId !== undefined && parentWebsiteId !== null && String(parentWebsiteId) !== '') {
|
|
73
|
+
chain.push({ scope: 'websites', scopeId: normalizeScopeId('websites', parentWebsiteId) })
|
|
74
|
+
}
|
|
75
|
+
chain.push({ scope: 'default', scopeId: '0' })
|
|
76
|
+
return chain
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
STATE_KEY_PREFIX,
|
|
81
|
+
SCOPES,
|
|
82
|
+
SENSITIVE_PLACEHOLDER,
|
|
83
|
+
USE_DEFAULT_SENTINEL,
|
|
84
|
+
isValidPath,
|
|
85
|
+
normalizeScope,
|
|
86
|
+
normalizeScopeId,
|
|
87
|
+
toStateKey,
|
|
88
|
+
buildInheritanceChain
|
|
89
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react'
|
|
9
|
+
import { Provider, lightTheme } from '@adobe/react-spectrum'
|
|
10
|
+
import { ErrorBoundary } from 'react-error-boundary'
|
|
11
|
+
import { Route, Routes, HashRouter } from 'react-router-dom'
|
|
12
|
+
import ExtensionRegistration from './ExtensionRegistration'
|
|
13
|
+
|
|
14
|
+
function App (props) {
|
|
15
|
+
props.runtime.on('configuration', ({ imsOrg, imsToken }) => {
|
|
16
|
+
console.log('configuration change', { imsOrg, imsToken })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<ErrorBoundary onError={onError} FallbackComponent={fallbackComponent}>
|
|
21
|
+
<HashRouter>
|
|
22
|
+
<Provider
|
|
23
|
+
theme={lightTheme}
|
|
24
|
+
colorScheme="light"
|
|
25
|
+
UNSAFE_className="sm-provider"
|
|
26
|
+
>
|
|
27
|
+
<Routes>
|
|
28
|
+
<Route index element={<ExtensionRegistration runtime={props.runtime} ims={props.ims} />} />
|
|
29
|
+
</Routes>
|
|
30
|
+
</Provider>
|
|
31
|
+
</HashRouter>
|
|
32
|
+
</ErrorBoundary>
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
function onError (e, componentStack) {}
|
|
36
|
+
|
|
37
|
+
function fallbackComponent ({ componentStack, error }) {
|
|
38
|
+
return (
|
|
39
|
+
<React.Fragment>
|
|
40
|
+
<h1 style={{ textAlign: 'center', marginTop: '20px' }}>Something went wrong :(</h1>
|
|
41
|
+
<pre>{componentStack + '\n' + error.message}</pre>
|
|
42
|
+
</React.Fragment>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default App
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useLocation, useNavigate } from 'react-router-dom'
|
|
9
|
+
import Settings from '@spectrum-icons/workflow/Settings'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Top-level navigation. Add a new sync-entity tab by appending an entry here
|
|
13
|
+
* and adding a matching render branch in MainPage.js. Styling lives in
|
|
14
|
+
* index.css under `.sm-tab*`.
|
|
15
|
+
*/
|
|
16
|
+
export const NAV_ITEMS = [
|
|
17
|
+
{ key: '/', label: 'System Configurations', Icon: Settings }
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
export default function AppSectionNav () {
|
|
21
|
+
const navigate = useNavigate()
|
|
22
|
+
const location = useLocation()
|
|
23
|
+
const activeKey = NAV_ITEMS.some((it) => it.key === location.pathname) ? location.pathname : '/'
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="sm-tab-bar">
|
|
27
|
+
<div className="sm-tab-bar__track" role="tablist" aria-label="Application sections">
|
|
28
|
+
{NAV_ITEMS.map(({ key, label, Icon }) => {
|
|
29
|
+
const active = key === activeKey
|
|
30
|
+
return (
|
|
31
|
+
<button
|
|
32
|
+
key={key}
|
|
33
|
+
type="button"
|
|
34
|
+
role="tab"
|
|
35
|
+
aria-selected={active}
|
|
36
|
+
className={`sm-tab${active ? ' is-active' : ''}`}
|
|
37
|
+
onClick={() => { if (!active) navigate(key) }}
|
|
38
|
+
>
|
|
39
|
+
<span className="sm-tab__icon">
|
|
40
|
+
<Icon size="XS" />
|
|
41
|
+
</span>
|
|
42
|
+
{label}
|
|
43
|
+
</button>
|
|
44
|
+
)
|
|
45
|
+
})}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
|
|
7
|
+
Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { register } from '@adobe/uix-guest'
|
|
14
|
+
import { MainPage } from './MainPage'
|
|
15
|
+
import { useEffect } from 'react'
|
|
16
|
+
import { getExtensionId } from '../settings'
|
|
17
|
+
|
|
18
|
+
export default function ExtensionRegistration(props) {
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
(async () => {
|
|
22
|
+
|
|
23
|
+
await register({
|
|
24
|
+
id: getExtensionId(),
|
|
25
|
+
methods: {
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
})()
|
|
30
|
+
}, [])
|
|
31
|
+
|
|
32
|
+
return <MainPage ims={props.ims} runtime={props.runtime} />
|
|
33
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { View } from '@adobe/react-spectrum'
|
|
9
|
+
import { attach } from '@adobe/uix-guest'
|
|
10
|
+
import { useEffect } from 'react'
|
|
11
|
+
import { useLocation } from 'react-router-dom'
|
|
12
|
+
import { getExtensionId } from '../settings'
|
|
13
|
+
import AppSectionNav from './AppSectionNav'
|
|
14
|
+
import SystemConfig from './SystemConfig'
|
|
15
|
+
|
|
16
|
+
export const MainPage = props => {
|
|
17
|
+
const location = useLocation()
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const fetchCredentials = async () => {
|
|
21
|
+
if (!props.ims.token) {
|
|
22
|
+
const guestConnection = await attach({ id: getExtensionId() })
|
|
23
|
+
props.ims.token = guestConnection?.sharedContext?.get('imsToken')
|
|
24
|
+
props.ims.org = guestConnection?.sharedContext?.get('imsOrgId')
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
fetchCredentials()
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
// Add a new route branch here when a new tab is added in AppSectionNav.NAV_ITEMS.
|
|
31
|
+
const renderContent = () => {
|
|
32
|
+
switch (location.pathname) {
|
|
33
|
+
default:
|
|
34
|
+
return <SystemConfig runtime={props.runtime} ims={props.ims} />
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View UNSAFE_style={{ overflowX: 'clip' }}>
|
|
40
|
+
<AppSectionNav />
|
|
41
|
+
<View>
|
|
42
|
+
{renderContent()}
|
|
43
|
+
</View>
|
|
44
|
+
</View>
|
|
45
|
+
)
|
|
46
|
+
}
|