@adobe/helix-config 4.2.0 → 4.3.1
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 +1 -1
- package/src/config-legacy.js +15 -9
- package/src/config-merge.js +6 -3
- package/src/config-object.js +58 -0
- package/src/config-view.js +78 -39
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [4.3.1](https://github.com/adobe/helix-config/compare/v4.3.0...v4.3.1) (2024-08-22)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* include and set last-modified ([#174](https://github.com/adobe/helix-config/issues/174)) ([b325569](https://github.com/adobe/helix-config/commit/b3255693807e1ee20609f4fc9411308a7174a50a)), closes [#171](https://github.com/adobe/helix-config/issues/171)
|
|
7
|
+
|
|
8
|
+
# [4.3.0](https://github.com/adobe/helix-config/compare/v4.2.0...v4.3.0) (2024-08-20)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* include org users in admin access config ([#173](https://github.com/adobe/helix-config/issues/173)) ([427d254](https://github.com/adobe/helix-config/commit/427d254e39b701529cdcd915469cbaa414e0afa7)), closes [#167](https://github.com/adobe/helix-config/issues/167)
|
|
14
|
+
|
|
1
15
|
# [4.2.0](https://github.com/adobe/helix-config/compare/v4.1.1...v4.2.0) (2024-08-19)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
package/src/config-legacy.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
import { SCOPE_PIPELINE } from './ConfigContext.js';
|
|
13
|
+
import { ConfigObject } from './config-object.js';
|
|
13
14
|
|
|
14
15
|
const HELIX_CODE_BUS = 'helix-code-bus';
|
|
15
16
|
|
|
@@ -78,12 +79,17 @@ async function fetchConfigAll(ctx, contentBusId, partition) {
|
|
|
78
79
|
* @param {ConfigContext} ctx the context
|
|
79
80
|
* @param {string} owner
|
|
80
81
|
* @param {string} repo
|
|
81
|
-
* @returns {Promise<
|
|
82
|
+
* @returns {Promise<ConfigObject>} the robots.txt
|
|
82
83
|
*/
|
|
83
84
|
export async function fetchRobotsTxt(ctx, owner, repo) {
|
|
84
85
|
const key = `${owner}/${repo}/main/robots.txt`;
|
|
85
86
|
const res = await ctx.loader.getObject(HELIX_CODE_BUS, key);
|
|
86
|
-
|
|
87
|
+
const robots = new ConfigObject();
|
|
88
|
+
if (res.body) {
|
|
89
|
+
robots.txt = res.body;
|
|
90
|
+
robots.updateLastModified(res.headers);
|
|
91
|
+
}
|
|
92
|
+
return robots;
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
async function resolveAdminAccess(ctx, admin) {
|
|
@@ -108,7 +114,7 @@ async function resolveAdminAccess(ctx, admin) {
|
|
|
108
114
|
* @param {ConfigContext} ctx
|
|
109
115
|
* @param {RSO} rso
|
|
110
116
|
* @param {string} scope
|
|
111
|
-
* @returns {Promise<
|
|
117
|
+
* @returns {Promise<ConfigObject|null>} the config object or {@code null} if not found.
|
|
112
118
|
*/
|
|
113
119
|
export async function resolveLegacyConfig(ctx, rso, scope) {
|
|
114
120
|
// set owner==org and repo==site and fetch from helix-config for now
|
|
@@ -178,12 +184,12 @@ export async function resolveLegacyConfig(ctx, rso, scope) {
|
|
|
178
184
|
if (configAllLive?.metadata) {
|
|
179
185
|
config.metadata.live = configAllLive.metadata;
|
|
180
186
|
}
|
|
181
|
-
|
|
182
|
-
if (robots) {
|
|
183
|
-
config.robots
|
|
184
|
-
txt: robots,
|
|
185
|
-
};
|
|
187
|
+
config.robots = await fetchRobotsTxt(ctx, rso.org, rso.site);
|
|
188
|
+
if (!config.robots.txt) {
|
|
189
|
+
delete config.robots;
|
|
186
190
|
}
|
|
187
191
|
}
|
|
188
|
-
|
|
192
|
+
const obj = new ConfigObject();
|
|
193
|
+
obj.data = config;
|
|
194
|
+
return obj;
|
|
189
195
|
}
|
package/src/config-merge.js
CHANGED
|
@@ -133,10 +133,13 @@ function mergeConfig(dst, src) {
|
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Returns the merge configuration of the site and the profile.
|
|
138
|
+
* @param {ConfigObject} site
|
|
139
|
+
* @param {ConfigObject} profile
|
|
140
|
+
* @returns {*}
|
|
141
|
+
*/
|
|
136
142
|
export function getMergedConfig(site, profile) {
|
|
137
|
-
if (!profile) {
|
|
138
|
-
return site;
|
|
139
|
-
}
|
|
140
143
|
const ret = Object.create(null);
|
|
141
144
|
ret.version = site.version;
|
|
142
145
|
mergeConfig(ret, profile);
|
|
@@ -0,0 +1,58 @@
|
|
|
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 { getMergedConfig } from './config-merge.js';
|
|
13
|
+
|
|
14
|
+
export class ConfigObject {
|
|
15
|
+
#lastModifiedTime = -Infinity;
|
|
16
|
+
|
|
17
|
+
lastModified;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Update the last modified if newer.
|
|
21
|
+
* @param {string|map|ConfigObject} [httpDate]
|
|
22
|
+
*/
|
|
23
|
+
updateLastModified(httpDate) {
|
|
24
|
+
if (!httpDate) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (httpDate instanceof ConfigObject) {
|
|
28
|
+
if (httpDate.#lastModifiedTime > this.#lastModifiedTime) {
|
|
29
|
+
this.#lastModifiedTime = httpDate.#lastModifiedTime;
|
|
30
|
+
this.lastModified = httpDate.lastModified;
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (typeof httpDate.get === 'function') {
|
|
35
|
+
// eslint-disable-next-line no-param-reassign
|
|
36
|
+
httpDate = httpDate.get('x-source-last-modified') ?? httpDate.get('last-modified');
|
|
37
|
+
}
|
|
38
|
+
if (!httpDate || typeof httpDate !== 'string') {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const time = new Date(httpDate).getTime();
|
|
42
|
+
if (Number.isNaN(time)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (time > this.#lastModifiedTime) {
|
|
46
|
+
this.#lastModifiedTime = time;
|
|
47
|
+
this.lastModified = httpDate;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
merge(other) {
|
|
52
|
+
if (!other) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.data = getMergedConfig(this.data, other.data);
|
|
56
|
+
this.updateLastModified(other);
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/config-view.js
CHANGED
|
@@ -20,8 +20,8 @@ import {
|
|
|
20
20
|
SCOPE_RAW, SCOPE_PUBLIC,
|
|
21
21
|
} from './ConfigContext.js';
|
|
22
22
|
import { resolveLegacyConfig, fetchRobotsTxt, toArray } from './config-legacy.js';
|
|
23
|
-
import { getMergedConfig } from './config-merge.js';
|
|
24
23
|
import { deepGetOrCreate } from './utils.js';
|
|
24
|
+
import { ConfigObject } from './config-object.js';
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* @typedef Config
|
|
@@ -152,7 +152,7 @@ export async function getAccessConfig(ctx, config, partition, rso) {
|
|
|
152
152
|
* @param ctx the context
|
|
153
153
|
* @param config the config
|
|
154
154
|
* @param partition the partition
|
|
155
|
-
* @returns {Promise<
|
|
155
|
+
* @returns {Promise<ConfigObject|{}>} the metadata
|
|
156
156
|
*/
|
|
157
157
|
async function loadMetadata(ctx, config, partition) {
|
|
158
158
|
const paths = config.metadata?.source ?? [];
|
|
@@ -161,6 +161,7 @@ async function loadMetadata(ctx, config, partition) {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
// generate the metadata-all.json first
|
|
164
|
+
const metadataConfig = new ConfigObject();
|
|
164
165
|
const metadata = [];
|
|
165
166
|
for (const path of paths) {
|
|
166
167
|
const key = `${config.content.contentBusId}/${partition}${path}`;
|
|
@@ -173,25 +174,25 @@ async function loadMetadata(ctx, config, partition) {
|
|
|
173
174
|
metadata.push(...data);
|
|
174
175
|
}
|
|
175
176
|
}
|
|
177
|
+
metadataConfig.updateLastModified(res.headers);
|
|
176
178
|
}
|
|
177
179
|
if (!metadata.length) {
|
|
178
180
|
return {};
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
// convert to modifiers map
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
data: ModifiersConfig.parseModifierSheet(metadata),
|
|
185
|
-
};
|
|
184
|
+
metadataConfig.data = ModifiersConfig.parseModifierSheet(metadata);
|
|
185
|
+
return metadataConfig;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
async function loadHeadHtml(ctx, config, ref) {
|
|
189
189
|
const key = `${config.code.owner}/${config.code.repo}/${ref}/head.html`;
|
|
190
190
|
const res = await ctx.loader.getObject(HELIX_CODE_BUS, key);
|
|
191
191
|
if (res.body) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
const head = new ConfigObject();
|
|
193
|
+
head.html = res.body;
|
|
194
|
+
head.updateLastModified(res.headers);
|
|
195
|
+
return head;
|
|
195
196
|
}
|
|
196
197
|
return {};
|
|
197
198
|
}
|
|
@@ -214,7 +215,10 @@ async function loadProfile(ctx, rso, name) {
|
|
|
214
215
|
const profileKey = `orgs/${rso.org}/profiles/${name}.json`;
|
|
215
216
|
const res = await ctx.loader.getObject(HELIX_CONFIG_BUS, profileKey);
|
|
216
217
|
if (res.body) {
|
|
217
|
-
|
|
218
|
+
const profile = new ConfigObject();
|
|
219
|
+
profile.data = res.json();
|
|
220
|
+
profile.updateLastModified(res.headers);
|
|
221
|
+
return profile;
|
|
218
222
|
}
|
|
219
223
|
return null;
|
|
220
224
|
}
|
|
@@ -224,7 +228,7 @@ async function loadProfile(ctx, rso, name) {
|
|
|
224
228
|
* @param ctx
|
|
225
229
|
* @param {RSO} rso
|
|
226
230
|
* @param {string} scope
|
|
227
|
-
* @return {Promise<
|
|
231
|
+
* @return {Promise<ConfigObject|null>}
|
|
228
232
|
*/
|
|
229
233
|
async function resolveConfig(ctx, rso, scope) {
|
|
230
234
|
// try to load site config from config-bus
|
|
@@ -235,39 +239,42 @@ async function resolveConfig(ctx, rso, scope) {
|
|
|
235
239
|
const config = await resolveLegacyConfig(ctx, rso, scope);
|
|
236
240
|
if (config) {
|
|
237
241
|
const profile = await loadProfile(ctx, rso, 'default');
|
|
238
|
-
config.tokens = profile?.tokens || {};
|
|
242
|
+
config.data.tokens = profile?.data.tokens || {};
|
|
239
243
|
}
|
|
240
244
|
return config;
|
|
241
245
|
} else {
|
|
242
246
|
return null;
|
|
243
247
|
}
|
|
244
248
|
}
|
|
245
|
-
const site =
|
|
249
|
+
const site = new ConfigObject();
|
|
250
|
+
site.data = res.json();
|
|
251
|
+
site.updateLastModified(res.headers);
|
|
246
252
|
|
|
247
253
|
// always load default profile if available
|
|
248
|
-
if (!site.extends?.profile) {
|
|
249
|
-
site.extends = {
|
|
254
|
+
if (!site.data.extends?.profile) {
|
|
255
|
+
site.data.extends = {
|
|
250
256
|
profile: 'default',
|
|
251
257
|
};
|
|
252
258
|
}
|
|
253
|
-
const profile = await loadProfile(ctx, rso, site.extends.profile);
|
|
254
|
-
|
|
259
|
+
const profile = await loadProfile(ctx, rso, site.data.extends.profile);
|
|
260
|
+
site.merge(profile);
|
|
261
|
+
const config = site.data;
|
|
255
262
|
if (scope === SCOPE_PIPELINE) {
|
|
256
263
|
config.metadata = {
|
|
257
264
|
preview: await loadMetadata(ctx, config, 'preview'),
|
|
258
265
|
live: await loadMetadata(ctx, config, 'live'),
|
|
259
266
|
};
|
|
260
267
|
if (!config.robots) {
|
|
261
|
-
const
|
|
262
|
-
if (txt) {
|
|
263
|
-
config.robots =
|
|
268
|
+
const robots = await fetchRobotsTxt(ctx, config.code.owner, config.code.repo);
|
|
269
|
+
if (robots.txt) {
|
|
270
|
+
config.robots = robots;
|
|
264
271
|
}
|
|
265
272
|
}
|
|
266
273
|
}
|
|
267
274
|
if (scope === SCOPE_PIPELINE || scope === SCOPE_DELIVERY) {
|
|
268
275
|
config.head = await loadHeadHtml(ctx, config, rso.ref);
|
|
269
276
|
}
|
|
270
|
-
return
|
|
277
|
+
return site;
|
|
271
278
|
}
|
|
272
279
|
|
|
273
280
|
async function getSurrogateKey(opts, config) {
|
|
@@ -331,13 +338,11 @@ function resolveGroup(groups, name) {
|
|
|
331
338
|
* @param admin
|
|
332
339
|
* @param configGroups
|
|
333
340
|
* @param orgGroups
|
|
341
|
+
* @param orgUsers
|
|
334
342
|
*/
|
|
335
|
-
function computeSiteAdminRoles(admin, configGroups = {}, orgGroups = {}) {
|
|
336
|
-
if (!admin.role) {
|
|
337
|
-
return admin;
|
|
338
|
-
}
|
|
343
|
+
function computeSiteAdminRoles(admin, configGroups = {}, orgGroups = {}, orgUsers = []) {
|
|
339
344
|
const roles = {};
|
|
340
|
-
for (const [roleName, role] of Object.entries(admin.role)) {
|
|
345
|
+
for (const [roleName, role] of Object.entries(admin.role ?? {})) {
|
|
341
346
|
const users = new Set();
|
|
342
347
|
for (const /* @type string */ entry of role) {
|
|
343
348
|
if (entry.indexOf('@') > 0) {
|
|
@@ -354,10 +359,30 @@ function computeSiteAdminRoles(admin, configGroups = {}, orgGroups = {}) {
|
|
|
354
359
|
}
|
|
355
360
|
roles[roleName] = Array.from(users);
|
|
356
361
|
}
|
|
357
|
-
|
|
362
|
+
// add org users
|
|
363
|
+
const hasRoles = Object.keys(roles).length > 0;
|
|
364
|
+
let hasOrgUsers = false;
|
|
365
|
+
for (const user of orgUsers) {
|
|
366
|
+
for (const role of user.roles) {
|
|
367
|
+
if (!(role in roles)) {
|
|
368
|
+
roles[role] = [];
|
|
369
|
+
}
|
|
370
|
+
if (!roles[role].includes(user.email)) {
|
|
371
|
+
roles[role].push(user.email);
|
|
372
|
+
hasOrgUsers = true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const ret = {
|
|
358
378
|
...admin,
|
|
359
379
|
role: roles,
|
|
360
380
|
};
|
|
381
|
+
// if there are only roles from the org, ensure that they don't enforce auth
|
|
382
|
+
if (hasOrgUsers && !hasRoles && (!admin.requireAuth || admin.requireAuth === 'auto')) {
|
|
383
|
+
ret.requireAuth = false;
|
|
384
|
+
}
|
|
385
|
+
return ret;
|
|
361
386
|
}
|
|
362
387
|
|
|
363
388
|
export async function getConfigResponse(ctx, opts) {
|
|
@@ -374,21 +399,22 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
374
399
|
}
|
|
375
400
|
|
|
376
401
|
const rso = { ref, site, org };
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
-
'x-surrogate-key': await getSurrogateKey(opts,
|
|
402
|
+
const siteConfig = await resolveConfig(ctx, rso, scope);
|
|
403
|
+
const headers = {
|
|
404
|
+
'x-surrogate-key': await getSurrogateKey(opts, siteConfig?.data),
|
|
380
405
|
};
|
|
381
|
-
if (!
|
|
406
|
+
if (!siteConfig) {
|
|
382
407
|
return new PipelineResponse('', {
|
|
383
408
|
status: 404,
|
|
384
409
|
headers: {
|
|
385
|
-
...
|
|
410
|
+
...headers,
|
|
386
411
|
'x-error': 'config not found.',
|
|
387
412
|
},
|
|
388
413
|
});
|
|
389
414
|
}
|
|
390
415
|
|
|
391
|
-
|
|
416
|
+
const config = siteConfig.data;
|
|
417
|
+
if (scope !== SCOPE_RAW) {
|
|
392
418
|
delete config.extends;
|
|
393
419
|
}
|
|
394
420
|
|
|
@@ -402,14 +428,27 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
402
428
|
// access.require.repository ?
|
|
403
429
|
};
|
|
404
430
|
if (opts.scope === SCOPE_ADMIN || opts.scope === SCOPE_RAW) {
|
|
405
|
-
|
|
431
|
+
// eslint-disable-next-line max-len
|
|
432
|
+
config.access.admin = computeSiteAdminRoles(admin, config.groups, orgConfig?.groups, orgConfig?.users);
|
|
406
433
|
}
|
|
407
434
|
}
|
|
408
435
|
|
|
436
|
+
// todo: improve
|
|
437
|
+
siteConfig.updateLastModified(config.head);
|
|
438
|
+
siteConfig.updateLastModified(config.robots);
|
|
439
|
+
siteConfig.updateLastModified(config.metadata?.preview);
|
|
440
|
+
siteConfig.updateLastModified(config.metadata?.live);
|
|
441
|
+
|
|
442
|
+
if (siteConfig.lastModified) {
|
|
443
|
+
headers['last-modified'] = siteConfig.lastModified;
|
|
444
|
+
delete siteConfig.lastModified;
|
|
445
|
+
delete siteConfig.lastModifiedTime;
|
|
446
|
+
}
|
|
447
|
+
|
|
409
448
|
if (opts.scope === SCOPE_DELIVERY) {
|
|
410
449
|
return new PipelineResponse('', {
|
|
411
450
|
headers: {
|
|
412
|
-
...
|
|
451
|
+
...headers,
|
|
413
452
|
'x-hlx-contentbus-id': config.content.contentBusId,
|
|
414
453
|
'x-hlx-owner': config.code.owner,
|
|
415
454
|
'x-hlx-repo': config.code.repo,
|
|
@@ -445,7 +484,7 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
445
484
|
return new PipelineResponse(JSON.stringify(adminConfig, null, 2), {
|
|
446
485
|
headers: {
|
|
447
486
|
'content-type': 'application/json',
|
|
448
|
-
...
|
|
487
|
+
...headers,
|
|
449
488
|
},
|
|
450
489
|
});
|
|
451
490
|
}
|
|
@@ -455,7 +494,7 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
455
494
|
return new PipelineResponse(JSON.stringify(config, null, 2), {
|
|
456
495
|
headers: {
|
|
457
496
|
'content-type': 'application/json',
|
|
458
|
-
...
|
|
497
|
+
...headers,
|
|
459
498
|
},
|
|
460
499
|
});
|
|
461
500
|
}
|
|
@@ -482,7 +521,7 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
482
521
|
return new PipelineResponse(JSON.stringify(pipelineConfig, null, 2), {
|
|
483
522
|
headers: {
|
|
484
523
|
'content-type': 'application/json',
|
|
485
|
-
...
|
|
524
|
+
...headers,
|
|
486
525
|
},
|
|
487
526
|
});
|
|
488
527
|
}
|
|
@@ -496,7 +535,7 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
496
535
|
return new PipelineResponse(JSON.stringify(publicConfig, null, 2), {
|
|
497
536
|
headers: {
|
|
498
537
|
'content-type': 'application/json',
|
|
499
|
-
...
|
|
538
|
+
...headers,
|
|
500
539
|
},
|
|
501
540
|
});
|
|
502
541
|
}
|