@adobe/spacecat-shared-data-access 3.80.0 → 4.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 (37) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +1 -1
  3. package/src/models/async-job/index.d.ts +11 -0
  4. package/src/models/base/entity.registry.js +15 -0
  5. package/src/models/idempotency-key/idempotency-key.collection.js +89 -0
  6. package/src/models/idempotency-key/idempotency-key.model.js +41 -0
  7. package/src/models/idempotency-key/idempotency-key.schema.js +51 -0
  8. package/src/models/idempotency-key/index.d.ts +41 -0
  9. package/src/models/idempotency-key/index.js +19 -0
  10. package/src/models/index.d.ts +5 -0
  11. package/src/models/index.js +5 -0
  12. package/src/models/oauth-nonce/index.d.ts +26 -0
  13. package/src/models/oauth-nonce/index.js +19 -0
  14. package/src/models/oauth-nonce/oauth-nonce.collection.js +59 -0
  15. package/src/models/oauth-nonce/oauth-nonce.model.js +35 -0
  16. package/src/models/oauth-nonce/oauth-nonce.schema.js +50 -0
  17. package/src/models/organization/index.d.ts +3 -1
  18. package/src/models/organization/organization.schema.js +1 -0
  19. package/src/models/preflight/index.d.ts +4 -7
  20. package/src/models/preflight/preflight.schema.js +13 -20
  21. package/src/models/task-management-connection/index.d.ts +82 -0
  22. package/src/models/task-management-connection/index.js +21 -0
  23. package/src/models/task-management-connection/metadata-validator.js +97 -0
  24. package/src/models/task-management-connection/task-management-connection.collection.js +61 -0
  25. package/src/models/task-management-connection/task-management-connection.model.js +119 -0
  26. package/src/models/task-management-connection/task-management-connection.schema.js +111 -0
  27. package/src/models/ticket/index.d.ts +40 -0
  28. package/src/models/ticket/index.js +19 -0
  29. package/src/models/ticket/ticket.collection.js +30 -0
  30. package/src/models/ticket/ticket.model.js +44 -0
  31. package/src/models/ticket/ticket.schema.js +72 -0
  32. package/src/models/ticket-suggestion/index.d.ts +26 -0
  33. package/src/models/ticket-suggestion/index.js +19 -0
  34. package/src/models/ticket-suggestion/ticket-suggestion.collection.js +29 -0
  35. package/src/models/ticket-suggestion/ticket-suggestion.model.js +36 -0
  36. package/src/models/ticket-suggestion/ticket-suggestion.schema.js +53 -0
  37. package/src/service/index.d.ts +10 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [@adobe/spacecat-shared-data-access-v4.0.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.81.0...@adobe/spacecat-shared-data-access-v4.0.0) (2026-07-03)
