@adobe/helix-config-storage 1.0.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.
Files changed (82) hide show
  1. package/.eslintrc.cjs +16 -0
  2. package/.husky/pre-commit +1 -0
  3. package/.mocha-multi.json +6 -0
  4. package/.nycrc.json +10 -0
  5. package/.releaserc.cjs +16 -0
  6. package/CHANGELOG.md +877 -0
  7. package/CODE_OF_CONDUCT.md +74 -0
  8. package/CONTRIBUTING.md +74 -0
  9. package/LICENSE.txt +264 -0
  10. package/README.md +40 -0
  11. package/package.json +71 -0
  12. package/src/ValidationError.js +95 -0
  13. package/src/config-merge.js +145 -0
  14. package/src/config-store.js +359 -0
  15. package/src/config-validator.js +112 -0
  16. package/src/index.js +15 -0
  17. package/src/schemas/access-admin.schema.cjs +12 -0
  18. package/src/schemas/access-admin.schema.json +57 -0
  19. package/src/schemas/access-site.schema.cjs +12 -0
  20. package/src/schemas/access-site.schema.json +24 -0
  21. package/src/schemas/access.schema.cjs +12 -0
  22. package/src/schemas/access.schema.json +22 -0
  23. package/src/schemas/cdn-prod-akamai.schema.cjs +12 -0
  24. package/src/schemas/cdn-prod-akamai.schema.json +45 -0
  25. package/src/schemas/cdn-prod-cloudflare.schema.cjs +12 -0
  26. package/src/schemas/cdn-prod-cloudflare.schema.json +41 -0
  27. package/src/schemas/cdn-prod-cloudfront.schema.cjs +12 -0
  28. package/src/schemas/cdn-prod-cloudfront.schema.json +41 -0
  29. package/src/schemas/cdn-prod-fastly.schema.cjs +12 -0
  30. package/src/schemas/cdn-prod-fastly.schema.json +40 -0
  31. package/src/schemas/cdn-prod-managed.schema.cjs +12 -0
  32. package/src/schemas/cdn-prod-managed.schema.json +29 -0
  33. package/src/schemas/cdn.schema.cjs +12 -0
  34. package/src/schemas/cdn.schema.json +75 -0
  35. package/src/schemas/code.schema.cjs +12 -0
  36. package/src/schemas/code.schema.json +43 -0
  37. package/src/schemas/common.schema.cjs +12 -0
  38. package/src/schemas/common.schema.json +27 -0
  39. package/src/schemas/content-source-google.schema.cjs +12 -0
  40. package/src/schemas/content-source-google.schema.json +26 -0
  41. package/src/schemas/content-source-markup.schema.cjs +12 -0
  42. package/src/schemas/content-source-markup.schema.json +24 -0
  43. package/src/schemas/content-source-onedrive.schema.cjs +12 -0
  44. package/src/schemas/content-source-onedrive.schema.json +28 -0
  45. package/src/schemas/content.schema.cjs +12 -0
  46. package/src/schemas/content.schema.json +30 -0
  47. package/src/schemas/folders.schema.cjs +12 -0
  48. package/src/schemas/folders.schema.json +13 -0
  49. package/src/schemas/groups.schema.cjs +12 -0
  50. package/src/schemas/groups.schema.json +39 -0
  51. package/src/schemas/headers.schema.cjs +12 -0
  52. package/src/schemas/headers.schema.json +16 -0
  53. package/src/schemas/metadata-source.schema.cjs +12 -0
  54. package/src/schemas/metadata-source.schema.json +16 -0
  55. package/src/schemas/org.schema.cjs +12 -0
  56. package/src/schemas/org.schema.json +26 -0
  57. package/src/schemas/profile.schema.cjs +12 -0
  58. package/src/schemas/profile.schema.json +57 -0
  59. package/src/schemas/profiles.schema.cjs +12 -0
  60. package/src/schemas/profiles.schema.json +28 -0
  61. package/src/schemas/public.schema.cjs +12 -0
  62. package/src/schemas/public.schema.json +8 -0
  63. package/src/schemas/robots.schema.cjs +12 -0
  64. package/src/schemas/robots.schema.json +13 -0
  65. package/src/schemas/sidekick.schema.cjs +12 -0
  66. package/src/schemas/sidekick.schema.json +112 -0
  67. package/src/schemas/site.schema.cjs +12 -0
  68. package/src/schemas/site.schema.json +74 -0
  69. package/src/schemas/sites.schema.cjs +12 -0
  70. package/src/schemas/sites.schema.json +28 -0
  71. package/src/schemas/tokens.schema.cjs +12 -0
  72. package/src/schemas/tokens.schema.json +28 -0
  73. package/src/schemas/user.schema.cjs +12 -0
  74. package/src/schemas/user.schema.json +42 -0
  75. package/src/schemas/users.schema.cjs +12 -0
  76. package/src/schemas/users.schema.json +10 -0
  77. package/src/status-code-error.js +22 -0
  78. package/src/utils.js +223 -0
  79. package/types/org-config.d.ts +51 -0
  80. package/types/profile-config.d.ts +368 -0
  81. package/types/site-config.d.ts +375 -0
  82. package/validate-json-schemas.sh +32 -0
