@adobe/helix-config-storage 1.12.0 → 1.14.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 +3 -3
- package/src/config-merge.js +1 -0
- package/src/config-store.js +130 -25
- package/src/config-validator.js +12 -10
- package/src/schemas/access-site.schema.json +5 -4
- package/src/schemas/org-access.schema.json +1 -1
- package/src/schemas/org.schema.json +3 -0
- package/src/schemas/profile.schema.json +3 -0
- package/src/schemas/secrets.schema.cjs +12 -0
- package/src/schemas/secrets.schema.json +86 -0
- package/src/schemas/site.schema.json +3 -0
- package/src/schemas/tokens.schema.json +2 -0
- package/src/utils.js +64 -0
- package/types/org-config.d.ts +17 -0
- package/types/profile-config.d.ts +26 -5
- package/types/site-config.d.ts +26 -5
- package/validate-json-schemas.sh +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.14.0](https://github.com/adobe/helix-config-storage/compare/v1.13.0...v1.14.0) (2024-12-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* secrets store ([1aaf99e](https://github.com/adobe/helix-config-storage/commit/1aaf99e8cd7e41b51ada2f46b4961d2fc2ee26af)), closes [#21](https://github.com/adobe/helix-config-storage/issues/21)
|
|
7
|
+
|
|
8
|
+
# [1.13.0](https://github.com/adobe/helix-config-storage/compare/v1.12.0...v1.13.0) (2024-12-16)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* remove clientCertDN ([#73](https://github.com/adobe/helix-config-storage/issues/73)) ([20dafca](https://github.com/adobe/helix-config-storage/commit/20dafcaf34cf18d3c1cdc096775b315ee4c76ceb))
|
|
14
|
+
|
|
1
15
|
# [1.12.0](https://github.com/adobe/helix-config-storage/compare/v1.11.0...v1.12.0) (2024-12-11)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/helix-config-storage",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "Helix Config Storage",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
"@semantic-release/git": "10.0.1",
|
|
42
42
|
"@semantic-release/npm": "12.0.1",
|
|
43
43
|
"ajv-cli": "5.0.0",
|
|
44
|
-
"c8": "10.1.
|
|
44
|
+
"c8": "10.1.3",
|
|
45
45
|
"eslint": "8.57.1",
|
|
46
46
|
"husky": "9.1.7",
|
|
47
47
|
"json-schema-to-typescript": "15.0.3",
|
|
48
48
|
"junit-report-builder": "5.1.1",
|
|
49
|
-
"lint-staged": "15.2.
|
|
49
|
+
"lint-staged": "15.2.11",
|
|
50
50
|
"mocha": "11.0.1",
|
|
51
51
|
"mocha-multi-reporters": "1.5.1",
|
|
52
52
|
"mocha-suppress-logs": "0.5.1",
|
package/src/config-merge.js
CHANGED
package/src/config-store.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
/* eslint-disable no-param-reassign */
|
|
13
|
+
import crypto from 'crypto';
|
|
13
14
|
import { isDeepStrictEqual } from 'util';
|
|
14
15
|
import { HelixStorage } from '@adobe/helix-shared-storage';
|
|
15
16
|
import { StatusCodeError } from './status-code-error.js';
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
migrateToken,
|
|
19
20
|
updateCodeSource,
|
|
20
21
|
updateContentSource,
|
|
21
|
-
deepGetOrCreate, deepPut,
|
|
22
|
+
deepGetOrCreate, deepPut, prune, createSecret, migrateSecret,
|
|
22
23
|
} from './utils.js';
|
|
23
24
|
import { validate as validateSchema } from './config-validator.js';
|
|
24
25
|
import { getMergedConfig } from './config-merge.js';
|
|
@@ -36,6 +37,13 @@ const FRAGMENTS_COMMON = {
|
|
|
36
37
|
preview: 'object',
|
|
37
38
|
live: 'object',
|
|
38
39
|
},
|
|
40
|
+
secrets: {
|
|
41
|
+
'.': 'secrets',
|
|
42
|
+
'.param': {
|
|
43
|
+
name: 'id',
|
|
44
|
+
'.': 'secret',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
39
47
|
access: {
|
|
40
48
|
'.': 'object',
|
|
41
49
|
site: 'object',
|
|
@@ -84,6 +92,13 @@ const FRAGMENTS = {
|
|
|
84
92
|
},
|
|
85
93
|
profiles: FRAGMENTS_COMMON,
|
|
86
94
|
org: {
|
|
95
|
+
secrets: {
|
|
96
|
+
'.': 'secrets',
|
|
97
|
+
'.param': {
|
|
98
|
+
name: 'id',
|
|
99
|
+
'.': 'secret',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
87
102
|
tokens: {
|
|
88
103
|
'.': 'tokens',
|
|
89
104
|
'.param': {
|
|
@@ -112,16 +127,23 @@ const FRAGMENTS = {
|
|
|
112
127
|
},
|
|
113
128
|
};
|
|
114
129
|
|
|
115
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Returns the fragment info for a given type and relative path.
|
|
132
|
+
* @param {string} type
|
|
133
|
+
* @param {string|array} relPath
|
|
134
|
+
* @returns {{relPath}|null}
|
|
135
|
+
*/
|
|
136
|
+
export function getFragmentInfo(type, relPath = '') {
|
|
116
137
|
if (!relPath) {
|
|
117
138
|
return null;
|
|
118
139
|
}
|
|
119
|
-
|
|
140
|
+
relPath = relPath.split('/');
|
|
120
141
|
let fragment = FRAGMENTS[type];
|
|
121
142
|
const info = {
|
|
122
143
|
relPath,
|
|
144
|
+
name: relPath[relPath.length - 1],
|
|
123
145
|
};
|
|
124
|
-
for (const part of
|
|
146
|
+
for (const part of relPath) {
|
|
125
147
|
let next = fragment[part];
|
|
126
148
|
if (next === '*') {
|
|
127
149
|
fragment = '*';
|
|
@@ -145,22 +167,25 @@ export function getFragmentInfo(type, relPath) {
|
|
|
145
167
|
}
|
|
146
168
|
|
|
147
169
|
/**
|
|
148
|
-
* Redact / transform the
|
|
170
|
+
* Redact / transform the secret config
|
|
149
171
|
* @param token
|
|
150
172
|
*/
|
|
151
|
-
function
|
|
152
|
-
return {
|
|
173
|
+
function redactSecret(token) {
|
|
174
|
+
return prune({
|
|
153
175
|
id: token.id,
|
|
176
|
+
type: token.type,
|
|
154
177
|
created: token.created,
|
|
155
|
-
|
|
178
|
+
lastModified: token.lastModified,
|
|
179
|
+
description: token.description,
|
|
180
|
+
});
|
|
156
181
|
}
|
|
157
182
|
|
|
158
183
|
/**
|
|
159
|
-
* Redact / transform the
|
|
184
|
+
* Redact / transform the secrets config
|
|
160
185
|
* @param tokens
|
|
161
186
|
*/
|
|
162
|
-
function
|
|
163
|
-
return Object.values(tokens).map(
|
|
187
|
+
function redactSecrets(tokens) {
|
|
188
|
+
return Object.values(tokens).map(redactSecret);
|
|
164
189
|
}
|
|
165
190
|
|
|
166
191
|
/**
|
|
@@ -173,19 +198,52 @@ function redact(config, frag) {
|
|
|
173
198
|
return config;
|
|
174
199
|
}
|
|
175
200
|
let ret = config;
|
|
176
|
-
if (frag?.type === 'tokens') {
|
|
177
|
-
ret =
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
201
|
+
if (frag?.type === 'secrets' || frag?.type === 'tokens') {
|
|
202
|
+
ret = redactSecrets(config);
|
|
203
|
+
} else if (frag?.type === 'secret' || frag?.type === 'token') {
|
|
204
|
+
ret = redactSecret(config);
|
|
205
|
+
} else if (ret.secrets) {
|
|
206
|
+
// eslint-disable-next-line no-param-reassign
|
|
207
|
+
ret.secrets = redactSecrets(ret.secrets);
|
|
208
|
+
} else if (ret.tokens) {
|
|
183
209
|
// eslint-disable-next-line no-param-reassign
|
|
184
|
-
ret.tokens =
|
|
210
|
+
ret.tokens = redactSecrets(ret.tokens);
|
|
185
211
|
}
|
|
186
212
|
return ret;
|
|
187
213
|
}
|
|
188
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Updates a secret based on its type. if type is 'key' the value is updated.
|
|
217
|
+
* @param {string} type
|
|
218
|
+
* @param {string} id
|
|
219
|
+
* @param {object} oldData
|
|
220
|
+
* @param {object} data
|
|
221
|
+
* @returns {object} the updated secret data
|
|
222
|
+
*/
|
|
223
|
+
function updateSecret(type, id, oldData, data) {
|
|
224
|
+
oldData.type = type;
|
|
225
|
+
oldData.id = id;
|
|
226
|
+
const now = new Date().toISOString();
|
|
227
|
+
|
|
228
|
+
// keep the description if missing in data
|
|
229
|
+
if ('description' in data) {
|
|
230
|
+
oldData.description = data.description;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// handle value specifically, as we don't allow deletion
|
|
234
|
+
if (type === 'key' && data.value) {
|
|
235
|
+
oldData.value = data.value;
|
|
236
|
+
oldData.lastModified = now;
|
|
237
|
+
}
|
|
238
|
+
if (!oldData.created) {
|
|
239
|
+
oldData.created = now;
|
|
240
|
+
}
|
|
241
|
+
if (!oldData.lastModified) {
|
|
242
|
+
oldData.lastModified = oldData.created;
|
|
243
|
+
}
|
|
244
|
+
return prune(oldData);
|
|
245
|
+
}
|
|
246
|
+
|
|
189
247
|
/**
|
|
190
248
|
* General purpose config store.
|
|
191
249
|
*/
|
|
@@ -321,6 +379,9 @@ export class ConfigStore {
|
|
|
321
379
|
if (data.tokens) {
|
|
322
380
|
throw new StatusCodeError(400, 'creating config with tokens not supported yet.');
|
|
323
381
|
}
|
|
382
|
+
if (data.secrets) {
|
|
383
|
+
throw new StatusCodeError(400, 'creating config with secrets not supported yet.');
|
|
384
|
+
}
|
|
324
385
|
if (this.type === 'org' && data.users) {
|
|
325
386
|
throw new StatusCodeError(400, 'creating org config with users is not supported yet.');
|
|
326
387
|
}
|
|
@@ -367,12 +428,12 @@ export class ConfigStore {
|
|
|
367
428
|
}
|
|
368
429
|
|
|
369
430
|
let ret = null;
|
|
370
|
-
if (!config && frag?.type !== 'tokens') {
|
|
431
|
+
if (!config && frag?.type !== 'secrets' && frag?.type !== 'tokens') {
|
|
371
432
|
throw new StatusCodeError(400, 'no config in body.');
|
|
372
433
|
}
|
|
373
434
|
if (frag) {
|
|
374
435
|
if (!old) {
|
|
375
|
-
if (this.type === 'profiles' && frag.type === 'tokens') {
|
|
436
|
+
if (this.type === 'profiles' && (frag.type === 'tokens' || frag.type === 'secrets')) {
|
|
376
437
|
old = {};
|
|
377
438
|
} else {
|
|
378
439
|
throw new StatusCodeError(404, 'config not found.');
|
|
@@ -396,11 +457,53 @@ export class ConfigStore {
|
|
|
396
457
|
};
|
|
397
458
|
// don't store token value in the config
|
|
398
459
|
delete data.value;
|
|
399
|
-
|
|
460
|
+
frag.name = token.id;
|
|
400
461
|
frag.type = 'token';
|
|
462
|
+
frag.relPath.push(frag.name);
|
|
401
463
|
ret = token;
|
|
402
464
|
// don't expose hash in return value
|
|
403
465
|
delete ret.hash;
|
|
466
|
+
}
|
|
467
|
+
if (frag.type === 'secrets') {
|
|
468
|
+
// create new secret with random id
|
|
469
|
+
frag.name = crypto.randomBytes(32).toString('base64url');
|
|
470
|
+
frag.type = 'secret';
|
|
471
|
+
frag.relPath.push(frag.name);
|
|
472
|
+
}
|
|
473
|
+
if (frag.type === 'secret') {
|
|
474
|
+
const oldData = deepGetOrCreate(old, frag.relPath);
|
|
475
|
+
if (oldData) {
|
|
476
|
+
if (oldData.type === 'hashed') {
|
|
477
|
+
data = updateSecret('hashed', frag.name, oldData, data);
|
|
478
|
+
} else {
|
|
479
|
+
data = updateSecret('key', frag.name, oldData, data);
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
if (data.value) {
|
|
483
|
+
data = updateSecret('key', frag.name, { id: frag.name }, data);
|
|
484
|
+
} else {
|
|
485
|
+
// TODO: remove support after all helix4 projects are migrated
|
|
486
|
+
let token;
|
|
487
|
+
if (data.jwt) {
|
|
488
|
+
token = await migrateSecret(this.org, data.jwt);
|
|
489
|
+
frag.name = token.id;
|
|
490
|
+
frag.relPath.splice(-1, 1, token.id);
|
|
491
|
+
} else {
|
|
492
|
+
// create new token
|
|
493
|
+
token = createSecret(this.org, 'hlx', data);
|
|
494
|
+
token.id = frag.name;
|
|
495
|
+
}
|
|
496
|
+
data = {
|
|
497
|
+
...token,
|
|
498
|
+
};
|
|
499
|
+
// don't store token value in the config
|
|
500
|
+
delete data.value;
|
|
501
|
+
ret = token;
|
|
502
|
+
// don't expose hash in return value
|
|
503
|
+
delete ret.hash;
|
|
504
|
+
}
|
|
505
|
+
data = prune(data);
|
|
506
|
+
}
|
|
404
507
|
} else if (frag.type === 'users') {
|
|
405
508
|
// todo: define via "schema"
|
|
406
509
|
if (!old.users) {
|
|
@@ -421,11 +524,12 @@ export class ConfigStore {
|
|
|
421
524
|
...data,
|
|
422
525
|
...user,
|
|
423
526
|
};
|
|
424
|
-
|
|
527
|
+
frag.name = user.id;
|
|
528
|
+
frag.relPath.push(user.id);
|
|
425
529
|
frag.type = 'user';
|
|
426
530
|
}
|
|
427
531
|
} else if (frag.type === 'user') {
|
|
428
|
-
const user = deepGetOrCreate(old, relPath);
|
|
532
|
+
const user = deepGetOrCreate(old, frag.relPath);
|
|
429
533
|
if (!user) {
|
|
430
534
|
throw new StatusCodeError(404, 'object not found.');
|
|
431
535
|
}
|
|
@@ -437,13 +541,14 @@ export class ConfigStore {
|
|
|
437
541
|
data.id = user.id;
|
|
438
542
|
}
|
|
439
543
|
}
|
|
440
|
-
config = deepPut(old, relPath, data);
|
|
544
|
+
config = deepPut(old, frag.relPath, data);
|
|
441
545
|
}
|
|
442
546
|
|
|
443
547
|
if (this.type !== 'org') {
|
|
444
548
|
updateContentSource(ctx, config.content);
|
|
445
549
|
updateCodeSource(ctx, config.code);
|
|
446
550
|
}
|
|
551
|
+
|
|
447
552
|
let oldConfig = buf ? JSON.parse(buf) : null;
|
|
448
553
|
config.created = oldConfig?.created;
|
|
449
554
|
this.#updateTimeStamps(config);
|
package/src/config-validator.js
CHANGED
|
@@ -33,6 +33,7 @@ import headersSchema from './schemas/headers.schema.cjs';
|
|
|
33
33
|
import markupSchema from './schemas/content-source-markup.schema.cjs';
|
|
34
34
|
import metadataSchema from './schemas/metadata-source.schema.cjs';
|
|
35
35
|
import orgAccessSchema from './schemas/org-access.schema.cjs';
|
|
36
|
+
import secretsSchema from './schemas/secrets.schema.cjs';
|
|
36
37
|
import orgSchema from './schemas/org.schema.cjs';
|
|
37
38
|
import onedriveSchema from './schemas/content-source-onedrive.schema.cjs';
|
|
38
39
|
import publicSchema from './schemas/public.schema.cjs';
|
|
@@ -47,13 +48,18 @@ import userSchema from './schemas/user.schema.cjs';
|
|
|
47
48
|
import usersSchema from './schemas/users.schema.cjs';
|
|
48
49
|
|
|
49
50
|
export const SCHEMAS = [
|
|
50
|
-
accessSchema,
|
|
51
51
|
accessAdminSchema,
|
|
52
|
+
accessSchema,
|
|
52
53
|
accessSiteSchema,
|
|
54
|
+
cdnProdAkamaiSchema,
|
|
55
|
+
cdnProdCloudflareSchema,
|
|
56
|
+
cdnProdCloudfrontSchema,
|
|
57
|
+
cdnProdFastlySchema,
|
|
58
|
+
cdnProdManagedSchema,
|
|
53
59
|
cdnSchema,
|
|
60
|
+
codeSchema,
|
|
54
61
|
commonSchema,
|
|
55
62
|
contentSchema,
|
|
56
|
-
codeSchema,
|
|
57
63
|
eventsSchema,
|
|
58
64
|
foldersSchema,
|
|
59
65
|
googleSchema,
|
|
@@ -61,24 +67,20 @@ export const SCHEMAS = [
|
|
|
61
67
|
headersSchema,
|
|
62
68
|
markupSchema,
|
|
63
69
|
metadataSchema,
|
|
64
|
-
orgSchema,
|
|
65
|
-
orgAccessSchema,
|
|
66
70
|
onedriveSchema,
|
|
67
|
-
|
|
71
|
+
orgAccessSchema,
|
|
72
|
+
orgSchema,
|
|
68
73
|
profileSchema,
|
|
69
74
|
profilesSchema,
|
|
75
|
+
publicSchema,
|
|
70
76
|
robotsSchema,
|
|
77
|
+
secretsSchema,
|
|
71
78
|
sidekickSchema,
|
|
72
79
|
siteSchema,
|
|
73
80
|
sitesSchema,
|
|
74
81
|
tokensSchema,
|
|
75
82
|
userSchema,
|
|
76
83
|
usersSchema,
|
|
77
|
-
cdnProdFastlySchema,
|
|
78
|
-
cdnProdCloudflareSchema,
|
|
79
|
-
cdnProdAkamaiSchema,
|
|
80
|
-
cdnProdManagedSchema,
|
|
81
|
-
cdnProdCloudfrontSchema,
|
|
82
84
|
];
|
|
83
85
|
|
|
84
86
|
const SCHEMA_TYPES = {
|
|
@@ -12,15 +12,16 @@
|
|
|
12
12
|
"type": "string"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
-
"
|
|
16
|
-
"description": "IDs of the
|
|
15
|
+
"secretId": {
|
|
16
|
+
"description": "IDs of the org secrets (tokens) that are allowed.",
|
|
17
17
|
"type": "array",
|
|
18
18
|
"items": {
|
|
19
19
|
"type": "string"
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
|
-
"
|
|
23
|
-
"description": "
|
|
22
|
+
"apiKeyId": {
|
|
23
|
+
"description": "IDs of the api keys (tokens) that are allowed. This is deprecated. use `secretId` instead.",
|
|
24
|
+
"deprecated": true,
|
|
24
25
|
"type": "array",
|
|
25
26
|
"items": {
|
|
26
27
|
"type": "string"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"type": "object",
|
|
10
10
|
"properties": {
|
|
11
11
|
"apiKeyId": {
|
|
12
|
-
"description": "the id of the API key(s)
|
|
12
|
+
"description": "the id of the API key(s) that are allowed to access this org (admin).",
|
|
13
13
|
"type": "array",
|
|
14
14
|
"items": {
|
|
15
15
|
"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('./secrets.schema.json');
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "https://github.com/adobe/helix-config/blob/main/LICENSE.txt",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
|
4
|
+
"$id": "https://ns.adobe.com/helix/config/secrets",
|
|
5
|
+
"title": "Secrets",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"description": "Defines organization level secrets.",
|
|
8
|
+
"patternProperties": {
|
|
9
|
+
"^[a-zA-Z0-9-_=]+$": {
|
|
10
|
+
"oneOf": [
|
|
11
|
+
{
|
|
12
|
+
"type": "object",
|
|
13
|
+
"properties": {
|
|
14
|
+
"hash": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"pattern": "^[a-zA-Z0-9-_=]+$"
|
|
17
|
+
},
|
|
18
|
+
"id": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"pattern": "^[a-zA-Z0-9-_=]+$"
|
|
21
|
+
},
|
|
22
|
+
"type": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"enum": ["hashed", "key"]
|
|
25
|
+
},
|
|
26
|
+
"created": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"format": "date-time"
|
|
29
|
+
},
|
|
30
|
+
"lastModified": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"format": "date-time"
|
|
33
|
+
},
|
|
34
|
+
"description": {
|
|
35
|
+
"type": "string"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"required": [
|
|
39
|
+
"hash",
|
|
40
|
+
"id",
|
|
41
|
+
"type",
|
|
42
|
+
"created",
|
|
43
|
+
"lastModified"
|
|
44
|
+
],
|
|
45
|
+
"additionalProperties": false
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"type": "object",
|
|
49
|
+
"properties": {
|
|
50
|
+
"value": {
|
|
51
|
+
"type": "string"
|
|
52
|
+
},
|
|
53
|
+
"id": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"pattern": "^[a-zA-Z0-9-_=]+$"
|
|
56
|
+
},
|
|
57
|
+
"type": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"enum": ["hashed", "key"]
|
|
60
|
+
},
|
|
61
|
+
"created": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"format": "date-time"
|
|
64
|
+
},
|
|
65
|
+
"lastModified": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"format": "date-time"
|
|
68
|
+
},
|
|
69
|
+
"description": {
|
|
70
|
+
"type": "string"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"required": [
|
|
74
|
+
"value",
|
|
75
|
+
"id",
|
|
76
|
+
"type",
|
|
77
|
+
"created",
|
|
78
|
+
"lastModified"
|
|
79
|
+
],
|
|
80
|
+
"additionalProperties": false
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"additionalProperties": false
|
|
86
|
+
}
|
package/src/utils.js
CHANGED
|
@@ -111,6 +111,30 @@ export function updateCodeSource(ctx, code) {
|
|
|
111
111
|
return modified;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* prunes a structure recursively by removing all empty values.
|
|
116
|
+
* @param obj
|
|
117
|
+
* @param path
|
|
118
|
+
* @returns {*}
|
|
119
|
+
*/
|
|
120
|
+
export function prune(obj, path = '') {
|
|
121
|
+
for (const key of Object.keys(obj)) {
|
|
122
|
+
const itemPath = `${path}.${key}`;
|
|
123
|
+
const prop = obj[key];
|
|
124
|
+
if ((prop === undefined || prop === '')) {
|
|
125
|
+
delete obj[key];
|
|
126
|
+
} else if (Array.isArray(prop)) {
|
|
127
|
+
// ignore
|
|
128
|
+
} else if (typeof prop === 'object') {
|
|
129
|
+
prune(prop, itemPath);
|
|
130
|
+
if (Object.keys(prop).length === 0) {
|
|
131
|
+
delete obj[key];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return obj;
|
|
136
|
+
}
|
|
137
|
+
|
|
114
138
|
/**
|
|
115
139
|
* Creates a random token and hashes it with the given key
|
|
116
140
|
* @param {string} key
|
|
@@ -135,6 +159,33 @@ export function createToken(key, pfx = 'hlx') {
|
|
|
135
159
|
};
|
|
136
160
|
}
|
|
137
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Creates a random token and hashes it with the given key
|
|
164
|
+
* @param {string} key
|
|
165
|
+
* @param {string} [pfx='hlx'] the prefix of the token
|
|
166
|
+
* @param {object} [data] additional token data.
|
|
167
|
+
* @returns {object} the token
|
|
168
|
+
*/
|
|
169
|
+
export function createSecret(key, pfx = 'hlx', data = {}) {
|
|
170
|
+
const secret = crypto.randomBytes(32).toString('base64url');
|
|
171
|
+
const value = `${pfx}_${secret}`;
|
|
172
|
+
const hash = crypto
|
|
173
|
+
.createHmac('sha512', key)
|
|
174
|
+
.update(value, 'utf-8')
|
|
175
|
+
.digest()
|
|
176
|
+
.toString('base64url');
|
|
177
|
+
const created = new Date().toISOString();
|
|
178
|
+
return prune({
|
|
179
|
+
type: 'hashed',
|
|
180
|
+
value,
|
|
181
|
+
hash,
|
|
182
|
+
created,
|
|
183
|
+
lastModified: created,
|
|
184
|
+
title: data.title,
|
|
185
|
+
description: data.description,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
138
189
|
export function createUser() {
|
|
139
190
|
const id = crypto.randomBytes(16).toString('base64url');
|
|
140
191
|
return {
|
|
@@ -176,6 +227,19 @@ export async function migrateToken(key, jwt) {
|
|
|
176
227
|
};
|
|
177
228
|
}
|
|
178
229
|
|
|
230
|
+
/**
|
|
231
|
+
* migrates an existing jwt token
|
|
232
|
+
* @param key
|
|
233
|
+
* @param jwt
|
|
234
|
+
* @returns {Promise<object>}
|
|
235
|
+
*/
|
|
236
|
+
export async function migrateSecret(key, jwt) {
|
|
237
|
+
const token = await migrateToken(key, jwt);
|
|
238
|
+
token.type = 'hashed';
|
|
239
|
+
token.lastModified = new Date().toISOString();
|
|
240
|
+
return token;
|
|
241
|
+
}
|
|
242
|
+
|
|
179
243
|
/**
|
|
180
244
|
* Returns the property addressed by the given path in the object.
|
|
181
245
|
* If {@code create} is {@code true}, intermediate objects will be created if they do not exist.
|
package/types/org-config.d.ts
CHANGED
|
@@ -16,6 +16,14 @@ export type Users = User[];
|
|
|
16
16
|
|
|
17
17
|
export interface HelixOrgConfig {
|
|
18
18
|
version: 1;
|
|
19
|
+
/**
|
|
20
|
+
* the date and time this configuration was created.
|
|
21
|
+
*/
|
|
22
|
+
created: string;
|
|
23
|
+
/**
|
|
24
|
+
* the date and time this configuration was modified last.
|
|
25
|
+
*/
|
|
26
|
+
lastModified: string;
|
|
19
27
|
/**
|
|
20
28
|
* human readable title. has no influence on the configuration.
|
|
21
29
|
*/
|
|
@@ -27,6 +35,7 @@ export interface HelixOrgConfig {
|
|
|
27
35
|
users?: Users;
|
|
28
36
|
groups?: Groups;
|
|
29
37
|
tokens?: Tokens;
|
|
38
|
+
access?: OrgAccessConfig;
|
|
30
39
|
}
|
|
31
40
|
export interface User {
|
|
32
41
|
id: string;
|
|
@@ -61,3 +70,11 @@ export interface Tokens {
|
|
|
61
70
|
created?: string;
|
|
62
71
|
};
|
|
63
72
|
}
|
|
73
|
+
export interface OrgAccessConfig {
|
|
74
|
+
admin?: {
|
|
75
|
+
/**
|
|
76
|
+
* the id of the API key(s). this is used to validate the API KEYS and allows to invalidate them.
|
|
77
|
+
*/
|
|
78
|
+
apiKeyId?: string[];
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -22,6 +22,14 @@ export interface HelixProfileConfig {
|
|
|
22
22
|
* description for clarity. has no influence on the configuration.
|
|
23
23
|
*/
|
|
24
24
|
description?: string;
|
|
25
|
+
/**
|
|
26
|
+
* the date and time this configuration was created.
|
|
27
|
+
*/
|
|
28
|
+
created: string;
|
|
29
|
+
/**
|
|
30
|
+
* the date and time this configuration was modified last.
|
|
31
|
+
*/
|
|
32
|
+
lastModified: string;
|
|
25
33
|
content?: ContentSource;
|
|
26
34
|
code?: CodeSource;
|
|
27
35
|
folders?: Folders;
|
|
@@ -34,6 +42,7 @@ export interface HelixProfileConfig {
|
|
|
34
42
|
metadata?: Metadata;
|
|
35
43
|
robots?: Robots;
|
|
36
44
|
public?: Public;
|
|
45
|
+
events?: EventsConfig;
|
|
37
46
|
}
|
|
38
47
|
/**
|
|
39
48
|
* Defines the content bus location and source.
|
|
@@ -49,6 +58,9 @@ export interface ContentSource {
|
|
|
49
58
|
description?: string;
|
|
50
59
|
contentBusId: string;
|
|
51
60
|
source: GoogleContentSource | OnedriveContentSource | MarkupContentSource;
|
|
61
|
+
/**
|
|
62
|
+
* Overlay from a BYOM source. Previewing resources will try the overlay source first. Please note, that the overlay config is tied to the base content and not to the site config. I.e. it's not possible to have multiple sites with different overlays on the same base content.
|
|
63
|
+
*/
|
|
52
64
|
overlay?: MarkupContentSource;
|
|
53
65
|
}
|
|
54
66
|
export interface GoogleContentSource {
|
|
@@ -90,6 +102,10 @@ export interface CodeSource {
|
|
|
90
102
|
source: {
|
|
91
103
|
type: 'github';
|
|
92
104
|
url: string;
|
|
105
|
+
raw_url?: string;
|
|
106
|
+
secretId?: string;
|
|
107
|
+
owner?: string;
|
|
108
|
+
repo?: string;
|
|
93
109
|
};
|
|
94
110
|
[k: string]: unknown;
|
|
95
111
|
}
|
|
@@ -103,7 +119,7 @@ export interface Folders {
|
|
|
103
119
|
export interface HelixHeadersConfig {
|
|
104
120
|
/**
|
|
105
121
|
* This interface was referenced by `HelixHeadersConfig`'s JSON-Schema definition
|
|
106
|
-
* via the `patternProperty` "
|
|
122
|
+
* via the `patternProperty` "^[a-zA-Z0-9-/._*]+$".
|
|
107
123
|
*/
|
|
108
124
|
[k: string]: KeyValuePair[];
|
|
109
125
|
}
|
|
@@ -249,13 +265,13 @@ export interface Role {
|
|
|
249
265
|
}
|
|
250
266
|
export interface SiteAccessConfig {
|
|
251
267
|
/**
|
|
252
|
-
*
|
|
268
|
+
* The email glob of the users or a group reference that are allowed access
|
|
253
269
|
*/
|
|
254
|
-
|
|
270
|
+
allow?: string[];
|
|
255
271
|
/**
|
|
256
|
-
*
|
|
272
|
+
* IDs of the api keys (tokens) that are allowed.
|
|
257
273
|
*/
|
|
258
|
-
|
|
274
|
+
apiKeyId?: string[];
|
|
259
275
|
}
|
|
260
276
|
export interface Tokens {
|
|
261
277
|
/**
|
|
@@ -367,3 +383,8 @@ export interface Robots {
|
|
|
367
383
|
export interface Public {
|
|
368
384
|
[k: string]: unknown;
|
|
369
385
|
}
|
|
386
|
+
export interface EventsConfig {
|
|
387
|
+
github: {
|
|
388
|
+
target: string;
|
|
389
|
+
};
|
|
390
|
+
}
|
package/types/site-config.d.ts
CHANGED
|
@@ -26,6 +26,14 @@ export interface HelixSiteConfig {
|
|
|
26
26
|
* description for clarity. has no influence on the configuration.
|
|
27
27
|
*/
|
|
28
28
|
description?: string;
|
|
29
|
+
/**
|
|
30
|
+
* the date and time this configuration was created.
|
|
31
|
+
*/
|
|
32
|
+
created: string;
|
|
33
|
+
/**
|
|
34
|
+
* the date and time this configuration was modified last.
|
|
35
|
+
*/
|
|
36
|
+
lastModified: string;
|
|
29
37
|
content: ContentSource;
|
|
30
38
|
code: CodeSource;
|
|
31
39
|
folders?: Folders;
|
|
@@ -38,6 +46,7 @@ export interface HelixSiteConfig {
|
|
|
38
46
|
metadata?: Metadata;
|
|
39
47
|
robots?: Robots;
|
|
40
48
|
public?: Public;
|
|
49
|
+
events?: EventsConfig;
|
|
41
50
|
extends?: {
|
|
42
51
|
profile?: string;
|
|
43
52
|
};
|
|
@@ -56,6 +65,9 @@ export interface ContentSource {
|
|
|
56
65
|
description?: string;
|
|
57
66
|
contentBusId: string;
|
|
58
67
|
source: GoogleContentSource | OnedriveContentSource | MarkupContentSource;
|
|
68
|
+
/**
|
|
69
|
+
* Overlay from a BYOM source. Previewing resources will try the overlay source first. Please note, that the overlay config is tied to the base content and not to the site config. I.e. it's not possible to have multiple sites with different overlays on the same base content.
|
|
70
|
+
*/
|
|
59
71
|
overlay?: MarkupContentSource;
|
|
60
72
|
}
|
|
61
73
|
export interface GoogleContentSource {
|
|
@@ -97,6 +109,10 @@ export interface CodeSource {
|
|
|
97
109
|
source: {
|
|
98
110
|
type: 'github';
|
|
99
111
|
url: string;
|
|
112
|
+
raw_url?: string;
|
|
113
|
+
secretId?: string;
|
|
114
|
+
owner?: string;
|
|
115
|
+
repo?: string;
|
|
100
116
|
};
|
|
101
117
|
[k: string]: unknown;
|
|
102
118
|
}
|
|
@@ -110,7 +126,7 @@ export interface Folders {
|
|
|
110
126
|
export interface HelixHeadersConfig {
|
|
111
127
|
/**
|
|
112
128
|
* This interface was referenced by `HelixHeadersConfig`'s JSON-Schema definition
|
|
113
|
-
* via the `patternProperty` "
|
|
129
|
+
* via the `patternProperty` "^[a-zA-Z0-9-/._*]+$".
|
|
114
130
|
*/
|
|
115
131
|
[k: string]: KeyValuePair[];
|
|
116
132
|
}
|
|
@@ -256,13 +272,13 @@ export interface Role {
|
|
|
256
272
|
}
|
|
257
273
|
export interface SiteAccessConfig {
|
|
258
274
|
/**
|
|
259
|
-
*
|
|
275
|
+
* The email glob of the users or a group reference that are allowed access
|
|
260
276
|
*/
|
|
261
|
-
|
|
277
|
+
allow?: string[];
|
|
262
278
|
/**
|
|
263
|
-
*
|
|
279
|
+
* IDs of the api keys (tokens) that are allowed.
|
|
264
280
|
*/
|
|
265
|
-
|
|
281
|
+
apiKeyId?: string[];
|
|
266
282
|
}
|
|
267
283
|
export interface Tokens {
|
|
268
284
|
/**
|
|
@@ -374,3 +390,8 @@ export interface Robots {
|
|
|
374
390
|
export interface Public {
|
|
375
391
|
[k: string]: unknown;
|
|
376
392
|
}
|
|
393
|
+
export interface EventsConfig {
|
|
394
|
+
github: {
|
|
395
|
+
target: string;
|
|
396
|
+
};
|
|
397
|
+
}
|
package/validate-json-schemas.sh
CHANGED
|
@@ -22,6 +22,7 @@ npx ajv-cli --spec=draft2019 -c ajv-formats compile \
|
|
|
22
22
|
-s src/schemas/metadata-source.schema.json \
|
|
23
23
|
-s src/schemas/user.schema.json \
|
|
24
24
|
-s src/schemas/users.schema.json \
|
|
25
|
+
-s src/schemas/secrets.schema.json \
|
|
25
26
|
-s src/schemas/tokens.schema.json \
|
|
26
27
|
-s src/schemas/org-access.schema.json \
|
|
27
28
|
-s src/schemas/org.schema.json \
|