@adobe/helix-config 5.10.2 → 5.11.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/CHANGELOG.md +14 -0
- package/package.json +5 -5
- package/src/ConfigContext.js +1 -1
- package/src/PipelineResponse.d.ts +1 -0
- package/src/config-merge.js +1 -0
- package/src/config-view.js +41 -43
- package/src/config-legacy.js +0 -228
- package/src/flatJson2Object.js +0 -104
- package/src/legacy-config-merge.js +0 -236
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [5.11.0](https://github.com/adobe/helix-config/compare/v5.10.3...v5.11.0) (2026-06-09)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* remove unsupported legacy config ([#385](https://github.com/adobe/helix-config/issues/385)) ([deb5e82](https://github.com/adobe/helix-config/commit/deb5e8237e2e36bed1739c9e6701c2b862b60de7))
|
|
7
|
+
|
|
8
|
+
## [5.10.3](https://github.com/adobe/helix-config/compare/v5.10.2...v5.10.3) (2026-05-19)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* keep created when merging profile ([#381](https://github.com/adobe/helix-config/issues/381)) ([77b65c0](https://github.com/adobe/helix-config/commit/77b65c06d87200ac161d86227cdcec29fc08bb70))
|
|
14
|
+
|
|
1
15
|
## [5.10.2](https://github.com/adobe/helix-config/compare/v5.10.1...v5.10.2) (2026-05-19)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/helix-config",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.11.0",
|
|
4
4
|
"description": "Helix Config",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"reporter-options": "configFile=.mocha-multi.json"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@adobe/eslint-config-helix": "3.0.
|
|
39
|
-
"@eslint/config-helpers": "0.
|
|
38
|
+
"@adobe/eslint-config-helix": "3.0.28",
|
|
39
|
+
"@eslint/config-helpers": "0.6.0",
|
|
40
40
|
"@semantic-release/changelog": "6.0.3",
|
|
41
41
|
"@semantic-release/git": "10.0.1",
|
|
42
42
|
"@semantic-release/npm": "13.1.5",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"eslint": "9.4.0",
|
|
45
45
|
"husky": "9.1.7",
|
|
46
46
|
"junit-report-builder": "5.1.2",
|
|
47
|
-
"lint-staged": "
|
|
48
|
-
"mocha": "11.7.
|
|
47
|
+
"lint-staged": "17.0.5",
|
|
48
|
+
"mocha": "11.7.6",
|
|
49
49
|
"mocha-multi-reporters": "1.5.1",
|
|
50
50
|
"mocha-suppress-logs": "0.6.0",
|
|
51
51
|
"semantic-release": "25.0.3"
|
package/src/ConfigContext.js
CHANGED
package/src/config-merge.js
CHANGED
package/src/config-view.js
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
SCOPE_DELIVERY,
|
|
19
19
|
SCOPE_RAW, SCOPE_PUBLIC,
|
|
20
20
|
} from './ConfigContext.js';
|
|
21
|
-
import { resolveLegacyConfig, fetchRobotsTxt } from './config-legacy.js';
|
|
22
21
|
import {
|
|
23
22
|
canonicalArrayString, deepGetOrCreate, prune, uniqueArray,
|
|
24
23
|
} from './utils.js';
|
|
@@ -146,11 +145,11 @@ function resolveSecret(object, idProp, dstProp, siteConfig, orgConfig) {
|
|
|
146
145
|
/**
|
|
147
146
|
* Returns the normalized access configuration for the given partition.
|
|
148
147
|
*
|
|
149
|
-
* SecretIds (
|
|
150
|
-
* The normalized config only has access.preview and access.live;
|
|
151
|
-
* the final object and is only used during resolution. For each
|
|
152
|
-
* the effective secretId and allow lists are the merge of the
|
|
153
|
-
* access.site's values (union, deduplicated).
|
|
148
|
+
* SecretIds (apiKeyId is the deprecated alias of secretId) can be set on access.preview,
|
|
149
|
+
* access.live, and access.site. The normalized config only has access.preview and access.live;
|
|
150
|
+
* access.site is not present in the final object and is only used during resolution. For each
|
|
151
|
+
* partition (preview or live), the effective secretId and allow lists are the merge of the
|
|
152
|
+
* partition's values and access.site's values (union, deduplicated).
|
|
154
153
|
*
|
|
155
154
|
* @param {object} ctx - The context
|
|
156
155
|
* @param {object} config - The config (with access.preview, access.live, access.site)
|
|
@@ -163,7 +162,8 @@ function resolveSecret(object, idProp, dstProp, siteConfig, orgConfig) {
|
|
|
163
162
|
export async function getAccessConfig(ctx, config, orgConfig, partition, rso) {
|
|
164
163
|
const { access = {} } = config;
|
|
165
164
|
const pAccess = access[partition] ?? {};
|
|
166
|
-
|
|
165
|
+
|
|
166
|
+
// union of the partition's and the site-wide secretIds (apiKeyId is the deprecated alias)
|
|
167
167
|
const secretId = uniqueArray(
|
|
168
168
|
pAccess.apiKeyId,
|
|
169
169
|
pAccess.secretId,
|
|
@@ -171,37 +171,43 @@ export async function getAccessConfig(ctx, config, orgConfig, partition, rso) {
|
|
|
171
171
|
access.site?.secretId,
|
|
172
172
|
);
|
|
173
173
|
const allow = uniqueArray(pAccess.allow, access.site?.allow);
|
|
174
|
-
const cfg = {
|
|
175
|
-
secretId,
|
|
176
|
-
allow,
|
|
177
|
-
tokenHash: secretId
|
|
178
|
-
// token ids are always stored in base64url format, but legacy apiKeyIds are not
|
|
179
|
-
.map((jti) => jti.replaceAll('/', '_').replaceAll('+', '-'))
|
|
180
|
-
.map((id) => lookupSecret(config, orgConfig, id, true))
|
|
181
|
-
.filter((hash) => !!hash),
|
|
182
|
-
};
|
|
183
|
-
// if an allow is defined but no secretId, create a fake one so that auth is still enforced.
|
|
184
|
-
if (allow.length && !cfg.secretId.length) {
|
|
185
|
-
cfg.secretId.push('dummy');
|
|
186
|
-
}
|
|
187
174
|
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
175
|
+
// resolve each secretId to its token hash (ids are stored base64url, apiKeyIds may not be)
|
|
176
|
+
const tokenHash = secretId
|
|
177
|
+
.map((id) => id.replaceAll('/', '_').replaceAll('+', '-'))
|
|
178
|
+
.map((id) => lookupSecret(config, orgConfig, id, true))
|
|
179
|
+
.filter((hash) => !!hash);
|
|
180
|
+
|
|
181
|
+
// when auth is required (an allow list or any secretId), always accept the global delivery
|
|
182
|
+
// token if configured; otherwise ensure at least one hash exists so auth stays enforced
|
|
183
|
+
if (allow.length || secretId.length) {
|
|
191
184
|
const globalTokenHash = await getGlobalTokenHash(ctx, rso);
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// add a dummy or global hash if no tokens match the secretIds.
|
|
197
|
-
if (!config.legacy || allow.length) {
|
|
198
|
-
// but only add for non-legacy sites or legacy with allows
|
|
199
|
-
cfg.tokenHash.push(globalTokenHash || 'n/a');
|
|
200
|
-
}
|
|
185
|
+
if (globalTokenHash) {
|
|
186
|
+
tokenHash.push(globalTokenHash);
|
|
187
|
+
} else if (!tokenHash.length) {
|
|
188
|
+
tokenHash.push('n/a');
|
|
201
189
|
}
|
|
202
190
|
}
|
|
203
191
|
|
|
204
|
-
return
|
|
192
|
+
return { secretId, allow, tokenHash };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Retrieves the robots.txt from the code-bus
|
|
197
|
+
* @param {ConfigContext} ctx the context
|
|
198
|
+
* @param {string} owner
|
|
199
|
+
* @param {string} repo
|
|
200
|
+
* @returns {Promise<ConfigObject>} the robots.txt
|
|
201
|
+
*/
|
|
202
|
+
async function fetchRobotsTxt(ctx, owner, repo) {
|
|
203
|
+
const key = `${owner}/${repo}/main/robots.txt`;
|
|
204
|
+
const res = await ctx.loader.getObject(HELIX_CODE_BUS, key);
|
|
205
|
+
const robots = new ConfigObject();
|
|
206
|
+
if (res.body) {
|
|
207
|
+
robots.txt = res.body;
|
|
208
|
+
robots.updateLastModified(res.headers);
|
|
209
|
+
}
|
|
210
|
+
return robots;
|
|
205
211
|
}
|
|
206
212
|
|
|
207
213
|
/**
|
|
@@ -318,14 +324,7 @@ async function resolveConfig(ctx, rso, scope) {
|
|
|
318
324
|
ctx.log.error('error loading config %s: %d', key, res.status);
|
|
319
325
|
throw new StatusCodeError('error loading config', res.status);
|
|
320
326
|
}
|
|
321
|
-
|
|
322
|
-
if (config) {
|
|
323
|
-
ctx.timer?.update('profile');
|
|
324
|
-
const profile = await loadProfile(ctx, rso, 'default');
|
|
325
|
-
config.merge(profile);
|
|
326
|
-
config.data.legacy = true;
|
|
327
|
-
}
|
|
328
|
-
return config;
|
|
327
|
+
return null;
|
|
329
328
|
}
|
|
330
329
|
const site = new ConfigObject();
|
|
331
330
|
site.data = res.json();
|
|
@@ -649,7 +648,7 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
649
648
|
},
|
|
650
649
|
});
|
|
651
650
|
}
|
|
652
|
-
headers['x-hlx-version'] =
|
|
651
|
+
headers['x-hlx-version'] = 5;
|
|
653
652
|
|
|
654
653
|
const config = siteConfig.data;
|
|
655
654
|
if (scope === SCOPE_PIPELINE || config.extends?.profile === 'default') {
|
|
@@ -771,7 +770,6 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
771
770
|
public: config.public,
|
|
772
771
|
robots: config.robots,
|
|
773
772
|
access: config.access,
|
|
774
|
-
legacy: config.legacy,
|
|
775
773
|
lastModified: siteConfig.lastModified,
|
|
776
774
|
created: config.created,
|
|
777
775
|
trustedHosts: computeTrustedHosts(org, site, config),
|
package/src/config-legacy.js
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2024 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
|
-
import {
|
|
13
|
-
SCOPE_ADMIN, SCOPE_DELIVERY, SCOPE_PIPELINE, SCOPE_RAW,
|
|
14
|
-
} from './ConfigContext.js';
|
|
15
|
-
import { ConfigObject } from './config-object.js';
|
|
16
|
-
import { contentConfigMerge } from './legacy-config-merge.js';
|
|
17
|
-
import { prune, toArray } from './utils.js';
|
|
18
|
-
|
|
19
|
-
const HELIX_CODE_BUS = 'helix-code-bus';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Retrieves the head.html from the code bus
|
|
23
|
-
* @param ctx
|
|
24
|
-
* @param rso
|
|
25
|
-
* @returns {Promise<ConfigObject|{}>}
|
|
26
|
-
*/
|
|
27
|
-
async function fetchHeadHtml(ctx, rso) {
|
|
28
|
-
const key = `${rso.org}/${rso.site}/${rso.ref}/head.html`;
|
|
29
|
-
const res = await ctx.loader.getObject(HELIX_CODE_BUS, key);
|
|
30
|
-
if (res.body) {
|
|
31
|
-
const head = new ConfigObject();
|
|
32
|
-
head.html = res.body;
|
|
33
|
-
head.updateLastModified(res.headers);
|
|
34
|
-
return head;
|
|
35
|
-
}
|
|
36
|
-
return {};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Retrieves the helix-config.json which is an aggregate from fstab.yaml and head.html.
|
|
41
|
-
*
|
|
42
|
-
* @param {ConfigContext} ctx the context
|
|
43
|
-
* @param {RSO} rso
|
|
44
|
-
* @returns {Promise<ConfigObject|null>} the helix-config or {@code null} if optional and not found.
|
|
45
|
-
*/
|
|
46
|
-
async function fetchHelixConfig(ctx, rso) {
|
|
47
|
-
const key = `${rso.org}/${rso.site}/main/helix-config.json`;
|
|
48
|
-
const res = await ctx.loader.getObject(HELIX_CODE_BUS, key);
|
|
49
|
-
if (!res.body) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
const config = res.json();
|
|
53
|
-
// set contentbus id, if present in header
|
|
54
|
-
const cbid = res.headers.get('x-amz-meta-x-contentbus-id');
|
|
55
|
-
if (cbid && !config.content) {
|
|
56
|
-
// create the content section if not already present
|
|
57
|
-
config.content = {
|
|
58
|
-
data: Object.create(null),
|
|
59
|
-
};
|
|
60
|
-
const content = config.content.data;
|
|
61
|
-
const nvps = cbid.split(/;\s+/);
|
|
62
|
-
for (const nv of nvps) {
|
|
63
|
-
const [path, id] = nv.split('=');
|
|
64
|
-
if (!content[path]) {
|
|
65
|
-
content[path] = {};
|
|
66
|
-
}
|
|
67
|
-
content[path].contentBusId = id;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const cfg = new ConfigObject();
|
|
71
|
-
cfg.updateLastModified(res.headers);
|
|
72
|
-
cfg.data = config;
|
|
73
|
-
return cfg;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Retrieves the robots.txt from the code-bus
|
|
78
|
-
* @param {ConfigContext} ctx the context
|
|
79
|
-
* @param {string} owner
|
|
80
|
-
* @param {string} repo
|
|
81
|
-
* @returns {Promise<ConfigObject>} the robots.txt
|
|
82
|
-
*/
|
|
83
|
-
export async function fetchRobotsTxt(ctx, owner, repo) {
|
|
84
|
-
const key = `${owner}/${repo}/main/robots.txt`;
|
|
85
|
-
const res = await ctx.loader.getObject(HELIX_CODE_BUS, key);
|
|
86
|
-
const robots = new ConfigObject();
|
|
87
|
-
if (res.body) {
|
|
88
|
-
robots.txt = res.body;
|
|
89
|
-
robots.updateLastModified(res.headers);
|
|
90
|
-
}
|
|
91
|
-
return robots;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function resolveAdminAccess(ctx, admin) {
|
|
95
|
-
const ret = {
|
|
96
|
-
...admin,
|
|
97
|
-
};
|
|
98
|
-
if (ret.apiKeyId) {
|
|
99
|
-
ret.apiKeyId = toArray(ret.apiKeyId);
|
|
100
|
-
}
|
|
101
|
-
if (ret.defaultRole) {
|
|
102
|
-
ret.defaultRole = toArray(ret.defaultRole);
|
|
103
|
-
}
|
|
104
|
-
for (const [role, users] of Object.entries(ret.role ?? [])) {
|
|
105
|
-
// todo: load users.json
|
|
106
|
-
ret.role[role] = toArray(users);
|
|
107
|
-
}
|
|
108
|
-
return ret;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Loads the content from a helix 4 project.
|
|
113
|
-
* @param {import('./ConfigContext.js)'.ConfigContext} ctx
|
|
114
|
-
* @param {RSO} rso
|
|
115
|
-
* @param {string} scope
|
|
116
|
-
* @returns {Promise<ConfigObject|null>} the config object or {@code null} if not found.
|
|
117
|
-
*/
|
|
118
|
-
export async function resolveLegacyConfig(ctx, rso, scope) {
|
|
119
|
-
ctx.timer?.update('legacy-helix-config');
|
|
120
|
-
|
|
121
|
-
// set owner==org and repo==site and fetch from helix-config for now
|
|
122
|
-
const cfg = await fetchHelixConfig(ctx, rso);
|
|
123
|
-
if (!cfg) {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
const helixConfig = cfg.data;
|
|
127
|
-
const { contentBusId } = helixConfig.content.data['/'];
|
|
128
|
-
if (!contentBusId) {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
const fstab = helixConfig.fstab?.data || helixConfig.fstab;
|
|
132
|
-
if (!fstab) {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
let source = fstab.mountpoints['/'];
|
|
136
|
-
if (!source) {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
if (typeof source === 'string') {
|
|
140
|
-
source = {
|
|
141
|
-
type: source.startsWith('https://drive.google.com/')
|
|
142
|
-
? 'google'
|
|
143
|
-
: 'onedrive',
|
|
144
|
-
url: source,
|
|
145
|
-
};
|
|
146
|
-
} else {
|
|
147
|
-
delete source.contentBusId;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const head = await fetchHeadHtml(ctx, rso);
|
|
151
|
-
const config = {
|
|
152
|
-
version: 1,
|
|
153
|
-
legacy: true,
|
|
154
|
-
code: {
|
|
155
|
-
owner: rso.org,
|
|
156
|
-
repo: rso.site,
|
|
157
|
-
source: {
|
|
158
|
-
type: 'github',
|
|
159
|
-
url: `https://github.com/${rso.org}/${rso.site}`,
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
content: {
|
|
163
|
-
contentBusId,
|
|
164
|
-
source,
|
|
165
|
-
},
|
|
166
|
-
folders: fstab.folders ?? {},
|
|
167
|
-
head,
|
|
168
|
-
};
|
|
169
|
-
cfg.data = config;
|
|
170
|
-
ctx.timer?.update('legacy-config-all');
|
|
171
|
-
const {
|
|
172
|
-
preview: configAllPreview,
|
|
173
|
-
live: configAllLive,
|
|
174
|
-
} = await contentConfigMerge(ctx, config.content.contentBusId, rso);
|
|
175
|
-
const { access, admin } = configAllPreview?.config?.data || {};
|
|
176
|
-
if (access) {
|
|
177
|
-
config.access = {
|
|
178
|
-
preview: access.preview,
|
|
179
|
-
live: access.live,
|
|
180
|
-
};
|
|
181
|
-
delete access.preview;
|
|
182
|
-
delete access.live;
|
|
183
|
-
config.access.site = access;
|
|
184
|
-
}
|
|
185
|
-
if (admin) {
|
|
186
|
-
if (!config.access) {
|
|
187
|
-
config.access = { };
|
|
188
|
-
}
|
|
189
|
-
config.access.admin = await resolveAdminAccess(ctx, admin);
|
|
190
|
-
}
|
|
191
|
-
if (configAllPreview) {
|
|
192
|
-
config.cdn = configAllPreview.config?.data.cdn ?? {};
|
|
193
|
-
if (!config.cdn.prod?.host && configAllPreview.config?.data.host) {
|
|
194
|
-
config.cdn.prod = {
|
|
195
|
-
host: configAllPreview.config.data.host,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (scope !== SCOPE_DELIVERY && configAllPreview) {
|
|
201
|
-
config.headers = configAllPreview.headers?.data ?? {};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (scope === SCOPE_PIPELINE) {
|
|
205
|
-
config.metadata = {};
|
|
206
|
-
if (configAllPreview) {
|
|
207
|
-
config.metadata.preview = configAllPreview.metadata;
|
|
208
|
-
}
|
|
209
|
-
if (configAllLive) {
|
|
210
|
-
config.metadata.live = configAllLive.metadata;
|
|
211
|
-
}
|
|
212
|
-
ctx.timer?.update('robots-txt');
|
|
213
|
-
config.robots = await fetchRobotsTxt(ctx, rso.org, rso.site);
|
|
214
|
-
if (!config.robots.txt) {
|
|
215
|
-
delete config.robots;
|
|
216
|
-
} else {
|
|
217
|
-
cfg.updateLastModified(config.robots);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if ((scope === SCOPE_ADMIN || scope === SCOPE_RAW) && configAllPreview?.config?.data?.metadata) {
|
|
221
|
-
if (!config.metadata) {
|
|
222
|
-
config.metadata = {};
|
|
223
|
-
}
|
|
224
|
-
config.metadata.source = configAllPreview?.config?.data?.metadata;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return prune(cfg);
|
|
228
|
-
}
|
package/src/flatJson2Object.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2022 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
|
-
/**
|
|
14
|
-
* Converts a config row-based name/value pair array with dotted key notation to an object.
|
|
15
|
-
*
|
|
16
|
-
* example:
|
|
17
|
-
*
|
|
18
|
-
* ```
|
|
19
|
-
* host: www.adobe.com
|
|
20
|
-
* document.level: 42,
|
|
21
|
-
* document.title: hello
|
|
22
|
-
* document.body.status: 200
|
|
23
|
-
* acl.allow: /foo
|
|
24
|
-
* acl.allow: /bar
|
|
25
|
-
* cnd.0.type: fastly
|
|
26
|
-
* cnd.0.host: www.adobe.com
|
|
27
|
-
* cnd.1.type: cloudflare
|
|
28
|
-
* ```
|
|
29
|
-
*
|
|
30
|
-
* becomes:
|
|
31
|
-
*
|
|
32
|
-
* ```
|
|
33
|
-
* {
|
|
34
|
-
* host: 'www.adobe.com',
|
|
35
|
-
* document: {
|
|
36
|
-
* level: 42,
|
|
37
|
-
* title: 'hello',
|
|
38
|
-
* body: {
|
|
39
|
-
* status: 200,
|
|
40
|
-
* }
|
|
41
|
-
* },
|
|
42
|
-
* acl: {
|
|
43
|
-
* allow: ['/foo', '/bar],
|
|
44
|
-
* }
|
|
45
|
-
* cnd: [
|
|
46
|
-
* { type: 'fastly', host: 'www.adobe.com' },
|
|
47
|
-
* { type: 'cloudflare' },
|
|
48
|
-
* ]
|
|
49
|
-
* }
|
|
50
|
-
* ```
|
|
51
|
-
*
|
|
52
|
-
* @param {object[]} rows array of columns
|
|
53
|
-
* @param {string} [keyName = 'key'] name of the key column.
|
|
54
|
-
* @param {string} [valueName = 'value'] name of the value column.
|
|
55
|
-
*/
|
|
56
|
-
export function flatJson2object(rows, keyName = 'key', valueName = 'value') {
|
|
57
|
-
const arrays = [];
|
|
58
|
-
const update = (o, segs, value) => {
|
|
59
|
-
const seg = segs.shift();
|
|
60
|
-
if (segs.length) {
|
|
61
|
-
if (seg in o) {
|
|
62
|
-
update(o[seg], segs, value);
|
|
63
|
-
return o;
|
|
64
|
-
}
|
|
65
|
-
const n = Number.parseInt(segs[0], 10);
|
|
66
|
-
if (Number.isNaN(n)) {
|
|
67
|
-
// eslint-disable-next-line no-param-reassign
|
|
68
|
-
o[seg] = update(Object.create(null), segs, value);
|
|
69
|
-
} else {
|
|
70
|
-
// eslint-disable-next-line no-param-reassign
|
|
71
|
-
o[seg] = update([], segs, value);
|
|
72
|
-
arrays.push(o[seg]);
|
|
73
|
-
}
|
|
74
|
-
return o;
|
|
75
|
-
}
|
|
76
|
-
if (!(seg in o)) {
|
|
77
|
-
// eslint-disable-next-line no-param-reassign
|
|
78
|
-
o[seg] = value;
|
|
79
|
-
} else if (Array.isArray(o[seg])) {
|
|
80
|
-
// eslint-disable-next-line no-param-reassign
|
|
81
|
-
o[seg].push(value);
|
|
82
|
-
} else {
|
|
83
|
-
// eslint-disable-next-line no-param-reassign
|
|
84
|
-
o[seg] = [o[seg], value];
|
|
85
|
-
}
|
|
86
|
-
return o;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const obj = Object.create(null);
|
|
90
|
-
rows.forEach((row) => {
|
|
91
|
-
update(obj, String(row[keyName]).trim().split('.'), String(row[valueName]).trim());
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// compact arrays
|
|
95
|
-
arrays.forEach((a) => {
|
|
96
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
97
|
-
if (!(i in a)) {
|
|
98
|
-
a.splice(i, 1);
|
|
99
|
-
i -= 1;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
return obj;
|
|
104
|
-
}
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2022 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
|
-
/* eslint-disable no-continue,no-param-reassign */
|
|
13
|
-
import { logLevelForStatusCode, propagateStatusCode } from '@adobe/helix-shared-utils';
|
|
14
|
-
import { flatJson2object } from './flatJson2Object.js';
|
|
15
|
-
import { ModifiersConfig } from './ModifiersConfig.js';
|
|
16
|
-
|
|
17
|
-
export const CONFIG_JSON_PATH = '/.helix/config.json';
|
|
18
|
-
|
|
19
|
-
export const HEADERS_JSON_PATH = '/.helix/headers.json';
|
|
20
|
-
|
|
21
|
-
export const METADATA_JSON_PATH = '/metadata.json';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Coerces the given value to an array. if the value is null or undefined, an empty array is
|
|
25
|
-
* returned.
|
|
26
|
-
* @param {*} value
|
|
27
|
-
* @param {boolean} [unique = false] if true, the resulting array will contain only unique values
|
|
28
|
-
* @return {[]}
|
|
29
|
-
*/
|
|
30
|
-
export function coerceArray(value, unique = false) {
|
|
31
|
-
if (value === null || value === undefined) {
|
|
32
|
-
return [];
|
|
33
|
-
}
|
|
34
|
-
const array = Array.isArray(value) ? value : [value];
|
|
35
|
-
if (unique) {
|
|
36
|
-
return Array.from(new Set(array));
|
|
37
|
-
}
|
|
38
|
-
return array;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* From a JSON response, retrieves the `data` sheet if this is a single sheet,
|
|
43
|
-
* or it returns the first existing sheet given by a list of names, if it is a
|
|
44
|
-
* multisheet.
|
|
45
|
-
* Returns `null` if there is neither.
|
|
46
|
-
*
|
|
47
|
-
* @param {any} json JSON object
|
|
48
|
-
* @param {String[]} names names to check in a multi sheet
|
|
49
|
-
*/
|
|
50
|
-
export function getSheetData(json, names) {
|
|
51
|
-
/* c8 ignore next 3 */
|
|
52
|
-
if (Array.isArray(json.data)) {
|
|
53
|
-
return json.data;
|
|
54
|
-
}
|
|
55
|
-
let sheet;
|
|
56
|
-
|
|
57
|
-
const match = names.find((name) => !!json[name]);
|
|
58
|
-
if (match) {
|
|
59
|
-
sheet = json[match];
|
|
60
|
-
}
|
|
61
|
-
if (Array.isArray(sheet?.data)) {
|
|
62
|
-
return sheet.data;
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Helper class to load and generate the aggregate configs
|
|
69
|
-
*/
|
|
70
|
-
export class ConfigMerger {
|
|
71
|
-
constructor(log, contentBusId, partition) {
|
|
72
|
-
Object.assign(this, {
|
|
73
|
-
log,
|
|
74
|
-
contentBusId,
|
|
75
|
-
partition,
|
|
76
|
-
lastModifiedTime: null,
|
|
77
|
-
lastModified: null,
|
|
78
|
-
all: {
|
|
79
|
-
version: 1,
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
toJSON() {
|
|
85
|
-
return this.all;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Loads a file from the content bus
|
|
90
|
-
* @param {ConfigContext} ctx
|
|
91
|
-
* @param {string} opts.path
|
|
92
|
-
* @param {string} opts.property
|
|
93
|
-
* @param {function} opts.mapping
|
|
94
|
-
* @returns {Promise<>}
|
|
95
|
-
*/
|
|
96
|
-
async loadFile(ctx, opts) {
|
|
97
|
-
const key = `${this.contentBusId}/${this.partition}${opts.path}`;
|
|
98
|
-
const fileResponse = await ctx.loader.getObject('helix-content-bus', key);
|
|
99
|
-
const fileText = fileResponse.body;
|
|
100
|
-
|
|
101
|
-
let data;
|
|
102
|
-
if (fileResponse.status === 200) {
|
|
103
|
-
try {
|
|
104
|
-
data = getSheetData(JSON.parse(fileText), ['default']);
|
|
105
|
-
// eslint-disable-next-line no-empty
|
|
106
|
-
} catch {
|
|
107
|
-
}
|
|
108
|
-
if (!data) {
|
|
109
|
-
this.log.info(`[config-all] Error loading ${key}: empty or invalid json.`);
|
|
110
|
-
} else {
|
|
111
|
-
this.log.info(`[config-all] loaded: ${key}`);
|
|
112
|
-
}
|
|
113
|
-
} else if (fileResponse.status === 404) {
|
|
114
|
-
this.log.info(`[config-all] Error loading ${key}: no such object`);
|
|
115
|
-
} else {
|
|
116
|
-
this.log[logLevelForStatusCode(fileResponse.status)](`Invalid response (${fileResponse.status}) when loading json from ${key}`);
|
|
117
|
-
const err = new Error(`Unable to fetch json: ${fileResponse.status} ${fileText}`);
|
|
118
|
-
err.status = propagateStatusCode(fileResponse.status);
|
|
119
|
-
throw err;
|
|
120
|
-
}
|
|
121
|
-
if (data) {
|
|
122
|
-
// get the config property for this file
|
|
123
|
-
let config = this.all[opts.property];
|
|
124
|
-
if (!config) {
|
|
125
|
-
config = {};
|
|
126
|
-
this.all[opts.property] = config;
|
|
127
|
-
}
|
|
128
|
-
// update the data of the config
|
|
129
|
-
config.data = opts.mapping(data, config);
|
|
130
|
-
this.updateLastModified(config, fileResponse.headers);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Update the last modified in the config object and tracks the most recent one
|
|
136
|
-
* @param {object} obj
|
|
137
|
-
* @param {Map} headers
|
|
138
|
-
*/
|
|
139
|
-
updateLastModified(obj, headers) {
|
|
140
|
-
const httpDate = headers.get('x-source-last-modified') ?? headers.get('last-modified');
|
|
141
|
-
/* c8 ignore next 3 */
|
|
142
|
-
if (!httpDate) {
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
const time = new Date(httpDate).getTime();
|
|
146
|
-
/* c8 ignore next 4 */
|
|
147
|
-
if (Number.isNaN(time)) {
|
|
148
|
-
this.log.warn(`[config-all] updateLastModified date is invalid: ${httpDate}`);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
obj.lastModified = httpDate;
|
|
152
|
-
if (time > (this.lastModifiedTime ?? 0)) {
|
|
153
|
-
this.lastModifiedTime = time;
|
|
154
|
-
this.lastModified = httpDate;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
clone(partition) {
|
|
159
|
-
const c = new ConfigMerger(this.log, this.contentBusId, partition);
|
|
160
|
-
c.lastModified = this.lastModified;
|
|
161
|
-
c.lastModifiedTime = this.lastModifiedTime;
|
|
162
|
-
// copy .helix/config.json and .helix/headers.json since it's the same for both partitions
|
|
163
|
-
c.all.config = this.all.config;
|
|
164
|
-
if (this.all.headers) {
|
|
165
|
-
c.all.headers = this.all.headers;
|
|
166
|
-
}
|
|
167
|
-
return c;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Handles merging content bus configuration files. The metadata and headers are specific to
|
|
173
|
-
* their partition, whereas the config is always loaded from preview.
|
|
174
|
-
*
|
|
175
|
-
* this means that config-all is always updated in both partitions
|
|
176
|
-
*
|
|
177
|
-
* @param {ConfigContext} ctx the universal context
|
|
178
|
-
* @param {string} contentBusId the content bus id
|
|
179
|
-
* @param {Response} res the response of the preceding content bus operation (preview/publish)
|
|
180
|
-
* @returns {Promise<void>}
|
|
181
|
-
* @implements ResponseHook
|
|
182
|
-
*/
|
|
183
|
-
export async function contentConfigMerge(ctx, contentBusId) {
|
|
184
|
-
const { log } = ctx;
|
|
185
|
-
|
|
186
|
-
// load config always from preview
|
|
187
|
-
const cfgPreview = new ConfigMerger(log, contentBusId, 'preview');
|
|
188
|
-
await cfgPreview.loadFile(ctx, {
|
|
189
|
-
path: CONFIG_JSON_PATH,
|
|
190
|
-
property: 'config',
|
|
191
|
-
mapping: (data) => flatJson2object(data),
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// load headers always from preview
|
|
195
|
-
await cfgPreview.loadFile(ctx, {
|
|
196
|
-
path: HEADERS_JSON_PATH,
|
|
197
|
-
property: 'headers',
|
|
198
|
-
mapping: (data) => ModifiersConfig.parseModifierSheet(data),
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const cfgLive = cfgPreview.clone('live');
|
|
202
|
-
const updates = [cfgPreview, cfgLive];
|
|
203
|
-
|
|
204
|
-
// generate preview and live versions
|
|
205
|
-
await Promise.all(updates.map((async (cfg) => {
|
|
206
|
-
// generate the metadata-all.json first
|
|
207
|
-
const metadataPaths = coerceArray(cfg.all.config?.data?.metadata || [METADATA_JSON_PATH], true);
|
|
208
|
-
for (const path of metadataPaths) {
|
|
209
|
-
const loadOpts = {
|
|
210
|
-
path,
|
|
211
|
-
property: 'metadata',
|
|
212
|
-
mapping: (data, metadata) => {
|
|
213
|
-
if (!Array.isArray(metadata.data)) {
|
|
214
|
-
metadata.data = [];
|
|
215
|
-
}
|
|
216
|
-
metadata.data.push(...data);
|
|
217
|
-
return metadata.data;
|
|
218
|
-
},
|
|
219
|
-
};
|
|
220
|
-
// eslint-disable-next-line no-await-in-loop
|
|
221
|
-
await cfg.loadFile(ctx, loadOpts);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// convert to modifiers map
|
|
225
|
-
if (cfg.all.metadata?.data) {
|
|
226
|
-
cfg.all.metadata.data = ModifiersConfig.parseModifierSheet(cfg.all.metadata.data);
|
|
227
|
-
} else {
|
|
228
|
-
cfg.all.metadata = {};
|
|
229
|
-
}
|
|
230
|
-
})));
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
preview: cfgPreview.toJSON(),
|
|
234
|
-
live: cfgLive.toJSON(),
|
|
235
|
-
};
|
|
236
|
-
}
|