@adobe/spacecat-shared-data-access 3.69.0 → 3.70.1

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,15 @@
1
+ ## [@adobe/spacecat-shared-data-access-v3.70.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.70.0...@adobe/spacecat-shared-data-access-v3.70.1) (2026-05-22)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **data-access:** drop dangling belongs_to: Brand on BrandSemrushProject ([#1617](https://github.com/adobe/spacecat-shared/issues/1617)) ([b52d815](https://github.com/adobe/spacecat-shared/commit/b52d815a2e0e86af63464c71b899b5d1057236b0))
6
+
7
+ ## [@adobe/spacecat-shared-data-access-v3.70.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.69.0...@adobe/spacecat-shared-data-access-v3.70.0) (2026-05-22)
8
+
9
+ ### Features
10
+
11
+ * **data-access:** extend DOMAIN_PATTERN to support subpath domains (LLMO-4187) ([#1593](https://github.com/adobe/spacecat-shared/issues/1593)) ([f6cd505](https://github.com/adobe/spacecat-shared/commit/f6cd505da6c61936538a39b26465c2bdc1c3863b))
12
+
1
13
  ## [@adobe/spacecat-shared-data-access-v3.69.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.68.0...@adobe/spacecat-shared-data-access-v3.69.0) (2026-05-22)
2
14
 
3
15
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-data-access",
3
- "version": "3.69.0",
3
+ "version": "3.70.1",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
@@ -10,7 +10,7 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import { hasText } from '@adobe/spacecat-shared-utils';
13
+ import { hasText, isValidUUID } from '@adobe/spacecat-shared-utils';
14
14
 
15
15
  import SchemaBuilder from '../base/schema.builder.js';
16
16
  import BrandSemrushProject from './brand-semrush-project.model.js';
@@ -26,9 +26,20 @@ import BrandSemrushProjectCollection from './brand-semrush-project.collection.js
26
26
  const LANGUAGE_TAG_REGEX = /^[a-z]{2,3}(-[a-z]{2,4})?$/;
27
27
 
28
28
  const schema = new SchemaBuilder(BrandSemrushProject, BrandSemrushProjectCollection)
29
- // Reference to Brand (many-to-one). The owning organization and Semrush
30
- // workspace are reachable via Brand -> Organization.
31
- .addReference('belongs_to', 'Brand')
29
+ // brandId is the FK to the brands table in mysticat-data-service. Declared
30
+ // explicitly (rather than via `.addReference('belongs_to', 'Brand')`)
31
+ // because this package does not ship a Brand entity — `belongs_to` would
32
+ // throw "Collection BrandCollection not found" at model instantiation
33
+ // (reference.js#toAccessorConfigs). The (brandId, updatedAt) index below
34
+ // produces the same `allByBrandId` accessor `belongs_to` would have.
35
+ // Swap back to `.addReference('belongs_to', 'Brand')` once Brand is
36
+ // registered.
37
+ .addAttribute('brandId', {
38
+ type: 'string',
39
+ required: true,
40
+ validate: (value) => isValidUUID(value),
41
+ })
42
+ .addIndex({ composite: ['brandId'] }, { composite: ['updatedAt'] })
32
43
  .addAttribute('semrushProjectId', {
33
44
  type: 'string',
34
45
  required: true,
@@ -24,7 +24,90 @@ class PlgOnboarding extends BaseModel {
24
24
 
25
25
  static IMS_ORG_ID_PATTERN = /^[a-z0-9]{24}@AdobeOrg$/i;
26
26
 
27
- static DOMAIN_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;
27
+ static MAX_HOSTNAME_LENGTH = 253; // RFC 1035 DNS name limit
28
+
29
+ // Practical cap, chosen for storage and sort-key index depth rather than for any
30
+ // specific browser/URL-bar limit (the domain field is a stored identifier, not a URL).
31
+ static MAX_DOMAIN_LENGTH = 2048;
32
+
33
+ // **WARNING for external consumers: do NOT use DOMAIN_PATTERN directly.**
34
+ // This regex is incomplete on its own — it has no length cap, no control-character
35
+ // rejection, no all-numeric-hostname check, no trailing-dot/consecutive-dot path
36
+ // rejection, and no typeof guard. Always call `PlgOnboarding.isValidDomain(value)`
37
+ // which composes this regex with the rest of the validator. The regex is exported
38
+ // only for legacy callers and may become module-private in a future major release.
39
+ //
40
+ // Matches lowercase hostnames (at least one dot required) and an optional subpath
41
+ // (e.g. nba.com, nba.com/kings, nba.com/us/kings).
42
+ // The final label (TLD) must be alphabetic (>= 2 chars) or punycode (xn--*). This
43
+ // structurally rejects every IP-literal form: dotted-quad (127.0.0.1), short-form
44
+ // (127.1), decimal (2130706433), hex (0x7f.0.0.1, 0xa9.254.169.254 → AWS IMDS),
45
+ // and octal (0177.0.0.1) — and also blocks foo.1-style typos. WHATWG URL would
46
+ // otherwise canonicalize hex/decimal IPs to their dotted-quad form, bypassing
47
+ // denylist-based SSRF gates downstream.
48
+ // Rejects: uppercase letters (use normalizeDomain() first), schemes (https://),
49
+ // ports (:8080), single-label hostnames (localhost, metadata), query strings,
50
+ // fragments, empty/trailing path segments, and any path segment starting with
51
+ // a dot (blocks ./, ../, .hidden, ..foo, etc.).
52
+ // Path-qualified domains (nba.com/kings) are distinct sort-key values from the bare
53
+ // hostname; callers must call normalizeDomain() before findByImsOrgIdAndDomain.
54
+ // Labels must not start or end with a hyphen (RFC 1035).
55
+ // Raw Unicode / IDN must be punycode-encoded before validation (xn-- form is accepted).
56
+ // Percent-encoded path characters (%20 etc.) are not accepted; decode before validation.
57
+ // Underscore is allowed in path segments but not in hostname labels.
58
+ static DOMAIN_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*\.(?:[a-z]{2,}|xn--[a-z0-9-]+)(\/(?!\.)[a-z0-9._~-]+)*$/;
59
+
60
+ // Returns the canonical form of a domain value: lowercased.
61
+ // Note: non-string inputs (null/undefined/number/object) are returned unchanged.
62
+ // Callers MUST also run `isValidDomain(value)` before using the result — calling
63
+ // `normalizeDomain` alone does not guarantee the value is a string or safe to
64
+ // pass to `findByImsOrgIdAndDomain` (which would otherwise treat a non-string
65
+ // sort key as something it isn't).
66
+ static normalizeDomain(value) {
67
+ return typeof value === 'string' ? value.toLowerCase() : value;
68
+ }
69
+
70
+ // Complete domain validator used by the schema and intended for external consumers.
71
+ // Layers a typeof guard, case-canonical check, control-character rejection,
72
+ // all-numeric-hostname rejection (defense-in-depth; DOMAIN_PATTERN's alphabetic-TLD
73
+ // requirement already rejects dotted-quad, short-form, decimal, hex, and octal IPs),
74
+ // trailing-dot path-segment rejection, DOMAIN_PATTERN test, and length caps.
75
+ // Note: DOMAIN_PATTERN alone is not sufficient — always prefer this method.
76
+ // Lowercase-only (host AND path) is intentional canonicalization, not a bug. The
77
+ // domain field is part of the dedup sort key on findByImsOrgIdAndDomain; allowing
78
+ // mixed-case paths would let `nba.com/Kings` and `nba.com/kings` create distinct
79
+ // onboarding rows for the same site. Callers should call normalizeDomain() first.
80
+ // This is a syntactic / data-integrity validator, not an SSRF gate. Callers that
81
+ // make outbound fetches must layer their own private-IP and DNS-resolution checks.
82
+ static isValidDomain(value) {
83
+ if (typeof value !== 'string' || value !== value.toLowerCase()) {
84
+ return false;
85
+ }
86
+ // Length caps run BEFORE the regex test so a multi-MB pathological input is
87
+ // rejected in O(1) rather than driving a multi-MB regex scan. The regex itself
88
+ // is linear (no overlapping quantifiers) but external consumers may not bound
89
+ // input size upstream.
90
+ if (value.length > PlgOnboarding.MAX_DOMAIN_LENGTH) {
91
+ return false;
92
+ }
93
+ if (/[^\x21-\x7e]/.test(value)) {
94
+ return false;
95
+ }
96
+ const [hostname, ...pathParts] = value.split('/');
97
+ if (hostname.length > PlgOnboarding.MAX_HOSTNAME_LENGTH) {
98
+ return false;
99
+ }
100
+ if (/^[\d.]+$/.test(hostname)) {
101
+ return false;
102
+ }
103
+ // Reject path segments that are purely dots, end with a dot, or contain
104
+ // consecutive dots (foo., foo.., foo../bar, v1..0). DOMAIN_PATTERN's
105
+ // negative lookahead only blocks segments STARTING with a dot.
106
+ if (pathParts.some((seg) => /\.$/.test(seg) || seg.includes('..'))) {
107
+ return false;
108
+ }
109
+ return PlgOnboarding.DOMAIN_PATTERN.test(value);
110
+ }
28
111
 
29
112
  static STATUSES = {
30
113
  PRE_ONBOARDING: 'PRE_ONBOARDING',
@@ -28,7 +28,7 @@ const schema = new SchemaBuilder(PlgOnboarding, PlgOnboardingCollection)
28
28
  type: 'string',
29
29
  required: true,
30
30
  readOnly: true,
31
- validate: (value) => PlgOnboarding.DOMAIN_PATTERN.test(value) && value.length <= 253,
31
+ validate: (value) => PlgOnboarding.isValidDomain(value),
32
32
  })
33
33
  .addAttribute('baseURL', {
34
34
  type: 'string',