@adobe/helix-config-storage 2.1.7 → 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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
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
+
1
8
  ## [2.1.7](https://github.com/adobe/helix-config-storage/compare/v2.1.6...v2.1.7) (2025-04-15)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-config-storage",
3
- "version": "2.1.7",
3
+ "version": "2.2.0",
4
4
  "description": "Helix Config Storage",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -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
- limits: '*',
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
 
@@ -530,7 +563,7 @@ export class ConfigStore {
530
563
  }
531
564
  if (frag) {
532
565
  if (!old) {
533
- if (this.type === 'profiles' && (frag.type === 'tokens' || frag.type === 'secrets' || frag.name === 'features' || frag.name === 'limits')) {
566
+ if (this.type === 'profiles' && frag.allowEmpty) {
534
567
  old = {};
535
568
  } else {
536
569
  throw new StatusCodeError(404, 'config not found.');
@@ -561,6 +594,34 @@ export class ConfigStore {
561
594
  // don't expose hash in return value
562
595
  delete ret.hash;
563
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
+ }
564
625
  if (frag.type === 'secrets') {
565
626
  // create new secret with random id
566
627
  frag.name = crypto.randomBytes(32).toString('base64url');
@@ -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
+ }
@@ -23,6 +23,9 @@
23
23
  "secrets": {
24
24
  "$ref": "https://ns.adobe.com/helix/config/secrets"
25
25
  },
26
+ "apiKeys": {
27
+ "$ref": "https://ns.adobe.com/helix/config/apikeys"
28
+ },
26
29
  "tokens": {
27
30
  "$ref": "https://ns.adobe.com/helix/config/tokens"
28
31
  },
@@ -44,6 +44,9 @@
44
44
  "secrets": {
45
45
  "$ref": "https://ns.adobe.com/helix/config/secrets"
46
46
  },
47
+ "apiKeys": {
48
+ "$ref": "https://ns.adobe.com/helix/config/apikeys"
49
+ },
47
50
  "groups": {
48
51
  "$ref": "https://ns.adobe.com/helix/config/groups"
49
52
  },
@@ -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 \