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.
Files changed (35) hide show
  1. package/README.md +199 -0
  2. package/actions/configurations/commerce/index.js +55 -0
  3. package/actions/configurations/export-config/index.js +259 -0
  4. package/actions/configurations/ext.config.yaml +151 -0
  5. package/actions/configurations/import-config/index.js +544 -0
  6. package/actions/configurations/registration/index.js +37 -0
  7. package/actions/configurations/sync-store-mappings-from-commerce/index.js +199 -0
  8. package/actions/configurations/system-config-list/index.js +127 -0
  9. package/actions/configurations/system-config-save/index.js +160 -0
  10. package/actions/configurations/system-config-schema/index.js +327 -0
  11. package/actions/utils.js +73 -0
  12. package/package.json +74 -0
  13. package/scripts/setup-app-config.js +114 -0
  14. package/src/abdb-config.js +241 -0
  15. package/src/abdb-helper.js +476 -0
  16. package/src/index.js +20 -0
  17. package/src/oauth1a.js +135 -0
  18. package/src/system-config-crypto.js +113 -0
  19. package/src/system-config-shared.js +89 -0
  20. package/web/src/components/App.js +47 -0
  21. package/web/src/components/AppSectionNav.js +49 -0
  22. package/web/src/components/ExtensionRegistration.js +33 -0
  23. package/web/src/components/MainPage.js +46 -0
  24. package/web/src/components/SystemConfig.js +1464 -0
  25. package/web/src/components/SystemConfigSchemaEditor.js +459 -0
  26. package/web/src/hooks/useConfirm.js +355 -0
  27. package/web/src/hooks/useSystemConfig.js +238 -0
  28. package/web/src/hooks/useSystemConfigSchema.js +102 -0
  29. package/web/src/index.js +41 -0
  30. package/web/src/schema/systemConfigSchema.js +82 -0
  31. package/web/src/settings.js +57 -0
  32. package/web/src/styles/index.css +326 -0
  33. package/web/src/theme.js +104 -0
  34. package/web/src/utils/storeMappingsFromCommerceRest.js +73 -0
  35. 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
+ }