@adobe/helix-config 2.7.1 → 2.9.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 +1 -1
- package/src/config-legacy.js +19 -0
- package/src/config-view.js +35 -7
- package/src/schemas/access-site.schema.json +1 -8
- package/src/schemas/profile.schema.json +6 -0
- package/src/schemas/robots.schema.cjs +12 -0
- package/src/schemas/robots.schema.json +23 -0
- package/src/schemas/site.schema.json +6 -0
- package/src/schemas/tokens.schema.cjs +12 -0
- package/src/schemas/tokens.schema.json +38 -0
- package/src/storage/config-store.js +137 -41
- package/src/storage/config-validator.js +4 -0
- package/src/storage/utils.js +23 -0
- package/types/profile-config.d.ts +17 -5
- package/types/site-config.d.ts +17 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [2.9.0](https://github.com/adobe/helix-config/compare/v2.8.0...v2.9.0) (2024-04-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add token support ([d8e25f5](https://github.com/adobe/helix-config/commit/d8e25f58ae8b083f9fa2970d84e5ab6f03c26372)), closes [#42](https://github.com/adobe/helix-config/issues/42)
|
|
7
|
+
|
|
8
|
+
# [2.8.0](https://github.com/adobe/helix-config/compare/v2.7.1...v2.8.0) (2024-04-24)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add support for robots.txt ([#57](https://github.com/adobe/helix-config/issues/57)) ([dedfec3](https://github.com/adobe/helix-config/commit/dedfec38c0ff97953b71ea3d71a4547f981eb56d)), closes [#56](https://github.com/adobe/helix-config/issues/56)
|
|
14
|
+
|
|
1
15
|
## [2.7.1](https://github.com/adobe/helix-config/compare/v2.7.0...v2.7.1) (2024-04-24)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
package/src/config-legacy.js
CHANGED
|
@@ -66,6 +66,19 @@ async function fetchConfigAll(ctx, contentBusId, partition) {
|
|
|
66
66
|
return res.json();
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Retrieves the robots.txt from the code-bus
|
|
71
|
+
* @param {ConfigContext} ctx the context
|
|
72
|
+
* @param {string} owner
|
|
73
|
+
* @param {string} repo
|
|
74
|
+
* @returns {Promise<string|null>} the robots.txt
|
|
75
|
+
*/
|
|
76
|
+
export async function fetchRobotsTxt(ctx, owner, repo) {
|
|
77
|
+
const key = `${owner}/${repo}/main/robots.txt`;
|
|
78
|
+
const res = await ctx.loader.getObject(HELIX_CODE_BUS, key);
|
|
79
|
+
return res.body;
|
|
80
|
+
}
|
|
81
|
+
|
|
69
82
|
/**
|
|
70
83
|
* Loads the content from a helix 4 project.
|
|
71
84
|
* @param {ConfigContext} ctx
|
|
@@ -131,6 +144,12 @@ export async function resolveLegacyConfig(ctx, rso, scope) {
|
|
|
131
144
|
if (configAllLive?.metadata) {
|
|
132
145
|
config.metadata.live = configAllLive.metadata;
|
|
133
146
|
}
|
|
147
|
+
const robots = await fetchRobotsTxt(ctx, rso.org, rso.site);
|
|
148
|
+
if (robots) {
|
|
149
|
+
config.robots = {
|
|
150
|
+
txt: robots,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
134
153
|
}
|
|
135
154
|
return config;
|
|
136
155
|
}
|
package/src/config-view.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
SCOPE_DELIVERY,
|
|
19
19
|
SCOPE_RAW,
|
|
20
20
|
} from './ConfigContext.js';
|
|
21
|
-
import { resolveLegacyConfig } from './config-legacy.js';
|
|
21
|
+
import { resolveLegacyConfig, fetchRobotsTxt } from './config-legacy.js';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* @typedef Config
|
|
@@ -72,12 +72,26 @@ export function canonicalArrayString(root, partition, prop) {
|
|
|
72
72
|
/**
|
|
73
73
|
* Returns the normalized access configuration for the give partition.
|
|
74
74
|
*/
|
|
75
|
-
export function getAccessConfig(
|
|
76
|
-
|
|
75
|
+
export function getAccessConfig(config, partition) {
|
|
76
|
+
const { access, tokens = {} } = config;
|
|
77
|
+
const apiKeyId = toArray(access[partition]?.apiKeyId ?? access.apiKeyId);
|
|
78
|
+
const cfg = {
|
|
77
79
|
allow: toArray(access[partition]?.allow ?? access.allow),
|
|
78
|
-
apiKeyId
|
|
80
|
+
apiKeyId,
|
|
81
|
+
tokenHash: apiKeyId.map((id) => tokens[id]?.hash).filter((hash) => !!hash),
|
|
79
82
|
clientCertDN: toArray(access[partition]?.clientCertDN ?? access.clientCertDN),
|
|
80
83
|
};
|
|
84
|
+
// if an allow is defined but no apiKeyId, create a fake one so that auth is still
|
|
85
|
+
// enforced. later we can remove the allow and the apiKeyId in favor of the tokenHash
|
|
86
|
+
if (cfg.allow.length && !cfg.apiKeyId.length) {
|
|
87
|
+
cfg.apiKeyId.push('fake');
|
|
88
|
+
}
|
|
89
|
+
// if an apiKeyId is defined but no tokenHash, create a fake one so that auth is still
|
|
90
|
+
// enforced.
|
|
91
|
+
if (cfg.apiKeyId.length && !cfg.tokenHash.length) {
|
|
92
|
+
cfg.tokenHash.push('n/a');
|
|
93
|
+
}
|
|
94
|
+
return cfg;
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
/**
|
|
@@ -167,6 +181,12 @@ async function resolveConfig(ctx, rso, scope) {
|
|
|
167
181
|
preview: await loadMetadata(ctx, config, 'preview'),
|
|
168
182
|
live: await loadMetadata(ctx, config, 'live'),
|
|
169
183
|
};
|
|
184
|
+
if (!config.robots) {
|
|
185
|
+
const txt = await fetchRobotsTxt(ctx, config.code.owner, config.code.repo);
|
|
186
|
+
if (txt) {
|
|
187
|
+
config.robots = { txt };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
170
190
|
}
|
|
171
191
|
if (scope === SCOPE_PIPELINE || scope === SCOPE_DELIVERY) {
|
|
172
192
|
config.head = await loadHeadHtml(ctx, config, rso.ref);
|
|
@@ -205,8 +225,8 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
205
225
|
// normalize access config
|
|
206
226
|
const { admin = {} } = config.access;
|
|
207
227
|
config.access = {
|
|
208
|
-
preview: getAccessConfig(config
|
|
209
|
-
live: getAccessConfig(config
|
|
228
|
+
preview: getAccessConfig(config, 'preview'),
|
|
229
|
+
live: getAccessConfig(config, 'live'),
|
|
210
230
|
// access.require.repository ?
|
|
211
231
|
};
|
|
212
232
|
if (opts.scope === SCOPE_ADMIN || opts.scope === SCOPE_RAW) {
|
|
@@ -235,13 +255,20 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
235
255
|
'x-hlx-auth-allow-preview': canonicalArrayString(config.access, 'preview', 'allow'),
|
|
236
256
|
'x-hlx-auth-apikey-preview': canonicalArrayString(config.access, 'preview', 'apiKeyId'),
|
|
237
257
|
'x-hlx-auth-clientdn-preview': canonicalArrayString(config.access, 'preview', 'clientCertDN'),
|
|
258
|
+
'x-hlx-auth-hash-preview': canonicalArrayString(config.access, 'preview', 'tokenHash'),
|
|
238
259
|
'x-hlx-auth-allow-live': canonicalArrayString(config.access, 'live', 'allow'),
|
|
239
260
|
'x-hlx-auth-apikey-live': canonicalArrayString(config.access, 'live', 'apiKeyId'),
|
|
240
261
|
'x-hlx-auth-clientdn-live': canonicalArrayString(config.access, 'live', 'clientCertDN'),
|
|
262
|
+
'x-hlx-auth-hash-live': canonicalArrayString(config.access, 'live', 'tokenHash'),
|
|
241
263
|
},
|
|
242
264
|
});
|
|
243
265
|
}
|
|
244
266
|
|
|
267
|
+
// delete token hashes
|
|
268
|
+
delete config.tokens;
|
|
269
|
+
delete config.access?.preview?.tokenHash;
|
|
270
|
+
delete config.access?.live?.tokenHash;
|
|
271
|
+
|
|
245
272
|
if (opts.scope === SCOPE_ADMIN) {
|
|
246
273
|
const adminConfig = {
|
|
247
274
|
...rso,
|
|
@@ -254,6 +281,7 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
254
281
|
},
|
|
255
282
|
};
|
|
256
283
|
delete adminConfig.public;
|
|
284
|
+
delete adminConfig.robots;
|
|
257
285
|
return new PipelineResponse(JSON.stringify(adminConfig, null, 2), {
|
|
258
286
|
headers: {
|
|
259
287
|
'content-type': 'application/json',
|
|
@@ -281,13 +309,13 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
281
309
|
repo: config.code.repo,
|
|
282
310
|
...rso,
|
|
283
311
|
contentBusId: config.content.contentBusId,
|
|
284
|
-
access: config.access,
|
|
285
312
|
headers: config.headers,
|
|
286
313
|
head: config.head,
|
|
287
314
|
metadata: config.metadata,
|
|
288
315
|
cdn: config.cdn,
|
|
289
316
|
folders: config.folders,
|
|
290
317
|
public: config.public,
|
|
318
|
+
robots: config.robots,
|
|
291
319
|
};
|
|
292
320
|
return new PipelineResponse(JSON.stringify(pipelineConfig, null, 2), {
|
|
293
321
|
headers: {
|
|
@@ -15,15 +15,8 @@
|
|
|
15
15
|
"title": "Site Access Config",
|
|
16
16
|
"type": "object",
|
|
17
17
|
"properties": {
|
|
18
|
-
"allow": {
|
|
19
|
-
"description": "The email glob of the users that are allowed.",
|
|
20
|
-
"type": "array",
|
|
21
|
-
"items": {
|
|
22
|
-
"type": "string"
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
18
|
"apiKeyId": {
|
|
26
|
-
"description": "IDs of the api keys that are allowed.",
|
|
19
|
+
"description": "IDs of the api keys (tokens) that are allowed.",
|
|
27
20
|
"type": "array",
|
|
28
21
|
"items": {
|
|
29
22
|
"type": "string"
|
|
@@ -46,6 +46,12 @@
|
|
|
46
46
|
},
|
|
47
47
|
"metadata": {
|
|
48
48
|
"$ref": "https://ns.adobe.com/helix/config/metadata-source"
|
|
49
|
+
},
|
|
50
|
+
"robots": {
|
|
51
|
+
"$ref": "https://ns.adobe.com/helix/config/robots"
|
|
52
|
+
},
|
|
53
|
+
"tokens": {
|
|
54
|
+
"$ref": "https://ns.adobe.com/helix/config/tokens"
|
|
49
55
|
}
|
|
50
56
|
},
|
|
51
57
|
"required": [
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
module.exports = require('./robots.schema.json');
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta:license": [
|
|
3
|
+
"Copyright 2024 Adobe. All rights reserved.",
|
|
4
|
+
"This file is licensed to you under the Apache License, Version 2.0 (the \"License\");",
|
|
5
|
+
"you may not use this file except in compliance with the License. You may obtain a copy",
|
|
6
|
+
"of the License at http://www.apache.org/licenses/LICENSE-2.0",
|
|
7
|
+
"",
|
|
8
|
+
"Unless required by applicable law or agreed to in writing, software distributed under",
|
|
9
|
+
"the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS",
|
|
10
|
+
"OF ANY KIND, either express or implied. See the License for the specific language",
|
|
11
|
+
"governing permissions and limitations under the License."
|
|
12
|
+
],
|
|
13
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
14
|
+
"$id": "https://ns.adobe.com/helix/config/robots",
|
|
15
|
+
"type": "object",
|
|
16
|
+
"title": "robots",
|
|
17
|
+
"properties": {
|
|
18
|
+
"txt": {
|
|
19
|
+
"type": "string"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"additionalProperties": false
|
|
23
|
+
}
|
|
@@ -51,6 +51,12 @@
|
|
|
51
51
|
},
|
|
52
52
|
"metadata": {
|
|
53
53
|
"$ref": "https://ns.adobe.com/helix/config/metadata-source"
|
|
54
|
+
},
|
|
55
|
+
"robots": {
|
|
56
|
+
"$ref": "https://ns.adobe.com/helix/config/robots"
|
|
57
|
+
},
|
|
58
|
+
"tokens": {
|
|
59
|
+
"$ref": "https://ns.adobe.com/helix/config/tokens"
|
|
54
60
|
}
|
|
55
61
|
},
|
|
56
62
|
"required": [
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
module.exports = require('./tokens.schema.json');
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta:license": [
|
|
3
|
+
"Copyright 2024 Adobe. All rights reserved.",
|
|
4
|
+
"This file is licensed to you under the Apache License, Version 2.0 (the \"License\");",
|
|
5
|
+
"you may not use this file except in compliance with the License. You may obtain a copy",
|
|
6
|
+
"of the License at http://www.apache.org/licenses/LICENSE-2.0",
|
|
7
|
+
"",
|
|
8
|
+
"Unless required by applicable law or agreed to in writing, software distributed under",
|
|
9
|
+
"the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS",
|
|
10
|
+
"OF ANY KIND, either express or implied. See the License for the specific language",
|
|
11
|
+
"governing permissions and limitations under the License."
|
|
12
|
+
],
|
|
13
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
14
|
+
"$id": "https://ns.adobe.com/helix/config/tokens",
|
|
15
|
+
"title": "tokens",
|
|
16
|
+
"type": "object",
|
|
17
|
+
"patternProperties": {
|
|
18
|
+
"^[a-zA-Z0-9-_=]+$": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {
|
|
21
|
+
"id": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"pattern": "^[a-zA-Z0-9-_=]+$"
|
|
24
|
+
},
|
|
25
|
+
"hash": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"pattern": "^[a-zA-Z0-9-_=]+$"
|
|
28
|
+
},
|
|
29
|
+
"created": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"format": "date-time"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"additionalProperties": false
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"additionalProperties": false
|
|
38
|
+
}
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
|
+
/* eslint-disable no-param-reassign */
|
|
12
13
|
import { HelixStorage } from './storage.js';
|
|
13
14
|
import { StatusCodeError } from './status-code-error.js';
|
|
14
15
|
import {
|
|
16
|
+
createToken,
|
|
15
17
|
jsonGet,
|
|
16
18
|
jsonPut,
|
|
17
19
|
updateCodeSource,
|
|
@@ -19,26 +21,111 @@ import {
|
|
|
19
21
|
} from './utils.js';
|
|
20
22
|
import { validate } from './config-validator.js';
|
|
21
23
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
'
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
const FRAGMENTS_COMMON = {
|
|
25
|
+
content: 'object',
|
|
26
|
+
code: 'object',
|
|
27
|
+
folders: 'object',
|
|
28
|
+
headers: 'object',
|
|
29
|
+
metadata: 'object',
|
|
30
|
+
sidekick: 'object',
|
|
31
|
+
cdn: {
|
|
32
|
+
'.': 'object',
|
|
33
|
+
prod: 'object',
|
|
34
|
+
preview: 'object',
|
|
35
|
+
live: 'object',
|
|
36
|
+
},
|
|
37
|
+
access: {
|
|
38
|
+
'.': 'object',
|
|
39
|
+
admin: 'object',
|
|
40
|
+
preview: 'object',
|
|
41
|
+
live: 'object',
|
|
42
|
+
},
|
|
43
|
+
public: 'object',
|
|
44
|
+
robots: 'object',
|
|
45
|
+
tokens: {
|
|
46
|
+
'.': 'tokens',
|
|
47
|
+
'.param': {
|
|
48
|
+
name: 'id',
|
|
49
|
+
'.': 'token',
|
|
50
|
+
},
|
|
39
51
|
},
|
|
40
52
|
};
|
|
41
53
|
|
|
54
|
+
const FRAGMENTS = {
|
|
55
|
+
sites: FRAGMENTS_COMMON,
|
|
56
|
+
profiles: FRAGMENTS_COMMON,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function getFragmentInfo(type, relPath) {
|
|
60
|
+
if (!relPath) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const parts = relPath.split('/');
|
|
64
|
+
let fragment = FRAGMENTS[type];
|
|
65
|
+
const info = {
|
|
66
|
+
relPath,
|
|
67
|
+
};
|
|
68
|
+
for (const part of parts) {
|
|
69
|
+
let next = fragment[part];
|
|
70
|
+
if (!next) {
|
|
71
|
+
next = fragment['.param'];
|
|
72
|
+
if (!next) {
|
|
73
|
+
throw new StatusCodeError(400, 'invalid object path.');
|
|
74
|
+
}
|
|
75
|
+
info[next.name] = part;
|
|
76
|
+
}
|
|
77
|
+
fragment = next;
|
|
78
|
+
}
|
|
79
|
+
if (typeof fragment === 'string') {
|
|
80
|
+
info.type = fragment;
|
|
81
|
+
} else {
|
|
82
|
+
info.type = fragment['.'];
|
|
83
|
+
}
|
|
84
|
+
return info;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Redact / transform the token config
|
|
89
|
+
* @param token
|
|
90
|
+
*/
|
|
91
|
+
function redactToken(token) {
|
|
92
|
+
return {
|
|
93
|
+
id: token.id,
|
|
94
|
+
created: token.created,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Redact / transform the tokens config
|
|
100
|
+
* @param tokens
|
|
101
|
+
*/
|
|
102
|
+
function redactTokens(tokens) {
|
|
103
|
+
return Object.values(tokens).map(redactToken);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* redact information from the config
|
|
108
|
+
* @param {object} config
|
|
109
|
+
* @param {object} frag
|
|
110
|
+
*/
|
|
111
|
+
function redact(config, frag) {
|
|
112
|
+
if (!config) {
|
|
113
|
+
return config;
|
|
114
|
+
}
|
|
115
|
+
let ret = config;
|
|
116
|
+
if (frag?.type === 'tokens') {
|
|
117
|
+
ret = redactTokens(config);
|
|
118
|
+
}
|
|
119
|
+
if (frag?.type === 'token') {
|
|
120
|
+
ret = redactToken(config);
|
|
121
|
+
}
|
|
122
|
+
if (ret.tokens) {
|
|
123
|
+
// eslint-disable-next-line no-param-reassign
|
|
124
|
+
ret.tokens = redactTokens(ret.tokens);
|
|
125
|
+
}
|
|
126
|
+
return ret;
|
|
127
|
+
}
|
|
128
|
+
|
|
42
129
|
/**
|
|
43
130
|
* General purpose config store.
|
|
44
131
|
*/
|
|
@@ -85,42 +172,54 @@ export class ConfigStore {
|
|
|
85
172
|
return null;
|
|
86
173
|
}
|
|
87
174
|
let obj = JSON.parse(buf);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
throw new StatusCodeError(400, 'invalid object path.');
|
|
92
|
-
}
|
|
93
|
-
obj = jsonGet(obj, relPath);
|
|
175
|
+
const frag = getFragmentInfo(this.type, relPath);
|
|
176
|
+
if (frag) {
|
|
177
|
+
obj = jsonGet(obj, frag.relPath);
|
|
94
178
|
}
|
|
95
|
-
return obj;
|
|
179
|
+
return redact(obj, frag);
|
|
96
180
|
}
|
|
97
181
|
|
|
98
182
|
async update(ctx, data, relPath = '') {
|
|
99
183
|
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
100
184
|
const buf = await storage.get(this.key);
|
|
101
185
|
const old = buf ? JSON.parse(buf) : null;
|
|
102
|
-
|
|
186
|
+
const frag = getFragmentInfo(this.type, relPath);
|
|
187
|
+
let config = data;
|
|
188
|
+
let ret = null;
|
|
189
|
+
if (frag) {
|
|
103
190
|
if (!old) {
|
|
104
191
|
throw new StatusCodeError(404, 'config not found.');
|
|
105
192
|
}
|
|
106
|
-
const fragment = FRAGMENTS[this.type][relPath];
|
|
107
|
-
if (!fragment) {
|
|
108
|
-
throw new StatusCodeError(400, 'invalid object path.');
|
|
109
|
-
}
|
|
110
193
|
if (relPath === 'code') {
|
|
111
194
|
updateCodeSource(ctx, data);
|
|
112
195
|
} else if (relPath === 'content') {
|
|
113
196
|
updateContentSource(ctx, data);
|
|
197
|
+
} else if (frag.type === 'token') {
|
|
198
|
+
// don't allow to update individual token
|
|
199
|
+
throw new StatusCodeError(400, 'invalid object path.');
|
|
200
|
+
} else if (frag.type === 'tokens') {
|
|
201
|
+
// create new token
|
|
202
|
+
const token = createToken(this.org);
|
|
203
|
+
data = {
|
|
204
|
+
...token,
|
|
205
|
+
};
|
|
206
|
+
// don't store token value in the config
|
|
207
|
+
delete data.value;
|
|
208
|
+
relPath = `tokens/${token.id}`;
|
|
209
|
+
frag.type = 'token';
|
|
210
|
+
ret = token;
|
|
211
|
+
// don't expose hash in return value
|
|
212
|
+
delete ret.hash;
|
|
114
213
|
}
|
|
115
|
-
|
|
116
|
-
data = jsonPut(JSON.parse(buf), relPath, data);
|
|
214
|
+
config = jsonPut(old, relPath, data);
|
|
117
215
|
} else {
|
|
118
|
-
updateContentSource(ctx,
|
|
119
|
-
updateCodeSource(ctx,
|
|
216
|
+
updateContentSource(ctx, config.content);
|
|
217
|
+
updateCodeSource(ctx, config.code);
|
|
120
218
|
}
|
|
121
|
-
await validate(
|
|
122
|
-
await storage.put(this.key, JSON.stringify(
|
|
123
|
-
await this.purge(ctx,
|
|
219
|
+
await validate(config, this.type);
|
|
220
|
+
await storage.put(this.key, JSON.stringify(config), 'application/json');
|
|
221
|
+
await this.purge(ctx, buf ? JSON.parse(buf) : null, config);
|
|
222
|
+
return ret ?? redact(data, frag);
|
|
124
223
|
}
|
|
125
224
|
|
|
126
225
|
async remove(ctx, relPath) {
|
|
@@ -129,11 +228,8 @@ export class ConfigStore {
|
|
|
129
228
|
if (!buf) {
|
|
130
229
|
throw new StatusCodeError(404, 'config not found.');
|
|
131
230
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (!fragment) {
|
|
135
|
-
throw new StatusCodeError(400, 'invalid object path.');
|
|
136
|
-
}
|
|
231
|
+
const frag = getFragmentInfo(this.type, relPath);
|
|
232
|
+
if (frag) {
|
|
137
233
|
const data = jsonPut(JSON.parse(buf), relPath, null);
|
|
138
234
|
await validate(data, this.type);
|
|
139
235
|
await storage.put(this.key, JSON.stringify(data), 'application/json');
|
|
@@ -26,8 +26,10 @@ import markupSchema from '../schemas/content-source-markup.schema.cjs';
|
|
|
26
26
|
import metadataSchema from '../schemas/metadata-source.schema.cjs';
|
|
27
27
|
import onedriveSchema from '../schemas/content-source-onedrive.schema.cjs';
|
|
28
28
|
import profileSchema from '../schemas/profile.schema.cjs';
|
|
29
|
+
import robotsSchema from '../schemas/robots.schema.cjs';
|
|
29
30
|
import sidekickSchema from '../schemas/sidekick.schema.cjs';
|
|
30
31
|
import siteSchema from '../schemas/site.schema.cjs';
|
|
32
|
+
import tokensSchema from '../schemas/tokens.schema.cjs';
|
|
31
33
|
|
|
32
34
|
const SCHEMAS = [
|
|
33
35
|
accessSchema,
|
|
@@ -43,8 +45,10 @@ const SCHEMAS = [
|
|
|
43
45
|
metadataSchema,
|
|
44
46
|
onedriveSchema,
|
|
45
47
|
profileSchema,
|
|
48
|
+
robotsSchema,
|
|
46
49
|
sidekickSchema,
|
|
47
50
|
siteSchema,
|
|
51
|
+
tokensSchema,
|
|
48
52
|
];
|
|
49
53
|
|
|
50
54
|
const SCHEMA_TYPES = {
|
package/src/storage/utils.js
CHANGED
|
@@ -111,3 +111,26 @@ export function updateCodeSource(ctx, code) {
|
|
|
111
111
|
}
|
|
112
112
|
return modified;
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creates a random token and hashes it with the given key
|
|
117
|
+
* @param {string} key
|
|
118
|
+
* @returns {object} the token
|
|
119
|
+
*/
|
|
120
|
+
export function createToken(key) {
|
|
121
|
+
const secret = crypto.randomBytes(32).toString('base64url');
|
|
122
|
+
const value = `hlx_${secret}`;
|
|
123
|
+
const hash = crypto
|
|
124
|
+
.createHmac('sha512', key)
|
|
125
|
+
.update(value, 'utf-8')
|
|
126
|
+
.digest()
|
|
127
|
+
.toString('base64url');
|
|
128
|
+
const id = crypto.createHash('sha256').update(hash).digest().toString('base64url');
|
|
129
|
+
const created = new Date().toISOString();
|
|
130
|
+
return {
|
|
131
|
+
id,
|
|
132
|
+
value,
|
|
133
|
+
hash,
|
|
134
|
+
created,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -31,6 +31,8 @@ export interface HelixProfileConfig {
|
|
|
31
31
|
access?: Access;
|
|
32
32
|
sidekick?: SidekickConfig;
|
|
33
33
|
metadata?: Metadata;
|
|
34
|
+
robots?: Robots;
|
|
35
|
+
tokens?: Tokens;
|
|
34
36
|
}
|
|
35
37
|
/**
|
|
36
38
|
* Defines the content bus location and source.
|
|
@@ -222,11 +224,7 @@ export interface Role {
|
|
|
222
224
|
}
|
|
223
225
|
export interface SiteAccessConfig {
|
|
224
226
|
/**
|
|
225
|
-
*
|
|
226
|
-
*/
|
|
227
|
-
allow?: string[];
|
|
228
|
-
/**
|
|
229
|
-
* IDs of the api keys that are allowed.
|
|
227
|
+
* IDs of the api keys (tokens) that are allowed.
|
|
230
228
|
*/
|
|
231
229
|
apiKeyId?: string[];
|
|
232
230
|
/**
|
|
@@ -247,3 +245,17 @@ export interface SidekickPlugin {
|
|
|
247
245
|
export interface Metadata {
|
|
248
246
|
source?: [] | [string];
|
|
249
247
|
}
|
|
248
|
+
export interface Robots {
|
|
249
|
+
txt?: string;
|
|
250
|
+
}
|
|
251
|
+
export interface Tokens {
|
|
252
|
+
/**
|
|
253
|
+
* This interface was referenced by `Tokens`'s JSON-Schema definition
|
|
254
|
+
* via the `patternProperty` "^[a-zA-Z0-9-_=]+$".
|
|
255
|
+
*/
|
|
256
|
+
[k: string]: {
|
|
257
|
+
id?: string;
|
|
258
|
+
hash?: string;
|
|
259
|
+
created?: string;
|
|
260
|
+
};
|
|
261
|
+
}
|
package/types/site-config.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface HelixSiteConfig {
|
|
|
34
34
|
access?: Access;
|
|
35
35
|
sidekick?: SidekickConfig;
|
|
36
36
|
metadata?: Metadata;
|
|
37
|
+
robots?: Robots;
|
|
38
|
+
tokens?: Tokens;
|
|
37
39
|
}
|
|
38
40
|
/**
|
|
39
41
|
* Defines the content bus location and source.
|
|
@@ -225,11 +227,7 @@ export interface Role {
|
|
|
225
227
|
}
|
|
226
228
|
export interface SiteAccessConfig {
|
|
227
229
|
/**
|
|
228
|
-
*
|
|
229
|
-
*/
|
|
230
|
-
allow?: string[];
|
|
231
|
-
/**
|
|
232
|
-
* IDs of the api keys that are allowed.
|
|
230
|
+
* IDs of the api keys (tokens) that are allowed.
|
|
233
231
|
*/
|
|
234
232
|
apiKeyId?: string[];
|
|
235
233
|
/**
|
|
@@ -250,3 +248,17 @@ export interface SidekickPlugin {
|
|
|
250
248
|
export interface Metadata {
|
|
251
249
|
source?: [] | [string];
|
|
252
250
|
}
|
|
251
|
+
export interface Robots {
|
|
252
|
+
txt?: string;
|
|
253
|
+
}
|
|
254
|
+
export interface Tokens {
|
|
255
|
+
/**
|
|
256
|
+
* This interface was referenced by `Tokens`'s JSON-Schema definition
|
|
257
|
+
* via the `patternProperty` "^[a-zA-Z0-9-_=]+$".
|
|
258
|
+
*/
|
|
259
|
+
[k: string]: {
|
|
260
|
+
id?: string;
|
|
261
|
+
hash?: string;
|
|
262
|
+
created?: string;
|
|
263
|
+
};
|
|
264
|
+
}
|