@adobe/helix-config-storage 2.1.6 → 2.2.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/.nycrc.json +4 -2
- package/CHANGELOG.md +14 -0
- package/package.json +2 -2
- package/src/config-store.js +82 -3
- package/src/config-validator.js +2 -0
- package/src/schemas/apikeys.schema.cjs +12 -0
- package/src/schemas/apikeys.schema.json +50 -0
- package/src/schemas/org.schema.json +3 -0
- package/src/schemas/site.schema.json +3 -0
- package/validate-json-schemas.sh +1 -0
package/.nycrc.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [2.2.0](https://github.com/adobe/helix-config-storage/compare/v2.1.7...v2.2.0) (2025-04-22)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* support storing api key information ([b4d7de7](https://github.com/adobe/helix-config-storage/commit/b4d7de7ae49105fdd5da4dfda2b2a94ea105cef9))
|
|
7
|
+
|
|
8
|
+
## [2.1.7](https://github.com/adobe/helix-config-storage/compare/v2.1.6...v2.1.7) (2025-04-15)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* reject duplicate users ([#114](https://github.com/adobe/helix-config-storage/issues/114)) ([5e064a0](https://github.com/adobe/helix-config-storage/commit/5e064a05fb213ebc1d86b77eea9c4680a7fb0096))
|
|
14
|
+
|
|
1
15
|
## [2.1.6](https://github.com/adobe/helix-config-storage/compare/v2.1.5...v2.1.6) (2025-04-10)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/helix-config-storage",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Helix Config Storage",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"husky": "9.1.7",
|
|
47
47
|
"json-schema-to-typescript": "15.0.4",
|
|
48
48
|
"junit-report-builder": "5.1.1",
|
|
49
|
-
"lint-staged": "15.5.
|
|
49
|
+
"lint-staged": "15.5.1",
|
|
50
50
|
"mocha": "11.1.0",
|
|
51
51
|
"mocha-multi-reporters": "1.5.1",
|
|
52
52
|
"mocha-suppress-logs": "0.5.1",
|
package/src/config-store.js
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
/* eslint-disable no-param-reassign */
|
|
13
13
|
import crypto from 'crypto';
|
|
14
|
+
import { decodeJwt } from 'jose';
|
|
14
15
|
import { HelixStorage } from '@adobe/helix-shared-storage';
|
|
16
|
+
import { sanitizeName } from '@adobe/helix-shared-string';
|
|
15
17
|
import { StatusCodeError } from './status-code-error.js';
|
|
16
18
|
import {
|
|
17
19
|
createToken, createUser,
|
|
@@ -43,6 +45,7 @@ const FRAGMENTS_COMMON = {
|
|
|
43
45
|
name: 'id',
|
|
44
46
|
'.': 'secret',
|
|
45
47
|
},
|
|
48
|
+
'.allowEmpty': true,
|
|
46
49
|
},
|
|
47
50
|
access: {
|
|
48
51
|
'.': 'object',
|
|
@@ -70,6 +73,7 @@ const FRAGMENTS_COMMON = {
|
|
|
70
73
|
name: 'id',
|
|
71
74
|
'.': 'token',
|
|
72
75
|
},
|
|
76
|
+
'.allowEmpty': true,
|
|
73
77
|
},
|
|
74
78
|
groups: {
|
|
75
79
|
'.': 'object',
|
|
@@ -79,8 +83,15 @@ const FRAGMENTS_COMMON = {
|
|
|
79
83
|
},
|
|
80
84
|
},
|
|
81
85
|
public: '*',
|
|
82
|
-
features:
|
|
83
|
-
|
|
86
|
+
features: {
|
|
87
|
+
'.': '*',
|
|
88
|
+
'.allowEmpty': true,
|
|
89
|
+
|
|
90
|
+
},
|
|
91
|
+
limits: {
|
|
92
|
+
'.': '*',
|
|
93
|
+
'.allowEmpty': true,
|
|
94
|
+
},
|
|
84
95
|
robots: 'object',
|
|
85
96
|
events: {
|
|
86
97
|
github: 'object',
|
|
@@ -91,6 +102,13 @@ const FRAGMENTS = {
|
|
|
91
102
|
sites: {
|
|
92
103
|
...FRAGMENTS_COMMON,
|
|
93
104
|
extends: 'object',
|
|
105
|
+
apiKeys: {
|
|
106
|
+
'.': 'apiKeys',
|
|
107
|
+
'.param': {
|
|
108
|
+
name: 'id',
|
|
109
|
+
'.': 'apiKey',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
94
112
|
},
|
|
95
113
|
profiles: FRAGMENTS_COMMON,
|
|
96
114
|
org: {
|
|
@@ -100,6 +118,15 @@ const FRAGMENTS = {
|
|
|
100
118
|
name: 'id',
|
|
101
119
|
'.': 'secret',
|
|
102
120
|
},
|
|
121
|
+
'.allowEmpty': true,
|
|
122
|
+
},
|
|
123
|
+
apiKeys: {
|
|
124
|
+
'.': 'apiKeys',
|
|
125
|
+
'.param': {
|
|
126
|
+
name: 'id',
|
|
127
|
+
'.': 'apiKey',
|
|
128
|
+
},
|
|
129
|
+
'.allowEmpty': true,
|
|
103
130
|
},
|
|
104
131
|
tokens: {
|
|
105
132
|
'.': 'tokens',
|
|
@@ -107,6 +134,7 @@ const FRAGMENTS = {
|
|
|
107
134
|
name: 'id',
|
|
108
135
|
'.': 'token',
|
|
109
136
|
},
|
|
137
|
+
'.allowEmpty': true,
|
|
110
138
|
},
|
|
111
139
|
users: {
|
|
112
140
|
'.': 'users',
|
|
@@ -151,6 +179,10 @@ export function getFragmentInfo(type, relPath = '') {
|
|
|
151
179
|
fragment = '*';
|
|
152
180
|
break;
|
|
153
181
|
}
|
|
182
|
+
if (typeof next === 'object' && next['.'] === '*') {
|
|
183
|
+
fragment = next;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
154
186
|
if (!next) {
|
|
155
187
|
next = fragment['.param'];
|
|
156
188
|
if (!next) {
|
|
@@ -165,6 +197,7 @@ export function getFragmentInfo(type, relPath = '') {
|
|
|
165
197
|
} else {
|
|
166
198
|
info.type = fragment['.'];
|
|
167
199
|
}
|
|
200
|
+
info.allowEmpty = fragment['.allowEmpty'] || false;
|
|
168
201
|
return info;
|
|
169
202
|
}
|
|
170
203
|
|
|
@@ -246,6 +279,22 @@ function updateSecret(type, id, oldData, data) {
|
|
|
246
279
|
return prune(oldData);
|
|
247
280
|
}
|
|
248
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Ensures that the given email is not used by another user.
|
|
284
|
+
* @param {string} email
|
|
285
|
+
* @param {User[]} users
|
|
286
|
+
* @throws {StatusCodeError} a 400 status code error if the email is already used
|
|
287
|
+
*/
|
|
288
|
+
function validateUniqueEmail(email, users) {
|
|
289
|
+
if (!email) {
|
|
290
|
+
throw new StatusCodeError(400, 'missing email');
|
|
291
|
+
}
|
|
292
|
+
const existing = users.find((user) => user.email === email);
|
|
293
|
+
if (existing) {
|
|
294
|
+
throw new StatusCodeError(400, `email already in use by '${existing.id}'`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
249
298
|
/**
|
|
250
299
|
* General purpose config store.
|
|
251
300
|
*/
|
|
@@ -514,7 +563,7 @@ export class ConfigStore {
|
|
|
514
563
|
}
|
|
515
564
|
if (frag) {
|
|
516
565
|
if (!old) {
|
|
517
|
-
if (this.type === 'profiles' &&
|
|
566
|
+
if (this.type === 'profiles' && frag.allowEmpty) {
|
|
518
567
|
old = {};
|
|
519
568
|
} else {
|
|
520
569
|
throw new StatusCodeError(404, 'config not found.');
|
|
@@ -545,6 +594,34 @@ export class ConfigStore {
|
|
|
545
594
|
// don't expose hash in return value
|
|
546
595
|
delete ret.hash;
|
|
547
596
|
}
|
|
597
|
+
if (frag.type === 'apiKeys') {
|
|
598
|
+
if (data.jwt) {
|
|
599
|
+
try {
|
|
600
|
+
const payload = await decodeJwt(data.jwt);
|
|
601
|
+
data.id = payload.jti;
|
|
602
|
+
data.roles = payload.roles;
|
|
603
|
+
data.subject = payload.sub;
|
|
604
|
+
data.expiration = new Date(payload.exp * 1000).toISOString();
|
|
605
|
+
delete data.jwt;
|
|
606
|
+
} catch (e) {
|
|
607
|
+
throw new StatusCodeError(400, e.message);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
frag.name = sanitizeName(data.id);
|
|
611
|
+
frag.type = 'apiKey';
|
|
612
|
+
frag.relPath.push(frag.name);
|
|
613
|
+
}
|
|
614
|
+
if (frag.type === 'apiKey') {
|
|
615
|
+
if (data.jwt) {
|
|
616
|
+
throw new StatusCodeError(400, 'jwt not allowed in existing apiKey');
|
|
617
|
+
}
|
|
618
|
+
const oldData = deepGetOrCreate(old, frag.relPath, true);
|
|
619
|
+
data.created = oldData.created || new Date().toISOString();
|
|
620
|
+
// ensure that the name is equal to the sanitized id
|
|
621
|
+
if (frag.name !== sanitizeName(data.id)) {
|
|
622
|
+
throw new StatusCodeError(400, 'apiKey id mismatch');
|
|
623
|
+
}
|
|
624
|
+
}
|
|
548
625
|
if (frag.type === 'secrets') {
|
|
549
626
|
// create new secret with random id
|
|
550
627
|
frag.name = crypto.randomBytes(32).toString('base64url');
|
|
@@ -593,6 +670,7 @@ export class ConfigStore {
|
|
|
593
670
|
if (Array.isArray(data)) {
|
|
594
671
|
const users = [...old.users];
|
|
595
672
|
for (const userData of data) {
|
|
673
|
+
validateUniqueEmail(userData.email, users);
|
|
596
674
|
users.push({
|
|
597
675
|
...createUser(),
|
|
598
676
|
...userData,
|
|
@@ -605,6 +683,7 @@ export class ConfigStore {
|
|
|
605
683
|
...data,
|
|
606
684
|
...user,
|
|
607
685
|
};
|
|
686
|
+
validateUniqueEmail(data.email, old.users);
|
|
608
687
|
frag.name = user.id;
|
|
609
688
|
frag.relPath.push(user.id);
|
|
610
689
|
frag.type = 'user';
|
package/src/config-validator.js
CHANGED
|
@@ -16,6 +16,7 @@ import { ValidationError } from './ValidationError.js';
|
|
|
16
16
|
import accessAdminSchema from './schemas/access-admin.schema.cjs';
|
|
17
17
|
import accessSchema from './schemas/access.schema.cjs';
|
|
18
18
|
import accessSiteSchema from './schemas/access-site.schema.cjs';
|
|
19
|
+
import apiKeysSchema from './schemas/apikeys.schema.cjs';
|
|
19
20
|
import cdnSchema from './schemas/cdn.schema.cjs';
|
|
20
21
|
import cdnProdFastlySchema from './schemas/cdn-prod-fastly.schema.cjs';
|
|
21
22
|
import cdnProdCloudflareSchema from './schemas/cdn-prod-cloudflare.schema.cjs';
|
|
@@ -53,6 +54,7 @@ export const SCHEMAS = [
|
|
|
53
54
|
accessAdminSchema,
|
|
54
55
|
accessSchema,
|
|
55
56
|
accessSiteSchema,
|
|
57
|
+
apiKeysSchema,
|
|
56
58
|
cdnProdAkamaiSchema,
|
|
57
59
|
cdnProdCloudflareSchema,
|
|
58
60
|
cdnProdCloudfrontSchema,
|
|
@@ -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('./apikeys.schema.json');
|
|
@@ -0,0 +1,50 @@
|
|
|
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/apikeys",
|
|
5
|
+
"title": "ApiKeys",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"description": "Stores information about generated admin API keys. The keys listed here have no operational meaning, but are used to track the keys that have been generated.",
|
|
8
|
+
"patternProperties": {
|
|
9
|
+
"^[a-zA-Z0-9-_]+$": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"id": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"pattern": "^[a-zA-Z0-9-_/+]+$",
|
|
15
|
+
"description": "the API token id (jti)"
|
|
16
|
+
},
|
|
17
|
+
"created": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"format": "date-time"
|
|
20
|
+
},
|
|
21
|
+
"expiration": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"format": "date-time"
|
|
24
|
+
},
|
|
25
|
+
"roles": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"items": {
|
|
28
|
+
"type": "string"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"subject": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "the subject of the API token (sub)"
|
|
34
|
+
},
|
|
35
|
+
"description": {
|
|
36
|
+
"type": "string"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"required": [
|
|
40
|
+
"id",
|
|
41
|
+
"subject",
|
|
42
|
+
"created",
|
|
43
|
+
"expiration",
|
|
44
|
+
"roles"
|
|
45
|
+
],
|
|
46
|
+
"additionalProperties": false
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"additionalProperties": false
|
|
50
|
+
}
|
package/validate-json-schemas.sh
CHANGED
|
@@ -4,6 +4,7 @@ npx ajv-cli --spec=draft2019 -c ajv-formats compile \
|
|
|
4
4
|
-s src/schemas/access-admin.schema.json \
|
|
5
5
|
-s src/schemas/access-site.schema.json \
|
|
6
6
|
-s src/schemas/access.schema.json \
|
|
7
|
+
-s src/schemas/apikeys.schema.json \
|
|
7
8
|
-s src/schemas/cdn-prod-akamai.schema.json \
|
|
8
9
|
-s src/schemas/cdn-prod-cloudflare.schema.json \
|
|
9
10
|
-s src/schemas/cdn-prod-cloudfront.schema.json \
|