@@ -0,0 +1,145 @@
1
+ /*
2
+ * Copyright 2023 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
+ /* eslint-disable no-continue, no-param-reassign */
13
+
14
+ /**
15
+ * @typedef RootProperty
16
+ * @property {boolean} atomic whether the property should be overwritten atomically
17
+ * @property {boolean} isModified whether the property need special merging
18
+ */
19
+
20
+ const ROOT_PROPERTIES = {
21
+ name: { atomic: true },
22
+ title: { atomic: true },
23
+ description: { atomic: true },
24
+ content: { atomic: true },
25
+ code: { atomic: true },
26
+ folders: {},
27
+ headers: { isModifier: true },
28
+ cdn: {},
29
+ access: {},
30
+ sidekick: {},
31
+ metadata: { isModifier: true },
32
+ robots: {},
33
+ extends: {},
34
+ tokens: {},
35
+ public: {},
36
+ groups: {},
37
+ };
38
+
39
+ const FORCED_TYPES = {
40
+ '.cdn.prod.route': 'array',
41
+ };
42
+
43
+ /**
44
+ * Merges the `src` object into the `dst` object.
45
+ * - if the dst property is a scalar, it will be overwritten by the dst property
46
+ * - if the src property is an empty string, it will delete the dst property
47
+ * - if the dst property is an array, it will be expanded by the src property uniquely
48
+ * - some well known paths are forced to be arrays
49
+ * @param {object} dst
50
+ * @param {object} src
51
+ * @param {boolean} isModifier specifies that the objects are modifiers sheets and need special
52
+ * merging
53
+ */
54
+ function merge(dst, src, path, isModifier) {
55
+ for (const [key, value] of Object.entries(src)) {
56
+ const itemPath = `${path}.${key}`;
57
+ if (value === '') {
58
+ // remove key if value is empty string (keep false, 0)
59
+ delete dst[key];
60
+ } else if (typeof value !== 'object') {
61
+ // handle non object
62
+ if (key in dst) {
63
+ let dstValue = dst[key];
64
+ if (!Array.isArray(dstValue) && FORCED_TYPES[itemPath] === 'array') {
65
+ dstValue = [dst[key]];
66
+ dst[key] = dstValue;
67
+ }
68
+ if (!Array.isArray(dstValue)) {
69
+ dst[key] = value;
70
+ } else if (!dstValue.includes(value)) {
71
+ // only add to array if not exist yet
72
+ dstValue.push(value);
73
+ }
74
+ } else {
75
+ dst[key] = value;
76
+ }
77
+ } else if (Array.isArray(value)) {
78
+ // handle array
79
+ if (key in dst) {
80
+ if (!Array.isArray(dst[key])) {
81
+ dst[key] = [dst[key]];
82
+ }
83
+ } else {
84
+ dst[key] = [];
85
+ }
86
+ const dstArray = dst[key];
87
+ for (const item of value) {
88
+ if (typeof item === 'object') {
89
+ // todo: handle arrays in arrays eventually
90
+ // special case for modifier sheets
91
+ if (isModifier && item.key) {
92
+ const idx = dstArray.findIndex((i) => i.key === item.key);
93
+ if (idx >= 0) {
94
+ dstArray.splice(idx, 1);
95
+ }
96
+ if (!item.value) {
97
+ // don't set empty modifier
98
+ continue;
99
+ }
100
+ }
101
+ const dstObj = Object.create(null);
102
+ dstArray.push(dstObj);
103
+ merge(dstObj, item, itemPath, isModifier);
104
+ } else if (!dstArray.includes(item)) {
105
+ dstArray.push(item);
106
+ }
107
+ }
108
+ } else {
109
+ // handle plain object
110
+ // eslint-disable-next-line no-use-before-define
111
+ mergeObject(dst, key, value, `${path}.${key}`, isModifier);
112
+ }
113
+ }
114
+ }
115
+
116
+ function mergeObject(dst, key, value, path, isModifier) {
117
+ const dstObj = dst[key] ?? Object.create(null);
118
+ if (!dst[key]) {
119
+ dst[key] = dstObj;
120
+ }
121
+ merge(dstObj, value, path, isModifier);
122
+ }
123
+
124
+ function mergeConfig(dst, src) {
125
+ Object.entries(ROOT_PROPERTIES).forEach(([key, { atomic, isModifier }]) => {
126
+ if (src[key]) {
127
+ if (atomic) {
128
+ dst[key] = src[key];
129
+ } else {
130
+ mergeObject(dst, key, src[key], `.${key}`, isModifier);
131
+ }
132
+ }
133
+ });
134
+ }
135
+
136
+ export function getMergedConfig(site, profile) {
137
+ if (!profile) {
138
+ return site;
139
+ }
140
+ const ret = Object.create(null);
141
+ ret.version = 1;
142
+ mergeConfig(ret, profile);
143
+ mergeConfig(ret, site);
144
+ return ret;
145
+ }
@@ -0,0 +1,359 @@
1
+ /*
2
+ * Copyright 2023 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
+ /* eslint-disable no-param-reassign */
13
+ import { isDeepStrictEqual } from 'util';
14
+ import { HelixStorage } from '@adobe/helix-shared-storage';
15
+ import { StatusCodeError } from './status-code-error.js';
16
+ import {
17
+ createToken, createUser,
18
+ migrateToken,
19
+ updateCodeSource,
20
+ updateContentSource,
21
+ deepGetOrCreate, deepPut,
22
+ } from './utils.js';
23
+ import { validate as validateSchema } from './config-validator.js';
24
+
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
+ tokens: {
45
+ '.': 'tokens',
46
+ '.param': {
47
+ name: 'id',
48
+ '.': 'token',
49
+ },
50
+ },
51
+ groups: {
52
+ '.': 'object',
53
+ '.param': {
54
+ '.': 'object',
55
+ members: 'object',
56
+ },
57
+ },
58
+ public: 'object',
59
+ robots: 'object',
60
+ };
61
+
62
+ const FRAGMENTS = {
63
+ sites: {
64
+ ...FRAGMENTS_COMMON,
65
+ extends: 'object',
66
+ },
67
+ profiles: FRAGMENTS_COMMON,
68
+ org: {
69
+ users: {
70
+ '.': 'users',
71
+ '.param': {
72
+ name: 'id',
73
+ '.': 'user',
74
+ },
75
+ },
76
+ groups: {
77
+ '.': 'object',
78
+ '.param': {
79
+ '.': 'object',
80
+ members: 'object',
81
+ },
82
+ },
83
+ },
84
+ };
85
+
86
+ export function getFragmentInfo(type, relPath) {
87
+ if (!relPath) {
88
+ return null;
89
+ }
90
+ const parts = relPath.split('/');
91
+ let fragment = FRAGMENTS[type];
92
+ const info = {
93
+ relPath,
94
+ };
95
+ for (const part of parts) {
96
+ let next = fragment[part];
97
+ if (!next) {
98
+ next = fragment['.param'];
99
+ if (!next) {
100
+ throw new StatusCodeError(400, 'invalid object path.');
101
+ }
102
+ info[next.name] = part;
103
+ }
104
+ fragment = next;
105
+ }
106
+ if (typeof fragment === 'string') {
107
+ info.type = fragment;
108
+ } else {
109
+ info.type = fragment['.'];
110
+ }
111
+ return info;
112
+ }
113
+
114
+ /**
115
+ * Redact / transform the token config
116
+ * @param token
117
+ */
118
+ function redactToken(token) {
119
+ return {
120
+ id: token.id,
121
+ created: token.created,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Redact / transform the tokens config
127
+ * @param tokens
128
+ */
129
+ function redactTokens(tokens) {
130
+ return Object.values(tokens).map(redactToken);
131
+ }
132
+
133
+ /**
134
+ * redact information from the config
135
+ * @param {object} config
136
+ * @param {object} frag
137
+ */
138
+ function redact(config, frag) {
139
+ if (!config) {
140
+ return config;
141
+ }
142
+ let ret = config;
143
+ if (frag?.type === 'tokens') {
144
+ ret = redactTokens(config);
145
+ }
146
+ if (frag?.type === 'token') {
147
+ ret = redactToken(config);
148
+ }
149
+ if (ret.tokens) {
150
+ // eslint-disable-next-line no-param-reassign
151
+ ret.tokens = redactTokens(ret.tokens);
152
+ }
153
+ return ret;
154
+ }
155
+
156
+ /**
157
+ * General purpose config store.
158
+ */
159
+ export class ConfigStore {
160
+ /**
161
+ * @param {string} org the org id
162
+ * @param {string} type store type (org, sites, profiles, secrets, users)
163
+ * @param {string} name config name
164
+ */
165
+ constructor(org, type = 'org', name = '') {
166
+ if (!org) {
167
+ throw new Error('org required');
168
+ }
169
+ if (org.includes('/') || type.includes('/') || name.includes('/')) {
170
+ throw new Error('orgId, type and name must not contain slashes');
171
+ }
172
+ this.org = org;
173
+ this.type = type;
174
+ this.name = name;
175
+ this.key = type === 'org'
176
+ ? `/orgs/${this.org}/${this.name || 'config'}.json`
177
+ : `/orgs/${this.org}/${this.type}/${this.name}.json`;
178
+ }
179
+
180
+ /**
181
+ * Validates the config and throws an error if not valid.
182
+ * @param {AdminContext} ctx
183
+ * @param {HelixSiteConfig|HelixProfileConfig} data the data of the config to be updated
184
+ * @returns {Promise<boolean|undefined>}
185
+ */
186
+ async validate(ctx, data) {
187
+ return validateSchema(data, this.type);
188
+ }
189
+
190
+ async create(ctx, data, relPath = '') {
191
+ if (relPath) {
192
+ throw new StatusCodeError(409, 'create not supported on substructures.');
193
+ }
194
+ const storage = HelixStorage.fromContext(ctx).configBus();
195
+ if (await storage.head(this.key)) {
196
+ throw new StatusCodeError(409, 'config already exists.');
197
+ }
198
+ if (data.tokens) {
199
+ throw new StatusCodeError(400, 'creating config with tokens not supported yet.');
200
+ }
201
+ if (this.type === 'org' && data.users) {
202
+ throw new StatusCodeError(400, 'creating org config with users is not supported yet.');
203
+ }
204
+ if (this.type !== 'org') {
205
+ updateContentSource(ctx, data.content);
206
+ updateCodeSource(ctx, data.code);
207
+ }
208
+ await this.validate(ctx, data);
209
+ await storage.put(this.key, JSON.stringify(data), 'application/json');
210
+ await this.purge(ctx, null, data);
211
+ }
212
+
213
+ async #list(ctx) {
214
+ const storage = HelixStorage.fromContext(ctx).configBus();
215
+ const key = `orgs/${this.org}/${this.type}/`;
216
+ const list = await storage.list(key);
217
+ return {
218
+ [this.type]: list.map((entry) => {
219
+ const siteKey = entry.key;
220
+ if (siteKey.endsWith('.json')) {
221
+ const name = siteKey.split('/').pop();
222
+ return {
223
+ path: `/config/${this.org}/${this.type}/${name}`,
224
+ name: name.substring(0, name.length - 5),
225
+ };
226
+ }
227
+ return null;
228
+ }).filter((entry) => entry),
229
+ };
230
+ }
231
+
232
+ async read(ctx, relPath = '') {
233
+ if (this.name === '' && (this.type === 'sites' || this.type === 'profiles')) {
234
+ return this.#list(ctx);
235
+ }
236
+
237
+ const storage = HelixStorage.fromContext(ctx).configBus();
238
+ const buf = await storage.get(this.key);
239
+ if (!buf) {
240
+ return null;
241
+ }
242
+ let obj = JSON.parse(buf);
243
+ const frag = getFragmentInfo(this.type, relPath);
244
+ if (frag) {
245
+ obj = deepGetOrCreate(obj, frag.relPath);
246
+ }
247
+ return redact(obj, frag) ?? null;
248
+ }
249
+
250
+ async update(ctx, data, relPath = '') {
251
+ const storage = HelixStorage.fromContext(ctx).configBus();
252
+ const buf = await storage.get(this.key);
253
+ let old = buf ? JSON.parse(buf) : null;
254
+ const frag = getFragmentInfo(this.type, relPath);
255
+ let config = data;
256
+ // set config to null if empty object
257
+ if (isDeepStrictEqual(config, {})) {
258
+ config = null;
259
+ }
260
+
261
+ let ret = null;
262
+ if (!config && frag?.type !== 'tokens') {
263
+ throw new StatusCodeError(400, 'no config in body.');
264
+ }
265
+ if (frag) {
266
+ if (!old) {
267
+ if (this.type === 'profiles' && frag.type === 'tokens') {
268
+ old = {};
269
+ } else {
270
+ throw new StatusCodeError(404, 'config not found.');
271
+ }
272
+ }
273
+ if (relPath === 'code') {
274
+ updateCodeSource(ctx, data);
275
+ } else if (relPath === 'content') {
276
+ updateContentSource(ctx, data);
277
+ } else if (frag.type === 'token') {
278
+ // don't allow to update individual token
279
+ throw new StatusCodeError(400, 'invalid object path.');
280
+ } else if (frag.type === 'tokens') {
281
+ // TODO: remove support after all helix4 projects are migrated
282
+ let token;
283
+ if (data.jwt) {
284
+ token = await migrateToken(this.org, data.jwt);
285
+ } else {
286
+ // create new token
287
+ token = createToken(this.org);
288
+ }
289
+
290
+ data = {
291
+ ...token,
292
+ };
293
+ // don't store token value in the config
294
+ delete data.value;
295
+ relPath = ['tokens', token.id];
296
+ frag.type = 'token';
297
+ ret = token;
298
+ // don't expose hash in return value
299
+ delete ret.hash;
300
+ } else if (frag.type === 'users') {
301
+ const user = createUser();
302
+ data = {
303
+ ...data,
304
+ ...user,
305
+ };
306
+ relPath = ['users', user.id];
307
+ frag.type = 'user';
308
+ // todo: define via "schema"
309
+ if (!old.users) {
310
+ old.users = [];
311
+ }
312
+ } else if (frag.type === 'user') {
313
+ const user = deepGetOrCreate(old, relPath);
314
+ if (!user) {
315
+ throw new StatusCodeError(404, 'object not found.');
316
+ }
317
+ if (data.id) {
318
+ if (data.id !== user.id) {
319
+ throw new StatusCodeError(400, 'object id mismatch.');
320
+ }
321
+ } else {
322
+ data.id = user.id;
323
+ }
324
+ }
325
+ config = deepPut(old, relPath, data);
326
+ } else if (this.type !== 'org') {
327
+ updateContentSource(ctx, config.content);
328
+ updateCodeSource(ctx, config.code);
329
+ }
330
+ await this.validate(ctx, config);
331
+ await storage.put(this.key, JSON.stringify(config), 'application/json');
332
+ await this.purge(ctx, buf ? JSON.parse(buf) : null, config);
333
+ return ret ?? redact(data, frag);
334
+ }
335
+
336
+ async remove(ctx, relPath) {
337
+ const storage = HelixStorage.fromContext(ctx).configBus();
338
+ const buf = await storage.get(this.key);
339
+ if (!buf) {
340
+ throw new StatusCodeError(404, 'config not found.');
341
+ }
342
+ const frag = getFragmentInfo(this.type, relPath);
343
+ if (frag) {
344
+ const data = deepPut(JSON.parse(buf), relPath, null);
345
+ await this.validate(ctx, data);
346
+ await storage.put(this.key, JSON.stringify(data), 'application/json');
347
+ await this.purge(ctx, JSON.parse(buf), data);
348
+ return;
349
+ }
350
+
351
+ await storage.remove(this.key);
352
+ await this.purge(ctx, JSON.parse(buf), null);
353
+ }
354
+
355
+ // eslint-disable-next-line class-methods-use-this,no-unused-vars
356
+ async purge(ctx, oldConfig, newConfig) {
357
+ // override in subclass
358
+ }
359
+ }
@@ -0,0 +1,112 @@
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
+ import Ajv2019 from 'ajv/dist/2019.js';
13
+ import ajvFormats from 'ajv-formats';
14
+ import { ValidationError } from './ValidationError.js';
15
+
16
+ import accessAdminSchema from './schemas/access-admin.schema.cjs';
17
+ import accessSchema from './schemas/access.schema.cjs';
18
+ import accessSiteSchema from './schemas/access-site.schema.cjs';
19
+ import cdnSchema from './schemas/cdn.schema.cjs';
20
+ import cdnProdFastlySchema from './schemas/cdn-prod-fastly.schema.cjs';
21
+ import cdnProdCloudflareSchema from './schemas/cdn-prod-cloudflare.schema.cjs';
22
+ import cdnProdAkamaiSchema from './schemas/cdn-prod-akamai.schema.cjs';
23
+ import cdnProdManagedSchema from './schemas/cdn-prod-managed.schema.cjs';
24
+ import cdnProdCloudfrontSchema from './schemas/cdn-prod-cloudfront.schema.cjs';
25
+ import commonSchema from './schemas/common.schema.cjs';
26
+ import codeSchema from './schemas/code.schema.cjs';
27
+ import contentSchema from './schemas/content.schema.cjs';
28
+ import foldersSchema from './schemas/folders.schema.cjs';
29
+ import groupsSchema from './schemas/groups.schema.cjs';
30
+ import googleSchema from './schemas/content-source-google.schema.cjs';
31
+ import headersSchema from './schemas/headers.schema.cjs';
32
+ import markupSchema from './schemas/content-source-markup.schema.cjs';
33
+ import metadataSchema from './schemas/metadata-source.schema.cjs';
34
+ import orgSchema from './schemas/org.schema.cjs';
35
+ import onedriveSchema from './schemas/content-source-onedrive.schema.cjs';
36
+ import publicSchema from './schemas/public.schema.cjs';
37
+ import profileSchema from './schemas/profile.schema.cjs';
38
+ import profilesSchema from './schemas/profiles.schema.cjs';
39
+ import robotsSchema from './schemas/robots.schema.cjs';
40
+ import sidekickSchema from './schemas/sidekick.schema.cjs';
41
+ import siteSchema from './schemas/site.schema.cjs';
42
+ import sitesSchema from './schemas/sites.schema.cjs';
43
+ import tokensSchema from './schemas/tokens.schema.cjs';
44
+ import userSchema from './schemas/user.schema.cjs';
45
+ import usersSchema from './schemas/users.schema.cjs';
46
+
47
+ export const SCHEMAS = [
48
+ accessSchema,
49
+ accessAdminSchema,
50
+ accessSiteSchema,
51
+ cdnSchema,
52
+ commonSchema,
53
+ contentSchema,
54
+ codeSchema,
55
+ foldersSchema,
56
+ googleSchema,
57
+ groupsSchema,
58
+ headersSchema,
59
+ markupSchema,
60
+ metadataSchema,
61
+ orgSchema,
62
+ onedriveSchema,
63
+ publicSchema,
64
+ profileSchema,
65
+ profilesSchema,
66
+ robotsSchema,
67
+ sidekickSchema,
68
+ siteSchema,
69
+ sitesSchema,
70
+ tokensSchema,
71
+ userSchema,
72
+ usersSchema,
73
+ cdnProdFastlySchema,
74
+ cdnProdCloudflareSchema,
75
+ cdnProdAkamaiSchema,
76
+ cdnProdManagedSchema,
77
+ cdnProdCloudfrontSchema,
78
+ ];
79
+
80
+ const SCHEMA_TYPES = {
81
+ profiles: profileSchema,
82
+ sites: siteSchema,
83
+ org: orgSchema,
84
+ };
85
+
86
+ /**
87
+ * Validates the loaded configuration and coerces types and sets defaults
88
+ * @param {object} config The configuration to validate
89
+ * @param {string} type the config type
90
+ * @returns {object} The validated configuration
91
+ */
92
+ export async function validate(config, type) {
93
+ const schema = SCHEMA_TYPES[type];
94
+ if (!schema) {
95
+ throw new Error(`no such type: ${type}`);
96
+ }
97
+ const ajv = new Ajv2019({
98
+ allErrors: true,
99
+ verbose: true,
100
+ useDefaults: true,
101
+ coerceTypes: 'array',
102
+ strict: false,
103
+ });
104
+ ajvFormats(ajv);
105
+
106
+ ajv.addSchema(SCHEMAS);
107
+ const res = ajv.validate(schema, config);
108
+ if (res) {
109
+ return res;
110
+ }
111
+ throw new ValidationError(ajv.errorsText(), ajv.errors);
112
+ }
package/src/index.js ADDED
@@ -0,0 +1,15 @@
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
+ export { ConfigStore } from './config-store.js';
13
+ export { SCHEMAS } from './config-validator.js';
14
+ export * from './ValidationError.js';
15
+ export * from './config-merge.js';
@@ -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('./access-admin.schema.json');
@@ -0,0 +1,57 @@
1
+ {
2
+ "$comment": "https://github.com/adobe/helix-config/blob/main/LICENSE.txt",
3
+ "$id": "https://ns.adobe.com/helix/config/access/admin",
4
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
5
+ "title": "Admin Access Config",
6
+ "type": "object",
7
+ "properties": {
8
+ "role": {
9
+ "title": "Role",
10
+ "type": "object",
11
+ "patternProperties": {
12
+ "^[a-z-_]+$": {
13
+ "description": "The email glob of the users or a group reference for the respective role.",
14
+ "type": "array",
15
+ "items": {"type": "string"}
16
+ }
17
+ },
18
+ "additionalProperties": false
19
+ },
20
+ "requireAuth": {
21
+ "description": "Enforce authentication if set to true. If set to 'auto' it will enforce authentication if a role mapping is defined. defaults to 'auto'.",
22
+ "default": "auto",
23
+ "oneOf": [
24
+ {
25
+ "type": "boolean"
26
+ },
27
+ {
28
+ "type": "string",
29
+ "enum": ["auto"]
30
+ }
31
+ ]
32
+ },
33
+ "defaultRole": {
34
+ "description": "the default roles assigned to the users. defaults to `basic_publish` for unauthenticated setups.",
35
+ "type": "array",
36
+ "items": {
37
+ "type": "string",
38
+ "enum": [
39
+ "admin",
40
+ "author",
41
+ "publish",
42
+ "develop",
43
+ "basic_author",
44
+ "basic_publish",
45
+ "config",
46
+ "config_admin"
47
+ ]
48
+ }
49
+ },
50
+ "apiKeyId": {
51
+ "description": "the id of the API key(s). this is used to validate the API KEYS and allows to invalidate them.",
52
+ "type": "array",
53
+ "items": {"type": "string"}
54
+ }
55
+ },
56
+ "additionalProperties": false
57
+ }