@pdtf/schemas 3.5.1-3 → 3.5.1-4

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/README.md CHANGED
@@ -277,6 +277,9 @@ const verifiedClaims = [
277
277
  },
278
278
  ],
279
279
  },
280
+ terms_of_use: {
281
+ confidentiality_level: "public",
282
+ },
280
283
  claims: {
281
284
  "/propertyPack/priceInformation/price": 350000,
282
285
  "/propertyPack/energyEfficiency/epcRating": "C",
@@ -297,6 +300,61 @@ if (errors.length === 0) {
297
300
  - **Provenance Tracking** - Each claim includes verification evidence and source attribution
298
301
  - **Trust Framework Integration** - Claims operate within the UK PDTF trust framework
299
302
  - **Attachment Support** - Supporting documents can be cryptographically linked to claims
303
+ - **Confidentiality Controls** - Terms of use define access levels and permitted recipients
304
+
305
+ #### Confidentiality Levels
306
+
307
+ Verified claims support three confidentiality levels through a pdtf-specific extension: a `terms_of_use` field, enabling fine-grained access control:
308
+
309
+ **Public Data** - Freely accessible data from official sources or public listing information:
310
+
311
+ ```javascript
312
+ {
313
+ terms_of_use: {
314
+ confidentiality_level: "public";
315
+ }
316
+ }
317
+ ```
318
+
319
+ Examples: Land Registry data, Energy Performance Certificates, planning records, council tax information
320
+
321
+ **Restricted Data** - Limited to transaction participants:
322
+
323
+ ```javascript
324
+ {
325
+ terms_of_use: {
326
+ confidentiality_level: "restricted";
327
+ }
328
+ }
329
+ ```
330
+
331
+ Examples: Commercial property reports, contents of legal forms
332
+
333
+ **Confidential Data** - Role-based access with explicit authorization:
334
+
335
+ ```javascript
336
+ {
337
+ terms_of_use: {
338
+ confidentiality_level: "confidential",
339
+ allowed_roles: [
340
+ "Estate Agent",
341
+ "Seller's Conveyancer",
342
+ "Buyer's Conveyancer",
343
+ "Mortgage Broker",
344
+ "Lender"
345
+ ]
346
+ }
347
+ }
348
+ ```
349
+
350
+ Examples: Identity verification reports, anti-money laundering checks, sensitive personal information
351
+
352
+ **Guidelines:**
353
+
354
+ - Use **public** for government/official data sources
355
+ - Use **restricted** for commercial data providers and processed information
356
+ - Use **confidential** for sensitive personal data requiring explicit role authorization
357
+ - Always consider data source and sensitivity when assigning levels
300
358
 
301
359
  **Roadmap - W3C Verifiable Credentials:**
302
360
  The next version of the PDTF framework will migrate to [W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) to provide:
package/index.js CHANGED
@@ -208,9 +208,30 @@ const getCachedSchemaData = (path, schemaId, overlays) => {
208
208
  if (pathArray.length >= 1) {
209
209
  subSchema = pathArray.reduce((schema, pathElement) => {
210
210
  if (!schema) return undefined;
211
- const { type, items, properties, oneOf } = schema;
211
+ const { type, items, properties, patternProperties, oneOf } = schema;
212
+
213
+ // Check array type
212
214
  if (type === "array") return items;
215
+
216
+ // Check explicit properties first
213
217
  if (properties?.[pathElement]) return properties[pathElement];
218
+
219
+ // Check patternProperties
220
+ if (patternProperties) {
221
+ for (const pattern in patternProperties) {
222
+ try {
223
+ const regex = new RegExp(pattern);
224
+ if (regex.test(pathElement)) {
225
+ return patternProperties[pattern];
226
+ }
227
+ } catch (e) {
228
+ // Invalid regex pattern in schema - skip it
229
+ console.warn(`Invalid regex pattern in patternProperties: ${pattern}`, e);
230
+ }
231
+ }
232
+ }
233
+
234
+ // Check oneOf discriminators
214
235
  if (oneOf) {
215
236
  let matchingProperty;
216
237
  oneOf.forEach((aOneOf) => {
@@ -222,6 +243,7 @@ const getCachedSchemaData = (path, schemaId, overlays) => {
222
243
  });
223
244
  if (matchingProperty) return matchingProperty;
224
245
  }
246
+
225
247
  return undefined;
226
248
  }, sourceSchema);
227
249
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pdtf/schemas",
3
- "version": "3.5.1-3",
3
+ "version": "3.5.1-4",
4
4
  "description": "Property Data Trust Framework Schemas and Utilities",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -20,6 +20,9 @@
20
20
  }
21
21
  ]
22
22
  },
23
+ "terms_of_use": {
24
+ "confidentiality_level": "restricted"
25
+ },
23
26
  "claims": {
24
27
  "/participants/-": {
25
28
  "name": {
@@ -32,6 +32,9 @@
32
32
  }
33
33
  ]
34
34
  },
35
+ "terms_of_use": {
36
+ "confidentiality_level": "public"
37
+ },
35
38
  "claims": {
36
39
  "/propertyPack/councilTax": {
37
40
  "councilTaxBand": "D",
@@ -21,6 +21,9 @@
21
21
  }
22
22
  ]
23
23
  },
24
+ "terms_of_use": {
25
+ "confidentiality_level": "public"
26
+ },
24
27
  "claims": {
25
28
  "/propertyPack/address": {
26
29
  "line1": "3 The Hollys",
@@ -21,6 +21,9 @@
21
21
  }
22
22
  ]
23
23
  },
