@classytic/primitives 0.33.0 → 0.34.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/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
4
  adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## 0.34.0 — 2026-10-17
7
+
8
+ ### Added — geo distance helpers (`./identity/address`)
9
+
10
+ - `distanceMeters(a, b)` — great-circle distance in metres (haversine, mean Earth radius 6,371,008.8 m; ~0.5% accurate: fine for geofences, not surveying). Throws `RangeError` on a non-finite or out-of-range WGS84 point.
11
+ - `isWithinRadius(point, center, radiusMeters, { accuracyMeters, mode })` — is a point inside a circle, optionally counting the device's GPS uncertainty: `strict` (default, anti-cheat) needs the whole uncertainty circle inside, `lenient` accepts any overlap. Pinned by `tests/unit/geo-distance.test.ts`.
12
+
13
+ ### Fixed — a malformed signed token is `null`, not an exception (`./signed-token`)
14
+
15
+ - Reading a token whose base64url body has an impossible length (`len % 4 === 1`) made `atob` throw out of the verifier. It now returns `null`, like every other malformed token.
16
+
6
17
  ## 0.33.0 — 2026-10-14
7
18
 
8
19
  ### Changed — `inventory/line-settlement` settles decimal quantities on the 6-dp grid
@@ -39,6 +39,19 @@ interface GeoJsonPoint {
39
39
  }
40
40
  declare function toGeoJsonPoint(p: GeoPoint): GeoJsonPoint;
41
41
  declare function fromGeoJsonPoint(g: GeoJsonPoint): GeoPoint;
42
+ /** Great-circle distance in metres (haversine). Accurate to ~0.5% — fine for geofences, not for surveying. */
43
+ declare function distanceMeters(a: GeoPoint, b: GeoPoint): number;
44
+ interface WithinRadiusOptions {
45
+ /** GPS accuracy (radius of uncertainty) in metres, as reported by the device. */
46
+ accuracyMeters?: number;
47
+ /**
48
+ * How accuracy counts. `strict`: the whole uncertainty circle must be inside (anti-cheat default).
49
+ * `lenient`: any overlap counts. Ignored when `accuracyMeters` is absent.
50
+ */
51
+ mode?: 'strict' | 'lenient';
52
+ }
53
+ /** Whether `point` lies within `radiusMeters` of `center`, optionally accounting for GPS accuracy. */
54
+ declare function isWithinRadius(point: GeoPoint, center: GeoPoint, radiusMeters: number, options?: WithinRadiusOptions): boolean;
42
55
  /**
43
56
  * How a country's postal address is CAPTURED — labels, order, requiredness, and
44
57
  * which fields come from a subdivision taxonomy rather than free text.
@@ -304,4 +317,4 @@ declare function isAddressValid(address: Address | PartialAddress | null | undef
304
317
  /** A readable default message, in the country's own vocabulary. */
305
318
  declare function describeAddressError(error: AddressFieldError): string;
306
319
  //#endregion
307
- export { Address, AddressExtensionFieldSpec, AddressFieldError, AddressFieldInput, AddressFieldKey, AddressFieldSpec, AddressFormat, AddressFormatRegistry, AddressTaxonomyPort, AnyAddressFieldSpec, ContactAddress, GeoJsonPoint, GeoPoint, INTERNATIONAL_ADDRESS_FORMAT, PartialAddress, describeAddressError, fieldName, fieldSpec, fromGeoJsonPoint, isAddressValid, isExtensionField, requiredFieldNames, requiredKeys, resolveAddressFormat, supportedCountries, toGeoJsonPoint, validateAddress };
320
+ export { Address, AddressExtensionFieldSpec, AddressFieldError, AddressFieldInput, AddressFieldKey, AddressFieldSpec, AddressFormat, AddressFormatRegistry, AddressTaxonomyPort, AnyAddressFieldSpec, ContactAddress, GeoJsonPoint, GeoPoint, INTERNATIONAL_ADDRESS_FORMAT, PartialAddress, WithinRadiusOptions, describeAddressError, distanceMeters, fieldName, fieldSpec, fromGeoJsonPoint, isAddressValid, isExtensionField, isWithinRadius, requiredFieldNames, requiredKeys, resolveAddressFormat, supportedCountries, toGeoJsonPoint, validateAddress };
package/dist/address.mjs CHANGED
@@ -12,6 +12,30 @@ function fromGeoJsonPoint(g) {
12
12
  latitude
13
13
  };
