@startsimpli/ui 0.4.100 → 0.4.101

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/ui",
3
- "version": "0.4.100",
3
+ "version": "0.4.101",
4
4
  "description": "Shared UI components package for StartSimpli applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -128,6 +128,30 @@ describe('no-hype', () => {
128
128
  describe('approved sources', () => {
129
129
  const approvedHosts = ['example.com', 'gov.uk'];
130
130
 
131
+ it('accepts a SUBDOMAIN of an approved domain', () => {
132
+ // Same organisation, own subdomain. Exact-host matching rejected
133
+ // documents.worldbank.org while worldbank.org was approved — on the live
134
+ // OGMC corpus that was the last thing blocking approval on 3 drafts.
135
+ const c = find(
136
+ runContentChecks(
137
+ { sources: 'https://docs.example.com/a https://a.b.gov.uk/c' },
138
+ { approvedHosts },
139
+ ),
140
+ 'approved-sources',
141
+ );
142
+ expect(c?.status).toBe('pass');
143
+ });
144
+
145
+ it('does NOT accept a domain that merely ENDS WITH an approved one', () => {
146
+ // The dot is load-bearing: `notexample.com` must not pass as `example.com`.
147
+ const c = find(
148
+ runContentChecks({ sources: 'https://notexample.com/a' }, { approvedHosts }),
149
+ 'approved-sources',
150
+ );
151
+ expect(c?.status).toBe('fail');
152
+ expect(c?.detail).toContain('notexample.com');
153
+ });
154
+
131
155
  it('is skipped entirely when approvedHosts is omitted', () => {
132
156
  expect(find(runContentChecks({ sources: 'https://example.com/a' }), 'approved-sources')).toBeUndefined();
133
157
  });
@@ -287,7 +287,22 @@ export function runContentChecks(fields: ContentFields, config: ContentChecksCon
287
287
 
288
288
  // Approved sources — only when the app supplied an allow-list.
289
289
  if (approvedHosts !== undefined) {
290
- const approved = new Set(approvedHosts.map((h) => h.toLowerCase().replace(/^www\./, '')));
290
+ const approved = approvedHosts.map((h) => h.toLowerCase().replace(/^www\./, ''));
291
+ /**
292
+ * A host is approved when it IS an approved domain or is a SUBDOMAIN of one.
293
+ *
294
+ * Exact equality alone rejected `documents.worldbank.org` while
295
+ * `worldbank.org` was approved, and `eservices.misa.gov.sa` while
296
+ * `misa.gov.sa` was — the same organisation publishing on its own
297
+ * subdomain. On the live OGMC corpus that was the last thing blocking
298
+ * approval on 3 drafts after the source list itself was fixed.
299
+ *
300
+ * The dot in `.${domain}` is load-bearing: without it `notworldbank.org`
301
+ * would match `worldbank.org`, which is exactly the attack a naive
302
+ * `endsWith` invites.
303
+ */
304
+ const isApproved = (host: string): boolean =>
305
+ approved.some((domain) => host === domain || host.endsWith(`.${domain}`));
291
306
  const urls = extractUrls(fields.sources);
292
307
  const hosts = urls.map(hostOf).filter(Boolean);
293
308
  if (hosts.length === 0) {
@@ -299,7 +314,7 @@ export function runContentChecks(fields: ContentFields, config: ContentChecksCon
299
314
  locations: [{ field: 'sources' }],
300
315
  });
301
316
  } else {
302
- const bad = Array.from(new Set(hosts.filter((h) => !approved.has(h) || FORUM_SOCIAL.test(h))));
317
+ const bad = Array.from(new Set(hosts.filter((h) => !isApproved(h) || FORUM_SOCIAL.test(h))));
303
318
  // Highlight the offending URLs as they appear in the sources text — the
304
319
  // reviewer pastes URLs, not bare hosts, so matching on the host alone
305
320
  // would highlight nothing.