@adobe/spacecat-shared-data-access 2.108.0 → 2.109.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
+ # [@adobe/spacecat-shared-data-access-v2.109.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v2.108.0...@adobe/spacecat-shared-data-access-v2.109.0) (2026-02-16)
2
+
3
+
4
+ ### Features
5
+
6
+ * consumer entity to register technical accounts ([#1341](https://github.com/adobe/spacecat-shared/issues/1341)) ([a0572e8](https://github.com/adobe/spacecat-shared/commit/a0572e8f52c95d90b986495a110ec66f58caa77e))
7
+
1
8
  # [@adobe/spacecat-shared-data-access-v2.108.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v2.107.0...@adobe/spacecat-shared-data-access-v2.108.0) (2026-02-13)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-data-access",
3
- "version": "2.108.0",
3
+ "version": "2.109.0",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
package/src/index.js CHANGED
@@ -40,12 +40,18 @@ export default function dataAccessWrapper(fn) {
40
40
  DYNAMO_TABLE_NAME_DATA = TABLE_NAME_DATA,
41
41
  S3_CONFIG_BUCKET: s3Bucket,
42
42
  AWS_REGION: region,
43
+ S2S_ALLOWED_IMS_ORG_IDS: s2sAllowedImsOrgIdsRaw,
43
44
  } = context.env;
44
45
 
46
+ const s2sAllowedImsOrgIds = s2sAllowedImsOrgIdsRaw
47
+ ? s2sAllowedImsOrgIdsRaw.split(',').map((id) => id.trim()).filter(Boolean)
48
+ : [];
49
+
45
50
  context.dataAccess = createDataAccess({
46
51
  tableNameData: DYNAMO_TABLE_NAME_DATA,
47
52
  s3Bucket,
48
53
  region,
54
+ s2sAllowedImsOrgIds,
49
55
  }, log);
50
56
  }
51
57
 
@@ -18,6 +18,7 @@ import AsyncJobCollection from '../async-job/async-job.collection.js';
18
18
  import AuditCollection from '../audit/audit.collection.js';
19
19
  import AuditUrlCollection from '../audit-url/audit-url.collection.js';
20
20
  import ConfigurationCollection from '../configuration/configuration.collection.js';
21
+ import ConsumerCollection from '../consumer/consumer.collection.js';
21
22
  import ExperimentCollection from '../experiment/experiment.collection.js';
22
23
  import EntitlementCollection from '../entitlement/entitlement.collection.js';
23
24
  import FixEntityCollection from '../fix-entity/fix-entity.collection.js';
@@ -49,6 +50,7 @@ import ApiKeySchema from '../api-key/api-key.schema.js';
49
50
  import AsyncJobSchema from '../async-job/async-job.schema.js';
50
51
  import AuditSchema from '../audit/audit.schema.js';
51
52
  import AuditUrlSchema from '../audit-url/audit-url.schema.js';
53
+ import ConsumerSchema from '../consumer/consumer.schema.js';
52
54
  import EntitlementSchema from '../entitlement/entitlement.schema.js';
53
55
  import FixEntitySchema from '../fix-entity/fix-entity.schema.js';
54
56
  import FixEntitySuggestionSchema from '../fix-entity-suggestion/fix-entity-suggestion.schema.js';
@@ -90,10 +92,12 @@ class EntityRegistry {
90
92
  * @param {Object} services - Dictionary of services keyed by datastore type.
91
93
  * @param {Object} services.dynamo - The ElectroDB service instance for DynamoDB operations.
92
94
  * @param {{s3Client: S3Client, s3Bucket: string}|null} [services.s3] - S3 service configuration.
95
+ * @param {Object} config - Configuration object containing environment-derived settings.
93
96
  * @param {Object} log - A logger for capturing and logging information.
94
97
  */
95
- constructor(services, log) {
98
+ constructor(services, config, log) {
96
99
  this.services = services;
100
+ this.config = config;
97
101
  this.log = log;
98
102
  this.collections = new Map();
99
103
 
@@ -142,6 +146,14 @@ class EntityRegistry {
142
146
  return collections;
143
147
  }
144
148
 
149
+ /**
150
+ * Returns the camelCase names of all registered entities (including Configuration).
151
+ * @returns {string[]} - An array of entity names.
152
+ */
153
+ getEntityNames() {
154
+ return [...Object.keys(this.constructor.entities), 'configuration'];
155
+ }
156
+
145
157
  static getEntities() {
146
158
  return Object.keys(this.entities).reduce((acc, key) => {
147
159
  acc[key] = this.entities[key].schema.toElectroDBSchema();
@@ -159,6 +171,7 @@ EntityRegistry.registerEntity(ApiKeySchema, ApiKeyCollection);
159
171
  EntityRegistry.registerEntity(AsyncJobSchema, AsyncJobCollection);
160
172
  EntityRegistry.registerEntity(AuditSchema, AuditCollection);
161
173
  EntityRegistry.registerEntity(AuditUrlSchema, AuditUrlCollection);
174
+ EntityRegistry.registerEntity(ConsumerSchema, ConsumerCollection);
162
175
  EntityRegistry.registerEntity(EntitlementSchema, EntitlementCollection);
163
176
  EntityRegistry.registerEntity(FixEntitySchema, FixEntityCollection);
164
177
  EntityRegistry.registerEntity(FixEntitySuggestionSchema, FixEntitySuggestionCollection);
@@ -0,0 +1,149 @@
1
+ /*
2
+ * Copyright 2025 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
+
13
+ import { hasText, isNonEmptyArray } from '@adobe/spacecat-shared-utils';
14
+
15
+ import BaseCollection from '../base/base.collection.js';
16
+ import ValidationError from '../../errors/validation.error.js';
17
+ import Consumer from './consumer.model.js';
18
+
19
+ /**
20
+ * ConsumerCollection - A collection class responsible for managing Consumer entities.
21
+ * Extends the BaseCollection to provide specific methods for interacting with Consumer records.
22
+ *
23
+ * @class ConsumerCollection
24
+ * @extends BaseCollection
25
+ */
26
+ class ConsumerCollection extends BaseCollection {
27
+ static COLLECTION_NAME = 'ConsumerCollection';
28
+
29
+ #validCapabilities = null;
30
+
31
+ /**
32
+ * Returns the set of valid capabilities, generated dynamically from all registered
33
+ * entity names and operations. Computed once and cached.
34
+ * Format: entityName:operation (e.g. "site:read", "organization:write")
35
+ * @returns {Set<string>} - The set of valid capability strings.
36
+ * @private
37
+ */
38
+ #getValidCapabilities() {
39
+ if (!this.#validCapabilities) {
40
+ const entityNames = this.entityRegistry.getEntityNames();
41
+ this.#validCapabilities = new Set(
42
+ entityNames.flatMap(
43
+ (name) => Consumer.CAPABILITIES.map((op) => `${name}:${op}`),
44
+ ),
45
+ );
46
+ }
47
+ return this.#validCapabilities;
48
+ }
49
+
50
+ /**
51
+ * Validates that all capabilities in the given array are known entity:operation pairs.
52
+ * Called during both create() and save() to prevent capability escalation.
53
+ * @param {string[]} capabilities - The capabilities to validate.
54
+ * @throws {ValidationError} - Throws if any capability is not recognized.
55
+ */
56
+ validateCapabilities(capabilities) {
57
+ if (!isNonEmptyArray(capabilities)) {
58
+ return;
59
+ }
60
+
61
+ const validCapabilities = this.#getValidCapabilities();
62
+ const invalid = capabilities.filter((cap) => !validCapabilities.has(cap));
63
+
64
+ if (invalid.length > 0) {
65
+ throw new ValidationError(
66
+ `Invalid capabilities: [${invalid.join(', ')}]`,
67
+ this,
68
+ );
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Validates that the given imsOrgId is in the allowed list from config.
74
+ * @param {string} imsOrgId - The IMS Org ID to validate.
75
+ * @throws {ValidationError} - Throws if the imsOrgId is not in the allowed list.
76
+ * @private
77
+ */
78
+ #validateImsOrgId(imsOrgId) {
79
+ const { s2sAllowedImsOrgIds } = this.entityRegistry.config;
80
+
81
+ if (!isNonEmptyArray(s2sAllowedImsOrgIds)) {
82
+ throw new ValidationError(
83
+ 'S2S_ALLOWED_IMS_ORG_IDS is not configured. Cannot create a consumer without an allowlist.',
84
+ this,
85
+ );
86
+ }
87
+
88
+ if (!s2sAllowedImsOrgIds.includes(imsOrgId)) {
89
+ throw new ValidationError(
90
+ `The imsOrgId "${imsOrgId}" is not in the list of allowed IMS Org IDs`,
91
+ this,
92
+ );
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Validates that no existing consumer has the same clientId.
98
+ * Uses findByClientId() to enforce global uniqueness, which is correct because IMS
99
+ * client_ids are globally unique per integration in Adobe Developer Console regardless
100
+ * of the owning org. The composite index ['clientId', 'imsOrgId'] exists for efficient
101
+ * scoped lookups, not to imply per-org uniqueness.
102
+ * @param {string} clientId - The clientId to check.
103
+ * @throws {ValidationError} - Throws if a consumer with this clientId already exists.
104
+ * @private
105
+ */
106
+ async #validateClientIdUniqueness(clientId) {
107
+ if (!hasText(clientId)) {
108
+ throw new ValidationError(
109
+ 'clientId is required to create a consumer',
110
+ this,
111
+ );
112
+ }
113
+
114
+ const existing = await this.findByClientId(clientId);
115
+ if (existing) {
116
+ throw new ValidationError(
117
+ `A consumer with clientId "${clientId}" already exists`,
118
+ this,
119
+ );
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Creates a new Consumer entity after validating imsOrgId, capabilities, and clientId uniqueness.
125
+ * @param {Object} item - The data for the entity to be created.
126
+ * @param {Object} [options] - Additional options for the creation process.
127
+ * @returns {Promise<BaseModel>} - A promise that resolves to the created model instance.
128
+ * @throws {ValidationError} - Throws if validation fails.
129
+ */
130
+ async create(item, options = {}) {
131
+ this.#validateImsOrgId(item?.imsOrgId);
132
+ this.validateCapabilities(item?.capabilities);
133
+ await this.#validateClientIdUniqueness(item?.clientId);
134
+ return super.create(item, options);
135
+ }
136
+
137
+ /**
138
+ * Bulk creation is not supported for Consumer entities.
139
+ * All consumers must be created individually via create() to enforce
140
+ * imsOrgId allowlist, capability validation, and clientId uniqueness checks.
141
+ * @throws {Error} Always throws — use create() instead.
142
+ */
143
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
144
+ async createMany(_items, _parent) {
145
+ throw new Error('createMany is not supported for Consumer. Use create() instead.');
146
+ }
147
+ }
148
+
149
+ export default ConsumerCollection;
@@ -0,0 +1,61 @@
1
+ /*
2
+ * Copyright 2025 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
+
13
+ import { isIsoDate } from '@adobe/spacecat-shared-utils';
14
+
15
+ import BaseModel from '../base/base.model.js';
16
+
17
+ /**
18
+ * Consumer - A class representing a Consumer entity.
19
+ * Provides methods to access and manipulate Consumer-specific data.
20
+ *
21
+ * @class Consumer
22
+ * @extends BaseModel
23
+ */
24
+ class Consumer extends BaseModel {
25
+ static ENTITY_NAME = 'Consumer';
26
+
27
+ static STATUS = {
28
+ ACTIVE: 'ACTIVE',
29
+ SUSPENDED: 'SUSPENDED',
30
+ REVOKED: 'REVOKED',
31
+ };
32
+
33
+ /**
34
+ * Checks whether this consumer has been revoked.
35
+ * @returns {boolean} - True if the consumer has been revoked.
36
+ */
37
+ isRevoked() {
38
+ return this.getStatus() === Consumer.STATUS.REVOKED
39
+ || (isIsoDate(this.getRevokedAt()) && new Date(this.getRevokedAt()) <= new Date());
40
+ }
41
+
42
+ /**
43
+ * Saves the consumer after validating capabilities against the allowlist.
44
+ * Prevents capability escalation via setCapabilities() + save().
45
+ * @async
46
+ * @returns {Promise<Consumer>} - The saved consumer instance.
47
+ * @throws {ValidationError} - If capabilities are invalid.
48
+ */
49
+ async save() {
50
+ this.collection.validateCapabilities(this.getCapabilities());
51
+ return super.save();
52
+ }
53
+
54
+ static CAPABILITIES = ['read', 'write', 'delete'];
55
+
56
+ static IMS_ORG_ID_REGEX = /^[a-z0-9]{24}@AdobeOrg$/i;
57
+
58
+ static TECHNICAL_ACCOUNT_ID_REGEX = /^[a-z0-9]{24}@techacct\.adobe\.com$/i;
59
+ }
60
+
61
+ export default Consumer;
@@ -0,0 +1,65 @@
1
+ /*
2
+ * Copyright 2025 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
+
13
+ import { isIsoDate } from '@adobe/spacecat-shared-utils';
14
+
15
+ import SchemaBuilder from '../base/schema.builder.js';
16
+ import Consumer from './consumer.model.js';
17
+ import ConsumerCollection from './consumer.collection.js';
18
+
19
+ /*
20
+ Schema Doc: https://electrodb.dev/en/modeling/schema/
21
+ Attribute Doc: https://electrodb.dev/en/modeling/attributes/
22
+ Indexes Doc: https://electrodb.dev/en/modeling/indexes/
23
+ */
24
+
25
+ const schema = new SchemaBuilder(Consumer, ConsumerCollection)
26
+ .addAttribute('clientId', {
27
+ type: 'string',
28
+ required: true,
29
+ readOnly: true,
30
+ })
31
+ .addAttribute('technicalAccountId', {
32
+ type: 'string',
33
+ required: true,
34
+ readOnly: true,
35
+ validate: (value) => Consumer.TECHNICAL_ACCOUNT_ID_REGEX.test(value),
36
+ })
37
+ .addAttribute('consumerName', {
38
+ type: 'string',
39
+ required: true,
40
+ })
41
+ .addAttribute('status', {
42
+ type: Object.values(Consumer.STATUS),
43
+ required: true,
44
+ })
45
+ .addAttribute('capabilities', {
46
+ type: 'list',
47
+ required: true,
48
+ items: {
49
+ type: 'string',
50
+ },
51
+ })
52
+ .addAttribute('revokedAt', {
53
+ type: 'string',
54
+ validate: (value) => !value || isIsoDate(value),
55
+ })
56
+ .addAttribute('imsOrgId', {
57
+ type: 'string',
58
+ required: true,
59
+ readOnly: true,
60
+ validate: (value) => Consumer.IMS_ORG_ID_REGEX.test(value),
61
+ })
62
+ .addAllIndex(['imsOrgId'])
63
+ .addAllIndex(['clientId', 'imsOrgId']);
64
+
65
+ export default schema.build();
@@ -0,0 +1,39 @@
1
+ /*
2
+ * Copyright 2025 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
+
13
+ import type { BaseCollection, BaseModel } from '../index';
14
+
15
+ export type ConsumerStatus = 'ACTIVE' | 'SUSPENDED' | 'REVOKED';
16
+
17
+ export interface Consumer extends BaseModel {
18
+ getClientId(): string;
19
+ getTechnicalAccountId(): string;
20
+ getConsumerName(): string;
21
+ getStatus(): ConsumerStatus;
22
+ getCapabilities(): string[];
23
+ getImsOrgId(): string;
24
+ getRevokedAt(): string | undefined;
25
+ isRevoked(): boolean;
26
+ setConsumerName(consumerName: string): Consumer;
27
+ setStatus(status: ConsumerStatus): Consumer;
28
+ setCapabilities(capabilities: string[]): Consumer;
29
+ setRevokedAt(revokedAt: string): Consumer;
30
+ }
31
+
32
+ export interface ConsumerCollection extends BaseCollection<Consumer> {
33
+ allByImsOrgId(imsOrgId: string): Promise<Consumer[]>;
34
+ allByClientId(clientId: string): Promise<Consumer[]>;
35
+ allByClientIdAndImsOrgId(clientId: string, imsOrgId: string): Promise<Consumer[]>;
36
+ findByImsOrgId(imsOrgId: string): Promise<Consumer | null>;
37
+ findByClientId(clientId: string): Promise<Consumer | null>;
38
+ findByClientIdAndImsOrgId(clientId: string, imsOrgId: string): Promise<Consumer | null>;
39
+ }
@@ -0,0 +1,19 @@
1
+ /*
2
+ * Copyright 2025 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
+
13
+ import Consumer from './consumer.model.js';
14
+ import ConsumerCollection from './consumer.collection.js';
15
+
16
+ export {
17
+ Consumer,
18
+ ConsumerCollection,
19
+ };
@@ -14,6 +14,7 @@ export type * from './audit';
14
14
  export type * from './async-job';
15
15
  export type * from './configuration';
16
16
  export type * from './base';
17
+ export type * from './consumer';
17
18
  export type * from './fix-entity';
18
19
  export type * from './fix-entity-suggestion';
19
20
  export type * from './experiment';
@@ -16,6 +16,7 @@ export * from './audit/index.js';
16
16
  export * from './audit-url/index.js';
17
17
  export * from './base/index.js';
18
18
  export * from './configuration/index.js';
19
+ export * from './consumer/index.js';
19
20
  export * from './entitlement/index.js';
20
21
  export * from './fix-entity/index.js';
21
22
  export * from './fix-entity-suggestion/index.js';
@@ -115,7 +115,7 @@ export const createDataAccess = (config, log = console, client = undefined) => {
115
115
  const rawClient = createRawClient(client);
116
116
  const electroService = createElectroService(rawClient, config, log);
117
117
  const services = createServices(electroService, config);
118
- const entityRegistry = new EntityRegistry(services, log);
118
+ const entityRegistry = new EntityRegistry(services, config, log);
119
119
 
120
120
  return entityRegistry.getCollections();
121
121
  };