14
14
  }
15
+ /** Mean Earth radius (IUGG), metres. */
16
+ const EARTH_RADIUS_M = 6371008.8;
17
+ const toRad = (deg) => deg * Math.PI / 180;
18
+ function assertValidPoint(p, label) {
19
+ if (!Number.isFinite(p.latitude) || !Number.isFinite(p.longitude) || Math.abs(p.latitude) > 90 || Math.abs(p.longitude) > 180) throw new RangeError(`${label} is not a valid WGS84 point`);
20
+ }
21
+ /** Great-circle distance in metres (haversine). Accurate to ~0.5% — fine for geofences, not for surveying. */
22
+ function distanceMeters(a, b) {
23
+ assertValidPoint(a, "a");
24
+ assertValidPoint(b, "b");
25
+ const dLat = toRad(b.latitude - a.latitude);
26
+ const dLon = toRad(b.longitude - a.longitude);
27
+ const h = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(a.latitude)) * Math.cos(toRad(b.latitude)) * Math.sin(dLon / 2) ** 2;
28
+ return 2 * EARTH_RADIUS_M * Math.asin(Math.min(1, Math.sqrt(h)));
29
+ }
30
+ /** Whether `point` lies within `radiusMeters` of `center`, optionally accounting for GPS accuracy. */
31
+ function isWithinRadius(point, center, radiusMeters, options = {}) {
32
+ if (!Number.isFinite(radiusMeters) || radiusMeters < 0) throw new RangeError("radiusMeters must be >= 0");
33
+ const d = distanceMeters(point, center);
34
+ const acc = options.accuracyMeters;
35
+ if (acc === void 0) return d <= radiusMeters;
36
+ if (!Number.isFinite(acc) || acc < 0) throw new RangeError("accuracyMeters must be >= 0");
37
+ return (options.mode ?? "strict") === "strict" ? d + acc <= radiusMeters : d - acc <= radiusMeters;
38
+ }
15
39
  /** The key a spec writes to, canonical or extension. */
16
40
  function fieldName(spec) {
17
41
  return "extension" in spec ? spec.name : spec.key;
@@ -203,4 +227,4 @@ function describeAddressError(error) {
203
227
  }
204
228
  }
205
229
  //#endregion
206
- export { INTERNATIONAL_ADDRESS_FORMAT, describeAddressError, fieldName, fieldSpec, fromGeoJsonPoint, isAddressValid, isExtensionField, requiredFieldNames, requiredKeys, resolveAddressFormat, supportedCountries, toGeoJsonPoint, validateAddress };
230
+ export { INTERNATIONAL_ADDRESS_FORMAT, describeAddressError, distanceMeters, fieldName, fieldSpec, fromGeoJsonPoint, isAddressValid, isExtensionField, isWithinRadius, requiredFieldNames, requiredKeys, resolveAddressFormat, supportedCountries, toGeoJsonPoint, validateAddress };
@@ -28,7 +28,12 @@ function toBase64Url(bytes) {
28
28
  }
29
29
  function fromBase64Url(text) {
30
30
  if (!/^[A-Za-z0-9_-]*$/.test(text)) return null;
31
- const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
31
+ let binary;
32
+ try {
33
+ binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
34
+ } catch {
35
+ return null;
36
+ }
32
37
  const bytes = new Uint8Array(new ArrayBuffer(binary.length));
33
38
  for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
34
39
  return bytes;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/primitives",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "Framework-agnostic TypeScript primitives shared across Classytic packages — Money, events, pagination, tenant config, and other building blocks.",
5
5
  "author": "Classytic",
6
6
  "license": "MIT",