@adobe/helix-config-storage 1.13.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 +7 -0
- package/package.json +1 -1
- 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 +9 -1
- 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/validate-json-schemas.sh +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
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
|
+
|
|
1
8
|
# [1.13.0](https://github.com/adobe/helix-config-storage/compare/v1.12.0...v1.13.0) (2024-12-16)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
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,8 +12,16 @@
|
|
|
12
12
|
"type": "string"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
+
"secretId": {
|
|
16
|
+
"description": "IDs of the org secrets (tokens) that are allowed.",
|
|
17
|
+
"type": "array",
|
|
18
|
+
"items": {
|
|
19
|
+
"type": "string"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
15
22
|
"apiKeyId": {
|
|
16
|
-
"description": "IDs of the api keys (tokens) that are allowed.",
|
|
23
|
+
"description": "IDs of the api keys (tokens) that are allowed. This is deprecated. use `secretId` instead.",
|
|
24
|
+
"deprecated": true,
|
|
17
25
|
"type": "array",
|
|
18
26
|
"items": {
|
|
19
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/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 \
|