24
+ "terms_of_use": {
25
+ "confidentiality_level": "restricted"
26
+ },
24
27
  "claims": {
25
28
  "/propertyPack/delayFactors/hasDelayFactors": {
26
29
  "yesNo": "No"
@@ -0,0 +1,569 @@
1
+ const {
2
+ isPathValid,
3
+ getSubschema,
4
+ getSubschemaValidator,
5
+ validateVerifiedClaims,
6
+ } = require("../../../index.js");
7
+
8
+ const schemaId =
9
+ "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json";
10
+
11
+ describe("patternProperties support in path validation", () => {
12
+ describe("isPathValid with patternProperties", () => {
13
+ test("validates path with patternProperties - simple offer ID", () => {
14
+ const path = "/offers/offer-123";
15
+ const isValid = isPathValid(path, schemaId);
16
+ expect(isValid).toBe(true);
17
+ });
18
+
19
+ test("validates path with patternProperties - UUID", () => {
20
+ const path = "/offers/550e8400-e29b-41d4-a716-446655440000";
21
+ const isValid = isPathValid(path, schemaId);
22
+ expect(isValid).toBe(true);
23
+ });
24
+
25
+ test("validates path with patternProperties - DID format", () => {
26
+ const path = "/offers/did:example:buyer123";
27
+ const isValid = isPathValid(path, schemaId);
28
+ expect(isValid).toBe(true);
29
+ });
30
+
31
+ test("validates path with patternProperties - alphanumeric with special chars", () => {
32
+ const path = "/offers/1sorzJCFHbWVlHA2tYXA";
33
+ const isValid = isPathValid(path, schemaId);
34
+ expect(isValid).toBe(true);
35
+ });
36
+
37
+ test("validates nested path within patternProperties", () => {
38
+ const path = "/offers/offer-456/amount";
39
+ const isValid = isPathValid(path, schemaId);
40
+ expect(isValid).toBe(true);
41
+ });
42
+
43
+ test("validates deeply nested path within patternProperties", () => {
44
+ const path = "/offers/offer-456/inclusions";
45
+ const isValid = isPathValid(path, schemaId);
46
+ expect(isValid).toBe(true);
47
+ });
48
+
49
+ test("rejects invalid nested path within patternProperties", () => {
50
+ const path = "/offers/offer-456/nonExistentField";
51
+ const isValid = isPathValid(path, schemaId);
52
+ expect(isValid).toBe(false);
53
+ });
54
+
55
+ test("validates path with patternProperties exists at root /offers", () => {
56
+ const path = "/offers";
57
+ const isValid = isPathValid(path, schemaId);
58
+ expect(isValid).toBe(true);
59
+ });
60
+ });
61
+
62
+ describe("getSubschema with patternProperties", () => {
63
+ test("returns correct subschema for patternProperties path", () => {
64
+ const path = "/offers/offer-123";
65
+ const subschema = getSubschema(path, schemaId);
66
+
67
+ expect(subschema).toBeDefined();
68
+ expect(subschema.type).toBe("object");
69
+ expect(subschema.properties).toBeDefined();
70
+ expect(subschema.properties.amount).toBeDefined();
71
+ expect(subschema.properties.currency).toBeDefined();
72
+ expect(subschema.properties.status).toBeDefined();
73
+ });
74
+
75
+ test("returns correct subschema for nested property in patternProperties", () => {
76
+ const path = "/offers/offer-123/amount";
77
+ const subschema = getSubschema(path, schemaId);
78
+
79
+ expect(subschema).toBeDefined();
80
+ expect(subschema.type).toBe("number");
81
+ expect(subschema.minimum).toBe(0);
82
+ });
83
+
84
+ test("returns correct subschema for status enum", () => {
85
+ const path = "/offers/offer-123/status";
86
+ const subschema = getSubschema(path, schemaId);
87
+
88
+ expect(subschema).toBeDefined();
89
+ expect(subschema.type).toBe("string");
90
+ expect(subschema.enum).toContain("pending");
91
+ expect(subschema.enum).toContain("accepted");
92
+ expect(subschema.enum).toContain("rejected");
93
+ expect(subschema.enum).toContain("withdrawn");
94
+ expect(subschema.enum).toContain("noteOfInterest");
95
+ });
96
+
97
+ test("returns correct subschema for currency pattern", () => {
98
+ const path = "/offers/offer-123/currency";
99
+ const subschema = getSubschema(path, schemaId);
100
+
101
+ expect(subschema).toBeDefined();
102
+ expect(subschema.type).toBe("string");
103
+ expect(subschema.pattern).toBe("^[A-Z]{3}$");
104
+ });
105
+
106
+ test("returns offers object schema", () => {
107
+ const path = "/offers";
108
+ const subschema = getSubschema(path, schemaId);
109
+
110
+ expect(subschema).toBeDefined();
111
+ expect(subschema.type).toBe("object");
112
+ expect(subschema.patternProperties).toBeDefined();
113
+ expect(subschema.propertyNames).toBeDefined();
114
+ });
115
+
116
+ test("returns undefined for invalid path", () => {
117
+ const path = "/offers/offer-123/invalidField";
118
+ const subschema = getSubschema(path, schemaId);
119
+
120
+ expect(subschema).toBeUndefined();
121
+ });
122
+ });
123
+
124
+ describe("getSubschemaValidator with patternProperties", () => {
125
+ test("returns working validator for patternProperties path", () => {
126
+ const path = "/offers/offer-123";
127
+ const validator = getSubschemaValidator(path, schemaId);
128
+
129
+ expect(validator).toBeDefined();
130
+ expect(typeof validator).toBe("function");
131
+
132
+ const validOffer = {
133
+ amount: 500000,
134
+ currency: "GBP",
135
+ status: "pending",
136
+ };
137
+
138
+ const isValid = validator(validOffer);
139
+ expect(isValid).toBe(true);
140
+ });
141
+
142
+ test("validator correctly validates valid offer data", () => {
143
+ const path = "/offers/offer-456";
144
+ const validator = getSubschemaValidator(path, schemaId);
145
+
146
+ const validOffer = {
147
+ amount: 300000,
148
+ currency: "EUR",
149
+ status: "accepted",
150
+ inclusions: ["carpets", "curtains"],
151
+ exclusions: ["garden furniture"],
152
+ conditions: ["Subject to survey"],
153
+ };
154
+
155
+ const isValid = validator(validOffer);
156
+ expect(isValid).toBe(true);
157
+ });
158
+
159
+ test("validator correctly rejects invalid offer data - missing required field", () => {
160
+ const path = "/offers/offer-789";
161
+ const validator = getSubschemaValidator(path, schemaId);
162
+
163
+ const invalidOffer = {
164
+ amount: 400000,
165
+ currency: "GBP",
166
+ // missing required status field
167
+ };
168
+
169
+ const isValid = validator(invalidOffer);
170
+ expect(isValid).toBe(false);
171
+ expect(validator.errors).toBeDefined();
172
+ expect(validator.errors.length).toBeGreaterThan(0);
173
+ });
174
+
175
+ test("validator correctly rejects invalid offer data - negative amount", () => {
176
+ const path = "/offers/offer-999";
177
+ const validator = getSubschemaValidator(path, schemaId);
178
+
179
+ const invalidOffer = {
180
+ amount: -100000,
181
+ currency: "GBP",
182
+ status: "pending",
183
+ };
184
+
185
+ const isValid = validator(invalidOffer);
186
+ expect(isValid).toBe(false);
187
+ expect(validator.errors.some((e) => e.keyword === "minimum")).toBe(true);
188
+ });
189
+
190
+ test("validator correctly rejects invalid offer data - invalid status", () => {
191
+ const path = "/offers/offer-abc";
192
+ const validator = getSubschemaValidator(path, schemaId);
193
+
194
+ const invalidOffer = {
195
+ amount: 250000,
196
+ currency: "GBP",
197
+ status: "invalid-status",
198
+ };
199
+
200
+ const isValid = validator(invalidOffer);
201
+ expect(isValid).toBe(false);
202
+ expect(validator.errors.some((e) => e.keyword === "enum")).toBe(true);
203
+ });
204
+
205
+ test("validator for nested path works correctly", () => {
206
+ const path = "/offers/offer-nested/amount";
207
+ const validator = getSubschemaValidator(path, schemaId);
208
+
209
+ expect(validator(500000)).toBe(true);
210
+ expect(validator(0)).toBe(true);
211
+ expect(validator(-1)).toBe(false);
212
+ expect(validator("not a number")).toBe(false);
213
+ });
214
+ });
215
+
216
+ describe("validateVerifiedClaims with patternProperties paths", () => {
217
+ test("accepts valid claim with patternProperties path", () => {
218
+ const claim = {
219
+ timestamp: 1643115903108,
220
+ verification: {
221
+ trust_framework: "uk_pdtf",
222
+ time: "2025-01-15T12:00:00Z",
223
+ },
224
+ claims: {
225
+ "/offers/offer-001": {
226
+ amount: 500000,
227
+ currency: "GBP",
228
+ status: "pending",
229
+ },
230
+ },
231
+ };
232
+
233
+ const errors = validateVerifiedClaims([claim], schemaId);
234
+ expect(errors).toHaveLength(0);
235
+ });
236
+
237
+ test("accepts valid claim with UUID offer ID", () => {
238
+ const claim = {
239
+ timestamp: 1643115903108,
240
+ verification: {
241
+ trust_framework: "uk_pdtf",
242
+ time: "2025-01-15T12:00:00Z",
243
+ },
244
+ claims: {
245
+ "/offers/1sorzJCFHbWVlHA2tYXA": {
246
+ amount: 750000,
247
+ currency: "GBP",
248
+ status: "accepted",
249
+ },
250
+ },
251
+ };
252
+
253
+ const errors = validateVerifiedClaims([claim], schemaId);
254
+ expect(errors).toHaveLength(0);
255
+ });
256
+
257
+ test("accepts valid claim with DID format offer ID", () => {
258
+ const claim = {
259
+ timestamp: 1643115903108,
260
+ verification: {
261
+ trust_framework: "uk_pdtf",
262
+ time: "2025-01-15T12:00:00Z",
263
+ },
264
+ claims: {
265
+ "/offers/did:example:buyer123": {
266
+ amount: 600000,
267
+ currency: "EUR",
268
+ status: "noteOfInterest",
269
+ },
270
+ },
271
+ };
272
+
273
+ const errors = validateVerifiedClaims([claim], schemaId);
274
+ expect(errors).toHaveLength(0);
275
+ });
276
+
277
+ test("accepts valid claim with nested patternProperties path", () => {
278
+ const claim = {
279
+ timestamp: 1643115903108,
280
+ verification: {
281
+ trust_framework: "uk_pdtf",
282
+ time: "2025-01-15T12:00:00Z",
283
+ },
284
+ claims: {
285
+ "/offers/offer-002/amount": 450000,
286
+ },
287
+ };
288
+
289
+ const errors = validateVerifiedClaims([claim], schemaId);
290
+ expect(errors).toHaveLength(0);
291
+ });
292
+
293
+ test("accepts claim with all optional offer fields", () => {
294
+ const claim = {
295
+ timestamp: 1643115903108,
296
+ verification: {
297
+ trust_framework: "uk_pdtf",
298
+ time: "2025-01-15T12:00:00Z",
299
+ },
300
+ claims: {
301
+ "/offers/offer-complete": {
302
+ amount: 550000,
303
+ currency: "GBP",
304
+ status: "accepted",
305
+ inclusions: ["carpets", "curtains", "light fixtures"],
306
+ exclusions: ["garden shed"],
307
+ conditions: ["Subject to survey", "Subject to mortgage approval"],
308
+ },
309
+ },
310
+ };
311
+
312
+ const errors = validateVerifiedClaims([claim], schemaId);
313
+ expect(errors).toHaveLength(0);
314
+ });
315
+
316
+ test("rejects claim with invalid offer data - missing required field", () => {
317
+ const claim = {
318
+ timestamp: 1643115903108,
319
+ verification: {
320
+ trust_framework: "uk_pdtf",
321
+ time: "2025-01-15T12:00:00Z",
322
+ },
323
+ claims: {
324
+ "/offers/offer-invalid": {
325
+ amount: 500000,
326
+ currency: "GBP",
327
+ // missing required status field
328
+ },
329
+ },
330
+ };
331
+
332
+ const errors = validateVerifiedClaims([claim], schemaId);
333
+ expect(errors.length).toBeGreaterThan(0);
334
+ });
335
+
336
+ test("rejects claim with invalid offer data - negative amount", () => {
337
+ const claim = {
338
+ timestamp: 1643115903108,
339
+ verification: {
340
+ trust_framework: "uk_pdtf",
341
+ time: "2025-01-15T12:00:00Z",
342
+ },
343
+ claims: {
344
+ "/offers/offer-negative": {
345
+ amount: -100000,
346
+ currency: "GBP",
347
+ status: "pending",
348
+ },
349
+ },
350
+ };
351
+
352
+ const errors = validateVerifiedClaims([claim], schemaId);
353
+ expect(errors.length).toBeGreaterThan(0);
354
+ });
355
+
356
+ test("rejects claim with invalid offer data - invalid status enum", () => {
357
+ const claim = {
358
+ timestamp: 1643115903108,
359
+ verification: {
360
+ trust_framework: "uk_pdtf",
361
+ time: "2025-01-15T12:00:00Z",
362
+ },
363
+ claims: {
364
+ "/offers/offer-bad-status": {
365
+ amount: 500000,
366
+ currency: "GBP",
367
+ status: "completed", // not a valid enum value
368
+ },
369
+ },
370
+ };
371
+
372
+ const errors = validateVerifiedClaims([claim], schemaId);
373
+ expect(errors.length).toBeGreaterThan(0);
374
+ });
375
+
376
+ test("rejects claim with invalid offer data - invalid currency format", () => {
377
+ const claim = {
378
+ timestamp: 1643115903108,
379
+ verification: {
380
+ trust_framework: "uk_pdtf",
381
+ time: "2025-01-15T12:00:00Z",
382
+ },
383
+ claims: {
384
+ "/offers/offer-bad-currency": {
385
+ amount: 500000,
386
+ currency: "gbp", // should be uppercase
387
+ status: "pending",
388
+ },
389
+ },
390
+ };
391
+
392
+ const errors = validateVerifiedClaims([claim], schemaId);
393
+ expect(errors.length).toBeGreaterThan(0);
394
+ });
395
+
396
+ test("accepts multiple claims with different patternProperties paths", () => {
397
+ const claims = [
398
+ {
399
+ timestamp: 1643115903108,
400
+ verification: {
401
+ trust_framework: "uk_pdtf",
402
+ time: "2025-01-15T12:00:00Z",
403
+ },
404
+ claims: {
405
+ "/offers/offer-1": {
406
+ amount: 500000,
407
+ currency: "GBP",
408
+ status: "pending",
409
+ },
410
+ },
411
+ },
412
+ {
413
+ timestamp: 1643115903109,
414
+ verification: {
415
+ trust_framework: "uk_pdtf",
416
+ time: "2025-01-15T12:01:00Z",
417
+ },
418
+ claims: {
419
+ "/offers/offer-2": {
420
+ amount: 520000,
421
+ currency: "GBP",
422
+ status: "accepted",
423
+ },
424
+ },
425
+ },
426
+ ];
427
+
428
+ const errors = validateVerifiedClaims(claims, schemaId);
429
+ expect(errors).toHaveLength(0);
430
+ });
431
+
432
+ test("accepts claim with multiple patternProperties paths", () => {
433
+ const claim = {
434
+ timestamp: 1643115903108,
435
+ verification: {
436
+ trust_framework: "uk_pdtf",
437
+ time: "2025-01-15T12:00:00Z",
438
+ },
439
+ claims: {
440
+ "/offers/offer-1": {
441
+ amount: 500000,
442
+ currency: "GBP",
443
+ status: "pending",
444
+ },
445
+ "/offers/offer-2": {
446
+ amount: 510000,
447
+ currency: "GBP",
448
+ status: "withdrawn",
449
+ },
450
+ "/offers/offer-3/status": "rejected",
451
+ },
452
+ };
453
+
454
+ const errors = validateVerifiedClaims([claim], schemaId);
455
+ expect(errors).toHaveLength(0);
456
+ });
457
+
458
+ test("rejects claim for non-existent nested property in patternProperties", () => {
459
+ const claim = {
460
+ timestamp: 1643115903108,
461
+ verification: {
462
+ trust_framework: "uk_pdtf",
463
+ time: "2025-01-15T12:00:00Z",
464
+ },
465
+ claims: {
466
+ "/offers/offer-1/nonExistentField": "some value",
467
+ },
468
+ };
469
+
470
+ const errors = validateVerifiedClaims([claim], schemaId);
471
+ expect(errors.length).toBeGreaterThan(0);
472
+ expect(errors.some((e) => e.includes("not a valid PDTF schema path"))).toBe(
473
+ true
474
+ );
475
+ });
476
+ });
477
+
478
+ describe("precedence: explicit properties over patternProperties", () => {
479
+ // This test documents the expected behavior that explicit properties
480
+ // should take precedence over patternProperties matches
481
+ test("explicit property should match before patternProperties", () => {
482
+ // If offers had an explicit property called "special" and also had
483
+ // patternProperties that would match "special", the explicit property
484
+ // should win. This is standard JSON Schema behavior.
485
+
486
+ // Since our current schema doesn't have this scenario, this test
487
+ // documents the expected behavior for future schema changes
488
+ expect(true).toBe(true);
489
+ });
490
+ });
491
+
492
+ describe("caching with patternProperties paths", () => {
493
+ test("caches subschema for patternProperties paths", () => {
494
+ const path = "/offers/cached-offer-123";
495
+
496
+ // First call - cache miss
497
+ const subschema1 = getSubschema(path, schemaId);
498
+ expect(subschema1).toBeDefined();
499
+
500
+ // Second call - should hit cache
501
+ const subschema2 = getSubschema(path, schemaId);
502
+ expect(subschema2).toBeDefined();
503
+ expect(subschema2).toEqual(subschema1);
504
+ });
505
+
506
+ test("caches validator for patternProperties paths", () => {
507
+ const path = "/offers/cached-offer-456";
508
+
509
+ // First call - cache miss
510
+ const validator1 = getSubschemaValidator(path, schemaId);
511
+ expect(validator1).toBeDefined();
512
+
513
+ // Second call - should hit cache
514
+ const validator2 = getSubschemaValidator(path, schemaId);
515
+ expect(validator2).toBeDefined();
516
+
517
+ // Should be the same validator function
518
+ expect(validator2).toBe(validator1);
519
+ });
520
+
521
+ test("different patternProperties paths get separate cache entries", () => {
522
+ const path1 = "/offers/offer-aaa";
523
+ const path2 = "/offers/offer-bbb";
524
+
525
+ const validator1 = getSubschemaValidator(path1, schemaId);
526
+ const validator2 = getSubschemaValidator(path2, schemaId);
527
+
528
+ // Both should work but point to same schema (since they match same pattern)
529
+ expect(validator1).toBeDefined();
530
+ expect(validator2).toBeDefined();
531
+
532
+ // They should be the same since they resolve to the same schema
533
+ expect(validator1).toBe(validator2);
534
+ });
535
+ });
536
+
537
+ describe("edge cases", () => {
538
+ test("handles offer ID with dots", () => {
539
+ const path = "/offers/offer.123.abc";
540
+ const isValid = isPathValid(path, schemaId);
541
+ expect(isValid).toBe(true);
542
+ });
543
+
544
+ test("handles offer ID with underscores", () => {
545
+ const path = "/offers/offer_123_abc";
546
+ const isValid = isPathValid(path, schemaId);
547
+ expect(isValid).toBe(true);
548
+ });
549
+
550
+ test("handles offer ID with colons (DID format)", () => {
551
+ const path = "/offers/did:web:example.com:users:alice";
552
+ const isValid = isPathValid(path, schemaId);
553
+ expect(isValid).toBe(true);
554
+ });
555
+
556
+ test("handles offer ID with plus signs", () => {
557
+ const path = "/offers/offer+123";
558
+ const isValid = isPathValid(path, schemaId);
559
+ expect(isValid).toBe(true);
560
+ });
561
+
562
+ test("handles very long offer ID", () => {
563
+ const longId = "a".repeat(100);
564
+ const path = `/offers/${longId}`;
565
+ const isValid = isPathValid(path, schemaId);
566
+ expect(isValid).toBe(true);
567
+ });
568
+ });
569
+ });