@adobe/helix-config 2.8.0 → 2.10.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/.husky/pre-commit +1 -0
- package/CHANGELOG.md +14 -0
- package/package.json +3 -2
- package/src/config-view.js +30 -6
- package/src/schemas/access-site.schema.json +1 -8
- package/src/schemas/profile.schema.json +3 -0
- package/src/schemas/site.schema.json +3 -0
- package/src/schemas/tokens.schema.cjs +12 -0
- package/src/schemas/tokens.schema.json +38 -0
- package/src/storage/config-store.js +145 -42
- package/src/storage/config-validator.js +2 -0
- package/src/storage/utils.js +60 -1
- package/types/profile-config.d.ts +13 -5
- package/types/site-config.d.ts +13 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npx lint-staged
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [2.10.0](https://github.com/adobe/helix-config/compare/v2.9.0...v2.10.0) (2024-04-26)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add support to migrate jwt api keys ([#59](https://github.com/adobe/helix-config/issues/59)) ([04adf04](https://github.com/adobe/helix-config/commit/04adf04c975ee38840a56f81aac8a1609959298a))
|
|
7
|
+
|
|
8
|
+
# [2.9.0](https://github.com/adobe/helix-config/compare/v2.8.0...v2.9.0) (2024-04-25)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add token support ([d8e25f5](https://github.com/adobe/helix-config/commit/d8e25f58ae8b083f9fa2970d84e5ab6f03c26372)), closes [#42](https://github.com/adobe/helix-config/issues/42)
|
|
14
|
+
|
|
1
15
|
# [2.8.0](https://github.com/adobe/helix-config/compare/v2.7.1...v2.8.0) (2024-04-24)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/helix-config",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "Helix Config",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"docs:types": "node ./test/dev/generate-types.js",
|
|
16
16
|
"semantic-release": "semantic-release",
|
|
17
17
|
"semantic-release-dry": "semantic-release --dry-run --branches $CI_BRANCH",
|
|
18
|
-
"prepare": "husky
|
|
18
|
+
"prepare": "husky"
|
|
19
19
|
},
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"@smithy/node-http-handler": "2.5.0",
|
|
69
69
|
"ajv": "8.12.0",
|
|
70
70
|
"ajv-formats": "3.0.1",
|
|
71
|
+
"jose": "5.2.4",
|
|
71
72
|
"mime": "4.0.1"
|
|
72
73
|
}
|
|
73
74
|
}
|
package/src/config-view.js
CHANGED
|
@@ -72,12 +72,30 @@ 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
|
|
82
|
+
// token ids are always stored in base64url format, but legacy apiKeyIds are not
|
|
83
|
+
.map((jti) => jti.replaceAll('/', '_').replaceAll('+', '-'))
|
|
84
|
+
.map((id) => tokens[id]?.hash)
|
|
85
|
+
.filter((hash) => !!hash),
|
|
79
86
|
clientCertDN: toArray(access[partition]?.clientCertDN ?? access.clientCertDN),
|
|
80
87
|
};
|
|
88
|
+
// if an allow is defined but no apiKeyId, create a fake one so that auth is still
|
|
89
|
+
// enforced. later we can remove the allow and the apiKeyId in favor of the tokenHash
|
|
90
|
+
if (cfg.allow.length && !cfg.apiKeyId.length) {
|
|
91
|
+
cfg.apiKeyId.push('fake');
|
|
92
|
+
}
|
|
93
|
+
// if an apiKeyId is defined but no tokenHash, create a fake one so that auth is still
|
|
94
|
+
// enforced.
|
|
95
|
+
if (cfg.apiKeyId.length && !cfg.tokenHash.length) {
|
|
96
|
+
cfg.tokenHash.push('n/a');
|
|
97
|
+
}
|
|
98
|
+
return cfg;
|
|
81
99
|
}
|
|
82
100
|
|
|
83
101
|
/**
|
|
@@ -211,8 +229,8 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
211
229
|
// normalize access config
|
|
212
230
|
const { admin = {} } = config.access;
|
|
213
231
|
config.access = {
|
|
214
|
-
preview: getAccessConfig(config
|
|
215
|
-
live: getAccessConfig(config
|
|
232
|
+
preview: getAccessConfig(config, 'preview'),
|
|
233
|
+
live: getAccessConfig(config, 'live'),
|
|
216
234
|
// access.require.repository ?
|
|
217
235
|
};
|
|
218
236
|
if (opts.scope === SCOPE_ADMIN || opts.scope === SCOPE_RAW) {
|
|
@@ -241,13 +259,20 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
241
259
|
'x-hlx-auth-allow-preview': canonicalArrayString(config.access, 'preview', 'allow'),
|
|
242
260
|
'x-hlx-auth-apikey-preview': canonicalArrayString(config.access, 'preview', 'apiKeyId'),
|
|
243
261
|
'x-hlx-auth-clientdn-preview': canonicalArrayString(config.access, 'preview', 'clientCertDN'),
|
|
262
|
+
'x-hlx-auth-hash-preview': canonicalArrayString(config.access, 'preview', 'tokenHash'),
|
|
244
263
|
'x-hlx-auth-allow-live': canonicalArrayString(config.access, 'live', 'allow'),
|
|
245
264
|
'x-hlx-auth-apikey-live': canonicalArrayString(config.access, 'live', 'apiKeyId'),
|
|
246
265
|
'x-hlx-auth-clientdn-live': canonicalArrayString(config.access, 'live', 'clientCertDN'),
|
|
266
|
+
'x-hlx-auth-hash-live': canonicalArrayString(config.access, 'live', 'tokenHash'),
|
|
247
267
|
},
|
|
248
268
|
});
|
|
249
269
|
}
|
|
250
270
|
|
|
271
|
+
// delete token hashes
|
|
272
|
+
delete config.tokens;
|
|
273
|
+
delete config.access?.preview?.tokenHash;
|
|
274
|
+
delete config.access?.live?.tokenHash;
|
|
275
|
+
|
|
251
276
|
if (opts.scope === SCOPE_ADMIN) {
|
|
252
277
|
const adminConfig = {
|
|
253
278
|
...rso,
|
|
@@ -288,7 +313,6 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
288
313
|
repo: config.code.repo,
|
|
289
314
|
...rso,
|
|
290
315
|
contentBusId: config.content.contentBusId,
|
|
291
|
-
access: config.access,
|
|
292
316
|
headers: config.headers,
|
|
293
317
|
head: config.head,
|
|
294
318
|
metadata: config.metadata,
|
|
@@ -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"
|
|
@@ -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,37 +9,124 @@
|
|
|
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,
|
|
19
|
+
migrateToken,
|
|
17
20
|
updateCodeSource,
|
|
18
21
|
updateContentSource,
|
|
19
22
|
} from './utils.js';
|
|
20
23
|
import { validate } from './config-validator.js';
|
|
21
24
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
'
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
25
|
+
const FRAGMENTS_COMMON = {
|
|
26
|
+
content: 'object',
|
|
27
|
+
code: 'object',
|
|
28
|
+
folders: 'object',
|
|
29
|
+
headers: 'object',
|
|
30
|
+
metadata: 'object',
|
|
31
|
+
sidekick: 'object',
|
|
32
|
+
cdn: {
|
|
33
|
+
'.': 'object',
|
|
34
|
+
prod: 'object',
|
|
35
|
+
preview: 'object',
|
|
36
|
+
live: 'object',
|
|
37
|
+
},
|
|
38
|
+
access: {
|
|
39
|
+
'.': 'object',
|
|
40
|
+
admin: 'object',
|
|
41
|
+
preview: 'object',
|
|
42
|
+
live: 'object',
|
|
43
|
+
},
|
|
44
|
+
public: 'object',
|
|
45
|
+
robots: 'object',
|
|
46
|
+
tokens: {
|
|
47
|
+
'.': 'tokens',
|
|
48
|
+
'.param': {
|
|
49
|
+
name: 'id',
|
|
50
|
+
'.': 'token',
|
|
51
|
+
},
|
|
40
52
|
},
|
|
41
53
|
};
|
|
42
54
|
|
|
55
|
+
const FRAGMENTS = {
|
|
56
|
+
sites: FRAGMENTS_COMMON,
|
|
57
|
+
profiles: FRAGMENTS_COMMON,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function getFragmentInfo(type, relPath) {
|
|
61
|
+
if (!relPath) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const parts = relPath.split('/');
|
|
65
|
+
let fragment = FRAGMENTS[type];
|
|
66
|
+
const info = {
|
|
67
|
+
relPath,
|
|
68
|
+
};
|
|
69
|
+
for (const part of parts) {
|
|
70
|
+
let next = fragment[part];
|
|
71
|
+
if (!next) {
|
|
72
|
+
next = fragment['.param'];
|
|
73
|
+
if (!next) {
|
|
74
|
+
throw new StatusCodeError(400, 'invalid object path.');
|
|
75
|
+
}
|
|
76
|
+
info[next.name] = part;
|
|
77
|
+
}
|
|
78
|
+
fragment = next;
|
|
79
|
+
}
|
|
80
|
+
if (typeof fragment === 'string') {
|
|
81
|
+
info.type = fragment;
|
|
82
|
+
} else {
|
|
83
|
+
info.type = fragment['.'];
|
|
84
|
+
}
|
|
85
|
+
return info;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Redact / transform the token config
|
|
90
|
+
* @param token
|
|
91
|
+
*/
|
|
92
|
+
function redactToken(token) {
|
|
93
|
+
return {
|
|
94
|
+
id: token.id,
|
|
95
|
+
created: token.created,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Redact / transform the tokens config
|
|
101
|
+
* @param tokens
|
|
102
|
+
*/
|
|
103
|
+
function redactTokens(tokens) {
|
|
104
|
+
return Object.values(tokens).map(redactToken);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* redact information from the config
|
|
109
|
+
* @param {object} config
|
|
110
|
+
* @param {object} frag
|
|
111
|
+
*/
|
|
112
|
+
function redact(config, frag) {
|
|
113
|
+
if (!config) {
|
|
114
|
+
return config;
|
|
115
|
+
}
|
|
116
|
+
let ret = config;
|
|
117
|
+
if (frag?.type === 'tokens') {
|
|
118
|
+
ret = redactTokens(config);
|
|
119
|
+
}
|
|
120
|
+
if (frag?.type === 'token') {
|
|
121
|
+
ret = redactToken(config);
|
|
122
|
+
}
|
|
123
|
+
if (ret.tokens) {
|
|
124
|
+
// eslint-disable-next-line no-param-reassign
|
|
125
|
+
ret.tokens = redactTokens(ret.tokens);
|
|
126
|
+
}
|
|
127
|
+
return ret;
|
|
128
|
+
}
|
|
129
|
+
|
|
43
130
|
/**
|
|
44
131
|
* General purpose config store.
|
|
45
132
|
*/
|
|
@@ -86,42 +173,61 @@ export class ConfigStore {
|
|
|
86
173
|
return null;
|
|
87
174
|
}
|
|
88
175
|
let obj = JSON.parse(buf);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
throw new StatusCodeError(400, 'invalid object path.');
|
|
93
|
-
}
|
|
94
|
-
obj = jsonGet(obj, relPath);
|
|
176
|
+
const frag = getFragmentInfo(this.type, relPath);
|
|
177
|
+
if (frag) {
|
|
178
|
+
obj = jsonGet(obj, frag.relPath);
|
|
95
179
|
}
|
|
96
|
-
return obj;
|
|
180
|
+
return redact(obj, frag);
|
|
97
181
|
}
|
|
98
182
|
|
|
99
183
|
async update(ctx, data, relPath = '') {
|
|
100
184
|
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
101
185
|
const buf = await storage.get(this.key);
|
|
102
186
|
const old = buf ? JSON.parse(buf) : null;
|
|
103
|
-
|
|
187
|
+
const frag = getFragmentInfo(this.type, relPath);
|
|
188
|
+
let config = data;
|
|
189
|
+
let ret = null;
|
|
190
|
+
if (frag) {
|
|
104
191
|
if (!old) {
|
|
105
192
|
throw new StatusCodeError(404, 'config not found.');
|
|
106
193
|
}
|
|
107
|
-
const fragment = FRAGMENTS[this.type][relPath];
|
|
108
|
-
if (!fragment) {
|
|
109
|
-
throw new StatusCodeError(400, 'invalid object path.');
|
|
110
|
-
}
|
|
111
194
|
if (relPath === 'code') {
|
|
112
195
|
updateCodeSource(ctx, data);
|
|
113
196
|
} else if (relPath === 'content') {
|
|
114
197
|
updateContentSource(ctx, data);
|
|
198
|
+
} else if (frag.type === 'token') {
|
|
199
|
+
// don't allow to update individual token
|
|
200
|
+
throw new StatusCodeError(400, 'invalid object path.');
|
|
201
|
+
} else if (frag.type === 'tokens') {
|
|
202
|
+
// TODO: remove support after all helix4 projects are migrated
|
|
203
|
+
let token;
|
|
204
|
+
if (data.jwt) {
|
|
205
|
+
token = await migrateToken(this.org, data.jwt);
|
|
206
|
+
} else {
|
|
207
|
+
// create new token
|
|
208
|
+
token = createToken(this.org);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
data = {
|
|
212
|
+
...token,
|
|
213
|
+
};
|
|
214
|
+
// don't store token value in the config
|
|
215
|
+
delete data.value;
|
|
216
|
+
relPath = ['tokens', token.id];
|
|
217
|
+
frag.type = 'token';
|
|
218
|
+
ret = token;
|
|
219
|
+
// don't expose hash in return value
|
|
220
|
+
delete ret.hash;
|
|
115
221
|
}
|
|
116
|
-
|
|
117
|
-
data = jsonPut(JSON.parse(buf), relPath, data);
|
|
222
|
+
config = jsonPut(old, relPath, data);
|
|
118
223
|
} else {
|
|
119
|
-
updateContentSource(ctx,
|
|
120
|
-
updateCodeSource(ctx,
|
|
224
|
+
updateContentSource(ctx, config.content);
|
|
225
|
+
updateCodeSource(ctx, config.code);
|
|
121
226
|
}
|
|
122
|
-
await validate(
|
|
123
|
-
await storage.put(this.key, JSON.stringify(
|
|
124
|
-
await this.purge(ctx,
|
|
227
|
+
await validate(config, this.type);
|
|
228
|
+
await storage.put(this.key, JSON.stringify(config), 'application/json');
|
|
229
|
+
await this.purge(ctx, buf ? JSON.parse(buf) : null, config);
|
|
230
|
+
return ret ?? redact(data, frag);
|
|
125
231
|
}
|
|
126
232
|
|
|
127
233
|
async remove(ctx, relPath) {
|
|
@@ -130,11 +236,8 @@ export class ConfigStore {
|
|
|
130
236
|
if (!buf) {
|
|
131
237
|
throw new StatusCodeError(404, 'config not found.');
|
|
132
238
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (!fragment) {
|
|
136
|
-
throw new StatusCodeError(400, 'invalid object path.');
|
|
137
|
-
}
|
|
239
|
+
const frag = getFragmentInfo(this.type, relPath);
|
|
240
|
+
if (frag) {
|
|
138
241
|
const data = jsonPut(JSON.parse(buf), relPath, null);
|
|
139
242
|
await validate(data, this.type);
|
|
140
243
|
await storage.put(this.key, JSON.stringify(data), 'application/json');
|
|
@@ -29,6 +29,7 @@ import profileSchema from '../schemas/profile.schema.cjs';
|
|
|
29
29
|
import robotsSchema from '../schemas/robots.schema.cjs';
|
|
30
30
|
import sidekickSchema from '../schemas/sidekick.schema.cjs';
|
|
31
31
|
import siteSchema from '../schemas/site.schema.cjs';
|
|
32
|
+
import tokensSchema from '../schemas/tokens.schema.cjs';
|
|
32
33
|
|
|
33
34
|
const SCHEMAS = [
|
|
34
35
|
accessSchema,
|
|
@@ -47,6 +48,7 @@ const SCHEMAS = [
|
|
|
47
48
|
robotsSchema,
|
|
48
49
|
sidekickSchema,
|
|
49
50
|
siteSchema,
|
|
51
|
+
tokensSchema,
|
|
50
52
|
];
|
|
51
53
|
|
|
52
54
|
const SCHEMA_TYPES = {
|
package/src/storage/utils.js
CHANGED
|
@@ -12,13 +12,15 @@
|
|
|
12
12
|
/* eslint-disable no-param-reassign */
|
|
13
13
|
import crypto from 'crypto';
|
|
14
14
|
import { GitUrl } from '@adobe/helix-shared-git';
|
|
15
|
+
import { decodeJwt } from 'jose';
|
|
16
|
+
import { StatusCodeError } from './status-code-error.js';
|
|
15
17
|
|
|
16
18
|
export function jsonGet(obj, path) {
|
|
17
19
|
return path.split('/').reduce((o, p) => o[p], obj);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export function jsonPut(obj, path, value) {
|
|
21
|
-
const parts = path.split('/');
|
|
23
|
+
const parts = Array.isArray(path) ? path : path.split('/');
|
|
22
24
|
const last = parts.pop();
|
|
23
25
|
const parent = parts.reduce((o, p) => {
|
|
24
26
|
if (!(p in o)) {
|
|
@@ -111,3 +113,60 @@ export function updateCodeSource(ctx, code) {
|
|
|
111
113
|
}
|
|
112
114
|
return modified;
|
|
113
115
|
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates a random token and hashes it with the given key
|
|
119
|
+
* @param {string} key
|
|
120
|
+
* @returns {object} the token
|
|
121
|
+
*/
|
|
122
|
+
export function createToken(key) {
|
|
123
|
+
const secret = crypto.randomBytes(32).toString('base64url');
|
|
124
|
+
const value = `hlx_${secret}`;
|
|
125
|
+
const hash = crypto
|
|
126
|
+
.createHmac('sha512', key)
|
|
127
|
+
.update(value, 'utf-8')
|
|
128
|
+
.digest()
|
|
129
|
+
.toString('base64url');
|
|
130
|
+
const id = crypto.createHash('sha256').update(hash).digest().toString('base64url');
|
|
131
|
+
const created = new Date().toISOString();
|
|
132
|
+
return {
|
|
133
|
+
id,
|
|
134
|
+
value,
|
|
135
|
+
hash,
|
|
136
|
+
created,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* migrates an existing jwt token
|
|
142
|
+
* @param key
|
|
143
|
+
* @param jwt
|
|
144
|
+
* @returns {Promise<object>}
|
|
145
|
+
*/
|
|
146
|
+
export async function migrateToken(key, jwt) {
|
|
147
|
+
let payload;
|
|
148
|
+
try {
|
|
149
|
+
payload = await decodeJwt(jwt);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
throw new StatusCodeError(400, `unable to migrate jwt: ${e.message}`);
|
|
152
|
+
}
|
|
153
|
+
const { jti } = payload;
|
|
154
|
+
if (!jti) {
|
|
155
|
+
throw new StatusCodeError(400, 'unable to migrate jwt: missing jti claim.');
|
|
156
|
+
}
|
|
157
|
+
const id = jti
|
|
158
|
+
.replaceAll('/', '_')
|
|
159
|
+
.replaceAll('+', '-');
|
|
160
|
+
const hash = crypto
|
|
161
|
+
.createHmac('sha512', key)
|
|
162
|
+
.update(jwt, 'utf-8')
|
|
163
|
+
.digest()
|
|
164
|
+
.toString('base64url');
|
|
165
|
+
const created = new Date().toISOString();
|
|
166
|
+
return {
|
|
167
|
+
id,
|
|
168
|
+
value: jwt,
|
|
169
|
+
hash,
|
|
170
|
+
created,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -32,6 +32,7 @@ export interface HelixProfileConfig {
|
|
|
32
32
|
sidekick?: SidekickConfig;
|
|
33
33
|
metadata?: Metadata;
|
|
34
34
|
robots?: Robots;
|
|
35
|
+
tokens?: Tokens;
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
37
38
|
* Defines the content bus location and source.
|
|
@@ -223,11 +224,7 @@ export interface Role {
|
|
|
223
224
|
}
|
|
224
225
|
export interface SiteAccessConfig {
|
|
225
226
|
/**
|
|
226
|
-
*
|
|
227
|
-
*/
|
|
228
|
-
allow?: string[];
|
|
229
|
-
/**
|
|
230
|
-
* IDs of the api keys that are allowed.
|
|
227
|
+
* IDs of the api keys (tokens) that are allowed.
|
|
231
228
|
*/
|
|
232
229
|
apiKeyId?: string[];
|
|
233
230
|
/**
|
|
@@ -251,3 +248,14 @@ export interface Metadata {
|
|
|
251
248
|
export interface Robots {
|
|
252
249
|
txt?: string;
|
|
253
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
|
@@ -35,6 +35,7 @@ export interface HelixSiteConfig {
|
|
|
35
35
|
sidekick?: SidekickConfig;
|
|
36
36
|
metadata?: Metadata;
|
|
37
37
|
robots?: Robots;
|
|
38
|
+
tokens?: Tokens;
|
|
38
39
|
}
|
|
39
40
|
/**
|
|
40
41
|
* Defines the content bus location and source.
|
|
@@ -226,11 +227,7 @@ export interface Role {
|
|
|
226
227
|
}
|
|
227
228
|
export interface SiteAccessConfig {
|
|
228
229
|
/**
|
|
229
|
-
*
|
|
230
|
-
*/
|
|
231
|
-
allow?: string[];
|
|
232
|
-
/**
|
|
233
|
-
* IDs of the api keys that are allowed.
|
|
230
|
+
* IDs of the api keys (tokens) that are allowed.
|
|
234
231
|
*/
|
|
235
232
|
apiKeyId?: string[];
|
|
236
233
|
/**
|
|
@@ -254,3 +251,14 @@ export interface Metadata {
|
|
|
254
251
|
export interface Robots {
|
|
255
252
|
txt?: string;
|
|
256
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
|
+
}
|