2
+
3
+ ### ⚠ BREAKING CHANGES
4
+
5
+ * **data-access:** drop startedAt/result/error from Preflight schema (SITES-47254) (#1740)
6
+
7
+ ### Features
8
+
9
+ * **data-access:** drop startedAt/result/error from Preflight schema (SITES-47254) ([#1740](https://github.com/adobe/spacecat-shared/issues/1740)) ([5b728bb](https://github.com/adobe/spacecat-shared/commit/5b728bb623060289221e744088b9762a8acfe519)), closes [#2713](https://github.com/adobe/spacecat-shared/issues/2713) [#2713](https://github.com/adobe/spacecat-shared/issues/2713) [post-#2713](https://github.com/adobe/post-/issues/2713)
10
+
11
+ ## [@adobe/spacecat-shared-data-access-v3.81.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.80.0...@adobe/spacecat-shared-data-access-v3.81.0) (2026-07-02)
12
+
13
+ ### Features
14
+
15
+ * **SITES-44690:** add TaskManagementConnection, Ticket, TicketSuggestion, IdempotencyKey, and OAuthNonce data models ([#1702](https://github.com/adobe/spacecat-shared/issues/1702)) ([2ca5096](https://github.com/adobe/spacecat-shared/commit/2ca5096b448a07b8e365021e06b31fa3bee96b09))
16
+
1
17
  ## [@adobe/spacecat-shared-data-access-v3.80.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.79.1...@adobe/spacecat-shared-data-access-v3.80.0) (2026-07-02)
2
18
 
3
19
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-data-access",
3
- "version": "3.80.0",
3
+ "version": "4.0.0",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
@@ -12,6 +12,13 @@
12
12
 
13
13
  import type { BaseCollection, BaseModel } from '../base';
14
14
 
15
+ // Note on nullable return types: `postgrest.utils.js::normalizeModelValue()`
16
+ // maps DB NULL → JS `undefined` (the key is skipped on `this.record`
17
+ // entirely), so auto-generated getters return `T | undefined` — never
18
+ // `T | null` — for nullable columns. Callers checking `=== null` on these
19
+ // will silently miss the unfinished-job branch. Existing declarations
20
+ // that read `... | null` predate this PR; the lifecycle additions below
21
+ // match the runtime contract.
15
22
  export interface AsyncJob extends BaseModel {
16
23
  getStatus(): string;
17
24
  getResultLocation(): string;
@@ -20,6 +27,8 @@ export interface AsyncJob extends BaseModel {
20
27
  getError(): { code: string; message: string; details?: object } | null;
21
28
  getMetadata(): object | null;
22
29
  getRecordExpiressAt(): number;
30
+ getStartedAt(): string | undefined;
31
+ getEndedAt(): string | undefined;
23
32
  setStatus(status: string): void;
24
33
  setResultLocation(location: string): void;
25
34
  setResultType(type: string): void;
@@ -27,6 +36,8 @@ export interface AsyncJob extends BaseModel {
27
36
  setError(error: { code: string; message: string; details?: object }): void;
28
37
  setMetadata(metadata: object): void;
29
38
  setExpiresAt(expiresAt: number): void;
39
+ setStartedAt(startedAt: string): void;
40
+ setEndedAt(endedAt: string): void;
30
41
  }
31
42
 
32
43
  export interface AsyncJobCollection extends BaseCollection<AsyncJob> {
@@ -55,6 +55,11 @@ import SentimentGuidelineCollection from '../sentiment-guideline/sentiment-guide
55
55
  import SentimentTopicCollection from '../sentiment-topic/sentiment-topic.collection.js';
56
56
  import AccessGrantLogCollection from '../access-grant-log/access-grant-log.collection.js';
57
57
  import SiteImsOrgAccessCollection from '../site-ims-org-access/site-ims-org-access.collection.js';
58
+ import IdempotencyKeyCollection from '../idempotency-key/idempotency-key.collection.js';
59
+ import OAuthNonceCollection from '../oauth-nonce/oauth-nonce.collection.js';
60
+ import TaskManagementConnectionCollection from '../task-management-connection/task-management-connection.collection.js';
61
+ import TicketCollection from '../ticket/ticket.collection.js';
62
+ import TicketSuggestionCollection from '../ticket-suggestion/ticket-suggestion.collection.js';
58
63
 
59
64
  import ApiKeySchema from '../api-key/api-key.schema.js';
60
65
  import AsyncJobSchema from '../async-job/async-job.schema.js';
@@ -97,6 +102,11 @@ import SentimentGuidelineSchema from '../sentiment-guideline/sentiment-guideline
97
102
  import SentimentTopicSchema from '../sentiment-topic/sentiment-topic.schema.js';
98
103
  import AccessGrantLogSchema from '../access-grant-log/access-grant-log.schema.js';
99
104
  import SiteImsOrgAccessSchema from '../site-ims-org-access/site-ims-org-access.schema.js';
105
+ import IdempotencyKeySchema from '../idempotency-key/idempotency-key.schema.js';
106
+ import OAuthNonceSchema from '../oauth-nonce/oauth-nonce.schema.js';
107
+ import TaskManagementConnectionSchema from '../task-management-connection/task-management-connection.schema.js';
108
+ import TicketSchema from '../ticket/ticket.schema.js';
109
+ import TicketSuggestionSchema from '../ticket-suggestion/ticket-suggestion.schema.js';
100
110
 
101
111
  /**
102
112
  * EntityRegistry - A registry class responsible for managing entities, their schema and collection.
@@ -234,6 +244,11 @@ EntityRegistry.registerEntity(SentimentGuidelineSchema, SentimentGuidelineCollec
234
244
  EntityRegistry.registerEntity(SentimentTopicSchema, SentimentTopicCollection);
235
245
  EntityRegistry.registerEntity(AccessGrantLogSchema, AccessGrantLogCollection);
236
246
  EntityRegistry.registerEntity(SiteImsOrgAccessSchema, SiteImsOrgAccessCollection);
247
+ EntityRegistry.registerEntity(IdempotencyKeySchema, IdempotencyKeyCollection);
248
+ EntityRegistry.registerEntity(OAuthNonceSchema, OAuthNonceCollection);
249
+ EntityRegistry.registerEntity(TaskManagementConnectionSchema, TaskManagementConnectionCollection);
250
+ EntityRegistry.registerEntity(TicketSchema, TicketCollection);
251
+ EntityRegistry.registerEntity(TicketSuggestionSchema, TicketSuggestionCollection);
237
252
  EntityRegistry.defaultEntities = { ...EntityRegistry.entities };
238
253
 
239
254
  export default EntityRegistry;
@@ -0,0 +1,89 @@
1
+ /*
2
+ * Copyright 2026 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, isValidUUID } from '@adobe/spacecat-shared-utils';
14
+
15
+ import { DataAccessError, ValidationError } from '../../errors/index.js';
16
+ import BaseCollection from '../base/base.collection.js';
17
+
18
+ /**
19
+ * IdempotencyKeyCollection — manages IdempotencyKey entities.
20
+ *
21
+ * Auto-generated index query methods (via schema references):
22
+ * allByOrganizationId(organizationId) — all keys for an organization
23
+ *
24
+ * @class IdempotencyKeyCollection
25
+ * @extends BaseCollection
26
+ */
27
+ class IdempotencyKeyCollection extends BaseCollection {
28
+ static COLLECTION_NAME = 'IdempotencyKeyCollection';
29
+
30
+ /**
31
+ * Finds a non-expired idempotency key by its value and organization.
32
+ *
33
+ * Returns null when:
34
+ * - No key exists with that value for the organization
35
+ * - The key exists but has expired (expires_at < NOW())
36
+ *
37
+ * @param {string} key - The idempotency key value.
38
+ * @param {string} organizationId - The organization UUID.
39
+ * @returns {Promise<import('./idempotency-key.model.js').default|null>}
40
+ */
41
+ async findActiveKey(key, organizationId) {
42
+ if (!hasText(key)) {
43
+ throw new ValidationError('key is required', this);
44
+ }
45
+ if (!isValidUUID(organizationId)) {
46
+ throw new ValidationError('organizationId must be a valid UUID', this);
47
+ }
48
+
49
+ const { data, error } = await this.postgrestService
50
+ .from(this.tableName)
51
+ .select()
52
+ .eq('key', key)
53
+ .eq('organization_id', organizationId)
54
+ .gt('expires_at', new Date().toISOString())
55
+ .limit(1);
56
+
57
+ if (error) {
58
+ throw new DataAccessError('Failed to find active idempotency key', { entityName: 'IdempotencyKey' }, error);
59
+ }
60
+
61
+ if (!data || data.length === 0) {
62
+ return null;
63
+ }
64
+
65
+ return this.createInstanceFromRow(data[0]);
66
+ }
67
+
68
+ /**
69
+ * Deletes all expired idempotency keys.
70
+ * Called by the cleanup scheduler (every 5 minutes).
71
+ *
72
+ * @returns {Promise<number>} Number of expired keys deleted.
73
+ */
74
+ async deleteExpired() {
75
+ const { data, error } = await this.postgrestService
76
+ .from(this.tableName)
77
+ .delete()
78
+ .lt('expires_at', new Date().toISOString())
79
+ .select('id');
80
+
81
+ if (error) {
82
+ throw new DataAccessError('Failed to delete expired idempotency keys', { entityName: 'IdempotencyKey' }, error);
83
+ }
84
+
85
+ return (data ?? []).length;
86
+ }
87
+ }
88
+
89
+ export default IdempotencyKeyCollection;
@@ -0,0 +1,41 @@
1
+ /*
2
+ * Copyright 2026 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 BaseModel from '../base/base.model.js';
14
+
15
+ /**
16
+ * IdempotencyKey — deduplicates concurrent or retried requests using the
17
+ * Stripe idempotency pattern.
18
+ *
19
+ * Status lifecycle:
20
+ * processing → completed (cached success response)
21
+ * processing → failed (cached error response, client must generate new key to retry)
22
+ *
23
+ * Keys are scoped to (key, organizationId, endpoint) — cross-org collision is impossible
24
+ * and the same key can be used independently across different operations.
25
+ *
26
+ * Keys expire after 24 hours (or shorter for ephemeral locks like token refresh dedup).
27
+ *
28
+ * @class IdempotencyKey
29
+ * @extends BaseModel
30
+ */
31
+ class IdempotencyKey extends BaseModel {
32
+ static ENTITY_NAME = 'IdempotencyKey';
33
+
34
+ static STATUSES = {
35
+ PROCESSING: 'processing',
36
+ COMPLETED: 'completed',
37
+ FAILED: 'failed',
38
+ };
39
+ }
40
+
41
+ export default IdempotencyKey;
@@ -0,0 +1,51 @@
1
+ /*
2
+ * Copyright 2026 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
+ /* c8 ignore start */
14
+
15
+ import { isIsoDate } from '@adobe/spacecat-shared-utils';
16
+
17
+ import SchemaBuilder from '../base/schema.builder.js';
18
+ import IdempotencyKey from './idempotency-key.model.js';
19
+ import IdempotencyKeyCollection from './idempotency-key.collection.js';
20
+
21
+ // idempotency_keys table has updated_at but no updated_by column.
22
+ const schema = new SchemaBuilder(IdempotencyKey, IdempotencyKeyCollection)
23
+ .addAttribute('updatedBy', { type: 'string', required: false, postgrestIgnore: true })
24
+ .addReference('belongs_to', 'Organization')
25
+ .addAttribute('key', {
26
+ type: 'string',
27
+ required: true,
28
+ readOnly: true,
29
+ })
30
+ .addAttribute('endpoint', {
31
+ type: 'string',
32
+ required: true,
33
+ readOnly: true,
34
+ })
35
+ .addAttribute('status', {
36
+ type: Object.values(IdempotencyKey.STATUSES),
37
+ required: true,
38
+ default: IdempotencyKey.STATUSES.PROCESSING,
39
+ })
40
+ .addAttribute('response', {
41
+ type: 'any',
42
+ required: false,
43
+ })
44
+ .addAttribute('expiresAt', {
45
+ type: 'string',
46
+ required: true,
47
+ readOnly: true,
48
+ validate: (value) => isIsoDate(value),
49
+ });
50
+
51
+ export default schema.build();
@@ -0,0 +1,41 @@
1
+ /*
2
+ * Copyright 2026 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, Organization } from '../index';
14
+
15
+ export interface IdempotencyKey extends BaseModel {
16
+ getEndpoint(): string;
17
+ getExpiresAt(): string;
18
+ getKey(): string;
19
+ getOrganization(): Promise<Organization>;
20
+ getOrganizationId(): string;
21
+ getResponse(): Record<string, unknown> | null;
22
+ getStatus(): string;
23
+
24
+ setResponse(response: Record<string, unknown> | null): IdempotencyKey;
25
+ setStatus(status: string): IdempotencyKey;
26
+ }
27
+
28
+ export interface IdempotencyKeyCollection extends BaseCollection<IdempotencyKey> {
29
+ allByOrganizationId(organizationId: string): Promise<IdempotencyKey[]>;
30
+
31
+ /**
32
+ * Finds a non-expired idempotency key by its value and organization.
33
+ * Returns null when no active key exists.
34
+ */
35
+ findActiveKey(key: string, organizationId: string): Promise<IdempotencyKey | null>;
36
+
37
+ /**
38
+ * Deletes all expired idempotency keys. Returns the number deleted.
39
+ */
40
+ deleteExpired(): Promise<number>;
41
+ }
@@ -0,0 +1,19 @@
1
+ /*
2
+ * Copyright 2026 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 IdempotencyKey from './idempotency-key.model.js';
14
+ import IdempotencyKeyCollection from './idempotency-key.collection.js';
15
+
16
+ export {
17
+ IdempotencyKey,
18
+ IdempotencyKeyCollection,
19
+ };
@@ -30,6 +30,8 @@ export type * from './import-url';
30
30
  export type * from './key-event';
31
31
  export type * from './latest-audit';
32
32
  export type * from './opportunity';
33
+ export type * from './idempotency-key';
34
+ export type * from './oauth-nonce';
33
35
  export type * from './organization';
34
36
  export type * from './page-citability';
35
37
  export type * from './page-intent';
@@ -40,6 +42,9 @@ export type * from './scrape-job';
40
42
  export type * from './scrape-url';
41
43
  export type * from './sentiment-guideline';
42
44
  export type * from './sentiment-topic';
45
+ export type * from './task-management-connection';
46
+ export type * from './ticket';
47
+ export type * from './ticket-suggestion';
43
48
  export type * from './site';
44
49
  export type * from './site-candidate';
45
50
  export type * from './site-enrollment';
@@ -54,3 +54,8 @@ export * from './page-citability/index.js';
54
54
  export * from './plg-onboarding/index.js';
55
55
  export * from './sentiment-guideline/index.js';
56
56
  export * from './sentiment-topic/index.js';
57
+ export * from './idempotency-key/index.js';
58
+ export * from './oauth-nonce/index.js';
59
+ export * from './task-management-connection/index.js';
60
+ export * from './ticket/index.js';
61
+ export * from './ticket-suggestion/index.js';
@@ -0,0 +1,26 @@
1
+ /*
2
+ * Copyright 2026 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 interface OAuthNonce extends BaseModel {
16
+ getNonce(): string;
17
+ getExpiresAt(): string;
18
+ }
19
+
20
+ export interface OAuthNonceCollection extends BaseCollection<OAuthNonce> {
21
+ /**
22
+ * Atomically deletes a nonce by its value.
23
+ * Returns the number of rows deleted (1 = consumed, 0 = not found or already consumed).
24
+ */
25
+ delete(keys: { nonce: string }): Promise<number>;
26
+ }
@@ -0,0 +1,19 @@
1
+ /*
2
+ * Copyright 2026 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 OAuthNonce from './oauth-nonce.model.js';
14
+ import OAuthNonceCollection from './oauth-nonce.collection.js';
15
+
16
+ export {
17
+ OAuthNonce,
18
+ OAuthNonceCollection,
19
+ };
@@ -0,0 +1,59 @@
1
+ /*
2
+ * Copyright 2026 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 BaseCollection from '../base/base.collection.js';
14
+
15
+ /**
16
+ * OAuthNonceCollection — manages OAuthNonce records.
17
+ *
18
+ * @class OAuthNonceCollection
19
+ * @extends BaseCollection
20
+ */
21
+ class OAuthNonceCollection extends BaseCollection {
22
+ static COLLECTION_NAME = 'OAuthNonceCollection';
23
+
24
+ /**
25
+ * Atomically consumes a nonce: deletes it only if it exists AND has not expired.
26
+ *
27
+ * Mirrors the intended DB operation from the migration:
28
+ * DELETE FROM oauth_nonces WHERE nonce = $1 AND expires_at > NOW() RETURNING id
29
+ *
30
+ * Returns the number of rows deleted:
31
+ * 1 = consumed (nonce was valid and not expired)
32
+ * 0 = not found, already consumed, OR expired
33
+ *
34
+ * Used by auth-service to enforce single-use replay prevention at OAuth callback time.
35
+ * An expired nonce must return 0 (rejected) even if the row still exists in the DB —
36
+ * the background cleanup job may not have swept it yet.
37
+ *
38
+ * @param {object} keys
39
+ * @param {string} keys.nonce - The nonce value to consume.
40
+ * @returns {Promise<number>} 1 if the nonce was found and deleted, 0 otherwise.
41
+ */
42
+ async delete({ nonce } = {}) {
43
+ if (!nonce || typeof nonce !== 'string') {
44
+ throw new Error('nonce is required and must be a non-empty string');
45
+ }
46
+ const { data, error } = await this.postgrestService
47
+ .from(this.tableName)
48
+ .delete()
49
+ .eq('nonce', nonce)
50
+ .gt('expires_at', new Date().toISOString())
51
+ .select();
52
+ if (error) {
53
+ throw error;
54
+ }
55
+ return (data ?? []).length;
56
+ }
57
+ }
58
+
59
+ export default OAuthNonceCollection;
@@ -0,0 +1,35 @@
1
+ /*
2
+ * Copyright 2026 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 BaseModel from '../base/base.model.js';
14
+
15
+ /**
16
+ * OAuthNonce — a single-use state token used for OAuth 2.0 CSRF/replay prevention.
17
+ *
18
+ * Lifecycle:
19
+ * 1. Created by auth-service at the start of an OAuth authorization flow.
20
+ * 2. Consumed atomically (via OAuthNonceCollection.delete) when the provider
21
+ * redirects back to the callback endpoint.
22
+ * 3. If the nonce cannot be consumed the callback is rejected (replay attack).
23
+ *
24
+ * Rows are short-lived (TTL ≈ 10 minutes). A background cleanup job can sweep
25
+ * expired rows, but replay protection does not depend on cleanup — the nonce is
26
+ * deleted on first use regardless of expiresAt.
27
+ *
28
+ * @class OAuthNonce
29
+ * @extends BaseModel
30
+ */
31
+ class OAuthNonce extends BaseModel {
32
+ static ENTITY_NAME = 'OAuthNonce';
33
+ }
34
+
35
+ export default OAuthNonce;
@@ -0,0 +1,50 @@
1
+ /*
2
+ * Copyright 2026 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
+ /* c8 ignore start */
14
+
15
+ import { isIsoDate } from '@adobe/spacecat-shared-utils';
16
+
17
+ import SchemaBuilder from '../base/schema.builder.js';
18
+ import OAuthNonce from './oauth-nonce.model.js';
19
+ import OAuthNonceCollection from './oauth-nonce.collection.js';
20
+
21
+ // nonce is the state parameter sent to the OAuth provider and consumed exactly once
22
+ // at callback time to prevent CSRF/replay attacks. The index on nonce powers the
23
+ // OAuthNonceCollection.delete({ nonce }) lookup used by auth-service at callback time.
24
+ const schema = new SchemaBuilder(OAuthNonce, OAuthNonceCollection)
25
+ // oauth_nonces is append-only — the DB grants no UPDATE privilege (postgrest_anon and
26
+ // postgrest_writer both lack UPDATE). Disabling updates at the model layer surfaces a
27
+ // clean ValidationError instead of an opaque PostgREST 403 at the DB level.
28
+ .allowUpdates(false)
29
+ // oauth_nonces is append-only — no updated_at or updated_by columns in the DB.
30
+ // Suppress the SchemaBuilder auto-added attributes so they are not included in INSERTs.
31
+ .addAttribute('updatedAt', {
32
+ type: 'string', required: false, readOnly: true, postgrestIgnore: true,
33
+ })
34
+ .addAttribute('updatedBy', { type: 'string', required: false, postgrestIgnore: true })
35
+ .addAttribute('nonce', {
36
+ type: 'string',
37
+ required: true,
38
+ readOnly: true,
39
+ })
40
+ .addAttribute('expiresAt', {
41
+ type: 'string',
42
+ required: true,
43
+ validate: (value) => isIsoDate(value),
44
+ })
45
+ .addIndex(
46
+ { composite: ['nonce'] },
47
+ { composite: [] },
48
+ );
49
+
50
+ export default schema.build();
@@ -11,7 +11,8 @@
11
11
  */
12
12
 
13
13
  import type {
14
- BaseCollection, BaseModel, Site, Project, Entitlement, OrganizationIdentityProvider, TrialUser,
14
+ BaseCollection, BaseModel, Site, Project, Entitlement, OrganizationIdentityProvider,
15
+ TaskManagementConnection, TrialUser,
15
16
  } from '../index';
16
17
 
17
18
  export interface Organization extends BaseModel {
@@ -24,6 +25,7 @@ export interface Organization extends BaseModel {
24
25
  getProjects(): Promise<Project[]>;
25
26
  getEntitlements(): Promise<Entitlement[]>;
26
27
  getOrganizationIdentityProviders(): Promise<OrganizationIdentityProvider[]>;
28
+ getTaskManagementConnections(): Promise<TaskManagementConnection[]>;
27
29
  getTrialUsers(): Promise<TrialUser[]>;
28
30
  setConfig(config: object): Organization;
29
31
  setFulfillableItems(fulfillableItems: object): Organization;
@@ -25,6 +25,7 @@ const schema = new SchemaBuilder(Organization, OrganizationCollection)
25
25
  .addReference('has_many', 'Projects')
26
26
  .addReference('has_many', 'Entitlements')
27
27
  .addReference('has_many', 'TrialUsers')
28
+ .addReference('has_many', 'TaskManagementConnections')
28
29
  .addAttribute('config', {
29
30
  type: 'any',
30
31
  required: true,
@@ -12,6 +12,7 @@
12
12
 
13
13
  import type { AsyncJob, BaseCollection, BaseModel, Site } from '../index.js';
14
14
 
15
+ // SITES-47254: startedAt/result/error live on AsyncJob — fetch via getAsyncJob().
15
16
  export interface Preflight extends BaseModel {
16
17
  getSiteId(): string;
17
18
  getSite(): Promise<Site>;
@@ -20,18 +21,14 @@ export interface Preflight extends BaseModel {
20
21
  getUrl(): string;
21
22
  getStatus(): string;
22
23
  getCreatedBy(): { email: string; displayName?: string };
23
- getStartedAt(): string | null;
24
- getEndedAt(): string | null;
25
- getResult(): object | null;
26
- getError(): { code: string; message: string } | null;
24
+ // `string | undefined` (not `| null`) because normalizeModelValue maps
25
+ // DB NULL → undefined on read — see AsyncJob/index.d.ts header.
26
+ getEndedAt(): string | undefined;
27
27
 
28
28
  setUrl(url: string): Preflight;
29
29
  setStatus(status: 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' | 'CANCELLED'): Preflight;
30
30
  setCreatedBy(createdBy: { email: string; displayName?: string }): Preflight;
31
- setStartedAt(startedAt: string): Preflight;
32
31
  setEndedAt(endedAt: string): Preflight;
33
- setResult(result: object): Preflight;
34
- setError(error: { code: string; message: string }): Preflight;
35
32
  }
36
33
 
37
34
  export interface PreflightCollection extends BaseCollection<Preflight> {