@nfcard/validation 0.6.0 → 0.9.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/dist/index.js CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nfcard/validation",
3
- "version": "0.6.0",
3
+ "version": "0.9.0",
4
4
  "description": "Shared Zod validation schemas for the NFCard product family.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "zod": "^3.24.0",
20
- "@nfcard/types": "0.5.0"
20
+ "@nfcard/types": "0.8.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "tsup": "^8.0.0"
package/src/index.test.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  sanitizeText,
17
17
  signupWithProfileSchema,
18
18
  socialLinksSchema,
19
+ socialOverridesSchema,
19
20
  userDataSchema,
20
21
  validateConfiguration,
21
22
  validateDigitalConfig,
@@ -209,6 +210,64 @@ describe('socialLinksSchema', () => {
209
210
  });
210
211
  });
211
212
 
213
+ // ── socialOverridesSchema (FOXHOLE-595 input side) ──────────────
214
+
215
+ describe('socialOverridesSchema (FOXHOLE-595 input side)', () => {
216
+ it('sanitizes every present URL field to safe https (or drops unsafe)', () => {
217
+ const out = socialOverridesSchema.parse({
218
+ facebook: 'https://facebook.com/foxhome',
219
+ linkedin: 'javascript:alert(1)', // unsafe scheme → ''
220
+ instagram: 'instagram.com/foxhome', // bare host → https
221
+ x: 'http://x.com/foxhome', // http upgraded → https
222
+ customUrl: '<script>x</script>https://foo.bar', // tags stripped
223
+ customUrlLabel: '<b>My link</b>',
224
+ });
225
+ expect(out.facebook).toBe('https://facebook.com/foxhome');
226
+ expect(out.linkedin).toBe('');
227
+ expect(out.instagram).toBe('https://instagram.com/foxhome');
228
+ expect(out.x?.startsWith('https://')).toBe(true);
229
+ expect(out.customUrl).not.toMatch(/script/i);
230
+ expect(out.customUrlLabel).toBe('My link');
231
+ });
232
+
233
+ it('preserves SPARSE semantics — omitted keys stay omitted (no merge clobber)', () => {
234
+ // The renderer merges { ...baseSocialLinks, ...socialOverrides }; an
235
+ // omitted override key must NOT appear as '' or it would wipe the
236
+ // inherited link. See socialOverridesSchema comment.
237
+ const out = socialOverridesSchema.parse({ facebook: 'https://fb.com/x' });
238
+ expect(out.facebook).toBe('https://fb.com/x');
239
+ expect('linkedin' in out).toBe(false);
240
+ expect('x' in out).toBe(false);
241
+ expect('customUrl' in out).toBe(false);
242
+ expect('customUrlLabel' in out).toBe(false);
243
+ expect(Object.keys(out)).toEqual(['facebook']);
244
+ });
245
+
246
+ it('includes x for parity with the SocialLinks type + renderer', () => {
247
+ const out = socialOverridesSchema.parse({ x: 'x.com/foo' });
248
+ expect(out.x).toBe('https://x.com/foo');
249
+ });
250
+
251
+ it.each([
252
+ 'javascript:alert(1)',
253
+ 'JaVaScRiPt:alert(1)',
254
+ 'data:text/html,<script>alert(1)</script>',
255
+ 'vbscript:msgbox(1)',
256
+ 'file:///etc/passwd',
257
+ 'mailto:a@b.c',
258
+ ])('coerces a dangerous override to empty rather than 400ing: %s', (bad) => {
259
+ const r = socialOverridesSchema.safeParse({ customUrl: bad });
260
+ expect(r.success).toBe(true);
261
+ if (r.success) expect(r.data.customUrl).toBe('');
262
+ });
263
+
264
+ it('accepts an empty object (every field optional)', () => {
265
+ const r = socialOverridesSchema.safeParse({});
266
+ expect(r.success).toBe(true);
267
+ if (r.success) expect(Object.keys(r.data)).toEqual([]);
268
+ });
269
+ });
270
+
212
271
  // ── userDataSchema ──────────────────────────────────────────────
213
272
 
214
273
  describe('userDataSchema / validateUserData', () => {
@@ -379,6 +438,23 @@ describe('digitalConfigSchema / validateDigitalConfig', () => {
379
438
  const r = validateDigitalConfig({ ...validDigitalConfig, fontKey: '' });
380
439
  expect(r.success).toBe(false);
381
440
  });
441
+
442
+ it('sanitizes socialOverrides on the full config (FOXHOLE-595 input side)', () => {
443
+ const r = validateDigitalConfig({
444
+ ...validDigitalConfig,
445
+ socialOverrides: {
446
+ linkedin: 'javascript:alert(1)', // unsafe → ''
447
+ facebook: 'fb.com/x', // bare host → https
448
+ },
449
+ });
450
+ expect(r.success).toBe(true);
451
+ if (r.success) {
452
+ expect(r.data.socialOverrides?.linkedin).toBe('');
453
+ expect(r.data.socialOverrides?.facebook).toBe('https://fb.com/x');
454
+ // omitted override keys stay absent — no merge clobber downstream
455
+ expect('instagram' in (r.data.socialOverrides ?? {})).toBe(false);
456
+ }
457
+ });
382
458
  });
383
459
 
384
460
  // ── configurationCreateSchema ───────────────────────────────────
package/src/index.ts CHANGED
Binary file